opentrons 8.7.0a6__py3-none-any.whl → 8.7.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.

Potentially problematic release.


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

Files changed (144) hide show
  1. opentrons/_version.py +2 -2
  2. opentrons/drivers/asyncio/communication/serial_connection.py +129 -52
  3. opentrons/drivers/heater_shaker/abstract.py +5 -0
  4. opentrons/drivers/heater_shaker/driver.py +10 -0
  5. opentrons/drivers/heater_shaker/simulator.py +4 -0
  6. opentrons/drivers/thermocycler/abstract.py +6 -0
  7. opentrons/drivers/thermocycler/driver.py +61 -10
  8. opentrons/drivers/thermocycler/simulator.py +6 -0
  9. opentrons/hardware_control/api.py +24 -5
  10. opentrons/hardware_control/backends/controller.py +8 -2
  11. opentrons/hardware_control/backends/ot3controller.py +3 -0
  12. opentrons/hardware_control/backends/ot3simulator.py +2 -1
  13. opentrons/hardware_control/backends/simulator.py +2 -1
  14. opentrons/hardware_control/backends/subsystem_manager.py +5 -2
  15. opentrons/hardware_control/emulation/abstract_emulator.py +6 -4
  16. opentrons/hardware_control/emulation/connection_handler.py +8 -5
  17. opentrons/hardware_control/emulation/heater_shaker.py +12 -3
  18. opentrons/hardware_control/emulation/settings.py +1 -1
  19. opentrons/hardware_control/emulation/thermocycler.py +67 -15
  20. opentrons/hardware_control/module_control.py +82 -8
  21. opentrons/hardware_control/modules/__init__.py +3 -0
  22. opentrons/hardware_control/modules/absorbance_reader.py +11 -4
  23. opentrons/hardware_control/modules/flex_stacker.py +38 -9
  24. opentrons/hardware_control/modules/heater_shaker.py +42 -5
  25. opentrons/hardware_control/modules/magdeck.py +8 -4
  26. opentrons/hardware_control/modules/mod_abc.py +13 -5
  27. opentrons/hardware_control/modules/tempdeck.py +25 -5
  28. opentrons/hardware_control/modules/thermocycler.py +68 -11
  29. opentrons/hardware_control/modules/types.py +20 -1
  30. opentrons/hardware_control/modules/utils.py +11 -4
  31. opentrons/hardware_control/nozzle_manager.py +3 -0
  32. opentrons/hardware_control/ot3api.py +26 -5
  33. opentrons/hardware_control/poller.py +22 -8
  34. opentrons/hardware_control/scripts/update_module_fw.py +5 -0
  35. opentrons/hardware_control/types.py +31 -2
  36. opentrons/legacy_commands/module_commands.py +23 -0
  37. opentrons/legacy_commands/protocol_commands.py +20 -0
  38. opentrons/legacy_commands/types.py +80 -0
  39. opentrons/motion_planning/deck_conflict.py +17 -12
  40. opentrons/motion_planning/waypoints.py +15 -29
  41. opentrons/protocol_api/__init__.py +5 -1
  42. opentrons/protocol_api/_types.py +6 -1
  43. opentrons/protocol_api/core/common.py +3 -1
  44. opentrons/protocol_api/core/engine/_default_labware_versions.py +32 -11
  45. opentrons/protocol_api/core/engine/labware.py +8 -1
  46. opentrons/protocol_api/core/engine/module_core.py +75 -8
  47. opentrons/protocol_api/core/engine/protocol.py +18 -1
  48. opentrons/protocol_api/core/engine/tasks.py +48 -0
  49. opentrons/protocol_api/core/engine/well.py +8 -0
  50. opentrons/protocol_api/core/legacy/legacy_module_core.py +24 -4
  51. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +11 -1
  52. opentrons/protocol_api/core/legacy/legacy_well_core.py +4 -0
  53. opentrons/protocol_api/core/legacy/tasks.py +19 -0
  54. opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +14 -2
  55. opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
  56. opentrons/protocol_api/core/module.py +37 -4
  57. opentrons/protocol_api/core/protocol.py +11 -2
  58. opentrons/protocol_api/core/tasks.py +31 -0
  59. opentrons/protocol_api/core/well.py +4 -0
  60. opentrons/protocol_api/labware.py +5 -0
  61. opentrons/protocol_api/module_contexts.py +117 -11
  62. opentrons/protocol_api/protocol_context.py +26 -4
  63. opentrons/protocol_api/robot_context.py +38 -21
  64. opentrons/protocol_api/tasks.py +48 -0
  65. opentrons/protocol_api/validation.py +6 -1
  66. opentrons/protocol_engine/actions/__init__.py +4 -2
  67. opentrons/protocol_engine/actions/actions.py +22 -9
  68. opentrons/protocol_engine/clients/sync_client.py +42 -7
  69. opentrons/protocol_engine/commands/__init__.py +42 -0
  70. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +2 -15
  71. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +2 -15
  72. opentrons/protocol_engine/commands/aspirate.py +1 -0
  73. opentrons/protocol_engine/commands/command.py +1 -0
  74. opentrons/protocol_engine/commands/command_unions.py +49 -0
  75. opentrons/protocol_engine/commands/create_timer.py +83 -0
  76. opentrons/protocol_engine/commands/dispense.py +1 -0
  77. opentrons/protocol_engine/commands/drop_tip.py +32 -8
  78. opentrons/protocol_engine/commands/heater_shaker/__init__.py +14 -0
  79. opentrons/protocol_engine/commands/heater_shaker/common.py +20 -0
  80. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +5 -4
  81. opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +136 -0
  82. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +31 -5
  83. opentrons/protocol_engine/commands/movement_common.py +2 -0
  84. opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
  85. opentrons/protocol_engine/commands/set_tip_state.py +97 -0
  86. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +38 -7
  87. opentrons/protocol_engine/commands/thermocycler/__init__.py +16 -0
  88. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +6 -0
  89. opentrons/protocol_engine/commands/thermocycler/run_profile.py +8 -0
  90. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +40 -6
  91. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +29 -5
  92. opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +191 -0
  93. opentrons/protocol_engine/commands/touch_tip.py +1 -1
  94. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +6 -22
  95. opentrons/protocol_engine/commands/wait_for_tasks.py +98 -0
  96. opentrons/protocol_engine/errors/__init__.py +4 -0
  97. opentrons/protocol_engine/errors/exceptions.py +55 -0
  98. opentrons/protocol_engine/execution/__init__.py +2 -0
  99. opentrons/protocol_engine/execution/command_executor.py +8 -0
  100. opentrons/protocol_engine/execution/create_queue_worker.py +5 -1
  101. opentrons/protocol_engine/execution/labware_movement.py +9 -12
  102. opentrons/protocol_engine/execution/movement.py +2 -0
  103. opentrons/protocol_engine/execution/queue_worker.py +4 -0
  104. opentrons/protocol_engine/execution/run_control.py +8 -0
  105. opentrons/protocol_engine/execution/task_handler.py +157 -0
  106. opentrons/protocol_engine/protocol_engine.py +75 -34
  107. opentrons/protocol_engine/resources/__init__.py +2 -0
  108. opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
  109. opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
  110. opentrons/protocol_engine/resources/labware_validation.py +10 -6
  111. opentrons/protocol_engine/state/_well_math.py +60 -18
  112. opentrons/protocol_engine/state/addressable_areas.py +2 -0
  113. opentrons/protocol_engine/state/commands.py +14 -11
  114. opentrons/protocol_engine/state/geometry.py +213 -374
  115. opentrons/protocol_engine/state/labware.py +52 -102
  116. opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
  117. opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1331 -0
  118. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
  119. opentrons/protocol_engine/state/modules.py +21 -8
  120. opentrons/protocol_engine/state/motion.py +44 -0
  121. opentrons/protocol_engine/state/state.py +14 -0
  122. opentrons/protocol_engine/state/state_summary.py +2 -0
  123. opentrons/protocol_engine/state/tasks.py +139 -0
  124. opentrons/protocol_engine/state/tips.py +177 -258
  125. opentrons/protocol_engine/state/update_types.py +16 -9
  126. opentrons/protocol_engine/types/__init__.py +9 -3
  127. opentrons/protocol_engine/types/deck_configuration.py +5 -1
  128. opentrons/protocol_engine/types/instrument.py +8 -1
  129. opentrons/protocol_engine/types/labware.py +1 -13
  130. opentrons/protocol_engine/types/module.py +10 -0
  131. opentrons/protocol_engine/types/tasks.py +38 -0
  132. opentrons/protocol_engine/types/tip.py +9 -0
  133. opentrons/protocol_runner/create_simulating_orchestrator.py +29 -2
  134. opentrons/protocol_runner/run_orchestrator.py +18 -2
  135. opentrons/protocols/api_support/definitions.py +1 -1
  136. opentrons/protocols/api_support/types.py +2 -1
  137. opentrons/simulate.py +48 -15
  138. opentrons/system/camera.py +1 -1
  139. {opentrons-8.7.0a6.dist-info → opentrons-8.7.0a7.dist-info}/METADATA +4 -4
  140. {opentrons-8.7.0a6.dist-info → opentrons-8.7.0a7.dist-info}/RECORD +143 -127
  141. opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
  142. {opentrons-8.7.0a6.dist-info → opentrons-8.7.0a7.dist-info}/WHEEL +0 -0
  143. {opentrons-8.7.0a6.dist-info → opentrons-8.7.0a7.dist-info}/entry_points.txt +0 -0
  144. {opentrons-8.7.0a6.dist-info → opentrons-8.7.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,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]
