opentrons 8.1.0a0__py2.py3-none-any.whl → 8.2.0a0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (230) 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 +207 -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/instruments/ot2/pipette_handler.py +22 -82
  20. opentrons/hardware_control/instruments/ot3/pipette_handler.py +10 -2
  21. opentrons/hardware_control/module_control.py +43 -2
  22. opentrons/hardware_control/modules/__init__.py +7 -1
  23. opentrons/hardware_control/modules/absorbance_reader.py +230 -83
  24. opentrons/hardware_control/modules/errors.py +7 -0
  25. opentrons/hardware_control/modules/heater_shaker.py +8 -3
  26. opentrons/hardware_control/modules/magdeck.py +12 -3
  27. opentrons/hardware_control/modules/mod_abc.py +27 -2
  28. opentrons/hardware_control/modules/tempdeck.py +15 -7
  29. opentrons/hardware_control/modules/thermocycler.py +69 -3
  30. opentrons/hardware_control/modules/types.py +11 -5
  31. opentrons/hardware_control/modules/update.py +11 -5
  32. opentrons/hardware_control/modules/utils.py +3 -1
  33. opentrons/hardware_control/ot3_calibration.py +6 -6
  34. opentrons/hardware_control/ot3api.py +126 -89
  35. opentrons/hardware_control/poller.py +15 -11
  36. opentrons/hardware_control/protocols/__init__.py +1 -7
  37. opentrons/hardware_control/protocols/instrument_configurer.py +14 -2
  38. opentrons/hardware_control/protocols/liquid_handler.py +5 -0
  39. opentrons/motion_planning/__init__.py +2 -0
  40. opentrons/motion_planning/waypoints.py +32 -0
  41. opentrons/protocol_api/__init__.py +2 -1
  42. opentrons/protocol_api/_liquid.py +87 -1
  43. opentrons/protocol_api/_parameter_context.py +10 -1
  44. opentrons/protocol_api/core/engine/deck_conflict.py +0 -297
  45. opentrons/protocol_api/core/engine/instrument.py +29 -25
  46. opentrons/protocol_api/core/engine/labware.py +10 -2
  47. opentrons/protocol_api/core/engine/module_core.py +129 -17
  48. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +355 -0
  49. opentrons/protocol_api/core/engine/protocol.py +55 -2
  50. opentrons/protocol_api/core/instrument.py +2 -0
  51. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +2 -0
  52. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +5 -2
  53. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +2 -0
  54. opentrons/protocol_api/core/module.py +22 -4
  55. opentrons/protocol_api/core/protocol.py +5 -2
  56. opentrons/protocol_api/instrument_context.py +52 -20
  57. opentrons/protocol_api/labware.py +13 -1
  58. opentrons/protocol_api/module_contexts.py +68 -13
  59. opentrons/protocol_api/protocol_context.py +38 -4
  60. opentrons/protocol_api/validation.py +5 -3
  61. opentrons/protocol_engine/__init__.py +10 -9
  62. opentrons/protocol_engine/actions/__init__.py +5 -0
  63. opentrons/protocol_engine/actions/actions.py +42 -25
  64. opentrons/protocol_engine/actions/get_state_update.py +38 -0
  65. opentrons/protocol_engine/clients/sync_client.py +7 -1
  66. opentrons/protocol_engine/clients/transports.py +1 -1
  67. opentrons/protocol_engine/commands/__init__.py +0 -4
  68. opentrons/protocol_engine/commands/absorbance_reader/__init__.py +41 -11
  69. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +161 -0
  70. opentrons/protocol_engine/commands/absorbance_reader/initialize.py +53 -9
  71. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +160 -0
  72. opentrons/protocol_engine/commands/absorbance_reader/read.py +196 -0
  73. opentrons/protocol_engine/commands/aspirate.py +29 -16
  74. opentrons/protocol_engine/commands/aspirate_in_place.py +32 -15
  75. opentrons/protocol_engine/commands/blow_out.py +63 -14
  76. opentrons/protocol_engine/commands/blow_out_in_place.py +55 -13
  77. opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +2 -5
  78. opentrons/protocol_engine/commands/calibration/calibrate_module.py +3 -4
  79. opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +2 -5
  80. opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +6 -4
  81. opentrons/protocol_engine/commands/command.py +28 -17
  82. opentrons/protocol_engine/commands/command_unions.py +37 -24
  83. opentrons/protocol_engine/commands/comment.py +5 -3
  84. opentrons/protocol_engine/commands/configure_for_volume.py +11 -14
  85. opentrons/protocol_engine/commands/configure_nozzle_layout.py +9 -15
  86. opentrons/protocol_engine/commands/custom.py +5 -3
  87. opentrons/protocol_engine/commands/dispense.py +42 -20
  88. opentrons/protocol_engine/commands/dispense_in_place.py +32 -14
  89. opentrons/protocol_engine/commands/drop_tip.py +68 -15
  90. opentrons/protocol_engine/commands/drop_tip_in_place.py +52 -11
  91. opentrons/protocol_engine/commands/get_tip_presence.py +5 -3
  92. opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +6 -6
  93. opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +6 -6
  94. opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +6 -6
  95. opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +8 -6
  96. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +8 -4
  97. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +6 -4
  98. opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +6 -6
  99. opentrons/protocol_engine/commands/home.py +11 -5
  100. opentrons/protocol_engine/commands/liquid_probe.py +146 -88
  101. opentrons/protocol_engine/commands/load_labware.py +19 -5
  102. opentrons/protocol_engine/commands/load_liquid.py +18 -7
  103. opentrons/protocol_engine/commands/load_module.py +43 -6
  104. opentrons/protocol_engine/commands/load_pipette.py +18 -17
  105. opentrons/protocol_engine/commands/magnetic_module/disengage.py +6 -6
  106. opentrons/protocol_engine/commands/magnetic_module/engage.py +6 -4
  107. opentrons/protocol_engine/commands/move_labware.py +106 -19
  108. opentrons/protocol_engine/commands/move_relative.py +15 -3
  109. opentrons/protocol_engine/commands/move_to_addressable_area.py +29 -4
  110. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +13 -4
  111. opentrons/protocol_engine/commands/move_to_coordinates.py +11 -5
  112. opentrons/protocol_engine/commands/move_to_well.py +37 -10
  113. opentrons/protocol_engine/commands/pick_up_tip.py +50 -29
  114. opentrons/protocol_engine/commands/pipetting_common.py +39 -15
  115. opentrons/protocol_engine/commands/prepare_to_aspirate.py +62 -15
  116. opentrons/protocol_engine/commands/reload_labware.py +13 -4
  117. opentrons/protocol_engine/commands/retract_axis.py +6 -3
  118. opentrons/protocol_engine/commands/save_position.py +2 -3
  119. opentrons/protocol_engine/commands/set_rail_lights.py +5 -3
  120. opentrons/protocol_engine/commands/set_status_bar.py +5 -3
  121. opentrons/protocol_engine/commands/temperature_module/deactivate.py +6 -4
  122. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +3 -4
  123. opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +6 -6
  124. opentrons/protocol_engine/commands/thermocycler/__init__.py +19 -0
  125. opentrons/protocol_engine/commands/thermocycler/close_lid.py +8 -8
  126. opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +6 -4
  127. opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +6 -4
  128. opentrons/protocol_engine/commands/thermocycler/open_lid.py +8 -4
  129. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +165 -0
  130. opentrons/protocol_engine/commands/thermocycler/run_profile.py +6 -6
  131. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +3 -4
  132. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +3 -4
  133. opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +6 -4
  134. opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +6 -4
  135. opentrons/protocol_engine/commands/touch_tip.py +19 -7
  136. opentrons/protocol_engine/commands/unsafe/__init__.py +30 -0
  137. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +6 -4
  138. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +12 -4
  139. opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +5 -3
  140. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +194 -0
  141. opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py +75 -0
  142. opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +5 -3
  143. opentrons/protocol_engine/commands/verify_tip_presence.py +5 -5
  144. opentrons/protocol_engine/commands/wait_for_duration.py +5 -3
  145. opentrons/protocol_engine/commands/wait_for_resume.py +5 -3
  146. opentrons/protocol_engine/create_protocol_engine.py +41 -8
  147. opentrons/protocol_engine/engine_support.py +2 -1
  148. opentrons/protocol_engine/error_recovery_policy.py +14 -3
  149. opentrons/protocol_engine/errors/__init__.py +18 -0
  150. opentrons/protocol_engine/errors/exceptions.py +114 -2
  151. opentrons/protocol_engine/execution/__init__.py +2 -0
  152. opentrons/protocol_engine/execution/command_executor.py +22 -13
  153. opentrons/protocol_engine/execution/create_queue_worker.py +5 -1
  154. opentrons/protocol_engine/execution/door_watcher.py +1 -1
  155. opentrons/protocol_engine/execution/equipment.py +2 -1
  156. opentrons/protocol_engine/execution/error_recovery_hardware_state_synchronizer.py +101 -0
  157. opentrons/protocol_engine/execution/gantry_mover.py +4 -2
  158. opentrons/protocol_engine/execution/hardware_stopper.py +3 -3
  159. opentrons/protocol_engine/execution/heater_shaker_movement_flagger.py +1 -4
  160. opentrons/protocol_engine/execution/labware_movement.py +6 -3
  161. opentrons/protocol_engine/execution/movement.py +8 -3
  162. opentrons/protocol_engine/execution/pipetting.py +7 -4
  163. opentrons/protocol_engine/execution/queue_worker.py +6 -2
  164. opentrons/protocol_engine/execution/run_control.py +1 -1
  165. opentrons/protocol_engine/execution/thermocycler_movement_flagger.py +1 -1
  166. opentrons/protocol_engine/execution/thermocycler_plate_lifter.py +2 -1
  167. opentrons/protocol_engine/execution/tip_handler.py +77 -43
  168. opentrons/protocol_engine/notes/__init__.py +14 -2
  169. opentrons/protocol_engine/notes/notes.py +18 -1
  170. opentrons/protocol_engine/plugins.py +1 -1
  171. opentrons/protocol_engine/protocol_engine.py +54 -31
  172. opentrons/protocol_engine/resources/__init__.py +2 -0
  173. opentrons/protocol_engine/resources/deck_data_provider.py +58 -5
  174. opentrons/protocol_engine/resources/file_provider.py +157 -0
  175. opentrons/protocol_engine/resources/fixture_validation.py +5 -0
  176. opentrons/protocol_engine/resources/labware_validation.py +10 -0
  177. opentrons/protocol_engine/state/__init__.py +0 -70
  178. opentrons/protocol_engine/state/addressable_areas.py +1 -1
  179. opentrons/protocol_engine/state/command_history.py +21 -2
  180. opentrons/protocol_engine/state/commands.py +110 -31
  181. opentrons/protocol_engine/state/files.py +59 -0
  182. opentrons/protocol_engine/state/frustum_helpers.py +440 -0
  183. opentrons/protocol_engine/state/geometry.py +359 -15
  184. opentrons/protocol_engine/state/labware.py +166 -63
  185. opentrons/protocol_engine/state/liquids.py +1 -1
  186. opentrons/protocol_engine/state/module_substates/absorbance_reader_substate.py +19 -3
  187. opentrons/protocol_engine/state/modules.py +167 -85
  188. opentrons/protocol_engine/state/motion.py +16 -9
  189. opentrons/protocol_engine/state/pipettes.py +157 -317
  190. opentrons/protocol_engine/state/state.py +30 -1
  191. opentrons/protocol_engine/state/state_summary.py +3 -0
  192. opentrons/protocol_engine/state/tips.py +69 -114
  193. opentrons/protocol_engine/state/update_types.py +408 -0
  194. opentrons/protocol_engine/state/wells.py +236 -0
  195. opentrons/protocol_engine/types.py +90 -0
  196. opentrons/protocol_reader/file_format_validator.py +83 -15
  197. opentrons/protocol_runner/json_translator.py +21 -5
  198. opentrons/protocol_runner/legacy_command_mapper.py +27 -6
  199. opentrons/protocol_runner/legacy_context_plugin.py +27 -71
  200. opentrons/protocol_runner/protocol_runner.py +6 -3
  201. opentrons/protocol_runner/run_orchestrator.py +26 -6
  202. opentrons/protocols/advanced_control/mix.py +3 -5
  203. opentrons/protocols/advanced_control/transfers.py +125 -56
  204. opentrons/protocols/api_support/constants.py +1 -1
  205. opentrons/protocols/api_support/definitions.py +1 -1
  206. opentrons/protocols/api_support/labware_like.py +4 -4
  207. opentrons/protocols/api_support/tip_tracker.py +2 -2
  208. opentrons/protocols/api_support/types.py +15 -2
  209. opentrons/protocols/api_support/util.py +30 -42
  210. opentrons/protocols/duration/errors.py +1 -1
  211. opentrons/protocols/duration/estimator.py +50 -29
  212. opentrons/protocols/execution/dev_types.py +2 -2
  213. opentrons/protocols/execution/execute_json_v4.py +15 -10
  214. opentrons/protocols/execution/execute_python.py +8 -3
  215. opentrons/protocols/geometry/planning.py +12 -12
  216. opentrons/protocols/labware.py +17 -33
  217. opentrons/simulate.py +3 -3
  218. opentrons/types.py +30 -3
  219. opentrons/util/logging_config.py +34 -0
  220. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/METADATA +5 -4
  221. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/RECORD +227 -215
  222. opentrons/protocol_engine/commands/absorbance_reader/measure.py +0 -94
  223. opentrons/protocol_engine/commands/configuring_common.py +0 -26
  224. opentrons/protocol_runner/thread_async_queue.py +0 -174
  225. /opentrons/protocol_engine/state/{abstract_store.py → _abstract_store.py} +0 -0
  226. /opentrons/protocol_engine/state/{move_types.py → _move_types.py} +0 -0
  227. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/LICENSE +0 -0
  228. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/WHEEL +0 -0
  229. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/entry_points.txt +0 -0
  230. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.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
