opentrons 8.1.0a0__py2.py3-none-any.whl → 8.2.0a0__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 (230) hide show
  1. opentrons/cli/analyze.py +71 -7
  2. opentrons/config/__init__.py +9 -0
  3. opentrons/config/advanced_settings.py +22 -0
  4. opentrons/config/defaults_ot3.py +14 -36
  5. opentrons/config/feature_flags.py +4 -0
  6. opentrons/config/types.py +6 -17
  7. opentrons/drivers/absorbance_reader/abstract.py +27 -3
  8. opentrons/drivers/absorbance_reader/async_byonoy.py +207 -154
  9. opentrons/drivers/absorbance_reader/driver.py +24 -15
  10. opentrons/drivers/absorbance_reader/hid_protocol.py +79 -50
  11. opentrons/drivers/absorbance_reader/simulator.py +32 -6
  12. opentrons/drivers/types.py +23 -1
  13. opentrons/execute.py +2 -2
  14. opentrons/hardware_control/api.py +18 -10
  15. opentrons/hardware_control/backends/controller.py +3 -2
  16. opentrons/hardware_control/backends/flex_protocol.py +11 -5
  17. opentrons/hardware_control/backends/ot3controller.py +18 -50
  18. opentrons/hardware_control/backends/ot3simulator.py +7 -6
  19. opentrons/hardware_control/instruments/ot2/pipette_handler.py +22 -82
  20. opentrons/hardware_control/instruments/ot3/pipette_handler.py +10 -2
  21. opentrons/hardware_control/module_control.py +43 -2
  22. opentrons/hardware_control/modules/__init__.py +7 -1
  23. opentrons/hardware_control/modules/absorbance_reader.py +230 -83
  24. opentrons/hardware_control/modules/errors.py +7 -0
  25. opentrons/hardware_control/modules/heater_shaker.py +8 -3
  26. opentrons/hardware_control/modules/magdeck.py +12 -3
  27. opentrons/hardware_control/modules/mod_abc.py +27 -2
  28. opentrons/hardware_control/modules/tempdeck.py +15 -7
  29. opentrons/hardware_control/modules/thermocycler.py +69 -3
  30. opentrons/hardware_control/modules/types.py +11 -5
  31. opentrons/hardware_control/modules/update.py +11 -5
  32. opentrons/hardware_control/modules/utils.py +3 -1
  33. opentrons/hardware_control/ot3_calibration.py +6 -6
  34. opentrons/hardware_control/ot3api.py +126 -89
  35. opentrons/hardware_control/poller.py +15 -11
  36. opentrons/hardware_control/protocols/__init__.py +1 -7
  37. opentrons/hardware_control/protocols/instrument_configurer.py +14 -2
  38. opentrons/hardware_control/protocols/liquid_handler.py +5 -0
  39. opentrons/motion_planning/__init__.py +2 -0
  40. opentrons/motion_planning/waypoints.py +32 -0
  41. opentrons/protocol_api/__init__.py +2 -1
  42. opentrons/protocol_api/_liquid.py +87 -1
  43. opentrons/protocol_api/_parameter_context.py +10 -1
  44. opentrons/protocol_api/core/engine/deck_conflict.py +0 -297
  45. opentrons/protocol_api/core/engine/instrument.py +29 -25
  46. opentrons/protocol_api/core/engine/labware.py +10 -2
  47. opentrons/protocol_api/core/engine/module_core.py +129 -17
  48. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +355 -0
  49. opentrons/protocol_api/core/engine/protocol.py +55 -2
  50. opentrons/protocol_api/core/instrument.py +2 -0
  51. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +2 -0
  52. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +5 -2
  53. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +2 -0
  54. opentrons/protocol_api/core/module.py +22 -4
  55. opentrons/protocol_api/core/protocol.py +5 -2
  56. opentrons/protocol_api/instrument_context.py +52 -20
  57. opentrons/protocol_api/labware.py +13 -1
  58. opentrons/protocol_api/module_contexts.py +68 -13
  59. opentrons/protocol_api/protocol_context.py +38 -4
  60. opentrons/protocol_api/validation.py +5 -3
  61. opentrons/protocol_engine/__init__.py +10 -9
  62. opentrons/protocol_engine/actions/__init__.py +5 -0
  63. opentrons/protocol_engine/actions/actions.py +42 -25
  64. opentrons/protocol_engine/actions/get_state_update.py +38 -0
  65. opentrons/protocol_engine/clients/sync_client.py +7 -1
  66. opentrons/protocol_engine/clients/transports.py +1 -1
  67. opentrons/protocol_engine/commands/__init__.py +0 -4
  68. opentrons/protocol_engine/commands/absorbance_reader/__init__.py +41 -11
  69. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +161 -0
  70. opentrons/protocol_engine/commands/absorbance_reader/initialize.py +53 -9
  71. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +160 -0
  72. opentrons/protocol_engine/commands/absorbance_reader/read.py +196 -0
  73. opentrons/protocol_engine/commands/aspirate.py +29 -16
  74. opentrons/protocol_engine/commands/aspirate_in_place.py +32 -15
  75. opentrons/protocol_engine/commands/blow_out.py +63 -14
  76. opentrons/protocol_engine/commands/blow_out_in_place.py +55 -13
  77. opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +2 -5
  78. opentrons/protocol_engine/commands/calibration/calibrate_module.py +3 -4
  79. opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +2 -5
  80. opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +6 -4
  81. opentrons/protocol_engine/commands/command.py +28 -17
  82. opentrons/protocol_engine/commands/command_unions.py +37 -24
  83. opentrons/protocol_engine/commands/comment.py +5 -3
  84. opentrons/protocol_engine/commands/configure_for_volume.py +11 -14
  85. opentrons/protocol_engine/commands/configure_nozzle_layout.py +9 -15
  86. opentrons/protocol_engine/commands/custom.py +5 -3
  87. opentrons/protocol_engine/commands/dispense.py +42 -20
  88. opentrons/protocol_engine/commands/dispense_in_place.py +32 -14
  89. opentrons/protocol_engine/commands/drop_tip.py +68 -15
  90. opentrons/protocol_engine/commands/drop_tip_in_place.py +52 -11
  91. opentrons/protocol_engine/commands/get_tip_presence.py +5 -3
  92. opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +6 -6
  93. opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +6 -6
  94. opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +6 -6
  95. opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +8 -6
  96. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +8 -4
  97. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +6 -4
  98. opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +6 -6
  99. opentrons/protocol_engine/commands/home.py +11 -5
  100. opentrons/protocol_engine/commands/liquid_probe.py +146 -88
  101. opentrons/protocol_engine/commands/load_labware.py +19 -5
  102. opentrons/protocol_engine/commands/load_liquid.py +18 -7
  103. opentrons/protocol_engine/commands/load_module.py +43 -6
  104. opentrons/protocol_engine/commands/load_pipette.py +18 -17
  105. opentrons/protocol_engine/commands/magnetic_module/disengage.py +6 -6
  106. opentrons/protocol_engine/commands/magnetic_module/engage.py +6 -4
  107. opentrons/protocol_engine/commands/move_labware.py +106 -19
  108. opentrons/protocol_engine/commands/move_relative.py +15 -3
  109. opentrons/protocol_engine/commands/move_to_addressable_area.py +29 -4
  110. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +13 -4
  111. opentrons/protocol_engine/commands/move_to_coordinates.py +11 -5
  112. opentrons/protocol_engine/commands/move_to_well.py +37 -10
  113. opentrons/protocol_engine/commands/pick_up_tip.py +50 -29
  114. opentrons/protocol_engine/commands/pipetting_common.py +39 -15
  115. opentrons/protocol_engine/commands/prepare_to_aspirate.py +62 -15
  116. opentrons/protocol_engine/commands/reload_labware.py +13 -4
  117. opentrons/protocol_engine/commands/retract_axis.py +6 -3
  118. opentrons/protocol_engine/commands/save_position.py +2 -3
  119. opentrons/protocol_engine/commands/set_rail_lights.py +5 -3
  120. opentrons/protocol_engine/commands/set_status_bar.py +5 -3
  121. opentrons/protocol_engine/commands/temperature_module/deactivate.py +6 -4
  122. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +3 -4
  123. opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +6 -6
  124. opentrons/protocol_engine/commands/thermocycler/__init__.py +19 -0
  125. opentrons/protocol_engine/commands/thermocycler/close_lid.py +8 -8
  126. opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +6 -4
  127. opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +6 -4
  128. opentrons/protocol_engine/commands/thermocycler/open_lid.py +8 -4
  129. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +165 -0
  130. opentrons/protocol_engine/commands/thermocycler/run_profile.py +6 -6
  131. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +3 -4
  132. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +3 -4
  133. opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +6 -4
  134. opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +6 -4
  135. opentrons/protocol_engine/commands/touch_tip.py +19 -7
  136. opentrons/protocol_engine/commands/unsafe/__init__.py +30 -0
  137. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +6 -4
  138. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +12 -4
  139. opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +5 -3
  140. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +194 -0
  141. opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py +75 -0
  142. opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +5 -3
  143. opentrons/protocol_engine/commands/verify_tip_presence.py +5 -5
  144. opentrons/protocol_engine/commands/wait_for_duration.py +5 -3
  145. opentrons/protocol_engine/commands/wait_for_resume.py +5 -3
  146. opentrons/protocol_engine/create_protocol_engine.py +41 -8
  147. opentrons/protocol_engine/engine_support.py +2 -1
  148. opentrons/protocol_engine/error_recovery_policy.py +14 -3
  149. opentrons/protocol_engine/errors/__init__.py +18 -0
  150. opentrons/protocol_engine/errors/exceptions.py +114 -2
  151. opentrons/protocol_engine/execution/__init__.py +2 -0
  152. opentrons/protocol_engine/execution/command_executor.py +22 -13
  153. opentrons/protocol_engine/execution/create_queue_worker.py +5 -1
  154. opentrons/protocol_engine/execution/door_watcher.py +1 -1
  155. opentrons/protocol_engine/execution/equipment.py +2 -1
  156. opentrons/protocol_engine/execution/error_recovery_hardware_state_synchronizer.py +101 -0
  157. opentrons/protocol_engine/execution/gantry_mover.py +4 -2
  158. opentrons/protocol_engine/execution/hardware_stopper.py +3 -3
  159. opentrons/protocol_engine/execution/heater_shaker_movement_flagger.py +1 -4
  160. opentrons/protocol_engine/execution/labware_movement.py +6 -3
  161. opentrons/protocol_engine/execution/movement.py +8 -3
  162. opentrons/protocol_engine/execution/pipetting.py +7 -4
  163. opentrons/protocol_engine/execution/queue_worker.py +6 -2
  164. opentrons/protocol_engine/execution/run_control.py +1 -1
  165. opentrons/protocol_engine/execution/thermocycler_movement_flagger.py +1 -1
  166. opentrons/protocol_engine/execution/thermocycler_plate_lifter.py +2 -1
  167. opentrons/protocol_engine/execution/tip_handler.py +77 -43
  168. opentrons/protocol_engine/notes/__init__.py +14 -2
  169. opentrons/protocol_engine/notes/notes.py +18 -1
  170. opentrons/protocol_engine/plugins.py +1 -1
  171. opentrons/protocol_engine/protocol_engine.py +54 -31
  172. opentrons/protocol_engine/resources/__init__.py +2 -0
  173. opentrons/protocol_engine/resources/deck_data_provider.py +58 -5
  174. opentrons/protocol_engine/resources/file_provider.py +157 -0
  175. opentrons/protocol_engine/resources/fixture_validation.py +5 -0
  176. opentrons/protocol_engine/resources/labware_validation.py +10 -0
  177. opentrons/protocol_engine/state/__init__.py +0 -70
  178. opentrons/protocol_engine/state/addressable_areas.py +1 -1
  179. opentrons/protocol_engine/state/command_history.py +21 -2
  180. opentrons/protocol_engine/state/commands.py +110 -31
  181. opentrons/protocol_engine/state/files.py +59 -0
  182. opentrons/protocol_engine/state/frustum_helpers.py +440 -0
  183. opentrons/protocol_engine/state/geometry.py +359 -15
  184. opentrons/protocol_engine/state/labware.py +166 -63
  185. opentrons/protocol_engine/state/liquids.py +1 -1
  186. opentrons/protocol_engine/state/module_substates/absorbance_reader_substate.py +19 -3
  187. opentrons/protocol_engine/state/modules.py +167 -85
  188. opentrons/protocol_engine/state/motion.py +16 -9
  189. opentrons/protocol_engine/state/pipettes.py +157 -317
  190. opentrons/protocol_engine/state/state.py +30 -1
  191. opentrons/protocol_engine/state/state_summary.py +3 -0
  192. opentrons/protocol_engine/state/tips.py +69 -114
  193. opentrons/protocol_engine/state/update_types.py +408 -0
  194. opentrons/protocol_engine/state/wells.py +236 -0
  195. opentrons/protocol_engine/types.py +90 -0
  196. opentrons/protocol_reader/file_format_validator.py +83 -15
  197. opentrons/protocol_runner/json_translator.py +21 -5
  198. opentrons/protocol_runner/legacy_command_mapper.py +27 -6
  199. opentrons/protocol_runner/legacy_context_plugin.py +27 -71
  200. opentrons/protocol_runner/protocol_runner.py +6 -3
  201. opentrons/protocol_runner/run_orchestrator.py +26 -6
  202. opentrons/protocols/advanced_control/mix.py +3 -5
  203. opentrons/protocols/advanced_control/transfers.py +125 -56
  204. opentrons/protocols/api_support/constants.py +1 -1
  205. opentrons/protocols/api_support/definitions.py +1 -1
  206. opentrons/protocols/api_support/labware_like.py +4 -4
  207. opentrons/protocols/api_support/tip_tracker.py +2 -2
  208. opentrons/protocols/api_support/types.py +15 -2
  209. opentrons/protocols/api_support/util.py +30 -42
  210. opentrons/protocols/duration/errors.py +1 -1
  211. opentrons/protocols/duration/estimator.py +50 -29
  212. opentrons/protocols/execution/dev_types.py +2 -2
  213. opentrons/protocols/execution/execute_json_v4.py +15 -10
  214. opentrons/protocols/execution/execute_python.py +8 -3
  215. opentrons/protocols/geometry/planning.py +12 -12
  216. opentrons/protocols/labware.py +17 -33
  217. opentrons/simulate.py +3 -3
  218. opentrons/types.py +30 -3
  219. opentrons/util/logging_config.py +34 -0
  220. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/METADATA +5 -4
  221. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/RECORD +227 -215
  222. opentrons/protocol_engine/commands/absorbance_reader/measure.py +0 -94
  223. opentrons/protocol_engine/commands/configuring_common.py +0 -26
  224. opentrons/protocol_runner/thread_async_queue.py +0 -174
  225. /opentrons/protocol_engine/state/{abstract_store.py → _abstract_store.py} +0 -0
  226. /opentrons/protocol_engine/state/{move_types.py → _move_types.py} +0 -0
  227. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/LICENSE +0 -0
  228. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/WHEEL +0 -0
  229. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/entry_points.txt +0 -0
  230. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/top_level.txt +0 -0
