opentrons 8.3.1a1__py2.py3-none-any.whl → 8.4.0a1__py2.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.
Files changed (192) hide show
  1. opentrons/calibration_storage/ot2/mark_bad_calibration.py +2 -0
  2. opentrons/calibration_storage/ot2/tip_length.py +6 -6
  3. opentrons/config/advanced_settings.py +9 -11
  4. opentrons/config/feature_flags.py +0 -4
  5. opentrons/config/reset.py +7 -2
  6. opentrons/drivers/asyncio/communication/__init__.py +2 -0
  7. opentrons/drivers/asyncio/communication/async_serial.py +4 -0
  8. opentrons/drivers/asyncio/communication/errors.py +41 -8
  9. opentrons/drivers/asyncio/communication/serial_connection.py +36 -10
  10. opentrons/drivers/flex_stacker/__init__.py +9 -3
  11. opentrons/drivers/flex_stacker/abstract.py +140 -15
  12. opentrons/drivers/flex_stacker/driver.py +593 -47
  13. opentrons/drivers/flex_stacker/errors.py +64 -0
  14. opentrons/drivers/flex_stacker/simulator.py +222 -24
  15. opentrons/drivers/flex_stacker/types.py +211 -15
  16. opentrons/drivers/flex_stacker/utils.py +19 -0
  17. opentrons/execute.py +4 -2
  18. opentrons/hardware_control/api.py +5 -0
  19. opentrons/hardware_control/backends/flex_protocol.py +4 -0
  20. opentrons/hardware_control/backends/ot3controller.py +12 -1
  21. opentrons/hardware_control/backends/ot3simulator.py +3 -0
  22. opentrons/hardware_control/backends/subsystem_manager.py +8 -4
  23. opentrons/hardware_control/instruments/ot2/instrument_calibration.py +10 -6
  24. opentrons/hardware_control/instruments/ot3/pipette_handler.py +59 -6
  25. opentrons/hardware_control/modules/__init__.py +12 -1
  26. opentrons/hardware_control/modules/absorbance_reader.py +11 -9
  27. opentrons/hardware_control/modules/flex_stacker.py +498 -0
  28. opentrons/hardware_control/modules/heater_shaker.py +12 -10
  29. opentrons/hardware_control/modules/magdeck.py +5 -1
  30. opentrons/hardware_control/modules/tempdeck.py +5 -1
  31. opentrons/hardware_control/modules/thermocycler.py +15 -14
  32. opentrons/hardware_control/modules/types.py +191 -1
  33. opentrons/hardware_control/modules/utils.py +3 -0
  34. opentrons/hardware_control/motion_utilities.py +20 -0
  35. opentrons/hardware_control/ot3api.py +145 -15
  36. opentrons/hardware_control/protocols/liquid_handler.py +47 -1
  37. opentrons/hardware_control/types.py +6 -0
  38. opentrons/legacy_commands/commands.py +19 -3
  39. opentrons/legacy_commands/helpers.py +15 -0
  40. opentrons/legacy_commands/types.py +3 -2
  41. opentrons/protocol_api/__init__.py +2 -0
  42. opentrons/protocol_api/_liquid.py +39 -8
  43. opentrons/protocol_api/_liquid_properties.py +20 -19
  44. opentrons/protocol_api/_transfer_liquid_validation.py +91 -0
  45. opentrons/protocol_api/core/common.py +3 -1
  46. opentrons/protocol_api/core/engine/deck_conflict.py +11 -1
  47. opentrons/protocol_api/core/engine/instrument.py +1233 -65
  48. opentrons/protocol_api/core/engine/labware.py +8 -4
  49. opentrons/protocol_api/core/engine/load_labware_params.py +68 -10
  50. opentrons/protocol_api/core/engine/module_core.py +118 -2
  51. opentrons/protocol_api/core/engine/protocol.py +253 -11
  52. opentrons/protocol_api/core/engine/stringify.py +19 -8
  53. opentrons/protocol_api/core/engine/transfer_components_executor.py +853 -0
  54. opentrons/protocol_api/core/engine/well.py +60 -5
  55. opentrons/protocol_api/core/instrument.py +65 -19
  56. opentrons/protocol_api/core/labware.py +6 -2
  57. opentrons/protocol_api/core/legacy/labware_offset_provider.py +7 -3
  58. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +69 -21
  59. opentrons/protocol_api/core/legacy/legacy_labware_core.py +8 -4
  60. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +36 -0
  61. opentrons/protocol_api/core/legacy/legacy_well_core.py +25 -1
  62. opentrons/protocol_api/core/legacy/load_info.py +4 -12
  63. opentrons/protocol_api/core/legacy/module_geometry.py +6 -1
  64. opentrons/protocol_api/core/legacy/well_geometry.py +3 -3
  65. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +67 -21
  66. opentrons/protocol_api/core/module.py +43 -0
  67. opentrons/protocol_api/core/protocol.py +33 -0
  68. opentrons/protocol_api/core/well.py +21 -1
  69. opentrons/protocol_api/instrument_context.py +246 -123
  70. opentrons/protocol_api/labware.py +75 -11
  71. opentrons/protocol_api/module_contexts.py +140 -0
  72. opentrons/protocol_api/protocol_context.py +156 -16
  73. opentrons/protocol_api/validation.py +51 -41
  74. opentrons/protocol_engine/__init__.py +21 -2
  75. opentrons/protocol_engine/actions/actions.py +5 -5
  76. opentrons/protocol_engine/clients/sync_client.py +6 -0
  77. opentrons/protocol_engine/commands/__init__.py +30 -0
  78. opentrons/protocol_engine/commands/absorbance_reader/__init__.py +0 -1
  79. opentrons/protocol_engine/commands/air_gap_in_place.py +3 -2
  80. opentrons/protocol_engine/commands/aspirate.py +6 -2
  81. opentrons/protocol_engine/commands/aspirate_in_place.py +3 -1
  82. opentrons/protocol_engine/commands/aspirate_while_tracking.py +237 -0
  83. opentrons/protocol_engine/commands/blow_out.py +2 -0
  84. opentrons/protocol_engine/commands/blow_out_in_place.py +4 -1
  85. opentrons/protocol_engine/commands/command_unions.py +69 -0
  86. opentrons/protocol_engine/commands/configure_for_volume.py +3 -0
  87. opentrons/protocol_engine/commands/dispense.py +3 -1
  88. opentrons/protocol_engine/commands/dispense_in_place.py +3 -0
  89. opentrons/protocol_engine/commands/dispense_while_tracking.py +240 -0
  90. opentrons/protocol_engine/commands/drop_tip.py +23 -1
  91. opentrons/protocol_engine/commands/evotip_dispense.py +6 -7
  92. opentrons/protocol_engine/commands/evotip_seal_pipette.py +24 -29
  93. opentrons/protocol_engine/commands/evotip_unseal_pipette.py +1 -7
  94. opentrons/protocol_engine/commands/flex_stacker/__init__.py +106 -0
  95. opentrons/protocol_engine/commands/flex_stacker/close_latch.py +72 -0
  96. opentrons/protocol_engine/commands/flex_stacker/common.py +15 -0
  97. opentrons/protocol_engine/commands/flex_stacker/empty.py +161 -0
  98. opentrons/protocol_engine/commands/flex_stacker/fill.py +164 -0
  99. opentrons/protocol_engine/commands/flex_stacker/open_latch.py +70 -0
  100. opentrons/protocol_engine/commands/flex_stacker/prepare_shuttle.py +112 -0
  101. opentrons/protocol_engine/commands/flex_stacker/retrieve.py +394 -0
  102. opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +190 -0
  103. opentrons/protocol_engine/commands/flex_stacker/store.py +288 -0
  104. opentrons/protocol_engine/commands/generate_command_schema.py +31 -2
  105. opentrons/protocol_engine/commands/labware_handling_common.py +24 -0
  106. opentrons/protocol_engine/commands/liquid_probe.py +21 -12
  107. opentrons/protocol_engine/commands/load_labware.py +42 -39
  108. opentrons/protocol_engine/commands/load_lid.py +21 -13
  109. opentrons/protocol_engine/commands/load_lid_stack.py +130 -47
  110. opentrons/protocol_engine/commands/load_module.py +18 -17
  111. opentrons/protocol_engine/commands/load_pipette.py +3 -0
  112. opentrons/protocol_engine/commands/move_labware.py +139 -20
  113. opentrons/protocol_engine/commands/pick_up_tip.py +5 -2
  114. opentrons/protocol_engine/commands/pipetting_common.py +154 -7
  115. opentrons/protocol_engine/commands/prepare_to_aspirate.py +17 -2
  116. opentrons/protocol_engine/commands/reload_labware.py +6 -19
  117. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +3 -1
  118. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +6 -1
  119. opentrons/protocol_engine/errors/__init__.py +8 -0
  120. opentrons/protocol_engine/errors/exceptions.py +50 -0
  121. opentrons/protocol_engine/execution/equipment.py +123 -106
  122. opentrons/protocol_engine/execution/labware_movement.py +8 -6
  123. opentrons/protocol_engine/execution/pipetting.py +233 -26
  124. opentrons/protocol_engine/execution/tip_handler.py +14 -5
  125. opentrons/protocol_engine/labware_offset_standardization.py +173 -0
  126. opentrons/protocol_engine/protocol_engine.py +22 -13
  127. opentrons/protocol_engine/resources/deck_configuration_provider.py +94 -2
  128. opentrons/protocol_engine/resources/deck_data_provider.py +1 -1
  129. opentrons/protocol_engine/resources/labware_data_provider.py +32 -12
  130. opentrons/protocol_engine/resources/labware_validation.py +7 -5
  131. opentrons/protocol_engine/slot_standardization.py +11 -23
  132. opentrons/protocol_engine/state/addressable_areas.py +84 -46
  133. opentrons/protocol_engine/state/frustum_helpers.py +26 -10
  134. opentrons/protocol_engine/state/geometry.py +683 -100
  135. opentrons/protocol_engine/state/labware.py +252 -55
  136. opentrons/protocol_engine/state/module_substates/__init__.py +4 -0
  137. opentrons/protocol_engine/state/module_substates/flex_stacker_substate.py +68 -0
  138. opentrons/protocol_engine/state/module_substates/heater_shaker_module_substate.py +22 -0
  139. opentrons/protocol_engine/state/module_substates/temperature_module_substate.py +13 -0
  140. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +20 -0
  141. opentrons/protocol_engine/state/modules.py +178 -52
  142. opentrons/protocol_engine/state/pipettes.py +54 -0
  143. opentrons/protocol_engine/state/state.py +1 -1
  144. opentrons/protocol_engine/state/tips.py +14 -0
  145. opentrons/protocol_engine/state/update_types.py +180 -25
  146. opentrons/protocol_engine/state/wells.py +54 -8
  147. opentrons/protocol_engine/types/__init__.py +292 -0
  148. opentrons/protocol_engine/types/automatic_tip_selection.py +39 -0
  149. opentrons/protocol_engine/types/command_annotations.py +53 -0
  150. opentrons/protocol_engine/types/deck_configuration.py +72 -0
  151. opentrons/protocol_engine/types/execution.py +96 -0
  152. opentrons/protocol_engine/types/hardware_passthrough.py +25 -0
  153. opentrons/protocol_engine/types/instrument.py +47 -0
  154. opentrons/protocol_engine/types/instrument_sensors.py +47 -0
  155. opentrons/protocol_engine/types/labware.py +110 -0
  156. opentrons/protocol_engine/types/labware_movement.py +22 -0
  157. opentrons/protocol_engine/types/labware_offset_location.py +108 -0
  158. opentrons/protocol_engine/types/labware_offset_vector.py +33 -0
  159. opentrons/protocol_engine/types/liquid.py +40 -0
  160. opentrons/protocol_engine/types/liquid_class.py +59 -0
  161. opentrons/protocol_engine/types/liquid_handling.py +13 -0
  162. opentrons/protocol_engine/types/liquid_level_detection.py +137 -0
  163. opentrons/protocol_engine/types/location.py +193 -0
  164. opentrons/protocol_engine/types/module.py +269 -0
  165. opentrons/protocol_engine/types/partial_tip_configuration.py +76 -0
  166. opentrons/protocol_engine/types/run_time_parameters.py +133 -0
  167. opentrons/protocol_engine/types/tip.py +18 -0
  168. opentrons/protocol_engine/types/util.py +21 -0
  169. opentrons/protocol_engine/types/well_position.py +107 -0
  170. opentrons/protocol_reader/extract_labware_definitions.py +7 -3
  171. opentrons/protocol_reader/file_format_validator.py +5 -3
  172. opentrons/protocol_runner/json_translator.py +4 -2
  173. opentrons/protocol_runner/legacy_command_mapper.py +6 -2
  174. opentrons/protocol_runner/run_orchestrator.py +4 -1
  175. opentrons/protocols/advanced_control/transfers/common.py +48 -1
  176. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +204 -0
  177. opentrons/protocols/api_support/definitions.py +1 -1
  178. opentrons/protocols/api_support/instrument.py +16 -3
  179. opentrons/protocols/labware.py +5 -6
  180. opentrons/protocols/models/__init__.py +0 -21
  181. opentrons/simulate.py +4 -2
  182. opentrons/types.py +15 -6
  183. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a1.dist-info}/METADATA +4 -4
  184. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a1.dist-info}/RECORD +188 -148
  185. opentrons/calibration_storage/ot2/models/defaults.py +0 -0
  186. opentrons/calibration_storage/ot3/models/defaults.py +0 -0
  187. opentrons/protocol_api/core/legacy/legacy_robot_core.py +0 -0
  188. opentrons/protocol_engine/types.py +0 -1311
  189. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a1.dist-info}/LICENSE +0 -0
  190. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a1.dist-info}/WHEEL +0 -0
  191. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a1.dist-info}/entry_points.txt +0 -0
  192. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a1.dist-info}/top_level.txt +0 -0