@@ -252,11 +264,16 @@ class GeometryView:
252
264
  def get_labware_parent_nominal_position(self, labware_id: str) -> Point:
253
265
  """Get the position of the labware's uncalibrated parent slot (deck, 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)
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
+ slot_pos = self._addressable_areas.get_addressable_area_position(
273
+ addressable_area_name
274
+ )
259
275
  labware_data = self._labware.get(labware_id)
276
+
260
277
  offset = self._get_labware_position_offset(labware_id, labware_data.location)
261
278
 
262
279
  return Point(
@@ -405,11 +422,51 @@ class GeometryView:
405
422
  z=origin_pos.z + cal_offset.z,
406
423
  )
407
424
 
425
+ WellLocations = Union[
426
+ WellLocation, LiquidHandlingWellLocation, PickUpTipWellLocation
427
+ ]
428
+
429
+ def validate_well_position(
430
+ self,
431
+ well_location: WellLocations,
432
+ z_offset: float,
433
+ pipette_id: Optional[str] = None,
434
+ ) -> None:
435
+ """Raise exception if operation location is not within well.
436
+
437
+ Primarily this checks if there is not enough liquid in a well to do meniscus-relative static aspiration.
438
+ """
439
+ if well_location.origin == WellOrigin.MENISCUS:
440
+ assert pipette_id is not None
441
+ lld_min_height = self._pipettes.get_current_tip_lld_settings(
442
+ pipette_id=pipette_id
443
+ )
444
+ if z_offset < lld_min_height:
445
+ if isinstance(well_location, PickUpTipWellLocation):
446
+ raise OperationLocationNotInWellError(
447
+ 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"
448
+ )
449
+ else:
450
+ raise OperationLocationNotInWellError(
451
+ 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"
452
+ )
453
+ elif z_offset < 0:
454
+ if isinstance(well_location, PickUpTipWellLocation):
455
+ raise OperationLocationNotInWellError(
456
+ f"Specifying {well_location.origin} with an offset of {well_location.offset} results in an operation location below the bottom of the well"
457
+ )
458
+ else:
459
+ raise OperationLocationNotInWellError(
460
+ 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"
461
+ )
462
+
408
463
  def get_well_position(
409
464
  self,
410
465
  labware_id: str,
411
466
  well_name: str,
412
- well_location: Optional[WellLocation] = None,
467
+ well_location: Optional[WellLocations] = None,
468
+ operation_volume: Optional[float] = None,
469
+ pipette_id: Optional[str] = None,
413
470
  ) -> Point:
414
471
  """Given relative well location in a labware, get absolute position."""
415
472
  labware_pos = self.get_labware_position(labware_id)
@@ -419,10 +476,17 @@ class GeometryView:
419
476
  offset = WellOffset(x=0, y=0, z=well_depth)
420
477
  if well_location is not None:
421
478
  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})
479
+ offset_adjustment = self.get_well_offset_adjustment(
480
+ labware_id=labware_id,
481
+ well_name=well_name,
482
+ well_location=well_location,
483
+ well_depth=well_depth,
484
+ operation_volume=operation_volume,
485
+ )
486
+ offset = offset.copy(update={"z": offset.z + offset_adjustment})
487
+ self.validate_well_position(
488
+ well_location=well_location, z_offset=offset.z, pipette_id=pipette_id
489
+ )
426
490
 
427
491
  return Point(
428
492
  x=labware_pos.x + offset.x + well_def.x,
@@ -457,6 +521,41 @@ class GeometryView:
457
521
 
458
522
  return WellLocation(offset=WellOffset(x=delta.x, y=delta.y, z=delta.z))
459
523
 
524
+ def get_relative_liquid_handling_well_location(
525
+ self,
526
+ labware_id: str,
527
+ well_name: str,
528
+ absolute_point: Point,
529
+ is_meniscus: Optional[bool] = None,
530
+ ) -> LiquidHandlingWellLocation:
531
+ """Given absolute position, get relative location of a well in a labware.
532
+
533
+ If is_meniscus is True, absolute_point will hold the z-offset in its z field.
534
+ """
535
+ if is_meniscus:
536
+ return LiquidHandlingWellLocation(
537
+ origin=WellOrigin.MENISCUS,
538
+ offset=WellOffset(x=0, y=0, z=absolute_point.z),
539
+ )
540
+ else:
541
+ well_absolute_point = self.get_well_position(labware_id, well_name)
542
+ delta = absolute_point - well_absolute_point
543
+ return LiquidHandlingWellLocation(
544
+ offset=WellOffset(x=delta.x, y=delta.y, z=delta.z)
545
+ )
546
+
547
+ def get_relative_pick_up_tip_well_location(
548
+ self,
549
+ labware_id: str,
550
+ well_name: str,
551
+ absolute_point: Point,
552
+ ) -> PickUpTipWellLocation:
553
+ """Given absolute position, get relative location of a well in a labware."""
554
+ well_absolute_point = self.get_well_position(labware_id, well_name)
555
+ delta = absolute_point - well_absolute_point
556
+
557
+ return PickUpTipWellLocation(offset=WellOffset(x=delta.x, y=delta.y, z=delta.z))
558
+
460
559
  def get_well_height(
461
560
  self,
462
561
  labware_id: str,
@@ -580,6 +679,14 @@ class GeometryView:
580
679
  ),
581
680
  )
582
681
 
682
+ def convert_pick_up_tip_well_location(
683
+ self, well_location: PickUpTipWellLocation
684
+ ) -> WellLocation:
685
+ """Convert PickUpTipWellLocation to WellLocation."""
686
+ return WellLocation(
687
+ origin=WellOrigin(well_location.origin.value), offset=well_location.offset
688
+ )
689
+
583
690
  # TODO(jbl 11-30-2023) fold this function into get_ancestor_slot_name see RSS-411
584
691
  def _get_staging_slot_name(self, labware_id: str) -> str:
585
692
  """Get the staging slot name that the labware is on."""
@@ -596,6 +703,12 @@ class GeometryView:
596
703
  "Cannot get staging slot name for labware not on staging slot."
597
704
  )
598
705
 
706
+ def _get_lid_dock_slot_name(self, labware_id: str) -> str:
707
+ """Get the staging slot name that the labware is on."""
708
+ labware_location = self._labware.get(labware_id).location
709
+ assert isinstance(labware_location, AddressableAreaLocation)
710
+ return labware_location.addressableAreaName
711
+
599
712
  def get_ancestor_slot_name(self, labware_id: str) -> DeckSlotName:
600
713
  """Get the slot name of the labware or the module that the labware is on."""
601
714
  labware = self._labware.get(labware_id)
@@ -613,10 +726,15 @@ class GeometryView:
613
726
  area_name = labware.location.addressableAreaName
614
727
  # TODO we might want to eventually return some sort of staging slot name when we're ready to work through
615
728
  # the linting nightmare it will create
729
+ if self._labware.is_absorbance_reader_lid(labware_id):
730
+ raise errors.LocationIsLidDockSlotError(
731
+ "Cannot get ancestor slot name for labware on lid dock slot."
732
+ )
616
733
  if fixture_validation.is_staging_slot(area_name):
617
734
  raise errors.LocationIsStagingSlotError(
618
735
  "Cannot get ancestor slot name for labware on staging slot."
619
736
  )
737
+ raise errors.LocationIs
620
738
  slot_name = DeckSlotName.from_primitive(area_name)
621
739
  elif labware.location == OFF_DECK_LOCATION:
622
740
  raise errors.LabwareNotOnDeckError(
@@ -962,17 +1080,22 @@ class GeometryView:
962
1080
  from_location: OnDeckLabwareLocation,
963
1081
  to_location: OnDeckLabwareLocation,
964
1082
  additional_offset_vector: LabwareMovementOffsetData,
1083
+ current_labware: LabwareDefinition,
965
1084
  ) -> LabwareMovementOffsetData:
966
1085
  """Calculate the final labware offset vector to use in labware movement."""
967
1086
  pick_up_offset = (
968
1087
  self.get_total_nominal_gripper_offset_for_move_type(
969
- location=from_location, move_type=_GripperMoveType.PICK_UP_LABWARE
1088
+ location=from_location,
1089
+ move_type=_GripperMoveType.PICK_UP_LABWARE,
1090
+ current_labware=current_labware,
970
1091
  )
971
1092
  + additional_offset_vector.pickUpOffset
972
1093
  )
973
1094
  drop_offset = (
974
1095
  self.get_total_nominal_gripper_offset_for_move_type(
975
- location=to_location, move_type=_GripperMoveType.DROP_LABWARE
1096
+ location=to_location,
1097
+ move_type=_GripperMoveType.DROP_LABWARE,
1098
+ current_labware=current_labware,
976
1099
  )
977
1100
  + additional_offset_vector.dropOffset
978
1101
  )
@@ -1003,7 +1126,10 @@ class GeometryView:
1003
1126
  return location
1004
1127
 
1005
1128
  def get_total_nominal_gripper_offset_for_move_type(
1006
- self, location: OnDeckLabwareLocation, move_type: _GripperMoveType
1129
+ self,
1130
+ location: OnDeckLabwareLocation,
1131
+ move_type: _GripperMoveType,
1132
+ current_labware: LabwareDefinition,
1007
1133
  ) -> LabwareOffsetVector:
1008
1134
  """Get the total of the offsets to be used to pick up labware in its current location."""
1009
1135
  if move_type == _GripperMoveType.PICK_UP_LABWARE:
@@ -1019,14 +1145,39 @@ class GeometryView:
1019
1145
  location
1020
1146
  )
1021
1147
  ancestor = self._labware.get_parent_location(location.labwareId)
1148
+ extra_offset = LabwareOffsetVector(x=0, y=0, z=0)
1149
+ if (
1150
+ isinstance(ancestor, ModuleLocation)
1151
+ and self._modules._state.requested_model_by_id[ancestor.moduleId]
1152
+ == ModuleModel.THERMOCYCLER_MODULE_V2
1153
+ and labware_validation.validate_definition_is_lid(current_labware)
1154
+ ):
1155
+ if "lidOffsets" in current_labware.gripperOffsets.keys():
1156
+ extra_offset = LabwareOffsetVector(
1157
+ x=current_labware.gripperOffsets[
1158
+ "lidOffsets"
1159
+ ].pickUpOffset.x,
1160
+ y=current_labware.gripperOffsets[
1161
+ "lidOffsets"
1162
+ ].pickUpOffset.y,
1163
+ z=current_labware.gripperOffsets[
1164
+ "lidOffsets"
1165
+ ].pickUpOffset.z,
1166
+ )
1167
+ else:
1168
+ raise errors.LabwareOffsetDoesNotExistError(
1169
+ f"Labware Definition {current_labware.parameters.loadName} does not contain required field 'lidOffsets' of 'gripperOffsets'."
1170
+ )
1171
+
1022
1172
  assert isinstance(
1023
- ancestor, (DeckSlotLocation, ModuleLocation)
1173
+ ancestor, (DeckSlotLocation, ModuleLocation, OnLabwareLocation)
1024
1174
  ), "No gripper offsets for off-deck labware"
1025
1175
  return (
1026
1176
  direct_parent_offset.pickUpOffset
1027
1177
  + self._nominal_gripper_offsets_for_location(
1028
1178
  location=ancestor
1029
1179
  ).pickUpOffset
1180
+ + extra_offset
1030
1181
  )
1031
1182
  else:
1032
1183
  if isinstance(
@@ -1041,14 +1192,39 @@ class GeometryView:
1041
1192
  location
1042
1193
  )
1043
1194
  ancestor = self._labware.get_parent_location(location.labwareId)
1195
+ extra_offset = LabwareOffsetVector(x=0, y=0, z=0)
1196
+ if (
1197
+ isinstance(ancestor, ModuleLocation)
1198
+ and self._modules._state.requested_model_by_id[ancestor.moduleId]
1199
+ == ModuleModel.THERMOCYCLER_MODULE_V2
1200
+ and labware_validation.validate_definition_is_lid(current_labware)
1201
+ ):
1202
+ if "lidOffsets" in current_labware.gripperOffsets.keys():
1203
+ extra_offset = LabwareOffsetVector(
1204
+ x=current_labware.gripperOffsets[
1205
+ "lidOffsets"
1206
+ ].pickUpOffset.x,
1207
+ y=current_labware.gripperOffsets[
1208
+ "lidOffsets"
1209
+ ].pickUpOffset.y,
1210
+ z=current_labware.gripperOffsets[
1211
+ "lidOffsets"
1212
+ ].pickUpOffset.z,
1213
+ )
1214
+ else:
1215
+ raise errors.LabwareOffsetDoesNotExistError(
1216
+ f"Labware Definition {current_labware.parameters.loadName} does not contain required field 'lidOffsets' of 'gripperOffsets'."
1217
+ )
1218
+
1044
1219
  assert isinstance(
1045
- ancestor, (DeckSlotLocation, ModuleLocation)
1220
+ ancestor, (DeckSlotLocation, ModuleLocation, OnLabwareLocation)
1046
1221
  ), "No gripper offsets for off-deck labware"
1047
1222
  return (
1048
1223
  direct_parent_offset.dropOffset
1049
1224
  + self._nominal_gripper_offsets_for_location(
1050
1225
  location=ancestor
1051
1226
  ).dropOffset
1227
+ + extra_offset
1052
1228
  )
1053
1229
 
1054
1230
  def check_gripper_labware_tip_collision(
@@ -1112,11 +1288,20 @@ class GeometryView:
1112
1288
  """
