diff --git a/pylabrobot/liquid_handling/backends/backend.py b/pylabrobot/liquid_handling/backends/backend.py index e8c73be01f..cb254eb617 100644 --- a/pylabrobot/liquid_handling/backends/backend.py +++ b/pylabrobot/liquid_handling/backends/backend.py @@ -1,7 +1,7 @@ from __future__ import annotations from abc import ABCMeta, abstractmethod -from typing import List, Optional, Union +from typing import Dict, List, Optional, Union from pylabrobot.liquid_handling.standard import ( Aspiration, @@ -20,6 +20,7 @@ ) from pylabrobot.machines.backends import MachineBackend from pylabrobot.resources import Deck, Resource +from pylabrobot.resources.tip_tracker import TipTracker class LiquidHandlerBackend(MachineBackend, metaclass=ABCMeta): @@ -36,17 +37,34 @@ class LiquidHandlerBackend(MachineBackend, metaclass=ABCMeta): def __init__(self): self.setup_finished = False self._deck: Optional[Deck] = None + self._head: Optional[Dict[int, TipTracker]] = None + self._head96: Optional[Dict[int, TipTracker]] = None def set_deck(self, deck: Deck): """Set the deck for the robot. Called automatically by `LiquidHandler.setup` or can be called manually if interacting with the backend directly. A deck must be set before setup.""" self._deck = deck + def set_heads(self, head: Dict[int, TipTracker], head96: Optional[Dict[int, TipTracker]] = None): + """Set the tip tracker for the robot. Called automatically by `LiquidHandler.setup` or can be + called manually if interacting with the backend directly. A head must be set before setup.""" + self._head = head + self._head96 = head96 + @property def deck(self) -> Deck: assert self._deck is not None, "Deck not set" return self._deck + @property + def head(self) -> Dict[int, TipTracker]: + assert self._head is not None, "Head not set" + return self._head + + @property + def head96(self) -> Optional[Dict[int, TipTracker]]: + return self._head96 + async def setup(self): """Set up the robot. This method should be called before any other method is called.""" assert self._deck is not None, "Deck not set" diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR.py b/pylabrobot/liquid_handling/backends/hamilton/STAR.py index 1846c67cb2..9abf26451f 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR.py @@ -7317,58 +7317,6 @@ async def clld_probe_z_height_using_channel( return result_in_mm - async def request_tip_len_on_channel( - self, - channel_idx: int, # 0-based indexing of channels! - ) -> float: - """ - Measures the length of the tip attached to the specified pipetting channel. - - Checks if a tip is present on the given channel. If present, moves all channels - to THE safe Z position, 334.3 mm, measures the tip bottom Z-coordinate, and calculates - the total tip length. Supports tips of lengths 50.4 mm, 59.9 mm, and 95.1 mm. - Raises an error if the tip length is unsupported or if no tip is present. - - Parameters: - channel_idx: Index of the pipetting channel (0-based). - - Returns: - The measured tip length in millimeters. - - Raises: - ValueError: If no tip is present on the channel or if the tip length is unsupported. - """ - - # Check there is a tip on the channel - all_channel_occupancy = await self.request_tip_presence() - if not all_channel_occupancy[channel_idx]: - raise ValueError(f"No tip present on channel {channel_idx}") - - # Level all channels - await self.move_all_channels_in_z_safety() - known_top_position_channel_head = 334.3 # mm - fitting_depth_of_all_standard_channel_tips = 8 # mm - unknown_offset_for_all_tips = 0.4 # mm - - # Request z-coordinate of channel+tip bottom - tip_bottom_z_coordinate = await self.request_z_pos_channel_n( - pipetting_channel_index=channel_idx - ) - - total_tip_len = round( - known_top_position_channel_head - - ( - tip_bottom_z_coordinate - - fitting_depth_of_all_standard_channel_tips - - unknown_offset_for_all_tips - ), - 1, - ) - - if total_tip_len in [50.4, 59.9, 95.1]: # 50ul, 300ul, 1000ul - return total_tip_len - raise ValueError(f"Tip of length {total_tip_len} not yet supported") - async def ztouch_probe_z_height_using_channel( self, channel_idx: int, # 0-based indexing of channels! @@ -7389,6 +7337,8 @@ async def ztouch_probe_z_height_using_channel( Args: channel_idx: The index of the channel to use for probing. Backmost channel = 0. + tip_len: override the tip length (of tip on channel `channel_idx`). Default is the tip length + of the tip that was picked up. lowest_immers_pos: The lowest immersion position in mm. start_pos_lld_search: The start position for z-touch search in mm. channel_speed: The speed of channel movement in mm/sec. @@ -7414,10 +7364,12 @@ async def ztouch_probe_z_height_using_channel( f"found version '{version}'" ) - fitting_depth = 8 # mm, for 10, 50, 300, 1000 ul Hamilton tips - if tip_len is None: - tip_len = await self.request_tip_len_on_channel(channel_idx=channel_idx) + tip_len = self.head[channel_idx].get_tip().total_tip_length + + # fitting_depth = 8 mm for 10, 50, 300, 1000 ul Hamilton tips + fitting_depth = self.head[channel_idx].get_tip().fitting_depth + if start_pos_search is None: start_pos_search = 334.7 - tip_len + fitting_depth diff --git a/pylabrobot/liquid_handling/liquid_handler.py b/pylabrobot/liquid_handling/liquid_handler.py index 7649b3dc73..ba18c81e75 100644 --- a/pylabrobot/liquid_handling/liquid_handler.py +++ b/pylabrobot/liquid_handling/liquid_handler.py @@ -173,6 +173,7 @@ async def setup(self, **backend_kwargs): raise RuntimeError("The setup has already finished. See `LiquidHandler.stop`.") self.backend.set_deck(self.deck) + self.backend.set_heads(head=self.head, head96=self.head96) await super().setup(**backend_kwargs) self.head = {c: TipTracker(thing=f"Channel {c}") for c in range(self.backend.num_channels)}