diff --git a/pylabrobot/legacy/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/legacy/liquid_handling/backends/hamilton/STAR_backend.py index 666094e0fd5..a7d75905812 100644 --- a/pylabrobot/legacy/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/legacy/liquid_handling/backends/hamilton/STAR_backend.py @@ -1473,8 +1473,7 @@ async def pick_up_tips( x_positions, y_positions, channels_involved = self._ops_to_fw_positions(ops, use_channels) - tip_spots = [op.resource for op in ops] - tips = set(cast(HamiltonTip, tip_spot.get_tip()) for tip_spot in tip_spots) + tips = set(cast(HamiltonTip, op.tip) for op in ops) if len(tips) > 1: raise ValueError("Cannot mix tips with different tip types.") ttti = await self.get_or_assign_tip_type_index(tips.pop()) @@ -1484,9 +1483,9 @@ async def pick_up_tips( max_tip_length = max((op.tip.total_tip_length - op.tip.fitting_depth) for op in ops) # not sure why this is necessary, but it is according to log files and experiments - if self._get_hamilton_tip([op.resource for op in ops]).tip_size == TipSize.LOW_VOLUME: + if self._get_hamilton_tip([op.tip for op in ops]).tip_size == TipSize.LOW_VOLUME: max_tip_length += 2 - elif self._get_hamilton_tip([op.resource for op in ops]).tip_size != TipSize.STANDARD_VOLUME: + elif self._get_hamilton_tip([op.tip for op in ops]).tip_size != TipSize.STANDARD_VOLUME: max_tip_length -= 2 tip = ops[0].tip diff --git a/pylabrobot/legacy/liquid_handling/backends/hamilton/STAR_tests.py b/pylabrobot/legacy/liquid_handling/backends/hamilton/STAR_tests.py index d2f822bcd4d..6ea1e11b8c0 100644 --- a/pylabrobot/legacy/liquid_handling/backends/hamilton/STAR_tests.py +++ b/pylabrobot/legacy/liquid_handling/backends/hamilton/STAR_tests.py @@ -1178,6 +1178,13 @@ async def test_core_96_tip_pickup(self): ] ) + async def test_tip_tracking_pick_up(self): + set_tip_tracking(enabled=True) + try: + await self.lh.pick_up_tips(self.tip_rack["A1", "B1"]) + finally: + set_tip_tracking(enabled=False) + async def test_tip_tracking_pick_up96(self): set_tip_tracking(enabled=True) await self.lh.pick_up_tips96(self.tip_rack) diff --git a/pylabrobot/legacy/liquid_handling/backends/hamilton/base.py b/pylabrobot/legacy/liquid_handling/backends/hamilton/base.py index a334e546051..ae2c7b42de9 100644 --- a/pylabrobot/legacy/liquid_handling/backends/hamilton/base.py +++ b/pylabrobot/legacy/liquid_handling/backends/hamilton/base.py @@ -21,7 +21,7 @@ LiquidHandlerBackend, ) from pylabrobot.legacy.liquid_handling.standard import PipettingOp -from pylabrobot.resources import TipSpot +from pylabrobot.resources import Tip from pylabrobot.resources.hamilton import ( HamiltonTip, TipPickupMethod, @@ -454,15 +454,14 @@ async def get_or_assign_tip_type_index(self, tip: HamiltonTip) -> int: return self._tth2tti[tip_type_hash] - def _get_hamilton_tip(self, tip_spots: List[TipSpot]) -> HamiltonTip: - """Get the single tip type for all tip spots. If it does not exist or is not a HamiltonTip, - raise an error.""" - tips = set(tip_spot.get_tip() for tip_spot in tip_spots) - if len(tips) > 1: + def _get_hamilton_tip(self, tips: Sequence[Tip]) -> HamiltonTip: + """Get the single Hamilton tip type. If mixed or not a HamiltonTip, raise an error.""" + unique = set(tips) + if len(unique) > 1: raise ValueError("Cannot mix tips with different tip types.") - if len(tips) == 0: + if len(unique) == 0: raise ValueError("No tips specified.") - tip = tips.pop() + tip = unique.pop() if not isinstance(tip, HamiltonTip): raise ValueError(f"Tip {tip} is not a HamiltonTip.") return tip diff --git a/pylabrobot/legacy/liquid_handling/backends/hamilton/vantage_backend.py b/pylabrobot/legacy/liquid_handling/backends/hamilton/vantage_backend.py index 07c02e4fc0c..e62aa199d3c 100644 --- a/pylabrobot/legacy/liquid_handling/backends/hamilton/vantage_backend.py +++ b/pylabrobot/legacy/liquid_handling/backends/hamilton/vantage_backend.py @@ -484,7 +484,7 @@ async def pick_up_tips( ): x_positions, y_positions, tip_pattern = self._ops_to_fw_positions(ops, use_channels) - tips = [cast(HamiltonTip, op.resource.get_tip()) for op in ops] + tips = [cast(HamiltonTip, op.tip) for op in ops] ttti = [await self.get_or_assign_tip_type_index(tip) for tip in tips] max_z = max(op.resource.get_location_wrt(self.deck).z + op.offset.z for op in ops) @@ -492,9 +492,9 @@ async def pick_up_tips( max_tip_length = max((op.tip.total_tip_length - op.tip.fitting_depth) for op in ops) # not sure why this is necessary, but it is according to log files and experiments - if self._get_hamilton_tip([op.resource for op in ops]).tip_size == TipSize.LOW_VOLUME: + if self._get_hamilton_tip([op.tip for op in ops]).tip_size == TipSize.LOW_VOLUME: max_tip_length += 2 - elif self._get_hamilton_tip([op.resource for op in ops]).tip_size != TipSize.STANDARD_VOLUME: + elif self._get_hamilton_tip([op.tip for op in ops]).tip_size != TipSize.STANDARD_VOLUME: max_tip_length -= 2 try: diff --git a/pylabrobot/legacy/liquid_handling/backends/hamilton/vantage_tests.py b/pylabrobot/legacy/liquid_handling/backends/hamilton/vantage_tests.py index ca3209b395b..ce8278d5301 100644 --- a/pylabrobot/legacy/liquid_handling/backends/hamilton/vantage_tests.py +++ b/pylabrobot/legacy/liquid_handling/backends/hamilton/vantage_tests.py @@ -360,6 +360,13 @@ async def test_tip_pickup_01(self): PICKUP_TIP_FORMAT, ) + async def test_tip_tracking_pick_up(self): + set_tip_tracking(enabled=True) + try: + await self.lh.pick_up_tips(self.tip_rack["A1", "B1"]) + finally: + set_tip_tracking(enabled=False) + async def test_tip_drop_01(self): await self.test_tip_pickup_01() # pick up tips first await self.lh.drop_tips(self.tip_rack["A1", "B1"]) diff --git a/pylabrobot/resources/tip_tracker.py b/pylabrobot/resources/tip_tracker.py index 1a876ce86f5..4b1ee76b92c 100644 --- a/pylabrobot/resources/tip_tracker.py +++ b/pylabrobot/resources/tip_tracker.py @@ -57,15 +57,15 @@ def has_tip(self) -> bool: return self._pending_tip is not None def get_tip(self) -> "Tip": - """Get the tip. Note that does includes pending operations. + """Get the tip. Note that this includes pending operations. Raises: NoTipError: If the tip spot does not have a tip. """ - if self._tip is None: + if self._pending_tip is None: raise NoTipError(f"{self.thing} does not have a tip.") - return self._tip + return self._pending_tip def disable(self) -> None: """Disable the tip tracker.""" diff --git a/pylabrobot/resources/tip_tracker_tests.py b/pylabrobot/resources/tip_tracker_tests.py index dabd924a528..6fff42bb598 100644 --- a/pylabrobot/resources/tip_tracker_tests.py +++ b/pylabrobot/resources/tip_tracker_tests.py @@ -43,3 +43,17 @@ def test_remove_tip(self): with self.assertRaises(NoTipError): tracker.get_tip() + + def test_get_tip_includes_pending_add(self): + tracker = TipTracker(thing="tester") + tracker.add_tip(self.tip, commit=False) + self.assertEqual(tracker.has_tip, True) + self.assertEqual(tracker.get_tip(), self.tip) + + def test_get_tip_includes_pending_remove(self): + tracker = TipTracker(thing="tester") + tracker.add_tip(self.tip) + tracker.remove_tip(commit=False) + self.assertEqual(tracker.has_tip, False) + with self.assertRaises(NoTipError): + tracker.get_tip()