Skip to content

Commit 2b3a14f

Browse files
authored
single container aspirate/dispense (#377)
1 parent f72b20f commit 2b3a14f

File tree

10 files changed

+59
-46
lines changed

10 files changed

+59
-46
lines changed

pylabrobot/liquid_handling/backends/backend.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
DropTipRack,
99
MultiHeadAspirationContainer,
1010
MultiHeadAspirationPlate,
11-
MultiHeadDispenseContainr,
11+
MultiHeadDispenseContainer,
1212
MultiHeadDispensePlate,
1313
Pickup,
1414
PickupTipRack,
@@ -123,7 +123,7 @@ async def aspirate96(
123123
"""Aspirate from all wells in 96 well plate."""
124124

125125
@abstractmethod
126-
async def dispense96(self, dispense: Union[MultiHeadDispensePlate, MultiHeadDispenseContainr]):
126+
async def dispense96(self, dispense: Union[MultiHeadDispensePlate, MultiHeadDispenseContainer]):
127127
"""Dispense to all wells in 96 well plate."""
128128

129129
@abstractmethod

pylabrobot/liquid_handling/backends/chatterbox.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
DropTipRack,
99
MultiHeadAspirationContainer,
1010
MultiHeadAspirationPlate,
11-
MultiHeadDispenseContainr,
11+
MultiHeadDispenseContainer,
1212
MultiHeadDispensePlate,
1313
Pickup,
1414
PickupTipRack,
@@ -221,7 +221,7 @@ async def aspirate96(
221221
resource = aspiration.container
222222
print(f"Aspirating {aspiration.volume} from {resource}.")
223223

224-
async def dispense96(self, dispense: Union[MultiHeadDispensePlate, MultiHeadDispenseContainr]):
224+
async def dispense96(self, dispense: Union[MultiHeadDispensePlate, MultiHeadDispenseContainer]):
225225
if isinstance(dispense, MultiHeadDispensePlate):
226226
resource = dispense.wells[0].parent
227227
else:

pylabrobot/liquid_handling/backends/hamilton/STAR.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
GripDirection,
3333
MultiHeadAspirationContainer,
3434
MultiHeadAspirationPlate,
35-
MultiHeadDispenseContainr,
35+
MultiHeadDispenseContainer,
3636
MultiHeadDispensePlate,
3737
Pickup,
3838
PickupTipRack,
@@ -2375,7 +2375,7 @@ async def aspirate96(
23752375

23762376
async def dispense96(
23772377
self,
2378-
dispense: Union[MultiHeadDispensePlate, MultiHeadDispenseContainr],
2378+
dispense: Union[MultiHeadDispensePlate, MultiHeadDispenseContainer],
23792379
jet: bool = False,
23802380
empty: bool = False,
23812381
blow_out: bool = False,

pylabrobot/liquid_handling/backends/hamilton/vantage.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
DropTipRack,
1717
MultiHeadAspirationContainer,
1818
MultiHeadAspirationPlate,
19-
MultiHeadDispenseContainr,
19+
MultiHeadDispenseContainer,
2020
MultiHeadDispensePlate,
2121
Pickup,
2222
PickupTipRack,
@@ -1135,7 +1135,7 @@ async def aspirate96(
11351135

11361136
async def dispense96(
11371137
self,
1138-
dispense: Union[MultiHeadDispensePlate, MultiHeadDispenseContainr],
1138+
dispense: Union[MultiHeadDispensePlate, MultiHeadDispenseContainer],
11391139
jet: bool = False,
11401140
blow_out: bool = False, # "empty" in the VENUS liquid editor
11411141
empty: bool = False, # truly "empty", does not exist in liquid editor, dm4

pylabrobot/liquid_handling/backends/opentrons_backend.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
DropTipRack,
1212
MultiHeadAspirationContainer,
1313
MultiHeadAspirationPlate,
14-
MultiHeadDispenseContainr,
14+
MultiHeadDispenseContainer,
1515
MultiHeadDispensePlate,
1616
Pickup,
1717
PickupTipRack,
@@ -584,7 +584,7 @@ async def aspirate96(
584584
):
585585
raise NotImplementedError("The Opentrons backend does not support the 96 head.")
586586

587-
async def dispense96(self, dispense: Union[MultiHeadDispensePlate, MultiHeadDispenseContainr]):
587+
async def dispense96(self, dispense: Union[MultiHeadDispensePlate, MultiHeadDispenseContainer]):
588588
raise NotImplementedError("The Opentrons backend does not support the 96 head.")
589589

590590
async def pick_up_resource(self, pickup: ResourcePickup):

pylabrobot/liquid_handling/backends/serializing_backend.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
DropTipRack,
1111
MultiHeadAspirationContainer,
1212
MultiHeadAspirationPlate,
13-
MultiHeadDispenseContainr,
13+
MultiHeadDispenseContainer,
1414
MultiHeadDispensePlate,
1515
Pickup,
1616
PickupTipRack,
@@ -173,7 +173,7 @@ async def aspirate96(
173173
data["aspiration"]["trough"] = aspiration.container.name
174174
await self.send_command(command="aspirate96", data=data)
175175

176-
async def dispense96(self, dispense: Union[MultiHeadDispensePlate, MultiHeadDispenseContainr]):
176+
async def dispense96(self, dispense: Union[MultiHeadDispensePlate, MultiHeadDispenseContainer]):
177177
data = {
178178
"dispense": {
179179
"offset": serialize(dispense.offset),

pylabrobot/liquid_handling/backends/tecan/EVO.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
DropTipRack,
3030
MultiHeadAspirationContainer,
3131
MultiHeadAspirationPlate,
32-
MultiHeadDispenseContainr,
32+
MultiHeadDispenseContainer,
3333
MultiHeadDispensePlate,
3434
Pickup,
3535
PickupTipRack,
@@ -516,7 +516,7 @@ async def aspirate96(
516516
):
517517
raise NotImplementedError()
518518

519-
async def dispense96(self, dispense: Union[MultiHeadDispensePlate, MultiHeadDispenseContainr]):
519+
async def dispense96(self, dispense: Union[MultiHeadDispensePlate, MultiHeadDispenseContainer]):
520520
raise NotImplementedError()
521521

522522
async def pick_up_resource(self, pickup: ResourcePickup):

pylabrobot/liquid_handling/liquid_handler.py

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
GripDirection,
6666
MultiHeadAspirationContainer,
6767
MultiHeadAspirationPlate,
68-
MultiHeadDispenseContainr,
68+
MultiHeadDispenseContainer,
6969
MultiHeadDispensePlate,
7070
Pickup,
7171
PickupTipRack,
@@ -1568,6 +1568,7 @@ async def aspirate96(
15681568
flow_rate = float(flow_rate) if flow_rate is not None else None
15691569
blow_out_air_volume = float(blow_out_air_volume) if blow_out_air_volume is not None else None
15701570

1571+
containers: Sequence[Container]
15711572
if isinstance(resource, Container):
15721573
if (
15731574
resource.get_absolute_size_x() < 108.0 or resource.get_absolute_size_y() < 70.0
@@ -1598,24 +1599,26 @@ async def aspirate96(
15981599
blow_out_air_volume=blow_out_air_volume,
15991600
liquids=cast(List[List[Tuple[Optional[Liquid], float]]], all_liquids), # stupid
16001601
)
1602+
1603+
containers = [resource]
16011604
else:
16021605
if isinstance(resource, Plate):
16031606
if resource.has_lid():
16041607
raise ValueError("Aspirating from plate with lid")
1605-
wells = resource.get_all_items()
1608+
containers = resource.get_all_items()
16061609
else:
1607-
wells = resource
1610+
containers = resource
16081611

16091612
# ensure that wells are all in the same plate
1610-
plate = wells[0].parent
1611-
for well in wells:
1613+
plate = containers[0].parent
1614+
for well in containers:
16121615
if well.parent != plate:
16131616
raise ValueError("All wells must be in the same plate")
16141617

1615-
if not len(wells) == 96:
1616-
raise ValueError(f"aspirate96 expects 96 wells, got {len(wells)}")
1618+
if not len(containers) == 96:
1619+
raise ValueError(f"aspirate96 expects 96 wells, got {len(containers)}")
16171620

1618-
for well, channel in zip(wells, self.head96.values()):
1621+
for well, channel in zip(containers, self.head96.values()):
16191622
# superfluous to have append in two places but the type checker is very angry and does not
16201623
# understand that Optional[Liquid] (remove_liquid) is the same as None from the first case
16211624
if well.tracker.is_disabled or not does_volume_tracking():
@@ -1629,7 +1632,7 @@ async def aspirate96(
16291632
channel.get_tip().tracker.add_liquid(liquid=liquid, volume=vol)
16301633

16311634
aspiration = MultiHeadAspirationPlate(
1632-
wells=wells,
1635+
wells=cast(List[Well], containers),
16331636
volume=volume,
16341637
offset=offset,
16351638
flow_rate=flow_rate,
@@ -1642,9 +1645,9 @@ async def aspirate96(
16421645
try:
16431646
await self.backend.aspirate96(aspiration=aspiration, **backend_kwargs)
16441647
except Exception as error:
1645-
for channel, well in zip(self.head96.values(), wells):
1646-
if does_volume_tracking() and not well.tracker.is_disabled:
1647-
well.tracker.rollback()
1648+
for channel, container in zip(self.head96.values(), containers):
1649+
if does_volume_tracking() and not container.tracker.is_disabled:
1650+
container.tracker.rollback()
16481651
channel.get_tip().tracker.rollback()
16491652
self._trigger_callback(
16501653
"aspirate96",
@@ -1654,10 +1657,11 @@ async def aspirate96(
16541657
**backend_kwargs,
16551658
)
16561659
else:
1657-
for channel, well in zip(self.head96.values(), wells):
1658-
if does_volume_tracking() and not well.tracker.is_disabled:
1659-
well.tracker.commit()
1660-
channel.get_tip().tracker.commit()
1660+
for channel, container in zip(self.head96.values(), containers):
1661+
if does_volume_tracking() and not container.tracker.is_disabled:
1662+
container.tracker.commit()
1663+
channel.get_tip().tracker.commit()
1664+
16611665
self._trigger_callback(
16621666
"aspirate96",
16631667
liquid_handler=self,
@@ -1708,13 +1712,14 @@ async def dispense96(
17081712

17091713
tips = [channel.get_tip() for channel in self.head96.values()]
17101714
all_liquids: List[List[Tuple[Optional[Liquid], float]]] = []
1711-
dispense: Union[MultiHeadDispensePlate, MultiHeadDispenseContainr]
1715+
dispense: Union[MultiHeadDispensePlate, MultiHeadDispenseContainer]
17121716

17131717
# Convert everything to floats to handle exotic number types
17141718
volume = float(volume)
17151719
flow_rate = float(flow_rate) if flow_rate is not None else None
17161720
blow_out_air_volume = float(blow_out_air_volume) if blow_out_air_volume is not None else None
17171721

1722+
containers: Sequence[Container]
17181723
if isinstance(resource, Container):
17191724
if (
17201725
resource.get_absolute_size_x() < 108.0 or resource.get_absolute_size_y() < 70.0
@@ -1735,7 +1740,7 @@ async def dispense96(
17351740
for liquid, vol in reversed(reversed_liquids):
17361741
channel.get_tip().tracker.add_liquid(liquid=liquid, volume=vol)
17371742

1738-
dispense = MultiHeadDispenseContainr(
1743+
dispense = MultiHeadDispenseContainer(
17391744
container=resource,
17401745
volume=volume,
17411746
offset=offset,
@@ -1745,24 +1750,26 @@ async def dispense96(
17451750
blow_out_air_volume=blow_out_air_volume,
17461751
liquids=cast(List[List[Tuple[Optional[Liquid], float]]], all_liquids), # stupid
17471752
)
1753+
1754+
containers = [resource]
17481755
else:
17491756
if isinstance(resource, Plate):
17501757
if resource.has_lid():
17511758
raise ValueError("Aspirating from plate with lid")
1752-
wells = resource.get_all_items()
1753-
else:
1754-
wells = resource
1759+
containers = resource.get_all_items()
1760+
else: # List[Well]
1761+
containers = resource
17551762

17561763
# ensure that wells are all in the same plate
1757-
plate = wells[0].parent
1758-
for well in wells:
1764+
plate = containers[0].parent
1765+
for well in containers:
17591766
if well.parent != plate:
17601767
raise ValueError("All wells must be in the same plate")
17611768

1762-
if not len(wells) == 96:
1763-
raise ValueError(f"dispense96 expects 96 wells, got {len(wells)}")
1769+
if not len(containers) == 96:
1770+
raise ValueError(f"dispense96 expects 96 wells, got {len(containers)}")
17641771

1765-
for channel, well in zip(self.head96.values(), wells):
1772+
for channel, well in zip(self.head96.values(), containers):
17661773
# even if the volume tracker is disabled, a liquid (None, volume) is added to the list
17671774
# during the aspiration command
17681775
liquids = channel.get_tip().tracker.remove_liquid(volume=volume)
@@ -1773,7 +1780,7 @@ async def dispense96(
17731780
well.tracker.add_liquid(liquid=liquid, volume=vol)
17741781

17751782
dispense = MultiHeadDispensePlate(
1776-
wells=wells,
1783+
wells=cast(List[Well], containers),
17771784
volume=volume,
17781785
offset=offset,
17791786
flow_rate=flow_rate,
@@ -1786,9 +1793,9 @@ async def dispense96(
17861793
try:
17871794
await self.backend.dispense96(dispense=dispense, **backend_kwargs)
17881795
except Exception as error:
1789-
for channel, well in zip(self.head96.values(), wells):
1796+
for channel, container in zip(self.head96.values(), containers):
17901797
if does_volume_tracking() and not well.tracker.is_disabled:
1791-
well.tracker.rollback()
1798+
container.tracker.rollback()
17921799
channel.get_tip().tracker.rollback()
17931800

17941801
self._trigger_callback(
@@ -1799,9 +1806,9 @@ async def dispense96(
17991806
**backend_kwargs,
18001807
)
18011808
else:
1802-
for channel, well in zip(self.head96.values(), wells):
1809+
for channel, container in zip(self.head96.values(), containers):
18031810
if does_volume_tracking() and not well.tracker.is_disabled:
1804-
well.tracker.commit()
1811+
container.tracker.commit()
18051812
channel.get_tip().tracker.commit()
18061813

18071814
self._trigger_callback(

pylabrobot/liquid_handling/liquid_handler_tests.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
NoTipError,
3636
)
3737
from pylabrobot.resources.hamilton import HTF, STF, STARLetDeck
38+
from pylabrobot.resources.opentrons.reservoirs import agilent_1_reservoir_290ml
3839
from pylabrobot.resources.utils import create_ordered_items_2d
3940
from pylabrobot.resources.volume_tracker import (
4041
set_cross_contamination_tracking,
@@ -1009,6 +1010,11 @@ async def test_save_state(self):
10091010

10101011
set_volume_tracking(enabled=False)
10111012

1013+
async def test_aspirate_single_reservoir(self):
1014+
reagent_reservoir = agilent_1_reservoir_290ml(name="reservoir")
1015+
await self.lh.pick_up_tips96(self.tip_rack)
1016+
await self.lh.aspirate96(reagent_reservoir.get_item("A1"), volume=100)
1017+
10121018

10131019
class TestLiquidHandlerVolumeTracking(unittest.IsolatedAsyncioTestCase):
10141020
async def asyncSetUp(self):

pylabrobot/liquid_handling/standard.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ class MultiHeadAspirationContainer:
109109

110110

111111
@dataclass(frozen=True)
112-
class MultiHeadDispenseContainr:
112+
class MultiHeadDispenseContainer:
113113
container: Container
114114
offset: Coordinate
115115
tips: List[Tip]

0 commit comments

Comments
 (0)