opentrons 8.7.0a6__py3-none-any.whl → 8.7.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.
Potentially problematic release.
This version of opentrons might be problematic. Click here for more details.
- opentrons/_version.py +2 -2
- opentrons/drivers/asyncio/communication/serial_connection.py +129 -52
- 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/hardware_control/api.py +24 -5
- opentrons/hardware_control/backends/controller.py +8 -2
- opentrons/hardware_control/backends/ot3controller.py +3 -0
- opentrons/hardware_control/backends/ot3simulator.py +2 -1
- 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 +82 -8
- 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 +13 -5
- 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/nozzle_manager.py +3 -0
- opentrons/hardware_control/ot3api.py +26 -5
- opentrons/hardware_control/poller.py +22 -8
- opentrons/hardware_control/scripts/update_module_fw.py +5 -0
- opentrons/hardware_control/types.py +31 -2
- opentrons/legacy_commands/module_commands.py +23 -0
- opentrons/legacy_commands/protocol_commands.py +20 -0
- opentrons/legacy_commands/types.py +80 -0
- 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/_types.py +6 -1
- opentrons/protocol_api/core/common.py +3 -1
- opentrons/protocol_api/core/engine/_default_labware_versions.py +32 -11
- opentrons/protocol_api/core/engine/labware.py +8 -1
- opentrons/protocol_api/core/engine/module_core.py +75 -8
- opentrons/protocol_api/core/engine/protocol.py +18 -1
- opentrons/protocol_api/core/engine/tasks.py +48 -0
- opentrons/protocol_api/core/engine/well.py +8 -0
- opentrons/protocol_api/core/legacy/legacy_module_core.py +24 -4
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +11 -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_protocol_core.py +14 -2
- opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
- opentrons/protocol_api/core/module.py +37 -4
- opentrons/protocol_api/core/protocol.py +11 -2
- opentrons/protocol_api/core/tasks.py +31 -0
- opentrons/protocol_api/core/well.py +4 -0
- opentrons/protocol_api/labware.py +5 -0
- opentrons/protocol_api/module_contexts.py +117 -11
- opentrons/protocol_api/protocol_context.py +26 -4
- opentrons/protocol_api/robot_context.py +38 -21
- opentrons/protocol_api/tasks.py +48 -0
- opentrons/protocol_api/validation.py +6 -1
- opentrons/protocol_engine/actions/__init__.py +4 -2
- opentrons/protocol_engine/actions/actions.py +22 -9
- opentrons/protocol_engine/clients/sync_client.py +42 -7
- opentrons/protocol_engine/commands/__init__.py +42 -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/aspirate.py +1 -0
- opentrons/protocol_engine/commands/command.py +1 -0
- opentrons/protocol_engine/commands/command_unions.py +49 -0
- opentrons/protocol_engine/commands/create_timer.py +83 -0
- opentrons/protocol_engine/commands/dispense.py +1 -0
- opentrons/protocol_engine/commands/drop_tip.py +32 -8
- 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/movement_common.py +2 -0
- opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
- 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 +40 -6
- opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +29 -5
- 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/errors/__init__.py +4 -0
- opentrons/protocol_engine/errors/exceptions.py +55 -0
- opentrons/protocol_engine/execution/__init__.py +2 -0
- opentrons/protocol_engine/execution/command_executor.py +8 -0
- opentrons/protocol_engine/execution/create_queue_worker.py +5 -1
- opentrons/protocol_engine/execution/labware_movement.py +9 -12
- opentrons/protocol_engine/execution/movement.py +2 -0
- 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 +75 -34
- opentrons/protocol_engine/resources/__init__.py +2 -0
- opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
- opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
- opentrons/protocol_engine/resources/labware_validation.py +10 -6
- opentrons/protocol_engine/state/_well_math.py +60 -18
- opentrons/protocol_engine/state/addressable_areas.py +2 -0
- opentrons/protocol_engine/state/commands.py +14 -11
- opentrons/protocol_engine/state/geometry.py +213 -374
- opentrons/protocol_engine/state/labware.py +52 -102
- opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
- opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1331 -0
- opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
- opentrons/protocol_engine/state/modules.py +21 -8
- opentrons/protocol_engine/state/motion.py +44 -0
- opentrons/protocol_engine/state/state.py +14 -0
- opentrons/protocol_engine/state/state_summary.py +2 -0
- opentrons/protocol_engine/state/tasks.py +139 -0
- opentrons/protocol_engine/state/tips.py +177 -258
- opentrons/protocol_engine/state/update_types.py +16 -9
- opentrons/protocol_engine/types/__init__.py +9 -3
- 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/module.py +10 -0
- 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/run_orchestrator.py +18 -2
- opentrons/protocols/api_support/definitions.py +1 -1
- opentrons/protocols/api_support/types.py +2 -1
- opentrons/simulate.py +48 -15
- opentrons/system/camera.py +1 -1
- {opentrons-8.7.0a6.dist-info → opentrons-8.7.0a7.dist-info}/METADATA +4 -4
- {opentrons-8.7.0a6.dist-info → opentrons-8.7.0a7.dist-info}/RECORD +143 -127
- opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
- {opentrons-8.7.0a6.dist-info → opentrons-8.7.0a7.dist-info}/WHEEL +0 -0
- {opentrons-8.7.0a6.dist-info → opentrons-8.7.0a7.dist-info}/entry_points.txt +0 -0
- {opentrons-8.7.0a6.dist-info → opentrons-8.7.0a7.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,6 +5,7 @@ from typing import NewType, Optional
|
|
|
5
5
|
from opentrons.protocol_engine.errors import (
|
|
6
6
|
InvalidTargetTemperatureError,
|
|
7
7
|
InvalidBlockVolumeError,
|
|
8
|
+
InvalidRampRateError,
|
|
8
9
|
NoTargetTemperatureSetError,
|
|
9
10
|
InvalidHoldTimeError,
|
|
10
11
|
)
|
|
@@ -23,6 +24,10 @@ from opentrons.hardware_control.modules import ModuleData, ModuleDataValidator
|
|
|
23
24
|
|
|
24
25
|
ThermocyclerModuleId = NewType("ThermocyclerModuleId", str)
|
|
25
26
|
|
|
27
|
+
# These are our published numbers, and from testing they are good bounds
|
|
28
|
+
MAX_HEATING_RATE = 4.25
|
|
29
|
+
MAX_COOLING_RATE = 2.0
|
|
30
|
+
|
|
26
31
|
|
|
27
32
|
@dataclass(frozen=True)
|
|
28
33
|
class ThermocyclerModuleSubState:
|
|
@@ -143,6 +148,38 @@ class ThermocyclerModuleSubState:
|
|
|
143
148
|
)
|
|
144
149
|
return target
|
|
145
150
|
|
|
151
|
+
def validate_ramp_rate(
|
|
152
|
+
self, ramp_rate: Optional[float], target_temp: float
|
|
153
|
+
) -> Optional[float]:
|
|
154
|
+
"""Validate a given temperature ramp rate.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
ramp_rate: The requested ramp rate in °C/second.
|
|
158
|
+
target_temp: The requested block temperature.
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
InvalidRampRateError: The given ramp_rate is invalid
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
The validated ramp rate in °C/second
|
|
165
|
+
"""
|
|
166
|
+
if ramp_rate is None:
|
|
167
|
+
return ramp_rate
|
|
168
|
+
|
|
169
|
+
heating = target_temp > self.get_target_block_temperature()
|
|
170
|
+
if (heating and ramp_rate > MAX_HEATING_RATE) or (
|
|
171
|
+
not heating and ramp_rate > MAX_COOLING_RATE
|
|
172
|
+
):
|
|
173
|
+
raise InvalidRampRateError(
|
|
174
|
+
f"Thermocycler ramp rate cannot exceed {MAX_HEATING_RATE}°C/s"
|
|
175
|
+
f" while heating or {MAX_COOLING_RATE}°C/s when cooling."
|
|
176
|
+
)
|
|
177
|
+
if ramp_rate <= 0:
|
|
178
|
+
raise InvalidRampRateError(
|
|
179
|
+
f"Thermocycler ramp rate cannot be less than or equal to 0, got {ramp_rate}"
|
|
180
|
+
)
|
|
181
|
+
return ramp_rate
|
|
182
|
+
|
|
146
183
|
@classmethod
|
|
147
184
|
def from_live_data(
|
|
148
185
|
cls, module_id: ThermocyclerModuleId, data: ModuleData | None
|
|
@@ -56,7 +56,6 @@ from ..types import (
|
|
|
56
56
|
HeaterShakerLatchStatus,
|
|
57
57
|
HeaterShakerMovementRestrictors,
|
|
58
58
|
DeckType,
|
|
59
|
-
LabwareMovementOffsetData,
|
|
60
59
|
AddressableAreaLocation,
|
|
61
60
|
StackerStoredLabwareGroup,
|
|
62
61
|
)
|
|
@@ -1336,13 +1335,6 @@ class ModuleView:
|
|
|
1336
1335
|
return True
|
|
1337
1336
|
return False
|
|
1338
1337
|
|
|
1339
|
-
def get_default_gripper_offsets(
|
|
1340
|
-
self, module_id: str
|
|
1341
|
-
) -> Optional[LabwareMovementOffsetData]:
|
|
1342
|
-
"""Get the deck's default gripper offsets."""
|
|
1343
|
-
offsets = self.get_definition(module_id).gripperOffsets
|
|
1344
|
-
return offsets.get("default") if offsets else None
|
|
1345
|
-
|
|
1346
1338
|
def get_overflowed_module_in_slot(
|
|
1347
1339
|
self, slot_name: DeckSlotName
|
|
1348
1340
|
) -> Optional[LoadedModule]:
|
|
@@ -1519,3 +1511,24 @@ class ModuleView:
|
|
|
1519
1511
|
f"Provided overlap offset {overlap_offset} does not match "
|
|
1520
1512
|
f"configured {configured}."
|
|
1521
1513
|
)
|
|
1514
|
+
|
|
1515
|
+
def get_has_module_probably_matching_hardware_details(
|
|
1516
|
+
self, module_model: ModuleModel, module_serial: str | None
|
|
1517
|
+
) -> bool:
|
|
1518
|
+
"""Get the ID of a model that possibly matches the provided details.
|
|
1519
|
+
|
|
1520
|
+
If the provided serial is not None, return True if there is a module with the same serial or
|
|
1521
|
+
False if there is not.
|
|
1522
|
+
If the provided serial is None, return True if there is a module with the same model or False if
|
|
1523
|
+
there is not.
|
|
1524
|
+
|
|
1525
|
+
This is intended to provide a good probability that a module matching the provided details
|
|
1526
|
+
is or is not present in the state store. It is used to drive whether the engine cancels a protocol
|
|
1527
|
+
in response to an asynchronous module error or not.
|
|
1528
|
+
"""
|
|
1529
|
+
for module_id, module in self._state.hardware_by_module_id.items():
|
|
1530
|
+
if module_serial is not None and module_serial == module.serial_number:
|
|
1531
|
+
return True
|
|
1532
|
+
if module_serial is None and module.definition.model == module_model:
|
|
1533
|
+
return True
|
|
1534
|
+
return False
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Motion state store and getters."""
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from typing import List, Optional, Union
|
|
4
|
+
import logging
|
|
4
5
|
|
|
5
6
|
from opentrons.types import MountType, Point, StagingSlotName
|
|
6
7
|
from opentrons.hardware_control.types import CriticalPoint
|
|
@@ -28,6 +29,8 @@ from .geometry import GeometryView
|
|
|
28
29
|
from .modules import ModuleView
|
|
29
30
|
from .module_substates import HeaterShakerModuleId
|
|
30
31
|
|
|
32
|
+
log = logging.getLogger(__name__)
|
|
33
|
+
|
|
31
34
|
|
|
32
35
|
@dataclass(frozen=True)
|
|
33
36
|
class PipetteLocationData:
|
|
@@ -85,6 +88,42 @@ class MotionView:
|
|
|
85
88
|
critical_point = CriticalPoint.XY_CENTER
|
|
86
89
|
return PipetteLocationData(mount=mount, critical_point=critical_point)
|
|
87
90
|
|
|
91
|
+
def _get_pipette_offset_for_reservoirs(
|
|
92
|
+
self, labware_id: str, well_name: str, pipette_id: str
|
|
93
|
+
) -> Point:
|
|
94
|
+
# 8 rows, 12 columns
|
|
95
|
+
subwells_96 = self._labware.get_has_96_subwells(labware_id)
|
|
96
|
+
# 1 row, 12 columns
|
|
97
|
+
subwells_12 = self._labware.get_has_12_subwells(labware_id)
|
|
98
|
+
if subwells_12 and subwells_96:
|
|
99
|
+
log.warning(
|
|
100
|
+
f"{self._labware.get_display_name(labware_id)} has both offsetPipetteFor96GridSubwells and"
|
|
101
|
+
" offsetPipetteFor12GridSubwells quirks."
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
pipette_rows = self._pipettes.get_nozzle_configuration(pipette_id).rows
|
|
105
|
+
pipette_cols = self._pipettes.get_nozzle_configuration(pipette_id).columns
|
|
106
|
+
|
|
107
|
+
even_labware_rows = subwells_96
|
|
108
|
+
even_labware_columns = subwells_96 or subwells_12
|
|
109
|
+
odd_pipette_rows = len(pipette_rows) % 2 == 1
|
|
110
|
+
odd_pipette_cols = len(pipette_cols) % 2 == 1
|
|
111
|
+
|
|
112
|
+
well_x_dim, well_y_dim, well_z_dim = self._labware.get_well_size(
|
|
113
|
+
labware_id=labware_id, well_name=well_name
|
|
114
|
+
)
|
|
115
|
+
x_offset = 0.0
|
|
116
|
+
y_offset = 0.0
|
|
117
|
+
if even_labware_rows and odd_pipette_rows:
|
|
118
|
+
# need to move up half a row
|
|
119
|
+
# there's 8 rows, so move 1/16 of reservoir length
|
|
120
|
+
y_offset = well_y_dim / 16
|
|
121
|
+
if even_labware_columns and odd_pipette_cols:
|
|
122
|
+
# need to move left half a column
|
|
123
|
+
# there's 12 columns, so move 1/24 of reservoir width
|
|
124
|
+
x_offset = -1 * well_x_dim / 24
|
|
125
|
+
return Point(x=x_offset, y=y_offset)
|
|
126
|
+
|
|
88
127
|
def get_movement_waypoints_to_well(
|
|
89
128
|
self,
|
|
90
129
|
pipette_id: str,
|
|
@@ -98,6 +137,7 @@ class MotionView:
|
|
|
98
137
|
force_direct: bool = False,
|
|
99
138
|
minimum_z_height: Optional[float] = None,
|
|
100
139
|
operation_volume: Optional[float] = None,
|
|
140
|
+
offset_pipette_for_reservoir_subwells: bool = False,
|
|
101
141
|
) -> List[motion_planning.Waypoint]:
|
|
102
142
|
"""Calculate waypoints to a destination that's specified as a well."""
|
|
103
143
|
location = current_well or self._pipettes.get_current_location()
|
|
@@ -115,6 +155,10 @@ class MotionView:
|
|
|
115
155
|
operation_volume=operation_volume,
|
|
116
156
|
pipette_id=pipette_id,
|
|
117
157
|
)
|
|
158
|
+
if offset_pipette_for_reservoir_subwells:
|
|
159
|
+
destination += self._get_pipette_offset_for_reservoirs(
|
|
160
|
+
labware_id=labware_id, well_name=well_name, pipette_id=pipette_id
|
|
161
|
+
)
|
|
118
162
|
|
|
119
163
|
move_type = _move_types.get_move_type_to_well(
|
|
120
164
|
pipette_id, labware_id, well_name, location, force_direct
|
|
@@ -34,6 +34,7 @@ from .files import FileView, FileState, FileStore
|
|
|
34
34
|
from .config import Config
|
|
35
35
|
from .state_summary import StateSummary
|
|
36
36
|
from ..types import DeckConfigurationType
|
|
37
|
+
from .tasks import TaskState, TaskView, TaskStore
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
_ParamsT = ParamSpec("_ParamsT")
|
|
@@ -54,6 +55,7 @@ class State:
|
|
|
54
55
|
tips: TipState
|
|
55
56
|
wells: WellState
|
|
56
57
|
files: FileState
|
|
58
|
+
tasks: TaskState
|
|
57
59
|
|
|
58
60
|
|
|
59
61
|
class StateView(HasState[State]):
|
|
@@ -73,6 +75,7 @@ class StateView(HasState[State]):
|
|
|
73
75
|
_motion: MotionView
|
|
74
76
|
_files: FileView
|
|
75
77
|
_config: Config
|
|
78
|
+
_tasks: TaskView
|
|
76
79
|
|
|
77
80
|
@property
|
|
78
81
|
def commands(self) -> CommandView:
|
|
@@ -139,6 +142,11 @@ class StateView(HasState[State]):
|
|
|
139
142
|
"""Get ProtocolEngine configuration."""
|
|
140
143
|
return self._config
|
|
141
144
|
|
|
145
|
+
@property
|
|
146
|
+
def tasks(self) -> TaskView:
|
|
147
|
+
"""Get state view selectors for task state."""
|
|
148
|
+
return self._tasks
|
|
149
|
+
|
|
142
150
|
def get_summary(self) -> StateSummary:
|
|
143
151
|
"""Get protocol run data."""
|
|
144
152
|
error = self._commands.get_error()
|
|
@@ -162,6 +170,7 @@ class StateView(HasState[State]):
|
|
|
162
170
|
)
|
|
163
171
|
for liquid_class_id, liquid_class_record in self._liquid_classes.get_all().items()
|
|
164
172
|
],
|
|
173
|
+
tasks=self._tasks.get_summary(),
|
|
165
174
|
)
|
|
166
175
|
|
|
167
176
|
|
|
@@ -231,6 +240,7 @@ class StateStore(StateView, ActionHandler):
|
|
|
231
240
|
self._tip_store = TipStore()
|
|
232
241
|
self._well_store = WellStore()
|
|
233
242
|
self._file_store = FileStore()
|
|
243
|
+
self._task_store = TaskStore()
|
|
234
244
|
|
|
235
245
|
self._substores: List[HandlesActions] = [
|
|
236
246
|
self._command_store,
|
|
@@ -243,6 +253,7 @@ class StateStore(StateView, ActionHandler):
|
|
|
243
253
|
self._tip_store,
|
|
244
254
|
self._well_store,
|
|
245
255
|
self._file_store,
|
|
256
|
+
self._task_store,
|
|
246
257
|
]
|
|
247
258
|
self._config = config
|
|
248
259
|
self._change_notifier = change_notifier or ChangeNotifier()
|
|
@@ -366,6 +377,7 @@ class StateStore(StateView, ActionHandler):
|
|
|
366
377
|
tips=self._tip_store.state,
|
|
367
378
|
wells=self._well_store.state,
|
|
368
379
|
files=self._file_store.state,
|
|
380
|
+
tasks=self._task_store.state,
|
|
369
381
|
)
|
|
370
382
|
|
|
371
383
|
def _initialize_state(self) -> None:
|
|
@@ -384,6 +396,7 @@ class StateStore(StateView, ActionHandler):
|
|
|
384
396
|
self._tips = TipView(state.tips)
|
|
385
397
|
self._wells = WellView(state.wells)
|
|
386
398
|
self._files = FileView(state.files)
|
|
399
|
+
self._tasks = TaskView(state.tasks)
|
|
387
400
|
|
|
388
401
|
# Derived states
|
|
389
402
|
self._geometry = GeometryView(
|
|
@@ -416,6 +429,7 @@ class StateStore(StateView, ActionHandler):
|
|
|
416
429
|
self._liquid_classes._state = next_state.liquid_classes
|
|
417
430
|
self._tips._state = next_state.tips
|
|
418
431
|
self._wells._state = next_state.wells
|
|
432
|
+
self._tasks._state = next_state.tasks
|
|
419
433
|
self._change_notifier.notify()
|
|
420
434
|
if self._notify_robot_server is not None:
|
|
421
435
|
self._notify_robot_server()
|
|
@@ -13,6 +13,7 @@ from ..types import (
|
|
|
13
13
|
Liquid,
|
|
14
14
|
LiquidClassRecordWithId,
|
|
15
15
|
WellInfoSummary,
|
|
16
|
+
TaskSummary,
|
|
16
17
|
)
|
|
17
18
|
|
|
18
19
|
|
|
@@ -34,3 +35,4 @@ class StateSummary(BaseModel):
|
|
|
34
35
|
wells: List[WellInfoSummary] = Field(default_factory=list)
|
|
35
36
|
files: List[str] = Field(default_factory=list)
|
|
36
37
|
liquidClasses: List[LiquidClassRecordWithId] = Field(default_factory=list)
|
|
38
|
+
tasks: List[TaskSummary] = Field(default_factory=list)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Task state tracking."""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from itertools import chain
|
|
4
|
+
from typing import Iterable
|
|
5
|
+
from ..types import Task, TaskSummary, FinishedTask
|
|
6
|
+
from ._abstract_store import HasState, HandlesActions
|
|
7
|
+
from opentrons.protocol_engine.state import update_types
|
|
8
|
+
from opentrons.protocol_engine.errors.exceptions import NoTaskFoundError
|
|
9
|
+
from ..actions import (
|
|
10
|
+
get_state_updates,
|
|
11
|
+
Action,
|
|
12
|
+
StartTaskAction,
|
|
13
|
+
FinishTaskAction,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class TaskState:
|
|
19
|
+
"""Task state tracking."""
|
|
20
|
+
|
|
21
|
+
current_tasks_by_id: dict[str, Task]
|
|
22
|
+
finished_tasks_by_id: dict[str, FinishedTask]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TaskStore(HasState[TaskState], HandlesActions):
|
|
26
|
+
"""Stores tasks."""
|
|
27
|
+
|
|
28
|
+
_state: TaskState
|
|
29
|
+
|
|
30
|
+
def __init__(self) -> None:
|
|
31
|
+
"""Initialize a TaskStore."""
|
|
32
|
+
self._state = TaskState(current_tasks_by_id={}, finished_tasks_by_id={})
|
|
33
|
+
|
|
34
|
+
def _handle_state_update(self, state_update: update_types.StateUpdate) -> None:
|
|
35
|
+
"""Handle a state update."""
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
def _handle_start_task_action(self, action: StartTaskAction) -> None:
|
|
39
|
+
self._state.current_tasks_by_id[action.task.id] = action.task
|
|
40
|
+
|
|
41
|
+
def _handle_finish_task_action(self, action: FinishTaskAction) -> None:
|
|
42
|
+
task = self._state.current_tasks_by_id[action.task_id]
|
|
43
|
+
self._state.finished_tasks_by_id[action.task_id] = FinishedTask(
|
|
44
|
+
id=task.id,
|
|
45
|
+
createdAt=task.createdAt,
|
|
46
|
+
finishedAt=action.finished_at,
|
|
47
|
+
error=action.error,
|
|
48
|
+
)
|
|
49
|
+
del self._state.current_tasks_by_id[action.task_id]
|
|
50
|
+
|
|
51
|
+
def handle_action(self, action: Action) -> None:
|
|
52
|
+
"""Modify the state in reaction to an action."""
|
|
53
|
+
for state_update in get_state_updates(action):
|
|
54
|
+
self._handle_state_update(state_update)
|
|
55
|
+
match action:
|
|
56
|
+
case StartTaskAction():
|
|
57
|
+
self._handle_start_task_action(action)
|
|
58
|
+
case FinishTaskAction():
|
|
59
|
+
self._handle_finish_task_action(action)
|
|
60
|
+
case _:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TaskView:
|
|
65
|
+
"""Read-only task state view."""
|
|
66
|
+
|
|
67
|
+
_state: TaskState
|
|
68
|
+
|
|
69
|
+
def __init__(self, state: TaskState) -> None:
|
|
70
|
+
"""Initialize a TaskView."""
|
|
71
|
+
self._state = state
|
|
72
|
+
|
|
73
|
+
def get_current(self, id: str) -> Task:
|
|
74
|
+
"""Get a task by ID."""
|
|
75
|
+
try:
|
|
76
|
+
return self._state.current_tasks_by_id[id]
|
|
77
|
+
except KeyError as e:
|
|
78
|
+
raise NoTaskFoundError(f"No current task with ID {id}") from e
|
|
79
|
+
|
|
80
|
+
def get_all_current(self) -> list[Task]:
|
|
81
|
+
"""Get all currently running tasks."""
|
|
82
|
+
return [task for task in self._state.current_tasks_by_id.values()]
|
|
83
|
+
|
|
84
|
+
def get_finished(self, id: str) -> FinishedTask:
|
|
85
|
+
"""Get a finished task by ID."""
|
|
86
|
+
try:
|
|
87
|
+
return self._state.finished_tasks_by_id[id]
|
|
88
|
+
except KeyError as e:
|
|
89
|
+
raise NoTaskFoundError(f"No finished task with ID {id}") from e
|
|
90
|
+
|
|
91
|
+
def get(self, id: str) -> Task | FinishedTask:
|
|
92
|
+
"""Get a single task by id."""
|
|
93
|
+
if id in self._state.current_tasks_by_id:
|
|
94
|
+
return self._state.current_tasks_by_id[id]
|
|
95
|
+
elif id in self._state.finished_tasks_by_id:
|
|
96
|
+
return self._state.finished_tasks_by_id[id]
|
|
97
|
+
else:
|
|
98
|
+
raise NoTaskFoundError(message=f"Task {id} not found.")
|
|
99
|
+
|
|
100
|
+
def get_summary(self) -> list[TaskSummary]:
|
|
101
|
+
"""Get a summary of all tasks."""
|
|
102
|
+
return [
|
|
103
|
+
TaskSummary(
|
|
104
|
+
id=task_id,
|
|
105
|
+
createdAt=task.createdAt,
|
|
106
|
+
finishedAt=getattr(task, "finishedAt", None),
|
|
107
|
+
error=getattr(task, "error", None),
|
|
108
|
+
)
|
|
109
|
+
for task_id, task in chain(
|
|
110
|
+
self._state.current_tasks_by_id.items(),
|
|
111
|
+
self._state.finished_tasks_by_id.items(),
|
|
112
|
+
)
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
def all_tasks_finished_or_any_task_failed(self, task_ids: Iterable[str]) -> bool:
|
|
116
|
+
"""Implements wait semantics of asyncio.gather(return_exceptions = False).
|
|
117
|
+
|
|
118
|
+
This returns true when any of the following are true:
|
|
119
|
+
- All tasks in task_ids are complete with or without an error
|
|
120
|
+
- Any task in task_ids is complete with an error.
|
|
121
|
+
|
|
122
|
+
NOTE: Does not raise the error that the errored task has.
|
|
123
|
+
"""
|
|
124
|
+
finished = set(self._state.finished_tasks_by_id.keys())
|
|
125
|
+
task_ids = set(task_ids)
|
|
126
|
+
if task_ids.issubset(finished):
|
|
127
|
+
return True
|
|
128
|
+
if self.get_failed_tasks(task_ids):
|
|
129
|
+
return True
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
def get_failed_tasks(self, task_ids: Iterable[str]) -> list[str]:
|
|
133
|
+
"""Return a list of failed task ids of the ones that were passed."""
|
|
134
|
+
failed_tasks: list[str] = []
|
|
135
|
+
for task_id in task_ids:
|
|
136
|
+
task = self._state.finished_tasks_by_id.get(task_id, None)
|
|
137
|
+
if task and task.error:
|
|
138
|
+
failed_tasks.append(task_id)
|
|
139
|
+
return failed_tasks
|