|
5 | 5 | import asyncio |
6 | 6 | import contextlib |
7 | 7 | import inspect |
| 8 | +from itertools import zip_longest |
8 | 9 | import json |
9 | 10 | import logging |
10 | 11 | import threading |
@@ -1263,92 +1264,93 @@ async def dispense( |
1263 | 1264 |
|
1264 | 1265 | async def transfer( |
1265 | 1266 | 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, |
1274 | 1272 | ): |
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. |
1276 | 1274 |
|
1277 | 1275 | 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 | + """ |
1278 | 1283 |
|
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 | + ) |
1282 | 1290 |
|
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.") |
1284 | 1293 |
|
1285 | | - >>> await lh.transfer(plate["A1"], plate["A1:H1"], source_vol=80) |
| 1294 | + use_channels = list(range(len(source_resources))) |
1286 | 1295 |
|
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 | + ) |
1288 | 1302 |
|
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 | + ) |
1290 | 1309 |
|
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. |
1292 | 1317 |
|
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 | + ... }) |
1294 | 1324 |
|
1295 | 1325 | 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. |
1311 | 1328 | """ |
1312 | 1329 |
|
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.") |
1335 | 1332 |
|
1336 | | - target_vols = [source_vol * r / sum(ratios) for r in ratios] |
| 1333 | + use_channels = list(range(len(operations))) |
1337 | 1334 |
|
| 1335 | + # Aspirate from all source resources |
1338 | 1336 | 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, |
1343 | 1340 | ) |
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 | + ) |
1346 | 1350 | 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), |
1352 | 1354 | ) |
1353 | 1355 |
|
1354 | 1356 | @contextlib.contextmanager |
|
0 commit comments