opentrons 8.1.0a0__py2.py3-none-any.whl → 8.2.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.
Files changed (238) hide show
  1. opentrons/cli/analyze.py +71 -7
  2. opentrons/config/__init__.py +9 -0
  3. opentrons/config/advanced_settings.py +22 -0
  4. opentrons/config/defaults_ot3.py +14 -36
  5. opentrons/config/feature_flags.py +4 -0
  6. opentrons/config/types.py +6 -17
  7. opentrons/drivers/absorbance_reader/abstract.py +27 -3
  8. opentrons/drivers/absorbance_reader/async_byonoy.py +208 -154
  9. opentrons/drivers/absorbance_reader/driver.py +24 -15
  10. opentrons/drivers/absorbance_reader/hid_protocol.py +79 -50
  11. opentrons/drivers/absorbance_reader/simulator.py +32 -6
  12. opentrons/drivers/types.py +23 -1
  13. opentrons/execute.py +2 -2
  14. opentrons/hardware_control/api.py +18 -10
  15. opentrons/hardware_control/backends/controller.py +3 -2
  16. opentrons/hardware_control/backends/flex_protocol.py +11 -5
  17. opentrons/hardware_control/backends/ot3controller.py +18 -50
  18. opentrons/hardware_control/backends/ot3simulator.py +7 -6
  19. opentrons/hardware_control/backends/ot3utils.py +1 -0
  20. opentrons/hardware_control/instruments/ot2/pipette_handler.py +22 -82
  21. opentrons/hardware_control/instruments/ot3/pipette_handler.py +10 -2
  22. opentrons/hardware_control/module_control.py +43 -2
  23. opentrons/hardware_control/modules/__init__.py +7 -1
  24. opentrons/hardware_control/modules/absorbance_reader.py +232 -83
  25. opentrons/hardware_control/modules/errors.py +7 -0
  26. opentrons/hardware_control/modules/heater_shaker.py +8 -3
  27. opentrons/hardware_control/modules/magdeck.py +12 -3
  28. opentrons/hardware_control/modules/mod_abc.py +27 -2
  29. opentrons/hardware_control/modules/tempdeck.py +15 -7
  30. opentrons/hardware_control/modules/thermocycler.py +69 -3
  31. opentrons/hardware_control/modules/types.py +11 -5
  32. opentrons/hardware_control/modules/update.py +11 -5
  33. opentrons/hardware_control/modules/utils.py +3 -1
  34. opentrons/hardware_control/ot3_calibration.py +6 -6
  35. opentrons/hardware_control/ot3api.py +131 -94
  36. opentrons/hardware_control/poller.py +15 -11
  37. opentrons/hardware_control/protocols/__init__.py +1 -7
  38. opentrons/hardware_control/protocols/instrument_configurer.py +14 -2
  39. opentrons/hardware_control/protocols/liquid_handler.py +5 -0
  40. opentrons/hardware_control/protocols/position_estimator.py +3 -1
  41. opentrons/hardware_control/types.py +2 -0
  42. opentrons/legacy_commands/helpers.py +8 -2
  43. opentrons/motion_planning/__init__.py +2 -0
  44. opentrons/motion_planning/waypoints.py +32 -0
  45. opentrons/protocol_api/__init__.py +2 -1
  46. opentrons/protocol_api/_liquid.py +87 -1
  47. opentrons/protocol_api/_parameter_context.py +10 -1
  48. opentrons/protocol_api/core/engine/deck_conflict.py +0 -297
  49. opentrons/protocol_api/core/engine/instrument.py +29 -25
  50. opentrons/protocol_api/core/engine/labware.py +20 -4
  51. opentrons/protocol_api/core/engine/module_core.py +166 -17
  52. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +362 -0
  53. opentrons/protocol_api/core/engine/protocol.py +30 -2
  54. opentrons/protocol_api/core/instrument.py +2 -0
  55. opentrons/protocol_api/core/labware.py +4 -0
  56. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +2 -0
  57. opentrons/protocol_api/core/legacy/legacy_labware_core.py +5 -0
  58. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +6 -2
  59. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +2 -0
  60. opentrons/protocol_api/core/module.py +22 -4
  61. opentrons/protocol_api/core/protocol.py +6 -2
  62. opentrons/protocol_api/instrument_context.py +52 -20
  63. opentrons/protocol_api/labware.py +13 -1
  64. opentrons/protocol_api/module_contexts.py +115 -17
  65. opentrons/protocol_api/protocol_context.py +49 -5
  66. opentrons/protocol_api/validation.py +5 -3
  67. opentrons/protocol_engine/__init__.py +10 -9
  68. opentrons/protocol_engine/actions/__init__.py +3 -0
  69. opentrons/protocol_engine/actions/actions.py +30 -25
  70. opentrons/protocol_engine/actions/get_state_update.py +38 -0
  71. opentrons/protocol_engine/clients/sync_client.py +1 -1
  72. opentrons/protocol_engine/clients/transports.py +1 -1
  73. opentrons/protocol_engine/commands/__init__.py +0 -4
  74. opentrons/protocol_engine/commands/absorbance_reader/__init__.py +41 -11
  75. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +148 -0
  76. opentrons/protocol_engine/commands/absorbance_reader/initialize.py +65 -9
  77. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +148 -0
  78. opentrons/protocol_engine/commands/absorbance_reader/read.py +200 -0
  79. opentrons/protocol_engine/commands/aspirate.py +29 -16
  80. opentrons/protocol_engine/commands/aspirate_in_place.py +33 -16
  81. opentrons/protocol_engine/commands/blow_out.py +63 -14
  82. opentrons/protocol_engine/commands/blow_out_in_place.py +55 -13
  83. opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +2 -5
  84. opentrons/protocol_engine/commands/calibration/calibrate_module.py +3 -4
  85. opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +2 -5
  86. opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +6 -4
  87. opentrons/protocol_engine/commands/command.py +31 -18
  88. opentrons/protocol_engine/commands/command_unions.py +37 -24
  89. opentrons/protocol_engine/commands/comment.py +5 -3
  90. opentrons/protocol_engine/commands/configure_for_volume.py +11 -14
  91. opentrons/protocol_engine/commands/configure_nozzle_layout.py +9 -15
  92. opentrons/protocol_engine/commands/custom.py +5 -3
  93. opentrons/protocol_engine/commands/dispense.py +42 -20
  94. opentrons/protocol_engine/commands/dispense_in_place.py +32 -14
  95. opentrons/protocol_engine/commands/drop_tip.py +70 -16
  96. opentrons/protocol_engine/commands/drop_tip_in_place.py +59 -13
  97. opentrons/protocol_engine/commands/get_tip_presence.py +5 -3
  98. opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +6 -6
  99. opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +6 -6
  100. opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +6 -6
  101. opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +8 -6
  102. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +8 -4
  103. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +6 -4
  104. opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +6 -6
  105. opentrons/protocol_engine/commands/home.py +11 -5
  106. opentrons/protocol_engine/commands/liquid_probe.py +146 -88
  107. opentrons/protocol_engine/commands/load_labware.py +28 -5
  108. opentrons/protocol_engine/commands/load_liquid.py +18 -7
  109. opentrons/protocol_engine/commands/load_module.py +4 -6
  110. opentrons/protocol_engine/commands/load_pipette.py +18 -17
  111. opentrons/protocol_engine/commands/magnetic_module/disengage.py +6 -6
  112. opentrons/protocol_engine/commands/magnetic_module/engage.py +6 -4
  113. opentrons/protocol_engine/commands/move_labware.py +155 -23
  114. opentrons/protocol_engine/commands/move_relative.py +15 -3
  115. opentrons/protocol_engine/commands/move_to_addressable_area.py +29 -4
  116. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +13 -4
  117. opentrons/protocol_engine/commands/move_to_coordinates.py +11 -5
  118. opentrons/protocol_engine/commands/move_to_well.py +37 -10
  119. opentrons/protocol_engine/commands/pick_up_tip.py +51 -30
  120. opentrons/protocol_engine/commands/pipetting_common.py +47 -16
  121. opentrons/protocol_engine/commands/prepare_to_aspirate.py +62 -15
  122. opentrons/protocol_engine/commands/reload_labware.py +13 -4
  123. opentrons/protocol_engine/commands/retract_axis.py +6 -3
  124. opentrons/protocol_engine/commands/save_position.py +2 -3
  125. opentrons/protocol_engine/commands/set_rail_lights.py +5 -3
  126. opentrons/protocol_engine/commands/set_status_bar.py +5 -3
  127. opentrons/protocol_engine/commands/temperature_module/deactivate.py +6 -4
  128. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +3 -4
  129. opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +6 -6
  130. opentrons/protocol_engine/commands/thermocycler/__init__.py +19 -0
  131. opentrons/protocol_engine/commands/thermocycler/close_lid.py +8 -8
  132. opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +6 -4
  133. opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +6 -4
  134. opentrons/protocol_engine/commands/thermocycler/open_lid.py +8 -4
  135. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +165 -0
  136. opentrons/protocol_engine/commands/thermocycler/run_profile.py +6 -6
  137. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +3 -4
  138. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +3 -4
  139. opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +6 -4
  140. opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +6 -4
  141. opentrons/protocol_engine/commands/touch_tip.py +19 -7
  142. opentrons/protocol_engine/commands/unsafe/__init__.py +30 -0
  143. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +6 -4
  144. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +12 -4
  145. opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +5 -3
  146. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +208 -0
  147. opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py +77 -0
  148. opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +10 -4
  149. opentrons/protocol_engine/commands/verify_tip_presence.py +5 -5
  150. opentrons/protocol_engine/commands/wait_for_duration.py +5 -3
  151. opentrons/protocol_engine/commands/wait_for_resume.py +5 -3
  152. opentrons/protocol_engine/create_protocol_engine.py +60 -10
  153. opentrons/protocol_engine/engine_support.py +2 -1
  154. opentrons/protocol_engine/error_recovery_policy.py +14 -3
  155. opentrons/protocol_engine/errors/__init__.py +20 -0
  156. opentrons/protocol_engine/errors/error_occurrence.py +8 -3
  157. opentrons/protocol_engine/errors/exceptions.py +127 -2
  158. opentrons/protocol_engine/execution/__init__.py +2 -0
  159. opentrons/protocol_engine/execution/command_executor.py +22 -13
  160. opentrons/protocol_engine/execution/create_queue_worker.py +5 -1
  161. opentrons/protocol_engine/execution/door_watcher.py +1 -1
  162. opentrons/protocol_engine/execution/equipment.py +2 -1
  163. opentrons/protocol_engine/execution/error_recovery_hardware_state_synchronizer.py +101 -0
  164. opentrons/protocol_engine/execution/gantry_mover.py +4 -2
  165. opentrons/protocol_engine/execution/hardware_stopper.py +3 -3
  166. opentrons/protocol_engine/execution/heater_shaker_movement_flagger.py +1 -4
  167. opentrons/protocol_engine/execution/labware_movement.py +73 -22
  168. opentrons/protocol_engine/execution/movement.py +17 -7
  169. opentrons/protocol_engine/execution/pipetting.py +7 -4
  170. opentrons/protocol_engine/execution/queue_worker.py +6 -2
  171. opentrons/protocol_engine/execution/run_control.py +1 -1
  172. opentrons/protocol_engine/execution/thermocycler_movement_flagger.py +1 -1
  173. opentrons/protocol_engine/execution/thermocycler_plate_lifter.py +2 -1
  174. opentrons/protocol_engine/execution/tip_handler.py +77 -43
  175. opentrons/protocol_engine/notes/__init__.py +14 -2
  176. opentrons/protocol_engine/notes/notes.py +18 -1
  177. opentrons/protocol_engine/plugins.py +1 -1
  178. opentrons/protocol_engine/protocol_engine.py +47 -31
  179. opentrons/protocol_engine/resources/__init__.py +2 -0
  180. opentrons/protocol_engine/resources/deck_data_provider.py +19 -5
  181. opentrons/protocol_engine/resources/file_provider.py +161 -0
  182. opentrons/protocol_engine/resources/fixture_validation.py +11 -1
  183. opentrons/protocol_engine/resources/labware_validation.py +10 -0
  184. opentrons/protocol_engine/state/__init__.py +0 -70
  185. opentrons/protocol_engine/state/addressable_areas.py +1 -1
  186. opentrons/protocol_engine/state/command_history.py +21 -2
  187. opentrons/protocol_engine/state/commands.py +110 -31
  188. opentrons/protocol_engine/state/files.py +59 -0
  189. opentrons/protocol_engine/state/frustum_helpers.py +440 -0
  190. opentrons/protocol_engine/state/geometry.py +445 -59
  191. opentrons/protocol_engine/state/labware.py +264 -84
  192. opentrons/protocol_engine/state/liquids.py +1 -1
  193. opentrons/protocol_engine/state/module_substates/absorbance_reader_substate.py +21 -3
  194. opentrons/protocol_engine/state/modules.py +145 -90
  195. opentrons/protocol_engine/state/motion.py +33 -14
  196. opentrons/protocol_engine/state/pipettes.py +157 -317
  197. opentrons/protocol_engine/state/state.py +30 -1
  198. opentrons/protocol_engine/state/state_summary.py +3 -0
  199. opentrons/protocol_engine/state/tips.py +69 -114
  200. opentrons/protocol_engine/state/update_types.py +424 -0
  201. opentrons/protocol_engine/state/wells.py +236 -0
  202. opentrons/protocol_engine/types.py +90 -0
  203. opentrons/protocol_reader/file_format_validator.py +83 -15
  204. opentrons/protocol_runner/json_translator.py +21 -5
  205. opentrons/protocol_runner/legacy_command_mapper.py +27 -6
  206. opentrons/protocol_runner/legacy_context_plugin.py +27 -71
  207. opentrons/protocol_runner/protocol_runner.py +6 -3
  208. opentrons/protocol_runner/run_orchestrator.py +41 -6
  209. opentrons/protocols/advanced_control/mix.py +3 -5
  210. opentrons/protocols/advanced_control/transfers.py +125 -56
  211. opentrons/protocols/api_support/constants.py +1 -1
  212. opentrons/protocols/api_support/definitions.py +1 -1
  213. opentrons/protocols/api_support/labware_like.py +4 -4
  214. opentrons/protocols/api_support/tip_tracker.py +2 -2
  215. opentrons/protocols/api_support/types.py +15 -2
  216. opentrons/protocols/api_support/util.py +30 -42
  217. opentrons/protocols/duration/errors.py +1 -1
  218. opentrons/protocols/duration/estimator.py +50 -29
  219. opentrons/protocols/execution/dev_types.py +2 -2
  220. opentrons/protocols/execution/execute_json_v4.py +15 -10
  221. opentrons/protocols/execution/execute_python.py +8 -3
  222. opentrons/protocols/geometry/planning.py +12 -12
  223. opentrons/protocols/labware.py +17 -33
  224. opentrons/protocols/parameters/csv_parameter_interface.py +3 -1
  225. opentrons/simulate.py +3 -3
  226. opentrons/types.py +30 -3
  227. opentrons/util/logging_config.py +34 -0
  228. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/METADATA +5 -4
  229. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/RECORD +235 -223
  230. opentrons/protocol_engine/commands/absorbance_reader/measure.py +0 -94
  231. opentrons/protocol_engine/commands/configuring_common.py +0 -26
  232. opentrons/protocol_runner/thread_async_queue.py +0 -174
  233. /opentrons/protocol_engine/state/{abstract_store.py → _abstract_store.py} +0 -0
  234. /opentrons/protocol_engine/state/{move_types.py → _move_types.py} +0 -0
  235. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/LICENSE +0 -0
  236. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/WHEEL +0 -0
  237. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/entry_points.txt +0 -0
  238. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/top_level.txt +0 -0
