opentrons 8.3.0a0__py2.py3-none-any.whl → 8.3.0a2__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 (229) 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/drivers/asyncio/communication/__init__.py +2 -0
  9. opentrons/drivers/asyncio/communication/errors.py +16 -3
  10. opentrons/drivers/asyncio/communication/serial_connection.py +24 -9
  11. opentrons/drivers/command_builder.py +2 -2
  12. opentrons/drivers/flex_stacker/__init__.py +9 -0
  13. opentrons/drivers/flex_stacker/abstract.py +89 -0
  14. opentrons/drivers/flex_stacker/driver.py +260 -0
  15. opentrons/drivers/flex_stacker/simulator.py +109 -0
  16. opentrons/drivers/flex_stacker/types.py +138 -0
  17. opentrons/drivers/heater_shaker/driver.py +18 -3
  18. opentrons/drivers/temp_deck/driver.py +13 -3
  19. opentrons/drivers/thermocycler/driver.py +17 -3
  20. opentrons/execute.py +3 -1
  21. opentrons/hardware_control/__init__.py +1 -2
  22. opentrons/hardware_control/api.py +28 -20
  23. opentrons/hardware_control/backends/flex_protocol.py +4 -6
  24. opentrons/hardware_control/backends/ot3controller.py +177 -59
  25. opentrons/hardware_control/backends/ot3simulator.py +10 -8
  26. opentrons/hardware_control/backends/ot3utils.py +3 -13
  27. opentrons/hardware_control/dev_types.py +2 -0
  28. opentrons/hardware_control/emulation/heater_shaker.py +4 -0
  29. opentrons/hardware_control/emulation/module_server/client.py +1 -1
  30. opentrons/hardware_control/emulation/module_server/server.py +5 -3
  31. opentrons/hardware_control/emulation/settings.py +3 -4
  32. opentrons/hardware_control/instruments/ot2/instrument_calibration.py +2 -1
  33. opentrons/hardware_control/instruments/ot2/pipette.py +9 -21
  34. opentrons/hardware_control/instruments/ot2/pipette_handler.py +8 -1
  35. opentrons/hardware_control/instruments/ot3/gripper.py +2 -2
  36. opentrons/hardware_control/instruments/ot3/pipette.py +13 -22
  37. opentrons/hardware_control/instruments/ot3/pipette_handler.py +10 -1
  38. opentrons/hardware_control/modules/mod_abc.py +2 -2
  39. opentrons/hardware_control/motion_utilities.py +68 -0
  40. opentrons/hardware_control/nozzle_manager.py +39 -41
  41. opentrons/hardware_control/ot3_calibration.py +1 -1
  42. opentrons/hardware_control/ot3api.py +34 -22
  43. opentrons/hardware_control/protocols/gripper_controller.py +3 -0
  44. opentrons/hardware_control/protocols/hardware_manager.py +5 -1
  45. opentrons/hardware_control/protocols/liquid_handler.py +18 -0
  46. opentrons/hardware_control/protocols/motion_controller.py +6 -0
  47. opentrons/hardware_control/robot_calibration.py +1 -1
  48. opentrons/hardware_control/types.py +61 -0
  49. opentrons/protocol_api/__init__.py +20 -1
  50. opentrons/protocol_api/_liquid.py +24 -49
  51. opentrons/protocol_api/_liquid_properties.py +754 -0
  52. opentrons/protocol_api/_types.py +24 -0
  53. opentrons/protocol_api/core/common.py +2 -0
  54. opentrons/protocol_api/core/engine/instrument.py +67 -10
  55. opentrons/protocol_api/core/engine/labware.py +29 -7
  56. opentrons/protocol_api/core/engine/protocol.py +130 -5
  57. opentrons/protocol_api/core/engine/robot.py +139 -0
  58. opentrons/protocol_api/core/engine/well.py +4 -1
  59. opentrons/protocol_api/core/instrument.py +42 -4
  60. opentrons/protocol_api/core/labware.py +13 -4
  61. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +34 -3
  62. opentrons/protocol_api/core/legacy/legacy_labware_core.py +13 -4
  63. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +32 -1
  64. opentrons/protocol_api/core/legacy/legacy_robot_core.py +0 -0
  65. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +34 -3
  66. opentrons/protocol_api/core/protocol.py +34 -1
  67. opentrons/protocol_api/core/robot.py +51 -0
  68. opentrons/protocol_api/instrument_context.py +145 -43
  69. opentrons/protocol_api/labware.py +231 -7
  70. opentrons/protocol_api/module_contexts.py +21 -17
  71. opentrons/protocol_api/protocol_context.py +125 -4
  72. opentrons/protocol_api/robot_context.py +204 -32
  73. opentrons/protocol_api/validation.py +261 -3
  74. opentrons/protocol_engine/__init__.py +4 -0
  75. opentrons/protocol_engine/actions/actions.py +2 -3
  76. opentrons/protocol_engine/clients/sync_client.py +18 -0
  77. opentrons/protocol_engine/commands/__init__.py +81 -0
  78. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +0 -2
  79. opentrons/protocol_engine/commands/absorbance_reader/initialize.py +19 -5
  80. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +0 -1
  81. opentrons/protocol_engine/commands/absorbance_reader/read.py +32 -9
  82. opentrons/protocol_engine/commands/air_gap_in_place.py +160 -0
  83. opentrons/protocol_engine/commands/aspirate.py +103 -53
  84. opentrons/protocol_engine/commands/aspirate_in_place.py +55 -51
  85. opentrons/protocol_engine/commands/blow_out.py +44 -39
  86. opentrons/protocol_engine/commands/blow_out_in_place.py +21 -32
  87. opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +13 -6
  88. opentrons/protocol_engine/commands/calibration/calibrate_module.py +1 -1
  89. opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +3 -3
  90. opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +1 -1
  91. opentrons/protocol_engine/commands/command.py +73 -66
  92. opentrons/protocol_engine/commands/command_unions.py +101 -1
  93. opentrons/protocol_engine/commands/comment.py +1 -1
  94. opentrons/protocol_engine/commands/configure_for_volume.py +10 -3
  95. opentrons/protocol_engine/commands/configure_nozzle_layout.py +6 -4
  96. opentrons/protocol_engine/commands/custom.py +6 -12
  97. opentrons/protocol_engine/commands/dispense.py +82 -48
  98. opentrons/protocol_engine/commands/dispense_in_place.py +71 -51
  99. opentrons/protocol_engine/commands/drop_tip.py +52 -31
  100. opentrons/protocol_engine/commands/drop_tip_in_place.py +13 -3
  101. opentrons/protocol_engine/commands/generate_command_schema.py +4 -11
  102. opentrons/protocol_engine/commands/get_next_tip.py +134 -0
  103. opentrons/protocol_engine/commands/get_tip_presence.py +1 -1
  104. opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +1 -1
  105. opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +1 -1
  106. opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +1 -1
  107. opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +1 -1
  108. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +1 -1
  109. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +1 -1
  110. opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +10 -4
  111. opentrons/protocol_engine/commands/home.py +13 -4
  112. opentrons/protocol_engine/commands/liquid_probe.py +60 -25
  113. opentrons/protocol_engine/commands/load_labware.py +29 -7
  114. opentrons/protocol_engine/commands/load_lid.py +146 -0
  115. opentrons/protocol_engine/commands/load_lid_stack.py +189 -0
  116. opentrons/protocol_engine/commands/load_liquid.py +12 -4
  117. opentrons/protocol_engine/commands/load_liquid_class.py +144 -0
  118. opentrons/protocol_engine/commands/load_module.py +31 -10
  119. opentrons/protocol_engine/commands/load_pipette.py +19 -8
  120. opentrons/protocol_engine/commands/magnetic_module/disengage.py +1 -1
  121. opentrons/protocol_engine/commands/magnetic_module/engage.py +1 -1
  122. opentrons/protocol_engine/commands/move_labware.py +19 -6
  123. opentrons/protocol_engine/commands/move_relative.py +35 -25
  124. opentrons/protocol_engine/commands/move_to_addressable_area.py +40 -27
  125. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +53 -32
  126. opentrons/protocol_engine/commands/move_to_coordinates.py +36 -22
  127. opentrons/protocol_engine/commands/move_to_well.py +40 -24
  128. opentrons/protocol_engine/commands/movement_common.py +338 -0
  129. opentrons/protocol_engine/commands/pick_up_tip.py +49 -27
  130. opentrons/protocol_engine/commands/pipetting_common.py +169 -87
  131. opentrons/protocol_engine/commands/prepare_to_aspirate.py +24 -33
  132. opentrons/protocol_engine/commands/reload_labware.py +1 -1
  133. opentrons/protocol_engine/commands/retract_axis.py +1 -1
  134. opentrons/protocol_engine/commands/robot/__init__.py +69 -0
  135. opentrons/protocol_engine/commands/robot/close_gripper_jaw.py +86 -0
  136. opentrons/protocol_engine/commands/robot/common.py +18 -0
  137. opentrons/protocol_engine/commands/robot/move_axes_relative.py +101 -0
  138. opentrons/protocol_engine/commands/robot/move_axes_to.py +100 -0
  139. opentrons/protocol_engine/commands/robot/move_to.py +94 -0
  140. opentrons/protocol_engine/commands/robot/open_gripper_jaw.py +77 -0
  141. opentrons/protocol_engine/commands/save_position.py +14 -5
  142. opentrons/protocol_engine/commands/set_rail_lights.py +1 -1
  143. opentrons/protocol_engine/commands/set_status_bar.py +1 -1
  144. opentrons/protocol_engine/commands/temperature_module/deactivate.py +1 -1
  145. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +1 -1
  146. opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +10 -4
  147. opentrons/protocol_engine/commands/thermocycler/close_lid.py +1 -1
  148. opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +1 -1
  149. opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +1 -1
  150. opentrons/protocol_engine/commands/thermocycler/open_lid.py +1 -1
  151. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +8 -2
  152. opentrons/protocol_engine/commands/thermocycler/run_profile.py +9 -3
  153. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +11 -4
  154. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +1 -1
  155. opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +1 -1
  156. opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +1 -1
  157. opentrons/protocol_engine/commands/touch_tip.py +65 -16
  158. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +4 -1
  159. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +12 -3
  160. opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +1 -4
  161. opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +1 -4
  162. opentrons/protocol_engine/commands/verify_tip_presence.py +11 -4
  163. opentrons/protocol_engine/commands/wait_for_duration.py +10 -3
  164. opentrons/protocol_engine/commands/wait_for_resume.py +10 -3
  165. opentrons/protocol_engine/errors/__init__.py +8 -0
  166. opentrons/protocol_engine/errors/error_occurrence.py +19 -20
  167. opentrons/protocol_engine/errors/exceptions.py +50 -0
  168. opentrons/protocol_engine/execution/command_executor.py +1 -1
  169. opentrons/protocol_engine/execution/equipment.py +73 -5
  170. opentrons/protocol_engine/execution/gantry_mover.py +364 -8
  171. opentrons/protocol_engine/execution/movement.py +27 -0
  172. opentrons/protocol_engine/execution/pipetting.py +5 -1
  173. opentrons/protocol_engine/execution/tip_handler.py +4 -6
  174. opentrons/protocol_engine/notes/notes.py +1 -1
  175. opentrons/protocol_engine/protocol_engine.py +7 -6
  176. opentrons/protocol_engine/resources/labware_data_provider.py +1 -1
  177. opentrons/protocol_engine/resources/labware_validation.py +5 -0
  178. opentrons/protocol_engine/resources/module_data_provider.py +1 -1
  179. opentrons/protocol_engine/resources/pipette_data_provider.py +12 -0
  180. opentrons/protocol_engine/slot_standardization.py +9 -9
  181. opentrons/protocol_engine/state/_move_types.py +9 -5
  182. opentrons/protocol_engine/state/_well_math.py +193 -0
  183. opentrons/protocol_engine/state/addressable_areas.py +25 -61
  184. opentrons/protocol_engine/state/command_history.py +12 -0
  185. opentrons/protocol_engine/state/commands.py +17 -13
  186. opentrons/protocol_engine/state/files.py +10 -12
  187. opentrons/protocol_engine/state/fluid_stack.py +138 -0
  188. opentrons/protocol_engine/state/frustum_helpers.py +57 -32
  189. opentrons/protocol_engine/state/geometry.py +47 -1
  190. opentrons/protocol_engine/state/labware.py +79 -25
  191. opentrons/protocol_engine/state/liquid_classes.py +82 -0
  192. opentrons/protocol_engine/state/liquids.py +16 -4
  193. opentrons/protocol_engine/state/modules.py +52 -70
  194. opentrons/protocol_engine/state/motion.py +6 -1
  195. opentrons/protocol_engine/state/pipettes.py +135 -58
  196. opentrons/protocol_engine/state/state.py +21 -2
  197. opentrons/protocol_engine/state/state_summary.py +4 -2
  198. opentrons/protocol_engine/state/tips.py +11 -44
  199. opentrons/protocol_engine/state/update_types.py +343 -48
  200. opentrons/protocol_engine/state/wells.py +19 -11
  201. opentrons/protocol_engine/types.py +176 -28
  202. opentrons/protocol_reader/extract_labware_definitions.py +5 -2
  203. opentrons/protocol_reader/file_format_validator.py +5 -5
  204. opentrons/protocol_runner/json_file_reader.py +9 -3
  205. opentrons/protocol_runner/json_translator.py +51 -25
  206. opentrons/protocol_runner/legacy_command_mapper.py +66 -64
  207. opentrons/protocol_runner/protocol_runner.py +35 -4
  208. opentrons/protocol_runner/python_protocol_wrappers.py +1 -1
  209. opentrons/protocol_runner/run_orchestrator.py +13 -3
  210. opentrons/protocols/advanced_control/common.py +38 -0
  211. opentrons/protocols/advanced_control/mix.py +1 -1
  212. opentrons/protocols/advanced_control/transfers/__init__.py +0 -0
  213. opentrons/protocols/advanced_control/transfers/common.py +56 -0
  214. opentrons/protocols/advanced_control/{transfers.py → transfers/transfer.py} +10 -85
  215. opentrons/protocols/api_support/definitions.py +1 -1
  216. opentrons/protocols/api_support/instrument.py +1 -1
  217. opentrons/protocols/api_support/util.py +10 -0
  218. opentrons/protocols/labware.py +70 -8
  219. opentrons/protocols/models/json_protocol.py +5 -9
  220. opentrons/simulate.py +3 -1
  221. opentrons/types.py +162 -2
  222. opentrons/util/entrypoint_util.py +2 -5
  223. opentrons/util/logging_config.py +1 -1
  224. {opentrons-8.3.0a0.dist-info → opentrons-8.3.0a2.dist-info}/METADATA +16 -15
  225. {opentrons-8.3.0a0.dist-info → opentrons-8.3.0a2.dist-info}/RECORD +229 -202
  226. {opentrons-8.3.0a0.dist-info → opentrons-8.3.0a2.dist-info}/WHEEL +1 -1
  227. {opentrons-8.3.0a0.dist-info → opentrons-8.3.0a2.dist-info}/LICENSE +0 -0
  228. {opentrons-8.3.0a0.dist-info → opentrons-8.3.0a2.dist-info}/entry_points.txt +0 -0
  229. {opentrons-8.3.0a0.dist-info → opentrons-8.3.0a2.dist-info}/top_level.txt +0 -0