@@ -410,145 +404,114 @@ class GeometryView:
410
404
  """
411
405
  location = self._labware.get(labware_id).location
412
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
+ )
413
423
 
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
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,
417
432
  )
418
433
  module_cal_offset = self._get_calibrated_module_offset(location)
419
434
 
420
435
  return slot_front_left + stackup_origin_to_lw_origin + module_cal_offset
421
436
 
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)
428
-
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
- )
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."
471
- )
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.
472
441
 
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)
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
480
450
 
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,
485
- )
451
+ while True:
452
+ definitions_locations_top_to_bottom.append(
453
+ (current_definition, current_location)
486
454
  )
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
455
 
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)
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
523
462
 
524
- elif isinstance(location, AddressableAreaLocation):
525
- addressable_area_name = location.addressableAreaName
526
- return self._addressable_areas.get_addressable_area(addressable_area_name)
463
+ return definitions_locations_top_to_bottom
527
464
 
528
- elif isinstance(location, ModuleLocation):
529
- module_id = location.moduleId
530
- 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
531
470
 
532
- elif isinstance(location, OnLabwareLocation):
533
- below_labware_id = location.labwareId
534
- 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
535
479
 
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
- )
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
541
486
 
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
- )
487
+ return None
547
488
 
548
- else:
549
- raise errors.InvalidLabwarePositionError(
550
- f"Cannot get ancestor from location {location}"
551
- )
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
+ )
552
515
 
553
516
  def _get_underlying_addressable_area_name(self, location: LabwareLocation) -> str:
554
517
  if isinstance(location, DeckSlotLocation):
@@ -776,7 +739,7 @@ class GeometryView:
776
739
 
777
740
  if well_def.shape != "circular":
778
741
  raise errors.LabwareIsNotTipRackError(
779
- 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."
780
743
  )
781
744
 
782
745
  return TipGeometry(
@@ -867,7 +830,7 @@ class GeometryView:
867
830
  slot_name = DeckSlotName.from_primitive(area_name)
868
831
  elif labware.location == OFF_DECK_LOCATION:
869
832
  raise errors.LabwareNotOnDeckError(
870
- 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"
871
834
  f" since it is no longer on the deck."
872
835
  )
873
836
  else:
@@ -1058,6 +1021,8 @@ class GeometryView:
1058
1021
  location: Union[
1059
1022
  DeckSlotLocation, ModuleLocation, OnLabwareLocation, AddressableAreaLocation
1060
1023
  ],
1024
+ move_type: GripperMoveType,
1025
+ user_additional_offset: Point | None,
1061
1026
  ) -> Point:
1062
1027
  """Get the grip point of the labware as placed on the given location.
