opentrons 8.7.0a0__py3-none-any.whl → 8.7.0a2__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.

Potentially problematic release.


This version of opentrons might be problematic. Click here for more details.

Files changed (121) hide show
  1. opentrons/_version.py +2 -2
  2. opentrons/drivers/thermocycler/abstract.py +1 -0
  3. opentrons/drivers/thermocycler/driver.py +33 -4
  4. opentrons/drivers/thermocycler/simulator.py +2 -0
  5. opentrons/hardware_control/api.py +24 -5
  6. opentrons/hardware_control/backends/controller.py +8 -2
  7. opentrons/hardware_control/backends/ot3controller.py +3 -0
  8. opentrons/hardware_control/backends/ot3simulator.py +2 -1
  9. opentrons/hardware_control/backends/simulator.py +2 -1
  10. opentrons/hardware_control/backends/subsystem_manager.py +5 -2
  11. opentrons/hardware_control/module_control.py +82 -8
  12. opentrons/hardware_control/modules/__init__.py +3 -0
  13. opentrons/hardware_control/modules/absorbance_reader.py +11 -4
  14. opentrons/hardware_control/modules/flex_stacker.py +38 -9
  15. opentrons/hardware_control/modules/heater_shaker.py +30 -5
  16. opentrons/hardware_control/modules/magdeck.py +8 -4
  17. opentrons/hardware_control/modules/mod_abc.py +13 -5
  18. opentrons/hardware_control/modules/tempdeck.py +25 -5
  19. opentrons/hardware_control/modules/thermocycler.py +56 -10
  20. opentrons/hardware_control/modules/types.py +20 -1
  21. opentrons/hardware_control/modules/utils.py +11 -4
  22. opentrons/hardware_control/nozzle_manager.py +3 -0
  23. opentrons/hardware_control/ot3api.py +26 -5
  24. opentrons/hardware_control/scripts/update_module_fw.py +5 -0
  25. opentrons/hardware_control/types.py +31 -2
  26. opentrons/legacy_commands/protocol_commands.py +20 -0
  27. opentrons/legacy_commands/types.py +42 -0
  28. opentrons/motion_planning/waypoints.py +15 -29
  29. opentrons/protocol_api/__init__.py +5 -0
  30. opentrons/protocol_api/_types.py +6 -1
  31. opentrons/protocol_api/core/common.py +3 -1
  32. opentrons/protocol_api/core/engine/_default_labware_versions.py +32 -11
  33. opentrons/protocol_api/core/engine/_default_liquid_class_versions.py +2 -0
  34. opentrons/protocol_api/core/engine/labware.py +8 -1
  35. opentrons/protocol_api/core/engine/module_core.py +4 -0
  36. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +77 -17
  37. opentrons/protocol_api/core/engine/protocol.py +18 -1
  38. opentrons/protocol_api/core/engine/tasks.py +35 -0
  39. opentrons/protocol_api/core/legacy/legacy_module_core.py +2 -0
  40. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +11 -1
  41. opentrons/protocol_api/core/legacy/tasks.py +19 -0
  42. opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +14 -2
  43. opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
  44. opentrons/protocol_api/core/module.py +1 -0
  45. opentrons/protocol_api/core/protocol.py +11 -2
  46. opentrons/protocol_api/core/tasks.py +31 -0
  47. opentrons/protocol_api/module_contexts.py +1 -0
  48. opentrons/protocol_api/protocol_context.py +26 -4
  49. opentrons/protocol_api/robot_context.py +38 -21
  50. opentrons/protocol_api/tasks.py +48 -0
  51. opentrons/protocol_api/validation.py +6 -1
  52. opentrons/protocol_engine/actions/__init__.py +4 -2
  53. opentrons/protocol_engine/actions/actions.py +22 -9
  54. opentrons/protocol_engine/clients/sync_client.py +6 -7
  55. opentrons/protocol_engine/commands/__init__.py +42 -0
  56. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +2 -15
  57. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +2 -15
  58. opentrons/protocol_engine/commands/aspirate.py +1 -0
  59. opentrons/protocol_engine/commands/command.py +1 -0
  60. opentrons/protocol_engine/commands/command_unions.py +39 -0
  61. opentrons/protocol_engine/commands/create_timer.py +83 -0
  62. opentrons/protocol_engine/commands/dispense.py +1 -0
  63. opentrons/protocol_engine/commands/drop_tip.py +32 -8
  64. opentrons/protocol_engine/commands/movement_common.py +2 -0
  65. opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
  66. opentrons/protocol_engine/commands/set_tip_state.py +97 -0
  67. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +6 -0
  68. opentrons/protocol_engine/commands/thermocycler/run_profile.py +8 -0
  69. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +17 -1
  70. opentrons/protocol_engine/commands/touch_tip.py +1 -1
  71. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +6 -22
  72. opentrons/protocol_engine/commands/wait_for_tasks.py +98 -0
  73. opentrons/protocol_engine/errors/__init__.py +4 -0
  74. opentrons/protocol_engine/errors/exceptions.py +55 -0
  75. opentrons/protocol_engine/execution/__init__.py +2 -0
  76. opentrons/protocol_engine/execution/command_executor.py +8 -0
  77. opentrons/protocol_engine/execution/create_queue_worker.py +5 -1
  78. opentrons/protocol_engine/execution/labware_movement.py +9 -12
  79. opentrons/protocol_engine/execution/movement.py +2 -0
  80. opentrons/protocol_engine/execution/queue_worker.py +4 -0
  81. opentrons/protocol_engine/execution/run_control.py +8 -0
  82. opentrons/protocol_engine/execution/task_handler.py +157 -0
  83. opentrons/protocol_engine/protocol_engine.py +67 -33
  84. opentrons/protocol_engine/resources/__init__.py +2 -0
  85. opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
  86. opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
  87. opentrons/protocol_engine/resources/labware_validation.py +10 -6
  88. opentrons/protocol_engine/state/_well_math.py +60 -18
  89. opentrons/protocol_engine/state/addressable_areas.py +2 -0
  90. opentrons/protocol_engine/state/commands.py +7 -7
  91. opentrons/protocol_engine/state/geometry.py +237 -379
  92. opentrons/protocol_engine/state/labware.py +52 -102
  93. opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
  94. opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1331 -0
  95. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
  96. opentrons/protocol_engine/state/modules.py +26 -7
  97. opentrons/protocol_engine/state/motion.py +44 -0
  98. opentrons/protocol_engine/state/state.py +14 -0
  99. opentrons/protocol_engine/state/state_summary.py +2 -0
  100. opentrons/protocol_engine/state/tasks.py +139 -0
  101. opentrons/protocol_engine/state/tips.py +177 -258
  102. opentrons/protocol_engine/state/update_types.py +16 -9
  103. opentrons/protocol_engine/types/__init__.py +9 -3
  104. opentrons/protocol_engine/types/deck_configuration.py +5 -1
  105. opentrons/protocol_engine/types/instrument.py +8 -1
  106. opentrons/protocol_engine/types/labware.py +1 -13
  107. opentrons/protocol_engine/types/module.py +10 -0
  108. opentrons/protocol_engine/types/tasks.py +38 -0
  109. opentrons/protocol_engine/types/tip.py +9 -0
  110. opentrons/protocol_runner/create_simulating_orchestrator.py +29 -2
  111. opentrons/protocol_runner/run_orchestrator.py +18 -2
  112. opentrons/protocols/api_support/definitions.py +1 -1
  113. opentrons/protocols/api_support/types.py +2 -1
  114. opentrons/simulate.py +48 -15
  115. opentrons/system/camera.py +1 -1
  116. {opentrons-8.7.0a0.dist-info → opentrons-8.7.0a2.dist-info}/METADATA +4 -4
  117. {opentrons-8.7.0a0.dist-info → opentrons-8.7.0a2.dist-info}/RECORD +120 -107
  118. opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
  119. {opentrons-8.7.0a0.dist-info → opentrons-8.7.0a2.dist-info}/WHEEL +0 -0
  120. {opentrons-8.7.0a0.dist-info → opentrons-8.7.0a2.dist-info}/entry_points.txt +0 -0
  121. {opentrons-8.7.0a0.dist-info → opentrons-8.7.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -2,12 +2,26 @@ from asyncio import Queue
2
2
  import enum
3
3
  import logging
4
4
  from dataclasses import dataclass
5
- from typing import cast, Tuple, Union, List, Callable, Dict, TypeVar, Type
5
+ from typing import (
6
+ cast,
7
+ Tuple,
8
+ Union,
9
+ List,
10
+ Callable,
11
+ Dict,
12
+ TypeVar,
13
+ Type,
14
+ TYPE_CHECKING,
15
+ )
6
16
  from typing_extensions import Literal
7
17
  from opentrons import types as top_types
8
18
  from opentrons_shared_data.pipette.types import PipetteChannelType
19
+ from opentrons_shared_data.errors.exceptions import EnumeratedError
9
20
  from opentrons.config import feature_flags
10
21
 
22
+ if TYPE_CHECKING:
23
+ from .modules.types import ModuleModel
24
+
11
25
  MODULE_LOG = logging.getLogger(__name__)
12
26
 
13
27
 
@@ -384,6 +398,7 @@ class HardwareEventType(enum.Enum):
384
398
  DOOR_SWITCH_CHANGE = enum.auto()
385
399
  ERROR_MESSAGE = enum.auto()
386
400
  ESTOP_CHANGE = enum.auto()
401
+ ASYNCHRONOUS_MODULE_ERROR = enum.auto()
387
402
 
388
403
 
389
404
  @dataclass
@@ -428,10 +443,24 @@ class ErrorMessageNotification:
428
443
  event: Literal[HardwareEventType.ERROR_MESSAGE] = HardwareEventType.ERROR_MESSAGE
429
444
 
430
445
 
446
+ @dataclass(frozen=True)
447
+ class AsynchronousModuleErrorNotification:
448
+ exception: EnumeratedError
449
+ module_serial: str | None
450
+ module_model: "ModuleModel"
451
+ port: str
452
+ event: Literal[
453
+ HardwareEventType.ASYNCHRONOUS_MODULE_ERROR
454
+ ] = HardwareEventType.ASYNCHRONOUS_MODULE_ERROR
455
+
456
+
431
457
  # new event types get new dataclasses
432
458
  # when we add more event types we add them here
433
459
  HardwareEvent = Union[
434
- DoorStateNotification, ErrorMessageNotification, EstopStateNotification
460
+ DoorStateNotification,
461
+ ErrorMessageNotification,
462
+ EstopStateNotification,
463
+ AsynchronousModuleErrorNotification,
435
464
  ]
436
465
 
437
466
  HardwareEventHandler = Callable[[HardwareEvent], None]
@@ -1,6 +1,7 @@
1
1
  from datetime import timedelta
2
2
  from typing import Optional
3
3
  from . import types as command_types
4
+ from opentrons.protocol_api.tasks import Task
4
5
 
5
6
 
6
7
  def comment(msg: str) -> command_types.CommentCommand:
@@ -52,3 +53,22 @@ def move_labware(text: str) -> command_types.MoveLabwareCommand:
52
53
  "name": command_types.MOVE_LABWARE,
53
54
  "payload": {"text": text},
54
55
  }
