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.
- 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 +92 -17
- opentrons/hardware_control/poller.py +22 -8
- opentrons/hardware_control/protocols/liquid_handler.py +12 -4
- 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/pipette_movement_conflict.py +4 -18
- 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 +26 -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 +60 -18
- 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.0a8.dist-info}/METADATA +4 -4
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/RECORD +189 -161
- opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/WHEEL +0 -0
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/entry_points.txt +0 -0
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.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
|
)
|
|
@@ -268,6 +267,7 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
|
|
|
268
267
|
heater_shaker.SetTargetTemperatureResult,
|
|
269
268
|
heater_shaker.DeactivateHeaterResult,
|
|
270
269
|
heater_shaker.SetAndWaitForShakeSpeedResult,
|
|
270
|
+
heater_shaker.SetShakeSpeedResult,
|
|
271
271
|
heater_shaker.DeactivateShakerResult,
|
|
272
272
|
heater_shaker.OpenLabwareLatchResult,
|
|
273
273
|
heater_shaker.CloseLabwareLatchResult,
|
|
@@ -431,6 +431,7 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
|
|
|
431
431
|
heater_shaker.SetTargetTemperature,
|
|
432
432
|
heater_shaker.DeactivateHeater,
|
|
433
433
|
heater_shaker.SetAndWaitForShakeSpeed,
|
|
434
|
+
heater_shaker.SetShakeSpeed,
|
|
434
435
|
heater_shaker.DeactivateShaker,
|
|
435
436
|
heater_shaker.OpenLabwareLatch,
|
|
436
437
|
heater_shaker.CloseLabwareLatch,
|
|
@@ -466,6 +467,13 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
|
|
|
466
467
|
is_plate_shaking=True,
|
|
467
468
|
plate_target_temperature=prev_state.plate_target_temperature,
|
|
468
469
|
)
|
|
470
|
+
elif isinstance(command.result, heater_shaker.SetShakeSpeedResult):
|
|
471
|
+
self._state.substate_by_module_id[module_id] = HeaterShakerModuleSubState(
|
|
472
|
+
module_id=HeaterShakerModuleId(module_id),
|
|
473
|
+
labware_latch_status=prev_state.labware_latch_status,
|
|
474
|
+
is_plate_shaking=True,
|
|
475
|
+
plate_target_temperature=prev_state.plate_target_temperature,
|
|
476
|
+
)
|
|
469
477
|
elif isinstance(command.result, heater_shaker.DeactivateShakerResult):
|
|
470
478
|
self._state.substate_by_module_id[module_id] = HeaterShakerModuleSubState(
|
|
471
479
|
module_id=HeaterShakerModuleId(module_id),
|
|
@@ -1336,13 +1344,6 @@ class ModuleView:
|
|
|
1336
1344
|
return True
|
|
1337
1345
|
return False
|
|
1338
1346
|
|
|
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
1347
|
def get_overflowed_module_in_slot(
|
|
1347
1348
|
self, slot_name: DeckSlotName
|
|
1348
1349
|
) -> Optional[LoadedModule]:
|
|
@@ -1519,3 +1520,24 @@ class ModuleView:
|
|
|
1519
1520
|
f"Provided overlap offset {overlap_offset} does not match "
|
|
1520
1521
|
f"configured {configured}."
|
|
1521
1522
|
)
|
|
1523
|
+
|
|
1524
|
+
def get_has_module_probably_matching_hardware_details(
|
|
1525
|
+
self, module_model: ModuleModel, module_serial: str | None
|
|
1526
|
+
) -> bool:
|
|
1527
|
+
"""Get the ID of a model that possibly matches the provided details.
|
|
1528
|
+
|
|
1529
|
+
If the provided serial is not None, return True if there is a module with the same serial or
|
|
1530
|
+
False if there is not.
|
|
1531
|
+
If the provided serial is None, return True if there is a module with the same model or False if
|
|
1532
|
+
there is not.
|
|
1533
|
+
|
|
1534
|
+
This is intended to provide a good probability that a module matching the provided details
|
|
1535
|
+
is or is not present in the state store. It is used to drive whether the engine cancels a protocol
|
|
1536
|
+
in response to an asynchronous module error or not.
|
|
1537
|
+
"""
|
|
1538
|
+
for module_id, module in self._state.hardware_by_module_id.items():
|
|
1539
|
+
if module_serial is not None and module_serial == module.serial_number:
|
|
1540
|
+
return True
|
|
1541
|
+
if module_serial is None and module.definition.model == module_model:
|
|
1542
|
+
return True
|
|
1543
|
+
return False
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""Motion state store and getters."""
|
|
2
|
+
|
|
2
3
|
from dataclasses import dataclass
|
|
3
4
|
from typing import List, Optional, Union
|
|
5
|
+
import logging
|
|
4
6
|
|
|
5
7
|
from opentrons.types import MountType, Point, StagingSlotName
|
|
6
8
|
from opentrons.hardware_control.types import CriticalPoint
|
|
@@ -28,6 +30,8 @@ from .geometry import GeometryView
|
|
|
28
30
|
from .modules import ModuleView
|
|
29
31
|
from .module_substates import HeaterShakerModuleId
|
|
30
32
|
|
|
33
|
+
log = logging.getLogger(__name__)
|
|
34
|
+
|
|
31
35
|
|
|
32
36
|
@dataclass(frozen=True)
|
|
33
37
|
class PipetteLocationData:
|
|
@@ -75,16 +79,58 @@ class MotionView:
|
|
|
75
79
|
isinstance(current_location, CurrentWell)
|
|
76
80
|
and current_location.pipette_id == pipette_id
|
|
77
81
|
):
|
|
78
|
-
|
|
79
|
-
current_location.labware_id
|
|
80
|
-
):
|
|
81
|
-
critical_point = CriticalPoint.Y_CENTER
|
|
82
|
-
elif self._labware.get_should_center_pipette_on_target_well(
|
|
82
|
+
critical_point = self.get_critical_point_for_wells_in_labware(
|
|
83
83
|
current_location.labware_id
|
|
84
|
-
)
|
|
85
|
-
critical_point = CriticalPoint.XY_CENTER
|
|
84
|
+
)
|
|
86
85
|
return PipetteLocationData(mount=mount, critical_point=critical_point)
|
|
87
86
|
|
|
87
|
+
def _get_pipette_offset_for_reservoirs(
|
|
88
|
+
self, labware_id: str, well_name: str, pipette_id: str
|
|
89
|
+
) -> Point:
|
|
90
|
+
# 8 rows, 12 columns
|
|
91
|
+
subwells_96 = self._labware.get_has_96_subwells(labware_id)
|
|
92
|
+
# 1 row, 12 columns
|
|
93
|
+
subwells_12 = self._labware.get_has_12_subwells(labware_id)
|
|
94
|
+
if subwells_12 and subwells_96:
|
|
95
|
+
log.warning(
|
|
96
|
+
f"{self._labware.get_display_name(labware_id)} has both offsetPipetteFor96GridSubwells and"
|
|
97
|
+
" offsetPipetteFor12GridSubwells quirks."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
pipette_rows = self._pipettes.get_nozzle_configuration(pipette_id).rows
|
|
101
|
+
pipette_cols = self._pipettes.get_nozzle_configuration(pipette_id).columns
|
|
102
|
+
|
|
103
|
+
even_labware_rows = subwells_96
|
|
104
|
+
even_labware_columns = subwells_96 or subwells_12
|
|
105
|
+
odd_pipette_rows = len(pipette_rows) % 2 == 1
|
|
106
|
+
odd_pipette_cols = len(pipette_cols) % 2 == 1
|
|
107
|
+
|
|
108
|
+
well_x_dim, well_y_dim, well_z_dim = self._labware.get_well_size(
|
|
109
|
+
labware_id=labware_id, well_name=well_name
|
|
110
|
+
)
|
|
111
|
+
x_offset = 0.0
|
|
112
|
+
y_offset = 0.0
|
|
113
|
+
if even_labware_rows and odd_pipette_rows:
|
|
114
|
+
# need to move up half a row
|
|
115
|
+
# there's 8 rows, so move 1/16 of reservoir length
|
|
116
|
+
y_offset = well_y_dim / 16
|
|
117
|
+
if even_labware_columns and odd_pipette_cols:
|
|
118
|
+
# need to move left half a column
|
|
119
|
+
# there's 12 columns, so move 1/24 of reservoir width
|
|
120
|
+
x_offset = -1 * well_x_dim / 24
|
|
121
|
+
return Point(x=x_offset, y=y_offset)
|
|
122
|
+
|
|
123
|
+
def get_critical_point_for_wells_in_labware(
|
|
124
|
+
self, labware_id: str
|
|
125
|
+
) -> CriticalPoint | None:
|
|
126
|
+
"""Get the appropriate critical point override for this labware."""
|
|
127
|
+
if self._labware.get_should_center_column_on_target_well(labware_id):
|
|
128
|
+
return CriticalPoint.Y_CENTER
|
|
129
|
+
elif self._labware.get_should_center_pipette_on_target_well(labware_id):
|
|
130
|
+
return CriticalPoint.XY_CENTER
|
|
131
|
+
else:
|
|
132
|
+
return None
|
|
133
|
+
|
|
88
134
|
def get_movement_waypoints_to_well(
|
|
89
135
|
self,
|
|
90
136
|
pipette_id: str,
|
|
@@ -98,15 +144,12 @@ class MotionView:
|
|
|
98
144
|
force_direct: bool = False,
|
|
99
145
|
minimum_z_height: Optional[float] = None,
|
|
100
146
|
operation_volume: Optional[float] = None,
|
|
147
|
+
offset_pipette_for_reservoir_subwells: bool = False,
|
|
101
148
|
) -> List[motion_planning.Waypoint]:
|
|
102
149
|
"""Calculate waypoints to a destination that's specified as a well."""
|
|
103
150
|
location = current_well or self._pipettes.get_current_location()
|
|
104
151
|
|
|
105
|
-
destination_cp
|
|
106
|
-
if self._labware.get_should_center_column_on_target_well(labware_id):
|
|
107
|
-
destination_cp = CriticalPoint.Y_CENTER
|
|
108
|
-
elif self._labware.get_should_center_pipette_on_target_well(labware_id):
|
|
109
|
-
destination_cp = CriticalPoint.XY_CENTER
|
|
152
|
+
destination_cp = self.get_critical_point_for_wells_in_labware(labware_id)
|
|
110
153
|
|
|
111
154
|
destination = self._geometry.get_well_position(
|
|
112
155
|
labware_id=labware_id,
|
|
@@ -115,6 +158,10 @@ class MotionView:
|
|
|
115
158
|
operation_volume=operation_volume,
|
|
116
159
|
pipette_id=pipette_id,
|
|
117
160
|
)
|
|
161
|
+
if offset_pipette_for_reservoir_subwells:
|
|
162
|
+
destination += self._get_pipette_offset_for_reservoirs(
|
|
163
|
+
labware_id=labware_id, well_name=well_name, pipette_id=pipette_id
|
|
164
|
+
)
|
|
118
165
|
|
|
119
166
|
move_type = _move_types.get_move_type_to_well(
|
|
120
167
|
pipette_id, labware_id, well_name, location, force_direct
|
|
@@ -353,12 +400,7 @@ class MotionView:
|
|
|
353
400
|
mm_from_edge=mm_from_edge,
|
|
354
401
|
edge_path_type=edge_path_type,
|
|
355
402
|
)
|
|
356
|
-
critical_point
|
|
357
|
-
|
|
358
|
-
if self._labware.get_should_center_column_on_target_well(labware_id):
|
|
359
|
-
critical_point = CriticalPoint.Y_CENTER
|
|
360
|
-
elif self._labware.get_should_center_pipette_on_target_well(labware_id):
|
|
361
|
-
critical_point = CriticalPoint.XY_CENTER
|
|
403
|
+
critical_point = self.get_critical_point_for_wells_in_labware(labware_id)
|
|
362
404
|
|
|
363
405
|
return [
|
|
364
406
|
motion_planning.Waypoint(position=p, critical_point=critical_point)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Command precondition state and store resource."""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from opentrons.protocol_engine.actions.get_state_update import get_state_updates
|
|
5
|
+
from opentrons.protocol_engine.state import update_types
|
|
6
|
+
from opentrons.protocol_engine.types import CommandPreconditions, PreconditionTypes
|
|
7
|
+
|
|
8
|
+
from ._abstract_store import HasState, HandlesActions
|
|
9
|
+
from ..actions import Action
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class CommandPreconditionState:
|
|
14
|
+
"""State of Engine command precondition references."""
|
|
15
|
+
|
|
16
|
+
preconditions: CommandPreconditions
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CommandPreconditionStore(HasState[CommandPreconditionState], HandlesActions):
|
|
20
|
+
"""Command Precondition container."""
|
|
21
|
+
|
|
22
|
+
_state: CommandPreconditionState
|
|
23
|
+
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
"""Initialize a Command Precondition store and its state."""
|
|
26
|
+
self._state = CommandPreconditionState(
|
|
27
|
+
preconditions=CommandPreconditions(isCameraUsed=False)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def handle_action(self, action: Action) -> None:
|
|
31
|
+
"""Modify state in reaction to an action."""
|
|
32
|
+
for state_update in get_state_updates(action):
|
|
33
|
+
self._handle_state_update(state_update)
|
|
34
|
+
|
|
35
|
+
def _handle_state_update(self, state_update: update_types.StateUpdate) -> None:
|
|
36
|
+
if state_update.precondition_update != update_types.NO_CHANGE:
|
|
37
|
+
for key in state_update.precondition_update.preconditions:
|
|
38
|
+
if key == PreconditionTypes.IS_CAMERA_USED:
|
|
39
|
+
self._state.preconditions.isCameraUsed = (
|
|
40
|
+
state_update.precondition_update.preconditions[key]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CommandPreconditionView:
|
|
45
|
+
"""Read-only engine created Command Precondition state view."""
|
|
46
|
+
|
|
47
|
+
_state: CommandPreconditionState
|
|
48
|
+
|
|
49
|
+
def __init__(self, state: CommandPreconditionState) -> None:
|
|
50
|
+
"""Initialize the view of Command Precondition state.
|
|
51
|
+
|
|
52
|
+
Arguments:
|
|
53
|
+
state: Command precondition dataclass used for tracking preconditions used during a protocol.
|
|
54
|
+
"""
|
|
55
|
+
self._state = state
|
|
56
|
+
|
|
57
|
+
def get_precondition(self) -> CommandPreconditions:
|
|
58
|
+
"""Get the Command Preconditions currently set by a protocol."""
|
|
59
|
+
return self._state.preconditions
|
|
@@ -34,6 +34,13 @@ 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
|
|
38
|
+
from .preconditions import (
|
|
39
|
+
CommandPreconditionState,
|
|
40
|
+
CommandPreconditionStore,
|
|
41
|
+
CommandPreconditionView,
|
|
42
|
+
)
|
|
43
|
+
from .camera import CameraState, CameraView, CameraStore
|
|
37
44
|
|
|
38
45
|
|
|
39
46
|
_ParamsT = ParamSpec("_ParamsT")
|
|
@@ -54,6 +61,9 @@ class State:
|
|
|
54
61
|
tips: TipState
|
|
55
62
|
wells: WellState
|
|
56
63
|
files: FileState
|
|
64
|
+
tasks: TaskState
|
|
65
|
+
preconditions: CommandPreconditionState
|
|
66
|
+
camera: CameraState
|
|
57
67
|
|
|
58
68
|
|
|
59
69
|
class StateView(HasState[State]):
|
|
@@ -73,6 +83,9 @@ class StateView(HasState[State]):
|
|
|
73
83
|
_motion: MotionView
|
|
74
84
|
_files: FileView
|
|
75
85
|
_config: Config
|
|
86
|
+
_tasks: TaskView
|
|
87
|
+
_preconditions: CommandPreconditionView
|
|
88
|
+
_camera: CameraView
|
|
76
89
|
|
|
77
90
|
@property
|
|
78
91
|
def commands(self) -> CommandView:
|
|
@@ -139,6 +152,21 @@ class StateView(HasState[State]):
|
|
|
139
152
|
"""Get ProtocolEngine configuration."""
|
|
140
153
|
return self._config
|
|
141
154
|
|
|
155
|
+
@property
|
|
156
|
+
def preconditions(self) -> CommandPreconditionView:
|
|
157
|
+
"""Get state view selectors for command preconditions."""
|
|
158
|
+
return self._preconditions
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def camera(self) -> CameraView:
|
|
162
|
+
"""Get state view for the Camera."""
|
|
163
|
+
return self._camera
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def tasks(self) -> TaskView:
|
|
167
|
+
"""Get state view selectors for task state."""
|
|
168
|
+
return self._tasks
|
|
169
|
+
|
|
142
170
|
def get_summary(self) -> StateSummary:
|
|
143
171
|
"""Get protocol run data."""
|
|
144
172
|
error = self._commands.get_error()
|
|
@@ -162,6 +190,8 @@ class StateView(HasState[State]):
|
|
|
162
190
|
)
|
|
163
191
|
for liquid_class_id, liquid_class_record in self._liquid_classes.get_all().items()
|
|
164
192
|
],
|
|
193
|
+
tasks=self._tasks.get_summary(),
|
|
194
|
+
cameraSettings=self._camera.get_enablement_settings(),
|
|
165
195
|
)
|
|
166
196
|
|
|
167
197
|
|
|
@@ -231,6 +261,9 @@ class StateStore(StateView, ActionHandler):
|
|
|
231
261
|
self._tip_store = TipStore()
|
|
232
262
|
self._well_store = WellStore()
|
|
233
263
|
self._file_store = FileStore()
|
|
264
|
+
self._task_store = TaskStore()
|
|
265
|
+
self._precondition_store = CommandPreconditionStore()
|
|
266
|
+
self._camera_store = CameraStore()
|
|
234
267
|
|
|
235
268
|
self._substores: List[HandlesActions] = [
|
|
236
269
|
self._command_store,
|
|
@@ -243,6 +276,9 @@ class StateStore(StateView, ActionHandler):
|
|
|
243
276
|
self._tip_store,
|
|
244
277
|
self._well_store,
|
|
245
278
|
self._file_store,
|
|
279
|
+
self._task_store,
|
|
280
|
+
self._precondition_store,
|
|
281
|
+
self._camera_store,
|
|
246
282
|
]
|
|
247
283
|
self._config = config
|
|
248
284
|
self._change_notifier = change_notifier or ChangeNotifier()
|
|
@@ -366,6 +402,9 @@ class StateStore(StateView, ActionHandler):
|
|
|
366
402
|
tips=self._tip_store.state,
|
|
367
403
|
wells=self._well_store.state,
|
|
368
404
|
files=self._file_store.state,
|
|
405
|
+
tasks=self._task_store.state,
|
|
406
|
+
preconditions=self._precondition_store.state,
|
|
407
|
+
camera=self._camera_store.state,
|
|
369
408
|
)
|
|
370
409
|
|
|
371
410
|
def _initialize_state(self) -> None:
|
|
@@ -384,6 +423,9 @@ class StateStore(StateView, ActionHandler):
|
|
|
384
423
|
self._tips = TipView(state.tips)
|
|
385
424
|
self._wells = WellView(state.wells)
|
|
386
425
|
self._files = FileView(state.files)
|
|
426
|
+
self._tasks = TaskView(state.tasks)
|
|
427
|
+
self._preconditions = CommandPreconditionView(state.preconditions)
|
|
428
|
+
self._camera = CameraView(state.camera)
|
|
387
429
|
|
|
388
430
|
# Derived states
|
|
389
431
|
self._geometry = GeometryView(
|
|
@@ -416,6 +458,8 @@ class StateStore(StateView, ActionHandler):
|
|
|
416
458
|
self._liquid_classes._state = next_state.liquid_classes
|
|
417
459
|
self._tips._state = next_state.tips
|
|
418
460
|
self._wells._state = next_state.wells
|
|
461
|
+
self._tasks._state = next_state.tasks
|
|
462
|
+
self._camera._state = next_state.camera
|
|
419
463
|
self._change_notifier.notify()
|
|
420
464
|
if self._notify_robot_server is not None:
|
|
421
465
|
self._notify_robot_server()
|
|
@@ -13,7 +13,9 @@ from ..types import (
|
|
|
13
13
|
Liquid,
|
|
14
14
|
LiquidClassRecordWithId,
|
|
15
15
|
WellInfoSummary,
|
|
16
|
+
TaskSummary,
|
|
16
17
|
)
|
|
18
|
+
from ..resources.camera_provider import CameraSettings
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
class StateSummary(BaseModel):
|
|
@@ -34,3 +36,5 @@ class StateSummary(BaseModel):
|
|
|
34
36
|
wells: List[WellInfoSummary] = Field(default_factory=list)
|
|
35
37
|
files: List[str] = Field(default_factory=list)
|
|
36
38
|
liquidClasses: List[LiquidClassRecordWithId] = Field(default_factory=list)
|
|
39
|
+
tasks: List[TaskSummary] = Field(default_factory=list)
|
|
40
|
+
cameraSettings: Optional[CameraSettings] = None
|
|
@@ -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
|