class ProtocolTest(unittest.TestCase):

    def setUp(self):
        self.protocol = Protocol()
        self.maxDiff = None

    @property
    def instructions(self):
        return self.protocol._commands

    def test_normalize_address(self):
        self.protocol.add_container('A1', 'microplate.96', label="Output")
        label = self.protocol._normalize_address('Output:A1')
        self.assertEqual(label, ((0, 0), (0, 0)))
        slot = self.protocol._normalize_address('A1:A1')
        self.assertEqual(slot, ((0, 0), (0, 0)))

    def test_info(self):
        name = "Foo Bar"
        desc = "Lorem ipsum dolor set amet."
        auth = "Jane Doe"
        self.protocol.set_info(name=name, description=desc, author=auth)
        i = self.protocol.info
        self.assertEqual(i['name'], name)
        self.assertEqual(i['description'], desc)
        self.assertEqual(i['author'], auth)
        self.assertTrue('created' in i)
        self.assertTrue('updated' in i)

    def test_humanize_address(self):
        self.protocol.add_container("A1", 'microplate.96', label="LaBeL")
        with self.assertRaises(x.ContainerConflict):
            self.protocol.add_container("A2", 'microplate.96', label="label")
        self.protocol.add_container("A2", 'microplate.96', label="stuff")
        lA1 = self.protocol.humanize_address(('label', 'A1'))
        sA1 = self.protocol.humanize_address(('STUFF', 'A1'))
        self.assertEqual(lA1, 'LaBeL:A1')
        self.assertEqual(sA1, 'stuff:A1')

    def test_transfer(self):
        """ Basic transfer. """
        self.protocol.add_container('A1', 'microplate.96')
        self.protocol.add_container('B1', 'microplate.96')
        self.protocol.add_instrument('A', 'p200')
        self.protocol.add_instrument('B', 'p20')
        self.protocol.transfer('A1:A1', 'B1:B1', ul=100, tool='p200')
        expected = [{
            'command': 'transfer',
            'tool': 'p200',
            'volume': 100,
            'start': ((0, 0), (0, 0)),
            'end': ((1, 0), (1, 0)),
            'blowout': True,
            'touchtip': True
        }]
        self.assertEqual(self.instructions, expected)

    def test_transfer_without_pipette(self):
        self.protocol.add_container('A1', 'microplate.96')
        with self.assertRaises(x.InstrumentMissing):
            self.protocol.transfer('A1:A1', 'A1:A2', ul=10)

    def test_transfer_without_volume(self):
        self.protocol.add_instrument('A', 'p200')
        self.protocol.add_container('A1', 'microplate.96')
        with self.assertRaises(ValueError):
            self.protocol.transfer("A1:A1", "A1:A1")

    def test_transfer_zero_volume(self):
        with self.assertRaises(ValueError):
            self.protocol.transfer("A1:A1", "A1:A1", ul=0)
        with self.assertRaises(ValueError):
            self.protocol.transfer("A1:A1", "A1:A1", ml=0)

    def test_transfer_conflicting_volume(self):
        with self.assertRaises(ValueError):
            self.protocol.transfer("A1:A1", "A1:A1", ul=1, ml=1)

    def test_transfer_group(self):
        """ Transfer group. """
        expected = [{
            'command': 'transfer_group',
            'tool': 'p20',
            'transfers': [
                {
                    'volume': 15,
                    'start': ((0, 0), (0, 0)),  # A1:A1
                    'end': ((0, 0), (1, 0)),  # A1:B1
                    'blowout': True,
                    'touchtip': True
                },
                {
                    'volume': 10,
                    'start': ((0, 0), (0, 1)),  # A1:A2
                    'end': ((0, 0), (1, 1)),  # A1:B2
                    'blowout': True,
                    'touchtip': True
                },
                {
                    'volume': 12,
                    'start': ((0, 0), (0, 2)),  # A1:A3
                    'end': ((0, 0), (1, 2)),  # A1:B3
                    'blowout': False,
                    'touchtip': True
                },
                {
                    'volume': 12,
                    'start': ((0, 0), (0, 3)),  # A1:A4
                    'end': ((0, 0), (1, 3)),  # A1:B4
                    'blowout': True,
                    'touchtip': True
                },
                {
                    'volume': 12,
                    'start': ((0, 0), (0, 4)),  # A1:A5
                    'end': ((0, 0), (1, 4)),  # A1:B5
                    'blowout': True,
                    'touchtip': True
                }
            ]
        }]
        self.protocol.add_container('A1', 'microplate.96', label="Label")
        self.protocol.add_instrument('A', 'p20')
        self.protocol.transfer_group(
            ('A1:A1', 'A1:B1', {'ul': 15}),
            ('A1:A2', 'A1:B2', {'ul': 10}),
            ('A1:A3', 'A1:B3', {'blowout': False}),
            ('A1:A4', 'A1:B4'),
            ('A1:A5', 'A1:B5'),
            ul=12,
            tool='p20'
        )
        self.assertEqual(self.instructions, expected)

    def test_transfer_group_without_pipette(self):
        self.protocol.add_instrument('A', 'p200')
        self.protocol.add_container('A1', 'microplate.96')
        with self.assertRaises(x.InstrumentMissing):
            self.protocol.transfer_group(
                ('A1:A1', 'A1:B1', {'ul': 15}),
                ('A1:A2', 'A1:B2', {'ml': 1}),
                ('A1:A3', 'A1:B3', {'blowout': False}),
                ('A1:A4', 'A1:B4'),
                ('A1:A5', 'A1:B5'),
                ul=12,
                tool='p10'
            )

    def test_transfer_group_without_volume(self):
        self.protocol.add_container('A1', 'microplate.96')
        with self.assertRaises(ValueError):
            self.protocol.add_instrument('A', 'p10')
            self.protocol.transfer_group(
                ('A1:A1', 'A1:B1'),
                ('A1:A2', 'A1:B2'),
                ('A1:A3', 'A1:B3', {'blowout': False}),
                ('A1:A4', 'A1:B4'),
                ('A1:A5', 'A1:B5'),
                tool='p10'
            )

    def test_transfer_group_zero_volume(self):
        self.protocol.add_instrument('A', 'p10')
        self.protocol.add_container('A1', 'microplate.96')
        with self.assertRaises(ValueError):
            self.protocol.transfer_group(
                ('A1:A1', 'A1:B1'),
                ('A1:A2', 'A1:B2'),
                ('A1:A3', 'A1:B3', {'blowout': False}),
                ('A1:A4', 'A1:B4'),
                ('A1:A5', 'A1:B5'),
                ul=0,
                tool='p10'
            )
        with self.assertRaises(ValueError):
            self.protocol.transfer_group(
                ('A1:A1', 'A1:B1'),
                ('A1:A2', 'A1:B2'),
                ('A1:A3', 'A1:B3', {'blowout': False}),
                ('A1:A4', 'A1:B4'),
                ('A1:A5', 'A1:B5'),
                ml=0,
                tool='p10'
            )

    def test_transfer_group_conflicting_volume(self):
        self.protocol.add_instrument('A', 'p10')
        self.protocol.add_container('A1', 'microplate.96')
        with self.assertRaises(ValueError):
            self.protocol.transfer_group(
                ('A1:A1', 'A1:B1'),
                ('A1:A2', 'A1:B2'),
                ('A1:A3', 'A1:B3', {'blowout': False}),
                ('A1:A4', 'A1:B4'),
                ('A1:A5', 'A1:B5'),
                ul=5,
                ml=4,
                tool='p10'
            )

    def test_distribute(self):
        self.protocol.add_instrument('A', 'p200')
        self.protocol.add_container('A1', 'microplate.96')
        self.protocol.distribute(
            'A1:A1',
            ('A1:B1', {'ul': 50}),
            ('A1:C1'),
            ('A1:D1', {'ul': 30}),
            ul=20
        )
        expected = [{
            'command': 'distribute',
            'tool': 'p200',
            'start': ((0, 0), (0, 0)),
            'transfers': [
                {
                    'volume': 50,
                    'end': ((0, 0), (1, 0)),  # A1:B1
                    'blowout': True,
                    'touchtip': True
                },
                {
                    'volume': 20,  # Default
                    'end': ((0, 0), (2, 0)),  # A1:C1
                    'blowout': True,
                    'touchtip': True
                },
                {
                    'volume': 30,
                    'end': ((0, 0), (3, 0)),  # A1:D1
                    'blowout': True,
                    'touchtip': True
                }
            ]
        }]
        self.assertEqual(self.instructions, expected)

    def test_distribute_without_pipette(self):
        self.protocol.add_container('A1', 'microplate.96')
        with self.assertRaises(x.InstrumentMissing):
            self.protocol.distribute(
                'A1:A1',
                ('A1:B1', {'ul': 50}),
                ('A1:C1', {'ul': 5}),
                ('A1:D1', {'ul': 10})
            )

    def test_distribute_zero_volume(self):
        self.protocol.add_instrument('A', 'p10')
        self.protocol.add_container('A1', 'microplate.96')
        with self.assertRaises(ValueError):
            self.protocol.distribute(
                'A1:A1',
                ('A1:B1', {'ul': 4}),
                ('A1:C1', {'ul': 5}),
                ('A1:D1', {'ul': 0})
            )

    def test_distribute_conflicting_volume(self):
        self.protocol.add_instrument('A', 'p10')
        self.protocol.add_container('A1', 'microplate.96')
        with self.assertRaises(ValueError):
            self.protocol.distribute(
                'A1:A1',
                ('A1:B1'),
                ('A1:C1'),
                ('A1:D1', {'ul': 10, 'ml': 5}),
                ul=5
            )

    def test_consolidate(self):
        """ Consolidate. """
        self.protocol.add_container('A1', 'microplate.96')
        self.protocol.add_instrument('A', 'p200')
        self.protocol.consolidate(
            'A1:A1',
            ('A1:B1', {'ul': 50}),
            ('A1:C1', {'ul': 25}),
            ('A1:D1'),
            ul=30
        )
        expected = [{
            'command': 'consolidate',
            'tool': 'p200',
            'end': ((0, 0), (0, 0)),
            'transfers': [
                {
                    'volume': 50,
                    'start': ((0, 0), (1, 0)),  # A1:B1
                    'blowout': True,
                    'touchtip': True
                },
                {
                    'volume': 25,
                    'start': ((0, 0), (2, 0)),  # A1:C1
                    'blowout': True,
                    'touchtip': True
                },
                {
                    'volume': 30,
                    'start': ((0, 0), (3, 0)), # A1:D1
                    'blowout': True,
                    'touchtip': True
                }
            ]
        }]
        self.assertEqual(self.instructions, expected)

    def test_consolidate_without_pipette(self):
        self.protocol.add_container('A1', 'microplate.96')
        with self.assertRaises(x.InstrumentMissing):
            self.protocol.consolidate(
                'A1:A1',
                ('A1:B1', {'ul': 50}),
                ('A1:C1', {'ul': 5}),
                ('A1:D1', {'ul': 10})
            )

    def test_consolidate_zero_volume(self):
        self.protocol.add_instrument('A', 'p10')
        self.protocol.add_container('A1', 'microplate.96')
        with self.assertRaises(ValueError):
            self.protocol.consolidate(
                'A1:A1',
                ('A1:B1', {'ul': 4}),
                ('A1:C1', {'ul': 5}),
                ('A1:D1', {'ul': 0})
            )

    def test_consolidate_conflicting_volume(self):
        self.protocol.add_instrument('A', 'p10')
        self.protocol.add_container('A1', 'microplate.96')
        with self.assertRaises(ValueError):
            self.protocol.consolidate(
                'A1:A1',
                ('A1:B1', {'ul': 5}),
                ('A1:C1', {'ul': 5}),
                ('A1:D1', {'ul': 10, 'ml': 5})
            )

    def test_mix(self):
        """ Mix. """
        self.protocol.add_container('A1', 'microplate.96')
        self.protocol.add_instrument('A', 'p200')
        self.protocol.mix('A1:A1', ul=50, repetitions=10)
        expected = [{
            'command': 'mix',
            'tool': 'p200',
            'start': ((0, 0), (0, 0)),  # A1:A1
            'blowout': True,
            'touchtip': True,
            'volume': 50,
            'reps': 10
        }]
        self.assertEqual(self.instructions, expected)

    def test_mix_without_pipette(self):
        self.protocol.add_container('A1', 'microplate.96')
        with self.assertRaises(x.InstrumentMissing):
            self.protocol.mix('A1:A1', ul=50, repetitions=10, tool='p200')

    def test_mix_without_volume(self):
        self.protocol.add_container('A1', 'microplate.96')
        self.protocol.add_instrument('A', 'p10')
        with self.assertRaises(ValueError):
            self.protocol.mix('A1:A1', repetitions=10, tool='p10')

    def test_mix_zero_volume(self):
        self.protocol.add_instrument('A', 'p10')
        self.protocol.add_container('A1', 'microplate.96')
        with self.assertRaises(ValueError):
            self.protocol.mix('A1:A1', ul=0, repetitions=10, tool='p10')
        with self.assertRaises(ValueError):
            self.protocol.mix('A1:A1', ml=0, repetitions=10, tool='p10')

    def test_mix_conflicting_volume(self):
        self.protocol.add_instrument('A', 'p10')
        self.protocol.add_container('A1', 'microplate.96')
        with self.assertRaises(ValueError):
            self.protocol.mix('A1:A1', ul=1, ml=1, repetitions=10, tool='p10')

    def test_protocol_run_twice(self):
        """ Run a protocol twice without error. """
        self.protocol.add_instrument('A', 'p200')
        self.protocol.add_container('C1', 'tiprack.p200')
        self.protocol.add_container('A1', 'microplate.96')
        self.protocol.calibrate('A1', x=1, y=2, z=3)
        self.protocol.calibrate_instrument('A', top=0, blowout=10)
        self.protocol.transfer('A1:A1', 'A1:A2', ul=100)
        self.protocol.transfer('A1:A2', 'A1:A3', ul=80)
        self.protocol.run_all()
        self.protocol.run_all()

    def test_protocol_equality(self):
        # Set up a protocol.
        p1 = Protocol()
        p1.add_instrument('A', 'p200')
        p1.add_container('C1', 'tiprack.p200')
        p1.add_container('A1', 'microplate.96')
        p1.calibrate('A1', x=1, y=2, z=3)
        p1.calibrate_instrument('A', top=0, blowout=10)
        p1.transfer('A1:A1', 'A1:A2', ul=100)
        p1.transfer('A1:A2', 'A1:A3', ul=80)

        # And a copy.
        p2 = Protocol()
        p2.add_container('A1', 'microplate.96')
        p2.add_container('C1', 'tiprack.p200')
        p2.add_instrument('A', 'p200')
        p2.calibrate('A1', x=1, y=2, z=3)
        p2.calibrate_instrument('A', top=0, blowout=10)
        p2.transfer('A1:A1', 'A1:A2', ul=100)
        p2.transfer('A1:A2', 'A1:A3', ul=80)

        # They're identical.
        self.assertEqual(p1, p2)

        # Make a change.
        p2.add_instrument('B', 'p10')

        # No longer identical.
        self.assertNotEqual(p1, p2)

    def test_protocol_version(self):
        # Set up a protocol.
        self.protocol.add_instrument('A', 'p200')
        self.protocol.add_container('C1', 'tiprack.p200')
        self.protocol.add_container('A1', 'microplate.96')
        self.protocol.calibrate('A1', x=1, y=2, z=3)
        self.protocol.calibrate_instrument('A', top=0, blowout=10)
        self.protocol.transfer('A1:A1', 'A1:A2', ul=100)
        self.protocol.transfer('A1:A2', 'A1:A3', ul=80)

        # First version bump.
        v1 = self.protocol.bump_version()
        self.assertEqual(v1, '0.0.1')

        # No changes, version will stay the same.
        v2 = self.protocol.bump_version()
        self.assertEqual(v1, v2)

        # Make a change, bump the version.
        self.protocol.transfer('A1:A2', 'A1:A3', ul=80)
        v3 = self.protocol.bump_version()
        self.assertEqual(v3, '0.0.2')

        # Make a change, bump the version.
        self.protocol.transfer('A1:A1', 'A1:A1', ul=20)
        v4 = self.protocol.bump_version('feature')
        self.assertEqual(v4, '0.1.0')

        # Make a change, bump the version.
        self.protocol.transfer('A1:A1', 'A1:A1', ul=20)
        v5 = self.protocol.bump_version('major')
        self.assertEqual(v5, '1.0.0')

    def test_partial_protocol(self):
        p = Protocol.partial()
        p.transfer('A1:A1', 'A1:A3', ul=1)
        # As long as it doesn't throw an Exception, we're good.

    def test_partial_protocol_run(self):
        p = Protocol.partial()
        p.transfer('A1:A1', 'A1:A3', ul=1)
        with self.assertRaises(x.PartialProtocolException):
            # This shouldn't run because it's not valid.
            p.run_all()

    def test_valid_partial_protocol_run(self):
        p = Protocol.partial()
        p.add_instrument('A', 'p10')
        p.add_container("A1", "microplate.96")
        p.transfer('A1:A1', 'A1:A3', ul=1)
        # This should run because there are no Partial problems.
        p.run_all()

    def test_partial_protocol_export(self):
        p = Protocol.partial()
        p.transfer('A1:A1', 'A1:A3', ul=1)
        with self.assertRaises(x.PartialProtocolException):
            # This shouldn't export because it's not valid.
            p.export(JSONFormatter)

    def test_valid_partial_protocol_export(self):
        p = Protocol.partial()
        p.add_instrument('A', 'p10')
        p.add_container("A1", "microplate.96")
        p.transfer('A1:A1', 'A1:A3', ul=1)
        # This should export because there are no Partial problems.
        p.export(JSONFormatter)

    def test_protocol_addition_of_partial(self):
        p1 = Protocol()
        p1.add_instrument('A', 'p20')
        p1.add_container('A1', 'microplate.96')
        p1.transfer('A1:A1', 'A1:A2', ul=10)

        p2 = Protocol.partial()
        p2.add_container('A2', 'microplate.96')
        p2.transfer('A1:A1', 'A1:A3', ul=20)
        p2.transfer('A2:A1', 'A2:A4', ul=15)

        p3 = Protocol()
        p3.add_instrument('A', 'p20')
        p3.add_container('A1', 'microplate.96')
        p3.add_container('A2', 'microplate.96')
        p3.transfer('A1:A1', 'A1:A2', ul=10)
        p3.transfer('A1:A1', 'A1:A3', ul=20)
        p3.transfer('A2:A1', 'A2:A4', ul=15)

        self.assertEqual(p3, p1 + p2)

    def test_protocol_addition_of_partials(self):
        p1 = Protocol()
        p1.add_instrument('A', 'p20')
        p1.add_container('A1', 'microplate.96')
        p1.transfer('A1:A1', 'A1:A2', ul=10)

        p2 = Protocol.partial()
        p2.add_container('A2', 'microplate.96')
        p2.transfer('A1:A1', 'A1:A3', ul=20)
        p2.transfer('A2:A1', 'A2:A4', ul=15)

        p3 = Protocol.partial()
        p3.add_container('A3', 'microplate.96')
        p3.transfer('A1:A1', 'A1:A3', ul=20)
        p3.transfer('A1:A1', 'A3:A4', ul=15)

        p4 = Protocol()
        p4.add_instrument('A', 'p20')
        p4.add_container('A1', 'microplate.96')
        p4.add_container('A2', 'microplate.96')
        p4.add_container('A3', 'microplate.96')
        p4.transfer('A1:A1', 'A1:A2', ul=10)
        p4.transfer('A1:A1', 'A1:A3', ul=20)
        p4.transfer('A2:A1', 'A2:A4', ul=15)
        p4.transfer('A1:A1', 'A1:A3', ul=20)
        p4.transfer('A1:A1', 'A3:A4', ul=15)

        self.assertEqual(p4, p1 + p2 + p3)

    def test_protocol_added_to_partial(self):
        p1 = Protocol()
        p1.add_instrument('A', 'p20')
        p1.add_container('A1', 'microplate.96')
        p1.transfer('A1:A1', 'A1:A2', ul=10)

        p2 = Protocol.partial()
        p2.add_container('A2', 'microplate.96')
        p2.transfer('A1:A1', 'A1:A3', ul=20)
        p2.transfer('A2:A1', 'A2:A4', ul=15)

        with self.assertRaises(TypeError):
            p2 + p1

    def test_protocol_addition(self):
        p1 = Protocol()
        p1.add_instrument('A', 'p10')
        p1.add_container('A1', 'microplate.96')
        p1.transfer('A1:A1', 'A1:A1', ul=10)

        p2 = Protocol()
        p2.add_instrument('A', 'p10')  # Same definition; no conflict.
        p2.add_instrument('B', 'p20')  # New instrument.
        p2.add_container('A1', 'microplate.96')  # No conflict.
        p2.add_container('A2', 'microplate.96')  # New container.
        p2.transfer('A1:A1', 'A1:A1', ul=12)
        p2.transfer('A1:A1', 'A2:A2', ul=20)

        p3 = Protocol()
        p3.add_instrument('A', 'p10')
        p3.add_instrument('B', 'p20')
        p3.add_container('A1', 'microplate.96')
        p3.add_container('A2', 'microplate.96')
        p3.transfer('A1:A1', 'A1:A1', ul=10)
        p3.transfer('A1:A1', 'A1:A1', ul=12)
        p3.transfer('A1:A1', 'A2:A2', ul=20)

        self.assertEqual(p3, p1 + p2)

    def test_protocol_label_addition(self):
        p1 = Protocol()
        p1.add_instrument('A', 'p10')
        p1.add_container('A1', 'microplate.96', label="Input")
        p1.transfer('A1:A1', 'A1:A1', ul=10)

        p2 = Protocol()
        p2.add_instrument('A', 'p10')  # Same definition; no conflict.
        p2.add_instrument('B', 'p20')  # New instrument.
        p2.add_container('A1', 'microplate.96', label="Input")  # No conflict.
        p2.add_container('A2', 'microplate.96')  # New container.
        p2.transfer('A1:A1', 'A1:A1', ul=9)
        p2.transfer('Input:A1', 'A2:A2', ul=20)

        p3 = Protocol()
        p3.add_instrument('A', 'p10')
        p3.add_instrument('B', 'p20')
        p3.add_container('A1', 'microplate.96', label="Input")
        p3.add_container('A2', 'microplate.96')
        p3.transfer('A1:A1', 'A1:A1', ul=10)
        p3.transfer('A1:A1', 'A1:A1', ul=9)
        p3.transfer('A1:A1', 'A2:A2', ul=20)

        self.assertEqual(p3, p1 + p2)

    def test_protocol_addition_label_conflict(self):
        p1 = Protocol()
        p1.add_instrument('A', 'p10')
        p1.add_container('A1', 'microplate.96', label="Input")
        p1.transfer('A1:A1', 'A1:A1', ul=10)

        p2 = Protocol()
        p2.add_instrument('A', 'p10')  # Same definition; no conflict.
        p2.add_instrument('B', 'p20')  # New instrument.
        p2.add_container('A1', 'microplate.96', label="Output")  # Conflict.
        p2.add_container('A2', 'microplate.96')  # New container.
        p2.transfer('A1:A1', 'A1:A1', ul=12)
        p2.transfer('Output:A1', 'A2:A2', ul=20)

        with self.assertRaises(x.ContainerConflict):
            p1 + p2

    def test_protocol_addition_label_case(self):
        p1 = Protocol()
        p1.add_instrument('A', 'p10')
        p1.add_container('A1', 'microplate.96', label="Input")

        p2 = Protocol()
        p2.add_instrument('A', 'p10')
        p2.add_container('A1', 'microplate.96', label="INPUT")

        p3 = Protocol()
        p3.add_instrument('A', 'p10')
        p3.add_container('A1', 'microplate.96', label="INPUT")

        self.assertEqual((p1 + p2), p3)

    def test_protocol_addition_info(self):
        p1 = Protocol()
        p1.set_info(author="John Doe", name="Lorem Ipsum")

        p2 = Protocol()
        p2.set_info(author="Jane Doe")

        p3 = p1 + p2

        self.assertEqual('Jane Doe', p3.info['author'])
        self.assertEqual('Lorem Ipsum', p3.info['name'])

    def test_protocol_addition_container_conflict(self):
        p1 = Protocol()
        p1.add_instrument('A', 'p10')
        p1.add_container('A1', 'microplate.96')
        p1.transfer('A1:A1', 'A1:A2', ul=10)

        p2 = Protocol()
        p2.add_container('A1', 'tiprack.p20')

        with self.assertRaises(x.ContainerConflict):
            p1 + p2
