opentrons 8.7.0a0__py3-none-any.whl → 8.7.0a2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of opentrons might be problematic. Click here for more details.

Files changed (121) hide show
  1. opentrons/_version.py +2 -2
  2. opentrons/drivers/thermocycler/abstract.py +1 -0
  3. opentrons/drivers/thermocycler/driver.py +33 -4
  4. opentrons/drivers/thermocycler/simulator.py +2 -0
  5. opentrons/hardware_control/api.py +24 -5
  6. opentrons/hardware_control/backends/controller.py +8 -2
  7. opentrons/hardware_control/backends/ot3controller.py +3 -0
  8. opentrons/hardware_control/backends/ot3simulator.py +2 -1
  9. opentrons/hardware_control/backends/simulator.py +2 -1
  10. opentrons/hardware_control/backends/subsystem_manager.py +5 -2
  11. opentrons/hardware_control/module_control.py +82 -8
  12. opentrons/hardware_control/modules/__init__.py +3 -0
  13. opentrons/hardware_control/modules/absorbance_reader.py +11 -4
  14. opentrons/hardware_control/modules/flex_stacker.py +38 -9
  15. opentrons/hardware_control/modules/heater_shaker.py +30 -5
  16. opentrons/hardware_control/modules/magdeck.py +8 -4
  17. opentrons/hardware_control/modules/mod_abc.py +13 -5
  18. opentrons/hardware_control/modules/tempdeck.py +25 -5
  19. opentrons/hardware_control/modules/thermocycler.py +56 -10
  20. opentrons/hardware_control/modules/types.py +20 -1
  21. opentrons/hardware_control/modules/utils.py +11 -4
  22. opentrons/hardware_control/nozzle_manager.py +3 -0
  23. opentrons/hardware_control/ot3api.py +26 -5
  24. opentrons/hardware_control/scripts/update_module_fw.py +5 -0
  25. opentrons/hardware_control/types.py +31 -2
  26. opentrons/legacy_commands/protocol_commands.py +20 -0
  27. opentrons/legacy_commands/types.py +42 -0
  28. opentrons/motion_planning/waypoints.py +15 -29
  29. opentrons/protocol_api/__init__.py +5 -0
  30. opentrons/protocol_api/_types.py +6 -1
  31. opentrons/protocol_api/core/common.py +3 -1
  32. opentrons/protocol_api/core/engine/_default_labware_versions.py +32 -11
  33. opentrons/protocol_api/core/engine/_default_liquid_class_versions.py +2 -0
  34. opentrons/protocol_api/core/engine/labware.py +8 -1
  35. opentrons/protocol_api/core/engine/module_core.py +4 -0
  36. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +77 -17
  37. opentrons/protocol_api/core/engine/protocol.py +18 -1
  38. opentrons/protocol_api/core/engine/tasks.py +35 -0
  39. opentrons/protocol_api/core/legacy/legacy_module_core.py +2 -0
  40. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +11 -1
  41. opentrons/protocol_api/core/legacy/tasks.py +19 -0
  42. opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +14 -2
  43. opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
  44. opentrons/protocol_api/core/module.py +1 -0
  45. opentrons/protocol_api/core/protocol.py +11 -2
  46. opentrons/protocol_api/core/tasks.py +31 -0
  47. opentrons/protocol_api/module_contexts.py +1 -0
  48. opentrons/protocol_api/protocol_context.py +26 -4
  49. opentrons/protocol_api/robot_context.py +38 -21
  50. opentrons/protocol_api/tasks.py +48 -0
  51. opentrons/protocol_api/validation.py +6 -1
  52. opentrons/protocol_engine/actions/__init__.py +4 -2
  53. opentrons/protocol_engine/actions/actions.py +22 -9
  54. opentrons/protocol_engine/clients/sync_client.py +6 -7
  55. opentrons/protocol_engine/commands/__init__.py +42 -0
  56. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +2 -15
  57. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +2 -15
  58. opentrons/protocol_engine/commands/aspirate.py +1 -0
  59. opentrons/protocol_engine/commands/command.py +1 -0
  60. opentrons/protocol_engine/commands/command_unions.py +39 -0
  61. opentrons/protocol_engine/commands/create_timer.py +83 -0
  62. opentrons/protocol_engine/commands/dispense.py +1 -0
  63. opentrons/protocol_engine/commands/drop_tip.py +32 -8
  64. opentrons/protocol_engine/commands/movement_common.py +2 -0
  65. opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
  66. opentrons/protocol_engine/commands/set_tip_state.py +97 -0
  67. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +6 -0
  68. opentrons/protocol_engine/commands/thermocycler/run_profile.py +8 -0
  69. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +17 -1
  70. opentrons/protocol_engine/commands/touch_tip.py +1 -1
  71. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +6 -22
  72. opentrons/protocol_engine/commands/wait_for_tasks.py +98 -0
  73. opentrons/protocol_engine/errors/__init__.py +4 -0
  74. opentrons/protocol_engine/errors/exceptions.py +55 -0
  75. opentrons/protocol_engine/execution/__init__.py +2 -0
  76. opentrons/protocol_engine/execution/command_executor.py +8 -0
  77. opentrons/protocol_engine/execution/create_queue_worker.py +5 -1
  78. opentrons/protocol_engine/execution/labware_movement.py +9 -12
  79. opentrons/protocol_engine/execution/movement.py +2 -0
  80. opentrons/protocol_engine/execution/queue_worker.py +4 -0
  81. opentrons/protocol_engine/execution/run_control.py +8 -0
  82. opentrons/protocol_engine/execution/task_handler.py +157 -0
  83. opentrons/protocol_engine/protocol_engine.py +67 -33
  84. opentrons/protocol_engine/resources/__init__.py +2 -0
  85. opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
  86. opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
  87. opentrons/protocol_engine/resources/labware_validation.py +10 -6
  88. opentrons/protocol_engine/state/_well_math.py +60 -18
  89. opentrons/protocol_engine/state/addressable_areas.py +2 -0
  90. opentrons/protocol_engine/state/commands.py +7 -7
  91. opentrons/protocol_engine/state/geometry.py +237 -379
  92. opentrons/protocol_engine/state/labware.py +52 -102
  93. opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
  94. opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1331 -0
  95. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
  96. opentrons/protocol_engine/state/modules.py +26 -7
  97. opentrons/protocol_engine/state/motion.py +44 -0
  98. opentrons/protocol_engine/state/state.py +14 -0
  99. opentrons/protocol_engine/state/state_summary.py +2 -0
  100. opentrons/protocol_engine/state/tasks.py +139 -0
  101. opentrons/protocol_engine/state/tips.py +177 -258
  102. opentrons/protocol_engine/state/update_types.py +16 -9
  103. opentrons/protocol_engine/types/__init__.py +9 -3
  104. opentrons/protocol_engine/types/deck_configuration.py +5 -1
  105. opentrons/protocol_engine/types/instrument.py +8 -1
  106. opentrons/protocol_engine/types/labware.py +1 -13
  107. opentrons/protocol_engine/types/module.py +10 -0
  108. opentrons/protocol_engine/types/tasks.py +38 -0
  109. opentrons/protocol_engine/types/tip.py +9 -0
  110. opentrons/protocol_runner/create_simulating_orchestrator.py +29 -2
  111. opentrons/protocol_runner/run_orchestrator.py +18 -2
  112. opentrons/protocols/api_support/definitions.py +1 -1
  113. opentrons/protocols/api_support/types.py +2 -1
  114. opentrons/simulate.py +48 -15
  115. opentrons/system/camera.py +1 -1
  116. {opentrons-8.7.0a0.dist-info → opentrons-8.7.0a2.dist-info}/METADATA +4 -4
  117. {opentrons-8.7.0a0.dist-info → opentrons-8.7.0a2.dist-info}/RECORD +120 -107
  118. opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
  119. {opentrons-8.7.0a0.dist-info → opentrons-8.7.0a2.dist-info}/WHEEL +0 -0
  120. {opentrons-8.7.0a0.dist-info → opentrons-8.7.0a2.dist-info}/entry_points.txt +0 -0
  121. {opentrons-8.7.0a0.dist-info → opentrons-8.7.0a2.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,12 +63,10 @@ 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,
73
71
  OnDeckLabwareLocation,
74
72
  AddressableAreaLocation,
@@ -91,7 +89,7 @@ from ..types import (
91
89
  labware_location_is_system,
92
90
  WellLocationType,
93
91
  WellLocationFunction,
94
- LabwareParentDefinition,
92
+ GripperMoveType,
95
93
  AddressableArea,
96
94
  )
97
95
  from ..types.liquid_level_detection import SimulatedProbeResult, LiquidTrackingType
@@ -108,8 +106,11 @@ from .inner_well_math_utils import (
108
106
  find_volume_user_defined_volumes,
109
107
  )
110
108
  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
-
109
+ from .labware_origin_math.stackup_origin_to_labware_origin import (
110
+ get_stackup_origin_to_labware_origin,
111
+ LabwareOriginContext,
112
+ LabwareStackupAncestorDefinition,
113
+ )
113
114
 
114
115
  _LOG = getLogger(__name__)
115
116
  SLOT_WIDTH = 128
@@ -125,13 +126,6 @@ class _TipDropSection(enum.Enum):
125
126
  RIGHT = "right"
126
127
 
127
128
 
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
129
  @dataclass
136
130
  class _AbsoluteRobotExtents:
137
131
  front_left: Dict[MountType, Point]
@@ -274,12 +268,20 @@ class GeometryView:
274
268
  try:
275
269
  labware_id = self._labware.get_id_by_module(module_id=module_id)
276
270
  except LabwareNotLoadedOnModuleError:
277
- return self._modules.get_module_highest_z(
278
- module_id=module_id,
279
- addressable_areas=self._addressable_areas,
280
- )
271
+ # For the time being we will ignore column 4 modules in this check to avoid conflating results
272
+ if self._modules.is_column_4_module(slot_item.model) is False:
273
+ return self._modules.get_module_highest_z(
274
+ module_id=module_id,
275
+ addressable_areas=self._addressable_areas,
276
+ )
281
277
  else:
282
- return self.get_highest_z_of_labware_stack(labware_id)
278
+ # For the time being we will ignore column 4 modules in this check to avoid conflating results
279
+ if self._modules.is_column_4_module(slot_item.model) is False:
280
+ return self.get_highest_z_of_labware_stack(labware_id)
281
+ # todo (cb, 2025-09-15): For now we skip column 4 modules and handle them seperately in
282
+ # get_highest_z_of_column_4_module, so this will return 0. In the future we may want to consolidate
283
+ # this to make it more apparently at this point in the query process.
284
+ return 0
283
285
  elif isinstance(slot_item, LoadedLabware):
284
286
  # get stacked heights of all labware in the slot
285
287
  return self.get_highest_z_of_labware_stack(slot_item.id)
@@ -301,6 +303,26 @@ class GeometryView:
301
303
  return self.get_labware_highest_z(labware_id)
302
304
  return self.get_highest_z_of_labware_stack(stacked_labware_id)
303
305
 
306
+ def get_highest_z_of_column_4_module(self, module: LoadedModule) -> float:
307
+ """Get the highest Z-point of the topmost labware in the stack of labware on the given column 4 module.
308
+
309
+ If there is no labware on the given module, returns highest z of the module.
310
+ """
311
+ if self._modules.is_column_4_module(module.model):
312
+ try:
313
+ labware_id = self._labware.get_id_by_module(module_id=module.id)
314
+ except LabwareNotLoadedOnModuleError:
315
+ return self._modules.get_module_highest_z(
316
+ module_id=module.id,
317
+ addressable_areas=self._addressable_areas,
318
+ )
319
+ else:
320
+ return self.get_highest_z_of_labware_stack(labware_id)
321
+ else:
322
+ raise ValueError(
323
+ "Module must be a Column 4 Module to determine maximum z height."
324
+ )
325
+
304
326
  def get_min_travel_z(
305
327
  self,
306
328
  pipette_id: str,
@@ -382,145 +404,114 @@ class GeometryView:
382
404
  """
383
405
  location = self._labware.get(labware_id).location
384
406
  definition = self._labware.get_definition(labware_id)
407
+ aa_name = self._get_underlying_addressable_area_name(location)
408
+ # TODO(jh, 08-18-25): Labware locations return the underlying slot as the "on location" for the fixed trash,
409
+ # but the underlying slot's name does not exist in addressable area state. Getting the addressable area from data is
410
+ # a workaround. Investigate further.
411
+ addressable_area = self._addressable_areas._get_addressable_area_from_deck_data(
412
+ aa_name, do_compatibility_check=False
413
+ )
414
+ stackup_lw_defs_locs = self._get_stackup_lw_info_top_to_bottom(
415
+ labware_definition=definition, location=location
416
+ )
417
+ underlying_ancestor_def = self._get_stackup_underlying_ancestor_definition(
418
+ location
419
+ )
420
+ module_parent_to_child_offset = self._get_stackup_module_parent_to_child_offset(
421
+ location
422
+ )
385
423
 
386
- slot_front_left = self._get_labware_ancestor_position(labware_id)
387
- stackup_origin_to_lw_origin = self._get_stackup_placement_origin_to_lw_origin(
388
- location=location, definition=definition, is_topmost_labware=True
424
+ slot_front_left = self._addressable_areas.get_addressable_area_position(aa_name)
425
+ stackup_origin_to_lw_origin = get_stackup_origin_to_labware_origin(
426
+ context=LabwareOriginContext.PIPETTING,
427
+ stackup_lw_info_top_to_bottom=stackup_lw_defs_locs,
428
+ underlying_ancestor_definition=underlying_ancestor_def,
429
+ module_parent_to_child_offset=module_parent_to_child_offset,
430
+ deck_definition=self._addressable_areas.deck_definition,
431
+ slot_name=addressable_area.base_slot,
389
432
  )
390
433
  module_cal_offset = self._get_calibrated_module_offset(location)
391
434
 
392
435
  return slot_front_left + stackup_origin_to_lw_origin + module_cal_offset
393
436
 
394
- def _get_labware_ancestor_position(self, labware_id: str) -> Point:
395
- """Get the position of the labware's underlying ancestor."""
396
- slot_name = self._get_underlying_addressable_area_name(
397
- self._labware.get(labware_id).location
398
- )
399
- parent_pos = self._addressable_areas.get_addressable_area_position(slot_name)
400
-
401
- return parent_pos
402
-
403
- def _get_stackup_placement_origin_to_lw_origin(
404
- self,
405
- location: LabwareLocation,
406
- definition: LabwareDefinition,
407
- is_topmost_labware: bool,
408
- ) -> Point:
409
- """Get the offset vector from the lowest entity in a stackup to the labware."""
410
- if isinstance(
411
- location, (AddressableAreaLocation, DeckSlotLocation, ModuleLocation)
412
- ):
413
- return self._get_parent_placement_origin_to_lw_origin(
414
- labware_location=location,
415
- labware_definition=definition,
416
- is_topmost_labware=is_topmost_labware,
417
- )
418
- elif isinstance(location, OnLabwareLocation):
419
- parent_id = location.labwareId
420
- parent_location = self._labware.get(parent_id).location
421
- parent_definition = self._labware.get_definition(parent_id)
422
-
423
- parent_placement_origin_to_lw_origin = (
424
- self._get_parent_placement_origin_to_lw_origin(
425
- labware_location=location,
426
- labware_definition=definition,
427
- is_topmost_labware=is_topmost_labware,
428
- )
429
- )
430
-
431
- return (
432
- parent_placement_origin_to_lw_origin
433
- + self._get_stackup_placement_origin_to_lw_origin(
434
- location=parent_location,
435
- definition=parent_definition,
436
- is_topmost_labware=False,
437
- )
438
- )
439
- else:
440
- raise errors.LabwareNotOnDeckError(
441
- "Cannot access labware since it is not on the deck. "
442
- "Either it has been loaded off-deck or its been moved off-deck."
443
- )
437
+ def _get_stackup_lw_info_top_to_bottom(
438
+ self, labware_definition: LabwareDefinition, location: LabwareLocation
439
+ ) -> list[tuple[LabwareDefinition, LabwareLocation]]:
440
+ """Returns info about each labware in the stackup.
444
441
 
445
- def _get_parent_placement_origin_to_lw_origin(
446
- self,
447
- labware_location: LabwareLocation,
448
- labware_definition: LabwareDefinition,
449
- is_topmost_labware: bool,
450
- ) -> Point:
451
- parent_deck_item = self._get_parent_definition(labware_location)
442
+ The list is ordered from the top labware to the bottom-most labware.
443
+ The first entry will always be the definition and location of the given labware itself.
444
+ """
445
+ definitions_locations_top_to_bottom: list[
446
+ tuple[LabwareDefinition, LabwareLocation]
447
+ ] = []
448
+ current_location = location
449
+ current_definition = labware_definition
452
450
 
453
- if isinstance(labware_location, ModuleLocation):
454
- module_parent_to_child_offset = (
455
- self._modules.get_nominal_offset_to_child_from_addressable_area(
456
- module_id=labware_location.moduleId,
457
- )
458
- )
459
- return get_parent_placement_origin_to_lw_origin(
460
- child_labware=labware_definition,
461
- parent_deck_item=parent_deck_item, # type: ignore[arg-type]
462
- module_parent_to_child_offset=module_parent_to_child_offset,
463
- deck_definition=self._addressable_areas.deck_definition,
464
- is_topmost_labware=is_topmost_labware,
465
- labware_location=labware_location,
466
- )
467
- elif isinstance(labware_location, OnLabwareLocation):
468
- return get_parent_placement_origin_to_lw_origin(
469
- child_labware=labware_definition,
470
- parent_deck_item=parent_deck_item, # type: ignore[arg-type]
471
- module_parent_to_child_offset=None,
472
- deck_definition=self._addressable_areas.deck_definition,
473
- is_topmost_labware=is_topmost_labware,
474
- labware_location=labware_location,
475
- )
476
- elif isinstance(labware_location, (DeckSlotLocation, AddressableAreaLocation)):
477
- return get_parent_placement_origin_to_lw_origin(
478
- child_labware=labware_definition,
479
- parent_deck_item=parent_deck_item, # type: ignore[arg-type]
480
- module_parent_to_child_offset=None,
481
- deck_definition=self._addressable_areas.deck_definition,
482
- is_topmost_labware=is_topmost_labware,
483
- labware_location=labware_location,
451
+ while True:
452
+ definitions_locations_top_to_bottom.append(
453
+ (current_definition, current_location)
484
454
  )
485
- else:
486
- raise ValueError(f"Invalid labware location: {labware_location}")
487
455
 
488
- def _get_parent_definition(
489
- self, location: LabwareLocation
490
- ) -> LabwareParentDefinition:
491
- """Get the parent's definition given the labware's location."""
492
- if isinstance(location, DeckSlotLocation):
493
- addressable_area_name = location.slotName.id
494
- return self._addressable_areas.get_slot_definition(addressable_area_name)
456
+ if isinstance(current_location, OnLabwareLocation):
457
+ current_labware_id = current_location.labwareId
458
+ current_location = self._labware.get(current_labware_id).location
459
+ current_definition = self._labware.get_definition(current_labware_id)
460
+ else:
461
+ break
495
462
 
496
- elif isinstance(location, AddressableAreaLocation):
497
- addressable_area_name = location.addressableAreaName
498
- return self._addressable_areas.get_addressable_area(addressable_area_name)
463
+ return definitions_locations_top_to_bottom
499
464
 
500
- elif isinstance(location, ModuleLocation):
501
- module_id = location.moduleId
502
- return self._modules.get_definition(module_id)
465
+ def _get_stackup_module_parent_to_child_offset(
466
+ self, top_most_lw_location: LabwareLocation
467
+ ) -> Union[Point, None]:
468
+ """Traverse the stackup to find the first parent-to-child module offset, if any."""
469
+ current_location = top_most_lw_location
503
470
 
504
- elif isinstance(location, OnLabwareLocation):
505
- below_labware_id = location.labwareId
506
- return self._labware.get_definition(below_labware_id)
471
+ while True:
472
+ if isinstance(current_location, ModuleLocation):
473
+ module_parent_to_child_offset = (
474
+ self._modules.get_nominal_offset_to_child_from_addressable_area(
475
+ module_id=current_location.moduleId,
476
+ )
477
+ )
478
+ return module_parent_to_child_offset
507
479
 
508
- elif location == OFF_DECK_LOCATION or location == SYSTEM_LOCATION:
509
- raise errors.LabwareNotOnDeckError(
510
- f"Labware location {location} does not have a slot associated with it"
511
- f" since it is no longer on the deck."
512
- )
480
+ if isinstance(current_location, OnLabwareLocation):
481
+ current_labware_id = current_location.labwareId
482
+ current_labware = self._labware.get(current_labware_id)
483
+ current_location = current_labware.location
484
+ else:
485
+ break
513
486
 
514
- elif isinstance(location, InStackerHopperLocation):
515
- raise errors.LabwareNotOnDeckError(
516
- "Labware does not have a slot or module associated with it"
517
- " since it is no longer on the deck."
518
- )
487
+ return None
519
488
 
520
- else:
521
- raise errors.InvalidLabwarePositionError(
522
- f"Cannot get ancestor from location {location}"
523
- )
489
+ def _get_stackup_underlying_ancestor_definition(
490
+ self, top_most_lw_location: LabwareLocation
491
+ ) -> LabwareStackupAncestorDefinition:
492
+ """Traverse the stackup to find the first non-labware definition."""
493
+ current_location = top_most_lw_location
494
+
495
+ while True:
496
+ if isinstance(current_location, OnLabwareLocation):
497
+ current_labware_id = current_location.labwareId
498
+ current_labware = self._labware.get(current_labware_id)
499
+ current_location = current_labware.location
500
+ else:
501
+ if isinstance(current_location, ModuleLocation):
502
+ return self._modules.get_definition(current_location.moduleId)
503
+ elif isinstance(current_location, AddressableAreaLocation):
504
+ return self._addressable_areas.get_addressable_area(
505
+ current_location.addressableAreaName
506
+ )
507
+ elif isinstance(current_location, DeckSlotLocation):
508
+ return self._addressable_areas.get_slot_definition(
509
+ current_location.slotName.id
510
+ )
511
+ else:
512
+ raise errors.InvalidLabwarePositionError(
513
+ f"Cannot get ancestor slot of location {current_location}"
514
+ )
524
515
 
525
516
  def _get_underlying_addressable_area_name(self, location: LabwareLocation) -> str:
526
517
  if isinstance(location, DeckSlotLocation):
@@ -748,7 +739,7 @@ class GeometryView:
748
739
 
749
740
  if well_def.shape != "circular":
750
741
  raise errors.LabwareIsNotTipRackError(
751
- f"Well {well_name} in labware {labware_id} is not circular."
742
+ f"Well {well_name} in labware {self._labware.get_display_name(labware_id)} is not circular."
752
743
  )
753
744
 
754
745
  return TipGeometry(
@@ -839,7 +830,7 @@ class GeometryView:
839
830
  slot_name = DeckSlotName.from_primitive(area_name)
840
831
  elif labware.location == OFF_DECK_LOCATION:
841
832
  raise errors.LabwareNotOnDeckError(
842
- f"Labware {labware_id} does not have a slot associated with it"
833
+ f"Labware {self._labware.get_display_name(labware_id)} does not have a slot associated with it"
843
834
  f" since it is no longer on the deck."
844
835
  )
845
836
  else:
@@ -1030,6 +1021,8 @@ class GeometryView:
1030
1021
  location: Union[
1031
1022
  DeckSlotLocation, ModuleLocation, OnLabwareLocation, AddressableAreaLocation
1032
1023
  ],
1024
+ move_type: GripperMoveType,
1025
+ user_additional_offset: Point | None,
1033
1026
  ) -> Point:
1034
1027
  """Get the grip point of the labware as placed on the given location.
1035
1028
 
@@ -1041,28 +1034,107 @@ class GeometryView:
1041
1034
  It is calculated as the xy center of the slot with z as the point indicated by
1042
1035
  z-position of labware bottom + grip height from labware bottom.
1043
1036
  """
1044
- grip_z_from_lw_origin = self._labware.get_grip_z(labware_definition)
1045
- aa_name = self._get_underlying_addressable_area_name(location)
1046
- parent_to_lw_offset = self._get_stackup_placement_origin_to_lw_origin(
1037
+ mod_cal_offset = self._get_calibrated_module_offset(location)
1038
+ user_additional_offset = user_additional_offset or Point()
1039
+ aa_origin_to_nominal_grip_point = self._get_aa_origin_to_nominal_grip_point(
1040
+ labware_definition=labware_definition,
1047
1041
  location=location,
1048
- definition=labware_definition,
1049
- is_topmost_labware=True, # We aren't concerned with entities above the gripped labware.
1042
+ move_type=move_type,
1050
1043
  )
1044
+
1045
+ return aa_origin_to_nominal_grip_point + mod_cal_offset + user_additional_offset
1046
+
1047
+ def _get_aa_origin_to_nominal_grip_point(
1048
+ self,
1049
+ labware_definition: LabwareDefinition,
1050
+ location: Union[
1051
+ DeckSlotLocation, ModuleLocation, OnLabwareLocation, AddressableAreaLocation
1052
+ ],
1053
+ move_type: GripperMoveType,
1054
+ ) -> Point:
1055
+ """Get the nominal grip point of a labware.
1056
+
1057
+ Does not include module calibration offsets or user additional offsets.
1058
+ """
1059
+ grip_z_from_lw_origin = self._labware.get_grip_z(labware_definition)
1060
+ aa_name = self._get_underlying_addressable_area_name(location)
1051
1061
  addressable_area = self._addressable_areas.get_addressable_area(aa_name)
1052
- lw_origin_to_parent = self._get_lw_origin_to_parent(
1053
- labware_definition=labware_definition, addressable_area=addressable_area
1062
+ stackup_defs_locs = self._get_stackup_lw_info_top_to_bottom(
1063
+ labware_definition=labware_definition, location=location
1064
+ )
1065
+ module_parent_to_child_offset = self._get_stackup_module_parent_to_child_offset(
1066
+ location
1067
+ )
1068
+ underlying_ancestor_def = self._get_stackup_underlying_ancestor_definition(
1069
+ location
1070
+ )
1071
+ context_type = (
1072
+ LabwareOriginContext.GRIPPER_PICKING_UP
1073
+ if move_type == GripperMoveType.PICK_UP_LABWARE
1074
+ else LabwareOriginContext.GRIPPER_DROPPING
1054
1075
  )
1055
- mod_cal_offset = self._get_calibrated_module_offset(location)
1056
- location_center = self._addressable_areas.get_addressable_area_center(aa_name)
1057
1076
 
1058
- return (
1059
- location_center
1060
- + parent_to_lw_offset
1061
- + lw_origin_to_parent
1062
- + mod_cal_offset
1063
- + Point(0, 0, grip_z_from_lw_origin)
1077
+ aa_origin_to_lw_origin = get_stackup_origin_to_labware_origin(
1078
+ context=context_type,
1079
+ module_parent_to_child_offset=module_parent_to_child_offset,
1080
+ underlying_ancestor_definition=underlying_ancestor_def,
1081
+ stackup_lw_info_top_to_bottom=stackup_defs_locs,
1082
+ slot_name=addressable_area.base_slot,
1083
+ deck_definition=self._addressable_areas.deck_definition,
1064
1084
  )
1065
1085
 
1086
+ if isinstance(labware_definition, LabwareDefinition2):
1087
+ lw_origin_to_aa_origin = self._get_lw_origin_to_parent(
1088
+ labware_definition=labware_definition, addressable_area=addressable_area
1089
+ )
1090
+ aa_origin_to_aa_center = (
1091
+ self._addressable_areas.get_addressable_area_center(aa_name)
1092
+ )
1093
+ aa_center_to_nominal_grip_point = Point(0, 0, grip_z_from_lw_origin)
1094
+
1095
+ return (
1096
+ aa_origin_to_lw_origin
1097
+ + lw_origin_to_aa_origin
1098
+ + aa_origin_to_aa_center
1099
+ + aa_center_to_nominal_grip_point
1100
+ )
1101
+
1102
+ else:
1103
+ assert isinstance(labware_definition, LabwareDefinition3)
1104
+
1105
+ aa_origin = self._addressable_areas.get_addressable_area_position(aa_name)
1106
+ lw_origin_to_lw_center = self._get_lw_origin_to_lw_center(
1107
+ labware_definition
1108
+ )
1109
+ lw_origin_to_lw_grip_center = Point(
1110
+ x=lw_origin_to_lw_center.x,
1111
+ y=lw_origin_to_lw_center.y,
1112
+ z=grip_z_from_lw_origin,
1113
+ )
1114
+
1115
+ return aa_origin + aa_origin_to_lw_origin + lw_origin_to_lw_grip_center
1116
+
1117
+ def _get_lw_origin_to_lw_center(
1118
+ self, labware_definition: LabwareDefinition
1119
+ ) -> Point:
1120
+ """Get the x,y,z center of the labware."""
1121
+ if isinstance(labware_definition, LabwareDefinition2):
1122
+ dimensions = labware_definition.dimensions
1123
+ x = dimensions.xDimension / 2
1124
+ y = dimensions.yDimension / 2
1125
+ z = dimensions.zDimension / 2
1126
+
1127
+ return Point(x, y, z)
1128
+ else:
1129
+ front_right_top = labware_definition.extents.total.frontRightTop
1130
+ back_left_bottom = labware_definition.extents.total.backLeftBottom
1131
+
1132
+ x = (front_right_top.x - back_left_bottom.x) / 2
1133
+ y = (front_right_top.y - back_left_bottom.y) / 2
1134
+ z = (front_right_top.z - back_left_bottom.z) / 2
1135
+
1136
+ return Point(x, y, z)
1137
+
1066
1138
  def _get_lw_origin_to_parent(
1067
1139
  self, labware_definition: LabwareDefinition, addressable_area: AddressableArea
1068
1140
  ) -> Point:
@@ -1096,7 +1168,6 @@ class GeometryView:
1096
1168
  if self._modules.should_dodge_thermocycler(
1097
1169
  from_slot=from_slot, to_slot=to_slot
1098
1170
  ):
1099
-
1100
1171
  middle_slot_fixture = (
1101
1172
  self._addressable_areas.get_fixture_by_deck_slot_name(
1102
1173
  DeckSlotName.SLOT_C2.to_equivalent_for_robot_type(
@@ -1365,41 +1436,6 @@ class GeometryView:
1365
1436
  x_well_offset = 0
1366
1437
  return x_well_offset
1367
1438
 
1368
- def get_final_labware_movement_offset_vectors(
1369
- self,
1370
- from_location: OnDeckLabwareLocation,
1371
- to_location: OnDeckLabwareLocation,
1372
- additional_pick_up_offset: Point,
1373
- additional_drop_offset: Point,
1374
- current_labware: LabwareDefinition,
1375
- ) -> LabwareMovementOffsetData:
1376
- """Calculate the final labware offset vector to use in labware movement."""
1377
- pick_up_offset = (
1378
- self.get_total_nominal_gripper_offset_for_move_type(
1379
- location=from_location,
1380
- move_type=_GripperMoveType.PICK_UP_LABWARE,
1381
- current_labware=current_labware,
1382
- )
1383
- + additional_pick_up_offset
1384
- )
1385
- drop_offset = (
1386
- self.get_total_nominal_gripper_offset_for_move_type(
1387
- location=to_location,
1388
- move_type=_GripperMoveType.DROP_LABWARE,
1389
- current_labware=current_labware,
1390
- )
1391
- + additional_drop_offset
1392
- )
1393
-
1394
- return LabwareMovementOffsetData(
1395
- pickUpOffset=LabwareOffsetVector(
1396
- x=pick_up_offset.x, y=pick_up_offset.y, z=pick_up_offset.z
1397
- ),
1398
- dropOffset=LabwareOffsetVector(
1399
- x=drop_offset.x, y=drop_offset.y, z=drop_offset.z
1400
- ),
1401
- )
1402
-
1403
1439
  @staticmethod
1404
1440
  def ensure_valid_gripper_location(
1405
1441
  location: LabwareLocation,
@@ -1421,130 +1457,6 @@ class GeometryView:
1421
1457
  )
1422
1458
  return location
1423
1459
 
1424
- def get_total_nominal_gripper_offset_for_move_type(
1425
- self,
1426
- location: OnDeckLabwareLocation,
1427
- move_type: _GripperMoveType,
1428
- current_labware: LabwareDefinition,
1429
- ) -> Point:
1430
- """Get the total of the offsets to be used to pick up labware in its current location."""
1431
- if move_type == _GripperMoveType.PICK_UP_LABWARE:
1432
- if isinstance(
1433
- location, (ModuleLocation, DeckSlotLocation, AddressableAreaLocation)
1434
- ):
1435
- return Point.from_xyz_attrs(
1436
- self._nominal_gripper_offsets_for_location(location).pickUpOffset
1437
- )
1438
- else:
1439
- # If it's a labware on a labware (most likely an adapter),
1440
- # we calculate the offset as sum of offsets for the direct parent labware
1441
- # and the underlying non-labware parent location.
1442
- direct_parent_offset = self._nominal_gripper_offsets_for_location(
1443
- location
1444
- )
1445
- ancestor = self._labware.get_parent_location(location.labwareId)
1446
- extra_offset = Point(x=0, y=0, z=0)
1447
- if (
1448
- isinstance(ancestor, ModuleLocation)
1449
- # todo(mm, 2025-06-20): Avoid this private attribute access.
1450
- and self._modules._state.requested_model_by_id[ancestor.moduleId]
1451
- == ModuleModel.THERMOCYCLER_MODULE_V2
1452
- and labware_validation.validate_definition_is_lid(current_labware)
1453
- ):
1454
- if "lidOffsets" in current_labware.gripperOffsets.keys():
1455
- extra_offset = Point(
1456
- x=current_labware.gripperOffsets[
1457
- "lidOffsets"
1458
- ].pickUpOffset.x,
1459
- y=current_labware.gripperOffsets[
1460
- "lidOffsets"
1461
- ].pickUpOffset.y,
1462
- z=current_labware.gripperOffsets[
1463
- "lidOffsets"
1464
- ].pickUpOffset.z,
1465
- )
1466
- else:
1467
- raise errors.LabwareOffsetDoesNotExistError(
1468
- f"Labware Definition {current_labware.parameters.loadName} does not contain required field 'lidOffsets' of 'gripperOffsets'."
1469
- )
1470
-
1471
- assert isinstance(
1472
- ancestor,
1473
- (
1474
- DeckSlotLocation,
1475
- ModuleLocation,
1476
- OnLabwareLocation,
1477
- AddressableAreaLocation,
1478
- ),
1479
- ), "No gripper offsets for off-deck labware"
1480
- return (
1481
- Point.from_xyz_attrs(direct_parent_offset.pickUpOffset)
1482
- + Point.from_xyz_attrs(
1483
- self._nominal_gripper_offsets_for_location(
1484
- location=ancestor
1485
- ).pickUpOffset
1486
- )
1487
- + extra_offset
1488
- )
1489
- else:
1490
- if isinstance(
1491
- location, (ModuleLocation, DeckSlotLocation, AddressableAreaLocation)
1492
- ):
1493
- return Point.from_xyz_attrs(
1494
- self._nominal_gripper_offsets_for_location(location).dropOffset
1495
- )
1496
- else:
1497
- # If it's a labware on a labware (most likely an adapter),
1498
- # we calculate the offset as sum of offsets for the direct parent labware
1499
- # and the underlying non-labware parent location.
1500
- direct_parent_offset = self._nominal_gripper_offsets_for_location(
1501
- location
1502
- )
1503
- ancestor = self._labware.get_parent_location(location.labwareId)
1504
- extra_offset = Point(x=0, y=0, z=0)
1505
- if (
1506
- isinstance(ancestor, ModuleLocation)
1507
- # todo(mm, 2024-11-06): Do not access private module state; only use public ModuleView methods.
1508
- and self._modules._state.requested_model_by_id[ancestor.moduleId]
1509
- == ModuleModel.THERMOCYCLER_MODULE_V2
1510
- and labware_validation.validate_definition_is_lid(current_labware)
1511
- ):
1512
- if "lidOffsets" in current_labware.gripperOffsets.keys():
1513
- extra_offset = Point(
1514
- x=current_labware.gripperOffsets[
1515
- "lidOffsets"
1516
- ].pickUpOffset.x,
1517
- y=current_labware.gripperOffsets[
1518
- "lidOffsets"
1519
- ].pickUpOffset.y,
1520
- z=current_labware.gripperOffsets[
1521
- "lidOffsets"
1522
- ].pickUpOffset.z,
1523
- )
1524
- else:
1525
- raise errors.LabwareOffsetDoesNotExistError(
1526
- f"Labware Definition {current_labware.parameters.loadName} does not contain required field 'lidOffsets' of 'gripperOffsets'."
1527
- )
1528
-
1529
- assert isinstance(
1530
- ancestor,
1531
- (
1532
- DeckSlotLocation,
1533
- ModuleLocation,
1534
- OnLabwareLocation,
1535
- AddressableAreaLocation,
1536
- ),
1537
- ), "No gripper offsets for off-deck labware"
1538
- return (
1539
- Point.from_xyz_attrs(direct_parent_offset.dropOffset)
1540
- + Point.from_xyz_attrs(
1541
- self._nominal_gripper_offsets_for_location(
1542
- location=ancestor
1543
- ).dropOffset
1544
- )
1545
- + extra_offset
1546
- )
1547
-
1548
1460
  # todo(mm, 2024-11-05): This may be incorrect because it does not take the following
1549
1461
  # offsets into account, which *are* taken into account for the actual gripper movement:
1550
1462
  #
@@ -1596,64 +1508,6 @@ class GeometryView:
1596
1508
  f"Cannot move labware '{labware_definition.parameters.loadName}' when {int(tip.volume)} µL tips are attached."
1597
1509
  )
1598
1510
 
1599
- def _nominal_gripper_offsets_for_location(
1600
- self, location: OnDeckLabwareLocation
1601
- ) -> LabwareMovementOffsetData:
1602
- """Provide the default gripper offset data for the given location type."""
1603
- if isinstance(location, (DeckSlotLocation, AddressableAreaLocation)):
1604
- # TODO we might need a separate type of gripper offset for addressable areas but that also might just
1605
- # be covered by the drop labware offset/location
1606
- offsets = self._labware.get_deck_default_gripper_offsets()
1607
- elif isinstance(location, ModuleLocation):
1608
- offsets = self._modules.get_default_gripper_offsets(location.moduleId)
1609
- else:
1610
- # Labware is on a labware/adapter
1611
- offsets = self._labware_gripper_offsets(location.labwareId)
1612
- return offsets or LabwareMovementOffsetData(
1613
- pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0),
1614
- dropOffset=LabwareOffsetVector(x=0, y=0, z=0),
1615
- )
1616
-
1617
- def _labware_gripper_offsets(
1618
- self, labware_id: str
1619
- ) -> Optional[LabwareMovementOffsetData]:
1620
- """Provide the most appropriate gripper offset data for the specified labware.
1621
-
1622
- We check the types of gripper offsets available for the labware ("default" or slot-based)
1623
- and return the most appropriate one for the overall location of the labware.
1624
- Currently, only module adapters (specifically, the H/S universal flat adapter)
1625
- have non-default offsets that are specific to location of the module on deck,
1626
- so, this code only checks for the presence of those known offsets.
1627
- """
1628
- parent_location = self._labware.get_parent_location(labware_id)
1629
- assert isinstance(
1630
- parent_location,
1631
- (
1632
- DeckSlotLocation,
1633
- ModuleLocation,
1634
- AddressableAreaLocation,
1635
- OnLabwareLocation,
1636
- ),
1637
- ), "No gripper offsets for off-deck labware"
1638
-
1639
- if isinstance(parent_location, DeckSlotLocation):
1640
- slot_name = parent_location.slotName
1641
- elif isinstance(parent_location, AddressableAreaLocation):
1642
- slot_name = self._addressable_areas.get_addressable_area_base_slot(
1643
- parent_location.addressableAreaName
1644
- )
1645
- else:
1646
- module_loc = self._modules.get_location(parent_location.moduleId)
1647
- slot_name = module_loc.slotName
1648
-
1649
- slot_based_offset = self._labware.get_child_gripper_offsets(
1650
- labware_id=labware_id, slot_name=slot_name.to_ot3_equivalent()
1651
- )
1652
-
1653
- return slot_based_offset or self._labware.get_child_gripper_offsets(
1654
- labware_id=labware_id, slot_name=None
1655
- )
1656
-
1657
1511
  def get_location_sequence(self, labware_id: str) -> LabwareLocationSequence:
1658
1512
  """Provide the LocationSequence specifying the current position of the labware.
1659
1513
 
@@ -2260,7 +2114,8 @@ class GeometryView:
2260
2114
  except InvalidLiquidHeightFound as _exception:
2261
2115
  raise InvalidLiquidHeightFound(
2262
2116
  message=_exception.message
2263
- + f"for well {well_name} of {self._labware.get_display_name(labware_id)} on slot {self.get_ancestor_slot_name(labware_id)}"
2117
+ + f"for well {well_name} of {self._labware.get_display_name(labware_id)}"
2118
+ f" on slot {self.get_ancestor_slot_name(labware_id)}"
2264
2119
  )
2265
2120
  # if meniscus volume is a simulated value, comparisons aren't meaningful
2266
2121
  if isinstance(meniscus_volume, SimulatedProbeResult):
@@ -2268,13 +2123,16 @@ class GeometryView:
2268
2123
  remaining_volume = well_volumetric_capacity - meniscus_volume
2269
2124
  if volume > remaining_volume:
2270
2125
  raise errors.InvalidDispenseVolumeError(
2271
- 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})"
2126
+ f"Attempting to dispense {volume}µL of liquid into a well that can currently only hold"
2127
+ f" {remaining_volume}µL (well {well_name} in labware {self._labware.get_display_name(labware_id)})"
2272
2128
  )
2273
2129
  else:
2274
2130
  # TODO(pbm, 10-08-24): factor in well (LabwareStore) state volume
2275
2131
  if volume > well_volumetric_capacity:
2276
2132
  raise errors.InvalidDispenseVolumeError(
2277
- 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})"
2133
+ f"Attempting to dispense {volume}µL of liquid into a well that can only hold"
2134
+ f" {well_volumetric_capacity}µL (well {well_name} in"
2135
+ f" labware {self._labware.get_display_name(labware_id)})"
2278
2136
  )
2279
2137
 
2280
2138
  def get_wells_covered_by_pipette_with_active_well(