From d09962d94c026df1bbab4ec7bd64104eb23232a7 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Mon, 29 Sep 2025 20:24:11 -0700 Subject: [PATCH 1/3] STARBackend.move_iswap_y_relative safety check --- .../backends/hamilton/STAR_backend.py | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 03f7e9c9ee9..fd536aa3f44 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -3112,7 +3112,7 @@ async def move_channel_y(self, channel: int, y: float): f"(channel {channel - 1} y-position is {round(y, 2)} mm)" ) else: - # STAR machines appear to lose connection to a channel if y > 635 mm + # STAR machines do not allow channels y > 635 mm max_y_pos = 635 if y > max_y_pos: raise ValueError(f"channel {channel} y-target must be <= {max_y_pos} mm (machine limit)") @@ -3125,7 +3125,7 @@ async def move_channel_y(self, channel: int, y: float): f"(channel {channel + 1} y-position is {round(y, 2)} mm)" ) else: - # STAR machines appear to lose connection to a channel if y < 6 mm + # STAR machines do not allow channels y < 6 mm min_y_pos = 6 if y < min_y_pos: raise ValueError(f"channel {channel} y-target must be >= {min_y_pos} mm (machine limit)") @@ -6476,6 +6476,17 @@ async def move_iswap_y_relative(self, step_size: float, allow_splitting: bool = allow_splitting: Allow splitting of the movement into multiple steps. Default False. """ + # check if iswap will hit the first (backmost) channel + # we only need to check for positive step sizes because the iswap is always behind the first channel + if step_size > 0: + y_pos_channel_0 = await self.request_y_pos_channel_n(0) + current_y_pos_iswap = await self.request_iswap_y() + if current_y_pos_iswap + step_size > y_pos_channel_0: + raise ValueError( + f"iSWAP will hit the first (backmost) channel. Current iSWAP Y position: {current_y_pos_iswap} mm, " + f"first channel Y position: {y_pos_channel_0} mm, requested step size: {step_size} mm" + ) + direction = 0 if step_size >= 0 else 1 max_step_size = 99.9 if abs(step_size) > max_step_size: @@ -7213,6 +7224,17 @@ async def request_iswap_position(self) -> Coordinate: z=(resp["zj"] / 10) * (1 if resp["zd"] == 0 else -1), ) + @staticmethod + def _iswap_y_inc_to_mm(y_inc: int) -> float: + mm_per_increment = 0.0025 + return round(y_inc * mm_per_increment, 2) + + async def request_iswap_y(self) -> float: + """Request iSWAP module Y position in mm""" + resp = await self.send_command(module="R0", command="RY", fmt="ry#### (n)") + iswap_y_pos = resp["ry"][1] # 0 = Target position, 2 = Absolute position [increment] + return STARBackend._iswap_y_inc_to_mm(iswap_y_pos) + async def request_iswap_initialization_status(self) -> bool: """Request iSWAP initialization status From b18db26bf1d0a691bf5053a28cadee2d3568b7fe Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Wed, 1 Oct 2025 13:51:47 -0700 Subject: [PATCH 2/3] iswap y position safety checks --- .../backends/hamilton/STAR_backend.py | 27 +++++++++++-------- pylabrobot/plate_reading/standard.py | 2 +- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index fd536aa3f44..280cc9a0404 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -3112,10 +3112,15 @@ async def move_channel_y(self, channel: int, y: float): f"(channel {channel - 1} y-position is {round(y, 2)} mm)" ) else: - # STAR machines do not allow channels y > 635 mm - max_y_pos = 635 + if self.iswap_installed: + max_y_pos = await self.iswap_request_module_y() + limit = "iswap module y-position" + else: + # STAR machines do not allow channels y > 635 mm + max_y_pos = 635 + limit = "machine limit" if y > max_y_pos: - raise ValueError(f"channel {channel} y-target must be <= {max_y_pos} mm (machine limit)") + raise ValueError(f"channel {channel} y-target must be <= {max_y_pos} mm ({limit})") if channel < (self.num_channels - 1): min_y_pos = await self.request_y_pos_channel_n(channel + 1) @@ -6478,10 +6483,10 @@ async def move_iswap_y_relative(self, step_size: float, allow_splitting: bool = # check if iswap will hit the first (backmost) channel # we only need to check for positive step sizes because the iswap is always behind the first channel - if step_size > 0: + if step_size < 0: y_pos_channel_0 = await self.request_y_pos_channel_n(0) - current_y_pos_iswap = await self.request_iswap_y() - if current_y_pos_iswap + step_size > y_pos_channel_0: + current_y_pos_iswap = await self.iswap_request_module_y() + if current_y_pos_iswap + step_size < y_pos_channel_0: raise ValueError( f"iSWAP will hit the first (backmost) channel. Current iSWAP Y position: {current_y_pos_iswap} mm, " f"first channel Y position: {y_pos_channel_0} mm, requested step size: {step_size} mm" @@ -7226,13 +7231,13 @@ async def request_iswap_position(self) -> Coordinate: @staticmethod def _iswap_y_inc_to_mm(y_inc: int) -> float: - mm_per_increment = 0.0025 + mm_per_increment = 0.046302083 return round(y_inc * mm_per_increment, 2) - async def request_iswap_y(self) -> float: - """Request iSWAP module Y position in mm""" - resp = await self.send_command(module="R0", command="RY", fmt="ry#### (n)") - iswap_y_pos = resp["ry"][1] # 0 = Target position, 2 = Absolute position [increment] + async def iswap_request_module_y(self) -> float: + """Request iSWAP module (not gripper) Y position in mm""" + resp = await self.send_command(module="R0", command="RY", fmt="ry##### (n)") + iswap_y_pos = resp["ry"][1] # 0 = FW counter, 1 = HW counter return STARBackend._iswap_y_inc_to_mm(iswap_y_pos) async def request_iswap_initialization_status(self) -> bool: diff --git a/pylabrobot/plate_reading/standard.py b/pylabrobot/plate_reading/standard.py index a85d44a7e16..6fa9c1aee73 100644 --- a/pylabrobot/plate_reading/standard.py +++ b/pylabrobot/plate_reading/standard.py @@ -106,9 +106,9 @@ class AutoExposure: @dataclass class AutoFocus: evaluate_focus: Callable[[Image], float] - timeout: float low: float high: float + timeout: float = 60 tolerance: float = 0.001 # 1 micron From 2e28d4a28dd3291a234b9acd10ea5b5f6755342d Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Wed, 1 Oct 2025 13:59:23 -0700 Subject: [PATCH 3/3] don't update this --- pylabrobot/plate_reading/standard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylabrobot/plate_reading/standard.py b/pylabrobot/plate_reading/standard.py index 6fa9c1aee73..a85d44a7e16 100644 --- a/pylabrobot/plate_reading/standard.py +++ b/pylabrobot/plate_reading/standard.py @@ -106,9 +106,9 @@ class AutoExposure: @dataclass class AutoFocus: evaluate_focus: Callable[[Image], float] + timeout: float low: float high: float - timeout: float = 60 tolerance: float = 0.001 # 1 micron