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
@@ -1,36 +1,91 @@
1
1
  """File interaction resource provider."""
2
2
  from datetime import datetime
3
+ from io import StringIO
4
+ import csv
3
5
  from typing import List, Optional, Callable, Awaitable, Dict
6
+ from dataclasses import dataclass
4
7
  from pydantic import BaseModel
5
- from ..errors import StorageLimitReachedError
8
+ from opentrons_shared_data.data_files import (
9
+ DataFileInfo,
10
+ MimeType,
11
+ RunFileNameMetadata,
12
+ )
13
+
14
+ SPECIAL_CHARACTERS = {
15
+ "#",
16
+ "%",
17
+ "&",
18
+ "{",
19
+ "}",
20
+ "\\",
21
+ "/",
22
+ "<",
23
+ ">",
24
+ "*",
25
+ "$",
26
+ "!",
27
+ "?",
28
+ ".",
29
+ "'",
30
+ '"',
31
+ ":",
32
+ ";",
33
+ "@",
34
+ "`",
35
+ "|",
36
+ }
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class FileNameCmdMetadata:
41
+ """Command metadata associated with a specific data file."""
42
+
43
+ command_id: str
44
+ prev_command_id: str
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class ReadCmdFileNameMetadata(FileNameCmdMetadata):
49
+ """Data from a plate reader `read` command used to build the finalized file name."""
50
+
51
+ base_filename: str
52
+ wavelength: int
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class ImageCaptureCmdFileNameMetadata(FileNameCmdMetadata):
57
+ """Data from a camera capture command used to build the finalized file name."""
6
58
 
59
+ step_number: int
60
+ command_timestamp: datetime
61
+ base_filename: Optional[str]
7
62
 
8
- MAXIMUM_CSV_FILE_LIMIT = 400
9
63
 
64
+ CommandFileNameMetadata = ReadCmdFileNameMetadata | ImageCaptureCmdFileNameMetadata
10
65
 
11
- class GenericCsvTransform:
12
- """Generic CSV File Type data for rows of data to be seperated by a delimeter."""
13
66
 
14
- filename: str
15
- rows: List[List[str]]
16
- delimiter: str = ","
67
+ class FileData:
68
+ """File data container for writing to a file."""
69
+
70
+ data: bytes
71
+ mime_type: MimeType
72
+ run_metadata: RunFileNameMetadata
73
+ command_metadata: CommandFileNameMetadata
17
74
 
18
75
  @staticmethod
19
76
  def build(
20
- filename: str, rows: List[List[str]], delimiter: str = ","
21
- ) -> "GenericCsvTransform":
22
- """Build a Generic CSV datatype class."""
23
- if "." in filename and not filename.endswith(".csv"):
24
- raise ValueError(
25
- f"Provided filename {filename} invalid. Only CSV file format is accepted."
26
- )
27
- elif "." not in filename:
28
- filename = f"{filename}.csv"
29
- csv = GenericCsvTransform()
30
- csv.filename = filename
31
- csv.rows = rows
32
- csv.delimiter = delimiter
33
- return csv
77
+ data: bytes,
78
+ mime_type: MimeType,
79
+ run_metadata: RunFileNameMetadata,
80
+ command_metadata: CommandFileNameMetadata,
81
+ ) -> "FileData":
82
+ """Build a generic file data class."""
83
+ file_data = FileData()
84
+ file_data.data = data
85
+ file_data.mime_type = mime_type
86
+ file_data.run_metadata = run_metadata
87
+ file_data.command_metadata = command_metadata
88
+ return file_data
34
89
 
35
90
 
36
91
  class ReadData(BaseModel):
@@ -41,7 +96,7 @@ class ReadData(BaseModel):
41
96
 
42
97
 
43
98
  class PlateReaderData(BaseModel):
44
- """Data from a Opentrons Plate Reader Read. Can be converted to CSV template format."""
99
+ """Data from an Opentrons Plate Reader Read. Can be converted to CSV format."""
45
100
 
46
101
  read_results: List[ReadData]
47
102
  reference_wavelength: Optional[int] = None
