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
@@ -19,6 +19,7 @@ from opentrons.drivers.thermocycler.driver import (
19
19
  LID_TARGET_MIN,
20
20
  LID_TARGET_MAX,
21
21
  )
22
+ from opentrons.hardware_control.modules import ModuleData, ModuleDataValidator
22
23
 
23
24
  ThermocyclerModuleId = NewType("ThermocyclerModuleId", str)
24
25
 
@@ -141,3 +142,22 @@ class ThermocyclerModuleSubState:
141
142
  f"Module {self.module_id} does not have a target block temperature set."
142
143
  )
143
144
  return target
145
+
146
+ @classmethod
147
+ def from_live_data(
148
+ cls, module_id: ThermocyclerModuleId, data: ModuleData | None
149
+ ) -> "ThermocyclerModuleSubState":
150
+ """Create a ThermocyclerModuleSubState from live data."""
151
+ if ModuleDataValidator.is_thermocycler_data(data):
152
+ return cls(
153
+ module_id=module_id,
154
+ is_lid_open=data["lid"] == "open",
155
+ target_block_temperature=data["targetTemp"],
156
+ target_lid_temperature=data["lidTarget"],
157
+ )
158
+ return cls(
159
+ module_id=module_id,
160
+ is_lid_open=False,
161
+ target_block_temperature=None,
162
+ target_lid_temperature=None,
163
+ )
@@ -36,8 +36,9 @@ from opentrons.protocol_engine.state.module_substates.absorbance_reader_substate
36
36
  AbsorbanceReaderMeasureMode,
37
37
  )
38
38
  from opentrons.types import DeckSlotName, MountType, StagingSlotName
39
- from .update_types import AbsorbanceReaderStateUpdate
40
- from ..errors import ModuleNotConnectedError
39
+ from .update_types import AbsorbanceReaderStateUpdate, FlexStackerStateUpdate
40
+ from ..errors import ModuleNotConnectedError, AreaNotInDeckConfigurationError
41
+ from ..resources import deck_configuration_provider
41
42
 
