opentrons 8.3.2__py2.py3-none-any.whl → 8.4.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.
Potentially problematic release.
This version of opentrons might be problematic. Click here for more details.
- opentrons/calibration_storage/ot2/mark_bad_calibration.py +2 -0
- opentrons/calibration_storage/ot2/tip_length.py +6 -6
- opentrons/config/advanced_settings.py +9 -11
- opentrons/config/feature_flags.py +0 -4
- opentrons/config/reset.py +7 -2
- opentrons/drivers/asyncio/communication/__init__.py +2 -0
- opentrons/drivers/asyncio/communication/async_serial.py +4 -0
- opentrons/drivers/asyncio/communication/errors.py +41 -8
- opentrons/drivers/asyncio/communication/serial_connection.py +36 -10
- opentrons/drivers/flex_stacker/__init__.py +9 -3
- opentrons/drivers/flex_stacker/abstract.py +140 -15
- opentrons/drivers/flex_stacker/driver.py +593 -47
- opentrons/drivers/flex_stacker/errors.py +64 -0
- opentrons/drivers/flex_stacker/simulator.py +222 -24
- opentrons/drivers/flex_stacker/types.py +211 -15
- opentrons/drivers/flex_stacker/utils.py +19 -0
- opentrons/execute.py +4 -2
- opentrons/hardware_control/api.py +5 -0
- opentrons/hardware_control/backends/flex_protocol.py +4 -0
- opentrons/hardware_control/backends/ot3controller.py +12 -1
- opentrons/hardware_control/backends/ot3simulator.py +3 -0
- opentrons/hardware_control/backends/subsystem_manager.py +8 -4
- opentrons/hardware_control/instruments/ot2/instrument_calibration.py +10 -6
- opentrons/hardware_control/instruments/ot3/pipette_handler.py +59 -6
- opentrons/hardware_control/modules/__init__.py +12 -1
- opentrons/hardware_control/modules/absorbance_reader.py +11 -9
- opentrons/hardware_control/modules/flex_stacker.py +498 -0
- opentrons/hardware_control/modules/heater_shaker.py +12 -10
- opentrons/hardware_control/modules/magdeck.py +5 -1
- opentrons/hardware_control/modules/tempdeck.py +5 -1
- opentrons/hardware_control/modules/thermocycler.py +15 -14
- opentrons/hardware_control/modules/types.py +191 -1
- opentrons/hardware_control/modules/utils.py +3 -0
- opentrons/hardware_control/motion_utilities.py +20 -0
- opentrons/hardware_control/ot3api.py +145 -15
- opentrons/hardware_control/protocols/liquid_handler.py +47 -1
- opentrons/hardware_control/types.py +6 -0
- opentrons/legacy_commands/commands.py +102 -5
- opentrons/legacy_commands/helpers.py +74 -1
- opentrons/legacy_commands/types.py +33 -2
- opentrons/protocol_api/__init__.py +2 -0
- opentrons/protocol_api/_liquid.py +39 -8
- opentrons/protocol_api/_liquid_properties.py +20 -19
- opentrons/protocol_api/_transfer_liquid_validation.py +91 -0
- opentrons/protocol_api/core/common.py +3 -1
- opentrons/protocol_api/core/engine/deck_conflict.py +11 -1
- opentrons/protocol_api/core/engine/instrument.py +1356 -107
- opentrons/protocol_api/core/engine/labware.py +8 -4
- opentrons/protocol_api/core/engine/load_labware_params.py +68 -10
- opentrons/protocol_api/core/engine/module_core.py +118 -2
- opentrons/protocol_api/core/engine/pipette_movement_conflict.py +6 -14
- opentrons/protocol_api/core/engine/protocol.py +253 -11
- opentrons/protocol_api/core/engine/stringify.py +19 -8
- opentrons/protocol_api/core/engine/transfer_components_executor.py +858 -0
- opentrons/protocol_api/core/engine/well.py +73 -5
- opentrons/protocol_api/core/instrument.py +71 -21
- opentrons/protocol_api/core/labware.py +6 -2
- opentrons/protocol_api/core/legacy/labware_offset_provider.py +7 -3
- opentrons/protocol_api/core/legacy/legacy_instrument_core.py +76 -49
- opentrons/protocol_api/core/legacy/legacy_labware_core.py +8 -4
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +36 -0
- opentrons/protocol_api/core/legacy/legacy_well_core.py +27 -2
- opentrons/protocol_api/core/legacy/load_info.py +4 -12
- opentrons/protocol_api/core/legacy/module_geometry.py +6 -1
- opentrons/protocol_api/core/legacy/well_geometry.py +3 -3
- opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +73 -23
- opentrons/protocol_api/core/module.py +43 -0
- opentrons/protocol_api/core/protocol.py +33 -0
- opentrons/protocol_api/core/well.py +23 -2
- opentrons/protocol_api/instrument_context.py +454 -150
- opentrons/protocol_api/labware.py +98 -50
- opentrons/protocol_api/module_contexts.py +140 -0
- opentrons/protocol_api/protocol_context.py +163 -19
- opentrons/protocol_api/validation.py +51 -41
- opentrons/protocol_engine/__init__.py +21 -2
- opentrons/protocol_engine/actions/actions.py +5 -5
- opentrons/protocol_engine/clients/sync_client.py +6 -0
- opentrons/protocol_engine/commands/__init__.py +66 -36
- opentrons/protocol_engine/commands/absorbance_reader/__init__.py +0 -1
- opentrons/protocol_engine/commands/air_gap_in_place.py +3 -2
- opentrons/protocol_engine/commands/aspirate.py +6 -2
- opentrons/protocol_engine/commands/aspirate_in_place.py +3 -1
- opentrons/protocol_engine/commands/aspirate_while_tracking.py +210 -0
- opentrons/protocol_engine/commands/blow_out.py +2 -0
- opentrons/protocol_engine/commands/blow_out_in_place.py +4 -1
- opentrons/protocol_engine/commands/command_unions.py +102 -33
- opentrons/protocol_engine/commands/configure_for_volume.py +3 -0
- opentrons/protocol_engine/commands/dispense.py +3 -1
- opentrons/protocol_engine/commands/dispense_in_place.py +3 -0
- opentrons/protocol_engine/commands/dispense_while_tracking.py +204 -0
- opentrons/protocol_engine/commands/drop_tip.py +23 -1
- opentrons/protocol_engine/commands/flex_stacker/__init__.py +106 -0
- opentrons/protocol_engine/commands/flex_stacker/close_latch.py +72 -0
- opentrons/protocol_engine/commands/flex_stacker/common.py +15 -0
- opentrons/protocol_engine/commands/flex_stacker/empty.py +161 -0
- opentrons/protocol_engine/commands/flex_stacker/fill.py +164 -0
- opentrons/protocol_engine/commands/flex_stacker/open_latch.py +70 -0
- opentrons/protocol_engine/commands/flex_stacker/prepare_shuttle.py +112 -0
- opentrons/protocol_engine/commands/flex_stacker/retrieve.py +394 -0
- opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +190 -0
- opentrons/protocol_engine/commands/flex_stacker/store.py +291 -0
- opentrons/protocol_engine/commands/generate_command_schema.py +31 -2
- opentrons/protocol_engine/commands/labware_handling_common.py +29 -0
- opentrons/protocol_engine/commands/liquid_probe.py +27 -13
- opentrons/protocol_engine/commands/load_labware.py +42 -39
- opentrons/protocol_engine/commands/load_lid.py +21 -13
- opentrons/protocol_engine/commands/load_lid_stack.py +130 -47
- opentrons/protocol_engine/commands/load_module.py +18 -17
- opentrons/protocol_engine/commands/load_pipette.py +3 -0
- opentrons/protocol_engine/commands/move_labware.py +139 -20
- opentrons/protocol_engine/commands/move_to_well.py +5 -11
- opentrons/protocol_engine/commands/pick_up_tip.py +5 -2
- opentrons/protocol_engine/commands/pipetting_common.py +159 -8
- opentrons/protocol_engine/commands/prepare_to_aspirate.py +15 -5
- opentrons/protocol_engine/commands/{evotip_dispense.py → pressure_dispense.py} +33 -34
- opentrons/protocol_engine/commands/reload_labware.py +6 -19
- opentrons/protocol_engine/commands/{evotip_seal_pipette.py → seal_pipette_to_tip.py} +97 -76
- opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +3 -1
- opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +6 -1
- opentrons/protocol_engine/commands/{evotip_unseal_pipette.py → unseal_pipette_from_tip.py} +31 -40
- opentrons/protocol_engine/errors/__init__.py +10 -0
- opentrons/protocol_engine/errors/exceptions.py +62 -0
- opentrons/protocol_engine/execution/equipment.py +123 -106
- opentrons/protocol_engine/execution/labware_movement.py +8 -6
- opentrons/protocol_engine/execution/pipetting.py +235 -25
- opentrons/protocol_engine/execution/tip_handler.py +82 -32
- opentrons/protocol_engine/labware_offset_standardization.py +194 -0
- opentrons/protocol_engine/protocol_engine.py +22 -13
- opentrons/protocol_engine/resources/deck_configuration_provider.py +98 -2
- opentrons/protocol_engine/resources/deck_data_provider.py +1 -1
- opentrons/protocol_engine/resources/labware_data_provider.py +32 -12
- opentrons/protocol_engine/resources/labware_validation.py +7 -5
- opentrons/protocol_engine/slot_standardization.py +11 -23
- opentrons/protocol_engine/state/addressable_areas.py +84 -46
- opentrons/protocol_engine/state/frustum_helpers.py +36 -14
- opentrons/protocol_engine/state/geometry.py +892 -227
- opentrons/protocol_engine/state/labware.py +252 -55
- opentrons/protocol_engine/state/module_substates/__init__.py +4 -0
- opentrons/protocol_engine/state/module_substates/flex_stacker_substate.py +68 -0
- opentrons/protocol_engine/state/module_substates/heater_shaker_module_substate.py +22 -0
- opentrons/protocol_engine/state/module_substates/temperature_module_substate.py +13 -0
- opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +20 -0
- opentrons/protocol_engine/state/modules.py +210 -67
- opentrons/protocol_engine/state/pipettes.py +54 -0
- opentrons/protocol_engine/state/state.py +1 -1
- opentrons/protocol_engine/state/tips.py +14 -0
- opentrons/protocol_engine/state/update_types.py +180 -25
- opentrons/protocol_engine/state/wells.py +55 -9
- opentrons/protocol_engine/types/__init__.py +300 -0
- opentrons/protocol_engine/types/automatic_tip_selection.py +39 -0
- opentrons/protocol_engine/types/command_annotations.py +53 -0
- opentrons/protocol_engine/types/deck_configuration.py +72 -0
- opentrons/protocol_engine/types/execution.py +96 -0
- opentrons/protocol_engine/types/hardware_passthrough.py +25 -0
- opentrons/protocol_engine/types/instrument.py +47 -0
- opentrons/protocol_engine/types/instrument_sensors.py +47 -0
- opentrons/protocol_engine/types/labware.py +111 -0
- opentrons/protocol_engine/types/labware_movement.py +22 -0
- opentrons/protocol_engine/types/labware_offset_location.py +111 -0
- opentrons/protocol_engine/types/labware_offset_vector.py +33 -0
- opentrons/protocol_engine/types/liquid.py +40 -0
- opentrons/protocol_engine/types/liquid_class.py +59 -0
- opentrons/protocol_engine/types/liquid_handling.py +13 -0
- opentrons/protocol_engine/types/liquid_level_detection.py +131 -0
- opentrons/protocol_engine/types/location.py +194 -0
- opentrons/protocol_engine/types/module.py +301 -0
- opentrons/protocol_engine/types/partial_tip_configuration.py +76 -0
- opentrons/protocol_engine/types/run_time_parameters.py +133 -0
- opentrons/protocol_engine/types/tip.py +18 -0
- opentrons/protocol_engine/types/util.py +21 -0
- opentrons/protocol_engine/types/well_position.py +124 -0
- opentrons/protocol_reader/extract_labware_definitions.py +7 -3
- opentrons/protocol_reader/file_format_validator.py +5 -3
- opentrons/protocol_runner/json_translator.py +4 -2
- opentrons/protocol_runner/legacy_command_mapper.py +6 -2
- opentrons/protocol_runner/run_orchestrator.py +4 -1
- opentrons/protocols/advanced_control/transfers/common.py +48 -1
- opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +204 -0
- opentrons/protocols/api_support/definitions.py +1 -1
- opentrons/protocols/api_support/instrument.py +16 -3
- opentrons/protocols/labware.py +27 -23
- opentrons/protocols/models/__init__.py +0 -21
- opentrons/simulate.py +4 -2
- opentrons/types.py +20 -7
- opentrons/util/logging_config.py +94 -25
- opentrons/util/logging_queue_handler.py +61 -0
- {opentrons-8.3.2.dist-info → opentrons-8.4.0.dist-info}/METADATA +4 -4
- {opentrons-8.3.2.dist-info → opentrons-8.4.0.dist-info}/RECORD +192 -151
- opentrons/calibration_storage/ot2/models/defaults.py +0 -0
- opentrons/calibration_storage/ot3/models/defaults.py +0 -0
- opentrons/protocol_api/core/legacy/legacy_robot_core.py +0 -0
- opentrons/protocol_engine/types.py +0 -1311
- {opentrons-8.3.2.dist-info → opentrons-8.4.0.dist-info}/LICENSE +0 -0
- {opentrons-8.3.2.dist-info → opentrons-8.4.0.dist-info}/WHEEL +0 -0
- {opentrons-8.3.2.dist-info → opentrons-8.4.0.dist-info}/entry_points.txt +0 -0
- {opentrons-8.3.2.dist-info → opentrons-8.4.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Protocol Engine types to deal with tips."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class TipGeometry:
|
|
8
|
+
"""Tip geometry data.
|
|
9
|
+
|
|
10
|
+
Props:
|
|
11
|
+
length: The effective length (total length minus overlap) of a tip in mm.
|
|
12
|
+
diameter: Tip diameter in mm.
|
|
13
|
+
volume: Maximum volume in µL.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
length: float
|
|
17
|
+
diameter: float
|
|
18
|
+
volume: float
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Protocol engine utility types for model components."""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class Dimensions:
|
|
9
|
+
"""Dimensions of an object in deck-space."""
|
|
10
|
+
|
|
11
|
+
x: float
|
|
12
|
+
y: float
|
|
13
|
+
z: float
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Vec3f(BaseModel):
|
|
17
|
+
"""A 3D vector of floats."""
|
|
18
|
+
|
|
19
|
+
x: float
|
|
20
|
+
y: float
|
|
21
|
+
z: float
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Protocol engine types to do with positions inside wells."""
|
|
2
|
+
from enum import Enum, auto
|
|
3
|
+
from typing import Union, Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WellOrigin(str, Enum):
|
|
9
|
+
"""Origin of WellLocation offset.
|
|
10
|
+
|
|
11
|
+
Props:
|
|
12
|
+
TOP: the top-center of the well
|
|
13
|
+
BOTTOM: the bottom-center of the well
|
|
14
|
+
CENTER: the middle-center of the well
|
|
15
|
+
MENISCUS: the meniscus-center of the well
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
TOP = "top"
|
|
19
|
+
BOTTOM = "bottom"
|
|
20
|
+
CENTER = "center"
|
|
21
|
+
MENISCUS = "meniscus"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PickUpTipWellOrigin(str, Enum):
|
|
25
|
+
"""The origin of a PickUpTipWellLocation offset.
|
|
26
|
+
|
|
27
|
+
Props:
|
|
28
|
+
TOP: the top-center of the well
|
|
29
|
+
BOTTOM: the bottom-center of the well
|
|
30
|
+
CENTER: the middle-center of the well
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
TOP = "top"
|
|
34
|
+
BOTTOM = "bottom"
|
|
35
|
+
CENTER = "center"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DropTipWellOrigin(str, Enum):
|
|
39
|
+
"""The origin of a DropTipWellLocation offset.
|
|
40
|
+
|
|
41
|
+
Props:
|
|
42
|
+
TOP: the top-center of the well
|
|
43
|
+
BOTTOM: the bottom-center of the well
|
|
44
|
+
CENTER: the middle-center of the well
|
|
45
|
+
DEFAULT: the default drop-tip location of the well,
|
|
46
|
+
based on pipette configuration and length of the tip.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
TOP = "top"
|
|
50
|
+
BOTTOM = "bottom"
|
|
51
|
+
CENTER = "center"
|
|
52
|
+
DEFAULT = "default"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class WellLocationFunction(int, Enum):
|
|
56
|
+
"""The type of well location object to be created."""
|
|
57
|
+
|
|
58
|
+
BASE = auto()
|
|
59
|
+
LIQUID_HANDLING = auto()
|
|
60
|
+
PICK_UP_TIP = auto()
|
|
61
|
+
DROP_TIP = auto()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# This is deliberately a separate type from Vec3f to let components default to 0.
|
|
65
|
+
class WellOffset(BaseModel):
|
|
66
|
+
"""An offset vector in (x, y, z)."""
|
|
67
|
+
|
|
68
|
+
x: float = 0
|
|
69
|
+
y: float = 0
|
|
70
|
+
z: float = 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class WellLocation(BaseModel):
|
|
74
|
+
"""A relative location in reference to a well's location."""
|
|
75
|
+
|
|
76
|
+
origin: WellOrigin = WellOrigin.TOP
|
|
77
|
+
offset: WellOffset = Field(default_factory=WellOffset)
|
|
78
|
+
volumeOffset: float = Field(
|
|
79
|
+
default=0.0,
|
|
80
|
+
description="""A volume of liquid, in µL, to offset the z-axis offset.""",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class LiquidHandlingWellLocation(BaseModel):
|
|
85
|
+
"""A relative location in reference to a well's location.
|
|
86
|
+
|
|
87
|
+
To be used with commands that handle liquids.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
origin: WellOrigin = WellOrigin.TOP
|
|
91
|
+
offset: WellOffset = Field(default_factory=WellOffset)
|
|
92
|
+
volumeOffset: Union[float, Literal["operationVolume"]] = Field(
|
|
93
|
+
default=0.0,
|
|
94
|
+
description="""A volume of liquid, in µL, to offset the z-axis offset. When "operationVolume" is specified, this volume is pulled from the command volume parameter.""",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class PickUpTipWellLocation(BaseModel):
|
|
99
|
+
"""A relative location in reference to a well's location.
|
|
100
|
+
|
|
101
|
+
To be used for picking up tips.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
origin: PickUpTipWellOrigin = PickUpTipWellOrigin.TOP
|
|
105
|
+
offset: WellOffset = Field(default_factory=WellOffset)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class DropTipWellLocation(BaseModel):
|
|
109
|
+
"""Like WellLocation, but for dropping tips.
|
|
110
|
+
|
|
111
|
+
Unlike a typical WellLocation, the location for a drop tip
|
|
112
|
+
defaults to location based on the tip length rather than the well's top.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
origin: DropTipWellOrigin = DropTipWellOrigin.DEFAULT
|
|
116
|
+
offset: WellOffset = Field(default_factory=WellOffset)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
WellLocationType = Union[
|
|
120
|
+
WellLocation,
|
|
121
|
+
LiquidHandlingWellLocation,
|
|
122
|
+
PickUpTipWellLocation,
|
|
123
|
+
DropTipWellLocation,
|
|
124
|
+
]
|
|
@@ -6,7 +6,10 @@ from typing import List
|
|
|
6
6
|
|
|
7
7
|
import anyio
|
|
8
8
|
|
|
9
|
-
from
|
|
9
|
+
from opentrons_shared_data.labware.labware_definition import (
|
|
10
|
+
LabwareDefinition,
|
|
11
|
+
labware_definition_type_adapter,
|
|
12
|
+
)
|
|
10
13
|
|
|
11
14
|
from .protocol_source import ProtocolFileRole, ProtocolSource, ProtocolType
|
|
12
15
|
|
|
@@ -42,7 +45,7 @@ async def extract_labware_definitions(
|
|
|
42
45
|
|
|
43
46
|
async def _extract_from_labware_file(path: Path) -> LabwareDefinition:
|
|
44
47
|
def _do_parse() -> LabwareDefinition:
|
|
45
|
-
return
|
|
48
|
+
return labware_definition_type_adapter.validate_json(path.read_bytes())
|
|
46
49
|
|
|
47
50
|
return await anyio.to_thread.run_sync(_do_parse)
|
|
48
51
|
|
|
@@ -55,7 +58,8 @@ async def _extract_from_json_protocol_file(path: Path) -> List[LabwareDefinition
|
|
|
55
58
|
# which require this labwareDefinitions key.
|
|
56
59
|
unvalidated_definitions = json_contents["labwareDefinitions"].values()
|
|
57
60
|
validated_definitions = [
|
|
58
|
-
|
|
61
|
+
labware_definition_type_adapter.validate_python(u)
|
|
62
|
+
for u in unvalidated_definitions
|
|
59
63
|
]
|
|
60
64
|
return validated_definitions
|
|
61
65
|
|
|
@@ -6,7 +6,9 @@ from typing import Iterable
|
|
|
6
6
|
import anyio
|
|
7
7
|
from pydantic import ValidationError as PydanticValidationError
|
|
8
8
|
|
|
9
|
-
from opentrons_shared_data.labware.labware_definition import
|
|
9
|
+
from opentrons_shared_data.labware.labware_definition import (
|
|
10
|
+
labware_definition_type_adapter,
|
|
11
|
+
)
|
|
10
12
|
from opentrons_shared_data.protocol.models import (
|
|
11
13
|
ProtocolSchemaV6 as JsonProtocolV6,
|
|
12
14
|
ProtocolSchemaV7 as JsonProtocolV7,
|
|
@@ -14,7 +16,7 @@ from opentrons_shared_data.protocol.models import (
|
|
|
14
16
|
)
|
|
15
17
|
from opentrons_shared_data.errors.exceptions import PythonException
|
|
16
18
|
|
|
17
|
-
from opentrons.protocols.models import
|
|
19
|
+
from opentrons.protocols.models.json_protocol import Model as JsonProtocolUpToV5
|
|
18
20
|
|
|
19
21
|
from .file_identifier import (
|
|
20
22
|
IdentifiedFile,
|
|
@@ -60,7 +62,7 @@ class FileFormatValidator:
|
|
|
60
62
|
async def _validate_labware_definition(info: IdentifiedLabwareDefinition) -> None:
|
|
61
63
|
def validate_sync() -> None:
|
|
62
64
|
try:
|
|
63
|
-
|
|
65
|
+
labware_definition_type_adapter.validate_python(info.unvalidated_json)
|
|
64
66
|
except PydanticValidationError as e:
|
|
65
67
|
raise FileFormatValidationError(
|
|
66
68
|
message=f"{info.original_file.name} could not be read as a labware definition.",
|
|
@@ -20,7 +20,7 @@ from opentrons_shared_data.errors.exceptions import InvalidProtocolData, PythonE
|
|
|
20
20
|
from opentrons.types import MountType
|
|
21
21
|
from opentrons.protocol_engine import (
|
|
22
22
|
commands as pe_commands,
|
|
23
|
-
|
|
23
|
+
LoadableLabwareLocation,
|
|
24
24
|
ModuleModel,
|
|
25
25
|
DeckSlotLocation,
|
|
26
26
|
Liquid,
|
|
@@ -37,7 +37,9 @@ class CommandTranslatorError(Exception):
|
|
|
37
37
|
# Each time a TypeAdapter is instantiated, it will construct a new validator and
|
|
38
38
|
# serializer. To improve performance, TypeAdapters are instantiated once.
|
|
39
39
|
# See https://docs.pydantic.dev/latest/concepts/performance/#typeadapter-instantiated-once
|
|
40
|
-
LabwareLocationAdapter: TypeAdapter[
|
|
40
|
+
LabwareLocationAdapter: TypeAdapter[LoadableLabwareLocation] = TypeAdapter(
|
|
41
|
+
LoadableLabwareLocation
|
|
42
|
+
)
|
|
41
43
|
CommandAnnotationAdapter: TypeAdapter[CommandAnnotation] = TypeAdapter(
|
|
42
44
|
CommandAnnotation
|
|
43
45
|
)
|
|
@@ -38,7 +38,9 @@ from opentrons.protocol_engine.state.update_types import (
|
|
|
38
38
|
StateUpdate,
|
|
39
39
|
)
|
|
40
40
|
|
|
41
|
-
from opentrons_shared_data.labware.labware_definition import
|
|
41
|
+
from opentrons_shared_data.labware.labware_definition import (
|
|
42
|
+
labware_definition_type_adapter,
|
|
43
|
+
)
|
|
42
44
|
from opentrons_shared_data.errors import ErrorCodes, EnumeratedError, PythonException
|
|
43
45
|
|
|
44
46
|
|
|
@@ -659,10 +661,12 @@ class LegacyCommandMapper:
|
|
|
659
661
|
notes=[],
|
|
660
662
|
result=pe_commands.LoadLabwareResult.model_construct(
|
|
661
663
|
labwareId=labware_id,
|
|
662
|
-
definition=
|
|
664
|
+
definition=labware_definition_type_adapter.validate_python(
|
|
663
665
|
labware_load_info.labware_definition
|
|
664
666
|
),
|
|
665
667
|
offsetId=labware_load_info.offset_id,
|
|
668
|
+
# These legacy json protocols don't get location sequences because
|
|
669
|
+
# to do so we'd have to go back and look up where the module gets loaded
|
|
666
670
|
),
|
|
667
671
|
)
|
|
668
672
|
queue_action = pe_actions.QueueCommandAction(
|
|
@@ -32,6 +32,7 @@ from ..protocol_engine.types import (
|
|
|
32
32
|
PostRunHardwareState,
|
|
33
33
|
EngineStatus,
|
|
34
34
|
LabwareOffsetCreate,
|
|
35
|
+
LegacyLabwareOffsetCreate,
|
|
35
36
|
LabwareOffset,
|
|
36
37
|
DeckConfigurationType,
|
|
37
38
|
RunTimeParameter,
|
|
@@ -346,7 +347,9 @@ class RunOrchestrator:
|
|
|
346
347
|
"""Get whether the run has stopped."""
|
|
347
348
|
return self._protocol_engine.state_view.commands.get_is_stopped()
|
|
348
349
|
|
|
349
|
-
def add_labware_offset(
|
|
350
|
+
def add_labware_offset(
|
|
351
|
+
self, request: LabwareOffsetCreate | LegacyLabwareOffsetCreate
|
|
352
|
+
) -> LabwareOffset:
|
|
350
353
|
"""Add a new labware offset to state."""
|
|
351
354
|
return self._protocol_engine.add_labware_offset(request)
|
|
352
355
|
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
"""Common functions between v1 transfer and liquid-class-based transfer."""
|
|
2
2
|
import enum
|
|
3
|
-
|
|
3
|
+
import math
|
|
4
|
+
from typing import Iterable, Generator, Tuple, TypeVar, Literal, List
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class NoLiquidClassPropertyError(ValueError):
|
|
8
|
+
"""An error raised when a liquid class property cannot be found for a pipette/tip combination"""
|
|
4
9
|
|
|
5
10
|
|
|
6
11
|
class TransferTipPolicyV2(enum.Enum):
|
|
@@ -33,6 +38,23 @@ def check_valid_volume_parameters(
|
|
|
33
38
|
)
|
|
34
39
|
|
|
35
40
|
|
|
41
|
+
def check_valid_liquid_class_volume_parameters(
|
|
42
|
+
aspirate_volume: float, air_gap: float, disposal_volume: float, max_volume: float
|
|
43
|
+
) -> None:
|
|
44
|
+
if air_gap + aspirate_volume > max_volume:
|
|
45
|
+
raise ValueError(
|
|
46
|
+
f"Cannot have an air gap of {air_gap} µL for an aspiration of {aspirate_volume} µL"
|
|
47
|
+
f" with a max volume of {max_volume} µL. Please adjust the retract air gap to fit within"
|
|
48
|
+
f" the bounds of the tip."
|
|
49
|
+
)
|
|
50
|
+
elif disposal_volume + aspirate_volume > max_volume:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"Cannot have a dispense volume of {disposal_volume} µL for an aspiration of {aspirate_volume} µL"
|
|
53
|
+
f" with a max volume of {max_volume} µL. Please adjust the dispense volume to fit within"
|
|
54
|
+
f" the bounds of the tip."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
36
58
|
def expand_for_volume_constraints(
|
|
37
59
|
volumes: Iterable[float],
|
|
38
60
|
targets: Iterable[Target],
|
|
@@ -54,3 +76,28 @@ def expand_for_volume_constraints(
|
|
|
54
76
|
volume /= 2
|
|
55
77
|
yield volume, target
|
|
56
78
|
yield volume, target
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _split_volume_equally(volume: float, max_volume: float) -> List[float]:
|
|
82
|
+
"""
|
|
83
|
+
Splits a given volume into a list of volumes that are all less than or equal to max volume.
|
|
84
|
+
|
|
85
|
+
If volume provided is more than the max volume, the volumes will be split evenly.
|
|
86
|
+
"""
|
|
87
|
+
if volume <= max_volume:
|
|
88
|
+
return [volume]
|
|
89
|
+
else:
|
|
90
|
+
iterations = math.ceil(volume / max_volume)
|
|
91
|
+
return [volume / iterations for _ in range(iterations)]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def expand_for_volume_constraints_for_liquid_classes(
|
|
95
|
+
volumes: Iterable[float],
|
|
96
|
+
targets: Iterable[Target],
|
|
97
|
+
max_volume: float,
|
|
98
|
+
) -> Generator[Tuple[float, "Target"], None, None]:
|
|
99
|
+
"""Split a sequence of proposed transfers to keep each under the max volume, splitting larger ones equally."""
|
|
100
|
+
assert max_volume > 0
|
|
101
|
+
for volume, target in zip(volumes, targets):
|
|
102
|
+
for split_volume in _split_volume_equally(volume, max_volume):
|
|
103
|
+
yield split_volume, target
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Utility functions for transfer_liquid, consolidate_liquid and distribute_liquid"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Literal, Sequence, List, Optional, TYPE_CHECKING
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from opentrons.protocol_engine.errors import LiquidHeightUnknownError
|
|
8
|
+
from opentrons.protocol_engine.state._well_math import (
|
|
9
|
+
wells_covered_by_pipette_configuration,
|
|
10
|
+
)
|
|
11
|
+
from opentrons.types import NozzleMapInterface, NozzleConfigurationType
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from logging import Logger
|
|
15
|
+
from opentrons.types import Location
|
|
16
|
+
from opentrons.protocol_api.core.engine import WellCore
|
|
17
|
+
from opentrons.protocol_api.labware import Well, Labware
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class LocationCheckDescriptors:
|
|
22
|
+
location_type: Literal["submerge start", "retract end"]
|
|
23
|
+
pipetting_action: Literal["aspirate", "dispense"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def raise_if_location_inside_liquid(
|
|
27
|
+
location: Location,
|
|
28
|
+
well_location: Location,
|
|
29
|
+
well_core: WellCore,
|
|
30
|
+
location_check_descriptors: LocationCheckDescriptors,
|
|
31
|
+
logger: Logger,
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Raise an error if the location in question would be inside the liquid.
|
|
34
|
+
|
|
35
|
+
This checker will raise an error if:
|
|
36
|
+
- the location in question is below the target well location during aspirate/dispense or,
|
|
37
|
+
- if we can find the liquid height AND the location in question is below this height.
|
|
38
|
+
If we can't find the liquid height, then we simply log a warning and no error is raised.
|
|
39
|
+
"""
|
|
40
|
+
if location.point.z < well_location.point.z:
|
|
41
|
+
raise RuntimeError(
|
|
42
|
+
f"Received {location_check_descriptors.location_type} location of {location}"
|
|
43
|
+
f" and {location_check_descriptors.pipetting_action} location of {well_location}."
|
|
44
|
+
f" {location_check_descriptors.location_type.capitalize()} location z should not be lower"
|
|
45
|
+
f" than the {location_check_descriptors.pipetting_action} location z."
|
|
46
|
+
)
|
|
47
|
+
try:
|
|
48
|
+
liquid_height_from_bottom = well_core.current_liquid_height()
|
|
49
|
+
except LiquidHeightUnknownError:
|
|
50
|
+
liquid_height_from_bottom = None
|
|
51
|
+
if liquid_height_from_bottom is not None:
|
|
52
|
+
if liquid_height_from_bottom + well_core.get_bottom(0).z > location.point.z:
|
|
53
|
+
raise RuntimeError(
|
|
54
|
+
f"{location_check_descriptors.location_type.capitalize()} location {location} is"
|
|
55
|
+
f" inside the liquid in well {well_core.get_display_name()} when it should be outside"
|
|
56
|
+
f"(above) the liquid."
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
# We could raise an error here but that would restrict the use of
|
|
60
|
+
# liquid classes-based transfer to only when LPD is enabled or when liquids are
|
|
61
|
+
# loaded in protocols using `load_liquid`. This can be quite restrictive
|
|
62
|
+
# so we will not raise but just log a warning.
|
|
63
|
+
logger.warning(
|
|
64
|
+
f"Could not verify height of liquid in well {well_core.get_display_name()}, either"
|
|
65
|
+
f" because the liquid in this well has not been probed or because"
|
|
66
|
+
f" liquid was not loaded in this well using `load_liquid`."
|
|
67
|
+
f" Proceeding without verifying if {location_check_descriptors.location_type}"
|
|
68
|
+
f" location is outside the liquid."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def group_wells_for_multi_channel_transfer(
|
|
73
|
+
targets: Sequence[Well],
|
|
74
|
+
nozzle_map: NozzleMapInterface,
|
|
75
|
+
) -> List[Well]:
|
|
76
|
+
"""Takes a list of wells and a nozzle map and returns a list of target wells to address every well given
|
|
77
|
+
|
|
78
|
+
This currently only supports 8-tip columns, 12-tip rows and full 96-channel configurations,
|
|
79
|
+
and only is used for 96 and 384 well plates. This assumes the wells are being given in a
|
|
80
|
+
contiguous order (or every other for 384), and will raise if a well is found that does not overlap
|
|
81
|
+
with the first target well given for a sequence, or if not all wells are given for that sequence.
|
|
82
|
+
"""
|
|
83
|
+
configuration = nozzle_map.configuration
|
|
84
|
+
active_nozzles = nozzle_map.tip_count
|
|
85
|
+
|
|
86
|
+
if (
|
|
87
|
+
(
|
|
88
|
+
(
|
|
89
|
+
configuration == NozzleConfigurationType.COLUMN
|
|
90
|
+
or configuration == NozzleConfigurationType.FULL
|
|
91
|
+
)
|
|
92
|
+
and active_nozzles == 8
|
|
93
|
+
)
|
|
94
|
+
or (configuration == NozzleConfigurationType.ROW and active_nozzles == 12)
|
|
95
|
+
or active_nozzles == 96
|
|
96
|
+
):
|
|
97
|
+
return _group_wells_for_nozzle_configuration(list(targets), nozzle_map)
|
|
98
|
+
else:
|
|
99
|
+
raise ValueError("Unsupported tip configuration for well grouping")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _group_wells_for_nozzle_configuration( # noqa: C901
|
|
103
|
+
targets: List[Well], nozzle_map: NozzleMapInterface
|
|
104
|
+
) -> List[Well]:
|
|
105
|
+
"""Groups wells together for a column, row, or full 96 configuration and returns a reduced list of target wells."""
|
|
106
|
+
grouped_wells = []
|
|
107
|
+
active_wells_covered: List[str] = []
|
|
108
|
+
active_labware: Optional[Labware] = None
|
|
109
|
+
alternate_384_well_coverage_count = 0
|
|
110
|
+
labware_format: Optional[str] = None
|
|
111
|
+
|
|
112
|
+
# We are assuming the wells are ordered A1, B1, C1... A2, B2, C2..., for columns and
|
|
113
|
+
# A1, A2, A3... B1, B2, B3 for rows. So if the active nozzle is on H row/12 column,
|
|
114
|
+
# reverse the list so the correct primary nozzle is chosen
|
|
115
|
+
reverse_lookup = (
|
|
116
|
+
nozzle_map.starting_nozzle == "H12"
|
|
117
|
+
or (
|
|
118
|
+
nozzle_map.configuration == NozzleConfigurationType.COLUMN
|
|
119
|
+
and nozzle_map.starting_nozzle == "H1"
|
|
120
|
+
)
|
|
121
|
+
or (
|
|
122
|
+
nozzle_map.configuration == NozzleConfigurationType.ROW
|
|
123
|
+
and nozzle_map.starting_nozzle == "A12"
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
if reverse_lookup:
|
|
127
|
+
targets.reverse()
|
|
128
|
+
|
|
129
|
+
for well in targets:
|
|
130
|
+
# If we have wells that are covered by the pipette's nozzles while primary nozzle is over
|
|
131
|
+
# a target well that aren't accounted for, check if the current well is in that list
|
|
132
|
+
if active_wells_covered:
|
|
133
|
+
if well.parent != active_labware:
|
|
134
|
+
raise ValueError(
|
|
135
|
+
"Could not resolve wells provided to pipette's nozzle configuration. "
|
|
136
|
+
"Please ensure wells are ordered to match pipette's nozzle layout."
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if well.well_name in active_wells_covered:
|
|
140
|
+
active_wells_covered.remove(well.well_name)
|
|
141
|
+
# If it's a 384 well plate, contiguous wells are not covered by the pipette targeting the
|
|
142
|
+
# initial target well. To support these kinds of transfers given a list of contiguous wells,
|
|
143
|
+
# allow another target well (or up to 4 total for a full 96-tip config) and add those wells
|
|
144
|
+
# to the list of covered wells
|
|
145
|
+
elif labware_format == "384Standard" and (
|
|
146
|
+
alternate_384_well_coverage_count == 0
|
|
147
|
+
or (
|
|
148
|
+
nozzle_map.tip_count == 96 and alternate_384_well_coverage_count < 3
|
|
149
|
+
)
|
|
150
|
+
):
|
|
151
|
+
active_wells_covered.extend(
|
|
152
|
+
list(
|
|
153
|
+
wells_covered_by_pipette_configuration(
|
|
154
|
+
nozzle_map, # type: ignore[arg-type]
|
|
155
|
+
well.well_name,
|
|
156
|
+
labware_wells_by_column=[
|
|
157
|
+
[labware_well.well_name for labware_well in column]
|
|
158
|
+
for column in well.parent.columns()
|
|
159
|
+
],
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
active_wells_covered.remove(well.well_name)
|
|
164
|
+
grouped_wells.append(well)
|
|
165
|
+
alternate_384_well_coverage_count += 1
|
|
166
|
+
else:
|
|
167
|
+
raise ValueError(
|
|
168
|
+
"Could not resolve wells provided to pipette's nozzle configuration. "
|
|
169
|
+
"Please ensure wells are ordered to match pipette's nozzle layout."
|
|
170
|
+
)
|
|
171
|
+
# If we have no active wells covered to account for, add a new target well and list of covered wells to check
|
|
172
|
+
else:
|
|
173
|
+
# If the labware is not a 96 or 384 well plate, add this well to the final result and move on to the next
|
|
174
|
+
labware_format = well.parent.parameters["format"]
|
|
175
|
+
if labware_format != "96Standard" and labware_format != "384Standard":
|
|
176
|
+
grouped_wells.append(well)
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
active_wells_covered = list(
|
|
180
|
+
wells_covered_by_pipette_configuration(
|
|
181
|
+
nozzle_map, # type: ignore[arg-type]
|
|
182
|
+
well.well_name,
|
|
183
|
+
labware_wells_by_column=[
|
|
184
|
+
[labware_well.well_name for labware_well in column]
|
|
185
|
+
for column in well.parent.columns()
|
|
186
|
+
],
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
active_wells_covered.remove(well.well_name)
|
|
190
|
+
grouped_wells.append(well)
|
|
191
|
+
active_labware = well.parent
|
|
192
|
+
alternate_384_well_coverage_count = 0
|
|
193
|
+
|
|
194
|
+
if active_wells_covered:
|
|
195
|
+
raise ValueError(
|
|
196
|
+
"Could not target all wells provided without aspirating or dispensing from other wells. "
|
|
197
|
+
f"Other wells that would be targeted: {active_wells_covered}"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# If we reversed the lookup of wells, reverse the grouped wells we will return
|
|
201
|
+
if reverse_lookup:
|
|
202
|
+
grouped_wells.reverse()
|
|
203
|
+
|
|
204
|
+
return grouped_wells
|
|
@@ -2,7 +2,7 @@ import logging
|
|
|
2
2
|
from typing import Optional, Any
|
|
3
3
|
|
|
4
4
|
from opentrons_shared_data.labware.types import (
|
|
5
|
-
|
|
5
|
+
LabwareDefinition2 as LabwareDefinition2Dict,
|
|
6
6
|
)
|
|
7
7
|
|
|
8
8
|
from opentrons import types
|
|
@@ -56,7 +56,7 @@ def validate_blowout_location(
|
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
def tip_length_for(
|
|
59
|
-
pipette: PipetteDict, tip_rack_definition:
|
|
59
|
+
pipette: PipetteDict, tip_rack_definition: LabwareDefinition2Dict
|
|
60
60
|
) -> float:
|
|
61
61
|
"""Get the tip length, including overlap, for a tip from this rack"""
|
|
62
62
|
try:
|
|
@@ -99,7 +99,20 @@ def validate_tiprack(
|
|
|
99
99
|
gen_lookup = (
|
|
100
100
|
"FLEX" if ("flex" in instr_metadata or "96" in instr_metadata) else "OT2"
|
|
101
101
|
)
|
|
102
|
-
|
|
102
|
+
|
|
103
|
+
# TODO (spp, 2025-01-30): do what AA's note above says or at least,
|
|
104
|
+
# fetch the 'pip_type' below from the 'model' field in pipette definitions
|
|
105
|
+
# so that we don't have to figure it out from pipette names
|
|
106
|
+
if instrument_name.split("_")[0] == "flex":
|
|
107
|
+
# Flex's API load names have the format 'flex_1channel_1000'
|
|
108
|
+
# From API v2.23 on, this is the name returned by InstrumentContext.name
|
|
109
|
+
pip_type = "p" + instrument_name.split("_")[2]
|
|
110
|
+
else:
|
|
111
|
+
# Until API v2.23, InstrumentContext.name returned the engine-specific names
|
|
112
|
+
# of Flex pipettes. These names, as well as OT2 pipette names,
|
|
113
|
+
# have the format- 'p1000_single_gen2' or 'p1000_single_flex'
|
|
114
|
+
pip_type = instrument_name.split("_")[0]
|
|
115
|
+
valid_vols = VALID_PIP_TIPRACK_VOL[gen_lookup][pip_type]
|
|
103
116
|
if tiprack_vol not in valid_vols:
|
|
104
117
|
log.warning(
|
|
105
118
|
f"The pipette {instrument_name} and its tip rack {tip_rack.load_name}"
|