1063
1028
 
@@ -1069,28 +1034,107 @@ class GeometryView:
1069
1034
  It is calculated as the xy center of the slot with z as the point indicated by
1070
1035
  z-position of labware bottom + grip height from labware bottom.
1071
1036
  """
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(
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,
1075
1041
  location=location,
1076
- definition=labware_definition,
1077
- is_topmost_labware=True, # We aren't concerned with entities above the gripped labware.
1042
+ move_type=move_type,
1078
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)
1079
1061
  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
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
1082
1075
  )
1083
- mod_cal_offset = self._get_calibrated_module_offset(location)
1084
- location_center = self._addressable_areas.get_addressable_area_center(aa_name)
1085
1076
 
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)
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,
1092
1084
  )
1093
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
+
1094
1138
  def _get_lw_origin_to_parent(
1095
1139
  self, labware_definition: LabwareDefinition, addressable_area: AddressableArea
1096
1140
  ) -> Point:
@@ -1124,7 +1168,6 @@ class GeometryView:
1124
1168
  if self._modules.should_dodge_thermocycler(
1125
1169
  from_slot=from_slot, to_slot=to_slot
1126
1170
  ):
1127
-
1128
1171
  middle_slot_fixture = (
1129
1172
  self._addressable_areas.get_fixture_by_deck_slot_name(
1130
1173
  DeckSlotName.SLOT_C2.to_equivalent_for_robot_type(
@@ -1393,41 +1436,6 @@ class GeometryView:
1393
1436
  x_well_offset = 0
1394
1437
  return x_well_offset
1395
1438
 
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
1439
  @staticmethod
1432
1440
  def ensure_valid_gripper_location(
1433
1441
  location: LabwareLocation,
@@ -1449,130 +1457,6 @@ class GeometryView:
1449
1457
  )
1450
1458
  return location
1451
1459
 
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
- )
1575
-
1576
1460
  # todo(mm, 2024-11-05): This may be incorrect because it does not take the following
1577
1461
  # offsets into account, which *are* taken into account for the actual gripper movement:
1578
1462
  #
@@ -1624,64 +1508,6 @@ class GeometryView:
1624
1508
  f"Cannot move labware '{labware_definition.parameters.loadName}' when {int(tip.volume)} µL tips are attached."
1625
1509
  )
1626
1510
 
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
1511
  def get_location_sequence(self, labware_id: str) -> LabwareLocationSequence:
1686
1512
  """Provide the LocationSequence specifying the current position of the labware.
