Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pipette transfer #151

Merged
merged 89 commits into from
Feb 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
c6e38fa
len(WellSeries) returns length of Wells contained
andySigler Dec 22, 2016
199f390
adds transfer() command and helper methods
andySigler Dec 22, 2016
80c13a5
adds gradient option to transfer
andySigler Dec 22, 2016
bb0cbf4
adds tests to Pipette.transfer()
andySigler Dec 26, 2016
0025f26
fixes bug when aspirate more volume than max_volume
andySigler Dec 26, 2016
72e01b5
adds helper methods to helpers.py
andySigler Dec 26, 2016
2aaa8f6
pylama
andySigler Dec 26, 2016
7bc0804
adds transfer test for linear gradient
andySigler Dec 26, 2016
199538f
pylama
andySigler Dec 26, 2016
efc6e82
returns tip if no trash attached to pipette
andySigler Dec 26, 2016
2aec640
transfer source/targets can take Placeables with children to iterate …
andySigler Dec 29, 2016
f6db9d8
adding special case where if using multi-channel and accessing WellSe…
andySigler Jan 4, 2017
051b896
Merge branch 'master' into pipette-transfer
andySigler Jan 4, 2017
e299c3e
pylama
andySigler Jan 4, 2017
994b923
tests multichannel transfer with WellSeries
andySigler Jan 4, 2017
44e070f
changes kwarg defaults and key names
andySigler Jan 6, 2017
a13450c
merges with master
andySigler Jan 6, 2017
79c35e4
Merge branch 'master' into pipette-transfer
andySigler Jan 9, 2017
d0270ea
fixes merge conflicts
andySigler Jan 9, 2017
b29e288
fixes tests
andySigler Jan 9, 2017
7ffa24a
adds tests
andySigler Jan 9, 2017
846e236
adds test
andySigler Jan 9, 2017
1365dca
handles Containers of length=1
andySigler Jan 9, 2017
eebd37b
refactors transfer to include consolidate and distribute commands
andySigler Jan 10, 2017
0857271
pylama
andySigler Jan 10, 2017
65a4cb6
adds tests for consolidate
andySigler Jan 10, 2017
84ac0a1
Merge branch 'master' into pipette-transfer
andySigler Jan 12, 2017
26a2a93
adds docstings
andySigler Jan 12, 2017
1c1c90f
adds more docstrings, comments
andySigler Jan 12, 2017
adf71b1
adds comments and organizes a bit
andySigler Jan 13, 2017
d00ba6e
adds mix_after and mix_before
andySigler Jan 13, 2017
ae6e22c
pylama
andySigler Jan 13, 2017
b7b0202
moves multichannel special case to helper method
andySigler Jan 13, 2017
ac35dfa
adds coverage to test
andySigler Jan 13, 2017
3b6da30
removes smoothie config file from repo
andySigler Jan 13, 2017
7a83414
containers with lenght=1 return list [container[0]]
andySigler Jan 18, 2017
922cb34
Merge branch 'master' into pipette-transfer
andySigler Jan 23, 2017
189a8ed
distribute/consolidate can take only 1 or 0 tips
andySigler Jan 23, 2017
9878345
setting kwarg defaults inside consolidate/distribute methods
andySigler Jan 23, 2017
8ca8007
does not drop tip if tips=0
andySigler Jan 23, 2017
3736958
updates tests
andySigler Jan 23, 2017
589d41c
Merge branch 'master' into pipette-transfer
andySigler Jan 23, 2017
c3ce245
Merge branch 'master' into pipette-transfer
andySigler Jan 30, 2017
115a13b
try and avoid small volumes occuring during carryover
andySigler Jan 31, 2017
5b20f24
Merge branch 'master' into pipette-transfer
andySigler Jan 31, 2017
25387ed
merge with master; fixed conflicts
andySigler Jan 31, 2017
13e8cff
adds tests for carryover volumes
andySigler Jan 31, 2017
91ac245
t
andySigler Jan 31, 2017
fa1f6fd
pylama
andySigler Jan 31, 2017
a397d64
adds test
andySigler Jan 31, 2017
86003a8
Merge branch 'master' into pipette-transfer
andySigler Jan 31, 2017
8648146
uses kwargs touch_tip and blow_out
andySigler Jan 31, 2017
eb8965a
adds support for container slices to accept strings for start and stop
andySigler Feb 4, 2017
d2ae7c5
adds Placeable.get_children_from_slice()
andySigler Feb 5, 2017
34279be
uses new method
andySigler Feb 5, 2017
d9a8a03
add Placeable.chain() and Placeable.group()
andySigler Feb 5, 2017
4cccf94
comment
andySigler Feb 5, 2017
5d3e42e
Placeable.chain() length defaults to length of container
andySigler Feb 5, 2017
63d7007
Placeable.well() return single well; Placeable.wells() returns list o…
andySigler Feb 5, 2017
c601162
refactors Placeable.get_name(); adds test for comparing WellSeries
andySigler Feb 5, 2017
0c9335d
methods return WellSeries instead of list
andySigler Feb 5, 2017
908ff60
undoes Placeable.get_name() refactor
andySigler Feb 5, 2017
5dc5a9d
.
andySigler Feb 5, 2017
6861802
typo
andySigler Feb 5, 2017
606f314
removes redudant methods between Placeable and WellSeries
andySigler Feb 5, 2017
28f293f
adds tests
andySigler Feb 5, 2017
eb6b34e
adds test for slices with negative step
andySigler Feb 5, 2017
b79208c
allows group() range() chain() to move in negative direction
andySigler Feb 5, 2017
3c2ba3a
refactors .chain()
andySigler Feb 5, 2017
734632f
add feature for using .chain() or .group() through calling Container …
andySigler Feb 5, 2017
1a98ded
adds access to Container.wells() through call instance
andySigler Feb 5, 2017
f971f4b
pylama
andySigler Feb 5, 2017
26d7308
cleans up parse_string a bit
andySigler Feb 5, 2017
fa37369
adds instance methods to WellSeries
andySigler Feb 5, 2017
25d79ad
multiple parsed argument on callable instrument, oh my
andySigler Feb 6, 2017
98aa16d
adds WellSeries.crop() and WellSeries.flatten()
andySigler Feb 6, 2017
d40435d
trim() and flatten() return new WellSeries
andySigler Feb 6, 2017
d79b8ba
adding blow_out to distribute
andySigler Feb 6, 2017
b87c856
tests Placeable.remove_child()
andySigler Feb 6, 2017
38e782a
includes blow_out commands while distributing with extra volume
andySigler Feb 6, 2017
054cf3a
Merge branch 'master' into container-slices-with-strings
andySigler Feb 6, 2017
c3fad5d
adds access to Placeable.range() through string syntax using ':'
andySigler Feb 6, 2017
e5e512a
Merge branch 'adds-test-to-placeable-remove-child' into pipette-transfer
andySigler Feb 6, 2017
9815092
sets kwarg trash to default to True
andySigler Feb 6, 2017
e252685
allows >3 args to consolidate and distribute
andySigler Feb 7, 2017
5fb8c13
tips kwargs is now new_tip='once'||'never'||'always'; repeater is now…
andySigler Feb 7, 2017
15846ef
adds tests
andySigler Feb 7, 2017
9db8a70
removes all features other than slicing with lists
andySigler Feb 7, 2017
9453e12
merge with slicing with strings branch
andySigler Feb 7, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ sample_protocol.py

