opentrons 8.7.0a2__py3-none-any.whl → 8.7.0a4__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 (119) hide show
  1. opentrons/_version.py +2 -2
  2. opentrons/drivers/thermocycler/abstract.py +0 -1
  3. opentrons/drivers/thermocycler/driver.py +4 -33
  4. opentrons/drivers/thermocycler/simulator.py +0 -2
  5. opentrons/hardware_control/api.py +5 -24
  6. opentrons/hardware_control/backends/controller.py +2 -8
  7. opentrons/hardware_control/backends/ot3controller.py +0 -3
  8. opentrons/hardware_control/backends/ot3simulator.py +1 -2
  9. opentrons/hardware_control/backends/simulator.py +1 -2
  10. opentrons/hardware_control/backends/subsystem_manager.py +2 -5
  11. opentrons/hardware_control/module_control.py +8 -82
  12. opentrons/hardware_control/modules/__init__.py +0 -3
  13. opentrons/hardware_control/modules/absorbance_reader.py +4 -11
  14. opentrons/hardware_control/modules/flex_stacker.py +9 -38
  15. opentrons/hardware_control/modules/heater_shaker.py +5 -30
  16. opentrons/hardware_control/modules/magdeck.py +4 -8
  17. opentrons/hardware_control/modules/mod_abc.py +5 -13
  18. opentrons/hardware_control/modules/tempdeck.py +5 -25
  19. opentrons/hardware_control/modules/thermocycler.py +10 -56
  20. opentrons/hardware_control/modules/types.py +1 -20
  21. opentrons/hardware_control/modules/utils.py +4 -11
  22. opentrons/hardware_control/nozzle_manager.py +0 -3
  23. opentrons/hardware_control/ot3api.py +5 -26
  24. opentrons/hardware_control/scripts/update_module_fw.py +0 -5
  25. opentrons/hardware_control/types.py +2 -31
  26. opentrons/legacy_commands/protocol_commands.py +0 -20
  27. opentrons/legacy_commands/types.py +0 -42
  28. opentrons/motion_planning/waypoints.py +29 -15
  29. opentrons/protocol_api/__init__.py +0 -5
  30. opentrons/protocol_api/_types.py +1 -6
  31. opentrons/protocol_api/core/common.py +1 -3
  32. opentrons/protocol_api/core/engine/_default_labware_versions.py +11 -32
  33. opentrons/protocol_api/core/engine/labware.py +1 -8
  34. opentrons/protocol_api/core/engine/module_core.py +0 -4
  35. opentrons/protocol_api/core/engine/protocol.py +43 -23
  36. opentrons/protocol_api/core/legacy/legacy_module_core.py +0 -2
  37. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +1 -11
  38. opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +2 -14
  39. opentrons/protocol_api/core/module.py +0 -1
  40. opentrons/protocol_api/core/protocol.py +2 -11
  41. opentrons/protocol_api/module_contexts.py +0 -1
  42. opentrons/protocol_api/protocol_context.py +4 -26
  43. opentrons/protocol_api/robot_context.py +21 -38
  44. opentrons/protocol_api/validation.py +1 -6
  45. opentrons/protocol_engine/actions/__init__.py +2 -4
  46. opentrons/protocol_engine/actions/actions.py +9 -22
  47. opentrons/protocol_engine/clients/sync_client.py +7 -6
  48. opentrons/protocol_engine/commands/__init__.py +0 -42
  49. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +15 -2
  50. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +15 -2
  51. opentrons/protocol_engine/commands/aspirate.py +0 -1
  52. opentrons/protocol_engine/commands/command.py +0 -1
  53. opentrons/protocol_engine/commands/command_unions.py +0 -39
  54. opentrons/protocol_engine/commands/dispense.py +0 -1
  55. opentrons/protocol_engine/commands/drop_tip.py +8 -32
  56. opentrons/protocol_engine/commands/movement_common.py +0 -2
  57. opentrons/protocol_engine/commands/pick_up_tip.py +11 -21
  58. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +0 -6
  59. opentrons/protocol_engine/commands/thermocycler/run_profile.py +0 -8
  60. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +1 -17
  61. opentrons/protocol_engine/commands/touch_tip.py +1 -1
  62. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +22 -6
  63. opentrons/protocol_engine/errors/__init__.py +0 -4
  64. opentrons/protocol_engine/errors/exceptions.py +0 -55
  65. opentrons/protocol_engine/execution/__init__.py +0 -2
  66. opentrons/protocol_engine/execution/command_executor.py +0 -8
  67. opentrons/protocol_engine/execution/create_queue_worker.py +1 -5
  68. opentrons/protocol_engine/execution/labware_movement.py +12 -9
  69. opentrons/protocol_engine/execution/movement.py +0 -2
  70. opentrons/protocol_engine/execution/queue_worker.py +0 -4
  71. opentrons/protocol_engine/execution/run_control.py +0 -8
  72. opentrons/protocol_engine/protocol_engine.py +33 -67
  73. opentrons/protocol_engine/resources/__init__.py +0 -2
  74. opentrons/protocol_engine/resources/deck_configuration_provider.py +0 -7
  75. opentrons/protocol_engine/resources/labware_validation.py +6 -10
  76. opentrons/protocol_engine/state/_labware_origin_math.py +636 -0
  77. opentrons/protocol_engine/state/_well_math.py +18 -60
  78. opentrons/protocol_engine/state/addressable_areas.py +0 -2
  79. opentrons/protocol_engine/state/commands.py +7 -7
  80. opentrons/protocol_engine/state/geometry.py +374 -204
  81. opentrons/protocol_engine/state/labware.py +102 -52
  82. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +0 -37
  83. opentrons/protocol_engine/state/modules.py +8 -21
  84. opentrons/protocol_engine/state/motion.py +0 -44
  85. opentrons/protocol_engine/state/state.py +0 -14
  86. opentrons/protocol_engine/state/state_summary.py +0 -2
  87. opentrons/protocol_engine/state/tips.py +258 -177
  88. opentrons/protocol_engine/state/update_types.py +9 -16
  89. opentrons/protocol_engine/types/__init__.py +3 -9
  90. opentrons/protocol_engine/types/deck_configuration.py +1 -5
  91. opentrons/protocol_engine/types/instrument.py +1 -8
  92. opentrons/protocol_engine/types/labware.py +13 -1
  93. opentrons/protocol_engine/types/module.py +0 -10
  94. opentrons/protocol_engine/types/tip.py +0 -9
  95. opentrons/protocol_runner/create_simulating_orchestrator.py +2 -29
  96. opentrons/protocol_runner/run_orchestrator.py +2 -18
  97. opentrons/protocols/api_support/definitions.py +1 -1
  98. opentrons/protocols/api_support/types.py +1 -2
  99. opentrons/simulate.py +15 -48
  100. opentrons/system/camera.py +1 -1
  101. {opentrons-8.7.0a2.dist-info → opentrons-8.7.0a4.dist-info}/METADATA +4 -4
  102. {opentrons-8.7.0a2.dist-info → opentrons-8.7.0a4.dist-info}/RECORD +105 -118
  103. opentrons/protocol_api/core/engine/tasks.py +0 -35
  104. opentrons/protocol_api/core/legacy/tasks.py +0 -19
  105. opentrons/protocol_api/core/legacy_simulator/tasks.py +0 -19
  106. opentrons/protocol_api/core/tasks.py +0 -31
  107. opentrons/protocol_api/tasks.py +0 -48
  108. opentrons/protocol_engine/commands/create_timer.py +0 -83
  109. opentrons/protocol_engine/commands/set_tip_state.py +0 -97
  110. opentrons/protocol_engine/commands/wait_for_tasks.py +0 -98
  111. opentrons/protocol_engine/execution/task_handler.py +0 -157
  112. opentrons/protocol_engine/resources/concurrency_provider.py +0 -27
  113. opentrons/protocol_engine/state/labware_origin_math/errors.py +0 -94
  114. opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +0 -1331
  115. opentrons/protocol_engine/state/tasks.py +0 -139
  116. opentrons/protocol_engine/types/tasks.py +0 -38
  117. {opentrons-8.7.0a2.dist-info → opentrons-8.7.0a4.dist-info}/WHEEL +0 -0
  118. {opentrons-8.7.0a2.dist-info → opentrons-8.7.0a4.dist-info}/entry_points.txt +0 -0
  119. {opentrons-8.7.0a2.dist-info → opentrons-8.7.0a4.dist-info}/licenses/LICENSE +0 -0
