opentrons 8.1.0__py2.py3-none-any.whl → 8.2.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.
Files changed (238) 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 +208 -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/backends/ot3utils.py +1 -0
  20. opentrons/hardware_control/instruments/ot2/pipette_handler.py +22 -82
  21. opentrons/hardware_control/instruments/ot3/pipette_handler.py +10 -2
  22. opentrons/hardware_control/module_control.py +43 -2
  23. opentrons/hardware_control/modules/__init__.py +7 -1
  24. opentrons/hardware_control/modules/absorbance_reader.py +232 -83
  25. opentrons/hardware_control/modules/errors.py +7 -0
  26. opentrons/hardware_control/modules/heater_shaker.py +8 -3
  27. opentrons/hardware_control/modules/magdeck.py +12 -3
  28. opentrons/hardware_control/modules/mod_abc.py +27 -2
  29. opentrons/hardware_control/modules/tempdeck.py +15 -7
  30. opentrons/hardware_control/modules/thermocycler.py +69 -3
  31. opentrons/hardware_control/modules/types.py +11 -5
  32. opentrons/hardware_control/modules/update.py +11 -5
  33. opentrons/hardware_control/modules/utils.py +3 -1
  34. opentrons/hardware_control/ot3_calibration.py +6 -6
  35. opentrons/hardware_control/ot3api.py +131 -94
  36. opentrons/hardware_control/poller.py +15 -11
  37. opentrons/hardware_control/protocols/__init__.py +1 -7
  38. opentrons/hardware_control/protocols/instrument_configurer.py +14 -2
  39. opentrons/hardware_control/protocols/liquid_handler.py +5 -0
  40. opentrons/hardware_control/protocols/position_estimator.py +3 -1
  41. opentrons/hardware_control/types.py +2 -0
  42. opentrons/legacy_commands/helpers.py +8 -2
  43. opentrons/motion_planning/__init__.py +2 -0
  44. opentrons/motion_planning/waypoints.py +32 -0
  45. opentrons/protocol_api/__init__.py +2 -1
  46. opentrons/protocol_api/_liquid.py +87 -1
  47. opentrons/protocol_api/_parameter_context.py +10 -1
  48. opentrons/protocol_api/core/engine/deck_conflict.py +0 -297
  49. opentrons/protocol_api/core/engine/instrument.py +29 -25
  50. opentrons/protocol_api/core/engine/labware.py +20 -4
  51. opentrons/protocol_api/core/engine/module_core.py +166 -17
  52. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +362 -0
  53. opentrons/protocol_api/core/engine/protocol.py +30 -2
  54. opentrons/protocol_api/core/instrument.py +2 -0
  55. opentrons/protocol_api/core/labware.py +4 -0
  56. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +2 -0
  57. opentrons/protocol_api/core/legacy/legacy_labware_core.py +5 -0
  58. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +6 -2
  59. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +2 -0
  60. opentrons/protocol_api/core/module.py +22 -4
  61. opentrons/protocol_api/core/protocol.py +6 -2
  62. opentrons/protocol_api/instrument_context.py +52 -20
  63. opentrons/protocol_api/labware.py +13 -1
  64. opentrons/protocol_api/module_contexts.py +115 -17
  65. opentrons/protocol_api/protocol_context.py +49 -5
  66. opentrons/protocol_api/validation.py +5 -3
  67. opentrons/protocol_engine/__init__.py +10 -9
  68. opentrons/protocol_engine/actions/__init__.py +3 -0
  69. opentrons/protocol_engine/actions/actions.py +30 -25
  70. opentrons/protocol_engine/actions/get_state_update.py +38 -0
  71. opentrons/protocol_engine/clients/sync_client.py +1 -1
  72. opentrons/protocol_engine/clients/transports.py +1 -1
  73. opentrons/protocol_engine/commands/__init__.py +0 -4
  74. opentrons/protocol_engine/commands/absorbance_reader/__init__.py +41 -11
  75. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +148 -0
  76. opentrons/protocol_engine/commands/absorbance_reader/initialize.py +65 -9
  77. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +148 -0
  78. opentrons/protocol_engine/commands/absorbance_reader/read.py +200 -0
  79. opentrons/protocol_engine/commands/aspirate.py +29 -16
  80. opentrons/protocol_engine/commands/aspirate_in_place.py +33 -16
  81. opentrons/protocol_engine/commands/blow_out.py +63 -14
  82. opentrons/protocol_engine/commands/blow_out_in_place.py +55 -13
  83. opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +2 -5
  84. opentrons/protocol_engine/commands/calibration/calibrate_module.py +3 -4
  85. opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +2 -5
  86. opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +6 -4
  87. opentrons/protocol_engine/commands/command.py +31 -18
  88. opentrons/protocol_engine/commands/command_unions.py +37 -24
  89. opentrons/protocol_engine/commands/comment.py +5 -3
  90. opentrons/protocol_engine/commands/configure_for_volume.py +11 -14
  91. opentrons/protocol_engine/commands/configure_nozzle_layout.py +9 -15
  92. opentrons/protocol_engine/commands/custom.py +5 -3
  93. opentrons/protocol_engine/commands/dispense.py +42 -20
  94. opentrons/protocol_engine/commands/dispense_in_place.py +32 -14
  95. opentrons/protocol_engine/commands/drop_tip.py +70 -16
  96. opentrons/protocol_engine/commands/drop_tip_in_place.py +59 -13
  97. opentrons/protocol_engine/commands/get_tip_presence.py +5 -3
  98. opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +6 -6
  99. opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +6 -6
  100. opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +6 -6
  101. opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +8 -6
  102. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +8 -4
  103. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +6 -4
  104. opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +6 -6
  105. opentrons/protocol_engine/commands/home.py +11 -5
  106. opentrons/protocol_engine/commands/liquid_probe.py +146 -88
  107. opentrons/protocol_engine/commands/load_labware.py +28 -5
  108. opentrons/protocol_engine/commands/load_liquid.py +18 -7
  109. opentrons/protocol_engine/commands/load_module.py +4 -6
  110. opentrons/protocol_engine/commands/load_pipette.py +18 -17
  111. opentrons/protocol_engine/commands/magnetic_module/disengage.py +6 -6
  112. opentrons/protocol_engine/commands/magnetic_module/engage.py +6 -4
  113. opentrons/protocol_engine/commands/move_labware.py +155 -23
  114. opentrons/protocol_engine/commands/move_relative.py +15 -3
  115. opentrons/protocol_engine/commands/move_to_addressable_area.py +29 -4
  116. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +13 -4
  117. opentrons/protocol_engine/commands/move_to_coordinates.py +11 -5
  118. opentrons/protocol_engine/commands/move_to_well.py +37 -10
  119. opentrons/protocol_engine/commands/pick_up_tip.py +51 -30
  120. opentrons/protocol_engine/commands/pipetting_common.py +47 -16
  121. opentrons/protocol_engine/commands/prepare_to_aspirate.py +62 -15
  122. opentrons/protocol_engine/commands/reload_labware.py +13 -4
  123. opentrons/protocol_engine/commands/retract_axis.py +6 -3
  124. opentrons/protocol_engine/commands/save_position.py +2 -3
  125. opentrons/protocol_engine/commands/set_rail_lights.py +5 -3
  126. opentrons/protocol_engine/commands/set_status_bar.py +5 -3
  127. opentrons/protocol_engine/commands/temperature_module/deactivate.py +6 -4
  128. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +3 -4
  129. opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +6 -6
  130. opentrons/protocol_engine/commands/thermocycler/__init__.py +19 -0
  131. opentrons/protocol_engine/commands/thermocycler/close_lid.py +8 -8
  132. opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +6 -4
  133. opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +6 -4
  134. opentrons/protocol_engine/commands/thermocycler/open_lid.py +8 -4
  135. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +165 -0
  136. opentrons/protocol_engine/commands/thermocycler/run_profile.py +6 -6
  137. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +3 -4
  138. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +3 -4
  139. opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +6 -4
  140. opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +6 -4
  141. opentrons/protocol_engine/commands/touch_tip.py +19 -7
  142. opentrons/protocol_engine/commands/unsafe/__init__.py +30 -0
  143. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +6 -4
  144. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +12 -4
  145. opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +5 -3
  146. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +208 -0
  147. opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py +77 -0
  148. opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +10 -4
  149. opentrons/protocol_engine/commands/verify_tip_presence.py +5 -5
  150. opentrons/protocol_engine/commands/wait_for_duration.py +5 -3
  151. opentrons/protocol_engine/commands/wait_for_resume.py +5 -3
  152. opentrons/protocol_engine/create_protocol_engine.py +60 -10
  153. opentrons/protocol_engine/engine_support.py +2 -1
  154. opentrons/protocol_engine/error_recovery_policy.py +14 -3
  155. opentrons/protocol_engine/errors/__init__.py +20 -0
  156. opentrons/protocol_engine/errors/error_occurrence.py +8 -3
  157. opentrons/protocol_engine/errors/exceptions.py +127 -2
  158. opentrons/protocol_engine/execution/__init__.py +2 -0
  159. opentrons/protocol_engine/execution/command_executor.py +22 -13
  160. opentrons/protocol_engine/execution/create_queue_worker.py +5 -1
  161. opentrons/protocol_engine/execution/door_watcher.py +1 -1
  162. opentrons/protocol_engine/execution/equipment.py +2 -1
  163. opentrons/protocol_engine/execution/error_recovery_hardware_state_synchronizer.py +101 -0
  164. opentrons/protocol_engine/execution/gantry_mover.py +4 -2
  165. opentrons/protocol_engine/execution/hardware_stopper.py +3 -3
  166. opentrons/protocol_engine/execution/heater_shaker_movement_flagger.py +1 -4
  167. opentrons/protocol_engine/execution/labware_movement.py +73 -22
  168. opentrons/protocol_engine/execution/movement.py +17 -7
  169. opentrons/protocol_engine/execution/pipetting.py +7 -4
  170. opentrons/protocol_engine/execution/queue_worker.py +6 -2
  171. opentrons/protocol_engine/execution/run_control.py +1 -1
  172. opentrons/protocol_engine/execution/thermocycler_movement_flagger.py +1 -1
  173. opentrons/protocol_engine/execution/thermocycler_plate_lifter.py +2 -1
  174. opentrons/protocol_engine/execution/tip_handler.py +77 -43
  175. opentrons/protocol_engine/notes/__init__.py +14 -2
  176. opentrons/protocol_engine/notes/notes.py +18 -1
  177. opentrons/protocol_engine/plugins.py +1 -1
  178. opentrons/protocol_engine/protocol_engine.py +47 -31
  179. opentrons/protocol_engine/resources/__init__.py +2 -0
  180. opentrons/protocol_engine/resources/deck_data_provider.py +19 -5
  181. opentrons/protocol_engine/resources/file_provider.py +161 -0
  182. opentrons/protocol_engine/resources/fixture_validation.py +11 -1
  183. opentrons/protocol_engine/resources/labware_validation.py +10 -0
  184. opentrons/protocol_engine/state/__init__.py +0 -70
  185. opentrons/protocol_engine/state/addressable_areas.py +1 -1
  186. opentrons/protocol_engine/state/command_history.py +21 -2
  187. opentrons/protocol_engine/state/commands.py +110 -31
  188. opentrons/protocol_engine/state/files.py +59 -0
  189. opentrons/protocol_engine/state/frustum_helpers.py +440 -0
  190. opentrons/protocol_engine/state/geometry.py +445 -59
  191. opentrons/protocol_engine/state/labware.py +264 -84
  192. opentrons/protocol_engine/state/liquids.py +1 -1
  193. opentrons/protocol_engine/state/module_substates/absorbance_reader_substate.py +21 -3
  194. opentrons/protocol_engine/state/modules.py +145 -90
  195. opentrons/protocol_engine/state/motion.py +33 -14
  196. opentrons/protocol_engine/state/pipettes.py +157 -317
  197. opentrons/protocol_engine/state/state.py +30 -1
  198. opentrons/protocol_engine/state/state_summary.py +3 -0
  199. opentrons/protocol_engine/state/tips.py +69 -114
  200. opentrons/protocol_engine/state/update_types.py +424 -0
  201. opentrons/protocol_engine/state/wells.py +236 -0
  202. opentrons/protocol_engine/types.py +90 -0
  203. opentrons/protocol_reader/file_format_validator.py +83 -15
  204. opentrons/protocol_runner/json_translator.py +21 -5
  205. opentrons/protocol_runner/legacy_command_mapper.py +27 -6
  206. opentrons/protocol_runner/legacy_context_plugin.py +27 -71
  207. opentrons/protocol_runner/protocol_runner.py +6 -3
  208. opentrons/protocol_runner/run_orchestrator.py +41 -6
  209. opentrons/protocols/advanced_control/mix.py +3 -5
  210. opentrons/protocols/advanced_control/transfers.py +125 -56
  211. opentrons/protocols/api_support/constants.py +1 -1
  212. opentrons/protocols/api_support/definitions.py +1 -1
  213. opentrons/protocols/api_support/labware_like.py +4 -4
  214. opentrons/protocols/api_support/tip_tracker.py +2 -2
  215. opentrons/protocols/api_support/types.py +15 -2
  216. opentrons/protocols/api_support/util.py +30 -42
  217. opentrons/protocols/duration/errors.py +1 -1
  218. opentrons/protocols/duration/estimator.py +50 -29
  219. opentrons/protocols/execution/dev_types.py +2 -2
  220. opentrons/protocols/execution/execute_json_v4.py +15 -10
  221. opentrons/protocols/execution/execute_python.py +8 -3
  222. opentrons/protocols/geometry/planning.py +12 -12
  223. opentrons/protocols/labware.py +17 -33
  224. opentrons/protocols/parameters/csv_parameter_interface.py +3 -1
  225. opentrons/simulate.py +3 -3
  226. opentrons/types.py +30 -3
  227. opentrons/util/logging_config.py +34 -0
  228. {opentrons-8.1.0.dist-info → opentrons-8.2.0.dist-info}/METADATA +5 -4
  229. {opentrons-8.1.0.dist-info → opentrons-8.2.0.dist-info}/RECORD +235 -223
  230. opentrons/protocol_engine/commands/absorbance_reader/measure.py +0 -94
  231. opentrons/protocol_engine/commands/configuring_common.py +0 -26
  232. opentrons/protocol_runner/thread_async_queue.py +0 -174
  233. /opentrons/protocol_engine/state/{abstract_store.py → _abstract_store.py} +0 -0
  234. /opentrons/protocol_engine/state/{move_types.py → _move_types.py} +0 -0
  235. {opentrons-8.1.0.dist-info → opentrons-8.2.0.dist-info}/LICENSE +0 -0
  236. {opentrons-8.1.0.dist-info → opentrons-8.2.0.dist-info}/WHEEL +0 -0
  237. {opentrons-8.1.0.dist-info → opentrons-8.2.0.dist-info}/entry_points.txt +0 -0
  238. {opentrons-8.1.0.dist-info → opentrons-8.2.0.dist-info}/top_level.txt +0 -0
