Skip to content

Commit 2abc341

Browse files
committed
add LH.transfer and LH.distribute
1 parent e4aa953 commit 2abc341

File tree

1 file changed

+69
-67
lines changed

1 file changed

+69
-67
lines changed

pylabrobot/liquid_handling/liquid_handler.py

Lines changed: 69 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import asyncio
66
import contextlib
77
import inspect
8+
from itertools import zip_longest
89
import json
910
import logging
1011
import threading
@@ -1263,92 +1264,93 @@ async def dispense(
12631264

12641265
async def transfer(
12651266
self,
1266-
source: Well,
1267-
targets: List[Well],
1268-
source_vol: Optional[float] = None,
1269-
ratios: Optional[List[float]] = None,
1270-
target_vols: Optional[List[float]] = None,
1271-
aspiration_flow_rate: Optional[float] = None,
1272-
dispense_flow_rates: Optional[List[Optional[float]]] = None,
1273-
**backend_kwargs,
1267+
source_resources: Sequence[Container],
1268+
dest_resources: Sequence[Container],
1269+
vols: List[float],
1270+
aspiration_kwargs: Optional[Dict[str, Any]] = None,
1271+
dispense_kwargs: Optional[Dict[str, Any]] = None,
12741272
):
1275-
"""Transfer liquid from one well to another.
1273+
"""Transfer liquid from one set of resources to another. Each input resource matches to exactly one output resource.
12761274
12771275
Examples:
1276+
Transfer liquid from one column to another column:
1277+
>>> await lh.transfer(
1278+
... source_resources=plate1["A1":"H8"],
1279+
... dest_resources=plate2["A1":"H8"],
1280+
... vols=[50] * 8,
1281+
... )
1282+
"""
12781283

1279-
Transfer 50 uL of liquid from the first well to the second well:
1280-
1281-
>>> await lh.transfer(plate["A1"], plate["B1"], source_vol=50)
1284+
if not (len(source_resources) == len(dest_resources) == len(vols)):
1285+
raise ValueError(
1286+
"Number of source and destination resources must match, but got "
1287+
f"{len(source_resources)} source resources, {len(dest_resources)} destination resources, "
1288+
f"and {len(vols)} volumes."
1289+
)
12821290

1283-
Transfer 80 uL of liquid from the first well equally to the first column:
1291+
if len(source_resources) > self.backend.num_channels:
1292+
raise ValueError("Number of resources exceeds number of channels.")
12841293

1285-
>>> await lh.transfer(plate["A1"], plate["A1:H1"], source_vol=80)
1294+
use_channels = list(range(len(source_resources)))
12861295

1287-
Transfer 60 uL of liquid from the first well in a 1:2 ratio to 2 other wells:
1296+
await self.aspirate(
1297+
resources=source_resources,
1298+
vols=vols,
1299+
use_channels=use_channels,
1300+
**(aspiration_kwargs or {}),
1301+
)
12881302

1289-
>>> await lh.transfer(plate["A1"], plate["B1:C1"], source_vol=60, ratios=[2, 1])
1303+
await self.dispense(
1304+
resources=dest_resources,
1305+
vols=vols,
1306+
use_channels=use_channels,
1307+
**(dispense_kwargs or {}),
1308+
)
12901309

1291-
Transfer arbitrary volumes to the first column:
1310+
async def distribute(
1311+
self,
1312+
operations: Dict[Container, List[Tuple[Container, float]]],
1313+
dead_volume: float = 10,
1314+
):
1315+
"""
1316+
Distribute liquid from one resource to multiple resources.
12921317
1293-
>>> await lh.transfer(plate["A1"], plate["A1:H1"], target_vols=[3, 1, 4, 1, 5, 9, 6, 2])
1318+
Examples:
1319+
Distribute liquid from one well to multiple wells:
1320+
>>> await lh.distribute({
1321+
... plate1["A1"]: [(plate2["A1"], 50), (plate2["A2"], 50)],
1322+
... plate1["A2"]: [(plate2["B1"], 100), (plate2["B2"], 100), (plate2["B3"], 100)],
1323+
... })
12941324
12951325
Args:
1296-
source: The source well.
1297-
targets: The target wells.
1298-
source_vol: The volume to transfer from the source well.
1299-
ratios: The ratios to use when transferring liquid to the target wells. If not specified, then
1300-
the volumes will be distributed equally.
1301-
target_vols: The volumes to transfer to the target wells. If specified, `source_vols` and
1302-
`ratios` must be `None`.
1303-
aspiration_flow_rate: The flow rate to use when aspirating, in ul/s. If `None`, the backend
1304-
default will be used.
1305-
dispense_flow_rates: The flow rates to use when dispensing, in ul/s. If `None`, the backend
1306-
default will be used. Either a single flow rate for all channels, or a list of flow rates,
1307-
one for each target well.
1308-
1309-
Raises:
1310-
RuntimeError: If the setup has not been run. See :meth:`~LiquidHandler.setup`.
1326+
operations: A dictionary mapping source resources to a list of tuples, each containing a
1327+
destination resource and the volume to dispense to that resource.
13111328
"""
13121329

1313-
self._log_command(
1314-
"transfer",
1315-
source=source,
1316-
targets=targets,
1317-
source_vol=source_vol,
1318-
ratios=ratios,
1319-
target_vols=target_vols,
1320-
aspiration_flow_rate=aspiration_flow_rate,
1321-
dispense_flow_rates=dispense_flow_rates,
1322-
)
1323-
1324-
if target_vols is not None:
1325-
if ratios is not None:
1326-
raise TypeError("Cannot specify ratios and target_vols at the same time")
1327-
if source_vol is not None:
1328-
raise TypeError("Cannot specify source_vol and target_vols at the same time")
1329-
else:
1330-
if source_vol is None:
1331-
raise TypeError("Must specify either source_vol or target_vols")
1332-
1333-
if ratios is None:
1334-
ratios = [1] * len(targets)
1330+
if len(operations) > self.backend.num_channels:
1331+
raise ValueError("Number of source resources exceeds number of channels.")
13351332

1336-
target_vols = [source_vol * r / sum(ratios) for r in ratios]
1333+
use_channels = list(range(len(operations)))
13371334

1335+
# Aspirate from all source resources
13381336
await self.aspirate(
1339-
resources=[source],
1340-
vols=[sum(target_vols)],
1341-
flow_rates=[aspiration_flow_rate],
1342-
**backend_kwargs,
1337+
resources=list(operations.keys()),
1338+
vols=[sum(v for _, v in dests) + dead_volume for dests in operations.values()],
1339+
use_channels=use_channels,
13431340
)
1344-
dispense_flow_rates = dispense_flow_rates or [None] * len(targets)
1345-
for target, vol, dfr in zip(targets, target_vols, dispense_flow_rates):
1341+
1342+
for group in zip_longest(*operations.values()):
1343+
dest, vols, channels = zip(
1344+
*(
1345+
(pair[0], pair[1], ch)
1346+
for pair, ch in zip_longest(group, use_channels)
1347+
if pair is not None
1348+
)
1349+
)
13461350
await self.dispense(
1347-
resources=[target],
1348-
vols=[vol],
1349-
flow_rates=[dfr],
1350-
use_channels=[0],
1351-
**backend_kwargs,
1351+
resources=list(dest),
1352+
vols=list(vols),
1353+
use_channels=list(channels),
13521354
)
13531355

13541356
@contextlib.contextmanager

0 commit comments

Comments
 (0)