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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. opentrons/drivers/absorbance_reader/async_byonoy.py +3 -3
  2. opentrons/hardware_control/ot3api.py +5 -5
  3. opentrons/hardware_control/protocols/position_estimator.py +3 -1
  4. opentrons/legacy_commands/helpers.py +8 -2
  5. opentrons/protocol_api/core/engine/labware.py +10 -2
  6. opentrons/protocol_api/core/engine/module_core.py +38 -1
  7. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +12 -5
  8. opentrons/protocol_api/core/engine/protocol.py +5 -30
  9. opentrons/protocol_api/core/labware.py +4 -0
  10. opentrons/protocol_api/core/legacy/legacy_labware_core.py +5 -0
  11. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +1 -0
  12. opentrons/protocol_api/core/protocol.py +1 -0
  13. opentrons/protocol_api/module_contexts.py +13 -0
  14. opentrons/protocol_api/protocol_context.py +12 -2
  15. opentrons/protocol_engine/actions/__init__.py +0 -2
  16. opentrons/protocol_engine/actions/actions.py +0 -12
  17. opentrons/protocol_engine/clients/sync_client.py +0 -6
  18. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +18 -31
  19. opentrons/protocol_engine/commands/absorbance_reader/initialize.py +19 -7
  20. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +17 -29
  21. opentrons/protocol_engine/commands/load_labware.py +9 -0
  22. opentrons/protocol_engine/commands/load_module.py +0 -39
  23. opentrons/protocol_engine/commands/move_labware.py +49 -4
  24. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +49 -35
  25. opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +5 -1
  26. opentrons/protocol_engine/create_protocol_engine.py +18 -1
  27. opentrons/protocol_engine/errors/__init__.py +2 -0
  28. opentrons/protocol_engine/errors/exceptions.py +13 -0
  29. opentrons/protocol_engine/execution/labware_movement.py +69 -21
  30. opentrons/protocol_engine/execution/movement.py +9 -4
  31. opentrons/protocol_engine/protocol_engine.py +0 -7
  32. opentrons/protocol_engine/resources/deck_data_provider.py +0 -39
  33. opentrons/protocol_engine/resources/file_provider.py +11 -7
  34. opentrons/protocol_engine/resources/fixture_validation.py +6 -1
  35. opentrons/protocol_engine/state/geometry.py +91 -49
  36. opentrons/protocol_engine/state/labware.py +102 -25
  37. opentrons/protocol_engine/state/module_substates/absorbance_reader_substate.py +3 -1
  38. opentrons/protocol_engine/state/modules.py +49 -79
  39. opentrons/protocol_engine/state/motion.py +17 -5
  40. opentrons/protocol_engine/state/update_types.py +16 -0
  41. opentrons/util/logging_config.py +1 -1
  42. {opentrons-8.2.0a0.dist-info → opentrons-8.2.0a2.dist-info}/METADATA +4 -4
  43. {opentrons-8.2.0a0.dist-info → opentrons-8.2.0a2.dist-info}/RECORD +47 -47
  44. {opentrons-8.2.0a0.dist-info → opentrons-8.2.0a2.dist-info}/LICENSE +0 -0
  45. {opentrons-8.2.0a0.dist-info → opentrons-8.2.0a2.dist-info}/WHEEL +0 -0
  46. {opentrons-8.2.0a0.dist-info → opentrons-8.2.0a2.dist-info}/entry_points.txt +0 -0
  47. {opentrons-8.2.0a0.dist-info → opentrons-8.2.0a2.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,9 @@
1
1
  """Labware movement command handling."""
2
2
  from __future__ import annotations
3
3
 
4
- from typing import Optional, TYPE_CHECKING
4
+ from typing import Optional, TYPE_CHECKING, overload
5
+
6
+ from opentrons_shared_data.labware.labware_definition import LabwareDefinition
5
7
 
6
8
  from opentrons.types import Point
7
9
 
@@ -79,24 +81,64 @@ class LabwareMovementHandler:
79
81
  )
80
82
  )
81
83
 
84
+ @overload
82
85
  async def move_labware_with_gripper(
83
86
  self,
87
+ *,
84
88
  labware_id: str,
85
89
  current_location: OnDeckLabwareLocation,
86
90
  new_location: OnDeckLabwareLocation,
87
91
  user_offset_data: LabwareMovementOffsetData,
88
92
  post_drop_slide_offset: Optional[Point],
89
93
  ) -> None:
