Skip to content

Commit a5cfff3

Browse files
authored
lh.{aspirate,dispense} mix parameter (OT2, STAR, Vantage) (#691)
1 parent db67117 commit a5cfff3

File tree

14 files changed

+169
-73
lines changed

14 files changed

+169
-73
lines changed

pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1712,6 +1712,11 @@ async def aspirate(
17121712
and 360. Defaults to well bottom + liquid height. Should use absolute z.
17131713
"""
17141714

1715+
if mix_volume is not None or mix_cycles is not None or mix_speed is not None:
1716+
raise NotImplementedError(
1717+
"Mixing through backend kwargs is deprecated. Use the `mix` parameter of LiquidHandler.aspirate instead."
1718+
)
1719+
17151720
x_positions, y_positions, channels_involved = self._ops_to_fw_positions(ops, use_channels)
17161721

17171722
n = len(ops)
@@ -1817,17 +1822,12 @@ async def aspirate(
18171822
hlc.aspiration_settling_time if hlc is not None else 0.0 for hlc in hamilton_liquid_classes
18181823
],
18191824
)
1820-
mix_volume = _fill_in_defaults(mix_volume, [0.0] * n)
1821-
mix_cycles = _fill_in_defaults(mix_cycles, [0] * n)
1825+
mix_volume = [op.mix.volume if op.mix is not None else 0.0 for op in ops]
1826+
mix_cycles = [op.mix.repetitions if op.mix is not None else 0 for op in ops]
18221827
mix_position_from_liquid_surface = _fill_in_defaults(
18231828
mix_position_from_liquid_surface, [0.0] * n
18241829
)
1825-
mix_speed = _fill_in_defaults(
1826-
mix_speed,
1827-
default=[
1828-
hlc.aspiration_mix_flow_rate if hlc is not None else 50.0 for hlc in hamilton_liquid_classes
1829-
],
1830-
)
1830+
mix_speed = [op.mix.flow_rate if op.mix is not None else 100.0 for op in ops]
18311831
mix_surface_following_distance = _fill_in_defaults(mix_surface_following_distance, [0.0] * n)
18321832
limit_curve_index = _fill_in_defaults(limit_curve_index, [0] * n)
18331833

@@ -2007,6 +2007,11 @@ async def dispense(
20072007
documentation. Dispense mode 4.
20082008
"""
20092009

2010+
if mix_volume is not None or mix_cycles is not None or mix_speed is not None:
2011+
raise NotImplementedError(
2012+
"Mixing through backend kwargs is deprecated. Use the `mix` parameter of LiquidHandler.dispense instead."
2013+
)
2014+
20102015
x_positions, y_positions, channels_involved = self._ops_to_fw_positions(ops, use_channels)
20112016

20122017
n = len(ops)
@@ -2113,17 +2118,12 @@ async def dispense(
21132118
hlc.dispense_settling_time if hlc is not None else 0.0 for hlc in hamilton_liquid_classes
21142119
],
21152120
)
2116-
mix_volume = _fill_in_defaults(mix_volume, [0.0] * n)
2117-
mix_cycles = _fill_in_defaults(mix_cycles, [0] * n)
2121+
mix_volume = [op.mix.volume if op.mix is not None else 0.0 for op in ops]
2122+
mix_cycles = [op.mix.repetitions if op.mix is not None else 0 for op in ops]
21182123
mix_position_from_liquid_surface = _fill_in_defaults(
21192124
mix_position_from_liquid_surface, [0.0] * n
21202125
)
2121-
mix_speed = _fill_in_defaults(
2122-
mix_speed,
2123-
default=[
2124-
hlc.dispense_mix_flow_rate if hlc is not None else 50.0 for hlc in hamilton_liquid_classes
2125-
],
2126-
)
2126+
mix_speed = [op.mix.flow_rate if op.mix is not None else 1.0 for op in ops]
21272127
mix_surface_following_distance = _fill_in_defaults(mix_surface_following_distance, [0.0] * n)
21282128
limit_curve_index = _fill_in_defaults(limit_curve_index, [0] * n)
21292129

@@ -2281,7 +2281,7 @@ async def aspirate96(
22812281
mix_cycles: int = 0,
22822282
mix_position_from_liquid_surface: float = 0,
22832283
surface_following_distance_during_mix: float = 0,
2284-
speed_of_mix: float = 120.0,
2284+
speed_of_mix: float = 0.0,
22852285
limit_curve_index: int = 0,
22862286
):
22872287
"""Aspirate using the Core96 head.
@@ -2327,6 +2327,11 @@ async def aspirate96(
23272327
limit_curve_index: The index of the limit curve to use.
23282328
"""
23292329

2330+
if mix_volume != 0 or mix_cycles != 0 or speed_of_mix != 0:
2331+
raise NotImplementedError(
2332+
"Mixing through backend kwargs is deprecated. Use the `mix` parameter of LiquidHandler.aspirate96 instead."
2333+
)
2334+
23302335
assert self.core96_head_installed, "96 head must be installed"
23312336

23322337
# get the first well and tip as representatives
@@ -2395,7 +2400,6 @@ async def aspirate96(
23952400
flow_rate = aspiration.flow_rate or (hlc.aspiration_flow_rate if hlc is not None else 250)
23962401
swap_speed = swap_speed or (hlc.aspiration_swap_speed if hlc is not None else 100)
23972402
settling_time = settling_time or (hlc.aspiration_settling_time if hlc is not None else 0.5)
2398-
speed_of_mix = speed_of_mix or (hlc.aspiration_mix_flow_rate if hlc is not None else 10.0)
23992403

24002404
channel_pattern = [True] * 12 * 8
24012405

@@ -2444,11 +2448,11 @@ async def aspirate96(
24442448
gamma_lld_sensitivity=gamma_lld_sensitivity,
24452449
swap_speed=round(swap_speed * 10),
24462450
settling_time=round(settling_time * 10),
2447-
mix_volume=round(mix_volume * 10),
2448-
mix_cycles=mix_cycles,
2451+
mix_volume=round(aspiration.mix.volume * 10) if aspiration.mix is not None else 0,
2452+
mix_cycles=aspiration.mix.repetitions if aspiration.mix is not None else 0,
24492453
mix_position_from_liquid_surface=round(mix_position_from_liquid_surface * 10),
24502454
surface_following_distance_during_mix=round(surface_following_distance_during_mix * 10),
2451-
speed_of_mix=round(speed_of_mix * 10),
2455+
speed_of_mix=round(aspiration.mix.flow_rate * 10) if aspiration.mix is not None else 1200,
24522456
channel_pattern=channel_pattern,
24532457
limit_curve_index=limit_curve_index,
24542458
tadm_algorithm=False,
@@ -2482,7 +2486,7 @@ async def dispense96(
24822486
mixing_cycles: int = 0,
24832487
mixing_position_from_liquid_surface: float = 0,
24842488
surface_following_distance_during_mixing: float = 0,
2485-
speed_of_mixing: float = 120.0,
2489+
speed_of_mixing: float = 0.0,
24862490
limit_curve_index: int = 0,
24872491
cut_off_speed: float = 5.0,
24882492
stop_back_volume: float = 0,
@@ -2523,6 +2527,11 @@ async def dispense96(
25232527
stop_back_volume: Unknown.
25242528
"""
25252529

2530+
if mixing_volume != 0 or mixing_cycles != 0 or speed_of_mixing != 0:
2531+
raise NotImplementedError(
2532+
"Mixing through backend kwargs is deprecated. Use the `mix` parameter of LiquidHandler.dispense instead."
2533+
)
2534+
25262535
assert self.core96_head_installed, "96 head must be installed"
25272536

25282537
# get the first well and tip as representatives
@@ -2593,7 +2602,6 @@ async def dispense96(
25932602
flow_rate = dispense.flow_rate or (hlc.dispense_flow_rate if hlc is not None else 120)
25942603
swap_speed = swap_speed or (hlc.dispense_swap_speed if hlc is not None else 100)
25952604
settling_time = settling_time or (hlc.dispense_settling_time if hlc is not None else 5)
2596-
speed_of_mixing = speed_of_mixing or (hlc.dispense_mix_flow_rate if hlc is not None else 100)
25972605

25982606
channel_pattern = [True] * 12 * 8
25992607

@@ -2628,11 +2636,11 @@ async def dispense96(
26282636
gamma_lld_sensitivity=gamma_lld_sensitivity,
26292637
swap_speed=round(swap_speed * 10),
26302638
settling_time=round(settling_time * 10),
2631-
mixing_volume=round(mixing_volume * 10),
2632-
mixing_cycles=mixing_cycles,
2639+
mixing_volume=round(dispense.mix.volume * 10) if dispense.mix is not None else 0,
2640+
mixing_cycles=dispense.mix.repetitions if dispense.mix is not None else 0,
26332641
mixing_position_from_liquid_surface=round(mixing_position_from_liquid_surface * 10),
26342642
surface_following_distance_during_mixing=round(surface_following_distance_during_mixing * 10),
2635-
speed_of_mixing=round(speed_of_mixing * 10),
2643+
speed_of_mixing=round(dispense.mix.flow_rate * 10) if dispense.mix is not None else 1200,
26362644
channel_pattern=channel_pattern,
26372645
limit_curve_index=limit_curve_index,
26382646
tadm_algorithm=False,

pylabrobot/liquid_handling/backends/hamilton/vantage_backend.py

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,11 @@ async def aspirate(
622622
determined automatically based on the tip and liquid used.
623623
"""
624624

625+
if mix_volume is not None or mix_cycles is not None or mix_speed is not None:
626+
raise NotImplementedError(
627+
"Mixing through backend kwargs is deprecated. Use the `mix` parameter of LiquidHandler.dispense instead."
628+
)
629+
625630
x_positions, y_positions, channels_involved = self._ops_to_fw_positions(ops, use_channels)
626631

627632
if jet is None:
@@ -730,12 +735,12 @@ async def aspirate(
730735
],
731736
swap_speed=[round(ss * 10) for ss in swap_speed or [2] * len(ops)],
732737
settling_time=[round(st * 10) for st in settling_time or [1] * len(ops)],
733-
mix_volume=[round(mv * 100) for mv in mix_volume or [0] * len(ops)],
734-
mix_cycles=mix_cycles or [0] * len(ops),
738+
mix_volume=[round(op.mix.volume * 100) if op.mix is not None else 0 for op in ops],
739+
mix_cycles=[op.mix.repetitions if op.mix is not None else 0 for op in ops],
735740
mix_position_in_z_direction_from_liquid_surface=[
736741
round(mp) for mp in mix_position_in_z_direction_from_liquid_surface or [0] * len(ops)
737742
],
738-
mix_speed=[round(ms * 10) for ms in mix_speed or [250] * len(ops)],
743+
mix_speed=[round(op.mix.flow_rate * 10) if op.mix is not None else 2500 for op in ops],
739744
surface_following_distance_during_mixing=[
740745
round(sfdm * 10) for sfdm in surface_following_distance_during_mixing or [0] * len(ops)
741746
],
@@ -808,6 +813,11 @@ async def dispense(
808813
documentation. Dispense mode 4.
809814
"""
810815

816+
if mix_volume is not None or mix_cycles is not None or mix_speed is not None:
817+
raise NotImplementedError(
818+
"Mixing through backend kwargs is deprecated. Use the `mix` parameter of LiquidHandler.dispense instead."
819+
)
820+
811821
x_positions, y_positions, channels_involved = self._ops_to_fw_positions(ops, use_channels)
812822

813823
if jet is None:
@@ -922,12 +932,12 @@ async def dispense(
922932
pressure_lld_sensitivity=pressure_lld_sensitivity or [1] * len(ops),
923933
swap_speed=[round(ss * 10) for ss in swap_speed or [1] * len(ops)],
924934
settling_time=[round(st * 10) for st in settling_time or [0] * len(ops)],
925-
mix_volume=[round(mv * 100) for mv in mix_volume or [0] * len(ops)],
926-
mix_cycles=mix_cycles or [0] * len(ops),
935+
mix_volume=[round(op.mix.volume * 100) if op.mix is not None else 0 for op in ops],
936+
mix_cycles=[op.mix.repetitions if op.mix is not None else 0 for op in ops],
927937
mix_position_in_z_direction_from_liquid_surface=[
928938
round(mp) for mp in mix_position_in_z_direction_from_liquid_surface or [0] * len(ops)
929939
],
930-
mix_speed=[round(ms * 10) for ms in mix_speed or [1] * len(ops)],
940+
mix_speed=[round(op.mix.flow_rate * 100) if op.mix is not None else 10 for op in ops],
931941
surface_following_distance_during_mixing=[
932942
round(sfdm * 10) for sfdm in surface_following_distance_during_mixing or [0] * len(ops)
933943
],
@@ -1028,7 +1038,7 @@ async def aspirate96(
10281038
mix_cycles: int = 0,
10291039
mix_position_in_z_direction_from_liquid_surface: float = 0,
10301040
surface_following_distance_during_mixing: float = 0,
1031-
mix_speed: float = 2,
1041+
mix_speed: float = 0,
10321042
limit_curve_index: int = 0,
10331043
tadm_channel_pattern: Optional[List[bool]] = None,
10341044
tadm_algorithm_on_off: int = 0,
@@ -1046,6 +1056,11 @@ async def aspirate96(
10461056
"""
10471057
# assert self.core96_head_installed, "96 head must be installed"
10481058

1059+
if mix_volume != 0 or mix_cycles != 0 or mix_speed != 0:
1060+
raise NotImplementedError(
1061+
"Mixing through backend kwargs is deprecated. Use the `mix` parameter of LiquidHandler.dispense96 instead."
1062+
)
1063+
10491064
if isinstance(aspiration, MultiHeadAspirationPlate):
10501065
plate = aspiration.wells[0].parent
10511066
assert isinstance(plate, Plate), "MultiHeadAspirationPlate well parent must be a Plate"
@@ -1141,15 +1156,15 @@ async def aspirate96(
11411156
lld_sensitivity=lld_sensitivity,
11421157
swap_speed=round(swap_speed * 10),
11431158
settling_time=round(settling_time * 10),
1144-
mix_volume=round(mix_volume * 100),
1145-
mix_cycles=mix_cycles,
1159+
mix_volume=round(aspiration.mix.volume * 100) if aspiration.mix is not None else 0,
1160+
mix_cycles=aspiration.mix.repetitions if aspiration.mix is not None else 0,
11461161
mix_position_in_z_direction_from_liquid_surface=round(
11471162
mix_position_in_z_direction_from_liquid_surface * 100
11481163
),
11491164
surface_following_distance_during_mixing=round(
11501165
surface_following_distance_during_mixing * 100
11511166
),
1152-
mix_speed=round(mix_speed * 10),
1167+
mix_speed=round(aspiration.mix.flow_rate * 10) if aspiration.mix is not None else 20,
11531168
limit_curve_index=limit_curve_index,
11541169
tadm_channel_pattern=tadm_channel_pattern,
11551170
tadm_algorithm_on_off=tadm_algorithm_on_off,
@@ -1205,6 +1220,11 @@ async def dispense96(
12051220
determined based on the jet, blow_out, and empty parameters.
12061221
"""
12071222

1223+
if mix_volume != 0 or mix_cycles != 0 or mix_speed is not None:
1224+
raise NotImplementedError(
1225+
"Mixing through backend kwargs is deprecated. Use the `mix` parameter of LiquidHandler.dispense96 instead."
1226+
)
1227+
12081228
if isinstance(dispense, MultiHeadDispensePlate):
12091229
plate = dispense.wells[0].parent
12101230
assert isinstance(plate, Plate), "MultiHeadDispensePlate well parent must be a Plate"
@@ -1267,7 +1287,6 @@ async def dispense96(
12671287
flow_rate = dispense.flow_rate or (hlc.dispense_flow_rate if hlc is not None else 250)
12681288
swap_speed = swap_speed or (hlc.dispense_swap_speed if hlc is not None else 100)
12691289
settling_time = settling_time or (hlc.dispense_settling_time if hlc is not None else 5)
1270-
mix_speed = mix_speed or (hlc.dispense_mix_flow_rate if hlc is not None else 100)
12711290
type_of_dispensing_mode = type_of_dispensing_mode or _get_dispense_mode(
12721291
jet=jet, empty=empty, blow_out=blow_out
12731292
)
@@ -1303,13 +1322,13 @@ async def dispense96(
13031322
side_touch_off_distance=round(side_touch_off_distance * 10),
13041323
swap_speed=round(swap_speed * 10),
13051324
settling_time=round(settling_time * 10),
1306-
mix_volume=round(mix_volume * 10),
1307-
mix_cycles=mix_cycles,
1325+
mix_volume=round(dispense.mix.volume * 100) if dispense.mix is not None else 0,
1326+
mix_cycles=dispense.mix.repetitions if dispense.mix is not None else 0,
13081327
mix_position_in_z_direction_from_liquid_surface=round(
13091328
mix_position_in_z_direction_from_liquid_surface * 10
13101329
),
13111330
surface_following_distance_during_mixing=round(surface_following_distance_during_mixing * 10),
1312-
mix_speed=round(mix_speed * 10),
1331+
mix_speed=round(dispense.mix.flow_rate * 10) if dispense.mix is not None else 10,
13131332
limit_curve_index=limit_curve_index,
13141333
tadm_channel_pattern=tadm_channel_pattern,
13151334
tadm_algorithm_on_off=tadm_algorithm_on_off,

pylabrobot/liquid_handling/backends/opentrons_backend.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,19 @@ async def aspirate(self, ops: List[SingleChannelAspiration], use_channels: List[
420420
pipette_id=pipette_id,
421421
)
422422

423+
if op.mix is not None:
424+
for _ in range(op.mix.repetitions):
425+
ot_api.lh.aspirate_in_place(
426+
volume=op.mix.volume,
427+
flow_rate=op.mix.flow_rate,
428+
pipette_id=pipette_id,
429+
)
430+
ot_api.lh.dispense_in_place(
431+
volume=op.mix.volume,
432+
flow_rate=op.mix.flow_rate,
433+
pipette_id=pipette_id,
434+
)
435+
423436
ot_api.lh.aspirate_in_place(
424437
volume=volume,
425438
flow_rate=flow_rate,
@@ -490,6 +503,19 @@ async def dispense(self, ops: List[SingleChannelDispense], use_channels: List[in
490503
pipette_id=pipette_id,
491504
)
492505

506+
if op.mix is not None:
507+
for _ in range(op.mix.repetitions):
508+
ot_api.lh.aspirate_in_place(
509+
volume=op.mix.volume,
510+
flow_rate=op.mix.flow_rate,
511+
pipette_id=pipette_id,
512+
)
513+
ot_api.lh.dispense_in_place(
514+
volume=op.mix.volume,
515+
flow_rate=op.mix.flow_rate,
516+
pipette_id=pipette_id,
517+
)
518+
493519
traversal_location = op.resource.get_absolute_location("c", "c", "cavity_bottom") + op.offset
494520
traversal_location.z = self.traversal_height
495521
await self.move_pipette_head(

pylabrobot/liquid_handling/backends/serializing_backend.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ async def aspirate(self, ops: List[SingleChannelAspiration], use_channels: List[
108108
"liquid_height": serialize(op.liquid_height),
109109
"blow_out_air_volume": serialize(op.blow_out_air_volume),
110110
"liquids": serialize(op.liquids),
111+
"mix": serialize(op.mix),
111112
}
112113
for op in ops
113114
]
@@ -127,6 +128,7 @@ async def dispense(self, ops: List[SingleChannelDispense], use_channels: List[in
127128
"liquid_height": serialize(op.liquid_height),
128129
"blow_out_air_volume": serialize(op.blow_out_air_volume),
129130
"liquids": serialize(op.liquids),
131+
"mix": serialize(op.mix),
130132
}
131133
for op in ops
132134
]

pylabrobot/liquid_handling/backends/serializing_backend_tests.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ async def test_aspirate(self):
105105
"liquid_height": None,
106106
"blow_out_air_volume": None,
107107
"liquids": [[None, 10]],
108+
"mix": None,
108109
}
109110
],
110111
"use_channels": [0],
@@ -133,6 +134,7 @@ async def test_dispense(self):
133134
"liquid_height": None,
134135
"blow_out_air_volume": None,
135136
"liquids": [[None, 10]],
137+
"mix": None,
136138
}
137139
],
138140
"use_channels": [0],

pylabrobot/liquid_handling/backends/tecan/EVO_tests.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ async def test_aspirate(self):
143143
liquid_height=10,
144144
blow_out_air_volume=0,
145145
liquids=[(None, 100)],
146+
mix=None,
146147
)
147148
await self.evo.aspirate([op], use_channels=[0])
148149
self.evo.send_command.assert_has_calls( # type: ignore[attr-defined]
@@ -284,6 +285,7 @@ async def test_dispense(self):
284285
liquid_height=10,
285286
blow_out_air_volume=0,
286287
liquids=[(None, 100)],
288+
mix=None,
287289
)
288290
await self.evo.dispense([op], use_channels=[0])
289291
self.evo.send_command.assert_has_calls( # type: ignore[attr-defined]

0 commit comments

Comments
 (0)