Skip to content

Commit 08ec6d2

Browse files
revert merge
1 parent 30bfe48 commit 08ec6d2

File tree

169 files changed

+4476
-1792
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

169 files changed

+4476
-1792
lines changed

api/docs/v2/conf.py

+1
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,7 @@
445445
("py:class", r".*protocol_api\.config.*"),
446446
("py:class", r".*opentrons_shared_data.*"),
447447
("py:class", r".*protocol_api._parameters.Parameters.*"),
448+
("py:class", r".*protocol_api._liquid_properties.TransferProperties*"),
448449
("py:class", r".*RobotContext"), # shh it's a secret (for now)
449450
("py:class", r".*FlexStackerContext"), # ssh it's a secret (for now)
450451
(

api/docs/v2/new_protocol_api.rst

+3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ Wells and Liquids
4747

4848
.. autoclass:: opentrons.protocol_api.Liquid
4949

50+
.. autoclass:: opentrons.protocol_api.LiquidClass
51+
:members:
52+
5053
.. _protocol-api-modules:
5154

5255
Modules

api/src/opentrons/legacy_commands/commands.py

+83-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
from __future__ import annotations
2-
from typing import TYPE_CHECKING, List, Union, overload
2+
from typing import TYPE_CHECKING, List, Sequence, Union, overload
33

44

5-
from .helpers import stringify_location, stringify_disposal_location, listify
5+
from .helpers import (
6+
stringify_location,
7+
stringify_disposal_location,
8+
stringify_well_list,
9+
listify,
10+
)
611
from . import types as command_types
712

813
from opentrons.types import Location
914
from opentrons.protocol_api.disposal_locations import TrashBin, WasteChute
15+
from opentrons.protocol_api._liquid import LiquidClass
1016

1117
if TYPE_CHECKING:
1218
from opentrons.protocol_api import InstrumentContext
@@ -317,6 +323,81 @@ def move_to_disposal_location(
317323
}
318324

319325

326+
def transfer_with_liquid_class(
327+
instrument: InstrumentContext,
328+
liquid_class: LiquidClass,
329+
volume: float,
330+
source: Union[Well, Sequence[Well], Sequence[Sequence[Well]]],
331+
destination: Union[Well, Sequence[Well], Sequence[Sequence[Well]]],
332+
) -> command_types.TransferWithLiquidClassCommand:
333+
text = (
334+
"Transferring "
335+
+ f"{volume} uL of {liquid_class.display_name} liquid class from "
336+
+ f"{stringify_well_list(source)} to {stringify_well_list(destination)}"
337+
)
338+
return {
339+
"name": command_types.TRANSFER_WITH_LIQUID_CLASS,
340+
"payload": {
341+
"instrument": instrument,
342+
"liquid_class": liquid_class,
343+
"volume": volume,
344+
"source": source,
345+
"destination": destination,
346+
"text": text,
347+
},
348+
}
349+
350+
351+
def distribute_with_liquid_class(
352+
instrument: InstrumentContext,
353+
liquid_class: LiquidClass,
354+
volume: float,
355+
source: Union[Well, Sequence[Well], Sequence[Sequence[Well]]],
356+
destination: Union[Well, Sequence[Well], Sequence[Sequence[Well]]],
357+
) -> command_types.DistributeWithLiquidClassCommand:
358+
text = (
359+
"Distributing "
360+
+ f"{volume} uL of {liquid_class.display_name} liquid class from "
361+
+ f"{stringify_well_list(source)} to {stringify_well_list(destination)}"
362+
)
363+
return {
364+
"name": command_types.DISTRIBUTE_WITH_LIQUID_CLASS,
365+
"payload": {
366+
"instrument": instrument,
367+
"liquid_class": liquid_class,
368+
"volume": volume,
369+
"source": source,
370+
"destination": destination,
371+
"text": text,
372+
},
373+
}
374+
375+
376+
def consolidate_with_liquid_class(
377+
instrument: InstrumentContext,
378+
liquid_class: LiquidClass,
379+
volume: float,
380+
source: Union[Well, Sequence[Well], Sequence[Sequence[Well]]],
381+
destination: Union[Well, Sequence[Well], Sequence[Sequence[Well]]],
382+
) -> command_types.ConsolidateWithLiquidClassCommand:
383+
text = (
384+
"Consolidating "
385+
+ f"{volume} uL of {liquid_class.display_name} liquid class from "
386+
+ f"{stringify_well_list(source)} to {stringify_well_list(destination)}"
387+
)
388+
return {
389+
"name": command_types.CONSOLIDATE_WITH_LIQUID_CLASS,
390+
"payload": {
391+
"instrument": instrument,
392+
"liquid_class": liquid_class,
393+
"volume": volume,
394+
"source": source,
395+
"destination": destination,
396+
"text": text,
397+
},
398+
}
399+
400+
320401
def seal(
321402
instrument: InstrumentContext,
322403
location: Well,

api/src/opentrons/legacy_commands/helpers.py

+59-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import List, Union
1+
from typing import List, Union, Sequence
22

33
from opentrons.protocol_api.labware import Well, Labware
44
from opentrons.protocol_api.module_contexts import ModuleContext
@@ -48,6 +48,64 @@ def stringify_disposal_location(location: Union[TrashBin, WasteChute]) -> str:
4848
return "Waste Chute"
4949

5050

51+
def _group_wells_by_labware(wells: List[Well]) -> List[List[Well]]:
52+
wells_by_labware: List[List[Well]] = []
53+
sub_list = []
54+
active_parent_labware = None
55+
for well in wells:
56+
if well.parent == active_parent_labware:
57+
sub_list.append(well)
58+
else:
59+
active_parent_labware = well.parent
60+
if sub_list:
61+
wells_by_labware.append(sub_list)
62+
sub_list = [well]
63+
if sub_list:
64+
wells_by_labware.append(sub_list)
65+
66+
return wells_by_labware
67+
68+
69+
def _stringify_multiple_wells_for_labware(wells: List[Well]) -> str:
70+
if len(wells) == 0:
71+
return ""
72+
elif len(wells) == 1:
73+
return str(wells[0])
74+
# TODO(jbl 2025-04-10) this logic can be improved to more intelligently group wells
75+
elif len(wells) < 9: # At most we'll print out a full column's worth of well
76+
return ", ".join([well.well_name for well in wells[:-1]]) + f", {wells[-1]}"
77+
else: # Otherwise print the first and last three
78+
return (
79+
", ".join([well.well_name for well in wells[:3]])
80+
+ ", ... "
81+
+ ", ".join([well.well_name for well in wells[-3:-1]])
82+
+ f", {wells[-1]}"
83+
)
84+
85+
86+
def stringify_well_list(
87+
wells: Union[Well, Sequence[Well], Sequence[Sequence[Well]]]
88+
) -> str:
89+
"""Takes an arbitrary sequence of wells and returns a string representation of each well, associated by labware."""
90+
if isinstance(wells, Well):
91+
well_list = [wells]
92+
elif len(wells) == 0:
93+
well_list = []
94+
elif isinstance(wells, list) and isinstance(wells[0], list):
95+
well_list = [well for sub_well_list in wells for well in sub_well_list]
96+
elif isinstance(wells, list):
97+
well_list = wells
98+
else:
99+
return ""
100+
101+
return "; ".join(
102+
[
103+
_stringify_multiple_wells_for_labware(wells_by_labware)
104+
for wells_by_labware in _group_wells_by_labware(well_list)
105+
]
106+
)
107+
108+
51109
def _stringify_labware_movement_location(
52110
location: Union[
53111
DeckLocation, OffDeckType, Labware, ModuleContext, WasteChute, TrashBin

api/src/opentrons/legacy_commands/types.py

+30
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from opentrons.protocol_api import InstrumentContext
99
from opentrons.protocol_api.labware import Well
1010
from opentrons.protocol_api.disposal_locations import TrashBin, WasteChute
11+
from opentrons.protocol_api._liquid import LiquidClass
1112

1213
from opentrons.types import Location
1314

@@ -43,6 +44,9 @@
4344
RETURN_TIP: Final = "command.RETURN_TIP"
4445
MOVE_TO: Final = "command.MOVE_TO"
4546
MOVE_TO_DISPOSAL_LOCATION: Final = "command.MOVE_TO_DISPOSAL_LOCATION"
47+
TRANSFER_WITH_LIQUID_CLASS: Final = "command.TRANSFER_WITH_LIQUID_CLASS"
48+
DISTRIBUTE_WITH_LIQUID_CLASS: Final = "command.DISTRIBUTE_WITH_LIQUID_CLASS"
49+
CONSOLIDATE_WITH_LIQUID_CLASS: Final = "command.CONSOLIDATE_WITH_LIQUID_CLASS"
4650
SEAL: Final = "command.SEAL"
4751
UNSEAL: Final = "command.UNSEAL"
4852
PRESSURIZE: Final = "command.PRESSURIZE"
@@ -540,6 +544,28 @@ class MoveLabwareCommandPayload(TextOnlyPayload):
540544
pass
541545

542546

547+
class LiquidClassCommandPayload(TextOnlyPayload, SingleInstrumentPayload):
548+
liquid_class: LiquidClass
549+
volume: float
550+
source: Union[Well, Sequence[Well], Sequence[Sequence[Well]]]
551+
destination: Union[Well, Sequence[Well], Sequence[Sequence[Well]]]
552+
553+
554+
class TransferWithLiquidClassCommand(TypedDict):
555+
name: Literal["command.TRANSFER_WITH_LIQUID_CLASS"]
556+
payload: LiquidClassCommandPayload
557+
558+
559+
class DistributeWithLiquidClassCommand(TypedDict):
560+
name: Literal["command.DISTRIBUTE_WITH_LIQUID_CLASS"]
561+
payload: LiquidClassCommandPayload
562+
563+
564+
class ConsolidateWithLiquidClassCommand(TypedDict):
565+
name: Literal["command.CONSOLIDATE_WITH_LIQUID_CLASS"]
566+
payload: LiquidClassCommandPayload
567+
568+
543569
class SealCommandPayload(TextOnlyPayload):
544570
instrument: InstrumentContext
545571
location: Union[None, Location, Well]
@@ -622,6 +648,9 @@ class PressurizeCommand(TypedDict):
622648
MoveToCommand,
623649
MoveToDisposalLocationCommand,
624650
MoveLabwareCommand,
651+
TransferWithLiquidClassCommand,
652+
DistributeWithLiquidClassCommand,
653+
ConsolidateWithLiquidClassCommand,
625654
SealCommand,
626655
UnsealCommand,
627656
PressurizeCommand,
@@ -674,6 +703,7 @@ class PressurizeCommand(TypedDict):
674703
MoveToCommandPayload,
675704
MoveToDisposalLocationCommandPayload,
676705
MoveLabwareCommandPayload,
706+
LiquidClassCommandPayload,
677707
SealCommandPayload,
678708
UnsealCommandPayload,
679709
PressurizeCommandPayload,

api/src/opentrons/protocol_api/core/engine/instrument.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -1141,7 +1141,7 @@ def get_next_tip(
11411141
result.nextTipInfo if isinstance(result.nextTipInfo, NextTipInfo) else None
11421142
)
11431143

1144-
def transfer_liquid( # noqa: C901
1144+
def transfer_with_liquid_class( # noqa: C901
11451145
self,
11461146
liquid_class: LiquidClass,
11471147
volume: float,
@@ -1335,7 +1335,7 @@ def _pick_up_tip() -> WellCore:
13351335
_drop_tip()
13361336

13371337
# TODO(spp, 2025-02-25): wire up return tip
1338-
def distribute_liquid( # noqa: C901
1338+
def distribute_with_liquid_class( # noqa: C901
13391339
self,
13401340
liquid_class: LiquidClass,
13411341
volume: float,
@@ -1417,7 +1417,7 @@ def distribute_liquid( # noqa: C901
14171417
tip_working_volume=working_volume,
14181418
)
14191419
):
1420-
self.transfer_liquid(
1420+
self.transfer_with_liquid_class(
14211421
liquid_class=liquid_class,
14221422
volume=volume,
14231423
source=[source for _ in range(len(dest))],
@@ -1657,7 +1657,7 @@ def _tip_can_hold_volume_for_multi_dispensing(
16571657
<= tip_working_volume
16581658
)
16591659

1660-
def consolidate_liquid( # noqa: C901
1660+
def consolidate_with_liquid_class( # noqa: C901
16611661
self,
16621662
liquid_class: LiquidClass,
16631663
volume: float,

api/src/opentrons/protocol_api/core/engine/transfer_components_executor.py

+12-22
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,7 @@ def submerge(
207207
minimum_z_height=None,
208208
speed=submerge_properties.speed,
209209
)
210-
if submerge_properties.delay.enabled:
211-
assert submerge_properties.delay.duration is not None
210+
if submerge_properties.delay.enabled and submerge_properties.delay.duration:
212211
self._instrument.delay(submerge_properties.delay.duration)
213212

214213
def aspirate_and_wait(self, volume: float) -> None:
@@ -227,9 +226,7 @@ def aspirate_and_wait(self, volume: float) -> None:
227226
)
228227
self._tip_state.append_liquid(volume)
229228
delay_props = aspirate_props.delay
230-
if delay_props.enabled:
231-
# Assertion only for mypy purposes
232-
assert delay_props.duration is not None
229+
if delay_props.enabled and delay_props.duration:
233230
self._instrument.delay(delay_props.duration)
234231

235232
def dispense_and_wait(
@@ -257,8 +254,7 @@ def dispense_and_wait(
257254
self._tip_state.ready_to_aspirate = False
258255
self._tip_state.delete_liquid(volume)
259256
dispense_delay = dispense_properties.delay
260-
if dispense_delay.enabled:
261-
assert dispense_delay.duration is not None
257+
if dispense_delay.enabled and dispense_delay.duration:
262258
self._instrument.delay(dispense_delay.duration)
263259

264260
def mix(self, mix_properties: MixProperties, last_dispense_push_out: bool) -> None:
@@ -361,8 +357,7 @@ def retract_after_aspiration(
361357
speed=retract_props.speed,
362358
)
363359
retract_delay = retract_props.delay
364-
if retract_delay.enabled:
365-
assert retract_delay.duration is not None
360+
if retract_delay.enabled and retract_delay.duration:
366361
self._instrument.delay(retract_delay.duration)
367362
touch_tip_props = retract_props.touch_tip
368363
if touch_tip_props.enabled:
@@ -459,8 +454,7 @@ def retract_after_dispensing(
459454
speed=retract_props.speed,
460455
)
461456
retract_delay = retract_props.delay
462-
if retract_delay.enabled:
463-
assert retract_delay.duration is not None
457+
if retract_delay.enabled and retract_delay.duration:
464458
self._instrument.delay(retract_delay.duration)
465459

466460
blowout_props = retract_props.blowout
@@ -599,8 +593,7 @@ def retract_during_multi_dispensing(
599593
speed=retract_props.speed,
600594
)
601595
retract_delay = retract_props.delay
602-
if retract_delay.enabled:
603-
assert retract_delay.duration is not None
596+
if retract_delay.enabled and retract_delay.duration:
604597
self._instrument.delay(retract_delay.duration)
605598

606599
blowout_props = retract_props.blowout
@@ -780,8 +773,8 @@ def _add_air_gap(self, air_gap_volume: float) -> None:
780773
correction_volume = aspirate_props.correction_by_volume.get_for_volume(
781774
air_gap_volume
782775
)
783-
# The maximum flow rate should be air_gap_volume per second
784-
flow_rate = min(
776+
# The minimum flow rate should be air_gap_volume per second
777+
flow_rate = max(
785778
aspirate_props.flow_rate_by_volume.get_for_volume(air_gap_volume),
786779
air_gap_volume,
787780
)
@@ -791,9 +784,7 @@ def _add_air_gap(self, air_gap_volume: float) -> None:
791784
correction_volume=correction_volume,
792785
)
793786
delay_props = aspirate_props.delay
794-
if delay_props.enabled:
795-
# Assertion only for mypy purposes
796-
assert delay_props.duration is not None
787+
if delay_props.enabled and delay_props.duration:
797788
self._instrument.delay(delay_props.duration)
798789
self._tip_state.append_air_gap(air_gap_volume)
799790

@@ -807,8 +798,8 @@ def _remove_air_gap(self, location: Location) -> None:
807798
correction_volume = dispense_props.correction_by_volume.get_for_volume(
808799
last_air_gap
809800
)
810-
# The maximum flow rate should be air_gap_volume per second
811-
flow_rate = min(
801+
# The minimum flow rate should be air_gap_volume per second
802+
flow_rate = max(
812803
dispense_props.flow_rate_by_volume.get_for_volume(last_air_gap),
813804
last_air_gap,
814805
)
@@ -824,8 +815,7 @@ def _remove_air_gap(self, location: Location) -> None:
824815
)
825816
self._tip_state.delete_air_gap(last_air_gap)
826817
dispense_delay = dispense_props.delay
827-
if dispense_delay.enabled:
828-
assert dispense_delay.duration is not None
818+
if dispense_delay.enabled and dispense_delay.duration:
829819
self._instrument.delay(dispense_delay.duration)
830820

831821

0 commit comments

Comments
 (0)