@@ -1,14 +1,33 @@
1
1
  """ProtocolEngine-based InstrumentContext core implementation."""
2
2
 
3
3
  from __future__ import annotations
4
-
5
- from typing import Optional, TYPE_CHECKING, cast, Union, List
6
- from opentrons.types import Location, Mount, NozzleConfigurationType, NozzleMapInterface
4
+ from contextlib import contextmanager
5
+ from typing import (
6
+ Optional,
7
+ TYPE_CHECKING,
8
+ cast,
9
+ Union,
10
+ List,
11
+ Tuple,
12
+ NamedTuple,
13
+ Generator,
14
+ )
15
+ from opentrons.types import (
16
+ Location,
17
+ Mount,
18
+ NozzleConfigurationType,
19
+ NozzleMapInterface,
20
+ MeniscusTrackingTarget,
21
+ )
7
22
  from opentrons.hardware_control import SyncHardwareAPI
8
23
  from opentrons.hardware_control.dev_types import PipetteDict
9
24
  from opentrons.protocols.api_support.util import FlowRates, find_value_for_api_version
10
25
  from opentrons.protocols.api_support.types import APIVersion
11
- from opentrons.protocols.advanced_control.transfers.common import TransferTipPolicyV2
26
+ from opentrons.protocols.advanced_control.transfers.common import (
27
+ TransferTipPolicyV2,
28
+ NoLiquidClassPropertyError,
29
+ )
30
+ from opentrons.protocols.advanced_control.transfers import common as tx_commons
12
31
  from opentrons.protocol_engine import commands as cmd
