opentrons 8.3.2a0__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.2a0.dist-info → opentrons-8.4.0.dist-info}/METADATA +4 -4
  188. {opentrons-8.3.2a0.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.2a0.dist-info → opentrons-8.4.0.dist-info}/LICENSE +0 -0
  194. {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/WHEEL +0 -0
  195. {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/entry_points.txt +0 -0
  196. {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,5 @@
1
1
  """Pipetting command handling."""
2
- from typing import Optional, Iterator
2
+ from typing import Optional, Iterator, Tuple
3
3
  from typing_extensions import Protocol as TypingProtocol
4
4
  from contextlib import contextmanager
5
5
 
@@ -13,8 +13,13 @@ from ..errors.exceptions import (
13
13
  InvalidAspirateVolumeError,
14
14
  InvalidPushOutVolumeError,
15
15
  InvalidDispenseVolumeError,
16
+ InvalidLiquidHeightFound,
16
17
  )
17
18
  from opentrons.protocol_engine.types import WellLocation
19
+ from opentrons.protocol_engine.types.liquid_level_detection import (
20
+ SimulatedProbeResult,
21
+ LiquidTrackingType,
22
+ )
18
23
 
19
24
  # 1e-9 µL (1 femtoliter!) is a good value because:
20
25
  # * It's large relative to rounding errors that occur in practice in protocols. For
@@ -30,6 +35,9 @@ _VOLUME_ROUNDING_ERROR_TOLERANCE = 1e-9
30
35
  class PipettingHandler(TypingProtocol):
31
36
  """Liquid handling commands."""
32
37
 
38
+ def get_state_view(self) -> StateView:
39
+ """Get the stateview associated with this handler."""
40
+
33
41
  def get_is_ready_to_aspirate(self, pipette_id: str) -> bool:
34
42
  """Get whether a pipette is ready to aspirate."""
35
43
 
@@ -42,15 +50,41 @@ class PipettingHandler(TypingProtocol):
42
50
  volume: float,
43
51
  flow_rate: float,
44
52
  command_note_adder: CommandNoteAdder,
53
+ correction_volume: float = 0.0,
45
54
  ) -> float:
46
55
  """Set flow-rate and aspirate."""
47
56
 
57
+ async def aspirate_while_tracking(
58
+ self,
59
+ pipette_id: str,
60
+ labware_id: str,
61
+ well_name: str,
62
+ volume: float,
63
+ flow_rate: float,
64
+ command_note_adder: CommandNoteAdder,
65
+ ) -> float:
66
+ """Set flow-rate and aspirate while tracking."""
67
+
68
+ async def dispense_while_tracking(
69
+ self,
70
+ pipette_id: str,
71
+ labware_id: str,
72
+ well_name: str,
73
+ volume: float,
74
+ flow_rate: float,
75
+ push_out: Optional[float],
76
+ is_full_dispense: bool = False,
77
+ ) -> float:
78
+ """Set flow-rate and dispense while tracking."""
79
+
48
80
  async def dispense_in_place(
49
81
  self,
50
82
  pipette_id: str,
51
83
  volume: float,
52
84
  flow_rate: float,
53
85
  push_out: Optional[float],
86
+ is_full_dispense: bool,
87
+ correction_volume: float = 0.0,
54
88
  ) -> float:
55
89
  """Set flow-rate and dispense."""
56
90
 
@@ -67,18 +101,25 @@ class PipettingHandler(TypingProtocol):
67
101
  labware_id: str,
68
102
  well_name: str,
69
103
  well_location: WellLocation,
70
- ) -> float:
104
+ ) -> LiquidTrackingType:
71
105
  """Detect liquid level."""
72
106
 
107
+ async def increase_evo_disp_count(self, pipette_id: str) -> None:
108
+ """Increase evo tip dispense action count."""
109
+
73
110
 
74
111
  class HardwarePipettingHandler(PipettingHandler):
75
- """Liquid handling, using the Hardware API.""" ""
112
+ """Liquid handling, using the Hardware API."""
76
113
 
