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.
Files changed (189) hide show
  1. opentrons/_version.py +2 -2
  2. opentrons/cli/analyze.py +4 -1
  3. opentrons/config/__init__.py +7 -0
  4. opentrons/drivers/asyncio/communication/serial_connection.py +126 -49
  5. opentrons/drivers/heater_shaker/abstract.py +5 -0
  6. opentrons/drivers/heater_shaker/driver.py +10 -0
  7. opentrons/drivers/heater_shaker/simulator.py +4 -0
  8. opentrons/drivers/thermocycler/abstract.py +6 -0
  9. opentrons/drivers/thermocycler/driver.py +61 -10
  10. opentrons/drivers/thermocycler/simulator.py +6 -0
  11. opentrons/drivers/vacuum_module/__init__.py +5 -0
  12. opentrons/drivers/vacuum_module/abstract.py +93 -0
  13. opentrons/drivers/vacuum_module/driver.py +208 -0
  14. opentrons/drivers/vacuum_module/errors.py +39 -0
  15. opentrons/drivers/vacuum_module/simulator.py +85 -0
  16. opentrons/drivers/vacuum_module/types.py +79 -0
  17. opentrons/execute.py +3 -0
  18. opentrons/hardware_control/api.py +24 -5
  19. opentrons/hardware_control/backends/controller.py +8 -2
  20. opentrons/hardware_control/backends/flex_protocol.py +1 -0
  21. opentrons/hardware_control/backends/ot3controller.py +35 -2
  22. opentrons/hardware_control/backends/ot3simulator.py +3 -1
  23. opentrons/hardware_control/backends/ot3utils.py +37 -0
  24. opentrons/hardware_control/backends/simulator.py +2 -1
  25. opentrons/hardware_control/backends/subsystem_manager.py +5 -2
  26. opentrons/hardware_control/emulation/abstract_emulator.py +6 -4
  27. opentrons/hardware_control/emulation/connection_handler.py +8 -5
  28. opentrons/hardware_control/emulation/heater_shaker.py +12 -3
  29. opentrons/hardware_control/emulation/settings.py +1 -1
  30. opentrons/hardware_control/emulation/thermocycler.py +67 -15
  31. opentrons/hardware_control/module_control.py +105 -10
  32. opentrons/hardware_control/modules/__init__.py +3 -0
  33. opentrons/hardware_control/modules/absorbance_reader.py +11 -4
  34. opentrons/hardware_control/modules/flex_stacker.py +38 -9
  35. opentrons/hardware_control/modules/heater_shaker.py +42 -5
  36. opentrons/hardware_control/modules/magdeck.py +8 -4
  37. opentrons/hardware_control/modules/mod_abc.py +14 -6
  38. opentrons/hardware_control/modules/tempdeck.py +25 -5
  39. opentrons/hardware_control/modules/thermocycler.py +68 -11
  40. opentrons/hardware_control/modules/types.py +20 -1
  41. opentrons/hardware_control/modules/utils.py +11 -4
  42. opentrons/hardware_control/motion_utilities.py +6 -6
  43. opentrons/hardware_control/nozzle_manager.py +3 -0
  44. opentrons/hardware_control/ot3api.py +85 -17
  45. opentrons/hardware_control/poller.py +22 -8
  46. opentrons/hardware_control/protocols/liquid_handler.py +6 -2
  47. opentrons/hardware_control/scripts/update_module_fw.py +5 -0
  48. opentrons/hardware_control/types.py +43 -2
  49. opentrons/legacy_commands/commands.py +58 -5
  50. opentrons/legacy_commands/module_commands.py +52 -0
  51. opentrons/legacy_commands/protocol_commands.py +53 -1
  52. opentrons/legacy_commands/types.py +155 -1
  53. opentrons/motion_planning/deck_conflict.py +17 -12
  54. opentrons/motion_planning/waypoints.py +15 -29
  55. opentrons/protocol_api/__init__.py +5 -1
  56. opentrons/protocol_api/_transfer_liquid_validation.py +17 -2
  57. opentrons/protocol_api/_types.py +8 -1
  58. opentrons/protocol_api/core/common.py +3 -1
  59. opentrons/protocol_api/core/engine/_default_labware_versions.py +33 -11
  60. opentrons/protocol_api/core/engine/deck_conflict.py +3 -1
  61. opentrons/protocol_api/core/engine/instrument.py +109 -26
  62. opentrons/protocol_api/core/engine/labware.py +8 -1
  63. opentrons/protocol_api/core/engine/module_core.py +95 -4
  64. opentrons/protocol_api/core/engine/protocol.py +51 -2
  65. opentrons/protocol_api/core/engine/stringify.py +2 -0
  66. opentrons/protocol_api/core/engine/tasks.py +48 -0
  67. opentrons/protocol_api/core/engine/well.py +8 -0
  68. opentrons/protocol_api/core/instrument.py +19 -2
  69. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +19 -2
  70. opentrons/protocol_api/core/legacy/legacy_module_core.py +33 -2
  71. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +23 -1
  72. opentrons/protocol_api/core/legacy/legacy_well_core.py +4 -0
  73. opentrons/protocol_api/core/legacy/tasks.py +19 -0
  74. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +19 -2
  75. opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +14 -2
  76. opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
  77. opentrons/protocol_api/core/module.py +58 -2
  78. opentrons/protocol_api/core/protocol.py +23 -2
  79. opentrons/protocol_api/core/tasks.py +31 -0
  80. opentrons/protocol_api/core/well.py +4 -0
  81. opentrons/protocol_api/instrument_context.py +388 -2
  82. opentrons/protocol_api/labware.py +10 -2
  83. opentrons/protocol_api/module_contexts.py +170 -6
  84. opentrons/protocol_api/protocol_context.py +87 -21
  85. opentrons/protocol_api/robot_context.py +41 -25
  86. opentrons/protocol_api/tasks.py +48 -0
  87. opentrons/protocol_api/validation.py +49 -3
  88. opentrons/protocol_engine/__init__.py +4 -0
  89. opentrons/protocol_engine/actions/__init__.py +6 -2
  90. opentrons/protocol_engine/actions/actions.py +31 -9
  91. opentrons/protocol_engine/clients/sync_client.py +42 -7
  92. opentrons/protocol_engine/commands/__init__.py +56 -0
  93. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +2 -15
  94. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +2 -15
  95. opentrons/protocol_engine/commands/absorbance_reader/read.py +22 -23
  96. opentrons/protocol_engine/commands/aspirate.py +1 -0
  97. opentrons/protocol_engine/commands/aspirate_while_tracking.py +52 -19
  98. opentrons/protocol_engine/commands/capture_image.py +302 -0
  99. opentrons/protocol_engine/commands/command.py +2 -0
  100. opentrons/protocol_engine/commands/command_unions.py +62 -0
  101. opentrons/protocol_engine/commands/create_timer.py +83 -0
  102. opentrons/protocol_engine/commands/dispense.py +1 -0
  103. opentrons/protocol_engine/commands/dispense_while_tracking.py +56 -19
  104. opentrons/protocol_engine/commands/drop_tip.py +32 -8
  105. opentrons/protocol_engine/commands/flex_stacker/common.py +35 -0
  106. opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +7 -0
  107. opentrons/protocol_engine/commands/heater_shaker/__init__.py +14 -0
  108. opentrons/protocol_engine/commands/heater_shaker/common.py +20 -0
  109. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +5 -4
  110. opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +136 -0
  111. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +31 -5
  112. opentrons/protocol_engine/commands/move_labware.py +3 -4
  113. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +1 -1
  114. opentrons/protocol_engine/commands/movement_common.py +31 -2
  115. opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
  116. opentrons/protocol_engine/commands/pipetting_common.py +48 -3
  117. opentrons/protocol_engine/commands/set_tip_state.py +97 -0
  118. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +38 -7
  119. opentrons/protocol_engine/commands/thermocycler/__init__.py +16 -0
  120. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +6 -0
  121. opentrons/protocol_engine/commands/thermocycler/run_profile.py +8 -0
  122. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +44 -7
  123. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +43 -14
  124. opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +191 -0
  125. opentrons/protocol_engine/commands/touch_tip.py +1 -1
  126. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +6 -22
  127. opentrons/protocol_engine/commands/wait_for_tasks.py +98 -0
  128. opentrons/protocol_engine/create_protocol_engine.py +12 -0
  129. opentrons/protocol_engine/engine_support.py +3 -0
  130. opentrons/protocol_engine/errors/__init__.py +12 -0
  131. opentrons/protocol_engine/errors/exceptions.py +119 -0
  132. opentrons/protocol_engine/execution/__init__.py +4 -0
  133. opentrons/protocol_engine/execution/command_executor.py +62 -1
  134. opentrons/protocol_engine/execution/create_queue_worker.py +9 -2
  135. opentrons/protocol_engine/execution/labware_movement.py +13 -15
  136. opentrons/protocol_engine/execution/movement.py +2 -0
  137. opentrons/protocol_engine/execution/pipetting.py +19 -25
  138. opentrons/protocol_engine/execution/queue_worker.py +4 -0
  139. opentrons/protocol_engine/execution/run_control.py +8 -0
  140. opentrons/protocol_engine/execution/task_handler.py +157 -0
  141. opentrons/protocol_engine/protocol_engine.py +137 -36
  142. opentrons/protocol_engine/resources/__init__.py +4 -0
  143. opentrons/protocol_engine/resources/camera_provider.py +110 -0
  144. opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
  145. opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
  146. opentrons/protocol_engine/resources/file_provider.py +133 -58
  147. opentrons/protocol_engine/resources/labware_validation.py +10 -6
  148. opentrons/protocol_engine/slot_standardization.py +2 -0
  149. opentrons/protocol_engine/state/_well_math.py +60 -18
  150. opentrons/protocol_engine/state/addressable_areas.py +2 -0
  151. opentrons/protocol_engine/state/camera.py +54 -0
  152. opentrons/protocol_engine/state/commands.py +37 -14
  153. opentrons/protocol_engine/state/geometry.py +276 -379
  154. opentrons/protocol_engine/state/labware.py +62 -108
  155. opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
  156. opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1336 -0
  157. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
  158. opentrons/protocol_engine/state/modules.py +30 -8
  159. opentrons/protocol_engine/state/motion.py +44 -0
  160. opentrons/protocol_engine/state/preconditions.py +59 -0
  161. opentrons/protocol_engine/state/state.py +44 -0
  162. opentrons/protocol_engine/state/state_summary.py +4 -0
  163. opentrons/protocol_engine/state/tasks.py +139 -0
  164. opentrons/protocol_engine/state/tips.py +177 -258
  165. opentrons/protocol_engine/state/update_types.py +26 -9
  166. opentrons/protocol_engine/types/__init__.py +23 -4
  167. opentrons/protocol_engine/types/command_preconditions.py +18 -0
  168. opentrons/protocol_engine/types/deck_configuration.py +5 -1
  169. opentrons/protocol_engine/types/instrument.py +8 -1
  170. opentrons/protocol_engine/types/labware.py +1 -13
  171. opentrons/protocol_engine/types/location.py +26 -2
  172. opentrons/protocol_engine/types/module.py +11 -1
  173. opentrons/protocol_engine/types/tasks.py +38 -0
  174. opentrons/protocol_engine/types/tip.py +9 -0
  175. opentrons/protocol_runner/create_simulating_orchestrator.py +29 -2
  176. opentrons/protocol_runner/protocol_runner.py +14 -1
  177. opentrons/protocol_runner/run_orchestrator.py +49 -2
  178. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +2 -2
  179. opentrons/protocols/api_support/definitions.py +1 -1
  180. opentrons/protocols/api_support/types.py +2 -1
  181. opentrons/simulate.py +51 -15
  182. opentrons/system/camera.py +334 -4
  183. opentrons/system/ffmpeg.py +110 -0
  184. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/METADATA +4 -4
  185. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/RECORD +188 -160
  186. opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
  187. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/WHEEL +0 -0
  188. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/entry_points.txt +0 -0
  189. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/licenses/LICENSE +0 -0
