opentrons 8.2.0a0__py2.py3-none-any.whl → 8.2.0a1__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 (41) hide show
  1. opentrons/drivers/absorbance_reader/async_byonoy.py +3 -3
  2. opentrons/legacy_commands/helpers.py +8 -2
  3. opentrons/protocol_api/core/engine/labware.py +10 -2
  4. opentrons/protocol_api/core/engine/module_core.py +38 -1
  5. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +12 -5
  6. opentrons/protocol_api/core/engine/protocol.py +5 -30
  7. opentrons/protocol_api/core/labware.py +4 -0
  8. opentrons/protocol_api/core/legacy/legacy_labware_core.py +5 -0
  9. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +1 -0
  10. opentrons/protocol_api/core/protocol.py +1 -0
  11. opentrons/protocol_api/module_contexts.py +13 -0
  12. opentrons/protocol_api/protocol_context.py +12 -2
  13. opentrons/protocol_engine/actions/__init__.py +0 -2
  14. opentrons/protocol_engine/actions/actions.py +0 -12
  15. opentrons/protocol_engine/clients/sync_client.py +0 -6
  16. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +18 -31
  17. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +17 -29
  18. opentrons/protocol_engine/commands/load_labware.py +9 -0
  19. opentrons/protocol_engine/commands/load_module.py +0 -39
  20. opentrons/protocol_engine/commands/move_labware.py +49 -4
  21. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +41 -32
  22. opentrons/protocol_engine/create_protocol_engine.py +18 -1
  23. opentrons/protocol_engine/execution/labware_movement.py +69 -21
  24. opentrons/protocol_engine/execution/movement.py +9 -4
  25. opentrons/protocol_engine/protocol_engine.py +0 -7
  26. opentrons/protocol_engine/resources/deck_data_provider.py +0 -39
  27. opentrons/protocol_engine/resources/file_provider.py +11 -7
  28. opentrons/protocol_engine/resources/fixture_validation.py +6 -1
  29. opentrons/protocol_engine/state/geometry.py +91 -49
  30. opentrons/protocol_engine/state/labware.py +102 -25
  31. opentrons/protocol_engine/state/module_substates/absorbance_reader_substate.py +3 -1
  32. opentrons/protocol_engine/state/modules.py +49 -79
  33. opentrons/protocol_engine/state/motion.py +17 -5
  34. opentrons/protocol_engine/state/update_types.py +16 -0
  35. opentrons/util/logging_config.py +1 -1
  36. {opentrons-8.2.0a0.dist-info → opentrons-8.2.0a1.dist-info}/METADATA +4 -4
  37. {opentrons-8.2.0a0.dist-info → opentrons-8.2.0a1.dist-info}/RECORD +41 -41
  38. {opentrons-8.2.0a0.dist-info → opentrons-8.2.0a1.dist-info}/LICENSE +0 -0
  39. {opentrons-8.2.0a0.dist-info → opentrons-8.2.0a1.dist-info}/WHEEL +0 -0
  40. {opentrons-8.2.0a0.dist-info → opentrons-8.2.0a1.dist-info}/entry_points.txt +0 -0
  41. {opentrons-8.2.0a0.dist-info → opentrons-8.2.0a1.dist-info}/top_level.txt +0 -0
@@ -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
 
@@ -13,6 +13,7 @@ from typing import (
13
13
  NamedTuple,
14
14
  cast,
15
15
  Union,
16
+ overload,
16
17
  )
17
18
 
18
19
  from opentrons.protocol_engine.state import update_types
@@ -81,6 +82,10 @@ _RIGHT_SIDE_SLOTS = {
81
82
  }
82
83
 
83
84
 
85
+ # The max height of the labware that can fit in a plate reader
86
+ _PLATE_READER_MAX_LABWARE_Z_MM = 16
87
+
88
+
84
89
  class LabwareLoadParams(NamedTuple):
85
90
  """Parameters required to load a labware in Protocol Engine."""