1687
1513
 
@@ -2041,6 +1867,15 @@ class GeometryView:
2041
1867
  else:
2042
1868
  return initial_handling_height
2043
1869
 
1870
+ def well_has_tracked_liquid(
1871
+ self,
1872
+ labware_id: str,
1873
+ well_name: str,
1874
+ ) -> bool:
1875
+ """Returns true if this well has had a liquid loaded or a probe result."""
1876
+ last_updated = self._wells.get_last_liquid_update(labware_id, well_name)
1877
+ return last_updated is not None
1878
+
2044
1879
  def get_current_well_volume(
2045
1880
  self,
2046
1881
  labware_id: str,
@@ -2288,7 +2123,8 @@ class GeometryView:
2288
2123
  except InvalidLiquidHeightFound as _exception:
2289
2124
  raise InvalidLiquidHeightFound(
2290
2125
  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)}"
2126
+ + f"for well {well_name} of {self._labware.get_display_name(labware_id)}"
2127
+ f" on slot {self.get_ancestor_slot_name(labware_id)}"
2292
2128
  )
2293
2129
  # if meniscus volume is a simulated value, comparisons aren't meaningful
2294
2130
  if isinstance(meniscus_volume, SimulatedProbeResult):
@@ -2296,13 +2132,16 @@ class GeometryView:
2296
2132
  remaining_volume = well_volumetric_capacity - meniscus_volume
2297
2133
  if volume > remaining_volume:
2298
2134
  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})"
2135
+ f"Attempting to dispense {volume}µL of liquid into a well that can currently only hold"
2136
+ f" {remaining_volume}µL (well {well_name} in labware {self._labware.get_display_name(labware_id)})"
2300
2137
  )
2301
2138
  else:
2302
2139
  # TODO(pbm, 10-08-24): factor in well (LabwareStore) state volume
2303
2140
  if volume > well_volumetric_capacity:
2304
2141
  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})"
2142
+ f"Attempting to dispense {volume}µL of liquid into a well that can only hold"
2143
+ f" {well_volumetric_capacity}µL (well {well_name} in"
2144
+ f" labware {self._labware.get_display_name(labware_id)})"
2306
2145
  )
2307
2146
 
2308
2147
  def get_wells_covered_by_pipette_with_active_well(