opentrons 8.4.1a2__py2.py3-none-any.whl → 8.5.0__py2.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/config/defaults_ot3.py +1 -1
- opentrons/hardware_control/backends/flex_protocol.py +25 -0
- opentrons/hardware_control/backends/ot3controller.py +76 -1
- opentrons/hardware_control/backends/ot3simulator.py +27 -0
- opentrons/hardware_control/instruments/ot3/pipette_handler.py +1 -0
- opentrons/hardware_control/ot3api.py +32 -0
- opentrons/legacy_commands/commands.py +16 -4
- opentrons/legacy_commands/robot_commands.py +51 -0
- opentrons/legacy_commands/types.py +91 -2
- opentrons/protocol_api/_liquid.py +60 -15
- opentrons/protocol_api/_liquid_properties.py +149 -90
- opentrons/protocol_api/_transfer_liquid_validation.py +43 -14
- opentrons/protocol_api/core/engine/instrument.py +367 -221
- opentrons/protocol_api/core/engine/protocol.py +14 -15
- opentrons/protocol_api/core/engine/robot.py +2 -2
- opentrons/protocol_api/core/engine/transfer_components_executor.py +275 -163
- opentrons/protocol_api/core/engine/well.py +16 -0
- opentrons/protocol_api/core/instrument.py +11 -5
- opentrons/protocol_api/core/legacy/legacy_instrument_core.py +11 -5
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +2 -2
- opentrons/protocol_api/core/legacy/legacy_well_core.py +8 -0
- opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +11 -5
- opentrons/protocol_api/core/protocol.py +3 -3
- opentrons/protocol_api/core/well.py +8 -0
- opentrons/protocol_api/instrument_context.py +478 -111
- opentrons/protocol_api/labware.py +10 -0
- opentrons/protocol_api/module_contexts.py +5 -2
- opentrons/protocol_api/protocol_context.py +76 -11
- opentrons/protocol_api/robot_context.py +48 -6
- opentrons/protocol_api/validation.py +15 -8
- opentrons/protocol_engine/commands/command_unions.py +10 -10
- opentrons/protocol_engine/commands/generate_command_schema.py +1 -1
- opentrons/protocol_engine/commands/get_next_tip.py +2 -2
- opentrons/protocol_engine/commands/load_labware.py +0 -19
- opentrons/protocol_engine/commands/pick_up_tip.py +9 -3
- opentrons/protocol_engine/commands/robot/__init__.py +20 -20
- opentrons/protocol_engine/commands/robot/close_gripper_jaw.py +34 -24
- opentrons/protocol_engine/commands/robot/open_gripper_jaw.py +29 -20
- opentrons/protocol_engine/commands/seal_pipette_to_tip.py +1 -1
- opentrons/protocol_engine/commands/unsafe/__init__.py +17 -1
- opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +1 -2
- opentrons/protocol_engine/execution/labware_movement.py +9 -2
- opentrons/protocol_engine/execution/movement.py +12 -9
- opentrons/protocol_engine/execution/queue_worker.py +8 -1
- opentrons/protocol_engine/execution/thermocycler_movement_flagger.py +52 -19
- opentrons/protocol_engine/resources/labware_validation.py +7 -1
- opentrons/protocol_engine/state/_well_math.py +2 -2
- opentrons/protocol_engine/state/commands.py +14 -28
- opentrons/protocol_engine/state/frustum_helpers.py +11 -7
- opentrons/protocol_engine/state/labware.py +12 -0
- opentrons/protocol_engine/state/modules.py +1 -1
- opentrons/protocol_engine/state/pipettes.py +8 -0
- opentrons/protocol_engine/state/tips.py +46 -83
- opentrons/protocol_engine/state/update_types.py +8 -23
- opentrons/protocol_engine/types/liquid_level_detection.py +68 -8
- opentrons/protocol_runner/legacy_command_mapper.py +12 -6
- opentrons/protocol_runner/run_orchestrator.py +1 -1
- opentrons/protocols/advanced_control/transfers/common.py +54 -11
- opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +55 -28
- opentrons/protocols/api_support/definitions.py +1 -1
- opentrons/types.py +6 -6
- {opentrons-8.4.1a2.dist-info → opentrons-8.5.0.dist-info}/METADATA +4 -4
- {opentrons-8.4.1a2.dist-info → opentrons-8.5.0.dist-info}/RECORD +67 -66
- {opentrons-8.4.1a2.dist-info → opentrons-8.5.0.dist-info}/LICENSE +0 -0
- {opentrons-8.4.1a2.dist-info → opentrons-8.5.0.dist-info}/WHEEL +0 -0
- {opentrons-8.4.1a2.dist-info → opentrons-8.5.0.dist-info}/entry_points.txt +0 -0
- {opentrons-8.4.1a2.dist-info → opentrons-8.5.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Command models for opening a gripper jaw."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
|
-
from typing import Literal, Type, Optional
|
|
4
|
+
from typing import Literal, Type, Optional, TYPE_CHECKING
|
|
4
5
|
from opentrons.hardware_control import HardwareControlAPI
|
|
5
6
|
from opentrons.protocol_engine.resources import ensure_ot3_hardware
|
|
6
7
|
|
|
@@ -14,64 +15,72 @@ from ..command import (
|
|
|
14
15
|
)
|
|
15
16
|
from opentrons.protocol_engine.errors.error_occurrence import ErrorOccurrence
|
|
16
17
|
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from ...state.state import StateView
|
|
20
|
+
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
OpenGripperJawCommandType = Literal["robot/openGripperJaw"]
|
|
19
23
|
|
|
20
24
|
|
|
21
|
-
class
|
|
25
|
+
class OpenGripperJawParams(BaseModel):
|
|
22
26
|
"""Payload required to release a gripper."""
|
|
23
27
|
|
|
24
28
|
pass
|
|
25
29
|
|
|
26
30
|
|
|
27
|
-
class
|
|
31
|
+
class OpenGripperJawResult(BaseModel):
|
|
28
32
|
"""Result data from the execution of a openGripperJaw command."""
|
|
29
33
|
|
|
30
34
|
pass
|
|
31
35
|
|
|
32
36
|
|
|
33
|
-
class
|
|
34
|
-
AbstractCommandImpl[
|
|
37
|
+
class OpenGripperJawImplementation(
|
|
38
|
+
AbstractCommandImpl[OpenGripperJawParams, SuccessData[OpenGripperJawResult]]
|
|
35
39
|
):
|
|
36
40
|
"""openGripperJaw command implementation."""
|
|
37
41
|
|
|
38
42
|
def __init__(
|
|
39
43
|
self,
|
|
40
44
|
hardware_api: HardwareControlAPI,
|
|
45
|
+
state_view: StateView,
|
|
41
46
|
**kwargs: object,
|
|
42
47
|
) -> None:
|
|
43
48
|
self._hardware_api = hardware_api
|
|
49
|
+
self._state_view = state_view
|
|
44
50
|
|
|
45
51
|
async def execute(
|
|
46
|
-
self, params:
|
|
47
|
-
) -> SuccessData[
|
|
52
|
+
self, params: OpenGripperJawParams
|
|
53
|
+
) -> SuccessData[OpenGripperJawResult]:
|
|
48
54
|
"""Release the gripper."""
|
|
55
|
+
if self._state_view.config.use_virtual_gripper:
|
|
56
|
+
return SuccessData(public=OpenGripperJawResult())
|
|
57
|
+
|
|
49
58
|
ot3_hardware_api = ensure_ot3_hardware(self._hardware_api)
|
|
50
59
|
|
|
51
60
|
await ot3_hardware_api.home_gripper_jaw()
|
|
52
61
|
return SuccessData(
|
|
53
|
-
public=
|
|
62
|
+
public=OpenGripperJawResult(),
|
|
54
63
|
)
|
|
55
64
|
|
|
56
65
|
|
|
57
|
-
class
|
|
58
|
-
BaseCommand[
|
|
66
|
+
class OpenGripperJaw(
|
|
67
|
+
BaseCommand[OpenGripperJawParams, OpenGripperJawResult, ErrorOccurrence]
|
|
59
68
|
):
|
|
60
69
|
"""openGripperJaw command model."""
|
|
61
70
|
|
|
62
|
-
commandType:
|
|
63
|
-
params:
|
|
64
|
-
result: Optional[
|
|
71
|
+
commandType: OpenGripperJawCommandType = "robot/openGripperJaw"
|
|
72
|
+
params: OpenGripperJawParams
|
|
73
|
+
result: Optional[OpenGripperJawResult] = None
|
|
65
74
|
|
|
66
75
|
_ImplementationCls: Type[
|
|
67
|
-
|
|
68
|
-
] =
|
|
76
|
+
OpenGripperJawImplementation
|
|
77
|
+
] = OpenGripperJawImplementation
|
|
69
78
|
|
|
70
79
|
|
|
71
|
-
class
|
|
80
|
+
class OpenGripperJawCreate(BaseCommandCreate[OpenGripperJawParams]):
|
|
72
81
|
"""openGripperJaw command request model."""
|
|
73
82
|
|
|
74
|
-
commandType:
|
|
75
|
-
params:
|
|
83
|
+
commandType: OpenGripperJawCommandType = "robot/openGripperJaw"
|
|
84
|
+
params: OpenGripperJawParams
|
|
76
85
|
|
|
77
|
-
_CommandCls: Type[
|
|
86
|
+
_CommandCls: Type[OpenGripperJaw] = OpenGripperJaw
|
|
@@ -273,7 +273,7 @@ class SealPipetteToTipImplementation(
|
|
|
273
273
|
|
|
274
274
|
# Begin relative pickup steps for the resin tips
|
|
275
275
|
|
|
276
|
-
channels = self._state_view.
|
|
276
|
+
channels = self._state_view.pipettes.get_active_channels(pipette_id)
|
|
277
277
|
mount = self._state_view.pipettes.get_mount(pipette_id)
|
|
278
278
|
tip_pick_up_params = params.tipPickUpParams
|
|
279
279
|
|
|
@@ -1,4 +1,20 @@
|
|
|
1
|
-
"""Commands that
|
|
1
|
+
"""Commands that are "unsafe".
|
|
2
|
+
|
|
3
|
+
"Unsafe" means that they can cause inaccuracy or incorrect behavior. They should
|
|
4
|
+
therefore never be used in protocols, and should only be used otherwise as a last
|
|
5
|
+
resort.
|
|
6
|
+
|
|
7
|
+
These exist as a necessary evil for implementing things like error recovery.
|
|
8
|
+
Even in those narrow contexts, these commands must be used with care.
|
|
9
|
+
e.g. after an `UpdatePositionEstimators` command, there must be a `Home` command,
|
|
10
|
+
or positioning will be subtly wrong. Each unsafe command should document its intended
|
|
11
|
+
use case and its caveats.
|
|
12
|
+
|
|
13
|
+
Because we don't expect unsafe commands to be used in any protocols whose behavior we
|
|
14
|
+
must preserve, we may change the commands' semantics over time. We may also change
|
|
15
|
+
their shapes if we're confident that it won't break something in robot-server's
|
|
16
|
+
persistent storage.
|
|
17
|
+
"""
|
|
2
18
|
|
|
3
19
|
from .unsafe_blow_out_in_place import (
|
|
4
20
|
UnsafeBlowOutInPlaceCommandType,
|
|
@@ -81,8 +81,7 @@ class UnsafeDropTipInPlaceImplementation(
|
|
|
81
81
|
pipette_id=params.pipetteId,
|
|
82
82
|
home_after=params.homeAfter,
|
|
83
83
|
ignore_plunger=(
|
|
84
|
-
self._state_view.
|
|
85
|
-
== 96
|
|
84
|
+
self._state_view.pipettes.get_active_channels(params.pipetteId) == 96
|
|
86
85
|
),
|
|
87
86
|
)
|
|
88
87
|
|
|
@@ -61,6 +61,7 @@ class LabwareMovementHandler:
|
|
|
61
61
|
"""Initialize a LabwareMovementHandler instance."""
|
|
62
62
|
self._hardware_api = hardware_api
|
|
63
63
|
self._state_store = state_store
|
|
64
|
+
self._equipment = equipment
|
|
64
65
|
self._thermocycler_plate_lifter = (
|
|
65
66
|
thermocycler_plate_lifter
|
|
66
67
|
or ThermocyclerPlateLifter(
|
|
@@ -72,7 +73,13 @@ class LabwareMovementHandler:
|
|
|
72
73
|
self._tc_movement_flagger = (
|
|
73
74
|
thermocycler_movement_flagger
|
|
74
75
|
or ThermocyclerMovementFlagger(
|
|
75
|
-
state_store=self._state_store,
|
|
76
|
+
state_store=self._state_store,
|
|
77
|
+
hardware_api=self._hardware_api,
|
|
78
|
+
equipment=self._equipment
|
|
79
|
+
or EquipmentHandler(
|
|
80
|
+
hardware_api=self._hardware_api,
|
|
81
|
+
state_store=self._state_store,
|
|
82
|
+
),
|
|
76
83
|
)
|
|
77
84
|
)
|
|
78
85
|
self._hs_movement_flagger = (
|
|
@@ -264,7 +271,7 @@ class LabwareMovementHandler:
|
|
|
264
271
|
)
|
|
265
272
|
for parent in (current_parent, new_location):
|
|
266
273
|
try:
|
|
267
|
-
await self._tc_movement_flagger.
|
|
274
|
+
await self._tc_movement_flagger.ensure_labware_in_open_thermocycler(
|
|
268
275
|
labware_parent=parent
|
|
269
276
|
)
|
|
270
277
|
await self._hs_movement_flagger.raise_if_labware_latched_on_heater_shaker(
|
|
@@ -24,6 +24,7 @@ from .thermocycler_movement_flagger import ThermocyclerMovementFlagger
|
|
|
24
24
|
from .heater_shaker_movement_flagger import HeaterShakerMovementFlagger
|
|
25
25
|
|
|
26
26
|
from .gantry_mover import GantryMover
|
|
27
|
+
from .equipment import EquipmentHandler
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
log = logging.getLogger(__name__)
|
|
@@ -43,6 +44,7 @@ class MovementHandler:
|
|
|
43
44
|
model_utils: Optional[ModelUtils] = None,
|
|
44
45
|
thermocycler_movement_flagger: Optional[ThermocyclerMovementFlagger] = None,
|
|
45
46
|
heater_shaker_movement_flagger: Optional[HeaterShakerMovementFlagger] = None,
|
|
47
|
+
equipment: Optional[EquipmentHandler] = None,
|
|
46
48
|
) -> None:
|
|
47
49
|
"""Initialize a MovementHandler instance."""
|
|
48
50
|
self._state_store = state_store
|
|
@@ -50,7 +52,13 @@ class MovementHandler:
|
|
|
50
52
|
self._tc_movement_flagger = (
|
|
51
53
|
thermocycler_movement_flagger
|
|
52
54
|
or ThermocyclerMovementFlagger(
|
|
53
|
-
state_store=self._state_store,
|
|
55
|
+
state_store=self._state_store,
|
|
56
|
+
hardware_api=hardware_api,
|
|
57
|
+
equipment=equipment
|
|
58
|
+
or EquipmentHandler(
|
|
59
|
+
hardware_api=hardware_api,
|
|
60
|
+
state_store=state_store,
|
|
61
|
+
),
|
|
54
62
|
)
|
|
55
63
|
)
|
|
56
64
|
self._hs_movement_flagger = (
|
|
@@ -83,8 +91,7 @@ class MovementHandler:
|
|
|
83
91
|
self._state_store.labware.raise_if_labware_has_labware_on_top(
|
|
84
92
|
labware_id=labware_id
|
|
85
93
|
)
|
|
86
|
-
|
|
87
|
-
await self._tc_movement_flagger.raise_if_labware_in_non_open_thermocycler(
|
|
94
|
+
await self._tc_movement_flagger.ensure_labware_in_open_thermocycler(
|
|
88
95
|
labware_parent=self._state_store.labware.get_location(labware_id=labware_id)
|
|
89
96
|
)
|
|
90
97
|
|
|
@@ -105,9 +112,7 @@ class MovementHandler:
|
|
|
105
112
|
self._hs_movement_flagger.raise_if_movement_restricted(
|
|
106
113
|
hs_movement_restrictors=hs_movement_restrictors,
|
|
107
114
|
destination_slot=dest_slot_int,
|
|
108
|
-
is_multi_channel=(
|
|
109
|
-
self._state_store.tips.get_pipette_channels(pipette_id) > 1
|
|
110
|
-
),
|
|
115
|
+
is_multi_channel=(self._state_store.pipettes.get_channels(pipette_id) > 1),
|
|
111
116
|
destination_is_tip_rack=self._state_store.labware.is_tiprack(labware_id),
|
|
112
117
|
)
|
|
113
118
|
|
|
@@ -204,9 +209,7 @@ class MovementHandler:
|
|
|
204
209
|
self._hs_movement_flagger.raise_if_movement_restricted(
|
|
205
210
|
hs_movement_restrictors=hs_movement_restrictors,
|
|
206
211
|
destination_slot=dest_slot_int,
|
|
207
|
-
is_multi_channel=(
|
|
208
|
-
self._state_store.tips.get_pipette_channels(pipette_id) > 1
|
|
209
|
-
),
|
|
212
|
+
is_multi_channel=(self._state_store.pipettes.get_channels(pipette_id) > 1),
|
|
210
213
|
destination_is_tip_rack=False,
|
|
211
214
|
)
|
|
212
215
|
|
|
@@ -72,7 +72,14 @@ class QueueWorker:
|
|
|
72
72
|
try:
|
|
73
73
|
await self._command_executor.execute(command_id=command_id)
|
|
74
74
|
except BaseException:
|
|
75
|
-
log.exception(
|
|
75
|
+
log.exception(
|
|
76
|
+
# The state can tear if e.g. we've finished updating PipetteStore,
|
|
77
|
+
# but the exception came before we could update LabwareStore. Or
|
|
78
|
+
# the exception could have interrupted updating a single store.
|
|
79
|
+
"Unhandled failure in command executor."
|
|
80
|
+
" This is a bug in opentrons.protocol_engine"
|
|
81
|
+
" and has probably left the ProtocolEngine in a torn state."
|
|
82
|
+
)
|
|
76
83
|
raise
|
|
77
84
|
# Yield to the event loop in case we're executing a long sequence of commands
|
|
78
85
|
# that never yields internally. For example, a long sequence of comment commands.
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
"""Helpers for flagging unsafe movements to a Thermocycler Module."""
|
|
2
|
-
|
|
3
2
|
from typing import Optional
|
|
4
3
|
|
|
5
4
|
from opentrons.drivers.types import ThermocyclerLidStatus
|
|
6
5
|
from opentrons.hardware_control import HardwareControlAPI
|
|
7
6
|
from opentrons.hardware_control.modules import Thermocycler as HardwareThermocycler
|
|
8
7
|
|
|
8
|
+
|
|
9
|
+
from opentrons.protocol_engine.state.module_substates import ThermocyclerModuleId
|
|
9
10
|
from ..types import ModuleLocation, LabwareLocation
|
|
10
11
|
from ..state.state import StateStore
|
|
11
12
|
from ..errors import ThermocyclerNotOpenError, WrongModuleTypeError
|
|
12
13
|
|
|
14
|
+
from .equipment import EquipmentHandler
|
|
15
|
+
|
|
13
16
|
|
|
14
17
|
class ThermocyclerMovementFlagger:
|
|
15
18
|
"""A helper for flagging unsafe movements to a Thermocycler Module.
|
|
@@ -19,7 +22,10 @@ class ThermocyclerMovementFlagger:
|
|
|
19
22
|
"""
|
|
20
23
|
|
|
21
24
|
def __init__(
|
|
22
|
-
self,
|
|
25
|
+
self,
|
|
26
|
+
state_store: StateStore,
|
|
27
|
+
hardware_api: HardwareControlAPI,
|
|
28
|
+
equipment: EquipmentHandler,
|
|
23
29
|
) -> None:
|
|
24
30
|
"""Initialize the ThermocyclerMovementFlagger.
|
|
25
31
|
|
|
@@ -28,18 +34,59 @@ class ThermocyclerMovementFlagger:
|
|
|
28
34
|
which Thermocycler a labware is in, if any.
|
|
29
35
|
hardware_api: The underlying hardware interface. Used to query
|
|
30
36
|
Thermocyclers' current lid states.
|
|
37
|
+
equipment: The protocol engine interface to move a present thermocycler to an
|
|
38
|
+
operable state if need be.
|
|
31
39
|
"""
|
|
32
40
|
self._state_store = state_store
|
|
33
41
|
self._hardware_api = hardware_api
|
|
42
|
+
self._equipment = equipment
|
|
43
|
+
|
|
44
|
+
async def _verify_tc_lid_status(self, module_id: str) -> None:
|
|
45
|
+
"""Ensure the thermocycler's lid state is correct or raise an error."""
|
|
46
|
+
try:
|
|
47
|
+
hw_tc_lid_status = await self._get_hardware_thermocycler_lid_status(
|
|
48
|
+
module_id=module_id
|
|
49
|
+
)
|
|
50
|
+
except self._HardwareThermocyclerMissingError as e:
|
|
51
|
+
raise ThermocyclerNotOpenError(
|
|
52
|
+
"Thermocycler must be open when moving to labware inside it,"
|
|
53
|
+
" but can't confirm Thermocycler's current status."
|
|
54
|
+
) from e
|
|
55
|
+
|
|
56
|
+
if (
|
|
57
|
+
hw_tc_lid_status == ThermocyclerLidStatus.IN_BETWEEN
|
|
58
|
+
or hw_tc_lid_status == ThermocyclerLidStatus.UNKNOWN
|
|
59
|
+
):
|
|
60
|
+
# NOTE(cm): due to a potential hardware bug, the thermocycler might lose its position
|
|
61
|
+
# status when idling in an open position and default to UNKNOWN or IN_BETWEEN.
|
|
62
|
+
# This tries to recover only from an unexpected known position.
|
|
63
|
+
await self._open_tc_lid(module_id=module_id)
|
|
64
|
+
hw_tc_lid_status = await self._get_hardware_thermocycler_lid_status(
|
|
65
|
+
module_id=module_id
|
|
66
|
+
)
|
|
67
|
+
if hw_tc_lid_status != ThermocyclerLidStatus.OPEN:
|
|
68
|
+
raise ThermocyclerNotOpenError(
|
|
69
|
+
f"Thermocycler must be open when moving to labware inside it,"
|
|
70
|
+
f' but Thermocycler is currently "{hw_tc_lid_status}".'
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
async def _open_tc_lid(self, module_id: str) -> None:
|
|
74
|
+
"""Try to open the thermocycler lid."""
|
|
75
|
+
tc_hardware = self._equipment.get_module_hardware_api(
|
|
76
|
+
ThermocyclerModuleId(module_id)
|
|
77
|
+
)
|
|
78
|
+
if tc_hardware:
|
|
79
|
+
await tc_hardware.open()
|
|
34
80
|
|
|
35
|
-
async def
|
|
81
|
+
async def ensure_labware_in_open_thermocycler(
|
|
36
82
|
self, labware_parent: LabwareLocation
|
|
37
83
|
) -> None:
|
|
38
84
|
"""Flag unsafe movements to a Thermocycler.
|
|
39
85
|
|
|
40
86
|
If the given labware is in a Thermocycler, and that Thermocycler's lid isn't
|
|
41
87
|
currently open according the engine's thermocycler state as well as
|
|
42
|
-
the hardware API (for non-virtual modules),
|
|
88
|
+
the hardware API (for non-virtual modules), tries to open the thermocycler lid.
|
|
89
|
+
If this is unsuccessful, raises ThermocyclerNotOpenError.
|
|
43
90
|
If it is a virtual module, checks only for thermocycler lid state in engine.
|
|
44
91
|
|
|
45
92
|
Otherwise, no-ops.
|
|
@@ -81,21 +128,7 @@ class ThermocyclerMovementFlagger:
|
|
|
81
128
|
# There is a chance that the engine might not have the latest lid status;
|
|
82
129
|
# do a hardware state check just to be sure that the lid is truly open.
|
|
83
130
|
if not self._state_store.config.use_virtual_modules:
|
|
84
|
-
|
|
85
|
-
hw_tc_lid_status = await self._get_hardware_thermocycler_lid_status(
|
|
86
|
-
module_id=tc_substate.module_id
|
|
87
|
-
)
|
|
88
|
-
except self._HardwareThermocyclerMissingError as e:
|
|
89
|
-
raise ThermocyclerNotOpenError(
|
|
90
|
-
"Thermocycler must be open when moving to labware inside it,"
|
|
91
|
-
" but can't confirm Thermocycler's current status."
|
|
92
|
-
) from e
|
|
93
|
-
|
|
94
|
-
if hw_tc_lid_status != ThermocyclerLidStatus.OPEN:
|
|
95
|
-
raise ThermocyclerNotOpenError(
|
|
96
|
-
f"Thermocycler must be open when moving to labware inside it,"
|
|
97
|
-
f' but Thermocycler is currently "{hw_tc_lid_status}".'
|
|
98
|
-
)
|
|
131
|
+
await self._verify_tc_lid_status(module_id=module_id)
|
|
99
132
|
|
|
100
133
|
async def _get_hardware_thermocycler_lid_status(
|
|
101
134
|
self,
|
|
@@ -48,7 +48,13 @@ def validate_labware_can_be_stacked(
|
|
|
48
48
|
top_labware_definition: LabwareDefinition, below_labware_load_name: str
|
|
49
49
|
) -> bool:
|
|
50
50
|
"""Validate that the labware being loaded onto is in the above labware's stackingOffsetWithLabware definition."""
|
|
51
|
-
return
|
|
51
|
+
return (
|
|
52
|
+
below_labware_load_name in top_labware_definition.stackingOffsetWithLabware
|
|
53
|
+
or (
|
|
54
|
+
"default" in top_labware_definition.stackingOffsetWithLabware
|
|
55
|
+
and top_labware_definition.compatibleParentLabware is None
|
|
56
|
+
)
|
|
57
|
+
)
|
|
52
58
|
|
|
53
59
|
|
|
54
60
|
def validate_labware_can_be_ondeck(definition: LabwareDefinition) -> bool:
|
|
@@ -63,7 +63,7 @@ def wells_covered_dense( # noqa: C901
|
|
|
63
63
|
row_downsample = len(target_wells_by_column[0]) // 8
|
|
64
64
|
if column_downsample < 1 or row_downsample < 1:
|
|
65
65
|
raise InvalidStoredData(
|
|
66
|
-
"This labware cannot be used wells_covered_dense because it is less dense than an SBS 96 standard"
|
|
66
|
+
"This labware cannot be used with wells_covered_dense() because it is less dense than an SBS 96 standard"
|
|
67
67
|
)
|
|
68
68
|
|
|
69
69
|
for nozzle_column in range(len(nozzle_map.columns)):
|
|
@@ -126,7 +126,7 @@ def wells_covered_sparse( # noqa: C901
|
|
|
126
126
|
row_upsample = 8 // len(target_wells_by_column[0])
|
|
127
127
|
if column_upsample < 1 or row_upsample < 1:
|
|
128
128
|
raise InvalidStoredData(
|
|
129
|
-
"This labware cannot be used with wells_covered_sparse because it is more dense than an SBS 96 standard."
|
|
129
|
+
"This labware cannot be used with wells_covered_sparse() because it is more dense than an SBS 96 standard."
|
|
130
130
|
)
|
|
131
131
|
for nozzle_column in range(max(1, len(nozzle_map.columns) // column_upsample)):
|
|
132
132
|
for nozzle_row in range(max(1, len(nozzle_map.rows) // row_upsample)):
|
|
@@ -611,49 +611,35 @@ class CommandView:
|
|
|
611
611
|
"""Get a subset of commands around a given cursor.
|
|
612
612
|
|
|
613
613
|
If the cursor is omitted, a cursor will be selected automatically
|
|
614
|
-
based on the currently running or most recently executed command
|
|
614
|
+
based on the currently running or most recently executed command,
|
|
615
|
+
and the slice of commands returned is the previous `length` commands
|
|
616
|
+
inclusive of the currently running or most recently executed command.
|
|
615
617
|
"""
|
|
616
618
|
command_ids = self._state.command_history.get_filtered_command_ids(
|
|
617
619
|
include_fixit_commands=include_fixit_commands
|
|
618
620
|
)
|
|
619
|
-
running_command = self._state.command_history.get_running_command()
|
|
620
|
-
queued_command_ids = self._state.command_history.get_queue_ids()
|
|
621
621
|
total_length = len(command_ids)
|
|
622
622
|
|
|
623
|
-
# TODO(mm, 2024-05-17): This looks like it's attempting to do the same thing
|
|
624
|
-
# as self.get_current(), but in a different way. Can we unify them?
|
|
625
623
|
if cursor is None:
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
# which we can find just before the first queued command.
|
|
631
|
-
cursor = (
|
|
632
|
-
self._state.command_history.get(queued_command_ids.head()).index - 1
|
|
633
|
-
)
|
|
634
|
-
elif (
|
|
635
|
-
self._state.run_result
|
|
636
|
-
and self._state.run_result == RunResult.FAILED
|
|
637
|
-
and self._state.failed_command
|
|
638
|
-
):
|
|
639
|
-
# Currently, if the run fails, we mark all the commands we didn't
|
|
640
|
-
# reach as failed. This makes command status alone insufficient to
|
|
641
|
-
# find the most recent command that actually executed, so we need to
|
|
642
|
-
# store that separately.
|
|
643
|
-
cursor = self._state.failed_command.index
|
|
624
|
+
current_pointer = self.get_current()
|
|
625
|
+
|
|
626
|
+
if current_pointer is not None:
|
|
627
|
+
cursor = current_pointer.index
|
|
644
628
|
else:
|
|
645
|
-
cursor = total_length -
|
|
629
|
+
cursor = total_length - 1
|
|
630
|
+
|
|
631
|
+
cursor = max(cursor - length + 1, 0)
|
|
646
632
|
|
|
647
633
|
# start is inclusive, stop is exclusive
|
|
648
|
-
|
|
649
|
-
stop = min(total_length,
|
|
634
|
+
start = max(0, min(cursor, total_length - 1))
|
|
635
|
+
stop = min(total_length, start + length)
|
|
650
636
|
commands = self._state.command_history.get_slice(
|
|
651
|
-
start=
|
|
637
|
+
start=start, stop=stop, command_ids=command_ids
|
|
652
638
|
)
|
|
653
639
|
|
|
654
640
|
return CommandSlice(
|
|
655
641
|
commands=commands,
|
|
656
|
-
cursor=
|
|
642
|
+
cursor=start,
|
|
657
643
|
total_length=total_length,
|
|
658
644
|
)
|
|
659
645
|
|
|
@@ -88,10 +88,12 @@ def _circular_frustum_polynomial_roots(
|
|
|
88
88
|
def _volume_from_height_circular(
|
|
89
89
|
target_height: float, segment: ConicalFrustum
|
|
90
90
|
) -> float:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
return segment.volume_from_height_circular(
|
|
92
|
+
top_radius=segment.topDiameter / 2,
|
|
93
|
+
bottom_radius=segment.bottomDiameter / 2,
|
|
94
|
+
target_height=target_height,
|
|
95
|
+
total_height=segment.topHeight - segment.bottomHeight,
|
|
96
|
+
)
|
|
95
97
|
|
|
96
98
|
|
|
97
99
|
def _volume_from_height_rectangular(
|
|
@@ -138,9 +140,7 @@ def _height_from_volume_circular(
|
|
|
138
140
|
target_volume: float, segment: ConicalFrustum
|
|
139
141
|
) -> float:
|
|
140
142
|
"""Find the height given a volume within a squared cone segment."""
|
|
141
|
-
|
|
142
|
-
best_fit_volume = min(volumes, key=lambda x: abs(x - target_volume))
|
|
143
|
-
return segment.volume_to_height_table[best_fit_volume]
|
|
143
|
+
return segment.height_from_volume_search(target_volume)
|
|
144
144
|
|
|
145
145
|
|
|
146
146
|
def _height_from_volume_rectangular(
|
|
@@ -401,8 +401,12 @@ def _find_height_in_partial_frustum(
|
|
|
401
401
|
) -> float:
|
|
402
402
|
"""Look through a sorted list of frusta for a target volume, and find the height at that volume."""
|
|
403
403
|
bottom_section_volume = 0.0
|
|
404
|
+
if target_volume == 0.0:
|
|
405
|
+
return 0.0
|
|
404
406
|
for section, capacity in zip(sorted_well, volumetric_capacity):
|
|
405
407
|
section_top_height, section_volume = capacity
|
|
408
|
+
if target_volume == section_volume + bottom_section_volume:
|
|
409
|
+
return section_top_height
|
|
406
410
|
if (
|
|
407
411
|
bottom_section_volume
|
|
408
412
|
<= target_volume
|
|
@@ -1131,6 +1131,18 @@ class LabwareView:
|
|
|
1131
1131
|
raise errors.LabwareCannotBeStackedError(
|
|
1132
1132
|
f"Labware {top_labware_definition.parameters.loadName} cannot be loaded onto labware {below_labware.loadName}"
|
|
1133
1133
|
)
|
|
1134
|
+
elif (
|
|
1135
|
+
labware_validation.validate_definition_is_lid(top_labware_definition)
|
|
1136
|
+
and top_labware_definition.compatibleParentLabware is not None
|
|
1137
|
+
and self.get_load_name(bottom_labware_id)
|
|
1138
|
+
not in top_labware_definition.compatibleParentLabware
|
|
1139
|
+
):
|
|
1140
|
+
# This parent is assumed to be compatible, unless the lid enumerates
|
|
1141
|
+
# all its compatible parents and this parent is missing from the list.
|
|
1142
|
+
raise ValueError(
|
|
1143
|
+
f"Labware Lid {top_labware_definition.parameters.loadName} may not be loaded on parent labware"
|
|
1144
|
+
f" {self.get_display_name(bottom_labware_id)}."
|
|
1145
|
+
)
|
|
1134
1146
|
elif isinstance(below_labware.location, ModuleLocation):
|
|
1135
1147
|
below_definition = self.get_definition(labware_id=below_labware.id)
|
|
1136
1148
|
if not labware_validation.validate_definition_is_adapter(
|
|
@@ -1361,7 +1361,7 @@ class ModuleView:
|
|
|
1361
1361
|
col = (i % 12) + 1 # Convert index to column (1-12)
|
|
1362
1362
|
well_key = f"{row}{col}"
|
|
1363
1363
|
# Truncate the value to the third decimal place
|
|
1364
|
-
well_map[well_key] = math.floor(value * 1000) / 1000
|
|
1364
|
+
well_map[well_key] = max(0.0, math.floor(value * 1000) / 1000)
|
|
1365
1365
|
return well_map
|
|
1366
1366
|
else:
|
|
1367
1367
|
raise ValueError(
|
|
@@ -650,6 +650,10 @@ class PipetteView:
|
|
|
650
650
|
"""Return the max channels of the pipette."""
|
|
651
651
|
return self.get_config(pipette_id).channels
|
|
652
652
|
|
|
653
|
+
def get_active_channels(self, pipette_id: str) -> int:
|
|
654
|
+
"""Get the number of channels being used in the given pipette's configuration."""
|
|
655
|
+
return self.get_nozzle_configuration(pipette_id).tip_count
|
|
656
|
+
|
|
653
657
|
def get_minimum_volume(self, pipette_id: str) -> float:
|
|
654
658
|
"""Return the given pipette's minimum volume."""
|
|
655
659
|
return self.get_config(pipette_id).min_volume
|
|
@@ -727,6 +731,10 @@ class PipetteView:
|
|
|
727
731
|
nozzle_map = self._state.nozzle_configuration_by_id[pipette_id]
|
|
728
732
|
return nozzle_map.starting_nozzle
|
|
729
733
|
|
|
734
|
+
def get_nozzle_configurations(self) -> Dict[str, NozzleMap]:
|
|
735
|
+
"""Get the nozzle maps of all pipettes, keyed by pipette ID."""
|
|
736
|
+
return self._state.nozzle_configuration_by_id.copy()
|
|
737
|
+
|
|
730
738
|
def get_nozzle_configuration(self, pipette_id: str) -> NozzleMap:
|
|
731
739
|
"""Get the nozzle map of the pipette."""
|
|
732
740
|
return self._state.nozzle_configuration_by_id[pipette_id]
|