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.

Files changed (40) hide show
  1. opentrons/_version.py +2 -2
  2. opentrons/cli/analyze.py +58 -2
  3. opentrons/drivers/asyncio/communication/serial_connection.py +8 -5
  4. opentrons/drivers/flex_stacker/driver.py +6 -1
  5. opentrons/hardware_control/backends/flex_protocol.py +1 -0
  6. opentrons/hardware_control/backends/ot3controller.py +25 -13
  7. opentrons/hardware_control/backends/ot3simulator.py +2 -1
  8. opentrons/hardware_control/dev_types.py +3 -1
  9. opentrons/hardware_control/instruments/ot2/pipette_handler.py +1 -0
  10. opentrons/hardware_control/instruments/ot3/pipette_handler.py +1 -0
  11. opentrons/hardware_control/ot3api.py +3 -1
  12. opentrons/hardware_control/protocols/gripper_controller.py +1 -0
  13. opentrons/protocol_api/core/engine/_default_liquid_class_versions.py +56 -0
  14. opentrons/protocol_api/core/engine/instrument.py +143 -18
  15. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +77 -17
  16. opentrons/protocol_api/core/engine/protocol.py +53 -7
  17. opentrons/protocol_api/core/engine/transfer_components_executor.py +36 -20
  18. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +1 -1
  19. opentrons/protocol_api/core/protocol.py +1 -1
  20. opentrons/protocol_api/labware.py +36 -2
  21. opentrons/protocol_api/module_contexts.py +146 -14
  22. opentrons/protocol_api/protocol_context.py +162 -12
  23. opentrons/protocol_api/validation.py +4 -0
  24. opentrons/protocol_engine/commands/command_unions.py +2 -0
  25. opentrons/protocol_engine/commands/flex_stacker/common.py +13 -0
  26. opentrons/protocol_engine/commands/flex_stacker/store.py +20 -2
  27. opentrons/protocol_engine/execution/labware_movement.py +14 -12
  28. opentrons/protocol_engine/resources/pipette_data_provider.py +3 -0
  29. opentrons/protocol_engine/state/geometry.py +33 -5
  30. opentrons/protocol_engine/state/labware.py +66 -0
  31. opentrons/protocol_engine/state/modules.py +6 -0
  32. opentrons/protocol_engine/state/pipettes.py +12 -3
  33. opentrons/protocol_engine/types/__init__.py +2 -0
  34. opentrons/protocol_engine/types/labware.py +9 -0
  35. opentrons/protocols/api_support/definitions.py +1 -1
  36. {opentrons-8.6.0a11.dist-info → opentrons-8.7.0.dist-info}/METADATA +4 -4
  37. {opentrons-8.6.0a11.dist-info → opentrons-8.7.0.dist-info}/RECORD +40 -39
  38. {opentrons-8.6.0a11.dist-info → opentrons-8.7.0.dist-info}/WHEEL +0 -0
  39. {opentrons-8.6.0a11.dist-info → opentrons-8.7.0.dist-info}/entry_points.txt +0 -0
  40. {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
- # TODO(jbl 2022-11-03) flow_rates should not live in the cores, and should be moved to the protocol context
126
- # along with other rate related refactors (for the hardware API)
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
- return self._aspirate_flow_rate * rate
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
- return self._dispense_flow_rate * rate
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
- return self._blow_out_flow_rate * rate
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._aspirate_flow_rate = aspirate
1149
+ self._user_aspirate_flow_rate = aspirate
1088
1150
  if dispense is not None:
1089
1151
  assert dispense > 0
1090
- self._dispense_flow_rate = dispense
1152
+ self._user_dispense_flow_rate = dispense
1091
1153
  if blow_out is not None:
1092
1154
  assert blow_out > 0
1093
- self._blow_out_flow_rate = blow_out
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 StagingSlotLocation, WellLocationType
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
- surrounding_slot=regular_slot,
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
- surrounding_slot=staging_slot,
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
- surrounding_slot: Union[DeckSlotName, StagingSlotName],
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
- # Check if slot overlaps with pipette position
185
- slot_pos = engine_state.addressable_areas.get_addressable_area_position(
186
- addressable_area_name=surrounding_slot.id,
187
- do_compatibility_check=False,
188
- )
189
- slot_bounds = engine_state.addressable_areas.get_addressable_area_bounding_box(
190
- addressable_area_name=surrounding_slot.id,
191
- do_compatibility_check=False,
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(surrounding_slot, DeckSlotName):
258
+ if isinstance(surrounding_location, DeckSlotName):
203
259
  slot_highest_z = engine_state.geometry.get_highest_z_in_slot(
204
- DeckSlotLocation(slotName=surrounding_slot)
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=surrounding_slot)
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 load_labware_params, deck_conflict, overlap_versions
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
- lid = self._engine_client.state.labware.get_lid_by_labware_id(
496
- labware.labware_id
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 lid is not None:
499
- lid_id = lid.id
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
- raise ValueError("Cannot move a lid off of a labware with no lid.")
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 (?????) (only if it’s in a non-trash location)
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=retract_props.touch_tip,
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=retract_props.touch_tip,
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( # noqa: C901
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
- try:
846
- self._instrument.touch_tip(
847
- location=location,
848
- well_core=well,
849
- radius=1,
850
- z_offset=touch_tip_properties.z_offset,
851
- speed=touch_tip_properties.speed,
852
- mm_from_edge=touch_tip_properties.mm_from_edge,
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