86
91
 
@@ -227,10 +232,11 @@ class LabwareStore(HasState[LabwareState], HandlesActions):
227
232
  if labware_location_update.new_location:
228
233
  new_location = labware_location_update.new_location
229
234
 
230
- if isinstance(
231
- new_location, AddressableAreaLocation
232
- ) and fixture_validation.is_gripper_waste_chute(
233
- new_location.addressableAreaName
235
+ if isinstance(new_location, AddressableAreaLocation) and (
236
+ fixture_validation.is_gripper_waste_chute(
237
+ new_location.addressableAreaName
238
+ )
239
+ or fixture_validation.is_trash(new_location.addressableAreaName)
234
240
  ):
235
241
  # If a labware has been moved into a waste chute it's been chuted away and is now technically off deck
236
242
  new_location = OFF_DECK_LOCATION
@@ -625,10 +631,26 @@ class LabwareView(HasState[LabwareState]):
625
631
  definition = self.get_definition(labware_id)
626
632
  return definition.parameters.loadName
627
633
 
628
- def get_dimensions(self, labware_id: str) -> Dimensions:
634
+ @overload
635
+ def get_dimensions(self, *, labware_definition: LabwareDefinition) -> Dimensions:
636
+ pass
637
+
638
+ @overload
639
+ def get_dimensions(self, *, labware_id: str) -> Dimensions:
640
+ pass
641
+
642
+ def get_dimensions(
643
+ self,
644
+ *,
645
+ labware_definition: LabwareDefinition | None = None,
646
+ labware_id: str | None = None,
647
+ ) -> Dimensions:
629
648
  """Get the labware's dimensions."""
630
- definition = self.get_definition(labware_id)
631
- dims = definition.dimensions
649
+ if labware_definition is None:
650
+ assert labware_id is not None # From our @overloads.
651
+ labware_definition = self.get_definition(labware_id)
652
+
653
+ dims = labware_definition.dimensions
632
654
 
633
655
  return Dimensions(
634
656
  x=dims.xDimension,
@@ -637,10 +659,9 @@ class LabwareView(HasState[LabwareState]):
637
659
  )
638
660
 
639
661
  def get_labware_overlap_offsets(
640
- self, labware_id: str, below_labware_name: str
662
+ self, definition: LabwareDefinition, below_labware_name: str
641
663
  ) -> OverlapOffset:
642
664
  """Get the labware's overlap with requested labware's load name."""
643
- definition = self.get_definition(labware_id)
644
665
  if below_labware_name in definition.stackingOffsetWithLabware.keys():
645
666
  stacking_overlap = definition.stackingOffsetWithLabware.get(
646
667
  below_labware_name, OverlapOffset(x=0, y=0, z=0)
@@ -654,10 +675,9 @@ class LabwareView(HasState[LabwareState]):
654
675
  )
655
676
 
656
677
  def get_module_overlap_offsets(
657
- self, labware_id: str, module_model: ModuleModel
678
+ self, definition: LabwareDefinition, module_model: ModuleModel
658
679
  ) -> OverlapOffset:
659
680
  """Get the labware's overlap with requested module model."""
660
- definition = self.get_definition(labware_id)
661
681
  stacking_overlap = definition.stackingOffsetWithModule.get(
662
682
  str(module_model.value)
663
683
  )
@@ -817,6 +837,24 @@ class LabwareView(HasState[LabwareState]):
817
837
  f"Labware {labware.loadName} is already present at {location}."
818
838
  )
819
839
 
840
+ def raise_if_labware_incompatible_with_plate_reader(
841
+ self,
842
+ labware_definition: LabwareDefinition,
843
+ ) -> None:
844
+ """Raise an error if the labware is not compatible with the plate reader."""
845
+ load_name = labware_definition.parameters.loadName
846
+ number_of_wells = len(labware_definition.wells)
847
+ if number_of_wells != 96:
848
+ raise errors.LabwareMovementNotAllowedError(
849
+ f"Cannot move '{load_name}' into plate reader because the"
850
+ f" labware contains {number_of_wells} wells where 96 wells is expected."
851
+ )
852
+ elif labware_definition.dimensions.zDimension > _PLATE_READER_MAX_LABWARE_Z_MM:
853
+ raise errors.LabwareMovementNotAllowedError(
854
+ f"Cannot move '{load_name}' into plate reader because the"
855
+ f" maximum allowed labware height is {_PLATE_READER_MAX_LABWARE_Z_MM}mm."
856
+ )
857
+
820
858
  def raise_if_labware_cannot_be_stacked( # noqa: C901
821
859
  self, top_labware_definition: LabwareDefinition, bottom_labware_id: str
822
860
  ) -> None:
@@ -900,22 +938,60 @@ class LabwareView(HasState[LabwareState]):
900
938
  else None
901
939
  )
902
940
 
