I need help with my Hamilton Liquid Handler; the tips aren’t following the liquid surface during aspiration or dispensing. My assumption is that lld_mode handles the initial surface detection, and then the system adjusts tip height based on volume changes (e.g., compute_height_from_volume and compute_volume_from_height, which seems like these function are not being called).
Here’s the relevant code:
For aspirate
await lh.aspirate(plate[“A1:H1”], vols=[100]*8, use_channels=[i for i in range(8)], lld_mode=[STARBackend.LLDMode.GAMMA]*8, surface_following_distance=[3.]*8)
For dispense
await lh.dispense(plate[“A1:H1”]*8, vols=[100]*8, use_channels=[i for i in range(8)], surface_following_distance=[3.]*8)
The issue is that the tips aren’t tracking the surface as expected. Has anyone faced this, and what are your recommendations for a fix?
lld mode is for detection, surface_following_distance is for surface following
you have to call the compute_height_from_volume function yourself. (in the future it would be nice to have an “auto” option for surface following but we don’t have that yet.)
in your code i see you provide values as floats directly. in this case, are tips are still not following the liquid?
I’ve observed that the pipette tip correctly moves to the specific position as specified in surface_following_distance, but seems like compute_height_from_volume or compute_volume_from_height don’t seem to have any effect.
I’m adding these functions to my custom Tube definition with the assumption that the Hamilton backend uses them for dynamic tracking. However, no matter how I change the logic in these functions, the behavior stays the same. To test this, I’ve tried modifying the geometry parameters (e.g., radius, height, etc.) in the functions, but it made no difference to the outcome.
Could you confirm if this is the correct approach? Please let me know if there’s anything else I should be aware of to make it work properly. Here is my code:
def _compute_volume_from_height_Eppendorf_1_5mL_Vb(h: float) -> float:
R = 10.5/2
Hk = 20.
Hc = 16.
total_height = Hk + Hc
if not (0 <= h <= total_height):
raise ValueError(f"Height must be between 0 and {total_height}")
# Case 1: Liquid is in the conical part (0 <= h <= Hk)
if h <= Hk:
volume = (math.pi * R**2 * h**3) / (3 * Hk**2)
print(f"input height < cone height: volume={volume}")
return round(volume,3)
# Case 2: Liquid is in the cylindrical part (h > Hk)
else:
volume_cone = (1/3) * math.pi * R**2 * Hk
volume_in_cylinder = math.pi * R**2 * (h - Hk)
print(f"input height > cone height: height={volume_cone+volume_in_cylinder}")
return round(volume_cone + volume_in_cylinder,3)
def _compute_volume_from_height_Eppendorf_1_5mL_Vb(h: float) -> float:
R = 10.5/2
Hk = 20.
Hc = 16.
total_height = Hk + Hc
if not (0 <= h <= total_height):
raise ValueError(f"Height must be between 0 and {total_height}")
# Case 1: Liquid is in the conical part (0 <= h <= Hk)
if h <= Hk:
volume = (math.pi * R**2 * h**3) / (3 * Hk**2)
print(f"input height < cone height: volume={volume}")
return round(volume,3)
# Case 2: Liquid is in the cylindrical part (h > Hk)
else:
volume_cone = (1/3) * math.pi * R**2 * Hk
volume_in_cylinder = math.pi * R**2 * (h - Hk)
print(f"input height > cone height: height={volume_cone+volume_in_cylinder}")
return round(volume_cone + volume_in_cylinder,3)
# Custom Tube Definition
def Eppendorf_1_5ml_Vb(name: str) -> Tube:
"""1.5 mL round-bottom snap-cap Eppendorf tube. cat. no.: 022431021
- bottom_type=TubeBottomType.V
- snap-cap lid
"""
material_z_thickness = 1
diameter = 10.6
return Tube(
name=name,
size_x=diameter,
size_y=diameter,
size_z=38.6,
model="Eppendorf_1_5ml_Vb",
max_volume=1_500, # units: ul
material_z_thickness=material_z_thickness,
compute_volume_from_height=_compute_volume_from_height_Eppendorf_1_5mL_Vb,
compute_height_from_volume=_compute_height_from_volume_Eppendorf_1_5mL_Vb
)
so the robot moves to the correct position from lld and then also moves during the aspiration/dispense the surface_following_distance? please confirm this.
these functions by themselves are not used by LH.
to use them, update your code like this:
op_volume = 100
wells = plate[“A1:H1”]
await lh.dispense(
wells,
vols=[op_volume]*8,
use_channels=[i for i in range(8)],
surface_following_distance=[well.compute_height_from_volume(volume_before) - well.compute_height_from_volume(volume_before - op_volume for well in wells])
also note i updated plate[“A1:H1”]*8 to plate[“A1:H1”]
I want to make sure I understand the surface_following_distance parameter correctly.
Initially, I thought this parameter was the distance the tip had to maintain from the liquid’s surface while aspirating. This would mean that the tip automatically follows the liquid surface, regardless of the volume aspirated, based on the compute_height_from_volume function running at the backend.
After reading your function usage example, I now think the parameter means the total distance the tip moves during aspiration, from the surface down to the new, reduced height.
Could you please confirm if this interpretation is correct? This will help me better understand your response.
ideally, that’s how it’s used. but on the firmware level (and PLR for now) you have to compute the distance for this yourself. I like the idea of automatically computing this for containers where we do have height<>volume functions