diff --git a/docs/user_guide/00_liquid-handling/hamilton-star/surface-following.ipynb b/docs/user_guide/00_liquid-handling/hamilton-star/surface-following.ipynb index 28f4e248f17..a0c741f228b 100644 --- a/docs/user_guide/00_liquid-handling/hamilton-star/surface-following.ipynb +++ b/docs/user_guide/00_liquid-handling/hamilton-star/surface-following.ipynb @@ -9,11 +9,11 @@ "\n", "Surface following is a feature on Hamilton liquid handling robots that makes the pipette tip follow the surface of a liquid when aspirating (going down) or dispensing (going up).\n", "\n", - "When using automatic surface following, the robot will automatically move the Z position of the pipette tip based on a user specified value. The amount of surface following required can be computed by subtracting the liquid level before and after each aspiration or dispense. PyLabRobot can do this automatically when the height<>volume functions for the given containers are defined. You can also specify the liquid surface following distance manually.\n", + "When using surface following, the robot will automatically move the Z position of the pipette tip the user-specified distance. The amount of surface following required can be computed by comparing the liquid level before and after each aspiration or dispense. PyLabRobot can do this automatically when the height<>volume functions for the given containers are defined. You can also specify the liquid surface following distance manually.\n", "\n", - "It is useful to start the surface following only at the liquid level, so it is recommended to use [liquid level detection](./star_lld) with the surface following feature. (See below). VENUS also supports this.\n", + "It is useful to start the surface following only at the liquid level, so it is recommended to use [liquid level detection](./star_lld) with the surface following feature. (See below for syntax, which differs from the LLD tutorial). VENUS also supports surface following while doing LLD.\n", "\n", - "In PLR, when we have LLD + automatic surface following, we can go beyond VENUS by computing the surface following amount based on the precise location of liquid inside the container. This is necessary because the surface following amount is not only a function of the volume of liquid aspirated or dispensed, but also of the location of liquid inside the container (see below). By doing liquid level detection first to get the precise liquid level, we can use that to compute the surface following amount based on the requested volume _and_ location of liquid inside the container.\n", + "In PLR, when we have LLD + automatic surface following, we can go beyond VENUS by computing the surface following amount based on the precise location of liquid inside the container. This is necessary because the surface following amount is not _just_ a function of the volume of liquid aspirated or dispensed, _but also_ of the location of liquid inside the container (see below). By doing liquid level detection first to get the precise liquid level, we can then use that liquid level height to compute the surface following amount based on the requested volume _and_ location of liquid inside the container.\n", "\n", "![](./img/surface_following/surface_following_distance.svg)" ] @@ -28,7 +28,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "id": "d3f86a7d", "metadata": {}, "outputs": [], @@ -64,7 +64,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "id": "5e9d4e1a", "metadata": {}, "outputs": [], @@ -78,12 +78,12 @@ "id": "aebdc554", "metadata": {}, "source": [ - "You can probe the liquid height first using liquid level detection, and then use automatic surface following for subsequent aspirations and dispenses as follows:" + "You can probe the liquid height first using liquid level detection (capacitive), and then use automatic surface following for subsequent aspirations and dispenses as follows:" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "id": "d4858585", "metadata": {}, "outputs": [], @@ -113,12 +113,12 @@ "id": "0b3ac98c", "metadata": {}, "source": [ - "You can also pass the liquid height directly to the aspiration and dispense methods, and still use automatic surface following:" + "You can also pass the liquid height directly to the aspiration and dispense methods, and still use automatic surface following. This can be useful when you cannot use LLD." ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 4, "id": "ffde0c4c", "metadata": {}, "outputs": [], @@ -152,12 +152,12 @@ "id": "aee70228", "metadata": {}, "source": [ - "To manually specify the surface following amount, you can use the `surface_following_distance` backend kwarg of the aspiration and dispense methods. For example, to aspirate 100 µL with a surface following amount of 2 mm above the detected liquid height:" + "To manually specify the surface following amount, you can use the `surface_following_distance` backend kwarg of the aspiration and dispense methods. For example, to aspirate 100 µL with a surface following amount of 2 mm starting at the detected liquid level:" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "e3e205b7", "metadata": {}, "outputs": [], @@ -167,14 +167,14 @@ " wells,\n", " vols,\n", " probe_liquid_height=True,\n", - " surface_following_distance=[2] * len(wells), # mm down from liquid_height\n", + " surface_following_distance=[2] * len(wells), # mm down after finding liquid\n", " )\n", "\n", " await lh.dispense(\n", " wells,\n", " vols,\n", " probe_liquid_height=True,\n", - " surface_following_distance=[2] * len(wells), # mm up from liquid_height\n", + " surface_following_distance=[2] * len(wells), # mm up after finding liquid\n", " )" ] } diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 8b8b377d3a3..9140b4fd007 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -1719,9 +1719,8 @@ async def probe_liquid_heights( # detect liquid heights current_absolute_liquid_heights = await asyncio.gather( *[ - self.clld_probe_z_height_using_channel( + self.move_z_drive_to_liquid_surface_using_clld( channel_idx=channel, - move_channels_to_save_pos_after=False, lowest_immers_pos=container.get_absolute_location("c", "c", "cavity_bottom").z + tip.total_tip_length - tip.fitting_depth, @@ -1734,6 +1733,11 @@ async def probe_liquid_heights( ] ) + liquid_levels: List[int] = (await self.request_pip_height_last_lld())["lh"] # type: ignore + current_absolute_liquid_heights = [ + float(liquid_levels[channel_idx] / 10) for channel_idx in use_channels + ] + relative_to_well = [ current_absolute_liquid_heights[i] - resource.get_absolute_location("c", "c", "cavity_bottom").z @@ -8198,7 +8202,7 @@ async def clld_probe_y_position_using_channel( return material_y_pos - async def clld_probe_z_height_using_channel( + async def move_z_drive_to_liquid_surface_using_clld( self, channel_idx: int, # 0-based indexing of channels! lowest_immers_pos: float = 99.98, # mm @@ -8209,28 +8213,7 @@ async def clld_probe_z_height_using_channel( detection_drop: int = 2, post_detection_trajectory: Literal[0, 1] = 1, post_detection_dist: float = 2.0, # mm - move_channels_to_save_pos_after: bool = False, - ) -> float: - """Probes the Z-height below the specified channel on a Hamilton STAR liquid handling machine - using the channels 'capacitive Liquid Level Detection' (cLLD) capabilities. - N.B.: this means only conductive materials can be probed! - - Args: - channel_idx: The index of the channel to use for probing. Backmost channel = 0. - lowest_immers_pos: The lowest immersion position in mm. This is the position of the channel, NOT including the tip length (as C0 commands do). So you have to add the total_tip_length - fitting_depth. - start_pos_lld_search: The start position for z-touch search in mm. This is the position of the channel, NOT including the tip length (as C0 commands do). So you have to add the total_tip_length - fitting_depth. - channel_speed: The speed of channel movement in mm/sec. - channel_acceleration: The acceleration of the channel in mm/sec**2. - detection_edge: The edge steepness at capacitive LLD detection. - detection_drop: The offset after capacitive LLD edge detection. - post_detection_trajectory (0, 1): Movement of the channel up (1) or down (0) after contacting the surface. - post_detection_dist: Distance to move into the trajectory after detection in mm. - move_channels_to_save_pos_after: Flag to move channels to a safe position after operation. - - Returns: - The detected Z-height in mm. - """ - + ): lowest_immers_pos_increments = STARBackend.mm_to_z_drive_increment(lowest_immers_pos) start_pos_search_increments = STARBackend.mm_to_z_drive_increment(start_pos_search) channel_speed_increments = STARBackend.mm_to_z_drive_increment(channel_speed) @@ -8266,18 +8249,63 @@ async def clld_probe_z_height_using_channel( + f" and {STARBackend.z_drive_increment_to_mm(9_999)} mm, is {post_detection_dist} mm" ) + await self.send_command( + module=STARBackend.channel_id(channel_idx), + command="ZL", + zh=f"{lowest_immers_pos_increments:05}", # Lowest immersion position [increment] + zc=f"{start_pos_search_increments:05}", # Start position of LLD search [increment] + zl=f"{channel_speed_increments:05}", # Speed of channel movement + zr=f"{channel_acceleration_thousand_increments:03}", # Acceleration [1000 increment/second^2] + gt=f"{detection_edge:04}", # Edge steepness at capacitive LLD detection + gl=f"{detection_drop:04}", # Offset after capacitive LLD edge detection + zj=post_detection_trajectory, # Movement of the channel after contacting surface + zi=f"{post_detection_dist_increments:04}", # Distance to move up after detection [increment] + ) + + async def clld_probe_z_height_using_channel( + self, + channel_idx: int, # 0-based indexing of channels! + lowest_immers_pos: float = 99.98, # mm + start_pos_search: float = 330.0, # mm + channel_speed: float = 10.0, # mm + channel_acceleration: float = 800.0, # mm/sec**2 + detection_edge: int = 10, + detection_drop: int = 2, + post_detection_trajectory: Literal[0, 1] = 1, + post_detection_dist: float = 2.0, # mm + move_channels_to_save_pos_after: bool = False, + ) -> float: + """Probes the Z-height below the specified channel on a Hamilton STAR liquid handling machine + using the channels 'capacitive Liquid Level Detection' (cLLD) capabilities. + N.B.: this means only conductive materials can be probed! + + Args: + channel_idx: The index of the channel to use for probing. Backmost channel = 0. + lowest_immers_pos: The lowest immersion position in mm. This is the position of the channel, NOT including the tip length (as C0 commands do). So you have to add the total_tip_length - fitting_depth. + start_pos_lld_search: The start position for z-touch search in mm. This is the position of the channel, NOT including the tip length (as C0 commands do). So you have to add the total_tip_length - fitting_depth. + channel_speed: The speed of channel movement in mm/sec. + channel_acceleration: The acceleration of the channel in mm/sec**2. + detection_edge: The edge steepness at capacitive LLD detection. + detection_drop: The offset after capacitive LLD edge detection. + post_detection_trajectory (0, 1): Movement of the channel up (1) or down (0) after contacting the surface. + post_detection_dist: Distance to move into the trajectory after detection in mm. + move_channels_to_save_pos_after: Flag to move channels to a safe position after operation. + + Returns: + The detected Z-height in mm. + """ + try: - await self.send_command( - module=STARBackend.channel_id(channel_idx), - command="ZL", - zh=f"{lowest_immers_pos_increments:05}", # Lowest immersion position [increment] - zc=f"{start_pos_search_increments:05}", # Start position of LLD search [increment] - zl=f"{channel_speed_increments:05}", # Speed of channel movement - zr=f"{channel_acceleration_thousand_increments:03}", # Acceleration [1000 increment/second^2] - gt=f"{detection_edge:04}", # Edge steepness at capacitive LLD detection - gl=f"{detection_drop:04}", # Offset after capacitive LLD edge detection - zj=post_detection_trajectory, # Movement of the channel after contacting surface - zi=f"{post_detection_dist_increments:04}", # Distance to move up after detection [increment] + await self.move_z_drive_to_liquid_surface_using_clld( + channel_idx=channel_idx, + lowest_immers_pos=lowest_immers_pos, + start_pos_search=start_pos_search, + channel_speed=channel_speed, + channel_acceleration=channel_acceleration, + detection_edge=detection_edge, + detection_drop=detection_drop, + post_detection_trajectory=post_detection_trajectory, + post_detection_dist=post_detection_dist, ) except STARFirmwareError: await self.move_all_channels_in_z_safety()