opentrons 8.7.0a9__py3-none-any.whl → 8.8.0a8__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 (190) hide show
  1. opentrons/_version.py +2 -2
  2. opentrons/cli/analyze.py +4 -1
  3. opentrons/config/__init__.py +7 -0
  4. opentrons/drivers/asyncio/communication/serial_connection.py +126 -49
  5. opentrons/drivers/heater_shaker/abstract.py +5 -0
  6. opentrons/drivers/heater_shaker/driver.py +10 -0
  7. opentrons/drivers/heater_shaker/simulator.py +4 -0
  8. opentrons/drivers/thermocycler/abstract.py +6 -0
  9. opentrons/drivers/thermocycler/driver.py +61 -10
  10. opentrons/drivers/thermocycler/simulator.py +6 -0
  11. opentrons/drivers/vacuum_module/__init__.py +5 -0
  12. opentrons/drivers/vacuum_module/abstract.py +93 -0
  13. opentrons/drivers/vacuum_module/driver.py +208 -0
  14. opentrons/drivers/vacuum_module/errors.py +39 -0
  15. opentrons/drivers/vacuum_module/simulator.py +85 -0
  16. opentrons/drivers/vacuum_module/types.py +79 -0
  17. opentrons/execute.py +3 -0
  18. opentrons/hardware_control/api.py +24 -5
  19. opentrons/hardware_control/backends/controller.py +8 -2
  20. opentrons/hardware_control/backends/flex_protocol.py +1 -0
  21. opentrons/hardware_control/backends/ot3controller.py +35 -2
  22. opentrons/hardware_control/backends/ot3simulator.py +3 -1
  23. opentrons/hardware_control/backends/ot3utils.py +37 -0
  24. opentrons/hardware_control/backends/simulator.py +2 -1
  25. opentrons/hardware_control/backends/subsystem_manager.py +5 -2
  26. opentrons/hardware_control/emulation/abstract_emulator.py +6 -4
  27. opentrons/hardware_control/emulation/connection_handler.py +8 -5
  28. opentrons/hardware_control/emulation/heater_shaker.py +12 -3
  29. opentrons/hardware_control/emulation/settings.py +1 -1
  30. opentrons/hardware_control/emulation/thermocycler.py +67 -15
  31. opentrons/hardware_control/module_control.py +105 -10
  32. opentrons/hardware_control/modules/__init__.py +3 -0
  33. opentrons/hardware_control/modules/absorbance_reader.py +11 -4
  34. opentrons/hardware_control/modules/flex_stacker.py +38 -9
  35. opentrons/hardware_control/modules/heater_shaker.py +42 -5
  36. opentrons/hardware_control/modules/magdeck.py +8 -4
  37. opentrons/hardware_control/modules/mod_abc.py +14 -6
  38. opentrons/hardware_control/modules/tempdeck.py +25 -5
  39. opentrons/hardware_control/modules/thermocycler.py +68 -11
  40. opentrons/hardware_control/modules/types.py +20 -1
  41. opentrons/hardware_control/modules/utils.py +11 -4
  42. opentrons/hardware_control/motion_utilities.py +6 -6
  43. opentrons/hardware_control/nozzle_manager.py +3 -0
  44. opentrons/hardware_control/ot3api.py +92 -17
  45. opentrons/hardware_control/poller.py +22 -8
  46. opentrons/hardware_control/protocols/liquid_handler.py +12 -4
  47. opentrons/hardware_control/scripts/update_module_fw.py +5 -0
  48. opentrons/hardware_control/types.py +43 -2
  49. opentrons/legacy_commands/commands.py +58 -5
  50. opentrons/legacy_commands/module_commands.py +52 -0
  51. opentrons/legacy_commands/protocol_commands.py +53 -1
  52. opentrons/legacy_commands/types.py +155 -1
  53. opentrons/motion_planning/deck_conflict.py +17 -12
  54. opentrons/motion_planning/waypoints.py +15 -29
  55. opentrons/protocol_api/__init__.py +5 -1
  56. opentrons/protocol_api/_transfer_liquid_validation.py +17 -2
  57. opentrons/protocol_api/_types.py +8 -1
  58. opentrons/protocol_api/core/common.py +3 -1
  59. opentrons/protocol_api/core/engine/_default_labware_versions.py +33 -11
  60. opentrons/protocol_api/core/engine/deck_conflict.py +3 -1
  61. opentrons/protocol_api/core/engine/instrument.py +109 -26
  62. opentrons/protocol_api/core/engine/labware.py +8 -1
  63. opentrons/protocol_api/core/engine/module_core.py +95 -4
  64. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +4 -18
  65. opentrons/protocol_api/core/engine/protocol.py +51 -2
  66. opentrons/protocol_api/core/engine/stringify.py +2 -0
  67. opentrons/protocol_api/core/engine/tasks.py +48 -0
  68. opentrons/protocol_api/core/engine/well.py +8 -0
  69. opentrons/protocol_api/core/instrument.py +19 -2
  70. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +19 -2
  71. opentrons/protocol_api/core/legacy/legacy_module_core.py +33 -2
  72. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +23 -1
  73. opentrons/protocol_api/core/legacy/legacy_well_core.py +4 -0
  74. opentrons/protocol_api/core/legacy/tasks.py +19 -0
  75. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +19 -2
  76. opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +14 -2
  77. opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
  78. opentrons/protocol_api/core/module.py +58 -2
  79. opentrons/protocol_api/core/protocol.py +23 -2
  80. opentrons/protocol_api/core/tasks.py +31 -0
  81. opentrons/protocol_api/core/well.py +4 -0
  82. opentrons/protocol_api/instrument_context.py +388 -2
  83. opentrons/protocol_api/labware.py +10 -2
  84. opentrons/protocol_api/module_contexts.py +170 -6
  85. opentrons/protocol_api/protocol_context.py +87 -21
  86. opentrons/protocol_api/robot_context.py +41 -25
  87. opentrons/protocol_api/tasks.py +48 -0
  88. opentrons/protocol_api/validation.py +49 -3
  89. opentrons/protocol_engine/__init__.py +4 -0
  90. opentrons/protocol_engine/actions/__init__.py +6 -2
  91. opentrons/protocol_engine/actions/actions.py +31 -9
  92. opentrons/protocol_engine/clients/sync_client.py +42 -7
  93. opentrons/protocol_engine/commands/__init__.py +56 -0
  94. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +2 -15
  95. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +2 -15
  96. opentrons/protocol_engine/commands/absorbance_reader/read.py +22 -23
  97. opentrons/protocol_engine/commands/aspirate.py +1 -0
  98. opentrons/protocol_engine/commands/aspirate_while_tracking.py +52 -19
  99. opentrons/protocol_engine/commands/capture_image.py +302 -0
  100. opentrons/protocol_engine/commands/command.py +2 -0
  101. opentrons/protocol_engine/commands/command_unions.py +62 -0
  102. opentrons/protocol_engine/commands/create_timer.py +83 -0
  103. opentrons/protocol_engine/commands/dispense.py +1 -0
  104. opentrons/protocol_engine/commands/dispense_while_tracking.py +56 -19
  105. opentrons/protocol_engine/commands/drop_tip.py +32 -8
  106. opentrons/protocol_engine/commands/flex_stacker/common.py +35 -0
  107. opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +7 -0
  108. opentrons/protocol_engine/commands/heater_shaker/__init__.py +14 -0
  109. opentrons/protocol_engine/commands/heater_shaker/common.py +20 -0
  110. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +5 -4
  111. opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +136 -0
  112. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +31 -5
  113. opentrons/protocol_engine/commands/move_labware.py +3 -4
  114. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +1 -1
  115. opentrons/protocol_engine/commands/movement_common.py +31 -2
  116. opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
  117. opentrons/protocol_engine/commands/pipetting_common.py +48 -3
  118. opentrons/protocol_engine/commands/set_tip_state.py +97 -0
  119. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +38 -7
  120. opentrons/protocol_engine/commands/thermocycler/__init__.py +16 -0
  121. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +6 -0
  122. opentrons/protocol_engine/commands/thermocycler/run_profile.py +8 -0
  123. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +44 -7
  124. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +43 -14
  125. opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +191 -0
  126. opentrons/protocol_engine/commands/touch_tip.py +1 -1
  127. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +6 -22
  128. opentrons/protocol_engine/commands/wait_for_tasks.py +98 -0
  129. opentrons/protocol_engine/create_protocol_engine.py +12 -0
  130. opentrons/protocol_engine/engine_support.py +3 -0
  131. opentrons/protocol_engine/errors/__init__.py +12 -0
  132. opentrons/protocol_engine/errors/exceptions.py +119 -0
  133. opentrons/protocol_engine/execution/__init__.py +4 -0
  134. opentrons/protocol_engine/execution/command_executor.py +62 -1
  135. opentrons/protocol_engine/execution/create_queue_worker.py +9 -2
  136. opentrons/protocol_engine/execution/labware_movement.py +13 -15
  137. opentrons/protocol_engine/execution/movement.py +2 -0
  138. opentrons/protocol_engine/execution/pipetting.py +26 -25
  139. opentrons/protocol_engine/execution/queue_worker.py +4 -0
  140. opentrons/protocol_engine/execution/run_control.py +8 -0
  141. opentrons/protocol_engine/execution/task_handler.py +157 -0
  142. opentrons/protocol_engine/protocol_engine.py +137 -36
  143. opentrons/protocol_engine/resources/__init__.py +4 -0
  144. opentrons/protocol_engine/resources/camera_provider.py +110 -0
  145. opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
  146. opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
  147. opentrons/protocol_engine/resources/file_provider.py +133 -58
  148. opentrons/protocol_engine/resources/labware_validation.py +10 -6
  149. opentrons/protocol_engine/slot_standardization.py +2 -0
  150. opentrons/protocol_engine/state/_well_math.py +60 -18
  151. opentrons/protocol_engine/state/addressable_areas.py +2 -0
  152. opentrons/protocol_engine/state/camera.py +54 -0
  153. opentrons/protocol_engine/state/commands.py +37 -14
  154. opentrons/protocol_engine/state/geometry.py +276 -379
  155. opentrons/protocol_engine/state/labware.py +62 -108
  156. opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
  157. opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1336 -0
  158. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
  159. opentrons/protocol_engine/state/modules.py +30 -8
  160. opentrons/protocol_engine/state/motion.py +60 -18
  161. opentrons/protocol_engine/state/preconditions.py +59 -0
  162. opentrons/protocol_engine/state/state.py +44 -0
  163. opentrons/protocol_engine/state/state_summary.py +4 -0
  164. opentrons/protocol_engine/state/tasks.py +139 -0
  165. opentrons/protocol_engine/state/tips.py +177 -258
  166. opentrons/protocol_engine/state/update_types.py +26 -9
  167. opentrons/protocol_engine/types/__init__.py +23 -4
  168. opentrons/protocol_engine/types/command_preconditions.py +18 -0
  169. opentrons/protocol_engine/types/deck_configuration.py +5 -1
  170. opentrons/protocol_engine/types/instrument.py +8 -1
  171. opentrons/protocol_engine/types/labware.py +1 -13
  172. opentrons/protocol_engine/types/location.py +26 -2
  173. opentrons/protocol_engine/types/module.py +11 -1
  174. opentrons/protocol_engine/types/tasks.py +38 -0
  175. opentrons/protocol_engine/types/tip.py +9 -0
  176. opentrons/protocol_runner/create_simulating_orchestrator.py +29 -2
  177. opentrons/protocol_runner/protocol_runner.py +14 -1
  178. opentrons/protocol_runner/run_orchestrator.py +49 -2
  179. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +2 -2
  180. opentrons/protocols/api_support/definitions.py +1 -1
  181. opentrons/protocols/api_support/types.py +2 -1
  182. opentrons/simulate.py +51 -15
  183. opentrons/system/camera.py +334 -4
  184. opentrons/system/ffmpeg.py +110 -0
  185. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/METADATA +4 -4
  186. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/RECORD +189 -161
  187. opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
  188. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/WHEEL +0 -0
  189. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/entry_points.txt +0 -0
  190. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/licenses/LICENSE +0 -0
