opentrons 8.3.1a1__py2.py3-none-any.whl → 8.4.0a1__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.
Files changed (192) hide show
  1. opentrons/calibration_storage/ot2/mark_bad_calibration.py +2 -0
  2. opentrons/calibration_storage/ot2/tip_length.py +6 -6
  3. opentrons/config/advanced_settings.py +9 -11
  4. opentrons/config/feature_flags.py +0 -4
  5. opentrons/config/reset.py +7 -2
  6. opentrons/drivers/asyncio/communication/__init__.py +2 -0
  7. opentrons/drivers/asyncio/communication/async_serial.py +4 -0
  8. opentrons/drivers/asyncio/communication/errors.py +41 -8
  9. opentrons/drivers/asyncio/communication/serial_connection.py +36 -10
  10. opentrons/drivers/flex_stacker/__init__.py +9 -3
  11. opentrons/drivers/flex_stacker/abstract.py +140 -15
  12. opentrons/drivers/flex_stacker/driver.py +593 -47
  13. opentrons/drivers/flex_stacker/errors.py +64 -0
  14. opentrons/drivers/flex_stacker/simulator.py +222 -24
  15. opentrons/drivers/flex_stacker/types.py +211 -15
  16. opentrons/drivers/flex_stacker/utils.py +19 -0
  17. opentrons/execute.py +4 -2
  18. opentrons/hardware_control/api.py +5 -0
  19. opentrons/hardware_control/backends/flex_protocol.py +4 -0
  20. opentrons/hardware_control/backends/ot3controller.py +12 -1
  21. opentrons/hardware_control/backends/ot3simulator.py +3 -0
  22. opentrons/hardware_control/backends/subsystem_manager.py +8 -4
  23. opentrons/hardware_control/instruments/ot2/instrument_calibration.py +10 -6
  24. opentrons/hardware_control/instruments/ot3/pipette_handler.py +59 -6
  25. opentrons/hardware_control/modules/__init__.py +12 -1
  26. opentrons/hardware_control/modules/absorbance_reader.py +11 -9
  27. opentrons/hardware_control/modules/flex_stacker.py +498 -0
  28. opentrons/hardware_control/modules/heater_shaker.py +12 -10
  29. opentrons/hardware_control/modules/magdeck.py +5 -1
  30. opentrons/hardware_control/modules/tempdeck.py +5 -1
  31. opentrons/hardware_control/modules/thermocycler.py +15 -14
  32. opentrons/hardware_control/modules/types.py +191 -1
  33. opentrons/hardware_control/modules/utils.py +3 -0
  34. opentrons/hardware_control/motion_utilities.py +20 -0
  35. opentrons/hardware_control/ot3api.py +145 -15
  36. opentrons/hardware_control/protocols/liquid_handler.py +47 -1
  37. opentrons/hardware_control/types.py +6 -0
  38. opentrons/legacy_commands/commands.py +19 -3
  39. opentrons/legacy_commands/helpers.py +15 -0
  40. opentrons/legacy_commands/types.py +3 -2
  41. opentrons/protocol_api/__init__.py +2 -0
  42. opentrons/protocol_api/_liquid.py +39 -8
  43. opentrons/protocol_api/_liquid_properties.py +20 -19
  44. opentrons/protocol_api/_transfer_liquid_validation.py +91 -0
  45. opentrons/protocol_api/core/common.py +3 -1
  46. opentrons/protocol_api/core/engine/deck_conflict.py +11 -1
  47. opentrons/protocol_api/core/engine/instrument.py +1233 -65
  48. opentrons/protocol_api/core/engine/labware.py +8 -4
  49. opentrons/protocol_api/core/engine/load_labware_params.py +68 -10
  50. opentrons/protocol_api/core/engine/module_core.py +118 -2
  51. opentrons/protocol_api/core/engine/protocol.py +253 -11
  52. opentrons/protocol_api/core/engine/stringify.py +19 -8
  53. opentrons/protocol_api/core/engine/transfer_components_executor.py +853 -0
  54. opentrons/protocol_api/core/engine/well.py +60 -5
  55. opentrons/protocol_api/core/instrument.py +65 -19
  56. opentrons/protocol_api/core/labware.py +6 -2
  57. opentrons/protocol_api/core/legacy/labware_offset_provider.py +7 -3
  58. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +69 -21
  59. opentrons/protocol_api/core/legacy/legacy_labware_core.py +8 -4
  60. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +36 -0
  61. opentrons/protocol_api/core/legacy/legacy_well_core.py +25 -1
  62. opentrons/protocol_api/core/legacy/load_info.py +4 -12
  63. opentrons/protocol_api/core/legacy/module_geometry.py +6 -1
  64. opentrons/protocol_api/core/legacy/well_geometry.py +3 -3
  65. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +67 -21
  66. opentrons/protocol_api/core/module.py +43 -0
  67. opentrons/protocol_api/core/protocol.py +33 -0
  68. opentrons/protocol_api/core/well.py +21 -1
  69. opentrons/protocol_api/instrument_context.py +246 -123
  70. opentrons/protocol_api/labware.py +75 -11
  71. opentrons/protocol_api/module_contexts.py +140 -0
  72. opentrons/protocol_api/protocol_context.py +156 -16
  73. opentrons/protocol_api/validation.py +51 -41
  74. opentrons/protocol_engine/__init__.py +21 -2
  75. opentrons/protocol_engine/actions/actions.py +5 -5
  76. opentrons/protocol_engine/clients/sync_client.py +6 -0
  77. opentrons/protocol_engine/commands/__init__.py +30 -0
  78. opentrons/protocol_engine/commands/absorbance_reader/__init__.py +0 -1
  79. opentrons/protocol_engine/commands/air_gap_in_place.py +3 -2
  80. opentrons/protocol_engine/commands/aspirate.py +6 -2
  81. opentrons/protocol_engine/commands/aspirate_in_place.py +3 -1
  82. opentrons/protocol_engine/commands/aspirate_while_tracking.py +237 -0
  83. opentrons/protocol_engine/commands/blow_out.py +2 -0
  84. opentrons/protocol_engine/commands/blow_out_in_place.py +4 -1
  85. opentrons/protocol_engine/commands/command_unions.py +69 -0
  86. opentrons/protocol_engine/commands/configure_for_volume.py +3 -0
  87. opentrons/protocol_engine/commands/dispense.py +3 -1
  88. opentrons/protocol_engine/commands/dispense_in_place.py +3 -0
  89. opentrons/protocol_engine/commands/dispense_while_tracking.py +240 -0
  90. opentrons/protocol_engine/commands/drop_tip.py +23 -1
  91. opentrons/protocol_engine/commands/evotip_dispense.py +6 -7
  92. opentrons/protocol_engine/commands/evotip_seal_pipette.py +24 -29
  93. opentrons/protocol_engine/commands/evotip_unseal_pipette.py +1 -7
  94. opentrons/protocol_engine/commands/flex_stacker/__init__.py +106 -0
  95. opentrons/protocol_engine/commands/flex_stacker/close_latch.py +72 -0
  96. opentrons/protocol_engine/commands/flex_stacker/common.py +15 -0
  97. opentrons/protocol_engine/commands/flex_stacker/empty.py +161 -0
  98. opentrons/protocol_engine/commands/flex_stacker/fill.py +164 -0
  99. opentrons/protocol_engine/commands/flex_stacker/open_latch.py +70 -0
  100. opentrons/protocol_engine/commands/flex_stacker/prepare_shuttle.py +112 -0
  101. opentrons/protocol_engine/commands/flex_stacker/retrieve.py +394 -0
  102. opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +190 -0
  103. opentrons/protocol_engine/commands/flex_stacker/store.py +288 -0
  104. opentrons/protocol_engine/commands/generate_command_schema.py +31 -2
  105. opentrons/protocol_engine/commands/labware_handling_common.py +24 -0
  106. opentrons/protocol_engine/commands/liquid_probe.py +21 -12
  107. opentrons/protocol_engine/commands/load_labware.py +42 -39
  108. opentrons/protocol_engine/commands/load_lid.py +21 -13
  109. opentrons/protocol_engine/commands/load_lid_stack.py +130 -47
  110. opentrons/protocol_engine/commands/load_module.py +18 -17
  111. opentrons/protocol_engine/commands/load_pipette.py +3 -0
  112. opentrons/protocol_engine/commands/move_labware.py +139 -20
  113. opentrons/protocol_engine/commands/pick_up_tip.py +5 -2
  114. opentrons/protocol_engine/commands/pipetting_common.py +154 -7
  115. opentrons/protocol_engine/commands/prepare_to_aspirate.py +17 -2
  116. opentrons/protocol_engine/commands/reload_labware.py +6 -19
  117. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +3 -1
  118. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +6 -1
  119. opentrons/protocol_engine/errors/__init__.py +8 -0
  120. opentrons/protocol_engine/errors/exceptions.py +50 -0
  121. opentrons/protocol_engine/execution/equipment.py +123 -106
  122. opentrons/protocol_engine/execution/labware_movement.py +8 -6
  123. opentrons/protocol_engine/execution/pipetting.py +233 -26
  124. opentrons/protocol_engine/execution/tip_handler.py +14 -5
  125. opentrons/protocol_engine/labware_offset_standardization.py +173 -0
  126. opentrons/protocol_engine/protocol_engine.py +22 -13
  127. opentrons/protocol_engine/resources/deck_configuration_provider.py +94 -2
  128. opentrons/protocol_engine/resources/deck_data_provider.py +1 -1
  129. opentrons/protocol_engine/resources/labware_data_provider.py +32 -12
  130. opentrons/protocol_engine/resources/labware_validation.py +7 -5
  131. opentrons/protocol_engine/slot_standardization.py +11 -23
  132. opentrons/protocol_engine/state/addressable_areas.py +84 -46
  133. opentrons/protocol_engine/state/frustum_helpers.py +26 -10
  134. opentrons/protocol_engine/state/geometry.py +683 -100
  135. opentrons/protocol_engine/state/labware.py +252 -55
  136. opentrons/protocol_engine/state/module_substates/__init__.py +4 -0
  137. opentrons/protocol_engine/state/module_substates/flex_stacker_substate.py +68 -0
  138. opentrons/protocol_engine/state/module_substates/heater_shaker_module_substate.py +22 -0
  139. opentrons/protocol_engine/state/module_substates/temperature_module_substate.py +13 -0
  140. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +20 -0
  141. opentrons/protocol_engine/state/modules.py +178 -52
  142. opentrons/protocol_engine/state/pipettes.py +54 -0
  143. opentrons/protocol_engine/state/state.py +1 -1
  144. opentrons/protocol_engine/state/tips.py +14 -0
  145. opentrons/protocol_engine/state/update_types.py +180 -25
  146. opentrons/protocol_engine/state/wells.py +54 -8
  147. opentrons/protocol_engine/types/__init__.py +292 -0
  148. opentrons/protocol_engine/types/automatic_tip_selection.py +39 -0
  149. opentrons/protocol_engine/types/command_annotations.py +53 -0
  150. opentrons/protocol_engine/types/deck_configuration.py +72 -0
  151. opentrons/protocol_engine/types/execution.py +96 -0
  152. opentrons/protocol_engine/types/hardware_passthrough.py +25 -0
  153. opentrons/protocol_engine/types/instrument.py +47 -0
  154. opentrons/protocol_engine/types/instrument_sensors.py +47 -0
  155. opentrons/protocol_engine/types/labware.py +110 -0
  156. opentrons/protocol_engine/types/labware_movement.py +22 -0
  157. opentrons/protocol_engine/types/labware_offset_location.py +108 -0
  158. opentrons/protocol_engine/types/labware_offset_vector.py +33 -0
  159. opentrons/protocol_engine/types/liquid.py +40 -0
  160. opentrons/protocol_engine/types/liquid_class.py +59 -0
  161. opentrons/protocol_engine/types/liquid_handling.py +13 -0
  162. opentrons/protocol_engine/types/liquid_level_detection.py +137 -0
  163. opentrons/protocol_engine/types/location.py +193 -0
  164. opentrons/protocol_engine/types/module.py +269 -0
  165. opentrons/protocol_engine/types/partial_tip_configuration.py +76 -0
  166. opentrons/protocol_engine/types/run_time_parameters.py +133 -0
  167. opentrons/protocol_engine/types/tip.py +18 -0
  168. opentrons/protocol_engine/types/util.py +21 -0
  169. opentrons/protocol_engine/types/well_position.py +107 -0
  170. opentrons/protocol_reader/extract_labware_definitions.py +7 -3
  171. opentrons/protocol_reader/file_format_validator.py +5 -3
  172. opentrons/protocol_runner/json_translator.py +4 -2
  173. opentrons/protocol_runner/legacy_command_mapper.py +6 -2
  174. opentrons/protocol_runner/run_orchestrator.py +4 -1
  175. opentrons/protocols/advanced_control/transfers/common.py +48 -1
  176. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +204 -0
  177. opentrons/protocols/api_support/definitions.py +1 -1
  178. opentrons/protocols/api_support/instrument.py +16 -3
  179. opentrons/protocols/labware.py +5 -6
  180. opentrons/protocols/models/__init__.py +0 -21
  181. opentrons/simulate.py +4 -2
  182. opentrons/types.py +15 -6
  183. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a1.dist-info}/METADATA +4 -4
  184. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a1.dist-info}/RECORD +188 -148
  185. opentrons/calibration_storage/ot2/models/defaults.py +0 -0
  186. opentrons/calibration_storage/ot3/models/defaults.py +0 -0
  187. opentrons/protocol_api/core/legacy/legacy_robot_core.py +0 -0
  188. opentrons/protocol_engine/types.py +0 -1311
  189. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a1.dist-info}/LICENSE +0 -0
  190. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a1.dist-info}/WHEEL +0 -0
  191. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a1.dist-info}/entry_points.txt +0 -0
  192. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a1.dist-info}/top_level.txt +0 -0
@@ -1,31 +1,44 @@
1
1
  """Geometry state getters."""
2
2
 
3
+ from logging import getLogger
3
4
  import enum
4
5
  from numpy import array, dot, double as npdouble
5
6
  from numpy.typing import NDArray
6
- from typing import Optional, List, Tuple, Union, cast, TypeVar, Dict
7
+ from typing import Optional, List, Tuple, Union, cast, TypeVar, Dict, Set
7
8
  from dataclasses import dataclass
8
9
  from functools import cached_property
9
10
 
10
- from opentrons.types import Point, DeckSlotName, StagingSlotName, MountType
11
+ from opentrons.types import (
12
+ Point,
13
+ DeckSlotName,
14
+ StagingSlotName,
15
+ MountType,
16
+ MeniscusTrackingTarget,
17
+ )
11
18
 
12
19
  from opentrons_shared_data.errors.exceptions import InvalidStoredData
13
20
  from opentrons_shared_data.labware.constants import WELL_NAME_PATTERN
21
+ from opentrons_shared_data.labware.labware_definition import LabwareDefinition
14
22
  from opentrons_shared_data.deck.types import CutoutFixture
15
23
  from opentrons_shared_data.pipette import PIPETTE_X_SPAN
16
- from opentrons_shared_data.pipette.types import ChannelCount
17
- from opentrons.protocols.models import LabwareDefinition
24
+ from opentrons_shared_data.pipette.types import ChannelCount, LabwareUri
18
25
 
19
26
  from .. import errors
20
27
  from ..errors import (
28
+ LabwareNotLoadedError,
21
29
  LabwareNotLoadedOnLabwareError,
22
30
  LabwareNotLoadedOnModuleError,
23
31
  LabwareMovementNotAllowedError,
24
32
  OperationLocationNotInWellError,
25
33
  )
26
- from ..resources import fixture_validation, labware_validation
34
+ from ..resources import (
35
+ fixture_validation,
36
+ labware_validation,
37
+ deck_configuration_provider,
38
+ )
27
39
  from ..types import (
28
40
  OFF_DECK_LOCATION,
41
+ SYSTEM_LOCATION,
29
42
  LoadedLabware,
30
43
  LoadedModule,
31
44
  WellLocation,
@@ -46,13 +59,28 @@ from ..types import (
46
59
  CurrentPipetteLocation,
47
60
  TipGeometry,
48
61
  LabwareMovementOffsetData,
62
+ InStackerHopperLocation,
49
63
  OnDeckLabwareLocation,
50
64
  AddressableAreaLocation,
51
65
  AddressableOffsetVector,
52
66
  StagingSlotLocation,
53
- LabwareOffsetLocation,
67
+ LabwareOffsetLocationSequence,
68
+ OnModuleOffsetLocationSequenceComponent,
69
+ OnAddressableAreaOffsetLocationSequenceComponent,
70
+ OnLabwareOffsetLocationSequenceComponent,
71
+ OnLabwareLocationSequenceComponent,
54
72
  ModuleModel,
73
+ PotentialCutoutFixture,
74
+ LabwareLocationSequence,
75
+ OnModuleLocationSequenceComponent,
76
+ OnAddressableAreaLocationSequenceComponent,
77
+ OnCutoutFixtureLocationSequenceComponent,
78
+ NotOnDeckLocationSequenceComponent,
79
+ AreaType,
80
+ labware_location_is_off_deck,
81
+ labware_location_is_system,
55
82
  )
83
+ from ..types.liquid_level_detection import SimulatedProbeResult, LiquidTrackingType
56
84
  from .config import Config
57
85
  from .labware import LabwareView
58
86
  from .wells import WellView
@@ -66,6 +94,7 @@ from .frustum_helpers import (
66
94
  from ._well_math import wells_covered_by_pipette_configuration, nozzles_per_well
67
95
 
68
96
 
97
+ _LOG = getLogger(__name__)
69
98
  SLOT_WIDTH = 128
70
99
  _PIPETTE_HOMED_POSITION_Z = (
71
100
  248.0 # Height of the bottom of the nozzle without the tip attached when homed
@@ -160,6 +189,7 @@ class GeometryView:
160
189
  self._get_highest_z_from_labware_data(lw_data)
161
190
  for lw_data in self._labware.get_all()
162
191
  if lw_data.location != OFF_DECK_LOCATION
192
+ and not self._labware.get_labware_by_lid_id(lw_data.id)
163
193
  ),
164
194
  default=0.0,
165
195
  )
@@ -306,13 +336,13 @@ class GeometryView:
306
336
  return LabwareOffsetVector(x=0, y=0, z=0)
307
337
  elif isinstance(parent, ModuleLocation):
308
338
  module_id = parent.moduleId
309
- module_to_child = self._modules.get_nominal_offset_to_child(
310
- module_id=module_id, addressable_areas=self._addressable_areas
311
- )
312
339
  module_model = self._modules.get_connected_model(module_id)
313
340
  stacking_overlap = self._labware.get_module_overlap_offsets(
314
341
  child_definition, module_model
315
342
  )
343
+ module_to_child = self._modules.get_nominal_offset_to_child(
344
+ module_id=module_id, addressable_areas=self._addressable_areas
345
+ )
316
346
  return LabwareOffsetVector(
317
347
  x=module_to_child.x - stacking_overlap.x,
318
348
  y=module_to_child.y - stacking_overlap.y,
@@ -388,7 +418,11 @@ class GeometryView:
388
418
  elif isinstance(location, OnLabwareLocation):
389
419
  labware_data = self._labware.get(location.labwareId)
390
420
  return self._get_calibrated_module_offset(labware_data.location)
391
- elif location == OFF_DECK_LOCATION:
421
+ elif (
422
+ location == OFF_DECK_LOCATION
423
+ or location == SYSTEM_LOCATION
424
+ or isinstance(location, InStackerHopperLocation)
425
+ ):
392
426
  raise errors.LabwareNotOnDeckError(
393
427
  "Labware does not have a slot or module associated with it"
394
428
  " since it is no longer on the deck."
@@ -489,10 +523,13 @@ class GeometryView:
489
523
  well_depth=well_depth,
490
524
  operation_volume=operation_volume,
491
525
  )
492
- offset = offset.model_copy(update={"z": offset.z + offset_adjustment})
493
- self.validate_well_position(
494
- well_location=well_location, z_offset=offset.z, pipette_id=pipette_id
495
- )
526
+ if not isinstance(offset_adjustment, SimulatedProbeResult):
527
+ offset = offset.model_copy(update={"z": offset.z + offset_adjustment})
528
+ self.validate_well_position(
529
+ well_location=well_location,
530
+ z_offset=offset.z,
531
+ pipette_id=pipette_id,
532
+ )
496
533
 
497
534
  return Point(
498
535
  x=labware_pos.x + offset.x + well_def.x,
@@ -532,23 +569,26 @@ class GeometryView:
532
569
  labware_id: str,
533
570
  well_name: str,
534
571
  absolute_point: Point,
535
- is_meniscus: Optional[bool] = None,
536
- ) -> LiquidHandlingWellLocation:
537
- """Given absolute position, get relative location of a well in a labware.
538
-
539
- If is_meniscus is True, absolute_point will hold the z-offset in its z field.
540
- """
541
- if is_meniscus:
542
- return LiquidHandlingWellLocation(
572
+ meniscus_tracking: Optional[MeniscusTrackingTarget] = None,
573
+ ) -> Tuple[LiquidHandlingWellLocation, bool]:
574
+ """Given absolute position, get relative location of a well in a labware."""
575
+ dynamic_liquid_tracking = False
576
+ if meniscus_tracking:
577
+ location = LiquidHandlingWellLocation(
543
578
  origin=WellOrigin.MENISCUS,
544
579
  offset=WellOffset(x=0, y=0, z=absolute_point.z),
545
580
  )
581
+ if meniscus_tracking == MeniscusTrackingTarget.END:
582
+ location.volumeOffset = "operationVolume"
583
+ elif meniscus_tracking == MeniscusTrackingTarget.DYNAMIC:
584
+ dynamic_liquid_tracking = True
546
585
  else:
547
586
  well_absolute_point = self.get_well_position(labware_id, well_name)
548
587
  delta = absolute_point - well_absolute_point
549
- return LiquidHandlingWellLocation(
588
+ location = LiquidHandlingWellLocation(
550
589
  offset=WellOffset(x=delta.x, y=delta.y, z=delta.z)
551
590
  )
591
+ return location, dynamic_liquid_tracking
552
592
 
553
593
  def get_relative_pick_up_tip_well_location(
554
594
  self,
@@ -635,7 +675,7 @@ class GeometryView:
635
675
 
636
676
  return TipGeometry(
637
677
  length=effective_length,
638
- diameter=well_def.diameter, # type: ignore[arg-type]
678
+ diameter=well_def.diameter,
639
679
  # TODO(mc, 2020-11-12): WellDefinition type says totalLiquidVolume
640
680
  # is a float, but hardware controller expects an int
641
681
  volume=int(well_def.totalLiquidVolume),
@@ -749,27 +789,151 @@ class GeometryView:
749
789
  return slot_name
750
790
 
751
791
  def ensure_location_not_occupied(
752
- self, location: _LabwareLocation
792
+ self,
793
+ location: _LabwareLocation,
794
+ desired_addressable_area: Optional[str] = None,
753
795
  ) -> _LabwareLocation:
754
796
  """Ensure that the location does not already have either Labware or a Module in it."""
755
- # TODO (spp, 2023-11-27): Slot locations can also be addressable areas
756
- # so we will need to cross-check against items loaded in both location types.
757
- # Something like 'check if an item is in lists of both- labware on addressable areas
758
- # as well as labware on slots'. Same for modules.
759
- if isinstance(
760
- location,
761
- (
762
- DeckSlotLocation,
763
- ModuleLocation,
764
- OnLabwareLocation,
765
- AddressableAreaLocation,
766
- ),
767
- ):
768
- self._labware.raise_if_labware_in_location(location)
769
- if isinstance(location, DeckSlotLocation):
770
- self._modules.raise_if_module_in_location(location)
797
+ # Collect set of existing fixtures, if any
798
+ existing_fixtures = self._get_potential_fixtures_for_location_occupation(
799
+ location
800
+ )
801
+ potential_fixtures = (
802
+ self._get_potential_fixtures_for_location_occupation(
803
+ AddressableAreaLocation(addressableAreaName=desired_addressable_area)
804
+ )
805
+ if desired_addressable_area is not None
806
+ else None
807
+ )
808
+
809
+ # Handle the checking conflict on an incoming fixture
810
+ if potential_fixtures is not None and isinstance(location, DeckSlotLocation):
811
+ if (
812
+ existing_fixtures is not None
813
+ and not any(
814
+ location.slotName.id in fixture.provided_addressable_areas
815
+ for fixture in potential_fixtures[1].intersection(
816
+ existing_fixtures[1]
817
+ )
818
+ )
819
+ ) or (
820
+ self._labware.get_by_slot(location.slotName) is not None
821
+ and not any(
822
+ location.slotName.id in fixture.provided_addressable_areas
823
+ for fixture in potential_fixtures[1]
824
+ )
825
+ ):
826
+ self._labware.raise_if_labware_in_location(location)
827
+
828
+ else:
829
+ self._modules.raise_if_module_in_location(location)
830
+
831
+ # Otherwise handle standard conflict checking
832
+ else:
833
+ if isinstance(
834
+ location,
835
+ (
836
+ DeckSlotLocation,
837
+ ModuleLocation,
838
+ OnLabwareLocation,
839
+ AddressableAreaLocation,
840
+ ),
841
+ ):
842
+ self._labware.raise_if_labware_in_location(location)
843
+
844
+ area = (
845
+ location.slotName.id
846
+ if isinstance(location, DeckSlotLocation)
847
+ else (
848
+ location.addressableAreaName
849
+ if isinstance(location, AddressableAreaLocation)
850
+ else None
851
+ )
852
+ )
853
+ if area is not None and (
854
+ existing_fixtures is None
855
+ or not any(
856
+ area in fixture.provided_addressable_areas
857
+ for fixture in existing_fixtures[1]
858
+ )
859
+ ):
860
+ if isinstance(location, DeckSlotLocation):
861
+ self._modules.raise_if_module_in_location(location)
862
+ elif isinstance(location, AddressableAreaLocation):
863
+ self._modules.raise_if_module_in_location(
864
+ DeckSlotLocation(
865
+ slotName=self._addressable_areas.get_addressable_area_base_slot(
866
+ location.addressableAreaName
867
+ )
868
+ )
869
+ )
870
+
771
871
  return location
772
872
 
873
+ def _get_potential_fixtures_for_location_occupation(
874
+ self, location: _LabwareLocation
875
+ ) -> Tuple[str, Set[PotentialCutoutFixture]] | None:
876
+ loc: DeckSlotLocation | AddressableAreaLocation | None = None
877
+ if isinstance(location, AddressableAreaLocation):
878
+ # Convert the addressable area into a staging slot if applicable
879
+ slots = StagingSlotName._value2member_map_
880
+ for slot in slots:
881
+ if location.addressableAreaName == slot:
882
+ loc = DeckSlotLocation(
883
+ slotName=DeckSlotName(location.addressableAreaName[0] + "3")
884
+ )
885
+ if loc is None:
886
+ loc = location
887
+ elif isinstance(location, DeckSlotLocation):
888
+ loc = location
889
+
890
+ if isinstance(loc, DeckSlotLocation):
891
+ module = self._modules.get_by_slot(loc.slotName)
892
+ if module is not None and self._config.robot_type != "OT-2 Standard":
893
+ fixtures = deck_configuration_provider.get_potential_cutout_fixtures(
894
+ addressable_area_name=self._modules.ensure_and_convert_module_fixture_location(
895
+ deck_slot=loc.slotName,
896
+ model=module.model,
897
+ ),
898
+ deck_definition=self._addressable_areas.deck_definition,
899
+ )
900
+ else:
901
+ fixtures = None
902
+ elif isinstance(loc, AddressableAreaLocation):
903
+ fixtures = deck_configuration_provider.get_potential_cutout_fixtures(
904
+ addressable_area_name=loc.addressableAreaName,
905
+ deck_definition=self._addressable_areas.deck_definition,
906
+ )
907
+ else:
908
+ fixtures = None
909
+ return fixtures
910
+
911
+ def _get_potential_disposal_location_cutout_fixtures(
912
+ self, slot_name: DeckSlotName
913
+ ) -> CutoutFixture | None:
914
+ for area in self._addressable_areas.get_all():
915
+ if (
916
+ self._addressable_areas.get_addressable_area(area).area_type
917
+ == AreaType.WASTE_CHUTE
918
+ or self._addressable_areas.get_addressable_area(area).area_type
919
+ == AreaType.MOVABLE_TRASH
920
+ ) and slot_name == self._addressable_areas.get_addressable_area_base_slot(
921
+ area
922
+ ):
923
+ # Given we only have one Waste Chute fixture and one type of Trash bin fixture it's
924
+ # fine to return the first result of our potential fixtures here. This will need to
925
+ # change in the future if there multiple trash fixtures that share the same area type.
926
+ potential_fixture = (
927
+ deck_configuration_provider.get_potential_cutout_fixtures(
928
+ area, self._addressable_areas.deck_definition
929
+ )[1].pop()
930
+ )
931
+ return deck_configuration_provider.get_cutout_fixture(
932
+ potential_fixture.cutout_fixture_id,
933
+ self._addressable_areas.deck_definition,
934
+ )
935
+ return None
936
+
773
937
  def get_labware_grip_point(
774
938
  self,
775
939
  labware_definition: LabwareDefinition,
@@ -791,6 +955,7 @@ class GeometryView:
791
955
  self._labware.get_grip_height_from_labware_bottom(labware_definition)
792
956
  )
793
957
  location_name: str
958
+ module_location: ModuleLocation | None = None
794
959
 
795
960
  if isinstance(location, DeckSlotLocation):
796
961
  location_name = location.slotName.id
@@ -809,11 +974,19 @@ class GeometryView:
809
974
  offset = LabwareOffsetVector(x=0, y=0, z=0)
810
975
  else:
811
976
  if isinstance(location, ModuleLocation):
812
- location_name = self._modules.get_location(
977
+ location_name = self._modules.get_provided_addressable_area(
813
978
  location.moduleId
814
- ).slotName.id
979
+ )
980
+ module_location = location
815
981
  else: # OnLabwareLocation
816
- location_name = self.get_ancestor_slot_name(location.labwareId).id
982
+ labware_loc = self._labware.get(location.labwareId).location
983
+ if isinstance(labware_loc, ModuleLocation):
984
+ location_name = self._modules.get_provided_addressable_area(
985
+ labware_loc.moduleId
986
+ )
987
+ module_location = labware_loc
988
+ else:
989
+ location_name = self.get_ancestor_slot_name(location.labwareId).id
817
990
  labware_offset = self._get_offset_from_parent(
818
991
  child_definition=labware_definition, parent=location
819
992
  )
@@ -825,9 +998,28 @@ class GeometryView:
825
998
  z=labware_offset.z + cal_offset.z,
826
999
  )
827
1000
 
828
- location_center = self._addressable_areas.get_addressable_area_center(
829
- location_name
830
- )
1001
+ if module_location is not None:
1002
+ # Location center must be determined from the cutout the Module is loaded in
1003
+ position = deck_configuration_provider.get_cutout_position(
1004
+ cutout_id=self._addressable_areas.get_cutout_id_by_deck_slot_name(
1005
+ self._modules.get_location(module_location.moduleId).slotName
1006
+ ),
1007
+ deck_definition=self._addressable_areas.deck_definition,
1008
+ )
1009
+ bounding_box = self._addressable_areas.get_addressable_area(
1010
+ location_name
1011
+ ).bounding_box
1012
+ location_center = Point(
1013
+ position.x + bounding_box.x / 2,
1014
+ position.y + bounding_box.y / 2,
1015
+ position.z,
1016
+ )
1017
+
1018
+ else:
1019
+ location_center = self._addressable_areas.get_addressable_area_center(
1020
+ location_name
1021
+ )
1022
+
831
1023
  return Point(
832
1024
  location_center.x + offset.x,
833
1025
  location_center.y + offset.y,
@@ -877,6 +1069,7 @@ class GeometryView:
877
1069
  maybe_fixture = self._addressable_areas.get_fixture_by_deck_slot_name(
878
1070
  slot_name
879
1071
  )
1072
+
880
1073
  # Ignore generic single slot fixtures
881
1074
  if maybe_fixture and maybe_fixture["id"] in {
882
1075
  "singleLeftSlot",
@@ -888,6 +1081,13 @@ class GeometryView:
888
1081
  maybe_module = self._modules.get_by_slot(
889
1082
  slot_name=slot_name,
890
1083
  ) or self._modules.get_overflowed_module_in_slot(slot_name=slot_name)
1084
+
1085
+ # For situations in which the deck config is none
1086
+ if maybe_fixture is None and maybe_labware is None and maybe_module is None:
1087
+ # todo(chb 2025-03-19): This can go away once we solve the problem of no deck config in analysis
1088
+ maybe_fixture = self._get_potential_disposal_location_cutout_fixtures(
1089
+ slot_name
1090
+ )
891
1091
  else:
892
1092
  # Modules and fixtures can't be loaded on staging slots
893
1093
  maybe_fixture = None
@@ -1359,50 +1559,297 @@ class GeometryView:
1359
1559
  labware_id=labware_id, slot_name=None
1360
1560
  )
1361
1561
 
1362
- def get_offset_location(self, labware_id: str) -> Optional[LabwareOffsetLocation]:
1363
- """Provide the LabwareOffsetLocation specifying the current position of the labware.
1562
+ def get_location_sequence(self, labware_id: str) -> LabwareLocationSequence:
1563
+ """Provide the LocationSequence specifying the current position of the labware.
1364
1564
 
1365
- If the labware is in a location that cannot be specified by a LabwareOffsetLocation
1366
- (for instance, OFF_DECK) then return None.
1565
+ Elements in this sequence contain instance IDs of things. The chain is valid only until the
1566
+ labware is moved.
1367
1567
  """
1368
- parent_location = self._labware.get_location(labware_id)
1568
+ return self.get_predicted_location_sequence(
1569
+ self._labware.get_location(labware_id)
1570
+ )
1369
1571
 
1370
- if isinstance(parent_location, DeckSlotLocation):
1371
- return LabwareOffsetLocation(
1372
- slotName=parent_location.slotName, moduleModel=None, definitionUri=None
1572
+ def get_predicted_location_sequence(
1573
+ self,
1574
+ labware_location: LabwareLocation,
1575
+ labware_pending_load: dict[str, LoadedLabware] | None = None,
1576
+ ) -> LabwareLocationSequence:
1577
+ """Get the location sequence for this location. Useful for a labware that hasn't been loaded."""
1578
+ return self._recurse_labware_location(
1579
+ labware_location, [], labware_pending_load or {}
1580
+ )
1581
+
1582
+ def _cutout_fixture_location_sequence_from_addressable_area(
1583
+ self, addressable_area_name: str
1584
+ ) -> OnCutoutFixtureLocationSequenceComponent:
1585
+ (
1586
+ cutout_id,
1587
+ potential_fixtures,
1588
+ ) = self._addressable_areas.get_current_potential_cutout_fixtures_for_addressable_area(
1589
+ addressable_area_name
1590
+ )
1591
+ return OnCutoutFixtureLocationSequenceComponent(
1592
+ possibleCutoutFixtureIds=sorted(
1593
+ [fixture.cutout_fixture_id for fixture in potential_fixtures]
1594
+ ),
1595
+ cutoutId=cutout_id,
1596
+ )
1597
+
1598
+ def _recurse_labware_location_from_aa_component(
1599
+ self,
1600
+ labware_location: AddressableAreaLocation,
1601
+ building: LabwareLocationSequence,
1602
+ ) -> LabwareLocationSequence:
1603
+ cutout_location = self._cutout_fixture_location_sequence_from_addressable_area(
1604
+ labware_location.addressableAreaName
1605
+ )
1606
+ # If the labware is loaded on an AA that is a module, we want to respect the convention
1607
+ # of giving it an OnModuleLocation.
1608
+ possible_module = self._modules.get_by_addressable_area(
1609
+ labware_location.addressableAreaName
1610
+ )
1611
+ if possible_module is not None:
1612
+ return building + [
1613
+ OnAddressableAreaLocationSequenceComponent(
1614
+ addressableAreaName=labware_location.addressableAreaName
1615
+ ),
1616
+ OnModuleLocationSequenceComponent(moduleId=possible_module.id),
1617
+ cutout_location,
1618
+ ]
1619
+ else:
1620
+ return building + [
1621
+ OnAddressableAreaLocationSequenceComponent(
1622
+ addressableAreaName=labware_location.addressableAreaName,
1623
+ ),
1624
+ cutout_location,
1625
+ ]
1626
+
1627
+ def _recurse_labware_location_from_module_component(
1628
+ self, labware_location: ModuleLocation, building: LabwareLocationSequence
1629
+ ) -> LabwareLocationSequence:
1630
+ module_id = labware_location.moduleId
1631
+ module_aa = self._modules.get_provided_addressable_area(module_id)
1632
+ base_location: (
1633
+ OnCutoutFixtureLocationSequenceComponent
1634
+ | NotOnDeckLocationSequenceComponent
1635
+ ) = self._cutout_fixture_location_sequence_from_addressable_area(module_aa)
1636
+
1637
+ if self._modules.get_deck_supports_module_fixtures():
1638
+ # On a deck with modules as cutout fixtures, we want, in order,
1639
+ # - the addressable area of the module
1640
+ # - the module with its module id, which is what clients want
1641
+ # - the cutout
1642
+ loc = self._modules.get_location(module_id)
1643
+ model = self._modules.get_connected_model(module_id)
1644
+ module_aa = self._modules.ensure_and_convert_module_fixture_location(
1645
+ loc.slotName, model
1373
1646
  )
1374
- elif isinstance(parent_location, ModuleLocation):
1375
- module_model = self._modules.get_requested_model(parent_location.moduleId)
1376
- module_location = self._modules.get_location(parent_location.moduleId)
1377
- return LabwareOffsetLocation(
1378
- slotName=module_location.slotName,
1379
- moduleModel=module_model,
1380
- definitionUri=None,
1647
+ return building + [
1648
+ OnAddressableAreaLocationSequenceComponent(
1649
+ addressableAreaName=module_aa
1650
+ ),
1651
+ OnModuleLocationSequenceComponent(moduleId=module_id),
1652
+ base_location,
1653
+ ]
1654
+ else:
1655
+ # If the module isn't a cutout fixture, then we want
1656
+ # - the module
1657
+ # - the addressable area the module is loaded on
1658
+ # - the cutout
1659
+ location = self._modules.get_location(module_id)
1660
+ return building + [
1661
+ OnModuleLocationSequenceComponent(moduleId=module_id),
1662
+ OnAddressableAreaLocationSequenceComponent(
1663
+ addressableAreaName=location.slotName.value
1664
+ ),
1665
+ base_location,
1666
+ ]
1667
+
1668
+ def _recurse_labware_location_from_stacker_hopper(
1669
+ self,
1670
+ labware_location: InStackerHopperLocation,
1671
+ building: LabwareLocationSequence,
1672
+ ) -> LabwareLocationSequence:
1673
+ loc = self._modules.get_location(labware_location.moduleId)
1674
+ model = self._modules.get_connected_model(labware_location.moduleId)
1675
+ module_aa = self._modules.ensure_and_convert_module_fixture_location(
1676
+ loc.slotName, model
1677
+ )
1678
+ cutout_base = self._cutout_fixture_location_sequence_from_addressable_area(
1679
+ module_aa
1680
+ )
1681
+ return building + [labware_location, cutout_base]
1682
+
1683
+ def _recurse_labware_location(
1684
+ self,
1685
+ labware_location: LabwareLocation,
1686
+ building: LabwareLocationSequence,
1687
+ labware_pending_load: dict[str, LoadedLabware],
1688
+ ) -> LabwareLocationSequence:
1689
+ if isinstance(labware_location, AddressableAreaLocation):
1690
+ return self._recurse_labware_location_from_aa_component(
1691
+ labware_location, building
1381
1692
  )
1382
- elif isinstance(parent_location, OnLabwareLocation):
1383
- non_labware_parent_location = self._labware.get_parent_location(labware_id)
1384
-
1385
- parent_uri = self._labware.get_definition_uri(parent_location.labwareId)
1386
- if isinstance(non_labware_parent_location, DeckSlotLocation):
1387
- return LabwareOffsetLocation(
1388
- slotName=non_labware_parent_location.slotName,
1389
- moduleModel=None,
1390
- definitionUri=parent_uri,
1391
- )
1392
- elif isinstance(non_labware_parent_location, ModuleLocation):
1393
- module_model = self._modules.get_requested_model(
1394
- non_labware_parent_location.moduleId
1395
- )
1396
- module_location = self._modules.get_location(
1397
- non_labware_parent_location.moduleId
1693
+ elif labware_location_is_off_deck(
1694
+ labware_location
1695
+ ) or labware_location_is_system(labware_location):
1696
+ return building + [
1697
+ NotOnDeckLocationSequenceComponent(logicalLocationName=labware_location)
1698
+ ]
1699
+
1700
+ elif isinstance(labware_location, OnLabwareLocation):
1701
+ labware = self._get_or_default_labware(
1702
+ labware_location.labwareId, labware_pending_load
1703
+ )
1704
+ return self._recurse_labware_location(
1705
+ labware.location,
1706
+ building
1707
+ + [
1708
+ OnLabwareLocationSequenceComponent(
1709
+ labwareId=labware_location.labwareId, lidId=labware.lid_id
1710
+ )
1711
+ ],
1712
+ labware_pending_load,
1713
+ )
1714
+ elif isinstance(labware_location, ModuleLocation):
1715
+ return self._recurse_labware_location_from_module_component(
1716
+ labware_location, building
1717
+ )
1718
+ elif isinstance(labware_location, DeckSlotLocation):
1719
+ return building + [
1720
+ OnAddressableAreaLocationSequenceComponent(
1721
+ addressableAreaName=labware_location.slotName.value,
1722
+ ),
1723
+ self._cutout_fixture_location_sequence_from_addressable_area(
1724
+ labware_location.slotName.value
1725
+ ),
1726
+ ]
1727
+ elif isinstance(labware_location, InStackerHopperLocation):
1728
+ return self._recurse_labware_location_from_stacker_hopper(
1729
+ labware_location, building
1730
+ )
1731
+ else:
1732
+ _LOG.warn(f"Unhandled labware location kind: {labware_location}")
1733
+ return building
1734
+
1735
+ def get_offset_location(
1736
+ self, labware_id: str
1737
+ ) -> Optional[LabwareOffsetLocationSequence]:
1738
+ """Provide the LegacyLabwareOffsetLocation specifying the current position of the labware.
1739
+
1740
+ If the labware is in a location that cannot be specified by a LabwareOffsetLocationSequence
1741
+ (for instance, OFF_DECK) then return None.
1742
+ """
1743
+ parent_location = self._labware.get_location(labware_id)
1744
+ return self.get_projected_offset_location(parent_location)
1745
+
1746
+ def get_projected_offset_location(
1747
+ self,
1748
+ labware_location: LabwareLocation,
1749
+ labware_pending_load: dict[str, LoadedLabware] | None = None,
1750
+ ) -> Optional[LabwareOffsetLocationSequence]:
1751
+ """Get the offset location that a labware loaded into this location would match."""
1752
+ return self._recurse_labware_offset_location(
1753
+ labware_location, [], labware_pending_load or {}
1754
+ )
1755
+
1756
+ def _recurse_labware_offset_location(
1757
+ self,
1758
+ labware_location: LabwareLocation,
1759
+ building: LabwareOffsetLocationSequence,
1760
+ labware_pending_load: dict[str, LoadedLabware],
1761
+ ) -> LabwareOffsetLocationSequence | None:
1762
+ if isinstance(labware_location, DeckSlotLocation):
1763
+ return building + [
1764
+ OnAddressableAreaOffsetLocationSequenceComponent(
1765
+ addressableAreaName=labware_location.slotName.value
1398
1766
  )
1399
- return LabwareOffsetLocation(
1400
- slotName=module_location.slotName,
1401
- moduleModel=module_model,
1402
- definitionUri=parent_uri,
1767
+ ]
1768
+
1769
+ elif isinstance(labware_location, ModuleLocation):
1770
+ module_id = labware_location.moduleId
1771
+ # Allow ModuleNotLoadedError to propagate.
1772
+ # Note also that we match based on the module's requested model, not its
1773
+ # actual model, to implement robot-server's documented HTTP API semantics.
1774
+ module_model = self._modules.get_requested_model(module_id=module_id)
1775
+
1776
+ # If `module_model is None`, it probably means that this module was added by
1777
+ # `ProtocolEngine.use_attached_modules()`, instead of an explicit
1778
+ # `loadModule` command.
1779
+ #
1780
+ # This assert should never raise in practice because:
1781
+ # 1. `ProtocolEngine.use_attached_modules()` is only used by
1782
+ # robot-server's "stateless command" endpoints, under `/commands`.
1783
+ # 2. Those endpoints don't support loading labware, so this code will
1784
+ # never run.
1785
+ #
1786
+ # Nevertheless, if it does happen somehow, we do NOT want to pass the
1787
+ # `None` value along to `LabwareView.find_applicable_labware_offset()`.
1788
+ # `None` means something different there, which will cause us to return
1789
+ # wrong results.
1790
+ assert module_model is not None, (
1791
+ "Can't find offsets for labware"
1792
+ " that are loaded on modules"
1793
+ " that were loaded with ProtocolEngine.use_attached_modules()."
1794
+ )
1795
+
1796
+ module_location = self._modules.get_location(module_id=module_id)
1797
+ if self._modules.get_deck_supports_module_fixtures():
1798
+ module_aa = self._modules.ensure_and_convert_module_fixture_location(
1799
+ module_location.slotName, module_model
1403
1800
  )
1801
+ else:
1802
+ module_aa = module_location.slotName.value
1803
+ return building + [
1804
+ OnModuleOffsetLocationSequenceComponent(moduleModel=module_model),
1805
+ OnAddressableAreaOffsetLocationSequenceComponent(
1806
+ addressableAreaName=module_aa
1807
+ ),
1808
+ ]
1404
1809
 
1405
- return None
1810
+ elif isinstance(labware_location, OnLabwareLocation):
1811
+ parent_labware_id = labware_location.labwareId
1812
+ parent_labware = self._get_or_default_labware(
1813
+ parent_labware_id, labware_pending_load
1814
+ )
1815
+ parent_labware_uri = LabwareUri(parent_labware.definitionUri)
1816
+ base_location = parent_labware.location
1817
+ return self._recurse_labware_offset_location(
1818
+ base_location,
1819
+ building
1820
+ + [
1821
+ OnLabwareOffsetLocationSequenceComponent(
1822
+ labwareUri=parent_labware_uri
1823
+ )
1824
+ ],
1825
+ labware_pending_load,
1826
+ )
1827
+
1828
+ else: # Off deck
1829
+ return None
1830
+
1831
+ def get_liquid_handling_z_change(
1832
+ self,
1833
+ labware_id: str,
1834
+ well_name: str,
1835
+ operation_volume: float,
1836
+ ) -> float:
1837
+ """Get the change in height from a liquid handling operation."""
1838
+ initial_handling_height = self.get_meniscus_height(
1839
+ labware_id=labware_id, well_name=well_name
1840
+ )
1841
+ final_height = self.get_well_height_after_liquid_handling(
1842
+ labware_id=labware_id,
1843
+ well_name=well_name,
1844
+ initial_height=initial_handling_height,
1845
+ volume=operation_volume,
1846
+ )
1847
+ # this function is only called by
1848
+ # HardwarePipetteHandler::aspirate/dispense while_tracking, and shouldn't
1849
+ # be reached in the case of a simulated liquid_probe
1850
+ assert not isinstance(initial_handling_height, SimulatedProbeResult)
1851
+ assert not isinstance(final_height, SimulatedProbeResult)
1852
+ return final_height - initial_handling_height
1406
1853
 
1407
1854
  def get_well_offset_adjustment(
1408
1855
  self,
@@ -1411,18 +1858,26 @@ class GeometryView:
1411
1858
  well_location: WellLocations,
1412
1859
  well_depth: float,
1413
1860
  operation_volume: Optional[float] = None,
1414
- ) -> float:
1861
+ ) -> LiquidTrackingType:
1415
1862
  """Return a z-axis distance that accounts for well handling height and operation volume.
1416
1863
 
1417
1864
  Distance is with reference to the well bottom.
1418
1865
  """
1419
1866
  # TODO(pbm, 10-23-24): refactor to smartly reduce height/volume conversions
1867
+
1420
1868
  initial_handling_height = self.get_well_handling_height(
1421
1869
  labware_id=labware_id,
1422
1870
  well_name=well_name,
1423
1871
  well_location=well_location,
1424
1872
  well_depth=well_depth,
1425
1873
  )
1874
+ # if we're tracking a MENISCUS origin, and targeting either the beginning
1875
+ # position of the liquid or doing dynamic tracking, return the initial height
1876
+ if (
1877
+ well_location.origin == WellOrigin.MENISCUS
1878
+ and not well_location.volumeOffset
1879
+ ):
1880
+ return initial_handling_height
1426
1881
  if isinstance(well_location, PickUpTipWellLocation):
1427
1882
  volume = 0.0
1428
1883
  elif isinstance(well_location.volumeOffset, float):
@@ -1431,32 +1886,85 @@ class GeometryView:
1431
1886
  volume = operation_volume or 0.0
1432
1887
 
1433
1888
  if volume:
1434
- return self.get_well_height_after_volume(
1889
+ liquid_height_after = self.get_well_height_after_liquid_handling(
1435
1890
  labware_id=labware_id,
1436
1891
  well_name=well_name,
1437
1892
  initial_height=initial_handling_height,
1438
1893
  volume=volume,
1439
1894
  )
1895
+ return liquid_height_after
1440
1896
  else:
1441
1897
  return initial_handling_height
1442
1898
 
1899
+ def get_current_well_volume(
1900
+ self,
1901
+ labware_id: str,
1902
+ well_name: str,
1903
+ ) -> LiquidTrackingType:
1904
+ """Returns most recently updated volume in specified well."""
1905
+ last_updated = self._wells.get_last_liquid_update(labware_id, well_name)
1906
+ if last_updated is None:
1907
+ raise errors.LiquidHeightUnknownError(
1908
+ "Must LiquidProbe or LoadLiquid before specifying WellOrigin.MENISCUS."
1909
+ )
1910
+
1911
+ well_liquid = self._wells.get_well_liquid_info(
1912
+ labware_id=labware_id, well_name=well_name
1913
+ )
1914
+ if (
1915
+ well_liquid.probed_height is not None
1916
+ and well_liquid.probed_height.height is not None
1917
+ and well_liquid.probed_height.last_probed == last_updated
1918
+ ):
1919
+ volume = self.get_well_volume_at_height(
1920
+ labware_id=labware_id,
1921
+ well_name=well_name,
1922
+ height=well_liquid.probed_height.height,
1923
+ )
1924
+ return volume
1925
+ elif (
1926
+ well_liquid.loaded_volume is not None
1927
+ and well_liquid.loaded_volume.volume is not None
1928
+ and well_liquid.loaded_volume.last_loaded == last_updated
1929
+ ):
1930
+ return well_liquid.loaded_volume.volume
1931
+ elif (
1932
+ well_liquid.probed_volume is not None
1933
+ and well_liquid.probed_volume.volume is not None
1934
+ and well_liquid.probed_volume.last_probed == last_updated
1935
+ ):
1936
+ return well_liquid.probed_volume.volume
1937
+ else:
1938
+ # This should not happen if there was an update but who knows
1939
+ raise errors.LiquidVolumeUnknownError(
1940
+ f"Unable to find liquid volume despite an update at {last_updated}."
1941
+ )
1942
+
1443
1943
  def get_meniscus_height(
1444
1944
  self,
1445
1945
  labware_id: str,
1446
1946
  well_name: str,
1447
- ) -> float:
1947
+ ) -> LiquidTrackingType:
1448
1948
  """Returns stored meniscus height in specified well."""
1949
+ last_updated = self._wells.get_last_liquid_update(labware_id, well_name)
1950
+ if last_updated is None:
1951
+ raise errors.LiquidHeightUnknownError(
1952
+ "Must LiquidProbe or LoadLiquid before specifying WellOrigin.MENISCUS."
1953
+ )
1954
+
1449
1955
  well_liquid = self._wells.get_well_liquid_info(
1450
1956
  labware_id=labware_id, well_name=well_name
1451
1957
  )
1452
1958
  if (
1453
1959
  well_liquid.probed_height is not None
1454
1960
  and well_liquid.probed_height.height is not None
1961
+ and well_liquid.probed_height.last_probed == last_updated
1455
1962
  ):
1456
1963
  return well_liquid.probed_height.height
1457
1964
  elif (
1458
1965
  well_liquid.loaded_volume is not None
1459
1966
  and well_liquid.loaded_volume.volume is not None
1967
+ and well_liquid.loaded_volume.last_loaded == last_updated
1460
1968
  ):
1461
1969
  return self.get_well_height_at_volume(
1462
1970
  labware_id=labware_id,
@@ -1466,6 +1974,7 @@ class GeometryView:
1466
1974
  elif (
1467
1975
  well_liquid.probed_volume is not None
1468
1976
  and well_liquid.probed_volume.volume is not None
1977
+ and well_liquid.probed_volume.last_probed == last_updated
1469
1978
  ):
1470
1979
  return self.get_well_height_at_volume(
1471
1980
  labware_id=labware_id,
@@ -1473,8 +1982,9 @@ class GeometryView:
1473
1982
  volume=well_liquid.probed_volume.volume,
1474
1983
  )
1475
1984
  else:
1985
+ # This should not happen if there was an update but who knows
1476
1986
  raise errors.LiquidHeightUnknownError(
1477
- "Must LiquidProbe or LoadLiquid before specifying WellOrigin.MENISCUS."
1987
+ f"Unable to find liquid height despite an update at {last_updated}."
1478
1988
  )
1479
1989
 
1480
1990
  def get_well_handling_height(
@@ -1483,22 +1993,26 @@ class GeometryView:
1483
1993
  well_name: str,
1484
1994
  well_location: WellLocations,
1485
1995
  well_depth: float,
1486
- ) -> float:
1996
+ ) -> LiquidTrackingType:
1487
1997
  """Return the handling height for a labware well (with reference to the well bottom)."""
1488
- handling_height = 0.0
1998
+ handling_height: LiquidTrackingType = 0.0
1489
1999
  if well_location.origin == WellOrigin.TOP:
1490
- handling_height = well_depth
2000
+ handling_height = float(well_depth)
1491
2001
  elif well_location.origin == WellOrigin.CENTER:
1492
- handling_height = well_depth / 2.0
2002
+ handling_height = float(well_depth / 2.0)
1493
2003
  elif well_location.origin == WellOrigin.MENISCUS:
1494
2004
  handling_height = self.get_meniscus_height(
1495
2005
  labware_id=labware_id, well_name=well_name
1496
2006
  )
1497
- return float(handling_height)
2007
+ return handling_height
1498
2008
 
1499
- def get_well_height_after_volume(
1500
- self, labware_id: str, well_name: str, initial_height: float, volume: float
1501
- ) -> float:
2009
+ def get_well_height_after_liquid_handling(
2010
+ self,
2011
+ labware_id: str,
2012
+ well_name: str,
2013
+ initial_height: LiquidTrackingType,
2014
+ volume: float,
2015
+ ) -> LiquidTrackingType:
1502
2016
  """Return the height of liquid in a labware well after a given volume has been handled.
1503
2017
 
1504
2018
  This is given an initial handling height, with reference to the well bottom.
@@ -1514,9 +2028,35 @@ class GeometryView:
1514
2028
  target_volume=final_volume, well_geometry=well_geometry
1515
2029
  )
1516
2030
 
2031
+ def get_well_height_after_liquid_handling_no_error(
2032
+ self,
2033
+ labware_id: str,
2034
+ well_name: str,
2035
+ initial_height: LiquidTrackingType,
2036
+ volume: float,
2037
+ ) -> LiquidTrackingType:
2038
+ """Return what the height of liquid in a labware well after liquid handling will be.
2039
+
2040
+ This raises no error if the value returned is an invalid physical location, so it should never be
2041
+ used for navigation, only for a pre-emptive estimate.
2042
+ """
2043
+ well_geometry = self._labware.get_well_geometry(
2044
+ labware_id=labware_id, well_name=well_name
2045
+ )
2046
+ initial_volume = find_volume_at_well_height(
2047
+ target_height=initial_height, well_geometry=well_geometry
2048
+ )
2049
+ final_volume = initial_volume + volume
2050
+ well_volume = find_height_at_well_volume(
2051
+ target_volume=final_volume,
2052
+ well_geometry=well_geometry,
2053
+ raise_error_if_result_invalid=False,
2054
+ )
2055
+ return well_volume
2056
+
1517
2057
  def get_well_height_at_volume(
1518
- self, labware_id: str, well_name: str, volume: float
1519
- ) -> float:
2058
+ self, labware_id: str, well_name: str, volume: LiquidTrackingType
2059
+ ) -> LiquidTrackingType:
1520
2060
  """Convert well volume to height."""
1521
2061
  well_geometry = self._labware.get_well_geometry(labware_id, well_name)
1522
2062
  return find_height_at_well_volume(
@@ -1524,8 +2064,11 @@ class GeometryView:
1524
2064
  )
1525
2065
 
1526
2066
  def get_well_volume_at_height(
1527
- self, labware_id: str, well_name: str, height: float
1528
- ) -> float:
2067
+ self,
2068
+ labware_id: str,
2069
+ well_name: str,
2070
+ height: LiquidTrackingType,
2071
+ ) -> LiquidTrackingType:
1529
2072
  """Convert well height to volume."""
1530
2073
  well_geometry = self._labware.get_well_geometry(labware_id, well_name)
1531
2074
  return find_volume_at_well_height(
@@ -1541,7 +2084,7 @@ class GeometryView:
1541
2084
  ) -> None:
1542
2085
  """Raise InvalidDispenseVolumeError if planned dispense volume will overflow well."""
1543
2086
  well_def = self._labware.get_well_definition(labware_id, well_name)
1544
- well_volumetric_capacity = well_def.totalLiquidVolume
2087
+ well_volumetric_capacity = float(well_def.totalLiquidVolume)
1545
2088
  if well_location.origin == WellOrigin.MENISCUS:
1546
2089
  # TODO(pbm, 10-23-24): refactor to smartly reduce height/volume conversions
1547
2090
  well_geometry = self._labware.get_well_geometry(labware_id, well_name)
@@ -1551,6 +2094,9 @@ class GeometryView:
1551
2094
  meniscus_volume = find_volume_at_well_height(
1552
2095
  target_height=meniscus_height, well_geometry=well_geometry
1553
2096
  )
2097
+ # if meniscus volume is a simulated value, comparisons aren't meaningful
2098
+ if isinstance(meniscus_volume, SimulatedProbeResult):
2099
+ return
1554
2100
  remaining_volume = well_volumetric_capacity - meniscus_volume
1555
2101
  if volume > remaining_volume:
1556
2102
  raise errors.InvalidDispenseVolumeError(
@@ -1605,3 +2151,40 @@ class GeometryView:
1605
2151
  target_well_name,
1606
2152
  self._labware.get_definition(labware_id).ordering,
1607
2153
  )
2154
+
2155
+ def get_height_of_labware_stack(
2156
+ self, definitions: list[LabwareDefinition]
2157
+ ) -> float:
2158
+ """Get the overall height of a stack of labware listed by definition in top-first order."""
2159
+ if len(definitions) == 0:
2160
+ return 0
2161
+ if len(definitions) == 1:
2162
+ return definitions[0].dimensions.zDimension
2163
+ total_height = 0.0
2164
+ upper_def: LabwareDefinition = definitions[0]
2165
+ for lower_def in definitions[1:]:
2166
+ overlap = self._labware.get_labware_overlap_offsets(
2167
+ upper_def, lower_def.parameters.loadName
2168
+ ).z
2169
+ total_height += upper_def.dimensions.zDimension - overlap
2170
+ upper_def = lower_def
2171
+ return total_height + upper_def.dimensions.zDimension
2172
+
2173
+ def get_height_of_stacker_labware_pool(self, module_id: str) -> float:
2174
+ """Get the overall height of a stack of labware in a Stacker module."""
2175
+ stacker = self._modules.get_flex_stacker_substate(module_id)
2176
+ pool_list = stacker.get_pool_definition_ordered_list()
2177
+ if not pool_list:
2178
+ return 0.0
2179
+ return self.get_height_of_labware_stack(pool_list)
2180
+
2181
+ def _get_or_default_labware(
2182
+ self, labware_id: str, pending_labware: dict[str, LoadedLabware]
2183
+ ) -> LoadedLabware:
2184
+ try:
2185
+ return self._labware.get(labware_id)
2186
+ except LabwareNotLoadedError as lnle:
2187
+ try:
2188
+ return pending_labware[labware_id]
2189
+ except KeyError as ke:
2190
+ raise lnle from ke