@@ -15,9 +15,13 @@ from typing import (
15
15
  Union,
16
16
  )
17
17
 
18
+ from opentrons.protocol_engine.state import update_types
18
19
  from opentrons_shared_data.deck.types import DeckDefinitionV5
19
20
  from opentrons_shared_data.gripper.constants import LABWARE_GRIP_FORCE
20
- from opentrons_shared_data.labware.labware_definition import LabwareRole
21
+ from opentrons_shared_data.labware.labware_definition import (
22
+ LabwareRole,
23
+ InnerWellGeometry,
24
+ )
21
25
  from opentrons_shared_data.pipette.types import LabwareUri
22
26
 
23
27
  from opentrons.types import DeckSlotName, StagingSlotName, MountType
@@ -27,12 +31,6 @@ from opentrons.calibration_storage.helpers import uri_from_details
27
31
 
28
32
  from .. import errors
29
33
  from ..resources import DeckFixedLabware, labware_validation, fixture_validation
30
- from ..commands import (
31
- Command,
32
- LoadLabwareResult,
33
- MoveLabwareResult,
34
- ReloadLabwareResult,
35
- )
36
34
  from ..types import (
37
35
  DeckSlotLocation,
38
36
  OnLabwareLocation,
@@ -53,12 +51,12 @@ from ..types import (
53
51
  )
54
52
  from ..actions import (
55
53
  Action,
56
- SucceedCommandAction,
57
54
  AddLabwareOffsetAction,
58
55
  AddLabwareDefinitionAction,
56
+ get_state_updates,
59
57
  )
60
- from .abstract_store import HasState, HandlesActions
61
- from .move_types import EdgePathType
58
+ from ._abstract_store import HasState, HandlesActions
59
+ from ._move_types import EdgePathType
62
60
 
63
61
 
64
62
  # URIs of labware whose definitions accidentally specify an engage height
@@ -69,8 +67,6 @@ _MAGDECK_HALF_MM_LABWARE = {
69
67
  "opentrons/usascientific_96_wellplate_2.4ml_deep/1",
70
68
  }
71
69
 
72
- _OT3_INSTRUMENT_ATTACH_SLOT = DeckSlotName.SLOT_D1
73
-
74
70
  _RIGHT_SIDE_SLOTS = {
75
71
  # OT-2:
76
72
  DeckSlotName.FIXED_TRASH,
@@ -153,10 +149,11 @@ class LabwareStore(HasState[LabwareState], HandlesActions):
153
149
 
154
150
  def handle_action(self, action: Action) -> None:
155
151
  """Modify state in reaction to an action."""
156
- if isinstance(action, SucceedCommandAction):
157
- self._handle_command(action.command)
152
+ for state_update in get_state_updates(action):
153
+ self._add_loaded_labware(state_update)
154
+ self._set_labware_location(state_update)
158
155
 
159
- elif isinstance(action, AddLabwareOffsetAction):
156
+ if isinstance(action, AddLabwareOffsetAction):
160
157
  labware_offset = LabwareOffset.construct(
161
158
  id=action.labware_offset_id,
162
159
  createdAt=action.created_at,
@@ -174,66 +171,71 @@ class LabwareStore(HasState[LabwareState], HandlesActions):
174
171
  )
175
172
  self._state.definitions_by_uri[uri] = action.definition
176
173
 
177
- def _handle_command(self, command: Command) -> None:
178
- """Modify state in reaction to a command."""
179
- if isinstance(command.result, LoadLabwareResult):
174
+ def _add_labware_offset(self, labware_offset: LabwareOffset) -> None:
175
+ """Add a new labware offset to state.
176
+
177
+ `labware_offset.id` must not match any existing labware offset ID.
178
+ `LoadLabwareCommand`s retain references to their corresponding labware offsets
179
+ and expect them to be immutable.
180
+ """
181
+ assert labware_offset.id not in self._state.labware_offsets_by_id
182
+
183
+ self._state.labware_offsets_by_id[labware_offset.id] = labware_offset
184
+
185
+ def _add_loaded_labware(self, state_update: update_types.StateUpdate) -> None:
186
+ loaded_labware_update = state_update.loaded_labware
187
+ if loaded_labware_update != update_types.NO_CHANGE:
180
188
  # If the labware load refers to an offset, that offset must actually exist.
181
- if command.result.offsetId is not None:
182
- assert command.result.offsetId in self._state.labware_offsets_by_id
189
+ if loaded_labware_update.offset_id is not None:
190
+ assert (
191
+ loaded_labware_update.offset_id in self._state.labware_offsets_by_id
192
+ )
183
193
 
184
194
  definition_uri = uri_from_details(
185
- namespace=command.result.definition.namespace,
186
- load_name=command.result.definition.parameters.loadName,
187
- version=command.result.definition.version,
195
+ namespace=loaded_labware_update.definition.namespace,
196
+ load_name=loaded_labware_update.definition.parameters.loadName,
197
+ version=loaded_labware_update.definition.version,
188
198
  )
189
199
 
190
- self._state.definitions_by_uri[definition_uri] = command.result.definition
191
- if isinstance(command.result, LoadLabwareResult):
192
- location = command.params.location
193
- else:
194
- location = self._state.labware_by_id[command.result.labwareId].location
200
+ self._state.definitions_by_uri[
201
+ definition_uri
202
+ ] = loaded_labware_update.definition
203
+
204
+ location = loaded_labware_update.new_location
205
+
206
+ display_name = loaded_labware_update.display_name
195
207
 
196
208
  self._state.labware_by_id[
197
- command.result.labwareId
209
+ loaded_labware_update.labware_id
198
210
  ] = LoadedLabware.construct(
199
- id=command.result.labwareId,
211
+ id=loaded_labware_update.labware_id,
200
212
  location=location,
201
- loadName=command.result.definition.parameters.loadName,
213
+ loadName=loaded_labware_update.definition.parameters.loadName,
202
214
  definitionUri=definition_uri,
203
- offsetId=command.result.offsetId,
204
- displayName=command.params.displayName,
215
+ offsetId=loaded_labware_update.offset_id,
216
+ displayName=display_name,
205
217
  )
206
218
 
207
- elif isinstance(command.result, ReloadLabwareResult):
208
- labware_id = command.params.labwareId
209
- new_offset_id = command.result.offsetId
210
- self._state.labware_by_id[labware_id].offsetId = new_offset_id
211
-
212
- elif isinstance(command.result, MoveLabwareResult):
213
- labware_id = command.params.labwareId
214
- new_location = command.params.newLocation
215
- new_offset_id = command.result.offsetId
219
+ def _set_labware_location(self, state_update: update_types.StateUpdate) -> None:
220
+ labware_location_update = state_update.labware_location
221
+ if labware_location_update != update_types.NO_CHANGE:
222
+ labware_id = labware_location_update.labware_id
223
+ new_offset_id = labware_location_update.offset_id
216
224
 
217
225
  self._state.labware_by_id[labware_id].offsetId = new_offset_id
218
- if isinstance(
219
- new_location, AddressableAreaLocation
220
- ) and fixture_validation.is_gripper_waste_chute(
221
- new_location.addressableAreaName
222
- ):
223
- # If a labware has been moved into a waste chute it's been chuted away and is now technically off deck
224
- new_location = OFF_DECK_LOCATION
225
- self._state.labware_by_id[labware_id].location = new_location
226
226
 
227
- def _add_labware_offset(self, labware_offset: LabwareOffset) -> None:
228
- """Add a new labware offset to state.
227
+ if labware_location_update.new_location:
228
+ new_location = labware_location_update.new_location
229
229
 
230
- `labware_offset.id` must not match any existing labware offset ID.
231
- `LoadLabwareCommand`s retain references to their corresponding labware offsets
232
- and expect them to be immutable.
233
- """
234
- assert labware_offset.id not in self._state.labware_offsets_by_id
230
+ if isinstance(
231
+ new_location, AddressableAreaLocation
232
+ ) and fixture_validation.is_gripper_waste_chute(
233
+ new_location.addressableAreaName
234
+ ):
235
+ # If a labware has been moved into a waste chute it's been chuted away and is now technically off deck
236
+ new_location = OFF_DECK_LOCATION
235
237
 
236
- self._state.labware_offsets_by_id[labware_offset.id] = labware_offset
238
+ self._state.labware_by_id[labware_id].location = new_location
237
239
 
238
240
 
239
241
  class LabwareView(HasState[LabwareState]):
@@ -313,6 +315,22 @@ class LabwareView(HasState[LabwareState]):
313
315
 
314
316
  return None
315
317
 
318
+ def get_by_addressable_area(
319
+ self,
320
+ addressable_area: str,
321
+ ) -> Optional[LoadedLabware]:
322
+ """Get the labware located in a given addressable area, if any."""
323
+ loaded_labware = list(self._state.labware_by_id.values())
324
+
325
+ for labware in loaded_labware:
326
+ if (
327
+ isinstance(labware.location, AddressableAreaLocation)
328
+ and labware.location.addressableAreaName == addressable_area
329
+ ):
330
+ return labware
331
+
332
+ return None
333
+
316
334
  def get_definition(self, labware_id: str) -> LabwareDefinition:
317
335
  """Get labware definition by the labware's unique identifier."""
318
336
  return self.get_definition_by_uri(
@@ -378,6 +396,16 @@ class LabwareView(HasState[LabwareState]):
378
396
  return self.get_parent_location(parent.labwareId)
379
397
  return parent
380
398
 
399
+ def get_labware_stack(
400
+ self, labware_stack: List[LoadedLabware]
401
+ ) -> List[LoadedLabware]:
402
+ """Get the a stack of labware starting from a given labware or existing stack."""
403
+ parent = self.get_location(labware_stack[-1].id)
404
+ if isinstance(parent, OnLabwareLocation):
405
+ labware_stack.append(self.get(parent.labwareId))
406
+ return self.get_labware_stack(labware_stack)
407
+ return labware_stack
408
+
381
409
  def get_all(self) -> List[LoadedLabware]:
382
410
  """Get a list of all labware entries in state."""
383
411
  return list(self._state.labware_by_id.values())
@@ -402,6 +430,27 @@ class LabwareView(HasState[LabwareState]):
402
430
  and len(self.get_definition(labware_id).wells) < 96
403
431
  )
404
432
 
433
+ def get_labware_stacking_maximum(self, labware: LabwareDefinition) -> int:
434
+ """Returns the maximum number of labware allowed in a stack for a given labware definition.
435
+
436
+ If not defined within a labware, defaults to one.
437
+ """
438
+ stacking_quirks = {
439
+ "stackingMaxFive": 5,
440
+ "stackingMaxFour": 4,
441
+ "stackingMaxThree": 3,
442
+ "stackingMaxTwo": 2,
443
+ "stackingMaxOne": 1,
444
+ "stackingMaxZero": 0,
445
+ }
446
+ for quirk in stacking_quirks.keys():
447
+ if (
448
+ labware.parameters.quirks is not None
449
+ and quirk in labware.parameters.quirks
450
+ ):
451
+ return stacking_quirks[quirk]
452
+ return 1
453
+
405
454
  def get_should_center_pipette_on_target_well(self, labware_id: str) -> bool:
406
455
  """True if a pipette moving to a well of this labware should center its body on the target.
407
456
 
@@ -435,6 +484,29 @@ class LabwareView(HasState[LabwareState]):
435
484
  f"{well_name} does not exist in {labware_id}."
436
485
  ) from e
437
486
 
487
+ def get_well_geometry(
488
+ self, labware_id: str, well_name: Optional[str] = None
489
+ ) -> InnerWellGeometry:
490
+ """Get a well's inner geometry by labware and well name."""
491
+ labware_def = self.get_definition(labware_id)
492
+ if labware_def.innerLabwareGeometry is None:
493
+ raise errors.IncompleteLabwareDefinitionError(
494
+ message=f"No innerLabwareGeometry found in labware definition for labware_id: {labware_id}."
495
+ )
496
+ well_def = self.get_well_definition(labware_id, well_name)
497
+ well_id = well_def.geometryDefinitionId
498
+ if well_id is None:
499
+ raise errors.IncompleteWellDefinitionError(
500
+ message=f"No geometryDefinitionId found in well definition for well: {well_name} in labware_id: {labware_id}"
501
+ )
502
+ else:
503
+ well_geometry = labware_def.innerLabwareGeometry.get(well_id)
504
+ if well_geometry is None:
505
+ raise errors.IncompleteLabwareDefinitionError(
506
+ message=f"No innerLabwareGeometry found in labware definition for well_id: {well_id} in labware_id: {labware_id}"
507
+ )
508
+ return well_geometry
509
+
438
510
  def get_well_size(
439
511
  self, labware_id: str, well_name: str
440
512
  ) -> Tuple[float, float, float]:
@@ -569,9 +641,14 @@ class LabwareView(HasState[LabwareState]):
569
641
  ) -> OverlapOffset:
570
642
  """Get the labware's overlap with requested labware's load name."""
571
643
  definition = self.get_definition(labware_id)
572
- stacking_overlap = definition.stackingOffsetWithLabware.get(
573
- below_labware_name, OverlapOffset(x=0, y=0, z=0)
574
- )
644
+ if below_labware_name in definition.stackingOffsetWithLabware.keys():
645
+ stacking_overlap = definition.stackingOffsetWithLabware.get(
646
+ below_labware_name, OverlapOffset(x=0, y=0, z=0)
647
+ )
648
+ else:
649
+ stacking_overlap = definition.stackingOffsetWithLabware.get(
650
+ "default", OverlapOffset(x=0, y=0, z=0)
651
+ )
575
652
  return OverlapOffset(
576
653
  x=stacking_overlap.x, y=stacking_overlap.y, z=stacking_overlap.z
577
654
  )
@@ -704,6 +781,12 @@ class LabwareView(HasState[LabwareState]):
704
781
  """Check if labware is fixed trash."""
705
782
  return self.get_has_quirk(labware_id, "fixedTrash")
706
783
 
784
+ def is_absorbance_reader_lid(self, labware_id: str) -> bool:
785
+ """Check if labware is an absorbance reader lid."""
786
+ return labware_validation.is_absorbance_reader_lid(
787
+ self.get(labware_id).loadName
788
+ )
789
+
707
790
  def raise_if_labware_inaccessible_by_pipette(self, labware_id: str) -> None:
708
791
  """Raise an error if the specified location cannot be reached via a pipette."""
709
792
  labware = self.get(labware_id)
@@ -734,7 +817,7 @@ class LabwareView(HasState[LabwareState]):
734
817
  f"Labware {labware.loadName} is already present at {location}."
735
818
  )