@@ -5,6 +5,7 @@ from typing import NewType, Optional
5
5
  from opentrons.protocol_engine.errors import (
6
6
  InvalidTargetTemperatureError,
7
7
  InvalidBlockVolumeError,
8
+ InvalidRampRateError,
8
9
  NoTargetTemperatureSetError,
9
10
  InvalidHoldTimeError,
10
11
  )
@@ -23,6 +24,10 @@ from opentrons.hardware_control.modules import ModuleData, ModuleDataValidator
23
24
 
24
25
  ThermocyclerModuleId = NewType("ThermocyclerModuleId", str)
25
26
 
27
+ # These are our published numbers, and from testing they are good bounds
28
+ MAX_HEATING_RATE = 4.25
29
+ MAX_COOLING_RATE = 2.0
30
+
26
31
 
27
32
  @dataclass(frozen=True)
28
33
  class ThermocyclerModuleSubState:
@@ -143,6 +148,38 @@ class ThermocyclerModuleSubState:
143
148
  )
144
149
  return target
145
150
 
151
+ def validate_ramp_rate(
152
+ self, ramp_rate: Optional[float], target_temp: float
153
+ ) -> Optional[float]:
154
+ """Validate a given temperature ramp rate.
155
+
156
+ Args:
157
+ ramp_rate: The requested ramp rate in °C/second.
158
+ target_temp: The requested block temperature.
159
+
160
+ Raises:
161
+ InvalidRampRateError: The given ramp_rate is invalid
162
+
163
+ Returns:
164
+ The validated ramp rate in °C/second
165
+ """
166
+ if ramp_rate is None:
167
+ return ramp_rate
168
+
169
+ heating = target_temp > self.get_target_block_temperature()
170
+ if (heating and ramp_rate > MAX_HEATING_RATE) or (
171
+ not heating and ramp_rate > MAX_COOLING_RATE
172
+ ):
173
+ raise InvalidRampRateError(
174
+ f"Thermocycler ramp rate cannot exceed {MAX_HEATING_RATE}°C/s"
175
+ f" while heating or {MAX_COOLING_RATE}°C/s when cooling."
176
+ )
177
+ if ramp_rate <= 0:
178
+ raise InvalidRampRateError(
179
+ f"Thermocycler ramp rate cannot be less than or equal to 0, got {ramp_rate}"
180
+ )
181
+ return ramp_rate
182
+
146
183
  @classmethod
