autoprotocol/autoprotocol-python

enhancement request: mix_before and mix_after should only be applied once per set of unique dest and source wells

scottbecker opened this issue · 6 comments

Currently, running p.transfer(well1,well2,'1000.0:microliter',mix_before=True,one_tip=True) will result in two transfer operations, each with their own mix_before (see below)

        'transfer': [{
            'volume': '900.0:microliter',
            'to': 'my_plate/0',
            'from': 'culture_medium/0',
            'mix_before': {
                'volume': '900.0:microliter',
                'repetitions': 5,
                'speed': '900.0:microliter/second'
            }
        }, {
            'volume': '100.0:microliter',
            'to': 'my_plate/0',
            'from': 'culture_medium/0',
            'mix_before': {
                'volume': '900.0:microliter',
                'repetitions': 5,
                'speed': '900.0:microliter/second'
            }
        }]

This seems like a waste of a mix operation when the source has already been mixed just a few seconds before. I would expect the following autoprotocol instead (where only the first transfer has the mix):

        'transfer': [{
            'volume': '900.0:microliter',
            'to': 'my_plate/0',
            'from': 'culture_medium/0',
            'mix_before': {
                'volume': '900.0:microliter',
                'repetitions': 5,
                'speed': '900.0:microliter/second'
            }
        }, {
            'volume': '100.0:microliter',
            'to': 'my_plate/0',
            'from': 'culture_medium/0'
        }]

This also applies to mix_after, where only the last transfer in the set of matching from/to pairs should have the mix_after operation attached.

If it helps, here is a function I am currently using that removes these redundant mix operations in a given transfer group:

    def _remove_redundant_mixing(self, transfer_instruction_group):

        #cleanup mix_before
        dest_source_cache = {}
        for xfer in transfer_instruction_group:
            mix_key = self._refify(xfer['from'])+'-'+self._refify(xfer['to'])

            if mix_key in dest_source_cache and \
               'mix_before' in xfer:
                del xfer['mix_before']

            dest_source_cache[mix_key] = True

        #cleanup mix_after
        dest_source_cache = {}
        for xfer in transfer_instruction_group:
            mix_key =self._refify(xfer['from'])+'-'+self._refify(xfer['to'])

            if 'mix_after' in xfer:       
                dest_source_cache[mix_key] = xfer

        for xfer in transfer_instruction_group:
            if 'mix_after' in xfer and xfer not in dest_source_cache.values():
                del xfer['mix_after']  

I just realized that this gets more complicated because it also applies when one_tip=False. e.g. There are redundant mix_before operations here as well.

'groups': [{
    'transfer': [{
        'volume': '900.0:microliter',
        'to': 'my_plate/0',
        'from': 'culture_medium/0',
        'mix_before': {
            'volume': '900.0:microliter',
            'repetitions': 5,
            'speed': '900.0:microliter/second'
        }
    }]
}, {
    'transfer': [{
        'volume': '100.0:microliter',
        'to': 'my_plate/0',
        'from': 'culture_medium/0',
        'mix_before': {
            'volume': '900.0:microliter',
            'repetitions': 5,
            'speed': '900.0:microliter/second'
        }
    }]
}],

here is an updated version that cleans up an entire pipette instruction:

     def _remove_redundant_mixing(self, pipette_instruction_groups):

        transfer_groups = []
        for xfer_group in pipette_instruction_groups:
            if xfer_group.keys() == ['transfer']:
                transfer_groups+=xfer_group['transfer']

        #cleanup mix_before
        dest_source_cache = {}
        for xfer in transfer_groups:

            mix_key = self._refify(xfer['from'])+'-'+self._refify(xfer['to'])

            if mix_key in dest_source_cache and \
               'mix_before' in xfer:
                del xfer['mix_before']

            dest_source_cache[mix_key] = True

        #cleanup mix_after
        dest_source_cache = {}
        for xfer in transfer_groups:
            mix_key =self._refify(xfer['from'])+'-'+self._refify(xfer['to'])

            if 'mix_after' in xfer:       
                dest_source_cache[mix_key] = xfer

        for xfer in transfer_groups:
            if 'mix_after' in xfer and xfer not in dest_source_cache.values():
                del xfer['mix_after']  

I realize now that this fix may be too general for the more important/specific case of requesting to transfer more than 900ul and having redundant mix operations applied when the transfer gets split up into many smaller transfers. It may be wiser to just tackle this specific use case and not attempt to solve the more general case (which likely has edge case issues).

I made a new version that is better at cleaning up extra mixing when there are all transfer operations. The trick is recognizing that the task gets very simple when sources never become destinations and visa versa (meaning you don't need to remix). This could be enhanced further to do extra cleanup when distribute, consolidate, and mix operations are involved. It also removes a bug where the old version removed too many mix operations if you intermix consolidate/distribute between transfer steps

def _remove_redundant_mixing(self, pipette_instruction_groups):

        transfer_groups = []
        all_transfers = True
        for xfer_group in pipette_instruction_groups:
            if xfer_group.keys() == ['transfer']:
                transfer_groups+=xfer_group['transfer']
            else:
                all_transfers = False

        if not transfer_groups:
            return 

        if not all_transfers:
            #cleanup the transfer operations in blocks to prevent 
            # distribute and consolidate from causing bad behavior
            serial_transfer_groups = []
            for xfer_group in pipette_instruction_groups:
                if xfer_group.keys() == ['transfer']:
                    serial_transfer_groups.append(xfer_group)
                elif serial_transfer_groups:
                    self._remove_redundant_mixing(serial_transfer_groups)
                    serial_transfer_groups = []
            if serial_transfer_groups:
                self._remove_redundant_mixing(serial_transfer_groups)
                serial_transfer_groups = []            

            return

        #check that sources are always sources and dests are always dests (this means that we don't need to remix)
        sources = set()
        dests = set()
        for xfer in transfer_groups:
            sources.add(self._refify(xfer['from']))
            dests.add(self._refify(xfer['to']))

        #we can't optimize a set of transfer groups if a source becomes a dest or visa versa right now
        if lists_intersect(sources,dests):
            return

        #cleanup mix_before
        dest_source_cache = {}
        for xfer in transfer_groups:
            mix_key = self._refify(xfer['from'])

            if mix_key in dest_source_cache and \
               'mix_before' in xfer:
                del xfer['mix_before']

            dest_source_cache[mix_key] = True

        #cleanup mix_after
        dest_source_cache = {}
        for xfer in transfer_groups:
            mix_key = self._refify(xfer['to'])

            if 'mix_after' in xfer:       
                dest_source_cache[mix_key] = xfer

        for xfer in transfer_groups:
            if 'mix_after' in xfer and xfer not in dest_source_cache.values():
                del xfer['mix_after'] 

Hey @scottbecker so two points that raise some questions for me:

  1. Removing the redundant mix_before maybe an issue for fast sedimenting materials like beads.
  2. Often in the wet lab I would mix_after not because I necessarily wanted to mix the bulk solution in the tube but I wanted to ensure that the internal wall of the pipette tip had been sufficiently washed if transferring a small volume of solution.

Thanks @bmiles.

  1. good point, probably hard to make the extra mix removal the default then (since your probably can't auto-detect fast sedimenting)
  2. Really good point, I will update my function to skip removing mix_after's when the volume is <10uL.

I didn't expect this to necessarily become part of the standard lib, I mostly wanted to get your feedback, so thanks!