@@ -49,13 +104,8 @@ class PlateReaderData(BaseModel):
49
104
  finish_time: datetime
50
105
  serial_number: str
51
106
 
52
- def build_generic_csv( # noqa: C901
53
- self, filename: str, measurement: ReadData
54
- ) -> GenericCsvTransform:
55
- """Builds a CSV compatible object containing Plate Reader Measurements.
56
-
57
- This will also automatically reformat the provided filename to include the wavelength of those measurements.
58
- """
107
+ def build_csv_bytes(self, measurement: ReadData) -> bytes: # noqa: C901
108
+ """Builds CSV data as bytes containing Plate Reader Measurements."""
59
109
  plate_alpharows = ["A", "B", "C", "D", "E", "F", "G", "H"]
60
110
  rows = []
61
111
 
@@ -63,7 +113,7 @@ class PlateReaderData(BaseModel):
63
113
  for i in range(8):
64
114
  row = [plate_alpharows[i]]
65
115
  for j in range(12):
66
- row.append(str(measurement.data[f"{plate_alpharows[i]}{j+1}"]))
116
+ row.append(str(measurement.data[f"{plate_alpharows[i]}{j + 1}"]))
67
117
  rows.append(row)
68
118
  for i in range(3):
69
119
  rows.append([])
@@ -116,16 +166,12 @@ class PlateReaderData(BaseModel):
116
166
  ["Measurement finished at", self.finish_time.strftime("%m %d %H:%M:%S %Y")]
117
167
  )
118
168
 
119
- # Ensure the filename adheres to ruleset contains the wavelength for a given measurement
120
- if filename.endswith(".csv"):
121
- filename = filename[:-4]
122
- filename = filename + str(measurement.wavelength) + "nm.csv"
169
+ output = StringIO()
170
+ writer = csv.writer(output, delimiter=",")
171
+ writer.writerows(rows)
172
+ csv_bytes = output.getvalue().encode("utf-8")
123
173
 
124
- return GenericCsvTransform.build(
125
- filename=filename,
126
- rows=rows,
127
- delimiter=",",
128
- )
174
+ return csv_bytes
129
175
 
130
176
 
131
177
  class FileProvider:
@@ -133,29 +179,58 @@ class FileProvider:
133
179
 