13
32
  from opentrons.protocol_engine import (
14
33
  DeckPoint,
@@ -28,31 +47,48 @@ from opentrons.protocol_engine.types import (
28
47
  NozzleLayoutConfigurationType,
29
48
  AddressableOffsetVector,
30
49
  LiquidClassRecord,
50
+ NextTipInfo,
31
51
  )
52
+ from opentrons.protocol_engine.types.liquid_level_detection import LiquidTrackingType
32
53
  from opentrons.protocol_engine.errors.exceptions import TipNotAttachedError
33
54
  from opentrons.protocol_engine.clients import SyncClient as EngineClient
34
55
  from opentrons.protocols.api_support.definitions import MAX_SUPPORTED_VERSION
35
- from opentrons_shared_data.pipette.types import PipetteNameType
56
+ from opentrons_shared_data.pipette.types import (
57
+ PIPETTE_API_NAMES_MAP,
58
+ LIQUID_PROBE_START_OFFSET_FROM_WELL_TOP,
59
+ )
36
60
  from opentrons_shared_data.errors.exceptions import (
37
61
  UnsupportedHardwareCommand,
38
62
  )
63
+ from opentrons_shared_data.liquid_classes.liquid_class_definition import BlowoutLocation
39
64
  from opentrons.protocol_api._nozzle_layout import NozzleLayout
40
65
  from . import overlap_versions, pipette_movement_conflict
66
+ from . import transfer_components_executor as tx_comps_executor
41
67
 
42
68
  from .well import WellCore
69
+ from .labware import LabwareCore
43
70
  from ..instrument import AbstractInstrument
44
71
  from ...disposal_locations import TrashBin, WasteChute
45
72
 
46
73
  if TYPE_CHECKING:
47
74
  from .protocol import ProtocolCore
48
75
  from opentrons.protocol_api._liquid import LiquidClass
76
+ from opentrons.protocol_api._liquid_properties import (
77
+ TransferProperties,
78
+ MultiDispenseProperties,
79
+ )
49
80
 
50
81
  _DISPENSE_VOLUME_VALIDATION_ADDED_IN = APIVersion(2, 17)
51
82
  _RESIN_TIP_DEFAULT_VOLUME = 400
52
83
  _RESIN_TIP_DEFAULT_FLOW_RATE = 10.0
53
84
 
85
+ _FLEX_PIPETTE_NAMES_FIXED_IN = APIVersion(2, 23)
86
+ """The version after which InstrumentContext.name returns the correct API-specific names of Flex pipettes."""
87
+ _RETURN_TIP_SCRAPE_ADDED_IN = APIVersion(2, 23)
88
+ """The version after which return-tip for 1/8 channels will scrape off."""
54
89
 
55
- class InstrumentCore(AbstractInstrument[WellCore]):
90
+
91
+ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
56
92
  """Instrument API core using a ProtocolEngine.
57
93
 
58
94
  Args:
@@ -115,7 +151,9 @@ class InstrumentCore(AbstractInstrument[WellCore]):
115
151
  pipette_id=self._pipette_id, speed=speed
116
152
  )
117
153
 
118
- def air_gap_in_place(self, volume: float, flow_rate: float) -> None:
154
+ def air_gap_in_place(
155
+ self, volume: float, flow_rate: float, correction_volume: Optional[float] = None
156
+ ) -> None:
119
157
  """Aspirate a given volume of air from the current location of the pipette.
120
158
 
121
159
  Args:
@@ -124,7 +162,10 @@ class InstrumentCore(AbstractInstrument[WellCore]):
124
162
  """
125
163
  self._engine_client.execute_command(
126
164
  cmd.AirGapInPlaceParams(
127
- pipetteId=self._pipette_id, volume=volume, flowRate=flow_rate
165
+ pipetteId=self._pipette_id,
166
+ volume=volume,
167
+ flowRate=flow_rate,
168
+ correctionVolume=correction_volume,
128
169
  )
129
170
  )
130
171
 
@@ -136,7 +177,8 @@ class InstrumentCore(AbstractInstrument[WellCore]):
136
177
  rate: float,
137
178
  flow_rate: float,
138
179
  in_place: bool,
139
- is_meniscus: Optional[bool] = None,
180
+ meniscus_tracking: Optional[MeniscusTrackingTarget] = None,
181
+ correction_volume: Optional[float] = None,
140
182
  ) -> None:
141
183
  """Aspirate a given volume of liquid from the specified location.
142
184
  Args:
@@ -146,7 +188,10 @@ class InstrumentCore(AbstractInstrument[WellCore]):
146
188
  rate: Not used in this core.
147
189
  flow_rate: The flow rate in µL/s to aspirate at.
148
190
  in_place: whether this is a in-place command.
191
+ meniscus_tracking: Optional data about where to aspirate from.
149
192
  """
193
+ if meniscus_tracking == MeniscusTrackingTarget.START:
194
+ raise ValueError("Cannot aspirate at the starting liquid height.")
150
195
  if well_core is None:
151
196
  if not in_place:
152
197
  self._engine_client.execute_command(
@@ -163,7 +208,10 @@ class InstrumentCore(AbstractInstrument[WellCore]):
163
208
 
164
209
  self._engine_client.execute_command(
165
210
  cmd.AspirateInPlaceParams(
166
- pipetteId=self._pipette_id, volume=volume, flowRate=flow_rate
211
+ pipetteId=self._pipette_id,
212
+ volume=volume,
213
+ flowRate=flow_rate,
214
+ correctionVolume=correction_volume,
167
215
  )
168
216
  )
169
217
 
@@ -171,14 +219,15 @@ class InstrumentCore(AbstractInstrument[WellCore]):
171
219
  well_name = well_core.get_name()
172
220
  labware_id = well_core.labware_id
173
221
 
174
- well_location = self._engine_client.state.geometry.get_relative_liquid_handling_well_location(
222
+ (
223
+ well_location,
224
+ dynamic_liquid_tracking,
225
+ ) = self._engine_client.state.geometry.get_relative_liquid_handling_well_location(
175
226
  labware_id=labware_id,
176
227
  well_name=well_name,
177
228
  absolute_point=location.point,
178
- is_meniscus=is_meniscus,
229
+ meniscus_tracking=meniscus_tracking,
179
230
  )
180
- if well_location.origin == WellOrigin.MENISCUS:
181
- well_location.volumeOffset = "operationVolume"
182
231
  pipette_movement_conflict.check_safe_for_pipette_movement(
183
232
  engine_state=self._engine_client.state,
184
233
  pipette_id=self._pipette_id,
@@ -186,16 +235,31 @@ class InstrumentCore(AbstractInstrument[WellCore]):
186
235
  well_name=well_name,
187
236
  well_location=well_location,
188
237
  )
189
- self._engine_client.execute_command(
190
- cmd.AspirateParams(
191
- pipetteId=self._pipette_id,
192
- labwareId=labware_id,
193
- wellName=well_name,
194
- wellLocation=well_location,
195
- volume=volume,
196
- flowRate=flow_rate,
238
+ if dynamic_liquid_tracking:
239
+
240
+ self._engine_client.execute_command(
241
+ cmd.AspirateWhileTrackingParams(
242
+ pipetteId=self._pipette_id,
243
+ labwareId=labware_id,
244
+ wellName=well_name,
245
+ wellLocation=well_location,
246
+ volume=volume,
247
+ flowRate=flow_rate,
248
+ correctionVolume=correction_volume,
249
+ )
250
+ )
251
+ else:
252
+ self._engine_client.execute_command(
253
+ cmd.AspirateParams(
254
+ pipetteId=self._pipette_id,
255
+ labwareId=labware_id,
256
+ wellName=well_name,
257
+ wellLocation=well_location,
258
+ volume=volume,
259
+ flowRate=flow_rate,
260
+ correctionVolume=correction_volume,
261
+ )
197
262
  )
198
- )
199
263
 
200
264
  self._protocol_core.set_last_location(location=location, mount=self.get_mount())
201
265
 
@@ -208,7 +272,8 @@ class InstrumentCore(AbstractInstrument[WellCore]):
208
272
  flow_rate: float,
209
273
  in_place: bool,
210
274
  push_out: Optional[float],
211
- is_meniscus: Optional[bool] = None,
275
+ meniscus_tracking: Optional[MeniscusTrackingTarget] = None,
276
+ correction_volume: Optional[float] = None,
212
277
  ) -> None:
213
278
  """Dispense a given volume of liquid into the specified location.
214
279
  Args:
@@ -219,6 +284,7 @@ class InstrumentCore(AbstractInstrument[WellCore]):
219
284
  flow_rate: The flow rate in µL/s to dispense at.
220
285
  in_place: whether this is a in-place command.
221
286
  push_out: The amount to push the plunger below bottom position.
287
+ meniscus_tracking: Optional data about where to dispense from.
222
288
  """
223
289
  if self._protocol_core.api_version < _DISPENSE_VOLUME_VALIDATION_ADDED_IN:
224
290
  # In older API versions, when you try to dispense more than you can,
@@ -256,6 +322,7 @@ class InstrumentCore(AbstractInstrument[WellCore]):
256
322
  volume=volume,
257
323
  flowRate=flow_rate,
258
324
  pushOut=push_out,
325
+ correctionVolume=correction_volume,
259
326
  )
260
327
  )
261
328
  else:
@@ -264,11 +331,14 @@ class InstrumentCore(AbstractInstrument[WellCore]):
264
331
  well_name = well_core.get_name()
265
332
  labware_id = well_core.labware_id
266
333
 
267
- well_location = self._engine_client.state.geometry.get_relative_liquid_handling_well_location(
334
+ (
335
+ well_location,
336
+ dynamic_liquid_tracking,
337
+ ) = self._engine_client.state.geometry.get_relative_liquid_handling_well_location(
268
338
  labware_id=labware_id,
269
339
  well_name=well_name,
270
340
  absolute_point=location.point,
271
- is_meniscus=is_meniscus,
341
+ meniscus_tracking=meniscus_tracking,
272
342
  )
273
343
  pipette_movement_conflict.check_safe_for_pipette_movement(
274
344
  engine_state=self._engine_client.state,
@@ -277,17 +347,32 @@ class InstrumentCore(AbstractInstrument[WellCore]):
277
347
  well_name=well_name,
278
348
  well_location=well_location,
279
349
  )
280
- self._engine_client.execute_command(
281
- cmd.DispenseParams(
282
- pipetteId=self._pipette_id,
283
- labwareId=labware_id,
284
- wellName=well_name,
285
- wellLocation=well_location,
286
- volume=volume,
287
- flowRate=flow_rate,
288
- pushOut=push_out,
350
+ if dynamic_liquid_tracking:
351
+ self._engine_client.execute_command(
352
+ cmd.DispenseWhileTrackingParams(
353
+ pipetteId=self._pipette_id,
354
+ labwareId=labware_id,
355
+ wellName=well_name,
356
+ wellLocation=well_location,
357
+ volume=volume,
358
+ flowRate=flow_rate,
359
+ pushOut=push_out,
360
+ correctionVolume=correction_volume,
361
+ )
362
+ )
363
+ else:
364
+ self._engine_client.execute_command(
365
+ cmd.DispenseParams(
366
+ pipetteId=self._pipette_id,
367
+ labwareId=labware_id,
368
+ wellName=well_name,
369
+ wellLocation=well_location,
370
+ volume=volume,
371
+ flowRate=flow_rate,
372
+ pushOut=push_out,
373
+ correctionVolume=correction_volume,
374
+ )
289
375
  )
290
- )
291
376
 
292
377
  if isinstance(location, (TrashBin, WasteChute)):
293
378
  self._protocol_core.set_last_location(location=None, mount=self.get_mount())
@@ -380,6 +465,7 @@ class InstrumentCore(AbstractInstrument[WellCore]):
380
465
  radius: float,
381
466
  z_offset: float,
382
467
  speed: float,
468
+ mm_from_edge: Optional[float] = None,
383
469
  ) -> None:
384
470
  """Touch pipette tip to edges of the well
385
471
 
@@ -389,7 +475,11 @@ class InstrumentCore(AbstractInstrument[WellCore]):
389
475
  radius: Percentage modifier for well radius to touch.
390
476
  z_offset: Vertical offset for pipette tip during touch tip.
391
477
  speed: Speed for the touch tip movements.
478
+ mm_from_edge: Offset from the edge of the well to move to. Requires a radius of 1.
392
479
  """
480
+ if mm_from_edge is not None and radius != 1.0:
481
+ raise ValueError("radius must be set to 1.0 if mm_from_edge is provided.")
482
+
393
483
  well_name = well_core.get_name()
394
484
  labware_id = well_core.labware_id
395
485
 
@@ -411,6 +501,7 @@ class InstrumentCore(AbstractInstrument[WellCore]):
411
501
  wellName=well_name,
412
502
  wellLocation=well_location,
413
503
  radius=radius,
504
+ mmFromEdge=mm_from_edge,
414
505
  speed=speed,
415
506
  )
416
507
  )
@@ -496,6 +587,7 @@ class InstrumentCore(AbstractInstrument[WellCore]):
496
587
  """
497
588
  well_name = well_core.get_name()
498
589
  labware_id = well_core.labware_id
590
+ scrape_tips = False
499
591
 
500
592
  if location is not None:
501
593
  relative_well_location = (
@@ -519,6 +611,7 @@ class InstrumentCore(AbstractInstrument[WellCore]):
519
611
  pipette_id=self._pipette_id,
520
612
  labware_id=labware_id,
521
613
  )
614
+ scrape_tips = self.get_channels() <= 8
522
615
  pipette_movement_conflict.check_safe_for_pipette_movement(
523
616
  engine_state=self._engine_client.state,
524
617
  pipette_id=self._pipette_id,
@@ -534,6 +627,7 @@ class InstrumentCore(AbstractInstrument[WellCore]):
534
627
  wellLocation=well_location,
535
628
  homeAfter=home_after,
536
629
  alternateDropLocation=alternate_drop_location,
630
+ scrape_tips=scrape_tips,
537
631
  )
538
632
  )
539
633
 
@@ -763,11 +857,13 @@ class InstrumentCore(AbstractInstrument[WellCore]):
763
857
  if flow_rate is None:
764
858
  flow_rate = _RESIN_TIP_DEFAULT_FLOW_RATE
765
859
 