@@ -13,11 +13,16 @@ from typing import (
13
13
  NamedTuple,
14
14
  cast,
15
15
  Union,
16
+ overload,
16
17
  )
17
18
 
19
+ from opentrons.protocol_engine.state import update_types
18
20
  from opentrons_shared_data.deck.types import DeckDefinitionV5
19
21
  from opentrons_shared_data.gripper.constants import LABWARE_GRIP_FORCE
20
- from opentrons_shared_data.labware.labware_definition import LabwareRole
22
+ from opentrons_shared_data.labware.labware_definition import (
23
+ LabwareRole,
24
+ InnerWellGeometry,
25
+ )
21
26
  from opentrons_shared_data.pipette.types import LabwareUri
22
27
 
23
28
  from opentrons.types import DeckSlotName, StagingSlotName, MountType
@@ -27,12 +32,6 @@ from opentrons.calibration_storage.helpers import uri_from_details
27
32
 
28
33
  from .. import errors
29
34
  from ..resources import DeckFixedLabware, labware_validation, fixture_validation
30
- from ..commands import (
31
- Command,
32
- LoadLabwareResult,
33
- MoveLabwareResult,
34
- ReloadLabwareResult,
35
- )
36
35
  from ..types import (
37
36
  DeckSlotLocation,
38
37
  OnLabwareLocation,
@@ -53,12 +52,12 @@ from ..types import (
53
52
  )
54
53
  from ..actions import (
55
54
  Action,
56
- SucceedCommandAction,
57
55
  AddLabwareOffsetAction,
58
56
  AddLabwareDefinitionAction,
57
+ get_state_updates,
59
58
  )
60
- from .abstract_store import HasState, HandlesActions
61
- from .move_types import EdgePathType
59
+ from ._abstract_store import HasState, HandlesActions
60
+ from ._move_types import EdgePathType
62
61
 
63
62
 
64
63
  # URIs of labware whose definitions accidentally specify an engage height
@@ -69,8 +68,6 @@ _MAGDECK_HALF_MM_LABWARE = {
69
68
  "opentrons/usascientific_96_wellplate_2.4ml_deep/1",
70
69
  }
71
70
 
72
- _OT3_INSTRUMENT_ATTACH_SLOT = DeckSlotName.SLOT_D1
73
-
74
71
  _RIGHT_SIDE_SLOTS = {
75
72
  # OT-2:
76
73
  DeckSlotName.FIXED_TRASH,
@@ -85,6 +82,10 @@ _RIGHT_SIDE_SLOTS = {
85
82
  }
86
83
 
87
84
 
85
+ # The max height of the labware that can fit in a plate reader
86
+ _PLATE_READER_MAX_LABWARE_Z_MM = 16
87
+
88
+
88
89
  class LabwareLoadParams(NamedTuple):
89
90
  """Parameters required to load a labware in Protocol Engine."""
90
91
 
@@ -153,10 +154,11 @@ class LabwareStore(HasState[LabwareState], HandlesActions):
153
154
 
154
155
  def handle_action(self, action: Action) -> None:
155
156
  """Modify state in reaction to an action."""
156
- if isinstance(action, SucceedCommandAction):
157
- self._handle_command(action.command)
157
+ for state_update in get_state_updates(action):
158
+ self._add_loaded_labware(state_update)
159
+ self._set_labware_location(state_update)
158
160
 
159
- elif isinstance(action, AddLabwareOffsetAction):
161
+ if isinstance(action, AddLabwareOffsetAction):
160
162
  labware_offset = LabwareOffset.construct(
161
163
  id=action.labware_offset_id,
162
164
  createdAt=action.created_at,
@@ -174,66 +176,72 @@ class LabwareStore(HasState[LabwareState], HandlesActions):
174
176
  )
175
177
  self._state.definitions_by_uri[uri] = action.definition
176
178
 
177
- def _handle_command(self, command: Command) -> None:
178
- """Modify state in reaction to a command."""
179
- if isinstance(command.result, LoadLabwareResult):
179
+ def _add_labware_offset(self, labware_offset: LabwareOffset) -> None:
180
+ """Add a new labware offset to state.
181
+
182
+ `labware_offset.id` must not match any existing labware offset ID.
183
+ `LoadLabwareCommand`s retain references to their corresponding labware offsets
184
+ and expect them to be immutable.
185
+ """
186
+ assert labware_offset.id not in self._state.labware_offsets_by_id
187
+
188
+ self._state.labware_offsets_by_id[labware_offset.id] = labware_offset
189
+
190
+ def _add_loaded_labware(self, state_update: update_types.StateUpdate) -> None:
191
+ loaded_labware_update = state_update.loaded_labware
192
+ if loaded_labware_update != update_types.NO_CHANGE:
180
193
  # 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
194
+ if loaded_labware_update.offset_id is not None:
195
+ assert (
196
+ loaded_labware_update.offset_id in self._state.labware_offsets_by_id
197
+ )
183
198
 
184
199
  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,
200
+ namespace=loaded_labware_update.definition.namespace,
201
+ load_name=loaded_labware_update.definition.parameters.loadName,
202
+ version=loaded_labware_update.definition.version,
188
203
  )
189
204
 
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
205
+ self._state.definitions_by_uri[
206
+ definition_uri
207
+ ] = loaded_labware_update.definition
208
+
209
+ location = loaded_labware_update.new_location
210
+
211
+ display_name = loaded_labware_update.display_name
195
212
 
196
213
  self._state.labware_by_id[
197
- command.result.labwareId
214
+ loaded_labware_update.labware_id
198
215
  ] = LoadedLabware.construct(
199
- id=command.result.labwareId,
216
+ id=loaded_labware_update.labware_id,
200
217
  location=location,
201
- loadName=command.result.definition.parameters.loadName,
218
+ loadName=loaded_labware_update.definition.parameters.loadName,
202
219
  definitionUri=definition_uri,
203
- offsetId=command.result.offsetId,
204
- displayName=command.params.displayName,
220
+ offsetId=loaded_labware_update.offset_id,
221
+ displayName=display_name,
205
222
  )
206
223
 
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
224
+ def _set_labware_location(self, state_update: update_types.StateUpdate) -> None:
225
+ labware_location_update = state_update.labware_location
226
+ if labware_location_update != update_types.NO_CHANGE:
227
+ labware_id = labware_location_update.labware_id
228
+ new_offset_id = labware_location_update.offset_id
216
229
 
217
230
  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
231
 
227
- def _add_labware_offset(self, labware_offset: LabwareOffset) -> None:
228
- """Add a new labware offset to state.
232
+ if labware_location_update.new_location:
233
+ new_location = labware_location_update.new_location
229
234
 
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
235
+ if isinstance(new_location, AddressableAreaLocation) and (
236
+ fixture_validation.is_gripper_waste_chute(
237
+ new_location.addressableAreaName
238
+ )
239
+ or fixture_validation.is_trash(new_location.addressableAreaName)
240
+ ):
241
+ # If a labware has been moved into a waste chute it's been chuted away and is now technically off deck
242
+ new_location = OFF_DECK_LOCATION
235
243
 
236
- self._state.labware_offsets_by_id[labware_offset.id] = labware_offset
244
+ self._state.labware_by_id[labware_id].location = new_location
237
245
 
238
246
 
239
247
  class LabwareView(HasState[LabwareState]):
@@ -313,6 +321,22 @@ class LabwareView(HasState[LabwareState]):
313
321
 
314
322
  return None
315
323
 
324
+ def get_by_addressable_area(
325
+ self,
326
+ addressable_area: str,
327
+ ) -> Optional[LoadedLabware]:
328
+ """Get the labware located in a given addressable area, if any."""
329
+ loaded_labware = list(self._state.labware_by_id.values())
330
+
331
+ for labware in loaded_labware:
332
+ if (
333
+ isinstance(labware.location, AddressableAreaLocation)
334
+ and labware.location.addressableAreaName == addressable_area
335
+ ):
336
+ return labware
337
+
338
+ return None
339
+
316
340
  def get_definition(self, labware_id: str) -> LabwareDefinition:
317
341
  """Get labware definition by the labware's unique identifier."""
318
342
  return self.get_definition_by_uri(
@@ -378,6 +402,16 @@ class LabwareView(HasState[LabwareState]):
378
402
  return self.get_parent_location(parent.labwareId)
379
403
  return parent
380
404
 
405
+ def get_labware_stack(
406
+ self, labware_stack: List[LoadedLabware]
407
+ ) -> List[LoadedLabware]:
408
+ """Get the a stack of labware starting from a given labware or existing stack."""
409
+ parent = self.get_location(labware_stack[-1].id)
410
+ if isinstance(parent, OnLabwareLocation):
411
+ labware_stack.append(self.get(parent.labwareId))
412
+ return self.get_labware_stack(labware_stack)
413
+ return labware_stack
414
+
381
415
  def get_all(self) -> List[LoadedLabware]:
382
416
  """Get a list of all labware entries in state."""
383
417
  return list(self._state.labware_by_id.values())
@@ -402,6 +436,27 @@ class LabwareView(HasState[LabwareState]):
402
436
  and len(self.get_definition(labware_id).wells) < 96
403
437
  )
404
438
 
439
+ def get_labware_stacking_maximum(self, labware: LabwareDefinition) -> int:
440
+ """Returns the maximum number of labware allowed in a stack for a given labware definition.
441
+
442
+ If not defined within a labware, defaults to one.
443
+ """
444
+ stacking_quirks = {
445
+ "stackingMaxFive": 5,
446
+ "stackingMaxFour": 4,
447
+ "stackingMaxThree": 3,
448
+ "stackingMaxTwo": 2,
449
+ "stackingMaxOne": 1,
450
+ "stackingMaxZero": 0,
451
+ }
452
+ for quirk in stacking_quirks.keys():
453
+ if (
454
+ labware.parameters.quirks is not None
455
+ and quirk in labware.parameters.quirks
456
+ ):
457
+ return stacking_quirks[quirk]
458
+ return 1
459
+
405
460
  def get_should_center_pipette_on_target_well(self, labware_id: str) -> bool:
406
461
  """True if a pipette moving to a well of this labware should center its body on the target.
407
462
 
@@ -435,6 +490,29 @@ class LabwareView(HasState[LabwareState]):
435
490
  f"{well_name} does not exist in {labware_id}."
436
491
  ) from e
437
492
 
493
+ def get_well_geometry(
494
+ self, labware_id: str, well_name: Optional[str] = None
495
+ ) -> InnerWellGeometry:
496
+ """Get a well's inner geometry by labware and well name."""
497
+ labware_def = self.get_definition(labware_id)
498
+ if labware_def.innerLabwareGeometry is None:
499
+ raise errors.IncompleteLabwareDefinitionError(
500
+ message=f"No innerLabwareGeometry found in labware definition for labware_id: {labware_id}."
501
+ )
502
+ well_def = self.get_well_definition(labware_id, well_name)
503
+ well_id = well_def.geometryDefinitionId
504
+ if well_id is None:
505
+ raise errors.IncompleteWellDefinitionError(
506
+ message=f"No geometryDefinitionId found in well definition for well: {well_name} in labware_id: {labware_id}"
507
+ )
508
+ else:
509
+ well_geometry = labware_def.innerLabwareGeometry.get(well_id)
510
+ if well_geometry is None:
511
+ raise errors.IncompleteLabwareDefinitionError(
512
+ message=f"No innerLabwareGeometry found in labware definition for well_id: {well_id} in labware_id: {labware_id}"
513
+ )
514
+ return well_geometry
515
+
438
516
  def get_well_size(
439
517
  self, labware_id: str, well_name: str
440
518
  ) -> Tuple[float, float, float]:
@@ -553,10 +631,26 @@ class LabwareView(HasState[LabwareState]):
553
631
  definition = self.get_definition(labware_id)
554
632
  return definition.parameters.loadName
555
633
 
556
- def get_dimensions(self, labware_id: str) -> Dimensions:
634
+ @overload
635
+ def get_dimensions(self, *, labware_definition: LabwareDefinition) -> Dimensions:
636
+ pass
637
+
638
+ @overload
639
+ def get_dimensions(self, *, labware_id: str) -> Dimensions:
640
+ pass
641
+
642
+ def get_dimensions(
643
+ self,
644
+ *,
645
+ labware_definition: LabwareDefinition | None = None,
646
+ labware_id: str | None = None,
647
+ ) -> Dimensions:
557
648
  """Get the labware's dimensions."""
558
- definition = self.get_definition(labware_id)
559
- dims = definition.dimensions
649
+ if labware_definition is None:
650
+ assert labware_id is not None # From our @overloads.
651
+ labware_definition = self.get_definition(labware_id)
652
+
653
+ dims = labware_definition.dimensions
560
654
 
561
655
  return Dimensions(
562
656
  x=dims.xDimension,
@@ -565,22 +659,25 @@ class LabwareView(HasState[LabwareState]):
565
659
  )
566
660
 
567
661
  def get_labware_overlap_offsets(
568
- self, labware_id: str, below_labware_name: str
662
+ self, definition: LabwareDefinition, below_labware_name: str
569
663
  ) -> OverlapOffset:
570
664
  """Get the labware's overlap with requested labware's load name."""
571
- definition = self.get_definition(labware_id)
572
- stacking_overlap = definition.stackingOffsetWithLabware.get(
573
- below_labware_name, OverlapOffset(x=0, y=0, z=0)
574
- )
665
+ if below_labware_name in definition.stackingOffsetWithLabware.keys():
666
+ stacking_overlap = definition.stackingOffsetWithLabware.get(
667
+ below_labware_name, OverlapOffset(x=0, y=0, z=0)
668
+ )
669
+ else:
670
+ stacking_overlap = definition.stackingOffsetWithLabware.get(
671
+ "default", OverlapOffset(x=0, y=0, z=0)
672
+ )
575
673
  return OverlapOffset(
576
674
  x=stacking_overlap.x, y=stacking_overlap.y, z=stacking_overlap.z
577
675
  )
578
676
 
579
677
  def get_module_overlap_offsets(
580
- self, labware_id: str, module_model: ModuleModel
678
+ self, definition: LabwareDefinition, module_model: ModuleModel
581
679
  ) -> OverlapOffset:
582
680
  """Get the labware's overlap with requested module model."""
583
- definition = self.get_definition(labware_id)
584
681
  stacking_overlap = definition.stackingOffsetWithModule.get(
585
682
  str(module_model.value)
586
683
  )
@@ -704,6 +801,12 @@ class LabwareView(HasState[LabwareState]):
704
801
  """Check if labware is fixed trash."""
705
802
  return self.get_has_quirk(labware_id, "fixedTrash")
706
803
 
804
+ def is_absorbance_reader_lid(self, labware_id: str) -> bool:
805
+ """Check if labware is an absorbance reader lid."""
806
+ return labware_validation.is_absorbance_reader_lid(
807
+ self.get(labware_id).loadName
808
+ )
809
+
707
810
  def raise_if_labware_inaccessible_by_pipette(self, labware_id: str) -> None:
708
811
  """Raise an error if the specified location cannot be reached via a pipette."""
709
812
  labware = self.get(labware_id)
@@ -734,7 +837,25 @@ class LabwareView(HasState[LabwareState]):
734
837
  f"Labware {labware.loadName} is already present at {location}."
735
838
  )