903
- def get_labware_gripper_offsets(
941
+ def get_absorbance_reader_lid_definition(self) -> LabwareDefinition:
942
+ """Return the special labware definition for the plate reader lid.
943
+
944
+ See todo comments in `create_protocol_engine().
945
+ """
946
+ # NOTE: This needs to stay in sync with create_protocol_engine().
947
+ return self._state.definitions_by_uri[
948
+ "opentrons/opentrons_flex_lid_absorbance_plate_reader_module/1"
949
+ ]
950
+
951
+ @overload
952
+ def get_child_gripper_offsets(
904
953
  self,
905
- labware_id: str,
954
+ *,
955
+ labware_definition: LabwareDefinition,
906
956
  slot_name: Optional[DeckSlotName],
907
957
  ) -> Optional[LabwareMovementOffsetData]:
908
- """Get the labware's gripper offsets of the specified type.
958
+ pass
959
+
960
+ @overload
961
+ def get_child_gripper_offsets(
962
+ self, *, labware_id: str, slot_name: Optional[DeckSlotName]
963
+ ) -> Optional[LabwareMovementOffsetData]:
964
+ pass
965
+
966
+ def get_child_gripper_offsets(
967
+ self,
968
+ *,
969
+ labware_definition: Optional[LabwareDefinition] = None,
970
+ labware_id: Optional[str] = None,
971
+ slot_name: Optional[DeckSlotName],
972
+ ) -> Optional[LabwareMovementOffsetData]:
973
+ """Get the grip offsets that a labware says should be applied to children stacked atop it.
974
+
975
+ Params:
976
+ labware_id: The ID of a parent labware (atop which another labware, the child, will be stacked).
977
+ slot_name: The ancestor slot that the parent labware is ultimately loaded into,
978
+ perhaps after going through a module in the middle.
909
979
 
910
980
  Returns:
911
- If `slot_name` is provided, returns the gripper offsets that the labware definition
981
+ If `slot_name` is provided, returns the gripper offsets that the parent labware definition
912
982
  specifies just for that slot, or `None` if the labware definition doesn't have an
913
983
  exact match.
914
984
 
915
- If `slot_name` is `None`, returns the gripper offsets that the labware
985
+ If `slot_name` is `None`, returns the gripper offsets that the parent labware
916
986
  definition designates as "default," or `None` if it doesn't designate any as such.
917
987
  """
918
- parsed_offsets = self.get_definition(labware_id).gripperOffsets
988
+ if labware_id is not None:
989
+ labware_definition = self.get_definition(labware_id)
990
+ else:
991
+ # Should be ensured by our @overloads.
992
+ assert labware_definition is not None
993
+
994
+ parsed_offsets = labware_definition.gripperOffsets
919
995
  offset_key = slot_name.id if slot_name else "default"
920
996
 
921
997
  if parsed_offsets is None or offset_key not in parsed_offsets:
@@ -930,20 +1006,22 @@ class LabwareView(HasState[LabwareState]):
930
1006
  ),
931
1007
  )
932
1008
 
933
- def get_grip_force(self, labware_id: str) -> float:
1009
+ def get_grip_force(self, labware_definition: LabwareDefinition) -> float:
934
1010
  """Get the recommended grip force for gripping labware using gripper."""
935
- recommended_force = self.get_definition(labware_id).gripForce
1011
+ recommended_force = labware_definition.gripForce
936
1012
  return (
937
1013
  recommended_force if recommended_force is not None else LABWARE_GRIP_FORCE
938
1014
  )
939
1015
 
940
- def get_grip_height_from_labware_bottom(self, labware_id: str) -> float:
1016
+ def get_grip_height_from_labware_bottom(
1017
+ self, labware_definition: LabwareDefinition
1018
+ ) -> float:
941
1019
  """Get the recommended grip height from labware bottom, if present."""
942
- recommended_height = self.get_definition(labware_id).gripHeightFromLabwareBottom
1020
+ recommended_height = labware_definition.gripHeightFromLabwareBottom
943
1021
  return (
944
1022
  recommended_height
945
1023
  if recommended_height is not None
946
- else self.get_dimensions(labware_id).z / 2
1024
+ else self.get_dimensions(labware_definition=labware_definition).z / 2
947
1025
  )
948
1026
 
949
1027
  @staticmethod
@@ -986,7 +1064,7 @@ class LabwareView(HasState[LabwareState]):
986
1064
  def _max_z_of_well(well_defn: WellDefinition) -> float:
987
1065
  return well_defn.z + well_defn.depth
988
1066
 
989
- def get_well_bbox(self, labware_id: str) -> Dimensions:
1067
+ def get_well_bbox(self, labware_definition: LabwareDefinition) -> Dimensions:
990
1068
  """Get the bounding box implied by the wells.
991
1069
 
992
1070
  The bounding box of the labware that is implied by the wells is that required
@@ -997,14 +1075,13 @@ class LabwareView(HasState[LabwareState]):
997
1075
  This is used for the specific purpose of finding the reasonable uncertainty bounds of
998
1076
  where and how a gripper will interact with a labware.
999
1077
  """
1000
- defn = self.get_definition(labware_id)
1001
1078
  max_x: Optional[float] = None
1002
1079
  min_x: Optional[float] = None
1003
1080
  max_y: Optional[float] = None
1004
1081
  min_y: Optional[float] = None
1005
1082
  max_z: Optional[float] = None
1006
1083
 
1007
- for well in defn.wells.values():
1084
+ for well in labware_definition.wells.values():
1008
1085
  well_max_x = self._max_x_of_well(well)
1009
1086
  well_min_x = self._min_x_of_well(well)
1010
1087
  well_max_y = self._max_y_of_well(well)
@@ -9,6 +9,9 @@ AbsorbanceReaderLidId = NewType("AbsorbanceReaderLidId", str)
9
9
  AbsorbanceReaderMeasureMode = NewType("AbsorbanceReaderMeasureMode", str)
10
10
 
11
11
 
12
+ # todo(mm, 2024-11-08): frozen=True is getting pretty painful because ModuleStore has
13
+ # no type-safe way to modify just a single attribute. Consider unfreezing this
14
+ # (taking care to ensure that consumers of ModuleView still only get a read-only view).
12
15
  @dataclass(frozen=True)
13
16
  class AbsorbanceReaderSubState:
14
17
  """Absorbance-Plate-Reader-specific state."""
@@ -21,7 +24,6 @@ class AbsorbanceReaderSubState:
21
24
  configured_wavelengths: Optional[List[int]]
22
25
  measure_mode: Optional[AbsorbanceReaderMeasureMode]
23
26
  reference_wavelength: Optional[int]
24
- lid_id: Optional[str]
25
27
 
26
28
  def raise_if_lid_status_not_expected(self, lid_on_expected: bool) -> None:
27
29
  """Raise if the lid status is not correct."""