opentrons 8.3.1a1__py2.py3-none-any.whl → 8.4.0a1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. opentrons/calibration_storage/ot2/mark_bad_calibration.py +2 -0
  2. opentrons/calibration_storage/ot2/tip_length.py +6 -6
  3. opentrons/config/advanced_settings.py +9 -11
  4. opentrons/config/feature_flags.py +0 -4
  5. opentrons/config/reset.py +7 -2
  6. opentrons/drivers/asyncio/communication/__init__.py +2 -0
  7. opentrons/drivers/asyncio/communication/async_serial.py +4 -0
  8. opentrons/drivers/asyncio/communication/errors.py +41 -8
  9. opentrons/drivers/asyncio/communication/serial_connection.py +36 -10
  10. opentrons/drivers/flex_stacker/__init__.py +9 -3
  11. opentrons/drivers/flex_stacker/abstract.py +140 -15
  12. opentrons/drivers/flex_stacker/driver.py +593 -47
  13. opentrons/drivers/flex_stacker/errors.py +64 -0
  14. opentrons/drivers/flex_stacker/simulator.py +222 -24
  15. opentrons/drivers/flex_stacker/types.py +211 -15
  16. opentrons/drivers/flex_stacker/utils.py +19 -0
  17. opentrons/execute.py +4 -2
  18. opentrons/hardware_control/api.py +5 -0
  19. opentrons/hardware_control/backends/flex_protocol.py +4 -0
  20. opentrons/hardware_control/backends/ot3controller.py +12 -1
  21. opentrons/hardware_control/backends/ot3simulator.py +3 -0
  22. opentrons/hardware_control/backends/subsystem_manager.py +8 -4
  23. opentrons/hardware_control/instruments/ot2/instrument_calibration.py +10 -6
  24. opentrons/hardware_control/instruments/ot3/pipette_handler.py +59 -6
  25. opentrons/hardware_control/modules/__init__.py +12 -1
  26. opentrons/hardware_control/modules/absorbance_reader.py +11 -9
  27. opentrons/hardware_control/modules/flex_stacker.py +498 -0
  28. opentrons/hardware_control/modules/heater_shaker.py +12 -10
  29. opentrons/hardware_control/modules/magdeck.py +5 -1
  30. opentrons/hardware_control/modules/tempdeck.py +5 -1
  31. opentrons/hardware_control/modules/thermocycler.py +15 -14
  32. opentrons/hardware_control/modules/types.py +191 -1
  33. opentrons/hardware_control/modules/utils.py +3 -0
  34. opentrons/hardware_control/motion_utilities.py +20 -0
  35. opentrons/hardware_control/ot3api.py +145 -15
  36. opentrons/hardware_control/protocols/liquid_handler.py +47 -1
  37. opentrons/hardware_control/types.py +6 -0
  38. opentrons/legacy_commands/commands.py +19 -3
  39. opentrons/legacy_commands/helpers.py +15 -0
  40. opentrons/legacy_commands/types.py +3 -2
  41. opentrons/protocol_api/__init__.py +2 -0
  42. opentrons/protocol_api/_liquid.py +39 -8
  43. opentrons/protocol_api/_liquid_properties.py +20 -19
  44. opentrons/protocol_api/_transfer_liquid_validation.py +91 -0
  45. opentrons/protocol_api/core/common.py +3 -1
  46. opentrons/protocol_api/core/engine/deck_conflict.py +11 -1
  47. opentrons/protocol_api/core/engine/instrument.py +1233 -65
  48. opentrons/protocol_api/core/engine/labware.py +8 -4
  49. opentrons/protocol_api/core/engine/load_labware_params.py +68 -10
  50. opentrons/protocol_api/core/engine/module_core.py +118 -2
  51. opentrons/protocol_api/core/engine/protocol.py +253 -11
  52. opentrons/protocol_api/core/engine/stringify.py +19 -8
  53. opentrons/protocol_api/core/engine/transfer_components_executor.py +853 -0
  54. opentrons/protocol_api/core/engine/well.py +60 -5
  55. opentrons/protocol_api/core/instrument.py +65 -19
  56. opentrons/protocol_api/core/labware.py +6 -2
  57. opentrons/protocol_api/core/legacy/labware_offset_provider.py +7 -3
  58. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +69 -21
  59. opentrons/protocol_api/core/legacy/legacy_labware_core.py +8 -4
  60. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +36 -0
  61. opentrons/protocol_api/core/legacy/legacy_well_core.py +25 -1
  62. opentrons/protocol_api/core/legacy/load_info.py +4 -12
  63. opentrons/protocol_api/core/legacy/module_geometry.py +6 -1
  64. opentrons/protocol_api/core/legacy/well_geometry.py +3 -3
  65. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +67 -21
  66. opentrons/protocol_api/core/module.py +43 -0
  67. opentrons/protocol_api/core/protocol.py +33 -0
  68. opentrons/protocol_api/core/well.py +21 -1
  69. opentrons/protocol_api/instrument_context.py +246 -123
  70. opentrons/protocol_api/labware.py +75 -11
  71. opentrons/protocol_api/module_contexts.py +140 -0
  72. opentrons/protocol_api/protocol_context.py +156 -16
  73. opentrons/protocol_api/validation.py +51 -41
  74. opentrons/protocol_engine/__init__.py +21 -2
  75. opentrons/protocol_engine/actions/actions.py +5 -5
  76. opentrons/protocol_engine/clients/sync_client.py +6 -0
  77. opentrons/protocol_engine/commands/__init__.py +30 -0
  78. opentrons/protocol_engine/commands/absorbance_reader/__init__.py +0 -1
  79. opentrons/protocol_engine/commands/air_gap_in_place.py +3 -2
  80. opentrons/protocol_engine/commands/aspirate.py +6 -2
  81. opentrons/protocol_engine/commands/aspirate_in_place.py +3 -1
  82. opentrons/protocol_engine/commands/aspirate_while_tracking.py +237 -0
  83. opentrons/protocol_engine/commands/blow_out.py +2 -0
  84. opentrons/protocol_engine/commands/blow_out_in_place.py +4 -1
  85. opentrons/protocol_engine/commands/command_unions.py +69 -0
  86. opentrons/protocol_engine/commands/configure_for_volume.py +3 -0
  87. opentrons/protocol_engine/commands/dispense.py +3 -1
  88. opentrons/protocol_engine/commands/dispense_in_place.py +3 -0
  89. opentrons/protocol_engine/commands/dispense_while_tracking.py +240 -0
  90. opentrons/protocol_engine/commands/drop_tip.py +23 -1
  91. opentrons/protocol_engine/commands/evotip_dispense.py +6 -7
  92. opentrons/protocol_engine/commands/evotip_seal_pipette.py +24 -29
  93. opentrons/protocol_engine/commands/evotip_unseal_pipette.py +1 -7
  94. opentrons/protocol_engine/commands/flex_stacker/__init__.py +106 -0
  95. opentrons/protocol_engine/commands/flex_stacker/close_latch.py +72 -0
  96. opentrons/protocol_engine/commands/flex_stacker/common.py +15 -0
  97. opentrons/protocol_engine/commands/flex_stacker/empty.py +161 -0
  98. opentrons/protocol_engine/commands/flex_stacker/fill.py +164 -0
  99. opentrons/protocol_engine/commands/flex_stacker/open_latch.py +70 -0
  100. opentrons/protocol_engine/commands/flex_stacker/prepare_shuttle.py +112 -0
  101. opentrons/protocol_engine/commands/flex_stacker/retrieve.py +394 -0
  102. opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +190 -0
  103. opentrons/protocol_engine/commands/flex_stacker/store.py +288 -0
  104. opentrons/protocol_engine/commands/generate_command_schema.py +31 -2
  105. opentrons/protocol_engine/commands/labware_handling_common.py +24 -0
  106. opentrons/protocol_engine/commands/liquid_probe.py +21 -12
  107. opentrons/protocol_engine/commands/load_labware.py +42 -39
  108. opentrons/protocol_engine/commands/load_lid.py +21 -13
  109. opentrons/protocol_engine/commands/load_lid_stack.py +130 -47
  110. opentrons/protocol_engine/commands/load_module.py +18 -17
  111. opentrons/protocol_engine/commands/load_pipette.py +3 -0
  112. opentrons/protocol_engine/commands/move_labware.py +139 -20
  113. opentrons/protocol_engine/commands/pick_up_tip.py +5 -2
  114. opentrons/protocol_engine/commands/pipetting_common.py +154 -7
  115. opentrons/protocol_engine/commands/prepare_to_aspirate.py +17 -2
  116. opentrons/protocol_engine/commands/reload_labware.py +6 -19
  117. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +3 -1
  118. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +6 -1
  119. opentrons/protocol_engine/errors/__init__.py +8 -0
  120. opentrons/protocol_engine/errors/exceptions.py +50 -0
  121. opentrons/protocol_engine/execution/equipment.py +123 -106
  122. opentrons/protocol_engine/execution/labware_movement.py +8 -6
  123. opentrons/protocol_engine/execution/pipetting.py +233 -26
  124. opentrons/protocol_engine/execution/tip_handler.py +14 -5
  125. opentrons/protocol_engine/labware_offset_standardization.py +173 -0
  126. opentrons/protocol_engine/protocol_engine.py +22 -13
  127. opentrons/protocol_engine/resources/deck_configuration_provider.py +94 -2
  128. opentrons/protocol_engine/resources/deck_data_provider.py +1 -1
  129. opentrons/protocol_engine/resources/labware_data_provider.py +32 -12
  130. opentrons/protocol_engine/resources/labware_validation.py +7 -5
  131. opentrons/protocol_engine/slot_standardization.py +11 -23
  132. opentrons/protocol_engine/state/addressable_areas.py +84 -46
  133. opentrons/protocol_engine/state/frustum_helpers.py +26 -10
  134. opentrons/protocol_engine/state/geometry.py +683 -100
  135. opentrons/protocol_engine/state/labware.py +252 -55
  136. opentrons/protocol_engine/state/module_substates/__init__.py +4 -0
  137. opentrons/protocol_engine/state/module_substates/flex_stacker_substate.py +68 -0
  138. opentrons/protocol_engine/state/module_substates/heater_shaker_module_substate.py +22 -0
  139. opentrons/protocol_engine/state/module_substates/temperature_module_substate.py +13 -0
  140. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +20 -0
  141. opentrons/protocol_engine/state/modules.py +178 -52
  142. opentrons/protocol_engine/state/pipettes.py +54 -0
  143. opentrons/protocol_engine/state/state.py +1 -1
  144. opentrons/protocol_engine/state/tips.py +14 -0
  145. opentrons/protocol_engine/state/update_types.py +180 -25
  146. opentrons/protocol_engine/state/wells.py +54 -8
  147. opentrons/protocol_engine/types/__init__.py +292 -0
  148. opentrons/protocol_engine/types/automatic_tip_selection.py +39 -0
  149. opentrons/protocol_engine/types/command_annotations.py +53 -0
  150. opentrons/protocol_engine/types/deck_configuration.py +72 -0
  151. opentrons/protocol_engine/types/execution.py +96 -0
  152. opentrons/protocol_engine/types/hardware_passthrough.py +25 -0
  153. opentrons/protocol_engine/types/instrument.py +47 -0
  154. opentrons/protocol_engine/types/instrument_sensors.py +47 -0
  155. opentrons/protocol_engine/types/labware.py +110 -0
  156. opentrons/protocol_engine/types/labware_movement.py +22 -0
  157. opentrons/protocol_engine/types/labware_offset_location.py +108 -0
  158. opentrons/protocol_engine/types/labware_offset_vector.py +33 -0
  159. opentrons/protocol_engine/types/liquid.py +40 -0
  160. opentrons/protocol_engine/types/liquid_class.py +59 -0
  161. opentrons/protocol_engine/types/liquid_handling.py +13 -0
  162. opentrons/protocol_engine/types/liquid_level_detection.py +137 -0
  163. opentrons/protocol_engine/types/location.py +193 -0
  164. opentrons/protocol_engine/types/module.py +269 -0
  165. opentrons/protocol_engine/types/partial_tip_configuration.py +76 -0
  166. opentrons/protocol_engine/types/run_time_parameters.py +133 -0
  167. opentrons/protocol_engine/types/tip.py +18 -0
  168. opentrons/protocol_engine/types/util.py +21 -0
  169. opentrons/protocol_engine/types/well_position.py +107 -0
  170. opentrons/protocol_reader/extract_labware_definitions.py +7 -3
  171. opentrons/protocol_reader/file_format_validator.py +5 -3
  172. opentrons/protocol_runner/json_translator.py +4 -2
  173. opentrons/protocol_runner/legacy_command_mapper.py +6 -2
  174. opentrons/protocol_runner/run_orchestrator.py +4 -1
  175. opentrons/protocols/advanced_control/transfers/common.py +48 -1
  176. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +204 -0
  177. opentrons/protocols/api_support/definitions.py +1 -1
  178. opentrons/protocols/api_support/instrument.py +16 -3
  179. opentrons/protocols/labware.py +5 -6
  180. opentrons/protocols/models/__init__.py +0 -21
  181. opentrons/simulate.py +4 -2
  182. opentrons/types.py +15 -6
  183. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a1.dist-info}/METADATA +4 -4
  184. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a1.dist-info}/RECORD +188 -148
  185. opentrons/calibration_storage/ot2/models/defaults.py +0 -0
  186. opentrons/calibration_storage/ot3/models/defaults.py +0 -0
  187. opentrons/protocol_api/core/legacy/legacy_robot_core.py +0 -0
  188. opentrons/protocol_engine/types.py +0 -1311
  189. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a1.dist-info}/LICENSE +0 -0
  190. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a1.dist-info}/WHEEL +0 -0
  191. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a1.dist-info}/entry_points.txt +0 -0
  192. {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a1.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,5 @@
1
1
  """Basic labware data state and store."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  from dataclasses import dataclass
@@ -11,23 +12,25 @@ from typing import (
11
12
  Sequence,
12
13
  Tuple,
13
14
  NamedTuple,
14
- cast,
15
15
  Union,
16
16
  overload,
17
17
  )
18
+ from typing_extensions import assert_never
18
19
 
19
20
  from opentrons.protocol_engine.state import update_types
20
21
  from opentrons_shared_data.deck.types import DeckDefinitionV5
21
22
  from opentrons_shared_data.gripper.constants import LABWARE_GRIP_FORCE
22
23
  from opentrons_shared_data.labware.labware_definition import (
23
- LabwareRole,
24
24
  InnerWellGeometry,
25
+ LabwareDefinition,
26
+ LabwareRole,
27
+ WellDefinition2,
28
+ WellDefinition3,
25
29
  )
26
30
  from opentrons_shared_data.pipette.types import LabwareUri
27
31
 
28
32
  from opentrons.types import DeckSlotName, StagingSlotName, MountType
29
33
  from opentrons.protocols.api_support.constants import OPENTRONS_NAMESPACE
30
- from opentrons.protocols.models import LabwareDefinition, WellDefinition
31
34
  from opentrons.calibration_storage.helpers import uri_from_details
32
35
 
33
36
  from .. import errors
@@ -40,7 +43,9 @@ from ..types import (
40
43
  Dimensions,
41
44
  LabwareOffset,
42
45
  LabwareOffsetVector,
43
- LabwareOffsetLocation,
46
+ LabwareOffsetLocationSequence,
47
+ LegacyLabwareOffsetLocation,
48
+ InStackerHopperLocation,
44
49
  LabwareLocation,
45
50
  LoadedLabware,
46
51
  ModuleLocation,
@@ -49,6 +54,7 @@ from ..types import (
49
54
  LabwareMovementOffsetData,
50
55
  OnDeckLabwareLocation,
51
56
  OFF_DECK_LOCATION,
57
+ SYSTEM_LOCATION,
52
58
  )
53
59
  from ..actions import (
54
60
  Action,
@@ -86,6 +92,9 @@ _RIGHT_SIDE_SLOTS = {
86
92
  _PLATE_READER_MAX_LABWARE_Z_MM = 16
87
93
 
88
94
 
95
+ _WellDefinition = WellDefinition2 | WellDefinition3
96
+
97
+
89
98
  class LabwareLoadParams(NamedTuple):
90
99
  """Parameters required to load a labware in Protocol Engine."""
91
100
 
@@ -156,8 +165,10 @@ class LabwareStore(HasState[LabwareState], HandlesActions):
156
165
  """Modify state in reaction to an action."""
157
166
  for state_update in get_state_updates(action):
158
167
  self._add_loaded_labware(state_update)
168
+ self._add_batch_loaded_labwares(state_update)
159
169
  self._add_loaded_lid_stack(state_update)
160
170
  self._set_labware_location(state_update)
171
+ self._set_batch_labware_location(state_update)
161
172
  self._set_labware_lid(state_update)
162
173
 
163
174
  if isinstance(action, AddLabwareOffsetAction):
@@ -165,7 +176,8 @@ class LabwareStore(HasState[LabwareState], HandlesActions):
165
176
  id=action.labware_offset_id,
166
177
  createdAt=action.created_at,
167
178
  definitionUri=action.request.definitionUri,
168
- location=action.request.location,
179
+ location=action.request.legacyLocation,
180
+ locationSequence=action.request.locationSequence,
169
181
  vector=action.request.vector,
170
182
  )
171
183
  self._add_labware_offset(labware_offset)
@@ -223,6 +235,49 @@ class LabwareStore(HasState[LabwareState], HandlesActions):
223
235
  displayName=display_name,
224
236
  )
225
237
 
238
+ def _add_batch_loaded_labwares(
239
+ self, state_update: update_types.StateUpdate
240
+ ) -> None:
241
+ batch_loaded_labware_update = state_update.batch_loaded_labware
242
+ if batch_loaded_labware_update == update_types.NO_CHANGE:
243
+ return
244
+ # If the labware load refers to an offset, that offset must actually exist.
245
+ for labware_id in batch_loaded_labware_update.new_locations_by_id:
246
+ if batch_loaded_labware_update.offset_ids_by_id[labware_id] is not None:
247
+ assert (
248
+ batch_loaded_labware_update.offset_ids_by_id[labware_id]
249
+ in self._state.labware_offsets_by_id
250
+ )
251
+
252
+ definition_uri = uri_from_details(
253
+ namespace=batch_loaded_labware_update.definitions_by_id[
254
+ labware_id
255
+ ].namespace,
256
+ load_name=batch_loaded_labware_update.definitions_by_id[
257
+ labware_id
258
+ ].parameters.loadName,
259
+ version=batch_loaded_labware_update.definitions_by_id[
260
+ labware_id
261
+ ].version,
262
+ )
263
+
264
+ self._state.definitions_by_uri[
265
+ definition_uri
266
+ ] = batch_loaded_labware_update.definitions_by_id[labware_id]
267
+
268
+ location = batch_loaded_labware_update.new_locations_by_id[labware_id]
269
+
270
+ self._state.labware_by_id[labware_id] = LoadedLabware.model_construct(
271
+ id=labware_id,
272
+ location=location,
273
+ loadName=batch_loaded_labware_update.definitions_by_id[
274
+ labware_id
275
+ ].parameters.loadName,
276
+ definitionUri=definition_uri,
277
+ offsetId=batch_loaded_labware_update.offset_ids_by_id[labware_id],
278
+ displayName=batch_loaded_labware_update.display_names_by_id[labware_id],
279
+ )
280
+
226
281
  def _add_loaded_lid_stack(self, state_update: update_types.StateUpdate) -> None:
227
282
  loaded_lid_stack_update = state_update.loaded_lid_stack
228
283
  if loaded_lid_stack_update != update_types.NO_CHANGE:
@@ -247,7 +302,11 @@ class LabwareStore(HasState[LabwareState], HandlesActions):
247
302
  )
248
303
 
249
304
  # Add the Lids on top of the stack object
250
- for i in range(len(loaded_lid_stack_update.labware_ids)):
305
+ for labware_id in loaded_lid_stack_update.new_locations_by_id:
306
+ if loaded_lid_stack_update.definition is None:
307
+ raise ValueError(
308
+ "Lid Stack Labware Definition cannot be None when multiple lids are loaded."
309
+ )
251
310
  definition_uri = uri_from_details(
252
311
  namespace=loaded_lid_stack_update.definition.namespace,
253
312
  load_name=loaded_lid_stack_update.definition.parameters.loadName,
@@ -258,14 +317,10 @@ class LabwareStore(HasState[LabwareState], HandlesActions):
258
317
  definition_uri
259
318
  ] = loaded_lid_stack_update.definition
260
319
 
261
- location = loaded_lid_stack_update.new_locations_by_id[
262
- loaded_lid_stack_update.labware_ids[i]
263
- ]
320
+ location = loaded_lid_stack_update.new_locations_by_id[labware_id]
264
321
 
265
- self._state.labware_by_id[
266
- loaded_lid_stack_update.labware_ids[i]
267
- ] = LoadedLabware.construct(
268
- id=loaded_lid_stack_update.labware_ids[i],
322
+ self._state.labware_by_id[labware_id] = LoadedLabware.construct(
323
+ id=labware_id,
269
324
  location=location,
270
325
  loadName=loaded_lid_stack_update.definition.parameters.loadName,
271
326
  definitionUri=definition_uri,
@@ -276,31 +331,51 @@ class LabwareStore(HasState[LabwareState], HandlesActions):
276
331
  def _set_labware_lid(self, state_update: update_types.StateUpdate) -> None:
277
332
  labware_lid_update = state_update.labware_lid
278
333
  if labware_lid_update != update_types.NO_CHANGE:
279
- parent_labware_id = labware_lid_update.parent_labware_id
280
- lid_id = labware_lid_update.lid_id
281
- self._state.labware_by_id[parent_labware_id].lid_id = lid_id
334
+ parent_labware_ids = labware_lid_update.parent_labware_ids
335
+ for i in range(len(parent_labware_ids)):
336
+ lid_id = labware_lid_update.lid_ids[i]
337
+ self._state.labware_by_id[parent_labware_ids[i]].lid_id = lid_id
282
338
 
283
- def _set_labware_location(self, state_update: update_types.StateUpdate) -> None:
284
- labware_location_update = state_update.labware_location
285
- if labware_location_update != update_types.NO_CHANGE:
286
- labware_id = labware_location_update.labware_id
287
- new_offset_id = labware_location_update.offset_id
339
+ def _do_update_labware_location(
340
+ self, labware_id: str, new_location: LabwareLocation, new_offset_id: str | None
341
+ ) -> None:
342
+ self._state.labware_by_id[labware_id].offsetId = new_offset_id
288
343
 
289
- self._state.labware_by_id[labware_id].offsetId = new_offset_id
344
+ if isinstance(new_location, AddressableAreaLocation) and (
345
+ fixture_validation.is_gripper_waste_chute(new_location.addressableAreaName)
346
+ or fixture_validation.is_trash(new_location.addressableAreaName)
347
+ ):
348
+ # If a labware has been moved into a waste chute it's been chuted away and is now technically off deck
349
+ new_location = OFF_DECK_LOCATION
290
350
 
291
- if labware_location_update.new_location:
292
- new_location = labware_location_update.new_location
351
+ self._state.labware_by_id[labware_id].location = new_location
293
352
 
294
- if isinstance(new_location, AddressableAreaLocation) and (
295
- fixture_validation.is_gripper_waste_chute(
296
- new_location.addressableAreaName
297
- )
298
- or fixture_validation.is_trash(new_location.addressableAreaName)
299
- ):
300
- # If a labware has been moved into a waste chute it's been chuted away and is now technically off deck
301
- new_location = OFF_DECK_LOCATION
353
+ def _set_labware_location(self, state_update: update_types.StateUpdate) -> None:
354
+ labware_location_update = state_update.labware_location
355
+ if labware_location_update == update_types.NO_CHANGE:
356
+ return
302
357
 
303
- self._state.labware_by_id[labware_id].location = new_location
358
+ self._do_update_labware_location(
359
+ labware_location_update.labware_id,
360
+ labware_location_update.new_location,
361
+ labware_location_update.offset_id,
362
+ )
363
+
364
+ def _set_batch_labware_location(
365
+ self, state_update: update_types.StateUpdate
366
+ ) -> None:
367
+ batch_location_update = state_update.batch_labware_location
368
+ if batch_location_update == update_types.NO_CHANGE:
369
+ return
370
+ for (
371
+ labware_id,
372
+ new_location,
373
+ ) in batch_location_update.new_locations_by_id.items():
374
+ self._do_update_labware_location(
375
+ labware_id,
376
+ new_location,
377
+ batch_location_update.new_offset_ids_by_id.get(labware_id, None),
378
+ )
304
379
 
305
380
 
306
381
  class LabwareView:
@@ -350,6 +425,19 @@ class LabwareView:
350
425
  f"There is not labware loaded onto labware {labware_id}"
351
426
  )
352
427
 
428
+ def raise_if_labware_has_non_lid_labware_on_top(self, labware_id: str) -> None:
429
+ """Raise if labware has another labware that is not its lid on top."""
430
+ lid_id = self.get_lid_id_by_labware_id(labware_id)
431
+ for candidate_id, candidate_labware in self._state.labware_by_id.items():
432
+ if (
433
+ isinstance(candidate_labware.location, OnLabwareLocation)
434
+ and candidate_labware.location.labwareId == labware_id
435
+ and candidate_id != lid_id
436
+ ):
437
+ raise errors.LabwareIsInStackError(
438
+ f"Cannot access labware {labware_id} because it has a non-lid labware stacked on top."
439
+ )
440
+
353
441
  def raise_if_labware_has_labware_on_top(self, labware_id: str) -> None:
354
442
  """Raise if labware has another labware on top."""
355
443
  for labware in self._state.labware_by_id.values():
@@ -358,7 +446,7 @@ class LabwareView:
358
446
  and labware.location.labwareId == labware_id
359
447
  ):
360
448
  raise errors.LabwareIsInStackError(
361
- f"Cannot move to labware {labware_id}, labware has other labware stacked on top."
449
+ f"Cannot access labware {labware_id} because it has another labware stacked on top."
362
450
  )
363
451
 
364
452
  def get_by_slot(
@@ -459,8 +547,42 @@ class LabwareView:
459
547
  parent = self.get_location(labware_id)
460
548
  if isinstance(parent, OnLabwareLocation):
461
549
  return self.get_parent_location(parent.labwareId)
550
+ elif isinstance(parent, InStackerHopperLocation):
551
+ # TODO: This function really wants to return something like an "EventuallyOnDeckLocation"
552
+ # and either raise or return None for labware that isn't traceable to a place on the robot
553
+ # deck (i.e. not in a stacker hopper, not off-deck, not in system). We don't really have
554
+ # that concept yet but should add it soon. In the meantime, other checks should prevent
555
+ # this being called in those cases.
556
+ return ModuleLocation(moduleId=parent.moduleId)
462
557
  return parent
463
558
 
559
+ def get_highest_child_labware(self, labware_id: str) -> str:
560
+ """Get labware's highest child labware returning the labware ID."""
561
+ if (child_id := self.get_next_child_labware(labware_id)) is not None:
562
+ return self.get_highest_child_labware(labware_id=child_id)
563
+ return labware_id
564
+
565
+ def get_next_child_labware(self, labware_id: str) -> str | None:
566
+ """Get the labware that is on this labware, if any.
567
+
568
+ This includes lids.
569
+ """
570
+ for labware in self._state.labware_by_id.values():
571
+ if (
572
+ isinstance(labware.location, OnLabwareLocation)
573
+ and labware.location.labwareId == labware_id
574
+ ):
575
+ return labware.id
576
+ return None
577
+
578
+ def get_labware_stack_from_parent(self, labware_id: str) -> list[str]:
579
+ """Get the stack of labware starting from the specified labware ID and moving up."""
580
+ labware_ids = [labware_id]
581
+ while (next_id := self.get_next_child_labware(labware_id)) is not None:
582
+ labware_ids.append(next_id)
583
+ labware_id = next_id
584
+ return labware_ids
585
+
464
586
  def get_labware_stack(
465
587
  self, labware_stack: List[LoadedLabware]
466
588
  ) -> List[LoadedLabware]:
@@ -471,6 +593,26 @@ class LabwareView:
471
593
  return self.get_labware_stack(labware_stack)
472
594
  return labware_stack
473
595
 
596
+ def get_lid_id_by_labware_id(self, labware_id: str) -> str | None:
597
+ """Get the ID of a lid labware on top of a given labware, if any."""
598
+ return self._state.labware_by_id[labware_id].lid_id
599
+
600
+ def get_lid_by_labware_id(self, labware_id: str) -> LoadedLabware | None:
601
+ """Get the Lid Labware that is currently on top of a given labware, if there is one."""
602
+ lid_id = self.get_lid_id_by_labware_id(labware_id)
603
+ if lid_id:
604
+ return self._state.labware_by_id[lid_id]
605
+ else:
606
+ return None
607
+
608
+ def get_labware_by_lid_id(self, lid_id: str) -> LoadedLabware | None:
609
+ """Get the labware that is currently covered by a given lid, if there is one."""
610
+ loaded_labware = list(self._state.labware_by_id.values())
611
+ for labware in loaded_labware:
612
+ if labware.lid_id == lid_id:
613
+ return labware
614
+ return None
615
+
474
616
  def get_all(self) -> List[LoadedLabware]:
475
617
  """Get a list of all labware entries in state."""
476
618
  return list(self._state.labware_by_id.values())
@@ -517,7 +659,7 @@ class LabwareView:
517
659
  self,
518
660
  labware_id: str,
519
661
  well_name: Optional[str] = None,
520
- ) -> WellDefinition:
662
+ ) -> WellDefinition2 | WellDefinition3:
521
663
  """Get a well's definition by labware and well name.
522
664
 
523
665
  If `well_name` is omitted, the first well in the labware
@@ -544,16 +686,16 @@ class LabwareView:
544
686
  message=f"No innerLabwareGeometry found in labware definition for labware_id: {labware_id}."
545
687
  )
546
688
  well_def = self.get_well_definition(labware_id, well_name)
547
- well_id = well_def.geometryDefinitionId
548
- if well_id is None:
689
+ geometry_id = well_def.geometryDefinitionId
690
+ if geometry_id is None:
549
691
  raise errors.IncompleteWellDefinitionError(
550
692
  message=f"No geometryDefinitionId found in well definition for well: {well_name} in labware_id: {labware_id}"
551
693
  )
552
694
  else:
553
- well_geometry = labware_def.innerLabwareGeometry.get(well_id)
695
+ well_geometry = labware_def.innerLabwareGeometry.get(geometry_id)
554
696
  if well_geometry is None:
555
697
  raise errors.IncompleteLabwareDefinitionError(
556
- message=f"No innerLabwareGeometry found in labware definition for well_id: {well_id} in labware_id: {labware_id}"
698
+ message=f"No innerLabwareGeometry found in labware definition for well_id: {geometry_id} in labware_id: {labware_id}"
557
699
  )
558
700
  return well_geometry
559
701
 
@@ -572,12 +714,13 @@ class LabwareView:
572
714
  """