@@ -24,7 +24,6 @@ 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,
28
27
  InnerWellGeometry,
29
28
  )
30
29
  from opentrons_shared_data.deck.types import CutoutFixture
@@ -45,6 +44,7 @@ from ..errors.exceptions import (
45
44
  )
46
45
  from ..resources import (
47
46
  fixture_validation,
47
+ labware_validation,
48
48
  deck_configuration_provider,
49
49
  )
50
50
  from ..types import (
@@ -63,10 +63,12 @@ from ..types import (
63
63
  ModuleLocation,
64
64
  OnLabwareLocation,
65
65
  LabwareLocation,
66
+ LabwareOffsetVector,
66
67
  ModuleOffsetData,
67
68
  CurrentWell,
68
69
  CurrentPipetteLocation,
69
70
  TipGeometry,
71
+ LabwareMovementOffsetData,
70
72
  InStackerHopperLocation,
71
73
  OnDeckLabwareLocation,
72
74
  AddressableAreaLocation,
@@ -89,7 +91,7 @@ from ..types import (
89
91
  labware_location_is_system,
90
92
  WellLocationType,
91
93
  WellLocationFunction,
92
- GripperMoveType,
94
+ LabwareParentDefinition,
93
95
  AddressableArea,
94
96
  )
95
97
  from ..types.liquid_level_detection import SimulatedProbeResult, LiquidTrackingType
@@ -106,11 +108,8 @@ from .inner_well_math_utils import (
106
108
  find_volume_user_defined_volumes,
107
109
  )
108
110
  from ._well_math import wells_covered_by_pipette_configuration, nozzles_per_well
109
- from .labware_origin_math.stackup_origin_to_labware_origin import (
110
- get_stackup_origin_to_labware_origin,
111
- LabwareOriginContext,
112
- LabwareStackupAncestorDefinition,
113
- )
111
+ from ._labware_origin_math import get_parent_placement_origin_to_lw_origin
112
+
114
113
 
115
114
  _LOG = getLogger(__name__)
116
115
  SLOT_WIDTH = 128
@@ -126,6 +125,13 @@ class _TipDropSection(enum.Enum):
126
125
  RIGHT = "right"
127
126
 
128
127
 
128
+ class _GripperMoveType(enum.Enum):
129
+ """Types of gripper movement."""
130
+
131
+ PICK_UP_LABWARE = enum.auto()
132
+ DROP_LABWARE = enum.auto()
133
+
134
+
129
135
  @dataclass
130
136
  class _AbsoluteRobotExtents:
131
137
  front_left: Dict[MountType, Point]
@@ -404,114 +410,145 @@ class GeometryView:
404
410
  """
405
411
  location = self._labware.get(labware_id).location
406
412
  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
- )
423
413
 
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,
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
417
  )
433
418
  module_cal_offset = self._get_calibrated_module_offset(location)
434
419
 
435
420
  return slot_front_left + stackup_origin_to_lw_origin + module_cal_offset
436
421
 
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.
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)
441
428
 
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
429
+ return parent_pos
450
430
 
451
- while True:
452
- definitions_locations_top_to_bottom.append(
453
- (current_definition, current_location)
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
+ )
454
457
  )
455
458
 
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
462
-
463
- return definitions_locations_top_to_bottom
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
+ )
464
472
 
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
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)
470
480
 
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
- )
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,
477
485
  )
478
- return module_parent_to_child_offset
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}")
479
515
 
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
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)
486
523
 
487
- return None
524
+ elif isinstance(location, AddressableAreaLocation):
525
+ addressable_area_name = location.addressableAreaName
526
+ return self._addressable_areas.get_addressable_area(addressable_area_name)
488
527
 
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
- )
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)
535
+
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
+ )
541
+
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
+ )
547
+
548
+ else:
549
+ raise errors.InvalidLabwarePositionError(
550
+ f"Cannot get ancestor from location {location}"
551
+ )
515
552
 
516
553
  def _get_underlying_addressable_area_name(self, location: LabwareLocation) -> str:
517
554
  if isinstance(location, DeckSlotLocation):
@@ -739,7 +776,7 @@ class GeometryView:
739
776
 
740
777
  if well_def.shape != "circular":
741
778
  raise errors.LabwareIsNotTipRackError(
742
- f"Well {well_name} in labware {self._labware.get_display_name(labware_id)} is not circular."
779
+ f"Well {well_name} in labware {labware_id} is not circular."
743
780
  )
744
781
 
745
782
  return TipGeometry(
@@ -830,7 +867,7 @@ class GeometryView:
830
867
  slot_name = DeckSlotName.from_primitive(area_name)
831
868
  elif labware.location == OFF_DECK_LOCATION:
832
869
  raise errors.LabwareNotOnDeckError(
833
- f"Labware {self._labware.get_display_name(labware_id)} does not have a slot associated with it"
870
+ f"Labware {labware_id} does not have a slot associated with it"
834
871
  f" since it is no longer on the deck."
835
872
  )
836
873
  else:
@@ -1021,8 +1058,6 @@ class GeometryView:
1021
1058
  location: Union[
1022
1059
  DeckSlotLocation, ModuleLocation, OnLabwareLocation, AddressableAreaLocation
1023
1060
  ],
1024
- move_type: GripperMoveType,
1025
- user_additional_offset: Point | None,
1026
1061
  ) -> Point:
1027
1062
  """Get the grip point of the labware as placed on the given location.
1028
1063
 
@@ -1034,107 +1069,28 @@ class GeometryView:
1034
1069
  It is calculated as the xy center of the slot with z as the point indicated by
1035
1070
  z-position of labware bottom + grip height from labware bottom.
1036
1071
  """
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,
1041
- location=location,
1042
- move_type=move_type,
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
1072
  grip_z_from_lw_origin = self._labware.get_grip_z(labware_definition)
1060
1073
  aa_name = self._get_underlying_addressable_area_name(location)
1061
- addressable_area = self._addressable_areas.get_addressable_area(aa_name)
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
1074
+ parent_to_lw_offset = self._get_stackup_placement_origin_to_lw_origin(
1075
+ location=location,
1076
+ definition=labware_definition,
1077
+ is_topmost_labware=True, # We aren't concerned with entities above the gripped labware.
1070
1078
  )
1071
- context_type = (
1072
- LabwareOriginContext.GRIPPER_PICKING_UP
1073
- if move_type == GripperMoveType.PICK_UP_LABWARE
1074
- else LabwareOriginContext.GRIPPER_DROPPING
1079
+ 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
1075
1082
  )
1083
+ mod_cal_offset = self._get_calibrated_module_offset(location)
1084
+ location_center = self._addressable_areas.get_addressable_area_center(aa_name)
1076
1085
 
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,
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)
1084
1092
  )
1085
1093
 
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
-
1138
1094
  def _get_lw_origin_to_parent(
1139
1095
  self, labware_definition: LabwareDefinition, addressable_area: AddressableArea
1140
1096
  ) -> Point:
@@ -1168,6 +1124,7 @@ class GeometryView:
1168
1124
  if self._modules.should_dodge_thermocycler(
1169
1125
  from_slot=from_slot, to_slot=to_slot
1170
1126
  ):
1127
+
1171
1128
  middle_slot_fixture = (
1172
1129
  self._addressable_areas.get_fixture_by_deck_slot_name(
1173
1130
  DeckSlotName.SLOT_C2.to_equivalent_for_robot_type(
@@ -1436,6 +1393,41 @@ class GeometryView:
1436
1393
  x_well_offset = 0
1437
1394
  return x_well_offset
1438
1395
 
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
+
1439
1431
  @staticmethod
1440
1432
  def ensure_valid_gripper_location(
1441
1433
  location: LabwareLocation,
@@ -1457,6 +1449,130 @@ class GeometryView:
1457
1449
  )
1458
1450
  return location
1459
1451
 
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
+
1460
1576
  # todo(mm, 2024-11-05): This may be incorrect because it does not take the following
1461
1577
  # offsets into account, which *are* taken into account for the actual gripper movement:
1462
1578
  #
@@ -1508,6 +1624,64 @@ class GeometryView:
1508
1624
  f"Cannot move labware '{labware_definition.parameters.loadName}' when {int(tip.volume)} µL tips are attached."
1509
1625
  )
1510
1626
 
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
+
1511
1685
  def get_location_sequence(self, labware_id: str) -> LabwareLocationSequence:
1512
1686
  """Provide the LocationSequence specifying the current position of the labware.
1513
1687
 
@@ -2114,8 +2288,7 @@ class GeometryView:
2114
2288
  except InvalidLiquidHeightFound as _exception:
2115
2289
  raise InvalidLiquidHeightFound(
2116
2290
  message=_exception.message
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)}"
2291
+ + f"for well {well_name} of {self._labware.get_display_name(labware_id)} on slot {self.get_ancestor_slot_name(labware_id)}"
2119
2292
  )
2120
2293
  # if meniscus volume is a simulated value, comparisons aren't meaningful
2121
2294
  if isinstance(meniscus_volume, SimulatedProbeResult):
@@ -2123,16 +2296,13 @@ class GeometryView:
2123
2296
  remaining_volume = well_volumetric_capacity - meniscus_volume
2124
2297
  if volume > remaining_volume:
2125
2298
  raise errors.InvalidDispenseVolumeError(
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)})"
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})"
2128
2300
  )
2129
2301
  else:
2130
2302
  # TODO(pbm, 10-08-24): factor in well (LabwareStore) state volume
2131
2303
  if volume > well_volumetric_capacity:
2132
2304
  raise errors.InvalidDispenseVolumeError(
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)})"
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})"
2136
2306
  )
2137
2307
 
2138
2308
  def get_wells_covered_by_pipette_with_active_well(