42
43
  from ..types import (
43
44
  LoadedModule,
@@ -78,11 +79,13 @@ from .module_substates import (
78
79
  TemperatureModuleSubState,
79
80
  ThermocyclerModuleSubState,
80
81
  AbsorbanceReaderSubState,
82
+ FlexStackerSubState,
81
83
  MagneticModuleId,
82
84
  HeaterShakerModuleId,
83
85
  TemperatureModuleId,
84
86
  ThermocyclerModuleId,
85
87
  AbsorbanceReaderId,
88
+ FlexStackerId,
86
89
  MagneticBlockSubState,
87
90
  MagneticBlockId,
88
91
  ModuleSubStateType,
@@ -147,11 +150,14 @@ class HardwareModule:
147
150
  class ModuleState:
148
151
  """The internal data to keep track of loaded modules."""
149
152
 
150
- slot_by_module_id: Dict[str, Optional[DeckSlotName]]
151
- """The deck slot that each module has been loaded into.
153
+ load_location_by_module_id: Dict[str, Optional[str]]
154
+ """The Cutout ID of the cutout (Flex) or slot (OT-2) that each module has been loaded.
152
155
 
153
156
  This will be None when the module was added via
154
157
  ProtocolEngine.use_attached_modules() instead of an explicit loadModule command.
158
+ AddressableAreaLocation is used to represent a literal Deck Slot for OT-2 locations.
159
+ The CutoutID string for a given Cutout that a Module Fixture is loaded into is used
160
+ for Flex. The type distinction is in place for implementation seperation between the two.
155
161
  """
156
162
 
157
163
  additional_slots_occupied_by_module_id: Dict[str, List[DeckSlotName]]
@@ -211,7 +217,7 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
211
217
  ) -> None:
212
218
  """Initialize a ModuleStore and its state."""
213
219
  self._state = ModuleState(
214
- slot_by_module_id={},
220
+ load_location_by_module_id={},
215
221
  additional_slots_occupied_by_module_id={},
216
222
  requested_model_by_id={},
217
223
  hardware_by_module_id={},
@@ -302,6 +308,8 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
302
308
  self._handle_absorbance_reader_commands(
303
309
  state_update.absorbance_reader_state_update
304
310
  )
311
+ if state_update.flex_stacker_state_update != update_types.NO_CHANGE:
312
+ self._handle_flex_stacker_commands(state_update.flex_stacker_state_update)
305
313
 
306
314
  def _add_module_substate(
307
315
  self,
@@ -312,11 +320,19 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
312
320
  requested_model: Optional[ModuleModel],
313
321
  module_live_data: Optional[LiveData],
314
322
  ) -> None:
323
+ # Loading slot name to Cutout ID (Flex)(OT-2) resolution
324
+ load_location: Optional[str]
325
+ if slot_name is not None:
326
+ load_location = deck_configuration_provider.get_cutout_id_by_deck_slot_name(
327
+ slot_name
328
+ )
329
+ else:
330
+ load_location = slot_name
331
+
315
332
  actual_model = definition.model
316
333
  live_data = module_live_data["data"] if module_live_data else None
317
-
318
334
  self._state.requested_model_by_id[module_id] = requested_model
319
- self._state.slot_by_module_id[module_id] = slot_name
335
+ self._state.load_location_by_module_id[module_id] = load_location
320
336
  self._state.hardware_by_module_id[module_id] = HardwareModule(
321
337
  serial_number=serial_number,
322
338
  definition=definition,
@@ -328,31 +344,24 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
328
344
  model=actual_model,
329
345
  )
330
346
  elif ModuleModel.is_heater_shaker_module_model(actual_model):
331
- if live_data is None:
332
- labware_latch_status = HeaterShakerLatchStatus.UNKNOWN
333
- elif live_data["labwareLatchStatus"] == "idle_closed":
334
- labware_latch_status = HeaterShakerLatchStatus.CLOSED
335
- else:
336
- labware_latch_status = HeaterShakerLatchStatus.OPEN
337
- self._state.substate_by_module_id[module_id] = HeaterShakerModuleSubState(
347
+ self._state.substate_by_module_id[
348
+ module_id
349
+ ] = HeaterShakerModuleSubState.from_live_data(
338
350
  module_id=HeaterShakerModuleId(module_id),
339
- labware_latch_status=labware_latch_status,
340
- is_plate_shaking=(
341
- live_data is not None and live_data["targetSpeed"] is not None
342
- ),
343
- plate_target_temperature=live_data["targetTemp"] if live_data else None, # type: ignore[arg-type]
351
+ data=live_data,
344
352
  )
345
353
  elif ModuleModel.is_temperature_module_model(actual_model):
346
- self._state.substate_by_module_id[module_id] = TemperatureModuleSubState(
354
+ self._state.substate_by_module_id[
355
+ module_id
356
+ ] = TemperatureModuleSubState.from_live_data(
347
357
  module_id=TemperatureModuleId(module_id),
348
- plate_target_temperature=live_data["targetTemp"] if live_data else None, # type: ignore[arg-type]
358
+ data=live_data,
349
359
  )
350
360
  elif ModuleModel.is_thermocycler_module_model(actual_model):
351
- self._state.substate_by_module_id[module_id] = ThermocyclerModuleSubState(
352
- module_id=ThermocyclerModuleId(module_id),
353
- is_lid_open=live_data is not None and live_data["lid"] == "open",
354
- target_block_temperature=live_data["targetTemp"] if live_data else None, # type: ignore[arg-type]
355
- target_lid_temperature=live_data["lidTarget"] if live_data else None, # type: ignore[arg-type]
361
+ self._state.substate_by_module_id[
362
+ module_id
363
+ ] = ThermocyclerModuleSubState.from_live_data(
364
+ module_id=ThermocyclerModuleId(module_id), data=live_data
356
365
  )
357
366
  self._update_additional_slots_occupied_by_thermocycler(
358
367
  module_id=module_id, slot_name=slot_name
@@ -372,6 +381,15 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
372
381
  configured_wavelengths=None,
373
382
  reference_wavelength=None,
374
383
  )
384
+ elif ModuleModel.is_flex_stacker(actual_model):
385
+ self._state.substate_by_module_id[module_id] = FlexStackerSubState(
386
+ module_id=FlexStackerId(module_id),
387
+ pool_primary_definition=None,
388
+ pool_adapter_definition=None,
389
+ pool_lid_definition=None,
390
+ pool_count=0,
391
+ max_pool_count=0,
392
+ )
375
393
 
376
394
  def _update_additional_slots_occupied_by_thermocycler(
377
395
  self,
@@ -613,6 +631,20 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
613
631
  data=data,
614
632
  )
615
633
 
634
+ def _handle_flex_stacker_commands(
635
+ self, state_update: FlexStackerStateUpdate
636
+ ) -> None:
637
+ """Handle Flex Stacker state updates."""
638
+ module_id = state_update.module_id
639
+ prev_substate = self._state.substate_by_module_id[module_id]
640
+ assert isinstance(
641
+ prev_substate, FlexStackerSubState
642
+ ), f"{module_id} is not a Flex Stacker."
643
+
644
+ self._state.substate_by_module_id[
645
+ module_id
646
+ ] = prev_substate.new_from_state_change(state_update)
647
+
616
648
 
617
649
  class ModuleView:
618
650
  """Read-only view of computed module state."""
@@ -626,12 +658,17 @@ class ModuleView:
626
658
  def get(self, module_id: str) -> LoadedModule:
627
659
  """Get module data by the module's unique identifier."""
628
660
  try:
629
- slot_name = self._state.slot_by_module_id[module_id]
661
+ load_location = self._state.load_location_by_module_id[module_id]
630
662
  attached_module = self._state.hardware_by_module_id[module_id]
631
663
 
632
664
  except KeyError as e:
633
665
  raise errors.ModuleNotLoadedError(module_id=module_id) from e
634
666
 
667
+ slot_name = None
668
+ if isinstance(load_location, str):
669
+ slot_name = deck_configuration_provider.get_deck_slot_for_cutout_id(
670
+ load_location
671
+ )
635
672
  location = (
636
673
  DeckSlotLocation(slotName=slot_name) if slot_name is not None else None
637
674
  )
@@ -645,21 +682,39 @@ class ModuleView:
645
682
 
646
683
  def get_all(self) -> List[LoadedModule]:
647
684
  """Get a list of all module entries in state."""
648
- return [self.get(mod_id) for mod_id in self._state.slot_by_module_id.keys()]
685
+ return [
686
+ self.get(mod_id) for mod_id in self._state.load_location_by_module_id.keys()
687
+ ]
649
688
 
650
689
  def get_by_slot(
651
690
  self,
652
691
  slot_name: DeckSlotName,
653
692
  ) -> Optional[LoadedModule]:
654
693
  """Get the module located in a given slot, if any."""
655
- slots_by_id = reversed(list(self._state.slot_by_module_id.items()))
694
+ locations_by_id = reversed(list(self._state.load_location_by_module_id.items()))
656
695
 
657
- for module_id, module_slot in slots_by_id:
696
+ for module_id, load_location in locations_by_id:
697
+ module_slot: Optional[DeckSlotName]
698
+ if isinstance(load_location, str):
699
+ module_slot = deck_configuration_provider.get_deck_slot_for_cutout_id(
700
+ load_location
701
+ )
702
+ else:
703
+ module_slot = load_location
658
704
  if module_slot == slot_name:
659
705
  return self.get(module_id)
660
706
 
661
707
  return None
662
708
 
709
+ def get_by_addressable_area(
710
+ self, addressable_area_name: str
711
+ ) -> Optional[LoadedModule]:
712
+ """Get the module associated with this addressable area, if any."""
713
+ for module_id in self._state.load_location_by_module_id.keys():
714
+ if addressable_area_name == self.get_provided_addressable_area(module_id):
715
+ return self.get(module_id)
716
+ return None
717
+
663
718
  def _get_module_substate(
664
719
  self, module_id: str, expected_type: Type[ModuleSubStateT], expected_name: str
665
720
  ) -> ModuleSubStateT:
@@ -764,6 +819,20 @@ class ModuleView:
764
819
  expected_name="Absorbance Reader",
765
820
  )
766
821
 
822
+ def get_flex_stacker_substate(self, module_id: str) -> FlexStackerSubState:
823
+ """Return a `FlexStackerSubState` for the given Flex Stacker.
824
+
825
+ Raises:
826
+ ModuleNotLoadedError: If module_id has not been loaded.
827
+ WrongModuleTypeError: If module_id has been loaded,
828
+ but it's not a Flex Stacker.
829
+ """
830
+ return self._get_module_substate(
831
+ module_id=module_id,
832
+ expected_type=FlexStackerSubState,
833
+ expected_name="Flex Stacker",
834
+ )
835
+
767
836
  def get_location(self, module_id: str) -> DeckSlotLocation:
768
837
  """Get the slot location of the given module."""
769
838
  location = self.get(module_id).location
@@ -773,6 +842,26 @@ class ModuleView:
773
842
  )