147
184
  def from_live_data(
148
185
  cls, module_id: ThermocyclerModuleId, data: ModuleData | None
@@ -56,7 +56,6 @@ from ..types import (
56
56
  HeaterShakerLatchStatus,
57
57
  HeaterShakerMovementRestrictors,
58
58
  DeckType,
59
- LabwareMovementOffsetData,
60
59
  AddressableAreaLocation,
61
60
  StackerStoredLabwareGroup,
62
61
  )
@@ -268,6 +267,7 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
268
267
  heater_shaker.SetTargetTemperatureResult,
269
268
  heater_shaker.DeactivateHeaterResult,
270
269
  heater_shaker.SetAndWaitForShakeSpeedResult,
270
+ heater_shaker.SetShakeSpeedResult,
271
271
  heater_shaker.DeactivateShakerResult,
272
272
  heater_shaker.OpenLabwareLatchResult,
273
273
  heater_shaker.CloseLabwareLatchResult,
@@ -431,6 +431,7 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
431
431
  heater_shaker.SetTargetTemperature,
432
432
  heater_shaker.DeactivateHeater,
433
433
  heater_shaker.SetAndWaitForShakeSpeed,
434
+ heater_shaker.SetShakeSpeed,
434
435
  heater_shaker.DeactivateShaker,
435
436
  heater_shaker.OpenLabwareLatch,
436
437
  heater_shaker.CloseLabwareLatch,
@@ -466,6 +467,13 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
466
467
  is_plate_shaking=True,
467
468
  plate_target_temperature=prev_state.plate_target_temperature,
468
469
  )