573
715
  well_definition = self.get_well_definition(labware_id, well_name)
574
716
 
575
- if well_definition.diameter is not None:
717
+ if well_definition.shape == "circular":
576
718
  x_size = y_size = well_definition.diameter
719
+ elif well_definition.shape == "rectangular":
720
+ x_size = well_definition.xDimension
721
+ y_size = well_definition.yDimension
577
722
  else:
578
- # If diameter is None we know these values will be floats
579
- x_size = cast(float, well_definition.xDimension)
580
- y_size = cast(float, well_definition.yDimension)
723
+ assert_never(well_definition.shape)
581
724
 
582
725
  return x_size, y_size, well_definition.depth
583
726
 
@@ -796,15 +939,32 @@ class LabwareView:
796
939
  """Get all labware offsets, in the order they were added."""
797
940
  return list(self._state.labware_offsets_by_id.values())
798
941
 
799
- # TODO: Make this slightly more ergonomic for the caller by
800
- # only returning the optional str ID, at the cost of baking redundant lookups
801
- # into the API?
802
942
  def find_applicable_labware_offset(
943
+ self, definition_uri: str, location: LabwareOffsetLocationSequence
944
+ ) -> Optional[LabwareOffset]:
945
+ """Find a labware offset that applies to the given definition and location sequence.
946
+
947
+ Returns the *most recently* added matching offset, so later ones can override earlier ones.
948
+ Returns ``None`` if no loaded offset matches the location.
949
+
950
+ An offset matches a labware instance if the sequence of locations formed by following the
951
+ .location elements of the labware instance until you reach an addressable area has the same
952
+ definition URIs as the sequence of definition URIs stored by the offset.
953
+ """
954
+ for candidate in reversed(list(self._state.labware_offsets_by_id.values())):
955
+ if (
956
+ candidate.definitionUri == definition_uri
957
+ and candidate.locationSequence == location
958
+ ):
959
+ return candidate
960
+ return None
961
+
962
+ def find_applicable_labware_offset_by_legacy_location(
803
963
  self,
804
964
  definition_uri: str,
805
- location: LabwareOffsetLocation,
965
+ location: LegacyLabwareOffsetLocation,
806
966
  ) -> Optional[LabwareOffset]:
807
- """Find a labware offset that applies to the given definition and location.
967
+ """Find a labware offset that applies to the given definition and legacy location.
808
968
 
809
969
  Returns the *most recently* added matching offset,
810
970
  so later offsets can override earlier ones.
@@ -870,7 +1030,9 @@ class LabwareView:
870
1030
  f"Cannot move pipette to {labware.loadName},"
871
1031
  f" labware is on staging slot {labware_location.addressableAreaName}"
872
1032
  )
873
- elif labware_location == OFF_DECK_LOCATION:
1033
+ elif (
1034
+ labware_location == OFF_DECK_LOCATION or labware_location == SYSTEM_LOCATION
1035
+ ):
874
1036
  raise errors.LocationNotAccessibleByPipetteError(
875
1037
  f"Cannot move pipette to {labware.loadName}, labware is off-deck."
876
1038
  )
@@ -917,6 +1079,41 @@ class LabwareView:
917
1079
  f" maximum allowed labware height is {_PLATE_READER_MAX_LABWARE_Z_MM}mm."
918
1080
  )
919
1081
 
1082
+ def raise_if_stacker_labware_pool_is_not_valid(
1083
+ self,
1084
+ primary_labware_definition: LabwareDefinition,
1085
+ lid_labware_definition: LabwareDefinition | None,
1086
+ adapter_labware_definition: LabwareDefinition | None,
1087
+ ) -> None:
1088
+ """Raise if the primary, lid, and adapter do not go together."""
1089
+ if lid_labware_definition:
1090
+ if not labware_validation.validate_definition_is_lid(
1091
+ lid_labware_definition
1092
+ ):
1093
+ raise errors.LabwareCannotBeStackedError(
1094
+ f"Labware {lid_labware_definition.parameters.loadName} cannot be used as a lid in the Flex Stacker."
1095
+ )
1096
+ if not labware_validation.validate_labware_can_be_stacked(
1097
+ lid_labware_definition, primary_labware_definition.parameters.loadName
1098
+ ):
1099
+ raise errors.LabwareCannotBeStackedError(
1100
+ f"Labware {lid_labware_definition.parameters.loadName} cannot be used as a lid for {primary_labware_definition.parameters.loadName}"
1101
+ )
1102
+ if adapter_labware_definition:
1103
+ if not labware_validation.validate_definition_is_adapter(
1104
+ adapter_labware_definition
1105
+ ):
1106
+ raise errors.LabwareCannotBeStackedError(
1107
+ f"Labware {adapter_labware_definition.parameters.loadName} cannot be used as an adapter in the Flex Stacker."
1108
+ )
1109
+ if not labware_validation.validate_labware_can_be_stacked(
1110
+ primary_labware_definition,
1111
+ adapter_labware_definition.parameters.loadName,
1112
+ ):
1113
+ raise errors.LabwareCannotBeStackedError(
1114
+ f"Labware {adapter_labware_definition.parameters.loadName} cannot be used as an adapter for {primary_labware_definition.parameters.loadName}"
1115
+ )
1116
+
920
1117
  def raise_if_labware_cannot_be_stacked( # noqa: C901
921
1118
  self, top_labware_definition: LabwareDefinition, bottom_labware_id: str
922
1119
  ) -> None:
@@ -951,7 +1148,7 @@ class LabwareView:
951
1148
  for lw in labware_stack:
952
1149
  if not labware_validation.validate_definition_is_adapter(
953
1150
  self.get_definition(lw.id)
954
- ):
1151
+ ) and not labware_validation.is_lid_stack(self.get_load_name(lw.id)):
955
1152
  stack_without_adapters.append(lw)
956
1153
  if len(stack_without_adapters) >= self.get_labware_stacking_maximum(
957
1154
  top_labware_definition
@@ -1091,7 +1288,7 @@ class LabwareView:
1091
1288
  )
1092
1289
 
1093
1290
  @staticmethod
1094
- def _max_x_of_well(well_defn: WellDefinition) -> float:
1291
+ def _max_x_of_well(well_defn: _WellDefinition) -> float:
1095
1292
  if well_defn.shape == "rectangular":
1096
1293
  return well_defn.x + (well_defn.xDimension or 0) / 2
1097
1294
  elif well_defn.shape == "circular":
@@ -1100,7 +1297,7 @@ class LabwareView:
1100
1297
  return well_defn.x
1101
1298
 
1102
1299
  @staticmethod
1103
- def _min_x_of_well(well_defn: WellDefinition) -> float:
1300
+ def _min_x_of_well(well_defn: _WellDefinition) -> float:
1104
1301
  if well_defn.shape == "rectangular":