77
114
  def __init__(self, state_view: StateView, hardware_api: HardwareControlAPI) -> None:
78
115
  """Initialize a PipettingHandler instance."""
79
116
  self._state_view = state_view
80
117
  self._hardware_api = hardware_api
81
118
 
119
+ def get_state_view(self) -> StateView:
120
+ """Get the stateview associated with this handler."""
121
+ return self._state_view
122
+
82
123
  def get_is_ready_to_aspirate(self, pipette_id: str) -> bool:
83
124
  """Get whether a pipette is ready to aspirate."""
84
125
  hw_pipette = self._state_view.pipettes.get_hardware_pipette(
@@ -88,6 +129,7 @@ class HardwarePipettingHandler(PipettingHandler):
88
129
  return (
89
130
  self._state_view.pipettes.get_aspirated_volume(pipette_id) is not None
90
131
  and hw_pipette.config["ready_to_aspirate"]
132
+ and self._state_view.pipettes.get_ready_to_aspirate(pipette_id)
91
133
  )
92
134
 
93
135
  async def prepare_for_aspirate(self, pipette_id: str) -> None:
@@ -99,10 +141,48 @@ class HardwarePipettingHandler(PipettingHandler):
99
141
  hw_mount = self._state_view.pipettes.get_mount(pipette_id).to_hw_mount()
100
142
  await self._hardware_api.prepare_for_aspirate(mount=hw_mount)
101
143
 
102
- async def aspirate_in_place(
144
+ def get_hw_aspirate_params(
145
+ self,
146
+ pipette_id: str,
147
+ volume: float,
148
+ command_note_adder: CommandNoteAdder,
149
+ ) -> Tuple[HardwarePipette, float]:
150
+ """Get params for hardware aspirate."""
151
+ _adjusted_volume = _validate_aspirate_volume(
152
+ state_view=self._state_view,
153
+ pipette_id=pipette_id,
154
+ aspirate_volume=volume,
155
+ command_note_adder=command_note_adder,
156
+ )
157
+ _hw_pipette = self._state_view.pipettes.get_hardware_pipette(
158
+ pipette_id=pipette_id,
159
+ attached_pipettes=self._hardware_api.attached_instruments,
160
+ )
161
+ return _hw_pipette, _adjusted_volume
162
+
163
+ def get_hw_dispense_params(
103
164
  self,
104
165
  pipette_id: str,
105
166
  volume: float,
167
+ ) -> Tuple[HardwarePipette, float]:
168
+ """Get params for hardware dispense."""
169
+ _adjusted_volume = _validate_dispense_volume(
170
+ state_view=self._state_view,
171
+ pipette_id=pipette_id,
172
+ dispense_volume=volume,
173
+ )
174
+ _hw_pipette = self._state_view.pipettes.get_hardware_pipette(
175
+ pipette_id=pipette_id,
176
+ attached_pipettes=self._hardware_api.attached_instruments,
177
+ )
178
+ return _hw_pipette, _adjusted_volume
179
+
180
+ async def aspirate_while_tracking(
181
+ self,
182
+ pipette_id: str,
183
+ labware_id: str,
184
+ well_name: str,
185
+ volume: float,
106
186
  flow_rate: float,
107
187
  command_note_adder: CommandNoteAdder,
108
188
  ) -> float:
@@ -112,19 +192,88 @@ class HardwarePipettingHandler(PipettingHandler):
112
192
  PipetteOverpressureError, propagated as-is from the hardware controller.
113
193
  """
114
194
  # get mount and config data from state and hardware controller
115
- adjusted_volume = _validate_aspirate_volume(
116
- state_view=self._state_view,
195
+ hw_pipette, adjusted_volume = self.get_hw_aspirate_params(
196
+ pipette_id, volume, command_note_adder
197
+ )
198
+ aspirate_z_distance = self._state_view.geometry.get_liquid_handling_z_change(
199
+ labware_id=labware_id,
200
+ well_name=well_name,
201
+ operation_volume=volume * -1,
117
202
  pipette_id=pipette_id,
118
- aspirate_volume=volume,
119
- command_note_adder=command_note_adder,
120
203
  )
121
- hw_pipette = self._state_view.pipettes.get_hardware_pipette(
204
+ if isinstance(aspirate_z_distance, SimulatedProbeResult):
205
+ raise InvalidLiquidHeightFound(
206
+ "Aspirate distance must be a float in Hardware pipetting handler."
207
+ )
208
+ with self._set_flow_rate(pipette=hw_pipette, aspirate_flow_rate=flow_rate):
209
+ await self._hardware_api.aspirate_while_tracking(
210
+ mount=hw_pipette.mount,
211
+ z_distance=aspirate_z_distance,
212
+ flow_rate=flow_rate,
213
+ volume=adjusted_volume,
214
+ )
215
+ return adjusted_volume
216
+
217
+ async def dispense_while_tracking(
218
+ self,
219
+ pipette_id: str,
220
+ labware_id: str,
221
+ well_name: str,
222
+ volume: float,
223
+ flow_rate: float,
224
+ push_out: Optional[float],
225
+ is_full_dispense: bool = False,
226
+ ) -> float:
227
+ """Set flow-rate and dispense.
228
+
229
+ Raises:
230
+ PipetteOverpressureError, propagated as-is from the hardware controller.
231
+ """
232
+ # get mount and config data from state and hardware controller
233
+ hw_pipette, adjusted_volume = self.get_hw_dispense_params(pipette_id, volume)
234
+ dispense_z_distance = self._state_view.geometry.get_liquid_handling_z_change(
235
+ labware_id=labware_id,
236
+ well_name=well_name,
237
+ operation_volume=volume,
122
238
  pipette_id=pipette_id,
123
- attached_pipettes=self._hardware_api.attached_instruments,
239
+ )
240
+ if isinstance(dispense_z_distance, SimulatedProbeResult):
241
+ raise InvalidLiquidHeightFound(
242
+ "Dispense distance must be a float in Hardware pipetting handler."
243
+ )
244
+ with self._set_flow_rate(pipette=hw_pipette, dispense_flow_rate=flow_rate):
245
+ await self._hardware_api.dispense_while_tracking(
246
+ mount=hw_pipette.mount,
247
+ z_distance=dispense_z_distance,
248
+ flow_rate=flow_rate,
249
+ volume=adjusted_volume,
250
+ push_out=push_out,
251
+ is_full_dispense=is_full_dispense,
252
+ )
253
+ return adjusted_volume
254
+
255
+ async def aspirate_in_place(
256
+ self,
257
+ pipette_id: str,
258
+ volume: float,
259
+ flow_rate: float,
260
+ command_note_adder: CommandNoteAdder,
261
+ correction_volume: float = 0.0,
262
+ ) -> float:
263
+ """Set flow-rate and aspirate.
264
+
265
+ Raises:
266
+ PipetteOverpressureError, propagated as-is from the hardware controller.
267
+ """
268
+ # get mount and config data from state and hardware controller
269
+ hw_pipette, adjusted_volume = self.get_hw_aspirate_params(
270
+ pipette_id, volume, command_note_adder
124
271
  )
125
272
  with self._set_flow_rate(pipette=hw_pipette, aspirate_flow_rate=flow_rate):
126
273
  await self._hardware_api.aspirate(
127
- mount=hw_pipette.mount, volume=adjusted_volume
274
+ mount=hw_pipette.mount,
275
+ volume=adjusted_volume,
276
+ correction_volume=correction_volume,
128
277
  )
129
278
 
130
279
  return adjusted_volume
@@ -135,15 +284,11 @@ class HardwarePipettingHandler(PipettingHandler):
135
284
  volume: float,
136
285
  flow_rate: float,
137
286
  push_out: Optional[float],
287
+ is_full_dispense: bool,
288
+ correction_volume: float = 0.0,
138
289
  ) -> float:
139
290
  """Dispense liquid without moving the pipette."""
140
- adjusted_volume = _validate_dispense_volume(
141
- state_view=self._state_view, pipette_id=pipette_id, dispense_volume=volume
142
- )
143
- hw_pipette = self._state_view.pipettes.get_hardware_pipette(
144
- pipette_id=pipette_id,
145
- attached_pipettes=self._hardware_api.attached_instruments,
146
- )
291
+ hw_pipette, adjusted_volume = self.get_hw_dispense_params(pipette_id, volume)
147
292
  # TODO (tz, 8-23-23): add a check for push_out not larger that the max volume allowed when working on this https://opentrons.atlassian.net/browse/RSS-329
148
293
  if push_out and push_out < 0:
149
294
  raise InvalidPushOutVolumeError(
@@ -151,7 +296,11 @@ class HardwarePipettingHandler(PipettingHandler):
151
296
  )
152
297
  with self._set_flow_rate(pipette=hw_pipette, dispense_flow_rate=flow_rate):
153
298
  await self._hardware_api.dispense(
154
- mount=hw_pipette.mount, volume=adjusted_volume, push_out=push_out
299
+ mount=hw_pipette.mount,
300
+ volume=adjusted_volume,
301
+ push_out=push_out,
302
+ correction_volume=correction_volume,
303
+ is_full_dispense=is_full_dispense,
155
304
  )
156
305
 
157
306
  return adjusted_volume
@@ -176,8 +325,8 @@ class HardwarePipettingHandler(PipettingHandler):
176
325
  labware_id: str,
177
326
  well_name: str,
178
327
  well_location: WellLocation,
179
- ) -> float:
180
- """Detect liquid level."""
328
+ ) -> LiquidTrackingType:
329
+ """Return liquid level relative to the bottom of the well."""
181
330
  hw_pipette = self._state_view.pipettes.get_hardware_pipette(
182
331
  pipette_id=pipette_id,
183
332
  attached_pipettes=self._hardware_api.attached_instruments,
@@ -223,6 +372,14 @@ class HardwarePipettingHandler(PipettingHandler):
223
372
  blow_out=original_blow_out_rate,
224
373
  )
225
374
 
375
+ async def increase_evo_disp_count(self, pipette_id: str) -> None:
376
+ """Increase evo tip dispense action count."""
377
+ hw_pipette = self._state_view.pipettes.get_hardware_pipette(
378
+ pipette_id=pipette_id,
379
+ attached_pipettes=self._hardware_api.attached_instruments,
380
+ )
381
+ await self._hardware_api.increase_evo_disp_count(mount=hw_pipette.mount)
382
+
226
383
 
227
384
  class VirtualPipettingHandler(PipettingHandler):
228
385
  """Liquid handling, using the virtual pipettes.""" ""
@@ -236,12 +393,19 @@ class VirtualPipettingHandler(PipettingHandler):
236
393
  """Initialize a PipettingHandler instance."""
237
394
  self._state_view = state_view
238
395
 
396
+ def get_state_view(self) -> StateView:
397
+ """Get the stateview associated with this handler."""
398
+ return self._state_view
399
+
239
400
  def get_is_ready_to_aspirate(self, pipette_id: str) -> bool:
240
401
  """Get whether a pipette is ready to aspirate."""
241
- return self._state_view.pipettes.get_aspirated_volume(pipette_id) is not None
402
+ return self._state_view.pipettes.get_aspirated_volume(
403
+ pipette_id
404
+ ) is not None and self._state_view.pipettes.get_ready_to_aspirate(pipette_id)
242
405
 
243
406
  async def prepare_for_aspirate(self, pipette_id: str) -> None:
244
407
  """Virtually prepare to aspirate (no-op)."""
408
+ self._validate_tip_attached(pipette_id=pipette_id, command_name="aspirate")
245
409
 
246
410
  async def aspirate_in_place(
247
411
  self,
@@ -249,6 +413,7 @@ class VirtualPipettingHandler(PipettingHandler):
249
413
  volume: float,
250
414
  flow_rate: float,
251
415
  command_note_adder: CommandNoteAdder,
416
+ correction_volume: float = 0.0,
252
417
  ) -> float:
253
418
  """Virtually aspirate (no-op)."""
254
419
  self._validate_tip_attached(pipette_id=pipette_id, command_name="aspirate")
@@ -265,6 +430,8 @@ class VirtualPipettingHandler(PipettingHandler):
265
430
  volume: float,
266
431
  flow_rate: float,
267
432
  push_out: Optional[float],
433
+ is_full_dispense: bool,
434
+ correction_volume: float = 0.0,
268
435
  ) -> float:
269
436
  """Virtually dispense (no-op)."""
270
437
  # TODO (tz, 8-23-23): add a check for push_out not larger that the max volume allowed when working on this https://opentrons.atlassian.net/browse/RSS-329
@@ -290,10 +457,9 @@ class VirtualPipettingHandler(PipettingHandler):
290
457
  labware_id: str,
291
458
  well_name: str,
292
459
  well_location: WellLocation,
293
- ) -> float:
460
+ ) -> LiquidTrackingType:
294
461
  """Detect liquid level."""
295
- well_def = self._state_view.labware.get_well_definition(labware_id, well_name)
296
- return well_def.depth
462
+ return SimulatedProbeResult()
297
463
 
298
464
  def _validate_tip_attached(self, pipette_id: str, command_name: str) -> None:
299
465
  """Validate if there is a tip attached."""
@@ -303,6 +469,50 @@ class VirtualPipettingHandler(PipettingHandler):
303
469
  f"Cannot perform {command_name} without a tip attached"
304
470
  )
305
471
 
472
+ async def aspirate_while_tracking(
473
+ self,
474
+ pipette_id: str,
475
+ labware_id: str,
476
+ well_name: str,
477
+ volume: float,
478
+ flow_rate: float,
479
+ command_note_adder: CommandNoteAdder,
480
+ ) -> float:
481
+ """Virtually aspirate (no-op)."""
482
+ self._validate_tip_attached(pipette_id=pipette_id, command_name="aspirate")
483
+
484
+ return _validate_aspirate_volume(
485
+ state_view=self._state_view,
486
+ pipette_id=pipette_id,
487
+ aspirate_volume=volume,
488
+ command_note_adder=command_note_adder,
489
+ )
490
+
491
+ async def dispense_while_tracking(
492
+ self,
493
+ pipette_id: str,
494
+ labware_id: str,
495
+ well_name: str,
496
+ volume: float,
497
+ flow_rate: float,
498
+ push_out: Optional[float],
499
+ is_full_dispense: bool = False,
500
+ ) -> float:
501
+ """Virtually dispense (no-op)."""
502
+ # TODO (tz, 8-23-23): add a check for push_out not larger that the max volume allowed when working on this https://opentrons.atlassian.net/browse/RSS-329
503
+ if push_out and push_out < 0:
504
+ raise InvalidPushOutVolumeError(
505
+ "push out value cannot have a negative value."
506
+ )
507
+ self._validate_tip_attached(pipette_id=pipette_id, command_name="dispense")
508
+ return _validate_dispense_volume(
509
+ state_view=self._state_view, pipette_id=pipette_id, dispense_volume=volume
510
+ )
511
+
512
+ async def increase_evo_disp_count(self, pipette_id: str) -> None:
513
+ """Increase evo tip dispense action count."""
514
+ pass
515
+
306
516
 
307
517
  def create_pipetting_handler(
308
518
  state_view: StateView, hardware_api: HardwareControlAPI
@@ -1,10 +1,14 @@
1
1
  """Tip pickup and drop procedures."""
2
2
 
3
- from typing import Optional, Dict
3
+ from typing import Optional, Dict, Tuple
4
4
  from typing_extensions import Protocol as TypingProtocol
5
5
 
6
6
  from opentrons.hardware_control import HardwareControlAPI
7
- from opentrons.hardware_control.types import FailedTipStateCheck, InstrumentProbeType
7
+ from opentrons.hardware_control.types import (
8
+ FailedTipStateCheck,
9
+ InstrumentProbeType,
10
+ TipScrapeType,
11
+ )
8
12
  from opentrons.protocol_engine.errors.exceptions import PickUpTipTipNotAttachedError
9
13
  from opentrons.types import Mount, NozzleConfigurationType
10
14
 
@@ -82,6 +86,7 @@ class TipHandler(TypingProtocol):
82
86
  home_after: Optional[bool],
83
87
  do_not_ignore_tip_presence: bool = True,
84
88
  ignore_plunger: bool = False,
89
+ scrape_type: TipScrapeType = TipScrapeType.NONE,
85
90
  ) -> None:
86
91
  """Drop the attached tip into the current location.
87
92
 
@@ -196,6 +201,18 @@ async def _available_for_nozzle_layout( # noqa: C901
196
201
  }
197
202
 
198
203
 
204
+ def tip_on_left_side_96(back_left_nozzle: str) -> bool:
205
+ """Return if there is a tip on the left edge of the 96 channel."""
206
+ left_most_column = int(back_left_nozzle[1:])
207
+ return left_most_column == 1
208
+
209
+
210
+ def tip_on_right_side_96(front_right_nozzle: str) -> bool:
211
+ """Return if there is a tip on the left edge of the 96 channel."""
212
+ right_most_column = int(front_right_nozzle[1:])
213
+ return right_most_column == 12
214
+
215
+
199
216
  class HardwareTipHandler(TipHandler):
200
217
  """Pick up and drop tips, using the Hardware API."""
201
218
 
@@ -232,6 +249,50 @@ class HardwareTipHandler(TipHandler):
232
249
  channels, style, primary_nozzle, front_right_nozzle, back_left_nozzle
233
250
  )
234
251
 
252
+ def get_tip_presence_config(
253
+ self, pipette_id: str
254
+ ) -> Tuple[bool, Optional[InstrumentProbeType]]:
255
+ """Return the supported settings for tip presence on a given pipette depending on it's current nozzle map."""
256
+ follow_singular_sensor = None
257
+
258
+ unsupported_layout_types_96 = [NozzleConfigurationType.SINGLE]
259
+ # NOTE: (09-20-2024) Current on multi-channel pipettes, utilizing less than 4 nozzles risks false positives on the tip presence sensor
260
+ supported_partial_nozzle_minimum = 4
261
+
262
+ nozzle_configuration = self._state_view.pipettes.get_nozzle_configuration(
263
+ pipette_id=pipette_id
264
+ )
265
+
266
+ match self._state_view.pipettes.get_channels(pipette_id):
267
+ case 1:
268
+ tip_presence_supported = True
269
+ case 8:
270
+ tip_presence_supported = (
271
+ nozzle_configuration.tip_count >= supported_partial_nozzle_minimum
272
+ )
273
+ case 96:
274
+ tip_presence_supported = (
275
+ nozzle_configuration.configuration
276
+ not in unsupported_layout_types_96
277
+ and nozzle_configuration.tip_count
278
+ >= supported_partial_nozzle_minimum
279
+ )
280
+ if (
281
+ nozzle_configuration.configuration != NozzleConfigurationType.FULL
282
+ and tip_presence_supported
283
+ ):
284
+ use_left = tip_on_left_side_96(nozzle_configuration.back_left)
285
+ use_right = tip_on_right_side_96(nozzle_configuration.front_right)
286
+ if not (use_left and use_right):
287
+ if use_left:
288
+ follow_singular_sensor = InstrumentProbeType.PRIMARY
289
+ else:
290
+ follow_singular_sensor = InstrumentProbeType.SECONDARY
291
+ case _:
292
+ raise ValueError("Unknown pipette type.")
293
+
294
+ return (tip_presence_supported, follow_singular_sensor)
295
+
235
296
  async def pick_up_tip(
236
297
  self,
237
298
  pipette_id: str,
@@ -261,9 +322,18 @@ class HardwareTipHandler(TipHandler):
261
322
  await self._hardware_api.tip_pickup_moves(
262
323
  mount=hw_mount, presses=None, increment=None
263
324
  )
264
- if do_not_ignore_tip_presence:
325
+
326
+ tip_presence_supported, follow_singular_sensor = self.get_tip_presence_config(
327
+ pipette_id
328
+ )
329
+
330
+ if do_not_ignore_tip_presence and tip_presence_supported:
265
331
  try:
266
- await self.verify_tip_presence(pipette_id, TipPresenceStatus.PRESENT)
332
+ await self.verify_tip_presence(
333
+ pipette_id,
334
+ TipPresenceStatus.PRESENT,
335
+ follow_singular_sensor=follow_singular_sensor,
336
+ )
267
337
  except TipNotAttachedError as e:
268
338
  raise PickUpTipTipNotAttachedError(tip_geometry=tip_geometry) from e
269
339
 
@@ -279,19 +349,22 @@ class HardwareTipHandler(TipHandler):
279
349
  home_after: Optional[bool],
280
350
  do_not_ignore_tip_presence: bool = True,
281
351
  ignore_plunger: bool = False,
352
+ scrape_type: TipScrapeType = TipScrapeType.NONE,
282
353
  ) -> None:
283
354
  """See documentation on abstract base class."""
284
355
  hw_mount = self._get_hw_mount(pipette_id)
285
356
 
286
357
  # Let the hardware controller handle defaulting home_after since its behavior
287
358
  # differs between machines
359
+ kwargs = {}
288
360
  if home_after is not None:
289
- kwargs = {"home_after": home_after}
290
- else:
291
- kwargs = {}
361
+ kwargs["home_after"] = home_after
292
362
 
293
363
  await self._hardware_api.tip_drop_moves(
294
- mount=hw_mount, ignore_plunger=ignore_plunger, **kwargs
364
+ mount=hw_mount,
365
+ ignore_plunger=ignore_plunger,
366
+ scrape_type=scrape_type,
367
+ **kwargs,
295
368
  )
296
369
 
297
370
  if do_not_ignore_tip_presence:
@@ -342,30 +415,6 @@ class HardwareTipHandler(TipHandler):
342
415
  follow_singular_sensor: Optional[InstrumentProbeType] = None,
343
416
  ) -> None:
