Skip to content

Commit 07539d6

Browse files
authored
fix(api): raise better error during LC transfer when trying to dispense more than tip liquid volume (#18844)
# Overview Without the change in this PR, if an attempt is made to dispense more than the volume present in tip, an obscure error saying `'Value must be a positive float.'`. This error stems from the `dispense_and_wait` method when trying to find the correction volume for a negative volume found by calculating tip volume after dispense as `current_tip_vol - dispense_vol`. This PR adds a quick check for tip volume and raises an informative error to the user. ## Risk assessment None. Only adds an error message.
1 parent 5268a4e commit 07539d6

File tree

5 files changed

+84
-4
lines changed

5 files changed

+84
-4
lines changed

api/src/opentrons/protocol_api/core/engine/instrument.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
NoLiquidClassPropertyError,
3131
)
3232
from opentrons.protocols.advanced_control.transfers import common as tx_commons
33+
from opentrons.protocols.advanced_control.transfers.transfer_liquid_utils import (
34+
check_current_volume_before_dispensing,
35+
)
3336
from opentrons.protocol_engine import commands as cmd
3437
from opentrons.protocol_engine import (
3538
DeckPoint,
@@ -2143,9 +2146,12 @@ def remove_air_gap_during_transfer_with_liquid_class(
21432146
"""Remove an air gap that was previously added during a transfer."""
21442147
if last_air_gap == 0:
21452148
return
2146-
2149+
current_vol = self.get_current_volume()
2150+
check_current_volume_before_dispensing(
2151+
current_volume=current_vol, dispense_volume=last_air_gap
2152+
)
21472153
correction_volume = dispense_props.correction_by_volume.get_for_volume(
2148-
self.get_current_volume() - last_air_gap
2154+
current_vol - last_air_gap
21492155
)
21502156
# The minimum flow rate should be air_gap_volume per second
21512157
flow_rate = max(

api/src/opentrons/protocol_api/core/engine/transfer_components_executor.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from opentrons.types import Location, Point, Mount
2626
from opentrons.protocols.advanced_control.transfers.transfer_liquid_utils import (
2727
LocationCheckDescriptors,
28+
check_current_volume_before_dispensing,
2829
)
2930
from opentrons.protocols.advanced_control.transfers import (
3031
transfer_liquid_utils as tx_utils,
@@ -253,8 +254,12 @@ def dispense_and_wait(
253254
push_out_override: Optional[float],
254255
) -> None:
255256
"""Dispense according to dispense properties and wait if enabled."""
257+
current_vol = self._instrument.get_current_volume()
258+
check_current_volume_before_dispensing(
259+
current_volume=current_vol, dispense_volume=volume
260+
)
256261
correction_volume = dispense_properties.correction_by_volume.get_for_volume(
257-
self._instrument.get_current_volume() - volume
262+
current_vol - volume
258263
)
259264
self._instrument.dispense(
260265
location=self._target_location,

api/src/opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,3 +212,20 @@ def _group_wells_for_nozzle_configuration( # noqa: C901
212212
grouped_wells.reverse()
213213

214214
return grouped_wells
215+
216+
217+
def check_current_volume_before_dispensing(
218+
current_volume: float,
219+
dispense_volume: float,
220+
) -> None:
221+
"""Check if the current volume is valid for dispensing the dispense volume."""
222+
if current_volume < dispense_volume:
223+
# Although this should never happen, we can get into an unexpected state
224+
# following error recovery and not have the expected amount of liquid in the tip.
225+
# If this happens, we want to raise a useful error so the user can understand
226+
# the cause of the problem. If we don't make this check for current volume,
227+
# an unhelpful error might get raised when a '..byVolume' property encounters
228+
# a negative volume (current_volume - dispense_volume).
229+
raise RuntimeError(
230+
f"Cannot dispense {dispense_volume}uL when the tip has only {current_volume}uL."
231+
)

api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2425,7 +2425,7 @@ def test_remove_air_gap_during_transfer_with_liquid_class(
24252425
"""It should remove air gap by calling dispense and delay with liquid class props."""
24262426
test_transfer_props = decoy.mock(cls=TransferProperties)
24272427
air_gap_correction_by_vol = 0.321
2428-
current_volume = 0.654
2428+
current_volume = 3.21
24292429

24302430
test_transfer_props.dispense.delay.duration = 321
24312431
test_transfer_props.dispense.delay.enabled = True
@@ -2463,6 +2463,32 @@ def test_remove_air_gap_during_transfer_with_liquid_class(
24632463
)
24642464

24652465

2466+
@pytest.mark.parametrize("version", versions_at_or_above(APIVersion(2, 24)))
2467+
def test_remove_air_gap_during_transfer_raises_if_tip_volume_less_than_dispense_vol(
2468+
decoy: Decoy,
2469+
mock_engine_client: EngineClient,
2470+
mock_protocol_core: ProtocolCore,
2471+
subject: InstrumentCore,
2472+
version: APIVersion,
2473+
) -> None:
2474+
"""It should raise an error if tip volume is less than dispense volume."""
2475+
test_transfer_props = decoy.mock(cls=TransferProperties)
2476+
decoy.when(mock_protocol_core.api_version).then_return(version)
2477+
decoy.when(
2478+
mock_engine_client.state.pipettes.get_aspirated_volume(
2479+
pipette_id=subject.pipette_id
2480+
)
2481+
).then_return(50)
2482+
with pytest.raises(
2483+
RuntimeError, match="Cannot dispense 50.1uL when the tip has only 50uL."
2484+
):
2485+
subject.remove_air_gap_during_transfer_with_liquid_class(
2486+
last_air_gap=50.1,
2487+
dispense_props=test_transfer_props.dispense,
2488+
location=Location(Point(1, 2, 3), labware=None),
2489+
)
2490+
2491+
24662492
@pytest.mark.parametrize("version", versions_at_or_above(APIVersion(2, 23)))
24672493
def test_remove_air_gap_during_transfer_with_liquid_class_handles_delays(
24682494
decoy: Decoy,

api/tests/opentrons/protocol_api/core/engine/test_transfer_components_executor.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,32 @@ def test_dispense_and_wait_skips_delay(
476476
)
477477

478478

479+
def test_dispense_and_wait_raises_if_tip_volume_less_than_dispense_vol(
480+
decoy: Decoy,
481+
mock_instrument_core: InstrumentCore,
482+
sample_transfer_props: TransferProperties,
483+
) -> None:
484+
"""Should raise a useful error if trying to dispense more than liquid present in tip."""
485+
decoy.when(mock_instrument_core.get_current_volume()).then_return(50)
486+
487+
subject = TransferComponentsExecutor(
488+
instrument_core=mock_instrument_core,
489+
transfer_properties=sample_transfer_props,
490+
target_location=Location(Point(1, 2, 3), labware=None),
491+
target_well=decoy.mock(cls=WellCore),
492+
tip_state=TipState(),
493+
transfer_type=TransferType.ONE_TO_ONE,
494+
)
495+
with pytest.raises(
496+
RuntimeError, match="Cannot dispense 51uL when the tip has only 50uL."
497+
):
498+
subject.dispense_and_wait(
499+
dispense_properties=sample_transfer_props.dispense,
500+
volume=51,
501+
push_out_override=123,
502+
)
503+
504+
479505
def test_dispense_into_trash_and_wait(
480506
decoy: Decoy,
481507
mock_instrument_core: InstrumentCore,

0 commit comments

Comments
 (0)