opentrons 8.7.0a9__py3-none-any.whl → 8.8.0a8__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 (190) hide show
  1. opentrons/_version.py +2 -2
  2. opentrons/cli/analyze.py +4 -1
  3. opentrons/config/__init__.py +7 -0
  4. opentrons/drivers/asyncio/communication/serial_connection.py +126 -49
  5. opentrons/drivers/heater_shaker/abstract.py +5 -0
  6. opentrons/drivers/heater_shaker/driver.py +10 -0
  7. opentrons/drivers/heater_shaker/simulator.py +4 -0
  8. opentrons/drivers/thermocycler/abstract.py +6 -0
  9. opentrons/drivers/thermocycler/driver.py +61 -10
  10. opentrons/drivers/thermocycler/simulator.py +6 -0
  11. opentrons/drivers/vacuum_module/__init__.py +5 -0
  12. opentrons/drivers/vacuum_module/abstract.py +93 -0
  13. opentrons/drivers/vacuum_module/driver.py +208 -0
  14. opentrons/drivers/vacuum_module/errors.py +39 -0
  15. opentrons/drivers/vacuum_module/simulator.py +85 -0
  16. opentrons/drivers/vacuum_module/types.py +79 -0
  17. opentrons/execute.py +3 -0
  18. opentrons/hardware_control/api.py +24 -5
  19. opentrons/hardware_control/backends/controller.py +8 -2
  20. opentrons/hardware_control/backends/flex_protocol.py +1 -0
  21. opentrons/hardware_control/backends/ot3controller.py +35 -2
  22. opentrons/hardware_control/backends/ot3simulator.py +3 -1
  23. opentrons/hardware_control/backends/ot3utils.py +37 -0
  24. opentrons/hardware_control/backends/simulator.py +2 -1
  25. opentrons/hardware_control/backends/subsystem_manager.py +5 -2
  26. opentrons/hardware_control/emulation/abstract_emulator.py +6 -4
  27. opentrons/hardware_control/emulation/connection_handler.py +8 -5
  28. opentrons/hardware_control/emulation/heater_shaker.py +12 -3
  29. opentrons/hardware_control/emulation/settings.py +1 -1
  30. opentrons/hardware_control/emulation/thermocycler.py +67 -15
  31. opentrons/hardware_control/module_control.py +105 -10
  32. opentrons/hardware_control/modules/__init__.py +3 -0
  33. opentrons/hardware_control/modules/absorbance_reader.py +11 -4
  34. opentrons/hardware_control/modules/flex_stacker.py +38 -9
  35. opentrons/hardware_control/modules/heater_shaker.py +42 -5
  36. opentrons/hardware_control/modules/magdeck.py +8 -4
  37. opentrons/hardware_control/modules/mod_abc.py +14 -6
  38. opentrons/hardware_control/modules/tempdeck.py +25 -5
  39. opentrons/hardware_control/modules/thermocycler.py +68 -11
  40. opentrons/hardware_control/modules/types.py +20 -1
  41. opentrons/hardware_control/modules/utils.py +11 -4
  42. opentrons/hardware_control/motion_utilities.py +6 -6
  43. opentrons/hardware_control/nozzle_manager.py +3 -0
  44. opentrons/hardware_control/ot3api.py +92 -17
  45. opentrons/hardware_control/poller.py +22 -8
  46. opentrons/hardware_control/protocols/liquid_handler.py +12 -4
  47. opentrons/hardware_control/scripts/update_module_fw.py +5 -0
  48. opentrons/hardware_control/types.py +43 -2
  49. opentrons/legacy_commands/commands.py +58 -5
  50. opentrons/legacy_commands/module_commands.py +52 -0
  51. opentrons/legacy_commands/protocol_commands.py +53 -1
  52. opentrons/legacy_commands/types.py +155 -1
  53. opentrons/motion_planning/deck_conflict.py +17 -12
  54. opentrons/motion_planning/waypoints.py +15 -29
  55. opentrons/protocol_api/__init__.py +5 -1
  56. opentrons/protocol_api/_transfer_liquid_validation.py +17 -2
  57. opentrons/protocol_api/_types.py +8 -1
  58. opentrons/protocol_api/core/common.py +3 -1
  59. opentrons/protocol_api/core/engine/_default_labware_versions.py +33 -11
  60. opentrons/protocol_api/core/engine/deck_conflict.py +3 -1
  61. opentrons/protocol_api/core/engine/instrument.py +109 -26
  62. opentrons/protocol_api/core/engine/labware.py +8 -1
  63. opentrons/protocol_api/core/engine/module_core.py +95 -4
  64. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +4 -18
  65. opentrons/protocol_api/core/engine/protocol.py +51 -2
  66. opentrons/protocol_api/core/engine/stringify.py +2 -0
  67. opentrons/protocol_api/core/engine/tasks.py +48 -0
  68. opentrons/protocol_api/core/engine/well.py +8 -0
  69. opentrons/protocol_api/core/instrument.py +19 -2
  70. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +19 -2
  71. opentrons/protocol_api/core/legacy/legacy_module_core.py +33 -2
  72. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +23 -1
  73. opentrons/protocol_api/core/legacy/legacy_well_core.py +4 -0
  74. opentrons/protocol_api/core/legacy/tasks.py +19 -0
  75. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +19 -2
  76. opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +14 -2
  77. opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
  78. opentrons/protocol_api/core/module.py +58 -2
  79. opentrons/protocol_api/core/protocol.py +23 -2
  80. opentrons/protocol_api/core/tasks.py +31 -0
  81. opentrons/protocol_api/core/well.py +4 -0
  82. opentrons/protocol_api/instrument_context.py +388 -2
  83. opentrons/protocol_api/labware.py +10 -2
  84. opentrons/protocol_api/module_contexts.py +170 -6
  85. opentrons/protocol_api/protocol_context.py +87 -21
  86. opentrons/protocol_api/robot_context.py +41 -25
  87. opentrons/protocol_api/tasks.py +48 -0
  88. opentrons/protocol_api/validation.py +49 -3
  89. opentrons/protocol_engine/__init__.py +4 -0
  90. opentrons/protocol_engine/actions/__init__.py +6 -2
  91. opentrons/protocol_engine/actions/actions.py +31 -9
  92. opentrons/protocol_engine/clients/sync_client.py +42 -7
  93. opentrons/protocol_engine/commands/__init__.py +56 -0
  94. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +2 -15
  95. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +2 -15
  96. opentrons/protocol_engine/commands/absorbance_reader/read.py +22 -23
  97. opentrons/protocol_engine/commands/aspirate.py +1 -0
  98. opentrons/protocol_engine/commands/aspirate_while_tracking.py +52 -19
  99. opentrons/protocol_engine/commands/capture_image.py +302 -0
  100. opentrons/protocol_engine/commands/command.py +2 -0
  101. opentrons/protocol_engine/commands/command_unions.py +62 -0
  102. opentrons/protocol_engine/commands/create_timer.py +83 -0
  103. opentrons/protocol_engine/commands/dispense.py +1 -0
  104. opentrons/protocol_engine/commands/dispense_while_tracking.py +56 -19
  105. opentrons/protocol_engine/commands/drop_tip.py +32 -8
  106. opentrons/protocol_engine/commands/flex_stacker/common.py +35 -0
  107. opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +7 -0
  108. opentrons/protocol_engine/commands/heater_shaker/__init__.py +14 -0
  109. opentrons/protocol_engine/commands/heater_shaker/common.py +20 -0
  110. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +5 -4
  111. opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +136 -0
  112. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +31 -5
  113. opentrons/protocol_engine/commands/move_labware.py +3 -4
  114. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +1 -1
  115. opentrons/protocol_engine/commands/movement_common.py +31 -2
  116. opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
  117. opentrons/protocol_engine/commands/pipetting_common.py +48 -3
  118. opentrons/protocol_engine/commands/set_tip_state.py +97 -0
  119. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +38 -7
  120. opentrons/protocol_engine/commands/thermocycler/__init__.py +16 -0
  121. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +6 -0
  122. opentrons/protocol_engine/commands/thermocycler/run_profile.py +8 -0
  123. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +44 -7
  124. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +43 -14
  125. opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +191 -0
  126. opentrons/protocol_engine/commands/touch_tip.py +1 -1
  127. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +6 -22
  128. opentrons/protocol_engine/commands/wait_for_tasks.py +98 -0
  129. opentrons/protocol_engine/create_protocol_engine.py +12 -0
  130. opentrons/protocol_engine/engine_support.py +3 -0
  131. opentrons/protocol_engine/errors/__init__.py +12 -0
  132. opentrons/protocol_engine/errors/exceptions.py +119 -0
  133. opentrons/protocol_engine/execution/__init__.py +4 -0
  134. opentrons/protocol_engine/execution/command_executor.py +62 -1
  135. opentrons/protocol_engine/execution/create_queue_worker.py +9 -2
  136. opentrons/protocol_engine/execution/labware_movement.py +13 -15
  137. opentrons/protocol_engine/execution/movement.py +2 -0
  138. opentrons/protocol_engine/execution/pipetting.py +26 -25
  139. opentrons/protocol_engine/execution/queue_worker.py +4 -0
  140. opentrons/protocol_engine/execution/run_control.py +8 -0
  141. opentrons/protocol_engine/execution/task_handler.py +157 -0
  142. opentrons/protocol_engine/protocol_engine.py +137 -36
  143. opentrons/protocol_engine/resources/__init__.py +4 -0
  144. opentrons/protocol_engine/resources/camera_provider.py +110 -0
  145. opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
  146. opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
  147. opentrons/protocol_engine/resources/file_provider.py +133 -58
  148. opentrons/protocol_engine/resources/labware_validation.py +10 -6
  149. opentrons/protocol_engine/slot_standardization.py +2 -0
  150. opentrons/protocol_engine/state/_well_math.py +60 -18
  151. opentrons/protocol_engine/state/addressable_areas.py +2 -0
  152. opentrons/protocol_engine/state/camera.py +54 -0
  153. opentrons/protocol_engine/state/commands.py +37 -14
  154. opentrons/protocol_engine/state/geometry.py +276 -379
  155. opentrons/protocol_engine/state/labware.py +62 -108
  156. opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
  157. opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1336 -0
  158. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
  159. opentrons/protocol_engine/state/modules.py +30 -8
  160. opentrons/protocol_engine/state/motion.py +60 -18
  161. opentrons/protocol_engine/state/preconditions.py +59 -0
  162. opentrons/protocol_engine/state/state.py +44 -0
  163. opentrons/protocol_engine/state/state_summary.py +4 -0
  164. opentrons/protocol_engine/state/tasks.py +139 -0
  165. opentrons/protocol_engine/state/tips.py +177 -258
  166. opentrons/protocol_engine/state/update_types.py +26 -9
  167. opentrons/protocol_engine/types/__init__.py +23 -4
  168. opentrons/protocol_engine/types/command_preconditions.py +18 -0
  169. opentrons/protocol_engine/types/deck_configuration.py +5 -1
  170. opentrons/protocol_engine/types/instrument.py +8 -1
  171. opentrons/protocol_engine/types/labware.py +1 -13
  172. opentrons/protocol_engine/types/location.py +26 -2
  173. opentrons/protocol_engine/types/module.py +11 -1
  174. opentrons/protocol_engine/types/tasks.py +38 -0
  175. opentrons/protocol_engine/types/tip.py +9 -0
  176. opentrons/protocol_runner/create_simulating_orchestrator.py +29 -2
  177. opentrons/protocol_runner/protocol_runner.py +14 -1
  178. opentrons/protocol_runner/run_orchestrator.py +49 -2
  179. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +2 -2
  180. opentrons/protocols/api_support/definitions.py +1 -1
  181. opentrons/protocols/api_support/types.py +2 -1
  182. opentrons/simulate.py +51 -15
  183. opentrons/system/camera.py +334 -4
  184. opentrons/system/ffmpeg.py +110 -0
  185. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/METADATA +4 -4
  186. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/RECORD +189 -161
  187. opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
  188. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/WHEEL +0 -0
  189. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/entry_points.txt +0 -0
  190. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1336 @@