344
417
  """See documentation on abstract base class."""
345
- nozzle_configuration = self._state_view.pipettes.get_nozzle_configuration(
346
- pipette_id=pipette_id
347
- )
348
-
349
- # Configuration metrics by which tip presence checking is ignored
350
- unsupported_pipette_types = [8, 96]
351
- unsupported_layout_types = [
352
- NozzleConfigurationType.SINGLE,
353
- NozzleConfigurationType.COLUMN,
354
- ]
355
- # NOTE: (09-20-2024) Current on multi-channel pipettes, utilizing less than 4 nozzles risks false positives on the tip presence sensor
356
- supported_partial_nozzle_minimum = 4
357
-
358
- if (
359
- nozzle_configuration is not None
360
- and self._state_view.pipettes.get_channels(pipette_id)
361
- in unsupported_pipette_types
362
- and nozzle_configuration.configuration in unsupported_layout_types
363
- and len(nozzle_configuration.map_store) < supported_partial_nozzle_minimum
364
- ):
365
- # Tip presence sensing is not supported for single tip pick up on the 96ch Flex Pipette, nor with single and some partial layous of the 8ch Flex Pipette.
366
- # This is due in part to a press distance tolerance which creates a risk case for false positives. In the case of single tip, the mechanical tolerance
367
- # for presses with 100% success is below the minimum average achieved press distance for a given multi channel pipette in that configuration.
368
- return
369
418
  try:
370
419
  ot3api = ensure_ot3_hardware(hardware_api=self._hardware_api)
371
420
  hw_mount = self._get_hw_mount(pipette_id)
@@ -445,6 +494,7 @@ class VirtualTipHandler(TipHandler):
445
494
  home_after: Optional[bool],
446
495
  do_not_ignore_tip_presence: bool = True,
447
496
  ignore_plunger: bool = False,
497
+ scrape_type: TipScrapeType = TipScrapeType.NONE,
448
498
  ) -> None:
449
499
  """Pick up a tip at the current location using a virtual pipette.
450
500