@@ -30,7 +30,6 @@ from .types import (
30
30
  HexColor,
31
31
  PostRunHardwareState,
32
32
  DeckConfigurationType,
33
- AddressableAreaLocation,
34
33
  )
35
34
  from .execution import (
36
35
  QueueWorker,
@@ -427,7 +426,7 @@ class ProtocolEngine:
427
426
  post_run_hardware_state: The state in which to leave the gantry and motors in
428
427
  after the run is over.
429
428
  """
430
- if self._state_store.commands.state.stopped_by_estop:
429
+ if self._state_store.commands.get_is_stopped_by_estop():
431
430
  # This handles the case where the E-stop was pressed while we were *not* in the middle
432
431
  # of some hardware interaction that would raise it as an exception. For example, imagine
433
432
  # we were paused between two commands, or imagine we were executing a waitForDuration.
@@ -565,15 +564,17 @@ class ProtocolEngine:
565
564
  description=(description or ""),
566
565
  displayColor=color,
567
566
  )
567
+ validated_liquid = self._state_store.liquid.validate_liquid_allowed(
568
+ liquid=liquid
569
+ )
568
570
 
569
- self._action_dispatcher.dispatch(AddLiquidAction(liquid=liquid))
570
- return liquid
571
+ self._action_dispatcher.dispatch(AddLiquidAction(liquid=validated_liquid))
572
+ return validated_liquid
571
573
 
572
574
  def add_addressable_area(self, addressable_area_name: str) -> None:
573
575
  """Add an addressable area to state."""
574
- area = AddressableAreaLocation(addressableAreaName=addressable_area_name)
575
576
  self._action_dispatcher.dispatch(
576
- AddAddressableAreaAction(addressable_area=area)
577
+ AddAddressableAreaAction(addressable_area_name)
577
578
  )
578
579
 
579
580
  def reset_tips(self, labware_id: str) -> None:
@@ -44,7 +44,7 @@ class LabwareDataProvider:
44
44
  def _get_labware_definition_sync(
45
45
  load_name: str, namespace: str, version: int
46
46
  ) -> LabwareDefinition:
47
- return LabwareDefinition.parse_obj(
47
+ return LabwareDefinition.model_validate(
48
48
  get_labware_definition(load_name, namespace, version)
49
49
  )
50
50
 
@@ -32,6 +32,11 @@ def validate_definition_is_lid(definition: LabwareDefinition) -> bool:
32
32
  return LabwareRole.lid in definition.allowedRoles
33
33
 
34
34
 
35
+ def validate_definition_is_system(definition: LabwareDefinition) -> bool:
36
+ """Validate that one of the definition's allowed roles is `system`."""
37
+ return LabwareRole.system in definition.allowedRoles
38
+
39
+
35
40
  def validate_labware_can_be_stacked(
36
41
  top_labware_definition: LabwareDefinition, below_labware_load_name: str
37
42
  ) -> bool:
@@ -22,7 +22,7 @@ class ModuleDataProvider:
22
22
  def get_definition(model: ModuleModel) -> ModuleDefinition:
23
23
  """Get the module definition."""
24
24
  data = load_definition(model_or_loadname=model.value, version="3")
25
- return ModuleDefinition.parse_obj(data)
25
+ return ModuleDefinition.model_validate(data)
26
26
 
27
27
  @staticmethod
28
28
  def load_module_calibrations() -> Dict[str, ModuleOffsetData]:
@@ -67,6 +67,8 @@ class LoadedStaticPipetteData:
67
67
  back_left_corner_offset: Point
68
68
  front_right_corner_offset: Point
69
69
  pipette_lld_settings: Optional[Dict[str, Dict[str, float]]]
70
+ plunger_positions: Dict[str, float]
71
+ shaft_ul_per_mm: float
70
72
  available_sensors: pipette_definition.AvailableSensorDefinition
71
73
 
72
74
 
@@ -258,6 +260,7 @@ class VirtualPipetteDataProvider:
258
260
 
259
261
  pip_back_left = config.pipette_bounding_box_offsets.back_left_corner
260
262
  pip_front_right = config.pipette_bounding_box_offsets.front_right_corner
263
+ plunger_positions = config.plunger_positions_configurations[liquid_class]
261
264
  return LoadedStaticPipetteData(
262
265
  model=str(pipette_model),
263
266
  display_name=config.display_name,
@@ -286,6 +289,13 @@ class VirtualPipetteDataProvider:
286
289
  pip_front_right[0], pip_front_right[1], pip_front_right[2]
287
290
  ),
288
291
  pipette_lld_settings=config.lld_settings,
292
+ plunger_positions={
293
+ "top": plunger_positions.top,
294
+ "bottom": plunger_positions.bottom,
295
+ "blow_out": plunger_positions.blow_out,
296
+ "drop_tip": plunger_positions.drop_tip,
297
+ },
298
+ shaft_ul_per_mm=config.shaft_ul_per_mm,
289
299
  available_sensors=config.available_sensors
290
300
  or pipette_definition.AvailableSensorDefinition(sensors=[]),
291
301
  )
