Skip to content

Commit

Permalink
fix(api): use smaller of max pipette volume and max tip volume for sp…
Browse files Browse the repository at this point in the history
…litting large transfer volumes (#17387)

# Overview

Found a bug in the logic that splits large volume transfers into smaller
volumes.
Bug is that we only check the transfer volume against max pipette
volume. So if you use a 50uL pipette with a 200uL tip, everything works
correctly, but if you use a 1000uL pipette with 50uL tip, it attempts to
pipette upto 1000uL, instead of clipping to the max tip volume of 50uL.

This PR fixes that by taking the smaller of pipette volume and tip
volume as the max volume a single transfer can take.

## Risk assessment

Low. A scope-limited bug fix
  • Loading branch information
sanni-t authored Jan 30, 2025
1 parent 18da739 commit 253c300
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 9 deletions.
10 changes: 8 additions & 2 deletions api/src/opentrons/protocol_api/core/engine/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -1008,7 +1008,12 @@ def transfer_liquid( # noqa: C901
source_dest_per_volume_step = tx_commons.expand_for_volume_constraints(
volumes=[volume for _ in range(len(source))],
targets=zip(source, dest),
max_volume=self.get_max_volume(),
max_volume=min(
self.get_max_volume(),
tip_racks[0][1]
.get_well_core("A1")
.get_max_volume(), # Assuming all tips in tiprack are of same volume
),
)

def _drop_tip() -> None:
Expand Down Expand Up @@ -1175,10 +1180,11 @@ def aspirate_liquid_class(
Return: List of liquid and air gap pairs in tip.
"""
aspirate_props = transfer_properties.aspirate
# TODO (spp, 2025-01-30): check if check_valid_volume_parameters is necessary and is enough.
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),
max_volume=self.get_max_volume(),
max_volume=self.get_working_volume(),
)
source_loc, source_well = source
aspirate_point = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1786,6 +1786,42 @@ def test_aspirate_liquid_class(
assert result == [LiquidAndAirGapPair(air_gap=222, liquid=111)]


def test_aspirate_liquid_class_raises_for_more_than_max_volume(
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(
mock_engine_client.state.pipettes.get_working_volume("abc123")
).then_return(100)
decoy.when(
tx_commons.check_valid_volume_parameters(
disposal_volume=0,
air_gap=test_transfer_properties.aspirate.retract.air_gap_by_volume.get_for_volume(
123
),
max_volume=100,
)
).then_raise(ValueError("Oh oh!"))
with pytest.raises(ValueError, match="Oh oh!"):
subject.aspirate_liquid_class(
volume=123,
source=(source_location, source_well),
transfer_properties=test_transfer_properties,
transfer_type=TransferType.ONE_TO_ONE,
tip_contents=[],
)


def test_dispense_liquid_class(
decoy: Decoy,
mock_engine_client: EngineClient,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ def test_water_transfer_with_volume_more_than_tip_max(
tiprack = simulated_protocol_context.load_labware(
"opentrons_flex_96_tiprack_50ul", "D1"
)
pipette_50 = simulated_protocol_context.load_instrument(
"flex_1channel_50", mount="left", tip_racks=[tiprack]
pipette_1k = simulated_protocol_context.load_instrument(
"flex_1channel_1000", mount="left", tip_racks=[tiprack]
)
nest_plate = simulated_protocol_context.load_labware(
"nest_96_wellplate_200ul_flat", "C3"
Expand All @@ -47,7 +47,7 @@ def test_water_transfer_with_volume_more_than_tip_max(
mock_manager = mock.Mock()
mock_manager.attach_mock(patched_pick_up_tip, "pick_up_tip")

pipette_50.transfer_liquid(
pipette_1k.transfer_liquid(
liquid_class=water,
volume=60,
source=nest_plate.rows()[0],
Expand All @@ -58,7 +58,7 @@ def test_water_transfer_with_volume_more_than_tip_max(
assert patched_pick_up_tip.call_count == 24
patched_pick_up_tip.reset_mock()

pipette_50.transfer_liquid(
pipette_1k.transfer_liquid(
liquid_class=water,
volume=100,
source=nest_plate.rows()[0],
Expand All @@ -69,16 +69,16 @@ def test_water_transfer_with_volume_more_than_tip_max(
assert patched_pick_up_tip.call_count == 12
patched_pick_up_tip.reset_mock()

pipette_50.pick_up_tip()
pipette_50.transfer_liquid(
pipette_1k.pick_up_tip()
pipette_1k.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()
pipette_1k.drop_tip()
assert patched_pick_up_tip.call_count == 1


Expand Down

0 comments on commit 253c300

Please sign in to comment.