Skip to content
127 changes: 107 additions & 20 deletions pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -1718,7 +1718,7 @@ async def probe_liquid_heights(
# detect liquid heights
current_absolute_liquid_heights = await asyncio.gather(
*[
self.move_z_drive_to_liquid_surface_using_clld(
self._move_z_drive_to_liquid_surface_using_clld(
channel_idx=channel,
lowest_immers_pos=container.get_absolute_location("c", "c", "cavity_bottom").z
+ tip.total_tip_length
Expand Down Expand Up @@ -8973,18 +8973,55 @@ async def clld_probe_y_position_using_channel(

return round(material_y_pos, 1)

async def move_z_drive_to_liquid_surface_using_clld(
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
start_pos_search: float = 330.0, # mm
start_pos_search: float = 334.7, # 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 the tip on a channel to the liquid surface using capacitive LLD (cLLD).

Runs a downward capacitive liquid-level detection (cLLD) search on the specified
0-indexed channel. Requires a tip to be mounted; tip length is queried and used
(with a fixed fitting depth of 8 mm) to convert the provided positions into the
Z-drive coordinates expected by the instrument. If start_pos_search is None, a
safe start height is computed from the tip length. The search will not go below
lowest_immers_pos. After detection, the channel performs the configured
post-detection move (by default retracting 2.0 mm).

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 tip length.
channel_speed: Search speed in mm/s. Defaults to 10.0.
channel_acceleration: Search acceleration in mm/s^2. Defaults to 800.0.
detection_edge: Edge steepness threshold for cLLD detection (0-1023). Defaults to 10.
detection_drop: Offset applied after cLLD edge detection (0-1023). Defaults to 2.
post_detection_trajectory: Instrument post-detection move mode (0 or 1). Defaults to 1.
post_detection_dist: Distance in mm to move after detection (interpreted per trajectory).
Defaults to 2.0.

Raises:
ValueError: If channel_idx 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}")

# Conversions & machine-compatibility check of parameters
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)
Expand Down Expand Up @@ -9046,31 +9083,81 @@ async def clld_probe_z_height_using_channel(
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!
"""Probe the liquid surface Z-height using a channel's capacitive LLD (cLLD).

Uses the specified channel to perform a downward cLLD search and returns the
last liquid level detected by the instrument for that channel.

This helper is responsible for:
- Ensuring a tip is mounted on the chosen channel.
- Reading the mounted tip length and applying the fixed fitting depth (8 mm)
to convert *tip-referenced* Z positions (C0-style coordinates) into the
channel Z-drive coordinates required by the firmware `ZL` cLLD command.
- Optionally moving channels to a Z-safe position after probing.

Note:
cLLD requires a conductive target (e.g., conductive liquid / surface).

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.
channel_idx: Channel index to probe with (0-based; backmost channel = 0).
lowest_immers_pos: Lowest allowed search position in mm, expressed in the
*tip-referenced* coordinate system (i.e., the position you would use for
commands that include tip length). Internally converted to channel Z-drive
coordinates before issuing `ZL`.
start_pos_search: Start position for the cLLD search in mm, expressed in the
*tip-referenced* coordinate system. Internally converted to channel Z-drive
coordinates before issuing `ZL`.
channel_speed: Search speed in mm/s. Defaults to 10.0.
channel_acceleration: Search acceleration in mm/s^2. Defaults to 800.0.
detection_edge: Edge steepness threshold for cLLD detection (0–1023). Defaults to 10.
detection_drop: Offset applied after cLLD edge detection (0–1023). Defaults to 2.
post_detection_trajectory: Firmware post-detection move mode (0 or 1). Defaults to 1.
post_detection_dist: Distance in mm to move after detection (interpreted per trajectory).
Defaults to 2.0.
move_channels_to_save_pos_after: If True, moves all channels to a Z-safe position
after the probing sequence completes.

Raises:
RuntimeError: If no tip is mounted on `channel_idx`.
AssertionError: If the computed start position is outside the allowed safe range.
STARFirmwareError: If the firmware reports an error during cLLD (channels are moved
to Z-safe before re-raising).

Returns:
The detected Z-height in mm.
The detected liquid surface Z-height in mm as reported by `request_pip_height_last_lld()`
for `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
safe_tip_bottom_z_pos = 99.98
safet_tip_top_z_pos = safe_head_top_z_pos - tip_len + fitting_depth

if start_pos_search is None:
start_pos_search = safe_head_top_z_pos - tip_len + fitting_depth
Comment on lines +9143 to +9144
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this implicit behaviour good?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please elaborate what you mean?


channel_head_start_pos = round(start_pos_search + tip_len - fitting_depth, 2)
safe_head_bottom_z_pos = round(safe_tip_bottom_z_pos + tip_len - fitting_depth, 2)
lowest_immers_pos_corrected = round(lowest_immers_pos + tip_len - fitting_depth, 2)

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_tip_bottom_z_pos}"
+ f" and {safet_tip_top_z_pos} mm, is {start_pos_search} mm "
+ f" ({safe_head_top_z_pos=}mm, {tip_len=} mm)"
)

try:
await self.move_z_drive_to_liquid_surface_using_clld(
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,
lowest_immers_pos=lowest_immers_pos_corrected,
start_pos_search=channel_head_start_pos,
channel_speed=channel_speed,
channel_acceleration=channel_acceleration,
detection_edge=detection_edge,
Expand Down