opentrons 8.3.2__py2.py3-none-any.whl → 8.4.0__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 (196) 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 +102 -5
  39. opentrons/legacy_commands/helpers.py +74 -1
  40. opentrons/legacy_commands/types.py +33 -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 +1356 -107
  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/pipette_movement_conflict.py +6 -14
  52. opentrons/protocol_api/core/engine/protocol.py +253 -11
  53. opentrons/protocol_api/core/engine/stringify.py +19 -8
  54. opentrons/protocol_api/core/engine/transfer_components_executor.py +858 -0
  55. opentrons/protocol_api/core/engine/well.py +73 -5
  56. opentrons/protocol_api/core/instrument.py +71 -21
  57. opentrons/protocol_api/core/labware.py +6 -2
  58. opentrons/protocol_api/core/legacy/labware_offset_provider.py +7 -3
  59. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +76 -49
  60. opentrons/protocol_api/core/legacy/legacy_labware_core.py +8 -4
  61. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +36 -0
  62. opentrons/protocol_api/core/legacy/legacy_well_core.py +27 -2
  63. opentrons/protocol_api/core/legacy/load_info.py +4 -12
  64. opentrons/protocol_api/core/legacy/module_geometry.py +6 -1
  65. opentrons/protocol_api/core/legacy/well_geometry.py +3 -3
  66. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +73 -23
  67. opentrons/protocol_api/core/module.py +43 -0
  68. opentrons/protocol_api/core/protocol.py +33 -0
  69. opentrons/protocol_api/core/well.py +23 -2
  70. opentrons/protocol_api/instrument_context.py +454 -150
  71. opentrons/protocol_api/labware.py +98 -50
  72. opentrons/protocol_api/module_contexts.py +140 -0
  73. opentrons/protocol_api/protocol_context.py +163 -19
  74. opentrons/protocol_api/validation.py +51 -41
  75. opentrons/protocol_engine/__init__.py +21 -2
  76. opentrons/protocol_engine/actions/actions.py +5 -5
  77. opentrons/protocol_engine/clients/sync_client.py +6 -0
  78. opentrons/protocol_engine/commands/__init__.py +66 -36
  79. opentrons/protocol_engine/commands/absorbance_reader/__init__.py +0 -1
  80. opentrons/protocol_engine/commands/air_gap_in_place.py +3 -2
  81. opentrons/protocol_engine/commands/aspirate.py +6 -2
  82. opentrons/protocol_engine/commands/aspirate_in_place.py +3 -1
  83. opentrons/protocol_engine/commands/aspirate_while_tracking.py +210 -0
  84. opentrons/protocol_engine/commands/blow_out.py +2 -0
  85. opentrons/protocol_engine/commands/blow_out_in_place.py +4 -1
  86. opentrons/protocol_engine/commands/command_unions.py +102 -33
  87. opentrons/protocol_engine/commands/configure_for_volume.py +3 -0
  88. opentrons/protocol_engine/commands/dispense.py +3 -1
  89. opentrons/protocol_engine/commands/dispense_in_place.py +3 -0
  90. opentrons/protocol_engine/commands/dispense_while_tracking.py +204 -0
  91. opentrons/protocol_engine/commands/drop_tip.py +23 -1
  92. opentrons/protocol_engine/commands/flex_stacker/__init__.py +106 -0
  93. opentrons/protocol_engine/commands/flex_stacker/close_latch.py +72 -0
  94. opentrons/protocol_engine/commands/flex_stacker/common.py +15 -0
  95. opentrons/protocol_engine/commands/flex_stacker/empty.py +161 -0
  96. opentrons/protocol_engine/commands/flex_stacker/fill.py +164 -0
  97. opentrons/protocol_engine/commands/flex_stacker/open_latch.py +70 -0
  98. opentrons/protocol_engine/commands/flex_stacker/prepare_shuttle.py +112 -0
  99. opentrons/protocol_engine/commands/flex_stacker/retrieve.py +394 -0
  100. opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +190 -0
  101. opentrons/protocol_engine/commands/flex_stacker/store.py +291 -0
  102. opentrons/protocol_engine/commands/generate_command_schema.py +31 -2
  103. opentrons/protocol_engine/commands/labware_handling_common.py +29 -0
  104. opentrons/protocol_engine/commands/liquid_probe.py +27 -13
  105. opentrons/protocol_engine/commands/load_labware.py +42 -39
  106. opentrons/protocol_engine/commands/load_lid.py +21 -13
  107. opentrons/protocol_engine/commands/load_lid_stack.py +130 -47
  108. opentrons/protocol_engine/commands/load_module.py +18 -17
  109. opentrons/protocol_engine/commands/load_pipette.py +3 -0
  110. opentrons/protocol_engine/commands/move_labware.py +139 -20
  111. opentrons/protocol_engine/commands/move_to_well.py +5 -11
  112. opentrons/protocol_engine/commands/pick_up_tip.py +5 -2
  113. opentrons/protocol_engine/commands/pipetting_common.py +159 -8
  114. opentrons/protocol_engine/commands/prepare_to_aspirate.py +15 -5
  115. opentrons/protocol_engine/commands/{evotip_dispense.py → pressure_dispense.py} +33 -34
  116. opentrons/protocol_engine/commands/reload_labware.py +6 -19
  117. opentrons/protocol_engine/commands/{evotip_seal_pipette.py → seal_pipette_to_tip.py} +97 -76
  118. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +3 -1
  119. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +6 -1
  120. opentrons/protocol_engine/commands/{evotip_unseal_pipette.py → unseal_pipette_from_tip.py} +31 -40
  121. opentrons/protocol_engine/errors/__init__.py +10 -0
  122. opentrons/protocol_engine/errors/exceptions.py +62 -0
  123. opentrons/protocol_engine/execution/equipment.py +123 -106
  124. opentrons/protocol_engine/execution/labware_movement.py +8 -6
  125. opentrons/protocol_engine/execution/pipetting.py +235 -25
  126. opentrons/protocol_engine/execution/tip_handler.py +82 -32
  127. opentrons/protocol_engine/labware_offset_standardization.py +194 -0
  128. opentrons/protocol_engine/protocol_engine.py +22 -13
  129. opentrons/protocol_engine/resources/deck_configuration_provider.py +98 -2
  130. opentrons/protocol_engine/resources/deck_data_provider.py +1 -1
  131. opentrons/protocol_engine/resources/labware_data_provider.py +32 -12
  132. opentrons/protocol_engine/resources/labware_validation.py +7 -5
  133. opentrons/protocol_engine/slot_standardization.py +11 -23
  134. opentrons/protocol_engine/state/addressable_areas.py +84 -46
  135. opentrons/protocol_engine/state/frustum_helpers.py +36 -14
  136. opentrons/protocol_engine/state/geometry.py +892 -227
  137. opentrons/protocol_engine/state/labware.py +252 -55
  138. opentrons/protocol_engine/state/module_substates/__init__.py +4 -0
  139. opentrons/protocol_engine/state/module_substates/flex_stacker_substate.py +68 -0
  140. opentrons/protocol_engine/state/module_substates/heater_shaker_module_substate.py +22 -0
  141. opentrons/protocol_engine/state/module_substates/temperature_module_substate.py +13 -0
  142. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +20 -0
  143. opentrons/protocol_engine/state/modules.py +210 -67
  144. opentrons/protocol_engine/state/pipettes.py +54 -0
  145. opentrons/protocol_engine/state/state.py +1 -1
  146. opentrons/protocol_engine/state/tips.py +14 -0
  147. opentrons/protocol_engine/state/update_types.py +180 -25
  148. opentrons/protocol_engine/state/wells.py +55 -9
  149. opentrons/protocol_engine/types/__init__.py +300 -0
  150. opentrons/protocol_engine/types/automatic_tip_selection.py +39 -0
  151. opentrons/protocol_engine/types/command_annotations.py +53 -0
  152. opentrons/protocol_engine/types/deck_configuration.py +72 -0
  153. opentrons/protocol_engine/types/execution.py +96 -0
  154. opentrons/protocol_engine/types/hardware_passthrough.py +25 -0
  155. opentrons/protocol_engine/types/instrument.py +47 -0
  156. opentrons/protocol_engine/types/instrument_sensors.py +47 -0
  157. opentrons/protocol_engine/types/labware.py +111 -0
  158. opentrons/protocol_engine/types/labware_movement.py +22 -0
  159. opentrons/protocol_engine/types/labware_offset_location.py +111 -0
  160. opentrons/protocol_engine/types/labware_offset_vector.py +33 -0
  161. opentrons/protocol_engine/types/liquid.py +40 -0
  162. opentrons/protocol_engine/types/liquid_class.py +59 -0
  163. opentrons/protocol_engine/types/liquid_handling.py +13 -0
  164. opentrons/protocol_engine/types/liquid_level_detection.py +131 -0
  165. opentrons/protocol_engine/types/location.py +194 -0
  166. opentrons/protocol_engine/types/module.py +301 -0
  167. opentrons/protocol_engine/types/partial_tip_configuration.py +76 -0
  168. opentrons/protocol_engine/types/run_time_parameters.py +133 -0
  169. opentrons/protocol_engine/types/tip.py +18 -0
  170. opentrons/protocol_engine/types/util.py +21 -0
  171. opentrons/protocol_engine/types/well_position.py +124 -0
  172. opentrons/protocol_reader/extract_labware_definitions.py +7 -3
  173. opentrons/protocol_reader/file_format_validator.py +5 -3
  174. opentrons/protocol_runner/json_translator.py +4 -2
  175. opentrons/protocol_runner/legacy_command_mapper.py +6 -2
  176. opentrons/protocol_runner/run_orchestrator.py +4 -1
  177. opentrons/protocols/advanced_control/transfers/common.py +48 -1
  178. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +204 -0
  179. opentrons/protocols/api_support/definitions.py +1 -1
  180. opentrons/protocols/api_support/instrument.py +16 -3
  181. opentrons/protocols/labware.py +27 -23
  182. opentrons/protocols/models/__init__.py +0 -21
  183. opentrons/simulate.py +4 -2
  184. opentrons/types.py +20 -7
  185. opentrons/util/logging_config.py +94 -25
  186. opentrons/util/logging_queue_handler.py +61 -0
  187. {opentrons-8.3.2.dist-info → opentrons-8.4.0.dist-info}/METADATA +4 -4
  188. {opentrons-8.3.2.dist-info → opentrons-8.4.0.dist-info}/RECORD +192 -151
  189. opentrons/calibration_storage/ot2/models/defaults.py +0 -0
  190. opentrons/calibration_storage/ot3/models/defaults.py +0 -0
  191. opentrons/protocol_api/core/legacy/legacy_robot_core.py +0 -0
  192. opentrons/protocol_engine/types.py +0 -1311
  193. {opentrons-8.3.2.dist-info → opentrons-8.4.0.dist-info}/LICENSE +0 -0
  194. {opentrons-8.3.2.dist-info → opentrons-8.4.0.dist-info}/WHEEL +0 -0
  195. {opentrons-8.3.2.dist-info → opentrons-8.4.0.dist-info}/entry_points.txt +0 -0
  196. {opentrons-8.3.2.dist-info → opentrons-8.4.0.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,5 @@
1
1
  """Basic addressable area data state and store."""
2
+
2
3
  from dataclasses import dataclass
3
4
  from functools import cached_property
4
5
  from typing import Dict, List, Optional, Set
@@ -112,43 +113,6 @@ def _get_conflicting_addressable_areas_error_string(
112
113
  return ", ".join(display_names)
113
114
 
114
115
 
115
- # This is a temporary shim while Protocol Engine's conflict-checking code
116
- # can only take deck slots as input.
117
- # Long-term solution: Check for conflicts based on bounding boxes, not slot adjacencies.
118
- # Shorter-term: Change the conflict-checking code to take cutouts instead of deck slots.
119
- CUTOUT_TO_DECK_SLOT_MAP: Dict[str, DeckSlotName] = {
120
- # OT-2
121
- "cutout1": DeckSlotName.SLOT_1,
122
- "cutout2": DeckSlotName.SLOT_2,
123
- "cutout3": DeckSlotName.SLOT_3,
124
- "cutout4": DeckSlotName.SLOT_4,
125
- "cutout5": DeckSlotName.SLOT_5,
126
- "cutout6": DeckSlotName.SLOT_6,
127
- "cutout7": DeckSlotName.SLOT_7,
128
- "cutout8": DeckSlotName.SLOT_8,
129
- "cutout9": DeckSlotName.SLOT_9,
130
- "cutout10": DeckSlotName.SLOT_10,
131
- "cutout11": DeckSlotName.SLOT_11,
132
- "cutout12": DeckSlotName.FIXED_TRASH,
133
- # Flex
134
- "cutoutA1": DeckSlotName.SLOT_A1,
135
- "cutoutA2": DeckSlotName.SLOT_A2,
136
- "cutoutA3": DeckSlotName.SLOT_A3,
137
- "cutoutB1": DeckSlotName.SLOT_B1,
138
- "cutoutB2": DeckSlotName.SLOT_B2,
139
- "cutoutB3": DeckSlotName.SLOT_B3,
140
- "cutoutC1": DeckSlotName.SLOT_C1,
141
- "cutoutC2": DeckSlotName.SLOT_C2,
142
- "cutoutC3": DeckSlotName.SLOT_C3,
143
- "cutoutD1": DeckSlotName.SLOT_D1,
144
- "cutoutD2": DeckSlotName.SLOT_D2,
145
- "cutoutD3": DeckSlotName.SLOT_D3,
146
- }
147
- DECK_SLOT_TO_CUTOUT_MAP = {
148
- deck_slot: cutout for cutout, deck_slot in CUTOUT_TO_DECK_SLOT_MAP.items()
149
- }
150
-
151
-
152
116
  class AddressableAreaStore(HasState[AddressableAreaState], HandlesActions):
153
117
  """Addressable area state container."""
154
118
 
@@ -221,13 +185,11 @@ class AddressableAreaStore(HasState[AddressableAreaState], HandlesActions):
221
185
  cutout_position = deck_configuration_provider.get_cutout_position(
222
186
  cutout_id, deck_definition
223
187
  )
224
- base_slot = CUTOUT_TO_DECK_SLOT_MAP[cutout_id]
225
188
  for addressable_area_name in provided_addressable_areas:
226
189
  addressable_areas.append(
227
190
  deck_configuration_provider.get_addressable_area_from_name(
228
191
  addressable_area_name=addressable_area_name,
229
192
  cutout_position=cutout_position,
230
- base_slot=base_slot,
231
193
  deck_definition=deck_definition,
232
194
  )
233
195
  )
@@ -242,12 +204,10 @@ class AddressableAreaStore(HasState[AddressableAreaState], HandlesActions):
242
204
  cutout_position = deck_configuration_provider.get_cutout_position(
243
205
  cutout_id, self._state.deck_definition
244
206
  )
245
- base_slot = CUTOUT_TO_DECK_SLOT_MAP[cutout_id]
246
207
  addressable_area = (
247
208
  deck_configuration_provider.get_addressable_area_from_name(
248
209
  addressable_area_name=addressable_area_name,
249
210
  cutout_position=cutout_position,
250
- base_slot=base_slot,
251
211
  deck_definition=self._state.deck_definition,
252
212
  )
253
213
  )
@@ -300,6 +260,11 @@ class AddressableAreaView:
300
260
  """
301
261
  self._state = state
302
262
 
263
+ @cached_property
264
+ def deck_definition(self) -> DeckDefinitionV5:
265
+ """The full deck definition."""
266
+ return self._state.deck_definition
267
+
303
268
  @cached_property
304
269
  def deck_extents(self) -> Point:
305
270
  """The maximum space on the deck."""
@@ -426,11 +391,9 @@ class AddressableAreaView:
426
391
  cutout_position = deck_configuration_provider.get_cutout_position(
427
392
  cutout_id, self._state.deck_definition
428
393
  )
429
- base_slot = CUTOUT_TO_DECK_SLOT_MAP[cutout_id]
430
394
  return deck_configuration_provider.get_addressable_area_from_name(
431
395
  addressable_area_name=addressable_area_name,
432
396
  cutout_position=cutout_position,
433
- base_slot=base_slot,
434
397
  deck_definition=self._state.deck_definition,
435
398
  )
436
399
 
@@ -526,7 +489,7 @@ class AddressableAreaView:
526
489
 
527
490
  def get_cutout_id_by_deck_slot_name(self, slot_name: DeckSlotName) -> str:
528
491
  """Get the Cutout ID of a given Deck Slot by Deck Slot Name."""
529
- return DECK_SLOT_TO_CUTOUT_MAP[slot_name]
492
+ return deck_configuration_provider.get_cutout_id_by_deck_slot_name(slot_name)
530
493
 
531
494
  def get_fixture_by_deck_slot_name(
532
495
  self, slot_name: DeckSlotName
@@ -534,7 +497,9 @@ class AddressableAreaView:
534
497
  """Get the Cutout Fixture currently loaded where a specific Deck Slot would be."""
535
498
  deck_config = self._state.deck_configuration
536
499
  if deck_config:
537
- slot_cutout_id = DECK_SLOT_TO_CUTOUT_MAP[slot_name]
500
+ slot_cutout_id = (
501
+ deck_configuration_provider.get_cutout_id_by_deck_slot_name(slot_name)
502
+ )
538
503
  slot_cutout_fixture = None
539
504
  # This will only ever be one under current assumptions
540
505
  for (
@@ -571,7 +536,9 @@ class AddressableAreaView:
571
536
  """Get the serial number provided by the deck configuration for a Fixture at a given location."""
572
537
  deck_config = self._state.deck_configuration
573
538
  if deck_config:
574
- slot_cutout_id = DECK_SLOT_TO_CUTOUT_MAP[slot_name]
539
+ slot_cutout_id = (
540
+ deck_configuration_provider.get_cutout_id_by_deck_slot_name(slot_name)
541
+ )
575
542
  # This will only ever be one under current assumptions
576
543
  for (
577
544
  cutout_id,
@@ -582,6 +549,44 @@ class AddressableAreaView:
582
549
  return opentrons_module_serial_number
583
550
  return None
584
551
 
552
+ def get_serial_number_by_cutout_id(self, slot_cutout_id: str) -> str | None:
553
+ """Gets serial number from deck at a given cutout ID if one exists."""
554
+ deck_config = self._state.deck_configuration
555
+ if deck_config:
556
+ for (
557
+ cutout_id,
558
+ cutout_fixture_id,
559
+ opentrons_module_serial_number,
560
+ ) in deck_config:
561
+ if cutout_id == slot_cutout_id:
562
+ return opentrons_module_serial_number
563
+ return None
564
+
565
+ def get_fixture_serial_from_deck_configuration_by_addressable_area(
566
+ self, addressable_area_name: str
567
+ ) -> Optional[str]:
568
+ """Get the serial number provided by the deck configuration for a Fixture that provides a given addressable area."""
569
+ deck_config = self._state.deck_configuration
570
+ if deck_config:
571
+ potential_fixtures = (
572
+ deck_configuration_provider.get_potential_cutout_fixtures(
573
+ addressable_area_name, self._state.deck_definition
574
+ )
575
+ )
576
+ slot_cutout_id = potential_fixtures[0]
577
+ fixture_ids = [
578
+ fixture.cutout_fixture_id for fixture in potential_fixtures[1]
579
+ ]
580
+ # This will only ever be one under current assumptions
581
+ for (
582
+ cutout_id,
583
+ cutout_fixture_id,
584
+ opentrons_module_serial_number,
585
+ ) in deck_config:
586
+ if cutout_id == slot_cutout_id and cutout_fixture_id in fixture_ids:
587
+ return opentrons_module_serial_number
588
+ return None
589
+
585
590
  def get_slot_definition(self, slot_id: str) -> SlotDefV3:
586
591
  """Get the definition of a slot in the deck.
587
592
 
@@ -658,3 +663,36 @@ class AddressableAreaView:
658
663
  raise AreaNotInDeckConfigurationError(
659
664
  f"{addressable_area_name} not provided by deck configuration."
660
665
  )
666
+
667
+ def get_current_potential_cutout_fixtures_for_addressable_area(
668
+ self, addressable_area_name: str
669
+ ) -> tuple[str, Set[PotentialCutoutFixture]]:
670
+ """Get the set of cutout fixtures that might provide a given addressable area.
671
+
672
+ This takes into account the constraints already established by load commands or by a loaded deck
673
+ configuration, and may therefore return different results for the same addressable area at
674
+ different points in the protocol after deck configuration constraints have changed.
675
+
676
+ This returns the common cutout id and the potential fixtures.
677
+ """
678
+ (
679
+ cutout_id,
680
+ base_potential_fixtures,
681
+ ) = deck_configuration_provider.get_potential_cutout_fixtures(
682
+ addressable_area_name, self._state.deck_definition
683
+ )
684
+ try:
685
+ loaded_potential_fixtures = (
686
+ self._state.potential_cutout_fixtures_by_cutout_id[cutout_id]
687
+ )
688
+ return cutout_id, loaded_potential_fixtures.intersection(
689
+ base_potential_fixtures
690
+ )
691
+ except KeyError:
692
+ # If there was a key error here, it's because this function was (eventually) called
693
+ # from the body of a command implementation whose state update will load the
694
+ # addressable area it's querying... but that state update has not been submitted
695
+ # and processed, so nothing has created the entry for this cutout id yet. Do what
696
+ # we'll do when we actually get to that state update, which is apply the base
697
+ # potential fixtures from the deck def.
698
+ return cutout_id, base_potential_fixtures
@@ -5,6 +5,10 @@ from math import isclose
5
5
 
6
6
  from ..errors.exceptions import InvalidLiquidHeightFound
7
7
 
8
+ from opentrons.protocol_engine.types.liquid_level_detection import (
9
+ LiquidTrackingType,
10
+ SimulatedProbeResult,
11
+ )
8
12
  from opentrons_shared_data.labware.labware_definition import (
9
13
  InnerWellGeometry,
10
14
  WellSegment,
@@ -241,9 +245,8 @@ def _get_segment_capacity(segment: WellSegment) -> float:
241
245
  def get_well_volumetric_capacity(
242
246
  well_geometry: InnerWellGeometry,
243
247
  ) -> List[Tuple[float, float]]:
244
- """Return the total volumetric capacity of a well as a map of height borders to volume."""
245
- # dictionary map of heights to volumetric capacities within their respective segment
246
- # {top_height_0: volume_0, top_height_1: volume_1, top_height_2: volume_2}
248
+ """Return the volumetric capacity of a well as a list of pairs relating segment heights to volumes."""
249
+ # [(top_height_0, section_0_volume), (top_height_1, section_1_volume), ...]
247
250
  well_volume = []
248
251
 
249
252
  # get the well segments sorted in ascending order
@@ -341,7 +344,7 @@ def _find_volume_in_partial_frustum(
341
344
  ) -> float:
342
345
  """Look through a sorted list of frusta for a target height, and find the volume at that height."""
343
346
  for segment in sorted_well:
344
- if segment.bottomHeight < target_height < segment.topHeight:
347
+ if segment.bottomHeight <= target_height <= segment.topHeight:
345
348
  relative_target_height = target_height - segment.bottomHeight
346
349
  section_height = segment.topHeight - segment.bottomHeight
347
350
  return volume_at_height_within_section(
@@ -350,19 +353,26 @@ def _find_volume_in_partial_frustum(
350
353
  section_height=section_height,
351
354
  )
352
355
  # if we've looked through all sections and can't find the target volume, raise an error
356
+ # this code should never be reached- an error should be raised by find_volume_at_well_height
353
357
  raise InvalidLiquidHeightFound(
354
- f"Unable to find volume at given well-height {target_height}."
358
+ f"Target height {target_height} mm exceeds the well height."
355
359
  )
356
360
 
357
361
 
358
362
  def find_volume_at_well_height(
359
- target_height: float, well_geometry: InnerWellGeometry
360
- ) -> float:
363
+ target_height: LiquidTrackingType,
364
+ well_geometry: InnerWellGeometry,
365
+ ) -> LiquidTrackingType:
361
366
  """Find the volume within a well, at a known height."""
367
+ # comparisons with SimulatedProbeResult objects aren't meaningful, just return
368
+ if isinstance(target_height, SimulatedProbeResult):
369
+ return target_height
362
370
  volumetric_capacity = get_well_volumetric_capacity(well_geometry)
363
371
  max_height = volumetric_capacity[-1][0]
364
372
  if target_height < 0 or target_height > max_height:
365
- raise InvalidLiquidHeightFound("Invalid target height.")
373
+ raise InvalidLiquidHeightFound(
374
+ f"Invalid target height {target_height} mm; max well height is {max_height} mm."
375
+ )
366
376
  # volumes in volumetric_capacity are relative to each frustum,
367
377
  # so we have to find the volume of all the full sections enclosed
368
378
  # beneath the target height
@@ -395,7 +405,7 @@ def _find_height_in_partial_frustum(
395
405
  section_top_height, section_volume = capacity
396
406
  if (
397
407
  bottom_section_volume
398
- < target_volume
408
+ <= target_volume
399
409
  <= (bottom_section_volume + section_volume)
400
410
  ):
401
411
  relative_target_volume = target_volume - bottom_section_volume
@@ -410,21 +420,33 @@ def _find_height_in_partial_frustum(
410
420
  # viewed section
411
421
  bottom_section_volume += section_volume
412
422
 
423
+ # if we finish looping through the whole well, bottom_section will be the well's volume
424
+ total_well_volume = bottom_section_volume
413
425
  # if we've looked through all sections and can't find the target volume, raise an error
426
+ # also this code should never be reached bc an invalid target volume should be changed
427
+ # by find_height_at_well_volume
414
428
  raise InvalidLiquidHeightFound(
415
- f"Unable to find height at given volume {target_volume}."
429
+ f"Target volume {target_volume} uL exceeds the well volume {total_well_volume} uL."
416
430
  )
417
431
 
418
432
 
419
433
  def find_height_at_well_volume(
420
- target_volume: float, well_geometry: InnerWellGeometry
421
- ) -> float:
434
+ target_volume: LiquidTrackingType,
435
+ well_geometry: InnerWellGeometry,
436
+ ) -> LiquidTrackingType:
422
437
  """Find the height within a well, at a known volume."""
438
+ # comparisons with SimulatedProbeResult objects aren't meaningful, just
439
+ # return if we have one of those
440
+ if isinstance(target_volume, SimulatedProbeResult):
441
+ return target_volume
442
+
423
443
  volumetric_capacity = get_well_volumetric_capacity(well_geometry)
424
444
  max_volume = sum(row[1] for row in volumetric_capacity)
425
- if target_volume < 0 or target_volume > max_volume:
426
- raise InvalidLiquidHeightFound("Invalid target volume.")
427
445
 
446
+ if target_volume < 0:
447
+ target_volume = 0
448
+ elif target_volume > max_volume:
449
+ target_volume = max_volume
428
450
  sorted_well = sorted(well_geometry.sections, key=lambda section: section.topHeight)
429
451
  # find the section the target volume is in and compute the height
430
452
  return _find_height_in_partial_frustum(