774
843
  return location
775
844
 
845
+ def get_provided_addressable_area(self, module_id: str) -> str:
846
+ """Get the addressable area provided by this module.
847
+
848
+ If the current deck does not allow modules to provide locations (i.e., is an OT-2 deck)
849
+ then return the addressable area underneath the module.
850
+ """
851
+ module = self.get(module_id)
852
+
853
+ if isinstance(module.location, DeckSlotLocation):
854
+ location = module.location.slotName
855
+ elif module.model == ModuleModel.THERMOCYCLER_MODULE_V2:
856
+ location = DeckSlotName.SLOT_B1
857
+ else:
858
+ raise ValueError(
859
+ "Module location invalid for nominal module offset calculation."
860
+ )
861
+ if not self.get_deck_supports_module_fixtures():
862
+ return location.value
863
+ return self.ensure_and_convert_module_fixture_location(location, module.model)
864
+
776
865
  def get_requested_model(self, module_id: str) -> Optional[ModuleModel]:
777
866
  """Return the model by which this module was requested.
778
867
 
@@ -880,18 +969,7 @@ class ModuleView:
880
969
  z=xformed[2],
881
970
  )
882
971
  else:
883
- module = self.get(module_id)
884
- if isinstance(module.location, DeckSlotLocation):
885
- location = module.location.slotName
886
- elif module.model == ModuleModel.THERMOCYCLER_MODULE_V2:
887
- location = DeckSlotName.SLOT_B1
888
- else:
889
- raise ValueError(
890
- "Module location invalid for nominal module offset calculation."
891
- )
892
- module_addressable_area = self.ensure_and_convert_module_fixture_location(
893
- location, module.model
894
- )
972
+ module_addressable_area = self.get_provided_addressable_area(module_id)
895
973
  module_addressable_area_position = (
896
974
  addressable_areas.get_addressable_area_offsets_from_cutout(
897
975
  module_addressable_area
@@ -1109,12 +1187,21 @@ class ModuleView:
1109
1187
  else:
1110
1188
  neighbor_slot = DeckSlotName.from_primitive(neighbor_int)
1111
1189
 
1112
- return neighbor_slot in self._state.slot_by_module_id.values()
1190
+ # Convert the load location list from addressable areas and cutout IDs to a slot name list
1191
+ load_locations = self._state.load_location_by_module_id.values()
1192
+ module_slots = []
1193
+ for location in load_locations:
1194
+ if isinstance(location, str):
1195
+ module_slots.append(
1196
+ deck_configuration_provider.get_deck_slot_for_cutout_id(location)
1197
+ )
1198
+
1199
+ return neighbor_slot in module_slots
1113
1200
 
1114
1201
  def select_hardware_module_to_load( # noqa: C901
1115
1202
  self,
1116
1203
  model: ModuleModel,
1117
- location: DeckSlotLocation,
1204
+ location: str,
1118
1205
  attached_modules: Sequence[HardwareModule],
1119
1206
  expected_serial_number: Optional[str] = None,
1120
1207
  ) -> HardwareModule:
@@ -1143,10 +1230,13 @@ class ModuleView:
1143
1230
  """
1144
1231
  existing_mod_in_slot = None
1145
1232
 
1146
- for mod_id, slot in self._state.slot_by_module_id.items():
1147
- if slot == location.slotName:
1233
+ for (
1234
+ mod_id,
1235
+ load_location,
1236
+ ) in self._state.load_location_by_module_id.items():
1237
+ if isinstance(load_location, str) and location == load_location:
1148
1238
  existing_mod_in_slot = self._state.hardware_by_module_id.get(mod_id)
1149
- break
1239
+
1150
1240
  if existing_mod_in_slot:
1151
1241
  existing_def = existing_mod_in_slot.definition
1152
1242
 
@@ -1154,9 +1244,9 @@ class ModuleView:
1154
1244
  return existing_mod_in_slot
1155
1245
 
1156
1246
  else:
1247
+ _err = f" present in {location}"
1157
1248
  raise errors.ModuleAlreadyPresentError(
1158
- f"A {existing_def.model.value} is already"
1159
- f" present in {location.slotName.value}"
1249
+ f"A {existing_def.model.value} is already" + _err
1160
1250
  )
1161
1251
 
1162
1252
  for m in attached_modules:
@@ -1168,7 +1258,10 @@ class ModuleView:
1168
1258
  else:
1169
1259
  return m
1170
1260
 
