Mixing during dispense

I’m looking to implement pipet mixing during my dispense cycles, though it would appear that this is not an allowable parameter for the standard LiquidHandler.dispense frontend method:

https://docs.pylabrobot.org/_autosummary/pylabrobot.liquid_handling.liquid_handler.LiquidHandler.dispense.html#pylabrobot.liquid_handling.liquid_handler.LiquidHandler.dispense

It is available via the STAR backend:

https://docs.pylabrobot.org/_autosummary/pylabrobot.liquid_handling.backends.hamilton.STAR.STAR.dispense.html

But I have to admit some naïveté when it comes to how one would actually use this backend method in practice. Does anyone have en example for how they have implemented mixing in PLR dispense steps? I have to imagine this is a relatively common need.

Any arguments taken by STAR that are not part of the LH method signature/definition can still be passed to LH, and by default these will just be passed onto the backend using the backend_kwargs. (This behavior is adjustable using Strictness.)

Thanks Rick, this makes sense, I think my original concern came because I was getting an error not consistent with the strictness check and was thus quite confused. Must have been another bug on my part.

With that in mind, when I try to run mix parameters with my dispense in the simulator, I see the warning that I assume comes from the default strictness check setting (WARN), but the method continues. Of course, I can’t see if it’s “mixing” as it’s in the simulator, but I assume it is doing so.

However, when I translate these to try and run on the robot itself, it is as if it does not even see the arguments in the method call. There is no mixing happening while I watch the robot, and there is no error or warning about the backend args like there is in the simulator. See below for code:

# defining liquid handle cycle function
async def dilution_cycle(column, cycle):
    await lh.pick_up_tips(growthtiprack[column])
    await lh.aspirate(culture_plate[column], vols=dilute_vollist)
    await lh.dispense(growth_plate[column], vols=dilute_vollist, mix_volume=3000, mix_cycles=10)
    await lh.aspirate(growth_plate[column], vols=dilute_vollist)
    if cycle % 2 == 0:
        await lh.dispense(read_plate[column], vols=read_vollist)
        await lh.dispense(bleach_plate[column], vols=75)
    else:
        await lh.dispense(bleach_plate[column], vols=dilute_vollist)
# call function across a number of columns of a plate (cycles)
for c in cycles:
    columnname = "A"+str(c+1)+":"+"H"+str(c+1)
    print(columnname, "+", c)
    await dilution_cycle(columnname, c)
    await lh.drop_tips(growthtiprack[columnname])

When I run this is it proceeds as if the , mix_volume=3000, mix_cycles=4 args are not present.

For some reason, mixing only works when we call lh.aspirate, not on dispense. So to mix, we just add an aspirate function with volume zero. Here is the correct function call:


await self.lh.aspirate(
    growth_plate[column],
    [0] * len(growth_plate[column]),
    homogenization_volume = 3000, #in units of 0.1ul
    homogenization_cycles = 10,
    homogenization_speed = 2600   #in units of 0.1ul
)

Here we use homogenization_volume, homogenization_cycles, and homogenization_speed because they are exactly the parameter names as outlined in the STAR.py backend. mix_volume and mix_cycles might not work.

Thanks Ben, at least I’m not the only one stumped by no mixing with dispense. I wonder if using the “homogenization” verbiage in the dispense call might work, rather than using “mix” as it says in the docs. Perhaps you’ve tried that?

I’ll give this a shot today when I can get back to it.

2 Likes

Can confirm that using the aspirate with vols=0 worked perfectly for me, thanks @ben. As probably expected, trying to plug in homogenization instead of mix with the dispense method did nothing.

Hi all, I’m back with an update on this question. If you’d rather I move it to a new thread, let me know.

In short, this worked quite well for using the span-8, but I wanted to try to do essentially the same thing with the CO-RE 96 head. I made some corresponding modifications to the commands, and am testing now. However, I get the timeout error pasted below.

One detail that might be relevant: You’ll notice the timeout error occurs on the third plate (of four). Before I noticed that the mixing speed arg for the aspirate_plate command (speed_of_homogenization) is different than for aspirate (homogenization_speed), I had kept the span-8 verbiage. In that case, the timeout error occurred on the first of the four plates, rather than the third. Notably the default mixing speed in that case was used, which is much slower than the requested 400 uL/s. In both cases, it occurs at the same point, which is just after finishing the “error” plate and before proceeding to the next plate.

Function definition and error trace

async def mix_plates(vol):
    await lh.dispense_plate(lh.get_resource('plate1'), volume=vol)
    await lh.aspirate_plate(lh.get_resource('plate1'), volume=0, 
                      homogenization_volume = 8000, homogenization_cycles = 4, 
                      speed_of_homogenization = 4000)
    await lh.aspirate_plate(lh.get_resource('plate2'), volume=0, 
                      homogenization_volume = 8000, homogenization_cycles = 4, 
                      speed_of_homogenization = 4000)
    await lh.aspirate_plate(lh.get_resource('plate3'), volume=0, 
                      homogenization_volume = 8000, homogenization_cycles = 4, 
                      speed_of_homogenization = 4000)
    await lh.aspirate_plate(lh.get_resource('plate4'), volume=0, 
                      homogenization_volume = 8000, homogenization_cycles = 4, 
                      speed_of_homogenization = 4000)
    await lh.drop_tips96(lh.get_resource('tips_01'))

Error trace

---------------------------------------------------------------------------
TimeoutError                              Traceback (most recent call last)
Cell In[45], line 2
      1 ## Run it
----> 2 await cycles(num_cycles)

Cell In[42], line 18, in cycles(cycles)
     17     vol = 0
---> 18     await mix_plates(vol)

Cell In[40], line 46, in mix_plates(vol)
     40 await lh.aspirate_plate(lh.get_resource('plate1'), volume=0, 
     41                   homogenization_volume = 8000, homogenization_cycles = 4, 
     42                   speed_of_homogenization = 4000)
     43 await lh.aspirate_plate(lh.get_resource('plate2'), volume=0, 
     44                   homogenization_volume = 8000, homogenization_cycles = 4, 
     45                   speed_of_homogenization = 4000)
---> 46 await lh.aspirate_plate(lh.get_resource('plate3'), volume=0, 
     47                   homogenization_volume = 8000, homogenization_cycles = 4, 
     48                   speed_of_homogenization = 4000)
     49 await lh.aspirate_plate(lh.get_resource('plate4'), volume=0, 
     50                   homogenization_volume = 8000, homogenization_cycles = 4, 
     51                   speed_of_homogenization = 4000)
     52 await lh.drop_tips96(lh.get_resource('tips_01'))

File ~/Documents/Lab/PyLabRobot/pylabrobot/pylabrobot/liquid_handling/liquid_handler.py:1154, in LiquidHandler.aspirate_plate(self, plate, volume, flow_rate, end_delay, **backend_kwargs)
   1143 if plate.num_items_x == 12 and plate.num_items_y == 8:
   1144   aspiration_plate = AspirationPlate(
   1145     resource=plate,
   1146     volume=volume,
   (...)
   1152     liquids=liquids,
   1153   )
-> 1154   await self.backend.aspirate96(aspiration=aspiration_plate, **backend_kwargs)
   1155   self._trigger_callback(
   1156     "aspirate_plate",
   1157     liquid_handler=self,
   (...)
   1160     **backend_kwargs,
   1161   )
   1162 else:

File ~/Documents/Lab/PyLabRobot/pylabrobot/pylabrobot/liquid_handling/backends/hamilton/STAR.py:61, in need_iswap_parked.<locals>.wrapper(self, *args, **kwargs)
     58 if self.iswap_installed and not self.iswap_parked:
     59   await self.park_iswap()
---> 61 result = await method(self, *args, **kwargs) # pylint: disable=not-callable
     63 return result

File ~/Documents/Lab/PyLabRobot/pylabrobot/pylabrobot/liquid_handling/backends/hamilton/STAR.py:2058, in STAR.aspirate96(self, aspiration, blow_out_air_volume, use_lld, liquid_height, air_transport_retract_dist, aspiration_type, minimum_traverse_height_at_beginning_of_a_command, minimal_end_height, lld_search_height, maximum_immersion_depth, tube_2nd_section_height_measured_from_zm, tube_2nd_section_ratio, immersion_depth, immersion_depth_direction, liquid_surface_sink_distance_at_the_end_of_aspiration, transport_air_volume, pre_wetting_volume, gamma_lld_sensitivity, swap_speed, settling_time, homogenization_volume, homogenization_cycles, homogenization_position_from_liquid_surface, surface_following_distance_during_homogenization, speed_of_homogenization, limit_curve_index)
   2049 if blow_out_air_volume is not None and blow_out_air_volume > 0:
   2050   await self.aspirate_core_96(
   2051     x_position=int(position.x * 10),
   2052     y_positions=int(position.y * 10),
   (...)
   2055     aspiration_volumes=int(blow_out_air_volume * 10)
   2056   )
