From 47b8f910bed2a72ceb11931c32d39733fc512c88 Mon Sep 17 00:00:00 2001 From: Sanniti Date: Thu, 9 Jan 2025 15:13:20 -0500 Subject: [PATCH] added tests --- .../protocol_api/core/engine/instrument.py | 30 ++- .../engine/transfer_components_executor.py | 1 + .../test_transfer_with_liquid_classes.py | 219 ++++++++++++++++-- 3 files changed, 216 insertions(+), 34 deletions(-) diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 77bf92fc4d1..795806231a4 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -1079,6 +1079,13 @@ def _pick_up_tip() -> None: # TODO: make sure that the tip has air gap when moving to the trash _drop_tip() + post_disp_tip_contents = [ + tx_comps_executor.LiquidAndAirGapPair( + liquid=45, + air_gap=67, + ) + ] + def _get_location_and_well_core_from_next_tip_info( self, tip_info: NextTipInfo, @@ -1107,6 +1114,7 @@ def aspirate_liquid_class( transfer_type: tx_comps_executor.TransferType, tip_contents: List[tx_comps_executor.LiquidAndAirGapPair], ) -> List[tx_comps_executor.LiquidAndAirGapPair]: + print("!!!!", tip_contents) """Execute aspiration steps. 1. Submerge @@ -1119,6 +1127,7 @@ def aspirate_liquid_class( Return: List of liquid and air gap pairs in tip. """ aspirate_props = transfer_properties.aspirate + _tip_contents = tip_contents.copy() tx_commons.check_valid_volume_parameters( disposal_volume=0, # No disposal volume for 1-to-1 transfer air_gap=aspirate_props.retract.air_gap_by_volume.get_for_volume(volume), @@ -1133,14 +1142,14 @@ def aspirate_liquid_class( ) ) aspirate_location = Location(aspirate_point, labware=source_loc.labware) - if len(tip_contents) > 0: - last_liquid_and_airgap_in_tip = tip_contents[-1] + if len(_tip_contents) > 0: + last_liquid_and_airgap_in_tip = _tip_contents[-1] else: last_liquid_and_airgap_in_tip = tx_comps_executor.LiquidAndAirGapPair( liquid=0, air_gap=0, ) - tip_contents = [last_liquid_and_airgap_in_tip] + _tip_contents = [last_liquid_and_airgap_in_tip] components_executor = tx_comps_executor.TransferComponentsExecutor( instrument_core=self, transfer_properties=transfer_properties, @@ -1163,8 +1172,8 @@ def aspirate_liquid_class( components_executor.aspirate_and_wait(volume=volume) components_executor.retract_after_aspiration(volume=volume) last_contents = components_executor.tip_state.last_liquid_and_air_gap_in_tip - tip_contents[-1] = last_contents - return tip_contents + _tip_contents[-1] = last_contents + return _tip_contents def dispense_liquid_class( self, @@ -1208,6 +1217,7 @@ def dispense_liquid_class( """ dispense_props = transfer_properties.dispense dest_loc, dest_well = dest + _tip_contents = tip_contents.copy() dispense_point = ( tx_comps_executor.absolute_point_from_position_reference_and_offset( well=dest_well, @@ -1216,14 +1226,14 @@ def dispense_liquid_class( ) ) dispense_location = Location(dispense_point, labware=dest_loc.labware) - if len(tip_contents) > 0: - last_liquid_and_airgap_in_tip = tip_contents[-1] + if len(_tip_contents) > 0: + last_liquid_and_airgap_in_tip = _tip_contents[-1] else: last_liquid_and_airgap_in_tip = tx_comps_executor.LiquidAndAirGapPair( liquid=0, air_gap=0, ) - tip_contents = [last_liquid_and_airgap_in_tip] + _tip_contents = [last_liquid_and_airgap_in_tip] components_executor = tx_comps_executor.TransferComponentsExecutor( instrument_core=self, transfer_properties=transfer_properties, @@ -1254,8 +1264,8 @@ def dispense_liquid_class( source_well=source[1] if source else None, ) last_contents = components_executor.tip_state.last_liquid_and_air_gap_in_tip - tip_contents[-1] = last_contents - return tip_contents + _tip_contents[-1] = last_contents + return _tip_contents def retract(self) -> None: """Retract this instrument to the top of the gantry.""" 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 43729edcfc4..477ef651ba2 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 @@ -387,6 +387,7 @@ def retract_after_dispensing( blowout_props.enabled and blowout_props.location != BlowoutLocation.DESTINATION ): + # TODO: no-op touch tip if touch tip is enabled and blowout is in trash/ reservoir/ any labware with touch-tip disabled assert blowout_props.flow_rate is not None self._instrument.set_flow_rate(blow_out=blowout_props.flow_rate) touch_tip_and_air_gap_location: Optional[Location] diff --git a/api/tests/opentrons/protocol_api_integration/test_transfer_with_liquid_classes.py b/api/tests/opentrons/protocol_api_integration/test_transfer_with_liquid_classes.py index 10b4616c0aa..3642ee6fd74 100644 --- a/api/tests/opentrons/protocol_api_integration/test_transfer_with_liquid_classes.py +++ b/api/tests/opentrons/protocol_api_integration/test_transfer_with_liquid_classes.py @@ -1,17 +1,23 @@ """Tests for the transfer APIs using liquid classes.""" import pytest +import mock from decoy import Decoy from opentrons_shared_data.robot.types import RobotTypeEnum from opentrons.protocol_api import ProtocolContext from opentrons.config import feature_flags as ff +from opentrons.protocol_api.core.engine import InstrumentCore +from opentrons.protocol_api.core.engine.transfer_components_executor import ( + TransferType, + LiquidAndAirGapPair, +) @pytest.mark.ot3_only @pytest.mark.parametrize( "simulated_protocol_context", [("2.20", "Flex")], indirect=True ) -def test_water_transfer( +def test_water_transfer_with_volume_more_than_tip_max( decoy: Decoy, mock_feature_flags: None, simulated_protocol_context: ProtocolContext ) -> None: """It should run the transfer steps without any errors. @@ -36,29 +42,194 @@ def test_water_transfer( ) water = simulated_protocol_context.define_liquid_class("water") - pipette_50.transfer_liquid( - liquid_class=water, - volume=60, - source=nest_plate.rows()[0], - dest=arma_plate.rows()[0], - new_tip="always", - trash_location=trash, + with mock.patch.object( + InstrumentCore, + "pick_up_tip", + side_effect=InstrumentCore.pick_up_tip, + autospec=True, + ) as patched_pick_up_tip: + mock_manager = mock.Mock() + mock_manager.attach_mock(patched_pick_up_tip, "pick_up_tip") + + pipette_50.transfer_liquid( + liquid_class=water, + volume=60, + source=nest_plate.rows()[0], + dest=arma_plate.rows()[0], + new_tip="always", + trash_location=trash, + ) + assert patched_pick_up_tip.call_count == 24 + patched_pick_up_tip.reset_mock() + + pipette_50.transfer_liquid( + liquid_class=water, + volume=100, + source=nest_plate.rows()[0], + dest=arma_plate.rows()[0], + new_tip="per source", + trash_location=trash, + ) + assert patched_pick_up_tip.call_count == 12 + patched_pick_up_tip.reset_mock() + + pipette_50.pick_up_tip() + pipette_50.transfer_liquid( + liquid_class=water, + volume=50, + source=nest_plate.rows()[0], + dest=arma_plate.rows()[0], + new_tip="never", + trash_location=trash, + ) + pipette_50.drop_tip() + assert patched_pick_up_tip.call_count == 1 + + +@pytest.mark.ot3_only +@pytest.mark.parametrize( + "simulated_protocol_context", [("2.20", "Flex")], indirect=True +) +def test_order_of_water_transfer_steps( + decoy: Decoy, mock_feature_flags: None, simulated_protocol_context: ProtocolContext +) -> None: + """It should run the transfer steps without any errors. + + This test only checks that various supported configurations for a transfer + analyze successfully. It doesn't check whether the steps are as expected. + That will be covered in analysis snapshot tests. + """ + decoy.when(ff.allow_liquid_classes(RobotTypeEnum.FLEX)).then_return(True) + trash = simulated_protocol_context.load_trash_bin("A3") + tiprack = simulated_protocol_context.load_labware( + "opentrons_flex_96_tiprack_50ul", "D1" ) - pipette_50.transfer_liquid( - liquid_class=water, - volume=60, - source=nest_plate.rows()[0], - dest=arma_plate.rows()[0], - new_tip="per source", - trash_location=trash, + pipette_50 = simulated_protocol_context.load_instrument( + "flex_1channel_50", mount="left", tip_racks=[tiprack] ) - pipette_50.pick_up_tip() - pipette_50.transfer_liquid( - liquid_class=water, - volume=50, - source=nest_plate.rows()[0], - dest=arma_plate.rows()[0], - new_tip="never", - trash_location=trash, + nest_plate = simulated_protocol_context.load_labware( + "nest_96_wellplate_200ul_flat", "C3" ) - pipette_50.drop_tip() + arma_plate = simulated_protocol_context.load_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", "C2" + ) + + water = simulated_protocol_context.define_liquid_class("water") + with ( + mock.patch.object( + InstrumentCore, + "load_liquid_class", + side_effect=InstrumentCore.load_liquid_class, + autospec=True, + ) as patched_load_liquid_class, + mock.patch.object( + InstrumentCore, + "pick_up_tip", + side_effect=InstrumentCore.pick_up_tip, + autospec=True, + ) as patched_pick_up_tip, + mock.patch.object( + InstrumentCore, + "aspirate_liquid_class", + side_effect=InstrumentCore.aspirate_liquid_class, + autospec=True, + ) as patched_aspirate, + mock.patch.object( + InstrumentCore, + "dispense_liquid_class", + side_effect=InstrumentCore.dispense_liquid_class, + autospec=True, + ) as patched_dispense, + mock.patch.object( + InstrumentCore, + "drop_tip_in_disposal_location", + side_effect=InstrumentCore.drop_tip_in_disposal_location, + autospec=True, + ) as patched_drop_tip, + ): + mock_manager = mock.Mock() + mock_manager.attach_mock(patched_pick_up_tip, "pick_up_tip") + mock_manager.attach_mock(patched_load_liquid_class, "load_liquid_class") + mock_manager.attach_mock(patched_aspirate, "aspirate_liquid_class") + mock_manager.attach_mock(patched_dispense, "dispense_liquid_class") + mock_manager.attach_mock(patched_drop_tip, "drop_tip_in_disposal_location") + pipette_50.transfer_liquid( + liquid_class=water, + volume=40, + source=nest_plate.rows()[0][:2], + dest=arma_plate.rows()[0][:2], + new_tip="always", + trash_location=trash, + ) + expected_calls = [ + mock.call.load_liquid_class( + mock.ANY, + name="water", + transfer_properties=mock.ANY, + tiprack_uri="opentrons/opentrons_flex_96_tiprack_50ul/1", + ), + mock.call.pick_up_tip( + mock.ANY, + location=mock.ANY, + well_core=mock.ANY, + presses=mock.ANY, + increment=mock.ANY, + ), + mock.call.aspirate_liquid_class( + mock.ANY, + volume=40, + source=mock.ANY, + transfer_properties=mock.ANY, + transfer_type=TransferType.ONE_TO_ONE, + tip_contents=[LiquidAndAirGapPair(liquid=0, air_gap=0)], + ), + mock.call.dispense_liquid_class( + mock.ANY, + volume=40, + dest=mock.ANY, + source=mock.ANY, + transfer_properties=mock.ANY, + transfer_type=TransferType.ONE_TO_ONE, + tip_contents=[LiquidAndAirGapPair(liquid=0, air_gap=0.1)], + trash_location=mock.ANY, + ), + mock.call.drop_tip_in_disposal_location( + mock.ANY, + disposal_location=trash, + home_after=False, + alternate_tip_drop=True, + ), + mock.call.pick_up_tip( + mock.ANY, + location=mock.ANY, + well_core=mock.ANY, + presses=mock.ANY, + increment=mock.ANY, + ), + mock.call.aspirate_liquid_class( + mock.ANY, + volume=40, + source=mock.ANY, + transfer_properties=mock.ANY, + transfer_type=TransferType.ONE_TO_ONE, + tip_contents=[LiquidAndAirGapPair(liquid=0, air_gap=0)], + ), + mock.call.dispense_liquid_class( + mock.ANY, + volume=40, + dest=mock.ANY, + source=mock.ANY, + transfer_properties=mock.ANY, + transfer_type=TransferType.ONE_TO_ONE, + tip_contents=[LiquidAndAirGapPair(liquid=40, air_gap=0.1)], + trash_location=mock.ANY, + ), + mock.call.drop_tip_in_disposal_location( + mock.ANY, + disposal_location=trash, + home_after=False, + alternate_tip_drop=True, + ), + ] + assert len(mock_manager.mock_calls) == 9 + assert mock_manager.mock_calls == expected_calls