470
+ elif isinstance(command.result, heater_shaker.SetShakeSpeedResult):
471
+ self._state.substate_by_module_id[module_id] = HeaterShakerModuleSubState(
472
+ module_id=HeaterShakerModuleId(module_id),
473
+ labware_latch_status=prev_state.labware_latch_status,
474
+ is_plate_shaking=True,
475
+ plate_target_temperature=prev_state.plate_target_temperature,
476
+ )
469
477
  elif isinstance(command.result, heater_shaker.DeactivateShakerResult):
470
478
  self._state.substate_by_module_id[module_id] = HeaterShakerModuleSubState(
471
479
  module_id=HeaterShakerModuleId(module_id),
@@ -1336,13 +1344,6 @@ class ModuleView:
1336
1344
  return True
1337
1345
  return False
1338
1346
 
1339
- def get_default_gripper_offsets(
1340
- self, module_id: str
1341
- ) -> Optional[LabwareMovementOffsetData]:
1342
- """Get the deck's default gripper offsets."""
1343
- offsets = self.get_definition(module_id).gripperOffsets
1344
- return offsets.get("default") if offsets else None
1345
-
1346
1347
  def get_overflowed_module_in_slot(
1347
1348
  self, slot_name: DeckSlotName
1348
1349
  ) -> Optional[LoadedModule]:
@@ -1519,3 +1520,24 @@ class ModuleView:
1519
1520
  f"Provided overlap offset {overlap_offset} does not match "
1520
1521
  f"configured {configured}."
1521
1522
  )
1523
+
1524
+ def get_has_module_probably_matching_hardware_details(
1525
+ self, module_model: ModuleModel, module_serial: str | None
1526
+ ) -> bool:
1527
+ """Get the ID of a model that possibly matches the provided details.
1528
+
1529
+ If the provided serial is not None, return True if there is a module with the same serial or
1530
+ False if there is not.
1531
+ If the provided serial is None, return True if there is a module with the same model or False if
1532
+ there is not.
1533
+
1534
+ This is intended to provide a good probability that a module matching the provided details
1535
+ is or is not present in the state store. It is used to drive whether the engine cancels a protocol
1536
+ in response to an asynchronous module error or not.
1537
+ """
1538
+ for module_id, module in self._state.hardware_by_module_id.items():
1539
+ if module_serial is not None and module_serial == module.serial_number:
1540
+ return True
1541
+ if module_serial is None and module.definition.model == module_model:
1542
+ return True
1543
+ return False
@@ -1,6 +1,8 @@
1
1
  """Motion state store and getters."""
2
+
2
3
  from dataclasses import dataclass
3
4
  from typing import List, Optional, Union
5
+ import logging
4
6
 
5
7
  from opentrons.types import MountType, Point, StagingSlotName
6
8
  from opentrons.hardware_control.types import CriticalPoint
@@ -28,6 +30,8 @@ from .geometry import GeometryView
28
30
  from .modules import ModuleView
29
31
  from .module_substates import HeaterShakerModuleId
30
32
 
33
+ log = logging.getLogger(__name__)
34
+
31
35
 
32
36
  @dataclass(frozen=True)
33
37
  class PipetteLocationData:
@@ -75,16 +79,58 @@ class MotionView:
75
79
  isinstance(current_location, CurrentWell)
76
80
  and current_location.pipette_id == pipette_id
77
81
  ):
78
- if self._labware.get_should_center_column_on_target_well(
79
- current_location.labware_id
80
- ):
81
- critical_point = CriticalPoint.Y_CENTER
82
- elif self._labware.get_should_center_pipette_on_target_well(
82
+ critical_point = self.get_critical_point_for_wells_in_labware(
83
83
  current_location.labware_id
84
- ):
85
- critical_point = CriticalPoint.XY_CENTER
84
+ )
86
85
  return PipetteLocationData(mount=mount, critical_point=critical_point)
87
86
 
87
+ def _get_pipette_offset_for_reservoirs(
88
+ self, labware_id: str, well_name: str, pipette_id: str
89
+ ) -> Point:
90
+ # 8 rows, 12 columns
91
+ subwells_96 = self._labware.get_has_96_subwells(labware_id)
92
+ # 1 row, 12 columns
93
+ subwells_12 = self._labware.get_has_12_subwells(labware_id)
94
+ if subwells_12 and subwells_96:
95
+ log.warning(
96
+ f"{self._labware.get_display_name(labware_id)} has both offsetPipetteFor96GridSubwells and"
97
+ " offsetPipetteFor12GridSubwells quirks."
98
+ )
99
+
100
+ pipette_rows = self._pipettes.get_nozzle_configuration(pipette_id).rows
101
+ pipette_cols = self._pipettes.get_nozzle_configuration(pipette_id).columns
102
+
103
+ even_labware_rows = subwells_96
104
+ even_labware_columns = subwells_96 or subwells_12
105
+ odd_pipette_rows = len(pipette_rows) % 2 == 1
106
+ odd_pipette_cols = len(pipette_cols) % 2 == 1
107
+
108
+ well_x_dim, well_y_dim, well_z_dim = self._labware.get_well_size(
109
+ labware_id=labware_id, well_name=well_name
110
+ )
111
+ x_offset = 0.0
112
+ y_offset = 0.0
113
+ if even_labware_rows and odd_pipette_rows:
114
+ # need to move up half a row
115
+ # there's 8 rows, so move 1/16 of reservoir length
116
+ y_offset = well_y_dim / 16
117
+ if even_labware_columns and odd_pipette_cols:
118
+ # need to move left half a column
119
+ # there's 12 columns, so move 1/24 of reservoir width
120
+ x_offset = -1 * well_x_dim / 24
121
+ return Point(x=x_offset, y=y_offset)
122
+
123
+ def get_critical_point_for_wells_in_labware(
124
+ self, labware_id: str
125
+ ) -> CriticalPoint | None:
126
+ """Get the appropriate critical point override for this labware."""
127
+ if self._labware.get_should_center_column_on_target_well(labware_id):
128
+ return CriticalPoint.Y_CENTER
129
+ elif self._labware.get_should_center_pipette_on_target_well(labware_id):
130
+ return CriticalPoint.XY_CENTER
131
+ else:
132
+ return None
133
+
88
134
  def get_movement_waypoints_to_well(
89
135
  self,
90
136
  pipette_id: str,
@@ -98,15 +144,12 @@ class MotionView:
98
144
  force_direct: bool = False,
99
145
  minimum_z_height: Optional[float] = None,
100
146
  operation_volume: Optional[float] = None,
147
+ offset_pipette_for_reservoir_subwells: bool = False,
101
148
  ) -> List[motion_planning.Waypoint]:
102
149
  """Calculate waypoints to a destination that's specified as a well."""
103
150
  location = current_well or self._pipettes.get_current_location()
104
151
 
105
- destination_cp: Optional[CriticalPoint] = None
106
- if self._labware.get_should_center_column_on_target_well(labware_id):
107
- destination_cp = CriticalPoint.Y_CENTER
108
- elif self._labware.get_should_center_pipette_on_target_well(labware_id):
109
- destination_cp = CriticalPoint.XY_CENTER
152
+ destination_cp = self.get_critical_point_for_wells_in_labware(labware_id)
110
153
 
111
154
  destination = self._geometry.get_well_position(
112
155
  labware_id=labware_id,
@@ -115,6 +158,10 @@ class MotionView:
115
158
  operation_volume=operation_volume,
116
159
  pipette_id=pipette_id,
117
160
  )
161
+ if offset_pipette_for_reservoir_subwells:
162
+ destination += self._get_pipette_offset_for_reservoirs(
163
+ labware_id=labware_id, well_name=well_name, pipette_id=pipette_id
164
+ )
118
165
 