1113
1289
  parent_location = self._labware.get_parent_location(labware_id)
1114
1290
  assert isinstance(
1115
- parent_location, (DeckSlotLocation, ModuleLocation)
1291
+ parent_location,
1292
+ (
1293
+ DeckSlotLocation,
1294
+ ModuleLocation,
1295
+ AddressableAreaLocation,
1296
+ ),
1116
1297
  ), "No gripper offsets for off-deck labware"
1117
1298
 
1118
1299
  if isinstance(parent_location, DeckSlotLocation):
1119
1300
  slot_name = parent_location.slotName
1301
+ elif isinstance(parent_location, AddressableAreaLocation):
1302
+ slot_name = self._addressable_areas.get_addressable_area_base_slot(
1303
+ parent_location.addressableAreaName
1304
+ )
1120
1305
  else:
1121
1306
  module_loc = self._modules.get_location(parent_location.moduleId)
1122
1307
  slot_name = module_loc.slotName
@@ -1173,3 +1358,162 @@ class GeometryView:
1173
1358
  )
1174
1359
 
1175
1360
  return None
1361
+
1362
+ def get_well_offset_adjustment(
1363
+ self,
1364
+ labware_id: str,
1365
+ well_name: str,
1366
+ well_location: WellLocations,
1367
+ well_depth: float,
1368
+ operation_volume: Optional[float] = None,
1369
+ ) -> float:
1370
+ """Return a z-axis distance that accounts for well handling height and operation volume.
1371
+
1372
+ Distance is with reference to the well bottom.
1373
+ """
1374
+ # TODO(pbm, 10-23-24): refactor to smartly reduce height/volume conversions
1375
+ initial_handling_height = self.get_well_handling_height(
1376
+ labware_id=labware_id,
1377
+ well_name=well_name,
1378
+ well_location=well_location,
1379
+ well_depth=well_depth,
1380
+ )
1381
+ if isinstance(well_location, PickUpTipWellLocation):
1382
+ volume = 0.0
1383
+ elif isinstance(well_location.volumeOffset, float):
1384
+ volume = well_location.volumeOffset
1385
+ elif well_location.volumeOffset == "operationVolume":
1386
+ volume = operation_volume or 0.0
1387
+
1388
+ if volume:
1389
+ return self.get_well_height_after_volume(
1390
+ labware_id=labware_id,
1391
+ well_name=well_name,
1392
+ initial_height=initial_handling_height,
1393
+ volume=volume,
1394
+ )
1395
+ else:
1396
+ return initial_handling_height
1397
+
1398
+ def get_meniscus_height(
1399
+ self,
1400
+ labware_id: str,
1401
+ well_name: str,
1402
+ ) -> float:
1403
+ """Returns stored meniscus height in specified well."""
1404
+ well_liquid = self._wells.get_well_liquid_info(
1405
+ labware_id=labware_id, well_name=well_name
1406
+ )
1407
+ if (
1408
+ well_liquid.probed_height is not None
1409
+ and well_liquid.probed_height.height is not None
1410
+ ):
1411
+ return well_liquid.probed_height.height
1412
+ elif (
1413
+ well_liquid.loaded_volume is not None
1414
+ and well_liquid.loaded_volume.volume is not None
1415
+ ):
1416
+ return self.get_well_height_at_volume(
1417
+ labware_id=labware_id,
1418
+ well_name=well_name,
1419
+ volume=well_liquid.loaded_volume.volume,
1420
+ )
1421
+ elif (
1422
+ well_liquid.probed_volume is not None
1423
+ and well_liquid.probed_volume.volume is not None
1424
+ ):
1425
+ return self.get_well_height_at_volume(
1426
+ labware_id=labware_id,
1427
+ well_name=well_name,
1428
+ volume=well_liquid.probed_volume.volume,
1429
+ )
1430
+ else:
1431
+ raise errors.LiquidHeightUnknownError(
1432
+ "Must LiquidProbe or LoadLiquid before specifying WellOrigin.MENISCUS."
1433
+ )
1434
+
1435
+ def get_well_handling_height(
1436
+ self,
1437
+ labware_id: str,
1438
+ well_name: str,
1439
+ well_location: WellLocations,
1440
+ well_depth: float,
1441
+ ) -> float:
1442
+ """Return the handling height for a labware well (with reference to the well bottom)."""
1443
+ handling_height = 0.0
1444
+ if well_location.origin == WellOrigin.TOP:
1445
+ handling_height = well_depth
1446
+ elif well_location.origin == WellOrigin.CENTER:
1447
+ handling_height = well_depth / 2.0
1448
+ elif well_location.origin == WellOrigin.MENISCUS:
1449
+ handling_height = self.get_meniscus_height(
1450
+ labware_id=labware_id, well_name=well_name
1451
+ )
1452
+ return float(handling_height)
1453
+
1454
+ def get_well_height_after_volume(
1455
+ self, labware_id: str, well_name: str, initial_height: float, volume: float
1456
+ ) -> float:
1457
+ """Return the height of liquid in a labware well after a given volume has been handled.
1458
+
1459
+ This is given an initial handling height, with reference to the well bottom.
1460
+ """
1461
+ well_geometry = self._labware.get_well_geometry(
1462
+ labware_id=labware_id, well_name=well_name
1463
+ )
1464
+ initial_volume = find_volume_at_well_height(
1465
+ target_height=initial_height, well_geometry=well_geometry
1466
+ )
1467
+ final_volume = initial_volume + volume
1468
+ return find_height_at_well_volume(
1469
+ target_volume=final_volume, well_geometry=well_geometry
1470
+ )
1471
+
1472
+ def get_well_height_at_volume(
1473
+ self, labware_id: str, well_name: str, volume: float
1474
+ ) -> float:
1475
+ """Convert well volume to height."""
1476
+ well_geometry = self._labware.get_well_geometry(labware_id, well_name)
1477
+ return find_height_at_well_volume(
1478
+ target_volume=volume, well_geometry=well_geometry
1479
+ )
1480
+
1481
+ def get_well_volume_at_height(
1482
+ self, labware_id: str, well_name: str, height: float
1483
+ ) -> float:
1484
+ """Convert well height to volume."""
1485
+ well_geometry = self._labware.get_well_geometry(labware_id, well_name)
1486
+ return find_volume_at_well_height(
1487
+ target_height=height, well_geometry=well_geometry
1488
+ )
1489
+
1490
+ def validate_dispense_volume_into_well(
1491
+ self,
1492
+ labware_id: str,
1493
+ well_name: str,
1494
+ well_location: WellLocations,
1495
+ volume: float,
1496
+ ) -> None:
1497
+ """Raise InvalidDispenseVolumeError if planned dispense volume will overflow well."""
1498
+ well_def = self._labware.get_well_definition(labware_id, well_name)
1499
+ well_volumetric_capacity = well_def.totalLiquidVolume
1500
+ if well_location.origin == WellOrigin.MENISCUS:
1501
+ # TODO(pbm, 10-23-24): refactor to smartly reduce height/volume conversions
1502
+ well_geometry = self._labware.get_well_geometry(labware_id, well_name)
1503
+ meniscus_height = self.get_meniscus_height(
1504
+ labware_id=labware_id, well_name=well_name
1505
+ )
1506
+ meniscus_volume = find_volume_at_well_height(
1507
+ target_height=meniscus_height, well_geometry=well_geometry
1508
+ )
1509
+ remaining_volume = well_volumetric_capacity - meniscus_volume
1510
+ if volume > remaining_volume:
1511
+ raise errors.InvalidDispenseVolumeError(
1512
+ 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})"
1513
+ )
1514
+ else:
1515
+ # TODO(pbm, 10-08-24): factor in well (LabwareStore) state volume
1516
+ if volume > well_volumetric_capacity:
1517
+ raise errors.InvalidDispenseVolumeError(
1518
+ 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})"
1519
+ )