# Local Calibration Data
calibrations/
*/calibrations/
smoothie-config.ini
*/smoothie-config.ini
smoothie/
*/smoothie
*/calibrations

# SDK logs
*.log
Expand Down
97 changes: 68 additions & 29 deletions opentrons/containers/placeable.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ def __getitem__(self, name):
Returns placeable by name or index
If slice is given, returns a list
"""
if isinstance(name, int) or isinstance(name, slice):
if isinstance(name, slice):
return self.get_children_from_slice(name)
elif isinstance(name, int):
return self.get_children_list()[name]
elif isinstance(name, str):
return self.get_child_by_name(name)
Expand All @@ -106,10 +108,10 @@ def __str__(self):
self.__class__.__name__, self.get_name())

def __iter__(self):
return iter(self.children_by_reference.keys())
return iter(self.get_children_list())

def __len__(self):
return len(self.children_by_name)
return len(self.get_children_list())

def __bool__(self):
return True
Expand Down Expand Up @@ -236,6 +238,25 @@ def get_child_by_name(self, name):
"""
return self.children_by_name.get(name)

def get_index_from_name(self, name):
"""
Retrieves child's name by index
"""
return self.get_children_list().index(
self.get_child_by_name(name))

def get_children_from_slice(self, s):
"""
Retrieves list of children within slice
"""
if isinstance(s.start, str):
s = slice(
self.get_index_from_name(s.start), s.stop, s.step)
if isinstance(s.stop, str):
s = slice(
s.start, self.get_index_from_name(s.stop), s.step)
return WellSeries(self.get_children_list()[s])

