opentrons 8.3.1a1__py2.py3-none-any.whl → 8.4.0a1__py2.py3-none-any.whl

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