@@ -24,6 +24,7 @@ from opentrons_shared_data.labware.constants import WELL_NAME_PATTERN
24
24
  from opentrons_shared_data.labware.labware_definition import (
25
25
  LabwareDefinition,
26
26
  LabwareDefinition2,
27
+ LabwareDefinition3,
27
28
  InnerWellGeometry,
28
29
  )
29
30
  from opentrons_shared_data.deck.types import CutoutFixture
@@ -44,7 +45,6 @@ from ..errors.exceptions import (
44
45
  )
45
46
  from ..resources import (
46
47
  fixture_validation,
47
- labware_validation,
48
48
  deck_configuration_provider,
49
49
  )
50
50
  from ..types import (
@@ -63,13 +63,13 @@ from ..types import (
63
63
  ModuleLocation,
64
64
  OnLabwareLocation,
65
65
  LabwareLocation,
66
- LabwareOffsetVector,
67
66
  ModuleOffsetData,
68
67
  CurrentWell,
69
68
  CurrentPipetteLocation,
70
69
  TipGeometry,
71
- LabwareMovementOffsetData,
72
70
  InStackerHopperLocation,
71
+ WASTE_CHUTE_LOCATION,
72
+ AccessibleByGripperLocation,
73
73
  OnDeckLabwareLocation,
74
74
  AddressableAreaLocation,
75
75
  AddressableOffsetVector,
@@ -91,7 +91,7 @@ from ..types import (
91
91
  labware_location_is_system,
92
92
  WellLocationType,
93
93
  WellLocationFunction,
94
- LabwareParentDefinition,
94
+ GripperMoveType,
95
95
  AddressableArea,
96
96
  )
97
97
  from ..types.liquid_level_detection import SimulatedProbeResult, LiquidTrackingType
@@ -108,8 +108,11 @@ from .inner_well_math_utils import (
108
108
  find_volume_user_defined_volumes,
109
109
  )
110
110
  from ._well_math import wells_covered_by_pipette_configuration, nozzles_per_well
111
- from ._labware_origin_math import get_parent_placement_origin_to_lw_origin
112
-
111
+ from .labware_origin_math.stackup_origin_to_labware_origin import (
112
+ get_stackup_origin_to_labware_origin,
113
+ LabwareOriginContext,
114
+ LabwareStackupAncestorDefinition,
115
+ )
113
116
 
114
117
  _LOG = getLogger(__name__)
115
118
  SLOT_WIDTH = 128
@@ -125,13 +128,6 @@ class _TipDropSection(enum.Enum):
125
128
  RIGHT = "right"
126
129
 
127
130
 
128
- class _GripperMoveType(enum.Enum):
129
- """Types of gripper movement."""
130
-
131
- PICK_UP_LABWARE = enum.auto()
132
- DROP_LABWARE = enum.auto()
133
-
134
-
135
131
  @dataclass
136
132
  class _AbsoluteRobotExtents:
137
133
  front_left: Dict[MountType, Point]
@@ -204,6 +200,7 @@ class GeometryView:
204
200
  if isinstance(loc, InStackerHopperLocation) or isinstance(
205
201
  loc, NotOnDeckLocationSequenceComponent
206
202
  ):
203
+
207
204
  return False
208
205
  return True
209
206
 
@@ -402,6 +399,11 @@ class GeometryView:
402
399
  "Labware does not have a slot or module associated with it"
403
400
  " since it is no longer on the deck."
404
401
  )
402
+ elif location == WASTE_CHUTE_LOCATION:
403
+ raise errors.LabwareNotOnDeckError(
404
+ "Labware does not have a slot or module associated with it"
405
+ " since it is in the waste chute."
406
+ )
405
407
 
406
408
  def get_labware_origin_position(self, labware_id: str) -> Point:
407
409
  """Get the deck coordinates of a labware's origin.
@@ -410,145 +412,117 @@ class GeometryView:
410
412
  """
411
413
  location = self._labware.get(labware_id).location
412
414
  definition = self._labware.get_definition(labware_id)
415
+ aa_name = self._get_underlying_addressable_area_name(location)
416
+ # TODO(jh, 08-18-25): Labware locations return the underlying slot as the "on location" for the fixed trash,
417
+ # but the underlying slot's name does not exist in addressable area state. Getting the addressable area from data is
418
+ # a workaround. Investigate further.
419
+ addressable_area = self._addressable_areas._get_addressable_area_from_deck_data(
420
+ aa_name, do_compatibility_check=False
421
+ )
422
+ stackup_lw_defs_locs = self._get_stackup_lw_info_top_to_bottom(
423
+ labware_definition=definition, location=location
424
+ )
425
+ underlying_ancestor_def = self._get_stackup_underlying_ancestor_definition(
426
+ location
427
+ )
428
+ module_parent_to_child_offset = self._get_stackup_module_parent_to_child_offset(
429
+ location
430
+ )
413
431
 
414
- slot_front_left = self._get_labware_ancestor_position(labware_id)
415
- stackup_origin_to_lw_origin = self._get_stackup_placement_origin_to_lw_origin(
416
- location=location, definition=definition, is_topmost_labware=True
432
+ slot_front_left = self._addressable_areas.get_addressable_area_position(aa_name)
433
+ stackup_origin_to_lw_origin = get_stackup_origin_to_labware_origin(
434
+ context=LabwareOriginContext.PIPETTING,
435
+ stackup_lw_info_top_to_bottom=stackup_lw_defs_locs,
436
+ underlying_ancestor_definition=underlying_ancestor_def,
437
+ module_parent_to_child_offset=module_parent_to_child_offset,
438
+ deck_definition=self._addressable_areas.deck_definition,
439
+ slot_name=addressable_area.base_slot,
417
440
  )
418
441
  module_cal_offset = self._get_calibrated_module_offset(location)
419
442
 
420
443
  return slot_front_left + stackup_origin_to_lw_origin + module_cal_offset
421
444
 
422
- def _get_labware_ancestor_position(self, labware_id: str) -> Point:
423
- """Get the position of the labware's underlying ancestor."""
424
- slot_name = self._get_underlying_addressable_area_name(
425
- self._labware.get(labware_id).location
426
- )
427
- parent_pos = self._addressable_areas.get_addressable_area_position(slot_name)
445
+ def _get_stackup_lw_info_top_to_bottom(
446
+ self, labware_definition: LabwareDefinition, location: LabwareLocation
447
+ ) -> list[tuple[LabwareDefinition, LabwareLocation]]:
448
+ """Returns info about each labware in the stackup.
428
449
 
429
- return parent_pos
430
-
431
- def _get_stackup_placement_origin_to_lw_origin(
432
- self,
433
- location: LabwareLocation,
434
- definition: LabwareDefinition,
435
- is_topmost_labware: bool,
436
- ) -> Point:
437
- """Get the offset vector from the lowest entity in a stackup to the labware."""
438
- if isinstance(
439
- location, (AddressableAreaLocation, DeckSlotLocation, ModuleLocation)
440
- ):
441
- return self._get_parent_placement_origin_to_lw_origin(
442
- labware_location=location,
443
- labware_definition=definition,
444
- is_topmost_labware=is_topmost_labware,
445
- )
446
- elif isinstance(location, OnLabwareLocation):
447
- parent_id = location.labwareId
448
- parent_location = self._labware.get(parent_id).location
449
- parent_definition = self._labware.get_definition(parent_id)
450
-
451
- parent_placement_origin_to_lw_origin = (
452
- self._get_parent_placement_origin_to_lw_origin(
453
- labware_location=location,
454
- labware_definition=definition,
455
- is_topmost_labware=is_topmost_labware,
456
- )
457
- )
450
+ The list is ordered from the top labware to the bottom-most labware.
451
+ The first entry will always be the definition and location of the given labware itself.
452
+ """
453
+ definitions_locations_top_to_bottom: list[
454
+ tuple[LabwareDefinition, LabwareLocation]
455
+ ] = []
456
+ current_location = location
457
+ current_definition = labware_definition
458
458
 
459
- return (
460
- parent_placement_origin_to_lw_origin
461
- + self._get_stackup_placement_origin_to_lw_origin(
462
- location=parent_location,
463
- definition=parent_definition,
464
- is_topmost_labware=False,
465
- )
466
- )
467
- else:
468
- raise errors.LabwareNotOnDeckError(
469
- "Cannot access labware since it is not on the deck. "
470
- "Either it has been loaded off-deck or its been moved off-deck."
459
+ while True:
460
+ definitions_locations_top_to_bottom.append(
461
+ (current_definition, current_location)
471
462
  )
472
463
 
473
- def _get_parent_placement_origin_to_lw_origin(
474
- self,
475
- labware_location: LabwareLocation,
476
- labware_definition: LabwareDefinition,
477
- is_topmost_labware: bool,
478
- ) -> Point:
479
- parent_deck_item = self._get_parent_definition(labware_location)
480
-
481
- if isinstance(labware_location, ModuleLocation):
482
- module_parent_to_child_offset = (
483
- self._modules.get_nominal_offset_to_child_from_addressable_area(
484
- module_id=labware_location.moduleId,
464
+ if isinstance(current_location, OnLabwareLocation):
465
+ current_labware_id = current_location.labwareId
466
+ current_location = self._labware.get(current_labware_id).location
467
+ current_definition = self._labware.get_definition(current_labware_id)
468
+ else:
469
+ break
470
+ return definitions_locations_top_to_bottom
471
+
472
+ def _get_stackup_module_parent_to_child_offset(
473
+ self, top_most_lw_location: LabwareLocation
474
+ ) -> Union[Point, None]:
475
+ """Traverse the stackup to find the first parent-to-child module offset, if any."""
476
+ current_location = top_most_lw_location
477
+
478
+ while True:
479
+ if isinstance(current_location, ModuleLocation):
480
+ module_parent_to_child_offset = (
481
+ self._modules.get_nominal_offset_to_child_from_addressable_area(
482
+ module_id=current_location.moduleId,
483
+ )
485
484
  )
486
- )
487
- return get_parent_placement_origin_to_lw_origin(
488
- child_labware=labware_definition,
489
- parent_deck_item=parent_deck_item, # type: ignore[arg-type]
490
- module_parent_to_child_offset=module_parent_to_child_offset,
491
- deck_definition=self._addressable_areas.deck_definition,
492
- is_topmost_labware=is_topmost_labware,
493
- labware_location=labware_location,
494
- )
495
- elif isinstance(labware_location, OnLabwareLocation):
496
- return get_parent_placement_origin_to_lw_origin(
497
- child_labware=labware_definition,
498
- parent_deck_item=parent_deck_item, # type: ignore[arg-type]
499
- module_parent_to_child_offset=None,
500
- deck_definition=self._addressable_areas.deck_definition,
501
- is_topmost_labware=is_topmost_labware,
502
- labware_location=labware_location,
503
- )
504
- elif isinstance(labware_location, (DeckSlotLocation, AddressableAreaLocation)):
505
- return get_parent_placement_origin_to_lw_origin(
506
- child_labware=labware_definition,
507
- parent_deck_item=parent_deck_item, # type: ignore[arg-type]
508
- module_parent_to_child_offset=None,
509
- deck_definition=self._addressable_areas.deck_definition,
510
- is_topmost_labware=is_topmost_labware,
511
- labware_location=labware_location,
512
- )
513
- else:
514
- raise ValueError(f"Invalid labware location: {labware_location}")
515
-
516
- def _get_parent_definition(
517
- self, location: LabwareLocation
518
- ) -> LabwareParentDefinition:
519
- """Get the parent's definition given the labware's location."""
520
- if isinstance(location, DeckSlotLocation):
521
- addressable_area_name = location.slotName.id
522
- return self._addressable_areas.get_slot_definition(addressable_area_name)
523
-
524
- elif isinstance(location, AddressableAreaLocation):
525
- addressable_area_name = location.addressableAreaName
526
- return self._addressable_areas.get_addressable_area(addressable_area_name)
485
+ return module_parent_to_child_offset
527
486
 
528
- elif isinstance(location, ModuleLocation):
529
- module_id = location.moduleId
530
- return self._modules.get_definition(module_id)
531
-
532
- elif isinstance(location, OnLabwareLocation):
533
- below_labware_id = location.labwareId
534
- return self._labware.get_definition(below_labware_id)
487
+ if isinstance(current_location, OnLabwareLocation):
488
+ current_labware_id = current_location.labwareId
489
+ current_labware = self._labware.get(current_labware_id)
490
+ current_location = current_labware.location
491
+ else:
492
+ break
535
493
 
536
- elif location == OFF_DECK_LOCATION or location == SYSTEM_LOCATION:
537
- raise errors.LabwareNotOnDeckError(
538
- f"Labware location {location} does not have a slot associated with it"
539
- f" since it is no longer on the deck."
540
- )
494
+ return None
541
495
 
542
- elif isinstance(location, InStackerHopperLocation):
543
- raise errors.LabwareNotOnDeckError(
544
- "Labware does not have a slot or module associated with it"
545
- " since it is no longer on the deck."
546
- )
496
+ def _get_stackup_underlying_ancestor_definition(
497
+ self, top_most_lw_location: LabwareLocation
498
+ ) -> LabwareStackupAncestorDefinition:
499
+ """Traverse the stackup to find the first non-labware definition."""
500
+ current_location = top_most_lw_location
501
+ while True:
502
+ if isinstance(current_location, OnLabwareLocation):
503
+ current_labware_id = current_location.labwareId
504
+ current_labware = self._labware.get(current_labware_id)
505
+ current_location = current_labware.location
506
+ else:
507
+ if isinstance(current_location, ModuleLocation):
508
+ return self._modules.get_definition(current_location.moduleId)
547
509
 
548
- else:
549
- raise errors.InvalidLabwarePositionError(
550
- f"Cannot get ancestor from location {location}"
551
- )
510
+ elif isinstance(current_location, AddressableAreaLocation):
511
+ return self._addressable_areas.get_addressable_area(
512
+ current_location.addressableAreaName
513
+ )
514
+ elif isinstance(current_location, DeckSlotLocation):
515
+ return self._addressable_areas.get_slot_definition(
516
+ current_location.slotName.id
517
+ )
518
+ elif current_location == WASTE_CHUTE_LOCATION:
519
+ return self._addressable_areas.get_addressable_area(
520
+ "gripperWasteChute"
521
+ )
522
+ else:
523
+ raise errors.InvalidLabwarePositionError(
524
+ f"Cannot get ancestor slot of location {current_location}"
525
+ )
552
526
 
553
527
  def _get_underlying_addressable_area_name(self, location: LabwareLocation) -> str:
554
528
  if isinstance(location, DeckSlotLocation):
@@ -559,6 +533,8 @@ class GeometryView:
559
533
  return self._modules.get_provided_addressable_area(location.moduleId)
560
534
  elif isinstance(location, OnLabwareLocation):
561
535
  return self.get_ancestor_addressable_area_name(location.labwareId)
536
+ elif location == WASTE_CHUTE_LOCATION:
537
+ return "gripperWasteChute"
562
538
  else:
563
539
  raise errors.InvalidLabwarePositionError(
564
540
  f"Cannot get ancestor slot of location {location}"
@@ -661,7 +637,9 @@ class GeometryView:
661
637
  if meniscus_tracking:
662
638
  location = LiquidHandlingWellLocation(
663
639
  origin=WellOrigin.MENISCUS,
664
- offset=WellOffset(x=0, y=0, z=absolute_point.z),
640
+ offset=WellOffset(
641
+ x=absolute_point.x, y=absolute_point.y, z=absolute_point.z
642
+ ),
665
643
  )
666
644
  # TODO(cm): handle operationVolume being a float other than 0
667
645
  if meniscus_tracking == MeniscusTrackingTarget.END:
@@ -717,6 +695,9 @@ class GeometryView:
717
695
  return well_def.depth
718
696
 
719
697
  def _get_highest_z_from_labware_data(self, lw_data: LoadedLabware) -> float:
698
+ if lw_data.location == WASTE_CHUTE_LOCATION:
699
+ # Returns 0 so that the waste chute height is not added to the height of the lbw
700
+ return 0
720
701
  labware_pos = self.get_labware_position(lw_data.id)
721
702
  z_dim = self._labware.get_dimensions(labware_id=lw_data.id).z
722
703
  height_over_labware: float = 0
@@ -776,7 +757,7 @@ class GeometryView:
776
757
 
777
758
  if well_def.shape != "circular":
778
759
  raise errors.LabwareIsNotTipRackError(
779
- f"Well {well_name} in labware {labware_id} is not circular."
760
+ f"Well {well_name} in labware {self._labware.get_display_name(labware_id)} is not circular."
780
761
  )
781
762
 
782
763
  return TipGeometry(
@@ -867,9 +848,14 @@ class GeometryView:
867
848
  slot_name = DeckSlotName.from_primitive(area_name)
868
849
  elif labware.location == OFF_DECK_LOCATION:
869
850
  raise errors.LabwareNotOnDeckError(
870
- f"Labware {labware_id} does not have a slot associated with it"
851
+ f"Labware {self._labware.get_display_name(labware_id)} does not have a slot associated with it"
871
852
  f" since it is no longer on the deck."
872
853
  )
854
+ elif labware.location == WASTE_CHUTE_LOCATION:
855
+ raise errors.LabwareNotOnDeckError(
856
+ f"Labware {self._labware.get_display_name(labware_id)} does not have a slot associated with it"
857
+ f" since it is in the waste chute."
858
+ )
873
859
  else:
874
860
  _LOG.error(
875
861
  f"Unhandled location type in get_ancestor_slot_name: {labware.location}"
@@ -1055,9 +1041,9 @@ class GeometryView:
1055
1041
  def get_labware_grip_point(
1056
1042
  self,
1057
1043
  labware_definition: LabwareDefinition,
1058
- location: Union[
1059
- DeckSlotLocation, ModuleLocation, OnLabwareLocation, AddressableAreaLocation
1060
- ],
1044
+ location: AccessibleByGripperLocation,
1045
+ move_type: GripperMoveType,
1046
+ user_additional_offset: Point | None,
1061
1047
  ) -> Point:
1062
1048
  """Get the grip point of the labware as placed on the given location.
1063
1049
 
@@ -1069,28 +1055,105 @@ class GeometryView:
1069
1055
  It is calculated as the xy center of the slot with z as the point indicated by
1070
1056
  z-position of labware bottom + grip height from labware bottom.
1071
1057
  """
1072
- grip_z_from_lw_origin = self._labware.get_grip_z(labware_definition)
1073
- aa_name = self._get_underlying_addressable_area_name(location)
1074
- parent_to_lw_offset = self._get_stackup_placement_origin_to_lw_origin(
1058
+ mod_cal_offset = self._get_calibrated_module_offset(location)
1059
+ user_additional_offset = user_additional_offset or Point()
1060
+ aa_origin_to_nominal_grip_point = self._get_aa_origin_to_nominal_grip_point(
1061
+ labware_definition=labware_definition,
1075
1062
  location=location,
1076
- definition=labware_definition,
1077
- is_topmost_labware=True, # We aren't concerned with entities above the gripped labware.
1063
+ move_type=move_type,
1078
1064
  )
1065
+
1066
+ return aa_origin_to_nominal_grip_point + mod_cal_offset + user_additional_offset
1067
+
1068
+ def _get_aa_origin_to_nominal_grip_point(
1069
+ self,
1070
+ labware_definition: LabwareDefinition,
1071
+ location: AccessibleByGripperLocation,
1072
+ move_type: GripperMoveType,
1073
+ ) -> Point:
1074
+ """Get the nominal grip point of a labware.
1075
+
1076
+ Does not include module calibration offsets or user additional offsets.
1077
+ """
1078
+ grip_z_from_lw_origin = self._labware.get_grip_z(labware_definition)
1079
+ aa_name = self._get_underlying_addressable_area_name(location)
1079
1080
  addressable_area = self._addressable_areas.get_addressable_area(aa_name)
1080
- lw_origin_to_parent = self._get_lw_origin_to_parent(
1081
- labware_definition=labware_definition, addressable_area=addressable_area
1081
+ stackup_defs_locs = self._get_stackup_lw_info_top_to_bottom(
1082
+ labware_definition=labware_definition, location=location
1083
+ )
1084
+ module_parent_to_child_offset = self._get_stackup_module_parent_to_child_offset(
1085
+ location
1086
+ )
1087
+ underlying_ancestor_def = self._get_stackup_underlying_ancestor_definition(
1088
+ location
1089
+ )
1090
+ context_type = (
1091
+ LabwareOriginContext.GRIPPER_PICKING_UP
1092
+ if move_type == GripperMoveType.PICK_UP_LABWARE
1093
+ else LabwareOriginContext.GRIPPER_DROPPING
1082
1094
  )
1083
- mod_cal_offset = self._get_calibrated_module_offset(location)
1084
- location_center = self._addressable_areas.get_addressable_area_center(aa_name)
1085
1095
 
1086
- return (
1087
- location_center
1088
- + parent_to_lw_offset
1089
- + lw_origin_to_parent
1090
- + mod_cal_offset
1091
- + Point(0, 0, grip_z_from_lw_origin)
1096
+ aa_origin_to_lw_origin = get_stackup_origin_to_labware_origin(
1097
+ context=context_type,
1098
+ module_parent_to_child_offset=module_parent_to_child_offset,
1099
+ underlying_ancestor_definition=underlying_ancestor_def,
1100
+ stackup_lw_info_top_to_bottom=stackup_defs_locs,
1101
+ slot_name=addressable_area.base_slot,
1102
+ deck_definition=self._addressable_areas.deck_definition,
1092
1103
  )
1093
1104
 
1105
+ if isinstance(labware_definition, LabwareDefinition2):
1106
+ lw_origin_to_aa_origin = self._get_lw_origin_to_parent(
1107
+ labware_definition=labware_definition, addressable_area=addressable_area
1108
+ )
1109
+ aa_origin_to_aa_center = (
1110
+ self._addressable_areas.get_addressable_area_center(aa_name)
1111
+ )
1112
+ aa_center_to_nominal_grip_point = Point(0, 0, grip_z_from_lw_origin)
1113
+
1114
+ return (
1115
+ aa_origin_to_lw_origin
1116
+ + lw_origin_to_aa_origin
1117
+ + aa_origin_to_aa_center
1118
+ + aa_center_to_nominal_grip_point
1119
+ )
1120
+
1121
+ else:
1122
+ assert isinstance(labware_definition, LabwareDefinition3)
1123
+
1124
+ aa_origin = self._addressable_areas.get_addressable_area_position(aa_name)
1125
+ lw_origin_to_lw_center = self._get_lw_origin_to_lw_center(
1126
+ labware_definition
1127
+ )
1128
+ lw_origin_to_lw_grip_center = Point(
1129
+ x=lw_origin_to_lw_center.x,
1130
+ y=lw_origin_to_lw_center.y,
1131
+ z=grip_z_from_lw_origin,
1132
+ )
1133
+
1134
+ return aa_origin + aa_origin_to_lw_origin + lw_origin_to_lw_grip_center
1135
+
1136
+ def _get_lw_origin_to_lw_center(
1137
+ self, labware_definition: LabwareDefinition
1138
+ ) -> Point:
1139
+ """Get the x,y,z center of the labware."""
1140
+ if isinstance(labware_definition, LabwareDefinition2):
1141
+ dimensions = labware_definition.dimensions
1142
+ x = dimensions.xDimension / 2
1143
+ y = dimensions.yDimension / 2
1144
+ z = dimensions.zDimension / 2
1145
+
1146
+ return Point(x, y, z)
1147
+ else:
1148
+ front_right_top = labware_definition.extents.total.frontRightTop
1149
+ back_left_bottom = labware_definition.extents.total.backLeftBottom
1150
+
1151
+ x = (front_right_top.x - back_left_bottom.x) / 2
1152
+ y = (front_right_top.y - back_left_bottom.y) / 2
1153
+ z = (front_right_top.z - back_left_bottom.z) / 2
1154
+
1155
+ return Point(x, y, z)
1156
+
1094
1157
  def _get_lw_origin_to_parent(
1095
1158
  self, labware_definition: LabwareDefinition, addressable_area: AddressableArea
1096
1159
  ) -> Point:
@@ -1124,7 +1187,6 @@ class GeometryView:
1124
1187
  if self._modules.should_dodge_thermocycler(
1125
1188
  from_slot=from_slot, to_slot=to_slot
1126
1189
  ):
1127
-
1128
1190
  middle_slot_fixture = (
1129
1191
  self._addressable_areas.get_fixture_by_deck_slot_name(
1130
1192
  DeckSlotName.SLOT_C2.to_equivalent_for_robot_type(
@@ -1393,48 +1455,20 @@ class GeometryView:
1393
1455
  x_well_offset = 0
1394
1456
  return x_well_offset
1395
1457
 
1396
- def get_final_labware_movement_offset_vectors(
1397
- self,
1398
- from_location: OnDeckLabwareLocation,
1399
- to_location: OnDeckLabwareLocation,
1400
- additional_pick_up_offset: Point,
1401
- additional_drop_offset: Point,
1402
- current_labware: LabwareDefinition,
1403
- ) -> LabwareMovementOffsetData:
1404
- """Calculate the final labware offset vector to use in labware movement."""
1405
- pick_up_offset = (
1406
- self.get_total_nominal_gripper_offset_for_move_type(
1407
- location=from_location,
1408
- move_type=_GripperMoveType.PICK_UP_LABWARE,
1409
- current_labware=current_labware,
1410
- )
1411
- + additional_pick_up_offset
1412
- )
1413
- drop_offset = (
1414
- self.get_total_nominal_gripper_offset_for_move_type(
1415
- location=to_location,
1416
- move_type=_GripperMoveType.DROP_LABWARE,
1417
- current_labware=current_labware,
1418
- )
1419
- + additional_drop_offset
1420
- )
1421
-
1422
- return LabwareMovementOffsetData(
1423
- pickUpOffset=LabwareOffsetVector(
1424
- x=pick_up_offset.x, y=pick_up_offset.y, z=pick_up_offset.z
1425
- ),
1426
- dropOffset=LabwareOffsetVector(
1427
- x=drop_offset.x, y=drop_offset.y, z=drop_offset.z
1428
- ),
1429
- )
1430
-
1431
1458
  @staticmethod
1432
1459
  def ensure_valid_gripper_location(
1433
1460
  location: LabwareLocation,
1434
1461
  ) -> Union[
1435
- DeckSlotLocation, ModuleLocation, OnLabwareLocation, AddressableAreaLocation
1462
+ DeckSlotLocation,
1463
+ ModuleLocation,
1464
+ OnLabwareLocation,
1465
+ AddressableAreaLocation,
1436
1466
  ]:
1437
1467
  """Ensure valid on-deck location for gripper, otherwise raise error."""
1468
+ if location == WASTE_CHUTE_LOCATION:
1469
+ raise errors.LabwareMovementNotAllowedError(
1470
+ "Labware movements out of the waste chute are not supported using the gripper."
1471
+ )
1438
1472
  if not isinstance(
1439
1473
  location,
1440
1474
  (
@@ -1449,129 +1483,27 @@ class GeometryView:
1449
1483
  )
1450
1484
  return location
1451
1485
 
1452
- def get_total_nominal_gripper_offset_for_move_type(
1453
- self,
1454
- location: OnDeckLabwareLocation,
1455
- move_type: _GripperMoveType,
1456
- current_labware: LabwareDefinition,
1457
- ) -> Point:
1458
- """Get the total of the offsets to be used to pick up labware in its current location."""
1459
- if move_type == _GripperMoveType.PICK_UP_LABWARE:
1460
- if isinstance(
1461
- location, (ModuleLocation, DeckSlotLocation, AddressableAreaLocation)
1462
- ):
1463
- return Point.from_xyz_attrs(
1464
- self._nominal_gripper_offsets_for_location(location).pickUpOffset
1465
- )
1466
- else:
1467
- # If it's a labware on a labware (most likely an adapter),
1468
- # we calculate the offset as sum of offsets for the direct parent labware
1469
- # and the underlying non-labware parent location.
1470
- direct_parent_offset = self._nominal_gripper_offsets_for_location(
1471
- location
1472
- )
1473
- ancestor = self._labware.get_parent_location(location.labwareId)
1474
- extra_offset = Point(x=0, y=0, z=0)
1475
- if (
1476
- isinstance(ancestor, ModuleLocation)
1477
- # todo(mm, 2025-06-20): Avoid this private attribute access.
1478
- and self._modules._state.requested_model_by_id[ancestor.moduleId]
1479
- == ModuleModel.THERMOCYCLER_MODULE_V2
1480
- and labware_validation.validate_definition_is_lid(current_labware)
1481
- ):
1482
- if "lidOffsets" in current_labware.gripperOffsets.keys():
1483
- extra_offset = Point(
1484
- x=current_labware.gripperOffsets[
1485
- "lidOffsets"
1486
- ].pickUpOffset.x,
1487
- y=current_labware.gripperOffsets[
1488
- "lidOffsets"
1489
- ].pickUpOffset.y,
1490
- z=current_labware.gripperOffsets[
1491
- "lidOffsets"
1492
- ].pickUpOffset.z,
1493
- )
1494
- else:
1495
- raise errors.LabwareOffsetDoesNotExistError(
1496
- f"Labware Definition {current_labware.parameters.loadName} does not contain required field 'lidOffsets' of 'gripperOffsets'."
1497
- )
1498
-
1499
- assert isinstance(
1500
- ancestor,
1501
- (
1502
- DeckSlotLocation,
1503
- ModuleLocation,
1504
- OnLabwareLocation,
1505
- AddressableAreaLocation,
1506
- ),
1507
- ), "No gripper offsets for off-deck labware"
1508
- return (
1509
- Point.from_xyz_attrs(direct_parent_offset.pickUpOffset)
1510
- + Point.from_xyz_attrs(
1511
- self._nominal_gripper_offsets_for_location(
1512
- location=ancestor
1513
- ).pickUpOffset
1514
- )
1515
- + extra_offset
1516
- )
1517
- else:
1518
- if isinstance(
1519
- location, (ModuleLocation, DeckSlotLocation, AddressableAreaLocation)
1520
- ):
1521
- return Point.from_xyz_attrs(
1522
- self._nominal_gripper_offsets_for_location(location).dropOffset
1523
- )
1524
- else:
1525
- # If it's a labware on a labware (most likely an adapter),
1526
- # we calculate the offset as sum of offsets for the direct parent labware
1527
- # and the underlying non-labware parent location.
1528
- direct_parent_offset = self._nominal_gripper_offsets_for_location(
1529
- location
1530
- )
1531
- ancestor = self._labware.get_parent_location(location.labwareId)
1532
- extra_offset = Point(x=0, y=0, z=0)
1533
- if (
1534
- isinstance(ancestor, ModuleLocation)
1535
- # todo(mm, 2024-11-06): Do not access private module state; only use public ModuleView methods.
1536
- and self._modules._state.requested_model_by_id[ancestor.moduleId]
1537
- == ModuleModel.THERMOCYCLER_MODULE_V2
1538
- and labware_validation.validate_definition_is_lid(current_labware)
1539
- ):
1540
- if "lidOffsets" in current_labware.gripperOffsets.keys():
1541
- extra_offset = Point(
1542
- x=current_labware.gripperOffsets[
1543
- "lidOffsets"
1544
- ].pickUpOffset.x,
1545
- y=current_labware.gripperOffsets[
1546
- "lidOffsets"
1547
- ].pickUpOffset.y,
1548
- z=current_labware.gripperOffsets[
1549
- "lidOffsets"
1550
- ].pickUpOffset.z,
1551
- )
1552
- else:
1553
- raise errors.LabwareOffsetDoesNotExistError(
1554
- f"Labware Definition {current_labware.parameters.loadName} does not contain required field 'lidOffsets' of 'gripperOffsets'."
1555
- )
1556
-
1557
- assert isinstance(
1558
- ancestor,
1559
- (
1560
- DeckSlotLocation,
1561
- ModuleLocation,
1562
- OnLabwareLocation,
1563
- AddressableAreaLocation,
1564
- ),
1565
- ), "No gripper offsets for off-deck labware"
1566
- return (
1567
- Point.from_xyz_attrs(direct_parent_offset.dropOffset)
1568
- + Point.from_xyz_attrs(
1569
- self._nominal_gripper_offsets_for_location(
1570
- location=ancestor
1571
- ).dropOffset
1572
- )
1573
- + extra_offset
1574
- )
1486
+ @staticmethod
1487
+ def ensure_valid_new_gripper_location(
1488
+ location: LabwareLocation,
1489
+ ) -> AccessibleByGripperLocation:
1490
+ """Ensure valid on-deck location for gripper, otherwise raise error."""
1491
+ if (
1492
+ not isinstance(
1493
+ location,
1494
+ (
1495
+ DeckSlotLocation,
1496
+ ModuleLocation,
1497
+ OnLabwareLocation,
1498
+ AddressableAreaLocation,
1499
+ ),
1500
+ )
1501
+ and location != WASTE_CHUTE_LOCATION
1502
+ ):
1503
+ raise errors.LabwareMovementNotAllowedError(
1504
+ "Off-deck labware movements are not supported using the gripper."
1505
+ )
1506
+ return location
1575
1507
 
1576
1508
  # todo(mm, 2024-11-05): This may be incorrect because it does not take the following
1577
1509
  # offsets into account, which *are* taken into account for the actual gripper movement:
@@ -1624,64 +1556,6 @@ class GeometryView:
1624
1556
  f"Cannot move labware '{labware_definition.parameters.loadName}' when {int(tip.volume)} µL tips are attached."
1625
1557
  )
1626
1558
 
1627
- def _nominal_gripper_offsets_for_location(
1628
- self, location: OnDeckLabwareLocation
1629
- ) -> LabwareMovementOffsetData:
1630
- """Provide the default gripper offset data for the given location type."""
1631
- if isinstance(location, (DeckSlotLocation, AddressableAreaLocation)):
1632
- # TODO we might need a separate type of gripper offset for addressable areas but that also might just
1633
- # be covered by the drop labware offset/location
1634
- offsets = self._labware.get_deck_default_gripper_offsets()
1635
- elif isinstance(location, ModuleLocation):
1636
- offsets = self._modules.get_default_gripper_offsets(location.moduleId)
1637
- else:
1638
- # Labware is on a labware/adapter
1639
- offsets = self._labware_gripper_offsets(location.labwareId)
1640
- return offsets or LabwareMovementOffsetData(
1641
- pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0),
1642
- dropOffset=LabwareOffsetVector(x=0, y=0, z=0),
1643
- )
1644
-
1645
- def _labware_gripper_offsets(
1646
- self, labware_id: str
1647
- ) -> Optional[LabwareMovementOffsetData]:
1648
- """Provide the most appropriate gripper offset data for the specified labware.
1649
-
1650
- We check the types of gripper offsets available for the labware ("default" or slot-based)
1651
- and return the most appropriate one for the overall location of the labware.
1652
- Currently, only module adapters (specifically, the H/S universal flat adapter)
1653
- have non-default offsets that are specific to location of the module on deck,
1654
- so, this code only checks for the presence of those known offsets.
1655
- """
1656
- parent_location = self._labware.get_parent_location(labware_id)
1657
- assert isinstance(
1658
- parent_location,
1659
- (
1660
- DeckSlotLocation,
1661
- ModuleLocation,
1662
- AddressableAreaLocation,
1663
- OnLabwareLocation,
1664
- ),
1665
- ), "No gripper offsets for off-deck labware"
1666
-
1667
- if isinstance(parent_location, DeckSlotLocation):
1668
- slot_name = parent_location.slotName
1669
- elif isinstance(parent_location, AddressableAreaLocation):
1670
- slot_name = self._addressable_areas.get_addressable_area_base_slot(
1671
- parent_location.addressableAreaName
1672
- )
1673
- else:
1674
- module_loc = self._modules.get_location(parent_location.moduleId)
1675
- slot_name = module_loc.slotName
1676
-
1677
- slot_based_offset = self._labware.get_child_gripper_offsets(
1678
- labware_id=labware_id, slot_name=slot_name.to_ot3_equivalent()
1679
- )
1680
-
1681
- return slot_based_offset or self._labware.get_child_gripper_offsets(
1682
- labware_id=labware_id, slot_name=None
1683
- )
1684
-
1685
1559
  def get_location_sequence(self, labware_id: str) -> LabwareLocationSequence:
1686
1560
  """Provide the LocationSequence specifying the current position of the labware.
1687
1561
 
@@ -1851,6 +1725,12 @@ class GeometryView:
1851
1725
  return self._recurse_labware_location_from_stacker_hopper(
1852
1726
  labware_location, building
1853
1727
  )
1728
+ elif labware_location == WASTE_CHUTE_LOCATION:
1729
+ return [
1730
+ NotOnDeckLocationSequenceComponent(
1731
+ logicalLocationName=WASTE_CHUTE_LOCATION
1732
+ )
1733
+ ]
1854
1734
  else:
1855
1735
  _LOG.warn(f"Unhandled labware location kind: {labware_location}")
1856
1736
  return building
@@ -2041,6 +1921,15 @@ class GeometryView:
2041
1921
  else:
2042
1922
  return initial_handling_height
2043
1923
 
1924
+ def well_has_tracked_liquid(
1925
+ self,
1926
+ labware_id: str,
1927
+ well_name: str,
1928
+ ) -> bool:
1929
+ """Returns true if this well has had a liquid loaded or a probe result."""
1930
+ last_updated = self._wells.get_last_liquid_update(labware_id, well_name)
1931
+ return last_updated is not None
1932
+
2044
1933
  def get_current_well_volume(
2045
1934
  self,
2046
1935
  labware_id: str,
@@ -2288,7 +2177,8 @@ class GeometryView:
2288
2177
  except InvalidLiquidHeightFound as _exception:
2289
2178
  raise InvalidLiquidHeightFound(
2290
2179
  message=_exception.message
2291
- + f"for well {well_name} of {self._labware.get_display_name(labware_id)} on slot {self.get_ancestor_slot_name(labware_id)}"
2180
+ + f"for well {well_name} of {self._labware.get_display_name(labware_id)}"
2181
+ f" on slot {self.get_ancestor_slot_name(labware_id)}"
2292
2182
  )
2293
2183
  # if meniscus volume is a simulated value, comparisons aren't meaningful
2294
2184
  if isinstance(meniscus_volume, SimulatedProbeResult):
@@ -2296,13 +2186,16 @@ class GeometryView:
2296
2186
  remaining_volume = well_volumetric_capacity - meniscus_volume
2297
2187
  if volume > remaining_volume:
2298
2188
  raise errors.InvalidDispenseVolumeError(
2299
- f"Attempting to dispense {volume}µL of liquid into a well that can currently only hold {remaining_volume}µL (well {well_name} in labware_id: {labware_id})"
2189
+ f"Attempting to dispense {volume}µL of liquid into a well that can currently only hold"
2190
+ f" {remaining_volume}µL (well {well_name} in labware {self._labware.get_display_name(labware_id)})"
2300
2191
  )
2301
2192
  else:
2302
2193
  # TODO(pbm, 10-08-24): factor in well (LabwareStore) state volume
2303
2194
  if volume > well_volumetric_capacity:
2304
2195
  raise errors.InvalidDispenseVolumeError(
2305
- f"Attempting to dispense {volume}µL of liquid into a well that can only hold {well_volumetric_capacity}µL (well {well_name} in labware_id: {labware_id})"
2196
+ f"Attempting to dispense {volume}µL of liquid into a well that can only hold"
2197
+ f" {well_volumetric_capacity}µL (well {well_name} in"
2198
+ f" labware {self._labware.get_display_name(labware_id)})"
2306
2199
  )
2307
2200
 
2308
2201
  def get_wells_covered_by_pipette_with_active_well(
@@ -2423,6 +2316,10 @@ class GeometryView:
2423
2316
  raise errors.LocationNotAccessibleByPipetteError(
2424
2317
  f"Cannot move pipette to {labware.loadName}, labware is off-deck."
2425
2318
  )
2319
+ elif labware_location == WASTE_CHUTE_LOCATION:
2320
+ raise errors.LocationNotAccessibleByPipetteError(
2321
+ f"Cannot move pipette to {labware.loadName}, labware is in waste chute."
2322
+ )
2426
2323
  elif isinstance(labware_location, ModuleLocation):
2427
2324
  module = self._modules.get(labware_location.moduleId)
2428
2325
  if ModuleModel.is_flex_stacker(module.model):