diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 936b704c426..acd4d9f68ed 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -42,12 +42,9 @@ ) from opentrons.protocol_api._nozzle_layout import NozzleLayout from . import overlap_versions, pipette_movement_conflict +from . import transfer_components_executor as tx_comps_executor from .well import WellCore -from .transfer_components_executor import ( - TransferComponentsExecutor, - absolute_point_from_position_reference_and_offset, -) from ..instrument import AbstractInstrument from ...disposal_locations import TrashBin, WasteChute @@ -980,20 +977,6 @@ def transfer_liquid( alternate_drop_location=True, ) - def _get_transfer_components_executor( - self, - transfer_properties: TransferProperties, - target_location: Location, - target_well: WellCore, - ) -> TransferComponentsExecutor: - """Get a TransferComponentsExecutor.""" - return TransferComponentsExecutor( - instrument_core=self, - transfer_properties=transfer_properties, - target_location=target_location, - target_well=target_well, - ) - def aspirate_liquid_class( self, volume: float, @@ -1011,21 +994,25 @@ def aspirate_liquid_class( """ aspirate_props = transfer_properties.aspirate source_loc, source_well = source - aspirate_point = absolute_point_from_position_reference_and_offset( - well=source_well, - position_reference=aspirate_props.position_reference, - offset=aspirate_props.offset, + aspirate_point = ( + tx_comps_executor.absolute_point_from_position_reference_and_offset( + well=source_well, + position_reference=aspirate_props.position_reference, + offset=aspirate_props.offset, + ) ) aspirate_location = Location(aspirate_point, labware=source_loc.labware) - components_executer = self._get_transfer_components_executor( + components_executer = tx_comps_executor.get_transfer_components_executor( + instrument_core=self, transfer_properties=transfer_properties, target_location=aspirate_location, target_well=source_well, ) components_executer.submerge( submerge_properties=aspirate_props.submerge, - # Assuming aspirate is not called with *liquid*git swta in the tip + # Assuming aspirate is not called with *liquid* in the tip + # TODO: evaluate if using the current volume to find air gap is not a good idea. air_gap_volume=self.get_current_volume(), ) # TODO: when aspirating for consolidation, do not perform mix 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 107a330a9df..3663501961c 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 @@ -257,6 +257,21 @@ def _remove_air_gap(self, location: Location, volume: float) -> None: self._instrument.delay(dispense_delay.duration) +def get_transfer_components_executor( + instrument_core: InstrumentCore, + transfer_properties: TransferProperties, + target_location: Location, + target_well: WellCore, +) -> TransferComponentsExecutor: + """Get a TransferComponentsExecutor.""" + return TransferComponentsExecutor( + instrument_core=instrument_core, + transfer_properties=transfer_properties, + target_location=target_location, + target_well=target_well, + ) + + def absolute_point_from_position_reference_and_offset( well: WellCore, position_reference: PositionReference, 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 352dcb35c58..b8600416de3 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 @@ -7,6 +7,8 @@ from decoy import errors from opentrons_shared_data.liquid_classes.liquid_class_definition import ( LiquidClassSchemaV1, + PositionReference, + Coordinate, ) from opentrons_shared_data.pipette.types import PipetteNameType @@ -14,6 +16,10 @@ from opentrons.hardware_control import SyncHardwareAPI from opentrons.hardware_control.dev_types import PipetteDict from opentrons.protocol_api._liquid_properties import TransferProperties +from opentrons.protocol_api.core.engine import transfer_components_executor +from opentrons.protocol_api.core.engine.transfer_components_executor import ( + TransferComponentsExecutor, +) from opentrons.protocol_engine import ( DeckPoint, LoadedPipette, @@ -90,6 +96,34 @@ def patch_mock_pipette_movement_safety_check( ) +@pytest.fixture +def mock_transfer_components_executor( + decoy: Decoy, +) -> TransferComponentsExecutor: + """Get a mocked out TransferComponentsExecutor.""" + return decoy.mock(cls=TransferComponentsExecutor) + + +@pytest.fixture(autouse=True) +def patch_mock_transfer_components_executor( + decoy: Decoy, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Replace transfer_components_executor functions with mocks.""" + monkeypatch.setattr( + transfer_components_executor, + "get_transfer_components_executor", + decoy.mock(func=transfer_components_executor.get_transfer_components_executor), + ) + monkeypatch.setattr( + transfer_components_executor, + "absolute_point_from_position_reference_and_offset", + decoy.mock( + func=transfer_components_executor.absolute_point_from_position_reference_and_offset + ), + ) + + @pytest.fixture def subject( decoy: Decoy, @@ -1556,3 +1590,54 @@ def test_load_liquid_class( tiprack_uri="opentrons_flex_96_tiprack_50ul", ) assert result == "liquid-class-id" + + +def test_aspirate_liquid_class( + decoy: Decoy, + mock_engine_client: EngineClient, + subject: InstrumentCore, + minimal_liquid_class_def2: LiquidClassSchemaV1, + mock_transfer_components_executor: TransferComponentsExecutor, +) -> None: + """It should call aspirate sub-steps execution based on liquid class.""" + source_well = decoy.mock(cls=WellCore) + source_location = Location(Point(1, 2, 3), labware=None) + test_liquid_class = LiquidClass.create(minimal_liquid_class_def2) + test_transfer_properties = test_liquid_class.get_for( + "flex_1channel_50", "opentrons_flex_96_tiprack_50ul" + ) + decoy.when( + transfer_components_executor.absolute_point_from_position_reference_and_offset( + well=source_well, + position_reference=PositionReference.WELL_BOTTOM, + offset=Coordinate(x=0, y=0, z=-5), + ) + ).then_return(Point(1, 2, 3)) + decoy.when( + transfer_components_executor.get_transfer_components_executor( + instrument_core=subject, + transfer_properties=test_transfer_properties, + target_location=Location(Point(1, 2, 3), labware=None), + target_well=source_well, + ) + ).then_return(mock_transfer_components_executor) + decoy.when( + mock_engine_client.state.pipettes.get_aspirated_volume("abc123") + ).then_return(111) + subject.aspirate_liquid_class( + volume=123, + source=(source_location, source_well), + transfer_properties=test_transfer_properties, + ) + decoy.verify( + mock_transfer_components_executor.submerge( + submerge_properties=test_transfer_properties.aspirate.submerge, + air_gap_volume=111, + ), + mock_transfer_components_executor.mix( + mix_properties=test_transfer_properties.aspirate.mix + ), + mock_transfer_components_executor.pre_wet(volume=123), + mock_transfer_components_executor.aspirate_and_wait(volume=123), + mock_transfer_components_executor.retract_after_aspiration(volume=123), + )