diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index d28597cecab..d7453f4b14c 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -1573,28 +1573,24 @@ async def pick_up_tips( ttti = await self.get_or_assign_tip_type_index(tips.pop()) max_z = max(op.resource.get_location_wrt(self.deck).z + op.offset.z for op in ops) - max_total_tip_length = max(op.tip.total_tip_length for op in ops) - max_tip_length = max((op.tip.total_tip_length - op.tip.fitting_depth) for op in ops) - - # not sure why this is necessary, but it is according to log files and experiments - if self._get_hamilton_tip([op.resource for op in ops]).tip_size == TipSize.LOW_VOLUME: - max_tip_length += 2 - elif self._get_hamilton_tip([op.resource for op in ops]).tip_size != TipSize.STANDARD_VOLUME: - max_tip_length -= 2 + tips = [op.tip for op in ops] + assert all(isinstance(tip, HamiltonTip) for tip in tips), "All tips must be HamiltonTip." + collar_heights = set((tip.collar_height) for tip in tips) + if len(collar_heights) > 1: + raise ValueError("Cannot mix tips with different collar heights.") + collar_height = collar_heights.pop() tip = ops[0].tip if not isinstance(tip, HamiltonTip): raise TypeError("Tip type must be HamiltonTip.") begin_tip_pick_up_process = ( - round((max_z + max_total_tip_length) * 10) + round((max_z + collar_height) * 10) if begin_tip_pick_up_process is None else int(begin_tip_pick_up_process * 10) ) end_tip_pick_up_process = ( - round((max_z + max_tip_length) * 10) - if end_tip_pick_up_process is None - else round(end_tip_pick_up_process * 10) + round(max_z * 10) if end_tip_pick_up_process is None else round(end_tip_pick_up_process * 10) ) minimum_traverse_height_at_beginning_of_a_command = ( round(self._channel_traversal_height * 10) @@ -1662,15 +1658,21 @@ async def drop_tips( else round(end_tip_deposit_process * 10) ) else: - max_total_tip_length = max(op.tip.total_tip_length for op in ops) - max_tip_length = max((op.tip.total_tip_length - op.tip.fitting_depth) for op in ops) + tips = [op.tip for op in ops] + assert all(isinstance(tip, HamiltonTip) for tip in tips), "All tips must be HamiltonTip." + collar_heights = set(tip.collar_height for tip in tips) + if len(collar_heights) > 1: + raise ValueError("Cannot mix tips with different collar heights.") + collar_height = collar_heights.pop() + fitting_depth = tips[0].fitting_depth + begin_tip_deposit_process = ( - round((max_z + max_total_tip_length) * 10) + round((max_z + collar_height) * 10) if begin_tip_deposit_process is None else round(begin_tip_deposit_process * 10) ) end_tip_deposit_process = ( - round((max_z + max_tip_length) * 10) + round((max_z + collar_height - fitting_depth) * 10) if end_tip_deposit_process is None else round(end_tip_deposit_process * 10) ) @@ -2933,26 +2935,11 @@ async def pick_up_tips96( ttti = await self.get_or_assign_tip_type_index(prototypical_tip) - tip_length = prototypical_tip.total_tip_length - fitting_depth = prototypical_tip.fitting_depth - tip_engage_height_from_tipspot = tip_length - fitting_depth - - # Adjust tip engage height based on tip size - if prototypical_tip.tip_size == TipSize.LOW_VOLUME: - tip_engage_height_from_tipspot += 2 - elif prototypical_tip.tip_size != TipSize.STANDARD_VOLUME: - tip_engage_height_from_tipspot -= 2 - - # Compute pickup Z + # Compute pickup position alignment_tipspot = pickup.resource.get_item(experimental_alignment_tipspot_identifier) - tip_spot_z = alignment_tipspot.get_location_wrt(self.deck).z + pickup.offset.z - z_pickup_position = tip_spot_z + tip_engage_height_from_tipspot - - # Compute full position (used for x/y) pickup_position = ( alignment_tipspot.get_location_wrt(self.deck) + alignment_tipspot.center() + pickup.offset ) - pickup_position.z = round(z_pickup_position, 2) self._check_96_position_legal(pickup_position, skip_z=True) @@ -3000,12 +2987,6 @@ async def drop_tips96( if isinstance(drop.resource, TipRack): tip_spot_a1 = drop.resource.get_item(experimental_alignment_tipspot_identifier) position = tip_spot_a1.get_location_wrt(self.deck) + tip_spot_a1.center() + drop.offset - tip_rack = tip_spot_a1.parent - assert tip_rack is not None - position.z = tip_rack.get_location_wrt(self.deck).z + 1.45 - # This should be the case for all normal hamilton tip carriers + racks - # In the future, we might want to make this more flexible - assert abs(position.z - 216.4) < 1e-6, f"z position must be 216.4, got {position.z}" else: position = self._position_96_head_in_resource(drop.resource) + drop.offset diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py index 89ade38b7f1..bcf1d6e05c0 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py @@ -1020,7 +1020,7 @@ async def test_portrait_tip_rack_handling(self): tip_car = TIP_CAR_288_C00(name="tip carrier") tip_car[0] = tr = hamilton_96_tiprack_1000uL(name="tips_01").rotated(z=90) assert tr.rotation.z == 90 - assert tr.location == Coordinate(82.6, 0, 0) + assert tr.location == Coordinate(82.6, 0, -6.1) deck.assign_child_resource(tip_car, rails=2) await lh.setup() diff --git a/pylabrobot/liquid_handling/backends/hamilton/nimbus_backend.py b/pylabrobot/liquid_handling/backends/hamilton/nimbus_backend.py index c6f4d0a941d..0269f12a2b4 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/nimbus_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/nimbus_backend.py @@ -44,6 +44,7 @@ from pylabrobot.resources.container import Container from pylabrobot.resources.hamilton import HamiltonTip, TipSize from pylabrobot.resources.hamilton.nimbus_decks import NimbusDeck +from pylabrobot.resources.tip_rack import EmbeddedTipRack from pylabrobot.resources.trash import Trash logger = logging.getLogger(__name__) @@ -93,7 +94,7 @@ def _get_tip_type_from_tip(tip: Tip) -> int: if tip.tip_size == TipSize.LOW_VOLUME: # 10ul tip return NimbusTipType.LOW_VOLUME_10UL_FILTER if tip.has_filter else NimbusTipType.LOW_VOLUME_10UL - if tip.tip_size == TipSize.STANDARD_VOLUME and tip.maximal_volume < 60: # 50ul tip + if tip.tip_size == TipSize.STANDARD_VOLUME and tip.maximal_volume < 100: # 50ul tip return NimbusTipType.TIP_50UL_FILTER if tip.has_filter else NimbusTipType.TIP_50UL if tip.tip_size == TipSize.STANDARD_VOLUME: # 300ul tip @@ -1385,54 +1386,6 @@ def _compute_ops_xy_locations( return x_positions_full, y_positions_full - def _compute_tip_handling_parameters( - self, - ops: Sequence[Union[Pickup, Drop]], - use_channels: List[int], - use_fixed_offset: bool = False, - fixed_offset_mm: float = 10.0, - ): - """Calculate Z positions for tip pickup/drop operations. - - Pickup (use_fixed_offset=False): Z based on tip length - z_start = max_z + max_total_tip_length, z_stop = max_z + max_tip_length - Drop (use_fixed_offset=True): Z based on fixed offset (matches VantageBackend default) - z_start = max_z + fixed_offset_mm (default 10.0mm), z_stop = max_z - - Returns: (begin_position, end_position) in 0.01mm units - """ - if not isinstance(self.deck, NimbusDeck): - raise RuntimeError("Deck must be a NimbusDeck for coordinate conversion") - - z_positions_mm: List[float] = [] - for op in ops: - abs_location = op.resource.get_location_wrt(self.deck) + op.offset - hamilton_coord = self.deck.to_hamilton_coordinate(abs_location) - z_positions_mm.append(hamilton_coord.z) - - max_z_hamilton = max(z_positions_mm) # Highest resource Z in Hamilton coordinates - - if use_fixed_offset: - # For drop operations: use fixed offsets relative to resource surface - begin_position_mm = max_z_hamilton + fixed_offset_mm - end_position_mm = max_z_hamilton - else: - # For pickup operations: use tip length - # Similar to STAR backend: z_start = max_z + max_total_tip_length, z_stop = max_z + max_tip_length - max_total_tip_length = max(op.tip.total_tip_length for op in ops) - max_tip_length = max((op.tip.total_tip_length - op.tip.fitting_depth) for op in ops) - begin_position_mm = max_z_hamilton + max_total_tip_length - end_position_mm = max_z_hamilton + max_tip_length - - # Convert to 0.01mm units - begin_position = [round(begin_position_mm * 100)] * len(ops) - end_position = [round(end_position_mm * 100)] * len(ops) - - begin_position_full = self._fill_by_channels(begin_position, use_channels, default=0) - end_position_full = self._fill_by_channels(end_position, use_channels, default=0) - - return begin_position_full, end_position_full - async def pick_up_tips( self, ops: List[Pickup], @@ -1483,8 +1436,36 @@ async def pick_up_tips( logger.warning(f"Could not check tip presence before pickup: {e}") x_positions_full, y_positions_full = self._compute_ops_xy_locations(ops, use_channels) - begin_tip_pick_up_process, end_tip_pick_up_process = self._compute_tip_handling_parameters( - ops, use_channels + + # Compute Z positions for pickup + z_positions_mm: List[float] = [] + for op in ops: + abs_location = op.resource.get_location_wrt(self.deck) + op.offset + hamilton_coord = self.deck.to_hamilton_coordinate(abs_location) + z_positions_mm.append(hamilton_coord.z) + + max_z_hamilton = max(z_positions_mm) + + tip_rack = ops[0].resource.parent + if not isinstance(tip_rack, EmbeddedTipRack): + raise TypeError("Tip rack must be an EmbeddedTipRack") + + collar_height = ops[0].tip.collar_height + if any(op.tip.collar_height != collar_height for op in ops): + raise ValueError("All tips must have the same collar_height") + + fitting_depth = ops[0].tip.fitting_depth + if any(op.tip.fitting_depth != fitting_depth for op in ops): + raise ValueError("All tips must have the same fitting_depth") + + begin_position_mm = max_z_hamilton + collar_height + end_position_mm = begin_position_mm - fitting_depth + + begin_tip_pick_up_process = self._fill_by_channels( + [round(begin_position_mm * 100)] * len(ops), use_channels, default=0 + ) + end_tip_pick_up_process = self._fill_by_channels( + [round(end_position_mm * 100)] * len(ops), use_channels, default=0 ) # Build tip pattern array (True for active channels, False for inactive) @@ -1618,9 +1599,35 @@ async def drop_tips( # Compute x and y positions for regular resources x_positions_full, y_positions_full = self._compute_ops_xy_locations(ops, use_channels) - # Compute Z positions using fixed offsets (not tip length) for drop operations - begin_tip_deposit_process, end_tip_deposit_process = self._compute_tip_handling_parameters( - ops, use_channels, use_fixed_offset=True + # Compute Z positions for drop operations + z_positions_mm: List[float] = [] + for op in ops: + abs_location = op.resource.get_location_wrt(self.deck) + op.offset + hamilton_coord = self.deck.to_hamilton_coordinate(abs_location) + z_positions_mm.append(hamilton_coord.z) + + max_z_hamilton = max(z_positions_mm) + + tip_rack = ops[0].resource.parent + if not isinstance(tip_rack, EmbeddedTipRack): + raise TypeError("Tip rack must be an EmbeddedTipRack") + + total_tip_length = ops[0].tip.total_tip_length + if any(op.tip.total_tip_length != total_tip_length for op in ops): + raise ValueError("All tips must have the same total_tip_length") + + collar_height = ops[0].tip.collar_height + if any(op.tip.collar_height != collar_height for op in ops): + raise ValueError("All tips must have the same collar_height") + + end_position_mm = max_z_hamilton - total_tip_length + collar_height + begin_position_mm = end_position_mm + 10.0 # I think 10mm might be the collar height + + begin_tip_deposit_process = self._fill_by_channels( + [round(begin_position_mm * 100)] * len(ops), use_channels, default=0 + ) + end_tip_deposit_process = self._fill_by_channels( + [round(end_position_mm * 100)] * len(ops), use_channels, default=0 ) # Compute final Z positions. Use the traverse height if not provided. Fill to num_channels. diff --git a/pylabrobot/liquid_handling/backends/hamilton/nimbus_backend_tests.py b/pylabrobot/liquid_handling/backends/hamilton/nimbus_backend_tests.py index 75c71692f91..750554b2281 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/nimbus_backend_tests.py +++ b/pylabrobot/liquid_handling/backends/hamilton/nimbus_backend_tests.py @@ -708,6 +708,7 @@ async def asyncSetUp(self): maximal_volume=300.0, tip_size=TipSize.STANDARD_VOLUME, pickup_method=TipPickupMethod.OUT_OF_RACK, + collar_height=8.0, ) def _get_commands(self, cmd_type): @@ -1080,7 +1081,10 @@ async def test_10uL_tips(self): from pylabrobot.resources.hamilton.tip_racks import hamilton_96_tiprack_10uL tip_rack = hamilton_96_tiprack_10uL("tips") - self.deck.assign_child_resource(tip_rack, rails=1) + rails_loc = self.deck.rails_to_location(1) + self.deck.assign_child_resource( + tip_rack, location=Coordinate(x=rails_loc.x, y=rails_loc.y, z=-6.1) + ) tip_spot = tip_rack.get_item("A1") tip = tip_spot.get_tip() @@ -1107,7 +1111,10 @@ async def test_50uL_tips(self): from pylabrobot.resources.hamilton.tip_racks import hamilton_96_tiprack_50uL tip_rack = hamilton_96_tiprack_50uL("tips") - self.deck.assign_child_resource(tip_rack, rails=1) + rails_loc = self.deck.rails_to_location(1) + self.deck.assign_child_resource( + tip_rack, location=Coordinate(x=rails_loc.x, y=rails_loc.y, z=-6.1) + ) tip_spot = tip_rack.get_item("A1") tip = tip_spot.get_tip() @@ -1134,7 +1141,10 @@ async def test_300uL_tips(self): from pylabrobot.resources.hamilton.tip_racks import hamilton_96_tiprack_300uL tip_rack = hamilton_96_tiprack_300uL("tips") - self.deck.assign_child_resource(tip_rack, rails=1) + rails_loc = self.deck.rails_to_location(1) + self.deck.assign_child_resource( + tip_rack, location=Coordinate(x=rails_loc.x, y=rails_loc.y, z=-6.1) + ) tip_spot = tip_rack.get_item("A1") tip = tip_spot.get_tip() @@ -1161,7 +1171,10 @@ async def test_1000uL_tips(self): from pylabrobot.resources.hamilton.tip_racks import hamilton_96_tiprack_1000uL tip_rack = hamilton_96_tiprack_1000uL("tips") - self.deck.assign_child_resource(tip_rack, rails=1) + rails_loc = self.deck.rails_to_location(1) + self.deck.assign_child_resource( + tip_rack, location=Coordinate(x=rails_loc.x, y=rails_loc.y, z=-6.1) + ) tip_spot = tip_rack.get_item("A1") tip = tip_spot.get_tip() diff --git a/pylabrobot/liquid_handling/backends/hamilton/vantage_backend.py b/pylabrobot/liquid_handling/backends/hamilton/vantage_backend.py index 688b7735f99..3fef3665056 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/vantage_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/vantage_backend.py @@ -34,6 +34,7 @@ Resource, Tip, TipRack, + Trash, Well, ) from pylabrobot.resources.hamilton import ( @@ -487,40 +488,32 @@ async def pick_up_tips( tips = [cast(HamiltonTip, op.resource.get_tip()) for op in ops] ttti = [await self.get_or_assign_tip_type_index(tip) for tip in tips] + collar_height = tips[0].collar_height + if any(tip.collar_height != collar_height for tip in tips): + raise ValueError("Cannot mix tips with different collar heights.") + max_z = max(op.resource.get_location_wrt(self.deck).z + op.offset.z for op in ops) - max_total_tip_length = max(op.tip.total_tip_length for op in ops) - max_tip_length = max((op.tip.total_tip_length - op.tip.fitting_depth) for op in ops) - - # not sure why this is necessary, but it is according to log files and experiments - if self._get_hamilton_tip([op.resource for op in ops]).tip_size == TipSize.LOW_VOLUME: - max_tip_length += 2 - elif self._get_hamilton_tip([op.resource for op in ops]).tip_size != TipSize.STANDARD_VOLUME: - max_tip_length -= 2 - - try: - return await self.pip_tip_pick_up( - x_position=x_positions, - y_position=y_positions, - tip_pattern=tip_pattern, - tip_type=ttti, - begin_z_deposit_position=[round((max_z + max_total_tip_length) * 10)] * len(ops), - end_z_deposit_position=[round((max_z + max_tip_length) * 10)] * len(ops), - minimal_traverse_height_at_begin_of_command=[ - round(th * 10) - for th in minimal_traverse_height_at_begin_of_command or [self._traversal_height] - ] - * len(ops), - minimal_height_at_command_end=[ - round(th * 10) for th in minimal_height_at_command_end or [self._traversal_height] - ] - * len(ops), - tip_handling_method=[1 for _ in tips], # always appears to be 1 # tip.pickup_method.value - blow_out_air_volume=[0] * len(ops), # Why is this here? Who knows. - ) - except Exception as e: - raise e - # @need_iswap_parked + return await self.pip_tip_pick_up( + x_position=x_positions, + y_position=y_positions, + tip_pattern=tip_pattern, + tip_type=ttti, + begin_z_deposit_position=[round((max_z + collar_height) * 10)] * len(ops), + end_z_deposit_position=[round(max_z * 10)] * len(ops), + minimal_traverse_height_at_begin_of_command=[ + round(th * 10) + for th in minimal_traverse_height_at_begin_of_command or [self._traversal_height] + ] + * len(ops), + minimal_height_at_command_end=[ + round(th * 10) for th in minimal_height_at_command_end or [self._traversal_height] + ] + * len(ops), + tip_handling_method=[1 for _ in tips], + blow_out_air_volume=[0] * len(ops), + ) + async def drop_tips( self, ops: List[Drop], @@ -532,31 +525,28 @@ async def drop_tips( x_positions, y_positions, channels_involved = self._ops_to_fw_positions(ops, use_channels) + tips = [cast(HamiltonTip, op.tip) for op in ops] max_z = max(op.resource.get_location_wrt(self.deck).z + op.offset.z for op in ops) + tip_length = max(tip.total_tip_length - tip.collar_height for tip in tips) - try: - return await self.pip_tip_discard( - x_position=x_positions, - y_position=y_positions, - tip_pattern=channels_involved, - begin_z_deposit_position=[round((max_z + 10) * 10)] * len(ops), # +10 - end_z_deposit_position=[round(max_z * 10)] * len(ops), - minimal_traverse_height_at_begin_of_command=[ - round(th * 10) - for th in minimal_traverse_height_at_begin_of_command or [self._traversal_height] - ] - * len(ops), - minimal_height_at_command_end=[ - round(th * 10) for th in minimal_height_at_command_end or [self._traversal_height] - ] - * len(ops), - tip_handling_method=[0 for _ in ops], # Always appears to be 0, even in trash. - # tip_handling_method=[TipDropMethod.DROP.value if isinstance(op.resource, TipSpot) \ - # else TipDropMethod.PLACE_SHIFT.value for op in ops], - TODO_TR_2=0, - ) - except Exception as e: - raise e + return await self.pip_tip_discard( + x_position=x_positions, + y_position=y_positions, + tip_pattern=channels_involved, + begin_z_deposit_position=[round((max_z - tip_length + 10) * 10)] * len(ops), + end_z_deposit_position=[round((max_z - tip_length) * 10)] * len(ops), + minimal_traverse_height_at_begin_of_command=[ + round(th * 10) + for th in minimal_traverse_height_at_begin_of_command or [self._traversal_height] + ] + * len(ops), + minimal_height_at_command_end=[ + round(th * 10) for th in minimal_height_at_command_end or [self._traversal_height] + ] + * len(ops), + tip_handling_method=[0 for _ in ops], + TODO_TR_2=0, + ) def _assert_valid_resources(self, resources: Sequence[Resource]) -> None: """Assert that resources are in a valid location for pipetting.""" diff --git a/pylabrobot/resources/__init__.py b/pylabrobot/resources/__init__.py index 4c3227c4019..074af62289a 100644 --- a/pylabrobot/resources/__init__.py +++ b/pylabrobot/resources/__init__.py @@ -41,7 +41,7 @@ from .sergi import * from .tecan import * from .thermo_fisher import * -from .tip_rack import TipRack, TipSpot +from .tip_rack import EmbeddedTipRack, NestedTipRack, StandingTipRack, TipRack, TipSpot from .tip_tracker import ( TipTracker, does_tip_tracking, diff --git a/pylabrobot/resources/hamilton/tip_carriers.py b/pylabrobot/resources/hamilton/tip_carriers.py index fc06281825f..0c5db4bdd8e 100644 --- a/pylabrobot/resources/hamilton/tip_carriers.py +++ b/pylabrobot/resources/hamilton/tip_carriers.py @@ -6,6 +6,7 @@ create_homogeneous_resources, ) from pylabrobot.resources.coordinate import Coordinate +from pylabrobot.resources.tip_rack_holder import EmbeddedTipRackHolder def TIP_CAR_120BC_4mlTF_A00(name: str) -> TipCarrier: @@ -16,7 +17,7 @@ def TIP_CAR_120BC_4mlTF_A00(name: str) -> TipCarrier: size_y=497.0, size_z=130.0, sites=create_homogeneous_resources( - klass=ResourceHolder, + klass=EmbeddedTipRackHolder, locations=[ Coordinate(6.2, 10.0, 114.95), Coordinate(6.2, 106.0, 114.95), @@ -40,7 +41,7 @@ def TIP_CAR_120BC_5mlT_A00(name: str) -> TipCarrier: size_y=497.0, size_z=130.0, sites=create_homogeneous_resources( - klass=ResourceHolder, + klass=EmbeddedTipRackHolder, locations=[ Coordinate(6.2, 10.0, 114.95), Coordinate(6.2, 106.0, 114.95), @@ -64,7 +65,7 @@ def TIP_CAR_288_A00(name: str) -> TipCarrier: size_y=497.0, size_z=130.0, sites=create_homogeneous_resources( - klass=ResourceHolder, + klass=EmbeddedTipRackHolder, locations=[ Coordinate(26.3, 36.3, 114.9), Coordinate(26.3, 182.213, 114.9), @@ -86,7 +87,7 @@ def TIP_CAR_288_B00(name: str) -> TipCarrier: size_y=497.0, size_z=130.0, sites=create_homogeneous_resources( - klass=ResourceHolder, + klass=EmbeddedTipRackHolder, locations=[ Coordinate(17.1, 36.25, 115.15), Coordinate(17.1, 182.25, 115.15), @@ -108,7 +109,7 @@ def TIP_CAR_288_C00(name: str) -> TipCarrier: size_y=497.0, size_z=130.0, sites=create_homogeneous_resources( - klass=ResourceHolder, + klass=EmbeddedTipRackHolder, locations=[ Coordinate(3.7, 36.3, 114.7), Coordinate(3.7, 182.3, 114.7), @@ -130,7 +131,7 @@ def TIP_CAR_384BC_A00(name: str) -> TipCarrier: size_y=497.0, size_z=130.0, sites=create_homogeneous_resources( - klass=ResourceHolder, + klass=EmbeddedTipRackHolder, locations=[ Coordinate(6.3, 78.2, 114.8), Coordinate(6.3, 163.1, 114.8), @@ -153,7 +154,7 @@ def TIP_CAR_384_A00(name: str) -> TipCarrier: size_y=497.0, size_z=130.0, sites=create_homogeneous_resources( - klass=ResourceHolder, + klass=EmbeddedTipRackHolder, locations=[ Coordinate(6.3, 78.2, 114.8), Coordinate(6.3, 163.1, 114.8), @@ -176,7 +177,7 @@ def TIP_CAR_480(name: str) -> TipCarrier: size_y=497.0, size_z=130.0, sites=create_homogeneous_resources( - klass=ResourceHolder, + klass=EmbeddedTipRackHolder, locations=[ Coordinate(6.2, 10.0, 114.95), Coordinate(6.2, 106.0, 114.95), @@ -200,7 +201,7 @@ def TIP_CAR_480BC_A00(name: str) -> TipCarrier: size_y=497.0, size_z=130.0, sites=create_homogeneous_resources( - klass=ResourceHolder, + klass=EmbeddedTipRackHolder, locations=[ Coordinate(6.2, 10.0, 114.95), Coordinate(6.2, 106.0, 114.95), @@ -224,7 +225,7 @@ def TIP_CAR_480_A00(name: str) -> TipCarrier: size_y=497.0, size_z=130.0, sites=create_homogeneous_resources( - klass=ResourceHolder, + klass=EmbeddedTipRackHolder, locations=[ Coordinate(6.2, 10.0, 114.95), Coordinate(6.2, 106.0, 114.95), @@ -248,7 +249,7 @@ def TIP_CAR_72_4mlTF_C00(name: str) -> TipCarrier: size_y=497.0, size_z=130.0, sites=create_homogeneous_resources( - klass=ResourceHolder, + klass=EmbeddedTipRackHolder, locations=[ Coordinate(3.7, 36.3, 114.7), Coordinate(3.7, 182.3, 114.7), @@ -270,7 +271,7 @@ def TIP_CAR_72_5mlT_C00(name: str) -> TipCarrier: size_y=497.0, size_z=130.0, sites=create_homogeneous_resources( - klass=ResourceHolder, + klass=EmbeddedTipRackHolder, locations=[ Coordinate(3.7, 36.3, 114.7), Coordinate(3.7, 182.3, 114.7), @@ -292,7 +293,7 @@ def TIP_CAR_96BC_4mlTF_A00(name: str) -> TipCarrier: size_y=497.0, size_z=130.0, sites=create_homogeneous_resources( - klass=ResourceHolder, + klass=EmbeddedTipRackHolder, locations=[ Coordinate(6.3, 78.2, 114.8), Coordinate(6.3, 163.1, 114.8), @@ -315,7 +316,7 @@ def TIP_CAR_96BC_5mlT_A00(name: str) -> TipCarrier: size_y=497.0, size_z=130.0, sites=create_homogeneous_resources( - klass=ResourceHolder, + klass=EmbeddedTipRackHolder, locations=[ Coordinate(6.3, 78.2, 114.8), Coordinate(6.3, 163.1, 114.8), @@ -338,7 +339,7 @@ def TIP_CAR_NTR_A00(name: str) -> TipCarrier: size_y=497.0, size_z=130.0, sites=create_homogeneous_resources( - klass=ResourceHolder, + klass=EmbeddedTipRackHolder, locations=[ Coordinate(6.2, 10.0, 29.0), Coordinate(6.2, 106.0, 29.0), diff --git a/pylabrobot/resources/hamilton/tip_creators.py b/pylabrobot/resources/hamilton/tip_creators.py index 042052099a6..c3d03eb2873 100644 --- a/pylabrobot/resources/hamilton/tip_creators.py +++ b/pylabrobot/resources/hamilton/tip_creators.py @@ -47,6 +47,7 @@ def __init__( maximal_volume: float, tip_size: Union[TipSize, str], # union for deserialization, will probably refactor pickup_method: Union[TipPickupMethod, str], # union for deserialization, will probably refactor + collar_height: Optional[float] = None, name: Optional[str] = None, ): if isinstance(tip_size, str): @@ -70,6 +71,7 @@ def __init__( has_filter=has_filter, maximal_volume=maximal_volume, fitting_depth=fitting_depth, + collar_height=collar_height, name=name, ) @@ -85,6 +87,7 @@ def __repr__(self) -> str: f"maximal_volume={self.maximal_volume}, " f"fitting_depth={self.fitting_depth}, " f"total_tip_length={self.total_tip_length}, " + f"collar_height={self._collar_height}, " f"pickup_method={self.pickup_method.name})" ) @@ -106,6 +109,7 @@ def deserialize(cls, data): maximal_volume=data["maximal_volume"], tip_size=TipSize[data["tip_size"]], pickup_method=TipPickupMethod[data["pickup_method"]], + collar_height=data.get("collar_height"), ) @@ -276,6 +280,7 @@ def hamilton_tip_10uL(name: Optional[str] = None) -> HamiltonTip: maximal_volume=15, tip_size=TipSize.LOW_VOLUME, pickup_method=TipPickupMethod.OUT_OF_RACK, + collar_height=6.0, ) @@ -291,6 +296,7 @@ def hamilton_tip_10uL_filter(name: Optional[str] = None) -> HamiltonTip: maximal_volume=10, tip_size=TipSize.LOW_VOLUME, pickup_method=TipPickupMethod.OUT_OF_RACK, + collar_height=6.0, ) @@ -306,6 +312,7 @@ def hamilton_tip_50uL(name: Optional[str] = None) -> HamiltonTip: maximal_volume=65, tip_size=TipSize.STANDARD_VOLUME, pickup_method=TipPickupMethod.OUT_OF_RACK, + collar_height=8.0, ) @@ -321,6 +328,7 @@ def hamilton_tip_50uL_filter(name: Optional[str] = None) -> HamiltonTip: maximal_volume=60, tip_size=TipSize.STANDARD_VOLUME, pickup_method=TipPickupMethod.OUT_OF_RACK, + collar_height=8.0, ) @@ -333,6 +341,7 @@ def hamilton_tip_300uL(name: Optional[str] = None) -> HamiltonTip: maximal_volume=400, tip_size=TipSize.STANDARD_VOLUME, pickup_method=TipPickupMethod.OUT_OF_RACK, + collar_height=8.0, ) @@ -345,6 +354,7 @@ def hamilton_tip_300uL_filter(name: Optional[str] = None) -> HamiltonTip: maximal_volume=360, tip_size=TipSize.STANDARD_VOLUME, pickup_method=TipPickupMethod.OUT_OF_RACK, + collar_height=8.0, ) @@ -357,6 +367,7 @@ def hamilton_tip_300uL_filter_slim(name: Optional[str] = None) -> HamiltonTip: maximal_volume=360, tip_size=TipSize.HIGH_VOLUME, pickup_method=TipPickupMethod.OUT_OF_RACK, + collar_height=8.0, ) @@ -369,6 +380,7 @@ def hamilton_tip_300uL_filter_ultrawide(name: Optional[str] = None) -> HamiltonT maximal_volume=360, tip_size=TipSize.STANDARD_VOLUME, pickup_method=TipPickupMethod.OUT_OF_RACK, + collar_height=8.0, ) @@ -381,6 +393,7 @@ def hamilton_tip_1000uL(name: Optional[str] = None) -> HamiltonTip: maximal_volume=1250, tip_size=TipSize.HIGH_VOLUME, pickup_method=TipPickupMethod.OUT_OF_RACK, + collar_height=10.0, ) @@ -393,6 +406,7 @@ def hamilton_tip_1000uL_filter(name: Optional[str] = None) -> HamiltonTip: maximal_volume=1065, tip_size=TipSize.HIGH_VOLUME, pickup_method=TipPickupMethod.OUT_OF_RACK, + collar_height=10.0, ) @@ -408,6 +422,7 @@ def hamilton_tip_1000uL_filter_wide(name: Optional[str] = None) -> HamiltonTip: maximal_volume=1065, tip_size=TipSize.HIGH_VOLUME, pickup_method=TipPickupMethod.OUT_OF_RACK, + collar_height=10.0, ) @@ -423,6 +438,7 @@ def hamilton_tip_1000uL_filter_ultrawide(name: Optional[str] = None) -> Hamilton maximal_volume=1065, tip_size=TipSize.HIGH_VOLUME, pickup_method=TipPickupMethod.OUT_OF_RACK, + collar_height=10.0, ) diff --git a/pylabrobot/resources/hamilton/tip_racks.py b/pylabrobot/resources/hamilton/tip_racks.py index a7aa15a5549..23e4c16a9d0 100644 --- a/pylabrobot/resources/hamilton/tip_racks.py +++ b/pylabrobot/resources/hamilton/tip_racks.py @@ -1,8 +1,10 @@ from pylabrobot.resources.tip_rack import ( + EmbeddedTipRack, NestedTipRack, TipRack, TipSpot, ) +from pylabrobot.resources.tip_rack_holder import EmbeddedTipRackHolder from pylabrobot.resources.utils import create_ordered_items_2d from .tip_creators import ( @@ -22,120 +24,67 @@ hamilton_tip_5000uL, ) -# # # # # # # # # # 10 ul Tips # # # # # # # # # # - -def hamilton_96_tiprack_10uL_filter(name: str, with_tips: bool = True) -> TipRack: - """Hamilton cat. no.: 235936 (sterile), 235901 (non-sterile) - Hamilton name: 'LTF' - Tip Rack with 96x 10ul Low Volume Tip with filter - """ - return TipRack( +def hamilton_universal_rack(name: str, make_tip, with_tips: bool = True) -> EmbeddedTipRackHolder: + """Universal tip rack for Hamilton STAR systems.""" + return EmbeddedTipRack( name=name, size_x=122.4, size_y=82.6, size_z=20.0, - model=hamilton_96_tiprack_10uL_filter.__name__, + model=hamilton_universal_rack.__name__, + sinking_depth=6.0, # sinking depth ordered_items=create_ordered_items_2d( TipSpot, num_items_x=12, num_items_y=8, dx=7.2, dy=5.3, - dz=-22.5, + dz=7.7 - 0.2, # depth of hole item_dx=9.0, item_dy=9.0, size_x=9.0, size_y=9.0, - make_tip=hamilton_tip_10uL_filter, + make_tip=make_tip, ), with_tips=with_tips, + frame_height=10.0, ) -# TODO: identify cat number -def hamilton_96_tiprack_10uL(name: str, with_tips: bool = True) -> TipRack: +# # # # # # # # # # 10 ul Tips # # # # # # # # # # + + +def hamilton_96_tiprack_10uL_filter(name: str, with_tips: bool = True) -> EmbeddedTipRackHolder: + """Hamilton cat. no.: 235936 (sterile), 235901 (non-sterile) + Hamilton name: 'LTF' + Tip Rack with 96x 10ul Low Volume Tip with filter + """ + return hamilton_universal_rack(name=name, make_tip=hamilton_tip_10uL, with_tips=with_tips) + + +def hamilton_96_tiprack_10uL(name: str, with_tips: bool = True) -> EmbeddedTipRackHolder: """Hamilton cat. no.: 235900 (non-sterile) 235935 (sterile) Hamilton name: 'LT' Tip Rack with 96x 10ul Low Volume Tip""" - return TipRack( - name=name, - size_x=122.4, - size_y=82.6, - size_z=20.0, - model=hamilton_96_tiprack_10uL.__name__, - ordered_items=create_ordered_items_2d( - TipSpot, - num_items_x=12, - num_items_y=8, - dx=7.2, - dy=5.3, - dz=-22.5, - item_dx=9.0, - item_dy=9.0, - size_x=9.0, - size_y=9.0, - make_tip=hamilton_tip_10uL, - ), - with_tips=with_tips, - ) + return hamilton_universal_rack(name=name, make_tip=hamilton_tip_10uL_filter, with_tips=with_tips) # # # # # # # # # # 50 ul Tips # # # # # # # # # # -def hamilton_96_tiprack_50uL_filter(name: str, with_tips: bool = True) -> TipRack: +def hamilton_96_tiprack_50uL_filter(name: str, with_tips: bool = True) -> EmbeddedTipRackHolder: """Hamilton cat. no.: 235948 (non-sterile), 235979 (sterile), 235829 (clear, non-sterile) Hamilton name: 'TIP_50ul_w_filter' Tip Rack with 96x 50ul Tip""" - return TipRack( - name=name, - size_x=122.4, - size_y=82.6, - size_z=18.0, - model=hamilton_96_tiprack_50uL_filter.__name__, - ordered_items=create_ordered_items_2d( - TipSpot, - num_items_x=12, - num_items_y=8, - dx=7.2, - dy=5.3, - dz=-40.5, - item_dx=9.0, - item_dy=9.0, - size_x=9.0, - size_y=9.0, - make_tip=hamilton_tip_50uL_filter, - ), - with_tips=with_tips, - ) + return hamilton_universal_rack(name=name, make_tip=hamilton_tip_50uL_filter, with_tips=with_tips) -def hamilton_96_tiprack_50uL(name: str, with_tips: bool = True) -> TipRack: +def hamilton_96_tiprack_50uL(name: str, with_tips: bool = True) -> EmbeddedTipRackHolder: """Hamilton cat. no.: 235966 (non-sterile) 235978 (sterile) Hamilton name: 'TIP_50ul' Tip Rack with 96x 50ul Tip no filter""" - return TipRack( - name=name, - size_x=122.4, - size_y=82.6, - size_z=18.0, - model=hamilton_96_tiprack_50uL.__name__, - ordered_items=create_ordered_items_2d( - TipSpot, - num_items_x=12, - num_items_y=8, - dx=7.2, - dy=5.3, - dz=-40.5, - item_dx=9.0, - item_dy=9.0, - size_x=9.0, - size_y=9.0, - make_tip=hamilton_tip_50uL, - ), - with_tips=with_tips, - ) + return hamilton_universal_rack(name=name, make_tip=hamilton_tip_50uL, with_tips=with_tips) def hamilton_96_tiprack_50uL_NTR(name: str, with_tips: bool = True) -> NestedTipRack: @@ -171,173 +120,65 @@ def hamilton_96_tiprack_50uL_NTR(name: str, with_tips: bool = True) -> NestedTip # # # # # # # # # # 300 ul Tips # # # # # # # # # # -def hamilton_96_tiprack_300uL_filter(name: str, with_tips: bool = True) -> TipRack: +def hamilton_96_tiprack_300uL_filter(name: str, with_tips: bool = True) -> EmbeddedTipRackHolder: """Hamilton cat. no.: 235830 (clear, non-sterile), 235903 (non-sterile), 235938 (sterile) Hamilton name: 'STF' Tip Rack with 96x 300ul Standard Volume Tip with filter""" - return TipRack( - name=name, - size_x=122.4, - size_y=82.6, - size_z=20.0, - model=hamilton_96_tiprack_300uL_filter.__name__, - ordered_items=create_ordered_items_2d( - TipSpot, - num_items_x=12, - num_items_y=8, - dx=7.2, - dy=5.3, - dz=-50.5, - item_dx=9.0, - item_dy=9.0, - size_x=9.0, - size_y=9.0, - make_tip=hamilton_tip_300uL_filter, - ), - with_tips=with_tips, - ) + return hamilton_universal_rack(name=name, make_tip=hamilton_tip_300uL_filter, with_tips=with_tips) -def hamilton_96_tiprack_300uL(name: str, with_tips: bool = True) -> TipRack: +def hamilton_96_tiprack_300uL(name: str, with_tips: bool = True) -> EmbeddedTipRackHolder: """Hamilton cat. no.: 235834 (clear, non-sterile), 235902 (non-sterile), 235937 (sterile) Hamilton name: 'ST' Tip Rack with 96x 300ul Standard Volume Tip""" - return TipRack( - name=name, - size_x=122.4, - size_y=82.6, - size_z=20.0, - model=hamilton_96_tiprack_300uL.__name__, - ordered_items=create_ordered_items_2d( - TipSpot, - num_items_x=12, - num_items_y=8, - dx=7.2, - dy=5.3, - dz=-50.5, - item_dx=9.0, - item_dy=9.0, - size_x=9.0, - size_y=9.0, - make_tip=hamilton_tip_300uL, - ), - with_tips=with_tips, - ) + return hamilton_universal_rack(name=name, make_tip=hamilton_tip_300uL, with_tips=with_tips) -def hamilton_96_tiprack_300uL_filter_slim(name: str, with_tips: bool = True) -> TipRack: +def hamilton_96_tiprack_300uL_filter_slim( + name: str, with_tips: bool = True +) -> EmbeddedTipRackHolder: """Hamilton cat. no.: 235646 (CORE-II: conductive) Hamilton name: 'STF_Slim' Tip Rack with 96x 300ul Slim Standard Volume Tip with filter""" - return TipRack( - name=name, - size_x=122.4, - size_y=82.6, - size_z=20.0, - model=hamilton_96_tiprack_300uL_filter_slim.__name__, - ordered_items=create_ordered_items_2d( - TipSpot, - num_items_x=12, - num_items_y=8, - dx=7.2, - dy=5.3, - dz=-83.5, - item_dx=9.0, - item_dy=9.0, - size_x=9.0, - size_y=9.0, - make_tip=hamilton_tip_300uL_filter_slim, - ), - with_tips=with_tips, + return hamilton_universal_rack( + name=name, make_tip=hamilton_tip_300uL_filter_slim, with_tips=with_tips ) -def hamilton_96_tiprack_300uL_filter_ultrawide(name: str, with_tips: bool = True) -> TipRack: +def hamilton_96_tiprack_300uL_filter_ultrawide( + name: str, with_tips: bool = True +) -> EmbeddedTipRackHolder: """Hamilton cat. no.: 235449 (1.55 mm oriface, non-sterile) Hamilton name: 'STF' Tip Rack with 96x 300ul Wide Bore Standard Volume Tip with filter""" - return TipRack( - name=name, - size_x=122.4, - size_y=82.6, - size_z=20.0, - model=hamilton_96_tiprack_300uL_filter_ultrawide.__name__, - ordered_items=create_ordered_items_2d( - TipSpot, - num_items_x=12, - num_items_y=8, - dx=7.2, - dy=5.3, - dz=-42.5, - item_dx=9.0, - item_dy=9.0, - size_x=9.0, - size_y=9.0, - make_tip=hamilton_tip_300uL_filter_ultrawide, - ), - with_tips=with_tips, + return hamilton_universal_rack( + name=name, make_tip=hamilton_tip_300uL_filter_ultrawide, with_tips=with_tips ) # # # # # # # # # # 1_000 uL Tips # # # # # # # # # # -def hamilton_96_tiprack_1000uL_filter(name: str, with_tips: bool = True) -> TipRack: +def hamilton_96_tiprack_1000uL_filter(name: str, with_tips: bool = True) -> EmbeddedTipRackHolder: """Hamilton cat. no.: 235820 (clear, non-sterile), 235905 (non-sterile), 235940 (sterile) Hamilton name: 'HTF' Tip Rack with 96x 1000ul High Volume Tip with filter """ - return TipRack( - name=name, - size_x=122.4, - size_y=82.6, - size_z=20.0, - model=hamilton_96_tiprack_1000uL_filter.__name__, - ordered_items=create_ordered_items_2d( - TipSpot, - num_items_x=12, - num_items_y=8, - dx=7.2, - dy=5.3, - dz=-83.5, - item_dx=9.0, - item_dy=9.0, - size_x=9.0, - size_y=9.0, - make_tip=hamilton_tip_1000uL_filter, - ), - with_tips=with_tips, + return hamilton_universal_rack( + name=name, make_tip=hamilton_tip_1000uL_filter, with_tips=with_tips ) -def hamilton_96_tiprack_1000uL(name: str, with_tips: bool = True) -> TipRack: +def hamilton_96_tiprack_1000uL(name: str, with_tips: bool = True) -> EmbeddedTipRackHolder: """Hamilton cat. no.: 235822 (clear, non-sterile), 235904 (non-sterile), 235939 (sterile) Hamilton name: 'HT' Tip Rack with 96x 1000ul High Volume Tip""" - return TipRack( - name=name, - size_x=122.4, - size_y=82.6, - size_z=20.0, - model=hamilton_96_tiprack_1000uL.__name__, - ordered_items=create_ordered_items_2d( - TipSpot, - num_items_x=12, - num_items_y=8, - dx=7.2, - dy=5.3, - dz=-83.5, - item_dx=9.0, - item_dy=9.0, - size_x=9.0, - size_y=9.0, - make_tip=hamilton_tip_1000uL, - ), - with_tips=with_tips, - ) + return hamilton_universal_rack(name=name, make_tip=hamilton_tip_1000uL, with_tips=with_tips) -def hamilton_96_tiprack_1000uL_filter_wide(name: str, with_tips: bool = True) -> TipRack: +def hamilton_96_tiprack_1000uL_filter_wide( + name: str, with_tips: bool = True +) -> EmbeddedTipRackHolder: """Hamilton cat. no.: core-ii: @@ -352,30 +193,14 @@ def hamilton_96_tiprack_1000uL_filter_wide(name: str, with_tips: bool = True) -> Orifice Size: 1.2mm """ - return TipRack( - name=name, - size_x=122.4, - size_y=82.6, - size_z=20.0, - model=hamilton_96_tiprack_1000uL_filter_wide.__name__, - ordered_items=create_ordered_items_2d( - TipSpot, - num_items_x=12, - num_items_y=8, - dx=7.2, - dy=5.3, - dz=-80.35, - item_dx=9.0, - item_dy=9.0, - size_x=9.0, - size_y=9.0, - make_tip=hamilton_tip_1000uL_filter_wide, - ), - with_tips=with_tips, + return hamilton_universal_rack( + name=name, make_tip=hamilton_tip_1000uL_filter_wide, with_tips=with_tips ) -def hamilton_96_tiprack_1000uL_filter_ultrawide(name: str, with_tips: bool = True) -> TipRack: +def hamilton_96_tiprack_1000uL_filter_ultrawide( + name: str, with_tips: bool = True +) -> EmbeddedTipRackHolder: """Hamilton cat. no.: core-ii: @@ -390,33 +215,15 @@ def hamilton_96_tiprack_1000uL_filter_ultrawide(name: str, with_tips: bool = Tru Orifice Size: 3.2mm """ - return TipRack( - name=name, - size_x=122.4, - size_y=82.6, - size_z=20.0, - model=hamilton_96_tiprack_1000uL_filter_ultrawide.__name__, - ordered_items=create_ordered_items_2d( - TipSpot, - num_items_x=12, - num_items_y=8, - dx=7.2, - dy=5.3, - dz=-68.4, - item_dx=9.0, - item_dy=9.0, - size_x=9.0, - size_y=9.0, - make_tip=hamilton_tip_1000uL_filter_ultrawide, - ), - with_tips=with_tips, + return hamilton_universal_rack( + name=name, make_tip=hamilton_tip_1000uL_filter_ultrawide, with_tips=with_tips ) # # # # # # # # # # 4 ml Tips # # # # # # # # # # -def hamilton_24_tiprack_4000uL_filter(name: str, with_tips: bool = True) -> TipRack: +def hamilton_24_tiprack_4000uL_filter(name: str, with_tips: bool = True) -> EmbeddedTipRackHolder: """Hamilton cat. no.: 184021 (non-sterile), 184023 (sterile) Hamilton name: 'FourmlTF' Tip Rack 24x 4ml Tip with Filter landscape oriented""" @@ -446,7 +253,7 @@ def hamilton_24_tiprack_4000uL_filter(name: str, with_tips: bool = True) -> TipR # # # # # # # # # # 5 ml Tips # # # # # # # # # # -def hamilton_24_tiprack_5000uL(name: str, with_tips: bool = True) -> TipRack: +def hamilton_24_tiprack_5000uL(name: str, with_tips: bool = True) -> EmbeddedTipRackHolder: """Hamilton cat. no.: 184020 (non-sterile), 184022 (sterile) Hamilton name: 'FivemlT' Tip Rack 24x 5ml Tip landscape oriented""" @@ -478,41 +285,41 @@ def hamilton_24_tiprack_5000uL(name: str, with_tips: bool = True) -> TipRack: # TODO: remove after December 2025 (giving approx. 3 month transition period) -def LTF(name: str) -> TipRack: +def LTF(name: str) -> EmbeddedTipRackHolder: raise NotImplementedError("LTF is deprecated. use hamilton_96_tiprack_10uL_filter instead") -def LT(name: str) -> TipRack: +def LT(name: str) -> EmbeddedTipRackHolder: raise NotImplementedError("LT is deprecated. use hamilton_96_tiprack_10uL instead") -def TIP_50ul_w_filter(name: str) -> TipRack: +def TIP_50ul_w_filter(name: str) -> EmbeddedTipRackHolder: raise NotImplementedError( "TIP_50ul_w_filter is deprecated. use hamilton_96_tiprack_50uL_filter instead" ) -def TIP_50ul(name: str) -> TipRack: +def TIP_50ul(name: str) -> EmbeddedTipRackHolder: raise NotImplementedError("TIP_50ul is deprecated. use hamilton_96_tiprack_50uL instead") -def STF(name: str) -> TipRack: +def STF(name: str) -> EmbeddedTipRackHolder: raise NotImplementedError("STF is deprecated. use hamilton_96_tiprack_300uL_filter instead") -def ST(name: str) -> TipRack: +def ST(name: str) -> EmbeddedTipRackHolder: raise NotImplementedError("ST is deprecated. use hamilton_96_tiprack_300uL instead") -def STF_Slim(name: str) -> TipRack: +def STF_Slim(name: str) -> EmbeddedTipRackHolder: raise NotImplementedError( "STF_Slim is deprecated. use hamilton_96_tiprack_300uL_filter_slim instead" ) -def HTF(name: str) -> TipRack: +def HTF(name: str) -> EmbeddedTipRackHolder: raise NotImplementedError("HTF is deprecated. use hamilton_96_tiprack_1000uL_filter instead") -def HT(name: str) -> TipRack: +def HT(name: str) -> EmbeddedTipRackHolder: raise NotImplementedError("HT is deprecated. use hamilton_96_tiprack_1000uL instead") diff --git a/pylabrobot/resources/opentrons/load.py b/pylabrobot/resources/opentrons/load.py index 868363a0e7e..c807b5c09a9 100644 --- a/pylabrobot/resources/opentrons/load.py +++ b/pylabrobot/resources/opentrons/load.py @@ -4,9 +4,10 @@ import urllib.request from typing import Dict, List, cast -from pylabrobot.resources import Coordinate, Tip, TipRack, TipSpot +from pylabrobot.resources import Coordinate, Tip, TipSpot from pylabrobot.resources.carrier import PlateHolder from pylabrobot.resources.resource_holder import ResourceHolder +from pylabrobot.resources.tip_rack import StandingTipRack from pylabrobot.resources.tube_rack import TubeRack @@ -41,7 +42,7 @@ def _download_ot_resource_file(ot_name: str, force_download: bool): def load_ot_tip_rack( ot_name: str, plr_resource_name: str, with_tips: bool = True, force_download: bool = False -) -> TipRack: +) -> StandingTipRack: """Convert an Opentrons tip rack definition file to a PyLabRobot TipRack resource.""" data = _download_ot_resource_file(ot_name=ot_name, force_download=force_download) @@ -88,19 +89,15 @@ def make_tip(name: str) -> Tip: flattened_ordering = [item for sublist in ordering for item in sublist] ordered_items = dict(zip(flattened_ordering, wells)) - tr = TipRack( + return StandingTipRack( name=plr_resource_name, size_x=data["dimensions"]["xDimension"], size_y=data["dimensions"]["yDimension"], size_z=data["dimensions"]["zDimension"], ordered_items=cast(Dict[str, TipSpot], ordered_items), model=data["metadata"]["displayName"], + with_tips=with_tips, ) - if with_tips: - tr.fill() - else: - tr.empty() - return tr def load_ot_tube_rack( diff --git a/pylabrobot/resources/opentrons/tip_racks.py b/pylabrobot/resources/opentrons/tip_racks.py index ae411ac0a78..4e3f5f16714 100644 --- a/pylabrobot/resources/opentrons/tip_racks.py +++ b/pylabrobot/resources/opentrons/tip_racks.py @@ -1,80 +1,80 @@ from pylabrobot.resources.opentrons.load import load_ot_tip_rack -from pylabrobot.resources.tip_rack import TipRack +from pylabrobot.resources.tip_rack import StandingTipRack -def eppendorf_96_tiprack_1000ul_eptips(name: str, with_tips=True) -> TipRack: +def eppendorf_96_tiprack_1000ul_eptips(name: str, with_tips=True) -> StandingTipRack: return load_ot_tip_rack( ot_name="eppendorf_96_tiprack_1000ul_eptips", plr_resource_name=name, with_tips=with_tips ) -def tipone_96_tiprack_200ul(name: str, with_tips=True) -> TipRack: +def tipone_96_tiprack_200ul(name: str, with_tips=True) -> StandingTipRack: return load_ot_tip_rack( ot_name="tipone_96_tiprack_200ul", plr_resource_name=name, with_tips=with_tips ) -def opentrons_96_tiprack_300ul(name: str, with_tips=True) -> TipRack: +def opentrons_96_tiprack_300ul(name: str, with_tips=True) -> StandingTipRack: return load_ot_tip_rack( ot_name="opentrons_96_tiprack_300ul", plr_resource_name=name, with_tips=with_tips ) -def opentrons_96_tiprack_10ul(name: str, with_tips=True) -> TipRack: +def opentrons_96_tiprack_10ul(name: str, with_tips=True) -> StandingTipRack: return load_ot_tip_rack( ot_name="opentrons_96_tiprack_10ul", plr_resource_name=name, with_tips=with_tips ) -def opentrons_96_filtertiprack_10ul(name: str, with_tips=True) -> TipRack: +def opentrons_96_filtertiprack_10ul(name: str, with_tips=True) -> StandingTipRack: return load_ot_tip_rack( ot_name="opentrons_96_filtertiprack_10ul", plr_resource_name=name, with_tips=with_tips ) -def geb_96_tiprack_10ul(name: str, with_tips=True) -> TipRack: +def geb_96_tiprack_10ul(name: str, with_tips=True) -> StandingTipRack: return load_ot_tip_rack( ot_name="geb_96_tiprack_10ul", plr_resource_name=name, with_tips=with_tips ) -def opentrons_96_filtertiprack_200ul(name: str, with_tips=True) -> TipRack: +def opentrons_96_filtertiprack_200ul(name: str, with_tips=True) -> StandingTipRack: return load_ot_tip_rack( ot_name="opentrons_96_filtertiprack_200ul", plr_resource_name=name, with_tips=with_tips ) -def eppendorf_96_tiprack_10ul_eptips(name: str, with_tips=True) -> TipRack: +def eppendorf_96_tiprack_10ul_eptips(name: str, with_tips=True) -> StandingTipRack: return load_ot_tip_rack( ot_name="eppendorf_96_tiprack_10ul_eptips", plr_resource_name=name, with_tips=with_tips ) -def opentrons_96_tiprack_1000ul(name: str, with_tips=True) -> TipRack: +def opentrons_96_tiprack_1000ul(name: str, with_tips=True) -> StandingTipRack: return load_ot_tip_rack( ot_name="opentrons_96_tiprack_1000ul", plr_resource_name=name, with_tips=with_tips ) -def opentrons_96_tiprack_20ul(name: str, with_tips=True) -> TipRack: +def opentrons_96_tiprack_20ul(name: str, with_tips=True) -> StandingTipRack: return load_ot_tip_rack( ot_name="opentrons_96_tiprack_20ul", plr_resource_name=name, with_tips=with_tips ) -def opentrons_96_filtertiprack_1000ul(name: str, with_tips=True) -> TipRack: +def opentrons_96_filtertiprack_1000ul(name: str, with_tips=True) -> StandingTipRack: return load_ot_tip_rack( ot_name="opentrons_96_filtertiprack_1000ul", plr_resource_name=name, with_tips=with_tips ) -def opentrons_96_filtertiprack_20ul(name: str, with_tips=True) -> TipRack: +def opentrons_96_filtertiprack_20ul(name: str, with_tips=True) -> StandingTipRack: return load_ot_tip_rack( ot_name="opentrons_96_filtertiprack_20ul", plr_resource_name=name, with_tips=with_tips ) -def geb_96_tiprack_1000ul(name: str, with_tips=True) -> TipRack: +def geb_96_tiprack_1000ul(name: str, with_tips=True) -> StandingTipRack: return load_ot_tip_rack( ot_name="geb_96_tiprack_1000ul", plr_resource_name=name, with_tips=with_tips ) diff --git a/pylabrobot/resources/tip.py b/pylabrobot/resources/tip.py index 58e26b0160c..01c697abf62 100644 --- a/pylabrobot/resources/tip.py +++ b/pylabrobot/resources/tip.py @@ -1,13 +1,11 @@ from __future__ import annotations import warnings -from dataclasses import dataclass from typing import Callable, Optional from pylabrobot.resources.volume_tracker import VolumeTracker -@dataclass class Tip: """A single tip. @@ -16,16 +14,26 @@ class Tip: total_tip_length: total length of the tip, in in mm maximal_volume: maximal volume of the tip, in ul fitting_depth: the overlap between the tip and the pipette, in mm + collar_height: the height of the collar, in mm name: optional identifier for this tip """ - has_filter: bool - total_tip_length: float - maximal_volume: float - fitting_depth: float - name: Optional[str] = None + def __init__( + self, + has_filter: bool, + total_tip_length: float, + maximal_volume: float, + fitting_depth: float, + collar_height: Optional[float] = None, + name: Optional[str] = None, + ): + self.has_filter = has_filter + self.total_tip_length = total_tip_length + self.maximal_volume = maximal_volume + self.fitting_depth = fitting_depth + self._collar_height = collar_height + self.name = name - def __post_init__(self): if self.name is None: warnings.warn( "Creating a Tip without a name is deprecated. " @@ -45,8 +53,20 @@ def serialize(self) -> dict: "has_filter": self.has_filter, "maximal_volume": self.maximal_volume, "fitting_depth": self.fitting_depth, + "collar_height": self._collar_height, } + @classmethod + def deserialize(cls, data: dict) -> "Tip": + return cls( + has_filter=data["has_filter"], + total_tip_length=data["total_tip_length"], + maximal_volume=data["maximal_volume"], + fitting_depth=data["fitting_depth"], + collar_height=data.get("collar_height"), + name=data.get("name"), + ) + def __hash__(self): return hash( ( @@ -54,6 +74,7 @@ def __hash__(self): self.total_tip_length, self.maximal_volume, self.fitting_depth, + self._collar_height, ) ) @@ -66,7 +87,15 @@ def __eq__(self, other: object) -> bool: and self.total_tip_length == other.total_tip_length and self.maximal_volume == other.maximal_volume and self.fitting_depth == other.fitting_depth + and self._collar_height == other._collar_height ) + @property + def collar_height(self) -> float: + """Return collar_height, raising if it is None.""" + if self._collar_height is None: + raise ValueError(f"collar_height is not defined for this tip: {self!r}") + return self._collar_height + TipCreator = Callable[[str], Tip] diff --git a/pylabrobot/resources/tip_rack.py b/pylabrobot/resources/tip_rack.py index 1d89293aabc..7d0861eb23f 100644 --- a/pylabrobot/resources/tip_rack.py +++ b/pylabrobot/resources/tip_rack.py @@ -157,6 +157,7 @@ def __init__( category: str = "tip_rack", model: Optional[str] = None, with_tips: bool = True, + frame_height: Optional[float] = None, ): super().__init__( name, @@ -168,6 +169,7 @@ def __init__( category=category, model=model, ) + self._frame_height = frame_height if ordered_items is not None and len(ordered_items) > 0: if with_tips: @@ -175,6 +177,13 @@ def __init__( else: self.empty() + @property + def frame_height(self) -> float: + """Return frame_height, raising if it is None.""" + if self._frame_height is None: + raise ValueError(f"frame_height is not defined for this tip rack: {self!r}") + return self._frame_height + def __repr__(self) -> str: return ( f"{self.__class__.__name__}(name={self.name!r}, size_x={self._size_x}, " @@ -304,3 +313,51 @@ def assign_child_resource( "Location must be specified if " + "resource is not a NestedTipRack." ) return super().assign_child_resource(resource, location=location, reassign=reassign) + + +class EmbeddedTipRack(TipRack): + """The EmbeddedTipRack - this is what some might call a "standard" TipRack; they cannot stand on their own, they require an EmbeddedTipRackHolder at all times to be functional. + + have historically been referred to as FTRs (officially "framed tip rack" from Hamilton, sometimes we used to call them "floating tip racks". + """ + + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + sinking_depth: float, + ordered_items: Optional[Dict[str, TipSpot]] = None, + ordering: Optional[List[str]] = None, + category: str = "tip_rack", + model: Optional[str] = None, + with_tips: bool = True, + frame_height: Optional[float] = None, + ): + """sinking_depth: the depth the tip rack sinks into the tip holder when placed inside it.""" + super().__init__( + name, + size_x, + size_y, + size_z, + ordered_items=ordered_items, + ordering=ordering, + category=category, + model=model, + with_tips=with_tips, + frame_height=frame_height, + ) + self.sinking_depth = sinking_depth + + def serialize(self) -> dict: + return { + **super().serialize(), + "sinking_depth": self.sinking_depth, + } + + +class StandingTipRack(TipRack): + """It's defining geometric characteristic is that it is completely self-sufficient and does not require a separate TipHolder to embed into.""" + + # TODO diff --git a/pylabrobot/resources/tip_rack_holder.py b/pylabrobot/resources/tip_rack_holder.py new file mode 100644 index 00000000000..a6ececd3a30 --- /dev/null +++ b/pylabrobot/resources/tip_rack_holder.py @@ -0,0 +1,33 @@ +from pylabrobot.resources.coordinate import Coordinate +from pylabrobot.resources.resource import Resource +from pylabrobot.resources.resource_holder import ResourceHolder, get_child_location +from pylabrobot.resources.tip_rack import EmbeddedTipRack + + +class EmbeddedTipRackHolder(ResourceHolder): + def __init__( + self, + name, + size_x, + size_y, + size_z, + rotation=None, + category="embedded_tip_rack_holder", + model=None, + child_location: Coordinate = Coordinate.zero(), + ): + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + rotation=rotation, + category=category, + model=model, + child_location=child_location, + ) + + def get_default_child_location(self, resource: Resource) -> Coordinate: + if not isinstance(resource, EmbeddedTipRack): + raise ValueError("Can only hold EmbeddedTipRack resources.") + return get_child_location(resource) + Coordinate(x=0, y=0, z=-resource.sinking_depth) diff --git a/pylabrobot/resources/tip_tests.py b/pylabrobot/resources/tip_tests.py index 668c78cbae3..0e89be8cd8b 100644 --- a/pylabrobot/resources/tip_tests.py +++ b/pylabrobot/resources/tip_tests.py @@ -23,6 +23,7 @@ def test_serialize(self): "total_tip_length": 10.0, "maximal_volume": 10.0, "fitting_depth": 1.0, + "collar_height": None, }, ) @@ -49,6 +50,7 @@ def test_serialize_subclass(self): "maximal_volume": 10.0, "pickup_method": "OUT_OF_RACK", "tip_size": "HIGH_VOLUME", + "collar_height": None, }, )