56
+
57
+
58
+ def wait_for_tasks(tasks: list[Task]) -> command_types.WaitForTasksCommand:
59
+ task_ids = [task.created_at.strftime("%Y-%m-%d %H:%M:%S") for task in tasks]
60
+ msg = f"Waiting for tasks that started at: {task_ids}."
61
+ return {
62
+ "name": command_types.WAIT_FOR_TASKS,
63
+ "payload": {"text": msg},
64
+ }
65
+
66
+
67
+ def create_timer(seconds: float) -> command_types.CreateTimerCommand:
68
+ return {
69
+ "name": command_types.CREATE_TIMER,
70
+ "payload": {
71
+ "text": f"Creating background timer for {seconds} seconds.",
72
+ "time": seconds,
73
+ },
74
+ }
@@ -102,6 +102,10 @@ ROBOT_MOVE_RELATIVE_TO: Final = "command.ROBOT_MOVE_RELATIVE_TO"
102
102
  ROBOT_OPEN_GRIPPER_JAW: Final = "command.ROBOT_OPEN_GRIPPER_JAW"
103
103
  ROBOT_CLOSE_GRIPPER_JAW: Final = "command.ROBOT_CLOSE_GRIPPER_JAW"
104
104
 
105
+ # Tasks #
106
+ WAIT_FOR_TASKS: Final = "command.WAIT_FOR_TASKS"
107
+ CREATE_TIMER: Final = "command.CREATE_TIMER"
108
+
105
109
 