736
839
 
737
- def raise_if_labware_cannot_be_stacked(
840
+ def raise_if_labware_incompatible_with_plate_reader(
841
+ self,
842
+ labware_definition: LabwareDefinition,
843
+ ) -> None:
844
+ """Raise an error if the labware is not compatible with the plate reader."""
845
+ load_name = labware_definition.parameters.loadName
846
+ number_of_wells = len(labware_definition.wells)
847
+ if number_of_wells != 96:
848
+ raise errors.LabwareMovementNotAllowedError(
849
+ f"Cannot move '{load_name}' into plate reader because the"
850
+ f" labware contains {number_of_wells} wells where 96 wells is expected."
851
+ )
852
+ elif labware_definition.dimensions.zDimension > _PLATE_READER_MAX_LABWARE_Z_MM:
853
+ raise errors.LabwareMovementNotAllowedError(
854
+ f"Cannot move '{load_name}' into plate reader because the"
855
+ f" maximum allowed labware height is {_PLATE_READER_MAX_LABWARE_Z_MM}mm."
856
+ )
857
+
858
+ def raise_if_labware_cannot_be_stacked( # noqa: C901
738
859
  self, top_labware_definition: LabwareDefinition, bottom_labware_id: str
739
860
  ) -> None:
740
861
  """Raise if the specified labware definition cannot be placed on top of the bottom labware."""
@@ -753,17 +874,37 @@ class LabwareView(HasState[LabwareState]):
753
874
  )
