Skip to content

fix(api): skip transfers if volume is zero, improve error when out of tips #18042

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions api/src/opentrons/protocol_api/core/engine/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -1238,7 +1238,8 @@ 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 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,
Expand Down Expand Up @@ -1478,7 +1479,8 @@ 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 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,
Expand Down Expand Up @@ -1569,7 +1571,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."
)
Expand Down Expand Up @@ -1751,7 +1753,8 @@ 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 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,
Expand Down
21 changes: 21 additions & 0 deletions api/src/opentrons/protocol_api/instrument_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Loading