opentrons 8.1.0a0__py2.py3-none-any.whl → 8.2.0a0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. opentrons/cli/analyze.py +71 -7
  2. opentrons/config/__init__.py +9 -0
  3. opentrons/config/advanced_settings.py +22 -0
  4. opentrons/config/defaults_ot3.py +14 -36
  5. opentrons/config/feature_flags.py +4 -0
  6. opentrons/config/types.py +6 -17
  7. opentrons/drivers/absorbance_reader/abstract.py +27 -3
  8. opentrons/drivers/absorbance_reader/async_byonoy.py +207 -154
  9. opentrons/drivers/absorbance_reader/driver.py +24 -15
  10. opentrons/drivers/absorbance_reader/hid_protocol.py +79 -50
  11. opentrons/drivers/absorbance_reader/simulator.py +32 -6
  12. opentrons/drivers/types.py +23 -1
  13. opentrons/execute.py +2 -2
  14. opentrons/hardware_control/api.py +18 -10
  15. opentrons/hardware_control/backends/controller.py +3 -2
  16. opentrons/hardware_control/backends/flex_protocol.py +11 -5
  17. opentrons/hardware_control/backends/ot3controller.py +18 -50
  18. opentrons/hardware_control/backends/ot3simulator.py +7 -6
  19. opentrons/hardware_control/instruments/ot2/pipette_handler.py +22 -82
  20. opentrons/hardware_control/instruments/ot3/pipette_handler.py +10 -2
  21. opentrons/hardware_control/module_control.py +43 -2
  22. opentrons/hardware_control/modules/__init__.py +7 -1
  23. opentrons/hardware_control/modules/absorbance_reader.py +230 -83
  24. opentrons/hardware_control/modules/errors.py +7 -0
  25. opentrons/hardware_control/modules/heater_shaker.py +8 -3
  26. opentrons/hardware_control/modules/magdeck.py +12 -3
  27. opentrons/hardware_control/modules/mod_abc.py +27 -2
  28. opentrons/hardware_control/modules/tempdeck.py +15 -7
  29. opentrons/hardware_control/modules/thermocycler.py +69 -3
  30. opentrons/hardware_control/modules/types.py +11 -5
  31. opentrons/hardware_control/modules/update.py +11 -5
  32. opentrons/hardware_control/modules/utils.py +3 -1
  33. opentrons/hardware_control/ot3_calibration.py +6 -6
  34. opentrons/hardware_control/ot3api.py +126 -89
  35. opentrons/hardware_control/poller.py +15 -11
  36. opentrons/hardware_control/protocols/__init__.py +1 -7
  37. opentrons/hardware_control/protocols/instrument_configurer.py +14 -2
  38. opentrons/hardware_control/protocols/liquid_handler.py +5 -0
  39. opentrons/motion_planning/__init__.py +2 -0
  40. opentrons/motion_planning/waypoints.py +32 -0
  41. opentrons/protocol_api/__init__.py +2 -1
  42. opentrons/protocol_api/_liquid.py +87 -1
  43. opentrons/protocol_api/_parameter_context.py +10 -1
  44. opentrons/protocol_api/core/engine/deck_conflict.py +0 -297
  45. opentrons/protocol_api/core/engine/instrument.py +29 -25
  46. opentrons/protocol_api/core/engine/labware.py +10 -2
  47. opentrons/protocol_api/core/engine/module_core.py +129 -17
  48. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +355 -0
  49. opentrons/protocol_api/core/engine/protocol.py +55 -2
  50. opentrons/protocol_api/core/instrument.py +2 -0
  51. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +2 -0
  52. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +5 -2
  53. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +2 -0
  54. opentrons/protocol_api/core/module.py +22 -4
  55. opentrons/protocol_api/core/protocol.py +5 -2
  56. opentrons/protocol_api/instrument_context.py +52 -20
  57. opentrons/protocol_api/labware.py +13 -1
  58. opentrons/protocol_api/module_contexts.py +68 -13
  59. opentrons/protocol_api/protocol_context.py +38 -4
  60. opentrons/protocol_api/validation.py +5 -3
  61. opentrons/protocol_engine/__init__.py +10 -9
  62. opentrons/protocol_engine/actions/__init__.py +5 -0
  63. opentrons/protocol_engine/actions/actions.py +42 -25
  64. opentrons/protocol_engine/actions/get_state_update.py +38 -0
  65. opentrons/protocol_engine/clients/sync_client.py +7 -1
  66. opentrons/protocol_engine/clients/transports.py +1 -1
  67. opentrons/protocol_engine/commands/__init__.py +0 -4
  68. opentrons/protocol_engine/commands/absorbance_reader/__init__.py +41 -11
  69. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +161 -0
  70. opentrons/protocol_engine/commands/absorbance_reader/initialize.py +53 -9
  71. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +160 -0
  72. opentrons/protocol_engine/commands/absorbance_reader/read.py +196 -0
  73. opentrons/protocol_engine/commands/aspirate.py +29 -16
  74. opentrons/protocol_engine/commands/aspirate_in_place.py +32 -15
  75. opentrons/protocol_engine/commands/blow_out.py +63 -14
  76. opentrons/protocol_engine/commands/blow_out_in_place.py +55 -13
  77. opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +2 -5
  78. opentrons/protocol_engine/commands/calibration/calibrate_module.py +3 -4
  79. opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +2 -5
  80. opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +6 -4
  81. opentrons/protocol_engine/commands/command.py +28 -17
  82. opentrons/protocol_engine/commands/command_unions.py +37 -24
  83. opentrons/protocol_engine/commands/comment.py +5 -3
  84. opentrons/protocol_engine/commands/configure_for_volume.py +11 -14
  85. opentrons/protocol_engine/commands/configure_nozzle_layout.py +9 -15
  86. opentrons/protocol_engine/commands/custom.py +5 -3
  87. opentrons/protocol_engine/commands/dispense.py +42 -20
  88. opentrons/protocol_engine/commands/dispense_in_place.py +32 -14
  89. opentrons/protocol_engine/commands/drop_tip.py +68 -15
  90. opentrons/protocol_engine/commands/drop_tip_in_place.py +52 -11
  91. opentrons/protocol_engine/commands/get_tip_presence.py +5 -3
  92. opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +6 -6
  93. opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +6 -6
  94. opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +6 -6
  95. opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +8 -6
  96. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +8 -4
  97. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +6 -4
  98. opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +6 -6
  99. opentrons/protocol_engine/commands/home.py +11 -5
  100. opentrons/protocol_engine/commands/liquid_probe.py +146 -88
  101. opentrons/protocol_engine/commands/load_labware.py +19 -5
  102. opentrons/protocol_engine/commands/load_liquid.py +18 -7
  103. opentrons/protocol_engine/commands/load_module.py +43 -6
  104. opentrons/protocol_engine/commands/load_pipette.py +18 -17
  105. opentrons/protocol_engine/commands/magnetic_module/disengage.py +6 -6
  106. opentrons/protocol_engine/commands/magnetic_module/engage.py +6 -4
  107. opentrons/protocol_engine/commands/move_labware.py +106 -19
  108. opentrons/protocol_engine/commands/move_relative.py +15 -3
  109. opentrons/protocol_engine/commands/move_to_addressable_area.py +29 -4
  110. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +13 -4
  111. opentrons/protocol_engine/commands/move_to_coordinates.py +11 -5
  112. opentrons/protocol_engine/commands/move_to_well.py +37 -10
  113. opentrons/protocol_engine/commands/pick_up_tip.py +50 -29
  114. opentrons/protocol_engine/commands/pipetting_common.py +39 -15
  115. opentrons/protocol_engine/commands/prepare_to_aspirate.py +62 -15
  116. opentrons/protocol_engine/commands/reload_labware.py +13 -4
  117. opentrons/protocol_engine/commands/retract_axis.py +6 -3
  118. opentrons/protocol_engine/commands/save_position.py +2 -3
  119. opentrons/protocol_engine/commands/set_rail_lights.py +5 -3
  120. opentrons/protocol_engine/commands/set_status_bar.py +5 -3
  121. opentrons/protocol_engine/commands/temperature_module/deactivate.py +6 -4
  122. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +3 -4
  123. opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +6 -6
  124. opentrons/protocol_engine/commands/thermocycler/__init__.py +19 -0
  125. opentrons/protocol_engine/commands/thermocycler/close_lid.py +8 -8
  126. opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +6 -4
  127. opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +6 -4
  128. opentrons/protocol_engine/commands/thermocycler/open_lid.py +8 -4
  129. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +165 -0
  130. opentrons/protocol_engine/commands/thermocycler/run_profile.py +6 -6
  131. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +3 -4
  132. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +3 -4
  133. opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +6 -4
  134. opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +6 -4
  135. opentrons/protocol_engine/commands/touch_tip.py +19 -7
  136. opentrons/protocol_engine/commands/unsafe/__init__.py +30 -0
  137. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +6 -4
  138. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +12 -4
  139. opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +5 -3
  140. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +194 -0
  141. opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py +75 -0
  142. opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +5 -3
  143. opentrons/protocol_engine/commands/verify_tip_presence.py +5 -5
  144. opentrons/protocol_engine/commands/wait_for_duration.py +5 -3
  145. opentrons/protocol_engine/commands/wait_for_resume.py +5 -3
  146. opentrons/protocol_engine/create_protocol_engine.py +41 -8
  147. opentrons/protocol_engine/engine_support.py +2 -1
  148. opentrons/protocol_engine/error_recovery_policy.py +14 -3
  149. opentrons/protocol_engine/errors/__init__.py +18 -0
  150. opentrons/protocol_engine/errors/exceptions.py +114 -2
  151. opentrons/protocol_engine/execution/__init__.py +2 -0
  152. opentrons/protocol_engine/execution/command_executor.py +22 -13
  153. opentrons/protocol_engine/execution/create_queue_worker.py +5 -1
  154. opentrons/protocol_engine/execution/door_watcher.py +1 -1
  155. opentrons/protocol_engine/execution/equipment.py +2 -1
  156. opentrons/protocol_engine/execution/error_recovery_hardware_state_synchronizer.py +101 -0
  157. opentrons/protocol_engine/execution/gantry_mover.py +4 -2
  158. opentrons/protocol_engine/execution/hardware_stopper.py +3 -3
  159. opentrons/protocol_engine/execution/heater_shaker_movement_flagger.py +1 -4
  160. opentrons/protocol_engine/execution/labware_movement.py +6 -3
  161. opentrons/protocol_engine/execution/movement.py +8 -3
  162. opentrons/protocol_engine/execution/pipetting.py +7 -4
  163. opentrons/protocol_engine/execution/queue_worker.py +6 -2
  164. opentrons/protocol_engine/execution/run_control.py +1 -1
  165. opentrons/protocol_engine/execution/thermocycler_movement_flagger.py +1 -1
  166. opentrons/protocol_engine/execution/thermocycler_plate_lifter.py +2 -1
  167. opentrons/protocol_engine/execution/tip_handler.py +77 -43
  168. opentrons/protocol_engine/notes/__init__.py +14 -2
  169. opentrons/protocol_engine/notes/notes.py +18 -1
  170. opentrons/protocol_engine/plugins.py +1 -1
  171. opentrons/protocol_engine/protocol_engine.py +54 -31
  172. opentrons/protocol_engine/resources/__init__.py +2 -0
  173. opentrons/protocol_engine/resources/deck_data_provider.py +58 -5
  174. opentrons/protocol_engine/resources/file_provider.py +157 -0
  175. opentrons/protocol_engine/resources/fixture_validation.py +5 -0
  176. opentrons/protocol_engine/resources/labware_validation.py +10 -0
  177. opentrons/protocol_engine/state/__init__.py +0 -70
  178. opentrons/protocol_engine/state/addressable_areas.py +1 -1
  179. opentrons/protocol_engine/state/command_history.py +21 -2
  180. opentrons/protocol_engine/state/commands.py +110 -31
  181. opentrons/protocol_engine/state/files.py +59 -0
  182. opentrons/protocol_engine/state/frustum_helpers.py +440 -0
  183. opentrons/protocol_engine/state/geometry.py +359 -15
  184. opentrons/protocol_engine/state/labware.py +166 -63
  185. opentrons/protocol_engine/state/liquids.py +1 -1
  186. opentrons/protocol_engine/state/module_substates/absorbance_reader_substate.py +19 -3
  187. opentrons/protocol_engine/state/modules.py +167 -85
  188. opentrons/protocol_engine/state/motion.py +16 -9
  189. opentrons/protocol_engine/state/pipettes.py +157 -317
  190. opentrons/protocol_engine/state/state.py +30 -1
  191. opentrons/protocol_engine/state/state_summary.py +3 -0
  192. opentrons/protocol_engine/state/tips.py +69 -114
  193. opentrons/protocol_engine/state/update_types.py +408 -0
  194. opentrons/protocol_engine/state/wells.py +236 -0
  195. opentrons/protocol_engine/types.py +90 -0
  196. opentrons/protocol_reader/file_format_validator.py +83 -15
  197. opentrons/protocol_runner/json_translator.py +21 -5
  198. opentrons/protocol_runner/legacy_command_mapper.py +27 -6
  199. opentrons/protocol_runner/legacy_context_plugin.py +27 -71
  200. opentrons/protocol_runner/protocol_runner.py +6 -3
  201. opentrons/protocol_runner/run_orchestrator.py +26 -6
  202. opentrons/protocols/advanced_control/mix.py +3 -5
  203. opentrons/protocols/advanced_control/transfers.py +125 -56
  204. opentrons/protocols/api_support/constants.py +1 -1
  205. opentrons/protocols/api_support/definitions.py +1 -1
  206. opentrons/protocols/api_support/labware_like.py +4 -4
  207. opentrons/protocols/api_support/tip_tracker.py +2 -2
  208. opentrons/protocols/api_support/types.py +15 -2
  209. opentrons/protocols/api_support/util.py +30 -42
  210. opentrons/protocols/duration/errors.py +1 -1
  211. opentrons/protocols/duration/estimator.py +50 -29
  212. opentrons/protocols/execution/dev_types.py +2 -2
  213. opentrons/protocols/execution/execute_json_v4.py +15 -10
  214. opentrons/protocols/execution/execute_python.py +8 -3
  215. opentrons/protocols/geometry/planning.py +12 -12
  216. opentrons/protocols/labware.py +17 -33
  217. opentrons/simulate.py +3 -3
  218. opentrons/types.py +30 -3
  219. opentrons/util/logging_config.py +34 -0
  220. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/METADATA +5 -4
  221. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/RECORD +227 -215
  222. opentrons/protocol_engine/commands/absorbance_reader/measure.py +0 -94
  223. opentrons/protocol_engine/commands/configuring_common.py +0 -26
  224. opentrons/protocol_runner/thread_async_queue.py +0 -174
  225. /opentrons/protocol_engine/state/{abstract_store.py → _abstract_store.py} +0 -0
  226. /opentrons/protocol_engine/state/{move_types.py → _move_types.py} +0 -0
  227. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/LICENSE +0 -0
  228. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/WHEEL +0 -0
  229. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/entry_points.txt +0 -0
  230. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/top_level.txt +0 -0