def has_children(self):
"""
Returns *True* if :Placeable: has children
Expand Down Expand Up @@ -490,7 +511,7 @@ def transpose(self, rows):

def get_wellseries(self, matrix):
"""
Returns the grid as a series of WellSeries
Returns the grid as a WellSeries of WellSeries
"""
res = OrderedDict()
for row, cells in matrix.items():
Expand All @@ -500,19 +521,19 @@ def get_wellseries(self, matrix):
res[row][col] = self.children_by_name[
''.join(reversed(cell))
]
res[row] = WellSeries(res[row])
res[row] = WellSeries(res[row], name=row)
return WellSeries(res)

@property
def rows(self):
"""
Rows can be accessed as:
>>> plate.row[0]
>>> plate.row['1']
>>> plate.rows[0]
>>> plate.rows['1']

Wells can be accessed as:
>>> plate.row[0][0]
>>> plate.row['1']['A']
>>> plate.rows[0][0]
>>> plate.rows['1']['A']
"""
self.calculate_grid()
return self.grid
Expand Down Expand Up @@ -544,20 +565,27 @@ def cols(self):
"""
return self.columns

def well(self, name):
def well(self, name=None):
"""
Returns well by :name:
"""
return self.get_child_by_name(name)
return self.__getitem__(name)

def wells(self):
def wells(self, *args):
"""
Returns all wells
Returns child Well or list of child Wells
"""
return self.get_children()
new_wells = None
if len(args) > 0:
new_wells = WellSeries([self.well(n) for n in args])
else:
new_wells = WellSeries(self.get_children_list())
if len(new_wells) is 1:
return new_wells[0]
return new_wells


class WellSeries(Placeable):
class WellSeries(Container):
"""
:WellSeries: represents a series of wells to make
accessing rows and columns easier. You can access
Expand All @@ -568,35 +596,46 @@ class WellSeries(Placeable):

Default well index can be overriden using :set_offset:
"""
def __init__(self, items):
self.items = items
if isinstance(items, dict):
items = list(self.items.values())
self.values = items
def __init__(self, wells, name=None):
if isinstance(wells, dict):
self.items = wells
self.values = list(wells.values())
else:
self.items = {w.get_name(): w for w in wells}
self.values = wells
self.offset = 0
self.name = name

def set_offset(self, offset):
"""
Set index of a well that will be used to mimic :Placeable:
"""
self.offset = offset

def __iter__(self):
return iter(self.values)

def __str__(self):
return '<Series: {}>'.format(
' '.join([str(well) for well in self.values]))

def __getitem__(self, index):
if isinstance(index, str):
return self.items[index]
else:
return list(self.values)[index]

def __getattr__(self, name):
# getstate/setstate are used by pickle and are not implemented by
# downstream objects (Wells) therefore raise attribute error
if name in ('__getstate__', '__setstate__'):
raise AttributeError()
return getattr(self.values[self.offset], name)

def get_name(self):
if self.name is None:
return str(self)
return str(self.name)

def get_name_by_instance(self, well):
for name, value in self.items.items():
if value is well:
return name
return None

def get_children_list(self):
return list(self.values)

def get_child_by_name(self, name):
return self.items.get(name)
182 changes: 182 additions & 0 deletions opentrons/helpers/helpers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json

from opentrons.util.vector import Vector
from opentrons.containers.placeable import Placeable


def unpack_coordinates(coordinates):
Expand Down Expand Up @@ -98,3 +99,184 @@ def import_calibration_file(file_name, robot):
with open(file_name) as f:
json_string = '\n'.join(f)
import_calibration_json(json_string, robot)


def _get_list(n):
if not hasattr(n, '__len__') or len(n) == 0 or isinstance(n, tuple):
n = [n]
if isinstance(n, Placeable) and len(n) == 1:
n = [n[0]]
return n


def _create_source_target_lists(s, t, **kwargs):
s = _get_list(s)
t = _get_list(t)
mode = kwargs.get('mode', 'transfer')
if mode == 'transfer':
if len(s) != len(t):
raise RuntimeError(
'Transfer sources/targets must be same length')
elif mode == 'distribute':
if not (len(t) >= len(s) == 1):
raise RuntimeError(
'Distribute must have 1 source and multiple targets')
s *= len(t)
elif mode == 'consolidate':
if not (len(s) >= len(t) == 1):
raise RuntimeError(
'Consolidate must have multiple sources and 1 target')
t *= len(s)
return (s, t)