class ProtocolRequirementsTest(unittest.TestCase):
    def setUp(self):
        self.protocol = Protocol()

    def assertRequirements(self, expected, reqs=None):
        reqs = reqs or self.protocol.run_requirements
        ouch = []
        extra = deepcopy(reqs)
        missing = deepcopy(expected)
        for ei, e in enumerate(expected):
            for ri, r in enumerate(reqs):
                try:
                    self.assertEqual(e, r)
                except AssertionError:
                    continue
                extra[ri] = None
                missing[ei] = None
        extra = [x for x in extra if x is not None]
        missing = [x for x in missing if x is not None]
        if len(extra) > 0:
            ouch.append("Found {} extra items: {}".format(len(extra), extra))
        if len(missing) > 0:
            ouch.append("Missing items: {}".format(missing))
        if len(ouch) > 0:
            assert False, "\n".join(ouch)

    def test_requirements_assertion(self):
        self.assertRequirements([{
            'foo': 'bar'
        }, {
            'bizz': 'buzz'
        }], [{
            'foo': 'bar'
        }, {
            'bizz': 'buzz'
        }])
        with self.assertRaises(AssertionError):
            self.assertRequirements([{
                'foo': 'bar'
            }], [{
                'foo': 'bar'
            }, {
                'bizz': 'buzz'
            }])
        with self.assertRaises(AssertionError):
            self.assertRequirements([{
                'foo': 'bar'
            }, {
                'bizz': 'buzz'
            }], [{
                'foo': 'bar'
            }])

    def test_require_instrument_calibration(self):
        self.protocol.add_instrument('A', 'p20')
        self.protocol.add_container('A1', 'microplate')
        self.protocol.add_container('A2', 'tiprack.p20')
        self.protocol.transfer('A1:A1', 'A1:A2', ul=10)
        reqs = [{
            'type': 'calibrate_instrument',
            'axis': 'A',
            'instrument_name': 'p20'
        }, {
            'type': 'calibrate_container',
            'axis': 'A',
            'address': (0, 0),
            'container_name': 'microplate',
            'instrument_name': 'p20'
        }, {
            'type': 'calibrate_container',
            'axis': 'A',
            'address': (0, 1),
            'container_name': 'tiprack.p20',
            'instrument_name': 'p20'
        }]
        self.assertRequirements(reqs)
        self.protocol.calibrate_instrument(axis='A',
                                           top=10,
                                           bottom=10,
                                           blowout=10)
        reqs.pop(0)
        self.assertRequirements(reqs)
        self.protocol.calibrate('A1', axis='A')
        reqs.pop(0)
        self.assertRequirements(reqs)
        self.protocol.calibrate('A2', axis='A')
        reqs.pop(0)
        self.assertRequirements(reqs)

    def test_requirements_calibration_multiple_racks(self):
        self.protocol.add_instrument('A', 'p20')
        self.protocol.add_container('A1', 'microplate')
        self.protocol.add_container('A2', 'tiprack.p20')
        self.protocol.add_container('A3', 'tiprack.p20')
        for _ in range(50):
            self.protocol.transfer('A1:A1', 'A1:A2', ul=10)
            self.protocol.transfer('A1:A2', 'A1:A1', ul=10)
        reqs = [{
            'type': 'calibrate_instrument',
            'axis': 'A',
            'instrument_name': 'p20'
        }, {
            'type': 'calibrate_container',
            'axis': 'A',
            'address': (0, 0),
            'container_name': 'microplate',
            'instrument_name': 'p20'
        }, {
            'type': 'calibrate_container',
            'axis': 'A',
            'address': (0, 1),
            'container_name': 'tiprack.p20',
            'instrument_name': 'p20'
        }, {
            'type': 'calibrate_container',
            'axis': 'A',
            'address': (0, 2),
            'container_name': 'tiprack.p20',
            'instrument_name': 'p20'
        }]
        self.assertRequirements(reqs)

    def test_no_errors_from_commands(self):
        self.protocol.add_instrument('B', 'p200')
        self.protocol.add_container('A1', 'microplate.96')
        self.protocol.add_container('B1', 'tiprack.p200')
        self.protocol.add_container('C1', 'point.trash')
        self.protocol.calibrate('A1')
        self.protocol.calibrate('B1')
        self.protocol.calibrate('C1')
        self.protocol.calibrate('A1', x=1, y=2, top=3, bottom=13)
        self.protocol.calibrate_instrument('B', top=0, blowout=10, droptip=25)
        self.protocol.transfer('A1:A1', 'A1:A2', ul=100)
        self.protocol.transfer_group(('A1:A1', 'A1:A2', {'ul': 100}))
        self.protocol.consolidate('A1:A1', ('A1:A2', {
            'ul': 100
        }), ('A1:A2', {
            'ul': 150
        }))
        self.protocol.distribute('A1:A1', ('A1:A2', {
            'ul': 100
        }), ('A1:A2', {
            'ul': 150
        }))
        self.protocol.mix('A1:A2', ul=100, repetitions=5)
        self.protocol.run_requirements
