From d8fd34dcda48dbc4b446a54015da5cd3d7037261 Mon Sep 17 00:00:00 2001 From: Sanniti Date: Thu, 10 Apr 2025 17:07:39 -0400 Subject: [PATCH 1/2] skip transfers if volume is zero, improve error when out of tips --- .../protocol_api/core/engine/instrument.py | 14 +++++++++---- .../protocol_api/instrument_context.py | 21 +++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 4e5f0a497e6..03d5e5c502b 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -1238,7 +1238,9 @@ def _pick_up_tip() -> WellCore: ) if next_tip is None: raise RuntimeError( - f"No tip available among {tip_racks} for this transfer." + f"No tip available for this transfer step" + f" among the tipracks assigned for {self.get_pipette_name()}:" + f" {[tip_rack[1].get_display_name() for tip_rack in tip_racks]}" ) ( tiprack_loc, @@ -1478,7 +1480,9 @@ def _pick_up_tip() -> WellCore: ) if next_tip is None: raise RuntimeError( - f"No tip available among {tip_racks} for this transfer." + f"No tip available for this distribute step" + f" among the tipracks assigned for {self.get_pipette_name()}:" + f" {[tip_rack[1].get_display_name() for tip_rack in tip_racks]}" ) ( tiprack_loc, @@ -1569,7 +1573,7 @@ def _pick_up_tip() -> WellCore: and not transfer_props.multi_dispense.retract.blowout.enabled ): raise RuntimeError( - "Distribute liquid uses a disposal volume but location for disposing of" + "Distribute uses a disposal volume but location for disposing of" " the disposal volume cannot be found when blowout is disabled." " Specify a blowout location and enable blowout when using a disposal volume." ) @@ -1751,7 +1755,9 @@ def _pick_up_tip() -> WellCore: ) if next_tip is None: raise RuntimeError( - f"No tip available among {tip_racks} for this transfer." + f"No tip available for this consolidate step" + f" among the tipracks assigned for {self.get_pipette_name()}:" + f" {[tip_rack[1].get_display_name() for tip_rack in tip_racks]}" ) ( tiprack_loc, diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 2886911f0ac..7424b380aa4 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -1553,6 +1553,13 @@ def transfer_with_liquid_class( :param return_tip: Whether to drop used tips in their original locations in the tip rack, instead of the trash. """ + if volume == 0.0: + _log.info( + f"Transfer of {liquid_class.name} specified with a volume of 0uL." + f" Skipping." + ) + return self + transfer_args = verify_and_normalize_transfer_args( source=source, dest=dest, @@ -1645,6 +1652,13 @@ def distribute_with_liquid_class( :param return_tip: Whether to drop used tips in their original locations in the tip rack, instead of the trash. """ + if volume == 0.0: + _log.info( + f"Distribution of {liquid_class.name} specified with a volume of 0uL." + f" Skipping." + ) + return self + transfer_args = verify_and_normalize_transfer_args( source=source, dest=dest, @@ -1743,6 +1757,13 @@ def consolidate_with_liquid_class( :param return_tip: Whether to drop used tips in their original locations in the tip rack, instead of the trash. """ + if volume == 0.0: + _log.info( + f"Consolidation of {liquid_class.name} specified with a volume of 0uL." + f" Skipping." + ) + return self + transfer_args = verify_and_normalize_transfer_args( source=source, dest=dest, From 97eaba69e099bf6339db2929cc9a897b8b02982d Mon Sep 17 00:00:00 2001 From: Sanniti Date: Thu, 10 Apr 2025 23:49:39 -0400 Subject: [PATCH 2/2] added test --- .../protocol_api/core/engine/instrument.py | 15 ++--- .../test_transfer_with_liquid_classes.py | 58 +++++++++++++++++++ 2 files changed, 64 insertions(+), 9 deletions(-) diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 03d5e5c502b..6ccfc131ac3 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -1238,9 +1238,8 @@ def _pick_up_tip() -> WellCore: ) if next_tip is None: raise RuntimeError( - f"No tip available for this transfer step" - f" among the tipracks assigned for {self.get_pipette_name()}:" - f" {[tip_rack[1].get_display_name() for tip_rack in tip_racks]}" + f"No tip available among the tipracks assigned for {self.get_pipette_name()}:" + f" {[f'{tip_rack[1].get_display_name()} in {tip_rack[1].get_deck_slot()}' for tip_rack in tip_racks]}" ) ( tiprack_loc, @@ -1480,9 +1479,8 @@ def _pick_up_tip() -> WellCore: ) if next_tip is None: raise RuntimeError( - f"No tip available for this distribute step" - f" among the tipracks assigned for {self.get_pipette_name()}:" - f" {[tip_rack[1].get_display_name() for tip_rack in tip_racks]}" + f"No tip available among the tipracks assigned for {self.get_pipette_name()}:" + f" {[f'{tip_rack[1].get_display_name()} in {tip_rack[1].get_deck_slot()}' for tip_rack in tip_racks]}" ) ( tiprack_loc, @@ -1755,9 +1753,8 @@ def _pick_up_tip() -> WellCore: ) if next_tip is None: raise RuntimeError( - f"No tip available for this consolidate step" - f" among the tipracks assigned for {self.get_pipette_name()}:" - f" {[tip_rack[1].get_display_name() for tip_rack in tip_racks]}" + f"No tip available among the tipracks assigned for {self.get_pipette_name()}:" + f" {[ f'{tip_rack[1].get_display_name()} in {tip_rack[1].get_deck_slot()}' for tip_rack in tip_racks]}" ) ( tiprack_loc, 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 3047131a7bf..27d70cd8c56 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 @@ -2063,3 +2063,61 @@ def test_water_transfer_with_multi_channel_pipette( ) assert patched_aspirate.call_count == 2 assert patched_dispense.call_count == 2 + + +@pytest.mark.ot3_only +@pytest.mark.parametrize( + "simulated_protocol_context", [("2.23", "Flex")], indirect=True +) +def test_raises_no_tips_available_error( + simulated_protocol_context: ProtocolContext, +) -> None: + """It should raise an error explaining that there aren't any tips available.""" + trash = simulated_protocol_context.load_trash_bin("A3") + tiprack1 = simulated_protocol_context.load_labware( + "opentrons_flex_96_tiprack_50ul", "D1" + ) + tiprack2 = simulated_protocol_context.load_labware( + "opentrons_flex_96_tiprack_50ul", "D2" + ) + pipette_50 = simulated_protocol_context.load_instrument( + "flex_1channel_50", mount="left", tip_racks=[tiprack1, tiprack2] + ) + nest_plate = simulated_protocol_context.load_labware( + "nest_96_wellplate_200ul_flat", "C3" + ) + arma_plate = simulated_protocol_context.load_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", "C2" + ) + water = simulated_protocol_context.define_liquid_class("water") + expected_error_msg = ( + "No tip available among the tipracks assigned for flex_1channel_50:" + " \\['Opentrons Flex 96 Tip Rack 50 µL in D1', 'Opentrons Flex 96 Tip Rack 50 µL in D2'\\]" + ) + with pytest.raises(RuntimeError, match=expected_error_msg): + pipette_50.transfer_with_liquid_class( + liquid_class=water, + volume=160, + source=nest_plate.columns(), + dest=arma_plate.columns(), + new_tip="always", + trash_location=trash, + ) + with pytest.raises(RuntimeError, match=f"{expected_error_msg}"): + pipette_50.distribute_with_liquid_class( + liquid_class=water, + volume=160, + source=nest_plate.wells()[-1], + dest=arma_plate.columns(), + new_tip="once", + trash_location=trash, + ) + with pytest.raises(RuntimeError, match=f"{expected_error_msg}"): + pipette_50.consolidate_with_liquid_class( + liquid_class=water, + volume=50, + source=nest_plate.columns(), + dest=arma_plate.wells()[0], + new_tip="once", + trash_location=trash, + )