opentrons 8.7.0a9__py3-none-any.whl → 8.8.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.
- opentrons/_version.py +2 -2
- opentrons/cli/analyze.py +4 -1
- opentrons/config/__init__.py +7 -0
- opentrons/drivers/asyncio/communication/serial_connection.py +126 -49
- opentrons/drivers/heater_shaker/abstract.py +5 -0
- opentrons/drivers/heater_shaker/driver.py +10 -0
- opentrons/drivers/heater_shaker/simulator.py +4 -0
- opentrons/drivers/thermocycler/abstract.py +6 -0
- opentrons/drivers/thermocycler/driver.py +61 -10
- opentrons/drivers/thermocycler/simulator.py +6 -0
- opentrons/drivers/vacuum_module/__init__.py +5 -0
- opentrons/drivers/vacuum_module/abstract.py +93 -0
- opentrons/drivers/vacuum_module/driver.py +208 -0
- opentrons/drivers/vacuum_module/errors.py +39 -0
- opentrons/drivers/vacuum_module/simulator.py +85 -0
- opentrons/drivers/vacuum_module/types.py +79 -0
- opentrons/execute.py +3 -0
- opentrons/hardware_control/api.py +24 -5
- opentrons/hardware_control/backends/controller.py +8 -2
- opentrons/hardware_control/backends/flex_protocol.py +1 -0
- opentrons/hardware_control/backends/ot3controller.py +35 -2
- opentrons/hardware_control/backends/ot3simulator.py +3 -1
- opentrons/hardware_control/backends/ot3utils.py +37 -0
- opentrons/hardware_control/backends/simulator.py +2 -1
- opentrons/hardware_control/backends/subsystem_manager.py +5 -2
- opentrons/hardware_control/emulation/abstract_emulator.py +6 -4
- opentrons/hardware_control/emulation/connection_handler.py +8 -5
- opentrons/hardware_control/emulation/heater_shaker.py +12 -3
- opentrons/hardware_control/emulation/settings.py +1 -1
- opentrons/hardware_control/emulation/thermocycler.py +67 -15
- opentrons/hardware_control/module_control.py +105 -10
- opentrons/hardware_control/modules/__init__.py +3 -0
- opentrons/hardware_control/modules/absorbance_reader.py +11 -4
- opentrons/hardware_control/modules/flex_stacker.py +38 -9
- opentrons/hardware_control/modules/heater_shaker.py +42 -5
- opentrons/hardware_control/modules/magdeck.py +8 -4
- opentrons/hardware_control/modules/mod_abc.py +14 -6
- opentrons/hardware_control/modules/tempdeck.py +25 -5
- opentrons/hardware_control/modules/thermocycler.py +68 -11
- opentrons/hardware_control/modules/types.py +20 -1
- opentrons/hardware_control/modules/utils.py +11 -4
- opentrons/hardware_control/motion_utilities.py +6 -6
- opentrons/hardware_control/nozzle_manager.py +3 -0
- opentrons/hardware_control/ot3api.py +85 -17
- opentrons/hardware_control/poller.py +22 -8
- opentrons/hardware_control/protocols/liquid_handler.py +6 -2
- opentrons/hardware_control/scripts/update_module_fw.py +5 -0
- opentrons/hardware_control/types.py +43 -2
- opentrons/legacy_commands/commands.py +58 -5
- opentrons/legacy_commands/module_commands.py +52 -0
- opentrons/legacy_commands/protocol_commands.py +53 -1
- opentrons/legacy_commands/types.py +155 -1
- opentrons/motion_planning/deck_conflict.py +17 -12
- opentrons/motion_planning/waypoints.py +15 -29
- opentrons/protocol_api/__init__.py +5 -1
- opentrons/protocol_api/_transfer_liquid_validation.py +17 -2
- opentrons/protocol_api/_types.py +8 -1
- opentrons/protocol_api/core/common.py +3 -1
- opentrons/protocol_api/core/engine/_default_labware_versions.py +33 -11
- opentrons/protocol_api/core/engine/deck_conflict.py +3 -1
- opentrons/protocol_api/core/engine/instrument.py +109 -26
- opentrons/protocol_api/core/engine/labware.py +8 -1
- opentrons/protocol_api/core/engine/module_core.py +95 -4
- opentrons/protocol_api/core/engine/protocol.py +51 -2
- opentrons/protocol_api/core/engine/stringify.py +2 -0
- opentrons/protocol_api/core/engine/tasks.py +48 -0
- opentrons/protocol_api/core/engine/well.py +8 -0
- opentrons/protocol_api/core/instrument.py +19 -2
- opentrons/protocol_api/core/legacy/legacy_instrument_core.py +19 -2
- opentrons/protocol_api/core/legacy/legacy_module_core.py +33 -2
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +23 -1
- opentrons/protocol_api/core/legacy/legacy_well_core.py +4 -0
- opentrons/protocol_api/core/legacy/tasks.py +19 -0
- opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +19 -2
- opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +14 -2
- opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
- opentrons/protocol_api/core/module.py +58 -2
- opentrons/protocol_api/core/protocol.py +23 -2
- opentrons/protocol_api/core/tasks.py +31 -0
- opentrons/protocol_api/core/well.py +4 -0
- opentrons/protocol_api/instrument_context.py +388 -2
- opentrons/protocol_api/labware.py +10 -2
- opentrons/protocol_api/module_contexts.py +170 -6
- opentrons/protocol_api/protocol_context.py +87 -21
- opentrons/protocol_api/robot_context.py +41 -25
- opentrons/protocol_api/tasks.py +48 -0
- opentrons/protocol_api/validation.py +49 -3
- opentrons/protocol_engine/__init__.py +4 -0
- opentrons/protocol_engine/actions/__init__.py +6 -2
- opentrons/protocol_engine/actions/actions.py +31 -9
- opentrons/protocol_engine/clients/sync_client.py +42 -7
- opentrons/protocol_engine/commands/__init__.py +56 -0
- opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +2 -15
- opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +2 -15
- opentrons/protocol_engine/commands/absorbance_reader/read.py +22 -23
- opentrons/protocol_engine/commands/aspirate.py +1 -0
- opentrons/protocol_engine/commands/aspirate_while_tracking.py +52 -19
- opentrons/protocol_engine/commands/capture_image.py +302 -0
- opentrons/protocol_engine/commands/command.py +2 -0
- opentrons/protocol_engine/commands/command_unions.py +62 -0
- opentrons/protocol_engine/commands/create_timer.py +83 -0
- opentrons/protocol_engine/commands/dispense.py +1 -0
- opentrons/protocol_engine/commands/dispense_while_tracking.py +56 -19
- opentrons/protocol_engine/commands/drop_tip.py +32 -8
- opentrons/protocol_engine/commands/flex_stacker/common.py +35 -0
- opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +7 -0
- opentrons/protocol_engine/commands/heater_shaker/__init__.py +14 -0
- opentrons/protocol_engine/commands/heater_shaker/common.py +20 -0
- opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +5 -4
- opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +136 -0
- opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +31 -5
- opentrons/protocol_engine/commands/move_labware.py +3 -4
- opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +1 -1
- opentrons/protocol_engine/commands/movement_common.py +31 -2
- opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
- opentrons/protocol_engine/commands/pipetting_common.py +48 -3
- opentrons/protocol_engine/commands/set_tip_state.py +97 -0
- opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +38 -7
- opentrons/protocol_engine/commands/thermocycler/__init__.py +16 -0
- opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +6 -0
- opentrons/protocol_engine/commands/thermocycler/run_profile.py +8 -0
- opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +44 -7
- opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +43 -14
- opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +191 -0
- opentrons/protocol_engine/commands/touch_tip.py +1 -1
- opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +6 -22
- opentrons/protocol_engine/commands/wait_for_tasks.py +98 -0
- opentrons/protocol_engine/create_protocol_engine.py +12 -0
- opentrons/protocol_engine/engine_support.py +3 -0
- opentrons/protocol_engine/errors/__init__.py +12 -0
- opentrons/protocol_engine/errors/exceptions.py +119 -0
- opentrons/protocol_engine/execution/__init__.py +4 -0
- opentrons/protocol_engine/execution/command_executor.py +62 -1
- opentrons/protocol_engine/execution/create_queue_worker.py +9 -2
- opentrons/protocol_engine/execution/labware_movement.py +13 -15
- opentrons/protocol_engine/execution/movement.py +2 -0
- opentrons/protocol_engine/execution/pipetting.py +19 -25
- opentrons/protocol_engine/execution/queue_worker.py +4 -0
- opentrons/protocol_engine/execution/run_control.py +8 -0
- opentrons/protocol_engine/execution/task_handler.py +157 -0
- opentrons/protocol_engine/protocol_engine.py +137 -36
- opentrons/protocol_engine/resources/__init__.py +4 -0
- opentrons/protocol_engine/resources/camera_provider.py +110 -0
- opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
- opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
- opentrons/protocol_engine/resources/file_provider.py +133 -58
- opentrons/protocol_engine/resources/labware_validation.py +10 -6
- opentrons/protocol_engine/slot_standardization.py +2 -0
- opentrons/protocol_engine/state/_well_math.py +60 -18
- opentrons/protocol_engine/state/addressable_areas.py +2 -0
- opentrons/protocol_engine/state/camera.py +54 -0
- opentrons/protocol_engine/state/commands.py +37 -14
- opentrons/protocol_engine/state/geometry.py +276 -379
- opentrons/protocol_engine/state/labware.py +62 -108
- opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
- opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1336 -0
- opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
- opentrons/protocol_engine/state/modules.py +30 -8
- opentrons/protocol_engine/state/motion.py +44 -0
- opentrons/protocol_engine/state/preconditions.py +59 -0
- opentrons/protocol_engine/state/state.py +44 -0
- opentrons/protocol_engine/state/state_summary.py +4 -0
- opentrons/protocol_engine/state/tasks.py +139 -0
- opentrons/protocol_engine/state/tips.py +177 -258
- opentrons/protocol_engine/state/update_types.py +26 -9
- opentrons/protocol_engine/types/__init__.py +23 -4
- opentrons/protocol_engine/types/command_preconditions.py +18 -0
- opentrons/protocol_engine/types/deck_configuration.py +5 -1
- opentrons/protocol_engine/types/instrument.py +8 -1
- opentrons/protocol_engine/types/labware.py +1 -13
- opentrons/protocol_engine/types/location.py +26 -2
- opentrons/protocol_engine/types/module.py +11 -1
- opentrons/protocol_engine/types/tasks.py +38 -0
- opentrons/protocol_engine/types/tip.py +9 -0
- opentrons/protocol_runner/create_simulating_orchestrator.py +29 -2
- opentrons/protocol_runner/protocol_runner.py +14 -1
- opentrons/protocol_runner/run_orchestrator.py +49 -2
- opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +2 -2
- opentrons/protocols/api_support/definitions.py +1 -1
- opentrons/protocols/api_support/types.py +2 -1
- opentrons/simulate.py +51 -15
- opentrons/system/camera.py +334 -4
- opentrons/system/ffmpeg.py +110 -0
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/METADATA +4 -4
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/RECORD +188 -160
- opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/WHEEL +0 -0
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/entry_points.txt +0 -0
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.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
|