1
+ from collections .abc import Sequence
1
2
from pathlib import Path
2
3
from typing import Optional , Union
3
4
5
+ import numpy as np
6
+
7
+ from robotools .liquidhandling .labware import Labware
4
8
from robotools .worklists .base import BaseWorklist
9
+ from robotools .worklists .utils import (
10
+ optimize_partition_by ,
11
+ partition_by_column ,
12
+ partition_volume ,
13
+ )
5
14
6
15
__all__ = ("FluentWorklist" ,)
7
16
@@ -15,5 +24,125 @@ def __init__(
15
24
max_volume : Union [int , float ] = 950 ,
16
25
auto_split : bool = True ,
17
26
) -> None :
18
- raise NotImplementedError ("Be patient." )
19
27
super ().__init__ (filepath , max_volume , auto_split )
28
+
29
+ def transfer (
30
+ self ,
31
+ source : Labware ,
32
+ source_wells : Union [str , Sequence [str ], np .ndarray ],
33
+ destination : Labware ,
34
+ destination_wells : Union [str , Sequence [str ], np .ndarray ],
35
+ volumes : Union [float , Sequence [float ], np .ndarray ],
36
+ * ,
37
+ label : Optional [str ] = None ,
38
+ wash_scheme : Optional [int ] = 1 ,
39
+ partition_by : str = "auto" ,
40
+ ** kwargs ,
41
+ ) -> None :
42
+ """Transfer operation between two labwares.
43
+
44
+ Parameters
45
+ ----------
46
+ source
47
+ Source labware
48
+ source_wells
49
+ List of source well ids
50
+ destination
51
+ Destination labware
52
+ destination_wells
53
+ List of destination well ids
54
+ volumes
55
+ Volume(s) to transfer
56
+ label
57
+ Label of the operation to log into labware history
58
+ wash_scheme
59
+ Wash scheme to apply after every tip use.
60
+ If ``None``, only a flush is inserted instead of a wash.
61
+ partition_by : str
62
+ one of 'auto' (default), 'source' or 'destination'
63
+ 'auto': partitioning by source unless the source is a Trough
64
+ 'source': partitioning by source columns
65
+ 'destination': partitioning by destination columns
66
+ kwargs
67
+ Additional keyword arguments to pass to aspirate and dispense.
68
+ Most prominent example: `liquid_class`.
69
+ Take a look at `Worklist.aspirate_well` for the full list of options.
70
+ """
71
+ # reformat the convenience parameters
72
+ source_wells = np .array (source_wells ).flatten ("F" )
73
+ destination_wells = np .array (destination_wells ).flatten ("F" )
74
+ volumes = np .array (volumes ).flatten ("F" )
75
+ nmax = max ((len (source_wells ), len (destination_wells ), len (volumes )))
76
+
77
+ if len (source_wells ) == 1 :
78
+ source_wells = np .repeat (source_wells , nmax )
79
+ if len (destination_wells ) == 1 :
80
+ destination_wells = np .repeat (destination_wells , nmax )
81
+ if len (volumes ) == 1 :
82
+ volumes = np .repeat (volumes , nmax )
83
+ lengths = (len (source_wells ), len (destination_wells ), len (volumes ))
84
+ if len (set (lengths )) != 1 :
85
+ raise ValueError (f"Number of source/destination/volumes must be equal. They were { lengths } " )
86
+
87
+ # automatic partitioning
88
+ partition_by = optimize_partition_by (source , destination , partition_by , label )
89
+
90
+ # the label applies to the entire transfer operation and is not logged at individual aspirate/dispense steps
91
+ self .comment (label )
92
+ nsteps = 0
93
+ lvh_extra = 0
94
+
95
+ for srcs , dsts , vols in partition_by_column (source_wells , destination_wells , volumes , partition_by ):
96
+ # make vector of volumes into vector of volume-lists
97
+ vol_lists = [
98
+ partition_volume (float (v ), max_volume = self .max_volume ) if self .auto_split else [v ]
99
+ for v in vols
100
+ ]
101
+ # transfer from this source column until all wells are done
102
+ npartitions = max (map (len , vol_lists ))
103
+ # Count only the extra steps created by LVH
104
+ lvh_extra += sum ([len (vs ) - 1 for vs in vol_lists ])
105
+ for p in range (npartitions ):
106
+ naccessed = 0
107
+ # iterate the rows
108
+ for s , d , vs in zip (srcs , dsts , vol_lists ):
109
+ # transfer the next volume-fraction for this well
110
+ if len (vs ) > p :
111
+ v = vs [p ]
112
+ if v > 0 :
113
+ self .aspirate (source , s , v , label = None , ** kwargs )
114
+ self .dispense (
115
+ destination ,
116
+ d ,
117
+ v ,
118
+ label = None ,
119
+ compositions = [source .get_well_composition (s )],
120
+ ** kwargs ,
121
+ )
122
+ nsteps += 1
123
+ if wash_scheme is not None :
124
+ self .wash (scheme = wash_scheme )
125
+ else :
126
+ self .flush ()
127
+ naccessed += 1
128
+ # LVH: if multiple wells are accessed, don't group across partitions
129
+ if npartitions > 1 and naccessed > 1 and not p == npartitions - 1 :
130
+ self .commit ()
131
+ # LVH: don't group across columns
132
+ if npartitions > 1 :
133
+ self .commit ()
134
+
135
+ # Condense the labware logs into one operation
136
+ # after the transfer operation completed to facilitate debugging.
137
+ # Also include the number of extra steps because of LVH if applicable.
138
+ if lvh_extra :
139
+ if label :
140
+ label = f"{ label } ({ lvh_extra } LVH steps)"
141
+ else :
142
+ label = f"{ lvh_extra } LVH steps"
143
+ if destination == source :
144
+ source .condense_log (nsteps * 2 , label = label )
145
+ else :
146
+ source .condense_log (nsteps , label = label )
147
+ destination .condense_log (nsteps , label = label )
148
+ return
0 commit comments