From 34c8a08317111ba27d0d4584168ba7aa5fa06642 Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Sat, 31 Jan 2026 11:39:22 +0000 Subject: [PATCH 1/5] Enable Variable Channel Spacing in `STARBackend` --- .../backends/hamilton/STAR_backend.py | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index d28597cecab..4ecf24c60a7 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -1453,6 +1453,7 @@ async def setup( async def set_up_pip(): if (not initialized or any(tip_presences)) and not skip_pip: await self.initialize_pip() + self.channel_minimum_y_spacing = 9.0 async def set_up_autoload(): if self.autoload_installed and not skip_autoload: @@ -4132,8 +4133,13 @@ async def core_check_resource_exists_at_location_center( center = location + resource.centers()[0] + offset y_width_to_gripper_bump = resource.get_absolute_size_y() - gripper_y_margin * 2 - assert 9 <= y_width_to_gripper_bump <= round(resource.get_absolute_size_y()), ( - f"width between channels must be between 9 and {resource.get_absolute_size_y()} mm" + assert ( + self.channel_minimum_y_spacing + <= y_width_to_gripper_bump + <= round(resource.get_absolute_size_y()) + ), ( + f"width between channels must be between {self.channel_minimum_y_spacing} and " + f"{resource.get_absolute_size_y()} mm" " (i.e. the minimal distance between channels and the max y size of the resource" ) @@ -9828,8 +9834,12 @@ async def clld_probe_y_position_using_channel( channel_idx_plus_one_y_pos = 6 # Insight: STAR machines appear to lose connection to a channel below y-position=6 mm - max_safe_upper_y_pos = channel_idx_minus_one_y_pos - 9 - max_safe_lower_y_pos = channel_idx_plus_one_y_pos + 9 if channel_idx_plus_one_y_pos != 0 else 6 + max_safe_upper_y_pos = channel_idx_minus_one_y_pos - self.channel_minimum_y_spacing + max_safe_lower_y_pos = ( + channel_idx_plus_one_y_pos + self.channel_minimum_y_spacing + if channel_idx_plus_one_y_pos != 0 + else 6 + ) # Enable safe start and end positions if start_pos_search: @@ -9910,7 +9920,9 @@ async def clld_probe_y_position_using_channel( else: # next channel adjacent_y_pos = await self.request_y_pos_channel_n(channel_idx + 1) - max_safe_y_mov_dist_post_detection = detected_material_y_pos - adjacent_y_pos - 9.0 + max_safe_y_mov_dist_post_detection = ( + detected_material_y_pos - adjacent_y_pos - self.channel_minimum_y_spacing + ) move_target = detected_material_y_pos - min( post_detection_dist, max_safe_y_mov_dist_post_detection ) @@ -9921,7 +9933,9 @@ async def clld_probe_y_position_using_channel( else: # previous channel adjacent_y_pos = await self.request_y_pos_channel_n(channel_idx - 1) - max_safe_y_mov_dist_post_detection = adjacent_y_pos - detected_material_y_pos - 9.0 + max_safe_y_mov_dist_post_detection = ( + adjacent_y_pos - detected_material_y_pos - self.channel_minimum_y_spacing + ) move_target = detected_material_y_pos + min( post_detection_dist, max_safe_y_mov_dist_post_detection ) @@ -10806,7 +10820,7 @@ async def get_channels_y_positions(self) -> Dict[int, float]: elif 5.8 <= y_positions[-1] < 6: y_positions[-1] = 6.0 - min_diff = 9.0 + min_diff = self.channel_minimum_y_spacing for i in range(len(y_positions) - 2, -1, -1): if y_positions[i] - y_positions[i + 1] < min_diff: y_positions[i] = y_positions[i + 1] + min_diff @@ -10845,8 +10859,12 @@ async def position_channels_in_y_direction(self, ys: Dict[int, float], make_spac use_channels = list(ys.keys()) back_channel = min(use_channels) for channel_idx in range(back_channel, 0, -1): - if (channel_locations[channel_idx - 1] - channel_locations[channel_idx]) < 9: - channel_locations[channel_idx - 1] = channel_locations[channel_idx] + 9 + if ( + channel_locations[channel_idx - 1] - channel_locations[channel_idx] + ) < self.channel_minimum_y_spacing: + channel_locations[channel_idx - 1] = ( + channel_locations[channel_idx] + self.channel_minimum_y_spacing + ) # Similarly for the channels to the front of `front_channel`, make sure they are all # spaced >=9mm apart. This time, we iterate from back (closest to `front_channel`) @@ -10854,8 +10872,12 @@ async def position_channels_in_y_direction(self, ys: Dict[int, float], make_spac # one behind it. front_channel = max(use_channels) for channel_idx in range(front_channel, self.num_channels - 1): - if (channel_locations[channel_idx] - channel_locations[channel_idx + 1]) < 9: - channel_locations[channel_idx + 1] = channel_locations[channel_idx] - 9 + if ( + channel_locations[channel_idx] - channel_locations[channel_idx + 1] + ) < self.channel_minimum_y_spacing: + channel_locations[channel_idx + 1] = ( + channel_locations[channel_idx] - self.channel_minimum_y_spacing + ) # Quick checks before movement. if channel_locations[0] > 650: From 5f31cf128ad0f5bbb1459eeb0026be371689ebcb Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Sat, 31 Jan 2026 12:30:59 +0000 Subject: [PATCH 2/5] set default `self.channel_minimum_y_spacing: float = 9.0` --- pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 4ecf24c60a7..6b0df18a350 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -1185,6 +1185,8 @@ def __init__( self._iswap_version: Optional[str] = None # loaded lazily + self.channel_minimum_y_spacing: float = 9.0 + self._default_1d_symbology: Barcode1DSymbology = "Code 128 (Subset B and C)" self._setup_done = False @@ -1453,7 +1455,7 @@ async def setup( async def set_up_pip(): if (not initialized or any(tip_presences)) and not skip_pip: await self.initialize_pip() - self.channel_minimum_y_spacing = 9.0 + self.channel_minimum_y_spacing = 9.0 # TODO: identify from machine directly to override default async def set_up_autoload(): if self.autoload_installed and not skip_autoload: From 2b7574fe36305f8863da1d789b6765f6f4dd2c3f Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Sat, 31 Jan 2026 12:41:33 +0000 Subject: [PATCH 3/5] `make format` --- pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 6b0df18a350..70ba86c13b5 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -1455,7 +1455,9 @@ async def setup( async def set_up_pip(): if (not initialized or any(tip_presences)) and not skip_pip: await self.initialize_pip() - self.channel_minimum_y_spacing = 9.0 # TODO: identify from machine directly to override default + self.channel_minimum_y_spacing = ( + 9.0 # TODO: identify from machine directly to override default + ) async def set_up_autoload(): if self.autoload_installed and not skip_autoload: From 092c94e88f97d57f0db26e944b1ce74c05a4887a Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Sat, 31 Jan 2026 12:49:07 +0000 Subject: [PATCH 4/5] correct comment --- .../liquid_handling/backends/hamilton/STAR_backend.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 70ba86c13b5..f887e8a711b 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -10871,9 +10871,9 @@ async def position_channels_in_y_direction(self, ys: Dict[int, float], make_spac ) # Similarly for the channels to the front of `front_channel`, make sure they are all - # spaced >=9mm apart. This time, we iterate from back (closest to `front_channel`) - # to the front (lh.backend.num_channels - 1), and put each channel >=9mm before the - # one behind it. + # spaced >= channel_minimum_y_spacing (usually 9mm) apart. This time, we iterate from + # back (closest to `front_channel`) to the front (lh.backend.num_channels - 1), and + # put each channel >= channel_minimum_y_spacing before the one behind it. front_channel = max(use_channels) for channel_idx in range(front_channel, self.num_channels - 1): if ( From 12eedddcb19f86403f8b27fc34e31ebcd7e3280d Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Sat, 31 Jan 2026 12:59:00 +0000 Subject: [PATCH 5/5] update naming to indicate private status --- .../backends/hamilton/STAR_backend.py | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index f887e8a711b..fe3a3ecbf07 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -1176,6 +1176,7 @@ def __init__( self._iswap_parked: Optional[bool] = None self._num_channels: Optional[int] = None + self._channel_minimum_y_spacing: float = 9.0 self._core_parked: Optional[bool] = None self._extended_conf: Optional[dict] = None self._channel_traversal_height: float = 245.0 @@ -1185,8 +1186,6 @@ def __init__( self._iswap_version: Optional[str] = None # loaded lazily - self.channel_minimum_y_spacing: float = 9.0 - self._default_1d_symbology: Barcode1DSymbology = "Code 128 (Subset B and C)" self._setup_done = False @@ -1455,7 +1454,7 @@ async def setup( async def set_up_pip(): if (not initialized or any(tip_presences)) and not skip_pip: await self.initialize_pip() - self.channel_minimum_y_spacing = ( + self._channel_minimum_y_spacing = ( 9.0 # TODO: identify from machine directly to override default ) @@ -4138,11 +4137,11 @@ async def core_check_resource_exists_at_location_center( center = location + resource.centers()[0] + offset y_width_to_gripper_bump = resource.get_absolute_size_y() - gripper_y_margin * 2 assert ( - self.channel_minimum_y_spacing + self._channel_minimum_y_spacing <= y_width_to_gripper_bump <= round(resource.get_absolute_size_y()) ), ( - f"width between channels must be between {self.channel_minimum_y_spacing} and " + f"width between channels must be between {self._channel_minimum_y_spacing} and " f"{resource.get_absolute_size_y()} mm" " (i.e. the minimal distance between channels and the max y size of the resource" ) @@ -9838,9 +9837,9 @@ async def clld_probe_y_position_using_channel( channel_idx_plus_one_y_pos = 6 # Insight: STAR machines appear to lose connection to a channel below y-position=6 mm - max_safe_upper_y_pos = channel_idx_minus_one_y_pos - self.channel_minimum_y_spacing + max_safe_upper_y_pos = channel_idx_minus_one_y_pos - self._channel_minimum_y_spacing max_safe_lower_y_pos = ( - channel_idx_plus_one_y_pos + self.channel_minimum_y_spacing + channel_idx_plus_one_y_pos + self._channel_minimum_y_spacing if channel_idx_plus_one_y_pos != 0 else 6 ) @@ -9925,7 +9924,7 @@ async def clld_probe_y_position_using_channel( adjacent_y_pos = await self.request_y_pos_channel_n(channel_idx + 1) max_safe_y_mov_dist_post_detection = ( - detected_material_y_pos - adjacent_y_pos - self.channel_minimum_y_spacing + detected_material_y_pos - adjacent_y_pos - self._channel_minimum_y_spacing ) move_target = detected_material_y_pos - min( post_detection_dist, max_safe_y_mov_dist_post_detection @@ -9938,7 +9937,7 @@ async def clld_probe_y_position_using_channel( adjacent_y_pos = await self.request_y_pos_channel_n(channel_idx - 1) max_safe_y_mov_dist_post_detection = ( - adjacent_y_pos - detected_material_y_pos - self.channel_minimum_y_spacing + adjacent_y_pos - detected_material_y_pos - self._channel_minimum_y_spacing ) move_target = detected_material_y_pos + min( post_detection_dist, max_safe_y_mov_dist_post_detection @@ -10824,7 +10823,7 @@ async def get_channels_y_positions(self) -> Dict[int, float]: elif 5.8 <= y_positions[-1] < 6: y_positions[-1] = 6.0 - min_diff = self.channel_minimum_y_spacing + min_diff = self._channel_minimum_y_spacing for i in range(len(y_positions) - 2, -1, -1): if y_positions[i] - y_positions[i + 1] < min_diff: y_positions[i] = y_positions[i + 1] + min_diff @@ -10865,9 +10864,9 @@ async def position_channels_in_y_direction(self, ys: Dict[int, float], make_spac for channel_idx in range(back_channel, 0, -1): if ( channel_locations[channel_idx - 1] - channel_locations[channel_idx] - ) < self.channel_minimum_y_spacing: + ) < self._channel_minimum_y_spacing: channel_locations[channel_idx - 1] = ( - channel_locations[channel_idx] + self.channel_minimum_y_spacing + channel_locations[channel_idx] + self._channel_minimum_y_spacing ) # Similarly for the channels to the front of `front_channel`, make sure they are all @@ -10878,9 +10877,9 @@ async def position_channels_in_y_direction(self, ys: Dict[int, float], make_spac for channel_idx in range(front_channel, self.num_channels - 1): if ( channel_locations[channel_idx] - channel_locations[channel_idx + 1] - ) < self.channel_minimum_y_spacing: + ) < self._channel_minimum_y_spacing: channel_locations[channel_idx + 1] = ( - channel_locations[channel_idx] - self.channel_minimum_y_spacing + channel_locations[channel_idx] - self._channel_minimum_y_spacing ) # Quick checks before movement.