736
819
 
737
- def raise_if_labware_cannot_be_stacked(
820
+ def raise_if_labware_cannot_be_stacked( # noqa: C901
738
821
  self, top_labware_definition: LabwareDefinition, bottom_labware_id: str
739
822
  ) -> None:
740
823
  """Raise if the specified labware definition cannot be placed on top of the bottom labware."""
@@ -753,17 +836,37 @@ class LabwareView(HasState[LabwareState]):
753
836
  )
754
837
  elif isinstance(below_labware.location, ModuleLocation):
755
838
  below_definition = self.get_definition(labware_id=below_labware.id)
756
- if not labware_validation.validate_definition_is_adapter(below_definition):
839
+ if not labware_validation.validate_definition_is_adapter(
840
+ below_definition
841
+ ) and not labware_validation.validate_definition_is_lid(
842
+ top_labware_definition
843
+ ):
757
844
  raise errors.LabwareCannotBeStackedError(
758
845
  f"Labware {top_labware_definition.parameters.loadName} cannot be loaded"
759
846
  f" onto a labware on top of a module"
760
847
  )
761
848
  elif isinstance(below_labware.location, OnLabwareLocation):
849
+ labware_stack = self.get_labware_stack([below_labware])
850
+ stack_without_adapters = []
851
+ for lw in labware_stack:
852
+ if not labware_validation.validate_definition_is_adapter(
853
+ self.get_definition(lw.id)
854
+ ):
855
+ stack_without_adapters.append(lw)
856
+ if len(stack_without_adapters) >= self.get_labware_stacking_maximum(
857
+ top_labware_definition
858
+ ):
859
+ raise errors.LabwareCannotBeStackedError(
860
+ f"Labware {top_labware_definition.parameters.loadName} cannot be loaded to stack of more than {self.get_labware_stacking_maximum(top_labware_definition)} labware."
861
+ )
862
+
762
863
  further_below_definition = self.get_definition(
763
864
  labware_id=below_labware.location.labwareId
764
865
  )
