Aspirating from multi-compartment reservoir (PLR)

Hi everyone,

Is there already a way to perform a multi-channel aspiration from a multi-compartment reservoir plate?

Let’s say you have a reservoir with 6 compartments (‘A1’, ‘A2’, ‘A3’, ‘A4’, ‘A5’, ‘A6’). The power of a reservoir comes from being able to aspirate with all channels simultaneously.

However, when performing the following command…

positions = tip_50ul_00['A1', 'B1']
await lh.pick_up_tips(positions)

await lh.aspirate(Porvair_6x47_Reservoir_00['A1'], vols=[5, 5], use_channels=[0, 1],
                  swap_speed = 50, homogenization_speed = 20,
                  dispensation_speed_during_emptying_tip = 25, 
                 )

…the following ValueError is raised…

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[65], line 1
----> 1 await lh.aspirate(Porvair_6x47_Reservoir_00['A1'], vols=[2.5, 2.5], use_channels=[0, 1],
      2                   swap_speed = 50, homogenization_speed = 20,
      3                   dispensation_speed_during_emptying_tip = 25, 
      4                  )

File ~/pylabrobot/pylabrobot/machine.py:20, in need_setup_finished.<locals>.wrapper(self, *args, **kwargs)
     18 if not self.setup_finished:
     19   raise RuntimeError("The setup has not finished. See `setup`.")
---> 20 return await func(self, *args, **kwargs)

File ~/pylabrobot/pylabrobot/liquid_handling/liquid_handler.py:689, in LiquidHandler.aspirate(self, resources, vols, use_channels, flow_rates, end_delay, offsets, liquid_height, **backend_kwargs)
    685   self._make_sure_channels_exist(use_channels)
    687   offsets = expand(offsets, n)
--> 689 vols = expand(vols, n)
    690 flow_rates = expand(flow_rates, n)
    691 liquid_height = expand(liquid_height, n)

File ~/pylabrobot/pylabrobot/utils/list.py:54, in expand(list_or_item, n)
     52 if isinstance(list_or_item, collections.abc.Sequence) and not isinstance(list_or_item, str):
     53   if len(list_or_item) != n:
---> 54     raise ValueError(f"Expected list of length {n}, got {len(list_or_item)}.")
     55   return list(list_or_item)
     56 # cast to T to avoid mypy error (thinks it's a string). This can probably be written better.

ValueError: Expected list of length 1, got 2.

Creating offsets manually with…

offsets = Porvair_6x47_Reservoir_00.get_item('A1').get_2d_center_offsets(n=2)

…and handing them to the function as an argument raises the same error.

At the same time…

await lh.aspirate(Porvair_6x47_Reservoir_00.get_item('A1'), vols=[5, 5], use_channels=[0, 1],
                  swap_speed = 50, homogenization_speed = 20, 
                  dispensation_speed_during_emptying_tip = 25, 
                 )

…does enforce the aspiration but from a completely wrong position (outside of the well ‘A1’).

A quick way I can see to get around this is to define the reservoir instead as a 96-deepwell plate but that is not a permanent solution :slight_smile:

Ah, this is because PLR will only spread channels in a single resource (such as a single well in this case) if it is passed a resource object. But [] indexing returns a list. This should be an easy fix.

It’s curious that the second command aspirates outside of target location and I’m not immediately sure what’s going on there. Do you notice any interesting patterns? What would you estimate the offset to be?

I see. So depending on how you access the location of a well (.get_item vs [ ]) changes whether multiple channels can be used simultaneously.
Can you please guide me towards what needs to be changed to make the [ ] location usable with multiple pipettes?
I’d assume that the same well_size_y check would need to be implemented to ensure proper spacing of the channels.

In general, what do you estimate would be the difficulty to set some additional default aspiration and dispensing behaviours (some may already be there):

  1. if pipetting multiple volumes from one well:
    a. use as many channels simultaneously as possible (based on the well_size_y being multiples of the minimal_channel_distance=9mm)
    b. automatically iterate through your channels (if not all channels fit into the well/resource) to achieve the volume aspiration commanded

Yes, I was quite surprised and multiple kernel restarts showed the same result (in case there was something in memory when I tried out offsets before).
The offset associated with .get_item() was always y+ by at least 20mm and x+ by only a few mm.

I’ll update LH to accommodate this. In the meantime, please use .get_item or [well][0].

I’m would prefer to not automatically iterate over channels because that violates the expectation of atomic commands. I think too much smartness and inference on what the user means will eventually lead to a worse API and unexpected behavior. Users are free to implement their own higher level function for this, where this behavior is explicit.

I’ll investigate.

2 Likes

Thank you, especially the [well][0] sounds like a perfect intermediate solution I will try out next.

That makes a lot of sense: usability over specificity. I will implement these functions on a need basis myself :slight_smile: