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
@@ -38,6 +38,7 @@ from ..types import (
38
38
  LabwareLocationSequence,
39
39
  NotOnDeckLocationSequenceComponent,
40
40
  OFF_DECK_LOCATION,
41
+ WASTE_CHUTE_LOCATION,
41
42
  )
42
43
  from ..errors import (
43
44
  LabwareMovementNotAllowedError,
@@ -228,9 +229,10 @@ class MoveLabwareImplementation(AbstractCommandImpl[MoveLabwareParams, _ExecuteR
228
229
  )
229
230
  eventual_destination_location_sequence = [
230
231
  NotOnDeckLocationSequenceComponent(
231
- logicalLocationName=OFF_DECK_LOCATION
232
+ logicalLocationName=WASTE_CHUTE_LOCATION
232
233
  )
233
234
  ]
235
+
234
236
  elif fixture_validation.is_trash(area_name):
235
237
  # When dropping labware in the trash bins we want to ensure they are lids
236
238
  # and enforce a y-axis drop offset to ensure they fall within the trash bin
@@ -351,7 +353,6 @@ class MoveLabwareImplementation(AbstractCommandImpl[MoveLabwareParams, _ExecuteR
351
353
  validated_new_loc = self._state_view.geometry.ensure_valid_gripper_location(
352
354
  available_new_location,
353
355
  )
354
-
355
356
  user_pick_up_offset = (
356
357
  Point.from_xyz_attrs(params.pickUpOffset)
357
358
  if params.pickUpOffset is not None
@@ -464,7 +465,6 @@ class MoveLabwareImplementation(AbstractCommandImpl[MoveLabwareParams, _ExecuteR
464
465
  new_location=available_new_location,
465
466
  new_offset_id=new_offset_id,
466
467
  )
467
-
468
468
  if labware_validation.validate_definition_is_lid(
469
469
  definition=self._state_view.labware.get_definition(params.labwareId)
470
470
  ):
@@ -498,7 +498,6 @@ class MoveLabwareImplementation(AbstractCommandImpl[MoveLabwareParams, _ExecuteR
498
498
  parent_labware_ids=parent_updates,
499
499
  lid_ids=lid_updates,
500
500
  )
501
-
502
501
  return SuccessData(
503
502
  public=MoveLabwareResult(
504
503
  offsetId=new_offset_id,
@@ -85,7 +85,7 @@ class MoveToAddressableAreaForDropTipParams(PipetteIdMixin, MovementMixin):
85
85
  ignoreTipConfiguration: bool | SkipJsonSchema[None] = Field(
86
86
  True,
87
87
  description=(
88
- "Whether to utilize the critical point of the tip configuraiton when moving to an addressable area."
88
+ "Whether to utilize the critical point of the tip configuration when moving to an addressable area."
89
89
  " If True, this command will ignore the tip configuration and use the center of the entire instrument"
90
90
  " as the critical point for movement."
91
91
  " If False, this command will use the critical point provided by the current tip configuration."
@@ -48,8 +48,8 @@ class WellLocationMixin(BaseModel):
48
48
  )
49
49
 
50
50
 
51
- class LiquidHandlingWellLocationMixin(BaseModel):
52
- """Mixin for command requests that take a location that's somewhere in a well."""
51
+ class LiquidHandlingWellMixin(BaseModel):
52
+ """Base Mixin for command requests that take a well."""
53
53
 
54
54
  labwareId: str = Field(
55
55
  ...,
@@ -59,12 +59,39 @@ class LiquidHandlingWellLocationMixin(BaseModel):
59
59
  ...,
60
60
  description="Name of well to use in labware.",
61
61
  )
62
+
63
+
64
+ class LiquidHandlingWellLocationMixin(LiquidHandlingWellMixin):
65
+ """Mixin for command requests that take a location that's somewhere in a well."""
66
+
62
67
  wellLocation: LiquidHandlingWellLocation = Field(
63
68
  default_factory=LiquidHandlingWellLocation,
64
69
  description="Relative well location at which to perform the operation",
65
70
  )
66
71
 
67
72
 
73
+ class DynamicLiquidHandlingWellLocationMixin(LiquidHandlingWellMixin):
74
+ """Mixin for command requests that move between two locations in a well."""
75
+
76
+ trackFromLocation: LiquidHandlingWellLocation = Field(
77
+ default_factory=LiquidHandlingWellLocation,
78
+ description="Relative well location at which to start the operation",
79
+ )
80
+ trackToLocation: LiquidHandlingWellLocation = Field(
81
+ default_factory=LiquidHandlingWellLocation,
82
+ description="Relative well location at which to end the operation",
83
+ )
84
+ movement_delay: float | SkipJsonSchema[None] = Field(
85
+ None,
86
+ description=(
87
+ "Optional movement delay in seconds."
88
+ " When this argument is used, the x/y/z movement will wait movement_delay seconds"
89
+ " after the pipetting action starts before beginning the x/y/z move."
90
+ ),
91
+ json_schema_extra=_remove_default,
92
+ )
93
+
94
+
68
95
  class MovementMixin(BaseModel):
69
96
  """Mixin for command requests that move a pipette."""
70
97
 
@@ -152,6 +179,7 @@ async def move_to_well(
152
179
  minimum_z_height: Optional[float] = None,
153
180
  speed: Optional[float] = None,
154
181
  operation_volume: Optional[float] = None,
182
+ offset_pipette_for_reservoir_subwells: bool = False,
155
183
  ) -> MoveToWellOperationReturn:
156
184
  """Execute a move to well microoperation."""
157
185
  try:
@@ -165,6 +193,7 @@ async def move_to_well(
165
193
  minimum_z_height=minimum_z_height,
166
194
  speed=speed,
167
195
  operation_volume=operation_volume,
196
+ offset_pipette_for_reservoir_subwells=offset_pipette_for_reservoir_subwells,
168
197
  )
169
198
  except StallOrCollisionDetectedError as e:
170
199
  return DefinedErrorData(
@@ -10,7 +10,7 @@ from typing_extensions import Literal
10
10
  from ..errors import ErrorOccurrence, PickUpTipTipNotAttachedError
11
11
  from ..resources import ModelUtils
12
12
  from ..state import update_types
13
- from ..types import PickUpTipWellLocation, LabwareWellId
13
+ from ..types import PickUpTipWellLocation, LabwareWellId, TipRackWellState
14
14
  from .pipetting_common import (
15
15
  PipetteIdMixin,
16
16
  )
@@ -121,10 +121,14 @@ class PickUpTipImplementation(AbstractCommandImpl[PickUpTipParams, _ExecuteRetur
121
121
  labware_id = params.labwareId
122
122
  well_name = params.wellName
123
123
 
124
- tips_to_mark_as_used = self._state_view.tips.compute_tips_to_mark_as_used(
125
- labware_id=labware_id,
126
- well_name=well_name,
127
- nozzle_map=self._state_view.pipettes.get_nozzle_configuration(pipette_id),
124
+ tips_to_mark_as_empty = (
125
+ self._state_view.tips.compute_tips_to_mark_as_used_or_empty(
126
+ labware_id=labware_id,
127
+ well_name=well_name,
128
+ nozzle_map=self._state_view.pipettes.get_nozzle_configuration(
129
+ pipette_id
130
+ ),
131
+ )
128
132
  )
129
133
 
130
134
  well_location = self._state_view.geometry.convert_pick_up_tip_well_location(
@@ -160,16 +164,20 @@ class PickUpTipImplementation(AbstractCommandImpl[PickUpTipParams, _ExecuteRetur
160
164
  ),
161
165
  )
162
166
  .set_fluid_empty(pipette_id=pipette_id, clean_tip=True)
163
- .mark_tips_as_used(
164
- labware_id=labware_id, well_names=tips_to_mark_as_used
167
+ .update_tip_rack_well_state(
168
+ tip_state=TipRackWellState.EMPTY,
169
+ labware_id=labware_id,
170
+ well_names=tips_to_mark_as_empty,
165
171
  )
166
172
  )
167
173
  state_update = (
168
174
  update_types.StateUpdate.reduce(
169
175
  update_types.StateUpdate(), move_result.state_update
170
176
  )
171
- .mark_tips_as_used(
172
- labware_id=labware_id, well_names=tips_to_mark_as_used
177
+ .update_tip_rack_well_state(
178
+ tip_state=TipRackWellState.EMPTY,
179
+ labware_id=labware_id,
180
+ well_names=tips_to_mark_as_empty,
173
181
  )
174
182
  .set_fluid_unknown(pipette_id=pipette_id)
175
183
  )
@@ -197,8 +205,10 @@ class PickUpTipImplementation(AbstractCommandImpl[PickUpTipParams, _ExecuteRetur
197
205
  labware_id=labware_id, well_name=well_name
198
206
  ),
199
207
  )
200
- .mark_tips_as_used(
201
- labware_id=labware_id, well_names=tips_to_mark_as_used
208
+ .update_tip_rack_well_state(
209
+ tip_state=TipRackWellState.EMPTY,
210
+ labware_id=labware_id,
211
+ well_names=tips_to_mark_as_empty,
202
212
  )
203
213
  .set_fluid_empty(pipette_id=pipette_id, clean_tip=True)
204
214
  .set_pipette_ready_to_aspirate(
@@ -9,10 +9,15 @@ from pydantic import BaseModel, Field
9
9
  from opentrons_shared_data.errors import ErrorCodes
10
10
  from opentrons.protocol_engine.errors.error_occurrence import ErrorOccurrence
11
11
  from opentrons.protocol_engine.types import AspiratedFluid, FluidKind
12
- from opentrons_shared_data.errors.exceptions import PipetteOverpressureError
12
+ from opentrons_shared_data.errors.exceptions import (
13
+ PipetteOverpressureError,
14
+ StallOrCollisionDetectedError,
15
+ )
13
16
  from .command import DefinedErrorData, SuccessData
14
17
  from opentrons.protocol_engine.state.update_types import StateUpdate
18
+ from opentrons.types import Point
15
19
 
20
+ from .movement_common import StallOrCollisionError
16
21
 
17
22
  if TYPE_CHECKING:
18
23
  from ..execution.pipetting import PipettingHandler
@@ -241,11 +246,15 @@ async def aspirate_while_tracking(
241
246
  well_name: str,
242
247
  volume: float,
243
248
  flow_rate: float,
249
+ end_point: Point,
244
250
  location_if_error: ErrorLocationInfo,
245
251
  command_note_adder: CommandNoteAdder,
246
252
  pipetting: PipettingHandler,
247
253
  model_utils: ModelUtils,
248
- ) -> SuccessData[BaseLiquidHandlingResult] | DefinedErrorData[OverpressureError]:
254
+ movement_delay: Optional[float] = None,
255
+ ) -> SuccessData[BaseLiquidHandlingResult] | DefinedErrorData[
256
+ OverpressureError
257
+ ] | DefinedErrorData[StallOrCollisionError]:
249
258
  """Execute an aspirate while tracking microoperation."""
250
259
  try:
251
260
  volume_aspirated = await pipetting.aspirate_while_tracking(
@@ -254,6 +263,7 @@ async def aspirate_while_tracking(
254
263
  well_name=well_name,
255
264
  volume=volume,
256
265
  flow_rate=flow_rate,
266
+ end_point=end_point,
257
267
  command_note_adder=command_note_adder,
258
268
  )
259
269
  except PipetteOverpressureError as e:
@@ -272,6 +282,21 @@ async def aspirate_while_tracking(
272
282
  ),
273
283
  state_update=StateUpdate().set_fluid_unknown(pipette_id=pipette_id),
274
284
  )
285
+ except StallOrCollisionDetectedError as e:
286
+ return DefinedErrorData(
287
+ public=StallOrCollisionError(
288
+ id=model_utils.generate_id(),
289
+ createdAt=model_utils.get_timestamp(),
290
+ wrappedErrors=[
291
+ ErrorOccurrence.from_failed(
292
+ id=model_utils.generate_id(),
293
+ createdAt=model_utils.get_timestamp(),
294
+ error=e,
295
+ )
296
+ ],
297
+ ),
298
+ state_update=StateUpdate().clear_all_pipette_locations(),
299
+ )
275
300
  else:
276
301
  return SuccessData(
277
302
  public=BaseLiquidHandlingResult(
@@ -290,11 +315,15 @@ async def dispense_while_tracking(
290
315
  well_name: str,
291
316
  volume: float,
292
317
  flow_rate: float,
318
+ end_point: Point,
293
319
  push_out: float | None,
294
320
  location_if_error: ErrorLocationInfo,
295
321
  pipetting: PipettingHandler,
296
322
  model_utils: ModelUtils,
297
- ) -> SuccessData[BaseLiquidHandlingResult] | DefinedErrorData[OverpressureError]:
323
+ movement_delay: Optional[float] = None,
324
+ ) -> SuccessData[BaseLiquidHandlingResult] | DefinedErrorData[
325
+ OverpressureError
326
+ ] | DefinedErrorData[StallOrCollisionError]:
298
327
  """Execute an dispense while tracking microoperation."""
299
328
  # The current volume won't be none since it passed validation
300
329
  current_volume = (
@@ -309,6 +338,7 @@ async def dispense_while_tracking(
309
338
  well_name=well_name,
310
339
  volume=volume,
311
340
  flow_rate=flow_rate,
341
+ end_point=end_point,
312
342
  push_out=push_out,
313
343
  is_full_dispense=is_full_dispense,
314
344
  )
@@ -332,6 +362,21 @@ async def dispense_while_tracking(
332
362
  pipette_id=pipette_id, ready_to_aspirate=False
333
363
  ),
334
364
  )
365
+ except StallOrCollisionDetectedError as e:
366
+ return DefinedErrorData(
367
+ public=StallOrCollisionError(
368
+ id=model_utils.generate_id(),
369
+ createdAt=model_utils.get_timestamp(),
370
+ wrappedErrors=[
371
+ ErrorOccurrence.from_failed(
372
+ id=model_utils.generate_id(),
373
+ createdAt=model_utils.get_timestamp(),
374
+ error=e,
375
+ )
376
+ ],
377
+ ),
378
+ state_update=StateUpdate().clear_all_pipette_locations(),
379
+ )
335
380
  else:
336
381
  return SuccessData(
337
382
  public=BaseLiquidHandlingResult(
@@ -0,0 +1,97 @@
1
+ """Set tip state command request, result, and implementation models."""
2
+
3
+ from __future__ import annotations
4
+ from pydantic import BaseModel, Field
5
+ from typing import TYPE_CHECKING, Optional, Type, List
6
+ from typing_extensions import Literal
7
+
8
+ from ..errors.error_occurrence import ErrorOccurrence
9
+ from ..state.update_types import StateUpdate
10
+ from ..types import TipRackWellState
11
+
12
+
13
+ from .command import (
14
+ AbstractCommandImpl,
15
+ BaseCommand,
16
+ BaseCommandCreate,
17
+ SuccessData,
18
+ )
19
+
20
+ if TYPE_CHECKING:
21
+ from ..state.state import StateView
22
+
23
+
24
+ SetTipStateCommandType = Literal["setTipState"]
25
+
26
+
27
+ class SetTipStateParams(BaseModel):
28
+ """Payload needed to set tip wells of a tip rack to the requested state."""
29
+
30
+ labwareId: str = Field(
31
+ ..., description="Identifier of tip rack labware to set tip wells in."
32
+ )
33
+ wellNames: List[str] = Field(
34
+ ..., description="Names of the well to set tip well state for."
35
+ )
36
+ tipWellState: TipRackWellState = Field(
37
+ ..., description="State to set tip wells to."
38
+ )
39
+
40
+
41
+ class SetTipStateResult(BaseModel):
42
+ """Result data from the execution of a setTipState command."""
43
+
44
+ pass
45
+
46
+
47
+ class SetTipStateImplementation(
48
+ AbstractCommandImpl[SetTipStateParams, SuccessData[SetTipStateResult]]
49
+ ):
50
+ """Set tip state command implementation."""
51
+
52
+ def __init__(
53
+ self,
54
+ state_view: StateView,
55
+ **kwargs: object,
56
+ ) -> None:
57
+ self._state_view = state_view
58
+
59
+ async def execute(
60
+ self, params: SetTipStateParams
61
+ ) -> SuccessData[SetTipStateResult]:
62
+ """Set the tip rack wells to the requested state."""
63
+ labware_id = params.labwareId
64
+ well_names = params.wellNames
65
+
66
+ self._state_view.labware.raise_if_not_tip_rack(labware_id=labware_id)
67
+ self._state_view.labware.raise_if_wells_are_invalid(
68
+ labware_id=labware_id, well_names=well_names
69
+ )
70
+
71
+ return SuccessData(
72
+ public=SetTipStateResult(),
73
+ state_update=StateUpdate().update_tip_rack_well_state(
74
+ tip_state=params.tipWellState,
75
+ labware_id=labware_id,
76
+ well_names=well_names,
77
+ ),
78
+ )
79
+
80
+
81
+ class SetTipState(BaseCommand[SetTipStateParams, SetTipStateResult, ErrorOccurrence]):
82
+ """Set tip state command model."""
83
+
84
+ commandType: SetTipStateCommandType = "setTipState"
85
+ params: SetTipStateParams
86
+ result: Optional[SetTipStateResult] = None
87
+
88
+ _ImplementationCls: Type[SetTipStateImplementation] = SetTipStateImplementation
89
+
90
+
91
+ class SetTipStateCreate(BaseCommandCreate[SetTipStateParams]):
92
+ """Set tip state command creation request model."""
93
+
94
+ commandType: SetTipStateCommandType = "setTipState"
95
+ params: SetTipStateParams
96
+
97
+ _CommandCls: Type[SetTipState] = SetTipState
@@ -1,25 +1,35 @@
1
1
  """Command models to start heating a Temperature Module."""
2
2
  from __future__ import annotations
3
- from typing import Optional, TYPE_CHECKING
3
+ from typing import Optional, TYPE_CHECKING, Any
4
4
  from typing_extensions import Literal, Type
5
5
 
6
6
  from pydantic import BaseModel, Field
7
+ from pydantic.json_schema import SkipJsonSchema
7
8
 
8
9
  from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
9
10
  from ...errors.error_occurrence import ErrorOccurrence
10
11
 
11
12
  if TYPE_CHECKING:
12
13
  from opentrons.protocol_engine.state.state import StateView
13
- from opentrons.protocol_engine.execution import EquipmentHandler
14
+ from opentrons.protocol_engine.execution import EquipmentHandler, TaskHandler
14
15
 
15
16
  SetTargetTemperatureCommandType = Literal["temperatureModule/setTargetTemperature"]
16
17
 
17
18
 
19
+ def _remove_default(s: dict[str, Any]) -> None:
20
+ s.pop("default", None)
21
+
22
+
18
23
  class SetTargetTemperatureParams(BaseModel):
19
24
  """Input parameters to set a Temperature Module's target temperature."""
20
25
 
21
26
  moduleId: str = Field(..., description="Unique ID of the Temperature Module.")
22
27
  celsius: float = Field(..., description="Target temperature in °C.")
28
+ taskId: str | SkipJsonSchema[None] = Field(
29
+ None,
30
+ description="Id for the background task that manages the temperature",
31
+ json_schema_extra=_remove_default,
32
+ )
23
33
 
24
34
 
25
35
  class SetTargetTemperatureResult(BaseModel):
@@ -30,6 +40,11 @@ class SetTargetTemperatureResult(BaseModel):
30
40
  description="The target temperature that was set after validation "
31
41
  "and type conversion (if any).",
32
42
  )
43
+ taskId: str | SkipJsonSchema[None] = Field(
44
+ None,
45
+ description="The task id for the setTargetTemperature task",
46
+ json_schema_extra=_remove_default,
47
+ )
33
48
 
34
49
 
35
50
  class SetTargetTemperatureImpl(
@@ -43,10 +58,12 @@ class SetTargetTemperatureImpl(
43
58
  self,
44
59
  state_view: StateView,
45
60
  equipment: EquipmentHandler,
61
+ task_handler: TaskHandler,
46
62
  **unused_dependencies: object,
47
63
  ) -> None:
48
64
  self._state_view = state_view
49
65
  self._equipment = equipment
66
+ self._task_handler = task_handler
50
67
 
51
68
  async def execute(
52
69
  self, params: SetTargetTemperatureParams
@@ -56,19 +73,33 @@ class SetTargetTemperatureImpl(
56
73
  module_substate = self._state_view.modules.get_temperature_module_substate(
57
74
  module_id=params.moduleId
58
75
  )
59
-
60
76
  # Verify temperature from temperature module view
61
77
  validated_temp = module_substate.validate_target_temperature(params.celsius)
62
-
63
78
  # Allow propagation of ModuleNotAttachedError.
64
79
  temp_hardware_module = self._equipment.get_module_hardware_api(
65
80
  module_substate.module_id
66
81
  )
67
82
 
68
- if temp_hardware_module is not None:
69
- await temp_hardware_module.start_set_temperature(celsius=validated_temp)
83
+ async def start_set_temperature(task_handler: TaskHandler) -> None:
84
+ if temp_hardware_module is not None:
85
+ async with task_handler.synchronize_cancel_previous(
86
+ module_substate.module_id
87
+ ):
88
+ await temp_hardware_module.start_set_temperature(
89
+ celsius=validated_temp
90
+ )
91
+ await temp_hardware_module.await_temperature(
92
+ awaiting_temperature=validated_temp
93
+ )
94
+
95
+ task = await self._task_handler.create_task(
96
+ task_function=start_set_temperature, id=params.taskId
97
+ )
98
+
70
99
  return SuccessData(
71
- public=SetTargetTemperatureResult(targetTemperature=validated_temp),
100
+ public=SetTargetTemperatureResult(
101
+ targetTemperature=validated_temp, taskId=task.id
102
+ ),
72
103
  )
73
104
 
74
105
 
@@ -73,6 +73,15 @@ from .run_profile import (
73
73
  RunProfileCreate,
74
74
  )
75
75
 
76
+ from .start_run_extended_profile import (
77
+ StartRunExtendedProfileCommandType,
78
+ StartRunExtendedProfileParams,
79
+ StartRunExtendedProfileStepParams,
80
+ StartRunExtendedProfileResult,
81
+ StartRunExtendedProfile,
82
+ StartRunExtendedProfileCreate,
83
+ )
84
+
76
85
  from .run_extended_profile import (
77
86
  RunExtendedProfileCommandType,
78
87
  RunExtendedProfileParams,
@@ -140,6 +149,13 @@ __all__ = [
140
149
  "RunProfileResult",
141
150
  "RunProfile",
142
151
  "RunProfileCreate",
152
+ # Start run profile command models,
153
+ "StartRunExtendedProfileCommandType",
154
+ "StartRunExtendedProfileParams",
155
+ "StartRunExtendedProfileStepParams",
156
+ "StartRunExtendedProfileResult",
157
+ "StartRunExtendedProfile",
158
+ "StartRunExtendedProfileCreate",
143
159
  # Run extended profile command models.
144
160
  "RunExtendedProfileCommandType",
145
161
  "RunExtendedProfileParams",
@@ -33,6 +33,11 @@ class ProfileStep(BaseModel):
33
33
  holdSeconds: float = Field(
34
34
  ..., description="Time to hold target temperature in seconds."
35
35
  )
36
+ rampRate: float | SkipJsonSchema[None] = Field(
37
+ None,
38
+ description="How quickly to change temperature in °C/second.",
39
+ json_schema_extra=_remove_default,
40
+ )
36
41
 
37
42
 
38
43
  class ProfileCycle(BaseModel):
@@ -68,6 +73,7 @@ def _transform_profile_step(
68
73
  return ThermocyclerStep(
69
74
  temperature=thermocycler_state.validate_target_block_temperature(step.celsius),
70
75
  hold_time_seconds=step.holdSeconds,
76
+ ramp_rate=thermocycler_state.validate_ramp_rate(step.rampRate, step.celsius),
71
77
  )
72
78
 
73
79
 
@@ -30,6 +30,11 @@ class RunProfileStepParams(BaseModel):
30
30
  holdSeconds: float = Field(
31
31
  ..., description="Time to hold target temperature at in seconds."
32
32
  )
33
+ rampRate: float | SkipJsonSchema[None] = Field(
34
+ None,
35
+ description="How quickly to change temperature in °C/second.",
36
+ json_schema_extra=_remove_default,
37
+ )
33
38
 
34
39
 
35
40
  class RunProfileParams(BaseModel):
@@ -81,6 +86,9 @@ class RunProfileImpl(
81
86
  profile_step.celsius
82
87
  ),
83
88
  hold_time_seconds=profile_step.holdSeconds,
89
+ ramp_rate=thermocycler_state.validate_ramp_rate(
90
+ profile_step.rampRate, profile_step.celsius
91
+ ),
84
92
  )
85
93
  for profile_step in params.profile
86
94
  ]
@@ -1,4 +1,4 @@
1
- """Command models to start heating a Thermocycler's block."""
1
+ """Command models for heating a Thermocycler's block."""
2
2
  from __future__ import annotations
3
3
  from typing import Optional, TYPE_CHECKING, Any
4
4
  from typing_extensions import Literal, Type
@@ -11,7 +11,7 @@ from ...errors.error_occurrence import ErrorOccurrence
11
11
 
12
12
  if TYPE_CHECKING:
13
13
  from opentrons.protocol_engine.state.state import StateView
14
- from opentrons.protocol_engine.execution import EquipmentHandler
14
+ from opentrons.protocol_engine.execution import EquipmentHandler, TaskHandler
15
15
 
16
16
 
17
17
  SetTargetBlockTemperatureCommandType = Literal["thermocycler/setTargetBlockTemperature"]
@@ -39,6 +39,17 @@ class SetTargetBlockTemperatureParams(BaseModel):
39
39
  " the given hold time has elapsed.",
40
40
  json_schema_extra=_remove_default,
41
41
  )
42
+ ramp_rate: float | SkipJsonSchema[None] = Field(
43
+ None,
44
+ description="The rate in C°/second to change temperature from the current target."
45
+ " If unspecified, the Thermocycler will change temperature at the fastest possible rate.",
46
+ json_schema_extra=_remove_default,
47
+ )
48
+ taskId: str | SkipJsonSchema[None] = Field(
49
+ None,
50
+ description="Id for the background task that manages the temperature.",
51
+ json_schema_extra=_remove_default,
52
+ )
42
53
 
43
54
 
44
55
  class SetTargetBlockTemperatureResult(BaseModel):
@@ -48,6 +59,11 @@ class SetTargetBlockTemperatureResult(BaseModel):
48
59
  ...,
49
60
  description="The target block temperature that was set after validation.",
50
61
  )
62
+ taskId: str | SkipJsonSchema[None] = Field(
63
+ None,
64
+ description="Id for the background task that manages the temperature.",
65
+ json_schema_extra=_remove_default,
66
+ )
51
67
 
52
68
 
53
69
  class SetTargetBlockTemperatureImpl(
@@ -62,10 +78,12 @@ class SetTargetBlockTemperatureImpl(
62
78
  self,
63
79
  state_view: StateView,
64
80
  equipment: EquipmentHandler,
81
+ task_handler: TaskHandler,
65
82
  **unused_dependencies: object,
66
83
  ) -> None:
67
84
  self._state_view = state_view
68
85
  self._equipment = equipment
86
+ self._task_handler = task_handler
69
87
 
70
88
  async def execute(
71
89
  self,
@@ -90,19 +108,38 @@ class SetTargetBlockTemperatureImpl(
90
108
  hold_time = thermocycler_state.validate_hold_time(params.holdTimeSeconds)
91
109
  else:
92
110
  hold_time = None
111
+ target_ramp_rate: Optional[float]
112
+ if params.ramp_rate is not None:
113
+ target_ramp_rate = thermocycler_state.validate_ramp_rate(
114
+ params.ramp_rate, target_temperature
115
+ )
116
+ else:
117
+ target_ramp_rate = None
93
118
 
94
119
  thermocycler_hardware = self._equipment.get_module_hardware_api(
95
120
  thermocycler_state.module_id
96
121
  )
97
122
 
98
- if thermocycler_hardware is not None:
99
- await thermocycler_hardware.set_target_block_temperature(
100
- target_temperature, volume=target_volume, hold_time_seconds=hold_time
101
- )
123
+ async def set_target_block_temperature(task_handler: TaskHandler) -> None:
124
+ if thermocycler_hardware is not None:
125
+ async with task_handler.synchronize_cancel_latest(
126
+ thermocycler_state.module_id + "-block"
127
+ ):
128
+ await thermocycler_hardware.set_target_block_temperature(
129
+ celsius=target_temperature,
130
+ volume=target_volume,
131
+ ramp_rate=target_ramp_rate,
132
+ hold_time_seconds=hold_time,
133
+ )
134
+ await thermocycler_hardware.wait_for_block_target()
135
+
136
+ task = await self._task_handler.create_task(
137
+ task_function=set_target_block_temperature, id=params.taskId
138
+ )
102
139
 
103
140
  return SuccessData(
104
141
  public=SetTargetBlockTemperatureResult(
105
- targetBlockTemperature=target_temperature
142
+ targetBlockTemperature=target_temperature, taskId=task.id
106
143
  ),
107
144
  )
108
145