class ProtocolFormatterTest(unittest.TestCase):

    maxDiff = None

    json = """
    {
        "info": {
            "name": "Test Protocol",
            "author": "Michelle Steigerwalt",
            "description": "A protocol to test JSON output.",
            "created": "Thu Aug 11 20:19:55 2016",
            "updated": ""
        },
        "instruments": {
            "p10_a": {
                "axis": "A",
                "name": "p10"
            },
            "p200_b": {
                "axis": "B",
                "name": "p200"
            }
        },
        "containers": [
            {
                "name": "microplate.96",
                "label": "Ingredients",
                "slot": "A1"
            },
            {
                "name": "microplate.96",
                "label": "Output",
                "slot": "B1"
            }
        ],
        "instructions": [
            {
                "command": "transfer",
                "start": "Ingredients:A1",
                "end": "Output:B1",
                "volume": 10,
                "tool": "p10",
                "blowout": true,
                "touchtip": true
            },
            {
                "command": "transfer_group",
                "tool": "p10",
                "transfers": [
                    {
                        "start": "Ingredients:A3",
                        "end": "Output:B3",
                        "volume": 3,
                        "blowout": true,
                        "touchtip": true
                    },
                    {
                        "start": "Ingredients:A4",
                        "end": "Output:B4",
                        "volume": 10,
                        "blowout": true,
                        "touchtip": true
                    },
                    {
                        "start": "Ingredients:A5",
                        "end": "Output:C1",
                        "volume": 10,
                        "blowout": true,
                        "touchtip": true
                    }
                ]
            },
            {
                "command": "consolidate",
                "tool": "p10",
                "end": "Output:B3",
                "transfers": [
                    {
                        "start": "Ingredients:A3",
                        "volume": 3,
                        "blowout": true,
                        "touchtip": true
                    },
                    {
                        "start": "Ingredients:A4",
                        "volume": 10,
                        "blowout": true,
                        "touchtip": true
                    },
                    {
                        "start": "Ingredients:A5",
                        "volume": 10,
                        "blowout": true,
                        "touchtip": true
                    }
                ]
            },
            {
                "command": "distribute",
                "tool": "p10",
                "start": "Ingredients:A1",
                "transfers": [
                    {
                        "end": "Output:A1",
                        "volume": 3,
                        "blowout": true,
                        "touchtip": true
                    },
                    {
                        "end": "Output:A2",
                        "volume": 10,
                        "blowout": true,
                        "touchtip": true
                    },
                    {
                        "end": "Output:A3",
                        "volume": 10,
                        "blowout": true,
                        "touchtip": true
                    }
                ]
            },
            {
                "command": "mix",
                "start": "Output:A1",
                "volume": 50,
                "tool": "p200",
                "repetitions": 30,
                "blowout": true,
                "touchtip": true
            }
        ]
    }
    """

    def setUp(self):
        self.protocol = Protocol()
        self.stub_info = {
            'name': "Test Protocol",
            'description': "A protocol to test JSON output.",
            'author': "Michelle Steigerwalt",
            'created': "Thu Aug 11 20:19:55 2016"
        }
        # Same definitions as the protocol JSON above.
        self.protocol.set_info(**self.stub_info)
        self.protocol.add_instrument('A', 'p10')
        self.protocol.add_instrument('B', 'p200')
        self.protocol.add_container('A1', 'microplate.96', label="Ingredients")
        self.protocol.add_container('B1', 'microplate.96', label="Output")
        self.protocol.transfer('A1:A1', 'B1:B1', ul=10, tool='p10')
        self.protocol.transfer_group(
            ('A1:A3', 'B1:B3', {'ul': 3}),
            ('INGREDIENTS:A4', 'B1:B4'),
            ('A1:A5', 'B1:C1'),
            tool='p10',
            ul=10
        )
        self.protocol.consolidate(
            'Output:B3',
            ('A1:A3', {'ul': 3}),
            'INGREDIENTS:A4',
            'A1:A5',
            tool='p10',
            ul=10
        )
        self.protocol.distribute(
            'Ingredients:A1',
            ('Output:A1', {'ul': 3}),
            'Output:A2',
            'Output:A3',
            tool='p10',
            ul=10
        )
        self.protocol.mix('Output:A1', ul=50, repetitions=30)

    def test_json_export(self):
        result = json.loads(self.protocol.export(JSONFormatter))
        expected = json.loads(self.json)
        self.assertEqual(self.protocol.version, '0.0.1')
        for k, v in self.stub_info.items():
            self.assertEqual(v, result['info'][k])
        self.assertEqual(result["info"]["version"], self.protocol.version)
        self.assertEqual(result["info"]["version_hash"], self.protocol.hash)
        expected['info'] = ""
        result['info'] = ""
        self.assertEqual(expected, result)

    def test_invalid_json(self):
        with self.assertRaises(x.ContainerMissing):
            # This fails because there's no tiprack or trash.
            self.protocol.export(JSONFormatter, validate_run=True)

    def test_load_json(self):
        start = self.json
        f = JSONLoader(self.json)
        dump = f.protocol.export(JSONFormatter)
        result = json.loads(dump)
        expected = json.loads(start)
        expected['info'] = ""
        result['info'] = ""
        self.assertEqual(expected, result)  # ✨  OMG isomorphic! ✨

    def test_equal_hashing(self):
        p = JSONLoader(self.json).protocol
        # Hashes of all protocol run-related data within the JSON and manually
        # defined protcol are equal.
        self.assertEqual(self.protocol, p)
        # Make a modification of the original protocol.
        p.add_instrument('B', 'p20')
        # Hashes are different.
        self.assertNotEqual(self.protocol, p)
