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