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
@@ -0,0 +1,858 @@
1
+ """Executor for liquid class based complex commands."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ from copy import deepcopy
6
+ from enum import Enum
7
+ from typing import TYPE_CHECKING, Optional, Union, Literal
8
+ from dataclasses import dataclass, field
9
+
10
+ from opentrons_shared_data.liquid_classes.liquid_class_definition import (
11
+ PositionReference,
12
+ Coordinate,
13
+ BlowoutLocation,
14
+ )
15
+ from opentrons_shared_data.pipette.types import LIQUID_PROBE_START_OFFSET_FROM_WELL_TOP
16
+
17
+ from opentrons.protocol_api._liquid_properties import (
18
+ Submerge,
19
+ TransferProperties,
20
+ MixProperties,
21
+ SingleDispenseProperties,
22
+ MultiDispenseProperties,
23
+ TouchTipProperties,
24
+ )
25
+ from opentrons.protocol_engine.errors import TouchTipDisabledError
26
+ from opentrons.types import Location, Point
27
+ from opentrons.protocols.advanced_control.transfers.transfer_liquid_utils import (
28
+ LocationCheckDescriptors,
29
+ )
30
+ from opentrons.protocols.advanced_control.transfers import (
31
+ transfer_liquid_utils as tx_utils,
32
+ )
33
+
34
+ if TYPE_CHECKING:
35
+ from .well import WellCore
36
+ from .instrument import InstrumentCore
37
+ from ... import TrashBin, WasteChute
38
+
39
+ log = logging.getLogger(__name__)
40
+
41
+
42
+ @dataclass
43
+ class LiquidAndAirGapPair:
44
+ """Pairing of a liquid and air gap in a tip, with air gap below the liquid in a tip."""
45
+
46
+ liquid: float = 0
47
+ air_gap: float = 0
48
+
49
+
50
+ @dataclass
51
+ class TipState:
52
+ """Carrier of the state of the pipette tip in use.
53
+
54
+ Properties:
55
+ last_liquid_and_air_gap_in_tip: The last liquid + air_gap combo in the tip.
56
+ This will only include the existing liquid and air gap in the tip that
57
+ an aspirate/ dispense interacts with. For example, the air gap from
58
+ a previous step that needs to be removed, or the liquid from a previous
59
+ aspirate that needs to be dispensed or the liquid that needs to be added to
60
+ during a consolidation.
61
+ ready_to_aspirate: Whether the pipette plunger is in a position that allows
62
+ correct aspiration. The starting state for the pipette at initialization of
63
+ `TransferComponentsExecutor`s should be ready_to_aspirate == True.
64
+ """
65
+
66
+ ready_to_aspirate: bool = True
67
+ # TODO: maybe use the tip contents from engine state instead.
68
+ last_liquid_and_air_gap_in_tip: LiquidAndAirGapPair = field(
69
+ default_factory=LiquidAndAirGapPair
70
+ )
71
+
72
+ def append_liquid(self, volume: float) -> None:
73
+ # Neither aspirate nor a dispense process should be adding liquid
74
+ # when there is an air gap present.
75
+ assert (
76
+ self.last_liquid_and_air_gap_in_tip.air_gap == 0
77
+ ), "Air gap present in the tip."
78
+ self.last_liquid_and_air_gap_in_tip.liquid += volume
79
+
80
+ def delete_liquid(self, volume: float) -> None:
81
+ # Neither aspirate nor a dispense process should be removing liquid
82
+ # when there is an air gap present.
83
+ assert (
84
+ self.last_liquid_and_air_gap_in_tip.air_gap == 0
85
+ ), "Air gap present in the tip."
86
+ self.last_liquid_and_air_gap_in_tip.liquid -= volume
87
+
88
+ def append_air_gap(self, volume: float) -> None:
89
+ # Neither aspirate nor a dispense process should be adding air gaps
90
+ # when there is already an air gap present.
91
+ assert (
92
+ self.last_liquid_and_air_gap_in_tip.air_gap == 0
93
+ ), "Air gap already present in the tip."
94
+ self.last_liquid_and_air_gap_in_tip.air_gap = volume
95
+
96
+ def delete_air_gap(self, volume: float) -> None:
97
+ assert (
98
+ self.last_liquid_and_air_gap_in_tip.air_gap == volume
99
+ ), "Last air gap volume doe not match the volume being removed"
100
+ self.last_liquid_and_air_gap_in_tip.air_gap = 0
101
+
102
+ def delete_last_air_gap_and_liquid(self) -> None:
103
+ air_gap_in_tip = self.last_liquid_and_air_gap_in_tip.air_gap
104
+ liquid_in_tip = self.last_liquid_and_air_gap_in_tip.liquid
105
+ if air_gap_in_tip:
106
+ self.delete_air_gap(air_gap_in_tip)
107
+ if liquid_in_tip:
108
+ self.delete_liquid(volume=liquid_in_tip)
109
+
110
+
111
+ class TransferType(Enum):
112
+ ONE_TO_ONE = "one_to_one"
113
+ MANY_TO_ONE = "many_to_one"
114
+ ONE_TO_MANY = "one_to_many"
115
+
116
+
117
+ class TransferComponentsExecutor:
118
+ def __init__(
119
+ self,
120
+ instrument_core: InstrumentCore,
121
+ transfer_properties: TransferProperties,
122
+ target_location: Location,
123
+ target_well: WellCore,
124
+ tip_state: TipState,
125
+ transfer_type: TransferType,
126
+ ) -> None:
127
+ self._instrument = instrument_core
128
+ self._transfer_properties = transfer_properties
129
+ self._target_location = target_location
130
+ self._target_well = target_well
131
+ self._tip_state: TipState = deepcopy(tip_state) # don't modify caller's object
132
+ self._transfer_type: TransferType = transfer_type
133
+
134
+ @property
135
+ def tip_state(self) -> TipState:
136
+ """Return the tip state."""
137
+ return self._tip_state
138
+
139
+ def submerge(
140
+ self,
141
+ submerge_properties: Submerge,
142
+ post_submerge_action: Literal["aspirate", "dispense"],
143
+ volume_for_pipette_mode_configuration: Optional[float],
144
+ ) -> None:
145
+ """Execute submerge steps.
146
+
147
+ 1. move to position shown by positionReference + offset (should practically be a point outside/above the liquid).
148
+ Should raise an error if this point is inside the liquid?
149
+ For liquid meniscus this is easy to tell. Can’t be below meniscus
150
+ For reference pos of anything else, do not allow submerge position to be below aspirate position
151
+ 2. move to aspirate position at desired speed
152
+ 3. delay
153
+ """
154
+ submerge_start_point = absolute_point_from_position_reference_and_offset(
155
+ well=self._target_well,
156
+ position_reference=submerge_properties.position_reference,
157
+ offset=submerge_properties.offset,
158
+ )
159
+ submerge_start_location = Location(
160
+ point=submerge_start_point, labware=self._target_location.labware
161
+ )
162
+ prep_before_moving_to_submerge = (
163
+ post_submerge_action == "aspirate"
164
+ and volume_for_pipette_mode_configuration is not None
165
+ )
166
+ if prep_before_moving_to_submerge:
167
+ # Move to the tip probe start position
168
+ self._instrument.move_to(
169
+ location=Location(
170
+ point=self._target_well.get_top(
171
+ LIQUID_PROBE_START_OFFSET_FROM_WELL_TOP.z
172
+ ),
173
+ labware=self._target_location.labware,
174
+ ),
175
+ well_core=self._target_well,
176
+ force_direct=False,
177
+ minimum_z_height=None,
178
+ speed=None,
179
+ )
180
+ self._remove_air_gap(location=submerge_start_location)
181
+ if (
182
+ self._transfer_type != TransferType.MANY_TO_ONE
183
+ and self._instrument.get_liquid_presence_detection()
184
+ ):
185
+ self._instrument.liquid_probe_with_recovery(
186
+ well_core=self._target_well, loc=submerge_start_location
187
+ )
188
+ # TODO: do volume configuration + prepare for aspirate only if the mode needs to be changed
189
+ self._instrument.configure_for_volume(volume_for_pipette_mode_configuration) # type: ignore[arg-type]
190
+ self._instrument.prepare_to_aspirate()
191
+ tx_utils.raise_if_location_inside_liquid(
192
+ location=submerge_start_location,
193
+ well_location=self._target_location,
194
+ well_core=self._target_well,
195
+ location_check_descriptors=LocationCheckDescriptors(
196
+ location_type="submerge start",
197
+ pipetting_action=post_submerge_action,
198
+ ),
199
+ logger=log,
200
+ )
201
+ self._instrument.move_to(
202
+ location=submerge_start_location,
203
+ well_core=self._target_well,
204
+ force_direct=False,
205
+ minimum_z_height=None,
206
+ speed=None,
207
+ )
208
+ if not prep_before_moving_to_submerge:
209
+ self._remove_air_gap(location=submerge_start_location)
210
+ self._instrument.move_to(
211
+ location=self._target_location,
212
+ well_core=self._target_well,
213
+ force_direct=True,
214
+ minimum_z_height=None,
215
+ speed=submerge_properties.speed,
216
+ )
217
+ if submerge_properties.delay.enabled and submerge_properties.delay.duration:
218
+ self._instrument.delay(submerge_properties.delay.duration)
219
+
220
+ def aspirate_and_wait(self, volume: float) -> None:
221
+ """Aspirate according to aspirate properties and wait if enabled."""
222
+ # TODO: handle volume correction
223
+ aspirate_props = self._transfer_properties.aspirate
224
+ correction_volume = aspirate_props.correction_by_volume.get_for_volume(volume)
225
+ self._instrument.aspirate(
226
+ location=self._target_location,
227
+ well_core=None,
228
+ volume=volume,
229
+ rate=1,
230
+ flow_rate=aspirate_props.flow_rate_by_volume.get_for_volume(volume),
231
+ in_place=True,
232
+ correction_volume=correction_volume,
233
+ )
234
+ self._tip_state.append_liquid(volume)
235
+ delay_props = aspirate_props.delay
236
+ if delay_props.enabled and delay_props.duration:
237
+ self._instrument.delay(delay_props.duration)
238
+
239
+ def dispense_and_wait(
240
+ self,
241
+ dispense_properties: Union[SingleDispenseProperties, MultiDispenseProperties],
242
+ volume: float,
243
+ push_out_override: Optional[float],
244
+ ) -> None:
245
+ """Dispense according to dispense properties and wait if enabled."""
246
+ correction_volume = dispense_properties.correction_by_volume.get_for_volume(
247
+ volume
248
+ )
249
+ self._instrument.dispense(
250
+ location=self._target_location,
251
+ well_core=None,
252
+ volume=volume,
253
+ rate=1,
254
+ flow_rate=dispense_properties.flow_rate_by_volume.get_for_volume(volume),
255
+ in_place=True,
256
+ push_out=push_out_override,
257
+ correction_volume=correction_volume,
258
+ )
259
+ if push_out_override:
260
+ # If a push out was performed, we need to reset the plunger before we can aspirate again
261
+ self._tip_state.ready_to_aspirate = False
262
+ self._tip_state.delete_liquid(volume)
263
+ dispense_delay = dispense_properties.delay
264
+ if dispense_delay.enabled and dispense_delay.duration:
265
+ self._instrument.delay(dispense_delay.duration)
266
+
267
+ def mix(self, mix_properties: MixProperties, last_dispense_push_out: bool) -> None:
268
+ """Execute mix steps.
269
+
270
+ 1. Use same flow rates and delays as aspirate and dispense
271
+ 2. Do [(aspirate + dispense) x repetitions] at the same position
272
+ 3. Do NOT push out at the end of dispense
273
+ 4. USE the delay property from aspirate & dispense during mix as well (flow rate and delay are coordinated with each other)
274
+ 5. Do not mix during consolidation
275
+ NOTE: For most of our built-in definitions, we will keep _mix_ off because it is a very application specific thing.
276
+ We should mention in our docs that users should adjust this property according to their application.
277
+ """
278
+ if not mix_properties.enabled:
279
+ return
280
+ # Assertion only for mypy purposes
281
+ assert (
282
+ mix_properties.repetitions is not None and mix_properties.volume is not None
283
+ )
284
+ push_out_vol = (
285
+ self._transfer_properties.dispense.push_out_by_volume.get_for_volume(
286
+ mix_properties.volume
287
+ )
288
+ )
289
+ for n in range(mix_properties.repetitions, 0, -1):
290
+ self.aspirate_and_wait(volume=mix_properties.volume)
291
+ self.dispense_and_wait(
292
+ dispense_properties=self._transfer_properties.dispense, # TODO: check that using single-dispense props during mix is correct
293
+ volume=mix_properties.volume,
294
+ push_out_override=push_out_vol
295
+ if last_dispense_push_out is True and n == 1
296
+ else 0,
297
+ )
298
+
299
+ def pre_wet(
300
+ self,
301
+ volume: float,
302
+ ) -> None:
303
+ """Do a pre-wet.
304
+
305
+ - 1 combo of aspirate + dispense at the same flow rate as specified in asp & disp and the delays in asp & disp
306
+ - Use the target volume/ volume we will be aspirating
307
+ - No push out
308
+ - No pre-wet for consolidation
309
+ """
310
+ if not self._transfer_properties.aspirate.pre_wet:
311
+ return
312
+ mix_props = MixProperties(_enabled=True, _repetitions=1, _volume=volume)
313
+ self.mix(mix_properties=mix_props, last_dispense_push_out=False)
314
+
315
+ def retract_after_aspiration(
316
+ self, volume: float, add_air_gap: Optional[bool] = True
317
+ ) -> None:
318
+ """Execute post-aspiration retraction steps.
319
+
320
+ 1. Move TO the position reference+offset AT the specified speed
321
+ Raise error if retract is below aspirate position or below the meniscus
322
+ 2. Delay
323
+ 3. Touch tip
324
+ - Move to the Z offset position
325
+ - Touch tip to the sides at the specified speed (tip moves back to the center as part of touch tip)
326
+ - Return back to the retract position
327
+ 4. Air gap
328
+ - Air gap volume depends on the amount of liquid in the pipette
329
+ So if total aspirated volume is 20, use the value for airGapByVolume[20]
330
+ Flow rate = min(aspirateFlowRate, (airGapByVolume)/sec)
331
+ - Use post-aspirate delay
332
+
333
+ Args:
334
+ volume: dispense volume
335
+ add_air_gap: whether to add an air gap before moving away from the current well.
336
+ This value is True for all retractions, except when retracting
337
+ during a multi-dispense.
338
+ """
339
+ # TODO: Raise error if retract is below the meniscus
340
+ retract_props = self._transfer_properties.aspirate.retract
341
+ retract_point = absolute_point_from_position_reference_and_offset(
342
+ well=self._target_well,
343
+ position_reference=retract_props.position_reference,
344
+ offset=retract_props.offset,
345
+ )
346
+ retract_location = Location(
347
+ retract_point, labware=self._target_location.labware
348
+ )
349
+ tx_utils.raise_if_location_inside_liquid(
350
+ location=retract_location,
351
+ well_location=self._target_location,
352
+ well_core=self._target_well,
353
+ location_check_descriptors=LocationCheckDescriptors(
354
+ location_type="retract end",
355
+ pipetting_action="aspirate",
356
+ ),
357
+ logger=log,
358
+ )
359
+ self._instrument.move_to(
360
+ location=retract_location,
361
+ well_core=self._target_well,
362
+ force_direct=True,
363
+ minimum_z_height=None,
364
+ speed=retract_props.speed,
365
+ )
366
+ retract_delay = retract_props.delay
367
+ if retract_delay.enabled and retract_delay.duration:
368
+ self._instrument.delay(retract_delay.duration)
369
+ touch_tip_props = retract_props.touch_tip
370
+ if touch_tip_props.enabled:
371
+ assert (
372
+ touch_tip_props.speed is not None
373
+ and touch_tip_props.z_offset is not None
374
+ and touch_tip_props.mm_to_edge is not None
375
+ )
376
+ self._instrument.touch_tip(
377
+ location=retract_location,
378
+ well_core=self._target_well,
379
+ radius=1,
380
+ z_offset=touch_tip_props.z_offset,
381
+ speed=touch_tip_props.speed,
382
+ mm_from_edge=touch_tip_props.mm_to_edge,
383
+ )
384
+ self._instrument.move_to(
385
+ location=retract_location,
386
+ well_core=self._target_well,
387
+ force_direct=True,
388
+ minimum_z_height=None,
389
+ # Full speed because the tip will already be out of the liquid
390
+ speed=None,
391
+ )
392
+ # For consolidate, we need to know the total amount that is in the pipette
393
+ # since this may not be the first aspirate
394
+ if self._transfer_type == TransferType.MANY_TO_ONE:
395
+ volume_for_air_gap = self._instrument.get_current_volume()
396
+ else:
397
+ volume_for_air_gap = volume
398
+ if add_air_gap:
399
+ self._add_air_gap(
400
+ air_gap_volume=self._transfer_properties.aspirate.retract.air_gap_by_volume.get_for_volume(
401
+ volume_for_air_gap
402
+ )
403
+ )
404
+
405
+ def retract_after_dispensing(
406
+ self,
407
+ trash_location: Union[Location, TrashBin, WasteChute],
408
+ source_location: Optional[Location],
409
+ source_well: Optional[WellCore],
410
+ add_final_air_gap: bool,
411
+ ) -> None:
412
+ """Execute post-dispense retraction steps.
413
+ 1. Position ref+offset is the ending position. Move to this position using specified speed
414
+ 2. If blowout is enabled and “destination”
415
+ - Do blow-out (at the retract position)
416
+ - Leave plunger down
417
+ 3. Touch-tip
418
+ 4. If not ready-to-aspirate
419
+ - Prepare-to-aspirate (at the retract position)
420
+ 5. Air-gap (at the retract position)
421
+ - This air gap is for preventing any stray droplets from falling while moving the pipette.
422
+ It will be performed out of caution even if we just did a blow_out and should *hypothetically*
423
+ have no liquid left in the tip.
424
+ - This air gap will be removed at the next aspirate.
425
+ If this is the last step of the transfer, and we aren't dropping the tip off,
426
+ then the air gap will be left as is(?).
427
+ 6. If blowout is “source” or “trash”
428
+ - Move to location (top of Well)
429
+ - Do blow-out (top of well)
430
+ - Do touch-tip (?????) (only if it’s in a non-trash location)
431
+ - Prepare-to-aspirate (top of well)
432
+ - Do air-gap (top of well)
433
+ 7. If drop tip, move to drop tip location, drop tip
434
+ """
435
+ # TODO: Raise error if retract is below the meniscus
436
+
437
+ retract_props = self._transfer_properties.dispense.retract
438
+ retract_point = absolute_point_from_position_reference_and_offset(
439
+ well=self._target_well,
440
+ position_reference=retract_props.position_reference,
441
+ offset=retract_props.offset,
442
+ )
443
+ retract_location = Location(
444
+ retract_point, labware=self._target_location.labware
445
+ )
446
+ tx_utils.raise_if_location_inside_liquid(
447
+ location=retract_location,
448
+ well_location=self._target_location,
449
+ well_core=self._target_well,
450
+ location_check_descriptors=LocationCheckDescriptors(
451
+ location_type="retract end",
452
+ pipetting_action="dispense",
453
+ ),
454
+ logger=log,
455
+ )
456
+ self._instrument.move_to(
457
+ location=retract_location,
458
+ well_core=self._target_well,
459
+ force_direct=True,
460
+ minimum_z_height=None,
461
+ speed=retract_props.speed,
462
+ )
463
+ retract_delay = retract_props.delay
464
+ if retract_delay.enabled and retract_delay.duration:
465
+ self._instrument.delay(retract_delay.duration)
466
+
467
+ blowout_props = retract_props.blowout
468
+ if (
469
+ blowout_props.enabled
470
+ and blowout_props.location == BlowoutLocation.DESTINATION
471
+ ):
472
+ assert blowout_props.flow_rate is not None
473
+ self._instrument.set_flow_rate(blow_out=blowout_props.flow_rate)
474
+ self._instrument.blow_out(
475
+ location=retract_location,
476
+ well_core=None,
477
+ in_place=True,
478
+ )
479
+ self._tip_state.ready_to_aspirate = False
480
+ is_final_air_gap = (
481
+ blowout_props.enabled
482
+ and blowout_props.location == BlowoutLocation.DESTINATION
483
+ ) or not blowout_props.enabled
484
+ # Regardless of the blowout location, do touch tip and air gap
485
+ # when leaving the dispense well. If this will be the final air gap, i.e,
486
+ # we won't be moving to a Trash or a Source for Blowout after this air gap,
487
+ # then skip the final air gap if we have been told to do so.
488
+ self._do_touch_tip_and_air_gap(
489
+ touch_tip_properties=retract_props.touch_tip,
490
+ location=retract_location,
491
+ well=self._target_well,
492
+ add_air_gap=False if is_final_air_gap and not add_final_air_gap else True,
493
+ )
494
+
495
+ if (
496
+ blowout_props.enabled
497
+ and blowout_props.location != BlowoutLocation.DESTINATION
498
+ ):
499
+ # TODO: no-op touch tip if touch tip is enabled and blowout is in trash/ reservoir/ any labware with touch-tip disabled
500
+ assert blowout_props.flow_rate is not None
501
+ self._instrument.set_flow_rate(blow_out=blowout_props.flow_rate)
502
+ touch_tip_and_air_gap_location: Optional[Location]
503
+ if blowout_props.location == BlowoutLocation.SOURCE:
504
+ if source_location is None or source_well is None:
505
+ raise RuntimeError(
506
+ "Blowout location is 'source' but source location &/or well is not provided."
507
+ )
508
+ # TODO: check if we should add a blowout location z-offset in liq class definition
509
+ self._instrument.blow_out(
510
+ location=Location(
511
+ source_well.get_top(0), labware=source_location.labware
512
+ ),
513
+ well_core=source_well,
514
+ in_place=False,
515
+ )
516
+ touch_tip_and_air_gap_location = Location(
517
+ source_well.get_top(0), labware=source_location.labware
518
+ )
519
+ touch_tip_and_air_gap_well = source_well
520
+ else:
521
+ self._instrument.blow_out(
522
+ location=trash_location,
523
+ well_core=None,
524
+ in_place=False,
525
+ )
526
+ touch_tip_and_air_gap_location = (
527
+ trash_location if isinstance(trash_location, Location) else None
528
+ )
529
+ touch_tip_and_air_gap_well = (
530
+ # We have already established that trash location of `Location` type
531
+ # has its `labware` as `Well` type.
532
+ trash_location.labware.as_well()._core # type: ignore[assignment]
533
+ if isinstance(trash_location, Location)
534
+ else None
535
+ )
536
+ # A non-multi-dispense blowout will only have air and maybe droplets in the tip
537
+ # since we only blowout after dispensing the full tip contents.
538
+ # So delete the air gap from tip state
539
+ last_air_gap = self._tip_state.last_liquid_and_air_gap_in_tip.air_gap
540
+ self._tip_state.delete_air_gap(last_air_gap)
541
+ self._tip_state.ready_to_aspirate = False
542
+ # Do touch tip and air gap again after blowing out into source well or trash
543
+ self._do_touch_tip_and_air_gap(
544
+ touch_tip_properties=retract_props.touch_tip,
545
+ location=touch_tip_and_air_gap_location,
546
+ well=touch_tip_and_air_gap_well,
547
+ add_air_gap=add_final_air_gap,
548
+ )
549
+
550
+ def retract_during_multi_dispensing(
551
+ self,
552
+ trash_location: Union[Location, TrashBin, WasteChute],
553
+ source_location: Optional[Location],
554
+ source_well: Optional[WellCore],
555
+ conditioning_volume: float,
556
+ add_final_air_gap: bool,
557
+ is_last_retract: bool,
558
+ ) -> None:
559
+ """Execute post-dispense retraction steps when the dispense is a part of a multi-dispense.
560
+
561
+ Args:
562
+ trash_location: Location where we can drop tips or blowout, if set to do so
563
+ source_location: Location where we can blowout, if set to do so
564
+ source_well: Well where we can blowout, if set to do so
565
+ conditioning_volume: Conditioning volume used for this multi-dispense. Can be 0
566
+ add_final_air_gap: Whether we should add the final air gap of the step
567
+ is_last_retract: Whether this is the last retract of the multi-dispense steps,
568
+ i.e., this is part of the last dispense in the series of consecutive dispenses.
569
+ This dispense might not be the last dispense of the entire distribution.
570
+
571
+ This function is mostly similar to the single-dispense retract function except
572
+ that it handles air gaps differently based on the disposal volume, conditioning volume
573
+ and whether we are moving to another dispense or going back to the source.
574
+ """
575
+ # TODO: Raise error if retract is below the meniscus
576
+
577
+ assert self._transfer_properties.multi_dispense is not None
578
+
579
+ retract_props = self._transfer_properties.multi_dispense.retract
580
+ retract_point = absolute_point_from_position_reference_and_offset(
581
+ well=self._target_well,
582
+ position_reference=retract_props.position_reference,
583
+ offset=retract_props.offset,
584
+ )
585
+ retract_location = Location(
586
+ retract_point, labware=self._target_location.labware
587
+ )
588
+ tx_utils.raise_if_location_inside_liquid(
589
+ location=retract_location,
590
+ well_location=self._target_location,
591
+ well_core=self._target_well,
592
+ location_check_descriptors=LocationCheckDescriptors(
593
+ location_type="retract end",
594
+ pipetting_action="dispense",
595
+ ),
596
+ logger=log,
597
+ )
598
+ self._instrument.move_to(
599
+ location=retract_location,
600
+ well_core=self._target_well,
601
+ force_direct=True,
602
+ minimum_z_height=None,
603
+ speed=retract_props.speed,
604
+ )
605
+ retract_delay = retract_props.delay
606
+ if retract_delay.enabled and retract_delay.duration:
607
+ self._instrument.delay(retract_delay.duration)
608
+
609
+ blowout_props = retract_props.blowout
610
+ if (
611
+ is_last_retract
612
+ and blowout_props.enabled
613
+ and blowout_props.location == BlowoutLocation.DESTINATION
614
+ ):
615
+ assert blowout_props.flow_rate is not None
616
+ self._instrument.set_flow_rate(blow_out=blowout_props.flow_rate)
617
+ self._instrument.blow_out(
618
+ location=retract_location,
619
+ well_core=None,
620
+ in_place=True,
621
+ )
622
+ # A blowout will remove all air gap and liquid (disposal volume) from the tip
623
+ # so delete them from tip state (although practically, there will not be
624
+ # any air gaps in the tip before blowing out in the destination well)
625
+ self._tip_state.delete_last_air_gap_and_liquid()
626
+ self._tip_state.ready_to_aspirate = False
627
+
628
+ # A retract will perform total of two air gaps if we need to blow out in source or trash:
629
+ # - 1st air gap: added before leaving the destination volume to go to src/ trash
630
+ # - 2nd air gap: added before leaving the blowout location to go to src or tip drop location
631
+ # But if blowout is disabled or is set to Destination well, then only one air gap
632
+ # will be added after retracting, before moving to src or tip drop location.
633
+ # `is_final_air_gap_of_current_retract` tells us whether the next air gap
634
+ # we will be adding, is going to be the last air gap of this step.
635
+ is_final_air_gap_of_current_retract = (
636
+ blowout_props.enabled
637
+ and blowout_props.location == BlowoutLocation.DESTINATION
638
+ ) or not blowout_props.enabled
639
+
640
+ # Whether we should add the next air gap depends on the cases as shown below.
641
+ # The main points when deciding this-
642
+ # - When we have used a conditioning volume, we do not want to add air gaps
643
+ # while there's still liquid in tip for dispensing
644
+ # - If we are not using conditioning volume then we want to add gaps just like
645
+ # we do during the one-to-one transfers
646
+ # - If this will be the last air gap of the step, if the above two conditions
647
+ # indicate that we should be adding an air gap, use `add_final_air_gap` as
648
+ # the final decider of whether to add the air gap.
649
+ if is_final_air_gap_of_current_retract:
650
+ if conditioning_volume > 0:
651
+ add_air_gap = is_last_retract and add_final_air_gap
652
+ else:
653
+ add_air_gap = add_final_air_gap
654
+ else:
655
+ if conditioning_volume > 0:
656
+ add_air_gap = is_last_retract
657
+ else:
658
+ add_air_gap = True
659
+
660
+ # Regardless of the blowout location, do touch tip
661
+ # when leaving the dispense well.
662
+ # Add an air gap depending on conditioning volume + whether this is
663
+ # the last step of a multi-dispense sequence + whether this is the last step
664
+ # of the entire liquid distribution.
665
+ self._do_touch_tip_and_air_gap(
666
+ touch_tip_properties=retract_props.touch_tip,
667
+ location=retract_location,
668
+ well=self._target_well,
669
+ add_air_gap=add_air_gap,
670
+ )
671
+
672
+ if (
673
+ is_last_retract # We can do a blowout only on the last multi-dispense step
674
+ and blowout_props.enabled
675
+ and blowout_props.location != BlowoutLocation.DESTINATION
676
+ ):
677
+ assert blowout_props.flow_rate is not None
678
+ self._instrument.set_flow_rate(blow_out=blowout_props.flow_rate)
679
+ touch_tip_and_air_gap_location: Optional[Location]
680
+ if blowout_props.location == BlowoutLocation.SOURCE:
681
+ if source_location is None or source_well is None:
682
+ raise RuntimeError(
683
+ "Blowout location is 'source' but source location &/or well is not provided."
684
+ )
685
+ # TODO: check if we should add a blowout location z-offset in liq class definition
686
+ self._instrument.blow_out(
687
+ location=Location(
688
+ source_well.get_top(0), labware=source_location.labware
689
+ ),
690
+ well_core=source_well,
691
+ in_place=False,
692
+ )
693
+ touch_tip_and_air_gap_location = Location(
694
+ source_well.get_top(0), labware=source_location.labware
695
+ )
696
+ touch_tip_and_air_gap_well = source_well
697
+ else:
698
+ self._instrument.blow_out(
699
+ location=trash_location,
700
+ well_core=None,
701
+ in_place=False,
702
+ )
703
+ touch_tip_and_air_gap_location = (
704
+ trash_location if isinstance(trash_location, Location) else None
705
+ )
706
+ touch_tip_and_air_gap_well = (
707
+ # We have already established that trash location of `Location` type
708
+ # has its `labware` as `Well` type.
709
+ trash_location.labware.as_well()._core # type: ignore[assignment]
710
+ if isinstance(trash_location, Location)
711
+ else None
712
+ )
713
+ # A blowout will remove all air gap and liquid (disposal volume) from the tip
714
+ # so delete them from tip state
715
+ self._tip_state.delete_last_air_gap_and_liquid()
716
+ self._tip_state.ready_to_aspirate = False
717
+
718
+ # Do touch tip and air gap again after blowing out into source well or trash
719
+ self._do_touch_tip_and_air_gap(
720
+ touch_tip_properties=retract_props.touch_tip,
721
+ location=touch_tip_and_air_gap_location,
722
+ well=touch_tip_and_air_gap_well,
723
+ add_air_gap=(
724
+ # Same check as before for when it's the final air gap of current retract
725
+ conditioning_volume > 0
726
+ and is_last_retract
727
+ and add_final_air_gap
728
+ ),
729
+ )
730
+
731
+ def _do_touch_tip_and_air_gap(
732
+ self,
733
+ touch_tip_properties: TouchTipProperties,
734
+ location: Optional[Location],
735
+ well: Optional[WellCore],
736
+ add_air_gap: bool,
737
+ ) -> None:
738
+ """Perform touch tip and air gap as part of post-dispense retract."""
739
+ if touch_tip_properties.enabled:
740
+ assert (
741
+ touch_tip_properties.speed is not None
742
+ and touch_tip_properties.z_offset is not None
743
+ and touch_tip_properties.mm_to_edge is not None
744
+ )
745
+ # TODO:, check that when blow out is a non-dest-well,
746
+ # whether the touch tip params from transfer props should be used for
747
+ # both dest-well touch tip and non-dest-well touch tip.
748
+ if well is not None and location is not None:
749
+ try:
750
+ self._instrument.touch_tip(
751
+ location=location,
752
+ well_core=well,
753
+ radius=1,
754
+ z_offset=touch_tip_properties.z_offset,
755
+ speed=touch_tip_properties.speed,
756
+ mm_from_edge=touch_tip_properties.mm_to_edge,
757
+ )
758
+ except TouchTipDisabledError:
759
+ # TODO: log a warning
760
+ pass
761
+
762
+ # Move back to the 'retract' position
763
+ self._instrument.move_to(
764
+ location=location,
765
+ well_core=well,
766
+ force_direct=True,
767
+ minimum_z_height=None,
768
+ # Full speed because the tip will already be out of the liquid
769
+ speed=None,
770
+ )
771
+
772
+ # TODO: check if it is okay to just do `prepare_to_aspirate` unconditionally
773
+ if not self._tip_state.ready_to_aspirate:
774
+ self._instrument.prepare_to_aspirate()
775
+ self._tip_state.ready_to_aspirate = True
776
+ if add_air_gap:
777
+ self._add_air_gap(
778
+ air_gap_volume=self._transfer_properties.aspirate.retract.air_gap_by_volume.get_for_volume(
779
+ 0
780
+ )
781
+ )
782
+
783
+ def _add_air_gap(self, air_gap_volume: float) -> None:
784
+ """Add an air gap."""
785
+ if air_gap_volume == 0:
786
+ return
787
+ aspirate_props = self._transfer_properties.aspirate
788
+ correction_volume = aspirate_props.correction_by_volume.get_for_volume(
789
+ air_gap_volume
790
+ )
791
+ # The minimum flow rate should be air_gap_volume per second
792
+ flow_rate = max(
793
+ aspirate_props.flow_rate_by_volume.get_for_volume(air_gap_volume),
794
+ air_gap_volume,
795
+ )
796
+ self._instrument.air_gap_in_place(
797
+ volume=air_gap_volume,
798
+ flow_rate=flow_rate,
799
+ correction_volume=correction_volume,
800
+ )
801
+ delay_props = aspirate_props.delay
802
+ if delay_props.enabled and delay_props.duration:
803
+ self._instrument.delay(delay_props.duration)
804
+ self._tip_state.append_air_gap(air_gap_volume)
805
+
806
+ def _remove_air_gap(self, location: Location) -> None:
807
+ """Remove a previously added air gap."""
808
+ last_air_gap = self._tip_state.last_liquid_and_air_gap_in_tip.air_gap
809
+ if last_air_gap == 0:
810
+ return
811
+
812
+ dispense_props = self._transfer_properties.dispense
813
+ correction_volume = dispense_props.correction_by_volume.get_for_volume(
814
+ last_air_gap
815
+ )
816
+ # The minimum flow rate should be air_gap_volume per second
817
+ flow_rate = max(
818
+ dispense_props.flow_rate_by_volume.get_for_volume(last_air_gap),
819
+ last_air_gap,
820
+ )
821
+ self._instrument.dispense(
822
+ location=location,
823
+ well_core=None,
824
+ volume=last_air_gap,
825
+ rate=1,
826
+ flow_rate=flow_rate,
827
+ in_place=True,
828
+ push_out=0,
829
+ correction_volume=correction_volume,
830
+ )
831
+ self._tip_state.delete_air_gap(last_air_gap)
832
+ dispense_delay = dispense_props.delay
833
+ if dispense_delay.enabled and dispense_delay.duration:
834
+ self._instrument.delay(dispense_delay.duration)
835
+
836
+
837
+ def absolute_point_from_position_reference_and_offset(
838
+ well: WellCore,
839
+ position_reference: PositionReference,
840
+ offset: Coordinate,
841
+ ) -> Point:
842
+ """Return the absolute point, given the well, the position reference and offset."""
843
+ match position_reference:
844
+ case PositionReference.WELL_TOP:
845
+ reference_point = well.get_top(0)
846
+ case PositionReference.WELL_BOTTOM:
847
+ reference_point = well.get_bottom(0)
848
+ case PositionReference.WELL_CENTER:
849
+ reference_point = well.get_center()
850
+ case PositionReference.LIQUID_MENISCUS:
851
+ meniscus_point = well.get_meniscus()
852
+ if not isinstance(meniscus_point, Point):
853
+ reference_point = well.get_center()
854
+ else:
855
+ reference_point = meniscus_point
856
+ case _:
857
+ raise ValueError(f"Unknown position reference {position_reference}")
858
+ return reference_point + Point(offset.x, offset.y, offset.z)