Exemple #4
0
class JSONLoader():

    _protocol = None

    def __init__(self, json_str):
        data = json.loads(json_str)
        self._protocol = Protocol()
        self._load_info(data['info'])
        self._load_containers(data['containers'])
        self._load_instruments(data['instruments'])
        self._load_instructions(data['instructions'])

    def _load_info(self, info):
        self._protocol.set_info(**info)

    def _load_instruments(self, instruments):
        for k, inst in instruments.items():
            self._protocol.add_instrument(inst['axis'], inst['name'])

    def _load_containers(self, deck):
        for container in deck:
            name = container.get('name', None)
            label = container.get('label', None)
            slot = container.get('slot', None)
            self._protocol.add_container(slot, name, label=label)

    def _load_instructions(self, instructions):
        for i in copy.deepcopy(instructions):
            command = i.pop('command')
            meth = getattr(self, '_load_{}_command'.format(command), None)
            if meth is None:
                raise KeyError("Can't unpack command: {}".format(command))
            meth(i)

    def _load_transfer_command(self, inst):
        volume = inst.pop('volume', None)
        inst['ul'] = volume
        start = inst.pop('start')
        end = inst.pop('end')
        self._protocol.transfer(start, end, **inst)

    def _load_mix_command(self, inst):
        volume = inst.pop('volume', None)
        inst['ul'] = volume
        start = inst.pop('start')
        self._protocol.mix(start, **inst)

    def _load_transfer_group_command(self, inst):
        transfers = []
        for t in inst.pop('transfers'):
            start = t.pop('start')
            end = t.pop('end')
            t['ul'] = t.pop('volume')
            transfers.append((start, end, t))
        self._protocol.transfer_group(*transfers, tool=inst['tool'])

    def _load_consolidate_command(self, inst):
        transfers = []
        for t in inst.pop('transfers'):
            start = t.pop('start')
            t['ul'] = t.pop('volume')
            transfers.append((start, t))
        self._protocol.consolidate(inst['end'], *transfers, tool=inst['tool'])

    def _load_distribute_command(self, inst):
        transfers = []
        for t in inst.pop('transfers'):
            start = t.pop('end')
            t['ul'] = t.pop('volume')
            transfers.append((start, t))
        self._protocol.distribute(inst['start'], *transfers, tool=inst['tool'])

    @property
    def protocol(self):
        return self._protocol
