Skip to content

Commit 6759f1f

Browse files
authored
407 tip tracking prediction (#153)
* Pipette raises RuntimeWarning when the tip iterator runs out of tips * pylama * fixes JSON protocol error that used too many tips * WellSeries can take list of Wells for initializing * adds Pipette.start_at_tip(tip) * adds and tests for .start_at_tip(); adds method .current_tip() to set/get Pipette's tip
1 parent da5e3da commit 6759f1f

File tree

4 files changed

+72
-31
lines changed

4 files changed

+72
-31
lines changed

opentrons/containers/placeable.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,9 @@ class WellSeries(Placeable):
570570
"""
571571
def __init__(self, items):
572572
self.items = items
573-
self.values = list(self.items.values())
573+
if isinstance(items, dict):
574+
items = list(self.items.values())
575+
self.values = items
574576
self.offset = 0
575577

576578
def set_offset(self, offset):

opentrons/instruments/pipette.py

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def __init__(
8888

8989
self.trash_container = trash_container
9090
self.tip_racks = tip_racks
91+
self.starting_tip = None
9192

9293
# default mm above tip to execute drop-tip
9394
# this gives room for the drop-tip mechanism to work
@@ -180,20 +181,44 @@ def reset_tip_tracking(self):
180181
"""
181182
Resets the :any:`Pipette` tip tracking, "refilling" the tip racks
182183
"""
183-
self.current_tip_home_well = None
184+
self.current_tip(None)
184185
self.tip_rack_iter = iter([])
185186

186187
if self.has_tip_rack():
187188
iterables = self.tip_racks
188189

189190
if self.channels > 1:
190-
iterables = []
191-
for rack in self.tip_racks:
192-
iterables.append(rack.rows)
191+
iterables = [r for rack in self.tip_racks for r in rack.rows]
192+
else:
193+
iterables = [w for rack in self.tip_racks for w in rack]
194+
195+
if self.starting_tip:
196+
iterables = iterables[iterables.index(self.starting_tip):]
197+
198+
self.tip_rack_iter = itertools.chain(iterables)
193199

194-
self.tip_rack_iter = itertools.cycle(
195-
itertools.chain(*iterables)
196-
)
200+
def current_tip(self, *args):
201+
if len(args) and (isinstance(args[0], Placeable) or args[0] is None):
202+
self.current_tip_home_well = args[0]
203+
return self.current_tip_home_well
204+
205+
def start_at_tip(self, _tip):
206+
if isinstance(_tip, Placeable):
207+
self.starting_tip = _tip
208+
self.reset_tip_tracking()
209+
210+
def get_next_tip(self):
211+
next_tip = None
212+
if self.has_tip_rack():
213+
try:
214+
next_tip = next(self.tip_rack_iter)
215+
except StopIteration as e:
216+
raise RuntimeWarning(
217+
'{0} has run out of tips'.format(self.name))
218+
else:
219+
self.robot.add_warning(
220+
'pick_up_tip called with no reference to a tip')
221+
return next_tip
197222

198223
def _associate_placeable(self, location):
199224
"""
@@ -803,11 +828,11 @@ def _do():
803828
description=_description,
804829
enqueue=enqueue)
805830

806-
if not self.current_tip_home_well:
831+
if not self.current_tip():
807832
self.robot.add_warning(
808833
'Pipette has no tip to return, dropping in place')
809834

810-
self.drop_tip(self.current_tip_home_well, enqueue=enqueue)
835+
self.drop_tip(self.current_tip(), enqueue=enqueue)
811836

812837
return self
813838

@@ -860,17 +885,12 @@ def pick_up_tip(self, location=None, enqueue=True):
860885
def _setup():
861886
nonlocal location
862887
if not location:
863-
if self.has_tip_rack():
864-
# TODO: raise warning/exception if looped back to first tip
865-
location = next(self.tip_rack_iter)
866-
else:
867-
self.robot.add_warning(
868-
'pick_up_tip called with no reference to a tip')
869-
870-
self.current_tip_home_well = None
888+
location = self.get_next_tip()
889+
890+
self.current_tip(None)
871891
if location:
872892
placeable, _ = containers.unpack_location(location)
873-
self.current_tip_home_well = placeable
893+
self.current_tip(placeable)
874894

875895
if isinstance(location, Placeable):
876896
location = location.bottom()
@@ -959,7 +979,7 @@ def _setup():
959979
location = location.bottom(self._drop_tip_offset)
960980