@@ -12,20 +12,24 @@ from opentrons_shared_data.labware.constants import WELL_NAME_PATTERN
12
12
  from opentrons_shared_data.deck.types import CutoutFixture
13
13
  from opentrons_shared_data.pipette import PIPETTE_X_SPAN
14
14
  from opentrons_shared_data.pipette.types import ChannelCount
15
+ from opentrons.protocols.models import LabwareDefinition
15
16
 
16
17
  from .. import errors
17
18
  from ..errors import (
18
19
  LabwareNotLoadedOnLabwareError,
19
20
  LabwareNotLoadedOnModuleError,
20
21
  LabwareMovementNotAllowedError,
22
+ OperationLocationNotInWellError,
21
23
  )
22
- from ..resources import fixture_validation
24
+ from ..resources import fixture_validation, labware_validation
23
25
  from ..types import (
24
26
  OFF_DECK_LOCATION,
25
27
  LoadedLabware,
26
28
  LoadedModule,
27
29
  WellLocation,
30
+ LiquidHandlingWellLocation,
28
31
  DropTipWellLocation,
32
+ PickUpTipWellLocation,
29
33
  WellOrigin,
30
34
  DropTipWellOrigin,
31
35
  WellOffset,
@@ -45,12 +49,18 @@ from ..types import (
45
49
  AddressableOffsetVector,
46
50
  StagingSlotLocation,
47
51
  LabwareOffsetLocation,
52
+ ModuleModel,
48
53
  )
49
54
  from .config import Config
50
55
  from .labware import LabwareView
56
+ from .wells import WellView
51
57
  from .modules import ModuleView
52
58
  from .pipettes import PipetteView
53
59
  from .addressable_areas import AddressableAreaView
60
+ from .frustum_helpers import (
61
+ find_volume_at_well_height,
62
+ find_height_at_well_volume,
63
+ )
54
64
 
55
65
 
56
66
  SLOT_WIDTH = 128
@@ -96,6 +106,7 @@ class GeometryView:
96
106
  self,
97
107
  config: Config,
98
108
  labware_view: LabwareView,
109
+ well_view: WellView,
99
110
  module_view: ModuleView,
100
111
  pipette_view: PipetteView,
101
112
  addressable_area_view: AddressableAreaView,
@@ -103,6 +114,7 @@ class GeometryView:
103
114
  """Initialize a GeometryView instance."""
104
115
  self._config = config
105
116
  self._labware = labware_view
117
+ self._wells = well_view
106
118
  self._modules = module_view
107
119
  self._pipettes = pipette_view
108
120
  self._addressable_areas = addressable_area_view
@@ -250,27 +262,33 @@ class GeometryView:
250
262
  return min_travel_z
251
263
 
252
264
  def get_labware_parent_nominal_position(self, labware_id: str) -> Point:
253
- """Get the position of the labware's uncalibrated parent slot (deck, module, or another labware)."""
265
+ """Get the position of the labware's uncalibrated parent (deck slot, module, or another labware)."""
254
266
  try:
255
- slot_name = self.get_ancestor_slot_name(labware_id).id
267
+ addressable_area_name = self.get_ancestor_slot_name(labware_id).id
256
268
  except errors.LocationIsStagingSlotError:
257
- slot_name = self._get_staging_slot_name(labware_id)
258
- slot_pos = self._addressable_areas.get_addressable_area_position(slot_name)
259
- labware_data = self._labware.get(labware_id)
260
- offset = self._get_labware_position_offset(labware_id, labware_data.location)
269
+ addressable_area_name = self._get_staging_slot_name(labware_id)
270
+ except errors.LocationIsLidDockSlotError:
271
+ addressable_area_name = self._get_lid_dock_slot_name(labware_id)
272
+ parent_pos = self._addressable_areas.get_addressable_area_position(
273
+ addressable_area_name
274
+ )
275
+
276
+ offset_from_parent = self._get_offset_from_parent(
277
+ child_definition=self._labware.get_definition(labware_id),
278
+ parent=self._labware.get(labware_id).location,
279
+ )
261
280
 
262
281
  return Point(
263
- slot_pos.x + offset.x,
264
- slot_pos.y + offset.y,
265
- slot_pos.z + offset.z,
282
+ parent_pos.x + offset_from_parent.x,
283
+ parent_pos.y + offset_from_parent.y,
284
+ parent_pos.z + offset_from_parent.z,
266
285
  )
267
286
 
268
- def _get_labware_position_offset(
269
- self, labware_id: str, labware_location: LabwareLocation
287
+ def _get_offset_from_parent(
288
+ self, child_definition: LabwareDefinition, parent: LabwareLocation
270
289
  ) -> LabwareOffsetVector:
271
- """Gets the offset vector of a labware on the given location.
290
+ """Gets the offset vector of a labware placed on the given location.
272
291
 
273
- NOTE: Not to be confused with LPC offset.
274
292
  - For labware on Deck Slot: returns an offset of (0, 0, 0)
275
293
  - For labware on a Module: returns the nominal offset for the labware's position
276
294
  when placed on the specified module (using slot-transformed labwareOffset
@@ -281,40 +299,42 @@ class GeometryView:
281
299
  on modules as well as stacking overlaps.
282
300
  Does not include module calibration offset or LPC offset.
283
301
  """
284
- if isinstance(labware_location, (AddressableAreaLocation, DeckSlotLocation)):
302
+ if isinstance(parent, (AddressableAreaLocation, DeckSlotLocation)):
285
303
  return LabwareOffsetVector(x=0, y=0, z=0)
286
- elif isinstance(labware_location, ModuleLocation):
287
- module_id = labware_location.moduleId
288
- module_offset = self._modules.get_nominal_module_offset(
304
+ elif isinstance(parent, ModuleLocation):
305
+ module_id = parent.moduleId
306
+ module_to_child = self._modules.get_nominal_offset_to_child(
289
307
  module_id=module_id, addressable_areas=self._addressable_areas
290
308
  )
291
309
  module_model = self._modules.get_connected_model(module_id)
292
310
  stacking_overlap = self._labware.get_module_overlap_offsets(
293
- labware_id, module_model
311
+ child_definition, module_model
294
312
  )
295
313
  return LabwareOffsetVector(
296
- x=module_offset.x - stacking_overlap.x,
297
- y=module_offset.y - stacking_overlap.y,
298
- z=module_offset.z - stacking_overlap.z,
314
+ x=module_to_child.x - stacking_overlap.x,
315
+ y=module_to_child.y - stacking_overlap.y,
316
+ z=module_to_child.z - stacking_overlap.z,
317
+ )
318
+ elif isinstance(parent, OnLabwareLocation):
319
+ on_labware = self._labware.get(parent.labwareId)
320
+ on_labware_dimensions = self._labware.get_dimensions(
321
+ labware_id=on_labware.id
299
322
  )
300
- elif isinstance(labware_location, OnLabwareLocation):
301
- on_labware = self._labware.get(labware_location.labwareId)
302
- on_labware_dimensions = self._labware.get_dimensions(on_labware.id)
303
323
  stacking_overlap = self._labware.get_labware_overlap_offsets(
304
- labware_id=labware_id, below_labware_name=on_labware.loadName
324
+ definition=child_definition, below_labware_name=on_labware.loadName
305
325
  )
306
326
  labware_offset = LabwareOffsetVector(
307
327
  x=stacking_overlap.x,
308
328
  y=stacking_overlap.y,
309
329
  z=on_labware_dimensions.z - stacking_overlap.z,
310
330
  )
311
- return labware_offset + self._get_labware_position_offset(
312
- on_labware.id, on_labware.location
331
+ return labware_offset + self._get_offset_from_parent(
332
+ self._labware.get_definition(on_labware.id), on_labware.location
313
333
  )
314
334
  else:
315
335
  raise errors.LabwareNotOnDeckError(
316
- f"Cannot access labware {labware_id} since it is not on the deck. "
317
- f"Either it has been loaded off-deck or its been moved off-deck."
336
+ "Cannot access labware since it is not on the deck. "
337
+ "Either it has been loaded off-deck or its been moved off-deck."
318
338
  )
319
339
 
320
340
  def _normalize_module_calibration_offset(
@@ -405,11 +425,51 @@ class GeometryView:
405
425
  z=origin_pos.z + cal_offset.z,
406
426
  )
407
427
 
428
+ WellLocations = Union[
429
+ WellLocation, LiquidHandlingWellLocation, PickUpTipWellLocation
430
+ ]
431
+
432
+ def validate_well_position(
433
+ self,
434
+ well_location: WellLocations,
435
+ z_offset: float,
436
+ pipette_id: Optional[str] = None,
437
+ ) -> None:
438
+ """Raise exception if operation location is not within well.
439
+
440
+ Primarily this checks if there is not enough liquid in a well to do meniscus-relative static aspiration.
441
+ """
442
+ if well_location.origin == WellOrigin.MENISCUS:
443
+ assert pipette_id is not None
444
+ lld_min_height = self._pipettes.get_current_tip_lld_settings(
445
+ pipette_id=pipette_id
446
+ )
447
+ if z_offset < lld_min_height:
448
+ if isinstance(well_location, PickUpTipWellLocation):
449
+ raise OperationLocationNotInWellError(
450
+ f"Specifying {well_location.origin} with an offset of {well_location.offset} results in an operation location that could be below the bottom of the well"
451
+ )
452
+ else:
453
+ raise OperationLocationNotInWellError(
454
+ f"Specifying {well_location.origin} with an offset of {well_location.offset} and a volume offset of {well_location.volumeOffset} results in an operation location that could be below the bottom of the well"
455
+ )
456
+ elif z_offset < 0:
457
+ if isinstance(well_location, PickUpTipWellLocation):
458
+ raise OperationLocationNotInWellError(
459
+ f"Specifying {well_location.origin} with an offset of {well_location.offset} results in an operation location below the bottom of the well"
460
+ )
461
+ else:
462
+ raise OperationLocationNotInWellError(
463
+ f"Specifying {well_location.origin} with an offset of {well_location.offset} and a volume offset of {well_location.volumeOffset} results in an operation location below the bottom of the well"
464
+ )
465
+
408
466
  def get_well_position(
409
467
  self,
410
468
  labware_id: str,
411
469
  well_name: str,
412
- well_location: Optional[WellLocation] = None,
470
+ well_location: Optional[WellLocations] = None,
471
+ operation_volume: Optional[float] = None,
472
+ pipette_id: Optional[str] = None,
413
473
  ) -> Point:
414
474
  """Given relative well location in a labware, get absolute position."""
415
475
  labware_pos = self.get_labware_position(labware_id)
@@ -419,10 +479,17 @@ class GeometryView:
419
479
  offset = WellOffset(x=0, y=0, z=well_depth)
420
480
  if well_location is not None:
421
481
  offset = well_location.offset
422
- if well_location.origin == WellOrigin.TOP:
423
- offset = offset.copy(update={"z": offset.z + well_depth})
424
- elif well_location.origin == WellOrigin.CENTER:
425
- offset = offset.copy(update={"z": offset.z + well_depth / 2.0})
482
+ offset_adjustment = self.get_well_offset_adjustment(
483
+ labware_id=labware_id,
484
+ well_name=well_name,
485
+ well_location=well_location,
486
+ well_depth=well_depth,
487
+ operation_volume=operation_volume,
488
+ )
489
+ offset = offset.copy(update={"z": offset.z + offset_adjustment})
490
+ self.validate_well_position(
491
+ well_location=well_location, z_offset=offset.z, pipette_id=pipette_id
492
+ )
426
493
 
427
494
  return Point(
428
495
  x=labware_pos.x + offset.x + well_def.x,
@@ -457,6 +524,41 @@ class GeometryView:
457
524
 
458
525
  return WellLocation(offset=WellOffset(x=delta.x, y=delta.y, z=delta.z))
459
526
 
527
+ def get_relative_liquid_handling_well_location(
528
+ self,
529
+ labware_id: str,
530
+ well_name: str,
531
+ absolute_point: Point,
532
+ is_meniscus: Optional[bool] = None,
533
+ ) -> LiquidHandlingWellLocation:
534
+ """Given absolute position, get relative location of a well in a labware.
535
+
536
+ If is_meniscus is True, absolute_point will hold the z-offset in its z field.
537
+ """
538
+ if is_meniscus:
539
+ return LiquidHandlingWellLocation(
540
+ origin=WellOrigin.MENISCUS,
541
+ offset=WellOffset(x=0, y=0, z=absolute_point.z),
542
+ )
543
+ else:
544
+ well_absolute_point = self.get_well_position(labware_id, well_name)
545
+ delta = absolute_point - well_absolute_point
546
+ return LiquidHandlingWellLocation(
547
+ offset=WellOffset(x=delta.x, y=delta.y, z=delta.z)
548
+ )
549
+
550
+ def get_relative_pick_up_tip_well_location(
551
+ self,
552
+ labware_id: str,
553
+ well_name: str,
554
+ absolute_point: Point,
555
+ ) -> PickUpTipWellLocation:
556
+ """Given absolute position, get relative location of a well in a labware."""
557
+ well_absolute_point = self.get_well_position(labware_id, well_name)
558
+ delta = absolute_point - well_absolute_point
559
+
560
+ return PickUpTipWellLocation(offset=WellOffset(x=delta.x, y=delta.y, z=delta.z))
561
+
460
562
  def get_well_height(
461
563
  self,
462
564
  labware_id: str,
@@ -580,6 +682,14 @@ class GeometryView:
580
682
  ),
581
683
  )
582
684
 
685
+ def convert_pick_up_tip_well_location(
686
+ self, well_location: PickUpTipWellLocation
687
+ ) -> WellLocation:
688
+ """Convert PickUpTipWellLocation to WellLocation."""
689
+ return WellLocation(
690
+ origin=WellOrigin(well_location.origin.value), offset=well_location.offset
691
+ )
692
+
583
693
  # TODO(jbl 11-30-2023) fold this function into get_ancestor_slot_name see RSS-411
584
694
  def _get_staging_slot_name(self, labware_id: str) -> str:
585
695
  """Get the staging slot name that the labware is on."""
@@ -596,10 +706,18 @@ class GeometryView:
596
706
  "Cannot get staging slot name for labware not on staging slot."
597
707
  )
598
708
 
599
- def get_ancestor_slot_name(self, labware_id: str) -> DeckSlotName:
709
+ def _get_lid_dock_slot_name(self, labware_id: str) -> str:
710
+ """Get the staging slot name that the labware is on."""
711
+ labware_location = self._labware.get(labware_id).location
712
+ assert isinstance(labware_location, AddressableAreaLocation)
713
+ return labware_location.addressableAreaName
714
+
715
+ def get_ancestor_slot_name(
716
+ self, labware_id: str
717
+ ) -> Union[DeckSlotName, StagingSlotName]:
600
718
  """Get the slot name of the labware or the module that the labware is on."""
601
719
  labware = self._labware.get(labware_id)
602
- slot_name: DeckSlotName
720
+ slot_name: Union[DeckSlotName, StagingSlotName]
603
721
 
604
722
  if isinstance(labware.location, DeckSlotLocation):
605
723
  slot_name = labware.location.slotName
@@ -611,13 +729,14 @@ class GeometryView:
611
729
  slot_name = self.get_ancestor_slot_name(below_labware_id)
612
730
  elif isinstance(labware.location, AddressableAreaLocation):
613
731
  area_name = labware.location.addressableAreaName
614
- # TODO we might want to eventually return some sort of staging slot name when we're ready to work through
615
- # the linting nightmare it will create
616
- if fixture_validation.is_staging_slot(area_name):
617
- raise errors.LocationIsStagingSlotError(
618
- "Cannot get ancestor slot name for labware on staging slot."
732
+ if self._labware.is_absorbance_reader_lid(labware_id):
733
+ raise errors.LocationIsLidDockSlotError(
734
+ "Cannot get ancestor slot name for labware on lid dock slot."
619
735
  )
620
- slot_name = DeckSlotName.from_primitive(area_name)
736
+ elif fixture_validation.is_staging_slot(area_name):
737
+ slot_name = StagingSlotName.from_primitive(area_name)
738
+ else:
739
+ slot_name = DeckSlotName.from_primitive(area_name)
621
740
  elif labware.location == OFF_DECK_LOCATION:
622
741
  raise errors.LabwareNotOnDeckError(
623
742
  f"Labware {labware_id} does not have a slot associated with it"
@@ -650,7 +769,7 @@ class GeometryView:
650
769
 
651
770
  def get_labware_grip_point(
652
771
  self,
653
- labware_id: str,
772
+ labware_definition: LabwareDefinition,
654
773
  location: Union[
655
774
  DeckSlotLocation, ModuleLocation, OnLabwareLocation, AddressableAreaLocation
656
775
  ],
@@ -666,7 +785,7 @@ class GeometryView:
666
785
  z-position of labware bottom + grip height from labware bottom.
667
786
  """
668
787
  grip_height_from_labware_bottom = (
669
- self._labware.get_grip_height_from_labware_bottom(labware_id)
788
+ self._labware.get_grip_height_from_labware_bottom(labware_definition)
670
789
  )
671
790
  location_name: str
672
791
 
@@ -692,7 +811,9 @@ class GeometryView:
692
811
  ).slotName.id
693
812
  else: # OnLabwareLocation
694
813
  location_name = self.get_ancestor_slot_name(location.labwareId).id
695
- labware_offset = self._get_labware_position_offset(labware_id, location)
814
+ labware_offset = self._get_offset_from_parent(
815
+ child_definition=labware_definition, parent=location
816
+ )
696
817
  # Get the calibrated offset if the on labware location is on top of a module, otherwise return empty one
697
818
  cal_offset = self._get_calibrated_module_offset(location)
698
819
  offset = LabwareOffsetVector(
@@ -711,7 +832,9 @@ class GeometryView:
711
832
  )
712
833
 
713
834
  def get_extra_waypoints(
714
- self, location: Optional[CurrentPipetteLocation], to_slot: DeckSlotName
835
+ self,
836
+ location: Optional[CurrentPipetteLocation],
837
+ to_slot: Union[DeckSlotName, StagingSlotName],
715
838
  ) -> List[Tuple[float, float]]:
716
839
  """Get extra waypoints for movement if thermocycler needs to be dodged."""
717
840
  if location is not None:
@@ -770,8 +893,10 @@ class GeometryView:
770
893
  return maybe_labware or maybe_module or maybe_fixture or None
771
894
 
772
895
  @staticmethod
773
- def get_slot_column(slot_name: DeckSlotName) -> int:
896
+ def get_slot_column(slot_name: Union[DeckSlotName, StagingSlotName]) -> int:
774
897
  """Get the column number for the specified slot."""
898
+ if isinstance(slot_name, StagingSlotName):
899
+ return 4
775
900
  row_col_name = slot_name.to_ot3_equivalent()
776
901
  slot_name_match = WELL_NAME_PATTERN.match(row_col_name.value)
777
902
  assert (
@@ -962,17 +1087,22 @@ class GeometryView:
962
1087
  from_location: OnDeckLabwareLocation,
963
1088
  to_location: OnDeckLabwareLocation,
964
1089
  additional_offset_vector: LabwareMovementOffsetData,
1090
+ current_labware: LabwareDefinition,
965
1091
  ) -> LabwareMovementOffsetData:
966
1092
  """Calculate the final labware offset vector to use in labware movement."""
967
1093
  pick_up_offset = (
968
1094
  self.get_total_nominal_gripper_offset_for_move_type(
969
- location=from_location, move_type=_GripperMoveType.PICK_UP_LABWARE
1095
+ location=from_location,
1096
+ move_type=_GripperMoveType.PICK_UP_LABWARE,
1097
+ current_labware=current_labware,
970
1098
  )
971
1099
  + additional_offset_vector.pickUpOffset
972
1100
  )
973
1101
  drop_offset = (
974
1102
  self.get_total_nominal_gripper_offset_for_move_type(
975
- location=to_location, move_type=_GripperMoveType.DROP_LABWARE
1103
+ location=to_location,
1104
+ move_type=_GripperMoveType.DROP_LABWARE,
1105
+ current_labware=current_labware,
976
1106
  )
977
1107
  + additional_offset_vector.dropOffset
978
1108
  )
@@ -1003,7 +1133,10 @@ class GeometryView:
1003
1133
  return location
1004
1134
 
1005
1135
  def get_total_nominal_gripper_offset_for_move_type(
1006
- self, location: OnDeckLabwareLocation, move_type: _GripperMoveType
1136
+ self,
1137
+ location: OnDeckLabwareLocation,
1138
+ move_type: _GripperMoveType,
1139
+ current_labware: LabwareDefinition,
1007
1140
  ) -> LabwareOffsetVector:
1008
1141
  """Get the total of the offsets to be used to pick up labware in its current location."""
1009
1142
  if move_type == _GripperMoveType.PICK_UP_LABWARE:
@@ -1019,14 +1152,45 @@ class GeometryView:
1019
1152
  location
1020
1153
  )
1021
1154
  ancestor = self._labware.get_parent_location(location.labwareId)
1155
+ extra_offset = LabwareOffsetVector(x=0, y=0, z=0)
1156
+ if (
1157
+ isinstance(ancestor, ModuleLocation)
1158
+ and self._modules._state.requested_model_by_id[ancestor.moduleId]
1159
+ == ModuleModel.THERMOCYCLER_MODULE_V2
1160
+ and labware_validation.validate_definition_is_lid(current_labware)
1161
+ ):
1162
+ if "lidOffsets" in current_labware.gripperOffsets.keys():
1163
+ extra_offset = LabwareOffsetVector(
1164
+ x=current_labware.gripperOffsets[
1165
+ "lidOffsets"
1166
+ ].pickUpOffset.x,
1167
+ y=current_labware.gripperOffsets[
1168
+ "lidOffsets"
1169
+ ].pickUpOffset.y,
1170
+ z=current_labware.gripperOffsets[
1171
+ "lidOffsets"
1172
+ ].pickUpOffset.z,
1173
+ )
1174
+ else:
1175
+ raise errors.LabwareOffsetDoesNotExistError(
1176
+ f"Labware Definition {current_labware.parameters.loadName} does not contain required field 'lidOffsets' of 'gripperOffsets'."
1177
+ )
1178
+
1022
1179
  assert isinstance(
1023
- ancestor, (DeckSlotLocation, ModuleLocation)
1180
+ ancestor,
1181
+ (
1182
+ DeckSlotLocation,
1183
+ ModuleLocation,
1184
+ OnLabwareLocation,
1185
+ AddressableAreaLocation,
1186
+ ),
1024
1187
  ), "No gripper offsets for off-deck labware"
1025
1188
  return (
1026
1189
  direct_parent_offset.pickUpOffset
1027
1190
  + self._nominal_gripper_offsets_for_location(
1028
1191
  location=ancestor
1029
1192
  ).pickUpOffset
1193
+ + extra_offset
1030
1194
  )
1031
1195
  else:
1032
1196
  if isinstance(
@@ -1041,16 +1205,65 @@ class GeometryView:
1041
1205
  location
1042
1206
  )
1043
1207
  ancestor = self._labware.get_parent_location(location.labwareId)
1208
+ extra_offset = LabwareOffsetVector(x=0, y=0, z=0)
1209
+ if (
1210
+ isinstance(ancestor, ModuleLocation)
1211
+ # todo(mm, 2024-11-06): Do not access private module state; only use public ModuleView methods.
1212
+ and self._modules._state.requested_model_by_id[ancestor.moduleId]
1213
+ == ModuleModel.THERMOCYCLER_MODULE_V2
1214
+ and labware_validation.validate_definition_is_lid(current_labware)
1215
+ ):
1216
+ if "lidOffsets" in current_labware.gripperOffsets.keys():
1217
+ extra_offset = LabwareOffsetVector(
1218
+ x=current_labware.gripperOffsets[
1219
+ "lidOffsets"
1220
+ ].pickUpOffset.x,
1221
+ y=current_labware.gripperOffsets[
1222
+ "lidOffsets"
1223
+ ].pickUpOffset.y,
1224
+ z=current_labware.gripperOffsets[
1225
+ "lidOffsets"
1226
+ ].pickUpOffset.z,
1227
+ )
1228
+ else:
1229
+ raise errors.LabwareOffsetDoesNotExistError(
1230
+ f"Labware Definition {current_labware.parameters.loadName} does not contain required field 'lidOffsets' of 'gripperOffsets'."
1231
+ )
1232
+
1044
1233
  assert isinstance(
1045
- ancestor, (DeckSlotLocation, ModuleLocation)
1234
+ ancestor,
1235
+ (
1236
+ DeckSlotLocation,
1237
+ ModuleLocation,
1238
+ OnLabwareLocation,
1239
+ AddressableAreaLocation,
1240
+ ),
1046
1241
  ), "No gripper offsets for off-deck labware"
1047
1242
  return (
1048
1243
  direct_parent_offset.dropOffset
1049
1244
  + self._nominal_gripper_offsets_for_location(
1050
1245
  location=ancestor
1051
1246
  ).dropOffset
1247
+ + extra_offset
1052
1248
  )
1053
1249
 
1250
+ # todo(mm, 2024-11-05): This may be incorrect because it does not take the following
1251
+ # offsets into account, which *are* taken into account for the actual gripper movement:
1252
+ #
1253
+ # * The pickup offset in the definition of the parent of the gripped labware.
1254
+ # * The "additional offset" or "user offset", e.g. the `pickUpOffset` and `dropOffset`
1255
+ # params in the `moveLabware` command.
1256
+ #
1257
+ # And this *does* take these extra offsets into account:
1258
+ #
1259
+ # * The labware's Labware Position Check offset
1260
+ #
1261
+ # For robustness, we should combine this with `get_gripper_labware_movement_waypoints()`.
1262
+ #
1263
+ # We should also be more explicit about which offsets act to move the gripper paddles
1264
+ # relative to the gripped labware, and which offsets act to change how the gripped
1265
+ # labware sits atop its parent. Those have different effects on how far the gripped
1266
+ # labware juts beyond the paddles while it's in transit.
1054
1267
  def check_gripper_labware_tip_collision(
1055
1268
  self,
1056
1269
  gripper_homed_position_z: float,
@@ -1058,18 +1271,22 @@ class GeometryView:
1058
1271
  current_location: OnDeckLabwareLocation,
1059
1272
  ) -> None:
1060
1273
  """Check for potential collision of tips against labware to be lifted."""
1061
- # TODO(cb, 2024-01-22): Remove the 1 and 8 channel special case once we are doing X axis validation
1274
+ labware_definition = self._labware.get_definition(labware_id)
1062
1275
  pipettes = self._pipettes.get_all()
1063
1276
  for pipette in pipettes:
1277
+ # TODO(cb, 2024-01-22): Remove the 1 and 8 channel special case once we are doing X axis validation
1064
1278
  if self._pipettes.get_channels(pipette.id) in [1, 8]:
1065
1279
  return
1066
1280
 
1067
1281
  tip = self._pipettes.get_attached_tip(pipette.id)
1068
1282
  if tip:
1283
+ # NOTE: This call to get_labware_highest_z() uses the labware's LPC offset,
1284
+ # which is an inconsistency between this and the actual gripper movement.
1285
+ # See the todo comment above this function.
1069
1286
  labware_top_z_when_gripped = gripper_homed_position_z + (
1070
1287
  self.get_labware_highest_z(labware_id=labware_id)
1071
1288
  - self.get_labware_grip_point(
1072
- labware_id=labware_id, location=current_location
1289
+ labware_definition=labware_definition, location=current_location
1073
1290
  ).z
1074
1291
  )
1075
1292
  # TODO(cb, 2024-01-18): Utilizing the nozzle map and labware X coordinates verify if collisions will occur on the X axis (analysis will use hard coded data to measure from the gripper critical point to the pipette mount)
@@ -1077,7 +1294,7 @@ class GeometryView:
1077
1294
  _PIPETTE_HOMED_POSITION_Z - tip.length
1078
1295
  ) < labware_top_z_when_gripped:
1079
1296
  raise LabwareMovementNotAllowedError(
1080
- f"Cannot move labware '{self._labware.get(labware_id).loadName}' when {int(tip.volume)} µL tips are attached."
1297
+ f"Cannot move labware '{labware_definition.parameters.loadName}' when {int(tip.volume)} µL tips are attached."
1081
1298
  )
1082
1299
  return
1083
1300
 
@@ -1112,20 +1329,30 @@ class GeometryView:
1112
1329
  """
1113
1330
  parent_location = self._labware.get_parent_location(labware_id)
1114
1331
  assert isinstance(
1115
- parent_location, (DeckSlotLocation, ModuleLocation)
1332
+ parent_location,
1333
+ (
1334
+ DeckSlotLocation,
1335
+ ModuleLocation,
1336
+ AddressableAreaLocation,
1337
+ OnLabwareLocation,
1338
+ ),
1116
1339
  ), "No gripper offsets for off-deck labware"
1117
1340
 
1118
1341
  if isinstance(parent_location, DeckSlotLocation):
1119
1342
  slot_name = parent_location.slotName
1343
+ elif isinstance(parent_location, AddressableAreaLocation):
1344
+ slot_name = self._addressable_areas.get_addressable_area_base_slot(
1345
+ parent_location.addressableAreaName
1346
+ )
1120
1347
  else:
1121
1348
  module_loc = self._modules.get_location(parent_location.moduleId)
1122
1349
  slot_name = module_loc.slotName
1123
1350
 
1124
- slot_based_offset = self._labware.get_labware_gripper_offsets(
1351
+ slot_based_offset = self._labware.get_child_gripper_offsets(
1125
1352
  labware_id=labware_id, slot_name=slot_name.to_ot3_equivalent()
1126
1353
  )
1127
1354
 
1128
- return slot_based_offset or self._labware.get_labware_gripper_offsets(
1355
+ return slot_based_offset or self._labware.get_child_gripper_offsets(
1129
1356
  labware_id=labware_id, slot_name=None
1130
1357
  )
1131
1358
 
@@ -1173,3 +1400,162 @@ class GeometryView:
1173
1400
  )
1174
1401
 
1175
1402
  return None
1403
+
1404
+ def get_well_offset_adjustment(
1405
+ self,
1406
+ labware_id: str,
1407
+ well_name: str,
1408
+ well_location: WellLocations,
1409
+ well_depth: float,
1410
+ operation_volume: Optional[float] = None,
1411
+ ) -> float:
1412
+ """Return a z-axis distance that accounts for well handling height and operation volume.
1413
+
1414
+ Distance is with reference to the well bottom.
1415
+ """
1416
+ # TODO(pbm, 10-23-24): refactor to smartly reduce height/volume conversions
1417
+ initial_handling_height = self.get_well_handling_height(
1418
+ labware_id=labware_id,
1419
+ well_name=well_name,
1420
+ well_location=well_location,
1421
+ well_depth=well_depth,
1422
+ )
1423
+ if isinstance(well_location, PickUpTipWellLocation):
1424
+ volume = 0.0
1425
+ elif isinstance(well_location.volumeOffset, float):
1426
+ volume = well_location.volumeOffset
1427
+ elif well_location.volumeOffset == "operationVolume":
1428
+ volume = operation_volume or 0.0
1429
+
1430
+ if volume:
1431
+ return self.get_well_height_after_volume(
1432
+ labware_id=labware_id,
1433
+ well_name=well_name,
1434
+ initial_height=initial_handling_height,
1435
+ volume=volume,
1436
+ )
1437
+ else:
1438
+ return initial_handling_height
1439
+
1440
+ def get_meniscus_height(
1441
+ self,
1442
+ labware_id: str,
1443
+ well_name: str,
1444
+ ) -> float:
1445
+ """Returns stored meniscus height in specified well."""
1446
+ well_liquid = self._wells.get_well_liquid_info(
1447
+ labware_id=labware_id, well_name=well_name
1448
+ )
1449
+ if (
1450
+ well_liquid.probed_height is not None
1451
+ and well_liquid.probed_height.height is not None
1452
+ ):
1453
+ return well_liquid.probed_height.height
1454
+ elif (
1455
+ well_liquid.loaded_volume is not None
1456
+ and well_liquid.loaded_volume.volume is not None
1457
+ ):
1458
+ return self.get_well_height_at_volume(
1459
+ labware_id=labware_id,
1460
+ well_name=well_name,
1461
+ volume=well_liquid.loaded_volume.volume,
1462
+ )
1463
+ elif (
1464
+ well_liquid.probed_volume is not None
1465
+ and well_liquid.probed_volume.volume is not None
1466
+ ):
1467
+ return self.get_well_height_at_volume(
1468
+ labware_id=labware_id,
1469
+ well_name=well_name,
1470
+ volume=well_liquid.probed_volume.volume,
1471
+ )
1472
+ else:
1473
+ raise errors.LiquidHeightUnknownError(
1474
+ "Must LiquidProbe or LoadLiquid before specifying WellOrigin.MENISCUS."
1475
+ )
1476
+
1477
+ def get_well_handling_height(
1478
+ self,
1479
+ labware_id: str,
1480
+ well_name: str,
1481
+ well_location: WellLocations,
1482
+ well_depth: float,
1483
+ ) -> float:
1484
+ """Return the handling height for a labware well (with reference to the well bottom)."""
1485
+ handling_height = 0.0
1486
+ if well_location.origin == WellOrigin.TOP:
1487
+ handling_height = well_depth
1488
+ elif well_location.origin == WellOrigin.CENTER:
1489
+ handling_height = well_depth / 2.0
1490
+ elif well_location.origin == WellOrigin.MENISCUS:
1491
+ handling_height = self.get_meniscus_height(
1492
+ labware_id=labware_id, well_name=well_name
1493
+ )
1494
+ return float(handling_height)
1495
+
1496
+ def get_well_height_after_volume(
1497
+ self, labware_id: str, well_name: str, initial_height: float, volume: float
1498
+ ) -> float:
1499
+ """Return the height of liquid in a labware well after a given volume has been handled.
1500
+
1501
+ This is given an initial handling height, with reference to the well bottom.
1502
+ """
1503
+ well_geometry = self._labware.get_well_geometry(
1504
+ labware_id=labware_id, well_name=well_name
1505
+ )
1506
+ initial_volume = find_volume_at_well_height(
1507
+ target_height=initial_height, well_geometry=well_geometry
1508
+ )
1509
+ final_volume = initial_volume + volume
1510
+ return find_height_at_well_volume(
1511
+ target_volume=final_volume, well_geometry=well_geometry
1512
+ )
1513
+
1514
+ def get_well_height_at_volume(
1515
+ self, labware_id: str, well_name: str, volume: float
1516
+ ) -> float:
1517
+ """Convert well volume to height."""
1518
+ well_geometry = self._labware.get_well_geometry(labware_id, well_name)
1519
+ return find_height_at_well_volume(
1520
+ target_volume=volume, well_geometry=well_geometry
1521
+ )
1522
+
1523
+ def get_well_volume_at_height(
1524
+ self, labware_id: str, well_name: str, height: float
1525
+ ) -> float:
1526
+ """Convert well height to volume."""
1527
+ well_geometry = self._labware.get_well_geometry(labware_id, well_name)
1528
+ return find_volume_at_well_height(
1529
+ target_height=height, well_geometry=well_geometry
1530
+ )
1531
+
1532
+ def validate_dispense_volume_into_well(
1533
+ self,
1534
+ labware_id: str,
1535
+ well_name: str,
1536
+ well_location: WellLocations,
1537
+ volume: float,
1538
+ ) -> None:
1539
+ """Raise InvalidDispenseVolumeError if planned dispense volume will overflow well."""
1540
+ well_def = self._labware.get_well_definition(labware_id, well_name)
1541
+ well_volumetric_capacity = well_def.totalLiquidVolume
1542
+ if well_location.origin == WellOrigin.MENISCUS:
1543
+ # TODO(pbm, 10-23-24): refactor to smartly reduce height/volume conversions
1544
+ well_geometry = self._labware.get_well_geometry(labware_id, well_name)
1545
+ meniscus_height = self.get_meniscus_height(
1546
+ labware_id=labware_id, well_name=well_name
1547
+ )
1548
+ meniscus_volume = find_volume_at_well_height(
1549
+ target_height=meniscus_height, well_geometry=well_geometry
1550
+ )
1551
+ remaining_volume = well_volumetric_capacity - meniscus_volume
1552
+ if volume > remaining_volume:
1553
+ raise errors.InvalidDispenseVolumeError(
1554
+ f"Attempting to dispense {volume}µL of liquid into a well that can currently only hold {remaining_volume}µL (well {well_name} in labware_id: {labware_id})"
1555
+ )
1556
+ else:
1557
+ # TODO(pbm, 10-08-24): factor in well (LabwareStore) state volume
1558
+ if volume > well_volumetric_capacity:
1559
+ raise errors.InvalidDispenseVolumeError(
1560
+ f"Attempting to dispense {volume}µL of liquid into a well that can only hold {well_volumetric_capacity}µL (well {well_name} in labware_id: {labware_id})"
1561
+ )