Exemple #5
0
class JSONLoader():

    _protocol = None

    def __init__(self, json_str):
        data = json.loads(json_str)
        self._protocol = Protocol()
        self._load_info(data['info'])
        self._load_containers(data['containers'])
        self._load_instruments(data['instruments'])
        self._load_instructions(data['instructions'])

    def _load_info(self, info):
        self._protocol.set_info(**info)

    def _load_instruments(self, instruments):
        for k, inst in instruments.items():
            self._protocol.add_instrument(inst['axis'], inst['name'])

    def _load_containers(self, deck):
        for container in deck:
            name = container.get('name', None)
            label = container.get('label', None)
            slot = container.get('slot', None)
            self._protocol.add_container(
                slot, name, label=label
            )

    def _load_instructions(self, instructions):
        for i in copy.deepcopy(instructions):
            command = i.pop('command')
            meth = getattr(self, '_load_{}_command'.format(command), None)
            if meth is None:
                raise KeyError("Can't unpack command: {}".format(command))
            meth(i)

    def _load_transfer_command(self, inst):
        volume = inst.pop('volume', None)
        inst['ul'] = volume
        start = inst.pop('start')
        end = inst.pop('end')
        self._protocol.transfer(start, end, **inst)

    def _load_mix_command(self, inst):
        volume = inst.pop('volume', None)
        inst['ul'] = volume
        start = inst.pop('start')
        self._protocol.mix(start, **inst)

    def _load_transfer_group_command(self, inst):
        transfers = []
        for t in inst.pop('transfers'):
            start = t.pop('start')
            end = t.pop('end')
            t['ul'] = t.pop('volume')
            transfers.append((start, end, t))
        self._protocol.transfer_group(*transfers, tool=inst['tool'])

    def _load_consolidate_command(self, inst):
        transfers = []
        for t in inst.pop('transfers'):
            start = t.pop('start')
            t['ul'] = t.pop('volume')
            transfers.append((start, t))
        self._protocol.consolidate(inst['end'], *transfers, tool=inst['tool'])

    def _load_distribute_command(self, inst):
        transfers = []
        for t in inst.pop('transfers'):
            start = t.pop('end')
            t['ul'] = t.pop('volume')
            transfers.append((start, t))
        self._protocol.distribute(inst['start'], *transfers, tool=inst['tool'])

    @property
    def protocol(self):
        return self._protocol
Exemple #6
0
class ProtocolTest(unittest.TestCase):

    def setUp(self):
        self.protocol = Protocol()

    @property
    def instructions(self):
        return self.protocol._commands

    def test_normalize_address(self):
        self.protocol.add_container('A1', 'microplate.96', label="Output")
        label = self.protocol._normalize_address('Output:A1')
        self.assertEqual(label, ('output', (0, 0)))
        slot = self.protocol._normalize_address('A1:A1')
        self.assertEqual(slot, ((0, 0), (0, 0)))

    def test_transfer(self):
        """ Basic transfer. """
        self.protocol.add_container('A1', 'microplate.96')
        self.protocol.add_container('B1', 'microplate.96')
        self.protocol.add_instrument('A', 'p200')
        self.protocol.add_instrument('B', 'p20')
        self.protocol.transfer('A1:A1', 'B1:B1', ul=100, tool='p20')
        expected = [{
            'transfer': {
                'tool': 'p20',
                'volume': 100,
                'start': ((0, 0), (0, 0)),
                'end': ((1, 0), (1, 0)),
                'blowout': True,
                'touchtip': True
            }
        }]
        self.assertEqual(self.instructions, expected)

    def test_transfer_group(self):
        """ Transfer group. """
        expected = [{
            'transfer_group': {
                'tool': 'p10',
                'transfers': [
                    {
                        'volume': 15,
                        'start': ((0, 0), (0, 0)),  # A1:A1
                        'end': ((1, 0), (1, 0)),  # B1:B1
                        'blowout': True,
                        'touchtip': True
                    },
                    {
                        'volume': 1000,
                        'start': ((0, 1), (0, 1)),  # A2:A2
                        'end': ((1, 1), (1, 1)),  # B2:B2
                        'blowout': True,
                        'touchtip': True
                    },
                    {
                        'volume': 12,
                        'start': ((0, 2), (0, 2)),  # A3:A3
                        'end': ((1, 2), (1, 2)),  # B3:B3
                        'blowout': False,
                        'touchtip': True
                    },
                    {
                        'volume': 12,
                        'start': ((0, 3), (0, 3)),  # A4:A4
                        'end': ((1, 3), (1, 3)),  # B4:B4
                        'blowout': True,
                        'touchtip': True
                    },
                    {
                        'volume': 12,
                        'start': ('label', (0, 4)),  # label:A5
                        'end': ((1, 4), (2, 0)),  # B5:C1
                        'blowout': True,
                        'touchtip': True
                    }
                ]
            }
        }]
        self.protocol.add_container('A1', 'microplate.96', label="Label")
        self.protocol.transfer_group(
            ('A1:A1', 'B1:B1', {'ul': 15}),
            ('A2:A2', 'B2:B2', {'ml': 1}),
            ('A3:A3', 'B3:B3', {'blowout': False}),
            ('A4:A4', 'B4:B4'),
            ('Label:A5', 'B5:C1'),
            ul=12,
            tool='p10'
        )
        self.assertEqual(self.instructions, expected)

    def test_distribute(self):
        self.protocol.distribute(
            'A1:A1',
            ('B1:B1', 50),
            ('C1:C1', 5),
            ('D1:D1', 10)
        )
        expected = [{
            'distribute': {
                'tool': 'p10',
                'blowout': True,
                'start': ((0, 0), (0, 0)),
                'transfers': [
                    {
                        'volume': 50,
                        'end': ((1, 0), (1, 0)),  # B1:B1
                    },
                    {
                        'volume': 5,
                        'end': ((2, 0), (2, 0)),  # C1:C1
                    },
                    {
                        'volume': 10,
                        'end': ((3, 0), (3, 0))  # D1:D1
                    }
                ]
            }
        }]
        self.assertEqual(self.instructions, expected)

    def test_consolidate(self):
        """ Consolidate. """
        self.protocol.consolidate(
            'A1:A1',
            ('B1:B1', 50),
            ('C1:C1', 5),
            ('D1:D1', 10)
        )
        expected = [{
            'consolidate': {
                'tool': 'p10',
                'blowout': True,
                'end': ((0, 0), (0, 0)),
                'transfers': [
                    {
                        'volume': 50,
                        'start': ((1, 0), (1, 0)),  # B1:B1
                    },
                    {
                        'volume': 5,
                        'start': ((2, 0), (2, 0)),  # C1:C1
                    },
                    {
                        'volume': 10,
                        'start': ((3, 0), (3, 0))  # D1:D1
                    }
                ]
            }
        }]
        self.assertEqual(self.instructions, expected)

    def test_mix(self):
        """ Mix. """
        self.protocol.mix(
            'A1:A1',
            volume=50,
            repetitions=10
        )
        expected = [{'mix': {
            'tool': 'p10',
            'start': ((0, 0), (0, 0)),  # A1:A1
            'blowout': True,
            'volume': 50,
            'reps': 10
        }}]
        self.assertEqual(self.instructions, expected)

    def test_protocol_run_twice(self):
        """ Run a protocol twice without error. """
        self.protocol.add_instrument('A', 'p200')
        self.protocol.add_container('C1', 'tiprack.p200')
        self.protocol.add_container('A1', 'microplate.96')
        self.protocol.calibrate('A1', x=1, y=2, z=3)
        self.protocol.calibrate_instrument('A', top=0, blowout=10)
        self.protocol.transfer('A1:A1', 'A1:A2', ul=100)
        self.protocol.transfer('A1:A2', 'A1:A3', ul=80)
        self.protocol.run_all()
        self.protocol.run_all()
class ContextHandlerTest(unittest.TestCase):

    def setUp(self):
        self.protocol = Protocol()

    def assertVolume(self, well, volume):
        result = self.protocol._context_handler.get_volume(well)
        self.assertEqual(volume, result)

    def test_transfer(self):
        """ Maintain well volumes during transfers. """
        self.protocol.add_container('A1', 'microplate.96')
        self.protocol.add_container('C1', 'tiprack.p200')
        self.protocol.add_instrument('A', 'p200')
        self.protocol.calibrate('A1', x=1, y=2, z=3)
        self.assertVolume('A1:A2', 0)
        self.protocol.transfer('A1:A1', 'A1:A2', ul=100)
        self.assertVolume('A1:A2', 100)
        self.protocol.transfer('A1:A2', 'A1:A3', ul=20)
        self.assertVolume('A1:A3', 20)
        self.assertVolume('A1:A2', 80)
        
        run = self.protocol.run()
        next(run)  # Yield to set progress.
        self.assertVolume('A1:A2', 0)
        next(run)  # transfer('A1:A1', 'A1:A2', ul=100)
        self.assertVolume('A1:A2', 100)
        next(run)  # transfer('A1:A2', 'A1:A3', ul=20)
        self.assertVolume('A1:A3', 20)
        self.assertVolume('A1:A2', 80)

    def test_distribute(self):
        self.protocol.add_container('A1', 'microplate.96')
        self.protocol.add_container('C1', 'tiprack.p200')
        self.protocol.add_instrument('A', 'p200')
        self.protocol.distribute(
            'A1:A1',
            ('A1:B1', {'ul': 50}),
            ('A1:C1', {'ul': 30}),
            ('A1:D1', {'ul': 40})
        )
        # Final volumes.
        self.assertVolume('A1:A1', -120)
        self.assertVolume('A1:B1', 50)
        self.assertVolume('A1:C1', 30)
        self.assertVolume('A1:D1', 40)
        
        # Try during a run.
        run = self.protocol.run()
        next(run)  # Yield to set progress.
        self.assertVolume('A1:A2', 0)
        next(run)  # Our command.

        # Final volumes
        self.assertVolume('A1:A1', -120)
        self.assertVolume('A1:B1', 50)
        self.assertVolume('A1:C1', 30)
        self.assertVolume('A1:D1', 40)

    def test_consolidate(self):
        self.protocol.add_container('A1', 'microplate.96')
        self.protocol.add_instrument('A', 'p200')
        self.protocol.consolidate(
            'A1:A1',
            ('A1:B1', {'ul': 50}),
            ('A1:C1', {'ul': 30}),
            ('A1:D1', {'ul': 40})
        )
        self.assertVolume('A1:A1', 120)
        self.assertVolume('A1:B1', -50)
        self.assertVolume('A1:C1', -30)
        self.assertVolume('A1:D1', -40)

    def test_transfer_group(self):
        self.protocol.add_container('A1', 'microplate.96')
        self.protocol.add_container('C1', 'tiprack.p200')
        self.protocol.add_instrument('A', 'p200')
        self.protocol.transfer_group(
            ('A1:A1', 'A1:B1', {'ul': 50}),
            ('A1:A1', 'A1:C1', {'ul': 50}),
            ('A1:A1', 'A1:D1', {'ul': 30}),
            tool='p200'
        )
        self.assertVolume('A1:A1', -130)
        self.assertVolume('A1:B1', 50)
        self.assertVolume('A1:C1', 50)
        self.assertVolume('A1:D1', 30)

    def test_find_instrument_by_volume(self):
        """ Find instrument by volume. """
        self.protocol.add_instrument('A', 'p10')
        i = self.protocol._context_handler.get_instrument(has_volume=6)
        self.assertEqual(i.supports_volume(6), True)
        j = self.protocol._context_handler.get_instrument(has_volume=50)
        self.assertEqual(j, None)
        self.protocol.add_instrument('B', 'p200')
        k = self.protocol._context_handler.get_instrument(has_volume=50)
        self.assertEqual(k.name, 'p200')

    def test_tip_coordinates(self):
        """ Return tip coordinates. """
        context = self.protocol._context_handler
        self.protocol.add_instrument('A', 'p200')
        self.protocol.add_instrument('B', 'p10')
        self.protocol.add_container('B1', 'tiprack.p200')
        self.protocol.calibrate('B1', axis="A", x=100, y=150, top=60)
        self.protocol.add_container('A1', 'tiprack.p10')
        self.protocol.calibrate('A1', axis="B", x=200, y=250, top=160)

        p200 = context.get_instrument(axis='A')
        p10 = context.get_instrument(axis='B')

        c1 = context.get_next_tip_coordinates(p200)
        self.assertEqual(c1, {'x': 100, 'y': 150, 'top': 60, 'bottom': 0})
        c2 = context.get_next_tip_coordinates(p200)  # Next tip.
        self.assertEqual(c2, {'x': 100, 'y': 159, 'top': 60, 'bottom': 0})

        c3 = context.get_next_tip_coordinates(p10)
        self.assertEqual(c3, {'x': 200, 'y': 250, 'top': 160, 'bottom': 0})
        c4 = context.get_next_tip_coordinates(p10)  # Next tip.
        self.assertEqual(c4, {'x': 200, 'y': 259, 'top': 160, 'bottom': 0})

    def test_tiprack_switch(self):
        """ Return second tiprack when first is used up. """
        context = self.protocol._context_handler
        self.protocol.add_instrument('A', 'p200')
        self.protocol.add_container('B1', 'tiprack.p200')
        self.protocol.calibrate('B1', axis="A", x=100, y=150, top=60)
        self.protocol.add_container('A1', 'tiprack.p200')
        self.protocol.calibrate('A1', axis="A", x=200, y=250, top=160)
        p200 = context.get_instrument(axis='A')
        rack = context.find_container(name='tiprack.p200', has_tips=True)
        self.assertEqual([(0, 0)], rack.address)
        rack.set_tips_used(95)  # We've used all but one tip from this rack.
        c1 = context.get_next_tip_coordinates(p200)  # Last tip.
        h12 = [(0, 0), (7, 11)]
        self.assertEqual(c1, context.get_coordinates(h12, axis="A"))
        rack = context.find_container(name='tiprack.p200', has_tips=True)
        self.assertEqual([(1, 0)], rack.address)

    def test_multichannel_search(self):
        """ Find a multichannel pipette. """
        context = self.protocol._context_handler
        self.protocol.add_instrument('A', 'p200')
        self.protocol.add_instrument('B', 'p20.12')
        i1 = context.get_instrument(axis='B')
        i2 = context.get_instrument(has_volume=20, channels=12)
        self.assertEqual(i1, i2)
        i3 = context.get_instrument(has_volume=200, channels=12)
        self.assertEqual(i3, None)

    def test_multichannel_tip_allocation(self):
        context = self.protocol._context_handler
        self.protocol.add_instrument('A', 'p20.12')
        self.protocol.add_instrument('B', 'p20.8')
        self.protocol.add_container('A1', 'tiprack.p20')
        a = context.get_instrument(axis='A')
        b = context.get_instrument(axis='B')
        self.protocol.calibrate('A1', axis="A")
        self.protocol.calibrate('A1', axis="B")
        # Get a row first.
        context.get_next_tip_coordinates(a)
        with self.assertRaises(x.TipMissing):
            context.get_next_tip_coordinates(b)
        # Get a col first.
        context = self.protocol.initialize_context()
        context.get_next_tip_coordinates(b)
        with self.assertRaises(x.TipMissing):
            context.get_next_tip_coordinates(a)
        # Add another tiprack, get both!
        self.protocol.add_container('A2', 'tiprack.p20')
        self.protocol.calibrate('A2', axis="A")
        self.protocol.calibrate('A2', axis="B")
        context = self.protocol.initialize_context()
        context.get_next_tip_coordinates(a)
        context.get_next_tip_coordinates(b)
        # Exhaust the supply.
        context = self.protocol.initialize_context()
        for _ in range(8):
            context.get_next_tip_coordinates(a)
        for _ in range(12):
            context.get_next_tip_coordinates(b)
        with self.assertRaises(x.TipMissing):
            context.get_next_tip_coordinates(a)
        with self.assertRaises(x.TipMissing):
            context.get_next_tip_coordinates(b)

    def test_multichannel_transfer_cols(self):
        """ Test multichannel transfer (cols). """
        self.protocol.add_instrument('A', 'p20.12')
        self.protocol.add_instrument('B', 'p20.8')
        self.protocol.add_container('A1', 'microplate')
        p = self.protocol._context_handler.find_container(name="microplate")
        self.protocol.transfer('A1:A1', 'A1:B1', ul=10, tool='p20.12')
        self.assertEqual(p.col('A').get_volume(), [-10 for n in range(12)])
        self.assertEqual(p.col('B').get_volume(), [ 10 for n in range(12)])

    def test_multichannel_transfer_rows(self):
        """ Test multichannel transfer (rows). """
        self.protocol.add_instrument('A', 'p20.12')
        self.protocol.add_instrument('B', 'p20.8')
        self.protocol.add_container('A1', 'microplate')
        p = self.protocol._context_handler.find_container(name="microplate")
        self.protocol.transfer('A1:A1', 'A1:A2', ul=15, tool='p20.8')
        self.assertEqual(p.row(0).get_volume(), [-15 for n in range(8)])
        self.assertEqual(p.row(1).get_volume(), [ 15 for n in range(8)])