106
110
  class TextOnlyPayload(TypedDict):
107
111
  text: str
@@ -714,6 +718,27 @@ class RobotCloseGripperJawCommand(TypedDict):
714
718
  payload: GripperCommandPayload
715
719
 
716
720
 
721
+ # Task Commands and Payloads
722
+
723
+
724
+ class WaitForTasksPayload(TextOnlyPayload):
725
+ pass
726
+
727
+
728
+ class CreateTimerPayload(TextOnlyPayload):
729
+ time: float
730
+
731
+
732
+ class WaitForTasksCommand(TypedDict):
733
+ name: Literal["command.WAIT_FOR_TASKS"]
734
+ payload: WaitForTasksPayload
735
+
736
+
737
+ class CreateTimerCommand(TypedDict):
738
+ name: Literal["command.CREATE_TIMER"]
739
+ payload: CreateTimerPayload
740
+
741
+
717
742
  Command = Union[
718
743
  DropTipCommand,
719
744
  DropTipInDisposalLocationCommand,
@@ -782,6 +807,9 @@ Command = Union[
782
807
  FlexStackerStoreCommand,
783
808
  FlexStackerEmptyCommand,
784
809
  FlexStackerFillCommand,
810
+ # Task commands
811
+ WaitForTasksCommand,
812
+ CreateTimerCommand,
785
813
  ]
786
814
 
787
815
 
@@ -842,6 +870,9 @@ CommandPayload = Union[
842
870
  RobotMoveAxisRelativeCommandPayload,
843
871
  RobotMoveAxisToCommandPayload,
844
872
  GripperCommandPayload,
873
+ # Task payloads
874
+ WaitForTasksPayload,
875
+ CreateTimerPayload,
845
876
  ]
846
877
 
847
878
 
@@ -1124,6 +1155,14 @@ class RobotCloseGripperJawMessage(CommandMessageFields, RobotCloseGripperJawComm
1124
1155
  pass
1125
1156
 
1126
1157
 
1158
+ class WaitForTasksMessage(CommandMessageFields, WaitForTasksCommand):
1159
+ pass
1160
+
1161
+
1162
+ class CreateTimerMessage(CommandMessageFields, CreateTimerCommand):
1163
+ pass
1164
+
1165
+
1127
1166
  CommandMessage = Union[
1128
1167
  DropTipMessage,
1129
1168
  DropTipInDisposalLocationMessage,
@@ -1183,4 +1222,7 @@ CommandMessage = Union[
1183
1222
  FlexStackerStoreMessage,
1184
1223
  FlexStackerEmptyMessage,
1185
1224
  FlexStackerFillMessage,
1225
+ # Task Messages
1226
+ WaitForTasksMessage,
1227
+ CreateTimerMessage,
1186
1228
  ]
@@ -7,7 +7,6 @@ from opentrons.hardware_control.types import CriticalPoint
7
7
 
8
8
  from .types import Waypoint, MoveType, GripperMovementWaypointsWithJawStatus
9
9
  from .errors import DestinationOutOfBoundsError, ArcOutOfBoundsError
10
- from ..protocol_engine.types import LabwareMovementOffsetData
11
10
 
12
11
  DEFAULT_GENERAL_ARC_Z_MARGIN: Final[float] = 10.0
13
12
  DEFAULT_IN_LABWARE_ARC_Z_MARGIN: Final[float] = 5.0
@@ -125,47 +124,41 @@ def get_gripper_labware_movement_waypoints(
125
124
  from_labware_center: Point,
126
125
  to_labware_center: Point,
127
126
  gripper_home_z: float,
128
- offset_data: LabwareMovementOffsetData,
129
127
  post_drop_slide_offset: Optional[Point],
130
128
  gripper_home_z_offset: Optional[float] = None,
131
129
  ) -> List[GripperMovementWaypointsWithJawStatus]:
132
130
  """Get waypoints for moving labware using a gripper."""
133
- pick_up_offset = offset_data.pickUpOffset
134
- drop_offset = offset_data.dropOffset
135
-
136
- pick_up_location = from_labware_center + Point(
137
- pick_up_offset.x, pick_up_offset.y, pick_up_offset.z
138
- )
139
- drop_location = to_labware_center + Point(
140
- drop_offset.x, drop_offset.y, drop_offset.z
141
- )
142
-
143
131
  gripper_max_z_home = gripper_home_z - (gripper_home_z_offset or 0)
144
-
145
- post_drop_home_pos = Point(drop_location.x, drop_location.y, gripper_home_z)
132
+ post_drop_home_pos = Point(to_labware_center.x, to_labware_center.y, gripper_home_z)
146
133
 
147
134
  waypoints_with_jaw_status = [
148
135
  GripperMovementWaypointsWithJawStatus(
149
- position=Point(pick_up_location.x, pick_up_location.y, gripper_home_z),
136
+ position=Point(
137
+ from_labware_center.x, from_labware_center.y, gripper_home_z
138
+ ),
150
139
  jaw_open=False,
151
140
  dropping=False,
152
141
  ),
153
142
  GripperMovementWaypointsWithJawStatus(
154
- position=pick_up_location, jaw_open=True, dropping=False
143
+ position=from_labware_center, jaw_open=True, dropping=False
155
144
  ),
156
145
  # Gripper grips the labware here
157
146
  GripperMovementWaypointsWithJawStatus(
158
- position=Point(pick_up_location.x, pick_up_location.y, gripper_max_z_home),
147
+ position=Point(
148
+ from_labware_center.x, from_labware_center.y, gripper_max_z_home
149
+ ),
159
150
  jaw_open=False,
160
151
  dropping=False,
161
152
  ),
162
153
  GripperMovementWaypointsWithJawStatus(
163
- position=Point(drop_location.x, drop_location.y, gripper_max_z_home),
154
+ position=Point(
155
+ to_labware_center.x, to_labware_center.y, gripper_max_z_home
156
+ ),
164
157
  jaw_open=False,
165
158
  dropping=False,
166
159
  ),
167
160
  GripperMovementWaypointsWithJawStatus(
168
- position=drop_location, jaw_open=False, dropping=False
161
+ position=to_labware_center, jaw_open=False, dropping=False
169
162
  ),
170
163
  # Gripper ungrips here
171
164
  GripperMovementWaypointsWithJawStatus(
@@ -189,25 +182,18 @@ def get_gripper_labware_movement_waypoints(
189
182
  def get_gripper_labware_placement_waypoints(
190
183
  to_labware_center: Point,
191
184
  gripper_home_z: float,
192
- drop_offset: Optional[Point],
193
185
  ) -> List[GripperMovementWaypointsWithJawStatus]:
194
186
  """Get waypoints for placing labware using a gripper."""
195
- drop_offset = drop_offset or Point()
196
-
197
- drop_location = to_labware_center + Point(
198
- drop_offset.x, drop_offset.y, drop_offset.z
199
- )
200
-
201
- post_drop_home_pos = Point(drop_location.x, drop_location.y, gripper_home_z)
187
+ post_drop_home_pos = Point(to_labware_center.x, to_labware_center.y, gripper_home_z)
202
188
 
203
189
  return [
204
190
  GripperMovementWaypointsWithJawStatus(
205
- position=Point(drop_location.x, drop_location.y, gripper_home_z),
191
+ position=Point(to_labware_center.x, to_labware_center.y, gripper_home_z),
206
192
  jaw_open=False,
207
193
  dropping=False,
208
194
  ),
209
195
  GripperMovementWaypointsWithJawStatus(
210
- position=drop_location, jaw_open=False, dropping=False
196
+ position=to_labware_center, jaw_open=False, dropping=False
211
197
  ),
212
198
  # Gripper ungrips here
213
199
  GripperMovementWaypointsWithJawStatus(
@@ -30,9 +30,11 @@ from .module_contexts import (
30
30
  AbsorbanceReaderContext,
31
31
  FlexStackerContext,
32
32
  )
33
+ from .tasks import Task
33
34
  from .disposal_locations import TrashBin, WasteChute
34
35
  from ._liquid import Liquid, LiquidClass
35
36
  from ._types import (
37
+ OffDeckType,
36
38
  OFF_DECK,
37
39
  PLUNGER_BLOWOUT,
38
40
  PLUNGER_TOP,
@@ -88,6 +90,7 @@ __all__ = [
88
90
  "ROW",
89
91
  "ALL",
90
92
  # Deck location types
93
+ "OffDeckType",
91
94
  "OFF_DECK",
92
95
  # Pipette plunger types
93
96
  "PLUNGER_BLOWOUT",
@@ -99,6 +102,8 @@ __all__ = [
99
102
  "BLOWOUT_ACTION",
100
103
  "RuntimeParameterRequiredError",
101
104
  "CSVParameter",
105
+ # Concurrent task types
106
+ "Task",
102
107
  # For internal Opentrons use only:
103
108
  "create_protocol_context",
104
109
  "ProtocolEngineCoreRequiredError",
@@ -3,8 +3,13 @@ from typing_extensions import Final
3
3
  import enum
4
4
 
5
5
 
6
- # TODO (tz, 5-18-23): think about a better name for it that would also work when we include staging area slots in the type.
6
+ # Implemented with an enum to support type narrowing via `== OFF_DECK`.
7
7
  class OffDeckType(enum.Enum):
8
+ """The type of the :py:obj:`OFF_DECK` constant.
9
+
10
+ Do not use directly, except in type annotations and ``isinstance`` calls.
11
+ """
12
+
8
13
  OFF_DECK = "off-deck"
9
14
 
10
15
 
@@ -16,6 +16,7 @@ from .module import (
16
16
  from .protocol import AbstractProtocol
17
17
  from .well import AbstractWellCore
18
18
  from .robot import AbstractRobot
19
+ from .tasks import AbstractTaskCore
19
20
 
20
21
 
21
22
  WellCore = AbstractWellCore
@@ -30,4 +31,5 @@ MagneticBlockCore = AbstractMagneticBlockCore[LabwareCore]
30
31
  AbsorbanceReaderCore = AbstractAbsorbanceReaderCore[LabwareCore]
31
32
  FlexStackerCore = AbstractFlexStackerCore[LabwareCore]
32
33
  RobotCore = AbstractRobot
33
- ProtocolCore = AbstractProtocol[InstrumentCore, LabwareCore, ModuleCore]
34
+ TaskCore = AbstractTaskCore
35
+ ProtocolCore = AbstractProtocol[InstrumentCore, LabwareCore, ModuleCore, TaskCore]
@@ -112,6 +112,37 @@ DEFAULT_LABWARE_VERSIONS: DefaultLabwareVersions = {
112
112
  "thermoscientificnunc_96_wellplate_2000ul": 3,
113
113
  "usascientific_96_wellplate_2.4ml_deep": 3,
114
114
  },
115
+ APIVersion(2, 27): {
116
+ "agilent_1_reservoir_290ml": 4,
117
+ "axygen_1_reservoir_90ml": 3,
118
+ "biorad_96_wellplate_200ul_pcr": 5,
119
+ "corning_12_wellplate_6.9ml_flat": 5,
120
+ "corning_24_wellplate_3.4ml_flat": 5,
121
+ "corning_384_wellplate_112ul_flat": 5,
122
+ "corning_48_wellplate_1.6ml_flat": 6,
123
+ "corning_6_wellplate_16.8ml_flat": 5,
124
+ "corning_96_wellplate_360ul_flat": 5,
125
+ "nest_12_reservoir_15ml": 3,
126
+ "nest_1_reservoir_195ml": 4,
127
+ "nest_1_reservoir_290ml": 4,
128
+ "nest_96_wellplate_100ul_pcr_full_skirt": 5,
129
+ "nest_96_wellplate_200ul_flat": 5,
130
+ "nest_96_wellplate_2ml_deep": 5,
131
+ "opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical": 3,
132
+ "opentrons_15_tuberack_falcon_15ml_conical": 3,
133
+ "opentrons_24_aluminumblock_nest_0.5ml_screwcap": 4,
134
+ "opentrons_24_aluminumblock_nest_1.5ml_screwcap": 3,
135
+ "opentrons_24_aluminumblock_nest_1.5ml_snapcap": 3,
136
+ "opentrons_24_aluminumblock_nest_2ml_screwcap": 3,
137
+ "opentrons_24_aluminumblock_nest_2ml_snapcap": 3,
138
+ "opentrons_24_tuberack_eppendorf_1.5ml_safelock_snapcap": 3,
139
+ "opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap": 3,
140
+ "opentrons_24_tuberack_nest_0.5ml_screwcap": 4,
141
+ "opentrons_6_tuberack_nest_50ml_conical": 3,
142
+ "opentrons_96_aluminumblock_generic_pcr_strip_200ul": 4,
143
+ "usascientific_12_reservoir_22ml": 4,
144
+ "usascientific_96_wellplate_2.4ml_deep": 4,
145
+ },
115
146
  }
116
147
 
117
148
 
@@ -139,17 +170,7 @@ KNOWN_EXCEPTIONS_FOR_TESTS: set[str] = {
139
170
  "schema3test_flex_tiprack_lid",
140
171
  "schema3test_tough_pcr_auto_sealing_lid",
141
172
  "schema3test_universal_flat_adapter",
142
- # These were supposed to be short-lived drafts as part of of a one-two punch of
143
- # https://github.com/Opentrons/opentrons/pull/18266 + https://github.com/Opentrons/opentrons/pull/18284,
144
- # but the second punch took a while. We should merge the second punch after v8.6.0
145
- # and remove these exceptions as part of that.
146
- "agilent_1_reservoir_290ml",
147
- "corning_384_wellplate_112ul_flat",
148
- "nest_1_reservoir_290ml",
149
- "opentrons_24_aluminumblock_nest_0.5ml_screwcap",
150
- "opentrons_24_tuberack_nest_0.5ml_screwcap",
151
- "opentrons_96_aluminumblock_generic_pcr_strip_200ul",
152
- "usascientific_12_reservoir_22ml",
173
+ "schema3test_96_wellplate_360ul_flat",
153
174
  }
154
175
 
155
176
 
@@ -28,6 +28,8 @@ DefaultLiquidClassVersions: TypeAlias = dict[APIVersion, dict[str, int]]
28
28
  # [any] [anything else] 1
29
29
  DEFAULT_LIQUID_CLASS_VERSIONS: DefaultLiquidClassVersions = {
30
30
  APIVersion(2, 26): {
31
+ "ethanol_80": 2,
32
+ "glycerol_50": 2,
31
33
  "water": 2,
32
34
  },
33
35
  }
@@ -23,6 +23,7 @@ from opentrons.protocol_engine.clients import SyncClient as ProtocolEngineClient
23
23
  from opentrons.protocol_engine.types import (
24
24
  LabwareOffsetCreate,
25
25
  LabwareOffsetVector,
26
+ TipRackWellState,
26
27
  )
27
28
  from opentrons.types import DeckSlotName, NozzleMapInterface, Point, StagingSlotName
28
29
 
@@ -165,7 +166,13 @@ class LabwareCore(AbstractLabware[WellCore]):
165
166
 
166
167
  def reset_tips(self) -> None:
167
168
  if self.is_tip_rack():
168
- self._engine_client.reset_tips(labware_id=self.labware_id)
169
+ self._engine_client.execute_command(
170
+ cmd.SetTipStateParams(
171
+ labwareId=self._labware_id,
172
+ wellNames=list(self._definition.wells),
173
+ tipWellState=TipRackWellState.CLEAN,
174
+ )
175
+ )
169
176
  else:
170
177
  raise TypeError(f"{self.get_display_name()} is not a tip rack.")
171
178
 
@@ -317,6 +317,7 @@ class ThermocyclerModuleCore(ModuleCore, AbstractThermocyclerCore[LabwareCore]):
317
317
  def set_target_block_temperature(
318
318
  self,
319
319
  celsius: float,
320
+ ramp_rate: Optional[float],
320
321
  hold_time_seconds: Optional[float] = None,
321
322
  block_max_volume: Optional[float] = None,
322
323
  ) -> None:
@@ -327,6 +328,7 @@ class ThermocyclerModuleCore(ModuleCore, AbstractThermocyclerCore[LabwareCore]):
327
328
  celsius=celsius,
328
329
  blockMaxVolumeUl=block_max_volume,
329
330
  holdTimeSeconds=hold_time_seconds,
331
+ ramp_rate=ramp_rate,
330
332
  )
331
333
  )
332
334
 
@@ -361,6 +363,7 @@ class ThermocyclerModuleCore(ModuleCore, AbstractThermocyclerCore[LabwareCore]):
361
363
  cmd.thermocycler.RunProfileStepParams(
362
364
  celsius=step["temperature"],
363
365
  holdSeconds=step["hold_time_seconds"],
366
+ rampRate=step["ramp_rate"],
364
367
  )
365
368
  for step in steps
366
369
  ]
@@ -389,6 +392,7 @@ class ThermocyclerModuleCore(ModuleCore, AbstractThermocyclerCore[LabwareCore]):
389
392
  cmd.thermocycler.ProfileStep(
390
393
  celsius=step["temperature"],
391
394
  holdSeconds=step["hold_time_seconds"],
395
+ rampRate=step["ramp_rate"],
392
396
  )
393
397
  for step in steps
394
398
  ],
@@ -21,7 +21,11 @@ from opentrons.protocol_engine import (
21
21
  OnLabwareLocation,
22
22
  DropTipWellLocation,
23
23
  )
24
- from opentrons.protocol_engine.types import StagingSlotLocation, WellLocationType
24
+ from opentrons.protocol_engine.types import (
25
+ StagingSlotLocation,
26
+ WellLocationType,
27
+ LoadedModule,
28
+ )
25
29
  from opentrons.types import DeckSlotName, StagingSlotName, Point
26
30
  from . import point_calculations
27
31
 
@@ -136,22 +140,47 @@ def check_safe_for_pipette_movement( # noqa: C901
136
140
  f" will result in collision with thermocycler lid in deck slot A1."
137
141
  )
138
142
 
143
+ def _check_for_column_4_module_collision(slot: DeckSlotName) -> None:
144
+ slot_module = engine_state.modules.get_by_slot(slot)
145
+ if (
146
+ slot_module
147
+ and engine_state.modules.is_column_4_module(slot_module.model)
148
+ and _slot_has_potential_colliding_object(
149
+ engine_state=engine_state,
150
+ pipette_bounds=pipette_bounds_at_well_location,
151
+ surrounding_location=slot_module,
152
+ )
153
+ ):
154
+ raise PartialTipMovementNotAllowedError(
155
+ f"Moving to {engine_state.labware.get_display_name(labware_id)} in slot"
156
+ f" {slot} with {primary_nozzle} nozzle partial configuration will"
157
+ f" result in collision with items on {slot_module.model} mounted in {slot}."
158
+ )
159
+
160
+ # We check the labware slot for a module that is mounted in the same cutout
161
+ # as the labwares slot but does not occupy the same heirarchy (like the stacker).
162
+ _check_for_column_4_module_collision(labware_slot)
163
+
139
164
  for regular_slot in surrounding_slots.regular_slots:
140
165
  if _slot_has_potential_colliding_object(
141
166
  engine_state=engine_state,
142
167
  pipette_bounds=pipette_bounds_at_well_location,
143
- surrounding_slot=regular_slot,
168
+ surrounding_location=regular_slot,
144
169
  ):
145
170
  raise PartialTipMovementNotAllowedError(
146
171
  f"Moving to {engine_state.labware.get_display_name(labware_id)} in slot"
147
172
  f" {labware_slot} with {primary_nozzle} nozzle partial configuration"
148
173
  f" will result in collision with items in deck slot {regular_slot}."
149
174
  )
175
+
176
+ # Check for Column 4 Modules that may be descendants of a given surrounding slot
177
+ _check_for_column_4_module_collision(regular_slot)
178
+
150
179
  for staging_slot in surrounding_slots.staging_slots:
151
180
  if _slot_has_potential_colliding_object(
152
181
  engine_state=engine_state,
153
182
  pipette_bounds=pipette_bounds_at_well_location,
154
- surrounding_slot=staging_slot,
183
+ surrounding_location=staging_slot,
155
184
  ):
156
185
  raise PartialTipMovementNotAllowedError(
157
186
  f"Moving to {engine_state.labware.get_display_name(labware_id)} in slot"
@@ -178,18 +207,45 @@ def _get_critical_point_to_use(
178
207
  def _slot_has_potential_colliding_object(
179
208
  engine_state: StateView,
180
209
  pipette_bounds: Tuple[Point, Point, Point, Point],
181
- surrounding_slot: Union[DeckSlotName, StagingSlotName],
210
+ surrounding_location: Union[DeckSlotName, StagingSlotName, LoadedModule],
182
211
  ) -> bool:
183
- """Return the slot, if any, that has an item that the pipette might collide into."""
184
- # Check if slot overlaps with pipette position
185
- slot_pos = engine_state.addressable_areas.get_addressable_area_position(
186
- addressable_area_name=surrounding_slot.id,
187
- do_compatibility_check=False,
188
- )
189
- slot_bounds = engine_state.addressable_areas.get_addressable_area_bounding_box(
190
- addressable_area_name=surrounding_slot.id,
191
- do_compatibility_check=False,
192
- )
212
+ """Return the slot, if any, that has an item that the pipette might collide into.
213
+ Can be provided a Deck Slot, Staging Slot, or Column 4 Module.
214
+ """
215
+ if isinstance(surrounding_location, LoadedModule):
216
+ if (
217
+ engine_state.modules.is_column_4_module(surrounding_location.model)
218
+ and surrounding_location.location is not None
219
+ ):
220
+ module_area = (
221
+ engine_state.modules.ensure_and_convert_module_fixture_location(
222
+ surrounding_location.location.slotName, surrounding_location.model
223
+ )
224
+ )
225
+ slot_pos = engine_state.addressable_areas.get_addressable_area_position(
226
+ addressable_area_name=module_area,
227
+ do_compatibility_check=False,
228
+ )
229
+ slot_bounds = (
230
+ engine_state.addressable_areas.get_addressable_area_bounding_box(
231
+ addressable_area_name=module_area,
232
+ do_compatibility_check=False,
233
+ )
234
+ )
235
+ else:
236
+ raise ValueError(
237
+ f"Error during collision validation, Module {surrounding_location.model} must be in Column 4."
238
+ )
239
+ else:
240
+ # Check if slot overlaps with pipette position
241
+ slot_pos = engine_state.addressable_areas.get_addressable_area_position(
242
+ addressable_area_name=surrounding_location.id,
243
+ do_compatibility_check=False,
244
+ )
245
+ slot_bounds = engine_state.addressable_areas.get_addressable_area_bounding_box(
246
+ addressable_area_name=surrounding_location.id,
247
+ do_compatibility_check=False,
248
+ )
193
249
  slot_back_left_coords = Point(slot_pos.x, slot_pos.y + slot_bounds.y, slot_pos.z)
194
250
  slot_front_right_coords = Point(slot_pos.x + slot_bounds.x, slot_pos.y, slot_pos.z)
195
251
 
@@ -199,13 +255,17 @@ def _slot_has_potential_colliding_object(
199
255
  rectangle2=(slot_back_left_coords, slot_front_right_coords),
200
256
  ):
201
257
  # Check z-height of items in overlapping slot
202
- if isinstance(surrounding_slot, DeckSlotName):
258
+ if isinstance(surrounding_location, DeckSlotName):
203
259
  slot_highest_z = engine_state.geometry.get_highest_z_in_slot(
204
- DeckSlotLocation(slotName=surrounding_slot)
260
+ DeckSlotLocation(slotName=surrounding_location)
261
+ )
262
+ elif isinstance(surrounding_location, LoadedModule):
263
+ slot_highest_z = engine_state.geometry.get_highest_z_of_column_4_module(
264
+ surrounding_location
205
265
  )
206
266
  else:
207
267
  slot_highest_z = engine_state.geometry.get_highest_z_in_slot(
208
- StagingSlotLocation(slotName=surrounding_slot)
268
+ StagingSlotLocation(slotName=surrounding_location)
209
269
  )
210
270
  return slot_highest_z >= pipette_bounds[0].z
211
271
  return False
@@ -1,7 +1,7 @@
1
1
  """ProtocolEngine-based Protocol API core implementation."""
2
2
 
3
3
  from __future__ import annotations
4
- from typing import Dict, Optional, Type, Union, List, Tuple, TYPE_CHECKING
4
+ from typing import Dict, Optional, Type, Union, List, Tuple, TYPE_CHECKING, Sequence
5
5
 
6
6
  from opentrons_shared_data.liquid_classes import LiquidClassDefinitionDoesNotExist
7
7
  from opentrons_shared_data.deck.types import DeckDefinitionV5, SlotDefV3
@@ -64,6 +64,7 @@ from ...disposal_locations import TrashBin, WasteChute
64
64
  from ..protocol import AbstractProtocol
65
65
  from ..labware import LabwareLoadParams
66
66
  from .labware import LabwareCore
67
+ from .tasks import EngineTaskCore
67
68
  from .instrument import InstrumentCore
68
69
  from .robot import RobotCore
69
70
  from .module_core import (
@@ -95,6 +96,7 @@ class ProtocolCore(
95
96
  InstrumentCore,
96
97
  LabwareCore,
97
98
  Union[ModuleCore, NonConnectedModuleCore],
99
+ EngineTaskCore,
98
100
  ]
99
101
  ):
100
102
  """Protocol API core using a ProtocolEngine.
@@ -875,6 +877,21 @@ class ProtocolCore(
875
877
  cmd.WaitForDurationParams(seconds=seconds, message=msg)
876
878
  )
877
879
 
880
+ def wait_for_tasks(self, task_cores: Sequence[EngineTaskCore]) -> None:
881
+ """Wait for specified tasks to complete."""
882
+ task_ids = [task._id for task in task_cores]
883
+ self._engine_client.execute_command(cmd.WaitForTasksParams(task_ids=task_ids))
884
+
885
+ def create_timer(self, seconds: float) -> EngineTaskCore:
886
+ """Create a timer task that runs in the background."""
887
+ result = self._engine_client.execute_command_without_recovery(
888
+ cmd.CreateTimerParams(time=seconds)
889
+ )
890
+ timer_task = EngineTaskCore(
891
+ engine_client=self._engine_client, task_id=result.task_id
892
+ )
893
+ return timer_task
894
+
878
895
  def home(self) -> None:
879
896
  """Move all axes to their home positions."""
880
897
  self._engine_client.execute_command(cmd.HomeParams(axes=None))
@@ -0,0 +1,35 @@
1
+ from datetime import datetime
2
+ from ..tasks import AbstractTaskCore
3
+ from opentrons.protocol_engine.clients import SyncClient as ProtocolEngineClient
4
+ from opentrons.protocol_engine.errors.exceptions import NoTaskFoundError
5
+
6
+
7
+ class EngineTaskCore(AbstractTaskCore):
8
+ def __init__(self, task_id: str, engine_client: ProtocolEngineClient) -> None:
9
+ self._id = task_id
10
+ self._engine_client = engine_client
11
+
12
+ def get_created_at_timestamp(self) -> datetime:
13
+ task = self._engine_client.state.tasks.get(self._id)
14
+ return task.createdAt
15
+
16
+ def is_done(self) -> bool:
17
+ try:
18
+ self._engine_client.state.tasks.get_finished(self._id)
19
+ return True
20
+ except NoTaskFoundError:
21
+ return False
22
+
23
+ def is_started(self) -> bool:
24
+ try:
25
+ self._engine_client.state.tasks.get_current(self._id)
26
+ return True
27
+ except NoTaskFoundError:
28
+ return self.is_done()
29
+
30
+ def get_finished_at_timestamp(self) -> datetime | None:
31
+ try:
32
+ finished_task = self._engine_client.state.tasks.get_finished(self._id)
33
+ return finished_task.finishedAt
34
+ except NoTaskFoundError:
35
+ return None