90
- """Move a loaded labware from one location to another using gripper."""
94
+ ...
95
+
96
+ @overload
97
+ async def move_labware_with_gripper(
98
+ self,
99
+ *,
100
+ labware_definition: LabwareDefinition,
101
+ current_location: OnDeckLabwareLocation,
102
+ new_location: OnDeckLabwareLocation,
103
+ user_offset_data: LabwareMovementOffsetData,
104
+ post_drop_slide_offset: Optional[Point],
105
+ ) -> None:
106
+ ...
107
+
108
+ async def move_labware_with_gripper( # noqa: C901
109
+ self,
110
+ *,
111
+ labware_id: str | None = None,
112
+ labware_definition: LabwareDefinition | None = None,
113
+ current_location: OnDeckLabwareLocation,
114
+ new_location: OnDeckLabwareLocation,
115
+ user_offset_data: LabwareMovementOffsetData,
116
+ post_drop_slide_offset: Optional[Point],
117
+ ) -> None:
118
+ """Physically move a labware from one location to another using the gripper.
119
+
120
+ Generally, provide the `labware_id` of a loaded labware, and this method will
121
+ automatically look up its labware definition. If you're physically moving
122
+ something that has not been loaded as a labware (this is not common),
123
+ provide the `labware_definition` yourself instead.
124
+ """
91
125
  use_virtual_gripper = self._state_store.config.use_virtual_gripper
92
126
 
127
+ if labware_definition is None:
128
+ assert labware_id is not None # From this method's @typing.overloads.
129
+ labware_definition = self._state_store.labware.get_definition(labware_id)
130
+
93
131
  if use_virtual_gripper:
94
- # During Analysis we will pass in hard coded estimates for certain positions only accessible during execution
95
- self._state_store.geometry.check_gripper_labware_tip_collision(
96
- gripper_homed_position_z=_GRIPPER_HOMED_POSITION_Z,
97
- labware_id=labware_id,
98
- current_location=current_location,
99
- )
132
+ # todo(mm, 2024-11-07): We should do this collision checking even when we
133
+ # only have a `labware_definition`, not a `labware_id`. Resolve when
134
+ # `check_gripper_labware_tip_collision()` can be made independent of `labware_id`.
135
+ if labware_id is not None:
136
+ self._state_store.geometry.check_gripper_labware_tip_collision(
137
+ # During Analysis we will pass in hard coded estimates for certain positions only accessible during execution
138
+ gripper_homed_position_z=_GRIPPER_HOMED_POSITION_Z,
139
+ labware_id=labware_id,
140
+ current_location=current_location,
141
+ )
100
142
  return
101
143
 
102
144
  ot3api = ensure_ot3_hardware(
@@ -119,14 +161,16 @@ class LabwareMovementHandler:
119
161
  await ot3api.home(axes=[Axis.Z_L, Axis.Z_R, Axis.Z_G])
120
162
  gripper_homed_position = await ot3api.gantry_position(mount=gripper_mount)
121
163
 
122
- # Verify that no tip collisions will occur during the move
123
- self._state_store.geometry.check_gripper_labware_tip_collision(
124
- gripper_homed_position_z=gripper_homed_position.z,
125
- labware_id=labware_id,
126
- current_location=current_location,
127
- )
164
+ # todo(mm, 2024-11-07): We should do this collision checking even when we
165
+ # only have a `labware_definition`, not a `labware_id`. Resolve when
166
+ # `check_gripper_labware_tip_collision()` can be made independent of `labware_id`.
167
+ if labware_id is not None:
168
+ self._state_store.geometry.check_gripper_labware_tip_collision(
169
+ gripper_homed_position_z=gripper_homed_position.z,
170
+ labware_id=labware_id,
171
+ current_location=current_location,
172
+ )
128
173
 
129
- current_labware = self._state_store.labware.get_definition(labware_id)
130
174
  async with self._thermocycler_plate_lifter.lift_plate_for_labware_movement(
131
175
  labware_location=current_location
132
176
  ):
@@ -135,14 +179,14 @@ class LabwareMovementHandler:
135
179
  from_location=current_location,
136
180
  to_location=new_location,
137
181
  additional_offset_vector=user_offset_data,
138
- current_labware=current_labware,
182
+ current_labware=labware_definition,
139
183
  )
140
184
  )