119
166
  move_type = _move_types.get_move_type_to_well(
120
167
  pipette_id, labware_id, well_name, location, force_direct
@@ -353,12 +400,7 @@ class MotionView:
353
400
  mm_from_edge=mm_from_edge,
354
401
  edge_path_type=edge_path_type,
355
402
  )
356
- critical_point: Optional[CriticalPoint] = None
357
-
358
- if self._labware.get_should_center_column_on_target_well(labware_id):
359
- critical_point = CriticalPoint.Y_CENTER
360
- elif self._labware.get_should_center_pipette_on_target_well(labware_id):
361
- critical_point = CriticalPoint.XY_CENTER
403
+ critical_point = self.get_critical_point_for_wells_in_labware(labware_id)
362
404
 
363
405
  return [
364
406
  motion_planning.Waypoint(position=p, critical_point=critical_point)
@@ -0,0 +1,59 @@
1
+ """Command precondition state and store resource."""
2
+ from dataclasses import dataclass
3
+
4
+ from opentrons.protocol_engine.actions.get_state_update import get_state_updates
5
+ from opentrons.protocol_engine.state import update_types
6
+ from opentrons.protocol_engine.types import CommandPreconditions, PreconditionTypes
7
+
8
+ from ._abstract_store import HasState, HandlesActions
9
+ from ..actions import Action
10
+
11
+
12
+ @dataclass
13
+ class CommandPreconditionState:
14
+ """State of Engine command precondition references."""
15
+
16
+ preconditions: CommandPreconditions
17
+
18
+
19
+ class CommandPreconditionStore(HasState[CommandPreconditionState], HandlesActions):
20
+ """Command Precondition container."""
21
+
22
+ _state: CommandPreconditionState
23
+
24
+ def __init__(self) -> None:
25
+ """Initialize a Command Precondition store and its state."""
26
+ self._state = CommandPreconditionState(
27
+ preconditions=CommandPreconditions(isCameraUsed=False)
28
+ )
29
+
30
+ def handle_action(self, action: Action) -> None:
31
+ """Modify state in reaction to an action."""
32
+ for state_update in get_state_updates(action):
33
+ self._handle_state_update(state_update)
34
+
35
+ def _handle_state_update(self, state_update: update_types.StateUpdate) -> None:
36
+ if state_update.precondition_update != update_types.NO_CHANGE:
37
+ for key in state_update.precondition_update.preconditions:
38
+ if key == PreconditionTypes.IS_CAMERA_USED:
39
+ self._state.preconditions.isCameraUsed = (
40
+ state_update.precondition_update.preconditions[key]
41
+ )
42
+
43
+
44
+ class CommandPreconditionView:
45
+ """Read-only engine created Command Precondition state view."""
46
+
47
+ _state: CommandPreconditionState
48
+
49
+ def __init__(self, state: CommandPreconditionState) -> None:
50
+ """Initialize the view of Command Precondition state.
51
+
52
+ Arguments:
53
+ state: Command precondition dataclass used for tracking preconditions used during a protocol.
54
+ """
55
+ self._state = state
56
+
57
+ def get_precondition(self) -> CommandPreconditions:
58
+ """Get the Command Preconditions currently set by a protocol."""
59
+ return self._state.preconditions
@@ -34,6 +34,13 @@ from .files import FileView, FileState, FileStore
34
34
  from .config import Config
35
35
  from .state_summary import StateSummary
36
36
  from ..types import DeckConfigurationType
37
+ from .tasks import TaskState, TaskView, TaskStore
38
+ from .preconditions import (
39
+ CommandPreconditionState,
40
+ CommandPreconditionStore,
41
+ CommandPreconditionView,
42
+ )
43
+ from .camera import CameraState, CameraView, CameraStore
37
44
 
38
45
 
39
46
  _ParamsT = ParamSpec("_ParamsT")
@@ -54,6 +61,9 @@ class State:
54
61
  tips: TipState
55
62
  wells: WellState
56
63
  files: FileState
64
+ tasks: TaskState
65
+ preconditions: CommandPreconditionState
66
+ camera: CameraState
57
67
 
58
68
 
59
69
  class StateView(HasState[State]):
@@ -73,6 +83,9 @@ class StateView(HasState[State]):
73
83
  _motion: MotionView
74
84
  _files: FileView
75
85
  _config: Config
86
+ _tasks: TaskView
87
+ _preconditions: CommandPreconditionView
88
+ _camera: CameraView
76
89
 
77
90
  @property
78
91
  def commands(self) -> CommandView:
@@ -139,6 +152,21 @@ class StateView(HasState[State]):
139
152
  """Get ProtocolEngine configuration."""
140
153
  return self._config
141
154
 
155
+ @property
156
+ def preconditions(self) -> CommandPreconditionView:
157
+ """Get state view selectors for command preconditions."""
158
+ return self._preconditions
159
+
160
+ @property
161
+ def camera(self) -> CameraView:
162
+ """Get state view for the Camera."""
163
+ return self._camera
164
+
165
+ @property
166
+ def tasks(self) -> TaskView:
167
+ """Get state view selectors for task state."""
168
+ return self._tasks
169
+
142
170
  def get_summary(self) -> StateSummary:
143
171
  """Get protocol run data."""
144
172
  error = self._commands.get_error()
@@ -162,6 +190,8 @@ class StateView(HasState[State]):
162
190
  )
