From c6baa46b0f49b9aac3ccca847c66aef0c578d7e6 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Fri, 6 Dec 2024 18:17:33 -0800 Subject: [PATCH 1/2] wip --- pylabrobot/liquid_handling/liquid_handler.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pylabrobot/liquid_handling/liquid_handler.py b/pylabrobot/liquid_handling/liquid_handler.py index 0d2b377de3..67d986b410 100644 --- a/pylabrobot/liquid_handling/liquid_handler.py +++ b/pylabrobot/liquid_handling/liquid_handler.py @@ -784,13 +784,17 @@ async def aspirate( resource = resources[0] n = len(use_channels) resources = [resource] * len(use_channels) + # space equally within 4.5mm margins of the edges if resource.get_absolute_rotation().z % 180 == 0: - centers = list(reversed(resource.centers(yn=n, zn=0))) + space_y = resource.get_size_y() - 4.5 * 2 + spacing = space_y / (n + 1) + centers = list(reversed([Coordinate(0, 4.5 + i * spacing, 0) for i in range(n)])) elif resource.get_absolute_rotation().z % 90 == 0: - centers = list(reversed(resource.centers(xn=n, zn=0))) + space_x = resource.get_size_x() - 4.5 * 2 + spacing = space_x / (n + 1) + centers = list(reversed([Coordinate(4.5 + i * spacing, 0, 0) for i in range(n)])) else: raise ValueError("Only 90 and 180 degree rotations are supported for now.") - centers = [c - resource.center() for c in centers] # offset is wrt center offsets = [c + o for c, o in zip(centers, offsets)] # user-defined # liquid(s) for each channel. If volume tracking is disabled, use None as the liquid. From ed1214025ffe4b86dfb1d23909f5e5906b365414 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Fri, 20 Dec 2024 11:32:55 -0800 Subject: [PATCH 2/2] space channels in single resource --- pylabrobot/liquid_handling/liquid_handler.py | 68 ++++++++++++++++---- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/pylabrobot/liquid_handling/liquid_handler.py b/pylabrobot/liquid_handling/liquid_handler.py index 67d986b410..2d120b98bb 100644 --- a/pylabrobot/liquid_handling/liquid_handler.py +++ b/pylabrobot/liquid_handling/liquid_handler.py @@ -90,6 +90,16 @@ def check_updatable(src_tracker: VolumeTracker, dest_tracker: VolumeTracker): ) +def _get_centers_with_margin(dim_size: float, n: int, margin: float, min_spacing: float): + """Get the centers of the channels with a minimum margin on the edges.""" + if dim_size < margin * 2 + (n - 1) * min_spacing: + raise ValueError("Resource is too small to space channels.") + if dim_size - (n - 1) * min_spacing <= min_spacing * 2: + remaining_space = dim_size - (n - 1) * min_spacing - margin * 2 + return [margin + remaining_space / 2 + i * min_spacing for i in range(n)] + return [(i + 1) * dim_size / (n + 1) for i in range(n)] + + class BlowOutVolumeError(Exception): pass @@ -688,6 +698,45 @@ def _check_containers(self, resources: Sequence[Resource]): if len(not_containers) > 0: raise TypeError(f"Resources must be `Container`s, got {not_containers}") + def _get_single_resource_liquid_op_offsets( + self, resource: Resource, num_channels: int + ) -> List[Coordinate]: + min_spacing_edge = ( + 2 # minimum spacing between the edge of the container and the center of channel + ) + min_spacing_between_channels = 9 + + resource_size: float + if resource.get_absolute_rotation().z % 180 == 0: + resource_size = resource.get_size_y() + elif resource.get_absolute_rotation().z % 90 == 0: + resource_size = resource.get_size_x() + else: + raise ValueError("Only 90 and 180 degree rotations are supported for now.") + + centers = list( + reversed( + _get_centers_with_margin( + dim_size=resource_size, + n=num_channels, + margin=min_spacing_edge, + min_spacing=min_spacing_between_channels, + ) + ) + ) # reverse because channels are from back to front + + center_offsets: List[Coordinate] = [] + if resource.get_absolute_rotation().z % 180 == 0: + x_offset = resource.get_size_x() / 2 + center_offsets = [Coordinate(x=x_offset, y=c, z=0) for c in centers] + elif resource.get_absolute_rotation().z % 90 == 0: + y_offset = resource.get_size_y() / 2 + center_offsets = [Coordinate(x=c, y=y_offset, z=0) for c in centers] + + # offsets are relative to the center of the resource, but above we computed them wrt lfb + # so we need to subtract the center of the resource + return [c - resource.center() for c in center_offsets] + @need_setup_finished async def aspirate( self, @@ -782,20 +831,13 @@ async def aspirate( # center of the resource. if len(set(resources)) == 1: resource = resources[0] - n = len(use_channels) resources = [resource] * len(use_channels) - # space equally within 4.5mm margins of the edges - if resource.get_absolute_rotation().z % 180 == 0: - space_y = resource.get_size_y() - 4.5 * 2 - spacing = space_y / (n + 1) - centers = list(reversed([Coordinate(0, 4.5 + i * spacing, 0) for i in range(n)])) - elif resource.get_absolute_rotation().z % 90 == 0: - space_x = resource.get_size_x() - 4.5 * 2 - spacing = space_x / (n + 1) - centers = list(reversed([Coordinate(4.5 + i * spacing, 0, 0) for i in range(n)])) - else: - raise ValueError("Only 90 and 180 degree rotations are supported for now.") - offsets = [c + o for c, o in zip(centers, offsets)] # user-defined + center_offsets = self._get_single_resource_liquid_op_offsets( + resource=resource, num_channels=len(use_channels) + ) + + # add user defined offsets to the computed centers + offsets = [c + o for c, o in zip(center_offsets, offsets)] # liquid(s) for each channel. If volume tracking is disabled, use None as the liquid. liquids: List[List[Tuple[Optional[Liquid], float]]] = []