-> 2058 return await self.aspirate_core_96(
   2059   x_position=int(position.x * 10),
   2060   x_direction=0,
   2061   y_positions=int(position.y * 10),
   2062   aspiration_type=aspiration_type,
   2063 
   2064   minimum_traverse_height_at_beginning_of_a_command=
   2065    minimum_traverse_height_at_beginning_of_a_command,
   2066   minimal_end_height=minimal_end_height,
   2067   lld_search_height=lld_search_height,
   2068   liquid_surface_at_function_without_lld=liquid_surface_at_function_without_lld,
   2069   pull_out_distance_to_take_transport_air_in_function_without_lld=
   2070    pull_out_distance_to_take_transport_air_in_function_without_lld,
   2071   maximum_immersion_depth=maximum_immersion_depth,
   2072   tube_2nd_section_height_measured_from_zm=tube_2nd_section_height_measured_from_zm,
   2073   tube_2nd_section_ratio=tube_2nd_section_ratio,
   2074   immersion_depth=immersion_depth,
   2075   immersion_depth_direction=immersion_depth_direction,
   2076   liquid_surface_sink_distance_at_the_end_of_aspiration=
   2077    liquid_surface_sink_distance_at_the_end_of_aspiration,
   2078   aspiration_volumes=aspiration_volumes,
   2079   aspiration_speed=flow_rate,
   2080   transport_air_volume=transport_air_volume,
   2081   blow_out_air_volume=0,
   2082   pre_wetting_volume=pre_wetting_volume,
   2083   lld_mode=int(use_lld),
   2084   gamma_lld_sensitivity=gamma_lld_sensitivity,
   2085   swap_speed=swap_speed,
   2086   settling_time=settling_time,
   2087   homogenization_volume=homogenization_volume,
   2088   homogenization_cycles=homogenization_cycles,
   2089   homogenization_position_from_liquid_surface=
   2090    homogenization_position_from_liquid_surface,
   2091   surface_following_distance_during_homogenization=
   2092    surface_following_distance_during_homogenization,
   2093   speed_of_homogenization=speed_of_homogenization,
   2094   channel_pattern=channel_pattern,
   2095   limit_curve_index=limit_curve_index,
   2096   tadm_algorithm=False,
   2097   recording_mode=0,
   2098 )

File ~/Documents/Lab/PyLabRobot/pylabrobot/pylabrobot/liquid_handling/backends/hamilton/STAR.py:4895, in STAR.aspirate_core_96(self, aspiration_type, x_position, x_direction, y_positions, minimum_traverse_height_at_beginning_of_a_command, minimal_end_height, lld_search_height, liquid_surface_at_function_without_lld, pull_out_distance_to_take_transport_air_in_function_without_lld, maximum_immersion_depth, tube_2nd_section_height_measured_from_zm, tube_2nd_section_ratio, immersion_depth, immersion_depth_direction, liquid_surface_sink_distance_at_the_end_of_aspiration, aspiration_volumes, aspiration_speed, transport_air_volume, blow_out_air_volume, pre_wetting_volume, lld_mode, gamma_lld_sensitivity, swap_speed, settling_time, homogenization_volume, homogenization_cycles, homogenization_position_from_liquid_surface, surface_following_distance_during_homogenization, speed_of_homogenization, channel_pattern, limit_curve_index, tadm_algorithm, recording_mode)
   4892 channel_pattern_bin_str = reversed(["1" if x else "0" for x in channel_pattern])
   4893 channel_pattern_hex = hex(int("".join(channel_pattern_bin_str), 2)).upper()[2:]
