Skip to content

Commit eabe3a6

Browse files
authored
fix(api): post-dispense air gaps in LC based transfers (#19025)
Closes AUTH-2138 # Overview We were previously using `airGapByVolume` of post-aspirate retract property for finding air gaps to use during post-dispense retracts. This PR fixes that so we use the `airGapByVolume` property of a post-single-dispense or a post-multi-dispense in such cases. Additionally, when adding air gaps in-between multi-dispenses (when conditioning volume is zero), we should be using the total tip volume as the lookup volume for `airGapByVolume` but we were just using `0`uL. ## Risk assessment Medium. This fixes the air gaps in a lot of cases, but the impact of this change is not too high practically speaking.
1 parent b59a51e commit eabe3a6

File tree

3 files changed

+297
-40
lines changed

3 files changed

+297
-40
lines changed

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

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,11 @@ def retract_after_dispensing(
543543
blowout_props.enabled
544544
and blowout_props.location == BlowoutLocation.DESTINATION
545545
) or not blowout_props.enabled
546+
547+
if is_final_air_gap and not add_final_air_gap:
548+
air_gap_volume = 0.0
549+
else:
550+
air_gap_volume = retract_props.air_gap_by_volume.get_for_volume(0)
546551
# Regardless of the blowout location, do touch tip and air gap
547552
# when leaving the dispense well. If this will be the final air gap, i.e,
548553
# we won't be moving to a Trash or a Source for Blowout after this air gap,
@@ -551,7 +556,7 @@ def retract_after_dispensing(
551556
touch_tip_properties=retract_props.touch_tip,
552557
location=retract_location,
553558
well=self._target_well,
554-
add_air_gap=False if is_final_air_gap and not add_final_air_gap else True,
559+
air_gap_volume=air_gap_volume,
555560
)
556561

557562
if (
@@ -599,15 +604,21 @@ def retract_after_dispensing(
599604
last_air_gap = self._tip_state.last_liquid_and_air_gap_in_tip.air_gap
600605
self._tip_state.delete_air_gap(last_air_gap)
601606
self._tip_state.ready_to_aspirate = False
607+
608+
air_gap_volume = (
609+
retract_props.air_gap_by_volume.get_for_volume(0)
610+
if add_final_air_gap
611+
else 0.0
612+
)
602613
# Do touch tip and air gap again after blowing out into source well or trash
603614
self._do_touch_tip_and_air_gap_after_dispense(
604615
touch_tip_properties=retract_props.touch_tip,
605616
location=touch_tip_and_air_gap_location,
606617
well=touch_tip_and_air_gap_well,
607-
add_air_gap=add_final_air_gap,
618+
air_gap_volume=air_gap_volume,
608619
)
609620

610-
def retract_during_multi_dispensing(
621+
def retract_during_multi_dispensing( # noqa: C901
611622
self,
612623
trash_location: Union[Location, TrashBin, WasteChute],
613624
source_location: Optional[Location],
@@ -720,6 +731,14 @@ def retract_during_multi_dispensing(
720731
else:
721732
add_air_gap = True
722733

734+
air_gap_volume = (
735+
retract_props.air_gap_by_volume.get_for_volume(
736+
self.tip_state.last_liquid_and_air_gap_in_tip.liquid
737+
)
738+
if add_air_gap
739+
else 0.0
740+
)
741+
723742
# Regardless of the blowout location, do touch tip
724743
# when leaving the dispense well.
725744
# Add an air gap depending on conditioning volume + whether this is
@@ -729,7 +748,7 @@ def retract_during_multi_dispensing(
729748
touch_tip_properties=retract_props.touch_tip,
730749
location=retract_location,
731750
well=self._target_well,
732-
add_air_gap=add_air_gap,
751+
air_gap_volume=air_gap_volume,
733752
)
734753

735754
if (
@@ -776,25 +795,30 @@ def retract_during_multi_dispensing(
776795
self._tip_state.delete_last_air_gap_and_liquid()
777796
self._tip_state.ready_to_aspirate = False
778797

798+
if (
799+
# Same check as before for when it's the final air gap of current retract
800+
conditioning_volume > 0
801+
and is_last_retract
802+
and add_final_air_gap
803+
):
804+
# The volume in tip at this point should be 0uL
805+
air_gap_volume = retract_props.air_gap_by_volume.get_for_volume(0)
806+
else:
807+
air_gap_volume = 0
779808
# Do touch tip and air gap again after blowing out into source well or trash
780809
self._do_touch_tip_and_air_gap_after_dispense(
781810
touch_tip_properties=retract_props.touch_tip,
782811
location=touch_tip_and_air_gap_location,
783812
well=touch_tip_and_air_gap_well,
784-
add_air_gap=(
785-
# Same check as before for when it's the final air gap of current retract
786-
conditioning_volume > 0
787-
and is_last_retract
788-
and add_final_air_gap
789-
),
813+
air_gap_volume=air_gap_volume,
790814
)
791815

792816
def _do_touch_tip_and_air_gap_after_dispense( # noqa: C901
793817
self,
794818
touch_tip_properties: TouchTipProperties,
795819
location: Union[Location, TrashBin, WasteChute],
796820
well: Optional[WellCore],
797-
add_air_gap: bool,
821+
air_gap_volume: float,
798822
) -> None:
799823
"""Perform touch tip and air gap as part of post-dispense retract.
800824
@@ -840,7 +864,7 @@ def _do_touch_tip_and_air_gap_after_dispense( # noqa: C901
840864
# Full speed because the tip will already be out of the liquid
841865
speed=None,
842866
)
843-
if add_air_gap or not self._tip_state.ready_to_aspirate:
867+
if air_gap_volume > 0 or not self._tip_state.ready_to_aspirate:
844868
# If we need to move the plunger up either to prepare for aspirate or to add air gap,
845869
# move to a safe location above the well if the retract location is not already
846870
# at or above this safe location
@@ -886,12 +910,8 @@ def _do_touch_tip_and_air_gap_after_dispense( # noqa: C901
886910
if not self._tip_state.ready_to_aspirate:
887911
self._instrument.prepare_to_aspirate()
888912
self._tip_state.ready_to_aspirate = True
889-
if add_air_gap:
890-
self._add_air_gap(
891-
air_gap_volume=self._transfer_properties.aspirate.retract.air_gap_by_volume.get_for_volume(
892-
0
893-
)
894-
)
913+
if air_gap_volume > 0:
914+
self._add_air_gap(air_gap_volume=air_gap_volume)
895915

896916
def _add_air_gap(
897917
self,

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

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,7 +1134,7 @@ def test_retract_after_dispense_with_blowout_in_source(
11341134
air_gap_flow_rate_by_vol = 123
11351135
air_gap_correction_by_vol = 0.321
11361136

1137-
sample_transfer_props.aspirate.retract.air_gap_by_volume.set_for_volume(
1137+
sample_transfer_props.dispense.retract.air_gap_by_volume.set_for_volume(
11381138
0, air_gap_volume
11391139
)
11401140
sample_transfer_props.aspirate.flow_rate_by_volume.set_for_volume(
@@ -1258,6 +1258,10 @@ def test_retract_after_dispense_with_blowout_in_destination(
12581258
air_gap_correction_by_vol = 0.321
12591259

12601260
sample_transfer_props.aspirate.retract.air_gap_by_volume.set_for_volume(
1261+
0,
1262+
1234, # Explicitly check that this value is not used during post-dispense air gap
1263+
)
1264+
sample_transfer_props.dispense.retract.air_gap_by_volume.set_for_volume(
12611265
0, air_gap_volume
12621266
)
12631267
sample_transfer_props.aspirate.flow_rate_by_volume.set_for_volume(
@@ -1366,7 +1370,7 @@ def test_retract_after_dispense_with_blowout_in_trash_well(
13661370
air_gap_flow_rate_by_vol = 123
13671371
air_gap_correction_by_vol = 0.321
13681372

1369-
sample_transfer_props.aspirate.retract.air_gap_by_volume.set_for_volume(
1373+
sample_transfer_props.dispense.retract.air_gap_by_volume.set_for_volume(
13701374
0, air_gap_volume
13711375
)
13721376
sample_transfer_props.aspirate.flow_rate_by_volume.set_for_volume(
@@ -1493,7 +1497,7 @@ def test_retract_after_dispense_with_blowout_in_disposal_location(
14931497
air_gap_flow_rate_by_vol = 123
14941498
air_gap_correction_by_vol = 0.321
14951499

1496-
sample_transfer_props.aspirate.retract.air_gap_by_volume.set_for_volume(
1500+
sample_transfer_props.dispense.retract.air_gap_by_volume.set_for_volume(
14971501
0, air_gap_volume
14981502
)
14991503
sample_transfer_props.aspirate.flow_rate_by_volume.set_for_volume(
@@ -1599,7 +1603,7 @@ def test_retract_after_dispense_in_trash_with_blowout_in_source(
15991603
air_gap_flow_rate_by_vol = 123
16001604
air_gap_correction_by_vol = 0.321
16011605

1602-
sample_transfer_props.aspirate.retract.air_gap_by_volume.set_for_volume(
1606+
sample_transfer_props.dispense.retract.air_gap_by_volume.set_for_volume(
16031607
0, air_gap_volume
16041608
)
16051609
sample_transfer_props.aspirate.flow_rate_by_volume.set_for_volume(
@@ -1697,7 +1701,7 @@ def test_retract_after_dispense_in_trash_with_blowout_in_destination(
16971701
air_gap_flow_rate_by_vol = 123
16981702
air_gap_correction_by_vol = 0.321
16991703

1700-
sample_transfer_props.aspirate.retract.air_gap_by_volume.set_for_volume(
1704+
sample_transfer_props.dispense.retract.air_gap_by_volume.set_for_volume(
17011705
0, air_gap_volume
17021706
)
17031707
sample_transfer_props.aspirate.flow_rate_by_volume.set_for_volume(
@@ -1780,7 +1784,7 @@ def test_retract_after_dispense_in_trash_with_blowout_in_disposal_location(
17801784
air_gap_flow_rate_by_vol = 123
17811785
air_gap_correction_by_vol = 0.321
17821786

1783-
sample_transfer_props.aspirate.retract.air_gap_by_volume.set_for_volume(
1787+
sample_transfer_props.dispense.retract.air_gap_by_volume.set_for_volume(
17841788
0, air_gap_volume
17851789
)
17861790
sample_transfer_props.aspirate.flow_rate_by_volume.set_for_volume(
@@ -1899,7 +1903,7 @@ def test_retract_after_dispense_with_blowout_in_src_moves_to_safe_loc_for_air_ga
18991903
air_gap_flow_rate_by_vol = 123
19001904
air_gap_correction_by_vol = 0.321
19011905

1902-
sample_transfer_props.aspirate.retract.air_gap_by_volume.set_for_volume(
1906+
sample_transfer_props.dispense.retract.air_gap_by_volume.set_for_volume(
19031907
0, air_gap_volume
19041908
)
19051909
sample_transfer_props.aspirate.flow_rate_by_volume.set_for_volume(
@@ -2039,26 +2043,33 @@ def test_multi_dispense_retract_after_dispense_without_conditioning_volume_or_bl
20392043
dest_well = decoy.mock(cls=WellCore)
20402044
well_top_point = Point(1, 2, 3)
20412045
sample_transfer_props.multi_dispense.retract.touch_tip.enabled = True # type: ignore[union-attr]
2042-
air_gap_volume = 0.123
20432046
air_gap_flow_rate_by_vol = 123
20442047
air_gap_correction_by_vol = 0.321
20452048

2046-
sample_transfer_props.aspirate.retract.air_gap_by_volume.set_for_volume(
2047-
0, air_gap_volume
2049+
sample_transfer_props.multi_dispense.retract.air_gap_by_volume.set_for_volume( # type: ignore[union-attr]
2050+
100, 51
20482051
)
20492052
sample_transfer_props.aspirate.flow_rate_by_volume.set_for_volume(
2050-
air_gap_volume, air_gap_flow_rate_by_vol
2053+
51, air_gap_flow_rate_by_vol
20512054
)
20522055
sample_transfer_props.aspirate.correction_by_volume.set_for_volume(
2053-
air_gap_volume, air_gap_correction_by_vol
2056+
51, air_gap_correction_by_vol
20542057
)
2055-
20562058
subject = TransferComponentsExecutor(
20572059
instrument_core=mock_instrument_core,
20582060
transfer_properties=sample_transfer_props,
20592061
target_location=Location(Point(1, 1, 1), labware=None),
20602062
target_well=dest_well,
2061-
tip_state=TipState(),
2063+
tip_state=TipState(
2064+
ready_to_aspirate=True,
2065+
last_liquid_and_air_gap_in_tip=LiquidAndAirGapPair(
2066+
# Since we'll be testing retract_during_multi_dispensing right after
2067+
# initializing the executor, this is the tip state that retract_during_multi_dispensing
2068+
# will start with.
2069+
liquid=100,
2070+
air_gap=0,
2071+
),
2072+
),
20622073
transfer_type=TransferType.ONE_TO_MANY,
20632074
)
20642075
decoy.when(mock_instrument_core.get_current_volume()).then_return(0)
@@ -2112,7 +2123,7 @@ def test_multi_dispense_retract_after_dispense_without_conditioning_volume_or_bl
21122123
and [
21132124
mock_instrument_core.air_gap_in_place(
21142125
# type: ignore[func-returns-value]
2115-
volume=air_gap_volume,
2126+
volume=51,
21162127
flow_rate=air_gap_flow_rate_by_vol,
21172128
correction_volume=air_gap_correction_by_vol,
21182129
),
@@ -2159,8 +2170,8 @@ def test_multi_dispense_retract_after_dispense_with_blowout_without_conditioning
21592170
air_gap_flow_rate_by_vol = 123
21602171
air_gap_correction_by_vol = 0.321
21612172

2162-
sample_transfer_props.aspirate.retract.air_gap_by_volume.set_for_volume(
2163-
0, air_gap_volume
2173+
sample_transfer_props.multi_dispense.retract.air_gap_by_volume.set_for_all_volumes( # type: ignore[union-attr]
2174+
air_gap_volume
21642175
)
21652176
sample_transfer_props.aspirate.flow_rate_by_volume.set_for_volume(
21662177
air_gap_volume, air_gap_flow_rate_by_vol

0 commit comments

Comments
 (0)