754
875
  elif isinstance(below_labware.location, ModuleLocation):
755
876
  below_definition = self.get_definition(labware_id=below_labware.id)
756
- if not labware_validation.validate_definition_is_adapter(below_definition):
877
+ if not labware_validation.validate_definition_is_adapter(
878
+ below_definition
879
+ ) and not labware_validation.validate_definition_is_lid(
880
+ top_labware_definition
881
+ ):
757
882
  raise errors.LabwareCannotBeStackedError(
758
883
  f"Labware {top_labware_definition.parameters.loadName} cannot be loaded"
759
884
  f" onto a labware on top of a module"
760
885
  )
761
886
  elif isinstance(below_labware.location, OnLabwareLocation):
887
+ labware_stack = self.get_labware_stack([below_labware])
888
+ stack_without_adapters = []
889
+ for lw in labware_stack:
890
+ if not labware_validation.validate_definition_is_adapter(
891
+ self.get_definition(lw.id)
892
+ ):
893
+ stack_without_adapters.append(lw)
894
+ if len(stack_without_adapters) >= self.get_labware_stacking_maximum(
895
+ top_labware_definition
896
+ ):
897
+ raise errors.LabwareCannotBeStackedError(
898
+ f"Labware {top_labware_definition.parameters.loadName} cannot be loaded to stack of more than {self.get_labware_stacking_maximum(top_labware_definition)} labware."
899
+ )
900
+
762
901
  further_below_definition = self.get_definition(
763
902
  labware_id=below_labware.location.labwareId
764
903
  )
