From 2abc341f9d7cda512f34bf37ec6ea58c2bf9a21e Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Tue, 7 Oct 2025 18:30:10 -0700 Subject: [PATCH 1/6] add LH.transfer and LH.distribute --- pylabrobot/liquid_handling/liquid_handler.py | 136 ++++++++++--------- 1 file changed, 69 insertions(+), 67 deletions(-) diff --git a/pylabrobot/liquid_handling/liquid_handler.py b/pylabrobot/liquid_handling/liquid_handler.py index 767e7cd0a22..45d0202d016 100644 --- a/pylabrobot/liquid_handling/liquid_handler.py +++ b/pylabrobot/liquid_handling/liquid_handler.py @@ -5,6 +5,7 @@ import asyncio import contextlib import inspect +from itertools import zip_longest import json import logging import threading @@ -1263,92 +1264,93 @@ async def dispense( async def transfer( self, - source: Well, - targets: List[Well], - source_vol: Optional[float] = None, - ratios: Optional[List[float]] = None, - target_vols: Optional[List[float]] = None, - aspiration_flow_rate: Optional[float] = None, - dispense_flow_rates: Optional[List[Optional[float]]] = None, - **backend_kwargs, + source_resources: Sequence[Container], + dest_resources: Sequence[Container], + vols: List[float], + aspiration_kwargs: Optional[Dict[str, Any]] = None, + dispense_kwargs: Optional[Dict[str, Any]] = None, ): - """Transfer liquid from one well to another. + """Transfer liquid from one set of resources to another. Each input resource matches to exactly one output resource. Examples: + Transfer liquid from one column to another column: + >>> await lh.transfer( + ... source_resources=plate1["A1":"H8"], + ... dest_resources=plate2["A1":"H8"], + ... vols=[50] * 8, + ... ) + """ - Transfer 50 uL of liquid from the first well to the second well: - - >>> await lh.transfer(plate["A1"], plate["B1"], source_vol=50) + if not (len(source_resources) == len(dest_resources) == len(vols)): + raise ValueError( + "Number of source and destination resources must match, but got " + f"{len(source_resources)} source resources, {len(dest_resources)} destination resources, " + f"and {len(vols)} volumes." + ) - Transfer 80 uL of liquid from the first well equally to the first column: + if len(source_resources) > self.backend.num_channels: + raise ValueError("Number of resources exceeds number of channels.") - >>> await lh.transfer(plate["A1"], plate["A1:H1"], source_vol=80) + use_channels = list(range(len(source_resources))) - Transfer 60 uL of liquid from the first well in a 1:2 ratio to 2 other wells: + await self.aspirate( + resources=source_resources, + vols=vols, + use_channels=use_channels, + **(aspiration_kwargs or {}), + ) - >>> await lh.transfer(plate["A1"], plate["B1:C1"], source_vol=60, ratios=[2, 1]) + await self.dispense( + resources=dest_resources, + vols=vols, + use_channels=use_channels, + **(dispense_kwargs or {}), + ) - Transfer arbitrary volumes to the first column: + async def distribute( + self, + operations: Dict[Container, List[Tuple[Container, float]]], + dead_volume: float = 10, + ): + """ + Distribute liquid from one resource to multiple resources. - >>> await lh.transfer(plate["A1"], plate["A1:H1"], target_vols=[3, 1, 4, 1, 5, 9, 6, 2]) + Examples: + Distribute liquid from one well to multiple wells: + >>> await lh.distribute({ + ... plate1["A1"]: [(plate2["A1"], 50), (plate2["A2"], 50)], + ... plate1["A2"]: [(plate2["B1"], 100), (plate2["B2"], 100), (plate2["B3"], 100)], + ... }) Args: - source: The source well. - targets: The target wells. - source_vol: The volume to transfer from the source well. - ratios: The ratios to use when transferring liquid to the target wells. If not specified, then - the volumes will be distributed equally. - target_vols: The volumes to transfer to the target wells. If specified, `source_vols` and - `ratios` must be `None`. - aspiration_flow_rate: The flow rate to use when aspirating, in ul/s. If `None`, the backend - default will be used. - dispense_flow_rates: The flow rates to use when dispensing, in ul/s. If `None`, the backend - default will be used. Either a single flow rate for all channels, or a list of flow rates, - one for each target well. - - Raises: - RuntimeError: If the setup has not been run. See :meth:`~LiquidHandler.setup`. + operations: A dictionary mapping source resources to a list of tuples, each containing a + destination resource and the volume to dispense to that resource. """ - self._log_command( - "transfer", - source=source, - targets=targets, - source_vol=source_vol, - ratios=ratios, - target_vols=target_vols, - aspiration_flow_rate=aspiration_flow_rate, - dispense_flow_rates=dispense_flow_rates, - ) - - if target_vols is not None: - if ratios is not None: - raise TypeError("Cannot specify ratios and target_vols at the same time") - if source_vol is not None: - raise TypeError("Cannot specify source_vol and target_vols at the same time") - else: - if source_vol is None: - raise TypeError("Must specify either source_vol or target_vols") - - if ratios is None: - ratios = [1] * len(targets) + if len(operations) > self.backend.num_channels: + raise ValueError("Number of source resources exceeds number of channels.") - target_vols = [source_vol * r / sum(ratios) for r in ratios] + use_channels = list(range(len(operations))) + # Aspirate from all source resources await self.aspirate( - resources=[source], - vols=[sum(target_vols)], - flow_rates=[aspiration_flow_rate], - **backend_kwargs, + resources=list(operations.keys()), + vols=[sum(v for _, v in dests) + dead_volume for dests in operations.values()], + use_channels=use_channels, ) - dispense_flow_rates = dispense_flow_rates or [None] * len(targets) - for target, vol, dfr in zip(targets, target_vols, dispense_flow_rates): + + for group in zip_longest(*operations.values()): + dest, vols, channels = zip( + *( + (pair[0], pair[1], ch) + for pair, ch in zip_longest(group, use_channels) + if pair is not None + ) + ) await self.dispense( - resources=[target], - vols=[vol], - flow_rates=[dfr], - use_channels=[0], - **backend_kwargs, + resources=list(dest), + vols=list(vols), + use_channels=list(channels), ) @contextlib.contextmanager From fe651e24a95ce72474d9ecbff7377b6cefd0bc5e Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Fri, 10 Oct 2025 11:18:03 -0700 Subject: [PATCH 2/6] allow tip_spots in LH.transfer --- pylabrobot/liquid_handling/liquid_handler.py | 29 +++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/pylabrobot/liquid_handling/liquid_handler.py b/pylabrobot/liquid_handling/liquid_handler.py index 45d0202d016..8f370fbc056 100644 --- a/pylabrobot/liquid_handling/liquid_handler.py +++ b/pylabrobot/liquid_handling/liquid_handler.py @@ -5,13 +5,15 @@ import asyncio import contextlib import inspect -from itertools import zip_longest import json import logging import threading import warnings +from itertools import zip_longest from typing import ( Any, + AsyncGenerator, + AsyncIterator, Awaitable, Callable, Dict, @@ -1269,6 +1271,7 @@ async def transfer( vols: List[float], aspiration_kwargs: Optional[Dict[str, Any]] = None, dispense_kwargs: Optional[Dict[str, Any]] = None, + tip_spots: Optional[Union[List[TipSpot], AsyncIterator[TipSpot]]] = None, ): """Transfer liquid from one set of resources to another. Each input resource matches to exactly one output resource. @@ -1293,6 +1296,27 @@ async def transfer( use_channels = list(range(len(source_resources))) + channels_with_tips = [self.get_mounted_tips()[ch] for ch in use_channels] + if any(channels_with_tips) and not all(channels_with_tips): + raise RuntimeError("Either all or none of the channels must have tips.") + + did_pick_up_tips = False + if not any(channels_with_tips): + if tip_spots is None: + raise ValueError("No tips are mounted and no tip generator was provided.") + if isinstance(tip_spots, list) and len(tip_spots) < len(use_channels): + raise ValueError( + "Number of tip spots must be at least the number of channels, " + f"but got {len(tip_spots)} tip spots and {len(use_channels)} channels." + ) + if hasattr(tip_spots, "__aiter__") and hasattr(tip_spots, "__anext__"): + tip_spots = [await tip_spots.__anext__() for _ in use_channels] # type: ignore + assert isinstance(tip_spots, list) + await self.pick_up_tips(tip_spots=tip_spots, use_channels=use_channels) + did_pick_up_tips = True + elif tip_spots is not None: + warnings.warn("Tips are already mounted, ignoring provided tips.") + await self.aspirate( resources=source_resources, vols=vols, @@ -1307,6 +1331,9 @@ async def transfer( **(dispense_kwargs or {}), ) + if did_pick_up_tips: + await self.discard_tips(use_channels=use_channels) + async def distribute( self, operations: Dict[Container, List[Tuple[Container, float]]], From 04d6fff6af6e39d5c4f22953bcdc76077d0cdd82 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Fri, 10 Oct 2025 11:58:04 -0700 Subject: [PATCH 3/6] tip_spots required, support more transfers than num_channels --- .../backends/hamilton/STAR_backend.py | 8 +- pylabrobot/liquid_handling/liquid_handler.py | 76 +++++++++---------- 2 files changed, 41 insertions(+), 43 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 31e0d53a414..eceb7a2b8ad 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -1795,7 +1795,9 @@ async def aspirate( immersion_depth_direction = immersion_depth_direction or [ 0 if (id_ >= 0) else 1 for id_ in immersion_depth ] - immersion_depth = [im * (-1 if immersion_depth_direction[i] else 1) for i, im in enumerate(immersion_depth)] + immersion_depth = [ + im * (-1 if immersion_depth_direction[i] else 1) for i, im in enumerate(immersion_depth) + ] surface_following_distance = _fill_in_defaults(surface_following_distance, [0.0] * n) flow_rates = [ op.flow_rate or (hlc.aspiration_flow_rate if hlc is not None else 100.0) @@ -2101,7 +2103,9 @@ async def dispense( immersion_depth_direction = immersion_depth_direction or [ 0 if (id_ >= 0) else 1 for id_ in immersion_depth ] - immersion_depth = [im * (-1 if immersion_depth_direction[i] else 1) for i, im in enumerate(immersion_depth)] + immersion_depth = [ + im * (-1 if immersion_depth_direction[i] else 1) for i, im in enumerate(immersion_depth) + ] surface_following_distance = _fill_in_defaults(surface_following_distance, [0.0] * n) flow_rates = [ op.flow_rate or (hlc.dispense_flow_rate if hlc is not None else 120.0) diff --git a/pylabrobot/liquid_handling/liquid_handler.py b/pylabrobot/liquid_handling/liquid_handler.py index cfa1818f134..c90468db6d3 100644 --- a/pylabrobot/liquid_handling/liquid_handler.py +++ b/pylabrobot/liquid_handling/liquid_handler.py @@ -66,6 +66,7 @@ from pylabrobot.resources.rotation import Rotation from pylabrobot.serializer import deserialize, serialize from pylabrobot.tilting.tilter import Tilter +from pylabrobot.utils.list import batched from .backends import LiquidHandlerBackend from .standard import ( @@ -1269,9 +1270,10 @@ async def transfer( source_resources: Sequence[Container], dest_resources: Sequence[Container], vols: List[float], + tip_spots: Union[List[TipSpot], AsyncIterator[TipSpot]], aspiration_kwargs: Optional[Dict[str, Any]] = None, dispense_kwargs: Optional[Dict[str, Any]] = None, - tip_spots: Optional[Union[List[TipSpot], AsyncIterator[TipSpot]]] = None, + tip_drop_method: Literal["return", "discard"] = "discard", ): """Transfer liquid from one set of resources to another. Each input resource matches to exactly one output resource. @@ -1281,6 +1283,7 @@ async def transfer( ... source_resources=plate1["A1":"H8"], ... dest_resources=plate2["A1":"H8"], ... vols=[50] * 8, + ... tip_spots=tip_rack["A1":"H1"], ... ) """ @@ -1291,48 +1294,39 @@ async def transfer( f"and {len(vols)} volumes." ) - if len(source_resources) > self.backend.num_channels: - raise ValueError("Number of resources exceeds number of channels.") - - use_channels = list(range(len(source_resources))) - - channels_with_tips = [self.get_mounted_tips()[ch] for ch in use_channels] - if any(channels_with_tips) and not all(channels_with_tips): - raise RuntimeError("Either all or none of the channels must have tips.") - - did_pick_up_tips = False - if not any(channels_with_tips): - if tip_spots is None: - raise ValueError("No tips are mounted and no tip generator was provided.") - if isinstance(tip_spots, list) and len(tip_spots) < len(use_channels): - raise ValueError( - "Number of tip spots must be at least the number of channels, " - f"but got {len(tip_spots)} tip spots and {len(use_channels)} channels." - ) - if hasattr(tip_spots, "__aiter__") and hasattr(tip_spots, "__anext__"): - tip_spots = [await tip_spots.__anext__() for _ in use_channels] # type: ignore - assert isinstance(tip_spots, list) - await self.pick_up_tips(tip_spots=tip_spots, use_channels=use_channels) - did_pick_up_tips = True - elif tip_spots is not None: - warnings.warn("Tips are already mounted, ignoring provided tips.") - - await self.aspirate( - resources=source_resources, - vols=vols, - use_channels=use_channels, - **(aspiration_kwargs or {}), - ) + if isinstance(tip_spots, list) and len(tip_spots) < len(source_resources): + raise ValueError( + "Number of tip spots must be at least the number of channels, " + f"but got {len(tip_spots)} tip spots and {len(source_resources)} transfers." + ) + if hasattr(tip_spots, "__aiter__") and hasattr(tip_spots, "__anext__"): + tip_spots = [await tip_spots.__anext__() for _ in source_resources] # type: ignore + assert isinstance(tip_spots, list) + + for batch in range(0, len(source_resources), self.backend.num_channels): + batch_sources = source_resources[batch : batch + self.backend.num_channels] + batch_destinations = dest_resources[batch : batch + self.backend.num_channels] + batch_vols = vols[batch : batch + self.backend.num_channels] + batch_tip_spots = tip_spots[batch : batch + self.backend.num_channels] + + await self.pick_up_tips(batch_tip_spots) + + await self.aspirate( + resources=batch_sources, + vols=batch_vols, + **(aspiration_kwargs or {}), + ) - await self.dispense( - resources=dest_resources, - vols=vols, - use_channels=use_channels, - **(dispense_kwargs or {}), - ) + await self.dispense( + resources=batch_destinations, + vols=batch_vols, + **(dispense_kwargs or {}), + ) - if did_pick_up_tips: - await self.discard_tips(use_channels=use_channels) + if tip_drop_method == "return": + await self.return_tips() + else: + await self.discard_tips() async def distribute( self, From 1a31dba1becd3b13492c0857e8000f81b5092997 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Wed, 15 Oct 2025 11:46:21 -0700 Subject: [PATCH 4/6] add tip_spots to distribute --- pylabrobot/liquid_handling/liquid_handler.py | 35 ++++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/pylabrobot/liquid_handling/liquid_handler.py b/pylabrobot/liquid_handling/liquid_handler.py index c90468db6d3..cdce5bd3e68 100644 --- a/pylabrobot/liquid_handling/liquid_handler.py +++ b/pylabrobot/liquid_handling/liquid_handler.py @@ -12,7 +12,6 @@ from itertools import zip_longest from typing import ( Any, - AsyncGenerator, AsyncIterator, Awaitable, Callable, @@ -66,7 +65,6 @@ from pylabrobot.resources.rotation import Rotation from pylabrobot.serializer import deserialize, serialize from pylabrobot.tilting.tilter import Tilter -from pylabrobot.utils.list import batched from .backends import LiquidHandlerBackend from .standard import ( @@ -1330,8 +1328,12 @@ async def transfer( async def distribute( self, - operations: Dict[Container, List[Tuple[Container, float]]], + operations: Dict[Container, List[Tuple[Union[Container, List[Container]], float]]], + tip_spots: Union[List[TipSpot], AsyncIterator[TipSpot]], dead_volume: float = 10, + tip_drop_method: Literal["return", "discard"] = "discard", + aspiration_kwargs: Optional[Dict[str, Any]] = None, + dispense_kwargs: Optional[Dict[str, Any]] = None, ): """ Distribute liquid from one resource to multiple resources. @@ -1353,13 +1355,34 @@ async def distribute( use_channels = list(range(len(operations))) + if isinstance(tip_spots, list) and len(tip_spots) < len(operations): + raise ValueError( + "Number of tip spots must be at least the number of channels, " + f"but got {len(tip_spots)} tip spots and {len(operations)} distributions." + ) + if hasattr(tip_spots, "__aiter__") and hasattr(tip_spots, "__anext__"): + tip_spots = [await tip_spots.__anext__() for _ in operations] # type: ignore + assert isinstance(tip_spots, list) + + await self.pick_up_tips(tip_spots, use_channels=use_channels) + # Aspirate from all source resources await self.aspirate( resources=list(operations.keys()), vols=[sum(v for _, v in dests) + dead_volume for dests in operations.values()], use_channels=use_channels, + **(aspiration_kwargs or {}), ) + for source, dests in operations.items(): + for i, (dest, vol) in enumerate(dests): + if isinstance(dest, list): + if len(dest) > 1: + raise ValueError("Only one destination per dispense operation is supported.") + if len(dest) == 0: + raise ValueError("Destination list cannot be empty.") + operations[source][i] = (dest[0], vol) + for group in zip_longest(*operations.values()): dest, vols, channels = zip( *( @@ -1372,8 +1395,14 @@ async def distribute( resources=list(dest), vols=list(vols), use_channels=list(channels), + **(dispense_kwargs or {}), ) + if tip_drop_method == "return": + await self.return_tips(use_channels=use_channels) + else: + await self.discard_tips(use_channels=use_channels) + @contextlib.contextmanager def use_channels(self, channels: List[int]): """Temporarily use the specified channels as a default argument to `use_channels`. From 3ac1deb638f28adad6bdc85b6e114cbf4536a371 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Wed, 15 Oct 2025 12:04:56 -0700 Subject: [PATCH 5/6] batch distribute by num_channels --- pylabrobot/liquid_handling/liquid_handler.py | 67 ++++++++++---------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/pylabrobot/liquid_handling/liquid_handler.py b/pylabrobot/liquid_handling/liquid_handler.py index cdce5bd3e68..0f6f43d77c3 100644 --- a/pylabrobot/liquid_handling/liquid_handler.py +++ b/pylabrobot/liquid_handling/liquid_handler.py @@ -1350,11 +1350,6 @@ async def distribute( destination resource and the volume to dispense to that resource. """ - if len(operations) > self.backend.num_channels: - raise ValueError("Number of source resources exceeds number of channels.") - - use_channels = list(range(len(operations))) - if isinstance(tip_spots, list) and len(tip_spots) < len(operations): raise ValueError( "Number of tip spots must be at least the number of channels, " @@ -1364,16 +1359,6 @@ async def distribute( tip_spots = [await tip_spots.__anext__() for _ in operations] # type: ignore assert isinstance(tip_spots, list) - await self.pick_up_tips(tip_spots, use_channels=use_channels) - - # Aspirate from all source resources - await self.aspirate( - resources=list(operations.keys()), - vols=[sum(v for _, v in dests) + dead_volume for dests in operations.values()], - use_channels=use_channels, - **(aspiration_kwargs or {}), - ) - for source, dests in operations.items(): for i, (dest, vol) in enumerate(dests): if isinstance(dest, list): @@ -1383,25 +1368,43 @@ async def distribute( raise ValueError("Destination list cannot be empty.") operations[source][i] = (dest[0], vol) - for group in zip_longest(*operations.values()): - dest, vols, channels = zip( - *( - (pair[0], pair[1], ch) - for pair, ch in zip_longest(group, use_channels) - if pair is not None - ) - ) - await self.dispense( - resources=list(dest), - vols=list(vols), - use_channels=list(channels), - **(dispense_kwargs or {}), + operations_list = list(operations.items()) + for batch in range(0, len(operations_list), self.backend.num_channels): + batch_operations = operations_list[batch : batch + self.backend.num_channels] + batch_tips = tip_spots[batch : batch + self.backend.num_channels] + batch_sources = [src for src, _ in batch_operations] + batch_destinations = [dest for _, dest in batch_operations] + batch_volumes = [sum(v for _, v in dests) + dead_volume for dests in batch_destinations] + use_channels = list(range(len(batch_operations))) + + await self.pick_up_tips(batch_tips) + + # Aspirate from all source resources + await self.aspirate( + resources=batch_sources, + vols=batch_volumes, + **(aspiration_kwargs or {}), ) - if tip_drop_method == "return": - await self.return_tips(use_channels=use_channels) - else: - await self.discard_tips(use_channels=use_channels) + for group in zip_longest(*batch_destinations): + dest, vols, channels = zip( + *( + (pair[0], pair[1], ch) + for pair, ch in zip_longest(group, use_channels) + if pair is not None + ) + ) + await self.dispense( + resources=list(dest), + vols=list(vols), + use_channels=list(channels), + **(dispense_kwargs or {}), + ) + + if tip_drop_method == "return": + await self.return_tips() + else: + await self.discard_tips() @contextlib.contextmanager def use_channels(self, channels: List[int]): From 7a9ed201228b4882662e2409abbcd9484d7ecf90 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Sun, 9 Nov 2025 20:26:53 -0800 Subject: [PATCH 6/6] mark them experimental --- pylabrobot/liquid_handling/liquid_handler.py | 4 ++-- .../liquid_handling/liquid_handler_tests.py | 16 ++++++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/pylabrobot/liquid_handling/liquid_handler.py b/pylabrobot/liquid_handling/liquid_handler.py index 0f6f43d77c3..cc9f5c3a787 100644 --- a/pylabrobot/liquid_handling/liquid_handler.py +++ b/pylabrobot/liquid_handling/liquid_handler.py @@ -1263,7 +1263,7 @@ async def dispense( if error is not None: raise error - async def transfer( + async def experimental_transfer( self, source_resources: Sequence[Container], dest_resources: Sequence[Container], @@ -1326,7 +1326,7 @@ async def transfer( else: await self.discard_tips() - async def distribute( + async def experimental_distribute( self, operations: Dict[Container, List[Tuple[Union[Container, List[Container]], float]]], tip_spots: Union[List[TipSpot], AsyncIterator[TipSpot]], diff --git a/pylabrobot/liquid_handling/liquid_handler_tests.py b/pylabrobot/liquid_handling/liquid_handler_tests.py index 93cab36c643..1ac4cb71f1e 100644 --- a/pylabrobot/liquid_handling/liquid_handler_tests.py +++ b/pylabrobot/liquid_handling/liquid_handler_tests.py @@ -663,7 +663,7 @@ async def test_transfer(self): # Simple transfer self.plate.get_item("A1").tracker.set_liquids([(None, 10)]) - await self.lh.transfer(self.plate.get_well("A1"), self.plate["A2"], source_vol=10) + await self.lh.experimental_transfer(self.plate.get_well("A1"), self.plate["A2"], source_vol=10) self.assertEqual( self.get_first_command("aspirate"), @@ -691,7 +691,9 @@ async def test_transfer(self): # Transfer to multiple wells self.plate.get_item("A1").tracker.set_liquids([(None, 80)]) - await self.lh.transfer(self.plate.get_well("A1"), self.plate["A1:H1"], source_vol=80) + await self.lh.experimental_transfer( + self.plate.get_well("A1"), self.plate["A1:H1"], source_vol=80 + ) self.assertEqual( self.get_first_command("aspirate"), { @@ -728,7 +730,7 @@ async def test_transfer(self): # Transfer with ratios self.plate.get_item("A1").tracker.set_liquids([(None, 60)]) - await self.lh.transfer( + await self.lh.experimental_transfer( self.plate.get_well("A1"), self.plate["B1:C1"], source_vol=60, @@ -770,7 +772,9 @@ async def test_transfer(self): # Transfer with target_vols vols: List[float] = [3, 1, 4, 1, 5, 9, 6, 2] self.plate.get_item("A1").tracker.set_liquids([(None, sum(vols))]) - await self.lh.transfer(self.plate.get_well("A1"), self.plate["A1:H1"], target_vols=vols) + await self.lh.experimental_transfer( + self.plate.get_well("A1"), self.plate["A1:H1"], target_vols=vols + ) self.assertEqual( self.get_first_command("aspirate"), { @@ -806,7 +810,7 @@ async def test_transfer(self): # target_vols and source_vol specified with self.assertRaises(TypeError): - await self.lh.transfer( + await self.lh.experimental_transfer( self.plate.get_well("A1"), self.plate["A1:H1"], source_vol=100, @@ -815,7 +819,7 @@ async def test_transfer(self): # target_vols and ratios specified with self.assertRaises(TypeError): - await self.lh.transfer( + await self.lh.experimental_transfer( self.plate.get_well("A1"), self.plate["A1:H1"], ratios=[1] * 8,