def _create_volume_list(v, total, **kwargs):

gradient = kwargs.get('gradient', None)

if isinstance(v, tuple):
return _create_volume_gradient(
v[0], v[-1], total, gradient=gradient)

v = _get_list(v)
t_vol = len(v)
if (t_vol < total and t_vol != 1) or t_vol > total:
raise RuntimeError(
'{0} volumes do not match with {1} transfers'.format(
t_vol, total))
if t_vol < total:
v = [v[0]] * total
return v


def _create_volume_gradient(min_v, max_v, total, gradient=None):

diff_vol = max_v - min_v

def _map_volume(i):
nonlocal diff_vol, total
rel_x = i / (total - 1)
rel_y = gradient(rel_x) if gradient else rel_x
return (rel_y * diff_vol) + min_v

return [_map_volume(i) for i in range(total)]


def _compress_for_repeater(min_vol, max_vol, plan, **kwargs):
min_vol = float(min_vol)
max_vol = float(max_vol)
mode = kwargs.get('mode', 'transfer')
if mode == 'distribute': # combine target volumes into single aspirate
return _compress_for_distribute(min_vol, max_vol, plan, **kwargs)
if mode == 'consolidate': # combine target volumes into multiple aspirates
return _compress_for_consolidate(min_vol, max_vol, plan, **kwargs)
else:
return plan


def _compress_for_distribute(min_vol, max_vol, plan, **kwargs):
source = plan[0]['aspirate']['location']
a_vol = 0
temp_dispenses = []
new_transfer_plan = []

def _add():
nonlocal a_vol, temp_dispenses, new_transfer_plan, source

if not temp_dispenses:
return

# distribute commands get an extra volume added
if len(temp_dispenses) > 1:
a_vol += min_vol

new_transfer_plan.append({
'aspirate': {
'location': source, 'volume': a_vol
}
})
for d in temp_dispenses:
new_transfer_plan.append({
'dispense': {
'location': d['location'], 'volume': d['volume']
}
})

if min_vol and len(temp_dispenses) > 1:
new_transfer_plan.append({
'blow_out': {'blow_out': True}
})

for p in plan:
this_vol = p['aspirate']['volume']
if this_vol + a_vol > max_vol - min_vol:
_add()
a_vol = 0
temp_dispenses = []
a_vol += this_vol
temp_dispenses.append(p['dispense'])
_add()
return new_transfer_plan


def _compress_for_consolidate(min_vol, max_vol, plan, **kwargs):
target = plan[0]['dispense']['location']
d_vol = 0
temp_aspirates = []
new_transfer_plan = []

def _add():
nonlocal d_vol, temp_aspirates, new_transfer_plan, target
for a in temp_aspirates:
new_transfer_plan.append({
'aspirate': {
'location': a['location'], 'volume': a['volume']
}
})
new_transfer_plan.append({
'dispense': {
'location': target, 'volume': d_vol
}
})
d_vol = 0
temp_aspirates = []

for i, p in enumerate(plan):
this_vol = p['aspirate']['volume']
if this_vol + d_vol > max_vol:
_add()
d_vol += this_vol
temp_aspirates.append(p['aspirate'])
_add()
return new_transfer_plan


def _expand_for_carryover(min_vol, max_vol, plan, **kwargs):
max_vol = float(max_vol)
carryover = kwargs.get('carryover', True)
if not carryover:
return plan
new_transfer_plan = []
for p in plan:
source = p['aspirate']['location']
target = p['dispense']['location']
volume = float(p['aspirate']['volume'])
while volume > max_vol * 2:
new_transfer_plan.append({
'aspirate': {'location': source, 'volume': max_vol},
'dispense': {'location': target, 'volume': max_vol}
})
volume -= max_vol

# try and make sure no volumes are less than min_vol
if volume > max_vol:
volume /= 2
new_transfer_plan.append({
'aspirate': {'location': source, 'volume': float(volume)},
'dispense': {'location': target, 'volume': float(volume)}
})
new_transfer_plan.append({
'aspirate': {'location': source, 'volume': float(volume)},
'dispense': {'location': target, 'volume': float(volume)}
})
return new_transfer_plan
Loading