765
866
  if labware_validation.validate_definition_is_adapter(
766
867
  further_below_definition
868
+ ) and not labware_validation.validate_definition_is_lid(
869
+ top_labware_definition
767
870
  ):
768
871
  raise errors.LabwareCannotBeStackedError(
769
872
  f"Labware {top_labware_definition.parameters.loadName} cannot be loaded"
@@ -3,7 +3,7 @@ from dataclasses import dataclass
3
3
  from typing import Dict, List
4
4
  from opentrons.protocol_engine.types import Liquid
5
5
 
6
- from .abstract_store import HasState, HandlesActions
6
+ from ._abstract_store import HasState, HandlesActions
7
7
  from ..actions import Action, AddLiquidAction
8
8
  from ..errors import LiquidDoesNotExistError
9
9
 
@@ -1,9 +1,12 @@
1
1
  """Heater-Shaker Module sub-state."""
2
2
  from dataclasses import dataclass
3
- from typing import NewType, Optional, List
3
+ from typing import List, NewType, Optional, Dict
4
4
 
5
+ from opentrons.protocol_engine.errors import CannotPerformModuleAction
5
6
 
6
7
  AbsorbanceReaderId = NewType("AbsorbanceReaderId", str)
8
+ AbsorbanceReaderLidId = NewType("AbsorbanceReaderLidId", str)
9
+ AbsorbanceReaderMeasureMode = NewType("AbsorbanceReaderMeasureMode", str)
7
10
 
8
11
 
9
12
  @dataclass(frozen=True)
@@ -13,5 +16,18 @@ class AbsorbanceReaderSubState:
13
16
  module_id: AbsorbanceReaderId
14
17
  configured: bool
15
18
  measured: bool
16
- data: Optional[List[float]]
17
- configured_wavelength: Optional[int]
19
+ is_lid_on: bool
20
+ data: Optional[Dict[int, Dict[str, float]]]
21
+ configured_wavelengths: Optional[List[int]]
22
+ measure_mode: Optional[AbsorbanceReaderMeasureMode]
23
+ reference_wavelength: Optional[int]
24
+ lid_id: Optional[str]
25
+
26
+ def raise_if_lid_status_not_expected(self, lid_on_expected: bool) -> None:
27
+ """Raise if the lid status is not correct."""
28
+ match = self.is_lid_on is lid_on_expected
29
+ if not match:
30
+ raise CannotPerformModuleAction(
31
+ "Cannot perform lid action because the lid is already "
32
+ f"{'closed' if self.is_lid_on else 'open'}"
33
+ )