opentrons 8.2.0a3__py2.py3-none-any.whl → 8.3.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. opentrons/calibration_storage/deck_configuration.py +3 -3
  2. opentrons/calibration_storage/file_operators.py +3 -3
  3. opentrons/calibration_storage/helpers.py +3 -1
  4. opentrons/calibration_storage/ot2/models/v1.py +16 -29
  5. opentrons/calibration_storage/ot2/tip_length.py +7 -4
  6. opentrons/calibration_storage/ot3/models/v1.py +14 -23
  7. opentrons/cli/analyze.py +18 -6
  8. opentrons/config/defaults_ot3.py +1 -0
  9. opentrons/drivers/asyncio/communication/__init__.py +2 -0
  10. opentrons/drivers/asyncio/communication/errors.py +16 -3
  11. opentrons/drivers/asyncio/communication/serial_connection.py +24 -9
  12. opentrons/drivers/command_builder.py +2 -2
  13. opentrons/drivers/flex_stacker/__init__.py +9 -0
  14. opentrons/drivers/flex_stacker/abstract.py +89 -0
  15. opentrons/drivers/flex_stacker/driver.py +260 -0
  16. opentrons/drivers/flex_stacker/simulator.py +109 -0
  17. opentrons/drivers/flex_stacker/types.py +138 -0
  18. opentrons/drivers/heater_shaker/driver.py +18 -3
  19. opentrons/drivers/temp_deck/driver.py +13 -3
  20. opentrons/drivers/thermocycler/driver.py +17 -3
  21. opentrons/execute.py +3 -1
  22. opentrons/hardware_control/__init__.py +1 -2
  23. opentrons/hardware_control/api.py +33 -21
  24. opentrons/hardware_control/backends/flex_protocol.py +17 -7
  25. opentrons/hardware_control/backends/ot3controller.py +213 -63
  26. opentrons/hardware_control/backends/ot3simulator.py +18 -9
  27. opentrons/hardware_control/backends/ot3utils.py +43 -15
  28. opentrons/hardware_control/dev_types.py +4 -0
  29. opentrons/hardware_control/emulation/heater_shaker.py +4 -0
  30. opentrons/hardware_control/emulation/module_server/client.py +1 -1
  31. opentrons/hardware_control/emulation/module_server/server.py +5 -3
  32. opentrons/hardware_control/emulation/settings.py +3 -4
  33. opentrons/hardware_control/instruments/ot2/instrument_calibration.py +2 -1
  34. opentrons/hardware_control/instruments/ot2/pipette.py +15 -22
  35. opentrons/hardware_control/instruments/ot2/pipette_handler.py +8 -1
  36. opentrons/hardware_control/instruments/ot3/gripper.py +2 -2
  37. opentrons/hardware_control/instruments/ot3/pipette.py +23 -22
  38. opentrons/hardware_control/instruments/ot3/pipette_handler.py +10 -1
  39. opentrons/hardware_control/modules/mod_abc.py +2 -2
  40. opentrons/hardware_control/motion_utilities.py +68 -0
  41. opentrons/hardware_control/nozzle_manager.py +39 -41
  42. opentrons/hardware_control/ot3_calibration.py +1 -1
  43. opentrons/hardware_control/ot3api.py +78 -31
  44. opentrons/hardware_control/protocols/gripper_controller.py +3 -0
  45. opentrons/hardware_control/protocols/hardware_manager.py +5 -1
  46. opentrons/hardware_control/protocols/liquid_handler.py +22 -1
  47. opentrons/hardware_control/protocols/motion_controller.py +7 -0
  48. opentrons/hardware_control/robot_calibration.py +1 -1
  49. opentrons/hardware_control/types.py +61 -0
  50. opentrons/legacy_commands/commands.py +37 -0
  51. opentrons/legacy_commands/types.py +39 -0
  52. opentrons/protocol_api/__init__.py +20 -1
  53. opentrons/protocol_api/_liquid.py +24 -49
  54. opentrons/protocol_api/_liquid_properties.py +754 -0
  55. opentrons/protocol_api/_types.py +24 -0
  56. opentrons/protocol_api/core/common.py +2 -0
  57. opentrons/protocol_api/core/engine/instrument.py +191 -10
  58. opentrons/protocol_api/core/engine/labware.py +29 -7
  59. opentrons/protocol_api/core/engine/protocol.py +130 -5
  60. opentrons/protocol_api/core/engine/robot.py +139 -0
  61. opentrons/protocol_api/core/engine/well.py +4 -1
  62. opentrons/protocol_api/core/instrument.py +73 -4
  63. opentrons/protocol_api/core/labware.py +13 -4
  64. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +87 -3
  65. opentrons/protocol_api/core/legacy/legacy_labware_core.py +13 -4
  66. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +32 -1
  67. opentrons/protocol_api/core/legacy/legacy_robot_core.py +0 -0
  68. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +61 -3
  69. opentrons/protocol_api/core/protocol.py +34 -1
  70. opentrons/protocol_api/core/robot.py +51 -0
  71. opentrons/protocol_api/instrument_context.py +299 -44
  72. opentrons/protocol_api/labware.py +248 -9
  73. opentrons/protocol_api/module_contexts.py +21 -17
  74. opentrons/protocol_api/protocol_context.py +125 -4
  75. opentrons/protocol_api/robot_context.py +204 -32
  76. opentrons/protocol_api/validation.py +262 -3
  77. opentrons/protocol_engine/__init__.py +4 -0
  78. opentrons/protocol_engine/actions/actions.py +2 -3
  79. opentrons/protocol_engine/clients/sync_client.py +18 -0
  80. opentrons/protocol_engine/commands/__init__.py +121 -0
  81. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +1 -3
  82. opentrons/protocol_engine/commands/absorbance_reader/initialize.py +20 -6
  83. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +1 -2
  84. opentrons/protocol_engine/commands/absorbance_reader/read.py +40 -10
  85. opentrons/protocol_engine/commands/air_gap_in_place.py +160 -0
  86. opentrons/protocol_engine/commands/aspirate.py +103 -53
  87. opentrons/protocol_engine/commands/aspirate_in_place.py +55 -51
  88. opentrons/protocol_engine/commands/blow_out.py +44 -39
  89. opentrons/protocol_engine/commands/blow_out_in_place.py +21 -32
  90. opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +13 -6
  91. opentrons/protocol_engine/commands/calibration/calibrate_module.py +1 -1
  92. opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +3 -3
  93. opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +1 -1
  94. opentrons/protocol_engine/commands/command.py +73 -66
  95. opentrons/protocol_engine/commands/command_unions.py +140 -1
  96. opentrons/protocol_engine/commands/comment.py +1 -1
  97. opentrons/protocol_engine/commands/configure_for_volume.py +10 -3
  98. opentrons/protocol_engine/commands/configure_nozzle_layout.py +6 -4
  99. opentrons/protocol_engine/commands/custom.py +6 -12
  100. opentrons/protocol_engine/commands/dispense.py +82 -48
  101. opentrons/protocol_engine/commands/dispense_in_place.py +71 -51
  102. opentrons/protocol_engine/commands/drop_tip.py +52 -31
  103. opentrons/protocol_engine/commands/drop_tip_in_place.py +79 -8
  104. opentrons/protocol_engine/commands/evotip_dispense.py +156 -0
  105. opentrons/protocol_engine/commands/evotip_seal_pipette.py +331 -0
  106. opentrons/protocol_engine/commands/evotip_unseal_pipette.py +160 -0
  107. opentrons/protocol_engine/commands/generate_command_schema.py +4 -11
  108. opentrons/protocol_engine/commands/get_next_tip.py +134 -0
  109. opentrons/protocol_engine/commands/get_tip_presence.py +1 -1
  110. opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +1 -1
  111. opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +1 -1
  112. opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +1 -1
  113. opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +1 -1
  114. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +1 -1
  115. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +1 -1
  116. opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +10 -4
  117. opentrons/protocol_engine/commands/home.py +13 -4
  118. opentrons/protocol_engine/commands/liquid_probe.py +125 -31
  119. opentrons/protocol_engine/commands/load_labware.py +33 -6
  120. opentrons/protocol_engine/commands/load_lid.py +146 -0
  121. opentrons/protocol_engine/commands/load_lid_stack.py +189 -0
  122. opentrons/protocol_engine/commands/load_liquid.py +12 -4
  123. opentrons/protocol_engine/commands/load_liquid_class.py +144 -0
  124. opentrons/protocol_engine/commands/load_module.py +31 -10
  125. opentrons/protocol_engine/commands/load_pipette.py +19 -8
  126. opentrons/protocol_engine/commands/magnetic_module/disengage.py +1 -1
  127. opentrons/protocol_engine/commands/magnetic_module/engage.py +1 -1
  128. opentrons/protocol_engine/commands/move_labware.py +28 -6
  129. opentrons/protocol_engine/commands/move_relative.py +35 -25
  130. opentrons/protocol_engine/commands/move_to_addressable_area.py +40 -27
  131. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +53 -32
  132. opentrons/protocol_engine/commands/move_to_coordinates.py +36 -22
  133. opentrons/protocol_engine/commands/move_to_well.py +40 -24
  134. opentrons/protocol_engine/commands/movement_common.py +338 -0
  135. opentrons/protocol_engine/commands/pick_up_tip.py +49 -27
  136. opentrons/protocol_engine/commands/pipetting_common.py +169 -87
  137. opentrons/protocol_engine/commands/prepare_to_aspirate.py +24 -33
  138. opentrons/protocol_engine/commands/reload_labware.py +1 -1
  139. opentrons/protocol_engine/commands/retract_axis.py +1 -1
  140. opentrons/protocol_engine/commands/robot/__init__.py +69 -0
  141. opentrons/protocol_engine/commands/robot/close_gripper_jaw.py +86 -0
  142. opentrons/protocol_engine/commands/robot/common.py +18 -0
  143. opentrons/protocol_engine/commands/robot/move_axes_relative.py +101 -0
  144. opentrons/protocol_engine/commands/robot/move_axes_to.py +100 -0
  145. opentrons/protocol_engine/commands/robot/move_to.py +94 -0
  146. opentrons/protocol_engine/commands/robot/open_gripper_jaw.py +77 -0
  147. opentrons/protocol_engine/commands/save_position.py +14 -5
  148. opentrons/protocol_engine/commands/set_rail_lights.py +1 -1
  149. opentrons/protocol_engine/commands/set_status_bar.py +1 -1
  150. opentrons/protocol_engine/commands/temperature_module/deactivate.py +1 -1
  151. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +1 -1
  152. opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +10 -4
  153. opentrons/protocol_engine/commands/thermocycler/close_lid.py +1 -1
  154. opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +1 -1
  155. opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +1 -1
  156. opentrons/protocol_engine/commands/thermocycler/open_lid.py +1 -1
  157. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +9 -3
  158. opentrons/protocol_engine/commands/thermocycler/run_profile.py +9 -3
  159. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +11 -4
  160. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +1 -1
  161. opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +1 -1
  162. opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +1 -1
  163. opentrons/protocol_engine/commands/touch_tip.py +65 -16
  164. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +5 -2
  165. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +13 -4
  166. opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +2 -5
  167. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +1 -1
  168. opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py +4 -2
  169. opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +2 -5
  170. opentrons/protocol_engine/commands/verify_tip_presence.py +11 -4
  171. opentrons/protocol_engine/commands/wait_for_duration.py +10 -3
  172. opentrons/protocol_engine/commands/wait_for_resume.py +10 -3
  173. opentrons/protocol_engine/errors/__init__.py +12 -0
  174. opentrons/protocol_engine/errors/error_occurrence.py +19 -20
  175. opentrons/protocol_engine/errors/exceptions.py +76 -0
  176. opentrons/protocol_engine/execution/command_executor.py +1 -1
  177. opentrons/protocol_engine/execution/equipment.py +73 -5
  178. opentrons/protocol_engine/execution/gantry_mover.py +369 -8
  179. opentrons/protocol_engine/execution/hardware_stopper.py +7 -7
  180. opentrons/protocol_engine/execution/movement.py +27 -0
  181. opentrons/protocol_engine/execution/pipetting.py +5 -1
  182. opentrons/protocol_engine/execution/tip_handler.py +34 -15
  183. opentrons/protocol_engine/notes/notes.py +1 -1
  184. opentrons/protocol_engine/protocol_engine.py +7 -6
  185. opentrons/protocol_engine/resources/labware_data_provider.py +1 -1
  186. opentrons/protocol_engine/resources/labware_validation.py +18 -0
  187. opentrons/protocol_engine/resources/module_data_provider.py +1 -1
  188. opentrons/protocol_engine/resources/pipette_data_provider.py +26 -0
  189. opentrons/protocol_engine/slot_standardization.py +9 -9
  190. opentrons/protocol_engine/state/_move_types.py +9 -5
  191. opentrons/protocol_engine/state/_well_math.py +193 -0
  192. opentrons/protocol_engine/state/addressable_areas.py +25 -61
  193. opentrons/protocol_engine/state/command_history.py +12 -0
  194. opentrons/protocol_engine/state/commands.py +22 -14
  195. opentrons/protocol_engine/state/files.py +10 -12
  196. opentrons/protocol_engine/state/fluid_stack.py +138 -0
  197. opentrons/protocol_engine/state/frustum_helpers.py +63 -69
  198. opentrons/protocol_engine/state/geometry.py +47 -1
  199. opentrons/protocol_engine/state/labware.py +92 -26
  200. opentrons/protocol_engine/state/liquid_classes.py +82 -0
  201. opentrons/protocol_engine/state/liquids.py +16 -4
  202. opentrons/protocol_engine/state/modules.py +56 -71
  203. opentrons/protocol_engine/state/motion.py +6 -1
  204. opentrons/protocol_engine/state/pipettes.py +149 -58
  205. opentrons/protocol_engine/state/state.py +21 -2
  206. opentrons/protocol_engine/state/state_summary.py +4 -2
  207. opentrons/protocol_engine/state/tips.py +11 -44
  208. opentrons/protocol_engine/state/update_types.py +343 -48
  209. opentrons/protocol_engine/state/wells.py +19 -11
  210. opentrons/protocol_engine/types.py +176 -28
  211. opentrons/protocol_reader/extract_labware_definitions.py +5 -2
  212. opentrons/protocol_reader/file_format_validator.py +5 -5
  213. opentrons/protocol_runner/json_file_reader.py +9 -3
  214. opentrons/protocol_runner/json_translator.py +51 -25
  215. opentrons/protocol_runner/legacy_command_mapper.py +66 -64
  216. opentrons/protocol_runner/protocol_runner.py +35 -4
  217. opentrons/protocol_runner/python_protocol_wrappers.py +1 -1
  218. opentrons/protocol_runner/run_orchestrator.py +13 -3
  219. opentrons/protocols/advanced_control/common.py +38 -0
  220. opentrons/protocols/advanced_control/mix.py +1 -1
  221. opentrons/protocols/advanced_control/transfers/__init__.py +0 -0
  222. opentrons/protocols/advanced_control/transfers/common.py +56 -0
  223. opentrons/protocols/advanced_control/{transfers.py → transfers/transfer.py} +10 -85
  224. opentrons/protocols/api_support/definitions.py +1 -1
  225. opentrons/protocols/api_support/instrument.py +1 -1
  226. opentrons/protocols/api_support/util.py +10 -0
  227. opentrons/protocols/labware.py +70 -8
  228. opentrons/protocols/models/json_protocol.py +5 -9
  229. opentrons/simulate.py +3 -1
  230. opentrons/types.py +162 -2
  231. opentrons/util/entrypoint_util.py +2 -5
  232. opentrons/util/logging_config.py +1 -1
  233. {opentrons-8.2.0a3.dist-info → opentrons-8.3.0.dist-info}/METADATA +16 -15
  234. {opentrons-8.2.0a3.dist-info → opentrons-8.3.0.dist-info}/RECORD +238 -208
  235. {opentrons-8.2.0a3.dist-info → opentrons-8.3.0.dist-info}/WHEEL +1 -1
  236. {opentrons-8.2.0a3.dist-info → opentrons-8.3.0.dist-info}/LICENSE +0 -0
  237. {opentrons-8.2.0a3.dist-info → opentrons-8.3.0.dist-info}/entry_points.txt +0 -0
  238. {opentrons-8.2.0a3.dist-info → opentrons-8.3.0.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  """Basic addressable area data state and store."""
2
2
  from dataclasses import dataclass
3
3
  from functools import cached_property
4
- from typing import Dict, List, Optional, Set, Union
4
+ from typing import Dict, List, Optional, Set
5
5
 
6
6
  from opentrons_shared_data.robot.types import RobotType, RobotDefinition
7
7
  from opentrons_shared_data.deck.types import (
@@ -12,14 +12,6 @@ from opentrons_shared_data.deck.types import (
12
12
 
13
13
  from opentrons.types import Point, DeckSlotName
14
14
 
15
- from ..commands import (
16
- Command,
17
- LoadLabwareResult,
18
- LoadModuleResult,
19
- MoveLabwareResult,
20
- MoveToAddressableAreaResult,
21
- MoveToAddressableAreaForDropTipResult,
22
- )
23
15
  from ..errors import (
24
16
  IncompatibleAddressableAreaError,
25
17
  AreaNotInDeckConfigurationError,
@@ -29,19 +21,18 @@ from ..errors import (
29
21
  )
30
22
  from ..resources import deck_configuration_provider
31
23
  from ..types import (
32
- DeckSlotLocation,
33
- AddressableAreaLocation,
34
24
  AddressableArea,
35
25
  PotentialCutoutFixture,
36
26
  DeckConfigurationType,
37
27
  Dimensions,
38
28
  )
29
+ from ..actions.get_state_update import get_state_updates
39
30
  from ..actions import (
40
31
  Action,
41
- SucceedCommandAction,
42
32
  SetDeckConfigurationAction,
43
33
  AddAddressableAreaAction,
44
34
  )
35
+ from . import update_types
45
36
  from .config import Config
46
37
  from ._abstract_store import HasState, HandlesActions
47
38
 
@@ -193,10 +184,14 @@ class AddressableAreaStore(HasState[AddressableAreaState], HandlesActions):
193
184
 
194
185
  def handle_action(self, action: Action) -> None:
195
186
  """Modify state in reaction to an action."""
196
- if isinstance(action, SucceedCommandAction):
197
- self._handle_command(action.command)
198
- elif isinstance(action, AddAddressableAreaAction):
199
- self._check_location_is_addressable_area(action.addressable_area)
187
+ for state_update in get_state_updates(action):
188
+ if state_update.addressable_area_used != update_types.NO_CHANGE:
189
+ self._add_addressable_area(
190
+ state_update.addressable_area_used.addressable_area_name
191
+ )
192
+
193
+ if isinstance(action, AddAddressableAreaAction):
194
+ self._add_addressable_area(action.addressable_area_name)
200
195
  elif isinstance(action, SetDeckConfigurationAction):
201
196
  current_state = self._state
202
197
  if (
@@ -211,28 +206,6 @@ class AddressableAreaStore(HasState[AddressableAreaState], HandlesActions):
211
206
  )
212
207
  )
213
208
 
214
- def _handle_command(self, command: Command) -> None:
215
- """Modify state in reaction to a command."""
216
- if isinstance(command.result, LoadLabwareResult):
217
- location = command.params.location
218
- if isinstance(location, (DeckSlotLocation, AddressableAreaLocation)):
219
- self._check_location_is_addressable_area(location)
220
-
221
- elif isinstance(command.result, MoveLabwareResult):
222
- location = command.params.newLocation
223
- if isinstance(location, (DeckSlotLocation, AddressableAreaLocation)):
224
- self._check_location_is_addressable_area(location)
225
-
226
- elif isinstance(command.result, LoadModuleResult):
227
- self._check_location_is_addressable_area(command.params.location)
228
-
229
- elif isinstance(
230
- command.result,
231
- (MoveToAddressableAreaResult, MoveToAddressableAreaForDropTipResult),
232
- ):
233
- addressable_area_name = command.params.addressableAreaName
234
- self._check_location_is_addressable_area(addressable_area_name)
235
-
236
209
  @staticmethod
237
210
  def _get_addressable_areas_from_deck_configuration(
238
211
  deck_config: DeckConfigurationType, deck_definition: DeckDefinitionV5
@@ -260,16 +233,7 @@ class AddressableAreaStore(HasState[AddressableAreaState], HandlesActions):
260
233
  )
261
234
  return {area.area_name: area for area in addressable_areas}
262
235
 
263
- def _check_location_is_addressable_area(
264
- self, location: Union[DeckSlotLocation, AddressableAreaLocation, str]
265
- ) -> None:
266
- if isinstance(location, DeckSlotLocation):
267
- addressable_area_name = location.slotName.id
268
- elif isinstance(location, AddressableAreaLocation):
269
- addressable_area_name = location.addressableAreaName
270
- else:
271
- addressable_area_name = location
272
-
236
+ def _add_addressable_area(self, addressable_area_name: str) -> None:
273
237
  if addressable_area_name not in self._state.loaded_addressable_areas_by_name:
274
238
  cutout_id = self._validate_addressable_area_for_simulation(
275
239
  addressable_area_name
@@ -323,7 +287,7 @@ class AddressableAreaStore(HasState[AddressableAreaState], HandlesActions):
323
287
  return cutout_id
324
288
 
325
289
 
326
- class AddressableAreaView(HasState[AddressableAreaState]):
290
+ class AddressableAreaView:
327
291
  """Read-only addressable area state view."""
328
292
 
329
293
  _state: AddressableAreaState
@@ -345,8 +309,8 @@ class AddressableAreaView(HasState[AddressableAreaState]):
345
309
  @cached_property
346
310
  def mount_offsets(self) -> Dict[str, Point]:
347
311
  """The left and right mount offsets of the robot."""
348
- left_offset = self.state.robot_definition["mountOffsets"]["left"]
349
- right_offset = self.state.robot_definition["mountOffsets"]["right"]
312
+ left_offset = self._state.robot_definition["mountOffsets"]["left"]
313
+ right_offset = self._state.robot_definition["mountOffsets"]["right"]
350
314
  return {
351
315
  "left": Point(x=left_offset[0], y=left_offset[1], z=left_offset[2]),
352
316
  "right": Point(x=right_offset[0], y=right_offset[1], z=right_offset[2]),
@@ -355,10 +319,10 @@ class AddressableAreaView(HasState[AddressableAreaState]):
355
319
  @cached_property
356
320
  def padding_offsets(self) -> Dict[str, float]:
357
321
  """The padding offsets to be applied to the deck extents of the robot."""
358
- rear_offset = self.state.robot_definition["paddingOffsets"]["rear"]
359
- front_offset = self.state.robot_definition["paddingOffsets"]["front"]
360
- left_side_offset = self.state.robot_definition["paddingOffsets"]["leftSide"]
361
- right_side_offset = self.state.robot_definition["paddingOffsets"]["rightSide"]
322
+ rear_offset = self._state.robot_definition["paddingOffsets"]["rear"]
323
+ front_offset = self._state.robot_definition["paddingOffsets"]["front"]
324
+ left_side_offset = self._state.robot_definition["paddingOffsets"]["leftSide"]
325
+ right_side_offset = self._state.robot_definition["paddingOffsets"]["rightSide"]
362
326
  return {
363
327
  "rear": rear_offset,
364
328
  "front": front_offset,
@@ -420,12 +384,12 @@ class AddressableAreaView(HasState[AddressableAreaState]):
420
384
  _get_conflicting_addressable_areas_error_string(
421
385
  self._state.potential_cutout_fixtures_by_cutout_id[cutout_id],
422
386
  self._state.loaded_addressable_areas_by_name,
423
- self.state.deck_definition,
387
+ self._state.deck_definition,
424
388
  )
425
389
  )
426
390
  area_display_name = (
427
391
  deck_configuration_provider.get_addressable_area_display_name(
428
- area_name, self.state.deck_definition
392
+ area_name, self._state.deck_definition
429
393
  )
430
394
  )
431
395
  raise IncompatibleAddressableAreaError(
@@ -504,7 +468,7 @@ class AddressableAreaView(HasState[AddressableAreaState]):
504
468
  addressable_area_name: str,
505
469
  ) -> Point:
506
470
  """Get the offset form cutout fixture of an addressable area."""
507
- for addressable_area in self.state.deck_definition["locations"][
471
+ for addressable_area in self._state.deck_definition["locations"][
508
472
  "addressableAreas"
509
473
  ]:
510
474
  if addressable_area["id"] == addressable_area_name:
@@ -568,7 +532,7 @@ class AddressableAreaView(HasState[AddressableAreaState]):
568
532
  self, slot_name: DeckSlotName
569
533
  ) -> Optional[CutoutFixture]:
570
534
  """Get the Cutout Fixture currently loaded where a specific Deck Slot would be."""
571
- deck_config = self.state.deck_configuration
535
+ deck_config = self._state.deck_configuration
572
536
  if deck_config:
573
537
  slot_cutout_id = DECK_SLOT_TO_CUTOUT_MAP[slot_name]
574
538
  slot_cutout_fixture = None
@@ -581,7 +545,7 @@ class AddressableAreaView(HasState[AddressableAreaState]):
581
545
  if cutout_id == slot_cutout_id:
582
546
  slot_cutout_fixture = (
583
547
  deck_configuration_provider.get_cutout_fixture(
584
- cutout_fixture_id, self.state.deck_definition
548
+ cutout_fixture_id, self._state.deck_definition
585
549
  )
586
550
  )
587
551
  return slot_cutout_fixture
@@ -605,7 +569,7 @@ class AddressableAreaView(HasState[AddressableAreaState]):
605
569
  self, slot_name: DeckSlotName
606
570
  ) -> Optional[str]:
607
571
  """Get the serial number provided by the deck configuration for a Fixture at a given location."""
608
- deck_config = self.state.deck_configuration
572
+ deck_config = self._state.deck_configuration
609
573
  if deck_config:
610
574
  slot_cutout_id = DECK_SLOT_TO_CUTOUT_MAP[slot_name]
611
575
  # This will only ever be one under current assumptions
@@ -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_failed_command_ids: List[str]
28
+ """All failed command IDs, in insertion order."""
29
+
27
30
  _all_command_ids_but_fixit_command_ids: List[str]
28
31
  """All command IDs besides fixit command intents, in insertion order."""
29
32
 
@@ -47,6 +50,7 @@ class CommandHistory:
47
50
 
48
51
  def __init__(self) -> None:
49
52
  self._all_command_ids = []
53
+ self._all_failed_command_ids = []
50
54
  self._all_command_ids_but_fixit_command_ids = []
51
55
  self._queued_command_ids = OrderedSet()
52
56
  self._queued_setup_command_ids = OrderedSet()
@@ -101,6 +105,13 @@ class CommandHistory:
101
105
  for command_id in self._all_command_ids
102
106
  ]
103
107
 
108
+ def get_all_failed_commands(self) -> List[Command]:
109
+ """Get all failed commands."""
110
+ return [
111
+ self._commands_by_id[command_id].command
112
+ for command_id in self._all_failed_command_ids
113
+ ]
114
+
104
115
  def get_filtered_command_ids(self, include_fixit_commands: bool) -> List[str]:
105
116
  """Get all fixit command IDs."""
106
117
  if include_fixit_commands:
@@ -242,6 +253,7 @@ class CommandHistory:
242
253
  self._remove_queue_id(command.id)
243
254
  self._remove_setup_queue_id(command.id)
244
255
  self._set_most_recently_completed_command_id(command.id)
256
+ self._all_failed_command_ids.append(command.id)
245
257
 
246
258
  def _add(self, command_id: str, command_entry: CommandEntry) -> None:
247
259
  """Create or update a command entry."""
@@ -1,4 +1,5 @@
1
1
  """Protocol engine commands sub-state."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import enum
@@ -228,9 +229,6 @@ class CommandState:
228
229
  This value can be used to generate future hashes.
229
230
  """
230
231
 
231
- failed_command_errors: List[ErrorOccurrence]
232
- """List of command errors that occurred during run execution."""
233
-
234
232
  has_entered_error_recovery: bool
235
233
  """Whether the run has entered error recovery."""
236
234
 
@@ -269,7 +267,6 @@ class CommandStore(HasState[CommandState], HandlesActions):
269
267
  run_started_at=None,
270
268
  latest_protocol_command_hash=None,
271
269
  stopped_by_estop=False,
272
- failed_command_errors=[],
273
270
  error_recovery_policy=error_recovery_policy,
274
271
  has_entered_error_recovery=False,
275
272
  )
@@ -308,7 +305,7 @@ class CommandStore(HasState[CommandState], HandlesActions):
308
305
  # TODO(mc, 2021-06-22): mypy has trouble with this automatic
309
306
  # request > command mapping, figure out how to type precisely
310
307
  # (or wait for a future mypy version that can figure it out).
311
- queued_command = action.request._CommandCls.construct(
308
+ queued_command = action.request._CommandCls.model_construct(
312
309
  id=action.command_id,
313
310
  key=(
314
311
  action.request.key
@@ -330,7 +327,7 @@ class CommandStore(HasState[CommandState], HandlesActions):
330
327
  def _handle_run_command_action(self, action: RunCommandAction) -> None:
331
328
  prev_entry = self._state.command_history.get(action.command_id)
332
329
 
333
- running_command = prev_entry.command.copy(
330
+ running_command = prev_entry.command.model_copy(
334
331
  update={
335
332
  "status": CommandStatus.RUNNING,
336
333
  "startedAt": action.started_at,
@@ -366,7 +363,6 @@ class CommandStore(HasState[CommandState], HandlesActions):
366
363
  notes=action.notes,
367
364
  )
368
365
  self._state.failed_command = self._state.command_history.get(action.command_id)
369
- self._state.failed_command_errors.append(public_error_occurrence)
370
366
 
371
367
  if (
372
368
  prev_entry.command.intent in (CommandIntent.PROTOCOL, None)
@@ -511,7 +507,10 @@ class CommandStore(HasState[CommandState], HandlesActions):
511
507
  pass
512
508
  case QueueStatus.RUNNING | QueueStatus.PAUSED:
513
509
  self._state.queue_status = QueueStatus.PAUSED
514
- case QueueStatus.AWAITING_RECOVERY | QueueStatus.AWAITING_RECOVERY_PAUSED:
510
+ case (
511
+ QueueStatus.AWAITING_RECOVERY
512
+ | QueueStatus.AWAITING_RECOVERY_PAUSED
513
+ ):
515
514
  self._state.queue_status = QueueStatus.AWAITING_RECOVERY_PAUSED
516
515
  elif action.door_state == DoorState.CLOSED:
517
516
  self._state.is_door_blocking = False
@@ -530,7 +529,7 @@ class CommandStore(HasState[CommandState], HandlesActions):
530
529
  notes: Optional[List[CommandNote]],
531
530
  ) -> None:
532
531
  prev_entry = self._state.command_history.get(command_id)
533
- failed_command = prev_entry.command.copy(
532
+ failed_command = prev_entry.command.model_copy(
534
533
  update={
535
534
  "completedAt": failed_at,
536
535
  "status": CommandStatus.FAILED,
@@ -584,7 +583,7 @@ class CommandStore(HasState[CommandState], HandlesActions):
584
583
  )
585
584
 
586
585
 
587
- class CommandView(HasState[CommandState]):
586
+ class CommandView:
588
587
  """Read-only command state view."""
589
588
 
590
589
  _state: CommandState
@@ -684,7 +683,7 @@ class CommandView(HasState[CommandState]):
684
683
  finish_error = self._state.finish_error
685
684
 
686
685
  if run_error and finish_error:
687
- combined_error = ErrorOccurrence.construct(
686
+ combined_error = ErrorOccurrence(
688
687
  id=finish_error.id,
689
688
  createdAt=finish_error.createdAt,
690
689
  errorType="RunAndFinishFailed",
@@ -706,7 +705,12 @@ class CommandView(HasState[CommandState]):
706
705
 
707
706
  def get_all_errors(self) -> List[ErrorOccurrence]:
708
707
  """Get the run's full error list, if there was none, returns an empty list."""
709
- return self._state.failed_command_errors
708
+ failed_commands = self._state.command_history.get_all_failed_commands()
709
+ return [
710
+ command_error.error
711
+ for command_error in failed_commands
712
+ if command_error.error is not None
713
+ ]
710
714
 
711
715
  def get_has_entered_recovery_mode(self) -> bool:
712
716
  """Get whether the run has entered recovery mode."""
@@ -916,7 +920,7 @@ class CommandView(HasState[CommandState]):
916
920
  fatal error of the overall run coming from anywhere in the Python script,
917
921
  including in between commands.
918
922
  """
919
- failed_command = self.state.failed_command
923
+ failed_command = self._state.failed_command
920
924
  if (
921
925
  failed_command
922
926
  and failed_command.command.error
@@ -932,12 +936,16 @@ class CommandView(HasState[CommandState]):
932
936
 
933
937
  The command ID is assumed to point to a failed command.
934
938
  """
935
- return self.state.command_error_recovery_types[command_id]
939
+ return self._state.command_error_recovery_types[command_id]
936
940
 
937
941
  def get_is_stopped(self) -> bool:
938
942
  """Get whether an engine stop has completed."""
939
943
  return self._state.run_completed_at is not None
940
944
 
945
+ def get_is_stopped_by_estop(self) -> bool:
946
+ """Return whether the engine was stopped specifically by an E-stop."""
947
+ return self._state.stopped_by_estop
948
+
941
949
  def has_been_played(self) -> bool:
942
950
  """Get whether engine has started."""
943
951
  return self._state.run_started_at is not None
@@ -2,12 +2,11 @@
2
2
  from dataclasses import dataclass
3
3
  from typing import List
4
4
 
5
+ from opentrons.protocol_engine.actions.get_state_update import get_state_updates
6
+ from opentrons.protocol_engine.state import update_types
7
+
5
8
  from ._abstract_store import HasState, HandlesActions
6
- from ..actions import Action, SucceedCommandAction
7
- from ..commands import (
8
- Command,
9
- absorbance_reader,
10
- )
9
+ from ..actions import Action
11
10
 
12
11
 
13
12
  @dataclass
@@ -28,16 +27,15 @@ class FileStore(HasState[FileState], HandlesActions):
28
27
 
29
28
  def handle_action(self, action: Action) -> None:
30
29
  """Modify state in reaction to an action."""
31
- if isinstance(action, SucceedCommandAction):
32
- self._handle_command(action.command)
30
+ for state_update in get_state_updates(action):
31
+ self._handle_state_update(state_update)
33
32
 
34
- def _handle_command(self, command: Command) -> None:
35
- if isinstance(command.result, absorbance_reader.ReadAbsorbanceResult):
36
- if command.result.fileIds is not None:
37
- self._state.file_ids.extend(command.result.fileIds)
33
+ def _handle_state_update(self, state_update: update_types.StateUpdate) -> None:
34
+ if state_update.files_added != update_types.NO_CHANGE:
35
+ self._state.file_ids.extend(state_update.files_added.file_ids)
38
36
 
39
37
 
40
- class FileView(HasState[FileState]):
38
+ class FileView:
41
39
  """Read-only engine created file state view."""
42
40
 
43
41
  _state: FileState
@@ -0,0 +1,138 @@
1
+ """Implements fluid stack tracking for pipettes.
2
+
3
+ Inside a pipette's tip, there can be a mix of kinds of fluids - here, "fluid" means "liquid" (i.e. a protocol-relevant
4
+ working liquid that is aspirated or dispensed from wells) or "air" (i.e. because there was an air gap). Since sometimes
5
+ you want air gaps in different places - physically-below liquid to prevent dripping, physically-above liquid to provide
6
+ extra room to push the plunger - we need to support some notion of at least phsyical ordinal position of air and liquid,
7
+ and we do so as a logical stack because that's physically relevant.
8
+ """
9
+ from logging import getLogger
10
+ from numpy import isclose
11
+ from ..types import AspiratedFluid, FluidKind
12
+
13
+ _LOG = getLogger(__name__)
14
+
15
+
16
+ class FluidStack:
17
+ """A FluidStack data structure is a list of AspiratedFluids, with stack-style (last-in-first-out) ordering.
18
+
19
+ The front of the list is the physical-top of the liquid stack (logical-bottom of the stack data structure)
20
+ and the back of the list is the physical-bottom of the liquid stack (logical-top of the stack data structure).
21
+ The state is internal and the interaction surface is the methods. This is a mutating API.
22
+ """
23
+
24
+ _FluidStack = list[AspiratedFluid]
25
+
26
+ _fluid_stack: _FluidStack
27
+
28
+ def __init__(self, _fluid_stack: _FluidStack | None = None) -> None:
29
+ """Build a FluidStack.
30
+
31
+ The argument is provided for testing and shouldn't be generally used.
32
+ """
33
+ self._fluid_stack = _fluid_stack or []
34
+
35
+ def add_fluid(self, new: AspiratedFluid) -> None:
36
+ """Add fluid to a stack.
37
+
38
+ If the new fluid is of a different kind than what's on the physical-bottom of the stack, add a new record.
39
+ If the new fluid is of the same kind as what's on the physical-bottom of the stack, add the new volume to
40
+ the same record.
41
+ """
42
+ if len(self._fluid_stack) == 0 or self._fluid_stack[-1].kind != new.kind:
43
+ # this is a new kind of fluid, append the record
44
+ self._fluid_stack.append(new)
45
+ else:
46
+ # this is more of the same kind of fluid, add the volumes
47
+ old_fluid = self._fluid_stack.pop(-1)
48
+ self._fluid_stack.append(
49
+ AspiratedFluid(kind=new.kind, volume=old_fluid.volume + new.volume)
50
+ )
51
+
52
+ def _alter_fluid_records(
53
+ self, remove: int, new_last: AspiratedFluid | None
54
+ ) -> None:
55
+ if remove >= len(self._fluid_stack) or len(self._fluid_stack) == 0:
56
+ self._fluid_stack = []
57
+ return
58
+ if remove != 0:
59
+ removed = self._fluid_stack[:-remove]
60
+ else:
61
+ removed = self._fluid_stack
62
+ if new_last:
63
+ removed[-1] = new_last
64
+ self._fluid_stack = removed
65
+
66
+ def remove_fluid(self, volume: float) -> None:
67
+ """Remove a specific amount of fluid from the physical-bottom of the stack.
68
+
69
+ This will consume records that are wholly included in the provided volume and alter the remaining
70
+ final records (if any) to decrement the amount of volume removed from it.
71
+
72
+ This function is designed to be used inside pipette store action handlers, which are generally not
73
+ exception-safe, and therefore swallows and logs errors.
74
+ """
75
+ self._fluid_stack_iterator = reversed(self._fluid_stack)
76
+ removed_elements: list[AspiratedFluid] = []
77
+ while volume > 0:
78
+ try:
79
+ last_stack_element = next(self._fluid_stack_iterator)
80
+ except StopIteration:
81
+ _LOG.error(
82
+ f"Attempting to remove more fluid than present, {volume}uL left over"
83
+ )
84
+ self._alter_fluid_records(len(removed_elements), None)
85
+ return
86
+ if last_stack_element.volume < volume:
87
+ removed_elements.append(last_stack_element)
88
+ volume -= last_stack_element.volume
89
+ elif isclose(last_stack_element.volume, volume):
90
+ self._alter_fluid_records(len(removed_elements) + 1, None)
91
+ return
92
+ else:
93
+ self._alter_fluid_records(
94
+ len(removed_elements),
95
+ AspiratedFluid(
96
+ kind=last_stack_element.kind,
97
+ volume=last_stack_element.volume - volume,
98
+ ),
99
+ )
100
+ return
101
+
102
+ _LOG.error(f"Failed to handle removing {volume}uL from {self._fluid_stack}")
103
+
104
+ def aspirated_volume(self, kind: FluidKind | None = None) -> float:
105
+ """Measure the total amount of fluid (optionally filtered by kind) in the stack."""
106
+ volume = 0.0
107
+ for el in self._fluid_stack:
108
+ if kind is not None and el.kind != kind:
109
+ continue
110
+ volume += el.volume
111
+ return volume
112
+
113
+ def liquid_part_of_dispense_volume(self, volume: float) -> float:
114
+ """Get the amount of liquid in the specified volume starting at the physical-bottom of the stack."""
115
+ liquid_volume = 0.0
116
+ for el in reversed(self._fluid_stack):
117
+ if el.kind == FluidKind.LIQUID:
118
+ liquid_volume += min(volume, el.volume)
119
+ volume -= min(el.volume, volume)
120
+ if isclose(volume, 0.0):
121
+ return liquid_volume
122
+ return liquid_volume
123
+
124
+ def __eq__(self, other: object) -> bool:
125
+ """Equality."""
126
+ if isinstance(other, type(self)):
127
+ return other._fluid_stack == self._fluid_stack
128
+ return False
129
+
130
+ def __repr__(self) -> str:
131
+ """String representation of a fluid stack."""
132
+ if self._fluid_stack:
133
+ stringified_stack = (
134
+ f'(top) {", ".join([str(item) for item in self._fluid_stack])} (bottom)'
135
+ )
136
+ else:
137
+ stringified_stack = "empty"
138
+ return f"<{self.__class__.__name__}: {stringified_stack}>"