From 9720017eda22b91ae1dfe946d2d604f2feee211d Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Tue, 14 Oct 2025 21:57:33 -0700 Subject: [PATCH 1/4] STARBackend.{aspirate,disense} auto_surface_following_distance parameter --- .../backends/hamilton/STAR_backend.py | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 92aface5ef6..61bcdebaae5 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -1723,6 +1723,7 @@ async def aspirate( liquid_surfaces_no_lld: Optional[List[float]] = None, # PLR: probe_liquid_height: bool = False, + auto_surface_following_distance: bool = False, # remove >2026-01 mix_volume: Optional[List[float]] = None, mix_cycles: Optional[List[int]] = None, @@ -1780,6 +1781,7 @@ async def aspirate( liquid_surface_no_lld: Liquid surface at function without LLD [mm]. Must be between 0 and 360. Defaults to well bottom + liquid height. Should use absolute z. probe_liquid_height: PLR-specific parameter. If True, probe the liquid height using cLLD before aspirating to set the liquid_height of every operation instead of using the default 0. Liquid heights must not be set when using this function. + auto_surface_following_distance: automatically compute the surface following distance based on the container height<->volume functions. Requires liquid height to be specified or `probe_liquid_height=True`. """ # # # TODO: delete > 2026-01 # # # @@ -1867,7 +1869,6 @@ async def aspirate( immersion_depth = [ im * (-1 if immersion_depth_direction[i] else 1) for i, im in enumerate(immersion_depth) ] - surface_following_distance = _fill_in_defaults(surface_following_distance, [0.0] * n) flow_rates = [ op.flow_rate or (hlc.aspiration_flow_rate if hlc is not None else 100.0) for op, hlc in zip(ops, hamilton_liquid_classes) @@ -1953,6 +1954,38 @@ async def aspirate( wb + lh for wb, lh in zip(well_bottoms, liquid_heights) ] + if auto_surface_following_distance: + if any(op.liquid_height is None for op in ops) and not probe_liquid_height: + raise ValueError( + "To use auto_surface_following_distance all liquid heights must be set or probe_liquid_height must be True." + ) + + current_volumes = [ + op.resource.compute_volume_from_height(liquid_heights[i]) for i, op in enumerate(ops) + ] + + # compute new liquid_height after aspiration + liquid_height_after_aspiration = [ + op.resource.compute_height_from_volume(current_volumes[i] - op.volume) + for i, op in enumerate(ops) + ] + + # compute new surface_following_distance + surface_following_distance = [ + liquid_heights[i] - liquid_height_after_aspiration[i] + for i in range(len(liquid_height_after_aspiration)) + ] + else: + surface_following_distance = _fill_in_defaults(surface_following_distance, [0.0] * n) + + # check if the surface_following_distance would fall below the minimum height + for i in range(n): + if (liquid_heights[i] - surface_following_distance[i]) < minimum_height[i]: + raise ValueError( + f"automatic_surface_following would result in a surface_following_distance that goes below the minimum_height. " + f"Well bottom: {well_bottoms[i]}, surface_following_distance: {surface_following_distance[i]}, minimum_height: {minimum_height[i]}" + ) + try: return await self.aspirate_pip( aspiration_type=[0 for _ in range(n)], From 641d60cd1ff36a58278f558a5009a8545564c358 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Tue, 14 Oct 2025 21:59:27 -0700 Subject: [PATCH 2/4] check if volume<->height functions are set --- pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 61bcdebaae5..fdd4739da77 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -1960,6 +1960,11 @@ async def aspirate( "To use auto_surface_following_distance all liquid heights must be set or probe_liquid_height must be True." ) + if any(not op.resource.supports_compute_height_volume_functions() for op in ops): + raise ValueError( + "automatic_surface_following can only be used with containers that support height<->volume functions." + ) + current_volumes = [ op.resource.compute_volume_from_height(liquid_heights[i]) for i, op in enumerate(ops) ] From 39a5016345f363f43d6785425894d52c2e26dc91 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Tue, 14 Oct 2025 22:48:09 -0700 Subject: [PATCH 3/4] fix test --- .../backends/hamilton/STAR_backend.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index fdd4739da77..2bb4d06aa09 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -1984,12 +1984,17 @@ async def aspirate( surface_following_distance = _fill_in_defaults(surface_following_distance, [0.0] * n) # check if the surface_following_distance would fall below the minimum height - for i in range(n): - if (liquid_heights[i] - surface_following_distance[i]) < minimum_height[i]: - raise ValueError( - f"automatic_surface_following would result in a surface_following_distance that goes below the minimum_height. " - f"Well bottom: {well_bottoms[i]}, surface_following_distance: {surface_following_distance[i]}, minimum_height: {minimum_height[i]}" - ) + if any( + ops[i].resource.get_absolute_location(z="cavity_bottom").z + + liquid_heights[i] + - surface_following_distance[i] + < minimum_height[i] + for i in range(n) + ): + raise ValueError( + f"automatic_surface_following would result in a surface_following_distance that goes below the minimum_height. " + f"Well bottom: {well_bottoms[i]}, surface_following_distance: {surface_following_distance[i]}, minimum_height: {minimum_height[i]}" + ) try: return await self.aspirate_pip( From af4c914a03b750f614c32985d715b83e7e318cb9 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Fri, 17 Oct 2025 12:31:18 -0700 Subject: [PATCH 4/4] add for dispense --- .../backends/hamilton/STAR_backend.py | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 2bb4d06aa09..6990f56d2df 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -2095,6 +2095,7 @@ async def dispense( empty: Optional[List[bool]] = None, # truly "empty", does not exist in liquid editor, dm4 # PLR specific probe_liquid_height: bool = False, + auto_surface_following_distance: bool = False, # remove in the future immersion_depth_direction: Optional[List[int]] = None, mix_volume: Optional[List[float]] = None, @@ -2151,6 +2152,7 @@ async def dispense( documentation. Dispense mode 4. probe_liquid_height: PLR-specific parameter. If True, probe the liquid height using cLLD before aspirating to set the liquid_height of every operation instead of using the default 0. Liquid heights must not be set when using this function. + auto_surface_following_distance: automatically compute the surface following distance based on the container height<->volume functions. Requires liquid height to be specified or `probe_liquid_height=True`. """ n = len(ops) @@ -2246,7 +2248,6 @@ async def dispense( immersion_depth = [ im * (-1 if immersion_depth_direction[i] else 1) for i, im in enumerate(immersion_depth) ] - surface_following_distance = _fill_in_defaults(surface_following_distance, [0.0] * n) flow_rates = [ op.flow_rate or (hlc.dispense_flow_rate if hlc is not None else 120.0) for op, hlc in zip(ops, hamilton_liquid_classes) @@ -2314,6 +2315,35 @@ async def dispense( else: liquid_heights = [op.liquid_height or 0 for op in ops] + if auto_surface_following_distance: + if any(op.liquid_height is None for op in ops) and not probe_liquid_height: + raise ValueError( + "To use auto_surface_following_distance all liquid heights must be set or probe_liquid_height must be True." + ) + + if any(not op.resource.supports_compute_height_volume_functions() for op in ops): + raise ValueError( + "automatic_surface_following can only be used with containers that support height<->volume functions." + ) + + current_volumes = [ + op.resource.compute_volume_from_height(liquid_heights[i]) for i, op in enumerate(ops) + ] + + # compute new liquid_height after aspiration + liquid_height_after_aspiration = [ + op.resource.compute_height_from_volume(current_volumes[i] + op.volume) + for i, op in enumerate(ops) + ] + + # compute new surface_following_distance + surface_following_distance = [ + liquid_height_after_aspiration[i] - liquid_heights[i] + for i in range(len(liquid_height_after_aspiration)) + ] + else: + surface_following_distance = _fill_in_defaults(surface_following_distance, [0.0] * n) + liquid_surfaces_no_lld = liquid_surface_no_lld or [ wb + lh for wb, lh in zip(well_bottoms, liquid_heights) ]