1
+ """Utilities for calculating the labware origin offset position."""
2
+ import dataclasses
3
+ import enum
4
+ from typing import Union, overload, Optional
5
+
6
+ from typing_extensions import assert_type
7
+
8
+ from opentrons.types import Point, DeckSlotName
9
+ from opentrons_shared_data.labware.labware_definition import (
10
+ LabwareDefinition,
11
+ LabwareDefinition2,
12
+ LabwareDefinition3,
13
+ Extents,
14
+ AxisAlignedBoundingBox3D,
15
+ Vector3D,
16
+ )
17
+ from opentrons_shared_data.labware.types import (
18
+ SlotFootprintAsChildFeature,
19
+ LocatingFeatures,
20
+ SpringDirectionalForce,
21
+ SlotFootprintAsParentFeature,
22
+ Vector3D as LabwareVector3D,
23
+ )
24
+ from opentrons_shared_data.module.types import ModuleOrientation
25
+ from opentrons.protocol_engine.resources.labware_validation import (
26
+ validate_definition_is_lid,
27
+ is_absorbance_reader_lid,
28
+ )
29
+ from opentrons.protocol_engine.errors import (
30
+ LabwareNotOnDeckError,
31
+ LabwareOffsetDoesNotExistError,
32
+ InvalidModuleOrientation,
33
+ )
34
+ from .errors import (
35
+ MissingLocatingFeatureError,
36
+ IncompatibleLocatingFeatureError,
37
+ InvalidLabwarePlacementError,
38
+ )
39
+ from opentrons.protocol_engine.types import AddressableArea
40
+ from opentrons_shared_data.deck.types import DeckDefinitionV5, SlotDefV3
41
+ from opentrons.protocol_engine.types import (
42
+ ModuleDefinition,
43
+ ModuleModel,
44
+ DeckLocationDefinition,
45
+ LabwareLocation,
46
+ ModuleLocation,
47
+ DeckSlotLocation,
48
+ AddressableAreaLocation,
49
+ OnLabwareLocation,
50
+ LabwareMovementOffsetData,
51
+ LabwareOffsetVector,
52
+ WASTE_CHUTE_LOCATION,
53
+ )
54
+
55
+ _OFFSET_ON_TC_OT2 = Point(x=0, y=0, z=10.7)
56
+
57
+ LabwareStackupAncestorDefinition = Union[
58
+ DeckLocationDefinition,
59
+ ModuleDefinition,
60
+ ]
61
+ _LabwareStackupDefinition = Union[
62
+ DeckLocationDefinition, ModuleDefinition, LabwareDefinition
63
+ ]
64
+
65
+
66
+ class LabwareOriginContext(enum.Enum):
67
+ """Context for labware origin calculations."""
68
+
69
+ PIPETTING = enum.auto()
70
+ GRIPPER_PICKING_UP = enum.auto()
71
+ GRIPPER_DROPPING = enum.auto()
72
+
73
+
74
+ @dataclasses.dataclass
75
+ class _Labware3SupportedParentDefinition:
76
+ features: LocatingFeatures
77
+ extents: Extents
78
+
79
+
80
+ @dataclasses.dataclass
81
+ class _GripperOffsets:
82
+ pick_up_offset: Point
83
+ drop_offset: Point
84
+
85
+
86
+ def get_stackup_origin_to_labware_origin(
87
+ context: LabwareOriginContext,
88
+ stackup_lw_info_top_to_bottom: list[tuple[LabwareDefinition, LabwareLocation]],
89
+ underlying_ancestor_definition: LabwareStackupAncestorDefinition,
90
+ slot_name: DeckSlotName,
91
+ module_parent_to_child_offset: Point | None,
92
+ deck_definition: DeckDefinitionV5,
93
+ ) -> Point:
94
+ """Returns the offset from the stackup placement origin to child labware origin.
95
+
96
+ Accounts for offset differences caused by context.
97
+ """
98
+ if context == LabwareOriginContext.PIPETTING:
99
+ return _get_stackup_origin_to_lw_origin(
100
+ stackup_lw_info_top_to_bottom=stackup_lw_info_top_to_bottom,
101
+ underlying_ancestor_definition=underlying_ancestor_definition,
102
+ module_parent_to_child_offset=module_parent_to_child_offset,
103
+ deck_definition=deck_definition,
104
+ slot_name=slot_name,
105
+ )
106
+ else:
107
+ gripper_offsets = _total_nominal_gripper_offsets(
108
+ stackup_lw_info_top_to_bottom=stackup_lw_info_top_to_bottom,
109
+ underlying_ancestor_definition=underlying_ancestor_definition,
110
+ slot_name=slot_name,
111
+ deck_definition=deck_definition,
112
+ )
113
+ gripper_offset = (
114
+ gripper_offsets.pick_up_offset
115
+ if context == LabwareOriginContext.GRIPPER_PICKING_UP
116
+ else gripper_offsets.drop_offset
117
+ )
118
+
119
+ return gripper_offset + _get_stackup_origin_to_lw_origin(
120
+ stackup_lw_info_top_to_bottom=stackup_lw_info_top_to_bottom,
121
+ underlying_ancestor_definition=underlying_ancestor_definition,
122
+ module_parent_to_child_offset=module_parent_to_child_offset,
123
+ deck_definition=deck_definition,
124
+ slot_name=slot_name,
125
+ )
126
+
127
+
128
+ def _get_stackup_origin_to_lw_origin(
129
+ stackup_lw_info_top_to_bottom: list[tuple[LabwareDefinition, LabwareLocation]],
130
+ underlying_ancestor_definition: LabwareStackupAncestorDefinition,
131
+ module_parent_to_child_offset: Point | None,
132
+ deck_definition: DeckDefinitionV5,
133
+ slot_name: DeckSlotName,
134
+ is_topmost_labware: bool = True,
135
+ ) -> Point:
136
+ """Returns the offset from the stackup placement origin to child labware origin."""
137
+ definition, location = stackup_lw_info_top_to_bottom[0]
138
+ underlying_ancestor_orientation = _get_underlying_ancestor_orientation(
139
+ underlying_ancestor_definition, slot_name
140
+ )
141
+
142
+ if isinstance(
143
+ location, (AddressableAreaLocation, DeckSlotLocation, ModuleLocation)
144
+ ):
145
+ return _get_parent_placement_origin_to_lw_origin_by_location(
146
+ labware_location=location,
147
+ labware_definition=definition,
148
+ parent_definition=underlying_ancestor_definition,
149
+ deck_definition=deck_definition,
150
+ module_parent_to_child_offset=module_parent_to_child_offset,
151
+ is_topmost_labware=is_topmost_labware,
152
+ underlying_ancestor_orientation=underlying_ancestor_orientation,
153
+ )
154
+ elif isinstance(location, OnLabwareLocation):
155
+ parent_definition = stackup_lw_info_top_to_bottom[1][0]
156
+
157
+ parent_placement_origin_to_lw_origin = (
158
+ _get_parent_placement_origin_to_lw_origin_by_location(
159
+ labware_location=location,
160
+ labware_definition=definition,
161
+ parent_definition=parent_definition,
162
+ deck_definition=deck_definition,
163
+ module_parent_to_child_offset=module_parent_to_child_offset,
164
+ is_topmost_labware=is_topmost_labware,
165
+ underlying_ancestor_orientation=underlying_ancestor_orientation,
166
+ )
167
+ )
168
+ remaining_lw_defs_locs_top_to_bottom = stackup_lw_info_top_to_bottom[1:]
169
+
170
+ return parent_placement_origin_to_lw_origin + _get_stackup_origin_to_lw_origin(
171
+ stackup_lw_info_top_to_bottom=remaining_lw_defs_locs_top_to_bottom,
172
+ underlying_ancestor_definition=underlying_ancestor_definition,
173
+ module_parent_to_child_offset=module_parent_to_child_offset,
174
+ deck_definition=deck_definition,
175
+ slot_name=slot_name,
176
+ is_topmost_labware=False,
177
+ )
178
+ elif location == WASTE_CHUTE_LOCATION:
179
+ raise LabwareNotOnDeckError(
180
+ f"Cannot access {definition.metadata.displayName} because it is in the waste chute."
181
+ )
182
+ else:
183
+ raise LabwareNotOnDeckError(
184
+ f"Cannot access {definition.metadata.displayName} since it is not on the deck. "
185
+ "Either it has been loaded off-deck or its been moved off-deck."
186
+ )
187
+
188
+
189
+ def _get_underlying_ancestor_orientation(
190
+ underlying_ancestor_definition: LabwareStackupAncestorDefinition,
191
+ slot_id: DeckSlotName,
192
+ ) -> ModuleOrientation:
193
+ if isinstance(underlying_ancestor_definition, ModuleDefinition):
194
+ orientation = underlying_ancestor_definition.orientation.get(slot_id.id)
195
+ if orientation == "left":
196
+ return ModuleOrientation.LEFT
197
+ elif orientation == "right":
198
+ return ModuleOrientation.RIGHT
199
+ elif orientation == "center":
200
+ return ModuleOrientation.CENTER
201
+ else:
202
+ raise InvalidModuleOrientation(
203
+ f"Module {underlying_ancestor_definition.moduleType} does "
204
+ f"not contain a valid orientation for slot {slot_id}."
205
+ )
206
+
207
+ elif isinstance(underlying_ancestor_definition, AddressableArea):
208
+ return underlying_ancestor_definition.orientation
209
+ else:
210
+ return underlying_ancestor_definition["orientation"]
211
+
212
+
213
+ def _get_parent_placement_origin_to_lw_origin_by_location(
214
+ labware_location: LabwareLocation,
215
+ labware_definition: LabwareDefinition,
216
+ parent_definition: _LabwareStackupDefinition,
217
+ deck_definition: DeckDefinitionV5,
218
+ module_parent_to_child_offset: Point | None,
219
+ underlying_ancestor_orientation: ModuleOrientation,
220
+ is_topmost_labware: bool,
221
+ ) -> Point:
222
+ if isinstance(labware_location, ModuleLocation):
223
+ if module_parent_to_child_offset is None:
224
+ raise ValueError(
225
+ "Expected value for module_parent_to_child_offset, received None."
226
+ )
227
+ else:
228
+ return _get_parent_placement_origin_to_lw_origin(
229
+ child_labware=labware_definition,
230
+ parent_deck_item=parent_definition, # type: ignore[arg-type]
231
+ module_parent_to_child_offset=module_parent_to_child_offset,
232
+ deck_definition=deck_definition,
233
+ is_topmost_labware=is_topmost_labware,
234
+ labware_location=labware_location,
235
+ underlying_ancestor_orientation=underlying_ancestor_orientation,
236
+ )
237
+ elif isinstance(labware_location, OnLabwareLocation):
238
+ return _get_parent_placement_origin_to_lw_origin(
239
+ child_labware=labware_definition,
240
+ parent_deck_item=parent_definition, # type: ignore[arg-type]
241
+ module_parent_to_child_offset=None,
242
+ deck_definition=deck_definition,
243
+ is_topmost_labware=is_topmost_labware,
244
+ labware_location=labware_location,
245
+ underlying_ancestor_orientation=underlying_ancestor_orientation,
246
+ )
247
+ elif isinstance(labware_location, (DeckSlotLocation, AddressableAreaLocation)):
248
+ return _get_parent_placement_origin_to_lw_origin(
249
+ child_labware=labware_definition,
250
+ parent_deck_item=parent_definition, # type: ignore[arg-type]
251
+ module_parent_to_child_offset=None,
252
+ deck_definition=deck_definition,
253
+ is_topmost_labware=is_topmost_labware,
254
+ labware_location=labware_location,
255
+ underlying_ancestor_orientation=underlying_ancestor_orientation,
256
+ )
257
+ else:
258
+ raise ValueError(f"Invalid labware location: {labware_location}")
259
+
260
+
261
+ @overload
262
+ def _get_parent_placement_origin_to_lw_origin(
263
+ child_labware: LabwareDefinition,
264
+ parent_deck_item: ModuleDefinition,
265
+ module_parent_to_child_offset: Point,
266
+ deck_definition: DeckDefinitionV5,
267
+ is_topmost_labware: bool,
268
+ labware_location: ModuleLocation,
269
+ underlying_ancestor_orientation: ModuleOrientation,
270
+ ) -> Point:
271
+ ...
272
+
273
+
274
+ @overload
275
+ def _get_parent_placement_origin_to_lw_origin(
276
+ child_labware: LabwareDefinition,
277
+ parent_deck_item: DeckLocationDefinition,
278
+ module_parent_to_child_offset: None,
279
+ deck_definition: DeckDefinitionV5,
280
+ is_topmost_labware: bool,
281
+ labware_location: Union[DeckSlotLocation, AddressableAreaLocation],
282
+ underlying_ancestor_orientation: ModuleOrientation,
283
+ ) -> Point:
284
+ ...
285
+
286
+
287
+ @overload
288
+ def _get_parent_placement_origin_to_lw_origin(
289
+ child_labware: LabwareDefinition,
290
+ parent_deck_item: LabwareDefinition,
291
+ module_parent_to_child_offset: None,
292
+ deck_definition: DeckDefinitionV5,
293
+ is_topmost_labware: bool,
294
+ labware_location: OnLabwareLocation,
295
+ underlying_ancestor_orientation: ModuleOrientation,
296
+ ) -> Point:
297
+ ...
298
+
299
+
300
+ def _get_parent_placement_origin_to_lw_origin(
301
+ child_labware: LabwareDefinition,
302
+ parent_deck_item: _LabwareStackupDefinition,
303
+ module_parent_to_child_offset: Point | None,
304
+ deck_definition: DeckDefinitionV5,
305
+ is_topmost_labware: bool,
306
+ labware_location: LabwareLocation,
307
+ underlying_ancestor_orientation: ModuleOrientation,
308
+ ) -> Point:
309
+ """Returns the offset from parent entity's placement origin to child labware origin.
310
+
311
+ Placement origin varies depending on the parent entity type (labware v3 are the back left bottom, and
312
+ labware v2, modules, & deck location types are the front left bottom).
313
+
314
+ Only parent-child specific offsets are calculated. Offsets that apply to a single entity
315
+ (ex., module cal) or the entire stackup (ex., LPC) are handled elsewhere.
316
+ """
317
+ if isinstance(child_labware, LabwareDefinition2) or isinstance(
318
+ parent_deck_item, LabwareDefinition2
319
+ ):
320
+ parent_deck_item_origin_to_child_labware_placement_origin = (
321
+ _get_parent_deck_item_origin_to_child_labware_placement_origin(
322
+ child_labware=child_labware,
323
+ parent_deck_item=parent_deck_item,
324
+ module_parent_to_child_offset=module_parent_to_child_offset,
325
+ deck_definition=deck_definition,
326
+ labware_location=labware_location,
327
+ )
328
+ )
329
+
330
+ # For v2 definitions, cornerOffsetFromSlot is the parent entity placement origin to child labware origin offset.
331
+ # For compatibility with historical (buggy?) behavior,
332
+ # we only consider it when the child labware is the topmost labware in a stackup.
333
+ if isinstance(child_labware, LabwareDefinition2):
334
+ parent_deck_item_to_child_labware_offset = (
335
+ Point.from_xyz_attrs(child_labware.cornerOffsetFromSlot)
336
+ if is_topmost_labware
337
+ else Point(0, 0, 0)
338
+ )
339
+
340
+ return (
341
+ parent_deck_item_origin_to_child_labware_placement_origin
342
+ + parent_deck_item_to_child_labware_offset
343
+ )
344
+ else:
345
+ assert isinstance(child_labware, LabwareDefinition3)
346
+ parent_deck_item_to_child_labware_back_left = Point(
347
+ x=0, y=child_labware.extents.total.frontRightTop.y * -1, z=0
348
+ )
349
+ child_labware_back_left_to_child_labware_origin = (
350
+ _get_corner_offset_from_extents(child_labware)
351
+ if is_topmost_labware
352
+ else Point(0, 0, 0)
353
+ )
354
+
355
+ return (
356
+ parent_deck_item_origin_to_child_labware_placement_origin # Only the Z-offset in this case.
357
+ + parent_deck_item_to_child_labware_back_left
358
+ + child_labware_back_left_to_child_labware_origin
359
+ )
360
+ else:
361
+ # For v3 definitions, get the vector from the back left bottom to the front right bottom.
362
+ assert_type(child_labware, LabwareDefinition3)
363
+
364
+ # TODO(jh, 06-25-25): This code is entirely temporary and only exists for the purposes of more useful
365
+ # snapshot testing. This code should exist in NO capacity after features are implemented outside of the
366
+ # module_parent_to_child_offset.
367
+ if _shim_does_locating_feature_pair_exist(
368
+ child_labware=child_labware,
369
+ parent_deck_item=_get_standardized_parent_deck_item(parent_deck_item),
370
+ ):
371
+ parent_deck_item_origin_to_child_labware_placement_origin = (
372
+ _module_parent_to_child_offset(
373
+ module_parent_to_child_offset, labware_location
374
+ )
375
+ )
376
+ else:
377
+ parent_deck_item_origin_to_child_labware_placement_origin = (
378
+ _get_parent_deck_item_origin_to_child_labware_placement_origin(
379
+ child_labware=child_labware,
380
+ parent_deck_item=parent_deck_item,
381
+ module_parent_to_child_offset=module_parent_to_child_offset,
382
+ deck_definition=deck_definition,
383
+ labware_location=labware_location,
384
+ )
385
+ )
386
+
387
+ parent_deck_item_to_child_labware_feature_offset = (
388
+ _parent_deck_item_to_child_labware_feature_offset(
389
+ child_labware=child_labware,
390
+ parent_deck_item=_get_standardized_parent_deck_item(parent_deck_item),
391
+ underlying_ancestor_orientation=underlying_ancestor_orientation,
392
+ )
393
+ ) + _feature_exception_offsets(
394
+ deck_definition=deck_definition, parent_deck_item=parent_deck_item
395
+ )
396
+
397
+ return (
398
+ parent_deck_item_origin_to_child_labware_placement_origin
399
+ + parent_deck_item_to_child_labware_feature_offset
400
+ )
401
+
402
+
403
+ def _get_parent_deck_item_origin_to_child_labware_placement_origin(
404
+ child_labware: LabwareDefinition,
405
+ parent_deck_item: _LabwareStackupDefinition,
406
+ module_parent_to_child_offset: Point | None,
407
+ deck_definition: DeckDefinitionV5,
408
+ labware_location: LabwareLocation,
409
+ ) -> Point:
410
+ """Get the offset vector from parent entity origin to child labware placement origin."""
411
+ if isinstance(labware_location, (DeckSlotLocation, AddressableAreaLocation)):
412
+ return Point(x=0, y=0, z=0)
413
+
414
+ elif isinstance(labware_location, ModuleLocation):
415
+ assert isinstance(parent_deck_item, ModuleDefinition)
416
+
417
+ child_labware_overlap_with_parent_deck_item = (
418
+ _get_child_labware_overlap_with_parent_module(
419
+ child_labware=child_labware,
420
+ parent_module_model=parent_deck_item.model,
421
+ deck_definition=deck_definition,
422
+ )
423
+ )
424
+ module_parent_to_child_offset = _module_parent_to_child_offset(
425
+ module_parent_to_child_offset, labware_location
426
+ )
427
+
428
+ return (
429
+ module_parent_to_child_offset - child_labware_overlap_with_parent_deck_item
430
+ )
431
+
432
+ elif isinstance(labware_location, OnLabwareLocation):
433
+ assert isinstance(parent_deck_item, (LabwareDefinition2, LabwareDefinition3))
434
+
435
+ # TODO(jh, 06-05-25): This logic is slightly duplicative of LabwareView get_dimensions. Can we unify?
436
+ if isinstance(parent_deck_item, LabwareDefinition2):
437
+ parent_deck_item_height = parent_deck_item.dimensions.zDimension
438
+ else:
439
+ assert_type(parent_deck_item, LabwareDefinition3)
440
+ parent_deck_item_height = (
441
+ parent_deck_item.extents.total.frontRightTop.z
442
+ - parent_deck_item.extents.total.backLeftBottom.z
443
+ )
444
+
445
+ child_labware_overlap_with_parent_deck_item = (
446
+ _get_child_labware_overlap_with_parent_labware(
447
+ child_labware=child_labware,
448
+ parent_labware_name=parent_deck_item.parameters.loadName,
449
+ )
450
+ )
451
+
452
+ return Point(
453
+ x=child_labware_overlap_with_parent_deck_item.x,
454
+ y=child_labware_overlap_with_parent_deck_item.y,
455
+ z=parent_deck_item_height - child_labware_overlap_with_parent_deck_item.z,
456
+ )
457
+
458
+ else:
459
+ raise TypeError(f"Unsupported labware location type: {labware_location}")
460
+
461
+
462
+ def _get_corner_offset_from_extents(child_labware: LabwareDefinition3) -> Point:
463
+ """Derive the corner offset from slot from a LabwareDefinition3's extents."""
464
+ back_left_bottom = child_labware.extents.total.backLeftBottom
465
+
466
+ x = back_left_bottom.x
467
+ y = back_left_bottom.y * -1
468
+ z = back_left_bottom.z
469
+
470
+ return Point(x, y, z)
471
+
472
+
473
+ def _module_parent_to_child_offset(
474
+ module_parent_to_child_offset: Point | None,
475
+ labware_location: LabwareLocation,
476
+ ) -> Point:
477
+ """Returns the module offset if applicable."""
478
+ if (
479
+ isinstance(labware_location, ModuleLocation)
480
+ and module_parent_to_child_offset is not None
481
+ ):
482
+ return Point.from_xyz_attrs(module_parent_to_child_offset)
483
+ else:
484
+ return Point(0, 0, 0)
485
+
486
+
487
+ def _shim_does_locating_feature_pair_exist(
488
+ child_labware: LabwareDefinition3,
489
+ parent_deck_item: _Labware3SupportedParentDefinition,
490
+ ) -> bool:
491
+ """Temporary util."""
492
+ slot_footprint_exists = (
493
+ parent_deck_item.features.get("slotFootprintAsParent") is not None
494
+ and child_labware.features.get("slotFootprintAsChild") is not None
495
+ )
496
+ flex_tiprack_lid_exists = (
497
+ parent_deck_item.features.get("opentronsFlexTipRackLidAsParent") is not None
498
+ and child_labware.features.get("opentronsFlexTipRackLidAsChild") is not None
499
+ )
500
+ hs_universal_flat_adapter_exists = (
501
+ parent_deck_item.features.get("heaterShakerUniversalFlatAdapter") is not None
502
+ and child_labware.features.get("flatSupportThermalCouplingAsChild") is not None
503
+ )
504
+ hs_universal_flat_adapter_screw_anchored_exists = (
505
+ parent_deck_item.features.get("screwAnchoredAsParent") is not None
506
+ and child_labware.features.get("heaterShakerUniversalFlatAdapter") is not None
507
+ )
508
+ screw_anchored_exists = (
509
+ parent_deck_item.features.get("screwAnchoredAsParent") is not None
510
+ and child_labware.features.get("screwAnchoredAsChild") is not None
511
+ )
512
+
513
+ return (
514
+ slot_footprint_exists
515
+ or flex_tiprack_lid_exists
516
+ or hs_universal_flat_adapter_exists
517
+ or hs_universal_flat_adapter_screw_anchored_exists
518
+ or screw_anchored_exists
519
+ )
520
+
521
+
522
+ def _get_standardized_parent_deck_item(
523
+ parent_deck_item: Union[
524
+ LabwareDefinition3, DeckLocationDefinition, ModuleDefinition
525
+ ],
526
+ ) -> _Labware3SupportedParentDefinition:
527
+ """Returns a standardized parent deck item interface."""
528
+ if isinstance(parent_deck_item, ModuleDefinition):
529
+ slot_footprint_as_parent = _module_slot_footprint_as_parent(parent_deck_item)
530
+ if slot_footprint_as_parent is not None:
531
+ return _Labware3SupportedParentDefinition(
532
+ features={
533
+ **parent_deck_item.features,
534
+ "slotFootprintAsParent": slot_footprint_as_parent,
535
+ },
536
+ extents=parent_deck_item.extents,
537
+ )
538
+ else:
539
+ return _Labware3SupportedParentDefinition(
540
+ features=parent_deck_item.features, extents=parent_deck_item.extents
541
+ )
542
+ elif isinstance(parent_deck_item, AddressableArea):
543
+ extents = Extents(
544
+ total=AxisAlignedBoundingBox3D(
545
+ backLeftBottom=Vector3D(x=0, y=0, z=0),
546
+ frontRightTop=Vector3D(
547
+ x=parent_deck_item.bounding_box.x,
548
+ y=parent_deck_item.bounding_box.y * 1,
549
+ z=parent_deck_item.bounding_box.z,
550
+ ),
551
+ )
552
+ )
553
+
554
+ slot_footprint_as_parent = _aa_slot_footprint_as_parent(parent_deck_item)
555
+ if slot_footprint_as_parent is not None:
556
+ return _Labware3SupportedParentDefinition(
557
+ features={
558
+ **parent_deck_item.features,
559
+ "slotFootprintAsParent": slot_footprint_as_parent,
560
+ },
561
+ extents=extents,
562
+ )
563
+ else:
564
+ return _Labware3SupportedParentDefinition(
565
+ parent_deck_item.features, extents=extents
566
+ )
567
+ elif isinstance(parent_deck_item, LabwareDefinition3):
568
+ return _Labware3SupportedParentDefinition(
569
+ features=parent_deck_item.features, extents=parent_deck_item.extents
570
+ )
571
+ # The slotDefV3 case.
572
+ else:
573
+ extents = Extents(
574
+ total=AxisAlignedBoundingBox3D(
575
+ backLeftBottom=Vector3D(x=0, y=0, z=0),
576
+ frontRightTop=Vector3D(
577
+ x=parent_deck_item["boundingBox"]["xDimension"],
578
+ y=parent_deck_item["boundingBox"]["yDimension"] * 1,
579
+ z=parent_deck_item["boundingBox"]["zDimension"],
580
+ ),
581
+ )
582
+ )
583
+ slot_footprint_as_parent = _slot_def_slot_footprint_as_parent(parent_deck_item)
584
+ return _Labware3SupportedParentDefinition(
585
+ features={
586
+ **parent_deck_item["features"],
587
+ "slotFootprintAsParent": slot_footprint_as_parent,
588
+ },
589
+ extents=extents,
590
+ )
591
+
592
+
593
+ def _module_slot_footprint_as_parent(
594
+ parent_deck_item: ModuleDefinition,
595
+ ) -> SlotFootprintAsParentFeature | None:
596
+ """Returns the slot footprint as parent feature if inherently supported by the module definition.
597
+
598
+ This utility is a normalization shim until labwareOffset + labwareInterfaceX/YDimension is deleted in module defs
599
+ and replaced with the same slotFootprintAsParent that exists in labware def v3.
600
+ """
601
+ dimensions = parent_deck_item.dimensions
602
+ if (
603
+ dimensions.labwareInterfaceYDimension is None
604
+ or dimensions.labwareInterfaceXDimension is None
605
+ ):
606
+ return None
607
+ else:
608
+ # Modules with springs would require special mating types and therefore are not handled here.
609
+ return SlotFootprintAsParentFeature(
610
+ z=0,
611
+ backLeft={"x": 0, "y": dimensions.labwareInterfaceYDimension},
612
+ frontRight={"x": dimensions.labwareInterfaceXDimension, "y": 0},
613
+ )
614
+
615
+
616
+ def _aa_slot_footprint_as_parent(
617
+ parent_deck_item: AddressableArea,
618
+ ) -> SlotFootprintAsParentFeature | None:
619
+ """Returns the slot footprint as parent feature for addressable areas.
620
+
621
+ This utility is a normalization shim until bounding box in deck defs and
622
+ replaced with the same slotFootprintAsParent that exists in labware def v3.
623
+ """
624
+ bb = parent_deck_item.bounding_box
625
+
626
+ if parent_deck_item.mating_surface_unit_vector is not None:
627
+ if parent_deck_item.mating_surface_unit_vector == [-1, 1, -1]:
628
+ return SlotFootprintAsParentFeature(
629
+ z=0,
630
+ backLeft={"x": 0, "y": bb.y},
631
+ frontRight={"x": bb.x, "y": 0},
632
+ springDirectionalForce="backLeftBottom",
633
+ )
634
+ else:
635
+ raise NotImplementedError(
636
+ "Slot footprint as parent does not support mating surface unit vector."
637
+ )
638
+ else:
639
+ return SlotFootprintAsParentFeature(
640
+ z=0,
641
+ backLeft={"x": 0, "y": bb.y},
642
+ frontRight={"x": bb.x, "y": 0},
643
+ )
644
+
645
+
646
+ def _slot_def_slot_footprint_as_parent(
647
+ parent_deck_item: SlotDefV3,
648
+ ) -> SlotFootprintAsParentFeature:
649
+ """Returns the slot footprint as parent feature for slot definitions.
650
+
651
+ This utility is a normalization shim until bounding box in deck defs and
652
+ replaced with the same slotFootprintAsParent that exists in labware def v3.
653
+ """
654
+ bb = parent_deck_item["boundingBox"]
655
+ return SlotFootprintAsParentFeature(
656
+ z=0,
657
+ backLeft={"x": 0, "y": bb["yDimension"]},
658
+ frontRight={"x": bb["xDimension"], "y": 0},
659
+ springDirectionalForce="backLeftBottom",
660
+ )
661
+
662
+
663
+ def _parent_deck_item_to_child_labware_feature_offset(
664
+ child_labware: LabwareDefinition3,
665
+ parent_deck_item: _Labware3SupportedParentDefinition,
666
+ underlying_ancestor_orientation: ModuleOrientation,
667
+ ) -> Point:
668
+ """Get the offset vector from the parent entity origin to the child labware origin."""
669
+ if parent_deck_item.features.get("heaterShakerUniversalFlatAdapter") is not None:
670
+ if child_labware.features.get("flatSupportThermalCouplingAsChild") is not None:
671
+ return _parent_origin_to_heater_shaker_universal_flat_adapter_feature(
672
+ parent_deck_item=parent_deck_item,
673
+ underlying_ancestor_orientation=underlying_ancestor_orientation,
674
+ ) + _heater_shaker_universal_flat_adapter_feature_to_child_origin(
675
+ child_labware=child_labware,
676
+ underlying_ancestor_orientation=underlying_ancestor_orientation,
677
+ )
678
+ else:
679
+ raise MissingLocatingFeatureError(
680
+ labware_name=child_labware.metadata.displayName,
681
+ required_feature="flatSupportThermalCouplingAsChild",
682
+ )
683
+
684
+ elif (
685
+ parent_deck_item.features.get("opentronsFlexTipRackLidAsParent") is not None
686
+ and child_labware.features.get("opentronsFlexTipRackLidAsChild") is not None
687
+ ):
688
+ # TODO(jh, 07-29-25): Support center X/Y calculation after addressing grip point
689
+ # calculations. See #18929 discussion.
690
+ return _parent_origin_to_flex_tip_rack_lid_feature(
691
+ parent_deck_item
692
+ ) + _flex_tip_rack_lid_feature_to_child_origin(child_labware)
693
+ elif (
694
+ parent_deck_item.features.get("screwAnchoredAsParent") is not None
695
+ and _get_screw_anchored_center_for_child(
696
+ child_labware, underlying_ancestor_orientation
697
+ )
698
+ is not None
699
+ ):
700
+ return _parent_origin_to_screw_anchored_feature(
701
+ parent_deck_item
702
+ ) + _screw_anchored_feature_to_child_origin(
703
+ child_labware, underlying_ancestor_orientation
704
+ )
705
+ elif (
706
+ parent_deck_item.features.get("slotFootprintAsParent") is not None
707
+ and child_labware.features.get("slotFootprintAsChild") is not None
708
+ ):
709
+ spring_force = _get_spring_force(child_labware, parent_deck_item)
710
+
711
+ if spring_force is not None:
712
+ if spring_force == "backLeftBottom":
713
+ return _parent_origin_to_slot_back_left_bottom(
714
+ parent_deck_item
715
+ ) + _slot_back_left_bottom_to_child_origin(child_labware)
716
+ else:
717
+ raise NotImplementedError(f"Spring force: {spring_force}")
718
+ else:
719
+ return _parent_origin_to_slot_bottom_center(
720
+ parent_deck_item
721
+ ) + slot_bottom_center_to_child_origin(child_labware)
722
+ else:
723
+ # TODO(jh, 06-25-25): This is a temporary shim to unblock FE usage with LW Def3 and more accurately diff
724
+ # ongoing positioning snapshot changes, but we should throw an error after adding all locating features
725
+ # if no appropriate LF pair is found.
726
+ return Point(0, 0, 0)
727
+
728
+
729
+ def _get_spring_force(
730
+ child_labware: LabwareDefinition3,
731
+ parent_deck_item: _Labware3SupportedParentDefinition,
732
+ ) -> SpringDirectionalForce | None:
733
+ """Returns whether the parent-child stackup has a spring that affects positioning."""
734
+ assert parent_deck_item.features.get("slotFootprintAsParent") is not None
735
+ assert child_labware.features.get("slotFootprintAsChild") is not None
736
+
737
+ parent_spring_force = parent_deck_item.features["slotFootprintAsParent"].get(
738
+ "springDirectionalForce"
739
+ )
740
+ child_spring_force = child_labware.features["slotFootprintAsChild"].get(
741
+ "springDirectionalForce"
742
+ )
743
+
744
+ if parent_spring_force is not None and child_spring_force is not None:
745
+ if parent_spring_force != child_spring_force:
746
+ raise IncompatibleLocatingFeatureError(
747
+ parent_feature=f"slotFootprintAsParent spring force: {parent_spring_force}",
748
+ child_feature=f"slotFootprintAsChild spring force: {child_spring_force}",
749
+ )
750
+
751
+ return parent_spring_force or child_spring_force
752
+
753
+
754
+ def _get_screw_anchored_center_for_child(
755
+ child_labware: LabwareDefinition3,
756
+ underlying_ancestor_orientation: ModuleOrientation,
757
+ ) -> LabwareVector3D | None:
758
+ """Returns the screw center if it exists in any locating feature that supports screw anchoring."""
759
+ hs_universal_flat_adapter_feature = child_labware.features.get(
760
+ "heaterShakerUniversalFlatAdapter"
761
+ )
762
+ screw_anchored_as_child_feature = child_labware.features.get("screwAnchoredAsChild")
763
+
764
+ if hs_universal_flat_adapter_feature is not None:
765
+ if underlying_ancestor_orientation == ModuleOrientation.LEFT:
766
+ x = hs_universal_flat_adapter_feature["deckLeft"]["screwCenter"]["x"]
767
+ y = hs_universal_flat_adapter_feature["deckLeft"]["screwCenter"]["y"]
768
+ return LabwareVector3D(x=x, y=y, z=0)
769
+ elif underlying_ancestor_orientation == ModuleOrientation.RIGHT:
770
+ x = hs_universal_flat_adapter_feature["deckRight"]["screwCenter"]["x"]
771
+ y = hs_universal_flat_adapter_feature["deckRight"]["screwCenter"]["y"]
772
+ return LabwareVector3D(x=x, y=y, z=0)
773
+ else:
774
+ raise InvalidLabwarePlacementError(
775
+ feature_name="heaterShakerUniversalFlatAdapter",
776
+ invalid_placement=ModuleOrientation.CENTER.value,
777
+ )
778
+ elif screw_anchored_as_child_feature is not None:
779
+ return screw_anchored_as_child_feature["screwCenter"]
780
+ else:
781
+ return None
782
+
783
+
784
+ def _parent_origin_to_flex_tip_rack_lid_feature(
785
+ parent_deck_item: _Labware3SupportedParentDefinition,
786
+ ) -> Point:
787
+ """Returns the offset from a deck item's origin to the Flex tip rack lid locating feature."""
788
+ flex_tip_rack_lid_as_parent = parent_deck_item.features.get(
789
+ "opentronsFlexTipRackLidAsParent"
790
+ )
791
+ assert flex_tip_rack_lid_as_parent is not None
792
+
793
+ return Point(x=0, y=0, z=flex_tip_rack_lid_as_parent["matingZ"])
794
+
795
+
796
+ def _parent_origin_to_slot_bottom_center(
797
+ parent_deck_item: _Labware3SupportedParentDefinition,
798
+ ) -> Point:
799
+ """Returns the offset from a deck item's origin to the bottom center of the slot that it provides."""
800
+ slot_footprint_as_parent = parent_deck_item.features.get("slotFootprintAsParent")
801
+ assert slot_footprint_as_parent is not None
802
+
803
+ x = (
804
+ slot_footprint_as_parent["frontRight"]["x"]
805
+ + slot_footprint_as_parent["backLeft"]["x"]
806
+ ) / 2
807
+ y = (
808
+ slot_footprint_as_parent["frontRight"]["y"]
809
+ + slot_footprint_as_parent["backLeft"]["y"]
810
+ ) / 2
811
+ z = slot_footprint_as_parent["z"]
812
+
813
+ return Point(x, y, z)
814
+
815
+
816
+ def _parent_origin_to_slot_back_left_bottom(
817
+ parent_deck_item: _Labware3SupportedParentDefinition,
818
+ ) -> Point:
819
+ """Returns the offset from a deck item's origin to the back left bottom of the slot that it provides."""
820
+ slot_footprint_as_parent = parent_deck_item.features.get("slotFootprintAsParent")
821
+ assert slot_footprint_as_parent is not None
822
+
823
+ x = slot_footprint_as_parent["backLeft"]["x"]
824
+ y = slot_footprint_as_parent["backLeft"]["y"]
825
+ z = slot_footprint_as_parent["z"]
826
+
827
+ return Point(x, y, z)
828
+
829
+
830
+ def _parent_origin_to_heater_shaker_universal_flat_adapter_feature(
831
+ parent_deck_item: _Labware3SupportedParentDefinition,
832
+ underlying_ancestor_orientation: ModuleOrientation,
833
+ ) -> Point:
834
+ """Returns the offset from a deck item's origin to the Heater Shaker Universal Flat Adapter locating feature."""
835
+ flat_adapter_feature = parent_deck_item.features.get(
836
+ "heaterShakerUniversalFlatAdapter"
837
+ )
838
+ assert flat_adapter_feature is not None
839
+
840
+ flat_well_support_z = flat_adapter_feature["flatSupportThermalCouplingZ"]
841
+ extents = parent_deck_item.extents.total
842
+
843
+ if underlying_ancestor_orientation == ModuleOrientation.LEFT:
844
+ left_wall_x = flat_adapter_feature["deckLeft"]["wallX"]
845
+ left_side_center_x = extents.backLeftBottom.x + left_wall_x
846
+ left_side_center_y = (extents.backLeftBottom.y + extents.frontRightTop.y) / 2
847
+
848
+ return Point(left_side_center_x, left_side_center_y, flat_well_support_z)
849
+ elif underlying_ancestor_orientation == ModuleOrientation.RIGHT:
850
+ right_wall_x = flat_adapter_feature["deckRight"]["wallX"]
851
+ right_side_center_x = extents.frontRightTop.x + right_wall_x
852
+ right_side_center_y = (extents.backLeftBottom.y + extents.frontRightTop.y) / 2
853
+
854
+ return Point(right_side_center_x, right_side_center_y, flat_well_support_z)
855
+
856
+ else:
857
+ raise InvalidLabwarePlacementError(
858
+ feature_name="heaterShakerUniversalFlatAdapter",
859
+ invalid_placement=ModuleOrientation.CENTER.value,
860
+ )
861
+
862
+
863
+ def _parent_origin_to_screw_anchored_feature(
864
+ parent_deck_item: _Labware3SupportedParentDefinition,
865
+ ) -> Point:
866
+ """Returns the offset from a deck item's origin to the `screwAnchoredAsParent` locating feature."""
867
+ feature = parent_deck_item.features.get("screwAnchoredAsParent")
868
+ assert feature is not None
869
+
870
+ screw_center_x = feature["screwCenter"]["x"]
871
+ screw_center_y = feature["screwCenter"]["y"]
872
+ screw_center_z = feature["screwCenter"]["z"]
873
+
874
+ return Point(x=screw_center_x, y=screw_center_y, z=screw_center_z)
875
+
876
+
877
+ def _flex_tip_rack_lid_feature_to_child_origin(
878
+ child_labware: LabwareDefinition3,
879
+ ) -> Point:
880
+ """Returns the offset from a Flex tip rack lid locating feature to the child origin."""
881
+ flex_tip_rack_lid_as_child = child_labware.features.get(
882
+ "opentronsFlexTipRackLidAsChild"
883
+ )
884
+ assert flex_tip_rack_lid_as_child is not None
885
+
886
+ return Point(x=0, y=0, z=flex_tip_rack_lid_as_child["matingZ"])
887
+
888
+
889
+ def slot_bottom_center_to_child_origin(
890
+ child_labware: LabwareDefinition3,
891
+ ) -> Point:
892
+ """Returns offset from a parent slot's bottom center to the child origin."""
893
+ slot_footprint_as_child = child_labware.features.get("slotFootprintAsChild")
894
+ assert slot_footprint_as_child is not None
895
+
896
+ x = (
897
+ slot_footprint_as_child["frontRight"]["x"]
898
+ + slot_footprint_as_child["backLeft"]["x"]
899
+ ) / 2
900
+ y = (
901
+ slot_footprint_as_child["frontRight"]["y"]
902
+ + slot_footprint_as_child["backLeft"]["y"]
903
+ ) / 2
904
+ z = slot_footprint_as_child["z"]
905
+
906
+ return Point(x, y, z) * -1
907
+
908
+
909
+ def _slot_back_left_bottom_to_child_origin(
910
+ child_labware: LabwareDefinition3,
911
+ ) -> Point:
912
+ """Returns offset from a parent slot's back left bottom to the child's origin."""
913
+ slot_footprint_as_child = child_labware.features.get("slotFootprintAsChild")
914
+ assert slot_footprint_as_child is not None
915
+
916
+ x = slot_footprint_as_child["backLeft"]["x"]
917
+ y = slot_footprint_as_child["backLeft"]["y"]
918
+ z = slot_footprint_as_child["z"]
919
+
920
+ return Point(x, y, z) * -1
921
+
922
+
923
+ def _child_back_left_bottom_position(child_labware: LabwareDefinition3) -> Point:
924
+ """Get the back left bottom position from a v3 labware definition."""
925
+ footprint_as_child = _get_labware_footprint_as_child(child_labware)
926
+
927
+ return Point(
928
+ x=footprint_as_child["backLeft"]["x"],
929
+ y=footprint_as_child["frontRight"]["y"],
930
+ z=footprint_as_child["z"],
931
+ )
932
+
933
+
934
+ def _heater_shaker_universal_flat_adapter_feature_to_child_origin(
935
+ child_labware: LabwareDefinition3,
936
+ underlying_ancestor_orientation: ModuleOrientation,
937
+ ) -> Point:
938
+ """Returns the offset from a Heater Shaker Universal Flat Adapter locating feature to the child origin."""
939
+ flat_well_support_as_child = child_labware.features.get(
940
+ "flatSupportThermalCouplingAsChild"
941
+ )
942
+
943
+ assert flat_well_support_as_child is not None
944
+
945
+ well_exterior_bottom_z = flat_well_support_as_child["wellExteriorBottomZ"]
946
+ extents = child_labware.extents.total
947
+
948
+ if underlying_ancestor_orientation == ModuleOrientation.LEFT:
949
+ left_side_center_x = extents.backLeftBottom.x
950
+ left_side_center_y = (extents.backLeftBottom.y + extents.frontRightTop.y) / 2
951
+
952
+ return (
953
+ Point(left_side_center_x, left_side_center_y, well_exterior_bottom_z) * -1
954
+ )
955
+ elif underlying_ancestor_orientation == ModuleOrientation.RIGHT:
956
+ right_side_center_x = extents.frontRightTop.x
957
+ right_side_center_y = (extents.backLeftBottom.y + extents.frontRightTop.y) / 2
958
+
959
+ return (
960
+ Point(right_side_center_x, right_side_center_y, well_exterior_bottom_z) * -1
961
+ )
962
+
963
+ else:
964
+ raise InvalidLabwarePlacementError(
965
+ feature_name="heaterShakerUniversalFlatAdapter",
966
+ invalid_placement=ModuleOrientation.CENTER.value,
967
+ )
968
+
969
+
970
+ def _screw_anchored_feature_to_child_origin(
971
+ child_labware: LabwareDefinition3,
972
+ underlying_ancestor_orientation: ModuleOrientation,
973
+ ) -> Point:
974
+ """Returns the offset from a `screwAnchoredAsChild` locating feature to the child origin."""
975
+ screw_center = _get_screw_anchored_center_for_child(
976
+ child_labware, underlying_ancestor_orientation
977
+ )
978
+ assert screw_center is not None
979
+
980
+ screw_center_x = screw_center["x"]
981
+ screw_center_y = screw_center["y"]
982
+ screw_center_z = screw_center["z"]
983
+
984
+ return Point(x=screw_center_x, y=screw_center_y, z=screw_center_z) * -1
985
+
986
+
987
+ def _get_child_labware_overlap_with_parent_labware(
988
+ child_labware: LabwareDefinition, parent_labware_name: str
989
+ ) -> Point:
990
+ """Get the child labware's overlap with the parent labware's load name."""
991
+ if isinstance(child_labware, LabwareDefinition3) and not hasattr(
992
+ child_labware, "legacyStackingOffsetWithLabware"
993
+ ):
994
+ raise NotImplementedError(
995
+ f"Labware {child_labware.metadata.displayName} contains no legacyStackingOffsetWithLabware. "
996
+ f"Either add this property explictly on the definition or update your protocol's API Level."
997
+ )
998
+
999
+ overlap = (
1000
+ child_labware.stackingOffsetWithLabware.get(parent_labware_name)
1001
+ if isinstance(child_labware, LabwareDefinition2)
1002
+ else child_labware.legacyStackingOffsetWithLabware.get(parent_labware_name)
1003
+ )
1004
+
1005
+ if overlap is None:
1006
+ overlap = (
1007
+ child_labware.stackingOffsetWithLabware.get("default")
1008
+ if isinstance(child_labware, LabwareDefinition2)
1009
+ else child_labware.legacyStackingOffsetWithLabware.get("default")
1010
+ )
1011
+
1012
+ if overlap is None:
1013
+ if isinstance(child_labware, LabwareDefinition3):
1014
+ raise ValueError(
1015
+ f"No default labware overlap specified for parent labware: {parent_labware_name} "
1016
+ f"in legacyStackingOffsetWithLabware."
1017
+ )
1018
+ else:
1019
+ raise ValueError(
1020
+ f"No default labware overlap specified for parent labware: {parent_labware_name} "
1021
+ f"in stackingOffsetWithLabware."
1022
+ )
1023
+ else:
1024
+ return Point.from_xyz_attrs(overlap)
1025
+
1026
+
1027
+ def _get_child_labware_overlap_with_parent_module(
1028
+ child_labware: LabwareDefinition,
1029
+ parent_module_model: ModuleModel,
1030
+ deck_definition: DeckDefinitionV5,
1031
+ ) -> Point:
1032
+ """Get the child labware's overlap with the parent module model."""
1033
+ child_labware_overlap = child_labware.stackingOffsetWithModule.get(
1034
+ str(parent_module_model.value)
1035
+ )
1036
+ if not child_labware_overlap:
1037
+ if _is_thermocycler_on_ot2(parent_module_model, deck_definition):
1038
+ return _OFFSET_ON_TC_OT2
1039
+ else:
1040
+ return Point(x=0, y=0, z=0)
1041
+
1042
+ return Point.from_xyz_attrs(child_labware_overlap)
1043
+
1044
+
1045
+ def _feature_exception_offsets(
1046
+ parent_deck_item: _LabwareStackupDefinition,
1047
+ deck_definition: DeckDefinitionV5,
1048
+ ) -> Point:
1049
+ """These offsets are intended for legacy reasons only and should generally be avoided post labware schema 2.
1050
+
1051
+ If you need to make exceptions for a parent-child stackup, use the `custom` locating feature.
1052
+ """
1053
+ if isinstance(parent_deck_item, ModuleDefinition) and _is_thermocycler_on_ot2(
1054
+ parent_deck_item.model, deck_definition
1055
+ ):
1056
+ return _OFFSET_ON_TC_OT2
1057
+ else:
1058
+ return Point(x=0, y=0, z=0)
1059
+
1060
+
1061
+ def _is_thermocycler_on_ot2(
1062
+ parent_module_model: ModuleModel,
1063
+ deck_definition: DeckDefinitionV5,
1064
+ ) -> bool:
1065
+ """Whether the given parent module is a thermocycler with the current deck being an OT2 deck."""
1066
+ robot_model = deck_definition["robot"]["model"]
1067
+ return (
1068
+ parent_module_model
1069
+ in [ModuleModel.THERMOCYCLER_MODULE_V1, ModuleModel.THERMOCYCLER_MODULE_V2]
1070
+ and robot_model == "OT-2 Standard"
1071
+ )
1072
+
1073
+
1074
+ def _get_labware_footprint_as_child(
1075
+ labware: LabwareDefinition3,
1076
+ ) -> SlotFootprintAsChildFeature:
1077
+ """Get the SlotFootprintAsChildFeature for labware definitions."""
1078
+ footprint_as_child = labware.features.get("slotFootprintAsChild")
1079
+ if footprint_as_child is None:
1080
+ raise MissingLocatingFeatureError(
1081
+ labware_name=labware.metadata.displayName,
1082
+ required_feature="slotFootprintAsChild",
1083
+ )
1084
+ else:
1085
+ return footprint_as_child
1086
+
1087
+
1088
+ def _total_nominal_gripper_offsets(
1089
+ stackup_lw_info_top_to_bottom: list[tuple[LabwareDefinition, LabwareLocation]],
1090
+ slot_name: DeckSlotName,
1091
+ deck_definition: DeckDefinitionV5,
1092
+ underlying_ancestor_definition: LabwareStackupAncestorDefinition,
1093
+ ) -> _GripperOffsets:
1094
+ """Get the total of the offsets to be used to pick up and drop labware."""
1095
+ top_most_lw_definition, top_most_lw_location = stackup_lw_info_top_to_bottom[0]
1096
+ special_offsets = _get_special_gripper_offsets(
1097
+ stackup_lw_info_top_to_bottom, underlying_ancestor_definition
1098
+ )
1099
+
1100
+ if isinstance(
1101
+ top_most_lw_location,
1102
+ (ModuleLocation, DeckSlotLocation, AddressableAreaLocation),
1103
+ ):
1104
+ offsets = _nominal_gripper_offsets_for_location(
1105
+ labware_location=top_most_lw_location,
1106
+ labware_definition=top_most_lw_definition,
1107
+ slot_name=slot_name,
1108
+ deck_definition=deck_definition,
1109
+ underlying_ancestor_definition=underlying_ancestor_definition,
1110
+ )
1111
+
1112
+ pick_up_offset = Point.from_xyz_attrs(offsets.pickUpOffset)
1113
+ drop_offset = Point.from_xyz_attrs(offsets.dropOffset)
1114
+
1115
+ return _GripperOffsets(
1116
+ pick_up_offset=pick_up_offset + special_offsets.pick_up_offset,
1117
+ drop_offset=drop_offset + special_offsets.drop_offset,
1118
+ )
1119
+ else:
1120
+ # If it's a labware on a labware (most likely an adapter),
1121
+ # we calculate the offset as sum of offsets for the direct parent labware
1122
+ # and the underlying non-labware parent location.
1123
+ direct_parent_def, direct_parent_loc = stackup_lw_info_top_to_bottom[1]
1124
+ direct_parent_offsets = _nominal_gripper_offsets_for_location(
1125
+ labware_location=direct_parent_loc,
1126
+ labware_definition=direct_parent_def,
1127
+ slot_name=slot_name,
1128
+ deck_definition=deck_definition,
1129
+ underlying_ancestor_definition=underlying_ancestor_definition,
1130
+ )
1131
+
1132
+ top_most_offsets = _nominal_gripper_offsets_for_location(
1133
+ labware_location=top_most_lw_location,
1134
+ labware_definition=top_most_lw_definition,
1135
+ slot_name=slot_name,
1136
+ deck_definition=deck_definition,
1137
+ underlying_ancestor_definition=underlying_ancestor_definition,
1138
+ )
1139
+
1140
+ pick_up_offset = Point.from_xyz_attrs(
1141
+ direct_parent_offsets.pickUpOffset
1142
+ ) + Point.from_xyz_attrs(top_most_offsets.pickUpOffset)
1143
+ drop_offset = Point.from_xyz_attrs(
1144
+ direct_parent_offsets.dropOffset
1145
+ ) + Point.from_xyz_attrs(top_most_offsets.dropOffset)
1146
+
1147
+ return _GripperOffsets(
1148
+ pick_up_offset=pick_up_offset + special_offsets.pick_up_offset,
1149
+ drop_offset=drop_offset + special_offsets.drop_offset,
1150
+ )
1151
+
1152
+
1153
+ # TODO(jh, 08-15-25): Return _GripperOffsets instead of LabwareMovementOffsetData.
1154
+ def _nominal_gripper_offsets_for_location(
1155
+ labware_location: LabwareLocation,
1156
+ labware_definition: LabwareDefinition,
1157
+ slot_name: DeckSlotName,
1158
+ deck_definition: DeckDefinitionV5,
1159
+ underlying_ancestor_definition: LabwareStackupAncestorDefinition,
1160
+ ) -> LabwareMovementOffsetData:
1161
+ """Provide the default gripper offset data for the given location type."""
1162
+ if isinstance(labware_location, (DeckSlotLocation, AddressableAreaLocation)):
1163
+ offsets = _get_deck_default_gripper_offsets(deck_definition)
1164
+ elif isinstance(labware_location, ModuleLocation):
1165
+ offsets = _get_module_default_gripper_offsets(underlying_ancestor_definition) # type: ignore[arg-type]
1166
+ else:
1167
+ offsets = _labware_gripper_offsets(
1168
+ top_most_lw_definition=labware_definition, slot_name=slot_name
1169
+ )
1170
+ return offsets or LabwareMovementOffsetData(
1171
+ pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0),
1172
+ dropOffset=LabwareOffsetVector(x=0, y=0, z=0),
1173
+ )
1174
+
1175
+
1176
+ def _get_deck_default_gripper_offsets(
1177
+ deck_definition: DeckDefinitionV5,
1178
+ ) -> Optional[LabwareMovementOffsetData]:
1179
+ """Get the deck's default gripper offsets."""
1180
+ parsed_offsets = deck_definition.get("gripperOffsets", {}).get("default")
1181
+ return (
1182
+ LabwareMovementOffsetData(
1183
+ pickUpOffset=LabwareOffsetVector(
1184
+ x=parsed_offsets["pickUpOffset"]["x"],
1185
+ y=parsed_offsets["pickUpOffset"]["y"],
1186
+ z=parsed_offsets["pickUpOffset"]["z"],
1187
+ ),
1188
+ dropOffset=LabwareOffsetVector(
1189
+ x=parsed_offsets["dropOffset"]["x"],
1190
+ y=parsed_offsets["dropOffset"]["y"],
1191
+ z=parsed_offsets["dropOffset"]["z"],
1192
+ ),
1193
+ )
1194
+ if parsed_offsets
1195
+ else None
1196
+ )
1197
+
1198
+
1199
+ def _get_module_default_gripper_offsets(
1200
+ module_definition: ModuleDefinition,
1201
+ ) -> Optional[LabwareMovementOffsetData]:
1202
+ """Get the deck's default gripper offsets."""
1203
+ offsets = module_definition.gripperOffsets
1204
+ return offsets.get("default") if offsets else None
1205
+
1206
+
1207
+ def _labware_gripper_offsets(
1208
+ top_most_lw_definition: LabwareDefinition, slot_name: DeckSlotName
1209
+ ) -> Optional[LabwareMovementOffsetData]:
1210
+ """Provide the most appropriate gripper offset data for the specified labware.
1211
+
1212
+ We check the types of gripper offsets available for the labware ("default" or slot-based)
1213
+ and return the most appropriate one for the overall location of the labware.
1214
+ Currently, only module adapters (specifically, the H/S universal flat adapter)
1215
+ have non-default offsets that are specific to location of the module on deck,
1216
+ so, this code only checks for the presence of those known offsets.
1217
+ """
1218
+ slot_based_offset = _get_child_gripper_offsets(
1219
+ top_most_lw_definition=top_most_lw_definition, slot_name=slot_name
1220
+ )
1221
+ return slot_based_offset or _get_child_gripper_offsets(
1222
+ top_most_lw_definition=top_most_lw_definition, slot_name=None
1223
+ )
1224
+
1225
+
1226
+ def _get_child_gripper_offsets(
1227
+ top_most_lw_definition: LabwareDefinition,
1228
+ slot_name: DeckSlotName | None,
1229
+ ) -> Optional[LabwareMovementOffsetData]:
1230
+ """Get the grip offsets that a labware says should be applied to children stacked atop it.
1231
+
1232
+ If `slot_name` is provided, returns the gripper offsets that the parent labware definition
1233
+ specifies just for that slot, or `None` if the labware definition doesn't have an
1234
+ exact match.
1235
+
1236
+ If `slot_name` is `None`, returns the gripper offsets that the parent labware
1237
+ definition designates as "default," or `None` if it doesn't designate any as such.
1238
+ """
1239
+ parsed_offsets = top_most_lw_definition.gripperOffsets
1240
+ offset_key = slot_name.id if slot_name else "default"
1241
+
1242
+ if parsed_offsets is None or offset_key not in parsed_offsets:
1243
+ return None
1244
+ else:
1245
+ return LabwareMovementOffsetData(
1246
+ pickUpOffset=LabwareOffsetVector.model_construct(
1247
+ x=parsed_offsets[offset_key].pickUpOffset.x,
1248
+ y=parsed_offsets[offset_key].pickUpOffset.y,
1249
+ z=parsed_offsets[offset_key].pickUpOffset.z,
1250
+ ),
1251
+ dropOffset=LabwareOffsetVector.model_construct(
1252
+ x=parsed_offsets[offset_key].dropOffset.x,
1253
+ y=parsed_offsets[offset_key].dropOffset.y,
1254
+ z=parsed_offsets[offset_key].dropOffset.z,
1255
+ ),
1256
+ )
1257
+
1258
+
1259
+ def _get_special_gripper_offsets(
1260
+ stackup_lw_info_top_to_bottom: list[tuple[LabwareDefinition, LabwareLocation]],
1261
+ underlying_ancestor_definition: LabwareStackupAncestorDefinition,
1262
+ ) -> _GripperOffsets:
1263
+ """Handles all special-cased gripper offsets."""
1264
+ tc_lid_offsets = (
1265
+ _get_tc_lid_gripper_offsets(
1266
+ stackup_lw_info_top_to_bottom, underlying_ancestor_definition
1267
+ )
1268
+ ) or _GripperOffsets(drop_offset=Point(), pick_up_offset=Point())
1269
+
1270
+ ar_lid_offsets = (
1271
+ _get_absorbance_reader_lid_gripper_offsets(stackup_lw_info_top_to_bottom)
1272
+ ) or _GripperOffsets(drop_offset=Point(), pick_up_offset=Point())
1273
+
1274
+ return _GripperOffsets(
1275
+ pick_up_offset=tc_lid_offsets.pick_up_offset + ar_lid_offsets.pick_up_offset,
1276
+ drop_offset=tc_lid_offsets.pick_up_offset + ar_lid_offsets.drop_offset,
1277
+ )
1278
+
1279
+
1280
+ def _get_tc_lid_gripper_offsets(
1281
+ stackup_lw_info_top_to_bottom: list[tuple[LabwareDefinition, LabwareLocation]],
1282
+ underlying_ancestor_definition: LabwareStackupAncestorDefinition,
1283
+ ) -> _GripperOffsets | None:
1284
+ top_most_lw_def, top_most_lw_loc = stackup_lw_info_top_to_bottom[0]
1285
+
1286
+ if isinstance(top_most_lw_loc, OnLabwareLocation):
1287
+ bottom_most_lw_location = stackup_lw_info_top_to_bottom[-1][1]
1288
+
1289
+ # This is done as a workaround for some TC geometry inaccuracies.
1290
+ # See PLAT-579 for context.
1291
+ if (
1292
+ isinstance(bottom_most_lw_location, ModuleLocation)
1293
+ and getattr(underlying_ancestor_definition, "model", None)
1294
+ == ModuleModel.THERMOCYCLER_MODULE_V2
1295
+ and validate_definition_is_lid(top_most_lw_def)
1296
+ ):
1297
+ # It is intentional to use the `pickUpOffset` in both the gripper pick up and drop cases.
1298
+ if "lidOffsets" in top_most_lw_def.gripperOffsets.keys():
1299
+ offset = Point(
1300
+ x=top_most_lw_def.gripperOffsets["lidOffsets"].pickUpOffset.x,
1301
+ y=top_most_lw_def.gripperOffsets["lidOffsets"].pickUpOffset.y,
1302
+ z=top_most_lw_def.gripperOffsets["lidOffsets"].pickUpOffset.z,
1303
+ )
1304
+ return _GripperOffsets(pick_up_offset=offset, drop_offset=offset)
1305
+ else:
1306
+ raise LabwareOffsetDoesNotExistError(
1307
+ f"Labware Definition {top_most_lw_def.parameters.loadName} does not contain required field 'lidOffsets' of 'gripperOffsets'."
1308
+ )
1309
+
1310
+ return None
1311
+
1312
+
1313
+ def _get_absorbance_reader_lid_gripper_offsets(
1314
+ stackup_lw_info_top_to_bottom: list[tuple[LabwareDefinition, LabwareLocation]],
1315
+ ) -> _GripperOffsets | None:
1316
+ top_most_lw_definition = stackup_lw_info_top_to_bottom[0][0]
1317
+ load_name = top_most_lw_definition.parameters.loadName
1318
+
1319
+ if is_absorbance_reader_lid(load_name):
1320
+ # todo(mm, 2024-11-06): This is only correct in the special case of an
1321
+ # absorbance reader lid. Its definition currently puts the offsets for *itself*
1322
+ # in the property that's normally meant for offsets for its *children.*
1323
+ offsets = _get_child_gripper_offsets(top_most_lw_definition, slot_name=None)
1324
+
1325
+ if offsets is None:
1326
+ raise ValueError(
1327
+ "Expected gripper offsets for absorbance reader lid to be defined."
1328
+ )
1329
+ else:
1330
+ return _GripperOffsets(
1331
+ pick_up_offset=Point.from_xyz_attrs(offsets.pickUpOffset),
1332
+ drop_offset=Point.from_xyz_attrs(offsets.dropOffset),
1333
+ )
1334
+
1335
+ else:
1336
+ return None