163
191
  for liquid_class_id, liquid_class_record in self._liquid_classes.get_all().items()
164
192
  ],
193
+ tasks=self._tasks.get_summary(),
194
+ cameraSettings=self._camera.get_enablement_settings(),
165
195
  )
166
196
 
167
197
 
@@ -231,6 +261,9 @@ class StateStore(StateView, ActionHandler):
231
261
  self._tip_store = TipStore()
232
262
  self._well_store = WellStore()
233
263
  self._file_store = FileStore()
264
+ self._task_store = TaskStore()
265
+ self._precondition_store = CommandPreconditionStore()
266
+ self._camera_store = CameraStore()
234
267
 
235
268
  self._substores: List[HandlesActions] = [
236
269
  self._command_store,
@@ -243,6 +276,9 @@ class StateStore(StateView, ActionHandler):
243
276
  self._tip_store,
244
277
  self._well_store,
245
278
  self._file_store,
279
+ self._task_store,
280
+ self._precondition_store,
281
+ self._camera_store,
246
282
  ]
247
283
  self._config = config
248
284
  self._change_notifier = change_notifier or ChangeNotifier()
@@ -366,6 +402,9 @@ class StateStore(StateView, ActionHandler):
366
402
  tips=self._tip_store.state,
367
403
  wells=self._well_store.state,
368
404
  files=self._file_store.state,
405
+ tasks=self._task_store.state,
406
+ preconditions=self._precondition_store.state,
407
+ camera=self._camera_store.state,
369
408
  )
370
409
 
371
410
  def _initialize_state(self) -> None:
@@ -384,6 +423,9 @@ class StateStore(StateView, ActionHandler):
384
423
  self._tips = TipView(state.tips)
385
424
  self._wells = WellView(state.wells)
386
425
  self._files = FileView(state.files)
426
+ self._tasks = TaskView(state.tasks)
427
+ self._preconditions = CommandPreconditionView(state.preconditions)
428
+ self._camera = CameraView(state.camera)
387
429
 
388
430
  # Derived states