134
180
  def __init__(
135
181
  self,
136
- data_files_write_csv_callback: Optional[
137
- Callable[[GenericCsvTransform], Awaitable[str]]
182
+ data_files_write_file_cb: Optional[
183
+ Callable[[FileData], Awaitable[DataFileInfo]]
138
184
  ] = None,
139
- data_files_filecount: Optional[Callable[[], Awaitable[int]]] = None,
140
185
  ) -> None:
141
186
  """Initialize the interface callbacks of the File Provider for data file handling within the Protocol Engine.
142
187
 
143
188
  Params:
144
- data_files_write_csv_callback: Callback to write a CSV file to the data files directory and add it to the database.
189
+ data_files_write_file_callback: Callback to write a file to the data files directory and add it to the database.
145
190
  data_files_filecount: Callback to check the amount of data files already present in the data files directory.
146
191
  """
147
- self._data_files_write_csv_callback = data_files_write_csv_callback
148
- self._data_files_filecount = data_files_filecount
149
-
150
- async def write_csv(self, write_data: GenericCsvTransform) -> str:
151
- """Writes the provided CSV object to a file in the Data Files directory. Returns the File ID of the file created."""
152
- if self._data_files_filecount is not None:
153
- file_count = await self._data_files_filecount()
154
- if file_count >= MAXIMUM_CSV_FILE_LIMIT:
155
- raise StorageLimitReachedError(
156
- f"Not enough space to store file {write_data.filename}."
157
- )
158
- if self._data_files_write_csv_callback is not None:
159
- return await self._data_files_write_csv_callback(write_data)
160
- # If we are in an analysis or simulation state, return an empty file ID
161
- return ""
192
+ self._data_files_write_file_cb = data_files_write_file_cb
193
+ self._run_metadata: RunFileNameMetadata | None = None
194
+
195
+ async def write_file(
196
+ self,
197
+ data: bytes,
198
+ mime_type: MimeType,
199
+ command_metadata: CommandFileNameMetadata,
200
+ ) -> DataFileInfo:
201
+ """Writes arbitrary data to a file in the Data Files directory.
202
+
203
+ Returns the `DataFileInfo` of the file created.
204
+
205
+ Raises:
206
+ Note that the callback may raise a StorageLimitReachedError.
207
+ """
208
+ if self._data_files_write_file_cb is not None:
209
+ assert self._run_metadata is not None
210
+ file_data = FileData.build(
211
+ data=data,
212
+ mime_type=mime_type,
213
+ command_metadata=command_metadata,
214
+ run_metadata=self._run_metadata,
215
+ )
216
+
217
+ return await self._data_files_write_file_cb(file_data)
218
+ # If we are in an analysis or simulation state, return an empty `DataFileInfo`
219
+ return DataFileInfo(
220
+ id="",
221
+ name="",
222
+ file_hash="",
223
+ created_at=datetime.now(),
224
+ generated=True,
225
+ stored=False,
226
+ path="",
227
+ mime_type=mime_type,
228
+ )
229
+
230
+ def set_run_metadata(self, metadata: RunFileNameMetadata) -> None:
231
+ """Sets metadata specific to the run."""
232
+ self._run_metadata = metadata
233
+
234
+ def clear_run_metadata(self) -> None:
235
+ """Clears metadata specific to the run."""
236
+ self._run_metadata = None
@@ -2,6 +2,7 @@
2
2
 
3
3
  from opentrons_shared_data.labware.labware_definition import (
4
4
  LabwareDefinition,
5
+ LabwareDefinition2,
5
6
  LabwareRole,
6
7
  )
7
8
 
@@ -44,15 +45,18 @@ def validate_definition_is_system(definition: LabwareDefinition) -> bool:
44
45
  return LabwareRole.system in definition.allowedRoles
45
46
 
46
47
 
47
- def validate_labware_can_be_stacked(
48
- top_labware_definition: LabwareDefinition, below_labware_load_name: str
48
+ def validate_legacy_labware_can_be_stacked(
49
+ child_labware_definition: LabwareDefinition2, parent_labware_load_name: str
49
50
  ) -> bool:
50
- """Validate that the labware being loaded onto is in the above labware's stackingOffsetWithLabware definition."""
51
+ """Validate that the parent labware is in the child labware's stackingOffsetWithLabware definition.
52
+
53
+ Schema 3 Labware stacking validation is handled in locating features.
54
+ """
51
55
  return (
52
- below_labware_load_name in top_labware_definition.stackingOffsetWithLabware
56
+ parent_labware_load_name in child_labware_definition.stackingOffsetWithLabware
53
57
  or (
54
- "default" in top_labware_definition.stackingOffsetWithLabware
55
- and top_labware_definition.compatibleParentLabware is None
58
+ "default" in child_labware_definition.stackingOffsetWithLabware
59
+ and child_labware_definition.compatibleParentLabware is None
56
60
  )
57
61
  )
58
62
 
@@ -27,6 +27,7 @@ from .types import (
27
27
  AddressableAreaLocation,
28
28
  ModuleLocation,
29
29
  OnLabwareLocation,
30
+ WASTE_CHUTE_LOCATION,
30
31
  )
31
32
 
32
33
 
@@ -116,6 +117,7 @@ def _standardize_labware_location(
116
117
  )
117
118
  or original == OFF_DECK_LOCATION
118
119
  or original == SYSTEM_LOCATION
120
+ or original == WASTE_CHUTE_LOCATION
119
121
  ):
120
122
  return original
121
123
 
@@ -17,13 +17,47 @@ def wells_covered_by_pipette_configuration(
17
17
  """Compute the wells covered by a pipette nozzle configuration."""
18
18
  if len(labware_wells_by_column) >= 12 and len(labware_wells_by_column[0]) >= 8:
19
19
  yield from wells_covered_dense(
20
- nozzle_map,
20
+ nozzle_map.columns,
21
+ nozzle_map.rows,
22
+ nozzle_map.starting_nozzle,
21
23
  target_well,
22
24
  labware_wells_by_column,
23
25
  )
24
26
  elif len(labware_wells_by_column) < 12 and len(labware_wells_by_column[0]) < 8:
25
27
  yield from wells_covered_sparse(
26
- nozzle_map, target_well, labware_wells_by_column
28
+ nozzle_map.columns,
29
+ nozzle_map.rows,
30
+ nozzle_map.starting_nozzle,
31
+ target_well,
32
+ labware_wells_by_column,
33
+ )
34
+ else:
35
+ raise InvalidStoredData(
36
+ "Labware of non-SBS and non-reservoir format cannot be handled"
37
+ )
38
+
39
+
40
+ def wells_covered_by_physical_pipette(
41
+ nozzle_map: NozzleMap,
42
+ target_well: str,
43
+ labware_wells_by_column: list[list[str]],
44
+ ) -> Iterator[str]:
45
+ """Compute the wells covered by a pipette nozzle configuration."""
46
+ if len(labware_wells_by_column) >= 12 and len(labware_wells_by_column[0]) >= 8:
47
+ yield from wells_covered_dense(
48
+ nozzle_map.full_instrument_columns,
49
+ nozzle_map.full_instrument_rows,
50
+ nozzle_map.starting_nozzle,
51
+ target_well,
52
+ labware_wells_by_column,
53
+ )
54
+ elif len(labware_wells_by_column) < 12 and len(labware_wells_by_column[0]) < 8:
55
+ yield from wells_covered_sparse(
56
+ nozzle_map.full_instrument_columns,
57
+ nozzle_map.full_instrument_rows,
58
+ nozzle_map.starting_nozzle,
59
+ target_well,
60
+ labware_wells_by_column,
27
61
  )
28
62
  else:
29
63
  raise InvalidStoredData(
@@ -42,7 +76,11 @@ def row_col_ordinals_from_column_major_map(
42
76
 
43
77
 
44
78
  def wells_covered_dense( # noqa: C901
45
- nozzle_map: NozzleMap, target_well: str, target_wells_by_column: list[list[str]]
79
+ columns: dict[str, list[str]],
80
+ rows: dict[str, list[str]],
81
+ starting_nozzle: str,
82
+ target_well: str,
83
+ target_wells_by_column: list[list[str]],
46
84
  ) -> Iterator[str]:
47
85
  """Get the list of wells covered by a nozzle map on an SBS format labware with a specified multiplier of 96 into the number of wells.
48
86
 
@@ -66,11 +104,11 @@ def wells_covered_dense( # noqa: C901
66
104
  "This labware cannot be used with wells_covered_dense() because it is less dense than an SBS 96 standard"
67
105
  )
68
106
 
69
- for nozzle_column in range(len(nozzle_map.columns)):
107
+ for nozzle_column in range(len(columns)):
70
108
  target_column_offset = nozzle_column * column_downsample
71
- for nozzle_row in range(len(nozzle_map.rows)):
109
+ for nozzle_row in range(len(rows)):
72
110
  target_row_offset = nozzle_row * row_downsample
73
- if nozzle_map.starting_nozzle == "A1":
111
+ if starting_nozzle == "A1":
74
112
  if (
75
113
  target_column_index + target_column_offset
76
114
  < len(target_wells_by_column)
@@ -81,7 +119,7 @@ def wells_covered_dense( # noqa: C901
81
119
  yield target_wells_by_column[
82
120
  target_column_index + target_column_offset
83
121
  ][target_row_index + target_row_offset]
84
- elif nozzle_map.starting_nozzle == "A12":
122
+ elif starting_nozzle == "A12":
85
123
  if (target_column_index - target_column_offset >= 0) and (
86
124
  target_row_index + target_row_offset
87
125
  < len(target_wells_by_column[target_column_index])
@@ -89,7 +127,7 @@ def wells_covered_dense( # noqa: C901
89
127
  yield target_wells_by_column[
90
128
  target_column_index - target_column_offset
91
129
  ][target_row_index + target_row_offset]
92
- elif nozzle_map.starting_nozzle == "H1":
130
+ elif starting_nozzle == "H1":
93
131
  if (
94
132
  target_column_index + target_column_offset
95
133
  < len(target_wells_by_column)
@@ -97,7 +135,7 @@ def wells_covered_dense( # noqa: C901
97
135
  yield target_wells_by_column[
98
136
  target_column_index + target_column_offset
99
137
  ][target_row_index - target_row_offset]
100
- elif nozzle_map.starting_nozzle == "H12":
138
+ elif starting_nozzle == "H12":
101
139
  if (target_column_index - target_column_offset >= 0) and (
102
140
  target_row_index - target_row_offset >= 0
103
141
  ):
@@ -106,12 +144,16 @@ def wells_covered_dense( # noqa: C901
106
144
  ][target_row_index - target_row_offset]
107
145
  else:
108
146
  raise InvalidProtocolData(
109
- f"A pipette nozzle configuration may not having a starting nozzle of {nozzle_map.starting_nozzle}"
147
+ f"A pipette nozzle configuration may not having a starting nozzle of {starting_nozzle}"
110
148
  )
111
149
 
112
150
 
113
151
  def wells_covered_sparse( # noqa: C901
114
- nozzle_map: NozzleMap, target_well: str, target_wells_by_column: list[list[str]]
152
+ columns: dict[str, list[str]],
153
+ rows: dict[str, list[str]],
154
+ starting_nozzle: str,
155
+ target_well: str,
156
+ target_wells_by_column: list[list[str]],
115
157
  ) -> Iterator[str]:
116
158
  """Get the list of wells covered by a nozzle map on a column-oriented reservoir.
117
159
 
@@ -128,9 +170,9 @@ def wells_covered_sparse( # noqa: C901
128
170
  raise InvalidStoredData(
129
171
  "This labware cannot be used with wells_covered_sparse() because it is more dense than an SBS 96 standard."
130
172
  )
131
- for nozzle_column in range(max(1, len(nozzle_map.columns) // column_upsample)):
132
- for nozzle_row in range(max(1, len(nozzle_map.rows) // row_upsample)):
133
- if nozzle_map.starting_nozzle == "A1":
173
+ for nozzle_column in range(max(1, len(columns) // column_upsample)):
174
+ for nozzle_row in range(max(1, len(rows) // row_upsample)):
175
+ if starting_nozzle == "A1":
134
176
  if (
135
177
  target_column_index + nozzle_column < len(target_wells_by_column)
136
178
  ) and (
@@ -140,7 +182,7 @@ def wells_covered_sparse( # noqa: C901
140
182
  yield target_wells_by_column[target_column_index + nozzle_column][
141
183
  target_row_index + nozzle_row
142
184
  ]
143
- elif nozzle_map.starting_nozzle == "A12":
185
+ elif starting_nozzle == "A12":
144
186
  if (target_column_index - nozzle_column >= 0) and (
145
187
  target_row_index + nozzle_row
146
188
  < len(target_wells_by_column[target_column_index])
@@ -148,7 +190,7 @@ def wells_covered_sparse( # noqa: C901
148
190
  yield target_wells_by_column[target_column_index - nozzle_column][
149
191
  target_row_index + nozzle_row
150
192
  ]
151
- elif nozzle_map.starting_nozzle == "H1":
193
+ elif starting_nozzle == "H1":
152
194
  if (
153
195
  target_column_index + nozzle_column
154
196
  < len(target_wells_by_column[target_column_index])
@@ -156,7 +198,7 @@ def wells_covered_sparse( # noqa: C901
156
198
  yield target_wells_by_column[target_column_index + nozzle_column][
157
199
  target_row_index - nozzle_row
158
200
  ]
159
- elif nozzle_map.starting_nozzle == "H12":
201
+ elif starting_nozzle == "H12":
160
202
  if (target_column_index - nozzle_column >= 0) and (
161
203
  target_row_index - nozzle_row >= 0
162
204
  ):
@@ -165,7 +207,7 @@ def wells_covered_sparse( # noqa: C901
165
207
  ]
166
208
  else:
167
209
  raise InvalidProtocolData(
168
- f"A pipette nozzle configuration may not having a starting nozzle of {nozzle_map.starting_nozzle}"
210
+ f"A pipette nozzle configuration may not having a starting nozzle of {starting_nozzle}"
169
211
  )
170
212
 
171
213
 
@@ -5,6 +5,7 @@ from functools import cached_property
5
5
  from typing import Dict, List, Optional, Set
6
6
 
7
7
  from opentrons_shared_data.robot.types import RobotType, RobotDefinition
8
+ from opentrons_shared_data.module.types import ModuleOrientation
8
9
  from opentrons_shared_data.deck.types import (
9
10
  DeckDefinitionV5,
10
11
  SlotDefV3,
@@ -614,6 +615,7 @@ class AddressableAreaView:
614
615
  "displayName": addressable_area.display_name,
615
616
  "compatibleModuleTypes": addressable_area.compatible_module_types,
616
617
  "features": addressable_area.features,
618
+ "orientation": ModuleOrientation.NOT_APPLICABLE,
617
619
  }
618
620
 
619
621
  def get_deck_slot_definitions(self) -> Dict[str, SlotDefV3]:
@@ -0,0 +1,54 @@
1
+ """Camera related state and store resource.
2
+
3
+ Camera settings, particularly for enablement, can be quieried from via the Camera Provider callback.
4
+ However, here Camera settings may also be provided to override or supercede those provided by the callbacks.
5
+ """
6
+ from dataclasses import dataclass
7
+ from typing import Optional
8
+ from opentrons.protocol_engine.actions import AddCameraSettingsAction
9
+ from opentrons.protocol_engine.resources.camera_provider import CameraSettings
10
+
11
+ from ._abstract_store import HasState, HandlesActions
12
+ from ..actions import Action
13
+
14
+
15
+ @dataclass
16
+ class CameraState:
17
+ """State of Engine Camera override settings."""
18
+
19
+ enablement_settings: Optional[CameraSettings]
20
+ # todo(chb, 2025-10-28): Eventually we will want to extend this to include the camera configurations overrides (contrast, zoom, etc)
21
+
22
+
23
+ class CameraStore(HasState[CameraState], HandlesActions):
24
+ """Camera container."""
25
+
26
+ _state: CameraState
27
+
28
+ def __init__(self) -> None:
29
+ """Initialize a Camera store and its state."""
30
+ self._state = CameraState(enablement_settings=None)
31
+
32
+ def handle_action(self, action: Action) -> None:
33
+ """Modify state in reaction to an action."""
34
+ if isinstance(action, AddCameraSettingsAction):
35
+ # Update the Camera Enablement settings to the newest override settings
36
+ self._state.enablement_settings = action.enablement_settings
37
+
38
+
39
+ class CameraView:
40
+ """Read-only engine created Camera state view."""
41
+
42
+ _state: CameraState
43
+
44
+ def __init__(self, state: CameraState) -> None:
45
+ """Initialize the view of Camera state.
46
+
47
+ Arguments:
48
+ state: Camera dataclass used for tracking override settings for the camera.
49
+ """
50
+ self._state = state
51
+
52
+ def get_enablement_settings(self) -> CameraSettings | None:
53
+ """Get the enablement settings override currently in use. This will take priority over Camera Provider callback provided settings."""
54
+ return self._state.enablement_settings
@@ -238,8 +238,11 @@ class CommandState:
238
238
  has_entered_error_recovery: bool
239
239
  """Whether the run has entered error recovery."""
240
240
 
241
- stopped_by_estop: bool
242
- """If this is set to True, the engine was stopped by an estop event."""
241
+ stopped_by_async_error: bool
242
+ """If this is set to True, the engine was stopped by an async event."""
243
+
244
+ is_stopping_because_of_async_error: bool
245
+ """If this is set to True, the engine was stopped by an asynch event and hasn't finished stopping."""
243
246
 
244
247
  error_recovery_policy: ErrorRecoveryPolicy
245
248
  """See `CommandView.get_error_recovery_policy()`."""
@@ -272,7 +275,8 @@ class CommandStore(HasState[CommandState], HandlesActions):
272
275
  run_completed_at=None,
273
276
  run_started_at=None,
274
277
  latest_protocol_command_hash=None,
275
- stopped_by_estop=False,
278
+ stopped_by_async_error=False,
279
+ is_stopping_because_of_async_error=False,
276
280
  error_recovery_policy=error_recovery_policy,
277
281
  has_entered_error_recovery=False,
278
282
  )
@@ -472,8 +476,9 @@ class CommandStore(HasState[CommandState], HandlesActions):
472
476
  self._state.recovery_target = None
473
477
  self._state.queue_status = QueueStatus.PAUSED
474
478
 
475
- if action.from_estop:
476
- self._state.stopped_by_estop = True
479
+ if action.from_asynchronous_error:
480
+ self._state.stopped_by_async_error = True
481
+ self._state.is_stopping_because_of_async_error = True
477
482
  self._state.run_result = RunResult.FAILED
478
483
  else:
479
484
  self._state.run_result = RunResult.STOPPED
@@ -499,14 +504,29 @@ class CommandStore(HasState[CommandState], HandlesActions):
499
504
  action.error_details.error,
500
505
  )
501
506
  else:
502
- # HACK(sf): There needs to be a better way to set
503
- # an estop error than this else clause
504
- if self._state.stopped_by_estop and action.error_details:
507
+ # HACK(sf): There needs to be a better way to handle async errors than this logic
508
+ # which is way too nonlocal. The idea here is that
509
+ # (1) there's an async error that calls one of the engine async error handlers,
510
+ # which emits a stop action and then tells the orchestrator to call finish
511
+ # (2) calling stop normally would lock out the run error field (since the idea is that
512
+ # stop happens in the handler of the error that stops the run, and thus the failed
513
+ # command has already set the run error), but here either the command didn't fail or
514
+ # the command failed with an error that isn't relevant or is duplicative, so we want
515
+ # to override that
516
+ # (3) but we don't want to override it twice, because some other error handler might
517
+ # tell us to finish with the cancelled error that happens because a command was cancelled
518
+ # in reaction to (2)
519
+ # So we set and clear this stopped_by_async_error and it's awful. Let's figure out a better
520
+ # way.
521
+
522
+ if self._state.is_stopping_because_of_async_error and action.error_details:
505
523
  self._state.run_error = self._map_run_exception_to_error_occurrence(
506
524
  action.error_details.error_id,
507
525
  action.error_details.created_at,
508
526
  action.error_details.error,
509
527
  )
528
+ self._state.is_stopping_because_of_async_error = False
529
+ self._state.stopped_by_async_error = True
510
530
 
511
531
  def _handle_hardware_stopped_action(self, action: HardwareStoppedAction) -> None:
512
532
  self._state.queue_status = QueueStatus.PAUSED
@@ -516,10 +536,13 @@ class CommandStore(HasState[CommandState], HandlesActions):
516
536
  )
517
537
 
518
538
  if action.finish_error_details:
519
- self._state.finish_error = self._map_finish_exception_to_error_occurrence(
520
- action.finish_error_details.error_id,
521
- action.finish_error_details.created_at,
522
- action.finish_error_details.error,
539
+ self._state.finish_error = (
540
+ self._state.finish_error
541
+ or self._map_finish_exception_to_error_occurrence(
542
+ action.finish_error_details.error_id,
543
+ action.finish_error_details.created_at,
544
+ action.finish_error_details.error,
545
+ )
523
546
  )
524
547
 
525
548
  def _handle_door_change_action(self, action: DoorChangeAction) -> None:
@@ -952,9 +975,9 @@ class CommandView:
952
975
  """Get whether an engine stop has completed."""
953
976
  return self._state.run_completed_at is not None
954
977
 
955
- def get_is_stopped_by_estop(self) -> bool:
978
+ def get_is_stopped_by_async_error(self) -> bool:
956
979
  """Return whether the engine was stopped specifically by an E-stop."""
957
- return self._state.stopped_by_estop
980
+ return self._state.stopped_by_async_error
958
981
 
959
982
  def has_been_played(self) -> bool:
960
983
  """Get whether engine has started."""