class ProtocolRequirementsTest(unittest.TestCase):

    def setUp(self):
        self.protocol = Protocol()

    def assertRequirements(self, expected, reqs=None):
        reqs = reqs or self.protocol.run_requirements
        ouch = []
        extra = deepcopy(reqs)
        missing = deepcopy(expected)
        for ei, e in enumerate(expected):
            for ri, r in enumerate(reqs):
                try:
                    self.assertEqual(e, r)
                except AssertionError:
                    continue
                extra[ri] = None
                missing[ei] = None
        extra = [x for x in extra if x is not None]
        missing = [x for x in missing if x is not None]
        if len(extra) > 0:
            ouch.append("Found {} extra items: {}".format(len(extra), extra))
        if len(missing) > 0:
            ouch.append("Missing items: {}".format(missing))
        if len(ouch) > 0:
            assert False, "\n".join(ouch)

    def test_requirements_assertion(self):
        self.assertRequirements(
            [{'foo': 'bar'}, {'bizz': 'buzz'}],
            [{'foo': 'bar'}, {'bizz': 'buzz'}]
        )
        with self.assertRaises(AssertionError):
            self.assertRequirements(
                [{'foo': 'bar'}],
                [{'foo': 'bar'}, {'bizz': 'buzz'}]
            )
        with self.assertRaises(AssertionError):
            self.assertRequirements(
                [{'foo': 'bar'}, {'bizz': 'buzz'}],
                [{'foo': 'bar'}]
            )

    def test_require_instrument_calibration(self):
        self.protocol.add_instrument('A', 'p20')
        self.protocol.add_container('A1', 'microplate')
        self.protocol.add_container('A2', 'tiprack.p20')
        self.protocol.transfer('A1:A1', 'A1:A2', ul=10)
        reqs = [
            {'type': 'calibrate_instrument', 'axis': 'A', 'instrument_name': 'p20'},
            {'type': 'calibrate_container', 'axis': 'A', 'address': (0, 0),
             'container_name': 'microplate', 'instrument_name': 'p20'},
            {'type': 'calibrate_container', 'axis': 'A', 'address': (0, 1),
             'container_name': 'tiprack.p20', 'instrument_name': 'p20'}
        ]
        self.assertRequirements(reqs)
        self.protocol.calibrate_instrument(axis='A', top=10, bottom=10, blowout=10)
        reqs.pop(0)
        self.assertRequirements(reqs)
        self.protocol.calibrate('A1', axis='A')
        reqs.pop(0)
        self.assertRequirements(reqs)
        self.protocol.calibrate('A2', axis='A')
        reqs.pop(0)
        self.assertRequirements(reqs)

    def test_requirements_calibration_multiple_racks(self):
        self.protocol.add_instrument('A', 'p20')
        self.protocol.add_container('A1', 'microplate')
        self.protocol.add_container('A2', 'tiprack.p20')
        self.protocol.add_container('A3', 'tiprack.p20')
        for _ in range(50):
            self.protocol.transfer('A1:A1', 'A1:A2', ul=10)
            self.protocol.transfer('A1:A2', 'A1:A1', ul=10)
        reqs = [
            {'type': 'calibrate_instrument', 'axis': 'A', 'instrument_name': 'p20'},
            {'type': 'calibrate_container', 'axis': 'A', 'address': (0, 0),
             'container_name': 'microplate', 'instrument_name': 'p20'},
            {'type': 'calibrate_container', 'axis': 'A', 'address': (0, 1),
             'container_name': 'tiprack.p20', 'instrument_name': 'p20'},
             {'type': 'calibrate_container', 'axis': 'A', 'address': (0, 2),
             'container_name': 'tiprack.p20', 'instrument_name': 'p20'}
        ]
        self.assertRequirements(reqs)

    def test_no_errors_from_commands(self):
        self.protocol.add_instrument('B', 'p200')
        self.protocol.add_container('A1', 'microplate.96')
        self.protocol.add_container('B1', 'tiprack.p200')
        self.protocol.add_container('C1', 'point.trash')
        self.protocol.calibrate('A1')
        self.protocol.calibrate('B1')
        self.protocol.calibrate('C1')
        self.protocol.calibrate('A1', x=1, y=2, top=3, bottom=13)
        self.protocol.calibrate_instrument('B', top=0, blowout=10, droptip=25)
        self.protocol.transfer('A1:A1', 'A1:A2', ul=100)
        self.protocol.transfer_group(
            ('A1:A1', 'A1:A2', {'ul': 100})
        )
        self.protocol.consolidate(
            'A1:A1',
            ('A1:A2', {'ul':100}),
            ('A1:A2', {'ul':150})
        )
        self.protocol.distribute(
            'A1:A1',
            ('A1:A2', {'ul':100}),
            ('A1:A2', {'ul':150})
        )
        self.protocol.mix('A1:A2', ul=100, repetitions=5)
        self.protocol.run_requirements
Exemple #9
0
class ContextHandlerTest(unittest.TestCase):
    def setUp(self):
        self.protocol = Protocol()

    def assertVolume(self, well, volume):
        result = self.protocol._context_handler.get_volume(well)
        self.assertEqual(volume, result)

    def test_transfer(self):
        """ Maintain well volumes during transfers. """
        self.protocol.add_container('A1', 'microplate.96')
        self.protocol.add_container('C1', 'tiprack.p200')
        self.protocol.add_instrument('A', 'p200')
        self.protocol.calibrate('A1', x=1, y=2, z=3)
        self.assertVolume('A1:A2', 0)
        self.protocol.transfer('A1:A1', 'A1:A2', ul=100)
        self.assertVolume('A1:A2', 100)
        self.protocol.transfer('A1:A2', 'A1:A3', ul=20)
        self.assertVolume('A1:A3', 20)
        self.assertVolume('A1:A2', 80)

        run = self.protocol.run()
        next(run)  # Yield to set progress.
        self.assertVolume('A1:A2', 0)
        next(run)  # transfer('A1:A1', 'A1:A2', ul=100)
        self.assertVolume('A1:A2', 100)
        next(run)  # transfer('A1:A2', 'A1:A3', ul=20)
        self.assertVolume('A1:A3', 20)
        self.assertVolume('A1:A2', 80)

    def test_distribute(self):
        self.protocol.add_container('A1', 'microplate.96')
        self.protocol.add_container('C1', 'tiprack.p200')
        self.protocol.add_instrument('A', 'p200')
        self.protocol.distribute('A1:A1', ('A1:B1', {
            'ul': 50
        }), ('A1:C1', {
            'ul': 30
        }), ('A1:D1', {
            'ul': 40
        }))
        # Final volumes.
        self.assertVolume('A1:A1', -120)
        self.assertVolume('A1:B1', 50)
        self.assertVolume('A1:C1', 30)
        self.assertVolume('A1:D1', 40)

        # Try during a run.
        run = self.protocol.run()
        next(run)  # Yield to set progress.
        self.assertVolume('A1:A2', 0)
        next(run)  # Our command.

        # Final volumes
        self.assertVolume('A1:A1', -120)
        self.assertVolume('A1:B1', 50)
        self.assertVolume('A1:C1', 30)
        self.assertVolume('A1:D1', 40)

    def test_consolidate(self):
        self.protocol.add_container('A1', 'microplate.96')
        self.protocol.add_instrument('A', 'p200')
        self.protocol.consolidate('A1:A1', ('A1:B1', {
            'ul': 50
        }), ('A1:C1', {
            'ul': 30
        }), ('A1:D1', {
            'ul': 40
        }))
        self.assertVolume('A1:A1', 120)
        self.assertVolume('A1:B1', -50)
        self.assertVolume('A1:C1', -30)
        self.assertVolume('A1:D1', -40)

    def test_transfer_group(self):
        self.protocol.add_container('A1', 'microplate.96')
        self.protocol.add_container('C1', 'tiprack.p200')
        self.protocol.add_instrument('A', 'p200')
        self.protocol.transfer_group(('A1:A1', 'A1:B1', {
            'ul': 50
        }), ('A1:A1', 'A1:C1', {
            'ul': 50
        }), ('A1:A1', 'A1:D1', {
            'ul': 30
        }),
                                     tool='p200')
        self.assertVolume('A1:A1', -130)
        self.assertVolume('A1:B1', 50)
        self.assertVolume('A1:C1', 50)
        self.assertVolume('A1:D1', 30)

    def test_find_instrument_by_volume(self):
        """ Find instrument by volume. """
        self.protocol.add_instrument('A', 'p10')
        i = self.protocol._context_handler.get_instrument(has_volume=6)
        self.assertEqual(i.supports_volume(6), True)
        j = self.protocol._context_handler.get_instrument(has_volume=50)
        self.assertEqual(j, None)
        self.protocol.add_instrument('B', 'p200')
        k = self.protocol._context_handler.get_instrument(has_volume=50)
        self.assertEqual(k.name, 'p200')

    def test_tip_coordinates(self):
        """ Return tip coordinates. """
        context = self.protocol._context_handler
        self.protocol.add_instrument('A', 'p200')
        self.protocol.add_instrument('B', 'p10')
        self.protocol.add_container('B1', 'tiprack.p200')
        self.protocol.calibrate('B1', axis="A", x=100, y=150, top=60)
        self.protocol.add_container('A1', 'tiprack.p10')
        self.protocol.calibrate('A1', axis="B", x=200, y=250, top=160)

        p200 = context.get_instrument(axis='A')
        p10 = context.get_instrument(axis='B')

        c1 = context.get_next_tip_coordinates(p200)
        self.assertEqual(c1, {'x': 100, 'y': 150, 'top': 60, 'bottom': 0})
        c2 = context.get_next_tip_coordinates(p200)  # Next tip.
        self.assertEqual(c2, {'x': 100, 'y': 159, 'top': 60, 'bottom': 0})

        c3 = context.get_next_tip_coordinates(p10)
        self.assertEqual(c3, {'x': 200, 'y': 250, 'top': 160, 'bottom': 0})
        c4 = context.get_next_tip_coordinates(p10)  # Next tip.
        self.assertEqual(c4, {'x': 200, 'y': 259, 'top': 160, 'bottom': 0})

    def test_tiprack_switch(self):
        """ Return second tiprack when first is used up. """
        context = self.protocol._context_handler
        self.protocol.add_instrument('A', 'p200')
        self.protocol.add_container('B1', 'tiprack.p200')
        self.protocol.calibrate('B1', axis="A", x=100, y=150, top=60)
        self.protocol.add_container('A1', 'tiprack.p200')
        self.protocol.calibrate('A1', axis="A", x=200, y=250, top=160)
        p200 = context.get_instrument(axis='A')
        rack = context.find_container(name='tiprack.p200', has_tips=True)
        self.assertEqual([(0, 0)], rack.address)
        rack.set_tips_used(95)  # We've used all but one tip from this rack.
        c1 = context.get_next_tip_coordinates(p200)  # Last tip.
        h12 = [(0, 0), (7, 11)]
        self.assertEqual(c1, context.get_coordinates(h12, axis="A"))
        rack = context.find_container(name='tiprack.p200', has_tips=True)
        self.assertEqual([(1, 0)], rack.address)

    def test_multichannel_search(self):
        """ Find a multichannel pipette. """
        context = self.protocol._context_handler
        self.protocol.add_instrument('A', 'p200')
        self.protocol.add_instrument('B', 'p20.12')
        i1 = context.get_instrument(axis='B')
        i2 = context.get_instrument(has_volume=20, channels=12)
        self.assertEqual(i1, i2)
        i3 = context.get_instrument(has_volume=200, channels=12)
        self.assertEqual(i3, None)

    def test_multichannel_tip_allocation(self):
        context = self.protocol._context_handler
        self.protocol.add_instrument('A', 'p20.12')
        self.protocol.add_instrument('B', 'p20.8')
        self.protocol.add_container('A1', 'tiprack.p20')
        a = context.get_instrument(axis='A')
        b = context.get_instrument(axis='B')
        self.protocol.calibrate('A1', axis="A")
        self.protocol.calibrate('A1', axis="B")
        # Get a row first.
        context.get_next_tip_coordinates(a)
        with self.assertRaises(x.TipMissing):
            context.get_next_tip_coordinates(b)
        # Get a col first.
        context = self.protocol.initialize_context()
        context.get_next_tip_coordinates(b)
        with self.assertRaises(x.TipMissing):
            context.get_next_tip_coordinates(a)
        # Add another tiprack, get both!
        self.protocol.add_container('A2', 'tiprack.p20')
        self.protocol.calibrate('A2', axis="A")
        self.protocol.calibrate('A2', axis="B")
        context = self.protocol.initialize_context()
        context.get_next_tip_coordinates(a)
        context.get_next_tip_coordinates(b)
        # Exhaust the supply.
        context = self.protocol.initialize_context()
        for _ in range(8):
            context.get_next_tip_coordinates(a)
        for _ in range(12):
            context.get_next_tip_coordinates(b)
        with self.assertRaises(x.TipMissing):
            context.get_next_tip_coordinates(a)
        with self.assertRaises(x.TipMissing):
            context.get_next_tip_coordinates(b)

    def test_multichannel_transfer_cols(self):
        """ Test multichannel transfer (cols). """
        self.protocol.add_instrument('A', 'p20.12')
        self.protocol.add_instrument('B', 'p20.8')
        self.protocol.add_container('A1', 'microplate')
        p = self.protocol._context_handler.find_container(name="microplate")
        self.protocol.transfer('A1:A1', 'A1:B1', ul=10, tool='p20.12')
        self.assertEqual(p.col('A').get_volume(), [-10 for n in range(12)])
        self.assertEqual(p.col('B').get_volume(), [10 for n in range(12)])

    def test_multichannel_transfer_rows(self):
        """ Test multichannel transfer (rows). """
        self.protocol.add_instrument('A', 'p20.12')
        self.protocol.add_instrument('B', 'p20.8')
        self.protocol.add_container('A1', 'microplate')
        p = self.protocol._context_handler.find_container(name="microplate")
        self.protocol.transfer('A1:A1', 'A1:A2', ul=15, tool='p20.8')
        self.assertEqual(p.row(0).get_volume(), [-15 for n in range(8)])
        self.assertEqual(p.row(1).get_volume(), [15 for n in range(8)])
