Skip to content

Commit 1a72b6d

Browse files
Implement FluentWorklist.transfer method
With ``wash_scheme=None`` option.
1 parent 8f9a7a9 commit 1a72b6d

File tree

4 files changed

+179
-8
lines changed

4 files changed

+179
-8
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import pytest
2+
3+
from robotools.fluenttools.worklist import FluentWorklist
4+
from robotools.liquidhandling.labware import Labware
5+
6+
7+
class TestFluentWorklist:
8+
def test_transfer(self):
9+
A = Labware("A", 3, 4, min_volume=10, max_volume=200)
10+
A.add("A01", 100)
11+
with FluentWorklist() as wl:
12+
wl.transfer(
13+
A,
14+
"A01",
15+
A,
16+
"B01",
17+
50,
18+
)
19+
pass
20+
21+
def test_input_checks(self):
22+
A = Labware("A", 3, 4, min_volume=10, max_volume=200, initial_volumes=150)
23+
with FluentWorklist() as wl:
24+
with pytest.raises(ValueError, match="must be equal"):
25+
wl.transfer(A, ["A01", "B01", "C01"], A, ["A01", "B01"], 20)
26+
with pytest.raises(ValueError, match="must be equal"):
27+
wl.transfer(A, ["A01", "B01"], A, ["A01", "B01", "C01"], 20)
28+
with pytest.raises(ValueError, match="must be equal"):
29+
wl.transfer(A, ["A01", "B01"], A, "A01", [30, 40, 25])
30+
pass
31+
32+
def test_transfer_flush(self):
33+
A = Labware("A", 3, 4, min_volume=10, max_volume=200, initial_volumes=150)
34+
with FluentWorklist() as wl:
35+
wl.transfer(A, "A01", A, "B01", 20, wash_scheme=None)
36+
assert len(wl) == 3
37+
assert wl[-1] == "F;"
38+
pass

robotools/fluenttools/worklist.py

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1+
from collections.abc import Sequence
12
from pathlib import Path
23
from typing import Optional, Union
34

5+
import numpy as np
6+
7+
from robotools.liquidhandling.labware import Labware
48
from robotools.worklists.base import BaseWorklist
9+
from robotools.worklists.utils import (
10+
optimize_partition_by,
11+
partition_by_column,
12+
partition_volume,
13+
)
514

615
__all__ = ("FluentWorklist",)
716

@@ -15,5 +24,125 @@ def __init__(
1524
max_volume: Union[int, float] = 950,
1625
auto_split: bool = True,
1726
) -> None:
18-
raise NotImplementedError("Be patient.")
1927
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

robotools/test_worklists.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ def test_recommended_instantiation():
3030
warnings.simplefilter("error")
3131
BaseWorklist()
3232
EvoWorklist()
33-
with pytest.raises(NotImplementedError):
3433
FluentWorklist()
3534
pass
3635

robotools/worklists/test_lvh.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import numpy as np
2+
import pytest
23

34
from robotools.evotools.worklist import EvoWorklist
5+
from robotools.fluenttools.worklist import FluentWorklist
46
from robotools.liquidhandling.labware import Labware
57

68

79
class TestLargeVolumeHandling:
8-
def test_single_split(self) -> None:
10+
@pytest.mark.parametrize("cls", [EvoWorklist, FluentWorklist])
11+
def test_single_split(self, cls) -> None:
912
src = Labware("A", 3, 2, min_volume=1000, max_volume=25000, initial_volumes=12000)
1013
dst = Labware("B", 3, 2, min_volume=1000, max_volume=25000)
11-
with EvoWorklist(auto_split=True) as wl:
14+
with cls(auto_split=True) as wl:
1215
wl.transfer(src, "A01", dst, "A01", 2000, label="Transfer more than 2x the max")
1316
assert wl == [
1417
"C;Transfer more than 2x the max",
@@ -46,10 +49,11 @@ def test_single_split(self) -> None:
4649
)
4750
return
4851

49-
def test_column_split(self) -> None:
52+
@pytest.mark.parametrize("cls", [EvoWorklist, FluentWorklist])
53+
def test_column_split(self, cls) -> None:
5054
src = Labware("A", 4, 2, min_volume=1000, max_volume=25000, initial_volumes=12000)
5155
dst = Labware("B", 4, 2, min_volume=1000, max_volume=25000)
52-
with EvoWorklist(auto_split=True) as wl:
56+
with cls(auto_split=True) as wl:
5357
wl.transfer(
5458
src, ["A01", "B01", "D01", "C01"], dst, ["A01", "B01", "D01", "C01"], [1500, 250, 0, 1200]
5559
)
@@ -93,10 +97,11 @@ def test_column_split(self) -> None:
9397
)
9498
return
9599

96-
def test_block_split(self) -> None:
100+
@pytest.mark.parametrize("cls", [EvoWorklist, FluentWorklist])
101+
def test_block_split(self, cls) -> None:
97102
src = Labware("A", 3, 2, min_volume=1000, max_volume=25000, initial_volumes=12000)
98103
dst = Labware("B", 3, 2, min_volume=1000, max_volume=25000)
99-
with EvoWorklist(auto_split=True) as wl:
104+
with cls(auto_split=True) as wl:
100105
wl.transfer(
101106
# A01, B01, A02, B02
102107
src,

0 commit comments

Comments
 (0)