-> 4895 return await self.send_command(
   4896   module="C0",
   4897   command="EA",
   4898   aa=aspiration_type,
   4899   xs=f"{x_position:05}",
   4900   xd=x_direction,
   4901   yh=f"{y_positions:04}",
   4902   zh=f"{minimum_traverse_height_at_beginning_of_a_command:04}",
   4903   ze=f"{minimal_end_height:04}",
   4904   lz=f"{lld_search_height:04}",
   4905   zt=f"{liquid_surface_at_function_without_lld:04}",
   4906   pp=f"{pull_out_distance_to_take_transport_air_in_function_without_lld:04}",
   4907   zm=f"{maximum_immersion_depth:04}",
   4908   zv=f"{tube_2nd_section_height_measured_from_zm:04}",
   4909   zq=f"{tube_2nd_section_ratio:05}",
   4910   iw=f"{immersion_depth:03}",
   4911   ix=immersion_depth_direction,
   4912   fh=f"{liquid_surface_sink_distance_at_the_end_of_aspiration:03}",
   4913   af=f"{aspiration_volumes:05}",
   4914   ag=f"{aspiration_speed:04}",
   4915   vt=f"{transport_air_volume:03}",
   4916   bv=f"{blow_out_air_volume:05}",
   4917   wv=f"{pre_wetting_volume:05}",
   4918   cm=lld_mode,
   4919   cs=gamma_lld_sensitivity,
   4920   bs=f"{swap_speed:04}",
   4921   wh=f"{settling_time:02}",
   4922   hv=f"{homogenization_volume:05}",
   4923   hc=f"{homogenization_cycles:02}",
   4924   hp=f"{homogenization_position_from_liquid_surface:03}",
   4925   mj=f"{surface_following_distance_during_homogenization:03}",
   4926   hs=f"{speed_of_homogenization:04}",
   4927   cw=channel_pattern_hex,
   4928   cr=f"{limit_curve_index:03}",
   4929   cj=tadm_algorithm,
   4930   cx=recording_mode,
   4931 )

File ~/Documents/Lab/PyLabRobot/pylabrobot/pylabrobot/liquid_handling/backends/hamilton/STAR.py:1172, in STAR.send_command(self, module, command, tip_pattern, write_timeout, read_timeout, wait, fmt, **kwargs)
   1158 async def send_command(
   1159   self,
   1160   module: str,
   (...)
   1167   **kwargs
   1168 ):
   1169   """ Send a command to the machine. Parse the response if `fmt != ""`, else return the raw
   1170   response. """
-> 1172   resp = await super().send_command(
   1173     module=module,
   1174     command=command,
   1175     tip_pattern=tip_pattern,
   1176     write_timeout=write_timeout,
   1177     read_timeout=read_timeout,
   1178     wait=wait,
   1179     **kwargs
   1180   )
   1181   if fmt != "":
   1182     parsed = parse_star_fw_string(resp, fmt)

File ~/Documents/Lab/PyLabRobot/pylabrobot/pylabrobot/liquid_handling/backends/hamilton/base.py:191, in HamiltonLiquidHandler.send_command(self, module, command, tip_pattern, write_timeout, read_timeout, wait, **kwargs)
    170 """ Send a firmware command to the Hamilton machine.
    171 
    172 Args:
   (...)
    186   A dictionary containing the parsed response, or None if no response was read within `timeout`.
    187 """
    189 cmd, id_ = self._assemble_command(module=module, command=command, tip_pattern=tip_pattern,
    190   **kwargs)
--> 191 return await self._write_and_read_command(id_=id_, cmd=cmd, write_timeout=write_timeout,
    192                 read_timeout=read_timeout, wait=wait)

File ~/Documents/Lab/PyLabRobot/pylabrobot/pylabrobot/liquid_handling/backends/hamilton/base.py:215, in HamiltonLiquidHandler._write_and_read_command(self, id_, cmd, write_timeout, read_timeout, wait)
    213 fut = loop.create_future()
    214 self._start_reading(id_, loop, fut, cmd, read_timeout)
--> 215 result = await fut
    216 return cast(dict, result)

TimeoutError: Timeout while waiting for response to command C0EAid0048aa0xs02530xd0yh3380zh2450ze2450lz1999zt1881pp0100zm1269zv0032zq06180iw000ix0fh000af00000ag2500vt050bv00000wv00050cm0cs1bs0020wh10hv08000hc04hp000mj000hs4000cwFFFFFFFFFFFFFFFFFFFFFFFFcr000cj0cx0.

Further testing:

If I lower homogenization cycles from 4 to 2, it completes without error.

It seems like there is some maximum time period in which the mix_plates function is allowed to complete, and if either the cycles or speed of mixing is too high, it times out. Ideally this parameter is editable so I can allow this function to run longer than whatever the default is?

You can set a custom read_timeout value in STAR.py, particularly the aspirate_pip command. Check out line 3725 in STAR.py, see how it is hardcoded for a read timeout of 60 seconds? If your aspirate function with mixing takes longer than 60 seconds to execute, increasing this number will solve your problem. I have this set at 300 seconds for multi-column mix commands

2 Likes

Awesome, thank you @ben!

1 Like