765
904
  if labware_validation.validate_definition_is_adapter(
766
905
  further_below_definition
906
+ ) and not labware_validation.validate_definition_is_lid(
907
+ top_labware_definition
767
908
  ):
768
909
  raise errors.LabwareCannotBeStackedError(
769
910
  f"Labware {top_labware_definition.parameters.loadName} cannot be loaded"
@@ -797,22 +938,60 @@ class LabwareView(HasState[LabwareState]):
797
938
  else None
798
939
  )
799
940
 
800
- def get_labware_gripper_offsets(
941
+ def get_absorbance_reader_lid_definition(self) -> LabwareDefinition:
942
+ """Return the special labware definition for the plate reader lid.
943
+
944
+ See todo comments in `create_protocol_engine().
945
+ """
946
+ # NOTE: This needs to stay in sync with create_protocol_engine().
947
+ return self._state.definitions_by_uri[
948
+ "opentrons/opentrons_flex_lid_absorbance_plate_reader_module/1"
949
+ ]
950
+
951
+ @overload
952
+ def get_child_gripper_offsets(
801
953
  self,
802
- labware_id: str,
954
+ *,
955
+ labware_definition: LabwareDefinition,
956
+ slot_name: Optional[DeckSlotName],
957
+ ) -> Optional[LabwareMovementOffsetData]:
958
+ pass
959
+
960
+ @overload
961
+ def get_child_gripper_offsets(
962
+ self, *, labware_id: str, slot_name: Optional[DeckSlotName]
963
+ ) -> Optional[LabwareMovementOffsetData]:
964
+ pass
965
+
966
+ def get_child_gripper_offsets(
967
+ self,
968
+ *,
969
+ labware_definition: Optional[LabwareDefinition] = None,
970
+ labware_id: Optional[str] = None,
803
971
  slot_name: Optional[DeckSlotName],
804
972
  ) -> Optional[LabwareMovementOffsetData]:
805
- """Get the labware's gripper offsets of the specified type.
973
+ """Get the grip offsets that a labware says should be applied to children stacked atop it.
974
+
975
+ Params:
976
+ labware_id: The ID of a parent labware (atop which another labware, the child, will be stacked).
977
+ slot_name: The ancestor slot that the parent labware is ultimately loaded into,
978
+ perhaps after going through a module in the middle.
806
979
 
807
980
  Returns:
808
- If `slot_name` is provided, returns the gripper offsets that the labware definition
981
+ If `slot_name` is provided, returns the gripper offsets that the parent labware definition
809
982
  specifies just for that slot, or `None` if the labware definition doesn't have an
810
983
  exact match.
811
984
 
812
- If `slot_name` is `None`, returns the gripper offsets that the labware
985
+ If `slot_name` is `None`, returns the gripper offsets that the parent labware
813
986
  definition designates as "default," or `None` if it doesn't designate any as such.
814
987
  """
815
- parsed_offsets = self.get_definition(labware_id).gripperOffsets
988
+ if labware_id is not None:
989
+ labware_definition = self.get_definition(labware_id)
990
+ else:
991
+ # Should be ensured by our @overloads.
992
+ assert labware_definition is not None
993
+
994
+ parsed_offsets = labware_definition.gripperOffsets
816
995
  offset_key = slot_name.id if slot_name else "default"
817
996
 
818
997
  if parsed_offsets is None or offset_key not in parsed_offsets:
@@ -827,20 +1006,22 @@ class LabwareView(HasState[LabwareState]):
827
1006
  ),
828
1007
  )
829
1008
 
830
- def get_grip_force(self, labware_id: str) -> float:
1009
+ def get_grip_force(self, labware_definition: LabwareDefinition) -> float:
831
1010
  """Get the recommended grip force for gripping labware using gripper."""
832
- recommended_force = self.get_definition(labware_id).gripForce
1011
+ recommended_force = labware_definition.gripForce
833
1012
  return (
834
1013
  recommended_force if recommended_force is not None else LABWARE_GRIP_FORCE
835
1014
  )
836
1015
 
837
- def get_grip_height_from_labware_bottom(self, labware_id: str) -> float:
1016
+ def get_grip_height_from_labware_bottom(
1017
+ self, labware_definition: LabwareDefinition
1018
+ ) -> float:
838
1019
  """Get the recommended grip height from labware bottom, if present."""
839
- recommended_height = self.get_definition(labware_id).gripHeightFromLabwareBottom
1020
+ recommended_height = labware_definition.gripHeightFromLabwareBottom
840
1021
  return (
841
1022
  recommended_height
842
1023
  if recommended_height is not None
843
- else self.get_dimensions(labware_id).z / 2
1024
+ else self.get_dimensions(labware_definition=labware_definition).z / 2
844
1025
  )
845
1026
 
846
1027
  @staticmethod
@@ -883,7 +1064,7 @@ class LabwareView(HasState[LabwareState]):
883
1064
  def _max_z_of_well(well_defn: WellDefinition) -> float:
884
1065
  return well_defn.z + well_defn.depth
885
1066
 
886
- def get_well_bbox(self, labware_id: str) -> Dimensions:
1067
+ def get_well_bbox(self, labware_definition: LabwareDefinition) -> Dimensions:
887
1068
  """Get the bounding box implied by the wells.
888
1069
 
889
1070
  The bounding box of the labware that is implied by the wells is that required
@@ -894,14 +1075,13 @@ class LabwareView(HasState[LabwareState]):
894
1075
  This is used for the specific purpose of finding the reasonable uncertainty bounds of
895
1076
  where and how a gripper will interact with a labware.
896
1077
  """
897
- defn = self.get_definition(labware_id)
898
1078
  max_x: Optional[float] = None
899
1079
  min_x: Optional[float] = None
900
1080
  max_y: Optional[float] = None
901
1081
  min_y: Optional[float] = None
902
1082
  max_z: Optional[float] = None
903
1083
 
904
- for well in defn.wells.values():
1084
+ for well in labware_definition.wells.values():
905
1085
  well_max_x = self._max_x_of_well(well)
906
1086
  well_min_x = self._min_x_of_well(well)
907
1087
  well_max_y = self._max_y_of_well(well)
@@ -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,11 +1,17 @@
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
 
12
+ # todo(mm, 2024-11-08): frozen=True is getting pretty painful because ModuleStore has
13
+ # no type-safe way to modify just a single attribute. Consider unfreezing this
14
+ # (taking care to ensure that consumers of ModuleView still only get a read-only view).
9
15
  @dataclass(frozen=True)
10
16
  class AbsorbanceReaderSubState:
11
17
  """Absorbance-Plate-Reader-specific state."""
@@ -13,5 +19,17 @@ class AbsorbanceReaderSubState:
13
19
  module_id: AbsorbanceReaderId
14
20
  configured: bool
15
21
  measured: bool
16
- data: Optional[List[float]]
17
- configured_wavelength: Optional[int]
22
+ is_lid_on: bool
23
+ data: Optional[Dict[int, Dict[str, float]]]
24
+ configured_wavelengths: Optional[List[int]]
25
+ measure_mode: Optional[AbsorbanceReaderMeasureMode]
26
+ reference_wavelength: Optional[int]
27
+
28
+ def raise_if_lid_status_not_expected(self, lid_on_expected: bool) -> None:
29
+ """Raise if the lid status is not correct."""
30
+ match = self.is_lid_on is lid_on_expected
31
+ if not match:
32
+ raise CannotPerformModuleAction(
33
+ "Cannot perform lid action because the lid is already "
34
+ f"{'closed' if self.is_lid_on else 'open'}"
35
+ )