opentrons 8.7.0a9__py3-none-any.whl → 8.8.0a7__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.
- opentrons/_version.py +2 -2
- opentrons/cli/analyze.py +4 -1
- opentrons/config/__init__.py +7 -0
- opentrons/drivers/asyncio/communication/serial_connection.py +126 -49
- opentrons/drivers/heater_shaker/abstract.py +5 -0
- opentrons/drivers/heater_shaker/driver.py +10 -0
- opentrons/drivers/heater_shaker/simulator.py +4 -0
- opentrons/drivers/thermocycler/abstract.py +6 -0
- opentrons/drivers/thermocycler/driver.py +61 -10
- opentrons/drivers/thermocycler/simulator.py +6 -0
- opentrons/drivers/vacuum_module/__init__.py +5 -0
- opentrons/drivers/vacuum_module/abstract.py +93 -0
- opentrons/drivers/vacuum_module/driver.py +208 -0
- opentrons/drivers/vacuum_module/errors.py +39 -0
- opentrons/drivers/vacuum_module/simulator.py +85 -0
- opentrons/drivers/vacuum_module/types.py +79 -0
- opentrons/execute.py +3 -0
- opentrons/hardware_control/api.py +24 -5
- opentrons/hardware_control/backends/controller.py +8 -2
- opentrons/hardware_control/backends/flex_protocol.py +1 -0
- opentrons/hardware_control/backends/ot3controller.py +35 -2
- opentrons/hardware_control/backends/ot3simulator.py +3 -1
- opentrons/hardware_control/backends/ot3utils.py +37 -0
- opentrons/hardware_control/backends/simulator.py +2 -1
- opentrons/hardware_control/backends/subsystem_manager.py +5 -2
- opentrons/hardware_control/emulation/abstract_emulator.py +6 -4
- opentrons/hardware_control/emulation/connection_handler.py +8 -5
- opentrons/hardware_control/emulation/heater_shaker.py +12 -3
- opentrons/hardware_control/emulation/settings.py +1 -1
- opentrons/hardware_control/emulation/thermocycler.py +67 -15
- opentrons/hardware_control/module_control.py +105 -10
- opentrons/hardware_control/modules/__init__.py +3 -0
- opentrons/hardware_control/modules/absorbance_reader.py +11 -4
- opentrons/hardware_control/modules/flex_stacker.py +38 -9
- opentrons/hardware_control/modules/heater_shaker.py +42 -5
- opentrons/hardware_control/modules/magdeck.py +8 -4
- opentrons/hardware_control/modules/mod_abc.py +14 -6
- opentrons/hardware_control/modules/tempdeck.py +25 -5
- opentrons/hardware_control/modules/thermocycler.py +68 -11
- opentrons/hardware_control/modules/types.py +20 -1
- opentrons/hardware_control/modules/utils.py +11 -4
- opentrons/hardware_control/motion_utilities.py +6 -6
- opentrons/hardware_control/nozzle_manager.py +3 -0
- opentrons/hardware_control/ot3api.py +85 -17
- opentrons/hardware_control/poller.py +22 -8
- opentrons/hardware_control/protocols/liquid_handler.py +6 -2
- opentrons/hardware_control/scripts/update_module_fw.py +5 -0
- opentrons/hardware_control/types.py +43 -2
- opentrons/legacy_commands/commands.py +58 -5
- opentrons/legacy_commands/module_commands.py +52 -0
- opentrons/legacy_commands/protocol_commands.py +53 -1
- opentrons/legacy_commands/types.py +155 -1
- opentrons/motion_planning/deck_conflict.py +17 -12
- opentrons/motion_planning/waypoints.py +15 -29
- opentrons/protocol_api/__init__.py +5 -1
- opentrons/protocol_api/_transfer_liquid_validation.py +17 -2
- opentrons/protocol_api/_types.py +8 -1
- opentrons/protocol_api/core/common.py +3 -1
- opentrons/protocol_api/core/engine/_default_labware_versions.py +33 -11
- opentrons/protocol_api/core/engine/deck_conflict.py +3 -1
- opentrons/protocol_api/core/engine/instrument.py +109 -26
- opentrons/protocol_api/core/engine/labware.py +8 -1
- opentrons/protocol_api/core/engine/module_core.py +95 -4
- opentrons/protocol_api/core/engine/protocol.py +51 -2
- opentrons/protocol_api/core/engine/stringify.py +2 -0
- opentrons/protocol_api/core/engine/tasks.py +48 -0
- opentrons/protocol_api/core/engine/well.py +8 -0
- opentrons/protocol_api/core/instrument.py +19 -2
- opentrons/protocol_api/core/legacy/legacy_instrument_core.py +19 -2
- opentrons/protocol_api/core/legacy/legacy_module_core.py +33 -2
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +23 -1
- opentrons/protocol_api/core/legacy/legacy_well_core.py +4 -0
- opentrons/protocol_api/core/legacy/tasks.py +19 -0
- opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +19 -2
- opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +14 -2
- opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
- opentrons/protocol_api/core/module.py +58 -2
- opentrons/protocol_api/core/protocol.py +23 -2
- opentrons/protocol_api/core/tasks.py +31 -0
- opentrons/protocol_api/core/well.py +4 -0
- opentrons/protocol_api/instrument_context.py +388 -2
- opentrons/protocol_api/labware.py +10 -2
- opentrons/protocol_api/module_contexts.py +170 -6
- opentrons/protocol_api/protocol_context.py +87 -21
- opentrons/protocol_api/robot_context.py +41 -25
- opentrons/protocol_api/tasks.py +48 -0
- opentrons/protocol_api/validation.py +49 -3
- opentrons/protocol_engine/__init__.py +4 -0
- opentrons/protocol_engine/actions/__init__.py +6 -2
- opentrons/protocol_engine/actions/actions.py +31 -9
- opentrons/protocol_engine/clients/sync_client.py +42 -7
- opentrons/protocol_engine/commands/__init__.py +56 -0
- opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +2 -15
- opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +2 -15
- opentrons/protocol_engine/commands/absorbance_reader/read.py +22 -23
- opentrons/protocol_engine/commands/aspirate.py +1 -0
- opentrons/protocol_engine/commands/aspirate_while_tracking.py +52 -19
- opentrons/protocol_engine/commands/capture_image.py +302 -0
- opentrons/protocol_engine/commands/command.py +2 -0
- opentrons/protocol_engine/commands/command_unions.py +62 -0
- opentrons/protocol_engine/commands/create_timer.py +83 -0
- opentrons/protocol_engine/commands/dispense.py +1 -0
- opentrons/protocol_engine/commands/dispense_while_tracking.py +56 -19
- opentrons/protocol_engine/commands/drop_tip.py +32 -8
- opentrons/protocol_engine/commands/flex_stacker/common.py +35 -0
- opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +7 -0
- opentrons/protocol_engine/commands/heater_shaker/__init__.py +14 -0
- opentrons/protocol_engine/commands/heater_shaker/common.py +20 -0
- opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +5 -4
- opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +136 -0
- opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +31 -5
- opentrons/protocol_engine/commands/move_labware.py +3 -4
- opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +1 -1
- opentrons/protocol_engine/commands/movement_common.py +31 -2
- opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
- opentrons/protocol_engine/commands/pipetting_common.py +48 -3
- opentrons/protocol_engine/commands/set_tip_state.py +97 -0
- opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +38 -7
- opentrons/protocol_engine/commands/thermocycler/__init__.py +16 -0
- opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +6 -0
- opentrons/protocol_engine/commands/thermocycler/run_profile.py +8 -0
- opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +44 -7
- opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +43 -14
- opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +191 -0
- opentrons/protocol_engine/commands/touch_tip.py +1 -1
- opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +6 -22
- opentrons/protocol_engine/commands/wait_for_tasks.py +98 -0
- opentrons/protocol_engine/create_protocol_engine.py +12 -0
- opentrons/protocol_engine/engine_support.py +3 -0
- opentrons/protocol_engine/errors/__init__.py +12 -0
- opentrons/protocol_engine/errors/exceptions.py +119 -0
- opentrons/protocol_engine/execution/__init__.py +4 -0
- opentrons/protocol_engine/execution/command_executor.py +62 -1
- opentrons/protocol_engine/execution/create_queue_worker.py +9 -2
- opentrons/protocol_engine/execution/labware_movement.py +13 -15
- opentrons/protocol_engine/execution/movement.py +2 -0
- opentrons/protocol_engine/execution/pipetting.py +19 -25
- opentrons/protocol_engine/execution/queue_worker.py +4 -0
- opentrons/protocol_engine/execution/run_control.py +8 -0
- opentrons/protocol_engine/execution/task_handler.py +157 -0
- opentrons/protocol_engine/protocol_engine.py +137 -36
- opentrons/protocol_engine/resources/__init__.py +4 -0
- opentrons/protocol_engine/resources/camera_provider.py +110 -0
- opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
- opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
- opentrons/protocol_engine/resources/file_provider.py +133 -58
- opentrons/protocol_engine/resources/labware_validation.py +10 -6
- opentrons/protocol_engine/slot_standardization.py +2 -0
- opentrons/protocol_engine/state/_well_math.py +60 -18
- opentrons/protocol_engine/state/addressable_areas.py +2 -0
- opentrons/protocol_engine/state/camera.py +54 -0
- opentrons/protocol_engine/state/commands.py +37 -14
- opentrons/protocol_engine/state/geometry.py +276 -379
- opentrons/protocol_engine/state/labware.py +62 -108
- opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
- opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1336 -0
- opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
- opentrons/protocol_engine/state/modules.py +30 -8
- opentrons/protocol_engine/state/motion.py +44 -0
- opentrons/protocol_engine/state/preconditions.py +59 -0
- opentrons/protocol_engine/state/state.py +44 -0
- opentrons/protocol_engine/state/state_summary.py +4 -0
- opentrons/protocol_engine/state/tasks.py +139 -0
- opentrons/protocol_engine/state/tips.py +177 -258
- opentrons/protocol_engine/state/update_types.py +26 -9
- opentrons/protocol_engine/types/__init__.py +23 -4
- opentrons/protocol_engine/types/command_preconditions.py +18 -0
- opentrons/protocol_engine/types/deck_configuration.py +5 -1
- opentrons/protocol_engine/types/instrument.py +8 -1
- opentrons/protocol_engine/types/labware.py +1 -13
- opentrons/protocol_engine/types/location.py +26 -2
- opentrons/protocol_engine/types/module.py +11 -1
- opentrons/protocol_engine/types/tasks.py +38 -0
- opentrons/protocol_engine/types/tip.py +9 -0
- opentrons/protocol_runner/create_simulating_orchestrator.py +29 -2
- opentrons/protocol_runner/protocol_runner.py +14 -1
- opentrons/protocol_runner/run_orchestrator.py +49 -2
- opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +2 -2
- opentrons/protocols/api_support/definitions.py +1 -1
- opentrons/protocols/api_support/types.py +2 -1
- opentrons/simulate.py +51 -15
- opentrons/system/camera.py +334 -4
- opentrons/system/ffmpeg.py +110 -0
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/METADATA +4 -4
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/RECORD +188 -160
- opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/WHEEL +0 -0
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/entry_points.txt +0 -0
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.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
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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
|
|
53
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
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
|
-
|
|
137
|
-
Callable[[
|
|
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
|
-
|
|
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.
|
|
148
|
-
self.
|
|
149
|
-
|
|
150
|
-
async def
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
48
|
-
|
|
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
|
|
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
|
-
|
|
56
|
+
parent_labware_load_name in child_labware_definition.stackingOffsetWithLabware
|
|
53
57
|
or (
|
|
54
|
-
"default" in
|
|
55
|
-
and
|
|
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,
|
|
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
|
-
|
|
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(
|
|
107
|
+
for nozzle_column in range(len(columns)):
|
|
70
108
|
target_column_offset = nozzle_column * column_downsample
|
|
71
|
-
for nozzle_row in range(len(
|
|
109
|
+
for nozzle_row in range(len(rows)):
|
|
72
110
|
target_row_offset = nozzle_row * row_downsample
|
|
73
|
-
if
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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(
|
|
132
|
-
for nozzle_row in range(max(1, len(
|
|
133
|
-
if
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
242
|
-
"""If this is set to True, the engine was stopped by an
|
|
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
|
-
|
|
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.
|
|
476
|
-
self._state.
|
|
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
|
|
503
|
-
#
|
|
504
|
-
|
|
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 =
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
|
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.
|
|
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."""
|