Skip to content

Commit 95b9d6e

Browse files
jbleon95ddcc4
andauthored
refactor(api): move last tip picked up tracking from context to engine (#19061)
# Overview This PR refactors the way the Python API tracks where a tip has been picked up from. Previously we were relying on the `InstrumentContext._last_tip_picked_up_from` to keep track of which tip rack well the current tip was picked up from, mostly for the purposes of storing it for `return_tip`. The issue with this approach is that anything that did not go through the top level instrument context, whether it is via the cores or directly in protocol engine, would lead to this not being updated and lead to either incorrect behavior for tip returns or an error. This became more acute with the introduction of liquid classes and allowing them to return tips and keep them at the end, as this variable wasn't getting updated and we had to manually code around that. Now the tracking of the last tip rack well has been moved entirely to the engine, where it will store it as a combination of `labware_id` and `well_name`. This is then reconstructed into a `LabwareCore` and `WellCore` by the `InstrumentCore`, which is further reconstructed into a `Well` object by the top level `InstrumentContext`. This allowed a lot of the liquid class scaffolding around this issue to be removed and those methods to be simplified. This change also required adding this functionality to the legacy core. With no engine, in those older cores we are now simply storing the legacy labware and well cores when a pick up tip or drop tip is called, allowing this new method to work via both intefaces. Because it was discovered a lot of older protocols rely on `_last_tip_picked_up_from`, a property of the same name was added that uses the new interface. In addition, a version gated `current_tip_source_well()` function was added to provide a version-protected method that should now be used instead. ## Test Plan and Hands on Testing Snapshots and integration tests should cover a lot of the regression testing, but the following protocols will be tested: ``` requirements = { "robotType": "Flex", "apiLevel": "2.25" } metadata = { "protocolName":'Return tip new', 'author':'Jeremy' } def run(protocol_context): tiprack = protocol_context.load_labware("opentrons_flex_96_tiprack_200ul", "C2") trash = protocol_context.load_trash_bin('A3') pipette_1k = protocol_context.load_instrument("flex_1channel_1000", "right", tip_racks=[tiprack]) nest_plate = protocol_context.load_labware("nest_96_wellplate_2ml_deep", "D1") arma_plate = protocol_context.load_labware("armadillo_96_wellplate_200ul_pcr_full_skirt", "D3") pipette_1k.pick_up_tip() pipette_1k.return_tip() water_class = protocol_context.get_liquid_class("water") pipette_1k.transfer_with_liquid_class( liquid_class=water_class, volume=200, source=nest_plate.wells()[:4], dest=arma_plate.wells()[:4], new_tip="always", trash_location=trash, return_tip=True, ) pipette_1k.transfer_with_liquid_class( liquid_class=water_class, volume=200, source=nest_plate.wells()[0], dest=arma_plate.wells()[0], new_tip="once", trash_location=trash, keep_last_tip=True ) pipette_1k.return_tip() ``` ``` requirements = { "robotType": "OT-2", "apiLevel": "2.13" } metadata = { "protocolName":'Return tip OT-2 2.13', 'author':'Jeremy' } def run(protocol_context): p300rack = protocol_context.load_labware('opentrons_96_tiprack_300ul', 1) p300 = protocol_context.load_instrument('p300_single_gen2', 'left', tip_racks=[p300rack]) p300.pick_up_tip() p300.return_tip() ``` ## Changelog - Refactored `_last_tip_picked_up_from` to use engine and cores to keep track of tip, not the instrument context ## Review requests Does the way we reconstruct the `Well` object make sense? ## Risk assessment Medium/high, this refactor touches a lot of long existing methods and is not gated by version. There's no new logic added but this affects not only Flex run code but OT-2 older API versions as well. --------- Co-authored-by: David Chau <[email protected]>
1 parent 1ec272e commit 95b9d6e

35 files changed

+383
-228
lines changed

api/src/opentrons/protocol_api/_transfer_liquid_validation.py

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from dataclasses import dataclass
2-
from typing import List, Union, Sequence, Optional, Tuple
2+
from typing import List, Union, Sequence, Optional
33

44
from opentrons.types import Location, NozzleMapInterface
55
from opentrons.protocols.api_support import instrument
@@ -13,7 +13,6 @@
1313

1414
from .disposal_locations import TrashBin, WasteChute
1515
from .labware import Labware, Well
16-
from .core.common import WellCore
1716
from . import validation
1817

1918

@@ -25,7 +24,6 @@ class TransferInfo:
2524
tip_policy: TransferTipPolicyV2
2625
tip_racks: List[Labware]
2726
trash_location: Union[Location, TrashBin, WasteChute]
28-
last_tip_location: Optional[Tuple[Location, WellCore]]
2927

3028

3129
def verify_and_normalize_transfer_args(
@@ -88,22 +86,12 @@ def verify_and_normalize_transfer_args(
8886
trash_location=_trash_location
8987
)
9088

91-
if last_tip_well is not None:
92-
parent_tip_rack = last_tip_well.parent
93-
last_tip_location = (
94-
Location(last_tip_well.top().point, parent_tip_rack),
95-
last_tip_well._core,
96-
)
97-
else:
98-
last_tip_location = None
99-
10089
return TransferInfo(
10190
source=flat_sources_list,
10291
dest=flat_dests_list if not isinstance(dest, (TrashBin, WasteChute)) else dest,
10392
tip_policy=valid_new_tip,
10493
tip_racks=valid_tip_racks,
10594
trash_location=valid_trash_location,
106-
last_tip_location=last_tip_location,
10795
)
10896

10997

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

Lines changed: 36 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,6 +1047,25 @@ def get_nozzle_configuration(self) -> NozzleConfigurationType:
10471047
def get_liquid_presence_detection(self) -> bool:
10481048
return self._liquid_presence_detection
10491049

1050+
def get_tip_origin(
1051+
self,
1052+
) -> Optional[Tuple[LabwareCore, WellCore]]:
1053+
last_tip_pickup_info = (
1054+
self._engine_client.state.pipettes.get_tip_rack_well_picked_up_from(
1055+
self._pipette_id
1056+
)
1057+
)
1058+
if last_tip_pickup_info is None:
1059+
return None
1060+
else:
1061+
tip_rack_labware_core = self._protocol_core._labware_cores_by_id[
1062+
last_tip_pickup_info.labware_id
1063+
]
1064+
tip_well_core = tip_rack_labware_core.get_well_core(
1065+
last_tip_pickup_info.well_name
1066+
)
1067+
return tip_rack_labware_core, tip_well_core
1068+
10501069
def is_tip_tracking_available(self) -> bool:
10511070
if self.get_nozzle_configuration() == NozzleConfigurationType.FULL:
10521071
return True
@@ -1215,8 +1234,7 @@ def transfer_with_liquid_class( # noqa: C901
12151234
trash_location: Union[Location, TrashBin, WasteChute],
12161235
return_tip: bool,
12171236
keep_last_tip: bool,
1218-
last_tip_location: Optional[Tuple[Location, WellCore]],
1219-
) -> Optional[Tuple[Location, WellCore]]:
1237+
) -> None:
12201238
"""Execute transfer using liquid class properties.
12211239
12221240
Args:
@@ -1238,10 +1256,6 @@ def transfer_with_liquid_class( # noqa: C901
12381256
return_tip: If `True`, return tips to the tip rack location they were picked up from,
12391257
otherwise drop in `trash_location`
12401258
keep_last_tip: When set to `True`, do not drop the final tip used in the transfer.
1241-
last_tip_location: If a tip is already attached, this will be the tiprack and well it was
1242-
picked up from, represented as a tuple of types.Location and WellCore.
1243-
Used so a tip can be returned if it was picked up outside this function
1244-
as could be the case for a new_tip of `never`.
12451259
"""
12461260
if not tip_racks:
12471261
raise RuntimeError(
@@ -1278,9 +1292,8 @@ def transfer_with_liquid_class( # noqa: C901
12781292
)
12791293
)
12801294

1281-
last_tip = last_tip_location
12821295
if new_tip == TransferTipPolicyV2.ONCE:
1283-
last_tip = self._pick_up_tip_for_liquid_class(
1296+
self._pick_up_tip_for_liquid_class(
12841297
tip_racks, starting_tip, tiprack_uri_for_transfer_props
12851298
)
12861299

@@ -1319,10 +1332,8 @@ def transfer_with_liquid_class( # noqa: C901
13191332
)
13201333
):
13211334
if prev_src is not None and prev_dest is not None:
1322-
self._drop_tip_for_liquid_class(
1323-
trash_location, last_tip, return_tip
1324-
)
1325-
last_tip = self._pick_up_tip_for_liquid_class(
1335+
self._drop_tip_for_liquid_class(trash_location, return_tip)
1336+
self._pick_up_tip_for_liquid_class(
13261337
tip_racks, starting_tip, tiprack_uri_for_transfer_props
13271338
)
13281339
post_disp_tip_contents = [
@@ -1354,10 +1365,7 @@ def transfer_with_liquid_class( # noqa: C901
13541365
prev_dest = step_destination
13551366

13561367
if not keep_last_tip:
1357-
self._drop_tip_for_liquid_class(trash_location, last_tip, return_tip)
1358-
last_tip = None
1359-
1360-
return last_tip
1368+
self._drop_tip_for_liquid_class(trash_location, return_tip)
13611369

13621370
def distribute_with_liquid_class( # noqa: C901
13631371
self,
@@ -1375,8 +1383,7 @@ def distribute_with_liquid_class( # noqa: C901
13751383
trash_location: Union[Location, TrashBin, WasteChute],
13761384
return_tip: bool,
13771385
keep_last_tip: bool,
1378-
last_tip_location: Optional[Tuple[Location, WellCore]],
1379-
) -> Optional[Tuple[Location, WellCore]]:
1386+
) -> None:
13801387
"""Execute a distribution using liquid class properties.
13811388
13821389
Args:
@@ -1399,10 +1406,6 @@ def distribute_with_liquid_class( # noqa: C901
13991406
return_tip: If `True`, return tips to the tip rack location they were picked up from,
14001407
otherwise drop in `trash_location`
14011408
keep_last_tip: When set to `True`, do not drop the final tip used in the distribute.
1402-
last_tip_location: If a tip is already attached, this will be the tiprack and well it was
1403-
picked up from, represented as a tuple of types.Location and WellCore.
1404-
Used so a tip can be returned if it was picked up outside this function
1405-
as could be the case for a new_tip of `never`
14061409
14071410
This method distributes the liquid in the source well into multiple destinations.
14081411
It can accomplish this by either doing a multi-dispense (aspirate once and then
@@ -1462,7 +1465,6 @@ def distribute_with_liquid_class( # noqa: C901
14621465
trash_location=trash_location,
14631466
return_tip=return_tip,
14641467
keep_last_tip=keep_last_tip,
1465-
last_tip_location=last_tip_location,
14661468
)
14671469

14681470
# TODO: use the ID returned by load_liquid_class in command annotations
@@ -1484,9 +1486,8 @@ def distribute_with_liquid_class( # noqa: C901
14841486
)
14851487
)
14861488

1487-
last_tip = last_tip_location
14881489
if new_tip != TransferTipPolicyV2.NEVER:
1489-
last_tip = self._pick_up_tip_for_liquid_class(
1490+
self._pick_up_tip_for_liquid_class(
14901491
tip_racks, starting_tip, tiprack_uri_for_transfer_props
14911492
)
14921493

@@ -1553,8 +1554,8 @@ def distribute_with_liquid_class( # noqa: C901
15531554
)
15541555

15551556
if not is_first_step and new_tip == TransferTipPolicyV2.ALWAYS:
1556-
self._drop_tip_for_liquid_class(trash_location, last_tip, return_tip)
1557-
last_tip = self._pick_up_tip_for_liquid_class(
1557+
self._drop_tip_for_liquid_class(trash_location, return_tip)
1558+
self._pick_up_tip_for_liquid_class(
15581559
tip_racks, starting_tip, tiprack_uri_for_transfer_props
15591560
)
15601561
tip_contents = [
@@ -1614,10 +1615,7 @@ def distribute_with_liquid_class( # noqa: C901
16141615
is_first_step = False
16151616

16161617
if not keep_last_tip:
1617-
self._drop_tip_for_liquid_class(trash_location, last_tip, return_tip)
1618-
last_tip = None
1619-
1620-
return last_tip
1618+
self._drop_tip_for_liquid_class(trash_location, return_tip)
16211619

16221620
def _tip_can_hold_volume_for_multi_dispensing(
16231621
self,
@@ -1656,8 +1654,7 @@ def consolidate_with_liquid_class( # noqa: C901
16561654
trash_location: Union[Location, TrashBin, WasteChute],
16571655
return_tip: bool,
16581656
keep_last_tip: bool,
1659-
last_tip_location: Optional[Tuple[Location, WellCore]],
1660-
) -> Optional[Tuple[Location, WellCore]]:
1657+
) -> None:
16611658
"""Execute consolidate using liquid class properties.
16621659
16631660
Args:
@@ -1681,10 +1678,6 @@ def consolidate_with_liquid_class( # noqa: C901
16811678
return_tip: If `True`, return tips to the tip rack location they were picked up from,
16821679
otherwise drop in `trash_location`
16831680
keep_last_tip: When set to `True`, do not drop the final tip used in the consolidate.
1684-
last_tip_location: If a tip is already attached, this will be the tiprack and well it was
1685-
picked up from, represented as a tuple of types.Location and WellCore.
1686-
Used so a tip can be returned if it was picked up outside this function
1687-
as could be the case for a new_tip of `never`.
16881681
"""
16891682
if not tip_racks:
16901683
raise RuntimeError(
@@ -1730,9 +1723,8 @@ def consolidate_with_liquid_class( # noqa: C901
17301723
)
17311724
)
17321725

1733-
last_tip = last_tip_location
17341726
if new_tip in [TransferTipPolicyV2.ONCE, TransferTipPolicyV2.ALWAYS]:
1735-
last_tip = self._pick_up_tip_for_liquid_class(
1727+
self._pick_up_tip_for_liquid_class(
17361728
tip_racks, starting_tip, tiprack_uri_for_transfer_props
17371729
)
17381730

@@ -1764,8 +1756,8 @@ def consolidate_with_liquid_class( # noqa: C901
17641756
break
17651757

17661758
if not is_first_step and new_tip == TransferTipPolicyV2.ALWAYS:
1767-
self._drop_tip_for_liquid_class(trash_location, last_tip, return_tip)
1768-
last_tip = self._pick_up_tip_for_liquid_class(
1759+
self._drop_tip_for_liquid_class(trash_location, return_tip)
1760+
self._pick_up_tip_for_liquid_class(
17691761
tip_racks, starting_tip, tiprack_uri_for_transfer_props
17701762
)
17711763
tip_contents = [
@@ -1802,10 +1794,7 @@ def consolidate_with_liquid_class( # noqa: C901
18021794
)
18031795

18041796
if not keep_last_tip:
1805-
self._drop_tip_for_liquid_class(trash_location, last_tip, return_tip)
1806-
last_tip = None
1807-
1808-
return last_tip
1797+
self._drop_tip_for_liquid_class(trash_location, return_tip)
18091798

18101799
def _get_location_and_well_core_from_next_tip_info(
18111800
self,
@@ -1857,7 +1846,7 @@ def _pick_up_tip_for_liquid_class(
18571846
tip_racks: List[Tuple[Location, LabwareCore]],
18581847
starting_tip: Optional[WellCore],
18591848
tiprack_uri_for_transfer_props: str,
1860-
) -> Tuple[Location, WellCore]:
1849+
) -> None:
18611850
"""Resolve next tip and pick it up, for use in liquid class transfer code."""
18621851
next_tip = self.get_next_tip(
18631852
tip_racks=[core for loc, core in tip_racks],
@@ -1884,16 +1873,15 @@ def _pick_up_tip_for_liquid_class(
18841873
presses=None,
18851874
increment=None,
18861875
)
1887-
return tiprack_loc, tip_well
18881876

18891877
def _drop_tip_for_liquid_class(
18901878
self,
18911879
trash_location: Union[Location, TrashBin, WasteChute],
1892-
last_tip: Optional[Tuple[Location, WellCore]],
18931880
return_tip: bool,
18941881
) -> None:
18951882
"""Drop or return tip for usage in liquid class transfers."""
18961883
if return_tip:
1884+
last_tip = self.get_tip_origin()
18971885
assert last_tip is not None
18981886
_, tip_well = last_tip
18991887
self.drop_tip(

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,12 @@ def get_blow_out_flow_rate(self, rate: float = 1.0) -> float:
310310
def get_liquid_presence_detection(self) -> bool:
311311
...
312312

313+
@abstractmethod
314+
def get_tip_origin(
315+
self,
316+
) -> Optional[Tuple[LabwareCoreType, WellCoreType]]:
317+
...
318+
313319
@abstractmethod
314320
def _pressure_supported_by_pipette(self) -> bool:
315321
...
@@ -372,8 +378,7 @@ def transfer_with_liquid_class(
372378
trash_location: Union[types.Location, TrashBin, WasteChute],
373379
return_tip: bool,
374380
keep_last_tip: bool,
375-
last_tip_location: Optional[Tuple[types.Location, WellCoreType]],
376-
) -> Optional[Tuple[types.Location, WellCoreType]]:
381+
) -> None:
377382
"""Transfer a liquid from source to dest according to liquid class properties."""
378383
...
379384

@@ -390,8 +395,7 @@ def distribute_with_liquid_class(
390395
trash_location: Union[types.Location, TrashBin, WasteChute],
391396
return_tip: bool,
392397
keep_last_tip: bool,
393-
last_tip_location: Optional[Tuple[types.Location, WellCoreType]],
394-
) -> Optional[Tuple[types.Location, WellCoreType]]:
398+
) -> None:
395399
"""
396400
Distribute a liquid from single source to multiple destinations
397401
according to liquid class properties.
@@ -411,8 +415,7 @@ def consolidate_with_liquid_class(
411415
trash_location: Union[types.Location, TrashBin, WasteChute],
412416
return_tip: bool,
413417
keep_last_tip: bool,
414-
last_tip_location: Optional[Tuple[types.Location, WellCoreType]],
415-
) -> Optional[Tuple[types.Location, WellCoreType]]:
418+
) -> None:
416419
"""
417420
Consolidate liquid from multiple sources to a single destination
418421
using the specified liquid class properties.

api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ def __init__(
6767
api_level=self._api_version,
6868
)
6969
self._liquid_presence_detection = False
70+
self._last_tip_rack_and_well: Optional[
71+
Tuple[LegacyLabwareCore, LegacyWellCore]
72+
] = None
7073

7174
def get_default_speed(self) -> float:
7275
"""Gets the speed at which the robot's gantry moves."""
@@ -252,6 +255,7 @@ def pick_up_tip(
252255
num_channels=self.get_channels(),
253256
fail_if_full=self._api_version < APIVersion(2, 2),
254257
)
258+
self._last_tip_rack_and_well = tip_rack_core, well_core
255259

256260
def drop_tip(
257261
self,
@@ -298,6 +302,7 @@ def drop_tip(
298302
hw = self._protocol_interface.get_hardware()
299303
self.move_to(location=location)
300304
hw.drop_tip(self._mount, home_after=True if home_after is None else home_after)
305+
self._last_tip_rack_and_well = None
301306

302307
if self._api_version < APIVersion(2, 2) and labware_core.is_tip_rack():
303308
# If this is a tiprack we can try and add the dirty tip back to the tracker
@@ -532,6 +537,11 @@ def get_blow_out_flow_rate(self, rate: float = 1.0) -> float:
532537
def get_speed(self) -> PlungerSpeeds:
533538
return self._speeds
534539

540+
def get_tip_origin(
541+
self,
542+
) -> Optional[Tuple[LegacyLabwareCore, LegacyWellCore]]:
543+
return self._last_tip_rack_and_well
544+
535545
def set_flow_rate(
536546
self,
537547
aspirate: Optional[float] = None,
@@ -612,8 +622,7 @@ def transfer_with_liquid_class(
612622
trash_location: Union[types.Location, TrashBin, WasteChute],
613623
return_tip: bool,
614624
keep_last_tip: bool,
615-
last_tip_location: Optional[Tuple[types.Location, LegacyWellCore]],
616-
) -> Optional[Tuple[types.Location, LegacyWellCore]]:
625+
) -> None:
617626
"""This will never be called because it was added in API 2.23"""
618627
assert False, "transfer_liquid is not supported in legacy context"
619628

@@ -629,8 +638,7 @@ def distribute_with_liquid_class(
629638
trash_location: Union[types.Location, TrashBin, WasteChute],
630639
return_tip: bool,
631640
keep_last_tip: bool,
632-
last_tip_location: Optional[Tuple[types.Location, LegacyWellCore]],
633-
) -> Optional[Tuple[types.Location, LegacyWellCore]]:
641+
) -> None:
634642
"""This will never be called because it was added in API 2.23"""
635643
assert False, "distribute_liquid is not supported in legacy context"
636644

@@ -646,8 +654,7 @@ def consolidate_with_liquid_class(
646654
trash_location: Union[types.Location, TrashBin, WasteChute],
647655
return_tip: bool,
648656
keep_last_tip: bool,
649-
last_tip_location: Optional[Tuple[types.Location, LegacyWellCore]],
650-
) -> Optional[Tuple[types.Location, LegacyWellCore]]:
657+
) -> None:
651658
"""This will never be called because it was added in API 2.23."""
652659
assert False, "consolidate_liquid is not supported in legacy context"
653660

0 commit comments

Comments
 (0)