opentrons 8.6.0a11__py3-none-any.whl → 8.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of opentrons might be problematic. Click here for more details.
- opentrons/_version.py +2 -2
- opentrons/cli/analyze.py +58 -2
- opentrons/drivers/asyncio/communication/serial_connection.py +8 -5
- opentrons/drivers/flex_stacker/driver.py +6 -1
- opentrons/hardware_control/backends/flex_protocol.py +1 -0
- opentrons/hardware_control/backends/ot3controller.py +25 -13
- opentrons/hardware_control/backends/ot3simulator.py +2 -1
- opentrons/hardware_control/dev_types.py +3 -1
- opentrons/hardware_control/instruments/ot2/pipette_handler.py +1 -0
- opentrons/hardware_control/instruments/ot3/pipette_handler.py +1 -0
- opentrons/hardware_control/ot3api.py +3 -1
- opentrons/hardware_control/protocols/gripper_controller.py +1 -0
- opentrons/protocol_api/core/engine/_default_liquid_class_versions.py +56 -0
- opentrons/protocol_api/core/engine/instrument.py +143 -18
- opentrons/protocol_api/core/engine/pipette_movement_conflict.py +77 -17
- opentrons/protocol_api/core/engine/protocol.py +53 -7
- opentrons/protocol_api/core/engine/transfer_components_executor.py +36 -20
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +1 -1
- opentrons/protocol_api/core/protocol.py +1 -1
- opentrons/protocol_api/labware.py +36 -2
- opentrons/protocol_api/module_contexts.py +146 -14
- opentrons/protocol_api/protocol_context.py +162 -12
- opentrons/protocol_api/validation.py +4 -0
- opentrons/protocol_engine/commands/command_unions.py +2 -0
- opentrons/protocol_engine/commands/flex_stacker/common.py +13 -0
- opentrons/protocol_engine/commands/flex_stacker/store.py +20 -2
- opentrons/protocol_engine/execution/labware_movement.py +14 -12
- opentrons/protocol_engine/resources/pipette_data_provider.py +3 -0
- opentrons/protocol_engine/state/geometry.py +33 -5
- opentrons/protocol_engine/state/labware.py +66 -0
- opentrons/protocol_engine/state/modules.py +6 -0
- opentrons/protocol_engine/state/pipettes.py +12 -3
- opentrons/protocol_engine/types/__init__.py +2 -0
- opentrons/protocol_engine/types/labware.py +9 -0
- opentrons/protocols/api_support/definitions.py +1 -1
- {opentrons-8.6.0a11.dist-info → opentrons-8.7.0.dist-info}/METADATA +4 -4
- {opentrons-8.6.0a11.dist-info → opentrons-8.7.0.dist-info}/RECORD +40 -39
- {opentrons-8.6.0a11.dist-info → opentrons-8.7.0.dist-info}/WHEEL +0 -0
- {opentrons-8.6.0a11.dist-info → opentrons-8.7.0.dist-info}/entry_points.txt +0 -0
- {opentrons-8.6.0a11.dist-info → opentrons-8.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -66,7 +66,6 @@ from opentrons.protocol_engine.types.automatic_tip_selection import (
|
|
|
66
66
|
)
|
|
67
67
|
from opentrons.protocol_engine.errors.exceptions import TipNotAttachedError
|
|
68
68
|
from opentrons.protocol_engine.clients import SyncClient as EngineClient
|
|
69
|
-
from opentrons.protocols.api_support.definitions import MAX_SUPPORTED_VERSION
|
|
70
69
|
from opentrons_shared_data.pipette.types import (
|
|
71
70
|
PIPETTE_API_NAMES_MAP,
|
|
72
71
|
LIQUID_PROBE_START_OFFSET_FROM_WELL_TOP,
|
|
@@ -101,6 +100,9 @@ _RESIN_TIP_DEFAULT_FLOW_RATE = 10.0
|
|
|
101
100
|
_FLEX_PIPETTE_NAMES_FIXED_IN = APIVersion(2, 23)
|
|
102
101
|
"""The version after which InstrumentContext.name returns the correct API-specific names of Flex pipettes."""
|
|
103
102
|
|
|
103
|
+
_DEFAULT_FLOW_RATE_BUG_FIXED_IN = APIVersion(2, 26)
|
|
104
|
+
"""The version after which default flow rates correctly update when pipette tip or volume changes."""
|
|
105
|
+
|
|
104
106
|
|
|
105
107
|
class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
|
|
106
108
|
"""Instrument API core using a ProtocolEngine.
|
|
@@ -122,18 +124,27 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
|
|
|
122
124
|
self._sync_hardware_api = sync_hardware_api
|
|
123
125
|
self._protocol_core = protocol_core
|
|
124
126
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
flow_rates = self._engine_client.state.pipettes.get_flow_rates(pipette_id)
|
|
128
|
-
self._aspirate_flow_rate = find_value_for_api_version(
|
|
129
|
-
MAX_SUPPORTED_VERSION, flow_rates.default_aspirate
|
|
130
|
-
)
|
|
131
|
-
self._dispense_flow_rate = find_value_for_api_version(
|
|
132
|
-
MAX_SUPPORTED_VERSION, flow_rates.default_dispense
|
|
133
|
-
)
|
|
134
|
-
self._blow_out_flow_rate = find_value_for_api_version(
|
|
135
|
-
MAX_SUPPORTED_VERSION, flow_rates.default_blow_out
|
|
127
|
+
self._initial_default_flow_rates = (
|
|
128
|
+
self._engine_client.state.pipettes.get_flow_rates(pipette_id)
|
|
136
129
|
)
|
|
130
|
+
self._user_aspirate_flow_rate: Optional[float] = None
|
|
131
|
+
self._user_dispense_flow_rate: Optional[float] = None
|
|
132
|
+
self._user_blow_out_flow_rate: Optional[float] = None
|
|
133
|
+
|
|
134
|
+
if self._protocol_core.api_version < _DEFAULT_FLOW_RATE_BUG_FIXED_IN:
|
|
135
|
+
# Set to the initial defaults to preserve buggy behavior where the default was not correctly updated
|
|
136
|
+
self._user_aspirate_flow_rate = find_value_for_api_version(
|
|
137
|
+
self._protocol_core.api_version,
|
|
138
|
+
self._initial_default_flow_rates.default_aspirate,
|
|
139
|
+
)
|
|
140
|
+
self._user_dispense_flow_rate = find_value_for_api_version(
|
|
141
|
+
self._protocol_core.api_version,
|
|
142
|
+
self._initial_default_flow_rates.default_dispense,
|
|
143
|
+
)
|
|
144
|
+
self._user_blow_out_flow_rate = find_value_for_api_version(
|
|
145
|
+
self._protocol_core.api_version,
|
|
146
|
+
self._initial_default_flow_rates.default_blow_out,
|
|
147
|
+
)
|
|
137
148
|
self._flow_rates = FlowRates(self)
|
|
138
149
|
|
|
139
150
|
self.set_default_speed(speed=default_movement_speed)
|
|
@@ -1031,13 +1042,64 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
|
|
|
1031
1042
|
return self._flow_rates
|
|
1032
1043
|
|
|
1033
1044
|
def get_aspirate_flow_rate(self, rate: float = 1.0) -> float:
|
|
1034
|
-
|
|
1045
|
+
"""Returns the user-set aspirate flow rate if that's been modified, otherwise return the default.
|
|
1046
|
+
|
|
1047
|
+
Note that in API versions 2.25 and below `_user_aspirate_flow_rate` will automatically be set to the initial
|
|
1048
|
+
default flow rate when the pipette is loaded (which is the same as the max tip capacity). This is to preserve
|
|
1049
|
+
buggy behavior in which the default was never correctly updated when the pipette picked up or dropped a tip or
|
|
1050
|
+
had its volume configuration changed.
|
|
1051
|
+
"""
|
|
1052
|
+
aspirate_flow_rate = (
|
|
1053
|
+
self._user_aspirate_flow_rate
|
|
1054
|
+
or find_value_for_api_version(
|
|
1055
|
+
self._protocol_core.api_version,
|
|
1056
|
+
self._engine_client.state.pipettes.get_flow_rates(
|
|
1057
|
+
self._pipette_id
|
|
1058
|
+
).default_aspirate,
|
|
1059
|
+
)
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
return aspirate_flow_rate * rate
|
|
1035
1063
|
|
|
1036
1064
|
def get_dispense_flow_rate(self, rate: float = 1.0) -> float:
|
|
1037
|
-
|
|
1065
|
+
"""Returns the user-set dispense flow rate if that's been modified, otherwise return the default.
|
|
1066
|
+
|
|
1067
|
+
Note that in API versions 2.25 and below `_user_dispense_flow_rate` will automatically be set to the initial
|
|
1068
|
+
default flow rate when the pipette is loaded (which is the same as the max tip capacity). This is to preserve
|
|
1069
|
+
buggy behavior in which the default was never correctly updated when the pipette picked up or dropped a tip or
|
|
1070
|
+
had its volume configuration changed.
|
|
1071
|
+
"""
|
|
1072
|
+
dispense_flow_rate = (
|
|
1073
|
+
self._user_dispense_flow_rate
|
|
1074
|
+
or find_value_for_api_version(
|
|
1075
|
+
self._protocol_core.api_version,
|
|
1076
|
+
self._engine_client.state.pipettes.get_flow_rates(
|
|
1077
|
+
self._pipette_id
|
|
1078
|
+
).default_dispense,
|
|
1079
|
+
)
|
|
1080
|
+
)
|
|
1081
|
+
|
|
1082
|
+
return dispense_flow_rate * rate
|
|
1038
1083
|
|
|
1039
1084
|
def get_blow_out_flow_rate(self, rate: float = 1.0) -> float:
|
|
1040
|
-
|
|
1085
|
+
"""Returns the user-set blow-out flow rate if that's been modified, otherwise return the default.
|
|
1086
|
+
|
|
1087
|
+
Note that in API versions 2.25 and below `_user_dispense_flow_rate` will automatically be set to the initial
|
|
1088
|
+
default flow rate when the pipette is loaded (which is the same as the max tip capacity). This is to preserve
|
|
1089
|
+
buggy behavior in which the default was never correctly updated when the pipette picked up or dropped a tip or
|
|
1090
|
+
had its volume configuration changed.
|
|
1091
|
+
"""
|
|
1092
|
+
blow_out_flow_rate = (
|
|
1093
|
+
self._user_blow_out_flow_rate
|
|
1094
|
+
or find_value_for_api_version(
|
|
1095
|
+
self._protocol_core.api_version,
|
|
1096
|
+
self._engine_client.state.pipettes.get_flow_rates(
|
|
1097
|
+
self._pipette_id
|
|
1098
|
+
).default_blow_out,
|
|
1099
|
+
)
|
|
1100
|
+
)
|
|
1101
|
+
|
|
1102
|
+
return blow_out_flow_rate * rate
|
|
1041
1103
|
|
|
1042
1104
|
def get_nozzle_configuration(self) -> NozzleConfigurationType:
|
|
1043
1105
|
return self._engine_client.state.pipettes.get_nozzle_layout_type(
|
|
@@ -1084,13 +1146,13 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
|
|
|
1084
1146
|
) -> None:
|
|
1085
1147
|
if aspirate is not None:
|
|
1086
1148
|
assert aspirate > 0
|
|
1087
|
-
self.
|
|
1149
|
+
self._user_aspirate_flow_rate = aspirate
|
|
1088
1150
|
if dispense is not None:
|
|
1089
1151
|
assert dispense > 0
|
|
1090
|
-
self.
|
|
1152
|
+
self._user_dispense_flow_rate = dispense
|
|
1091
1153
|
if blow_out is not None:
|
|
1092
1154
|
assert blow_out > 0
|
|
1093
|
-
self.
|
|
1155
|
+
self._user_blow_out_flow_rate = blow_out
|
|
1094
1156
|
|
|
1095
1157
|
def set_liquid_presence_detection(self, enable: bool) -> None:
|
|
1096
1158
|
self._liquid_presence_detection = enable
|
|
@@ -1105,6 +1167,10 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
|
|
|
1105
1167
|
),
|
|
1106
1168
|
)
|
|
1107
1169
|
)
|
|
1170
|
+
if self._protocol_core.api_version >= _DEFAULT_FLOW_RATE_BUG_FIXED_IN:
|
|
1171
|
+
self._user_aspirate_flow_rate = None
|
|
1172
|
+
self._user_dispense_flow_rate = None
|
|
1173
|
+
self._user_blow_out_flow_rate = None
|
|
1108
1174
|
|
|
1109
1175
|
def prepare_to_aspirate(self) -> None:
|
|
1110
1176
|
self._engine_client.execute_command(
|
|
@@ -1273,6 +1339,13 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
|
|
|
1273
1339
|
tiprack_uri=tiprack_uri_for_transfer_props,
|
|
1274
1340
|
)
|
|
1275
1341
|
|
|
1342
|
+
original_aspirate_flow_rate = self._user_aspirate_flow_rate
|
|
1343
|
+
original_dispense_flow_rate = self._user_dispense_flow_rate
|
|
1344
|
+
original_blow_out_flow_rate = self._user_blow_out_flow_rate
|
|
1345
|
+
in_low_volume_mode = self._engine_client.state.pipettes.get_is_low_volume_mode(
|
|
1346
|
+
self._pipette_id
|
|
1347
|
+
)
|
|
1348
|
+
|
|
1276
1349
|
target_destinations: Sequence[
|
|
1277
1350
|
Union[Tuple[Location, WellCore], TrashBin, WasteChute]
|
|
1278
1351
|
]
|
|
@@ -1367,6 +1440,14 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
|
|
|
1367
1440
|
if not keep_last_tip:
|
|
1368
1441
|
self._drop_tip_for_liquid_class(trash_location, return_tip)
|
|
1369
1442
|
|
|
1443
|
+
if self._protocol_core.api_version >= _DEFAULT_FLOW_RATE_BUG_FIXED_IN:
|
|
1444
|
+
self._restore_pipette_flow_rates_and_volume_mode(
|
|
1445
|
+
aspirate_flow_rate=original_aspirate_flow_rate,
|
|
1446
|
+
dispense_flow_rate=original_dispense_flow_rate,
|
|
1447
|
+
blow_out_flow_rate=original_blow_out_flow_rate,
|
|
1448
|
+
is_low_volume=in_low_volume_mode,
|
|
1449
|
+
)
|
|
1450
|
+
|
|
1370
1451
|
def distribute_with_liquid_class( # noqa: C901
|
|
1371
1452
|
self,
|
|
1372
1453
|
liquid_class: LiquidClass,
|
|
@@ -1474,6 +1555,13 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
|
|
|
1474
1555
|
tiprack_uri=tiprack_uri_for_transfer_props,
|
|
1475
1556
|
)
|
|
1476
1557
|
|
|
1558
|
+
original_aspirate_flow_rate = self._user_aspirate_flow_rate
|
|
1559
|
+
original_dispense_flow_rate = self._user_dispense_flow_rate
|
|
1560
|
+
original_blow_out_flow_rate = self._user_blow_out_flow_rate
|
|
1561
|
+
in_low_volume_mode = self._engine_client.state.pipettes.get_is_low_volume_mode(
|
|
1562
|
+
self._pipette_id
|
|
1563
|
+
)
|
|
1564
|
+
|
|
1477
1565
|
# This will return a generator that provides pairs of destination well and
|
|
1478
1566
|
# the volume to dispense into it
|
|
1479
1567
|
dest_per_volume_step = (
|
|
@@ -1617,6 +1705,14 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
|
|
|
1617
1705
|
if not keep_last_tip:
|
|
1618
1706
|
self._drop_tip_for_liquid_class(trash_location, return_tip)
|
|
1619
1707
|
|
|
1708
|
+
if self._protocol_core.api_version >= _DEFAULT_FLOW_RATE_BUG_FIXED_IN:
|
|
1709
|
+
self._restore_pipette_flow_rates_and_volume_mode(
|
|
1710
|
+
aspirate_flow_rate=original_aspirate_flow_rate,
|
|
1711
|
+
dispense_flow_rate=original_dispense_flow_rate,
|
|
1712
|
+
blow_out_flow_rate=original_blow_out_flow_rate,
|
|
1713
|
+
is_low_volume=in_low_volume_mode,
|
|
1714
|
+
)
|
|
1715
|
+
|
|
1620
1716
|
def _tip_can_hold_volume_for_multi_dispensing(
|
|
1621
1717
|
self,
|
|
1622
1718
|
transfer_volume: float,
|
|
@@ -1712,6 +1808,13 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
|
|
|
1712
1808
|
tiprack_uri=tiprack_uri_for_transfer_props,
|
|
1713
1809
|
)
|
|
1714
1810
|
|
|
1811
|
+
original_aspirate_flow_rate = self._user_aspirate_flow_rate
|
|
1812
|
+
original_dispense_flow_rate = self._user_dispense_flow_rate
|
|
1813
|
+
original_blow_out_flow_rate = self._user_blow_out_flow_rate
|
|
1814
|
+
in_low_volume_mode = self._engine_client.state.pipettes.get_is_low_volume_mode(
|
|
1815
|
+
self._pipette_id
|
|
1816
|
+
)
|
|
1817
|
+
|
|
1715
1818
|
working_volume = self.get_working_volume_for_tip_rack(tip_racks[0][1])
|
|
1716
1819
|
|
|
1717
1820
|
source_per_volume_step = (
|
|
@@ -1796,6 +1899,14 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
|
|
|
1796
1899
|
if not keep_last_tip:
|
|
1797
1900
|
self._drop_tip_for_liquid_class(trash_location, return_tip)
|
|
1798
1901
|
|
|
1902
|
+
if self._protocol_core.api_version >= _DEFAULT_FLOW_RATE_BUG_FIXED_IN:
|
|
1903
|
+
self._restore_pipette_flow_rates_and_volume_mode(
|
|
1904
|
+
aspirate_flow_rate=original_aspirate_flow_rate,
|
|
1905
|
+
dispense_flow_rate=original_dispense_flow_rate,
|
|
1906
|
+
blow_out_flow_rate=original_blow_out_flow_rate,
|
|
1907
|
+
is_low_volume=in_low_volume_mode,
|
|
1908
|
+
)
|
|
1909
|
+
|
|
1799
1910
|
def _get_location_and_well_core_from_next_tip_info(
|
|
1800
1911
|
self,
|
|
1801
1912
|
tip_info: NextTipInfo,
|
|
@@ -1904,6 +2015,20 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
|
|
|
1904
2015
|
alternate_drop_location=True,
|
|
1905
2016
|
)
|
|
1906
2017
|
|
|
2018
|
+
def _restore_pipette_flow_rates_and_volume_mode(
|
|
2019
|
+
self,
|
|
2020
|
+
aspirate_flow_rate: Optional[float],
|
|
2021
|
+
dispense_flow_rate: Optional[float],
|
|
2022
|
+
blow_out_flow_rate: Optional[float],
|
|
2023
|
+
is_low_volume: bool,
|
|
2024
|
+
) -> None:
|
|
2025
|
+
# TODO(jbl 2025-09-17) this works for p50 low volume mode but is not guaranteed to work for future low volume
|
|
2026
|
+
# modes, this should be replaced with something less flaky
|
|
2027
|
+
self.configure_for_volume(self.get_max_volume() if not is_low_volume else 1)
|
|
2028
|
+
self._user_aspirate_flow_rate = aspirate_flow_rate
|
|
2029
|
+
self._user_dispense_flow_rate = dispense_flow_rate
|
|
2030
|
+
self._user_blow_out_flow_rate = blow_out_flow_rate
|
|
2031
|
+
|
|
1907
2032
|
def aspirate_liquid_class(
|
|
1908
2033
|
self,
|
|
1909
2034
|
volume: float,
|
|
@@ -21,7 +21,11 @@ from opentrons.protocol_engine import (
|
|
|
21
21
|
OnLabwareLocation,
|
|
22
22
|
DropTipWellLocation,
|
|
23
23
|
)
|
|
24
|
-
from opentrons.protocol_engine.types import
|
|
24
|
+
from opentrons.protocol_engine.types import (
|
|
25
|
+
StagingSlotLocation,
|
|
26
|
+
WellLocationType,
|
|
27
|
+
LoadedModule,
|
|
28
|
+
)
|
|
25
29
|
from opentrons.types import DeckSlotName, StagingSlotName, Point
|
|
26
30
|
from . import point_calculations
|
|
27
31
|
|
|
@@ -136,22 +140,47 @@ def check_safe_for_pipette_movement( # noqa: C901
|
|
|
136
140
|
f" will result in collision with thermocycler lid in deck slot A1."
|
|
137
141
|
)
|
|
138
142
|
|
|
143
|
+
def _check_for_column_4_module_collision(slot: DeckSlotName) -> None:
|
|
144
|
+
slot_module = engine_state.modules.get_by_slot(slot)
|
|
145
|
+
if (
|
|
146
|
+
slot_module
|
|
147
|
+
and engine_state.modules.is_column_4_module(slot_module.model)
|
|
148
|
+
and _slot_has_potential_colliding_object(
|
|
149
|
+
engine_state=engine_state,
|
|
150
|
+
pipette_bounds=pipette_bounds_at_well_location,
|
|
151
|
+
surrounding_location=slot_module,
|
|
152
|
+
)
|
|
153
|
+
):
|
|
154
|
+
raise PartialTipMovementNotAllowedError(
|
|
155
|
+
f"Moving to {engine_state.labware.get_display_name(labware_id)} in slot"
|
|
156
|
+
f" {slot} with {primary_nozzle} nozzle partial configuration will"
|
|
157
|
+
f" result in collision with items on {slot_module.model} mounted in {slot}."
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# We check the labware slot for a module that is mounted in the same cutout
|
|
161
|
+
# as the labwares slot but does not occupy the same heirarchy (like the stacker).
|
|
162
|
+
_check_for_column_4_module_collision(labware_slot)
|
|
163
|
+
|
|
139
164
|
for regular_slot in surrounding_slots.regular_slots:
|
|
140
165
|
if _slot_has_potential_colliding_object(
|
|
141
166
|
engine_state=engine_state,
|
|
142
167
|
pipette_bounds=pipette_bounds_at_well_location,
|
|
143
|
-
|
|
168
|
+
surrounding_location=regular_slot,
|
|
144
169
|
):
|
|
145
170
|
raise PartialTipMovementNotAllowedError(
|
|
146
171
|
f"Moving to {engine_state.labware.get_display_name(labware_id)} in slot"
|
|
147
172
|
f" {labware_slot} with {primary_nozzle} nozzle partial configuration"
|
|
148
173
|
f" will result in collision with items in deck slot {regular_slot}."
|
|
149
174
|
)
|
|
175
|
+
|
|
176
|
+
# Check for Column 4 Modules that may be descendants of a given surrounding slot
|
|
177
|
+
_check_for_column_4_module_collision(regular_slot)
|
|
178
|
+
|
|
150
179
|
for staging_slot in surrounding_slots.staging_slots:
|
|
151
180
|
if _slot_has_potential_colliding_object(
|
|
152
181
|
engine_state=engine_state,
|
|
153
182
|
pipette_bounds=pipette_bounds_at_well_location,
|
|
154
|
-
|
|
183
|
+
surrounding_location=staging_slot,
|
|
155
184
|
):
|
|
156
185
|
raise PartialTipMovementNotAllowedError(
|
|
157
186
|
f"Moving to {engine_state.labware.get_display_name(labware_id)} in slot"
|
|
@@ -178,18 +207,45 @@ def _get_critical_point_to_use(
|
|
|
178
207
|
def _slot_has_potential_colliding_object(
|
|
179
208
|
engine_state: StateView,
|
|
180
209
|
pipette_bounds: Tuple[Point, Point, Point, Point],
|
|
181
|
-
|
|
210
|
+
surrounding_location: Union[DeckSlotName, StagingSlotName, LoadedModule],
|
|
182
211
|
) -> bool:
|
|
183
|
-
"""Return the slot, if any, that has an item that the pipette might collide into.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
212
|
+
"""Return the slot, if any, that has an item that the pipette might collide into.
|
|
213
|
+
Can be provided a Deck Slot, Staging Slot, or Column 4 Module.
|
|
214
|
+
"""
|
|
215
|
+
if isinstance(surrounding_location, LoadedModule):
|
|
216
|
+
if (
|
|
217
|
+
engine_state.modules.is_column_4_module(surrounding_location.model)
|
|
218
|
+
and surrounding_location.location is not None
|
|
219
|
+
):
|
|
220
|
+
module_area = (
|
|
221
|
+
engine_state.modules.ensure_and_convert_module_fixture_location(
|
|
222
|
+
surrounding_location.location.slotName, surrounding_location.model
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
slot_pos = engine_state.addressable_areas.get_addressable_area_position(
|
|
226
|
+
addressable_area_name=module_area,
|
|
227
|
+
do_compatibility_check=False,
|
|
228
|
+
)
|
|
229
|
+
slot_bounds = (
|
|
230
|
+
engine_state.addressable_areas.get_addressable_area_bounding_box(
|
|
231
|
+
addressable_area_name=module_area,
|
|
232
|
+
do_compatibility_check=False,
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
else:
|
|
236
|
+
raise ValueError(
|
|
237
|
+
f"Error during collision validation, Module {surrounding_location.model} must be in Column 4."
|
|
238
|
+
)
|
|
239
|
+
else:
|
|
240
|
+
# Check if slot overlaps with pipette position
|
|
241
|
+
slot_pos = engine_state.addressable_areas.get_addressable_area_position(
|
|
242
|
+
addressable_area_name=surrounding_location.id,
|
|
243
|
+
do_compatibility_check=False,
|
|
244
|
+
)
|
|
245
|
+
slot_bounds = engine_state.addressable_areas.get_addressable_area_bounding_box(
|
|
246
|
+
addressable_area_name=surrounding_location.id,
|
|
247
|
+
do_compatibility_check=False,
|
|
248
|
+
)
|
|
193
249
|
slot_back_left_coords = Point(slot_pos.x, slot_pos.y + slot_bounds.y, slot_pos.z)
|
|
194
250
|
slot_front_right_coords = Point(slot_pos.x + slot_bounds.x, slot_pos.y, slot_pos.z)
|
|
195
251
|
|
|
@@ -199,13 +255,17 @@ def _slot_has_potential_colliding_object(
|
|
|
199
255
|
rectangle2=(slot_back_left_coords, slot_front_right_coords),
|
|
200
256
|
):
|
|
201
257
|
# Check z-height of items in overlapping slot
|
|
202
|
-
if isinstance(
|
|
258
|
+
if isinstance(surrounding_location, DeckSlotName):
|
|
203
259
|
slot_highest_z = engine_state.geometry.get_highest_z_in_slot(
|
|
204
|
-
DeckSlotLocation(slotName=
|
|
260
|
+
DeckSlotLocation(slotName=surrounding_location)
|
|
261
|
+
)
|
|
262
|
+
elif isinstance(surrounding_location, LoadedModule):
|
|
263
|
+
slot_highest_z = engine_state.geometry.get_highest_z_of_column_4_module(
|
|
264
|
+
surrounding_location
|
|
205
265
|
)
|
|
206
266
|
else:
|
|
207
267
|
slot_highest_z = engine_state.geometry.get_highest_z_in_slot(
|
|
208
|
-
StagingSlotLocation(slotName=
|
|
268
|
+
StagingSlotLocation(slotName=surrounding_location)
|
|
209
269
|
)
|
|
210
270
|
return slot_highest_z >= pipette_bounds[0].z
|
|
211
271
|
return False
|
|
@@ -78,7 +78,12 @@ from .module_core import (
|
|
|
78
78
|
FlexStackerCore,
|
|
79
79
|
)
|
|
80
80
|
from .exceptions import InvalidModuleLocationError
|
|
81
|
-
from . import
|
|
81
|
+
from . import (
|
|
82
|
+
load_labware_params,
|
|
83
|
+
deck_conflict,
|
|
84
|
+
overlap_versions,
|
|
85
|
+
_default_liquid_class_versions,
|
|
86
|
+
)
|
|
82
87
|
from opentrons.protocol_engine.resources import labware_validation
|
|
83
88
|
|
|
84
89
|
if TYPE_CHECKING:
|
|
@@ -492,13 +497,28 @@ class ProtocolCore(
|
|
|
492
497
|
)
|
|
493
498
|
# if this is a labware with a lid, we just need to find its lid_id
|
|
494
499
|
else:
|
|
495
|
-
|
|
496
|
-
|
|
500
|
+
# we need to check to see if this labware is hosting a lid stack
|
|
501
|
+
potential_lid_stack = (
|
|
502
|
+
self._engine_client.state.labware.get_next_child_labware(
|
|
503
|
+
labware.labware_id
|
|
504
|
+
)
|
|
497
505
|
)
|
|
498
|
-
if
|
|
499
|
-
|
|
506
|
+
if potential_lid_stack and labware_validation.is_lid_stack(
|
|
507
|
+
self._engine_client.state.labware.get_load_name(potential_lid_stack)
|
|
508
|
+
):
|
|
509
|
+
lid_id = self._engine_client.state.labware.get_highest_child_labware(
|
|
510
|
+
labware.labware_id
|
|
511
|
+
)
|
|
500
512
|
else:
|
|
501
|
-
|
|
513
|
+
lid = self._engine_client.state.labware.get_lid_by_labware_id(
|
|
514
|
+
labware.labware_id
|
|
515
|
+
)
|
|
516
|
+
if lid is not None:
|
|
517
|
+
lid_id = lid.id
|
|
518
|
+
else:
|
|
519
|
+
raise ValueError(
|
|
520
|
+
f"Cannot move a lid off of {labware.get_display_name()} because it has no lid."
|
|
521
|
+
)
|
|
502
522
|
|
|
503
523
|
_pick_up_offset = (
|
|
504
524
|
LabwareOffsetVector(
|
|
@@ -602,6 +622,9 @@ class ProtocolCore(
|
|
|
602
622
|
)
|
|
603
623
|
|
|
604
624
|
# Handle leftover empty lid stack if there is one
|
|
625
|
+
potential_lid_stack = self._engine_client.state.labware.get_next_child_labware(
|
|
626
|
+
labware.labware_id
|
|
627
|
+
)
|
|
605
628
|
if (
|
|
606
629
|
labware_validation.is_lid_stack(labware.load_name)
|
|
607
630
|
and self._engine_client.state.labware.get_highest_child_labware(
|
|
@@ -619,6 +642,25 @@ class ProtocolCore(
|
|
|
619
642
|
dropOffset=None,
|
|
620
643
|
)
|
|
621
644
|
)
|
|
645
|
+
elif (
|
|
646
|
+
potential_lid_stack
|
|
647
|
+
and labware_validation.is_lid_stack(
|
|
648
|
+
self._engine_client.state.labware.get_load_name(potential_lid_stack)
|
|
649
|
+
)
|
|
650
|
+
and self._engine_client.state.labware.get_highest_child_labware(
|
|
651
|
+
potential_lid_stack
|
|
652
|
+
)
|
|
653
|
+
== potential_lid_stack
|
|
654
|
+
):
|
|
655
|
+
self._engine_client.execute_command(
|
|
656
|
+
cmd.MoveLabwareParams(
|
|
657
|
+
labwareId=potential_lid_stack,
|
|
658
|
+
newLocation=SYSTEM_LOCATION,
|
|
659
|
+
strategy=LabwareMovementStrategy.MANUAL_MOVE_WITHOUT_PAUSE,
|
|
660
|
+
pickUpOffset=None,
|
|
661
|
+
dropOffset=None,
|
|
662
|
+
)
|
|
663
|
+
)
|
|
622
664
|
|
|
623
665
|
if strategy == LabwareMovementStrategy.USING_GRIPPER:
|
|
624
666
|
# Clear out last location since it is not relevant to pipetting
|
|
@@ -1068,8 +1110,12 @@ class ProtocolCore(
|
|
|
1068
1110
|
display_color=(liquid.displayColor.root if liquid.displayColor else None),
|
|
1069
1111
|
)
|
|
1070
1112
|
|
|
1071
|
-
def get_liquid_class(self, name: str, version: int) -> LiquidClass:
|
|
1113
|
+
def get_liquid_class(self, name: str, version: Optional[int]) -> LiquidClass:
|
|
1072
1114
|
"""Get an instance of a built-in liquid class."""
|
|
1115
|
+
if version is None:
|
|
1116
|
+
version = _default_liquid_class_versions.get_liquid_class_version(
|
|
1117
|
+
self._api_version, name
|
|
1118
|
+
)
|
|
1073
1119
|
try:
|
|
1074
1120
|
# Check if we have already loaded this liquid class' definition
|
|
1075
1121
|
liquid_class_def = self._liquid_class_def_cache[(name, version)]
|
|
@@ -5,7 +5,7 @@ import logging
|
|
|
5
5
|
from copy import deepcopy
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from typing import TYPE_CHECKING, Optional, Union, Literal
|
|
8
|
-
from dataclasses import dataclass, field
|
|
8
|
+
from dataclasses import dataclass, field, replace
|
|
9
9
|
|
|
10
10
|
from opentrons_shared_data.liquid_classes.liquid_class_definition import (
|
|
11
11
|
PositionReference,
|
|
@@ -21,7 +21,6 @@ from opentrons.protocol_api._liquid_properties import (
|
|
|
21
21
|
MultiDispenseProperties,
|
|
22
22
|
TouchTipProperties,
|
|
23
23
|
)
|
|
24
|
-
from opentrons.protocol_engine.errors import TouchTipDisabledError
|
|
25
24
|
from opentrons.types import Location, Point, Mount
|
|
26
25
|
from opentrons.protocols.advanced_control.transfers.transfer_liquid_utils import (
|
|
27
26
|
LocationCheckDescriptors,
|
|
@@ -466,7 +465,7 @@ class TransferComponentsExecutor:
|
|
|
466
465
|
2. If blowout is enabled and “destination”
|
|
467
466
|
- Do blow-out (at the retract position)
|
|
468
467
|
- Leave plunger down
|
|
469
|
-
3. Touch-tip
|
|
468
|
+
3. Touch-tip in the destination well.
|
|
470
469
|
4. If not ready-to-aspirate
|
|
471
470
|
- Prepare-to-aspirate (at the retract position)
|
|
472
471
|
5. Air-gap (at the retract position)
|
|
@@ -479,7 +478,7 @@ class TransferComponentsExecutor:
|
|
|
479
478
|
6. If blowout is “source” or “trash”
|
|
480
479
|
- Move to location (top of Well)
|
|
481
480
|
- Do blow-out (top of well)
|
|
482
|
-
- Do touch-tip
|
|
481
|
+
- Do touch-tip AGAIN at the source well (if blowout in a non-trash location)
|
|
483
482
|
- Prepare-to-aspirate (top of well)
|
|
484
483
|
- Do air-gap (top of well)
|
|
485
484
|
7. If drop tip, move to drop tip location, drop tip
|
|
@@ -563,9 +562,9 @@ class TransferComponentsExecutor:
|
|
|
563
562
|
blowout_props.enabled
|
|
564
563
|
and blowout_props.location != BlowoutLocation.DESTINATION
|
|
565
564
|
):
|
|
566
|
-
# TODO: no-op touch tip if touch tip is enabled and blowout is in trash/ reservoir/ any labware with touch-tip disabled
|
|
567
565
|
assert blowout_props.flow_rate is not None
|
|
568
566
|
self._instrument.set_flow_rate(blow_out=blowout_props.flow_rate)
|
|
567
|
+
blowout_touch_tip_props = retract_props.touch_tip
|
|
569
568
|
touch_tip_and_air_gap_location: Union[Location, TrashBin, WasteChute]
|
|
570
569
|
if blowout_props.location == BlowoutLocation.SOURCE:
|
|
571
570
|
if source_location is None or source_well is None:
|
|
@@ -584,6 +583,13 @@ class TransferComponentsExecutor:
|
|
|
584
583
|
source_well.get_top(0), labware=source_location.labware
|
|
585
584
|
)
|
|
586
585
|
touch_tip_and_air_gap_well = source_well
|
|
586
|
+
# Skip touch tip if blowing out at the SOURCE and it's untouchable:
|
|
587
|
+
if (
|
|
588
|
+
"touchTipDisabled"
|
|
589
|
+
in source_location.labware.quirks_from_any_parent()
|
|
590
|
+
):
|
|
591
|
+
blowout_touch_tip_props = replace(blowout_touch_tip_props)
|
|
592
|
+
blowout_touch_tip_props.enabled = False
|
|
587
593
|
else:
|
|
588
594
|
self._instrument.blow_out(
|
|
589
595
|
location=trash_location,
|
|
@@ -612,7 +618,7 @@ class TransferComponentsExecutor:
|
|
|
612
618
|
)
|
|
613
619
|
# Do touch tip and air gap again after blowing out into source well or trash
|
|
614
620
|
self._do_touch_tip_and_air_gap_after_dispense(
|
|
615
|
-
touch_tip_properties=
|
|
621
|
+
touch_tip_properties=blowout_touch_tip_props,
|
|
616
622
|
location=touch_tip_and_air_gap_location,
|
|
617
623
|
well=touch_tip_and_air_gap_well,
|
|
618
624
|
air_gap_volume=air_gap_volume,
|
|
@@ -758,6 +764,7 @@ class TransferComponentsExecutor:
|
|
|
758
764
|
):
|
|
759
765
|
assert blowout_props.flow_rate is not None
|
|
760
766
|
self._instrument.set_flow_rate(blow_out=blowout_props.flow_rate)
|
|
767
|
+
blowout_touch_tip_props = retract_props.touch_tip
|
|
761
768
|
touch_tip_and_air_gap_location: Union[Location, TrashBin, WasteChute]
|
|
762
769
|
if blowout_props.location == BlowoutLocation.SOURCE:
|
|
763
770
|
if source_location is None or source_well is None:
|
|
@@ -776,6 +783,13 @@ class TransferComponentsExecutor:
|
|
|
776
783
|
source_well.get_top(0), labware=source_location.labware
|
|
777
784
|
)
|
|
778
785
|
touch_tip_and_air_gap_well = source_well
|
|
786
|
+
# Skip touch tip if blowing out at the SOURCE and it's untouchable:
|
|
787
|
+
if (
|
|
788
|
+
"touchTipDisabled"
|
|
789
|
+
in source_location.labware.quirks_from_any_parent()
|
|
790
|
+
):
|
|
791
|
+
blowout_touch_tip_props = replace(blowout_touch_tip_props)
|
|
792
|
+
blowout_touch_tip_props.enabled = False
|
|
779
793
|
else:
|
|
780
794
|
self._instrument.blow_out(
|
|
781
795
|
location=trash_location,
|
|
@@ -807,13 +821,13 @@ class TransferComponentsExecutor:
|
|
|
807
821
|
air_gap_volume = 0
|
|
808
822
|
# Do touch tip and air gap again after blowing out into source well or trash
|
|
809
823
|
self._do_touch_tip_and_air_gap_after_dispense(
|
|
810
|
-
touch_tip_properties=
|
|
824
|
+
touch_tip_properties=blowout_touch_tip_props,
|
|
811
825
|
location=touch_tip_and_air_gap_location,
|
|
812
826
|
well=touch_tip_and_air_gap_well,
|
|
813
827
|
air_gap_volume=air_gap_volume,
|
|
814
828
|
)
|
|
815
829
|
|
|
816
|
-
def _do_touch_tip_and_air_gap_after_dispense(
|
|
830
|
+
def _do_touch_tip_and_air_gap_after_dispense(
|
|
817
831
|
self,
|
|
818
832
|
touch_tip_properties: TouchTipProperties,
|
|
819
833
|
location: Union[Location, TrashBin, WasteChute],
|
|
@@ -822,6 +836,12 @@ class TransferComponentsExecutor:
|
|
|
822
836
|
) -> None:
|
|
823
837
|
"""Perform touch tip and air gap as part of post-dispense retract.
|
|
824
838
|
|
|
839
|
+
This function can be invoked up to 2 times for each dispense:
|
|
840
|
+
1) Once for touching tip at the dispense location.
|
|
841
|
+
2) Then again in the blowout location if it is not the dispense location.
|
|
842
|
+
For case (2), the caller should disable touch-tip in touch_tip_properties
|
|
843
|
+
if the blowout location is not touchable (such as reservoirs).
|
|
844
|
+
|
|
825
845
|
If the retract location is at or above the safe location of
|
|
826
846
|
AIR_GAP_LOC_Z_OFFSET_FROM_WELL_TOP, then add the air gap at the retract location
|
|
827
847
|
(where the pipette is already assumed to be at).
|
|
@@ -842,18 +862,14 @@ class TransferComponentsExecutor:
|
|
|
842
862
|
# whether the touch tip params from transfer props should be used for
|
|
843
863
|
# both dest-well touch tip and non-dest-well touch tip.
|
|
844
864
|
if isinstance(location, Location) and well is not None:
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
)
|
|
854
|
-
except TouchTipDisabledError:
|
|
855
|
-
# TODO: log a warning
|
|
856
|
-
pass
|
|
865
|
+
self._instrument.touch_tip(
|
|
866
|
+
location=location,
|
|
867
|
+
well_core=well,
|
|
868
|
+
radius=1,
|
|
869
|
+
z_offset=touch_tip_properties.z_offset,
|
|
870
|
+
speed=touch_tip_properties.speed,
|
|
871
|
+
mm_from_edge=touch_tip_properties.mm_from_edge,
|
|
872
|
+
)
|
|
857
873
|
|
|
858
874
|
# Move back to the 'retract' position
|
|
859
875
|
self._instrument.move_to(
|
|
@@ -599,7 +599,7 @@ class LegacyProtocolCore(
|
|
|
599
599
|
"""Define a liquid to load into a well."""
|
|
600
600
|
assert False, "define_liquid only supported on engine core"
|
|
601
601
|
|
|
602
|
-
def get_liquid_class(self, name: str, version: int) -> LiquidClass:
|
|
602
|
+
def get_liquid_class(self, name: str, version: Optional[int]) -> LiquidClass:
|
|
603
603
|
"""Get an instance of a built-in liquid class."""
|
|
604
604
|
assert False, "define_liquid_class is only supported on engine core"
|
|
605
605
|
|
|
@@ -297,7 +297,7 @@ class AbstractProtocol(
|
|
|
297
297
|
"""Define a liquid to load into a well."""
|
|
298
298
|
|
|
299
299
|
@abstractmethod
|
|
300
|
-
def get_liquid_class(self, name: str, version: int) -> LiquidClass:
|
|
300
|
+
def get_liquid_class(self, name: str, version: Optional[int]) -> LiquidClass:
|
|
301
301
|
"""Get an instance of a built-in liquid class."""
|
|
302
302
|
|
|
303
303
|
@abstractmethod
|