From b596fe9d09dc4d29a74e3c4719f07357a4d214c3 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Tue, 21 Oct 2025 21:48:49 -0700 Subject: [PATCH 1/2] hamilton star: support both core gripper types --- .../backends/hamilton/STAR_backend.py | 28 ++++- .../backends/hamilton/STAR_tests.py | 2 +- .../resources/hamilton/hamilton_deck_tests.py | 27 ++--- .../resources/hamilton/hamilton_decks.py | 107 ++++++++++++++++-- 4 files changed, 134 insertions(+), 30 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 6990f56d2df..384ca6900dc 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -76,6 +76,7 @@ from pylabrobot.resources.hamilton.hamilton_decks import ( STAR_SIZE_X, STARLET_SIZE_X, + HamiltonCoreGrippers, ) from pylabrobot.resources.liquid import Liquid from pylabrobot.resources.rotation import Rotation @@ -5161,6 +5162,27 @@ async def dispense_pip( # -------------- 3.5.5 CoRe gripper commands -------------- + def _get_core_front_back(self): + core_grippers = self.deck.get_resource("core_grippers") + assert isinstance(core_grippers, HamiltonCoreGrippers), "core_grippers must be CoReGrippers" + back_channel_y_center = round( + ( + core_grippers.get_location_wrt(self.deck).y + + core_grippers.back_channel_y_center + + self.core_adjustment.y + ) + * 10 + ) + front_channel_y_center = round( + ( + core_grippers.get_location_wrt(self.deck).y + + core_grippers.front_channel_y_center + + self.core_adjustment.y + ) + * 10 + ) + return back_channel_y_center, front_channel_y_center + @need_iswap_parked async def get_core(self, p1: int, p2: int): """Get CoRe gripper tool from wasteblock mount.""" @@ -5182,8 +5204,7 @@ async def get_core(self, p1: int, p2: int): raise ValueError(f"Deck size {deck_size} not supported") channel_x_coord = round(xs + self.core_adjustment.x * 10) - back_channel_y_center = round(1250 + self.core_adjustment.y * 10) - front_channel_y_center = round(1070 + self.core_adjustment.y * 10) + back_channel_y_center, front_channel_y_center = self._get_core_front_back() begin_z_coord = round(2350 + self.core_adjustment.z * 10) end_z_coord = round(2250 + self.core_adjustment.z * 10) @@ -5219,8 +5240,7 @@ async def put_core(self): raise ValueError(f"Deck size {deck_size} not supported") channel_x_coord = round(xs + self.core_adjustment.x * 10) - back_channel_y_center = round(1240 + self.core_adjustment.y * 10) - front_channel_y_center = round(1065 + self.core_adjustment.y * 10) + back_channel_y_center, front_channel_y_center = self._get_core_front_back() begin_z_coord = round(2150 + self.core_adjustment.z * 10) end_z_coord = round(2050 + self.core_adjustment.z * 10) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py index a2f3b5ec7a8..45d03e0163f 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py @@ -936,7 +936,7 @@ async def test_move_core(self): "C0ZRid0003xs03479xd0yj2102zj1876zi000zy0500yo0885th2800te2800" ), _any_write_and_read_command_call( - "C0ZSid0004xs07975xd0ya1240yb1065tp2150tz2050th2800te2800" + "C0ZSid0004xs07975xd0ya1250yb1070tp2150tz2050th2800te2800" ), ] ) diff --git a/pylabrobot/resources/hamilton/hamilton_deck_tests.py b/pylabrobot/resources/hamilton/hamilton_deck_tests.py index cdba4bab4c5..9c7a314f41a 100644 --- a/pylabrobot/resources/hamilton/hamilton_deck_tests.py +++ b/pylabrobot/resources/hamilton/hamilton_deck_tests.py @@ -44,28 +44,29 @@ def test_summary(self): deck.summary(), textwrap.dedent( """ - Rail Resource Type Coordinates (mm) - ================================================================================= - (-6) ├── trash_core96 Trash (-58.200, 106.000, 216.400) + Rail Resource Type Coordinates (mm) + ======================================================================================= + (-6) ├── trash_core96 Trash (-58.200, 106.000, 216.400) │ - (1) ├── tip_carrier TipCarrier (100.000, 063.000, 100.000) - │ ├── tip_rack_01 TipRack (106.200, 073.000, 214.950) - │ ├── tip_rack_02 TipRack (106.200, 169.000, 214.950) + (1) ├── tip_carrier TipCarrier (100.000, 063.000, 100.000) + │ ├── tip_rack_01 TipRack (106.200, 073.000, 214.950) + │ ├── tip_rack_02 TipRack (106.200, 169.000, 214.950) │ ├── - │ ├── tip_rack_04 TipRack (106.200, 361.000, 214.950) + │ ├── tip_rack_04 TipRack (106.200, 361.000, 214.950) │ ├── │ - (21) ├── plate carrier PlateCarrier (550.000, 063.000, 100.000) - │ ├── aspiration plate Plate (554.000, 071.500, 183.120) + (21) ├── plate carrier PlateCarrier (550.000, 063.000, 100.000) + │ ├── aspiration plate Plate (554.000, 071.500, 183.120) │ ├── - │ ├── dispense plate Plate (554.000, 263.500, 183.120) + │ ├── dispense plate Plate (554.000, 263.500, 183.120) │ ├── │ ├── │ - (31) ├── waste_block Resource (775.000, 115.000, 100.000) - │ ├── teaching_tip_rack TipRack (780.900, 461.100, 100.000) + (31) ├── waste_block Resource (775.000, 115.000, 100.000) + │ ├── teaching_tip_rack TipRack (780.900, 461.100, 100.000) + │ ├── core_grippers HamiltonCoreGrippers (797.500, 125.000, 205.000) │ - (32) ├── trash Trash (800.000, 190.600, 137.100) + (32) ├── trash Trash (800.000, 190.600, 137.100) """[1:] ), ) diff --git a/pylabrobot/resources/hamilton/hamilton_decks.py b/pylabrobot/resources/hamilton/hamilton_decks.py index a33ffffba7d..d4e7d33a32f 100644 --- a/pylabrobot/resources/hamilton/hamilton_decks.py +++ b/pylabrobot/resources/hamilton/hamilton_decks.py @@ -2,7 +2,7 @@ import logging from abc import ABCMeta, abstractmethod -from typing import Optional, cast +from typing import Literal, Optional, cast from pylabrobot.resources.carrier import ResourceHolder from pylabrobot.resources.coordinate import Coordinate @@ -167,7 +167,13 @@ def assign_child_resource( else: raise ValueError("Either rails or location must be provided.") - if not ignore_collision: + def should_check_collision(res: Resource) -> bool: + """Determine if collision detection should be performed for this resource.""" + if isinstance(res, HamiltonCoreGrippers): + return False + return True + + if not ignore_collision and should_check_collision(resource): if resource_location is not None: # collision detection if ( resource_location.x + resource.get_absolute_size_x() @@ -262,7 +268,7 @@ def find_longest_type_name(resource: Resource): name_column_length = max( max_name_length + 4, 30 ) # 4 per depth (by find_longest_child), 4 extra - type_column_length = max_type_length + 3 - 4 + type_column_length = max_type_length + 1 location_column_length = 30 # Print header @@ -349,6 +355,75 @@ def print_tree(resource: Resource, depth=0): return summary_ +class HamiltonCoreGrippers(Resource): + def __init__( + self, + name: str, + back_channel_y_center: float, + front_channel_y_center: float, + size_x: float, + size_y: float, + size_z: float, + model, + rotation=None, + category="core_grippers", + barcode=None, + ): + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + rotation=rotation, + category=category, + model=model, + barcode=barcode, + ) + self.back_channel_y_center = back_channel_y_center + self.front_channel_y_center = front_channel_y_center + + def serialize(self): + return { + **super().serialize(), + "back_channel_y_center": self.back_channel_y_center, + "front_channel_y_center": self.front_channel_y_center, + } + + +def hamilton_core_gripper_1000ul_at_waste() -> HamiltonCoreGrippers: + # inner hole diameter is 8.6mm + # distance from base of rack to outer base of containers: -7mm + # left outer edge of rack is 22.5mm + # front outer edge of rack is 9.5mm + + return HamiltonCoreGrippers( + name="core_grippers", + size_x=45, # from venus + size_y=45, # from venus + size_z=24, # from venus + back_channel_y_center=0, + front_channel_y_center=-26, + model=hamilton_core_gripper_1000ul_at_waste.__name__, + ) + + +def hamilton_core_gripper_1000ul_5ml_on_waste() -> HamiltonCoreGrippers: + # distance from base of rack to outer base of containers: 0mm + # inner hole diameter is 8.6mm + # left outer edge of rack is 19.5mm + # front outer edge of rack is 39.5mm + + return HamiltonCoreGrippers( + name="core_grippers", + size_x=39, # from venus + size_y=61, # from venus + size_z=24, # from venus + back_channel_y_center=0, + front_channel_y_center=-18, + model=hamilton_core_gripper_1000ul_5ml_on_waste.__name__, + ) + + class HamiltonSTARDeck(HamiltonDeck): """Base class for a Hamilton STAR(let) deck.""" @@ -364,8 +439,7 @@ def __init__( with_trash: bool = True, with_trash96: bool = True, with_teaching_rack: bool = True, - no_trash: Optional[bool] = None, - no_teaching_rack: Optional[bool] = None, + core_grippers: Optional[Literal["1000uL", "1000uL-5mL"]] = "1000uL-5mL", ) -> None: """Create a new STAR(let) deck of the given size.""" @@ -379,13 +453,6 @@ def __init__( origin=origin, ) - if no_trash is not None: - raise NotImplementedError("no_trash is deprecated. Use with_trash=False instead.") - if no_teaching_rack is not None: - raise NotImplementedError( - "no_teaching_rack is deprecated. Use with_teaching_rack=False instead." - ) - # assign trash area if with_trash: trash_x = ( @@ -436,10 +503,26 @@ def __init__( location=Coordinate(x=self.rails_to_location(self.num_rails - 1).x, y=115.0, z=100), ) + if core_grippers == "1000uL": # "at waste" + x: float = 1338 if num_rails == STAR_NUM_RAILS else 798 + waste_block.assign_child_resource( + hamilton_core_gripper_1000ul_at_waste(), + location=Coordinate(x=x, y=105.550, z=205) - waste_block.location, + # ignore_collision=True, + ) + elif core_grippers == "1000uL-5mL": # "on waste" + x = 1337.5 if num_rails == STAR_NUM_RAILS else 797.5 + waste_block.assign_child_resource( + hamilton_core_gripper_1000ul_5ml_on_waste(), + location=Coordinate(x=x, y=125, z=205) - waste_block.location, + # ignore_collision=True, + ) + def serialize(self) -> dict: return { **super().serialize(), "with_teaching_rack": False, # data encoded as child. (not very pretty to have this key though...) + "core_grippers": None, # data encoded as child. (not very pretty to have this key though...) } def rails_to_location(self, rails: int) -> Coordinate: From 14648b0eb410a1dae10eb59cb4b80cb2933b5eaa Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Tue, 21 Oct 2025 21:56:48 -0700 Subject: [PATCH 2/2] add --- pylabrobot/resources/hamilton/hamilton_decks.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/pylabrobot/resources/hamilton/hamilton_decks.py b/pylabrobot/resources/hamilton/hamilton_decks.py index d4e7d33a32f..291b72167ed 100644 --- a/pylabrobot/resources/hamilton/hamilton_decks.py +++ b/pylabrobot/resources/hamilton/hamilton_decks.py @@ -439,7 +439,9 @@ def __init__( with_trash: bool = True, with_trash96: bool = True, with_teaching_rack: bool = True, - core_grippers: Optional[Literal["1000uL", "1000uL-5mL"]] = "1000uL-5mL", + core_grippers: Optional[ + Literal["1000uL-at-waste", "1000uL-5mL-on-waste"] + ] = "1000uL-5mL-on-waste", ) -> None: """Create a new STAR(let) deck of the given size.""" @@ -503,14 +505,14 @@ def __init__( location=Coordinate(x=self.rails_to_location(self.num_rails - 1).x, y=115.0, z=100), ) - if core_grippers == "1000uL": # "at waste" + if core_grippers == "1000uL-at-waste": # "at waste" x: float = 1338 if num_rails == STAR_NUM_RAILS else 798 waste_block.assign_child_resource( hamilton_core_gripper_1000ul_at_waste(), location=Coordinate(x=x, y=105.550, z=205) - waste_block.location, # ignore_collision=True, ) - elif core_grippers == "1000uL-5mL": # "on waste" + elif core_grippers == "1000uL-5mL-on-waste": # "on waste" x = 1337.5 if num_rails == STAR_NUM_RAILS else 797.5 waste_block.assign_child_resource( hamilton_core_gripper_1000ul_5ml_on_waste(), @@ -553,6 +555,9 @@ def STARLetDeck( with_trash: bool = True, with_trash96: bool = True, with_teaching_rack: bool = True, + core_grippers: Optional[ + Literal["1000uL-at-waste", "1000uL-5mL-on-waste"] + ] = "1000uL-5mL-on-waste", ) -> HamiltonSTARDeck: """Create a new STARLet deck. @@ -568,6 +573,7 @@ def STARLetDeck( with_trash=with_trash, with_trash96=with_trash96, with_teaching_rack=with_teaching_rack, + core_grippers=core_grippers, ) @@ -576,6 +582,9 @@ def STARDeck( with_trash: bool = True, with_trash96: bool = True, with_teaching_rack: bool = True, + core_grippers: Optional[ + Literal["1000uL-at-waste", "1000uL-5mL-on-waste"] + ] = "1000uL-5mL-on-waste", ) -> HamiltonSTARDeck: """Create a new STAR deck. @@ -591,4 +600,5 @@ def STARDeck( with_trash=with_trash, with_trash96=with_trash96, with_teaching_rack=with_teaching_rack, + core_grippers=core_grippers, )