141
185
  from_labware_center = self._state_store.geometry.get_labware_grip_point(
142
- labware_id=labware_id, location=current_location
186
+ labware_definition=labware_definition, location=current_location
143
187
  )
144
188
  to_labware_center = self._state_store.geometry.get_labware_grip_point(
145
- labware_id=labware_id, location=new_location
189
+ labware_definition=labware_definition, location=new_location
146
190
  )
147
191
  movement_waypoints = get_gripper_labware_movement_waypoints(
148
192
  from_labware_center=from_labware_center,
@@ -151,7 +195,9 @@ class LabwareMovementHandler:
151
195
  offset_data=final_offsets,
152
196
  post_drop_slide_offset=post_drop_slide_offset,
153
197
  )
154
- labware_grip_force = self._state_store.labware.get_grip_force(labware_id)
198
+ labware_grip_force = self._state_store.labware.get_grip_force(
199
+ labware_definition
200
+ )
155
201
  holding_labware = False
156
202
  for waypoint_data in movement_waypoints:
157
203
  if waypoint_data.jaw_open:
@@ -174,9 +220,11 @@ class LabwareMovementHandler:
174
220
  # should be holding labware
175
221
  if holding_labware:
176
222
  labware_bbox = self._state_store.labware.get_dimensions(
177
- labware_id
223
+ labware_definition=labware_definition
224
+ )
225
+ well_bbox = self._state_store.labware.get_well_bbox(
226
+ labware_definition=labware_definition
178
227
  )
179
- well_bbox = self._state_store.labware.get_well_bbox(labware_id)
180
228
  # todo(mm, 2024-09-26): This currently raises a lower-level 2015 FailedGripperPickupError.
181
229
  # Convert this to a higher-level 3001 LabwareDroppedError or 3002 LabwareNotPickedUpError,
182
230
  # depending on what waypoint we're at, to propagate a more specific error code to users.
@@ -4,9 +4,10 @@ from __future__ import annotations
4
4
  import logging
5
5
  from typing import Optional, List, Union
6
6
 
7
- from opentrons.types import Point, MountType
7
+ from opentrons.types import Point, MountType, StagingSlotName
8
8
  from opentrons.hardware_control import HardwareControlAPI
9
9
  from opentrons_shared_data.errors.exceptions import PositionUnknownError
10
+ from opentrons.protocol_engine.errors import LocationIsStagingSlotError
10
11
 
11
12
  from ..types import (
12
13
  WellLocation,
@@ -93,9 +94,13 @@ class MovementHandler:
93
94
  self._state_store.modules.get_heater_shaker_movement_restrictors()
94
95
  )
95
96
 
96
- dest_slot_int = self._state_store.geometry.get_ancestor_slot_name(
97
- labware_id
98
- ).as_int()
97
+ ancestor = self._state_store.geometry.get_ancestor_slot_name(labware_id)
98
+ if isinstance(ancestor, StagingSlotName):
99
+ raise LocationIsStagingSlotError(
100
+ "Cannot move to well on labware in Staging Area Slot."
101
+ )
102
+
103
+ dest_slot_int = ancestor.as_int()
99
104
 
100
105
  self._hs_movement_flagger.raise_if_movement_restricted(
101
106
  hs_movement_restrictors=hs_movement_restrictors,
@@ -59,7 +59,6 @@ from .actions import (
59
59
  HardwareStoppedAction,
60
60
  ResetTipsAction,
61
61
  SetPipetteMovementSpeedAction,
62
- AddAbsorbanceReaderLidAction,
63
62
  )
64
63
 
65
64
 
@@ -577,12 +576,6 @@ class ProtocolEngine:
577
576
  AddAddressableAreaAction(addressable_area=area)
578
577
  )
579
578
 
580
- def add_absorbance_reader_lid(self, module_id: str, lid_id: str) -> None:
581
- """Add an absorbance reader lid to the module state."""
582
- self._action_dispatcher.dispatch(
583
- AddAbsorbanceReaderLidAction(module_id=module_id, lid_id=lid_id)
584
- )
585
-
586
579
  def reset_tips(self, labware_id: str) -> None:
587
580
  """Reset the tip state of a given labware."""
588
581
  # TODO(mm, 2023-03-10): Safely raise an error if the given labware isn't a