@@ -340,6 +350,8 @@ def get_pipette_static_config(
340
350
  front_right_offset[0], front_right_offset[1], front_right_offset[2]
341
351
  ),
342
352
  pipette_lld_settings=pipette_dict["lld_settings"],
353
+ plunger_positions=pipette_dict["plunger_positions"],
354
+ shaft_ul_per_mm=pipette_dict["shaft_ul_per_mm"],
343
355
  available_sensors=available_sensors,
344
356
  )
345
357
 
@@ -35,9 +35,9 @@ def standardize_labware_offset(
35
35
  original: LabwareOffsetCreate, robot_type: RobotType
36
36
  ) -> LabwareOffsetCreate:
37
37
  """Convert the deck slot in the given `LabwareOffsetCreate` to match the given robot type."""
38
- return original.copy(
38
+ return original.model_copy(
39
39
  update={
40
- "location": original.location.copy(
40
+ "location": original.location.model_copy(
41
41
  update={
42
42
  "slotName": original.location.slotName.to_equivalent_for_robot_type(
43
43
  robot_type
@@ -70,40 +70,40 @@ def standardize_command(
70
70
  def _standardize_load_labware(
71
71
  original: commands.LoadLabwareCreate, robot_type: RobotType
72
72
  ) -> commands.LoadLabwareCreate:
73
- params = original.params.copy(
73
+ params = original.params.model_copy(
74
74
  update={
75
75
  "location": _standardize_labware_location(
76
76
  original.params.location, robot_type
77
77
  )
78
78
  }
79
79
  )
80
- return original.copy(update={"params": params})
80
+ return original.model_copy(update={"params": params})
81
81
 
82
82
 
83
83
  def _standardize_load_module(
84
84
  original: commands.LoadModuleCreate, robot_type: RobotType
85
85
  ) -> commands.LoadModuleCreate:
86
- params = original.params.copy(
86
+ params = original.params.model_copy(
87
87
  update={
88
88
  "location": _standardize_deck_slot_location(
89
89
  original.params.location, robot_type
90
90
  )
91
91
  }
92
92
  )
93
- return original.copy(update={"params": params})
93
+ return original.model_copy(update={"params": params})
94
94
 
95
95
 
96
96
  def _standardize_move_labware(
97
97
  original: commands.MoveLabwareCreate, robot_type: RobotType
98
98
  ) -> commands.MoveLabwareCreate:
99
- params = original.params.copy(
99
+ params = original.params.model_copy(
100
100
  update={
101
101
  "newLocation": _standardize_labware_location(
102
102
  original.params.newLocation, robot_type
103
103
  )
104
104
  }
105
105
  )
106
- return original.copy(update={"params": params})
106
+ return original.model_copy(update={"params": params})
107
107
 
108
108
 
109
109
  _standardize_command_functions: Dict[
@@ -135,6 +135,6 @@ def _standardize_labware_location(
135
135
  def _standardize_deck_slot_location(
136
136
  original: DeckSlotLocation, robot_type: RobotType
137
137
  ) -> DeckSlotLocation:
138
- return original.copy(
138
+ return original.model_copy(
139
139
  update={"slotName": original.slotName.to_equivalent_for_robot_type(robot_type)}
140
140
  )
@@ -53,15 +53,19 @@ def get_move_type_to_well(
53
53
 
54
54
 
55
55
  def get_edge_point_list(
56
- center: Point, x_radius: float, y_radius: float, edge_path_type: EdgePathType
56
+ center: Point,
57
+ x_radius: float,
58
+ y_radius: float,
59
+ mm_from_edge: float,
60
+ edge_path_type: EdgePathType,
57
61
  ) -> List[Point]:
58
62
  """Get list of edge points dependent on edge path type."""
59
63
  edges = EdgeList(
60
- right=center + Point(x=x_radius, y=0, z=0),
61
- left=center + Point(x=-x_radius, y=0, z=0),
64
+ right=center + Point(x=x_radius - mm_from_edge, y=0, z=0),
65
+ left=center + Point(x=-x_radius + mm_from_edge, y=0, z=0),
62
66
  center=center,
63
- forward=center + Point(x=0, y=y_radius, z=0),
64
- back=center + Point(x=0, y=-y_radius, z=0),
67
+ forward=center + Point(x=0, y=y_radius - mm_from_edge, z=0),
68
+ back=center + Point(x=0, y=-y_radius + mm_from_edge, z=0),
65
69
  )
66
70
 
67
71
  if edge_path_type == EdgePathType.LEFT:
@@ -0,0 +1,193 @@
1
+ """Utilities for doing coverage math on wells."""
2
+
3
+ from typing import Iterator
4
+ from opentrons_shared_data.errors.exceptions import (
5
+ InvalidStoredData,
6
+ InvalidProtocolData,
7
+ )
8
+
9
+ from opentrons.hardware_control.nozzle_manager import NozzleMap
10
+
11
+
12
+ def wells_covered_by_pipette_configuration(
13
+ nozzle_map: NozzleMap,
14
+ target_well: str,
15
+ labware_wells_by_column: list[list[str]],
16
+ ) -> Iterator[str]:
17
+ """Compute the wells covered by a pipette nozzle configuration."""
18
+ if len(labware_wells_by_column) >= 12 and len(labware_wells_by_column[0]) >= 8:
19
+ yield from wells_covered_dense(
20
+ nozzle_map,
21
+ target_well,
22
+ labware_wells_by_column,
23
+ )
24
+ elif len(labware_wells_by_column) < 12 and len(labware_wells_by_column[0]) < 8:
25
+ yield from wells_covered_sparse(
26
+ nozzle_map, target_well, labware_wells_by_column
27
+ )
28
+ else:
29
+ raise InvalidStoredData(
30
+ "Labware of non-SBS and non-reservoir format cannot be handled"
31
+ )
32
+
33
+
34
+ def row_col_ordinals_from_column_major_map(
35
+ target_well: str, column_major_wells: list[list[str]]
36
+ ) -> tuple[int, int]:
37
+ """Turn a well name into the index of its row and column (in that order) within the labware."""
38
+ for column_index, column in enumerate(column_major_wells):
39
+ if target_well in column:
40
+ return column.index(target_well), column_index
41
+ raise InvalidStoredData(f"Well name {target_well} is not present in labware")
42
+
43
+
44
+ def wells_covered_dense( # noqa: C901
45
+ nozzle_map: NozzleMap, target_well: str, target_wells_by_column: list[list[str]]
46
+ ) -> Iterator[str]:
47
+ """Get the list of wells covered by a nozzle map on an SBS format labware with a specified multiplier of 96 into the number of wells.
48
+
49
+ This will handle the offsetting of the nozzle map into higher-density well plates. For instance, a full column config target at A1 of a
50
+ 96 plate would cover wells A1, B1, C1, D1, E1, F1, G1, H1, and use downsample_factor 1.0 (96*1 = 96). A full column config target on a
51
+ 384 plate would cover wells A1, C1, E1, G1, I1, K1, M1, O1 and use downsample_factor 4.0 (96*4 = 384), while a full column config
52
+ targeting B1 would cover wells B1, D1, F1, H1, J1, L1, N1, P1 - still using downsample_factor 4.0, with the offset gathered from the
53
+ target well.
54
+
55
+ The function may also handle sub-96 regular labware with fractional downsample factors, but that's physically improbable and it's not
56
+ tested. If you have a regular labware with fewer than 96 wells that is still regularly-spaced and has little enough space between well
57
+ walls that it's reasonable to use with multiple channels, you probably want wells_covered_trough.
58
+ """
59
+ target_row_index, target_column_index = row_col_ordinals_from_column_major_map(
60
+ target_well, target_wells_by_column
61
+ )
62
+ column_downsample = len(target_wells_by_column) // 12
63
+ row_downsample = len(target_wells_by_column[0]) // 8
64
+ if column_downsample < 1 or row_downsample < 1:
65
+ raise InvalidStoredData(
66
+ "This labware cannot be used wells_covered_dense because it is less dense than an SBS 96 standard"
67
+ )
68
+
69
+ for nozzle_column in range(len(nozzle_map.columns)):
70
+ target_column_offset = nozzle_column * column_downsample
71
+ for nozzle_row in range(len(nozzle_map.rows)):
72
+ target_row_offset = nozzle_row * row_downsample
73
+ if nozzle_map.starting_nozzle == "A1":
74
+ if (
75
+ target_column_index + target_column_offset
76
+ < len(target_wells_by_column)
77
+ ) and (
78
+ target_row_index + target_row_offset
79
+ < len(target_wells_by_column[target_column_index])
80
+ ):
81
+ yield target_wells_by_column[
82
+ target_column_index + target_column_offset
83
+ ][target_row_index + target_row_offset]
84
+ elif nozzle_map.starting_nozzle == "A12":
85
+ if (target_column_index - target_column_offset >= 0) and (
86
+ target_row_index + target_row_offset
87
+ < len(target_wells_by_column[target_column_index])
88
+ ):
89
+ yield target_wells_by_column[
90
+ target_column_index - target_column_offset
91
+ ][target_row_index + target_row_offset]
92
+ elif nozzle_map.starting_nozzle == "H1":
93
+ if (
94
+ target_column_index + target_column_offset
95
+ < len(target_wells_by_column)
96
+ ) and (target_row_index - target_row_offset >= 0):
97
+ yield target_wells_by_column[
98
+ target_column_index + target_column_offset
99
+ ][target_row_index - target_row_offset]
100
+ elif nozzle_map.starting_nozzle == "H12":
101
+ if (target_column_index - target_column_offset >= 0) and (
102
+ target_row_index - target_row_offset >= 0
103
+ ):
104
+ yield target_wells_by_column[
105
+ target_column_index - target_column_offset
106
+ ][target_row_index - target_row_offset]
107
+ else:
108
+ raise InvalidProtocolData(
109
+ f"A pipette nozzle configuration may not having a starting nozzle of {nozzle_map.starting_nozzle}"
110
+ )
111
+
112
+
113
+ def wells_covered_sparse( # noqa: C901
114
+ nozzle_map: NozzleMap, target_well: str, target_wells_by_column: list[list[str]]
115
+ ) -> Iterator[str]:
116
+ """Get the list of wells covered by a nozzle map on a column-oriented reservoir.
117
+
118
+ This function handles reservoirs whose wells span multiple rows and columns - the most common case is something like a
119
+ 12-well reservoir, whose wells are the height of an SBS column and the width of an SBS row, or a 1-well reservoir whose well
120
+ is the size of an SBS active area.
121
+ """
122
+ target_row_index, target_column_index = row_col_ordinals_from_column_major_map(
123
+ target_well, target_wells_by_column
124
+ )
125
+ column_upsample = 12 // len(target_wells_by_column)
126
+ row_upsample = 8 // len(target_wells_by_column[0])
127
+ if column_upsample < 1 or row_upsample < 1:
128
+ raise InvalidStoredData(
129
+ "This labware cannot be used with wells_covered_sparse because it is more dense than an SBS 96 standard."
130
+ )
131
+ for nozzle_column in range(max(1, len(nozzle_map.columns) // column_upsample)):
132
+ for nozzle_row in range(max(1, len(nozzle_map.rows) // row_upsample)):
133
+ if nozzle_map.starting_nozzle == "A1":
134
+ if (
135
+ target_column_index + nozzle_column < len(target_wells_by_column)
136
+ ) and (
137
+ target_row_index + nozzle_row
138
+ < len(target_wells_by_column[target_column_index])
139
+ ):
140
+ yield target_wells_by_column[target_column_index + nozzle_column][
141
+ target_row_index + nozzle_row
142
+ ]
143
+ elif nozzle_map.starting_nozzle == "A12":
144
+ if (target_column_index - nozzle_column >= 0) and (
145
+ target_row_index + nozzle_row
146
+ < len(target_wells_by_column[target_column_index])
147
+ ):
148
+ yield target_wells_by_column[target_column_index - nozzle_column][
149
+ target_row_index + nozzle_row
150
+ ]
151
+ elif nozzle_map.starting_nozzle == "H1":
152
+ if (
153
+ target_column_index + nozzle_column
154
+ < len(target_wells_by_column[target_column_index])
155
+ ) and (target_row_index - nozzle_row >= 0):
156
+ yield target_wells_by_column[target_column_index + nozzle_column][
157
+ target_row_index - nozzle_row
158
+ ]
159
+ elif nozzle_map.starting_nozzle == "H12":
160
+ if (target_column_index - nozzle_column >= 0) and (
161
+ target_row_index - nozzle_row >= 0
162
+ ):
163
+ yield target_wells_by_column[target_column_index - nozzle_column][
164
+ target_row_index - nozzle_row
165
+ ]
166
+ else:
167
+ raise InvalidProtocolData(
168
+ f"A pipette nozzle configuration may not having a starting nozzle of {nozzle_map.starting_nozzle}"
169
+ )
170
+
171
+
172
+ def nozzles_per_well(
173
+ nozzle_map: NozzleMap, target_well: str, target_wells_by_column: list[list[str]]
174
+ ) -> int:
175
+ """Get the number of nozzles that will interact with each well in the labware.
176
+
177
+ For instance, if this is an SBS 96 or more dense, there is always 1 nozzle per well
178
+ that is interacted with (and some wells may not be interacted with at all). If this is
179
+ a 12-column reservoir, then all active nozzles in each column of the configuration will
180
+ interact with each well; so an 8-channel full config would have 8 nozzles per well,
181
+ and a 96 channel with a rectangle config from A1 to D12 would have 4 nozzles per well.
182
+ """
183
+ _, target_column_index = row_col_ordinals_from_column_major_map(
184
+ target_well, target_wells_by_column
185
+ )
186
+ # labware as or more dense than a 96 plate will only ever have 1 nozzle per well (and some wells won't be touched)
187
+ if len(target_wells_by_column) >= len(nozzle_map.columns) and len(
188
+ target_wells_by_column[target_column_index]
189
+ ) >= len(nozzle_map.rows):
190
+ return 1
191
+ return max(1, len(nozzle_map.columns) // len(target_wells_by_column)) * max(
192
+ 1, len(nozzle_map.rows) // len(target_wells_by_column[target_column_index])
193
+ )
@@ -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."""