@@ -6,7 +6,6 @@ from opentrons.protocol_engine.actions.actions import (
6
6
  ResumeFromRecoveryAction,
7
7
  SetErrorRecoveryPolicyAction,
8
8
  )
9
- from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryPolicy
10
9
 
11
10
  from opentrons.protocols.models import LabwareDefinition
12
11
  from opentrons.hardware_control import HardwareControlAPI
@@ -19,8 +18,9 @@ from opentrons_shared_data.errors import (
19
18
 
20
19
  from .errors import ProtocolCommandFailedError, ErrorOccurrence, CommandNotAllowedError
21
20
  from .errors.exceptions import EStopActivatedError
21
+ from .error_recovery_policy import ErrorRecoveryPolicy
22
22
  from . import commands, slot_standardization
23
- from .resources import ModelUtils, ModuleDataProvider
23
+ from .resources import ModelUtils, ModuleDataProvider, FileProvider
24
24
  from .types import (
25
25
  LabwareOffset,
26
26
  LabwareOffsetCreate,
@@ -38,7 +38,8 @@ from .execution import (
38
38
  DoorWatcher,
39
39
  HardwareStopper,
40
40
  )
41
- from .state import StateStore, StateView
41
+ from .state.state import StateStore, StateView
42
+ from .state.update_types import StateUpdate
42
43
  from .plugins import AbstractPlugin, PluginStarter
43
44
  from .actions import (
44
45
  ActionDispatcher,
@@ -58,6 +59,7 @@ from .actions import (
58
59
  HardwareStoppedAction,
59
60
  ResetTipsAction,
60
61
  SetPipetteMovementSpeedAction,
62
+ AddAbsorbanceReaderLidAction,
61
63
  )
62
64
 
63
65
 
@@ -87,41 +89,31 @@ class ProtocolEngine:
87
89
  self,
88
90
  hardware_api: HardwareControlAPI,
89
91
  state_store: StateStore,
90
- action_dispatcher: Optional[ActionDispatcher] = None,
91
- plugin_starter: Optional[PluginStarter] = None,
92
+ action_dispatcher: ActionDispatcher,
93
+ plugin_starter: PluginStarter,
94
+ model_utils: ModelUtils,
95
+ hardware_stopper: HardwareStopper,
96
+ door_watcher: DoorWatcher,
97
+ module_data_provider: ModuleDataProvider,
98
+ file_provider: FileProvider,
92
99
  queue_worker: Optional[QueueWorker] = None,
93
- model_utils: Optional[ModelUtils] = None,
94
- hardware_stopper: Optional[HardwareStopper] = None,
95
- door_watcher: Optional[DoorWatcher] = None,
96
- module_data_provider: Optional[ModuleDataProvider] = None,
97
100
  ) -> None:
98
101
  """Initialize a ProtocolEngine instance.
99
102
 
100
103
  Must be called while an event loop is active.
101
104
 
102
- This constructor does not inject provider implementations.
105
+ This constructor is only for `ProtocolEngine` unit tests.
103
106
  Prefer the `create_protocol_engine()` factory function.
104
107
  """
105
108
  self._hardware_api = hardware_api
109
+ self._file_provider = file_provider
106
110
  self._state_store = state_store
107
- self._model_utils = model_utils or ModelUtils()
108
- self._action_dispatcher = action_dispatcher or ActionDispatcher(
109
- sink=self._state_store
110
- )
111
- self._plugin_starter = plugin_starter or PluginStarter(
112
- state=self._state_store,
113
- action_dispatcher=self._action_dispatcher,
114
- )
115
- self._hardware_stopper = hardware_stopper or HardwareStopper(
116
- hardware_api=hardware_api,
117
- state_store=state_store,
118
- )
119
- self._door_watcher = door_watcher or DoorWatcher(
120
- state_store=state_store,
121
- hardware_api=hardware_api,
122
- action_dispatcher=self._action_dispatcher,
123
- )
124
- self._module_data_provider = module_data_provider or ModuleDataProvider()
111
+ self._model_utils = model_utils
112
+ self._action_dispatcher = action_dispatcher
113
+ self._plugin_starter = plugin_starter
114
+ self._hardware_stopper = hardware_stopper
115
+ self._door_watcher = door_watcher
116
+ self._module_data_provider = module_data_provider
125
117
  self._queue_worker = queue_worker
126
118
  if self._queue_worker:
127
119
  self._queue_worker.start()
@@ -183,11 +175,35 @@ class ProtocolEngine:
183
175
  self._action_dispatcher.dispatch(action)
184
176
  self._hardware_api.pause(HardwarePauseType.PAUSE)
185
177
 
186
- def resume_from_recovery(self) -> None:
187
- """Resume normal protocol execution after the engine was `AWAITING_RECOVERY`."""
178
+ def resume_from_recovery(self, reconcile_false_positive: bool) -> None:
179
+ """Resume normal protocol execution after the engine was `AWAITING_RECOVERY`.
180
+
181
+ If `reconcile_false_positive` is `False`, the engine will continue naively from
182
+ whatever state the error left it in. (Each defined error individually documents
183
+ exactly how it affects state.) This is appropriate for client-driven error
184
+ recovery, where the client wants predictable behavior from the engine.
185
+
186
+ If `reconcile_false_positive` is `True`, the engine may apply additional fixups
187
+ to its state to try to get the rest of the run to just work, assuming the error
188
+ was a false-positive.
189
+
190
+ For example, a `tipPhysicallyMissing` error from a `pickUpTip` would normally
191
+ leave the engine state without a tip on the pipette. If `reconcile_false_positive=True`,
192
+ the engine will set the pipette to have that missing tip before continuing, so
193
+ subsequent path planning, aspirates, dispenses, etc. will work as if nothing
194
+ went wrong.
195
+ """
196
+ if reconcile_false_positive:
197
+ state_update = (
198
+ self._state_store.commands.get_state_update_for_false_positive()
199
+ )
200
+ else:
201
+ state_update = StateUpdate() # Empty/no-op.
202
+
188
203
  action = self._state_store.commands.validate_action_allowed(
189
- ResumeFromRecoveryAction()
204
+ ResumeFromRecoveryAction(state_update)
190
205
  )
206
+
191
207
  self._action_dispatcher.dispatch(action)
192
208
 
193
209
  def add_command(
@@ -561,6 +577,12 @@ class ProtocolEngine:
561
577
  AddAddressableAreaAction(addressable_area=area)
562
578
  )
563
579
 
580
+ def add_absorbance_reader_lid(self, module_id: str, lid_id: str) -> None:
581
+ """Add an absorbance reader lid to the module state."""
582
+ self._action_dispatcher.dispatch(
583
+ AddAbsorbanceReaderLidAction(module_id=module_id, lid_id=lid_id)
584
+ )
585
+
564
586
  def reset_tips(self, labware_id: str) -> None:
565
587
  """Reset the tip state of a given labware."""
566
588
  # TODO(mm, 2023-03-10): Safely raise an error if the given labware isn't a
@@ -609,6 +631,7 @@ class ProtocolEngine:
609
631
  assert self._queue_worker is None
610
632
  self._queue_worker = create_queue_worker(
611
633
  hardware_api=self._hardware_api,
634
+ file_provider=self._file_provider,
612
635
  state_store=self._state_store,
613
636
  action_dispatcher=self._action_dispatcher,
614
637
  command_generator=command_generator,
@@ -9,6 +9,7 @@ from .model_utils import ModelUtils
9
9
  from .deck_data_provider import DeckDataProvider, DeckFixedLabware
10
10
  from .labware_data_provider import LabwareDataProvider
11
11
  from .module_data_provider import ModuleDataProvider
12
+ from .file_provider import FileProvider
12
13
  from .ot3_validation import ensure_ot3_hardware
13
14
 
14
15
 
@@ -18,6 +19,7 @@ __all__ = [
18
19
  "DeckDataProvider",
19
20
  "DeckFixedLabware",
20
21
  "ModuleDataProvider",
22
+ "FileProvider",
21
23
  "ensure_ot3_hardware",
22
24
  "pipette_data_provider",
23
25
  "labware_validation",
@@ -13,8 +13,15 @@ from opentrons_shared_data.deck.types import DeckDefinitionV5
13
13
  from opentrons.protocols.models import LabwareDefinition
14
14
  from opentrons.types import DeckSlotName
15
15
 
16
- from ..types import DeckSlotLocation, DeckType
16
+ from ..types import (
17
+ DeckSlotLocation,
18
+ DeckType,
19
+ LabwareLocation,
20
+ AddressableAreaLocation,
21
+ DeckConfigurationType,
22
+ )
17
23
  from .labware_data_provider import LabwareDataProvider
24
+ from ..resources import deck_configuration_provider
18
25
 
19
26
 
20
27
  @final
@@ -23,7 +30,7 @@ class DeckFixedLabware:
23
30
  """A labware fixture that is always present on a deck."""
24
31
 
25
32
  labware_id: str
26
- location: DeckSlotLocation
33
+ location: LabwareLocation
27
34
  definition: LabwareDefinition
28
35
 
29
36
 
@@ -51,7 +58,9 @@ class DeckDataProvider:
51
58
 
52
59
  async def get_deck_fixed_labware(
53
60
  self,
61
+ load_fixed_trash: bool,
54
62
  deck_definition: DeckDefinitionV5,
63
+ deck_configuration: Optional[DeckConfigurationType] = None,
55
64
  ) -> List[DeckFixedLabware]:
56
65
  """Get a list of all labware fixtures from a given deck definition."""
57
66
  labware: List[DeckFixedLabware] = []
@@ -61,8 +70,52 @@ class DeckDataProvider:
61
70
  load_name = cast(Optional[str], fixture.get("labware"))
62
71
  slot = cast(Optional[str], fixture.get("slot"))
63
72
 
64
- if load_name is not None and slot is not None:
65
- location = DeckSlotLocation(slotName=DeckSlotName.from_primitive(slot))
73
+ if (
74
+ deck_configuration is not None
75
+ and load_name is not None
76
+ and slot is not None
77
+ and slot not in DeckSlotName._value2member_map_
78
+ ):
79
+ # The provided slot is likely to be an addressable area for Module-required labware Eg: Plate Reader Lid
80
+ for (
81
+ cutout_id,
82
+ cutout_fixture_id,
83
+ opentrons_module_serial_number,
84
+ ) in deck_configuration:
85
+ provided_addressable_areas = (
86
+ deck_configuration_provider.get_provided_addressable_area_names(
87
+ cutout_fixture_id=cutout_fixture_id,
88
+ cutout_id=cutout_id,
89
+ deck_definition=deck_definition,
90
+ )
91
+ )
92
+ if slot in provided_addressable_areas:
93
+ addressable_area_location = AddressableAreaLocation(
94
+ addressableAreaName=slot
95
+ )
96
+ definition = await self._labware_data.get_labware_definition(
97
+ load_name=load_name,
98
+ namespace="opentrons",
99
+ version=1,
100
+ )
101
+
102
+ labware.append(
103
+ DeckFixedLabware(
104
+ labware_id=labware_id,
105
+ definition=definition,
106
+ location=addressable_area_location,
107
+ )
108
+ )
109
+
110
+ elif (
111
+ load_fixed_trash
112
+ and load_name is not None
113
+ and slot is not None
114
+ and slot in DeckSlotName._value2member_map_
115
+ ):
116
+ deck_slot_location = DeckSlotLocation(
117
+ slotName=DeckSlotName.from_primitive(slot)
118
+ )
66
119
  definition = await self._labware_data.get_labware_definition(
67
120
  load_name=load_name,
68
121
  namespace="opentrons",
@@ -73,7 +126,7 @@ class DeckDataProvider:
73
126
  DeckFixedLabware(
74
127
  labware_id=labware_id,
75
128
  definition=definition,
76
- location=location,
129
+ location=deck_slot_location,
77
130
  )
78
131
  )
79
132
 
@@ -0,0 +1,157 @@
1
+ """File interaction resource provider."""
2
+ from datetime import datetime
3
+ from typing import List, Optional, Callable, Awaitable, Dict
4
+ from pydantic import BaseModel
5
+ from ..errors import StorageLimitReachedError
6
+
7
+
8
+ MAXIMUM_CSV_FILE_LIMIT = 400
9
+
10
+
11
+ class GenericCsvTransform:
12
+ """Generic CSV File Type data for rows of data to be seperated by a delimeter."""
13
+
14
+ filename: str
15
+ rows: List[List[str]]
16
+ delimiter: str = ","
17
+
18
+ @staticmethod
19
+ def build(
20
+ filename: str, rows: List[List[str]], delimiter: str = ","
21
+ ) -> "GenericCsvTransform":
22
+ """Build a Generic CSV datatype class."""
23
+ if "." in filename and not filename.endswith(".csv"):
24
+ raise ValueError(
25
+ f"Provided filename {filename} invalid. Only CSV file format is accepted."
26
+ )
27
+ elif "." not in filename:
28
+ filename = f"{filename}.csv"
29
+ csv = GenericCsvTransform()
30
+ csv.filename = filename
31
+ csv.rows = rows
32
+ csv.delimiter = delimiter
33
+ return csv
34
+
35
+
36
+ class ReadData(BaseModel):
37
+ """Read Data type containing the wavelength for a Plate Reader read alongside the Measurement Data of that read."""
38
+
39
+ wavelength: int
40
+ data: Dict[str, float]
41
+
42
+
43
+ class PlateReaderData(BaseModel):
44
+ """Data from a Opentrons Plate Reader Read. Can be converted to CSV template format."""
45
+
46
+ read_results: List[ReadData]
47
+ reference_wavelength: Optional[int] = None
48
+ start_time: datetime
49
+ finish_time: datetime
50
+ serial_number: str
51
+
52
+ def build_generic_csv( # noqa: C901
53
+ self, filename: str, measurement: ReadData
54
+ ) -> GenericCsvTransform:
55
+ """Builds a CSV compatible object containing Plate Reader Measurements.
56
+
57
+ This will also automatically reformat the provided filename to include the wavelength of those measurements.
58
+ """
59
+ plate_alpharows = ["A", "B", "C", "D", "E", "F", "G", "H"]
60
+ rows = []
61
+
62
+ rows.append(["", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"])
63
+ for i in range(8):
64
+ row = [plate_alpharows[i]]
65
+ for j in range(12):
66
+ row.append(str(measurement.data[f"{plate_alpharows[i]}{j+1}"]))
67
+ rows.append(row)
68
+ for i in range(3):
69
+ rows.append([""])
70
+ rows.append(["", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"])
71
+ for i in range(8):
72
+ row = [plate_alpharows[i]]
73
+ for j in range(12):
74
+ row.append("")
75
+ rows.append(row)
76
+ for i in range(3):
77
+ rows.append([""])
78
+ rows.append(
79
+ [
80
+ "",
81
+ "ID",
82
+ "Well",
83
+ "Absorbance (OD)",
84
+ "Mean Absorbance (OD)",
85
+ "Absorbance %CV",
86
+ ]
87
+ )
88
+ for i in range(3):
89
+ rows.append([""])
90
+ rows.append(
91
+ [
92
+ "",
93
+ "ID",
94
+ "Well",
95
+ "Absorbance (OD)",
96
+ "Mean Absorbance (OD)",
97
+ "Dilution Factor",
98
+ "Absorbance %CV",
99
+ ]
100
+ )
101
+ rows.append(["1", "Sample 1", "", "", "", "1", "", "", "", "", "", ""])
102
+ for i in range(3):
103
+ rows.append([""])
104
+
105
+ # end of file metadata
106
+ rows.append(["Protocol"])
107
+ rows.append(["Assay"])
108
+ rows.append(["Sample Wavelength (nm)", str(measurement.wavelength)])
109
+ if self.reference_wavelength is not None:
110
+ rows.append(["Reference Wavelength (nm)", str(self.reference_wavelength)])
111
+ rows.append(["Serial No.", self.serial_number])
112
+ rows.append(["Measurement started at", str(self.start_time)])
113
+ rows.append(["Measurement finished at", str(self.finish_time)])
114
+
115
+ # Ensure the filename adheres to ruleset contains the wavelength for a given measurement
116
+ if filename.endswith(".csv"):
117
+ filename = filename[:-4]
118
+ filename = filename + "_" + str(measurement.wavelength) + ".csv"
119
+
120
+ return GenericCsvTransform.build(
121
+ filename=filename,
122
+ rows=rows,
123
+ delimiter=",",
124
+ )
125
+
126
+
127
+ class FileProvider:
128
+ """Provider class to wrap file read write interactions to the data files directory in the engine."""
129
+
130
+ def __init__(
131
+ self,
132
+ data_files_write_csv_callback: Optional[
133
+ Callable[[GenericCsvTransform], Awaitable[str]]
134
+ ] = None,
135
+ data_files_filecount: Optional[Callable[[], Awaitable[int]]] = None,
136
+ ) -> None:
137
+ """Initialize the interface callbacks of the File Provider for data file handling within the Protocol Engine.
138
+
139
+ Params:
140
+ data_files_write_csv_callback: Callback to write a CSV file to the data files directory and add it to the database.
141
+ data_files_filecount: Callback to check the amount of data files already present in the data files directory.
142
+ """
143
+ self._data_files_write_csv_callback = data_files_write_csv_callback
144
+ self._data_files_filecount = data_files_filecount
145
+
146
+ async def write_csv(self, write_data: GenericCsvTransform) -> str:
147
+ """Writes the provided CSV object to a file in the Data Files directory. Returns the File ID of the file created."""
148
+ if self._data_files_filecount is not None:
149
+ file_count = await self._data_files_filecount()
150
+ if file_count >= MAXIMUM_CSV_FILE_LIMIT:
151
+ raise StorageLimitReachedError(
152
+ f"Not enough space to store file {write_data.filename}."
153
+ )
154
+ if self._data_files_write_csv_callback is not None:
155
+ return await self._data_files_write_csv_callback(write_data)
156
+ # If we are in an analysis or simulation state, return an empty file ID
157
+ return ""
@@ -46,3 +46,8 @@ def is_deck_slot(addressable_area_name: str) -> bool:
46
46
  except ValueError:
47
47
  return False
48
48
  return True
49
+
50
+
51
+ def is_abs_reader(addressable_area_name: str) -> bool:
52
+ """Check if an addressable area is an absorbance plate reader area."""
53
+ return "absorbanceReaderV1" in addressable_area_name
@@ -9,6 +9,11 @@ def is_flex_trash(load_name: str) -> bool:
9
9
  return load_name == "opentrons_1_trash_3200ml_fixed"
10
10
 
11
11
 
12
+ def is_absorbance_reader_lid(load_name: str) -> bool:
13
+ """Check if a labware is an absorbance reader lid."""
14
+ return load_name == "opentrons_flex_lid_absorbance_plate_reader_module"
15
+
16
+
12
17
  def validate_definition_is_labware(definition: LabwareDefinition) -> bool:
13
18
  """Validate that one of the definition's allowed roles is `labware`.
14
19
 
@@ -22,6 +27,11 @@ def validate_definition_is_adapter(definition: LabwareDefinition) -> bool:
22
27
  return LabwareRole.adapter in definition.allowedRoles
23
28
 
24
29
 
30
+ def validate_definition_is_lid(definition: LabwareDefinition) -> bool:
31
+ """Validate that one of the definition's allowed roles is `lid`."""
32
+ return LabwareRole.lid in definition.allowedRoles
33
+
34
+
25
35
  def validate_labware_can_be_stacked(
26
36
  top_labware_definition: LabwareDefinition, below_labware_load_name: str
27
37
  ) -> bool:
@@ -1,71 +1 @@
1
1
  """Protocol engine state module."""
2
-
3
- from .state import State, StateStore, StateView
4
- from .state_summary import StateSummary
5
- from .config import Config
6
- from .commands import (
7
- CommandState,
8
- CommandView,
9
- CommandSlice,
10
- CommandErrorSlice,
11
- CommandPointer,
12
- )
13
- from .command_history import CommandEntry
14
- from .labware import LabwareState, LabwareView
15
- from .pipettes import PipetteState, PipetteView, HardwarePipette
16
- from .modules import ModuleState, ModuleView, HardwareModule
17
- from .module_substates import (
18
- MagneticModuleId,
19
- MagneticModuleSubState,
20
- HeaterShakerModuleId,
21
- HeaterShakerModuleSubState,
22
- TemperatureModuleId,
23
- TemperatureModuleSubState,
24
- ThermocyclerModuleId,
25
- ThermocyclerModuleSubState,
26
- ModuleSubStateType,
27
- )
28
- from .geometry import GeometryView
29
- from .motion import MotionView, PipetteLocationData
30
-
31
- __all__ = [
32
- # top level state value and interfaces
33
- "State",
34
- "StateStore",
35
- "StateView",
36
- "StateSummary",
37
- # static engine configuration
38
- "Config",
39
- # command state and values
40
- "CommandState",
41
- "CommandView",
42
- "CommandSlice",
43
- "CommandErrorSlice",
44
- "CommandPointer",
45
- "CommandEntry",
46
- # labware state and values
47
- "LabwareState",
48
- "LabwareView",
49
- # pipette state and values
50
- "PipetteState",
51
- "PipetteView",
52
- "HardwarePipette",
53
- # module state and values
54
- "ModuleState",
55
- "ModuleView",
56
- "HardwareModule",
57
- "MagneticModuleId",
58
- "MagneticModuleSubState",
59
- "HeaterShakerModuleId",
60
- "HeaterShakerModuleSubState",
61
- "TemperatureModuleId",
62
- "TemperatureModuleSubState",
63
- "ThermocyclerModuleId",
64
- "ThermocyclerModuleSubState",
65
- "ModuleSubStateType",
66
- # computed geometry state
67
- "GeometryView",
68
- # computed motion state
69
- "MotionView",
70
- "PipetteLocationData",
71
- ]
@@ -43,7 +43,7 @@ from ..actions import (
43
43
  AddAddressableAreaAction,
44
44
  )
45
45
  from .config import Config
46
- from .abstract_store import HasState, HandlesActions
46
+ from ._abstract_store import HasState, HandlesActions
47
47
 
48
48
 
49
49
  @dataclass
@@ -24,6 +24,9 @@ class CommandHistory:
24
24
  _all_command_ids: List[str]
25
25
  """All command IDs, in insertion order."""
26
26
 
27
+ _all_command_ids_but_fixit_command_ids: List[str]
28
+ """All command IDs besides fixit command intents, in insertion order."""
29
+
27
30
  _commands_by_id: Dict[str, CommandEntry]
28
31
  """All command resources, in insertion order, mapped by their unique IDs."""
29
32
 
@@ -44,6 +47,7 @@ class CommandHistory:
44
47
 
45
48
  def __init__(self) -> None:
46
49
  self._all_command_ids = []
50
+ self._all_command_ids_but_fixit_command_ids = []
47
51
  self._queued_command_ids = OrderedSet()
48
52
  self._queued_setup_command_ids = OrderedSet()
49
53
  self._queued_fixit_command_ids = OrderedSet()
@@ -97,13 +101,26 @@ class CommandHistory:
97
101
  for command_id in self._all_command_ids
98
102
  ]
99
103
 
104
+ def get_filtered_command_ids(self, include_fixit_commands: bool) -> List[str]:
105
+ """Get all fixit command IDs."""
106
+ if include_fixit_commands:
107
+ return self._all_command_ids
108
+ else:
109
+ return self._all_command_ids_but_fixit_command_ids
110
+
100
111
  def get_all_ids(self) -> List[str]:
101
112
  """Get all command IDs."""
102
113
  return self._all_command_ids
103
114
 
104
- def get_slice(self, start: int, stop: int) -> List[Command]:
105
- """Get a list of commands between start and stop."""
115
+ def get_slice(
116
+ self, start: int, stop: int, command_ids: Optional[list[str]] = None
117
+ ) -> List[Command]:
118
+ """Get a list of commands between start and stop.""" """Get a list of commands between start and stop."""
106
119
  commands = self._all_command_ids[start:stop]
120
+ selected_command_ids = (
121
+ command_ids if command_ids is not None else self._all_command_ids
122
+ )
123
+ commands = selected_command_ids[start:stop]
107
124
  return [self._commands_by_id[command].command for command in commands]
108
125
 
109
126
  def get_tail_command(self) -> Optional[CommandEntry]:
@@ -230,6 +247,8 @@ class CommandHistory:
230
247
  """Create or update a command entry."""
231
248
  if command_id not in self._commands_by_id:
232
249
  self._all_command_ids.append(command_id)
250
+ if command_entry.command.intent != CommandIntent.FIXIT:
251
+ self._all_command_ids_but_fixit_command_ids.append(command_id)
233
252
  self._commands_by_id[command_id] = command_entry
234
253
 
235
254
  def _add_to_queue(self, command_id: str) -> None: