From d96ca1a2b65748323ff16b918e2d90719f319308 Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Mon, 29 Dec 2025 17:31:57 +0000 Subject: [PATCH 01/13] integrate dispensing drive conversion information --- .../backends/hamilton/STAR_backend.py | 45 +++++++++++++++---- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 521fa5adaa..7ec5c82cde 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -8655,6 +8655,9 @@ async def request_cover_open(self) -> bool: y_drive_mm_per_increment = 0.046302082 z_drive_mm_per_increment = 0.01072765 + dispensing_drive_vol_per_increment = 0.046876 # uL / increment + dispensing_drive_mm_per_increment = 0.002734375 + @staticmethod def mm_to_y_drive_increment(value_mm: float) -> int: return round(value_mm / STARBackend.y_drive_mm_per_increment) @@ -8671,6 +8674,36 @@ def mm_to_z_drive_increment(value_mm: float) -> int: def z_drive_increment_to_mm(value_increments: int) -> float: return round(value_increments * STARBackend.z_drive_mm_per_increment, 2) + # Dispensing drive conversions + # --- uL <-> increments --- + @staticmethod + def dispensing_drive_vol_to_increment(value_uL: float) -> int: + return round(value_uL / STARBackend.dispensing_drive_vol_per_increment) + + @staticmethod + def dispensing_drive_increment_to_volume(value_increment: int) -> float: + return round(value_increment * STARBackend.dispensing_drive_vol_per_increment, 1) + + # --- mm <-> increments --- + @staticmethod + def dispensing_drive_mm_to_increment(value_mm: float) -> int: + return round(value_mm / STARBackend.dispensing_drive_mm_per_increment) + + @staticmethod + def dispensing_drive_increment_to_mm(value_increment: int) -> float: + return round(value_increment * STARBackend.dispensing_drive_mm_per_increment, 3) + + # --- uL <-> mm --- + @staticmethod + def dispensing_drive_vol_to_mm(value_uL: float) -> float: + inc = STARBackend.dispensing_drive_vol_to_increment(value_uL) + return STARBackend.dispensing_drive_increment_to_mm(inc) + + @staticmethod + def dispensing_drive_mm_to_vol(value_mm: float) -> float: + inc = STARBackend.dispensing_drive_mm_to_increment(value_mm) + return STARBackend.dispensing_drive_increment_to_volume(inc) + async def clld_probe_x_position_using_channel( self, channel_idx: int, # 0-based indexing of channels! @@ -8915,15 +8948,9 @@ async def clld_probe_y_position_using_channel( assert ( 0 <= detection_edge <= 1_0234 ), "Edge steepness at capacitive LLD detection must be between 0 and 1023" - assert current_limit_int in [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - ], f"Current limit must be in [1, 2, 3, 4, 5, 6, 7], is {channel_speed} mm/sec" + assert ( + 0 <= current_limit_int <= 7 + ), f"Current limit must be in [0, 1, 2, 3, 4, 5, 6, 7], is {channel_speed} mm/sec" # Move channel for cLLD (Note: does not return detected y-position!) await self.send_command( From d2dec558254a233c7f56f54f47747c7a7b549560 Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Mon, 29 Dec 2025 17:33:25 +0000 Subject: [PATCH 02/13] create `PressureLLDMode` enum parallel to `LLDMode` which sub-specifies `LLDMode.PRESSURE` --- .../liquid_handling/backends/hamilton/STAR_backend.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 7ec5c82cde..8f41e98997 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -1667,6 +1667,12 @@ class LLDMode(enum.Enum): DUAL = 3 Z_TOUCH_OFF = 4 + class PressureLLDMode(enum.Enum): + """Pressure liquid level detection mode.""" + + LIQUID = 0 + FOAM = 1 + async def probe_liquid_heights( self, containers: List[Container], From 2cce08783bdbf7ed5cdadeae5306290c82deca11 Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Mon, 29 Dec 2025 17:37:38 +0000 Subject: [PATCH 03/13] create v1 `move_tip_to_liquid_surface_using_plld_and_optional_clld` --- .../backends/hamilton/STAR_backend.py | 291 ++++++++++++++++++ 1 file changed, 291 insertions(+) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 8f41e98997..ef0e32a0f7 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -9123,6 +9123,297 @@ async def clld_probe_z_height_using_channel( return result_probed_z_height + async def move_tip_to_liquid_surface_using_plld_and_optional_clld( + self, + channel_idx: int, # 0-based indexing of channels! + lowest_immers_pos: float = 99.98, # mm + start_pos_search: Optional[float] = None, # mm + channel_speed_above_start_pos_search: float = 120.0, # mm/sec + channel_speed: float = 10.0, # mm + channel_acceleration: float = 800.0, # mm/sec**2 + z_drive_current_limit: int = 3, # unknown unit + tip_has_filter: bool = False, + dispense_drive_speed: float = 5.0, # mm/sec + dispense_drive_acceleration: float = 0.2, # mm/sec**2 + dispense_drive_max_speed: float = 14.5, # mm/sec + dispense_drive_current_limit: int = 3, # unknown unit + clld_detection_edge: int = 10, + clld_detection_drop: int = 2, + plld_detection_edge: int = 30, + plld_detection_drop: int = 10, + lld_mode: Optional[LLDMode] = None, + plld_mode: Optional[PressureLLDMode] = None, + max_delta_plld_clld: float = 5.0, # mm + plld_foam_detection_drop: int = 30, + plld_foam_detection_edge_tolerance: int = 30, + plld_foam_ad_values: int = 30, # unknown unit + plld_foam_search_speed: float = 10.0, # mm/sec + dispense_back_plld_volume: Optional[float] = None, # uL + post_detection_trajectory: Literal[0, 1] = 1, + post_detection_dist: float = 2.0, # mm + ): + """Move a channel tip to the liquid surface using pressure LLD (pLLD) and optionally capacitive LLD (cLLD). + + This command performs a downward liquid-level detection (LLD) search on the specified 0-indexed channel. + Depending on `lld_mode`, the instrument runs pressure LLD only (PRESSURE) or a combined mode (DUAL) that + uses both pressure and capacitive signals. The search starts at `start_pos_search` (or a computed safe + start height if None) and will not go below `lowest_immers_pos`. + + Positions are specified in millimetres relative to the deck coordinate system used by this API. Internally, + the method queries the mounted tip length, applies a fixed fitting depth (8 mm), and converts the resulting + positions and speeds into the instrument “increment” units expected by the channel Z-drive and dispensing drive. + After detection, the channel performs the configured post-detection motion given by + `post_detection_trajectory` and `post_detection_dist`. + + All numeric parameters are validated against instrument-supported ranges (assertions). A tip must be mounted + on the target channel. + + Args: + channel_idx: Channel index (0-based). + lowest_immers_pos: Lowest allowed search position in mm (hard stop). Defaults to 99.98. + start_pos_search: Search start position in mm. If None, computed from the tip length and safe head top position. + channel_speed_above_start_pos_search: Z-drive speed above the start position (mm/s). + channel_speed: Z-drive search speed (mm/s). + channel_acceleration: Z-drive acceleration (mm/s^2). + z_drive_current_limit: Z-drive current limit (instrument units; 0-7). + + tip_has_filter: Whether the mounted tip has a filter (bool). + + dispense_drive_speed: Dispensing drive speed (mm/s). + dispense_drive_acceleration: Dispensing drive acceleration (mm/s^2). + dispense_drive_max_speed: Dispensing drive max speed (mm/s). + dispense_drive_current_limit: Dispensing drive current limit (instrument units; 0-7). + + clld_detection_edge: cLLD edge steepness threshold (0-1023). + clld_detection_drop: cLLD drop/offset after detection (0-1023). + plld_detection_edge: pLLD edge steepness threshold (0-1023). + plld_detection_drop: pLLD drop/offset after detection (0-1023). + + lld_mode: LLD mode (PRESSURE or DUAL). If None, defaults to PRESSURE. + plld_mode: Pressure LLD mode (LIQUID or FOAM). If None, defaults to LIQUID. + max_delta_plld_clld: Max allowed delta between pLLD and cLLD detection positions (mm). + + plld_foam_detection_drop: Foam detection drop (0-1023). + plld_foam_detection_edge_tolerance: Foam detection edge tolerance (0-1023). + plld_foam_ad_values: Foam AD values (instrument units; 0-4999). + plld_foam_search_speed: Foam search speed (mm/s). + + dispense_back_plld_volume: Optional dispense-back volume after pLLD detection (uL). If None, disabled. + post_detection_trajectory: Post-detection movement mode (0 or 1). + post_detection_dist: Post-detection movement distance (mm). + + Raises: + ValueError: If `channel_idx` is not an int or is out of range. + RuntimeError: If no tip is mounted on `channel_idx`. + AssertionError: If any parameter is outside the instrument-supported range. + + Returns: + None + """ + + # Preconditions checks + # Ensure valid channel index + if not isinstance(channel_idx, int) or not (0 <= channel_idx <= self.num_channels - 1): + raise ValueError(f"channel_idx must be in [0, {self.num_channels - 1}], is {channel_idx}") + + # Ensure tip is mounted + tip_presence = await self.request_tip_presence() + if not tip_presence[channel_idx]: + raise RuntimeError(f"No tip mounted on channel {channel_idx}") + + # Correct for tip length + fitting depth + tip_len = await self.request_tip_len_on_channel(channel_idx) + + fitting_depth = 8 # mm, for 10, 50, 300, 1000 ul Hamilton tips + safe_head_top_z_pos = 334.7 + + if start_pos_search is None: + start_pos_search = safe_head_top_z_pos - tip_len + fitting_depth + + channel_head_start_pos = start_pos_search + tip_len - fitting_depth + safe_head_bottom_z_pos = 99.98 + tip_len - fitting_depth + 0.5 # add 0.5 mm safety margin + + if lld_mode is None: + lld_mode = self.LLDMode.PRESSURE + + if plld_mode is None: + plld_mode = self.PressureLLDMode.LIQUID + + if dispense_back_plld_volume is None: + dispense_back_plld_volume_mode = 0 + dispense_back_plld_volume_increments = 0 + else: + dispense_back_plld_volume_mode = 1 + dispense_back_plld_volume_increments = STARBackend.dispensing_drive_vol_to_increment( + dispense_back_plld_volume + ) + + # Conversions to machine units + lowest_immers_pos_increments = STARBackend.mm_to_z_drive_increment(lowest_immers_pos) + start_pos_search_increments = STARBackend.mm_to_z_drive_increment(channel_head_start_pos) + + channel_speed_above_start_pos_search_increments = STARBackend.mm_to_z_drive_increment( + channel_speed_above_start_pos_search + ) + channel_speed_increments = STARBackend.mm_to_z_drive_increment(channel_speed) + channel_acceleration_thousand_increments = STARBackend.mm_to_z_drive_increment( + channel_acceleration / 1000 + ) + + dispense_drive_speed_increments = STARBackend.dispensing_drive_mm_to_increment( + dispense_drive_speed + ) + dispense_drive_acceleration_increments = STARBackend.dispensing_drive_mm_to_increment( + dispense_drive_acceleration + ) + dispense_drive_max_speed_increments = STARBackend.dispensing_drive_mm_to_increment( + dispense_drive_max_speed + ) + + post_detection_dist_increments = STARBackend.mm_to_z_drive_increment(post_detection_dist) + max_delta_plld_clld_increments = STARBackend.mm_to_z_drive_increment(max_delta_plld_clld) + + plld_foam_search_speed_increments = STARBackend.mm_to_z_drive_increment(plld_foam_search_speed) + + # Machine-compatibility parameter checks + assert tip_has_filter in [True, False], "tip_has_filter must be a boolean" + + assert lld_mode in [self.LLDMode.PRESSURE, self.LLDMode.DUAL], ( + f"lld_mode must be either LLDMode.PRESSURE ({self.LLDMode.PRESSURE}) or " + + f"LLDMode.DUAL ({self.LLDMode.DUAL}), is {lld_mode}" + ) + assert plld_mode in [self.PressureLLDMode.LIQUID, self.PressureLLDMode.FOAM], ( + f"plld_mode must be either PressureLLDMode.LIQUID ({self.PressureLLDMode.LIQUID}) or " + + f"PressureLLDMode.FOAM ({self.PressureLLDMode.FOAM}), is {plld_mode}" + ) + + assert 9_320 <= lowest_immers_pos_increments <= 31_200, ( + f"Lowest immersion position must be between \n{STARBackend.z_drive_increment_to_mm(9_320)}" + + f" and {STARBackend.z_drive_increment_to_mm(31_200)} mm, is {lowest_immers_pos} mm" + ) + assert safe_head_bottom_z_pos <= channel_head_start_pos <= safe_head_top_z_pos, ( + f"Start position of LLD search must be between \n{safe_head_bottom_z_pos}" + + f" and {safe_head_top_z_pos} mm, is {channel_head_start_pos} mm" + ) + assert 20 <= channel_speed_above_start_pos_search_increments <= 15_000, ( + f"Speed above start position of LLD search must be between \n" + + f"{STARBackend.z_drive_increment_to_mm(20)} and " + + f"{STARBackend.z_drive_increment_to_mm(15_000)} mm/sec, is " + + f"{channel_speed_above_start_pos_search} mm/sec" + ) + assert 20 <= channel_speed_increments <= 15_000, ( + f"LLD search speed must be between \n{STARBackend.z_drive_increment_to_mm(20)}" + + f"and {STARBackend.z_drive_increment_to_mm(15_000)} mm/sec, is {channel_speed} mm/sec" + ) + assert 5 <= channel_acceleration_thousand_increments <= 150, ( + f"Channel acceleration must be between \n{STARBackend.z_drive_increment_to_mm(5*1_000)} " + + f" and {STARBackend.z_drive_increment_to_mm(150*1_000)} mm/sec**2, is {channel_acceleration} mm/sec**2" + ) + assert ( + 0 <= z_drive_current_limit <= 7 + ), f"Z-drive current limit must be between 0 and 7, is {z_drive_current_limit}" + + assert 20 <= dispense_drive_speed_increments <= 13_500, ( + f"Dispensing drive speed must be between \n" + + f"{STARBackend.dispensing_drive_increment_to_mm(20)} and " + + f"{STARBackend.dispensing_drive_increment_to_mm(13_500)} mm/sec, is {dispense_drive_speed} mm/sec" + ) + assert 1 <= dispense_drive_acceleration_increments <= 100, ( + f"Dispensing drive acceleration must be between \n" + + f"{STARBackend.dispensing_drive_increment_to_mm(1)} and " + + f"{STARBackend.dispensing_drive_increment_to_mm(100)} mm/sec**2, is {dispense_drive_acceleration} mm/sec**2" + ) + assert 20 <= dispense_drive_max_speed_increments <= 13_500, ( + f"Dispensing drive max speed must be between \n" + + f"{STARBackend.dispensing_drive_increment_to_mm(20)} and " + + f"{STARBackend.dispensing_drive_increment_to_mm(13_500)} mm/sec, is {dispense_drive_max_speed} mm/sec" + ) + assert ( + 0 <= dispense_drive_current_limit <= 7 + ), f"Dispensing drive current limit must be between 0 and 7, is {dispense_drive_current_limit}" + + assert ( + 0 <= clld_detection_edge <= 1_023 + ), "Edge steepness at capacitive LLD detection must be between 0 and 1023" + assert ( + 0 <= clld_detection_drop <= 1_023 + ), "Offset after capacitive LLD edge detection must be between 0 and 1023" + assert ( + 0 <= plld_detection_edge <= 1_023 + ), "Edge steepness at pressure LLD detection must be between 0 and 1023" + assert ( + 0 <= plld_detection_drop <= 1_023 + ), "Offset after pressure LLD edge detection must be between 0 and 1023" + + assert 0 <= max_delta_plld_clld_increments <= 9_999, ( + "Maximum allowed difference between pressure LLD and capacitive LLD detection z-positions " + + f"must be between 0 and {STARBackend.z_drive_increment_to_mm(9_999)} mm," + + f" is {max_delta_plld_clld} mm" + ) + + assert ( + 0 <= plld_foam_detection_drop <= 1_023 + ), f"Pressure LLD foam detection drop must be between 0 and 1023, is {plld_foam_detection_drop}" + assert 0 <= plld_foam_detection_edge_tolerance <= 1_023, ( + f"Pressure LLD foam detection edge tolerance must be between 0 and 1023, " + + f"is {plld_foam_detection_edge_tolerance}" + ) + assert ( + 0 <= plld_foam_ad_values <= 4_999 + ), f"Pressure LLD foam AD values must be between 0 and 4999, is {plld_foam_ad_values}" + assert 20 <= plld_foam_search_speed_increments <= 13_500, ( + f"Pressure LLD foam search speed must be between \n" + + f"{STARBackend.z_drive_increment_to_mm(20)} and " + + f"{STARBackend.z_drive_increment_to_mm(13_500)} mm/sec, is {plld_foam_search_speed} mm/sec" + ) + + assert dispense_back_plld_volume_mode in [0, 1], ( + "dispense_back_plld_volume_mode must be either 0 ('normal') or 1 " + + "('dispense back dispense_back_plld_volume'), " + + f"is {dispense_back_plld_volume_mode}" + ) + + assert 0 <= dispense_back_plld_volume_increments <= 26_666, ( + "Dispense back pressure LLD volume must be between \n0" + + f" and {STARBackend.z_drive_increment_to_mm(26_666)} uL, is {dispense_back_plld_volume} mm" + ) + + assert 0 <= post_detection_dist_increments <= 9_999, ( + "Post cLLD-detection movement distance must be between \n0" + + 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="ZE", + zh=f"{lowest_immers_pos_increments:05}", # Lowest immersion position [increment] + zc=f"{start_pos_search_increments:05}", # Start position of LLD search [increment] + zi=f"{post_detection_dist_increments:04}", # Distance to move up after detection [increment] + zj=post_detection_trajectory, # Movement of the channel after contacting surface + gf=str(int(tip_has_filter)), # Tip has filter + gt=f"{clld_detection_edge:04}", # Edge steepness at capacitive LLD detection + gl=f"{clld_detection_drop:04}", # Offset after capacitive LLD edge detection + gu=f"{plld_detection_edge:04}", # Edge steepness at pressure LLD detection + gn=f"{plld_detection_drop:04}", # Offset after pressure LLD edge detection + gm="0" if lld_mode == self.LLDMode.PRESSURE else "1", # LLD mode + gz=f"{max_delta_plld_clld_increments:04}", # Max allowed delta between pLLD and cLLD + cj=str(plld_mode.value), # Pressure LLD mode + co=f"{plld_foam_detection_drop:04}", # Pressure LLD foam detection drop + cp=f"{plld_foam_detection_edge_tolerance:04}", # Pressure LLD foam detection edge tolerance + cq=f"{plld_foam_ad_values:04}", # Pressure LLD foam AD values + cl=f"{plld_foam_search_speed_increments:05}", # Pressure LLD foam search speed + cc=str(dispense_back_plld_volume_mode), # Dispense back pLLD volume mode + cd=f"{dispense_back_plld_volume_increments:05}", # Dispense back pressure LLD volume + zv=f"{channel_speed_above_start_pos_search_increments:05}", # Speed above start pos search + zl=f"{channel_speed_increments:05}", # Speed of channel movement + zr=f"{channel_acceleration_thousand_increments:03}", # Acceleration [1000 increment/second^2] + zw=f"{z_drive_current_limit}", # Z-drive current limit + dl=f"{dispense_drive_speed_increments:04}", # Dispensing drive speed + dr=f"{dispense_drive_acceleration_increments:04}", # Dispensing drive acceleration + dv=f"{dispense_drive_max_speed_increments:04}", # Dispensing + dw=f"{dispense_drive_current_limit}", # Dispensing drive current limit + ) + async def request_probe_z_position(self, channel_idx: int) -> float: """Request the z-position of the channel probe (EXCLUDING the tip)""" resp = await self.send_command( From e68a5e43641fa92a170fb4ee53b238cfa6e1fb12 Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Mon, 29 Dec 2025 18:25:25 +0000 Subject: [PATCH 04/13] on machine verification --- .../liquid_handling/backends/hamilton/STAR_backend.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index ef0e32a0f7..395dfc5163 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -9389,7 +9389,7 @@ async def move_tip_to_liquid_surface_using_plld_and_optional_clld( zh=f"{lowest_immers_pos_increments:05}", # Lowest immersion position [increment] zc=f"{start_pos_search_increments:05}", # Start position of LLD search [increment] zi=f"{post_detection_dist_increments:04}", # Distance to move up after detection [increment] - zj=post_detection_trajectory, # Movement of the channel after contacting surface + zj=f"{post_detection_trajectory:01}", # Movement of the channel after contacting surface gf=str(int(tip_has_filter)), # Tip has filter gt=f"{clld_detection_edge:04}", # Edge steepness at capacitive LLD detection gl=f"{clld_detection_drop:04}", # Offset after capacitive LLD edge detection @@ -9408,9 +9408,9 @@ async def move_tip_to_liquid_surface_using_plld_and_optional_clld( zl=f"{channel_speed_increments:05}", # Speed of channel movement zr=f"{channel_acceleration_thousand_increments:03}", # Acceleration [1000 increment/second^2] zw=f"{z_drive_current_limit}", # Z-drive current limit - dl=f"{dispense_drive_speed_increments:04}", # Dispensing drive speed - dr=f"{dispense_drive_acceleration_increments:04}", # Dispensing drive acceleration - dv=f"{dispense_drive_max_speed_increments:04}", # Dispensing + dl=f"{dispense_drive_speed_increments:05}", # Dispensing drive speed + dr=f"{dispense_drive_acceleration_increments:03}", # Dispensing drive acceleration + dv=f"{dispense_drive_max_speed_increments:05}", # Dispensing dw=f"{dispense_drive_current_limit}", # Dispensing drive current limit ) From ddf807ab4b2cde88faa9435d5643f13758422c6c Mon Sep 17 00:00:00 2001 From: Camillo Moschner <122165124+BioCam@users.noreply.github.com> Date: Mon, 29 Dec 2025 22:29:04 +0000 Subject: [PATCH 05/13] Update pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 395dfc5163..06e0dee78f 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -9375,7 +9375,7 @@ async def move_tip_to_liquid_surface_using_plld_and_optional_clld( assert 0 <= dispense_back_plld_volume_increments <= 26_666, ( "Dispense back pressure LLD volume must be between \n0" - + f" and {STARBackend.z_drive_increment_to_mm(26_666)} uL, is {dispense_back_plld_volume} mm" + + f" and {STARBackend.dispensing_drive_increment_to_volume(26_666)} uL, is {dispense_back_plld_volume} uL" ) assert 0 <= post_detection_dist_increments <= 9_999, ( From 1d37538505c76e3fabae1889be71cc41a3691d38 Mon Sep 17 00:00:00 2001 From: camos95 Date: Mon, 29 Dec 2025 22:42:20 +0000 Subject: [PATCH 06/13] `ruff --fix` --- .../backends/hamilton/STAR_backend.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 395dfc5163..c6396f8df3 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -9296,7 +9296,7 @@ async def move_tip_to_liquid_surface_using_plld_and_optional_clld( + f" and {safe_head_top_z_pos} mm, is {channel_head_start_pos} mm" ) assert 20 <= channel_speed_above_start_pos_search_increments <= 15_000, ( - f"Speed above start position of LLD search must be between \n" + "Speed above start position of LLD search must be between \n" + f"{STARBackend.z_drive_increment_to_mm(20)} and " + f"{STARBackend.z_drive_increment_to_mm(15_000)} mm/sec, is " + f"{channel_speed_above_start_pos_search} mm/sec" @@ -9314,17 +9314,17 @@ async def move_tip_to_liquid_surface_using_plld_and_optional_clld( ), f"Z-drive current limit must be between 0 and 7, is {z_drive_current_limit}" assert 20 <= dispense_drive_speed_increments <= 13_500, ( - f"Dispensing drive speed must be between \n" + "Dispensing drive speed must be between \n" + f"{STARBackend.dispensing_drive_increment_to_mm(20)} and " + f"{STARBackend.dispensing_drive_increment_to_mm(13_500)} mm/sec, is {dispense_drive_speed} mm/sec" ) assert 1 <= dispense_drive_acceleration_increments <= 100, ( - f"Dispensing drive acceleration must be between \n" + "Dispensing drive acceleration must be between \n" + f"{STARBackend.dispensing_drive_increment_to_mm(1)} and " + f"{STARBackend.dispensing_drive_increment_to_mm(100)} mm/sec**2, is {dispense_drive_acceleration} mm/sec**2" ) assert 20 <= dispense_drive_max_speed_increments <= 13_500, ( - f"Dispensing drive max speed must be between \n" + "Dispensing drive max speed must be between \n" + f"{STARBackend.dispensing_drive_increment_to_mm(20)} and " + f"{STARBackend.dispensing_drive_increment_to_mm(13_500)} mm/sec, is {dispense_drive_max_speed} mm/sec" ) @@ -9355,14 +9355,14 @@ async def move_tip_to_liquid_surface_using_plld_and_optional_clld( 0 <= plld_foam_detection_drop <= 1_023 ), f"Pressure LLD foam detection drop must be between 0 and 1023, is {plld_foam_detection_drop}" assert 0 <= plld_foam_detection_edge_tolerance <= 1_023, ( - f"Pressure LLD foam detection edge tolerance must be between 0 and 1023, " + "Pressure LLD foam detection edge tolerance must be between 0 and 1023, " + f"is {plld_foam_detection_edge_tolerance}" ) assert ( 0 <= plld_foam_ad_values <= 4_999 ), f"Pressure LLD foam AD values must be between 0 and 4999, is {plld_foam_ad_values}" assert 20 <= plld_foam_search_speed_increments <= 13_500, ( - f"Pressure LLD foam search speed must be between \n" + "Pressure LLD foam search speed must be between \n" + f"{STARBackend.z_drive_increment_to_mm(20)} and " + f"{STARBackend.z_drive_increment_to_mm(13_500)} mm/sec, is {plld_foam_search_speed} mm/sec" ) From 54ee7c10222749a84584dd3a0150734abe06216c Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Tue, 30 Dec 2025 11:06:51 +0000 Subject: [PATCH 07/13] rename to `plld_and_optional_clld_probe_z_height_using_channel` + return response_raw --- .../backends/hamilton/STAR_backend.py | 62 ++++++++++--------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 8e9e4ef191..fa6c0e9cce 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -9123,7 +9123,7 @@ async def clld_probe_z_height_using_channel( return result_probed_z_height - async def move_tip_to_liquid_surface_using_plld_and_optional_clld( + async def plld_and_optional_clld_probe_z_height_using_channel( self, channel_idx: int, # 0-based indexing of channels! lowest_immers_pos: float = 99.98, # mm @@ -9142,8 +9142,8 @@ async def move_tip_to_liquid_surface_using_plld_and_optional_clld( plld_detection_edge: int = 30, plld_detection_drop: int = 10, lld_mode: Optional[LLDMode] = None, - plld_mode: Optional[PressureLLDMode] = None, max_delta_plld_clld: float = 5.0, # mm + plld_mode: Optional[PressureLLDMode] = None, plld_foam_detection_drop: int = 30, plld_foam_detection_edge_tolerance: int = 30, plld_foam_ad_values: int = 30, # unknown unit @@ -9383,36 +9383,38 @@ async def move_tip_to_liquid_surface_using_plld_and_optional_clld( + f" and {STARBackend.z_drive_increment_to_mm(9_999)} mm, is {post_detection_dist} mm" ) - await self.send_command( + resp_raw = await self.send_command( module=STARBackend.channel_id(channel_idx), command="ZE", - zh=f"{lowest_immers_pos_increments:05}", # Lowest immersion position [increment] - zc=f"{start_pos_search_increments:05}", # Start position of LLD search [increment] - zi=f"{post_detection_dist_increments:04}", # Distance to move up after detection [increment] - zj=f"{post_detection_trajectory:01}", # Movement of the channel after contacting surface - gf=str(int(tip_has_filter)), # Tip has filter - gt=f"{clld_detection_edge:04}", # Edge steepness at capacitive LLD detection - gl=f"{clld_detection_drop:04}", # Offset after capacitive LLD edge detection - gu=f"{plld_detection_edge:04}", # Edge steepness at pressure LLD detection - gn=f"{plld_detection_drop:04}", # Offset after pressure LLD edge detection - gm="0" if lld_mode == self.LLDMode.PRESSURE else "1", # LLD mode - gz=f"{max_delta_plld_clld_increments:04}", # Max allowed delta between pLLD and cLLD - cj=str(plld_mode.value), # Pressure LLD mode - co=f"{plld_foam_detection_drop:04}", # Pressure LLD foam detection drop - cp=f"{plld_foam_detection_edge_tolerance:04}", # Pressure LLD foam detection edge tolerance - cq=f"{plld_foam_ad_values:04}", # Pressure LLD foam AD values - cl=f"{plld_foam_search_speed_increments:05}", # Pressure LLD foam search speed - cc=str(dispense_back_plld_volume_mode), # Dispense back pLLD volume mode - cd=f"{dispense_back_plld_volume_increments:05}", # Dispense back pressure LLD volume - zv=f"{channel_speed_above_start_pos_search_increments:05}", # Speed above start pos search - zl=f"{channel_speed_increments:05}", # Speed of channel movement - zr=f"{channel_acceleration_thousand_increments:03}", # Acceleration [1000 increment/second^2] - zw=f"{z_drive_current_limit}", # Z-drive current limit - dl=f"{dispense_drive_speed_increments:05}", # Dispensing drive speed - dr=f"{dispense_drive_acceleration_increments:03}", # Dispensing drive acceleration - dv=f"{dispense_drive_max_speed_increments:05}", # Dispensing - dw=f"{dispense_drive_current_limit}", # Dispensing drive current limit - ) + zh=f"{lowest_immers_pos_increments:05}", + zc=f"{start_pos_search_increments:05}", + zi=f"{post_detection_dist_increments:04}", + zj=f"{post_detection_trajectory:01}", + gf=str(int(tip_has_filter)), + gt=f"{clld_detection_edge:04}", + gl=f"{clld_detection_drop:04}", + gu=f"{plld_detection_edge:04}", + gn=f"{plld_detection_drop:04}", + gm="0" if lld_mode == self.LLDMode.PRESSURE else "1", + gz=f"{max_delta_plld_clld_increments:04}", + cj=str(plld_mode.value), + co=f"{plld_foam_detection_drop:04}", + cp=f"{plld_foam_detection_edge_tolerance:04}", + cq=f"{plld_foam_ad_values:04}", + cl=f"{plld_foam_search_speed_increments:05}", + cc=str(dispense_back_plld_volume_mode), + cd=f"{dispense_back_plld_volume_increments:05}", + zv=f"{channel_speed_above_start_pos_search_increments:05}", + zl=f"{channel_speed_increments:05}", + zr=f"{channel_acceleration_thousand_increments:03}", + zw=f"{z_drive_current_limit}", + dl=f"{dispense_drive_speed_increments:05}", + dr=f"{dispense_drive_acceleration_increments:03}", + dv=f"{dispense_drive_max_speed_increments:05}", + dw=f"{dispense_drive_current_limit}", + ) + + return resp_raw async def request_probe_z_position(self, channel_idx: int) -> float: """Request the z-position of the channel probe (EXCLUDING the tip)""" From 12a97563483fc05855d3c4b362412e2db39902bc Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Tue, 30 Dec 2025 13:09:38 +0000 Subject: [PATCH 08/13] return list of identified z positions --- .../backends/hamilton/STAR_backend.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index fa6c0e9cce..a32b4f2bb5 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -9230,8 +9230,8 @@ async def plld_and_optional_clld_probe_z_height_using_channel( if start_pos_search is None: start_pos_search = safe_head_top_z_pos - tip_len + fitting_depth - channel_head_start_pos = start_pos_search + tip_len - fitting_depth - safe_head_bottom_z_pos = 99.98 + tip_len - fitting_depth + 0.5 # add 0.5 mm safety margin + channel_head_start_pos = round(start_pos_search + tip_len - fitting_depth, 2) + safe_head_bottom_z_pos = round(99.98 + tip_len - fitting_depth + 0.5, 2) # add 0.5 mm safety margin if lld_mode is None: lld_mode = self.LLDMode.PRESSURE @@ -9293,7 +9293,8 @@ async def plld_and_optional_clld_probe_z_height_using_channel( ) assert safe_head_bottom_z_pos <= channel_head_start_pos <= safe_head_top_z_pos, ( f"Start position of LLD search must be between \n{safe_head_bottom_z_pos}" - + f" and {safe_head_top_z_pos} mm, is {channel_head_start_pos} mm" + + f" and {safe_head_top_z_pos} mm, is {channel_head_start_pos} mm because tip length " + + f"- fitting depth is {tip_len - fitting_depth} mm and start_pos_search is {start_pos_search} mm" ) assert 20 <= channel_speed_above_start_pos_search_increments <= 15_000, ( "Speed above start position of LLD search must be between \n" @@ -9414,7 +9415,13 @@ async def plld_and_optional_clld_probe_z_height_using_channel( dw=f"{dispense_drive_current_limit}", ) - return resp_raw + resp_mm = [ + STARBackend.z_drive_increment_to_mm(int(return_val)) + for return_val in + resp_raw.split("if")[-1].split() + ] + + return resp_mm async def request_probe_z_position(self, channel_idx: int) -> float: """Request the z-position of the channel probe (EXCLUDING the tip)""" From f0d7036cb61d7b50d28be56153a9b721c053e53d Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Tue, 30 Dec 2025 14:48:01 +0000 Subject: [PATCH 09/13] split `_plld_or_dual_clld_probe_z_height_using_channel` in two --- .../backends/hamilton/STAR_backend.py | 259 ++++++++++++------ 1 file changed, 175 insertions(+), 84 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index a32b4f2bb5..b1b876996d 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -9123,11 +9123,11 @@ async def clld_probe_z_height_using_channel( return result_probed_z_height - async def plld_and_optional_clld_probe_z_height_using_channel( + async def _plld_or_dual_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: Optional[float] = None, # mm + lowest_immers_pos: float = 99.98, # mm of the head_probe! + start_pos_search: float = 334.7, # mm of the head_probe! channel_speed_above_start_pos_search: float = 120.0, # mm/sec channel_speed: float = 10.0, # mm channel_acceleration: float = 800.0, # mm/sec**2 @@ -9152,63 +9152,45 @@ async def plld_and_optional_clld_probe_z_height_using_channel( post_detection_trajectory: Literal[0, 1] = 1, post_detection_dist: float = 2.0, # mm ): - """Move a channel tip to the liquid surface using pressure LLD (pLLD) and optionally capacitive LLD (cLLD). - - This command performs a downward liquid-level detection (LLD) search on the specified 0-indexed channel. - Depending on `lld_mode`, the instrument runs pressure LLD only (PRESSURE) or a combined mode (DUAL) that - uses both pressure and capacitive signals. The search starts at `start_pos_search` (or a computed safe - start height if None) and will not go below `lowest_immers_pos`. + """Detect liquid level using either (1) pressured-based or + (2) pressure AND capacitive liquid level detection (LLD). + + (a) with foam detection sub-mode or (b) without foam detection sub-mode. - Positions are specified in millimetres relative to the deck coordinate system used by this API. Internally, - the method queries the mounted tip length, applies a fixed fitting depth (8 mm), and converts the resulting - positions and speeds into the instrument “increment” units expected by the channel Z-drive and dispensing drive. - After detection, the channel performs the configured post-detection motion given by - `post_detection_trajectory` and `post_detection_dist`. - - All numeric parameters are validated against instrument-supported ranges (assertions). A tip must be mounted - on the target channel. + Notes: + - This command is implemented via the PX command module, i.e. it is parallelisable! + - lowest_immers_pos & start_pos_search refer to the head_probe z-coordinate (not the tip)! Args: - channel_idx: Channel index (0-based). - lowest_immers_pos: Lowest allowed search position in mm (hard stop). Defaults to 99.98. - start_pos_search: Search start position in mm. If None, computed from the tip length and safe head top position. - channel_speed_above_start_pos_search: Z-drive speed above the start position (mm/s). - channel_speed: Z-drive search speed (mm/s). - channel_acceleration: Z-drive acceleration (mm/s^2). - z_drive_current_limit: Z-drive current limit (instrument units; 0-7). - - tip_has_filter: Whether the mounted tip has a filter (bool). - - dispense_drive_speed: Dispensing drive speed (mm/s). - dispense_drive_acceleration: Dispensing drive acceleration (mm/s^2). - dispense_drive_max_speed: Dispensing drive max speed (mm/s). - dispense_drive_current_limit: Dispensing drive current limit (instrument units; 0-7). - - clld_detection_edge: cLLD edge steepness threshold (0-1023). - clld_detection_drop: cLLD drop/offset after detection (0-1023). - plld_detection_edge: pLLD edge steepness threshold (0-1023). - plld_detection_drop: pLLD drop/offset after detection (0-1023). - - lld_mode: LLD mode (PRESSURE or DUAL). If None, defaults to PRESSURE. - plld_mode: Pressure LLD mode (LIQUID or FOAM). If None, defaults to LIQUID. - max_delta_plld_clld: Max allowed delta between pLLD and cLLD detection positions (mm). - - plld_foam_detection_drop: Foam detection drop (0-1023). - plld_foam_detection_edge_tolerance: Foam detection edge tolerance (0-1023). - plld_foam_ad_values: Foam AD values (instrument units; 0-4999). - plld_foam_search_speed: Foam search speed (mm/s). - - dispense_back_plld_volume: Optional dispense-back volume after pLLD detection (uL). If None, disabled. - post_detection_trajectory: Post-detection movement mode (0 or 1). - post_detection_dist: Post-detection movement distance (mm). - - Raises: - ValueError: If `channel_idx` is not an int or is out of range. - RuntimeError: If no tip is mounted on `channel_idx`. - AssertionError: If any parameter is outside the instrument-supported range. + lowest_immers_pos: Lowest allowed Z during the search (mm). Default 99.98. + start_pos_search: Z position where the search begins (mm). Default 334.7. + channel_speed_above_start_pos_search: Z speed above the start position (mm/s). Default 120.0. + channel_speed: Z search speed (mm/s). Default 10.0. + channel_acceleration: Z acceleration (mm/s²). Default 800.0. + z_drive_current_limit: Z drive current limit (instrument units). Default 3. + tip_has_filter: Whether a filter tip is mounted. Default False. + dispense_drive_speed: Dispense drive speed (mm/s). Default 5.0. + dispense_drive_acceleration: Dispense drive acceleration (mm/s²). Default 0.2. + dispense_drive_max_speed: Dispense drive max speed (mm/s). Default 14.5. + dispense_drive_current_limit: Dispense drive current limit (instrument units). Default 3. + clld_detection_edge: Capacitive detection edge threshold. Default 10. + clld_detection_drop: Capacitive detection drop threshold. Default 2. + plld_detection_edge: Pressure detection edge threshold. Default 30. + plld_detection_drop: Pressure detection drop threshold. Default 10. + lld_mode: Pressure-only vs dual (pressure + capacitive) behaviour. Default None. + max_delta_plld_clld: Max allowed delta between pressure/capacitive detections (mm). Default 5.0. + plld_mode: Pressure-detection sub-mode (instrument-defined). Default None. + plld_foam_detection_drop: Foam detection drop threshold. Default 30. + plld_foam_detection_edge_tolerance: Foam detection edge tolerance. Default 30. + plld_foam_ad_values: Foam AD values (instrument units). Default 30. + plld_foam_search_speed: Foam search speed (mm/s). Default 10.0. + dispense_back_plld_volume: Optional dispense-back volume after detection (µL). Default None. + post_detection_trajectory: Post-detection movement pattern selector. Default 1. + post_detection_dist: Post-detection movement distance (mm). Default 2.0. Returns: - None + list[float]: Two Z-drive positions (mm), meaning depends on the selected pressure sub-mode: + - Single-detection modes/PressureLLDMode.LIQUID: [liquid_level_pos, 0.0] + - Two-detection modes/PressureLLDMode.FOAM: [first_detection_pos, liquid_level_pos] """ # Preconditions checks @@ -9216,23 +9198,6 @@ async def plld_and_optional_clld_probe_z_height_using_channel( if not isinstance(channel_idx, int) or not (0 <= channel_idx <= self.num_channels - 1): raise ValueError(f"channel_idx must be in [0, {self.num_channels - 1}], is {channel_idx}") - # Ensure tip is mounted - tip_presence = await self.request_tip_presence() - if not tip_presence[channel_idx]: - raise RuntimeError(f"No tip mounted on channel {channel_idx}") - - # Correct for tip length + fitting depth - tip_len = await self.request_tip_len_on_channel(channel_idx) - - fitting_depth = 8 # mm, for 10, 50, 300, 1000 ul Hamilton tips - safe_head_top_z_pos = 334.7 - - if start_pos_search is None: - start_pos_search = safe_head_top_z_pos - tip_len + fitting_depth - - channel_head_start_pos = round(start_pos_search + tip_len - fitting_depth, 2) - safe_head_bottom_z_pos = round(99.98 + tip_len - fitting_depth + 0.5, 2) # add 0.5 mm safety margin - if lld_mode is None: lld_mode = self.LLDMode.PRESSURE @@ -9250,7 +9215,7 @@ async def plld_and_optional_clld_probe_z_height_using_channel( # Conversions to machine units lowest_immers_pos_increments = STARBackend.mm_to_z_drive_increment(lowest_immers_pos) - start_pos_search_increments = STARBackend.mm_to_z_drive_increment(channel_head_start_pos) + start_pos_search_increments = STARBackend.mm_to_z_drive_increment(start_pos_search) channel_speed_above_start_pos_search_increments = STARBackend.mm_to_z_drive_increment( channel_speed_above_start_pos_search @@ -9276,6 +9241,15 @@ async def plld_and_optional_clld_probe_z_height_using_channel( plld_foam_search_speed_increments = STARBackend.mm_to_z_drive_increment(plld_foam_search_speed) # Machine-compatibility parameter checks + assert 9320 <= lowest_immers_pos_increments <= 31_200, ( + f"Lowest immersion position must be between \n{STARBackend.z_drive_increment_to_mm(9_320)}" + + f" and {STARBackend.z_drive_increment_to_mm(31_200)} mm, is {lowest_immers_pos} mm" + ) + assert 9320 <= start_pos_search_increments <= 31_200, ( + f"Start position of LLD search must be between \n{STARBackend.z_drive_increment_to_mm(9_320)}" + + f" and {STARBackend.z_drive_increment_to_mm(31_200)} mm, is {start_pos_search} mm" + ) + assert tip_has_filter in [True, False], "tip_has_filter must be a boolean" assert lld_mode in [self.LLDMode.PRESSURE, self.LLDMode.DUAL], ( @@ -9291,11 +9265,7 @@ async def plld_and_optional_clld_probe_z_height_using_channel( f"Lowest immersion position must be between \n{STARBackend.z_drive_increment_to_mm(9_320)}" + f" and {STARBackend.z_drive_increment_to_mm(31_200)} mm, is {lowest_immers_pos} mm" ) - assert safe_head_bottom_z_pos <= channel_head_start_pos <= safe_head_top_z_pos, ( - f"Start position of LLD search must be between \n{safe_head_bottom_z_pos}" - + f" and {safe_head_top_z_pos} mm, is {channel_head_start_pos} mm because tip length " - + f"- fitting depth is {tip_len - fitting_depth} mm and start_pos_search is {start_pos_search} mm" - ) + assert 20 <= channel_speed_above_start_pos_search_increments <= 15_000, ( "Speed above start position of LLD search must be between \n" + f"{STARBackend.z_drive_increment_to_mm(20)} and " @@ -9401,11 +9371,11 @@ async def plld_and_optional_clld_probe_z_height_using_channel( cj=str(plld_mode.value), co=f"{plld_foam_detection_drop:04}", cp=f"{plld_foam_detection_edge_tolerance:04}", - cq=f"{plld_foam_ad_values:04}", - cl=f"{plld_foam_search_speed_increments:05}", + cq=f"{plld_foam_ad_values:04}", + cl=f"{plld_foam_search_speed_increments:05}", cc=str(dispense_back_plld_volume_mode), cd=f"{dispense_back_plld_volume_increments:05}", - zv=f"{channel_speed_above_start_pos_search_increments:05}", + zv=f"{channel_speed_above_start_pos_search_increments:05}", zl=f"{channel_speed_increments:05}", zr=f"{channel_acceleration_thousand_increments:03}", zw=f"{z_drive_current_limit}", @@ -9416,13 +9386,134 @@ async def plld_and_optional_clld_probe_z_height_using_channel( ) resp_mm = [ - STARBackend.z_drive_increment_to_mm(int(return_val)) - for return_val in - resp_raw.split("if")[-1].split() + STARBackend.z_drive_increment_to_mm(int(return_val)) + for return_val in resp_raw.split("if")[-1].split() ] return resp_mm + async def plld_or_dual_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 = 334.7, # mm + channel_speed_above_start_pos_search: float = 120.0, # mm/sec + channel_speed: float = 10.0, # mm + channel_acceleration: float = 800.0, # mm/sec**2 + z_drive_current_limit: int = 3, # unknown unit + tip_has_filter: bool = False, + dispense_drive_speed: float = 5.0, # mm/sec + dispense_drive_acceleration: float = 0.2, # mm/sec**2 + dispense_drive_max_speed: float = 14.5, # mm/sec + dispense_drive_current_limit: int = 3, # unknown unit + clld_detection_edge: int = 10, + clld_detection_drop: int = 2, + plld_detection_edge: int = 30, + plld_detection_drop: int = 10, + lld_mode: Optional[LLDMode] = None, + max_delta_plld_clld: float = 5.0, # mm + plld_mode: Optional[PressureLLDMode] = None, + plld_foam_detection_drop: int = 30, + plld_foam_detection_edge_tolerance: int = 30, + plld_foam_ad_values: int = 30, # unknown unit + plld_foam_search_speed: float = 10.0, # mm/sec + dispense_back_plld_volume: Optional[float] = None, # uL + post_detection_trajectory: Literal[0, 1] = 1, + post_detection_dist: float = 2.0, # mm + ): + """Detect liquid level using either (1) pressured-based or + (2) pressure AND capacitive liquid level detection (LLD). + + (a) with foam detection sub-mode or (b) without foam detection sub-mode. + + Notes: + - This command is implemented via BOTH the PX and C0 command modules, i.e. it is NOT parallelisable! + - lowest_immers_pos & start_pos_search refer to the tip z-coordinate (not the head_probe)! + + Args: + lowest_immers_pos: Lowest allowed Z during the search (mm). Default 99.98. + start_pos_search: Z position where the search begins (mm). Default 334.7. + channel_speed_above_start_pos_search: Z speed above the start position (mm/s). Default 120.0. + channel_speed: Z search speed (mm/s). Default 10.0. + channel_acceleration: Z acceleration (mm/s²). Default 800.0. + z_drive_current_limit: Z drive current limit (instrument units). Default 3. + tip_has_filter: Whether a filter tip is mounted. Default False. + dispense_drive_speed: Dispense drive speed (mm/s). Default 5.0. + dispense_drive_acceleration: Dispense drive acceleration (mm/s²). Default 0.2. + dispense_drive_max_speed: Dispense drive max speed (mm/s). Default 14.5. + dispense_drive_current_limit: Dispense drive current limit (instrument units). Default 3. + clld_detection_edge: Capacitive detection edge threshold. Default 10. + clld_detection_drop: Capacitive detection drop threshold. Default 2. + plld_detection_edge: Pressure detection edge threshold. Default 30. + plld_detection_drop: Pressure detection drop threshold. Default 10. + lld_mode: Pressure-only vs dual (pressure + capacitive) behaviour. Default None. + max_delta_plld_clld: Max allowed delta between pressure/capacitive detections (mm). Default 5.0. + plld_mode: Pressure-detection sub-mode (instrument-defined). Default None. + plld_foam_detection_drop: Foam detection drop threshold. Default 30. + plld_foam_detection_edge_tolerance: Foam detection edge tolerance. Default 30. + plld_foam_ad_values: Foam AD values (instrument units). Default 30. + plld_foam_search_speed: Foam search speed (mm/s). Default 10.0. + dispense_back_plld_volume: Optional dispense-back volume after detection (µL). Default None. + post_detection_trajectory: Post-detection movement pattern selector. Default 1. + post_detection_dist: Post-detection movement distance (mm). Default 2.0. + + Returns: + list[float]: Two Z-drive positions (mm), meaning depends on the selected pressure sub-mode: + - Single-detection modes/PressureLLDMode.LIQUID: [liquid_level_pos, 0.0] + - Two-detection modes/PressureLLDMode.FOAM: [first_detection_pos, liquid_level_pos] + """ + # Ensure tip is mounted + tip_presence = await self.request_tip_presence() + if not tip_presence[channel_idx]: + raise RuntimeError(f"No tip mounted on channel {channel_idx}") + + # Correct for tip length + fitting depth + tip_len = await self.request_tip_len_on_channel(channel_idx) + + fitting_depth = 8 # mm, for 10, 50, 300, 1000 ul Hamilton tips + safe_head_top_z_pos = 334.7 + safe_tip_bottom_z_pos = 99.98 + safe_tip_top_z_pos = safe_head_top_z_pos - tip_len + fitting_depth + + channel_head_start_pos = round(start_pos_search + tip_len - fitting_depth, 2) + lowest_immers_pos_corrected = round(lowest_immers_pos + tip_len - fitting_depth, 2) + + assert safe_tip_bottom_z_pos <= start_pos_search <= safe_tip_top_z_pos, ( + f"Start position of LLD search must be between \n{safe_tip_bottom_z_pos}" + + f" and {safe_tip_top_z_pos} mm, is {start_pos_search} mm " + + f"({safe_head_top_z_pos=}, {tip_len=} mm)" + ) + + resp_mm = await self._plld_or_dual_clld_probe_z_height_using_channel( + channel_idx=channel_idx, + lowest_immers_pos=lowest_immers_pos_corrected, + start_pos_search=channel_head_start_pos, + channel_speed_above_start_pos_search=channel_speed_above_start_pos_search, + channel_speed=channel_speed, + channel_acceleration=channel_acceleration, + z_drive_current_limit=z_drive_current_limit, + tip_has_filter=tip_has_filter, + dispense_drive_speed=dispense_drive_speed, + dispense_drive_acceleration=dispense_drive_acceleration, + dispense_drive_max_speed=dispense_drive_max_speed, + dispense_drive_current_limit=dispense_drive_current_limit, + clld_detection_edge=clld_detection_edge, + clld_detection_drop=clld_detection_drop, + plld_detection_edge=plld_detection_edge, + plld_detection_drop=plld_detection_drop, + lld_mode=lld_mode, + max_delta_plld_clld=max_delta_plld_clld, + plld_mode=plld_mode, + plld_foam_detection_drop=plld_foam_detection_drop, + plld_foam_detection_edge_tolerance=plld_foam_detection_edge_tolerance, + plld_foam_ad_values=plld_foam_ad_values, + plld_foam_search_speed=plld_foam_search_speed, + dispense_back_plld_volume=dispense_back_plld_volume, + post_detection_trajectory=post_detection_trajectory, + post_detection_dist=post_detection_dist, + ) + + return resp_mm + async def request_probe_z_position(self, channel_idx: int) -> float: """Request the z-position of the channel probe (EXCLUDING the tip)""" resp = await self.send_command( From 7b72c0859851f5951a35aaf42fcdd07912a6bd3c Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Tue, 30 Dec 2025 15:39:33 +0000 Subject: [PATCH 10/13] on machine testing --- .../backends/hamilton/STAR_backend.py | 49 ++++++++++++++----- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index b1b876996d..3a2b72baa9 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -9123,7 +9123,7 @@ async def clld_probe_z_height_using_channel( return result_probed_z_height - async def _plld_or_dual_clld_probe_z_height_using_channel( + async def _plld_or_dual_probe_z_height_using_channel( self, channel_idx: int, # 0-based indexing of channels! lowest_immers_pos: float = 99.98, # mm of the head_probe! @@ -9159,6 +9159,7 @@ async def _plld_or_dual_clld_probe_z_height_using_channel( Notes: - This command is implemented via the PX command module, i.e. it is parallelisable! - lowest_immers_pos & start_pos_search refer to the head_probe z-coordinate (not the tip)! + - The return values represent head_probe z-positions (not the tip) in mm! Args: lowest_immers_pos: Lowest allowed Z during the search (mm). Default 99.98. @@ -9188,8 +9189,8 @@ async def _plld_or_dual_clld_probe_z_height_using_channel( post_detection_dist: Post-detection movement distance (mm). Default 2.0. Returns: - list[float]: Two Z-drive positions (mm), meaning depends on the selected pressure sub-mode: - - Single-detection modes/PressureLLDMode.LIQUID: [liquid_level_pos, 0.0] + list[int]: Two z-coordinates (mm), head_probe, meaning depends on the selected pressure sub-mode: + - Single-detection modes/PressureLLDMode.LIQUID: [liquid_level_pos, 0] - Two-detection modes/PressureLLDMode.FOAM: [first_detection_pos, liquid_level_pos] """ @@ -9385,18 +9386,18 @@ async def _plld_or_dual_clld_probe_z_height_using_channel( dw=f"{dispense_drive_current_limit}", ) - resp_mm = [ + resp_probe_mm = [ STARBackend.z_drive_increment_to_mm(int(return_val)) for return_val in resp_raw.split("if")[-1].split() ] - return resp_mm + return resp_probe_mm - async def plld_or_dual_clld_probe_z_height_using_channel( + async def plld_or_dual_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 = 334.7, # mm + start_pos_search: Optional[float] = None, # mm channel_speed_above_start_pos_search: float = 120.0, # mm/sec channel_speed: float = 10.0, # mm channel_acceleration: float = 800.0, # mm/sec**2 @@ -9420,7 +9421,7 @@ async def plld_or_dual_clld_probe_z_height_using_channel( dispense_back_plld_volume: Optional[float] = None, # uL post_detection_trajectory: Literal[0, 1] = 1, post_detection_dist: float = 2.0, # mm - ): + ) -> List[float]: """Detect liquid level using either (1) pressured-based or (2) pressure AND capacitive liquid level detection (LLD). + (a) with foam detection sub-mode or (b) without foam detection sub-mode. @@ -9428,6 +9429,7 @@ async def plld_or_dual_clld_probe_z_height_using_channel( Notes: - This command is implemented via BOTH the PX and C0 command modules, i.e. it is NOT parallelisable! - lowest_immers_pos & start_pos_search refer to the tip z-coordinate (not the head_probe)! + - The return values represent tip z-positions (not the head_probe) in mm! Args: lowest_immers_pos: Lowest allowed Z during the search (mm). Default 99.98. @@ -9457,7 +9459,7 @@ async def plld_or_dual_clld_probe_z_height_using_channel( post_detection_dist: Post-detection movement distance (mm). Default 2.0. Returns: - list[float]: Two Z-drive positions (mm), meaning depends on the selected pressure sub-mode: + list[int]: Two z-coordinates (mm), tip, meaning depends on the selected pressure sub-mode: - Single-detection modes/PressureLLDMode.LIQUID: [liquid_level_pos, 0.0] - Two-detection modes/PressureLLDMode.FOAM: [first_detection_pos, liquid_level_pos] """ @@ -9474,6 +9476,9 @@ async def plld_or_dual_clld_probe_z_height_using_channel( safe_tip_bottom_z_pos = 99.98 safe_tip_top_z_pos = safe_head_top_z_pos - tip_len + fitting_depth + if start_pos_search is None: + start_pos_search = safe_tip_top_z_pos # mm, + channel_head_start_pos = round(start_pos_search + tip_len - fitting_depth, 2) lowest_immers_pos_corrected = round(lowest_immers_pos + tip_len - fitting_depth, 2) @@ -9482,8 +9487,17 @@ async def plld_or_dual_clld_probe_z_height_using_channel( + f" and {safe_tip_top_z_pos} mm, is {start_pos_search} mm " + f"({safe_head_top_z_pos=}, {tip_len=} mm)" ) + assert lowest_immers_pos < start_pos_search, ( + f"Lowest LLD search position (given {lowest_immers_pos} mm) must be " + + f"lower than start position of LLD search (given {start_pos_search} mm)" + ) + assert safe_tip_bottom_z_pos <= lowest_immers_pos <= safe_tip_top_z_pos-1, ( + f"Lowest LLD search must be between \n{safe_tip_bottom_z_pos}" + + f" and {safe_tip_top_z_pos}-1 mm, is {lowest_immers_pos} mm " + + f"({tip_len=} mm)" + ) - resp_mm = await self._plld_or_dual_clld_probe_z_height_using_channel( + resp_probe_mm = await self._plld_or_dual_probe_z_height_using_channel( channel_idx=channel_idx, lowest_immers_pos=lowest_immers_pos_corrected, start_pos_search=channel_head_start_pos, @@ -9510,10 +9524,21 @@ async def plld_or_dual_clld_probe_z_height_using_channel( dispense_back_plld_volume=dispense_back_plld_volume, post_detection_trajectory=post_detection_trajectory, post_detection_dist=post_detection_dist, - ) + ) + + if plld_mode == self.PressureLLDMode.FOAM: + resp_tip_mm = [ + round(z_pos - tip_len + fitting_depth, 2) for z_pos in resp_probe_mm + ] + else: + resp_tip_mm = [ + round(resp_probe_mm[0] - tip_len + fitting_depth, 2), + 0.0 + ] - return resp_mm + return resp_tip_mm + async def request_probe_z_position(self, channel_idx: int) -> float: """Request the z-position of the channel probe (EXCLUDING the tip)""" resp = await self.send_command( From c2b12e57931d72d669627a0a93ad084dcfc8069f Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Tue, 30 Dec 2025 15:41:47 +0000 Subject: [PATCH 11/13] `make format` --- .../backends/hamilton/STAR_backend.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 3a2b72baa9..039ea23f64 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -9491,7 +9491,7 @@ async def plld_or_dual_probe_z_height_using_channel( f"Lowest LLD search position (given {lowest_immers_pos} mm) must be " + f"lower than start position of LLD search (given {start_pos_search} mm)" ) - assert safe_tip_bottom_z_pos <= lowest_immers_pos <= safe_tip_top_z_pos-1, ( + assert safe_tip_bottom_z_pos <= lowest_immers_pos <= safe_tip_top_z_pos - 1, ( f"Lowest LLD search must be between \n{safe_tip_bottom_z_pos}" + f" and {safe_tip_top_z_pos}-1 mm, is {lowest_immers_pos} mm " + f"({tip_len=} mm)" @@ -9524,21 +9524,15 @@ async def plld_or_dual_probe_z_height_using_channel( dispense_back_plld_volume=dispense_back_plld_volume, post_detection_trajectory=post_detection_trajectory, post_detection_dist=post_detection_dist, - ) + ) if plld_mode == self.PressureLLDMode.FOAM: - resp_tip_mm = [ - round(z_pos - tip_len + fitting_depth, 2) for z_pos in resp_probe_mm - ] + resp_tip_mm = [round(z_pos - tip_len + fitting_depth, 2) for z_pos in resp_probe_mm] else: - resp_tip_mm = [ - round(resp_probe_mm[0] - tip_len + fitting_depth, 2), - 0.0 - ] + resp_tip_mm = [round(resp_probe_mm[0] - tip_len + fitting_depth, 2), 0.0] return resp_tip_mm - async def request_probe_z_position(self, channel_idx: int) -> float: """Request the z-position of the channel probe (EXCLUDING the tip)""" resp = await self.send_command( From 2dae1dd712f46f8bc6e6c0a52324c80597dc3455 Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Wed, 31 Dec 2025 13:13:14 +0000 Subject: [PATCH 12/13] swap `lld_mode` for `clld_verification` argument --- .../backends/hamilton/STAR_backend.py | 98 ++++++++++--------- 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 039ea23f64..3a73f9ce46 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -9123,7 +9123,7 @@ async def clld_probe_z_height_using_channel( return result_probed_z_height - async def _plld_or_dual_probe_z_height_using_channel( + async def _plld_probe_z_height_using_channel( self, channel_idx: int, # 0-based indexing of channels! lowest_immers_pos: float = 99.98, # mm of the head_probe! @@ -9137,27 +9137,27 @@ async def _plld_or_dual_probe_z_height_using_channel( dispense_drive_acceleration: float = 0.2, # mm/sec**2 dispense_drive_max_speed: float = 14.5, # mm/sec dispense_drive_current_limit: int = 3, # unknown unit - clld_detection_edge: int = 10, - clld_detection_drop: int = 2, plld_detection_edge: int = 30, plld_detection_drop: int = 10, - lld_mode: Optional[LLDMode] = None, - max_delta_plld_clld: float = 5.0, # mm - plld_mode: Optional[PressureLLDMode] = None, - plld_foam_detection_drop: int = 30, - plld_foam_detection_edge_tolerance: int = 30, - plld_foam_ad_values: int = 30, # unknown unit - plld_foam_search_speed: float = 10.0, # mm/sec + clld_verification: bool = False, # cLLD Verification feature + clld_detection_edge: int = 10, # cLLD Verification feature + clld_detection_drop: int = 2, # cLLD Verification feature + max_delta_plld_clld: float = 5.0, # cLLD Verification feature; mm + plld_mode: Optional[PressureLLDMode] = None, # Foam feature + plld_foam_detection_drop: int = 30, # Foam feature + plld_foam_detection_edge_tolerance: int = 30, # Foam feature + plld_foam_ad_values: int = 30, # Foam feature; unknown unit + plld_foam_search_speed: float = 10.0, # Foam feature; mm/sec dispense_back_plld_volume: Optional[float] = None, # uL post_detection_trajectory: Literal[0, 1] = 1, post_detection_dist: float = 2.0, # mm ): - """Detect liquid level using either (1) pressured-based or - (2) pressure AND capacitive liquid level detection (LLD). - + (a) with foam detection sub-mode or (b) without foam detection sub-mode. + """Detect liquid level using pressured-based liquid level detection (pLLD) + (1) with or (2) without additional cLLD verification, and (a) with foam detection sub-mode or + (b) without foam detection sub-mode. Notes: - - This command is implemented via the PX command module, i.e. it is parallelisable! + - This command is implemented via the PX command module, i.e. it IS parallelisable! - lowest_immers_pos & start_pos_search refer to the head_probe z-coordinate (not the tip)! - The return values represent head_probe z-positions (not the tip) in mm! @@ -9173,12 +9173,16 @@ async def _plld_or_dual_probe_z_height_using_channel( dispense_drive_acceleration: Dispense drive acceleration (mm/s²). Default 0.2. dispense_drive_max_speed: Dispense drive max speed (mm/s). Default 14.5. dispense_drive_current_limit: Dispense drive current limit (instrument units). Default 3. - clld_detection_edge: Capacitive detection edge threshold. Default 10. - clld_detection_drop: Capacitive detection drop threshold. Default 2. plld_detection_edge: Pressure detection edge threshold. Default 30. plld_detection_drop: Pressure detection drop threshold. Default 10. - lld_mode: Pressure-only vs dual (pressure + capacitive) behaviour. Default None. + clld_verification: Activates cLLD sensing concurrently with the pressure probing. Note: cLLD + measurement itself cannot be retrieved. Instead it can be used for other applications, including + (1) verification of the surface level detected by pLLD based on max_delta_plld_clld, + (2) detection of foam (more easily triggers cLLD), if desired causing and error. + This activates all cLLD-specific arguments. Default False. max_delta_plld_clld: Max allowed delta between pressure/capacitive detections (mm). Default 5.0. + clld_detection_edge: Capacitive detection edge threshold. Default 10. + clld_detection_drop: Capacitive detection drop threshold. Default 2. plld_mode: Pressure-detection sub-mode (instrument-defined). Default None. plld_foam_detection_drop: Foam detection drop threshold. Default 30. plld_foam_detection_edge_tolerance: Foam detection edge tolerance. Default 30. @@ -9199,9 +9203,6 @@ async def _plld_or_dual_probe_z_height_using_channel( if not isinstance(channel_idx, int) or not (0 <= channel_idx <= self.num_channels - 1): raise ValueError(f"channel_idx must be in [0, {self.num_channels - 1}], is {channel_idx}") - if lld_mode is None: - lld_mode = self.LLDMode.PRESSURE - if plld_mode is None: plld_mode = self.PressureLLDMode.LIQUID @@ -9253,10 +9254,10 @@ async def _plld_or_dual_probe_z_height_using_channel( assert tip_has_filter in [True, False], "tip_has_filter must be a boolean" - assert lld_mode in [self.LLDMode.PRESSURE, self.LLDMode.DUAL], ( - f"lld_mode must be either LLDMode.PRESSURE ({self.LLDMode.PRESSURE}) or " - + f"LLDMode.DUAL ({self.LLDMode.DUAL}), is {lld_mode}" - ) + assert isinstance( + clld_verification, bool + ), f"clld_verification must be a boolean, is {clld_verification}" + assert plld_mode in [self.PressureLLDMode.LIQUID, self.PressureLLDMode.FOAM], ( f"plld_mode must be either PressureLLDMode.LIQUID ({self.PressureLLDMode.LIQUID}) or " + f"PressureLLDMode.FOAM ({self.PressureLLDMode.FOAM}), is {plld_mode}" @@ -9367,7 +9368,7 @@ async def _plld_or_dual_probe_z_height_using_channel( gl=f"{clld_detection_drop:04}", gu=f"{plld_detection_edge:04}", gn=f"{plld_detection_drop:04}", - gm="0" if lld_mode == self.LLDMode.PRESSURE else "1", + gm=str(int(clld_verification)), gz=f"{max_delta_plld_clld_increments:04}", cj=str(plld_mode.value), co=f"{plld_foam_detection_drop:04}", @@ -9393,7 +9394,7 @@ async def _plld_or_dual_probe_z_height_using_channel( return resp_probe_mm - async def plld_or_dual_probe_z_height_using_channel( + async def plld_probe_z_height_using_channel( self, channel_idx: int, # 0-based indexing of channels! lowest_immers_pos: float = 99.98, # mm @@ -9407,27 +9408,28 @@ async def plld_or_dual_probe_z_height_using_channel( dispense_drive_acceleration: float = 0.2, # mm/sec**2 dispense_drive_max_speed: float = 14.5, # mm/sec dispense_drive_current_limit: int = 3, # unknown unit - clld_detection_edge: int = 10, - clld_detection_drop: int = 2, plld_detection_edge: int = 30, plld_detection_drop: int = 10, - lld_mode: Optional[LLDMode] = None, - max_delta_plld_clld: float = 5.0, # mm - plld_mode: Optional[PressureLLDMode] = None, - plld_foam_detection_drop: int = 30, - plld_foam_detection_edge_tolerance: int = 30, - plld_foam_ad_values: int = 30, # unknown unit - plld_foam_search_speed: float = 10.0, # mm/sec + clld_verification: bool = False, # cLLD Verification feature + clld_detection_edge: int = 10, # cLLD Verification feature + clld_detection_drop: int = 2, # cLLD Verification feature + max_delta_plld_clld: float = 5.0, # cLLD Verification feature; mm + plld_mode: Optional[PressureLLDMode] = None, # Foam feature + plld_foam_detection_drop: int = 30, # Foam feature + plld_foam_detection_edge_tolerance: int = 30, # Foam feature + plld_foam_ad_values: int = 30, # Foam feature; unknown unit + plld_foam_search_speed: float = 10.0, # Foam feature; mm/sec dispense_back_plld_volume: Optional[float] = None, # uL post_detection_trajectory: Literal[0, 1] = 1, post_detection_dist: float = 2.0, # mm ) -> List[float]: - """Detect liquid level using either (1) pressured-based or - (2) pressure AND capacitive liquid level detection (LLD). - + (a) with foam detection sub-mode or (b) without foam detection sub-mode. + """Detect liquid level using pressured-based liquid level detection (pLLD) + (1) with or (2) without additional cLLD verification, and (a) with foam detection sub-mode or + (b) without foam detection sub-mode. Notes: - - This command is implemented via BOTH the PX and C0 command modules, i.e. it is NOT parallelisable! + - This command is implemented via BOTH the PX and C0 command modules, + i.e. it is NOT parallelisable! - lowest_immers_pos & start_pos_search refer to the tip z-coordinate (not the head_probe)! - The return values represent tip z-positions (not the head_probe) in mm! @@ -9443,11 +9445,15 @@ async def plld_or_dual_probe_z_height_using_channel( dispense_drive_acceleration: Dispense drive acceleration (mm/s²). Default 0.2. dispense_drive_max_speed: Dispense drive max speed (mm/s). Default 14.5. dispense_drive_current_limit: Dispense drive current limit (instrument units). Default 3. - clld_detection_edge: Capacitive detection edge threshold. Default 10. - clld_detection_drop: Capacitive detection drop threshold. Default 2. plld_detection_edge: Pressure detection edge threshold. Default 30. plld_detection_drop: Pressure detection drop threshold. Default 10. - lld_mode: Pressure-only vs dual (pressure + capacitive) behaviour. Default None. + clld_verification: Activates cLLD sensing concurrently with the pressure probing. Note: cLLD + measurement itself cannot be retrieved. Instead it can be used for other applications, including + (1) verification of the surface level detected by pLLD based on max_delta_plld_clld, + (2) detection of foam (more easily triggers cLLD), if desired causing and error. + This activates all cLLD-specific arguments. Default False. + clld_detection_edge: Capacitive detection edge threshold. Default 10. + clld_detection_drop: Capacitive detection drop threshold. Default 2. max_delta_plld_clld: Max allowed delta between pressure/capacitive detections (mm). Default 5.0. plld_mode: Pressure-detection sub-mode (instrument-defined). Default None. plld_foam_detection_drop: Foam detection drop threshold. Default 30. @@ -9497,7 +9503,7 @@ async def plld_or_dual_probe_z_height_using_channel( + f"({tip_len=} mm)" ) - resp_probe_mm = await self._plld_or_dual_probe_z_height_using_channel( + resp_probe_mm = await self._plld_probe_z_height_using_channel( channel_idx=channel_idx, lowest_immers_pos=lowest_immers_pos_corrected, start_pos_search=channel_head_start_pos, @@ -9510,11 +9516,11 @@ async def plld_or_dual_probe_z_height_using_channel( dispense_drive_acceleration=dispense_drive_acceleration, dispense_drive_max_speed=dispense_drive_max_speed, dispense_drive_current_limit=dispense_drive_current_limit, - clld_detection_edge=clld_detection_edge, - clld_detection_drop=clld_detection_drop, plld_detection_edge=plld_detection_edge, plld_detection_drop=plld_detection_drop, - lld_mode=lld_mode, + clld_verification=clld_verification, + clld_detection_edge=clld_detection_edge, + clld_detection_drop=clld_detection_drop, max_delta_plld_clld=max_delta_plld_clld, plld_mode=plld_mode, plld_foam_detection_drop=plld_foam_detection_drop, From 5cba5e6bef8f8dd59cd8524480c23cf45b40ddff Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Wed, 31 Dec 2025 13:50:25 -0800 Subject: [PATCH 13/13] mini --- .../backends/hamilton/STAR_backend.py | 183 ++++++++---------- 1 file changed, 84 insertions(+), 99 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 3a73f9ce46..3f23954ef0 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -8863,7 +8863,7 @@ async def clld_probe_y_position_using_channel( start_pos_search: Initial y-position for the search (in mm). If not set, defaults to the current channel y-position. end_pos_search: Final y-position for the search (in mm). If not set, defaults to the maximum safe travel range. channel_speed: Channel movement speed during probing (mm/sec). Defaults to 10.0 mm/sec. - channel_acceleration_int: Acceleration ramp setting [1-4], where the physical acceleration is `value * 5,000 steps/sec²`. Defaults to 4. + channel_acceleration_int: Acceleration ramp setting [1-4], where the physical acceleration is `value * 5,000 steps/sec**2`. Defaults to 4. detection_edge: Edge steepness for capacitive detection [0-1024]. Defaults to 10. current_limit_int: Current limit setting [1-7]. Defaults to 7. post_detection_dist: Retraction distance after detection (in mm). Defaults to 2.0 mm. @@ -9123,7 +9123,7 @@ async def clld_probe_z_height_using_channel( return result_probed_z_height - async def _plld_probe_z_height_using_channel( + async def _search_for_surface_using_plld( self, channel_idx: int, # 0-based indexing of channels! lowest_immers_pos: float = 99.98, # mm of the head_probe! @@ -9131,12 +9131,12 @@ async def _plld_probe_z_height_using_channel( channel_speed_above_start_pos_search: float = 120.0, # mm/sec channel_speed: float = 10.0, # mm channel_acceleration: float = 800.0, # mm/sec**2 - z_drive_current_limit: int = 3, # unknown unit + z_drive_current_limit: int = 3, tip_has_filter: bool = False, dispense_drive_speed: float = 5.0, # mm/sec dispense_drive_acceleration: float = 0.2, # mm/sec**2 dispense_drive_max_speed: float = 14.5, # mm/sec - dispense_drive_current_limit: int = 3, # unknown unit + dispense_drive_current_limit: int = 3, plld_detection_edge: int = 30, plld_detection_drop: int = 10, clld_verification: bool = False, # cLLD Verification feature @@ -9152,50 +9152,50 @@ async def _plld_probe_z_height_using_channel( post_detection_trajectory: Literal[0, 1] = 1, post_detection_dist: float = 2.0, # mm ): - """Detect liquid level using pressured-based liquid level detection (pLLD) - (1) with or (2) without additional cLLD verification, and (a) with foam detection sub-mode or - (b) without foam detection sub-mode. + """Search a surface using pressured-based liquid level detection (pLLD) + (1) with or (2) without additional cLLD verification, and (a) with foam detection sub-mode or + (b) without foam detection sub-mode. Notes: - - This command is implemented via the PX command module, i.e. it IS parallelisable! - - lowest_immers_pos & start_pos_search refer to the head_probe z-coordinate (not the tip)! - - The return values represent head_probe z-positions (not the tip) in mm! + - This command is implemented via the PX command module, i.e. it IS parallelisable + - lowest_immers_pos & start_pos_search refer to the head_probe z-coordinate (not the tip) + - The return values represent head_probe z-positions (not the tip) in mm Args: - lowest_immers_pos: Lowest allowed Z during the search (mm). Default 99.98. - start_pos_search: Z position where the search begins (mm). Default 334.7. - channel_speed_above_start_pos_search: Z speed above the start position (mm/s). Default 120.0. - channel_speed: Z search speed (mm/s). Default 10.0. - channel_acceleration: Z acceleration (mm/s²). Default 800.0. - z_drive_current_limit: Z drive current limit (instrument units). Default 3. - tip_has_filter: Whether a filter tip is mounted. Default False. - dispense_drive_speed: Dispense drive speed (mm/s). Default 5.0. - dispense_drive_acceleration: Dispense drive acceleration (mm/s²). Default 0.2. - dispense_drive_max_speed: Dispense drive max speed (mm/s). Default 14.5. - dispense_drive_current_limit: Dispense drive current limit (instrument units). Default 3. - plld_detection_edge: Pressure detection edge threshold. Default 30. - plld_detection_drop: Pressure detection drop threshold. Default 10. - clld_verification: Activates cLLD sensing concurrently with the pressure probing. Note: cLLD - measurement itself cannot be retrieved. Instead it can be used for other applications, including - (1) verification of the surface level detected by pLLD based on max_delta_plld_clld, - (2) detection of foam (more easily triggers cLLD), if desired causing and error. - This activates all cLLD-specific arguments. Default False. - max_delta_plld_clld: Max allowed delta between pressure/capacitive detections (mm). Default 5.0. - clld_detection_edge: Capacitive detection edge threshold. Default 10. - clld_detection_drop: Capacitive detection drop threshold. Default 2. - plld_mode: Pressure-detection sub-mode (instrument-defined). Default None. - plld_foam_detection_drop: Foam detection drop threshold. Default 30. - plld_foam_detection_edge_tolerance: Foam detection edge tolerance. Default 30. - plld_foam_ad_values: Foam AD values (instrument units). Default 30. - plld_foam_search_speed: Foam search speed (mm/s). Default 10.0. - dispense_back_plld_volume: Optional dispense-back volume after detection (µL). Default None. - post_detection_trajectory: Post-detection movement pattern selector. Default 1. - post_detection_dist: Post-detection movement distance (mm). Default 2.0. + lowest_immers_pos: Lowest allowed Z during the search (mm). Default 99.98. + start_pos_search: Z position where the search begins (mm). Default 334.7. + channel_speed_above_start_pos_search: Z speed above the start position (mm/s). Default 120.0. + channel_speed: Z search speed (mm/s). Default 10.0. + channel_acceleration: Z acceleration (mm/s**2). Default 800.0. + z_drive_current_limit: Z drive current limit (instrument units). Default 3. + tip_has_filter: Whether a filter tip is mounted. Default False. + dispense_drive_speed: Dispense drive speed (mm/s). Default 5.0. + dispense_drive_acceleration: Dispense drive acceleration (mm/s**2). Default 0.2. + dispense_drive_max_speed: Dispense drive max speed (mm/s). Default 14.5. + dispense_drive_current_limit: Dispense drive current limit (instrument units). Default 3. + plld_detection_edge: Pressure detection edge threshold. Default 30. + plld_detection_drop: Pressure detection drop threshold. Default 10. + clld_verification: Activates cLLD sensing concurrently with the pressure probing. Note: cLLD + measurement itself cannot be retrieved. Instead it can be used for other applications, including + (1) verification of the surface level detected by pLLD based on max_delta_plld_clld, + (2) detection of foam (more easily triggers cLLD), if desired causing and error. + This activates all cLLD-specific arguments. Default False. + max_delta_plld_clld: Max allowed delta between pressure/capacitive detections (mm). Default 5.0. + clld_detection_edge: Capacitive detection edge threshold. Default 10. + clld_detection_drop: Capacitive detection drop threshold. Default 2. + plld_mode: Pressure-detection sub-mode (instrument-defined). Default None. + plld_foam_detection_drop: Foam detection drop threshold. Default 30. + plld_foam_detection_edge_tolerance: Foam detection edge tolerance. Default 30. + plld_foam_ad_values: Foam AD values (instrument units). Default 30. + plld_foam_search_speed: Foam search speed (mm/s). Default 10.0. + dispense_back_plld_volume: Optional dispense-back volume after detection (µL). Default None. + post_detection_trajectory: Post-detection movement pattern selector. Default 1. + post_detection_dist: Post-detection movement distance (mm). Default 2.0. Returns: - list[int]: Two z-coordinates (mm), head_probe, meaning depends on the selected pressure sub-mode: - - Single-detection modes/PressureLLDMode.LIQUID: [liquid_level_pos, 0] - - Two-detection modes/PressureLLDMode.FOAM: [first_detection_pos, liquid_level_pos] + Two z-coordinates (mm), head_probe, meaning depends on the selected pressure sub-mode: + - Single-detection modes/PressureLLDMode.LIQUID: [liquid_level_pos, 0] + - Two-detection modes/PressureLLDMode.FOAM: [first_detection_pos, liquid_level_pos] """ # Preconditions checks @@ -9386,6 +9386,7 @@ async def _plld_probe_z_height_using_channel( dv=f"{dispense_drive_max_speed_increments:05}", dw=f"{dispense_drive_current_limit}", ) + assert resp_raw is not None resp_probe_mm = [ STARBackend.z_drive_increment_to_mm(int(return_val)) @@ -9402,12 +9403,12 @@ async def plld_probe_z_height_using_channel( channel_speed_above_start_pos_search: float = 120.0, # mm/sec channel_speed: float = 10.0, # mm channel_acceleration: float = 800.0, # mm/sec**2 - z_drive_current_limit: int = 3, # unknown unit + z_drive_current_limit: int = 3, tip_has_filter: bool = False, dispense_drive_speed: float = 5.0, # mm/sec dispense_drive_acceleration: float = 0.2, # mm/sec**2 dispense_drive_max_speed: float = 14.5, # mm/sec - dispense_drive_current_limit: int = 3, # unknown unit + dispense_drive_current_limit: int = 3, plld_detection_edge: int = 30, plld_detection_drop: int = 10, clld_verification: bool = False, # cLLD Verification feature @@ -9424,50 +9425,49 @@ async def plld_probe_z_height_using_channel( post_detection_dist: float = 2.0, # mm ) -> List[float]: """Detect liquid level using pressured-based liquid level detection (pLLD) - (1) with or (2) without additional cLLD verification, and (a) with foam detection sub-mode or - (b) without foam detection sub-mode. + (1) with or (2) without additional cLLD verification, and (a) with foam detection sub-mode or + (b) without foam detection sub-mode. Notes: - - This command is implemented via BOTH the PX and C0 command modules, - i.e. it is NOT parallelisable! - - lowest_immers_pos & start_pos_search refer to the tip z-coordinate (not the head_probe)! - - The return values represent tip z-positions (not the head_probe) in mm! + - This command is implemented via BOTH the PX and C0 command modules, i.e. it is NOT parallelisable! + - lowest_immers_pos & start_pos_search refer to the tip z-coordinate (not the head_probe)! + - The return values represent tip z-positions (not the head_probe) in mm! Args: - lowest_immers_pos: Lowest allowed Z during the search (mm). Default 99.98. - start_pos_search: Z position where the search begins (mm). Default 334.7. - channel_speed_above_start_pos_search: Z speed above the start position (mm/s). Default 120.0. - channel_speed: Z search speed (mm/s). Default 10.0. - channel_acceleration: Z acceleration (mm/s²). Default 800.0. - z_drive_current_limit: Z drive current limit (instrument units). Default 3. - tip_has_filter: Whether a filter tip is mounted. Default False. - dispense_drive_speed: Dispense drive speed (mm/s). Default 5.0. - dispense_drive_acceleration: Dispense drive acceleration (mm/s²). Default 0.2. - dispense_drive_max_speed: Dispense drive max speed (mm/s). Default 14.5. - dispense_drive_current_limit: Dispense drive current limit (instrument units). Default 3. - plld_detection_edge: Pressure detection edge threshold. Default 30. - plld_detection_drop: Pressure detection drop threshold. Default 10. - clld_verification: Activates cLLD sensing concurrently with the pressure probing. Note: cLLD - measurement itself cannot be retrieved. Instead it can be used for other applications, including - (1) verification of the surface level detected by pLLD based on max_delta_plld_clld, - (2) detection of foam (more easily triggers cLLD), if desired causing and error. - This activates all cLLD-specific arguments. Default False. - clld_detection_edge: Capacitive detection edge threshold. Default 10. - clld_detection_drop: Capacitive detection drop threshold. Default 2. - max_delta_plld_clld: Max allowed delta between pressure/capacitive detections (mm). Default 5.0. - plld_mode: Pressure-detection sub-mode (instrument-defined). Default None. - plld_foam_detection_drop: Foam detection drop threshold. Default 30. - plld_foam_detection_edge_tolerance: Foam detection edge tolerance. Default 30. - plld_foam_ad_values: Foam AD values (instrument units). Default 30. - plld_foam_search_speed: Foam search speed (mm/s). Default 10.0. - dispense_back_plld_volume: Optional dispense-back volume after detection (µL). Default None. - post_detection_trajectory: Post-detection movement pattern selector. Default 1. - post_detection_dist: Post-detection movement distance (mm). Default 2.0. + lowest_immers_pos: Lowest allowed Z during the search (mm). Default 99.98. + start_pos_search: Z position where the search begins (mm). Default 334.7. + channel_speed_above_start_pos_search: Z speed above the start position (mm/s). Default 120.0. + channel_speed: Z search speed (mm/s). Default 10.0. + channel_acceleration: Z acceleration (mm/s**2). Default 800.0. + z_drive_current_limit: Z drive current limit (instrument units). Default 3. + tip_has_filter: Whether a filter tip is mounted. Default False. + dispense_drive_speed: Dispense drive speed (mm/s). Default 5.0. + dispense_drive_acceleration: Dispense drive acceleration (mm/s**2). Default 0.2. + dispense_drive_max_speed: Dispense drive max speed (mm/s). Default 14.5. + dispense_drive_current_limit: Dispense drive current limit (instrument units). Default 3. + plld_detection_edge: Pressure detection edge threshold. Default 30. + plld_detection_drop: Pressure detection drop threshold. Default 10. + clld_verification: Activates cLLD sensing concurrently with the pressure probing. Note: cLLD + measurement itself cannot be retrieved. Instead it can be used for other applications, including + (1) verification of the surface level detected by pLLD based on max_delta_plld_clld, + (2) detection of foam (more easily triggers cLLD), if desired causing and error. + This activates all cLLD-specific arguments. Default False. + clld_detection_edge: Capacitive detection edge threshold. Default 10. + clld_detection_drop: Capacitive detection drop threshold. Default 2. + max_delta_plld_clld: Max allowed delta between pressure/capacitive detections (mm). Default 5.0. + plld_mode: Pressure-detection sub-mode (instrument-defined). Default None. + plld_foam_detection_drop: Foam detection drop threshold. Default 30. + plld_foam_detection_edge_tolerance: Foam detection edge tolerance. Default 30. + plld_foam_ad_values: Foam AD values (instrument units). Default 30. + plld_foam_search_speed: Foam search speed (mm/s). Default 10.0. + dispense_back_plld_volume: Optional dispense-back volume after detection (µL). Default None. + post_detection_trajectory: Post-detection movement pattern selector. Default 1. + post_detection_dist: Post-detection movement distance (mm). Default 2.0. Returns: - list[int]: Two z-coordinates (mm), tip, meaning depends on the selected pressure sub-mode: - - Single-detection modes/PressureLLDMode.LIQUID: [liquid_level_pos, 0.0] - - Two-detection modes/PressureLLDMode.FOAM: [first_detection_pos, liquid_level_pos] + Two z-coordinates (mm), tip, meaning depends on the selected pressure sub-mode: + - Single-detection modes/PressureLLDMode.LIQUID: [liquid_level_pos, 0.0] + - Two-detection modes/PressureLLDMode.FOAM: [first_detection_pos, liquid_level_pos] """ # Ensure tip is mounted tip_presence = await self.request_tip_presence() @@ -9483,27 +9483,12 @@ async def plld_probe_z_height_using_channel( safe_tip_top_z_pos = safe_head_top_z_pos - tip_len + fitting_depth if start_pos_search is None: - start_pos_search = safe_tip_top_z_pos # mm, + start_pos_search = safe_tip_top_z_pos channel_head_start_pos = round(start_pos_search + tip_len - fitting_depth, 2) lowest_immers_pos_corrected = round(lowest_immers_pos + tip_len - fitting_depth, 2) - assert safe_tip_bottom_z_pos <= start_pos_search <= safe_tip_top_z_pos, ( - f"Start position of LLD search must be between \n{safe_tip_bottom_z_pos}" - + f" and {safe_tip_top_z_pos} mm, is {start_pos_search} mm " - + f"({safe_head_top_z_pos=}, {tip_len=} mm)" - ) - assert lowest_immers_pos < start_pos_search, ( - f"Lowest LLD search position (given {lowest_immers_pos} mm) must be " - + f"lower than start position of LLD search (given {start_pos_search} mm)" - ) - assert safe_tip_bottom_z_pos <= lowest_immers_pos <= safe_tip_top_z_pos - 1, ( - f"Lowest LLD search must be between \n{safe_tip_bottom_z_pos}" - + f" and {safe_tip_top_z_pos}-1 mm, is {lowest_immers_pos} mm " - + f"({tip_len=} mm)" - ) - - resp_probe_mm = await self._plld_probe_z_height_using_channel( + resp_probe_mm = await self._search_for_surface_using_plld( channel_idx=channel_idx, lowest_immers_pos=lowest_immers_pos_corrected, start_pos_search=channel_head_start_pos,