961981
self._associate_placeable(location)
962-
self.current_tip_home_well = None
982+
self.current_tip(None)
963983

964984
self.current_volume = 0
965985

tests/opentrons/json_importer/protocol_data/lewistanner_auto_pcr.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,21 @@
1111
"labware": "tiprack-10ul",
1212
"slot": "B1"
1313
},
14+
"p10-rack-ST-2": {
15+
"labware": "tiprack-10ul",
16+
"slot": "B2"
17+
},
18+
"p10-rack-ST-3": {
19+
"labware": "tiprack-10ul",
20+
"slot": "B3"
21+
},
1422
"p200-rack": {
1523
"labware": "tiprack-200ul",
1624
"slot": "A2"
1725
},
1826
"trash": {
1927
"labware": "point",
20-
"slot": "B2"
28+
"slot": "C2"
2129
},
2230
"reagents": {
2331
"labware": "tube-rack-2ml",
@@ -72,6 +80,12 @@
7280
"tip-racks": [
7381
{
7482
"container": "p10-rack-ST"
83+
},
84+
{
85+
"container": "p10-rack-ST-2"
86+
},
87+
{
88+
"container": "p10-rack-ST-3"
7589
}
7690
],
7791
"trash-container": {

tests/opentrons/labware/test_pipette.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,7 @@ def generate_plate(wells, cols, spacing, offset, radius):
623623

624624
self.p200.move_to = mock.Mock()
625625

626-
for _ in range(0, total_tips_per_plate * 4):
626+
for _ in range(0, total_tips_per_plate * 2):
627627
self.p200.pick_up_tip()
628628

629629
self.robot.simulate()
@@ -633,16 +633,20 @@ def generate_plate(wells, cols, spacing, offset, radius):
633633
expected.append(self.build_move_to_bottom(self.tiprack1[i]))
634634
for i in range(0, total_tips_per_plate):
635635
expected.append(self.build_move_to_bottom(self.tiprack2[i]))
636-
for i in range(0, total_tips_per_plate):
637-
expected.append(self.build_move_to_bottom(self.tiprack1[i]))
638-
for i in range(0, total_tips_per_plate):
639-
expected.append(self.build_move_to_bottom(self.tiprack2[i]))
640636

641637
self.assertEqual(
642638
self.p200.move_to.mock_calls,
643639
expected
644640
)
645641

642+
# test then when we go over the total number of tips,
643+
# Pipette raises a RuntimeWarning
644+
self.robot.clear_commands()
645+
self.p200.reset()
646+
for _ in range(0, total_tips_per_plate * 2):
647+
self.p200.pick_up_tip()
648+
self.assertRaises(RuntimeWarning, self.p200.pick_up_tip)
649+
646650
def test_tip_tracking_chain_multi_channel(self):
647651
p200_multi = instruments.Pipette(
648652
trash_container=self.trash,
@@ -657,7 +661,7 @@ def test_tip_tracking_chain_multi_channel(self):
657661
top=0, bottom=10, blow_out=12, drop_tip=13)
658662
p200_multi.move_to = mock.Mock()
659663

660-
for _ in range(0, 12 * 4):
664+
for _ in range(0, 12 * 2):
661665
p200_multi.pick_up_tip()
662666

663667
self.robot.simulate()
@@ -667,16 +671,17 @@ def test_tip_tracking_chain_multi_channel(self):
667671
expected.append(self.build_move_to_bottom(self.tiprack1.rows[i]))
668672
for i in range(0, 12):
669673
expected.append(self.build_move_to_bottom(self.tiprack2.rows[i]))
670-
for i in range(0, 12):
671-
expected.append(self.build_move_to_bottom(self.tiprack1.rows[i]))
672-
for i in range(0, 12):
673-
expected.append(self.build_move_to_bottom(self.tiprack2.rows[i]))
674674

675675
self.assertEqual(
676676
p200_multi.move_to.mock_calls,
677677
expected
678678
)
679679

680+
def test_tip_tracking_start_at_tip(self):
681+
self.p200.start_at_tip(self.tiprack1['B2'])
682+
self.p200.pick_up_tip()
683+
self.assertEquals(self.tiprack1['B2'], self.p200.current_tip())
684+
680685
def test_tip_tracking_return(self):
681686
self.p200.drop_tip = mock.Mock()
682687

0 commit comments

Comments
 (0)