diff --git a/CHANGELOG.md b/CHANGELOG.md index a904b456179..2332f39b545 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -151,6 +151,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - `spread: Literal["wide", "tight"]` for single-resource multi-channel aspirations/dispenses (https://github.com/PyLabRobot/pylabrobot/pull/378) - `STAR.request_volume_in_tip` (https://github.com/PyLabRobot/pylabrobot/pull/376) - `ItemizedResource.{row,column}` (https://github.com/PyLabRobot/pylabrobot/pull/384) +- `STAR.set_minimum_iswap_traversal_height` and `STAR.set_minimum_channel_traversal_height` (https://github.com/PyLabRobot/pylabrobot/pull/398) ### Deprecated @@ -163,6 +164,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Resource definitions with `_L` and `_P`, it is easy enough to use the stem and `.rotated(z=90)` for `_P` (https://github.com/PyLabRobot/pylabrobot/pull/288) - `Cor_6_wellplate_Fl` (https://github.com/PyLabRobot/pylabrobot/pull/311) - `AGenBio_1_wellplate_Fl` -> `AGenBio_1_troughplate_190000uL_Fl`, `AGenBio_4_wellplate_Vb` -> `AGenBio_4_troughplate_75000_Vb` (https://github.com/PyLabRobot/pylabrobot/pull/319) +- `STAR.set_minimum_traversal_height` (https://github.com/PyLabRobot/pylabrobot/pull/398) ### Fixed diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR.py b/pylabrobot/liquid_handling/backends/hamilton/STAR.py index 5bbba9f90e7..14b236dd1e2 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR.py @@ -4,7 +4,7 @@ import logging import re from abc import ABCMeta -from contextlib import asynccontextmanager +from contextlib import asynccontextmanager, contextmanager from typing import ( Callable, Dict, @@ -93,7 +93,7 @@ def need_iswap_parked(method: Callable): async def wrapper(self: "STAR", *args, **kwargs): if self.iswap_installed and not self.iswap_parked: await self.park_iswap( - minimum_traverse_height_at_beginning_of_a_command=int(self._traversal_height * 10) + minimum_traverse_height_at_beginning_of_a_command=int(self._iswap_traversal_height * 10) ) result = await method(self, *args, **kwargs) @@ -1164,7 +1164,8 @@ def __init__( self._num_channels: Optional[int] = None self._core_parked: Optional[bool] = None self._extended_conf: Optional[dict] = None - self._traversal_height: float = 245.0 + self._channel_traversal_height: float = 245.0 + self._iswap_traversal_height: float = 245.0 self.core_adjustment = Coordinate.zero() self._unsafe = UnSafe(self) @@ -1183,7 +1184,13 @@ def num_channels(self) -> int: return self._num_channels def set_minimum_traversal_height(self, traversal_height: float): - """Set the minimum traversal height for the robot. + raise NotImplementedError( + "set_minimum_traversal_height is depricated. use set_minimum_channel_traversal_height or " + "set_minimum_iswap_traversal_height instead." + ) + + def set_minimum_channel_traversal_height(self, traversal_height: float): + """Set the minimum traversal height for the pip channels. This refers to the bottom of the pipetting channel when no tip is present, or the bottom of the tip when a tip is present. This value will be used as the default value for the @@ -1193,7 +1200,24 @@ def set_minimum_traversal_height(self, traversal_height: float): assert 0 < traversal_height < 285, "Traversal height must be between 0 and 285 mm" - self._traversal_height = traversal_height + self._channel_traversal_height = traversal_height + + def set_minimum_iswap_traversal_height(self, traversal_height: float): + """Set the minimum traversal height for the iswap.""" + + assert 0 < traversal_height < 285, "Traversal height must be between 0 and 285 mm" + + self._iswap_traversal_height = traversal_height + + @contextmanager + def iswap_minimum_traversal_height(self, traversal_height: float): + orig = self._iswap_traversal_height + self._iswap_traversal_height = traversal_height + try: + yield + except Exception as e: + self._iswap_traversal_height = orig + raise e @property def module_id_length(self): @@ -1367,7 +1391,7 @@ async def setup( await self.initialize_pipetting_channels( x_positions=[self.extended_conf["xw"]], # Tip eject waste X position. y_positions=y_positions, - begin_of_tip_deposit_process=int(self._traversal_height * 10), + begin_of_tip_deposit_process=int(self._channel_traversal_height * 10), end_of_tip_deposit_process=1220, z_position_at_end_of_a_command=3600, tip_pattern=[True] * self.num_channels, @@ -1388,7 +1412,7 @@ async def setup( await self.initialize_iswap() await self.park_iswap( - minimum_traverse_height_at_beginning_of_a_command=int(self._traversal_height * 10) + minimum_traverse_height_at_beginning_of_a_command=int(self._iswap_traversal_height * 10) ) if self.core96_head_installed and not skip_core96_head: @@ -1396,7 +1420,7 @@ async def setup( if not core96_head_initialized: await self.initialize_core_96_head( trash96=self.deck.get_trash_area96(), - z_position_at_the_command_end=self._traversal_height, + z_position_at_the_command_end=self._channel_traversal_height, ) # After setup, STAR will have thrown out anything mounted on the pipetting channels, including @@ -1449,7 +1473,7 @@ async def pick_up_tips( else round(end_tip_pick_up_process * 10) ) minimum_traverse_height_at_beginning_of_a_command = ( - round(self._traversal_height * 10) + round(self._channel_traversal_height * 10) if minimum_traverse_height_at_beginning_of_a_command is None else round(minimum_traverse_height_at_beginning_of_a_command * 10) ) @@ -1526,12 +1550,12 @@ async def drop_tips( ) minimum_traverse_height_at_beginning_of_a_command = ( - round(self._traversal_height * 10) + round(self._channel_traversal_height * 10) if minimum_traverse_height_at_beginning_of_a_command is None else round(minimum_traverse_height_at_beginning_of_a_command * 10) ) z_position_at_end_of_a_command = ( - round(self._traversal_height * 10) + round(self._channel_traversal_height * 10) if z_position_at_end_of_a_command is None else round(z_position_at_end_of_a_command * 10) ) @@ -1869,9 +1893,9 @@ async def aspirate( ratio_liquid_rise_to_tip_deep_in=ratio_liquid_rise_to_tip_deep_in, immersion_depth_2nd_section=[round(id_ * 10) for id_ in immersion_depth_2nd_section], minimum_traverse_height_at_beginning_of_a_command=round( - (minimum_traverse_height_at_beginning_of_a_command or self._traversal_height) * 10 + (minimum_traverse_height_at_beginning_of_a_command or self._channel_traversal_height) * 10 ), - min_z_endpos=round((min_z_endpos or self._traversal_height) * 10), + min_z_endpos=round((min_z_endpos or self._channel_traversal_height) * 10), ) except STARFirmwareError as e: if plr_e := convert_star_firmware_error_to_plr_error(e): @@ -2129,9 +2153,9 @@ async def dispense( ], limit_curve_index=limit_curve_index, minimum_traverse_height_at_beginning_of_a_command=round( - (minimum_traverse_height_at_beginning_of_a_command or self._traversal_height) * 10 + (minimum_traverse_height_at_beginning_of_a_command or self._channel_traversal_height) * 10 ), - min_z_endpos=round((min_z_endpos or self._traversal_height) * 10), + min_z_endpos=round((min_z_endpos or self._channel_traversal_height) * 10), side_touch_off_distance=side_touch_off_distance, ) except STARFirmwareError as e: @@ -2167,9 +2191,11 @@ async def pick_up_tips96( tip_pickup_method=tip_pickup_method, z_deposit_position=round(z_deposit_position * 10), minimum_traverse_height_at_beginning_of_a_command=round( - (minimum_traverse_height_at_beginning_of_a_command or self._traversal_height) * 10 + (minimum_traverse_height_at_beginning_of_a_command or self._channel_traversal_height) * 10 + ), + minimum_height_command_end=round( + (minimum_height_command_end or self._channel_traversal_height) * 10 ), - minimum_height_command_end=round((minimum_height_command_end or self._traversal_height) * 10), ) async def drop_tips96( @@ -2194,9 +2220,11 @@ async def drop_tips96( y_position=round(position.y * 10), z_deposit_position=round(z_deposit_position * 10), minimum_traverse_height_at_beginning_of_a_command=round( - (minimum_traverse_height_at_beginning_of_a_command or self._traversal_height) * 10 + (minimum_traverse_height_at_beginning_of_a_command or self._channel_traversal_height) * 10 + ), + minimum_height_command_end=round( + (minimum_height_command_end or self._channel_traversal_height) * 10 ), - minimum_height_command_end=round((minimum_height_command_end or self._traversal_height) * 10), ) async def aspirate96( @@ -2345,9 +2373,9 @@ async def aspirate96( y_positions=round(position.y * 10), aspiration_type=aspiration_type, minimum_traverse_height_at_beginning_of_a_command=round( - (minimum_traverse_height_at_beginning_of_a_command or self._traversal_height) * 10 + (minimum_traverse_height_at_beginning_of_a_command or self._channel_traversal_height) * 10 ), - minimal_end_height=round((minimal_end_height or self._traversal_height) * 10), + minimal_end_height=round((minimal_end_height or self._channel_traversal_height) * 10), lld_search_height=round(lld_search_height * 10), liquid_surface_at_function_without_lld=round(liquid_height * 10), pull_out_distance_to_take_transport_air_in_function_without_lld=round( @@ -2509,9 +2537,9 @@ async def dispense96( x_direction=0, y_position=round(position.y * 10), minimum_traverse_height_at_beginning_of_a_command=round( - (minimum_traverse_height_at_beginning_of_a_command or self._traversal_height) * 10 + (minimum_traverse_height_at_beginning_of_a_command or self._channel_traversal_height) * 10 ), - minimal_end_height=round((minimal_end_height or self._traversal_height) * 10), + minimal_end_height=round((minimal_end_height or self._channel_traversal_height) * 10), lld_search_height=round(lld_search_height * 10), liquid_surface_at_function_without_lld=round(liquid_height * 10), pull_out_distance_to_take_transport_air_in_function_without_lld=round( @@ -2649,10 +2677,10 @@ async def core_pick_up_resource( plate_width=round(grip_width * 10) - 30, grip_strength=grip_strength, minimum_traverse_height_at_beginning_of_a_command=round( - (minimum_traverse_height_at_beginning_of_a_command or self._traversal_height) * 10 + (minimum_traverse_height_at_beginning_of_a_command or self._channel_traversal_height) * 10 ), minimum_z_position_at_the_command_end=round( - (minimum_z_position_at_the_command_end or self._traversal_height) * 10 + (minimum_z_position_at_the_command_end or self._channel_traversal_height) * 10 ), ) @@ -2689,7 +2717,7 @@ async def core_move_picked_up_resource( z_position=round(center.z * 10), z_speed=round(z_speed * 10), minimum_traverse_height_at_beginning_of_a_command=round( - (minimum_traverse_height_at_beginning_of_a_command or self._traversal_height) * 10 + (minimum_traverse_height_at_beginning_of_a_command or self._channel_traversal_height) * 10 ), ) @@ -2732,10 +2760,10 @@ async def core_release_picked_up_resource( z_speed=500, open_gripper_position=round(grip_width * 10) + 30, minimum_traverse_height_at_beginning_of_a_command=round( - (minimum_traverse_height_at_beginning_of_a_command or self._traversal_height) * 10 + (minimum_traverse_height_at_beginning_of_a_command or self._channel_traversal_height) * 10 ), z_position_at_the_command_end=round( - (z_position_at_the_command_end or self._traversal_height) * 10 + (z_position_at_the_command_end or self._channel_traversal_height) * 10 ), return_tool=return_tool, ) @@ -2786,9 +2814,9 @@ async def pick_up_resource( z -= pickup.pickup_distance_from_top traverse_height_at_beginning = ( - minimum_traverse_height_at_beginning_of_a_command or self._traversal_height + minimum_traverse_height_at_beginning_of_a_command or self._iswap_traversal_height ) - z_position_at_the_command_end = z_position_at_the_command_end or self._traversal_height + z_position_at_the_command_end = z_position_at_the_command_end or self._iswap_traversal_height if open_gripper_position is None: if use_unsafe_hotel: @@ -2851,8 +2879,8 @@ async def pick_up_resource( resource=pickup.resource, pickup_distance_from_top=pickup.pickup_distance_from_top, offset=pickup.offset, - minimum_traverse_height_at_beginning_of_a_command=self._traversal_height, - minimum_z_position_at_the_command_end=self._traversal_height, + minimum_traverse_height_at_beginning_of_a_command=self._channel_traversal_height, + minimum_z_position_at_the_command_end=self._channel_traversal_height, channel_1=channel_1, channel_2=channel_2, grip_strength=core_grip_strength, @@ -2868,7 +2896,7 @@ async def move_picked_up_resource( location=move.location, resource=move.resource, grip_direction=move.gripped_direction, - minimum_traverse_height_at_beginning_of_a_command=self._traversal_height, + minimum_traverse_height_at_beginning_of_a_command=self._iswap_traversal_height, collision_control_level=1, acceleration_index_high_acc=4, acceleration_index_low_acc=1, @@ -2877,7 +2905,7 @@ async def move_picked_up_resource( await self.core_move_picked_up_resource( location=move.location, resource=move.resource, - minimum_traverse_height_at_beginning_of_a_command=self._traversal_height, + minimum_traverse_height_at_beginning_of_a_command=self._channel_traversal_height, acceleration_index=4, ) @@ -2897,9 +2925,9 @@ async def drop_resource( ): if use_arm == "iswap": traversal_height_start = ( - minimum_traverse_height_at_beginning_of_a_command or self._traversal_height + minimum_traverse_height_at_beginning_of_a_command or self._iswap_traversal_height ) - z_position_at_the_command_end = z_position_at_the_command_end or self._traversal_height + z_position_at_the_command_end = z_position_at_the_command_end or self._iswap_traversal_height assert ( drop.resource.get_absolute_rotation().x == 0 and drop.resource.get_absolute_rotation().y == 0 @@ -2996,8 +3024,8 @@ async def drop_resource( resource=drop.resource, offset=drop.offset, pickup_distance_from_top=drop.pickup_distance_from_top, - minimum_traverse_height_at_beginning_of_a_command=self._traversal_height, - z_position_at_the_command_end=self._traversal_height, + minimum_traverse_height_at_beginning_of_a_command=self._channel_traversal_height, + z_position_at_the_command_end=self._channel_traversal_height, # int(previous_location.z + move.resource.get_size_z() / 2) * 10, return_tool=return_core_gripper, ) @@ -4665,7 +4693,7 @@ async def get_core(self, p1: int, p2: int): pb=f"{p2:02}", tp=f"{2350 + self.core_adjustment.z:04}", tz=f"{2250 + self.core_adjustment.z:04}", - th=round(self._traversal_height * 10), + th=round(self._channel_traversal_height * 10), tt="14", ) self._core_parked = False @@ -4691,8 +4719,8 @@ async def put_core(self): yb=f"{1065 + self.core_adjustment.y:04}", tp=f"{2150 + self.core_adjustment.z:04}", tz=f"{2050 + self.core_adjustment.z:04}", - th=round(self._traversal_height * 10), - te=round(self._traversal_height * 10), + th=round(self._channel_traversal_height * 10), + te=round(self._channel_traversal_height * 10), ) self._core_parked = True return command_output