766
- well_location = self._engine_client.state.geometry.get_relative_liquid_handling_well_location(
860
+ (
861
+ well_location,
862
+ dynamic_tracking,
863
+ ) = self._engine_client.state.geometry.get_relative_liquid_handling_well_location(
767
864
  labware_id=labware_id,
768
865
  well_name=well_name,
769
866
  absolute_point=location.point,
770
- is_meniscus=None,
771
867
  )
772
868
  pipette_movement_conflict.check_safe_for_pipette_movement(
773
869
  engine_state=self._engine_client.state,
@@ -794,19 +890,33 @@ class InstrumentCore(AbstractInstrument[WellCore]):
794
890
  ).mount.to_hw_mount()
795
891
 
796
892
  def get_pipette_name(self) -> str:
797
- """Get the pipette's load name as a string.
893
+ """Get the pipette's name as a string.
798
894
 
799
895
  Will match the load name of the actually loaded pipette,
800
896
  which may differ from the requested load name.
897
+
898
+ From API v2.15 to v2.22, this property returned an internal, engine-specific,
899
+ name for Flex pipettes (eg, "p50_multi_flex" instead of "flex_8channel_50").
900
+
901
+ From API v2.23 onwards, this behavior is fixed so that this property returns
902
+ the API-specific names of Flex pipettes.
801
903
  """
802
904
  # TODO (tz, 11-23-22): revert this change when merging
803
905
  # https://opentrons.atlassian.net/browse/RLIQ-251
804
906
  pipette = self._engine_client.state.pipettes.get(self._pipette_id)
805
- return (
806
- pipette.pipetteName.value
807
- if isinstance(pipette.pipetteName, PipetteNameType)
808
- else pipette.pipetteName
809
- )
907
+ if self._protocol_core.api_version < _FLEX_PIPETTE_NAMES_FIXED_IN:
908
+ return pipette.pipetteName.value
909
+ else:
910
+ name = next(
911
+ (
912
+ pip_api_name
913
+ for pip_api_name, pip_name in PIPETTE_API_NAMES_MAP.items()
914
+ if pip_name == pipette.pipetteName
915
+ ),
916
+ None,
917
+ )
918
+ assert name, "Pipette name not found."
919
+ return name
810
920
 
811
921
  def get_model(self) -> str:
812
922
  return self._engine_client.state.pipettes.get_model_name(self._pipette_id)
@@ -833,6 +943,16 @@ class InstrumentCore(AbstractInstrument[WellCore]):
833
943
 
834
944
  return current_volume or 0
835
945
 
946
+ def get_has_clean_tip(self) -> bool:
947
+ try:
948
+ clean_tip = self._engine_client.state.pipettes.get_has_clean_tip(
949
+ self._pipette_id
950
+ )
951
+ except TipNotAttachedError:
952
+ clean_tip = False
953
+
954
+ return clean_tip
955
+
836
956
  def get_available_volume(self) -> float:
837
957
  try:
838
958
  available_volume = self._engine_client.state.pipettes.get_available_volume(
@@ -975,23 +1095,28 @@ class InstrumentCore(AbstractInstrument[WellCore]):
975
1095
 
976
1096
  def load_liquid_class(
977
1097
  self,
978
- liquid_class: LiquidClass,
979
- pipette_load_name: str,
1098
+ name: str,
1099
+ transfer_properties: TransferProperties,
980
1100
  tiprack_uri: str,
981
1101
  ) -> str:
982
- """Load a liquid class into the engine and return its ID."""
983
- transfer_props = liquid_class.get_for(
984
- pipette=pipette_load_name, tiprack=tiprack_uri
985
- )
1102
+ """Load a liquid class into the engine and return its ID.
986
1103
 
1104
+ Args:
1105
+ name: Name of the liquid class
1106
+ transfer_properties: Liquid class properties for a specific pipette & tiprack combination
1107
+ tiprack_uri: URI of the tiprack whose transfer properties we will be using.
1108
+
1109
+ Returns:
1110
+ Liquid class record's ID, as generated by the protocol engine.
1111
+ """
987
1112
  liquid_class_record = LiquidClassRecord(
988
- liquidClassName=liquid_class.name,
989
- pipetteModel=self.get_model(), # TODO: verify this is the correct 'model' to use
1113
+ liquidClassName=name,
1114
+ pipetteModel=self.get_pipette_name(),
990
1115
  tiprack=tiprack_uri,
991
- aspirate=transfer_props.aspirate.as_shared_data_model(),
992
- singleDispense=transfer_props.dispense.as_shared_data_model(),
993
- multiDispense=transfer_props.multi_dispense.as_shared_data_model()
994
- if transfer_props.multi_dispense
1116
+ aspirate=transfer_properties.aspirate.as_shared_data_model(),
1117
+ singleDispense=transfer_properties.dispense.as_shared_data_model(),
1118
+ multiDispense=transfer_properties.multi_dispense.as_shared_data_model()
1119
+ if transfer_properties.multi_dispense
995
1120
  else None,
996
1121
  )
997
1122
  result = self._engine_client.execute_command_without_recovery(
@@ -1001,16 +1126,1019 @@ class InstrumentCore(AbstractInstrument[WellCore]):
1001
1126
  )
1002
1127
  return result.liquidClassId
1003
1128
 
1004
- def transfer_liquid(
1129
+ def get_next_tip(
1130
+ self, tip_racks: List[LabwareCore], starting_well: Optional[str]
1131
+ ) -> Optional[NextTipInfo]:
1132
+ """Get the next tip to pick up."""
1133
+ result = self._engine_client.execute_command_without_recovery(
1134
+ cmd.GetNextTipParams(
1135
+ pipetteId=self._pipette_id,
1136
+ labwareIds=[tip_rack.labware_id for tip_rack in tip_racks],
1137
+ startingTipWell=starting_well,
1138
+ )
1139
+ )
1140
+ return (
1141
+ result.nextTipInfo if isinstance(result.nextTipInfo, NextTipInfo) else None
1142
+ )
1143
+
1144
+ def transfer_liquid( # noqa: C901
1145
+ self,
1146
+ liquid_class: LiquidClass,
1147
+ volume: float,
1148
+ source: List[Tuple[Location, WellCore]],
1149
+ dest: List[Tuple[Location, WellCore]],
1150
+ new_tip: TransferTipPolicyV2,
1151
+ tip_racks: List[Tuple[Location, LabwareCore]],
1152
+ trash_location: Union[Location, TrashBin, WasteChute],
1153
+ return_tip: bool,
1154
+ ) -> None:
1155
+ """Execute transfer using liquid class properties.
1156
+
1157
+ Args:
1158
+ liquid_class: The liquid class to use for transfer properties.
1159
+ volume: Volume to transfer per well.
1160
+ source: List of source wells, with each well represented as a tuple of
1161
+ types.Location and WellCore.
1162
+ types.Location is only necessary for saving the last accessed location.
1163
+ dest: List of destination wells, with each well represented as a tuple of
1164
+ types.Location and WellCore.
1165
+ types.Location is only necessary for saving the last accessed location.
1166
+ new_tip: Whether the transfer should use a new tip 'once', 'never', 'always',
1167
+ or 'per source'.
1168
+ tiprack_uri: The URI of the tiprack that the transfer settings are for.
1169
+ tip_drop_location: Location where the tip will be dropped (if appropriate).
1170
+ """
1171
+ if not tip_racks:
1172
+ raise RuntimeError(
1173
+ "No tipracks found for pipette in order to perform transfer"
1174
+ )
1175
+ tiprack_uri_for_transfer_props = tip_racks[0][1].get_uri()
1176
+ try:
1177
+ transfer_props = liquid_class.get_for(
1178
+ pipette=self.get_pipette_name(), tip_rack=tiprack_uri_for_transfer_props
1179
+ )
1180
+ except NoLiquidClassPropertyError:
1181
+ if self._protocol_core.robot_type == "OT-2 Standard":
1182
+ raise NoLiquidClassPropertyError(
1183
+ "Default liquid classes are not supported with OT-2 pipettes and tip racks."
1184
+ ) from None
1185
+ raise
1186
+
1187
+ # TODO: use the ID returned by load_liquid_class in command annotations
1188
+ self.load_liquid_class(
1189
+ name=liquid_class.name,
1190
+ transfer_properties=transfer_props,
1191
+ tiprack_uri=tiprack_uri_for_transfer_props,
1192
+ )
1193
+
1194
+ source_dest_per_volume_step = (
1195
+ tx_commons.expand_for_volume_constraints_for_liquid_classes(
1196
+ volumes=[volume for _ in range(len(source))],
1197
+ targets=zip(source, dest),
1198
+ max_volume=min(
1199
+ self.get_max_volume(),
1200
+ self._engine_client.state.geometry.get_nominal_tip_geometry(
1201
+ pipette_id=self.pipette_id,
1202
+ labware_id=tip_racks[0][1].labware_id,
1203
+ well_name=None,
1204
+ ).volume,
1205
+ ),
1206
+ )
1207
+ )
1208
+
1209
+ last_tip_picked_up_from: Optional[WellCore] = None
1210
+
1211
+ def _drop_tip() -> None:
1212
+ if return_tip:
1213
+ assert last_tip_picked_up_from is not None
1214
+ self.drop_tip(
1215
+ location=None,
1216
+ well_core=last_tip_picked_up_from,
1217
+ home_after=False,
1218
+ alternate_drop_location=False,
1219
+ )
1220
+ elif isinstance(trash_location, (TrashBin, WasteChute)):
1221
+ self.drop_tip_in_disposal_location(
1222
+ disposal_location=trash_location,
1223
+ home_after=False,
1224
+ alternate_tip_drop=True,
1225
+ )
1226
+ elif isinstance(trash_location, Location):
1227
+ self.drop_tip(
1228
+ location=trash_location,
1229
+ well_core=trash_location.labware.as_well()._core, # type: ignore[arg-type]
1230
+ home_after=False,
1231
+ alternate_drop_location=True,
1232
+ )
1233
+
1234
+ def _pick_up_tip() -> WellCore:
1235
+ next_tip = self.get_next_tip(
1236
+ tip_racks=[core for loc, core in tip_racks],
1237
+ starting_well=None,
1238
+ )
1239
+ if next_tip is None:
1240
+ raise RuntimeError(
1241
+ f"No tip available among {tip_racks} for this transfer."
1242
+ )
1243
+ (
1244
+ tiprack_loc,
1245
+ tiprack_uri,
1246
+ tip_well,
1247
+ ) = self._get_location_and_well_core_from_next_tip_info(next_tip, tip_racks)
1248
+ if tiprack_uri != tiprack_uri_for_transfer_props:
1249
+ raise RuntimeError(
1250
+ f"Tiprack {tiprack_uri} does not match the tiprack designated "
1251
+ f"for this transfer- {tiprack_uri_for_transfer_props}."
1252
+ )
1253
+ self.pick_up_tip(
1254
+ location=tiprack_loc,
1255
+ well_core=tip_well,
1256
+ presses=None,
1257
+ increment=None,
1258
+ )
1259
+ return tip_well
1260
+
1261
+ if new_tip == TransferTipPolicyV2.ONCE:
1262
+ last_tip_picked_up_from = _pick_up_tip()
1263
+
1264
+ prev_src: Optional[Tuple[Location, WellCore]] = None
1265
+ post_disp_tip_contents = [
1266
+ tx_comps_executor.LiquidAndAirGapPair(
1267
+ liquid=0,
1268
+ air_gap=0,
1269
+ )
1270
+ ]
1271
+ next_step_volume, next_src_dest_combo = next(source_dest_per_volume_step)
1272
+ is_last_step = False
1273
+ while not is_last_step:
1274
+ step_volume = next_step_volume
1275
+ src_dest_combo = next_src_dest_combo
1276
+ step_source, step_destination = src_dest_combo
1277
+ try:
1278
+ next_step_volume, next_src_dest_combo = next(
1279
+ source_dest_per_volume_step
1280
+ )
1281
+ except StopIteration:
1282
+ is_last_step = True
1283
+
1284
+ if new_tip == TransferTipPolicyV2.ALWAYS or (
1285
+ new_tip == TransferTipPolicyV2.PER_SOURCE and step_source != prev_src
1286
+ ):
1287
+ if prev_src is not None:
1288
+ _drop_tip()
1289
+ last_tip_picked_up_from = _pick_up_tip()
1290
+ post_disp_tip_contents = [
1291
+ tx_comps_executor.LiquidAndAirGapPair(
1292
+ liquid=0,
1293
+ air_gap=0,
1294
+ )
1295
+ ]
1296
+ # Enable LPD only if all of these apply:
1297
+ # - LPD is globally enabled for this pipette
1298
+ # - it is the first time visiting this well
1299
+ # - pipette tip is unused
1300
+ enable_lpd = (
1301
+ self.get_liquid_presence_detection() and step_source != prev_src
1302
+ )
1303
+ elif new_tip == TransferTipPolicyV2.ONCE:
1304
+ # Enable LPD only if:
1305
+ # - LPD is globally enabled for this pipette
1306
+ # - this is the first source well of the entire transfer, which means
1307
+ # that the current tip is unused
1308
+ enable_lpd = self.get_liquid_presence_detection() and prev_src is None
1309
+ else:
1310
+ enable_lpd = False
1311
+
1312
+ with self.lpd_for_transfer(enable_lpd):
1313
+ post_asp_tip_contents = self.aspirate_liquid_class(
1314
+ volume=step_volume,
1315
+ source=step_source,
1316
+ transfer_properties=transfer_props,
1317
+ transfer_type=tx_comps_executor.TransferType.ONE_TO_ONE,
1318
+ tip_contents=post_disp_tip_contents,
1319
+ volume_for_pipette_mode_configuration=step_volume,
1320
+ )
1321
+ post_disp_tip_contents = self.dispense_liquid_class(
1322
+ volume=step_volume,
1323
+ dest=step_destination,
1324
+ source=step_source,
1325
+ transfer_properties=transfer_props,
1326
+ transfer_type=tx_comps_executor.TransferType.ONE_TO_ONE,
1327
+ tip_contents=post_asp_tip_contents,
1328
+ add_final_air_gap=False
1329
+ if is_last_step and new_tip == TransferTipPolicyV2.NEVER
1330
+ else True,
1331
+ trash_location=trash_location,
1332
+ )
1333
+ prev_src = step_source
1334
+ if new_tip != TransferTipPolicyV2.NEVER:
1335
+ _drop_tip()
1336
+
1337
+ # TODO(spp, 2025-02-25): wire up return tip
1338
+ def distribute_liquid( # noqa: C901
1005
1339
  self,
1006
- liquid_class_id: str,
1340
+ liquid_class: LiquidClass,
1007
1341
  volume: float,
1008
- source: List[WellCore],
1009
- dest: List[WellCore],
1342
+ source: Tuple[Location, WellCore],
1343
+ dest: List[Tuple[Location, WellCore]],
1010
1344
  new_tip: TransferTipPolicyV2,
1011
- trash_location: Union[WellCore, Location, TrashBin, WasteChute],
1345
+ tip_racks: List[Tuple[Location, LabwareCore]],
1346
+ trash_location: Union[Location, TrashBin, WasteChute],
1347
+ return_tip: bool,
1012
1348
  ) -> None:
1013
- """Execute transfer using liquid class properties."""
1349
+ """Execute a distribution using liquid class properties.
1350
+
1351
+ Args:
1352
+ liquid_class: The liquid class to use for transfer properties.
1353
+ volume: The amount of liquid in uL, to dispense into each destination well.
1354
+ source: Source well represented as a tuple of types.Location and WellCore.
1355
+ types.Location is only necessary for saving the last accessed location.
1356
+ dest: List of destination wells, with each well represented as a tuple of
1357
+ types.Location and WellCore.
1358
+ types.Location is only necessary for saving the last accessed location.
1359
+ new_tip: Whether the transfer should use a new tip 'once', 'never', 'always',
1360
+ or 'per source'.
1361
+ tiprack_uri: The URI of the tiprack that the transfer settings are for.
1362
+ tip_drop_location: Location where the tip will be dropped (if appropriate).
1363
+
1364
+ This method distributes the liquid in the source well into multiple destinations.
1365
+ It can accomplish this by either doing a multi-dispense (aspirate once and then
1366
+ dispense multiple times consecutively) or by doing multiple single-dispenses
1367
+ (going back to aspirate after each dispense). Whether it does a multi-dispense or
1368
+ multiple single dispenses is determined by whether multi-dispense properties
1369
+ are available in the liquid class and whether the tip in use can hold multiple
1370
+ volumes to be dispensed whithout having to refill.
1371
+ """
1372
+ if not tip_racks:
1373
+ raise RuntimeError(
1374
+ "No tipracks found for pipette in order to perform transfer"
1375
+ )
1376
+ tiprack_uri_for_transfer_props = tip_racks[0][1].get_uri()
1377
+ working_volume = min(
1378
+ self.get_max_volume(),
1379
+ self._engine_client.state.geometry.get_nominal_tip_geometry(
1380
+ pipette_id=self.pipette_id,
1381
+ labware_id=tip_racks[0][1].labware_id,
1382
+ well_name=None,
1383
+ ).volume,
1384
+ )
1385
+
1386
+ try:
1387
+ transfer_props = liquid_class.get_for(
1388
+ pipette=self.get_pipette_name(), tip_rack=tiprack_uri_for_transfer_props
1389
+ )
1390
+ except NoLiquidClassPropertyError:
1391
+ if self._protocol_core.robot_type == "OT-2 Standard":
1392
+ raise NoLiquidClassPropertyError(
1393
+ "Default liquid classes are not supported with OT-2 pipettes and tip racks."
1394
+ ) from None
1395
+ raise
1396
+
1397
+ # If the volume to dispense into a well is less than threashold for low volume mode,
1398
+ # then set the max working volume to the max volume of low volume mode.
1399
+ # NOTE: this logic will need to be updated once we support list of volumes
1400
+ # TODO (spp): refactor this to use the volume thresholds from shared data
1401
+ has_low_volume_mode = self.get_pipette_name() in [
1402
+ "flex_1channel_50",
1403
+ "flex_8channel_50",
1404
+ ]
1405
+ if has_low_volume_mode and volume < 5:
1406
+ working_volume = 30
1407
+ # If there are no multi-dispense properties or if the volume to distribute
1408
+ # per destination well is so large that the tip cannot hold enough liquid
1409
+ # to consecutively distribute to at least two wells, then we resort to using
1410
+ # a regular, one-to-one transfer to carry out the distribution.
1411
+ min_asp_vol_for_multi_dispense = 2 * volume
1412
+ if transfer_props.multi_dispense is None or (
1413
+ transfer_props.multi_dispense is not None
1414
+ and not self._tip_can_hold_volume_for_multi_dispensing(
1415
+ transfer_volume=min_asp_vol_for_multi_dispense,
1416
+ multi_dispense_properties=transfer_props.multi_dispense,
1417
+ tip_working_volume=working_volume,
1418
+ )
1419
+ ):
1420
+ self.transfer_liquid(
1421
+ liquid_class=liquid_class,
1422
+ volume=volume,
1423
+ source=[source for _ in range(len(dest))],
1424
+ dest=dest,
1425
+ new_tip=new_tip,
1426
+ tip_racks=tip_racks,
1427
+ trash_location=trash_location,
1428
+ return_tip=return_tip,
1429
+ )
1430
+ return
1431
+
1432
+ # TODO: use the ID returned by load_liquid_class in command annotations
1433
+ self.load_liquid_class(
1434
+ name=liquid_class.name,
1435
+ transfer_properties=transfer_props,
1436
+ tiprack_uri=tiprack_uri_for_transfer_props,
1437
+ )
1438
+
1439
+ # This will return a generator that provides pairs of destination well and
1440
+ # the volume to dispense into it
1441
+ dest_per_volume_step = (
1442
+ tx_commons.expand_for_volume_constraints_for_liquid_classes(
1443
+ volumes=[volume for _ in range(len(dest))],
1444
+ targets=dest,
1445
+ max_volume=working_volume,
1446
+ )
1447
+ )
1448
+
1449
+ last_tip_picked_up_from: Optional[WellCore] = None
1450
+
1451
+ def _drop_tip() -> None:
1452
+ if return_tip:
1453
+ assert last_tip_picked_up_from is not None
1454
+ self.drop_tip(
1455
+ location=None,
1456
+ well_core=last_tip_picked_up_from,
1457
+ home_after=False,
1458
+ alternate_drop_location=False,
1459
+ )
1460
+ elif isinstance(trash_location, (TrashBin, WasteChute)):
1461
+ self.drop_tip_in_disposal_location(
1462
+ disposal_location=trash_location,
1463
+ home_after=False,
1464
+ alternate_tip_drop=True,
1465
+ )
1466
+ elif isinstance(trash_location, Location):
1467
+ self.drop_tip(
1468
+ location=trash_location,
1469
+ well_core=trash_location.labware.as_well()._core, # type: ignore[arg-type]
1470
+ home_after=False,
1471
+ alternate_drop_location=True,
1472
+ )
1473
+
1474
+ def _pick_up_tip() -> WellCore:
1475
+ next_tip = self.get_next_tip(
1476
+ tip_racks=[core for loc, core in tip_racks],
1477
+ starting_well=None,
1478
+ )
1479
+ if next_tip is None:
1480
+ raise RuntimeError(
1481
+ f"No tip available among {tip_racks} for this transfer."
1482
+ )
1483
+ (
1484
+ tiprack_loc,
1485
+ tiprack_uri,
1486
+ tip_well,
1487
+ ) = self._get_location_and_well_core_from_next_tip_info(next_tip, tip_racks)
1488
+ if tiprack_uri != tiprack_uri_for_transfer_props:
1489
+ raise RuntimeError(
1490
+ f"Tiprack {tiprack_uri} does not match the tiprack designated "
1491
+ f"for this transfer- {tiprack_uri_for_transfer_props}."
1492
+ )
1493
+ self.pick_up_tip(
1494
+ location=tiprack_loc,
1495
+ well_core=tip_well,
1496
+ presses=None,
1497
+ increment=None,
1498
+ )
1499
+ return tip_well
1500
+
1501
+ tip_used = False
1502
+ if new_tip != TransferTipPolicyV2.NEVER:
1503
+ last_tip_picked_up_from = _pick_up_tip()
1504
+
1505
+ tip_contents = [
1506
+ tx_comps_executor.LiquidAndAirGapPair(
1507
+ liquid=0,
1508
+ air_gap=0,
1509
+ )
1510
+ ]
1511
+ next_step_volume, next_dest = next(dest_per_volume_step)
1512
+ is_last_step = False
1513
+ is_first_step = True
1514
+
1515
+ # This loop will run until the last step has been executed
1516
+ while not is_last_step:
1517
+ total_aspirate_volume = 0.0
1518
+ vol_dest_combo = []
1519
+
1520
+ # This loop looks at the next volumes to dispense and calculates how many
1521
+ # dispense volumes plus their conditioning & disposal volumes can fit into
1522
+ # the tip. It then collects these volumes and their destinations in a list.
1523
+ while not is_last_step and self._tip_can_hold_volume_for_multi_dispensing(
1524
+ transfer_volume=total_aspirate_volume + next_step_volume,
1525
+ multi_dispense_properties=transfer_props.multi_dispense,
1526
+ tip_working_volume=working_volume,
1527
+ ):
1528
+ total_aspirate_volume += next_step_volume
1529
+ vol_dest_combo.append((next_step_volume, next_dest))
1530
+ try:
1531
+ next_step_volume, next_dest = next(dest_per_volume_step)
1532
+ except StopIteration:
1533
+ is_last_step = True
1534
+
1535
+ conditioning_vol = (
1536
+ transfer_props.multi_dispense.conditioning_by_volume.get_for_volume(
1537
+ total_aspirate_volume
1538
+ )
1539
+ )
1540
+ disposal_vol = (
1541
+ transfer_props.multi_dispense.disposal_by_volume.get_for_volume(
1542
+ total_aspirate_volume
1543
+ )
1544
+ )
1545
+
1546
+ if new_tip == TransferTipPolicyV2.ALWAYS and tip_used:
1547
+ _drop_tip()
1548
+ last_tip_picked_up_from = _pick_up_tip()
1549
+ tip_contents = [
1550
+ tx_comps_executor.LiquidAndAirGapPair(
1551
+ liquid=0,
1552
+ air_gap=0,
1553
+ )
1554
+ ]
1555
+
1556
+ use_single_dispense = False
1557
+ if total_aspirate_volume == volume and len(vol_dest_combo) == 1:
1558
+ # We are only doing a single transfer. Either because this is the last
1559
+ # remaining volume to dispense or, once this function accepts a list of
1560
+ # volumes, the next pair of volumes is too large to be multi-dispensed.
1561
+ # So we won't use conditioning volume or disposal volume
1562
+ conditioning_vol = 0
1563
+ disposal_vol = 0
1564
+ use_single_dispense = True
1565
+
1566
+ if (
1567
+ not use_single_dispense
1568
+ and disposal_vol > 0
1569
+ and not transfer_props.multi_dispense.retract.blowout.enabled
1570
+ ):
1571
+ raise RuntimeError(
1572
+ "Distribute liquid uses a disposal volume but location for disposing of"
1573
+ " the disposal volume cannot be found when blowout is disabled."
1574
+ " Specify a blowout location and enable blowout when using a disposal volume."
1575
+ )
1576
+
1577
+ if (
1578
+ self.get_liquid_presence_detection()
1579
+ and new_tip != TransferTipPolicyV2.NEVER
1580
+ and is_first_step
1581
+ ):
1582
+ enable_lpd = True
1583
+ else:
1584
+ enable_lpd = False
1585
+ with self.lpd_for_transfer(enable=enable_lpd):
1586
+ # Aspirate the total volume determined by the loop above
1587
+ tip_contents = self.aspirate_liquid_class(
1588
+ volume=total_aspirate_volume + conditioning_vol + disposal_vol,
1589
+ source=source,
1590
+ transfer_properties=transfer_props,
1591
+ transfer_type=tx_comps_executor.TransferType.ONE_TO_MANY,
1592
+ tip_contents=tip_contents,
1593
+ # We configure the mode based on the last dispense volume and disposal volume
1594
+ # since the mode is only used to determine the dispense push out volume
1595
+ # and we can do a push out only at the last dispense, that too if there is no disposal volume.
1596
+ volume_for_pipette_mode_configuration=vol_dest_combo[-1][0],
1597
+ conditioning_volume=conditioning_vol,
1598
+ )
1599
+
1600
+ # If the tip has volumes correspoinding to multiple destinations, then
1601
+ # multi-dispense in those destinations.
1602
+ # If the tip has a volume corresponding to a single destination, then
1603
+ # do a single-dispense into that destination.
1604
+ for next_vol, next_dest in vol_dest_combo:
1605
+ if use_single_dispense:
1606
+ tip_contents = self.dispense_liquid_class(
1607
+ volume=next_vol,
1608
+ dest=next_dest,
1609
+ source=source,
1610
+ transfer_properties=transfer_props,
1611
+ transfer_type=tx_comps_executor.TransferType.ONE_TO_MANY,
1612
+ tip_contents=tip_contents,
1613
+ add_final_air_gap=False
1614
+ if is_last_step and new_tip == TransferTipPolicyV2.NEVER
1615
+ else True,
1616
+ trash_location=trash_location,
1617
+ )
1618
+ else:
1619
+ tip_contents = self.dispense_liquid_class_during_multi_dispense(
1620
+ volume=next_vol,
1621
+ dest=next_dest,
1622
+ source=source,
1623
+ transfer_properties=transfer_props,
1624
+ transfer_type=tx_comps_executor.TransferType.ONE_TO_MANY,
1625
+ tip_contents=tip_contents,
1626
+ add_final_air_gap=False
1627
+ if is_last_step and new_tip == TransferTipPolicyV2.NEVER
1628
+ else True,
1629
+ trash_location=trash_location,
1630
+ conditioning_volume=conditioning_vol,
1631
+ disposal_volume=disposal_vol,
1632
+ )
1633
+ is_first_step = False
1634
+ tip_used = True
1635
+
1636
+ if new_tip != TransferTipPolicyV2.NEVER:
1637
+ _drop_tip()
1638
+
1639
+ def _tip_can_hold_volume_for_multi_dispensing(
1640
+ self,
1641
+ transfer_volume: float,
1642
+ multi_dispense_properties: MultiDispenseProperties,
1643
+ tip_working_volume: float,
1644
+ ) -> bool:
1645
+ """
1646
+ Whether the tip can hold the volume plus the conditioning and disposal volumes
1647
+ required for multi-dispensing.
1648
+ """
1649
+ return (
1650
+ transfer_volume
1651
+ + multi_dispense_properties.conditioning_by_volume.get_for_volume(
1652
+ transfer_volume
1653
+ )
1654
+ + multi_dispense_properties.disposal_by_volume.get_for_volume(
1655
+ transfer_volume
1656
+ )
1657
+ <= tip_working_volume
1658
+ )
1659
+
1660
+ def consolidate_liquid( # noqa: C901
1661
+ self,
1662
+ liquid_class: LiquidClass,
1663
+ volume: float,
1664
+ source: List[Tuple[Location, WellCore]],
1665
+ dest: Tuple[Location, WellCore],
1666
+ new_tip: TransferTipPolicyV2,
1667
+ tip_racks: List[Tuple[Location, LabwareCore]],
1668
+ trash_location: Union[Location, TrashBin, WasteChute],
1669
+ return_tip: bool,
1670
+ ) -> None:
1671
+ if not tip_racks:
1672
+ raise RuntimeError(
1673
+ "No tipracks found for pipette in order to perform transfer"
1674
+ )
1675
+
1676
+ tiprack_uri_for_transfer_props = tip_racks[0][1].get_uri()
1677
+ try:
1678
+ transfer_props = liquid_class.get_for(
1679
+ pipette=self.get_pipette_name(), tip_rack=tiprack_uri_for_transfer_props
1680
+ )
1681
+ except NoLiquidClassPropertyError:
1682
+ if self._protocol_core.robot_type == "OT-2 Standard":
1683
+ raise NoLiquidClassPropertyError(
1684
+ "Default liquid classes are not supported with OT-2 pipettes and tip racks."
1685
+ ) from None
1686
+ raise
1687
+
1688
+ blow_out_properties = transfer_props.dispense.retract.blowout
1689
+ if (
1690
+ blow_out_properties.enabled
1691
+ and blow_out_properties.location == BlowoutLocation.SOURCE
1692
+ ):
1693
+ raise RuntimeError(
1694
+ 'Blowout location "source" incompatible with consolidate liquid.'
1695
+ ' Please choose "destination" or "trash".'
1696
+ )
1697
+
1698
+ # TODO: use the ID returned by load_liquid_class in command annotations
1699
+ self.load_liquid_class(
1700
+ name=liquid_class.name,
1701
+ transfer_properties=transfer_props,
1702
+ tiprack_uri=tiprack_uri_for_transfer_props,
1703
+ )
1704
+
1705
+ max_volume = min(
1706
+ self.get_max_volume(),
1707
+ self._engine_client.state.geometry.get_nominal_tip_geometry(
1708
+ pipette_id=self.pipette_id,
1709
+ labware_id=tip_racks[0][1].labware_id,
1710
+ well_name=None,
1711
+ ).volume,
1712
+ )
1713
+
1714
+ source_per_volume_step = (
1715
+ tx_commons.expand_for_volume_constraints_for_liquid_classes(
1716
+ volumes=[volume for _ in range(len(source))],
1717
+ targets=source,
1718
+ max_volume=max_volume,
1719
+ )
1720
+ )
1721
+
1722
+ last_tip_picked_up_from: Optional[WellCore] = None
1723
+
1724
+ def _drop_tip() -> None:
1725
+ if return_tip:
1726
+ assert last_tip_picked_up_from is not None
1727
+ self.drop_tip(
1728
+ location=None,
1729
+ well_core=last_tip_picked_up_from,
1730
+ home_after=False,
1731
+ alternate_drop_location=False,
1732
+ )
1733
+ elif isinstance(trash_location, (TrashBin, WasteChute)):
1734
+ self.drop_tip_in_disposal_location(
1735
+ disposal_location=trash_location,
1736
+ home_after=False,
1737
+ alternate_tip_drop=True,
1738
+ )
1739
+ elif isinstance(trash_location, Location):
1740
+ self.drop_tip(
1741
+ location=trash_location,
1742
+ well_core=trash_location.labware.as_well()._core, # type: ignore[arg-type]
1743
+ home_after=False,
1744
+ alternate_drop_location=True,
1745
+ )
1746
+
1747
+ def _pick_up_tip() -> WellCore:
1748
+ next_tip = self.get_next_tip(
1749
+ tip_racks=[core for loc, core in tip_racks],
1750
+ starting_well=None,
1751
+ )
1752
+ if next_tip is None:
1753
+ raise RuntimeError(
1754
+ f"No tip available among {tip_racks} for this transfer."
1755
+ )
1756
+ (
1757
+ tiprack_loc,
1758
+ tiprack_uri,
1759
+ tip_well,
1760
+ ) = self._get_location_and_well_core_from_next_tip_info(next_tip, tip_racks)
1761
+ if tiprack_uri != tiprack_uri_for_transfer_props:
1762
+ raise RuntimeError(
1763
+ f"Tiprack {tiprack_uri} does not match the tiprack designated "
1764
+ f"for this transfer- {tiprack_uri_for_transfer_props}."
1765
+ )
1766
+ self.pick_up_tip(
1767
+ location=tiprack_loc,
1768
+ well_core=tip_well,
1769
+ presses=None,
1770
+ increment=None,
1771
+ )
1772
+ return tip_well
1773
+
1774
+ if new_tip == TransferTipPolicyV2.ONCE:
1775
+ last_tip_picked_up_from = _pick_up_tip()
1776
+
1777
+ prev_src: Optional[Tuple[Location, WellCore]] = None
1778
+ tip_contents = [
1779
+ tx_comps_executor.LiquidAndAirGapPair(
1780
+ liquid=0,
1781
+ air_gap=0,
1782
+ )
1783
+ ]
1784
+ next_step_volume, next_source = next(source_per_volume_step)
1785
+ is_last_step = False
1786
+ while not is_last_step:
1787
+ total_dispense_volume = 0.0
1788
+ vol_aspirate_combo = []
1789
+ # Take air gap into account because there will be a final air gap before the dispense
1790
+ while total_dispense_volume + next_step_volume <= max_volume:
1791
+ total_dispense_volume += next_step_volume
1792
+ vol_aspirate_combo.append((next_step_volume, next_source))
1793
+ try:
1794
+ next_step_volume, next_source = next(source_per_volume_step)
1795
+ except StopIteration:
1796
+ is_last_step = True
1797
+ break
1798
+
1799
+ if new_tip == TransferTipPolicyV2.ALWAYS:
1800
+ if prev_src is not None:
1801
+ _drop_tip()
1802
+ last_tip_picked_up_from = _pick_up_tip()
1803
+ tip_contents = [
1804
+ tx_comps_executor.LiquidAndAirGapPair(
1805
+ liquid=0,
1806
+ air_gap=0,
1807
+ )
1808
+ ]
1809
+ # TODO (spp, 2025-03-24): add LPD feature when 'per source' tip policy is added for consolidate_liquid
1810
+ with self.lpd_for_transfer(enable=False):
1811
+ for step_num, (step_volume, step_source) in enumerate(
1812
+ vol_aspirate_combo
1813
+ ):
1814
+ tip_contents = self.aspirate_liquid_class(
1815
+ volume=step_volume,
1816
+ source=step_source,
1817
+ transfer_properties=transfer_props,
1818
+ transfer_type=tx_comps_executor.TransferType.MANY_TO_ONE,
1819
+ tip_contents=tip_contents,
1820
+ volume_for_pipette_mode_configuration=total_dispense_volume
1821
+ if step_num == 0
1822
+ else None,
1823
+ )
1824
+ tip_contents = self.dispense_liquid_class(
1825
+ volume=total_dispense_volume,
1826
+ dest=dest,
1827
+ source=None, # Cannot have source as location for blowout so hardcoded to None
1828
+ transfer_properties=transfer_props,
1829
+ transfer_type=tx_comps_executor.TransferType.MANY_TO_ONE,
1830
+ tip_contents=tip_contents,
1831
+ add_final_air_gap=False
1832
+ if is_last_step and new_tip == TransferTipPolicyV2.NEVER
1833
+ else True,
1834
+ trash_location=trash_location,
1835
+ )
1836
+ prev_src = next_source
1837
+ if new_tip != TransferTipPolicyV2.NEVER:
1838
+ _drop_tip()
1839
+
1840
+ def _get_location_and_well_core_from_next_tip_info(
1841
+ self,
1842
+ tip_info: NextTipInfo,
1843
+ tip_racks: List[Tuple[Location, LabwareCore]],
1844
+ ) -> _TipInfo:
1845
+ tiprack_labware_core = self._protocol_core._labware_cores_by_id[
1846
+ tip_info.labwareId
1847
+ ]
1848
+ tip_well = tiprack_labware_core.get_well_core(tip_info.tipStartingWell)
1849
+
1850
+ tiprack_loc = [
1851
+ loc for loc, lw_core in tip_racks if lw_core == tiprack_labware_core
1852
+ ]
1853
+
1854
+ return _TipInfo(
1855
+ Location(tip_well.get_top(0), tiprack_loc[0].labware),
1856
+ tiprack_labware_core.get_uri(),
1857
+ tip_well,
1858
+ )
1859
+
1860
+ def aspirate_liquid_class(
1861
+ self,
1862
+ volume: float,
1863
+ source: Tuple[Location, WellCore],
1864
+ transfer_properties: TransferProperties,
1865
+ transfer_type: tx_comps_executor.TransferType,
1866
+ tip_contents: List[tx_comps_executor.LiquidAndAirGapPair],
1867
+ volume_for_pipette_mode_configuration: Optional[float],
1868
+ conditioning_volume: Optional[float] = None,
1869
+ ) -> List[tx_comps_executor.LiquidAndAirGapPair]:
1870
+ """Execute aspiration steps.
1871
+
1872
+ 1. Submerge
1873
+ 2. Mix
1874
+ 3. pre-wet
1875
+ 4. Aspirate
1876
+ 5. Delay- wait inside the liquid
1877
+ 6. Aspirate retract
1878
+
1879
+ Return: List of liquid and air gap pairs in tip.
1880
+ """
1881
+ aspirate_props = transfer_properties.aspirate
1882
+ tx_commons.check_valid_liquid_class_volume_parameters(
1883
+ aspirate_volume=volume,
1884
+ air_gap=aspirate_props.retract.air_gap_by_volume.get_for_volume(volume)
1885
+ if conditioning_volume is None
1886
+ else 0,
1887
+ disposal_volume=0, # Disposal volume is accounted for in aspirate vol
1888
+ max_volume=self.get_working_volume(),
1889
+ )
1890
+ source_loc, source_well = source
1891
+ aspirate_point = (
1892
+ tx_comps_executor.absolute_point_from_position_reference_and_offset(
1893
+ well=source_well,
1894
+ position_reference=aspirate_props.position_reference,
1895
+ offset=aspirate_props.offset,
1896
+ )
1897
+ )
1898
+ aspirate_location = Location(aspirate_point, labware=source_loc.labware)
1899
+ last_liquid_and_airgap_in_tip = (
1900
+ tip_contents[-1]
1901
+ if tip_contents
1902
+ else tx_comps_executor.LiquidAndAirGapPair(
1903
+ liquid=0,
1904
+ air_gap=0,
1905
+ )
1906
+ )
1907
+ components_executor = tx_comps_executor.TransferComponentsExecutor(
1908
+ instrument_core=self,
1909
+ transfer_properties=transfer_properties,
1910
+ target_location=aspirate_location,
1911
+ target_well=source_well,
1912
+ transfer_type=transfer_type,
1913
+ tip_state=tx_comps_executor.TipState(
1914
+ last_liquid_and_air_gap_in_tip=last_liquid_and_airgap_in_tip
1915
+ ),
1916
+ )
1917
+ components_executor.submerge(
1918
+ submerge_properties=aspirate_props.submerge,
1919
+ post_submerge_action="aspirate",
1920
+ volume_for_pipette_mode_configuration=volume_for_pipette_mode_configuration,
1921
+ )
1922
+ # Do not do a pre-aspirate mix or pre-wet if consolidating
1923
+ if transfer_type != tx_comps_executor.TransferType.MANY_TO_ONE:
1924
+ # TODO: check if we want to do a mix only once when we're splitting a transfer
1925
+ # and coming back to the source multiple times.
1926
+ # We will have to do pre-wet always even for split volumes
1927
+ components_executor.mix(
1928
+ mix_properties=aspirate_props.mix, last_dispense_push_out=False
1929
+ )
1930
+ # TODO: check if pre-wet needs to be enabled for first well of consolidate
1931
+ components_executor.pre_wet(
1932
+ volume=volume,
1933
+ )
1934
+ components_executor.aspirate_and_wait(volume=volume)
1935
+ if (
1936
+ transfer_type == tx_comps_executor.TransferType.ONE_TO_MANY
1937
+ and conditioning_volume not in [None, 0.0]
1938
+ and transfer_properties.multi_dispense is not None
1939
+ ):
1940
+ # Dispense the conditioning volume
1941
+ components_executor.dispense_and_wait(
1942
+ dispense_properties=transfer_properties.multi_dispense,
1943
+ volume=conditioning_volume or 0.0,
1944
+ push_out_override=0,
1945
+ )
1946
+ components_executor.retract_after_aspiration(
1947
+ volume=volume, add_air_gap=False
1948
+ )
1949
+ else:
1950
+ components_executor.retract_after_aspiration(
1951
+ volume=volume, add_air_gap=True
1952
+ )
1953
+
1954
+ # return copy of tip_contents with last entry replaced by tip state from executor
1955
+ last_contents = components_executor.tip_state.last_liquid_and_air_gap_in_tip
1956
+ new_tip_contents = tip_contents[0:-1] + [last_contents]
1957
+ return new_tip_contents
1958
+
1959
+ def dispense_liquid_class(
1960
+ self,
1961
+ volume: float,
1962
+ dest: Tuple[Location, WellCore],
1963
+ source: Optional[Tuple[Location, WellCore]],
1964
+ transfer_properties: TransferProperties,
1965
+ transfer_type: tx_comps_executor.TransferType,
1966
+ tip_contents: List[tx_comps_executor.LiquidAndAirGapPair],
1967
+ add_final_air_gap: bool,
1968
+ trash_location: Union[Location, TrashBin, WasteChute],
1969
+ ) -> List[tx_comps_executor.LiquidAndAirGapPair]:
1970
+ """Execute single-dispense steps.
1971
+ 1. Move pipette to the ‘submerge’ position with normal speed.
1972
+ - The pipette will move in an arc- move to max z height of labware
1973
+ (if asp & disp are in same labware)
1974
+ or max z height of all labware (if asp & disp are in separate labware)
1975
+ 2. Air gap removal:
1976
+ - If dispense location is above the meniscus, DO NOT remove air gap
1977
+ (it will be dispensed along with rest of the liquid later).
1978
+ All other scenarios, remove the air gap by doing a dispense
1979
+ - Flow rate = min(dispenseFlowRate, (airGapByVolume)/sec)
1980
+ - Use the post-dispense delay
1981
+ 4. Move to the dispense position at the specified ‘submerge’ speed
1982
+ (even if we might not be moving into the liquid)
1983
+ - Do a delay (submerge delay)
1984
+ 6. Dispense:
1985
+ - Dispense at the specified flow rate.
1986
+ - Do a push out as specified ONLY IF there is no mix following the dispense AND the tip is empty.
1987
+ Volume for push out is the volume being dispensed. So if we are dispensing 50uL, use pushOutByVolume[50] as push out volume.
1988
+ 7. Delay
1989
+ 8. Mix using the same flow rate and delays as specified for asp+disp,
1990
+ with the volume and the number of repetitions specified. Use the delays in asp & disp.
1991
+ - If the dispense position is outside the liquid, then raise error if mix is enabled.
1992
+ Can only be checked if using liquid level detection/ meniscus-based positioning.
1993
+ - If the user wants to perform a mix then they should specify a dispense position that’s inside the liquid OR do mix() on the wells after transfer.
1994
+ - Do push out at the last dispense.
1995
+ 9. Retract
1996
+
1997
+ Return:
1998
+ List of liquid and air gap pairs in tip.
1999
+ """
2000
+ dispense_props = transfer_properties.dispense
2001
+ dest_loc, dest_well = dest
2002
+ dispense_point = (
2003
+ tx_comps_executor.absolute_point_from_position_reference_and_offset(
2004
+ well=dest_well,
2005
+ position_reference=dispense_props.position_reference,
2006
+ offset=dispense_props.offset,
2007
+ )
2008
+ )
2009
+ dispense_location = Location(dispense_point, labware=dest_loc.labware)
2010
+ last_liquid_and_airgap_in_tip = (
2011
+ tip_contents[-1]
2012
+ if tip_contents
2013
+ else tx_comps_executor.LiquidAndAirGapPair(
2014
+ liquid=0,
2015
+ air_gap=0,
2016
+ )
2017
+ )
2018
+ components_executor = tx_comps_executor.TransferComponentsExecutor(
2019
+ instrument_core=self,
2020
+ transfer_properties=transfer_properties,
2021
+ target_location=dispense_location,
2022
+ target_well=dest_well,
2023
+ transfer_type=transfer_type,
2024
+ tip_state=tx_comps_executor.TipState(
2025
+ last_liquid_and_air_gap_in_tip=last_liquid_and_airgap_in_tip
2026
+ ),
2027
+ )
2028
+ components_executor.submerge(
2029
+ submerge_properties=dispense_props.submerge,
2030
+ post_submerge_action="dispense",
2031
+ volume_for_pipette_mode_configuration=None,
2032
+ )
2033
+ push_out_vol = (
2034
+ 0.0
2035
+ if dispense_props.mix.enabled
2036
+ else dispense_props.push_out_by_volume.get_for_volume(volume)
2037
+ )
2038
+ components_executor.dispense_and_wait(
2039
+ dispense_properties=dispense_props,
2040
+ volume=volume,
2041
+ push_out_override=push_out_vol,
2042
+ )
2043
+ components_executor.mix(
2044
+ mix_properties=dispense_props.mix,
2045
+ last_dispense_push_out=True,
2046
+ )
2047
+ components_executor.retract_after_dispensing(
2048
+ trash_location=trash_location,
2049
+ source_location=source[0] if source else None,
2050
+ source_well=source[1] if source else None,
2051
+ add_final_air_gap=add_final_air_gap,
2052
+ )
2053
+ last_contents = components_executor.tip_state.last_liquid_and_air_gap_in_tip
2054
+ new_tip_contents = tip_contents[0:-1] + [last_contents]
2055
+ return new_tip_contents
2056
+
2057
+ def dispense_liquid_class_during_multi_dispense(
2058
+ self,
2059
+ volume: float,
2060
+ dest: Tuple[Location, WellCore],
2061
+ source: Optional[Tuple[Location, WellCore]],
2062
+ transfer_properties: TransferProperties,
2063
+ transfer_type: tx_comps_executor.TransferType,
2064
+ tip_contents: List[tx_comps_executor.LiquidAndAirGapPair],
2065
+ add_final_air_gap: bool,
2066
+ trash_location: Union[Location, TrashBin, WasteChute],
2067
+ conditioning_volume: float,
2068
+ disposal_volume: float,
2069
+ ) -> List[tx_comps_executor.LiquidAndAirGapPair]:
2070
+ """Execute a dispense step that's part of a multi-dispense.
2071
+
2072
+ This executes a dispense step very similar to a single dispense except that:
2073
+ - it uses the multi-dispense properties from the liquid class
2074
+ - handles push-out based on disposal volume in addition to the existing conditions
2075
+ - delegates the retraction steps to a different, multi-dispense retract function
2076
+
2077
+ Return:
2078
+ List of liquid and air gap pairs in tip.
2079
+ """
2080
+ assert transfer_properties.multi_dispense is not None
2081
+ dispense_props = transfer_properties.multi_dispense
2082
+
2083
+ dest_loc, dest_well = dest
2084
+ dispense_point = (
2085
+ tx_comps_executor.absolute_point_from_position_reference_and_offset(
2086
+ well=dest_well,
2087
+ position_reference=dispense_props.position_reference,
2088
+ offset=dispense_props.offset,
2089
+ )
2090
+ )
2091
+ dispense_location = Location(dispense_point, labware=dest_loc.labware)
2092
+ last_liquid_and_airgap_in_tip = (
2093
+ tip_contents[-1]
2094
+ if tip_contents
2095
+ else tx_comps_executor.LiquidAndAirGapPair(
2096
+ liquid=0,
2097
+ air_gap=0,
2098
+ )
2099
+ )
2100
+ components_executor = tx_comps_executor.TransferComponentsExecutor(
2101
+ instrument_core=self,
2102
+ transfer_properties=transfer_properties,
2103
+ target_location=dispense_location,
2104
+ target_well=dest_well,
2105
+ transfer_type=transfer_type,
2106
+ tip_state=tx_comps_executor.TipState(
2107
+ last_liquid_and_air_gap_in_tip=last_liquid_and_airgap_in_tip
2108
+ ),
2109
+ )
2110
+ components_executor.submerge(
2111
+ submerge_properties=dispense_props.submerge,
2112
+ post_submerge_action="dispense",
2113
+ volume_for_pipette_mode_configuration=None,
2114
+ )
2115
+ tip_starting_volume = self.get_current_volume()
2116
+ is_last_dispense_without_disposal_vol = (
2117
+ disposal_volume == 0 and tip_starting_volume == volume
2118
+ )
2119
+ push_out_vol = (
2120
+ # TODO (spp): verify if it's okay to use push_out_by_volume of single dispense
2121
+ transfer_properties.dispense.push_out_by_volume.get_for_volume(volume)
2122
+ if is_last_dispense_without_disposal_vol
2123
+ else 0.0
2124
+ )
2125
+
2126
+ components_executor.dispense_and_wait(
2127
+ dispense_properties=dispense_props,
2128
+ volume=volume,
2129
+ push_out_override=push_out_vol,
2130
+ )
2131
+ components_executor.retract_during_multi_dispensing(
2132
+ trash_location=trash_location,
2133
+ source_location=source[0] if source else None,
2134
+ source_well=source[1] if source else None,
2135
+ conditioning_volume=conditioning_volume,
2136
+ add_final_air_gap=add_final_air_gap,
2137
+ is_last_retract=tip_starting_volume - volume == disposal_volume,
2138
+ )
2139
+ last_contents = components_executor.tip_state.last_liquid_and_air_gap_in_tip
2140
+ new_tip_contents = tip_contents[0:-1] + [last_contents]
2141
+ return new_tip_contents
1014
2142
 
1015
2143
  def retract(self) -> None:
1016
2144
  """Retract this instrument to the top of the gantry."""
@@ -1060,11 +2188,33 @@ class InstrumentCore(AbstractInstrument[WellCore]):
1060
2188
 
1061
2189
  return result.z_position is not None
1062
2190
 
2191
+ def get_minimum_liquid_sense_height(self) -> float:
2192
+ attached_tip = self._engine_client.state.pipettes.get_attached_tip(
2193
+ self._pipette_id
2194
+ )
2195
+ if attached_tip:
2196
+ tip_volume = attached_tip.volume
2197
+ else:
2198
+ raise TipNotAttachedError(
2199
+ "Need to have a tip attached for liquid-sense operations."
2200
+ )
2201
+ lld_settings = self._engine_client.state.pipettes.get_pipette_lld_settings(
2202
+ pipette_id=self.pipette_id
2203
+ )
2204
+ if lld_settings:
2205
+ lld_min_height_for_tip_attached = lld_settings[f"t{tip_volume}"][
2206
+ "minHeight"
2207
+ ]
2208
+ return lld_min_height_for_tip_attached
2209
+ else:
2210
+ raise ValueError("liquid-level detection settings not found.")
2211
+
1063
2212
  def liquid_probe_with_recovery(self, well_core: WellCore, loc: Location) -> None:
1064
2213
  labware_id = well_core.labware_id
1065
2214
  well_name = well_core.get_name()
2215
+ offset = LIQUID_PROBE_START_OFFSET_FROM_WELL_TOP
1066
2216
  well_location = WellLocation(
1067
- origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=2)
2217
+ origin=WellOrigin.TOP, offset=WellOffset(x=offset.x, y=offset.y, z=offset.z)
1068
2218
  )
1069
2219
  self._engine_client.execute_command(
1070
2220
  cmd.LiquidProbeParams(
@@ -1077,9 +2227,10 @@ class InstrumentCore(AbstractInstrument[WellCore]):
1077
2227
 
1078
2228
  self._protocol_core.set_last_location(location=loc, mount=self.get_mount())
1079
2229
 
2230
+ # TODO(cm, 3.4.25): decide whether to allow users to try and do math on a potential SimulatedProbeResult
1080
2231
  def liquid_probe_without_recovery(
1081
2232
  self, well_core: WellCore, loc: Location
1082
- ) -> float:
2233
+ ) -> LiquidTrackingType:
1083
2234
  labware_id = well_core.labware_id
1084
2235
  well_name = well_core.get_name()
1085
2236
  well_location = WellLocation(
@@ -1095,7 +2246,6 @@ class InstrumentCore(AbstractInstrument[WellCore]):
1095
2246
  )
1096
2247
 
1097
2248
  self._protocol_core.set_last_location(location=loc, mount=self.get_mount())
1098
-
1099
2249
  return result.z_position
1100
2250
 
1101
2251
  def nozzle_configuration_valid_for_lld(self) -> bool:
@@ -1103,3 +2253,21 @@ class InstrumentCore(AbstractInstrument[WellCore]):
1103
2253
  return self._engine_client.state.pipettes.get_nozzle_configuration_supports_lld(
1104
2254
  self.pipette_id
1105
2255
  )
2256
+
2257
+ def delay(self, seconds: float) -> None:
2258
+ """Call a protocol delay."""
2259
+ self._protocol_core.delay(seconds=seconds, msg=None)
2260
+
2261
+ @contextmanager
2262
+ def lpd_for_transfer(self, enable: bool) -> Generator[None, None, None]:
2263
+ """Context manager for the instrument's LPD state during a transfer."""
2264
+ global_lpd_enabled = self.get_liquid_presence_detection()
2265
+ self.set_liquid_presence_detection(enable=enable)
2266
+ yield
2267
+ self.set_liquid_presence_detection(enable=global_lpd_enabled)
2268
+
2269
+
2270
+ class _TipInfo(NamedTuple):
2271
+ tiprack_location: Location
2272
+ tiprack_uri: str
2273
+ tip_well: WellCore