opentrons 8.4.0a6__py2.py3-none-any.whl → 8.4.0a8__py2.py3-none-any.whl

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

Potentially problematic release.


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

Files changed (27) hide show
  1. opentrons/protocol_api/core/engine/instrument.py +4 -4
  2. opentrons/protocol_api/core/engine/well.py +12 -1
  3. opentrons/protocol_api/core/instrument.py +1 -1
  4. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +1 -1
  5. opentrons/protocol_api/core/legacy/legacy_well_core.py +2 -1
  6. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +1 -1
  7. opentrons/protocol_api/core/well.py +2 -1
  8. opentrons/protocol_api/instrument_context.py +87 -33
  9. opentrons/protocol_api/labware.py +3 -1
  10. opentrons/protocol_api/protocol_context.py +2 -0
  11. opentrons/protocol_engine/commands/liquid_probe.py +6 -0
  12. opentrons/protocol_engine/commands/seal_pipette_to_tip.py +44 -23
  13. opentrons/protocol_engine/commands/unseal_pipette_from_tip.py +8 -11
  14. opentrons/protocol_engine/errors/__init__.py +2 -0
  15. opentrons/protocol_engine/errors/exceptions.py +12 -0
  16. opentrons/protocol_engine/execution/pipetting.py +3 -1
  17. opentrons/protocol_engine/state/geometry.py +161 -83
  18. opentrons/protocol_engine/state/modules.py +33 -16
  19. opentrons/protocol_engine/types/__init__.py +2 -0
  20. opentrons/protocol_engine/types/liquid_level_detection.py +14 -20
  21. opentrons/protocol_engine/types/module.py +32 -0
  22. {opentrons-8.4.0a6.dist-info → opentrons-8.4.0a8.dist-info}/METADATA +4 -4
  23. {opentrons-8.4.0a6.dist-info → opentrons-8.4.0a8.dist-info}/RECORD +27 -27
  24. {opentrons-8.4.0a6.dist-info → opentrons-8.4.0a8.dist-info}/LICENSE +0 -0
  25. {opentrons-8.4.0a6.dist-info → opentrons-8.4.0a8.dist-info}/WHEEL +0 -0
  26. {opentrons-8.4.0a6.dist-info → opentrons-8.4.0a8.dist-info}/entry_points.txt +0 -0
  27. {opentrons-8.4.0a6.dist-info → opentrons-8.4.0a8.dist-info}/top_level.txt +0 -0
@@ -17,7 +17,10 @@ from opentrons.types import (
17
17
  MeniscusTrackingTarget,
18
18
  )
19
19
 
20
- from opentrons_shared_data.errors.exceptions import InvalidStoredData
20
+ from opentrons_shared_data.errors.exceptions import (
21
+ InvalidStoredData,
22
+ PipetteLiquidNotFoundError,
23
+ )
21
24
  from opentrons_shared_data.labware.constants import WELL_NAME_PATTERN
22
25
  from opentrons_shared_data.labware.labware_definition import LabwareDefinition
23
26
  from opentrons_shared_data.deck.types import CutoutFixture
@@ -31,6 +34,8 @@ from ..errors import (
31
34
  LabwareNotLoadedOnModuleError,
32
35
  LabwareMovementNotAllowedError,
33
36
  OperationLocationNotInWellError,
37
+ InvalidLabwarePositionError,
38
+ LabwareNotOnDeckError,
34
39
  )
35
40
  from ..errors.exceptions import InvalidLiquidHeightFound
36
41
  from ..resources import (
@@ -373,6 +378,59 @@ class GeometryView:
373
378
  "Either it has been loaded off-deck or its been moved off-deck."
374
379
  )
375
380
 