@@ -17,11 +17,9 @@ from ..types import (
17
17
  DeckSlotLocation,
18
18
  DeckType,
19
19
  LabwareLocation,
20
- AddressableAreaLocation,
21
20
  DeckConfigurationType,
22
21
  )
23
22
  from .labware_data_provider import LabwareDataProvider
24
- from ..resources import deck_configuration_provider
25
23
 
26
24
 
27
25
  @final
@@ -71,43 +69,6 @@ class DeckDataProvider:
71
69
  slot = cast(Optional[str], fixture.get("slot"))
72
70
 
73
71
  if (
74
- deck_configuration is not None
75
- and load_name is not None
76
- and slot is not None
77
- and slot not in DeckSlotName._value2member_map_
78
- ):
79
- # The provided slot is likely to be an addressable area for Module-required labware Eg: Plate Reader Lid
80
- for (
81
- cutout_id,
82
- cutout_fixture_id,
83
- opentrons_module_serial_number,
84
- ) in deck_configuration:
85
- provided_addressable_areas = (
86
- deck_configuration_provider.get_provided_addressable_area_names(
87
- cutout_fixture_id=cutout_fixture_id,
88
- cutout_id=cutout_id,
89
- deck_definition=deck_definition,
90
- )
91
- )
92
- if slot in provided_addressable_areas:
93
- addressable_area_location = AddressableAreaLocation(
94
- addressableAreaName=slot
95
- )
96
- definition = await self._labware_data.get_labware_definition(
97
- load_name=load_name,
98
- namespace="opentrons",
99
- version=1,
100
- )
101
-
102
- labware.append(
103
- DeckFixedLabware(
104
- labware_id=labware_id,
105
- definition=definition,
106
- location=addressable_area_location,
107
- )
108
- )
109
-
110
- elif (
111
72
  load_fixed_trash
112
73
  and load_name is not None
113
74
  and slot is not None
@@ -66,7 +66,7 @@ class PlateReaderData(BaseModel):
66
66
  row.append(str(measurement.data[f"{plate_alpharows[i]}{j+1}"]))
67
67
  rows.append(row)
68
68
  for i in range(3):
69
- rows.append([""])
69
+ rows.append([])
70
70
  rows.append(["", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"])
71
71
  for i in range(8):
72
72
  row = [plate_alpharows[i]]
@@ -74,7 +74,7 @@ class PlateReaderData(BaseModel):
74
74
  row.append("")
75
75
  rows.append(row)
76
76
  for i in range(3):
77
- rows.append([""])
77
+ rows.append([])
78
78
  rows.append(
79
79
  [
80
80
  "",
@@ -86,7 +86,7 @@ class PlateReaderData(BaseModel):
86
86
  ]
87
87
  )
88
88
  for i in range(3):
89
- rows.append([""])
89
+ rows.append([])
90
90
  rows.append(
91
91
  [
92
92
  "",
@@ -100,7 +100,7 @@ class PlateReaderData(BaseModel):
100
100
  )
101
101
  rows.append(["1", "Sample 1", "", "", "", "1", "", "", "", "", "", ""])
102
102
  for i in range(3):
103
- rows.append([""])
103
+ rows.append([])
104
104
 
105
105
  # end of file metadata
106
106
  rows.append(["Protocol"])
@@ -109,13 +109,17 @@ class PlateReaderData(BaseModel):
109
109
  if self.reference_wavelength is not None:
110
110
  rows.append(["Reference Wavelength (nm)", str(self.reference_wavelength)])
111
111
  rows.append(["Serial No.", self.serial_number])
112
- rows.append(["Measurement started at", str(self.start_time)])
113
- rows.append(["Measurement finished at", str(self.finish_time)])
112
+ rows.append(
113
+ ["Measurement started at", self.start_time.strftime("%m %d %H:%M:%S %Y")]
114
+ )
115
+ rows.append(
116
+ ["Measurement finished at", self.finish_time.strftime("%m %d %H:%M:%S %Y")]
117
+ )
114
118
 
115
119
  # Ensure the filename adheres to ruleset contains the wavelength for a given measurement
116
120
  if filename.endswith(".csv"):
117
121
  filename = filename[:-4]
118
- filename = filename + "_" + str(measurement.wavelength) + ".csv"
122
+ filename = filename + str(measurement.wavelength) + "nm.csv"
119
123
 
120
124
  return GenericCsvTransform.build(
121
125
  filename=filename,
@@ -29,7 +29,12 @@ def is_drop_tip_waste_chute(addressable_area_name: str) -> bool:
29
29
 
30
30
  def is_trash(addressable_area_name: str) -> bool:
31
31
  """Check if an addressable area is a trash bin."""
32
- return addressable_area_name in {"movableTrash", "fixedTrash", "shortFixedTrash"}
32
+ return any(
33
+ [
34
+ s in addressable_area_name
35
+ for s in {"movableTrash", "fixedTrash", "shortFixedTrash"}
36
+ ]
37
+ )
33
38
 
34
39
 
35
40
  def is_staging_slot(addressable_area_name: str) -> bool:
@@ -262,32 +262,33 @@ class GeometryView:
262
262
  return min_travel_z
263
263
 
264
264
  def get_labware_parent_nominal_position(self, labware_id: str) -> Point:
265
- """Get the position of the labware's uncalibrated parent slot (deck, module, or another labware)."""
265
+ """Get the position of the labware's uncalibrated parent (deck slot, module, or another labware)."""
266
266
  try:
267
267
  addressable_area_name = self.get_ancestor_slot_name(labware_id).id
268
268
  except errors.LocationIsStagingSlotError:
269
269
  addressable_area_name = self._get_staging_slot_name(labware_id)
270
270
  except errors.LocationIsLidDockSlotError:
271
271
  addressable_area_name = self._get_lid_dock_slot_name(labware_id)
272
- slot_pos = self._addressable_areas.get_addressable_area_position(
272
+ parent_pos = self._addressable_areas.get_addressable_area_position(
273
273
  addressable_area_name
274
274
  )
275
- labware_data = self._labware.get(labware_id)
276
275
 
277
- offset = self._get_labware_position_offset(labware_id, labware_data.location)
276
+ offset_from_parent = self._get_offset_from_parent(
277
+ child_definition=self._labware.get_definition(labware_id),
278
+ parent=self._labware.get(labware_id).location,
279
+ )
278
280
 
279
281
  return Point(
280
- slot_pos.x + offset.x,
281
- slot_pos.y + offset.y,
282
- slot_pos.z + offset.z,
282
+ parent_pos.x + offset_from_parent.x,
283
+ parent_pos.y + offset_from_parent.y,
284
+ parent_pos.z + offset_from_parent.z,
283
285
  )
284
286
 
285
- def _get_labware_position_offset(
286
- self, labware_id: str, labware_location: LabwareLocation
287
+ def _get_offset_from_parent(
288
+ self, child_definition: LabwareDefinition, parent: LabwareLocation
287
289
  ) -> LabwareOffsetVector:
288
- """Gets the offset vector of a labware on the given location.
290
+ """Gets the offset vector of a labware placed on the given location.
289
291
 
290
- NOTE: Not to be confused with LPC offset.
291
292
  - For labware on Deck Slot: returns an offset of (0, 0, 0)
292
293
  - For labware on a Module: returns the nominal offset for the labware's position
293
294
  when placed on the specified module (using slot-transformed labwareOffset
@@ -298,40 +299,42 @@ class GeometryView:
298
299
  on modules as well as stacking overlaps.
299
300
  Does not include module calibration offset or LPC offset.
300
301
  """
301
- if isinstance(labware_location, (AddressableAreaLocation, DeckSlotLocation)):
302
+ if isinstance(parent, (AddressableAreaLocation, DeckSlotLocation)):
302
303
  return LabwareOffsetVector(x=0, y=0, z=0)
303
- elif isinstance(labware_location, ModuleLocation):
304
- module_id = labware_location.moduleId
305
- module_offset = self._modules.get_nominal_module_offset(
304
+ elif isinstance(parent, ModuleLocation):
305
+ module_id = parent.moduleId
306
+ module_to_child = self._modules.get_nominal_offset_to_child(
306
307
  module_id=module_id, addressable_areas=self._addressable_areas
307
308
  )
308
309
  module_model = self._modules.get_connected_model(module_id)
309
310
  stacking_overlap = self._labware.get_module_overlap_offsets(
310
- labware_id, module_model
311
+ child_definition, module_model
311
312
  )
312
313
  return LabwareOffsetVector(
313
- x=module_offset.x - stacking_overlap.x,
314
- y=module_offset.y - stacking_overlap.y,
315
- z=module_offset.z - stacking_overlap.z,
314
+ x=module_to_child.x - stacking_overlap.x,
315
+ y=module_to_child.y - stacking_overlap.y,
316
+ z=module_to_child.z - stacking_overlap.z,
317
+ )
318
+ elif isinstance(parent, OnLabwareLocation):
319
+ on_labware = self._labware.get(parent.labwareId)
320
+ on_labware_dimensions = self._labware.get_dimensions(
321
+ labware_id=on_labware.id
316
322
  )
317
- elif isinstance(labware_location, OnLabwareLocation):
318
- on_labware = self._labware.get(labware_location.labwareId)
319
- on_labware_dimensions = self._labware.get_dimensions(on_labware.id)
320
323
  stacking_overlap = self._labware.get_labware_overlap_offsets(
321
- labware_id=labware_id, below_labware_name=on_labware.loadName
324
+ definition=child_definition, below_labware_name=on_labware.loadName
322
325
  )
323
326
  labware_offset = LabwareOffsetVector(
324
327
  x=stacking_overlap.x,
325
328
  y=stacking_overlap.y,
326
329
  z=on_labware_dimensions.z - stacking_overlap.z,
327
330
  )
328
- return labware_offset + self._get_labware_position_offset(
329
- on_labware.id, on_labware.location
331
+ return labware_offset + self._get_offset_from_parent(
332
+ self._labware.get_definition(on_labware.id), on_labware.location
330
333
  )
331
334
  else:
332
335
  raise errors.LabwareNotOnDeckError(
333
- f"Cannot access labware {labware_id} since it is not on the deck. "
334
- f"Either it has been loaded off-deck or its been moved off-deck."
336
+ "Cannot access labware since it is not on the deck. "
337
+ "Either it has been loaded off-deck or its been moved off-deck."
335
338
  )
336
339
 
337
340
  def _normalize_module_calibration_offset(
@@ -709,10 +712,12 @@ class GeometryView:
709
712
  assert isinstance(labware_location, AddressableAreaLocation)
710
713
  return labware_location.addressableAreaName
711
714
 
712
- def get_ancestor_slot_name(self, labware_id: str) -> DeckSlotName:
715
+ def get_ancestor_slot_name(
716
+ self, labware_id: str
717
+ ) -> Union[DeckSlotName, StagingSlotName]:
713
718
  """Get the slot name of the labware or the module that the labware is on."""
714
719
  labware = self._labware.get(labware_id)
715
- slot_name: DeckSlotName
720
+ slot_name: Union[DeckSlotName, StagingSlotName]
716
721
 
717
722
  if isinstance(labware.location, DeckSlotLocation):
718
723
  slot_name = labware.location.slotName
@@ -724,18 +729,14 @@ class GeometryView:
724
729
  slot_name = self.get_ancestor_slot_name(below_labware_id)
725
730
  elif isinstance(labware.location, AddressableAreaLocation):
726
731
  area_name = labware.location.addressableAreaName
727
- # TODO we might want to eventually return some sort of staging slot name when we're ready to work through
728
- # the linting nightmare it will create
729
732
  if self._labware.is_absorbance_reader_lid(labware_id):
730
733
  raise errors.LocationIsLidDockSlotError(
731
734
  "Cannot get ancestor slot name for labware on lid dock slot."
732
735
  )
733
- if fixture_validation.is_staging_slot(area_name):
734
- raise errors.LocationIsStagingSlotError(
735
- "Cannot get ancestor slot name for labware on staging slot."
736
- )
737
- raise errors.LocationIs
738
- slot_name = DeckSlotName.from_primitive(area_name)
736
+ elif fixture_validation.is_staging_slot(area_name):
737
+ slot_name = StagingSlotName.from_primitive(area_name)
738
+ else:
739
+ slot_name = DeckSlotName.from_primitive(area_name)
739
740
  elif labware.location == OFF_DECK_LOCATION:
740
741
  raise errors.LabwareNotOnDeckError(
741
742
  f"Labware {labware_id} does not have a slot associated with it"
@@ -768,7 +769,7 @@ class GeometryView:
768
769
 
769
770
  def get_labware_grip_point(
770
771
  self,
771
- labware_id: str,
772
+ labware_definition: LabwareDefinition,
772
773
  location: Union[
773
774
  DeckSlotLocation, ModuleLocation, OnLabwareLocation, AddressableAreaLocation
774
775
  ],
@@ -784,7 +785,7 @@ class GeometryView:
784
785
  z-position of labware bottom + grip height from labware bottom.
785
786
  """
786
787
  grip_height_from_labware_bottom = (
787
- self._labware.get_grip_height_from_labware_bottom(labware_id)
788
+ self._labware.get_grip_height_from_labware_bottom(labware_definition)
788
789
  )
789
790
  location_name: str
790
791
 
@@ -810,7 +811,9 @@ class GeometryView:
810
811
  ).slotName.id
811
812
  else: # OnLabwareLocation
812
813
  location_name = self.get_ancestor_slot_name(location.labwareId).id
813
- labware_offset = self._get_labware_position_offset(labware_id, location)
814
+ labware_offset = self._get_offset_from_parent(
815
+ child_definition=labware_definition, parent=location
816
+ )
814
817
  # Get the calibrated offset if the on labware location is on top of a module, otherwise return empty one
815
818
  cal_offset = self._get_calibrated_module_offset(location)
816
819
  offset = LabwareOffsetVector(
@@ -829,7 +832,9 @@ class GeometryView:
829
832
  )
830
833
 
831
834
  def get_extra_waypoints(
832
- self, location: Optional[CurrentPipetteLocation], to_slot: DeckSlotName
835
+ self,
836
+ location: Optional[CurrentPipetteLocation],
837
+ to_slot: Union[DeckSlotName, StagingSlotName],
833
838
  ) -> List[Tuple[float, float]]:
834
839
  """Get extra waypoints for movement if thermocycler needs to be dodged."""
835
840
  if location is not None:
@@ -888,8 +893,10 @@ class GeometryView:
888
893
  return maybe_labware or maybe_module or maybe_fixture or None
889
894
 
890
895
  @staticmethod
891
- def get_slot_column(slot_name: DeckSlotName) -> int:
896
+ def get_slot_column(slot_name: Union[DeckSlotName, StagingSlotName]) -> int:
892
897
  """Get the column number for the specified slot."""
898
+ if isinstance(slot_name, StagingSlotName):
899
+ return 4
893
900
  row_col_name = slot_name.to_ot3_equivalent()
894
901
  slot_name_match = WELL_NAME_PATTERN.match(row_col_name.value)
895
902
  assert (
@@ -1170,7 +1177,13 @@ class GeometryView:
1170
1177
  )
1171
1178
 
1172
1179
  assert isinstance(
1173
- ancestor, (DeckSlotLocation, ModuleLocation, OnLabwareLocation)
1180
+ ancestor,
1181
+ (
1182
+ DeckSlotLocation,
1183
+ ModuleLocation,
1184
+ OnLabwareLocation,
1185
+ AddressableAreaLocation,
1186
+ ),
1174
1187
  ), "No gripper offsets for off-deck labware"
1175
1188
  return (
1176
1189
  direct_parent_offset.pickUpOffset
@@ -1195,6 +1208,7 @@ class GeometryView:
1195
1208
  extra_offset = LabwareOffsetVector(x=0, y=0, z=0)
1196
1209
  if (
1197
1210
  isinstance(ancestor, ModuleLocation)
1211
+ # todo(mm, 2024-11-06): Do not access private module state; only use public ModuleView methods.
1198
1212
  and self._modules._state.requested_model_by_id[ancestor.moduleId]
1199
1213
  == ModuleModel.THERMOCYCLER_MODULE_V2
1200
1214
  and labware_validation.validate_definition_is_lid(current_labware)
@@ -1217,7 +1231,13 @@ class GeometryView:
1217
1231
  )
1218
1232
 
1219
1233
  assert isinstance(
1220
- ancestor, (DeckSlotLocation, ModuleLocation, OnLabwareLocation)
1234
+ ancestor,
1235
+ (
1236
+ DeckSlotLocation,
1237
+ ModuleLocation,
1238
+ OnLabwareLocation,
1239
+ AddressableAreaLocation,
1240
+ ),
1221
1241
  ), "No gripper offsets for off-deck labware"
1222
1242
  return (
1223
1243
  direct_parent_offset.dropOffset
@@ -1227,6 +1247,23 @@ class GeometryView:
1227
1247
  + extra_offset
1228
1248
  )
1229
1249
 
1250
+ # todo(mm, 2024-11-05): This may be incorrect because it does not take the following
1251
+ # offsets into account, which *are* taken into account for the actual gripper movement:
1252
+ #
1253
+ # * The pickup offset in the definition of the parent of the gripped labware.
1254
+ # * The "additional offset" or "user offset", e.g. the `pickUpOffset` and `dropOffset`
1255
+ # params in the `moveLabware` command.
1256
+ #
1257
+ # And this *does* take these extra offsets into account:
1258
+ #
1259
+ # * The labware's Labware Position Check offset
1260
+ #
1261
+ # For robustness, we should combine this with `get_gripper_labware_movement_waypoints()`.
1262
+ #
1263
+ # We should also be more explicit about which offsets act to move the gripper paddles
1264
+ # relative to the gripped labware, and which offsets act to change how the gripped
1265
+ # labware sits atop its parent. Those have different effects on how far the gripped
1266
+ # labware juts beyond the paddles while it's in transit.
1230
1267
  def check_gripper_labware_tip_collision(
1231
1268
  self,
1232
1269
  gripper_homed_position_z: float,
@@ -1234,18 +1271,22 @@ class GeometryView:
1234
1271
  current_location: OnDeckLabwareLocation,
1235
1272
  ) -> None:
1236
1273
  """Check for potential collision of tips against labware to be lifted."""
1237
- # TODO(cb, 2024-01-22): Remove the 1 and 8 channel special case once we are doing X axis validation
1274
+ labware_definition = self._labware.get_definition(labware_id)
1238
1275
  pipettes = self._pipettes.get_all()
1239
1276
  for pipette in pipettes:
1277
+ # TODO(cb, 2024-01-22): Remove the 1 and 8 channel special case once we are doing X axis validation
1240
1278
  if self._pipettes.get_channels(pipette.id) in [1, 8]:
1241
1279
  return
1242
1280
 
1243
1281
  tip = self._pipettes.get_attached_tip(pipette.id)
1244
1282
  if tip:
1283
+ # NOTE: This call to get_labware_highest_z() uses the labware's LPC offset,
1284
+ # which is an inconsistency between this and the actual gripper movement.
1285
+ # See the todo comment above this function.
1245
1286
  labware_top_z_when_gripped = gripper_homed_position_z + (
1246
1287
  self.get_labware_highest_z(labware_id=labware_id)
1247
1288
  - self.get_labware_grip_point(
1248
- labware_id=labware_id, location=current_location
1289
+ labware_definition=labware_definition, location=current_location
1249
1290
  ).z
1250
1291
  )
1251
1292
  # TODO(cb, 2024-01-18): Utilizing the nozzle map and labware X coordinates verify if collisions will occur on the X axis (analysis will use hard coded data to measure from the gripper critical point to the pipette mount)
@@ -1253,7 +1294,7 @@ class GeometryView:
1253
1294
  _PIPETTE_HOMED_POSITION_Z - tip.length
1254
1295
  ) < labware_top_z_when_gripped:
1255
1296
  raise LabwareMovementNotAllowedError(
1256
- f"Cannot move labware '{self._labware.get(labware_id).loadName}' when {int(tip.volume)} µL tips are attached."
1297
+ f"Cannot move labware '{labware_definition.parameters.loadName}' when {int(tip.volume)} µL tips are attached."
1257
1298
  )
1258
1299
  return
1259
1300
 
@@ -1293,6 +1334,7 @@ class GeometryView:
1293
1334
  DeckSlotLocation,
1294
1335
  ModuleLocation,
1295
1336
  AddressableAreaLocation,
1337
+ OnLabwareLocation,
1296
1338
  ),
1297
1339
  ), "No gripper offsets for off-deck labware"
1298
1340
 
@@ -1306,11 +1348,11 @@ class GeometryView:
1306
1348
  module_loc = self._modules.get_location(parent_location.moduleId)
1307
1349
  slot_name = module_loc.slotName
1308
1350
 
1309
- slot_based_offset = self._labware.get_labware_gripper_offsets(
1351
+ slot_based_offset = self._labware.get_child_gripper_offsets(
1310
1352
  labware_id=labware_id, slot_name=slot_name.to_ot3_equivalent()
1311
1353
  )
1312
1354
 
1313
- return slot_based_offset or self._labware.get_labware_gripper_offsets(
1355
+ return slot_based_offset or self._labware.get_child_gripper_offsets(
1314
1356
  labware_id=labware_id, slot_name=None
1315
1357
  )
1316
1358