389
431
  self._geometry = GeometryView(
@@ -416,6 +458,8 @@ class StateStore(StateView, ActionHandler):
416
458
  self._liquid_classes._state = next_state.liquid_classes
417
459
  self._tips._state = next_state.tips
418
460
  self._wells._state = next_state.wells
461
+ self._tasks._state = next_state.tasks
462
+ self._camera._state = next_state.camera
419
463
  self._change_notifier.notify()
420
464
  if self._notify_robot_server is not None:
421
465
  self._notify_robot_server()
@@ -13,7 +13,9 @@ from ..types import (
13
13
  Liquid,
14
14
  LiquidClassRecordWithId,
15
15
  WellInfoSummary,
16
+ TaskSummary,
16
17
  )
18
+ from ..resources.camera_provider import CameraSettings
17
19
 
18
20
 
19
21
  class StateSummary(BaseModel):
@@ -34,3 +36,5 @@ class StateSummary(BaseModel):
34
36
  wells: List[WellInfoSummary] = Field(default_factory=list)
35
37
  files: List[str] = Field(default_factory=list)
36
38
  liquidClasses: List[LiquidClassRecordWithId] = Field(default_factory=list)
39
+ tasks: List[TaskSummary] = Field(default_factory=list)
40
+ cameraSettings: Optional[CameraSettings] = None
@@ -0,0 +1,139 @@
1
+ """Task state tracking."""
2
+ from dataclasses import dataclass
3
+ from itertools import chain
4
+ from typing import Iterable
5
+ from ..types import Task, TaskSummary, FinishedTask
6
+ from ._abstract_store import HasState, HandlesActions
7
+ from opentrons.protocol_engine.state import update_types
8
+ from opentrons.protocol_engine.errors.exceptions import NoTaskFoundError
9
+ from ..actions import (
10
+ get_state_updates,
11
+ Action,
12
+ StartTaskAction,
13
+ FinishTaskAction,
14
+ )
15
+
16
+
17
+ @dataclass
18
+ class TaskState:
19
+ """Task state tracking."""
20
+
21
+ current_tasks_by_id: dict[str, Task]
22
+ finished_tasks_by_id: dict[str, FinishedTask]
23
+
24
+
25
+ class TaskStore(HasState[TaskState], HandlesActions):
26
+ """Stores tasks."""
27
+
28
+ _state: TaskState
29
+
30
+ def __init__(self) -> None:
31
+ """Initialize a TaskStore."""
32
+ self._state = TaskState(current_tasks_by_id={}, finished_tasks_by_id={})
33
+
34
+ def _handle_state_update(self, state_update: update_types.StateUpdate) -> None:
35
+ """Handle a state update."""
36
+ return
37
+
38
+ def _handle_start_task_action(self, action: StartTaskAction) -> None:
39
+ self._state.current_tasks_by_id[action.task.id] = action.task
40
+
41
+ def _handle_finish_task_action(self, action: FinishTaskAction) -> None:
42
+ task = self._state.current_tasks_by_id[action.task_id]
43
+ self._state.finished_tasks_by_id[action.task_id] = FinishedTask(
44
+ id=task.id,
45
+ createdAt=task.createdAt,
46
+ finishedAt=action.finished_at,
47
+ error=action.error,
48
+ )
49
+ del self._state.current_tasks_by_id[action.task_id]
50
+
51
+ def handle_action(self, action: Action) -> None:
52
+ """Modify the state in reaction to an action."""
53
+ for state_update in get_state_updates(action):
54
+ self._handle_state_update(state_update)
55
+ match action:
56
+ case StartTaskAction():
57
+ self._handle_start_task_action(action)
58
+ case FinishTaskAction():
59
+ self._handle_finish_task_action(action)
60
+ case _:
61
+ pass
62
+
63
+
64
+ class TaskView:
65
+ """Read-only task state view."""
66
+
67
+ _state: TaskState
68
+
69
+ def __init__(self, state: TaskState) -> None:
70
+ """Initialize a TaskView."""
71
+ self._state = state
72
+
73
+ def get_current(self, id: str) -> Task:
74
+ """Get a task by ID."""
75
+ try:
76
+ return self._state.current_tasks_by_id[id]
77
+ except KeyError as e:
78
+ raise NoTaskFoundError(f"No current task with ID {id}") from e
79
+
80
+ def get_all_current(self) -> list[Task]:
81
+ """Get all currently running tasks."""
82
+ return [task for task in self._state.current_tasks_by_id.values()]
83
+
84
+ def get_finished(self, id: str) -> FinishedTask:
85
+ """Get a finished task by ID."""
86
+ try:
87
+ return self._state.finished_tasks_by_id[id]
88
+ except KeyError as e:
89
+ raise NoTaskFoundError(f"No finished task with ID {id}") from e
90
+
91
+ def get(self, id: str) -> Task | FinishedTask:
92
+ """Get a single task by id."""
93
+ if id in self._state.current_tasks_by_id:
94
+ return self._state.current_tasks_by_id[id]
95
+ elif id in self._state.finished_tasks_by_id:
96
+ return self._state.finished_tasks_by_id[id]
97
+ else:
98
+ raise NoTaskFoundError(message=f"Task {id} not found.")
99
+
100
+ def get_summary(self) -> list[TaskSummary]:
101
+ """Get a summary of all tasks."""
102
+ return [
103
+ TaskSummary(
104
+ id=task_id,
105
+ createdAt=task.createdAt,
106
+ finishedAt=getattr(task, "finishedAt", None),
107
+ error=getattr(task, "error", None),
108
+ )
109
+ for task_id, task in chain(
110
+ self._state.current_tasks_by_id.items(),
111
+ self._state.finished_tasks_by_id.items(),
112
+ )
113
+ ]
114
+
115
+ def all_tasks_finished_or_any_task_failed(self, task_ids: Iterable[str]) -> bool:
116
+ """Implements wait semantics of asyncio.gather(return_exceptions = False).
117
+
118
+ This returns true when any of the following are true:
119
+ - All tasks in task_ids are complete with or without an error
120
+ - Any task in task_ids is complete with an error.
121
+
122
+ NOTE: Does not raise the error that the errored task has.
123
+ """
124
+ finished = set(self._state.finished_tasks_by_id.keys())
125
+ task_ids = set(task_ids)
126
+ if task_ids.issubset(finished):
127
+ return True
128
+ if self.get_failed_tasks(task_ids):
129
+ return True
130
+ return False
131
+
132
+ def get_failed_tasks(self, task_ids: Iterable[str]) -> list[str]:
133
+ """Return a list of failed task ids of the ones that were passed."""
134
+ failed_tasks: list[str] = []
135
+ for task_id in task_ids:
136
+ task = self._state.finished_tasks_by_id.get(task_id, None)
137
+ if task and task.error:
138
+ failed_tasks.append(task_id)
139
+ return failed_tasks