opentrons 8.3.2__py2.py3-none-any.whl → 8.4.0__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.

Potentially problematic release.


This version of opentrons might be problematic. Click here for more details.

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