From 13da453f911f294ff35a02a2b4621bf3959d2971 Mon Sep 17 00:00:00 2001 From: caila-marashaj Date: Thu, 16 Jan 2025 17:56:57 -0500 Subject: [PATCH] make em track in place --- .../commands/aspirate_while_tracking.py | 194 +++++++----------- .../commands/dispense_while_tracking.py | 123 ++++++----- .../protocol_engine/execution/pipetting.py | 38 ---- 3 files changed, 148 insertions(+), 207 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/aspirate_while_tracking.py b/api/src/opentrons/protocol_engine/commands/aspirate_while_tracking.py index 4f3ec9cf6db..157d3d9115b 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate_while_tracking.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate_while_tracking.py @@ -11,13 +11,11 @@ FlowRateMixin, BaseLiquidHandlingResult, aspirate_while_tracking, - prepare_for_aspirate, ) from .movement_common import ( LiquidHandlingWellLocationMixin, DestinationPositionResult, StallOrCollisionError, - move_to_well, ) from .command import ( AbstractCommandImpl, @@ -26,18 +24,13 @@ DefinedErrorData, SuccessData, ) - +from ..errors.exceptions import PipetteNotReadyToAspirateError from opentrons.hardware_control import HardwareControlAPI - -from ..state.update_types import StateUpdate, CLEAR -from ..types import ( - WellLocation, - WellOrigin, - CurrentWell, -) +from ..state.update_types import CLEAR +from ..types import CurrentWell, DeckPoint if TYPE_CHECKING: - from ..execution import MovementHandler, PipettingHandler + from ..execution import MovementHandler, PipettingHandler, GantryMover from ..resources import ModelUtils from ..state.state import StateView from ..notes import CommandNoteAdder @@ -82,6 +75,7 @@ def __init__( movement: MovementHandler, command_note_adder: CommandNoteAdder, model_utils: ModelUtils, + gantry_mover: GantryMover, **kwargs: object, ) -> None: self._pipetting = pipetting @@ -90,136 +84,106 @@ def __init__( self._movement = movement self._command_note_adder = command_note_adder self._model_utils = model_utils + self._gantry_mover = gantry_mover async def execute(self, params: AspirateWhileTrackingParams) -> _ExecuteReturn: """Move to and aspirate from the requested well. Raises: TipNotAttachedError: if no tip is attached to the pipette. + PipetteNotReadyToAspirateError: pipette plunger is not ready. """ - pipette_id = params.pipetteId - labware_id = params.labwareId - well_name = params.wellName - well_location = params.wellLocation - - state_update = StateUpdate() - - final_location = self._state_view.geometry.get_well_position( - labware_id=labware_id, - well_name=well_name, - well_location=well_location, - operation_volume=-params.volume, - pipette_id=pipette_id, - ) - ready_to_aspirate = self._pipetting.get_is_ready_to_aspirate( - pipette_id=pipette_id + pipette_id=params.pipetteId, ) - - current_well = None - if not ready_to_aspirate: - move_result = await move_to_well( - movement=self._movement, - model_utils=self._model_utils, - pipette_id=pipette_id, - labware_id=labware_id, - well_name=well_name, - well_location=WellLocation(origin=WellOrigin.TOP), - ) - state_update.append(move_result.state_update) - if isinstance(move_result, DefinedErrorData): - return DefinedErrorData(move_result.public, state_update=state_update) - - prepare_result = await prepare_for_aspirate( - pipette_id=pipette_id, - pipetting=self._pipetting, - model_utils=self._model_utils, - # Note that the retryLocation is the final location, inside the liquid, - # because that's where we'd want the client to try re-aspirating if this - # command fails and the run enters error recovery. - location_if_error={"retryLocation": final_location}, + raise PipetteNotReadyToAspirateError( + "Pipette cannot aspirate in place because of a previous blow out." + " 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.append(prepare_result.state_update) - if isinstance(prepare_result, DefinedErrorData): - return DefinedErrorData( - public=prepare_result.public, state_update=state_update - ) - # set our current deck location to the well now that we've made - # an intermediate move for the "prepare for aspirate" step - current_well = CurrentWell( - pipette_id=pipette_id, - labware_id=labware_id, - well_name=well_name, - ) - move_result = await move_to_well( - movement=self._movement, - model_utils=self._model_utils, - pipette_id=pipette_id, - labware_id=labware_id, - well_name=well_name, - well_location=well_location, - current_well=current_well, - ) - state_update.append(move_result.state_update) - if isinstance(move_result, DefinedErrorData): - return DefinedErrorData( - public=move_result.public, state_update=state_update - ) + current_position = await self._gantry_mover.get_position(params.pipetteId) + current_location = self._state_view.pipettes.get_current_location() aspirate_result = await aspirate_while_tracking( - pipette_id=pipette_id, - labware_id=labware_id, - well_name=well_name, + pipette_id=params.pipetteId, + labware_id=params.labwareId, + well_name=params.wellName, volume=params.volume, flow_rate=params.flowRate, location_if_error={ "retryLocation": ( - move_result.public.position.x, - move_result.public.position.y, - move_result.public.position.z, + current_position.x, + current_position.y, + current_position.z, ) }, command_note_adder=self._command_note_adder, pipetting=self._pipetting, model_utils=self._model_utils, ) - state_update.append(aspirate_result.state_update) - if isinstance(aspirate_result, DefinedErrorData): - state_update.set_liquid_operated( - labware_id=labware_id, - well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well( - labware_id, - well_name, - params.pipetteId, - ), - volume_added=CLEAR, - ) - return DefinedErrorData( - public=aspirate_result.public, state_update=state_update - ) - - state_update.set_liquid_operated( - labware_id=labware_id, - well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well( - labware_id, well_name, pipette_id - ), - volume_added=-aspirate_result.public.volume - * self._state_view.geometry.get_nozzles_per_well( - labware_id, - well_name, - params.pipetteId, - ), + position_after_aspirate = await self._gantry_mover.get_position( + params.pipetteId ) - - return SuccessData( - public=AspirateWhileTrackingResult( - volume=aspirate_result.public.volume, - position=move_result.public.position, - ), - state_update=state_update, + result_deck_point = DeckPoint.model_construct( + x=position_after_aspirate.x, + y=position_after_aspirate.y, + z=position_after_aspirate.z, ) + if isinstance(aspirate_result, DefinedErrorData): + if ( + isinstance(current_location, CurrentWell) + and current_location.pipette_id == params.pipetteId + ): + return DefinedErrorData( + public=aspirate_result.public, + state_update=aspirate_result.state_update.set_liquid_operated( + labware_id=current_location.labware_id, + well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well( + current_location.labware_id, + current_location.well_name, + params.pipetteId, + ), + volume_added=CLEAR, + ), + state_update_if_false_positive=aspirate_result.state_update_if_false_positive, + ) + else: + return aspirate_result + else: + if ( + isinstance(current_location, CurrentWell) + and current_location.pipette_id == params.pipetteId + ): + return SuccessData( + public=AspirateWhileTrackingResult( + volume=aspirate_result.public.volume, + position=result_deck_point, + ), + state_update=aspirate_result.state_update.set_liquid_operated( + labware_id=current_location.labware_id, + well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well( + current_location.labware_id, + current_location.well_name, + params.pipetteId, + ), + volume_added=-aspirate_result.public.volume + * self._state_view.geometry.get_nozzles_per_well( + current_location.labware_id, + current_location.well_name, + params.pipetteId, + ), + ), + ) + else: + return SuccessData( + public=AspirateWhileTrackingResult( + volume=aspirate_result.public.volume, + position=result_deck_point, + ), + state_update=aspirate_result.state_update, + ) class AspirateWhileTracking( diff --git a/api/src/opentrons/protocol_engine/commands/dispense_while_tracking.py b/api/src/opentrons/protocol_engine/commands/dispense_while_tracking.py index 7d287c7c01d..4a5a80cab81 100644 --- a/api/src/opentrons/protocol_engine/commands/dispense_while_tracking.py +++ b/api/src/opentrons/protocol_engine/commands/dispense_while_tracking.py @@ -8,7 +8,8 @@ from pydantic import Field from pydantic.json_schema import SkipJsonSchema -from ..state.update_types import StateUpdate, CLEAR +from ..state.update_types import CLEAR +from ..types import CurrentWell, DeckPoint from .pipetting_common import ( PipetteIdMixin, DispenseVolumeMixin, @@ -21,7 +22,6 @@ LiquidHandlingWellLocationMixin, DestinationPositionResult, StallOrCollisionError, - move_to_well, ) from .command import ( AbstractCommandImpl, @@ -32,7 +32,7 @@ ) if TYPE_CHECKING: - from ..execution import MovementHandler, PipettingHandler + from ..execution import MovementHandler, PipettingHandler, GantryMover from ..resources import ModelUtils from ..state.state import StateView @@ -82,31 +82,24 @@ def __init__( movement: MovementHandler, pipetting: PipettingHandler, model_utils: ModelUtils, + gantry_mover: GantryMover, **kwargs: object, ) -> None: self._state_view = state_view self._movement = movement self._pipetting = pipetting self._model_utils = model_utils + self._gantry_mover = gantry_mover async def execute(self, params: DispenseWhileTrackingParams) -> _ExecuteReturn: """Move to and dispense to the requested well.""" - well_location = params.wellLocation labware_id = params.labwareId well_name = params.wellName # TODO(pbm, 10-15-24): call self._state_view.geometry.validate_dispense_volume_into_well() - move_result = await move_to_well( - movement=self._movement, - model_utils=self._model_utils, - pipette_id=params.pipetteId, - labware_id=labware_id, - well_name=well_name, - well_location=well_location, - ) - if isinstance(move_result, DefinedErrorData): - return move_result + current_location = self._state_view.pipettes.get_current_location() + current_position = await self._gantry_mover.get_position(params.pipetteId) dispense_result = await dispense_while_tracking( pipette_id=params.pipetteId, labware_id=labware_id, @@ -116,63 +109,85 @@ async def execute(self, params: DispenseWhileTrackingParams) -> _ExecuteReturn: push_out=params.pushOut, location_if_error={ "retryLocation": ( - move_result.public.position.x, - move_result.public.position.y, - move_result.public.position.z, + current_position.x, + current_position.y, + current_position.z, ) }, pipetting=self._pipetting, model_utils=self._model_utils, ) + position_after_dispense = await self._gantry_mover.get_position( + params.pipetteId + ) + result_deck_point = DeckPoint.model_construct( + x=position_after_dispense.x, + y=position_after_dispense.y, + z=position_after_dispense.z, + ) if isinstance(dispense_result, DefinedErrorData): - return DefinedErrorData( - public=dispense_result.public, - state_update=( - StateUpdate.reduce( - move_result.state_update, dispense_result.state_update - ).set_liquid_operated( - labware_id=labware_id, + if ( + isinstance(current_location, CurrentWell) + and current_location.pipette_id == params.pipetteId + ): + return DefinedErrorData( + public=dispense_result.public, + state_update=dispense_result.state_update.set_liquid_operated( + labware_id=current_location.labware_id, well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well( - labware_id, well_name, params.pipetteId + current_location.labware_id, + current_location.well_name, + params.pipetteId, ), volume_added=CLEAR, - ) - ), - state_update_if_false_positive=StateUpdate.reduce( - move_result.state_update, - dispense_result.state_update_if_false_positive, - ), - ) - else: - volume_added = ( - self._state_view.pipettes.get_liquid_dispensed_by_ejecting_volume( - pipette_id=params.pipetteId, volume=dispense_result.public.volume + ), + state_update_if_false_positive=dispense_result.state_update_if_false_positive, ) - ) - if volume_added is not None: - volume_added *= self._state_view.geometry.get_nozzles_per_well( - labware_id, well_name, params.pipetteId + else: + return dispense_result + else: + if ( + isinstance(current_location, CurrentWell) + and current_location.pipette_id == params.pipetteId + ): + volume_added = ( + self._state_view.pipettes.get_liquid_dispensed_by_ejecting_volume( + pipette_id=params.pipetteId, + volume=dispense_result.public.volume, + ) ) - return SuccessData( - public=DispenseWhileTrackingResult( - volume=dispense_result.public.volume, - position=move_result.public.position, - ), - state_update=( - StateUpdate.reduce( - move_result.state_update, dispense_result.state_update - ).set_liquid_operated( - labware_id=labware_id, + if volume_added is not None: + volume_added *= self._state_view.geometry.get_nozzles_per_well( + current_location.labware_id, + current_location.well_name, + params.pipetteId, + ) + return SuccessData( + public=DispenseWhileTrackingResult( + volume=dispense_result.public.volume, + position=result_deck_point, + ), + state_update=dispense_result.state_update.set_liquid_operated( + labware_id=current_location.labware_id, well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well( - labware_id, well_name, params.pipetteId + current_location.labware_id, + current_location.well_name, + params.pipetteId, ), volume_added=volume_added if volume_added is not None else CLEAR, - ) - ), - ) + ), + ) + else: + return SuccessData( + public=DispenseWhileTrackingResult( + volume=dispense_result.public.volume, + position=result_deck_point, + ), + state_update=dispense_result.state_update, + ) class DispenseWhileTracking( diff --git a/api/src/opentrons/protocol_engine/execution/pipetting.py b/api/src/opentrons/protocol_engine/execution/pipetting.py index 2365347348e..2fab81f1454 100644 --- a/api/src/opentrons/protocol_engine/execution/pipetting.py +++ b/api/src/opentrons/protocol_engine/execution/pipetting.py @@ -171,25 +171,6 @@ async def aspirate_while_tracking( Raises: PipetteOverpressureError, propagated as-is from the hardware controller. """ - # get mount and config data from state and hardware controller - """hw_pipette, adjusted_volume = self.get_hw_aspirate_params( - pipette_id, volume, command_note_adder - ) - - aspirate_z_distance = self._state_view.geometry.get_liquid_handling_z_change( - labware_id=labware_id, - well_name=well_name, - operation_volume=volume * -1, - ) - with self._set_flow_rate(pipette=hw_pipette, aspirate_flow_rate=flow_rate): - await self._hardware_api.aspirate_while_tracking( - mount=hw_pipette.mount, - z_distance=aspirate_z_distance, - flow_rate=flow_rate, - volume=adjusted_volume, - ) - return adjusted_volume - """ return 0.0 async def dispense_while_tracking( @@ -206,25 +187,6 @@ async def dispense_while_tracking( Raises: PipetteOverpressureError, propagated as-is from the hardware controller. """ - # get mount and config data from state and hardware controller - """hw_pipette, adjusted_volume = self.get_hw_dispense_params(pipette_id, volume) - - dispense_z_distance = self._state_view.geometry.get_liquid_handling_z_change( - labware_id=labware_id, - well_name=well_name, - operation_volume=volume, - ) - with self._set_flow_rate(pipette=hw_pipette, dispense_flow_rate=flow_rate): - await self._hardware_api.dispense_while_tracking( - mount=hw_pipette.mount, - z_distance=dispense_z_distance, - flow_rate=flow_rate, - volume=adjusted_volume, - push_out=push_out, - ) - - return adjusted_volume - """ return 0.0 async def aspirate_in_place(