class MotorHandlerTest(unittest.TestCase):

    def setUp(self):
        self.protocol = Protocol()

    def test_basic_transfer(self):
        """ Basic transfer. """
        motor = self.protocol.attach_motor()
        output_log = motor._driver
        self.protocol.add_instrument('A', 'p20')
        self.protocol.add_instrument('B', 'p200')
        self.protocol.add_container('A1', 'microplate.96')
        self.protocol.add_container('C1', 'tiprack.p200')
        self.protocol.add_container('B1', 'tiprack.p20')
        self.protocol.add_container('B2', 'point.trash')
        self.protocol.calibrate('A1', axis="A", x=1, y=2, top=3, bottom=10)
        self.protocol.calibrate('B1', axis="A", x=4, y=5, top=6, bottom=15)
        self.protocol.calibrate('B2', axis="A", x=50, y=60, top=70)
        self.protocol.calibrate('A1:A2', axis="B", bottom=5)
        self.protocol.calibrate('C1', axis="B", x=100, y=100, top=50)
        self.protocol.calibrate('B2', axis="B", x=200, y=250, top=15)
        self.protocol.calibrate('A1', axis="B", x=1, y=2, top=3, bottom=13)
        self.protocol.calibrate_instrument('A', top=0, blowout=1, droptip=2)
        self.protocol.calibrate_instrument('B', top=0, blowout=10, droptip=25)
        self.protocol.transfer('A1:A1', 'A1:A2', ul=10)
        self.protocol.transfer('A1:A2', 'A1:A3', ul=80)
        prog_out = []
        for progress in self.protocol.run():
            prog_out.append(progress)
        expected = [
            # Transfer 1.
            {'x': 4, 'y': 5},  # Pickup tip.
            {'z': 6},
            {'z': 0},  # Move to well.
            {'x': 1, 'y': 2},
            {'z': 3},
            {'a': 0.5},  # Plunge.
            {'x': 1, 'y': 2},
            {'z': 10},  # Move into well.
            {'a': 0},  # Release.
            {'z': 0},  # Move up.
            {'x': 1, 'y': 11},  # Move to well.
            {'z': 3},
            {'x': 1, 'y': 11},
            {'z': 10},  # Move into well.
            {'a': 1},  # Blowout.
            {'z': 0},  # Move up.
            {'a': 0},  # Release.
            {'x': 50, 'y': 60},  # Dispose tip.
            {'z': 70},
            {'a': 2},
            {'a': 0},
            # Transfer 2.
            {'x': 100, 'y': 100},
            {'z': 50},
            {'z': 0},
            {'x': 1, 'y': 11},
            {'z': 3},
            {'b': 4.0},
            {'x': 1, 'y': 11},
            {'z': 5},
            {'b': 0},
            {'z': 0},
            {'x': 1, 'y': 20},
            {'z': 3},
            {'x': 1, 'y': 20},
            {'z': 13},
            {'b': 10},
            {'z': 0},
            {'b': 0},
            {'x': 200, 'y': 250},
            {'z': 15},
            {'b': 25},
            {'b': 0}
        ]
        self.assertEqual(expected, output_log.movements)
        self.assertEqual([(0, 2), (1, 2), (2, 2)], prog_out)

    def test_calibrate_without_axis(self):
        self.protocol.add_instrument('A', 'p20')
        self.protocol.add_instrument('B', 'p200')
        with self.assertRaises(x.DataMissing):
            self.protocol.calibrate('A1', top=0, bottom=0)

    def test_transfer_without_tiprack(self):
        """ Raise error when no tiprack found. """
        self.protocol.attach_motor()
        self.protocol.add_instrument('B', 'p200')
        self.protocol.add_container('A1', 'microplate.96')
        self.protocol.calibrate('A1', top=0, bottom=0)
        self.protocol.calibrate_instrument('B', top=0, blowout=10)
        self.protocol.transfer('A1:A1', 'A1:A2', ul=100)
        with self.assertRaises(x.ContainerMissing):
            self.protocol.run_all()

    def test_transfer_without_dispose_point(self):
        """ Raise when no dispose point set. """
        self.protocol.attach_motor()
        self.protocol.add_instrument('B', 'p200')
        self.protocol.add_container('A1', 'microplate.96')
        self.protocol.add_container('C1', 'tiprack.p200')
        self.protocol.calibrate('A1')
        self.protocol.calibrate('C1')
        self.protocol.calibrate_instrument('B', top=0, blowout=10)
        self.protocol.transfer('A1:A1', 'A1:A2', ul=100)
        self.protocol.transfer('A1:A2', 'A1:A3', ul=80)

        with self.assertRaises(x.ContainerMissing):
            self.protocol.run_all()

    def test_instrument_missing(self):
        with self.assertRaises(x.InstrumentMissing):
            m = self.protocol.attach_motor()
            m.get_pipette(has_volume=1000)

    def test_no_errors_from_commands(self):
        self.protocol.add_instrument('B', 'p200')
        self.protocol.add_container('A1', 'microplate.96')
        self.protocol.add_container('B1', 'tiprack.p200')
        self.protocol.add_container('C1', 'point.trash')
        self.protocol.calibrate('A1')
        self.protocol.calibrate('B1')
        self.protocol.calibrate('C1')
        self.protocol.calibrate('A1', x=1, y=2, top=3, bottom=13)
        self.protocol.calibrate_instrument('B', top=0, blowout=10, droptip=25)
        self.protocol.transfer('A1:A1', 'A1:A2', ul=100)
        self.protocol.transfer_group(
            ('A1:A1', 'A1:A2', {'ul': 100})
        )
        self.protocol.consolidate(
            'A1:A1',
            ('A1:A2', {'ul':100}),
            ('A1:A2', {'ul':150})
        )
        self.protocol.distribute(
            'A1:A1',
            ('A1:A2', {'ul':100}),
            ('A1:A2', {'ul':150})
        )
        self.protocol.mix('A1:A2', ul=100, repetitions=5)
        motor = self.protocol.attach_motor()
        output_log = motor._driver
        movements = output_log.movements

        # We're not really testing anything except that it runs without
        # errors.
        self.protocol.run_all()