Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
"\n",
"Surface following is a feature on Hamilton liquid handling robots that makes the pipette tip follow the surface of a liquid when aspirating (going down) or dispensing (going up).\n",
"\n",
"When using automatic surface following, the robot will automatically move the Z position of the pipette tip based on a user specified value. The amount of surface following required can be computed by subtracting the liquid level before and after each aspiration or dispense. PyLabRobot can do this automatically when the height<>volume functions for the given containers are defined. You can also specify the liquid surface following distance manually.\n",
"When using surface following, the robot will automatically move the Z position of the pipette tip the user-specified distance. The amount of surface following required can be computed by comparing the liquid level before and after each aspiration or dispense. PyLabRobot can do this automatically when the height<>volume functions for the given containers are defined. You can also specify the liquid surface following distance manually.\n",
"\n",
"It is useful to start the surface following only at the liquid level, so it is recommended to use [liquid level detection](./star_lld) with the surface following feature. (See below). VENUS also supports this.\n",
"It is useful to start the surface following only at the liquid level, so it is recommended to use [liquid level detection](./star_lld) with the surface following feature. (See below for syntax, which differs from the LLD tutorial). VENUS also supports surface following while doing LLD.\n",
"\n",
"In PLR, when we have LLD + automatic surface following, we can go beyond VENUS by computing the surface following amount based on the precise location of liquid inside the container. This is necessary because the surface following amount is not only a function of the volume of liquid aspirated or dispensed, but also of the location of liquid inside the container (see below). By doing liquid level detection first to get the precise liquid level, we can use that to compute the surface following amount based on the requested volume _and_ location of liquid inside the container.\n",
"In PLR, when we have LLD + automatic surface following, we can go beyond VENUS by computing the surface following amount based on the precise location of liquid inside the container. This is necessary because the surface following amount is not _just_ a function of the volume of liquid aspirated or dispensed, _but also_ of the location of liquid inside the container (see below). By doing liquid level detection first to get the precise liquid level, we can then use that liquid level height to compute the surface following amount based on the requested volume _and_ location of liquid inside the container.\n",
"\n",
"![](./img/surface_following/surface_following_distance.svg)"
]
Expand All @@ -28,7 +28,7 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": 1,
"id": "d3f86a7d",
"metadata": {},
"outputs": [],
Expand Down Expand Up @@ -64,7 +64,7 @@
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": 2,
"id": "5e9d4e1a",
"metadata": {},
"outputs": [],
Expand All @@ -78,12 +78,12 @@
"id": "aebdc554",
"metadata": {},
"source": [
"You can probe the liquid height first using liquid level detection, and then use automatic surface following for subsequent aspirations and dispenses as follows:"
"You can probe the liquid height first using liquid level detection (capacitive), and then use automatic surface following for subsequent aspirations and dispenses as follows:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": 3,
"id": "d4858585",
"metadata": {},
"outputs": [],
Expand Down Expand Up @@ -113,12 +113,12 @@
"id": "0b3ac98c",
"metadata": {},
"source": [
"You can also pass the liquid height directly to the aspiration and dispense methods, and still use automatic surface following:"
"You can also pass the liquid height directly to the aspiration and dispense methods, and still use automatic surface following. This can be useful when you cannot use LLD."
]
},
{
"cell_type": "code",
"execution_count": 6,
"execution_count": 4,
"id": "ffde0c4c",
"metadata": {},
"outputs": [],
Expand Down Expand Up @@ -152,12 +152,12 @@
"id": "aee70228",
"metadata": {},
"source": [
"To manually specify the surface following amount, you can use the `surface_following_distance` backend kwarg of the aspiration and dispense methods. For example, to aspirate 100 µL with a surface following amount of 2 mm above the detected liquid height:"
"To manually specify the surface following amount, you can use the `surface_following_distance` backend kwarg of the aspiration and dispense methods. For example, to aspirate 100 µL with a surface following amount of 2 mm starting at the detected liquid level:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"execution_count": null,
"id": "e3e205b7",
"metadata": {},
"outputs": [],
Expand All @@ -167,14 +167,14 @@
" wells,\n",
" vols,\n",
" probe_liquid_height=True,\n",
" surface_following_distance=[2] * len(wells), # mm down from liquid_height\n",
" surface_following_distance=[2] * len(wells), # mm down after finding liquid\n",
" )\n",
"\n",
" await lh.dispense(\n",
" wells,\n",
" vols,\n",
" probe_liquid_height=True,\n",
" surface_following_distance=[2] * len(wells), # mm up from liquid_height\n",
" surface_following_distance=[2] * len(wells), # mm up after finding liquid\n",
" )"
]
}
Expand Down
100 changes: 64 additions & 36 deletions pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -1719,9 +1719,8 @@ async def probe_liquid_heights(
# detect liquid heights
current_absolute_liquid_heights = await asyncio.gather(
*[
self.clld_probe_z_height_using_channel(
self.move_z_drive_to_liquid_surface_using_clld(
channel_idx=channel,
move_channels_to_save_pos_after=False,
lowest_immers_pos=container.get_absolute_location("c", "c", "cavity_bottom").z
+ tip.total_tip_length
- tip.fitting_depth,
Expand All @@ -1734,6 +1733,11 @@ async def probe_liquid_heights(
]
)

liquid_levels: List[int] = (await self.request_pip_height_last_lld())["lh"] # type: ignore
current_absolute_liquid_heights = [
float(liquid_levels[channel_idx] / 10) for channel_idx in use_channels
]

relative_to_well = [
current_absolute_liquid_heights[i]
- resource.get_absolute_location("c", "c", "cavity_bottom").z
Expand Down Expand Up @@ -8198,7 +8202,7 @@ async def clld_probe_y_position_using_channel(

return material_y_pos

async def clld_probe_z_height_using_channel(
async def move_z_drive_to_liquid_surface_using_clld(
self,
channel_idx: int, # 0-based indexing of channels!
lowest_immers_pos: float = 99.98, # mm
Expand All @@ -8209,28 +8213,7 @@ async def clld_probe_z_height_using_channel(
detection_drop: int = 2,
post_detection_trajectory: Literal[0, 1] = 1,
post_detection_dist: float = 2.0, # mm
move_channels_to_save_pos_after: bool = False,
) -> float:
"""Probes the Z-height below the specified channel on a Hamilton STAR liquid handling machine
using the channels 'capacitive Liquid Level Detection' (cLLD) capabilities.
N.B.: this means only conductive materials can be probed!

Args:
channel_idx: The index of the channel to use for probing. Backmost channel = 0.
lowest_immers_pos: The lowest immersion position in mm. This is the position of the channel, NOT including the tip length (as C0 commands do). So you have to add the total_tip_length - fitting_depth.
start_pos_lld_search: The start position for z-touch search in mm. This is the position of the channel, NOT including the tip length (as C0 commands do). So you have to add the total_tip_length - fitting_depth.
channel_speed: The speed of channel movement in mm/sec.
channel_acceleration: The acceleration of the channel in mm/sec**2.
detection_edge: The edge steepness at capacitive LLD detection.
detection_drop: The offset after capacitive LLD edge detection.
post_detection_trajectory (0, 1): Movement of the channel up (1) or down (0) after contacting the surface.
post_detection_dist: Distance to move into the trajectory after detection in mm.
move_channels_to_save_pos_after: Flag to move channels to a safe position after operation.

Returns:
The detected Z-height in mm.
"""

):
lowest_immers_pos_increments = STARBackend.mm_to_z_drive_increment(lowest_immers_pos)
start_pos_search_increments = STARBackend.mm_to_z_drive_increment(start_pos_search)
channel_speed_increments = STARBackend.mm_to_z_drive_increment(channel_speed)
Expand Down Expand Up @@ -8266,18 +8249,63 @@ async def clld_probe_z_height_using_channel(
+ f" and {STARBackend.z_drive_increment_to_mm(9_999)} mm, is {post_detection_dist} mm"
)

await self.send_command(
module=STARBackend.channel_id(channel_idx),
command="ZL",
zh=f"{lowest_immers_pos_increments:05}", # Lowest immersion position [increment]
zc=f"{start_pos_search_increments:05}", # Start position of LLD search [increment]
zl=f"{channel_speed_increments:05}", # Speed of channel movement
zr=f"{channel_acceleration_thousand_increments:03}", # Acceleration [1000 increment/second^2]
gt=f"{detection_edge:04}", # Edge steepness at capacitive LLD detection
gl=f"{detection_drop:04}", # Offset after capacitive LLD edge detection
zj=post_detection_trajectory, # Movement of the channel after contacting surface
zi=f"{post_detection_dist_increments:04}", # Distance to move up after detection [increment]
)

async def clld_probe_z_height_using_channel(
self,
channel_idx: int, # 0-based indexing of channels!
lowest_immers_pos: float = 99.98, # mm
start_pos_search: float = 330.0, # mm
channel_speed: float = 10.0, # mm
channel_acceleration: float = 800.0, # mm/sec**2
detection_edge: int = 10,
detection_drop: int = 2,
post_detection_trajectory: Literal[0, 1] = 1,
post_detection_dist: float = 2.0, # mm
move_channels_to_save_pos_after: bool = False,
) -> float:
"""Probes the Z-height below the specified channel on a Hamilton STAR liquid handling machine
using the channels 'capacitive Liquid Level Detection' (cLLD) capabilities.
N.B.: this means only conductive materials can be probed!

Args:
channel_idx: The index of the channel to use for probing. Backmost channel = 0.
lowest_immers_pos: The lowest immersion position in mm. This is the position of the channel, NOT including the tip length (as C0 commands do). So you have to add the total_tip_length - fitting_depth.
start_pos_lld_search: The start position for z-touch search in mm. This is the position of the channel, NOT including the tip length (as C0 commands do). So you have to add the total_tip_length - fitting_depth.
channel_speed: The speed of channel movement in mm/sec.
channel_acceleration: The acceleration of the channel in mm/sec**2.
detection_edge: The edge steepness at capacitive LLD detection.
detection_drop: The offset after capacitive LLD edge detection.
post_detection_trajectory (0, 1): Movement of the channel up (1) or down (0) after contacting the surface.
post_detection_dist: Distance to move into the trajectory after detection in mm.
move_channels_to_save_pos_after: Flag to move channels to a safe position after operation.

Returns:
The detected Z-height in mm.
"""

try:
await self.send_command(
module=STARBackend.channel_id(channel_idx),
command="ZL",
zh=f"{lowest_immers_pos_increments:05}", # Lowest immersion position [increment]
zc=f"{start_pos_search_increments:05}", # Start position of LLD search [increment]
zl=f"{channel_speed_increments:05}", # Speed of channel movement
zr=f"{channel_acceleration_thousand_increments:03}", # Acceleration [1000 increment/second^2]
gt=f"{detection_edge:04}", # Edge steepness at capacitive LLD detection
gl=f"{detection_drop:04}", # Offset after capacitive LLD edge detection
zj=post_detection_trajectory, # Movement of the channel after contacting surface
zi=f"{post_detection_dist_increments:04}", # Distance to move up after detection [increment]
await self.move_z_drive_to_liquid_surface_using_clld(
channel_idx=channel_idx,
lowest_immers_pos=lowest_immers_pos,
start_pos_search=start_pos_search,
channel_speed=channel_speed,
channel_acceleration=channel_acceleration,
detection_edge=detection_edge,
detection_drop=detection_drop,
post_detection_trajectory=post_detection_trajectory,
post_detection_dist=post_detection_dist,
)
except STARFirmwareError:
await self.move_all_channels_in_z_safety()
Expand Down