1171
- raise errors.ModuleNotAttachedError(f"No available {model.value} found.")
1261
+ raise errors.ModuleNotAttachedError(
1262
+ f"No available {model.value} with {expected_serial_number or 'any'}"
1263
+ " serial found."
1264
+ )
1172
1265
 
1173
1266
  def get_heater_shaker_movement_restrictors(
1174
1267
  self,
@@ -1258,6 +1351,11 @@ class ModuleView:
1258
1351
  "Only readings of 96 Well labware are supported for conversion to map of values by well."
1259
1352
  )
1260
1353
 
1354
+ def get_deck_supports_module_fixtures(self) -> bool:
1355
+ """Check if the loaded deck supports modules as fixtures."""
1356
+ deck_type = self._state.deck_type
1357
+ return deck_type not in [DeckType.OT2_STANDARD, DeckType.OT2_SHORT_TRASH]
1358
+
1261
1359
  def ensure_and_convert_module_fixture_location(
1262
1360
  self,
1263
1361
  deck_slot: DeckSlotName,
@@ -1269,8 +1367,8 @@ class ModuleView:
1269
1367
  """
1270
1368
  deck_type = self._state.deck_type
1271
1369
 
1272
- if deck_type == DeckType.OT2_STANDARD or deck_type == DeckType.OT2_SHORT_TRASH:
1273
- raise ValueError(
1370
+ if not self.get_deck_supports_module_fixtures():
1371
+ raise AreaNotInDeckConfigurationError(
1274
1372
  f"Invalid Deck Type: {deck_type.name} - Does not support modules as fixtures."
1275
1373
  )
1276
1374
 
@@ -1296,6 +1394,11 @@ class ModuleView:
1296
1394
  assert deck_slot.value[-1] == "3"
1297
1395
  return f"absorbanceReaderV1{deck_slot.value}"
1298
1396
 
1397
+ elif model == ModuleModel.FLEX_STACKER_MODULE_V1:
1398
+ # loaded to column 3 but the addressable area is in column 4
1399
+ assert deck_slot.value[-1] == "3"
1400
+ return f"flexStackerModuleV1{deck_slot.value[0]}4"
1401
+
1299
1402
  raise ValueError(
1300
1403
  f"Unknown module {model.name} has no addressable areas to provide."
1301
1404
  )
@@ -1311,3 +1414,26 @@ class ModuleView:
1311
1414
  addressableAreaName="absorbanceReaderV1LidDock" + lid_doc_slot.value
1312
1415
  )
1313
1416
  return lid_dock_area
1417
+
1418
+ def get_stacker_max_fill_height(self, module_id: str) -> float:
1419
+ """Get the maximum fill height for the Flex Stacker."""
1420
+ definition = self.get_definition(module_id)
1421
+
1422
+ if (
1423
+ definition.moduleType == ModuleType.FLEX_STACKER
1424
+ and hasattr(definition.dimensions, "maxStackerFillHeight")
1425
+ and definition.dimensions.maxStackerFillHeight is not None
1426
+ ):
1427
+ return definition.dimensions.maxStackerFillHeight
1428
+ else:
1429
+ raise errors.WrongModuleTypeError(
1430
+ f"Cannot get max fill height of {definition.moduleType}"
1431
+ )
1432
+
1433
+ def stacker_max_pool_count_by_height(
1434
+ self, module_id: str, pool_height: float
1435
+ ) -> int:
1436
+ """Get the maximum stack count for the Flex Stacker by stack height."""
1437
+ max_fill_height = self.get_stacker_max_fill_height(module_id)
1438
+ assert max_fill_height > 0
1439
+ return math.floor(max_fill_height / pool_height)
@@ -125,6 +125,8 @@ class PipetteState:
125
125
  flow_rates_by_id: Dict[str, FlowRates]
126
126
  nozzle_configuration_by_id: Dict[str, NozzleMap]
127
127
  liquid_presence_detection_by_id: Dict[str, bool]
128
+ ready_to_aspirate_by_id: Dict[str, bool]
129
+ has_clean_tips_by_id: Dict[str, bool]
128
130
 
129
131
 
130
132
  class PipetteStore(HasState[PipetteState], HandlesActions):
@@ -145,6 +147,8 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
145
147
  flow_rates_by_id={},
146
148
  nozzle_configuration_by_id={},
147
149
  liquid_presence_detection_by_id={},
150
+ ready_to_aspirate_by_id={},
151
+ has_clean_tips_by_id={},
148
152
  )
149
153
 
150
154
  def handle_action(self, action: Action) -> None:
@@ -156,6 +160,7 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
156
160
  self._update_pipette_nozzle_map(state_update)
157
161
  self._update_tip_state(state_update)
158
162
  self._update_volumes(state_update)
163
+ self._update_ready_for_aspirate(state_update)
159
164
 
160
165
  if isinstance(action, SetPipetteMovementSpeedAction):
161
166
  self._state.movement_speed_by_id[action.pipette_id] = action.speed
@@ -174,6 +179,7 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
174
179
  )
175
180
  self._state.movement_speed_by_id[pipette_id] = None
176
181
  self._state.attached_tip_by_id[pipette_id] = None
182
+ self._state.ready_to_aspirate_by_id[pipette_id] = False
177
183
 
178
184
  def _update_tip_state(self, state_update: update_types.StateUpdate) -> None:
179
185
  if state_update.pipette_tip_state != update_types.NO_CHANGE:
@@ -209,6 +215,7 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
209
215
  else:
210
216
  pipette_id = state_update.pipette_tip_state.pipette_id
211
217
  self._state.attached_tip_by_id[pipette_id] = None
218
+ self._state.has_clean_tips_by_id[pipette_id] = False
212
219
 
213
220
  static_config = self._state.static_config_by_id.get(pipette_id)
214
221
  if static_config:
@@ -314,9 +321,23 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
314
321
  state_update.pipette_nozzle_map.pipette_id
315
322
  ] = state_update.pipette_nozzle_map.nozzle_map
316
323
 
324
+ def _update_ready_for_aspirate(
325
+ self, state_update: update_types.StateUpdate
326
+ ) -> None:
327
+ if state_update.ready_to_aspirate != update_types.NO_CHANGE:
328
+ self._state.ready_to_aspirate_by_id[
329
+ state_update.ready_to_aspirate.pipette_id
330
+ ] = state_update.ready_to_aspirate.ready_to_aspirate
331
+
317
332
  def _update_volumes(self, state_update: update_types.StateUpdate) -> None:
318
333
  if state_update.pipette_aspirated_fluid == update_types.NO_CHANGE:
319
334
  return
335
+ # set the tip state to unclean, if an "empty" update has a clean_tip flag
336
+ # it will set it to true
337
+ self._state.has_clean_tips_by_id[
338
+ state_update.pipette_aspirated_fluid.pipette_id
339
+ ] = False
340
+
320
341
  if state_update.pipette_aspirated_fluid.type == "aspirated":
321
342
  self._update_aspirated(state_update.pipette_aspirated_fluid)
322
343
  elif state_update.pipette_aspirated_fluid.type == "ejected":
@@ -343,6 +364,7 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
343
364
 
344
365
  def _update_empty(self, update: update_types.PipetteEmptyFluidUpdate) -> None:
345
366
  self._state.pipette_contents_by_id[update.pipette_id] = fluid_stack.FluidStack()
367
+ self._state.has_clean_tips_by_id[update.pipette_id] = update.clean_tip
346
368
 
347
369
  def _update_unknown(self, update: update_types.PipetteUnknownFluidUpdate) -> None:
348
370
  self._state.pipette_contents_by_id[update.pipette_id] = None
@@ -482,6 +504,29 @@ class PipetteView:
482
504
  f"Pipette {pipette_id} not found; unable to get current volume."
483
505
  ) from e
484
506
 
507
+ def get_has_clean_tip(self, pipette_id: str) -> bool:
508
+ """Get if the tip of a pipette by ID is clean.
509
+
510
+ This is only true directly after a pick up tip, once any kind of aspirate happens
511
+ it is no longer clean
512
+
513
+ Returns:
514
+ True if the tip is clean
515
+ False if it is unclean
516
+
517
+ Raises:
518
+ PipetteNotLoadedError: pipette ID does not exist.
519
+ TipNotAttachedError: if no tip is attached to the pipette.
520
+ """
521
+ self.validate_tip_state(pipette_id, True)
522
+
523
+ try:
524
+ return self._state.has_clean_tips_by_id[pipette_id]
525
+ except KeyError as e:
526
+ raise errors.PipetteNotLoadedError(
527
+ f"Pipette {pipette_id} not found; unable to get current volume."
528
+ ) from e
529
+
485
530
  def get_liquid_dispensed_by_ejecting_volume(
486
531
  self, pipette_id: str, volume: float
487
532
  ) -> Optional[float]:
@@ -822,3 +867,12 @@ class PipetteView:
822
867
  ) -> float:
823
868
  """Get the plunger position provided for the given pipette id."""
824
869
  return self.get_config(pipette_id).plunger_positions[position_name]
870
+
871
+ def get_ready_to_aspirate(self, pipette_id: str) -> bool:
872
+ """Get if the provided pipette is ready to aspirate for the given pipette id."""
873
+ try:
874
+ return self._state.ready_to_aspirate_by_id[pipette_id]
875
+ except KeyError as e:
876
+ raise errors.PipetteNotLoadedError(
877
+ f"Pipette {pipette_id} not found; unable to determine if pipette ready to aspirate."
878
+ ) from e
@@ -374,7 +374,7 @@ class StateStore(StateView, ActionHandler):
374
374
  self._addressable_areas = AddressableAreaView(state.addressable_areas)
375
375
  self._labware = LabwareView(state.labware)
376
376
  self._pipettes = PipetteView(state.pipettes)
377
- self._modules = ModuleView(state.modules)
377
+ self._modules = ModuleView(state=state.modules)
378
378
  self._liquid = LiquidView(state.liquids)
379
379
  self._liquid_classes = LiquidClassView(state.liquid_classes)
380
380
  self._tips = TipView(state.tips)
@@ -110,6 +110,20 @@ class TipStore(HasState[TipState], HandlesActions):
110
110
  self._state.column_by_labware_id[labware_id] = [
111
111
  column for column in definition.ordering
112
112
  ]
113
+ if state_update.batch_loaded_labware != update_types.NO_CHANGE:
114
+ for labware_id in state_update.batch_loaded_labware.new_locations_by_id:
115
+ definition = state_update.batch_loaded_labware.definitions_by_id[
116
+ labware_id
117
+ ]
118
+ if definition.parameters.isTiprack:
119
+ self._state.tips_by_labware_id[labware_id] = {
120
+ well_name: TipRackWellState.CLEAN
121
+ for column in definition.ordering
122
+ for well_name in column
123
+ }
124
+ self._state.column_by_labware_id[labware_id] = [
125
+ column for column in definition.ordering
126
+ ]
113
127
 
114
128
  def _set_used_tips(self, pipette_id: str, well_name: str, labware_id: str) -> None:
115
129
  columns = self._state.column_by_labware_id.get(labware_id, [])