List Comprehensions in PyHamilton

List Comprehensions in PyHamilton (Checkerboard Dispense)

List comprehensions are an invaluable tool for specifying liquid-handling patterns in PyHamilton. They are one-line expressions for returning a list with a function applied to each element, written like this:

[f(x) for x in range(10)]

where f(x) is a function and x is an index ranging over the right-hand expression following in. List comprehensions are very similar to for loops, but they are calculated in parallel rather than sequentially. In other words, each element of the return list is calculated separately in a list comprehension, whereas in a for loop each iteration is processed one at a time.

A1:D1 Aspiration

Suppose we want to aspirate some reagent from wells A1:D1 of a source plate and dispense it to a target plate in a checkerboard pattern. We only need 4 channels for this.

aspiration_poss = [(plates[1], x) for x in range(4)]
asp_vols = [200]*4

Here is an aspiration pattern that uses the first 4 channels to aspirate from wells A1:D1, but we need to specify what the remaining channels are doing. Let’s write a function that tells those channels to do nothing by padding with None.

def pad_to_eight(channels_var):
    pad_length = 8-len(channels_var)
    channels_var = channels_var + [None]*pad_length
    return channels_var

This function takes a list of length <= 8 and pads it with None values to length 8. Let’s wrap the previous lists with this function.

aspiration_poss = pad_to_eight([(plates[1], x) for x in range(4)])
asp_vols = pad_to_eight([200]*4)

Checkerboard Dispense

We’ve defined our simple A1:D1 aspiration sequence. How do we define the checkerboard dispense? First let’s remember we are dispensing across multiple columns of a plate. This requires multiple dispense steps, so we should use a nested list comprehension.

[[(plates[0], x) for x in range(8*i, 8*i + 8)] for i in range(12)]

This is a nested list comprehension where each element is a list of all of the wells in a column indexed by the i variable. We can modify this to get to the checkerboard pattern we want. What is a checkerboard pattern exactly? In each column, every other well will be dispensed to. That can be expressed by x%2, which returns the remainder of x divided by 2. We can apply a conditional to the inner list-comprehension to only return values that satisfy x%2==0. These are even numbers.

dispense_columns = [[(plates[0], x) for x in range(8*i, 8*i + 8) if (x%2)==0] for i in range(12)]

But a checkerboard pattern means that the set of target wells also has to alternate across columns. First even wells, then odd wells, then even, then odd, etc. To accomplish this we change the right-hand side of the conditional equation from 0 to i%2.

dispense_columns = [pad_to_eight([(plates[0], x) for x in range(8*i, 8*i + 8) if (x%2)==(i%2)]) for i in range(12)]

Deck

We’ll also make sure to wrap our inner list comprehension with the padding function from earlier. What if we wanted the inverse of the pattern we just specified? Nothing could be more simple. Just change i%2 to (i+1)%2 in the above list comprehension, like this:

dispense_columns = [pad_to_eight([(plates[0], x) for x in range(8*i, 8*i + 8) if (x%2)==((i+1)%2)]) for i in range(12)]

Deck

The only difference is that the right-hand side of the conditional equation now returns 0,1,0,1,... rather than 1,0,1,0,...

The Whole Script

Here’s how it all comes together in a script. You can replace robot_method.py in the project template instantiated by pyhamilton-new-project with this script.

import os
from pyhamilton import (HamiltonInterface,  LayoutManager, 
 Plate96, Tip96, initialize, tip_pick_up, tip_eject, 
 aspirate, dispense,  oemerr, resource_list_with_prefix, normal_logging)

liq_class = 'StandardVolumeFilter_Water_DispenseJet_Empty'

lmgr = LayoutManager('deck.lay')

plates = resource_list_with_prefix(lmgr, 'plate_', Plate96, 5)
tips = resource_list_with_prefix(lmgr, 'tips_', Tip96, 1)


def pad_to_eight(channels_var):
    pad_length = 8-len(channels_var)
    channels_var = channels_var + [None]*pad_length
    return channels_var

aspiration_poss = pad_to_eight([(plates[1], x) for x in range(4)])
asp_vols = pad_to_eight([200]*4)

dispense_columns = [pad_to_eight([(plates[0], x) for x in range(8*i, 8*i + 8) if (x%2)==(i%2)]) for i in range(12)]
disp_vols = [10]*4 + [None]*4

tips_poss = [(tips[0], x) for x in range(8)]


if __name__ == '__main__': 
    with HamiltonInterface(simulate=True) as ham_int:
        normal_logging(ham_int, os.getcwd())
        initialize(ham_int)
        tip_pick_up(ham_int, tips_poss)
        aspirate(ham_int, aspiration_poss, asp_vols, liquidClass = liq_class)
        for dispense_column in dispense_columns:
            dispense(ham_int, dispense_column, disp_vols, liquidClass = liq_class)
        tip_eject(ham_int, tips_poss)
2 Likes