Skip to content

Commit 29085ff

Browse files
authored
STARBackend.{aspirate,disense} auto_surface_following_distance parameter (#713)
1 parent 2b88140 commit 29085ff

File tree

1 file changed

+75
-2
lines changed

1 file changed

+75
-2
lines changed

pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1723,6 +1723,7 @@ async def aspirate(
17231723
liquid_surfaces_no_lld: Optional[List[float]] = None,
17241724
# PLR:
17251725
probe_liquid_height: bool = False,
1726+
auto_surface_following_distance: bool = False,
17261727
# remove >2026-01
17271728
mix_volume: Optional[List[float]] = None,
17281729
mix_cycles: Optional[List[int]] = None,
@@ -1780,6 +1781,7 @@ async def aspirate(
17801781
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.
17811782
17821783
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.
1784+
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`.
17831785
"""
17841786

17851787
# # # TODO: delete > 2026-01 # # #
@@ -1867,7 +1869,6 @@ async def aspirate(
18671869
immersion_depth = [
18681870
im * (-1 if immersion_depth_direction[i] else 1) for i, im in enumerate(immersion_depth)
18691871
]
1870-
surface_following_distance = _fill_in_defaults(surface_following_distance, [0.0] * n)
18711872
flow_rates = [
18721873
op.flow_rate or (hlc.aspiration_flow_rate if hlc is not None else 100.0)
18731874
for op, hlc in zip(ops, hamilton_liquid_classes)
@@ -1953,6 +1954,48 @@ async def aspirate(
19531954
wb + lh for wb, lh in zip(well_bottoms, liquid_heights)
19541955
]
19551956

1957+
if auto_surface_following_distance:
1958+
if any(op.liquid_height is None for op in ops) and not probe_liquid_height:
1959+
raise ValueError(
1960+
"To use auto_surface_following_distance all liquid heights must be set or probe_liquid_height must be True."
1961+
)
1962+
1963+
if any(not op.resource.supports_compute_height_volume_functions() for op in ops):
1964+
raise ValueError(
1965+
"automatic_surface_following can only be used with containers that support height<->volume functions."
1966+
)
1967+
1968+
current_volumes = [
1969+
op.resource.compute_volume_from_height(liquid_heights[i]) for i, op in enumerate(ops)
1970+
]
1971+
1972+
# compute new liquid_height after aspiration
1973+
liquid_height_after_aspiration = [
1974+
op.resource.compute_height_from_volume(current_volumes[i] - op.volume)
1975+
for i, op in enumerate(ops)
1976+
]
1977+
1978+
# compute new surface_following_distance
1979+
surface_following_distance = [
1980+
liquid_heights[i] - liquid_height_after_aspiration[i]
1981+
for i in range(len(liquid_height_after_aspiration))
1982+
]
1983+
else:
1984+
surface_following_distance = _fill_in_defaults(surface_following_distance, [0.0] * n)
1985+
1986+
# check if the surface_following_distance would fall below the minimum height
1987+
if any(
1988+
ops[i].resource.get_absolute_location(z="cavity_bottom").z
1989+
+ liquid_heights[i]
1990+
- surface_following_distance[i]
1991+
< minimum_height[i]
1992+
for i in range(n)
1993+
):
1994+
raise ValueError(
1995+
f"automatic_surface_following would result in a surface_following_distance that goes below the minimum_height. "
1996+
f"Well bottom: {well_bottoms[i]}, surface_following_distance: {surface_following_distance[i]}, minimum_height: {minimum_height[i]}"
1997+
)
1998+
19561999
try:
19572000
return await self.aspirate_pip(
19582001
aspiration_type=[0 for _ in range(n)],
@@ -2052,6 +2095,7 @@ async def dispense(
20522095
empty: Optional[List[bool]] = None, # truly "empty", does not exist in liquid editor, dm4
20532096
# PLR specific
20542097
probe_liquid_height: bool = False,
2098+
auto_surface_following_distance: bool = False,
20552099
# remove in the future
20562100
immersion_depth_direction: Optional[List[int]] = None,
20572101
mix_volume: Optional[List[float]] = None,
@@ -2108,6 +2152,7 @@ async def dispense(
21082152
documentation. Dispense mode 4.
21092153
21102154
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.
2155+
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`.
21112156
"""
21122157

21132158
n = len(ops)
@@ -2203,7 +2248,6 @@ async def dispense(
22032248
immersion_depth = [
22042249
im * (-1 if immersion_depth_direction[i] else 1) for i, im in enumerate(immersion_depth)
22052250
]
2206-
surface_following_distance = _fill_in_defaults(surface_following_distance, [0.0] * n)
22072251
flow_rates = [
22082252
op.flow_rate or (hlc.dispense_flow_rate if hlc is not None else 120.0)
22092253
for op, hlc in zip(ops, hamilton_liquid_classes)
@@ -2271,6 +2315,35 @@ async def dispense(
22712315
else:
22722316
liquid_heights = [op.liquid_height or 0 for op in ops]
22732317

2318+
if auto_surface_following_distance:
2319+
if any(op.liquid_height is None for op in ops) and not probe_liquid_height:
2320+
raise ValueError(
2321+
"To use auto_surface_following_distance all liquid heights must be set or probe_liquid_height must be True."
2322+
)
2323+
2324+
if any(not op.resource.supports_compute_height_volume_functions() for op in ops):
2325+
raise ValueError(
2326+
"automatic_surface_following can only be used with containers that support height<->volume functions."
2327+
)
2328+
2329+
current_volumes = [
2330+
op.resource.compute_volume_from_height(liquid_heights[i]) for i, op in enumerate(ops)
2331+
]
2332+
2333+
# compute new liquid_height after aspiration
2334+
liquid_height_after_aspiration = [
2335+
op.resource.compute_height_from_volume(current_volumes[i] + op.volume)
2336+
for i, op in enumerate(ops)
2337+
]
2338+
2339+
# compute new surface_following_distance
2340+
surface_following_distance = [
2341+
liquid_height_after_aspiration[i] - liquid_heights[i]
2342+
for i in range(len(liquid_height_after_aspiration))
2343+
]
2344+
else:
2345+
surface_following_distance = _fill_in_defaults(surface_following_distance, [0.0] * n)
2346+
22742347
liquid_surfaces_no_lld = liquid_surface_no_lld or [
22752348
wb + lh for wb, lh in zip(well_bottoms, liquid_heights)
22762349
]

0 commit comments

Comments
 (0)