381
+ def _get_offset_from_parent_addressable_area(
382
+ self, child_definition: LabwareDefinition, parent: LabwareLocation
383
+ ) -> LabwareOffsetVector:
384
+ """Gets the offset vector of a labware from its eventual parent addressable area.
385
+
386
+ This returns the sum of the offsets for any labware-on-labware pairs plus the
387
+ "base offset", which is (0, 0, 0) in all cases except for modules on the
388
+ OT-2. See
389
+ protocol_engine.state.modules.get_nominal_offset_to_child_from_addressable_area
390
+ for more.
391
+
392
+ This does not incorporate LPC offsets or module calibration offsets.
393
+ """
394
+ if isinstance(parent, (AddressableAreaLocation, DeckSlotLocation)):
395
+ return LabwareOffsetVector(x=0, y=0, z=0)
396
+ elif isinstance(parent, ModuleLocation):
397
+ module_id = parent.moduleId
398
+ module_model = self._modules.get_connected_model(module_id)
399
+ stacking_overlap = self._labware.get_module_overlap_offsets(
400
+ child_definition, module_model
401
+ )
402
+ module_to_child = (
403
+ self._modules.get_nominal_offset_to_child_from_addressable_area(
404
+ module_id=module_id
405
+ )
406
+ )
407
+ return LabwareOffsetVector(
408
+ x=module_to_child.x - stacking_overlap.x,
409
+ y=module_to_child.y - stacking_overlap.y,
410
+ z=module_to_child.z - stacking_overlap.z,
411
+ )
412
+ elif isinstance(parent, OnLabwareLocation):
413
+ on_labware = self._labware.get(parent.labwareId)
414
+ on_labware_dimensions = self._labware.get_dimensions(
415
+ labware_id=on_labware.id
416
+ )
417
+ stacking_overlap = self._labware.get_labware_overlap_offsets(
418
+ definition=child_definition, below_labware_name=on_labware.loadName
419
+ )
420
+ labware_offset = LabwareOffsetVector(
421
+ x=stacking_overlap.x,
422
+ y=stacking_overlap.y,
423
+ z=on_labware_dimensions.z - stacking_overlap.z,
424
+ )
425
+ return labware_offset + self._get_offset_from_parent_addressable_area(
426
+ self._labware.get_definition(on_labware.id), on_labware.location
427
+ )
428
+ else:
429
+ raise errors.LabwareNotOnDeckError(
430
+ "Cannot access labware since it is not on the deck. "
431
+ "Either it has been loaded off-deck or it has been moved off-deck."
432
+ )
433
+
376
434
  def _normalize_module_calibration_offset(
377
435
  self,
378
436
  module_location: DeckSlotLocation,
@@ -498,6 +556,30 @@ class GeometryView:
498
556
  f"Specifying {well_location.origin} with an offset of {well_location.offset} results in an operation location below the bottom of the well"
499
557
  )
500
558
 
559
+ def validate_probed_height(
560
+ self,
561
+ labware_id: str,
562
+ well_name: str,
563
+ pipette_id: str,
564
+ probed_height: LiquidTrackingType,
565
+ ) -> None:
566
+ """Raise an error if a probed liquid height is not within well bounds."""
567
+ if isinstance(probed_height, SimulatedProbeResult):
568
+ return
569
+ lld_min_height = self._pipettes.get_current_tip_lld_settings(
570
+ pipette_id=pipette_id
571
+ )
572
+ well_def = self._labware.get_well_definition(labware_id, well_name)
573
+ well_depth = well_def.depth
574
+ if probed_height < lld_min_height:
575
+ raise PipetteLiquidNotFoundError(
576
+ f"Liquid Height of {probed_height} mm is lower minumum allowed lld height {lld_min_height} mm."
577
+ )
578
+ if probed_height > well_depth:
579
+ raise PipetteLiquidNotFoundError(
580
+ f"Liquid Height of {probed_height} mm is greater than maximum well height {well_depth} mm."
581
+ )
582
+
501
583
  def get_well_position(
502
584
  self,
503
585
  labware_id: str,
@@ -520,6 +602,7 @@ class GeometryView:
520
602
  well_location=well_location,
521
603
  well_depth=well_depth,
522
604
  operation_volume=operation_volume,
605
+ pipette_id=pipette_id,
523
606
  )
524
607
  if not isinstance(offset_adjustment, SimulatedProbeResult):
525
608
  offset = offset.model_copy(update={"z": offset.z + offset_adjustment})
@@ -694,6 +777,7 @@ class GeometryView:
694
777
  labware_id: str,
695
778
  well_location: DropTipWellLocation,
696
779
  partially_configured: bool = False,
780
+ override_default_offset: float | None = None,
697
781
  ) -> WellLocation:
698
782
  """Get tip drop location given labware and hardware pipette.
699
783
 
@@ -712,8 +796,9 @@ class GeometryView:
712
796
  origin=WellOrigin(well_location.origin.value),
713
797
  offset=well_location.offset,
714
798
  )
715
-
716
- if self._labware.get_definition(labware_id).parameters.isTiprack:
799
+ if override_default_offset is not None:
800
+ z_offset = override_default_offset
801
+ elif self._labware.get_definition(labware_id).parameters.isTiprack:
717
802
  z_offset = self._labware.get_tip_drop_z_offset(
718
803
  labware_id=labware_id,
719
804
  length_scale=self._pipettes.get_return_tip_scale(pipette_id),
@@ -794,6 +879,32 @@ class GeometryView:
794
879
 
795
880
  return slot_name
796
881
 
882
+ def get_ancestor_addressable_area_name(self, labware_id: str) -> str:
883
+ """Get the name of the addressable area the labware is eventually on."""
884
+ labware = self._labware.get(labware_id)
885
+ original_display_name = self._labware.get_display_name(labware_id)
886
+ seen: Set[str] = set((labware_id,))
887
+ while isinstance(labware.location, OnLabwareLocation):
888
+ labware = self._labware.get(labware.location.labwareId)
889
+ if labware.id in seen:
890
+ raise InvalidLabwarePositionError(
891
+ f"Cycle detected in labware positioning for {original_display_name}"
892
+ )
893
+ seen.add(labware.id)
894
+ if isinstance(labware.location, DeckSlotLocation):
895
+ return labware.location.slotName.id
896
+ elif isinstance(labware.location, AddressableAreaLocation):
897
+ return labware.location.addressableAreaName
898
+ elif isinstance(labware.location, ModuleLocation):
899
+ return self._modules.get_provided_addressable_area(
900
+ labware.location.moduleId
901
+ )
902
+ else:
903
+ raise LabwareNotOnDeckError(
904
+ f"Labware {original_display_name} is not loaded on deck",
905
+ details={"eventual-location": repr(labware.location)},
906
+ )
907
+
797
908
  def ensure_location_not_occupied(
798
909
  self,
799
910
  location: _LabwareLocation,
@@ -961,70 +1072,23 @@ class GeometryView:
961
1072
  self._labware.get_grip_height_from_labware_bottom(labware_definition)
962
1073
  )
963
1074
  location_name: str
964
- module_location: ModuleLocation | None = None
965
-
1075
+ offset = self._get_offset_from_parent_addressable_area(
1076
+ child_definition=labware_definition, parent=location
1077
+ ) + self._get_calibrated_module_offset(location)
966
1078
  if isinstance(location, DeckSlotLocation):
967
1079
  location_name = location.slotName.id
968
- offset = LabwareOffsetVector(x=0, y=0, z=0)
969
1080
  elif isinstance(location, AddressableAreaLocation):
970
1081
  location_name = location.addressableAreaName
971
- if fixture_validation.is_gripper_waste_chute(location_name):
972
- drop_labware_location = (
973
- self._addressable_areas.get_addressable_area_move_to_location(
974
- location_name
975
- )
976
- )
977
- return drop_labware_location + Point(z=grip_height_from_labware_bottom)
978
- # Location should have been pre-validated so this will be a deck/staging area slot
979
- else:
980
- offset = LabwareOffsetVector(x=0, y=0, z=0)
981
- else:
982
- if isinstance(location, ModuleLocation):
983
- location_name = self._modules.get_provided_addressable_area(
984
- location.moduleId
985
- )
986
- module_location = location
987
- else: # OnLabwareLocation
988
- labware_loc = self._labware.get(location.labwareId).location
989
- if isinstance(labware_loc, ModuleLocation):
990
- location_name = self._modules.get_provided_addressable_area(
991
- labware_loc.moduleId
992
- )
993
- module_location = labware_loc
994
- else:
995
- location_name = self.get_ancestor_slot_name(location.labwareId).id
996
- labware_offset = self._get_offset_from_parent(
997
- child_definition=labware_definition, parent=location
998
- )
999
- # Get the calibrated offset if the on labware location is on top of a module, otherwise return empty one
1000
- cal_offset = self._get_calibrated_module_offset(location)
1001
- offset = LabwareOffsetVector(
1002
- x=labware_offset.x + cal_offset.x,
1003
- y=labware_offset.y + cal_offset.y,
1004
- z=labware_offset.z + cal_offset.z,
1005
- )
1006
-
1007
- if module_location is not None:
1008
- # Location center must be determined from the cutout the Module is loaded in
1009
- position = deck_configuration_provider.get_cutout_position(
1010
- cutout_id=self._addressable_areas.get_cutout_id_by_deck_slot_name(
1011
- self._modules.get_location(module_location.moduleId).slotName
1012
- ),
1013
- deck_definition=self._addressable_areas.deck_definition,
1014
- )
1015
- bounding_box = self._addressable_areas.get_addressable_area(
1016
- location_name
1017
- ).bounding_box
1018
- location_center = Point(
1019
- position.x + bounding_box.x / 2,
1020
- position.y + bounding_box.y / 2,
1021
- position.z,
1082
+ elif isinstance(location, ModuleLocation):
1083
+ location_name = self._modules.get_provided_addressable_area(
1084
+ location.moduleId
1022
1085
  )
1086
+ else: # OnLabwareLocation
1087
+ location_name = self.get_ancestor_addressable_area_name(location.labwareId)
1023
1088
 
1024
- else:
1025
- location_center = self._addressable_areas.get_addressable_area_center(
1026
- location_name
1027
- )
1089
+ location_center = self._addressable_areas.get_addressable_area_center(
1090
+ location_name
1091
+ )
1028
1092
 
1029
1093
  return Point(
1030
1094
  location_center.x + offset.x,
@@ -1463,10 +1527,6 @@ class GeometryView:
1463
1527
  # * The "additional offset" or "user offset", e.g. the `pickUpOffset` and `dropOffset`
1464
1528
  # params in the `moveLabware` command.
1465
1529
  #
1466
- # And this *does* take these extra offsets into account:
1467
- #
1468
- # * The labware's Labware Position Check offset
1469
- #
1470
1530
  # For robustness, we should combine this with `get_gripper_labware_movement_waypoints()`.
1471
1531
  #
1472
1532
  # We should also be more explicit about which offsets act to move the gripper paddles
@@ -1488,23 +1548,17 @@ class GeometryView:
1488
1548
  return
1489
1549
 
1490
1550
  tip = self._pipettes.get_attached_tip(pipette.id)
1491
- if tip:
1492
- # NOTE: This call to get_labware_highest_z() uses the labware's LPC offset,
1493
- # which is an inconsistency between this and the actual gripper movement.
1494
- # See the todo comment above this function.
1495
- labware_top_z_when_gripped = gripper_homed_position_z + (
1496
- self.get_labware_highest_z(labware_id=labware_id)
1497
- - self.get_labware_grip_point(
1498
- labware_definition=labware_definition, location=current_location
1499
- ).z
1551
+ if not tip:
1552
+ continue
1553
+ labware_top_z_when_gripped = gripper_homed_position_z + (
1554
+ self._labware.get_dimensions(labware_definition=labware_definition).z
1555
+ - self._labware.get_grip_height_from_labware_bottom(labware_definition)
1556
+ )
1557
+ # TODO(cb, 2024-01-18): Utilizing the nozzle map and labware X coordinates verify if collisions will occur on the X axis (analysis will use hard coded data to measure from the gripper critical point to the pipette mount)
1558
+ if (_PIPETTE_HOMED_POSITION_Z - tip.length) < labware_top_z_when_gripped:
1559
+ raise LabwareMovementNotAllowedError(
1560
+ f"Cannot move labware '{labware_definition.parameters.loadName}' when {int(tip.volume)} µL tips are attached."
1500
1561
  )
1501
- # TODO(cb, 2024-01-18): Utilizing the nozzle map and labware X coordinates verify if collisions will occur on the X axis (analysis will use hard coded data to measure from the gripper critical point to the pipette mount)
1502
- if (
1503
- _PIPETTE_HOMED_POSITION_Z - tip.length
1504
- ) < labware_top_z_when_gripped:
1505
- raise LabwareMovementNotAllowedError(
1506
- f"Cannot move labware '{labware_definition.parameters.loadName}' when {int(tip.volume)} µL tips are attached."
1507
- )
1508
1562
  return
1509
1563
 
1510
1564
  def _nominal_gripper_offsets_for_location(
@@ -1844,6 +1898,7 @@ class GeometryView:
1844
1898
  self,
1845
1899
  labware_id: str,
1846
1900
  well_name: str,
1901
+ pipette_id: str,
1847
1902
  operation_volume: float,
1848
1903
  ) -> float:
1849
1904
  """Get the change in height from a liquid handling operation."""
@@ -1853,6 +1908,7 @@ class GeometryView:
1853
1908
  final_height = self.get_well_height_after_liquid_handling(
1854
1909
  labware_id=labware_id,
1855
1910
  well_name=well_name,
1911
+ pipette_id=pipette_id,
1856
1912
  initial_height=initial_handling_height,
1857
1913
  volume=operation_volume,
1858
1914
  )
@@ -1873,6 +1929,7 @@ class GeometryView:
1873
1929
  well_name: str,
1874
1930
  well_location: WellLocationType,
1875
1931
  well_depth: float,
1932
+ pipette_id: Optional[str] = None,
1876
1933
  operation_volume: Optional[float] = None,
1877
1934
  ) -> LiquidTrackingType:
1878
1935
  """Return a z-axis distance that accounts for well handling height and operation volume.
@@ -1906,9 +1963,14 @@ class GeometryView:
1906
1963
  volume = well_location.volumeOffset
1907
1964
 
1908
1965
  if volume:
1966
+ if pipette_id is None:
1967
+ raise ValueError(
1968
+ "cannot get liquid handling offset without pipette id."
1969
+ )
1909
1970
  liquid_height_after = self.get_well_height_after_liquid_handling(
1910
1971
  labware_id=labware_id,
1911
1972
  well_name=well_name,
1973
+ pipette_id=pipette_id,
1912
1974
  initial_height=initial_handling_height,
1913
1975
  volume=volume,
1914
1976
  )
@@ -2030,6 +2092,7 @@ class GeometryView:
2030
2092
  self,
2031
2093
  labware_id: str,
2032
2094
  well_name: str,
2095
+ pipette_id: str,
2033
2096
  initial_height: LiquidTrackingType,
2034
2097
  volume: float,
2035
2098
  ) -> LiquidTrackingType:
@@ -2044,7 +2107,14 @@ class GeometryView:
2044
2107
  initial_volume = find_volume_at_well_height(
2045
2108
  target_height=initial_height, well_geometry=well_geometry
2046
2109
  )
2047
- final_volume = initial_volume + volume
2110
+ final_volume = initial_volume + (
2111
+ volume
2112
+ * self.get_nozzles_per_well(
2113
+ labware_id=labware_id,
2114
+ target_well_name=well_name,
2115
+ pipette_id=pipette_id,
2116
+ )
2117
+ )
2048
2118
  return find_height_at_well_volume(
2049
2119
  target_volume=final_volume, well_geometry=well_geometry
2050
2120
  )
@@ -2058,6 +2128,7 @@ class GeometryView:
2058
2128
  self,
2059
2129
  labware_id: str,
2060
2130
  well_name: str,
2131
+ pipette_id: str,
2061
2132
  initial_height: LiquidTrackingType,
2062
2133
  volume: float,
2063
2134
  ) -> LiquidTrackingType:
@@ -2073,7 +2144,14 @@ class GeometryView:
2073
2144
  initial_volume = find_volume_at_well_height(
2074
2145
  target_height=initial_height, well_geometry=well_geometry
2075
2146
  )
2076
- final_volume = initial_volume + volume
2147
+ final_volume = initial_volume + (
2148
+ volume
2149
+ * self.get_nozzles_per_well(
2150
+ labware_id=labware_id,
2151
+ target_well_name=well_name,
2152
+ pipette_id=pipette_id,
2153
+ )
2154
+ )
2077
2155
  well_volume = find_height_at_well_volume(
2078
2156
  target_volume=final_volume,
2079
2157
  well_geometry=well_geometry,
@@ -930,10 +930,39 @@ class ModuleView:
930
930
  Includes the slot-specific transform. Does not include the child's
931
931
  Labware Position Check offset.
932
932
  """
933
- if (
934
- self._state.deck_type == DeckType.OT2_STANDARD
935
- or self._state.deck_type == DeckType.OT2_SHORT_TRASH
936
- ):
933
+ base = self.get_nominal_offset_to_child_from_addressable_area(module_id)
934
+ if self.get_deck_supports_module_fixtures():
935
+ module_addressable_area = self.get_provided_addressable_area(module_id)
936
+ module_addressable_area_position = (
937
+ addressable_areas.get_addressable_area_offsets_from_cutout(
938
+ module_addressable_area
939
+ )
940
+ )
941
+ return base + LabwareOffsetVector(
942
+ x=module_addressable_area_position.x,
943
+ y=module_addressable_area_position.y,
944
+ z=module_addressable_area_position.z,
945
+ )
946
+ else:
947
+ return base
948
+
949
+ def get_nominal_offset_to_child_from_addressable_area(
950
+ self, module_id: str
951
+ ) -> LabwareOffsetVector:
952
+ """Get the position offset for a child of this module from the nearest AA.
953
+
954
+ On the Flex, this is always (0, 0, 0); on the OT-2, since modules load on top
955
+ of addressable areas rather than providing addressable areas, the offset is
956
+ the labwareOffset from the module definition, rotated by the module's
957
+ slotTransform if appropriate.
958
+ """
959
+ if self.get_deck_supports_module_fixtures():
960
+ return LabwareOffsetVector(
961
+ x=0,
962
+ y=0,
963
+ z=0,
964
+ )
965
+ else:
937
966
  definition = self.get_definition(module_id)
938
967
  slot = self.get_location(module_id).slotName.id
939
968
 
@@ -968,18 +997,6 @@ class ModuleView:
968
997
  y=xformed[1],
969
998
  z=xformed[2],
970
999
  )
971
- else:
972
- module_addressable_area = self.get_provided_addressable_area(module_id)
973
- module_addressable_area_position = (
974
- addressable_areas.get_addressable_area_offsets_from_cutout(
975
- module_addressable_area
976
- )
977
- )
978
- return LabwareOffsetVector(
979
- x=module_addressable_area_position.x,
980
- y=module_addressable_area_position.y,
981
- z=module_addressable_area_position.z,
982
- )
983
1000
 
984
1001
  def get_module_calibration_offset(
985
1002
  self, module_id: str
@@ -135,6 +135,7 @@ from .liquid_level_detection import (
135
135
  WellInfoSummary,
136
136
  WellLiquidInfo,
137
137
  LiquidTrackingType,
138
+ SimulatedProbeResult,
138
139
  )
139
140
  from .liquid_handling import FlowRates
140
141
  from .labware_movement import LabwareMovementStrategy, LabwareMovementOffsetData
@@ -280,6 +281,7 @@ __all__ = [
280
281
  "WellInfoSummary",
281
282
  "WellLiquidInfo",
282
283
  "LiquidTrackingType",
284
+ "SimulatedProbeResult",
283
285
  # Liquid handling
284
286
  "FlowRates",
285
287
  # Labware movement
@@ -1,9 +1,10 @@
1
1
  """Protocol Engine types to do with liquid level detection."""
2
+
2
3
  from __future__ import annotations
3
4
  from dataclasses import dataclass
4
5
  from datetime import datetime
5
- from typing import Optional, List
6
- from pydantic import BaseModel, model_serializer, field_validator
6
+ from typing import Optional, List, Any
7
+ from pydantic import BaseModel, model_serializer, model_validator
7
8
 
8
9
 
9
10
  class SimulatedProbeResult(BaseModel):
@@ -17,6 +18,14 @@ class SimulatedProbeResult(BaseModel):
17
18
  """Serialize instances of this class as a string."""
18
19
  return "SimulatedProbeResult"
19
20
 
21
+ @model_validator(mode="before")
22
+ @classmethod
23
+ def validate_model(cls, data: object) -> Any:
24
+ """Handle deserializing from a simulated probe result."""
25
+ if isinstance(data, str) and data == "SimulatedProbeResult":
26
+ return {}
27
+ return data
28
+
20
29
  def __add__(
21
30
  self, other: float | SimulatedProbeResult
22
31
  ) -> float | SimulatedProbeResult:
@@ -75,7 +84,9 @@ class SimulatedProbeResult(BaseModel):
75
84
  self.operations_after_probe.append(volume)
76
85
 
77
86
 
78
- LiquidTrackingType = SimulatedProbeResult | float
87
+ # Work around https://github.com/pydantic/pydantic/issues/6830 - do not change the order of
88
+ # this union
89
+ LiquidTrackingType = float | SimulatedProbeResult
79
90
 
80
91
 
81
92
  class LoadedVolumeInfo(BaseModel):
@@ -104,23 +115,6 @@ class ProbedVolumeInfo(BaseModel):
104
115
  class WellInfoSummary(BaseModel):
105
116
  """Payload for a well's liquid info in StateSummary."""
106
117
 
107
- # TODO(cm): 3/21/25: refactor SimulatedLiquidProbe in a way that
108
- # doesn't require models like this one that are just using it to
109
- # need a custom validator
110
- @field_validator("probed_height", "probed_volume", mode="before")
111
- @classmethod
112
- def validate_simulated_probe_result(
113
- cls, input_val: object
114
- ) -> LiquidTrackingType | None:
115
- """Return the appropriate input to WellInfoSummary from json data."""
116
- if input_val is None:
117
- return None
118
- if isinstance(input_val, LiquidTrackingType):
119
- return input_val
120
- if isinstance(input_val, str) and input_val == "SimulatedProbeResult":
121
- return SimulatedProbeResult()
122
- raise ValueError(f"Invalid input value {input_val} to WellInfoSummary")
123
-
124
118
  labware_id: str
125
119
  well_name: str
126
120
  loaded_volume: Optional[float] = None
@@ -253,6 +253,38 @@ class ModuleOffsetVector(BaseModel):
253
253
  y: float
254
254
  z: float
255
255
 
256
+ def __add__(self, other: Any) -> ModuleOffsetVector:
257
+ """Adds two vectors together."""
258
+ if not isinstance(other, (LabwareOffsetVector, ModuleOffsetVector)):
259
+ return NotImplemented
260
+ return ModuleOffsetVector(
261
+ x=self.x + other.x, y=self.y + other.y, z=self.z + other.z
262
+ )
263
+
264
+ def __radd__(self, other: Any) -> ModuleOffsetVector:
265
+ """Adds two vectors together, the other way."""
266
+ if not isinstance(other, (LabwareOffsetVector, ModuleOffsetVector)):
267
+ return NotImplemented
268
+ return ModuleOffsetVector(
269
+ x=other.x + self.x, y=other.y + self.y, z=other.z + self.z
270
+ )
271
+
272
+ def __sub__(self, other: Any) -> ModuleOffsetVector:
273
+ """Subtracts two vectors."""
274
+ if not isinstance(other, (LabwareOffsetVector, ModuleOffsetVector)):
275
+ return NotImplemented
276
+ return ModuleOffsetVector(
277
+ x=self.x - other.x, y=self.y - other.y, z=self.z - other.z
278
+ )
279
+
280
+ def __rsub__(self, other: Any) -> ModuleOffsetVector:
281
+ """Subtracts two vectors, the other way."""
282
+ if not isinstance(other, (LabwareOffsetVector, ModuleOffsetVector)):
283
+ return NotImplemented
284
+ return ModuleOffsetVector(
285
+ x=other.x - self.x, y=other.y - self.y, z=other.z - self.z
286
+ )
287
+
256
288
 
257
289
  @dataclass
258
290
  class ModuleOffsetData:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: opentrons
3
- Version: 8.4.0a6
3
+ Version: 8.4.0a8
4
4
  Summary: The Opentrons API is a simple framework designed to make writing automated biology lab protocols easy.
5
5
  Author: Opentrons
6
6
  Author-email: engineering@opentrons.com
@@ -21,7 +21,7 @@ Classifier: Programming Language :: Python :: 3.10
21
21
  Classifier: Topic :: Scientific/Engineering
22
22
  Requires-Python: >=3.10
23
23
  License-File: ../LICENSE
24
- Requires-Dist: opentrons-shared-data (==8.4.0a6)
24
+ Requires-Dist: opentrons-shared-data (==8.4.0a8)
25
25
  Requires-Dist: aionotify (==0.3.1)
26
26
  Requires-Dist: anyio (<4.0.0,>=3.6.1)
27
27
  Requires-Dist: jsonschema (<4.18.0,>=3.0.1)
@@ -35,9 +35,9 @@ Requires-Dist: pyusb (==1.2.1)
35
35
  Requires-Dist: packaging (>=21.0)
36
36
  Requires-Dist: importlib-metadata (>=1.0) ; python_version < "3.8"
37
37
  Provides-Extra: flex-hardware
38
- Requires-Dist: opentrons-hardware[flex] (==8.4.0a6) ; extra == 'flex-hardware'
38
+ Requires-Dist: opentrons-hardware[flex] (==8.4.0a8) ; extra == 'flex-hardware'
39
39
  Provides-Extra: ot2-hardware
40
- Requires-Dist: opentrons-hardware (==8.4.0a6) ; extra == 'ot2-hardware'
40
+ Requires-Dist: opentrons-hardware (==8.4.0a8) ; extra == 'ot2-hardware'
41
41
 
42
42
  .. _Full API Documentation: http://docs.opentrons.com
43
43