1105
1302
  return well_defn.x - (well_defn.xDimension or 0) / 2
1106
1303
  elif well_defn.shape == "circular":
@@ -1109,7 +1306,7 @@ class LabwareView:
1109
1306
  return 0
1110
1307
 
1111
1308
  @staticmethod
1112
- def _max_y_of_well(well_defn: WellDefinition) -> float:
1309
+ def _max_y_of_well(well_defn: _WellDefinition) -> float:
1113
1310
  if well_defn.shape == "rectangular":
1114
1311
  return well_defn.y + (well_defn.yDimension or 0) / 2
1115
1312
  elif well_defn.shape == "circular":
@@ -1118,7 +1315,7 @@ class LabwareView:
1118
1315
  return 0
1119
1316
 
1120
1317
  @staticmethod
1121
- def _min_y_of_well(well_defn: WellDefinition) -> float:
1318
+ def _min_y_of_well(well_defn: _WellDefinition) -> float:
1122
1319
  if well_defn.shape == "rectangular":
1123
1320
  return well_defn.y - (well_defn.yDimension or 0) / 2
1124
1321
  elif well_defn.shape == "circular":
@@ -1127,7 +1324,7 @@ class LabwareView:
1127
1324
  return 0
1128
1325
 
1129
1326
  @staticmethod
1130
- def _max_z_of_well(well_defn: WellDefinition) -> float:
1327
+ def _max_z_of_well(well_defn: _WellDefinition) -> float:
1131
1328
  return well_defn.z + well_defn.depth
1132
1329
 
1133
1330
  def get_well_bbox(self, labware_definition: LabwareDefinition) -> Dimensions:
@@ -13,6 +13,7 @@ from .thermocycler_module_substate import (
13
13
  )
14
14
  from .magnetic_block_substate import MagneticBlockSubState, MagneticBlockId
15
15
  from .absorbance_reader_substate import AbsorbanceReaderSubState, AbsorbanceReaderId
16
+ from .flex_stacker_substate import FlexStackerSubState, FlexStackerId
16
17
 
17
18
  ModuleSubStateType = Union[
18
19
  HeaterShakerModuleSubState,
@@ -21,6 +22,7 @@ ModuleSubStateType = Union[
21
22
  ThermocyclerModuleSubState,
22
23
  MagneticBlockSubState,
23
24
  AbsorbanceReaderSubState,
25
+ FlexStackerSubState,
24
26
  ]
25
27
 
26
28
  __all__ = [
@@ -36,6 +38,8 @@ __all__ = [
36
38
  "MagneticBlockId",
37
39
  "AbsorbanceReaderSubState",
38
40
  "AbsorbanceReaderId",
41
+ "FlexStackerSubState",
42
+ "FlexStackerId",
39
43
  # Union of all module substates
40
44
  "ModuleSubStateType",
41
45
  ]
@@ -0,0 +1,68 @@
1
+ """Flex Stacker substate."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import NewType
5
+ from opentrons_shared_data.labware.labware_definition import LabwareDefinition
6
+ from opentrons.protocol_engine.state.update_types import (
7
+ FlexStackerStateUpdate,
8
+ NO_CHANGE,
9
+ )
10
+
11
+
12
+ FlexStackerId = NewType("FlexStackerId", str)
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class FlexStackerSubState:
17
+ """Flex Stacker-specific state.
18
+
19
+ Provides calculations and read-only state access
20
+ for an individual loaded Flex Stacker Module.
21
+ """
22
+
23
+ module_id: FlexStackerId
24
+ pool_primary_definition: LabwareDefinition | None
25
+ pool_adapter_definition: LabwareDefinition | None
26
+ pool_lid_definition: LabwareDefinition | None
27
+ pool_count: int
28
+ max_pool_count: int
29
+
30
+ def new_from_state_change(
31
+ self, update: FlexStackerStateUpdate
32
+ ) -> "FlexStackerSubState":
33
+ """Return a new state with the given update applied."""
34
+ pool_primary_definition = self.pool_primary_definition
35
+ pool_adapter_definition = self.pool_adapter_definition
36
+ pool_lid_definition = self.pool_lid_definition
37
+ max_pool_count = self.max_pool_count
38
+ if update.pool_constraint != NO_CHANGE:
39
+ max_pool_count = update.pool_constraint.max_pool_count
40
+ pool_primary_definition = update.pool_constraint.primary_definition
41
+ pool_adapter_definition = update.pool_constraint.adapter_definition
42
+ pool_lid_definition = update.pool_constraint.lid_definition
43
+
44
+ pool_count = self.pool_count
45
+ if update.pool_count != NO_CHANGE:
46
+ pool_count = update.pool_count
47
+
48
+ return FlexStackerSubState(
49
+ module_id=self.module_id,
50
+ pool_primary_definition=pool_primary_definition,
51
+ pool_adapter_definition=pool_adapter_definition,
52
+ pool_lid_definition=pool_lid_definition,
53
+ pool_count=pool_count,
54
+ max_pool_count=max_pool_count,
55
+ )
56
+
57
+ def get_pool_definition_ordered_list(self) -> list[LabwareDefinition] | None:
58
+ """Get the pool definitions in a list suitable for getting the height."""
59
+ if not self.pool_primary_definition:
60
+ return None
61
+
62
+ defs: list[LabwareDefinition] = []
63
+ if self.pool_lid_definition is not None:
64
+ defs.append(self.pool_lid_definition)
65
+ defs.append(self.pool_primary_definition)
66
+ if self.pool_adapter_definition is not None:
67
+ defs.append(self.pool_adapter_definition)
68
+ return defs
@@ -2,6 +2,7 @@
2
2
  from dataclasses import dataclass
3
3
  from typing import NewType, Optional
4
4
 
5
+ from opentrons.hardware_control.modules import ModuleDataValidator, ModuleData
5
6
  from opentrons.protocol_engine.types import (
6
7
  TemperatureRange,
7
8
  SpeedRange,
@@ -89,3 +90,24 @@ class HeaterShakerModuleSubState:
89
90
  raise CannotPerformModuleAction(
90
91
  "Heater-Shaker cannot open its labware latch while it is shaking."
91
92
  )
93
+
94
+ @classmethod
95
+ def from_live_data(
96
+ cls, module_id: HeaterShakerModuleId, data: ModuleData | None
97
+ ) -> "HeaterShakerModuleSubState":
98
+ """Create a HeaterShakerModuleSubState from live data."""
99
+ if ModuleDataValidator.is_heater_shaker_data(data):
100
+ return cls(
101
+ module_id=module_id,
102
+ labware_latch_status=HeaterShakerLatchStatus.CLOSED
103
+ if data["labwareLatchStatus"] == "idle_closed"
104
+ else HeaterShakerLatchStatus.OPEN,
105
+ is_plate_shaking=data["targetSpeed"] is not None,
106
+ plate_target_temperature=data["targetTemp"],
107
+ )
108
+ return cls(
109
+ module_id=module_id,
110
+ labware_latch_status=HeaterShakerLatchStatus.UNKNOWN,
111
+ is_plate_shaking=False,
112
+ plate_target_temperature=None,
113
+ )
@@ -3,6 +3,7 @@
3
3
  from dataclasses import dataclass
4
4
  from typing import NewType, Optional
5
5
 
6
+ from opentrons.hardware_control.modules import ModuleDataValidator, ModuleData
6
7
  from opentrons.protocol_engine.types import TemperatureRange
7
8
  from opentrons.protocol_engine.errors import (
8
9
  InvalidTargetTemperatureError,
@@ -52,3 +53,15 @@ class TemperatureModuleSubState:
52
53
  f"Module {self.module_id} does not have a target temperature set."
53
54
  )
54
55
  return target
56
+
57
+ @classmethod
58
+ def from_live_data(
59
+ cls, module_id: TemperatureModuleId, data: ModuleData | None
60
+ ) -> "TemperatureModuleSubState":
61
+ """Create a TemperatureModuleSubState from live data."""
62
+ return cls(
63
+ module_id=module_id,
64
+ plate_target_temperature=data["targetTemp"]
65
+ if ModuleDataValidator.is_temperature_module_data(data)
66
+ else None,
67
+ )