opentrons 8.3.2a0__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.
- 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.2a0.dist-info → opentrons-8.4.0.dist-info}/METADATA +4 -4
- {opentrons-8.3.2a0.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.2a0.dist-info → opentrons-8.4.0.dist-info}/LICENSE +0 -0
- {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/WHEEL +0 -0
- {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/entry_points.txt +0 -0
- {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/top_level.txt +0 -0
|
@@ -1,14 +1,35 @@
|
|
|
1
1
|
"""ProtocolEngine-based InstrumentContext core implementation."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from
|
|
6
|
-
from
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from itertools import dropwhile
|
|
6
|
+
from typing import (
|
|
7
|
+
Optional,
|
|
8
|
+
TYPE_CHECKING,
|
|
9
|
+
cast,
|
|
10
|
+
Union,
|
|
11
|
+
List,
|
|
12
|
+
Tuple,
|
|
13
|
+
NamedTuple,
|
|
14
|
+
Generator,
|
|
15
|
+
Literal,
|
|
16
|
+
)
|
|
17
|
+
from opentrons.types import (
|
|
18
|
+
Location,
|
|
19
|
+
Mount,
|
|
20
|
+
NozzleConfigurationType,
|
|
21
|
+
NozzleMapInterface,
|
|
22
|
+
MeniscusTrackingTarget,
|
|
23
|
+
)
|
|
7
24
|
from opentrons.hardware_control import SyncHardwareAPI
|
|
8
25
|
from opentrons.hardware_control.dev_types import PipetteDict
|
|
9
26
|
from opentrons.protocols.api_support.util import FlowRates, find_value_for_api_version
|
|
10
27
|
from opentrons.protocols.api_support.types import APIVersion
|
|
11
|
-
from opentrons.protocols.advanced_control.transfers.common import
|
|
28
|
+
from opentrons.protocols.advanced_control.transfers.common import (
|
|
29
|
+
TransferTipPolicyV2,
|
|
30
|
+
NoLiquidClassPropertyError,
|
|
31
|
+
)
|
|
32
|
+
from opentrons.protocols.advanced_control.transfers import common as tx_commons
|
|
12
33
|
from opentrons.protocol_engine import commands as cmd
|
|
13
34
|
from opentrons.protocol_engine import (
|
|
14
35
|
DeckPoint,
|
|
@@ -28,31 +49,58 @@ from opentrons.protocol_engine.types import (
|
|
|
28
49
|
NozzleLayoutConfigurationType,
|
|
29
50
|
AddressableOffsetVector,
|
|
30
51
|
LiquidClassRecord,
|
|
52
|
+
NextTipInfo,
|
|
53
|
+
PickUpTipWellLocation,
|
|
54
|
+
LiquidHandlingWellLocation,
|
|
55
|
+
)
|
|
56
|
+
from opentrons.protocol_engine.types import (
|
|
57
|
+
LiquidTrackingType,
|
|
58
|
+
WellLocationFunction,
|
|
59
|
+
)
|
|
60
|
+
from opentrons.protocol_engine.types.automatic_tip_selection import (
|
|
61
|
+
NoTipAvailable,
|
|
62
|
+
NoTipReason,
|
|
31
63
|
)
|
|
32
64
|
from opentrons.protocol_engine.errors.exceptions import TipNotAttachedError
|
|
33
65
|
from opentrons.protocol_engine.clients import SyncClient as EngineClient
|
|
34
66
|
from opentrons.protocols.api_support.definitions import MAX_SUPPORTED_VERSION
|
|
35
|
-
from opentrons_shared_data.pipette.types import
|
|
67
|
+
from opentrons_shared_data.pipette.types import (
|
|
68
|
+
PIPETTE_API_NAMES_MAP,
|
|
69
|
+
LIQUID_PROBE_START_OFFSET_FROM_WELL_TOP,
|
|
70
|
+
)
|
|
36
71
|
from opentrons_shared_data.errors.exceptions import (
|
|
37
72
|
UnsupportedHardwareCommand,
|
|
73
|
+
CommandPreconditionViolated,
|
|
38
74
|
)
|
|
75
|
+
from opentrons_shared_data.liquid_classes.liquid_class_definition import BlowoutLocation
|
|
39
76
|
from opentrons.protocol_api._nozzle_layout import NozzleLayout
|
|
40
77
|
from . import overlap_versions, pipette_movement_conflict
|
|
78
|
+
from . import transfer_components_executor as tx_comps_executor
|
|
41
79
|
|
|
42
80
|
from .well import WellCore
|
|
81
|
+
from .labware import LabwareCore
|
|
43
82
|
from ..instrument import AbstractInstrument
|
|
44
83
|
from ...disposal_locations import TrashBin, WasteChute
|
|
45
84
|
|
|
46
85
|
if TYPE_CHECKING:
|
|
47
86
|
from .protocol import ProtocolCore
|
|
48
87
|
from opentrons.protocol_api._liquid import LiquidClass
|
|
88
|
+
from opentrons.protocol_api._liquid_properties import (
|
|
89
|
+
TransferProperties,
|
|
90
|
+
MultiDispenseProperties,
|
|
91
|
+
)
|
|
49
92
|
|
|
50
93
|
_DISPENSE_VOLUME_VALIDATION_ADDED_IN = APIVersion(2, 17)
|
|
51
94
|
_RESIN_TIP_DEFAULT_VOLUME = 400
|
|
52
95
|
_RESIN_TIP_DEFAULT_FLOW_RATE = 10.0
|
|
53
96
|
|
|
97
|
+
_FLEX_PIPETTE_NAMES_FIXED_IN = APIVersion(2, 23)
|
|
98
|
+
"""The version after which InstrumentContext.name returns the correct API-specific names of Flex pipettes."""
|
|
99
|
+
_RETURN_TIP_SCRAPE_ADDED_IN = APIVersion(2, 23)
|
|
100
|
+
"""The version after which return-tip for 1/8 channels will scrape off."""
|
|
101
|
+
|
|
54
102
|
|
|
55
|
-
class InstrumentCore(AbstractInstrument[WellCore]):
|
|
103
|
+
class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
|
|
56
104
|
"""Instrument API core using a ProtocolEngine.
|
|
57
105
|
|
|
58
106
|
Args:
|
|
@@ -115,7 +163,9 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
115
163
|
pipette_id=self._pipette_id, speed=speed
|
|
116
164
|
)
|
|
117
165
|
|
|
118
|
-
def air_gap_in_place(
|
|
166
|
+
def air_gap_in_place(
|
|
167
|
+
self, volume: float, flow_rate: float, correction_volume: Optional[float] = None
|
|
168
|
+
) -> None:
|
|
119
169
|
"""Aspirate a given volume of air from the current location of the pipette.
|
|
120
170
|
|
|
121
171
|
Args:
|
|
@@ -124,7 +174,10 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
124
174
|
"""
|
|
125
175
|
self._engine_client.execute_command(
|
|
126
176
|
cmd.AirGapInPlaceParams(
|
|
127
|
-
pipetteId=self._pipette_id,
|
|
177
|
+
pipetteId=self._pipette_id,
|
|
178
|
+
volume=volume,
|
|
179
|
+
flowRate=flow_rate,
|
|
180
|
+
correctionVolume=correction_volume,
|
|
128
181
|
)
|
|
129
182
|
)
|
|
130
183
|
|
|
@@ -136,7 +189,8 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
136
189
|
rate: float,
|
|
137
190
|
flow_rate: float,
|
|
138
191
|
in_place: bool,
|
|
139
|
-
|
|
192
|
+
meniscus_tracking: Optional[MeniscusTrackingTarget] = None,
|
|
193
|
+
correction_volume: Optional[float] = None,
|
|
140
194
|
) -> None:
|
|
141
195
|
"""Aspirate a given volume of liquid from the specified location.
|
|
142
196
|
Args:
|
|
@@ -146,7 +200,10 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
146
200
|
rate: Not used in this core.
|
|
147
201
|
flow_rate: The flow rate in µL/s to aspirate at.
|
|
148
202
|
in_place: whether this is a in-place command.
|
|
203
|
+
meniscus_tracking: Optional data about where to aspirate from.
|
|
149
204
|
"""
|
|
205
|
+
if meniscus_tracking == MeniscusTrackingTarget.START:
|
|
206
|
+
raise ValueError("Cannot aspirate at the starting liquid height.")
|
|
150
207
|
if well_core is None:
|
|
151
208
|
if not in_place:
|
|
152
209
|
self._engine_client.execute_command(
|
|
@@ -163,7 +220,10 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
163
220
|
|
|
164
221
|
self._engine_client.execute_command(
|
|
165
222
|
cmd.AspirateInPlaceParams(
|
|
166
|
-
pipetteId=self._pipette_id,
|
|
223
|
+
pipetteId=self._pipette_id,
|
|
224
|
+
volume=volume,
|
|
225
|
+
flowRate=flow_rate,
|
|
226
|
+
correctionVolume=correction_volume,
|
|
167
227
|
)
|
|
168
228
|
)
|
|
169
229
|
|
|
@@ -171,14 +231,16 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
171
231
|
well_name = well_core.get_name()
|
|
172
232
|
labware_id = well_core.labware_id
|
|
173
233
|
|
|
174
|
-
|
|
234
|
+
(
|
|
235
|
+
well_location,
|
|
236
|
+
dynamic_liquid_tracking,
|
|
237
|
+
) = self._engine_client.state.geometry.get_relative_well_location(
|
|
175
238
|
labware_id=labware_id,
|
|
176
239
|
well_name=well_name,
|
|
177
240
|
absolute_point=location.point,
|
|
178
|
-
|
|
241
|
+
location_type=WellLocationFunction.LIQUID_HANDLING,
|
|
242
|
+
meniscus_tracking=meniscus_tracking,
|
|
179
243
|
)
|
|
180
|
-
if well_location.origin == WellOrigin.MENISCUS:
|
|
181
|
-
well_location.volumeOffset = "operationVolume"
|
|
182
244
|
pipette_movement_conflict.check_safe_for_pipette_movement(
|
|
183
245
|
engine_state=self._engine_client.state,
|
|
184
246
|
pipette_id=self._pipette_id,
|
|
@@ -186,16 +248,31 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
186
248
|
well_name=well_name,
|
|
187
249
|
well_location=well_location,
|
|
188
250
|
)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
251
|
+
assert isinstance(well_location, LiquidHandlingWellLocation)
|
|
252
|
+
if dynamic_liquid_tracking:
|
|
253
|
+
self._engine_client.execute_command(
|
|
254
|
+
cmd.AspirateWhileTrackingParams(
|
|
255
|
+
pipetteId=self._pipette_id,
|
|
256
|
+
labwareId=labware_id,
|
|
257
|
+
wellName=well_name,
|
|
258
|
+
wellLocation=well_location,
|
|
259
|
+
volume=volume,
|
|
260
|
+
flowRate=flow_rate,
|
|
261
|
+
correctionVolume=correction_volume,
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
else:
|
|
265
|
+
self._engine_client.execute_command(
|
|
266
|
+
cmd.AspirateParams(
|
|
267
|
+
pipetteId=self._pipette_id,
|
|
268
|
+
labwareId=labware_id,
|
|
269
|
+
wellName=well_name,
|
|
270
|
+
wellLocation=well_location,
|
|
271
|
+
volume=volume,
|
|
272
|
+
flowRate=flow_rate,
|
|
273
|
+
correctionVolume=correction_volume,
|
|
274
|
+
)
|
|
197
275
|
)
|
|
198
|
-
)
|
|
199
276
|
|
|
200
277
|
self._protocol_core.set_last_location(location=location, mount=self.get_mount())
|
|
201
278
|
|
|
@@ -208,7 +285,8 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
208
285
|
flow_rate: float,
|
|
209
286
|
in_place: bool,
|
|
210
287
|
push_out: Optional[float],
|
|
211
|
-
|
|
288
|
+
meniscus_tracking: Optional[MeniscusTrackingTarget] = None,
|
|
289
|
+
correction_volume: Optional[float] = None,
|
|
212
290
|
) -> None:
|
|
213
291
|
"""Dispense a given volume of liquid into the specified location.
|
|
214
292
|
Args:
|
|
@@ -219,6 +297,7 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
219
297
|
flow_rate: The flow rate in µL/s to dispense at.
|
|
220
298
|
in_place: whether this is a in-place command.
|
|
221
299
|
push_out: The amount to push the plunger below bottom position.
|
|
300
|
+
meniscus_tracking: Optional data about where to dispense from.
|
|
222
301
|
"""
|
|
223
302
|
if self._protocol_core.api_version < _DISPENSE_VOLUME_VALIDATION_ADDED_IN:
|
|
224
303
|
# In older API versions, when you try to dispense more than you can,
|
|
@@ -256,6 +335,7 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
256
335
|
volume=volume,
|
|
257
336
|
flowRate=flow_rate,
|
|
258
337
|
pushOut=push_out,
|
|
338
|
+
correctionVolume=correction_volume,
|
|
259
339
|
)
|
|
260
340
|
)
|
|
261
341
|
else:
|
|
@@ -264,12 +344,17 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
264
344
|
well_name = well_core.get_name()
|
|
265
345
|
labware_id = well_core.labware_id
|
|
266
346
|
|
|
267
|
-
|
|
347
|
+
(
|
|
348
|
+
well_location,
|
|
349
|
+
dynamic_liquid_tracking,
|
|
350
|
+
) = self._engine_client.state.geometry.get_relative_well_location(
|
|
268
351
|
labware_id=labware_id,
|
|
269
352
|
well_name=well_name,
|
|
270
353
|
absolute_point=location.point,
|
|
271
|
-
|
|
354
|
+
location_type=WellLocationFunction.LIQUID_HANDLING,
|
|
355
|
+
meniscus_tracking=meniscus_tracking,
|
|
272
356
|
)
|
|
357
|
+
assert isinstance(well_location, LiquidHandlingWellLocation)
|
|
273
358
|
pipette_movement_conflict.check_safe_for_pipette_movement(
|
|
274
359
|
engine_state=self._engine_client.state,
|
|
275
360
|
pipette_id=self._pipette_id,
|
|
@@ -277,17 +362,32 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
277
362
|
well_name=well_name,
|
|
278
363
|
well_location=well_location,
|
|
279
364
|
)
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
365
|
+
if dynamic_liquid_tracking:
|
|
366
|
+
self._engine_client.execute_command(
|
|
367
|
+
cmd.DispenseWhileTrackingParams(
|
|
368
|
+
pipetteId=self._pipette_id,
|
|
369
|
+
labwareId=labware_id,
|
|
370
|
+
wellName=well_name,
|
|
371
|
+
wellLocation=well_location,
|
|
372
|
+
volume=volume,
|
|
373
|
+
flowRate=flow_rate,
|
|
374
|
+
pushOut=push_out,
|
|
375
|
+
correctionVolume=correction_volume,
|
|
376
|
+
)
|
|
377
|
+
)
|
|
378
|
+
else:
|
|
379
|
+
self._engine_client.execute_command(
|
|
380
|
+
cmd.DispenseParams(
|
|
381
|
+
pipetteId=self._pipette_id,
|
|
382
|
+
labwareId=labware_id,
|
|
383
|
+
wellName=well_name,
|
|
384
|
+
wellLocation=well_location,
|
|
385
|
+
volume=volume,
|
|
386
|
+
flowRate=flow_rate,
|
|
387
|
+
pushOut=push_out,
|
|
388
|
+
correctionVolume=correction_volume,
|
|
389
|
+
)
|
|
289
390
|
)
|
|
290
|
-
)
|
|
291
391
|
|
|
292
392
|
if isinstance(location, (TrashBin, WasteChute)):
|
|
293
393
|
self._protocol_core.set_last_location(location=None, mount=self.get_mount())
|
|
@@ -340,13 +440,16 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
340
440
|
well_name = well_core.get_name()
|
|
341
441
|
labware_id = well_core.labware_id
|
|
342
442
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
443
|
+
(
|
|
444
|
+
well_location,
|
|
445
|
+
_,
|
|
446
|
+
) = self._engine_client.state.geometry.get_relative_well_location(
|
|
447
|
+
labware_id=labware_id,
|
|
448
|
+
well_name=well_name,
|
|
449
|
+
absolute_point=location.point,
|
|
450
|
+
location_type=WellLocationFunction.BASE,
|
|
349
451
|
)
|
|
452
|
+
|
|
350
453
|
pipette_movement_conflict.check_safe_for_pipette_movement(
|
|
351
454
|
engine_state=self._engine_client.state,
|
|
352
455
|
pipette_id=self._pipette_id,
|
|
@@ -354,6 +457,7 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
354
457
|
well_name=well_name,
|
|
355
458
|
well_location=well_location,
|
|
356
459
|
)
|
|
460
|
+
assert isinstance(well_location, WellLocation)
|
|
357
461
|
self._engine_client.execute_command(
|
|
358
462
|
cmd.BlowOutParams(
|
|
359
463
|
pipetteId=self._pipette_id,
|
|
@@ -380,6 +484,7 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
380
484
|
radius: float,
|
|
381
485
|
z_offset: float,
|
|
382
486
|
speed: float,
|
|
487
|
+
mm_from_edge: Optional[float] = None,
|
|
383
488
|
) -> None:
|
|
384
489
|
"""Touch pipette tip to edges of the well
|
|
385
490
|
|
|
@@ -389,7 +494,11 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
389
494
|
radius: Percentage modifier for well radius to touch.
|
|
390
495
|
z_offset: Vertical offset for pipette tip during touch tip.
|
|
391
496
|
speed: Speed for the touch tip movements.
|
|
497
|
+
mm_from_edge: Offset from the edge of the well to move to. Requires a radius of 1.
|
|
392
498
|
"""
|
|
499
|
+
if mm_from_edge is not None and radius != 1.0:
|
|
500
|
+
raise ValueError("radius must be set to 1.0 if mm_from_edge is provided.")
|
|
501
|
+
|
|
393
502
|
well_name = well_core.get_name()
|
|
394
503
|
labware_id = well_core.labware_id
|
|
395
504
|
|
|
@@ -411,6 +520,7 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
411
520
|
wellName=well_name,
|
|
412
521
|
wellLocation=well_location,
|
|
413
522
|
radius=radius,
|
|
523
|
+
mmFromEdge=mm_from_edge,
|
|
414
524
|
speed=speed,
|
|
415
525
|
)
|
|
416
526
|
)
|
|
@@ -442,12 +552,14 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
442
552
|
well_name = well_core.get_name()
|
|
443
553
|
labware_id = well_core.labware_id
|
|
444
554
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
555
|
+
(
|
|
556
|
+
well_location,
|
|
557
|
+
_,
|
|
558
|
+
) = self._engine_client.state.geometry.get_relative_well_location(
|
|
559
|
+
labware_id=labware_id,
|
|
560
|
+
well_name=well_name,
|
|
561
|
+
absolute_point=location.point,
|
|
562
|
+
location_type=WellLocationFunction.PICK_UP_TIP,
|
|
451
563
|
)
|
|
452
564
|
pipette_movement_conflict.check_safe_for_tip_pickup_and_return(
|
|
453
565
|
engine_state=self._engine_client.state,
|
|
@@ -461,7 +573,7 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
461
573
|
well_name=well_name,
|
|
462
574
|
well_location=well_location,
|
|
463
575
|
)
|
|
464
|
-
|
|
576
|
+
assert isinstance(well_location, PickUpTipWellLocation)
|
|
465
577
|
self._engine_client.execute_command(
|
|
466
578
|
cmd.PickUpTipParams(
|
|
467
579
|
pipetteId=self._pipette_id,
|
|
@@ -496,14 +608,17 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
496
608
|
"""
|
|
497
609
|
well_name = well_core.get_name()
|
|
498
610
|
labware_id = well_core.labware_id
|
|
611
|
+
scrape_tips = False
|
|
499
612
|
|
|
500
613
|
if location is not None:
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
614
|
+
(
|
|
615
|
+
relative_well_location,
|
|
616
|
+
_,
|
|
617
|
+
) = self._engine_client.state.geometry.get_relative_well_location(
|
|
618
|
+
labware_id=labware_id,
|
|
619
|
+
well_name=well_name,
|
|
620
|
+
absolute_point=location.point,
|
|
621
|
+
location_type=WellLocationFunction.DROP_TIP,
|
|
507
622
|
)
|
|
508
623
|
|
|
509
624
|
well_location = DropTipWellLocation(
|
|
@@ -519,6 +634,7 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
519
634
|
pipette_id=self._pipette_id,
|
|
520
635
|
labware_id=labware_id,
|
|
521
636
|
)
|
|
637
|
+
scrape_tips = self.get_channels() <= 8
|
|
522
638
|
pipette_movement_conflict.check_safe_for_pipette_movement(
|
|
523
639
|
engine_state=self._engine_client.state,
|
|
524
640
|
pipette_id=self._pipette_id,
|
|
@@ -534,6 +650,7 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
534
650
|
wellLocation=well_location,
|
|
535
651
|
homeAfter=home_after,
|
|
536
652
|
alternateDropLocation=alternate_drop_location,
|
|
653
|
+
scrape_tips=scrape_tips,
|
|
537
654
|
)
|
|
538
655
|
)
|
|
539
656
|
|
|
@@ -631,20 +748,33 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
631
748
|
force_direct: bool,
|
|
632
749
|
minimum_z_height: Optional[float],
|
|
633
750
|
speed: Optional[float],
|
|
751
|
+
check_for_movement_conflicts: bool = True,
|
|
634
752
|
) -> None:
|
|
635
753
|
if well_core is not None:
|
|
636
754
|
if isinstance(location, (TrashBin, WasteChute)):
|
|
637
755
|
raise ValueError("Trash Bin and Waste Chute have no Wells.")
|
|
638
756
|
labware_id = well_core.labware_id
|
|
639
757
|
well_name = well_core.get_name()
|
|
640
|
-
|
|
641
|
-
|
|
758
|
+
(
|
|
759
|
+
well_location,
|
|
760
|
+
_,
|
|
761
|
+
) = self._engine_client.state.geometry.get_relative_well_location(
|
|
762
|
+
labware_id=labware_id,
|
|
763
|
+
well_name=well_name,
|
|
764
|
+
absolute_point=location.point,
|
|
765
|
+
location_type=WellLocationFunction.LIQUID_HANDLING,
|
|
766
|
+
)
|
|
767
|
+
assert isinstance(well_location, LiquidHandlingWellLocation)
|
|
768
|
+
if well_location.volumeOffset and well_location.volumeOffset != 0:
|
|
769
|
+
raise ValueError("volume offset not supported with move_to")
|
|
770
|
+
if check_for_movement_conflicts:
|
|
771
|
+
pipette_movement_conflict.check_safe_for_pipette_movement(
|
|
772
|
+
engine_state=self._engine_client.state,
|
|
773
|
+
pipette_id=self._pipette_id,
|
|
642
774
|
labware_id=labware_id,
|
|
643
775
|
well_name=well_name,
|
|
644
|
-
|
|
776
|
+
well_location=well_location,
|
|
645
777
|
)
|
|
646
|
-
)
|
|
647
|
-
|
|
648
778
|
self._engine_client.execute_command(
|
|
649
779
|
cmd.MoveToWellParams(
|
|
650
780
|
pipetteId=self._pipette_id,
|
|
@@ -685,16 +815,18 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
685
815
|
) -> None:
|
|
686
816
|
labware_id = well_core.labware_id
|
|
687
817
|
well_name = well_core.get_name()
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
818
|
+
(
|
|
819
|
+
well_location,
|
|
820
|
+
_,
|
|
821
|
+
) = self._engine_client.state.geometry.get_relative_well_location(
|
|
822
|
+
labware_id=labware_id,
|
|
823
|
+
well_name=well_name,
|
|
824
|
+
absolute_point=location.point,
|
|
825
|
+
location_type=WellLocationFunction.PICK_UP_TIP,
|
|
694
826
|
)
|
|
695
|
-
|
|
827
|
+
assert isinstance(well_location, PickUpTipWellLocation)
|
|
696
828
|
self._engine_client.execute_command(
|
|
697
|
-
cmd.
|
|
829
|
+
cmd.SealPipetteToTipParams(
|
|
698
830
|
pipetteId=self._pipette_id,
|
|
699
831
|
labwareId=labware_id,
|
|
700
832
|
wellName=well_name,
|
|
@@ -702,17 +834,19 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
702
834
|
)
|
|
703
835
|
)
|
|
704
836
|
|
|
705
|
-
def resin_tip_unseal(self, location: Location, well_core: WellCore) -> None:
|
|
837
|
+
def resin_tip_unseal(self, location: Location | None, well_core: WellCore) -> None:
|
|
706
838
|
well_name = well_core.get_name()
|
|
707
839
|
labware_id = well_core.labware_id
|
|
708
840
|
|
|
709
841
|
if location is not None:
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
842
|
+
(
|
|
843
|
+
relative_well_location,
|
|
844
|
+
_,
|
|
845
|
+
) = self._engine_client.state.geometry.get_relative_well_location(
|
|
846
|
+
labware_id=labware_id,
|
|
847
|
+
well_name=well_name,
|
|
848
|
+
absolute_point=location.point,
|
|
849
|
+
location_type=WellLocationFunction.BASE,
|
|
716
850
|
)
|
|
717
851
|
|
|
718
852
|
well_location = DropTipWellLocation(
|
|
@@ -730,7 +864,7 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
730
864
|
well_location=well_location,
|
|
731
865
|
)
|
|
732
866
|
self._engine_client.execute_command(
|
|
733
|
-
cmd.
|
|
867
|
+
cmd.UnsealPipetteFromTipParams(
|
|
734
868
|
pipetteId=self._pipette_id,
|
|
735
869
|
labwareId=labware_id,
|
|
736
870
|
wellName=well_name,
|
|
@@ -763,11 +897,14 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
763
897
|
if flow_rate is None:
|
|
764
898
|
flow_rate = _RESIN_TIP_DEFAULT_FLOW_RATE
|
|
765
899
|
|
|
766
|
-
|
|
900
|
+
(
|
|
901
|
+
well_location,
|
|
902
|
+
dynamic_tracking,
|
|
903
|
+
) = self._engine_client.state.geometry.get_relative_well_location(
|
|
767
904
|
labware_id=labware_id,
|
|
768
905
|
well_name=well_name,
|
|
769
906
|
absolute_point=location.point,
|
|
770
|
-
|
|
907
|
+
location_type=WellLocationFunction.LIQUID_HANDLING,
|
|
771
908
|
)
|
|
772
909
|
pipette_movement_conflict.check_safe_for_pipette_movement(
|
|
773
910
|
engine_state=self._engine_client.state,
|
|
@@ -776,8 +913,9 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
776
913
|
well_name=well_name,
|
|
777
914
|
well_location=well_location,
|
|
778
915
|
)
|
|
916
|
+
assert isinstance(well_location, LiquidHandlingWellLocation)
|
|
779
917
|
self._engine_client.execute_command(
|
|
780
|
-
cmd.
|
|
918
|
+
cmd.PressureDispenseParams(
|
|
781
919
|
pipetteId=self._pipette_id,
|
|
782
920
|
labwareId=labware_id,
|
|
783
921
|
wellName=well_name,
|
|
@@ -794,19 +932,33 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
794
932
|
).mount.to_hw_mount()
|
|
795
933
|
|
|
796
934
|
def get_pipette_name(self) -> str:
|
|
797
|
-
"""Get the pipette's
|
|
935
|
+
"""Get the pipette's name as a string.
|
|
798
936
|
|
|
799
937
|
Will match the load name of the actually loaded pipette,
|
|
800
938
|
which may differ from the requested load name.
|
|
939
|
+
|
|
940
|
+
From API v2.15 to v2.22, this property returned an internal, engine-specific,
|
|
941
|
+
name for Flex pipettes (eg, "p50_multi_flex" instead of "flex_8channel_50").
|
|
942
|
+
|
|
943
|
+
From API v2.23 onwards, this behavior is fixed so that this property returns
|
|
944
|
+
the API-specific names of Flex pipettes.
|
|
801
945
|
"""
|
|
802
946
|
# TODO (tz, 11-23-22): revert this change when merging
|
|
803
947
|
# https://opentrons.atlassian.net/browse/RLIQ-251
|
|
804
948
|
pipette = self._engine_client.state.pipettes.get(self._pipette_id)
|
|
805
|
-
|
|
806
|
-
pipette.pipetteName.value
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
949
|
+
if self._protocol_core.api_version < _FLEX_PIPETTE_NAMES_FIXED_IN:
|
|
950
|
+
return pipette.pipetteName.value
|
|
951
|
+
else:
|
|
952
|
+
name = next(
|
|
953
|
+
(
|
|
954
|
+
pip_api_name
|
|
955
|
+
for pip_api_name, pip_name in PIPETTE_API_NAMES_MAP.items()
|
|
956
|
+
if pip_name == pipette.pipetteName
|
|
957
|
+
),
|
|
958
|
+
None,
|
|
959
|
+
)
|
|
960
|
+
assert name, "Pipette name not found."
|
|
961
|
+
return name
|
|
810
962
|
|
|
811
963
|
def get_model(self) -> str:
|
|
812
964
|
return self._engine_client.state.pipettes.get_model_name(self._pipette_id)
|
|
@@ -833,6 +985,16 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
833
985
|
|
|
834
986
|
return current_volume or 0
|
|
835
987
|
|
|
988
|
+
def get_has_clean_tip(self) -> bool:
|
|
989
|
+
try:
|
|
990
|
+
clean_tip = self._engine_client.state.pipettes.get_has_clean_tip(
|
|
991
|
+
self._pipette_id
|
|
992
|
+
)
|
|
993
|
+
except TipNotAttachedError:
|
|
994
|
+
clean_tip = False
|
|
995
|
+
|
|
996
|
+
return clean_tip
|
|
997
|
+
|
|
836
998
|
def get_available_volume(self) -> float:
|
|
837
999
|
try:
|
|
838
1000
|
available_volume = self._engine_client.state.pipettes.get_available_volume(
|
|
@@ -975,24 +1137,31 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
975
1137
|
|
|
976
1138
|
def load_liquid_class(
|
|
977
1139
|
self,
|
|
978
|
-
|
|
979
|
-
|
|
1140
|
+
name: str,
|
|
1141
|
+
transfer_properties: TransferProperties,
|
|
980
1142
|
tiprack_uri: str,
|
|
981
1143
|
) -> str:
|
|
982
|
-
"""Load a liquid class into the engine and return its ID.
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1144
|
+
"""Load a liquid class into the engine and return its ID.
|
|
1145
|
+
|
|
1146
|
+
Args:
|
|
1147
|
+
name: Name of the liquid class
|
|
1148
|
+
transfer_properties: Liquid class properties for a specific pipette & tiprack combination
|
|
1149
|
+
tiprack_uri: URI of the tiprack whose transfer properties we will be using.
|
|
986
1150
|
|
|
1151
|
+
Returns:
|
|
1152
|
+
Liquid class record's ID, as generated by the protocol engine.
|
|
1153
|
+
"""
|
|
987
1154
|
liquid_class_record = LiquidClassRecord(
|
|
988
|
-
liquidClassName=
|
|
989
|
-
pipetteModel=self.
|
|
1155
|
+
liquidClassName=name,
|
|
1156
|
+
pipetteModel=self.get_pipette_name(),
|
|
990
1157
|
tiprack=tiprack_uri,
|
|
991
|
-
aspirate=
|
|
992
|
-
singleDispense=
|
|
993
|
-
multiDispense=
|
|
994
|
-
|
|
995
|
-
|
|
1158
|
+
aspirate=transfer_properties.aspirate.as_shared_data_model(),
|
|
1159
|
+
singleDispense=transfer_properties.dispense.as_shared_data_model(),
|
|
1160
|
+
multiDispense=(
|
|
1161
|
+
transfer_properties.multi_dispense.as_shared_data_model()
|
|
1162
|
+
if transfer_properties.multi_dispense
|
|
1163
|
+
else None
|
|
1164
|
+
),
|
|
996
1165
|
)
|
|
997
1166
|
result = self._engine_client.execute_command_without_recovery(
|
|
998
1167
|
cmd.LoadLiquidClassParams(
|
|
@@ -1001,16 +1170,1042 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
1001
1170
|
)
|
|
1002
1171
|
return result.liquidClassId
|
|
1003
1172
|
|
|
1004
|
-
def
|
|
1173
|
+
def get_next_tip(
|
|
1174
|
+
self, tip_racks: List[LabwareCore], starting_well: Optional[WellCore]
|
|
1175
|
+
) -> Optional[NextTipInfo]:
|
|
1176
|
+
"""Get the next tip to pick up."""
|
|
1177
|
+
if starting_well is not None:
|
|
1178
|
+
# Drop tip racks until the one with the starting tip is reached (if any)
|
|
1179
|
+
valid_tip_racks = list(
|
|
1180
|
+
dropwhile(
|
|
1181
|
+
lambda tr: starting_well.labware_id != tr.labware_id, tip_racks
|
|
1182
|
+
)
|
|
1183
|
+
)
|
|
1184
|
+
else:
|
|
1185
|
+
valid_tip_racks = tip_racks
|
|
1186
|
+
|
|
1187
|
+
result = self._engine_client.execute_command_without_recovery(
|
|
1188
|
+
cmd.GetNextTipParams(
|
|
1189
|
+
pipetteId=self._pipette_id,
|
|
1190
|
+
labwareIds=[tip_rack.labware_id for tip_rack in valid_tip_racks],
|
|
1191
|
+
startingTipWell=(
|
|
1192
|
+
starting_well.get_name() if starting_well is not None else None
|
|
1193
|
+
),
|
|
1194
|
+
)
|
|
1195
|
+
)
|
|
1196
|
+
next_tip_info = result.nextTipInfo
|
|
1197
|
+
if isinstance(next_tip_info, NoTipAvailable):
|
|
1198
|
+
if next_tip_info.noTipReason == NoTipReason.STARTING_TIP_WITH_PARTIAL:
|
|
1199
|
+
raise CommandPreconditionViolated(
|
|
1200
|
+
"Automatic tip tracking is not available when using a partial pipette"
|
|
1201
|
+
" nozzle configuration and InstrumentContext.starting_tip."
|
|
1202
|
+
" Switch to a full configuration or set starting_tip to None."
|
|
1203
|
+
)
|
|
1204
|
+
return None
|
|
1205
|
+
else:
|
|
1206
|
+
return next_tip_info
|
|
1207
|
+
|
|
1208
|
+
def transfer_with_liquid_class( # noqa: C901
|
|
1005
1209
|
self,
|
|
1006
|
-
|
|
1210
|
+
liquid_class: LiquidClass,
|
|
1007
1211
|
volume: float,
|
|
1008
|
-
source: List[WellCore],
|
|
1009
|
-
dest: List[WellCore],
|
|
1212
|
+
source: List[Tuple[Location, WellCore]],
|
|
1213
|
+
dest: List[Tuple[Location, WellCore]],
|
|
1010
1214
|
new_tip: TransferTipPolicyV2,
|
|
1011
|
-
|
|
1215
|
+
tip_racks: List[Tuple[Location, LabwareCore]],
|
|
1216
|
+
starting_tip: Optional[WellCore],
|
|
1217
|
+
trash_location: Union[Location, TrashBin, WasteChute],
|
|
1218
|
+
return_tip: bool,
|
|
1219
|
+
) -> None:
|
|
1220
|
+
"""Execute transfer using liquid class properties.
|
|
1221
|
+
|
|
1222
|
+
Args:
|
|
1223
|
+
liquid_class: The liquid class to use for transfer properties.
|
|
1224
|
+
volume: Volume to transfer per well.
|
|
1225
|
+
source: List of source wells, with each well represented as a tuple of
|
|
1226
|
+
types.Location and WellCore.
|
|
1227
|
+
types.Location is only necessary for saving the last accessed location.
|
|
1228
|
+
dest: List of destination wells, with each well represented as a tuple of
|
|
1229
|
+
types.Location and WellCore.
|
|
1230
|
+
types.Location is only necessary for saving the last accessed location.
|
|
1231
|
+
new_tip: Whether the transfer should use a new tip 'once', 'never', 'always',
|
|
1232
|
+
or 'per source'.
|
|
1233
|
+
tiprack_uri: The URI of the tiprack that the transfer settings are for.
|
|
1234
|
+
tip_drop_location: Location where the tip will be dropped (if appropriate).
|
|
1235
|
+
"""
|
|
1236
|
+
if not tip_racks:
|
|
1237
|
+
raise RuntimeError(
|
|
1238
|
+
"No tipracks found for pipette in order to perform transfer"
|
|
1239
|
+
)
|
|
1240
|
+
tiprack_uri_for_transfer_props = tip_racks[0][1].get_uri()
|
|
1241
|
+
try:
|
|
1242
|
+
transfer_props = liquid_class.get_for(
|
|
1243
|
+
pipette=self.get_pipette_name(), tip_rack=tiprack_uri_for_transfer_props
|
|
1244
|
+
)
|
|
1245
|
+
except NoLiquidClassPropertyError:
|
|
1246
|
+
if self._protocol_core.robot_type == "OT-2 Standard":
|
|
1247
|
+
raise NoLiquidClassPropertyError(
|
|
1248
|
+
"Default liquid classes are not supported with OT-2 pipettes and tip racks."
|
|
1249
|
+
) from None
|
|
1250
|
+
raise
|
|
1251
|
+
|
|
1252
|
+
# TODO: use the ID returned by load_liquid_class in command annotations
|
|
1253
|
+
self.load_liquid_class(
|
|
1254
|
+
name=liquid_class.name,
|
|
1255
|
+
transfer_properties=transfer_props,
|
|
1256
|
+
tiprack_uri=tiprack_uri_for_transfer_props,
|
|
1257
|
+
)
|
|
1258
|
+
|
|
1259
|
+
source_dest_per_volume_step = (
|
|
1260
|
+
tx_commons.expand_for_volume_constraints_for_liquid_classes(
|
|
1261
|
+
volumes=[volume for _ in range(len(source))],
|
|
1262
|
+
targets=zip(source, dest),
|
|
1263
|
+
max_volume=min(
|
|
1264
|
+
self.get_max_volume(),
|
|
1265
|
+
self._engine_client.state.geometry.get_nominal_tip_geometry(
|
|
1266
|
+
pipette_id=self.pipette_id,
|
|
1267
|
+
labware_id=tip_racks[0][1].labware_id,
|
|
1268
|
+
well_name=None,
|
|
1269
|
+
).volume,
|
|
1270
|
+
),
|
|
1271
|
+
)
|
|
1272
|
+
)
|
|
1273
|
+
|
|
1274
|
+
last_tip_picked_up_from: Optional[WellCore] = None
|
|
1275
|
+
|
|
1276
|
+
def _drop_tip() -> None:
|
|
1277
|
+
if return_tip:
|
|
1278
|
+
assert last_tip_picked_up_from is not None
|
|
1279
|
+
self.drop_tip(
|
|
1280
|
+
location=None,
|
|
1281
|
+
well_core=last_tip_picked_up_from,
|
|
1282
|
+
home_after=False,
|
|
1283
|
+
alternate_drop_location=False,
|
|
1284
|
+
)
|
|
1285
|
+
elif isinstance(trash_location, (TrashBin, WasteChute)):
|
|
1286
|
+
self.drop_tip_in_disposal_location(
|
|
1287
|
+
disposal_location=trash_location,
|
|
1288
|
+
home_after=False,
|
|
1289
|
+
alternate_tip_drop=True,
|
|
1290
|
+
)
|
|
1291
|
+
elif isinstance(trash_location, Location):
|
|
1292
|
+
self.drop_tip(
|
|
1293
|
+
location=trash_location,
|
|
1294
|
+
well_core=trash_location.labware.as_well()._core, # type: ignore[arg-type]
|
|
1295
|
+
home_after=False,
|
|
1296
|
+
alternate_drop_location=True,
|
|
1297
|
+
)
|
|
1298
|
+
|
|
1299
|
+
def _pick_up_tip() -> WellCore:
|
|
1300
|
+
next_tip = self.get_next_tip(
|
|
1301
|
+
tip_racks=[core for loc, core in tip_racks],
|
|
1302
|
+
starting_well=starting_tip,
|
|
1303
|
+
)
|
|
1304
|
+
if next_tip is None:
|
|
1305
|
+
raise RuntimeError(
|
|
1306
|
+
f"No tip available among the tipracks assigned for {self.get_pipette_name()}:"
|
|
1307
|
+
f" {[f'{tip_rack[1].get_display_name()} in {tip_rack[1].get_deck_slot()}' for tip_rack in tip_racks]}"
|
|
1308
|
+
)
|
|
1309
|
+
(
|
|
1310
|
+
tiprack_loc,
|
|
1311
|
+
tiprack_uri,
|
|
1312
|
+
tip_well,
|
|
1313
|
+
) = self._get_location_and_well_core_from_next_tip_info(next_tip, tip_racks)
|
|
1314
|
+
if tiprack_uri != tiprack_uri_for_transfer_props:
|
|
1315
|
+
raise RuntimeError(
|
|
1316
|
+
f"Tiprack {tiprack_uri} does not match the tiprack designated "
|
|
1317
|
+
f"for this transfer- {tiprack_uri_for_transfer_props}."
|
|
1318
|
+
)
|
|
1319
|
+
self.pick_up_tip(
|
|
1320
|
+
location=tiprack_loc,
|
|
1321
|
+
well_core=tip_well,
|
|
1322
|
+
presses=None,
|
|
1323
|
+
increment=None,
|
|
1324
|
+
)
|
|
1325
|
+
return tip_well
|
|
1326
|
+
|
|
1327
|
+
if new_tip == TransferTipPolicyV2.ONCE:
|
|
1328
|
+
last_tip_picked_up_from = _pick_up_tip()
|
|
1329
|
+
|
|
1330
|
+
prev_src: Optional[Tuple[Location, WellCore]] = None
|
|
1331
|
+
post_disp_tip_contents = [
|
|
1332
|
+
tx_comps_executor.LiquidAndAirGapPair(
|
|
1333
|
+
liquid=0,
|
|
1334
|
+
air_gap=0,
|
|
1335
|
+
)
|
|
1336
|
+
]
|
|
1337
|
+
next_step_volume, next_src_dest_combo = next(source_dest_per_volume_step)
|
|
1338
|
+
is_last_step = False
|
|
1339
|
+
while not is_last_step:
|
|
1340
|
+
step_volume = next_step_volume
|
|
1341
|
+
src_dest_combo = next_src_dest_combo
|
|
1342
|
+
step_source, step_destination = src_dest_combo
|
|
1343
|
+
try:
|
|
1344
|
+
next_step_volume, next_src_dest_combo = next(
|
|
1345
|
+
source_dest_per_volume_step
|
|
1346
|
+
)
|
|
1347
|
+
except StopIteration:
|
|
1348
|
+
is_last_step = True
|
|
1349
|
+
|
|
1350
|
+
if new_tip == TransferTipPolicyV2.ALWAYS or (
|
|
1351
|
+
new_tip == TransferTipPolicyV2.PER_SOURCE and step_source != prev_src
|
|
1352
|
+
):
|
|
1353
|
+
if prev_src is not None:
|
|
1354
|
+
_drop_tip()
|
|
1355
|
+
last_tip_picked_up_from = _pick_up_tip()
|
|
1356
|
+
post_disp_tip_contents = [
|
|
1357
|
+
tx_comps_executor.LiquidAndAirGapPair(
|
|
1358
|
+
liquid=0,
|
|
1359
|
+
air_gap=0,
|
|
1360
|
+
)
|
|
1361
|
+
]
|
|
1362
|
+
# Enable LPD only if all of these apply:
|
|
1363
|
+
# - LPD is globally enabled for this pipette
|
|
1364
|
+
# - it is the first time visiting this well
|
|
1365
|
+
# - pipette tip is unused
|
|
1366
|
+
enable_lpd = (
|
|
1367
|
+
self.get_liquid_presence_detection() and step_source != prev_src
|
|
1368
|
+
)
|
|
1369
|
+
elif new_tip == TransferTipPolicyV2.ONCE:
|
|
1370
|
+
# Enable LPD only if:
|
|
1371
|
+
# - LPD is globally enabled for this pipette
|
|
1372
|
+
# - this is the first source well of the entire transfer, which means
|
|
1373
|
+
# that the current tip is unused
|
|
1374
|
+
enable_lpd = self.get_liquid_presence_detection() and prev_src is None
|
|
1375
|
+
else:
|
|
1376
|
+
enable_lpd = False
|
|
1377
|
+
|
|
1378
|
+
with self.lpd_for_transfer(enable_lpd):
|
|
1379
|
+
post_asp_tip_contents = self.aspirate_liquid_class(
|
|
1380
|
+
volume=step_volume,
|
|
1381
|
+
source=step_source,
|
|
1382
|
+
transfer_properties=transfer_props,
|
|
1383
|
+
transfer_type=tx_comps_executor.TransferType.ONE_TO_ONE,
|
|
1384
|
+
tip_contents=post_disp_tip_contents,
|
|
1385
|
+
volume_for_pipette_mode_configuration=step_volume,
|
|
1386
|
+
)
|
|
1387
|
+
post_disp_tip_contents = self.dispense_liquid_class(
|
|
1388
|
+
volume=step_volume,
|
|
1389
|
+
dest=step_destination,
|
|
1390
|
+
source=step_source,
|
|
1391
|
+
transfer_properties=transfer_props,
|
|
1392
|
+
transfer_type=tx_comps_executor.TransferType.ONE_TO_ONE,
|
|
1393
|
+
tip_contents=post_asp_tip_contents,
|
|
1394
|
+
add_final_air_gap=(
|
|
1395
|
+
False
|
|
1396
|
+
if is_last_step and new_tip == TransferTipPolicyV2.NEVER
|
|
1397
|
+
else True
|
|
1398
|
+
),
|
|
1399
|
+
trash_location=trash_location,
|
|
1400
|
+
)
|
|
1401
|
+
prev_src = step_source
|
|
1402
|
+
if new_tip != TransferTipPolicyV2.NEVER:
|
|
1403
|
+
_drop_tip()
|
|
1404
|
+
|
|
1405
|
+
# TODO(spp, 2025-02-25): wire up return tip
|
|
1406
|
+
def distribute_with_liquid_class( # noqa: C901
|
|
1407
|
+
self,
|
|
1408
|
+
liquid_class: LiquidClass,
|
|
1409
|
+
volume: float,
|
|
1410
|
+
source: Tuple[Location, WellCore],
|
|
1411
|
+
dest: List[Tuple[Location, WellCore]],
|
|
1412
|
+
new_tip: Literal[TransferTipPolicyV2.NEVER, TransferTipPolicyV2.ONCE],
|
|
1413
|
+
tip_racks: List[Tuple[Location, LabwareCore]],
|
|
1414
|
+
starting_tip: Optional[WellCore],
|
|
1415
|
+
trash_location: Union[Location, TrashBin, WasteChute],
|
|
1416
|
+
return_tip: bool,
|
|
1012
1417
|
) -> None:
|
|
1013
|
-
"""Execute
|
|
1418
|
+
"""Execute a distribution using liquid class properties.
|
|
1419
|
+
|
|
1420
|
+
Args:
|
|
1421
|
+
liquid_class: The liquid class to use for transfer properties.
|
|
1422
|
+
volume: The amount of liquid in uL, to dispense into each destination well.
|
|
1423
|
+
source: Source well represented as a tuple of types.Location and WellCore.
|
|
1424
|
+
types.Location is only necessary for saving the last accessed location.
|
|
1425
|
+
dest: List of destination wells, with each well represented as a tuple of
|
|
1426
|
+
types.Location and WellCore.
|
|
1427
|
+
types.Location is only necessary for saving the last accessed location.
|
|
1428
|
+
new_tip: Whether the transfer should use a new tip 'once' or 'never'.
|
|
1429
|
+
tiprack_uri: The URI of the tiprack that the transfer settings are for.
|
|
1430
|
+
tip_drop_location: Location where the tip will be dropped (if appropriate).
|
|
1431
|
+
|
|
1432
|
+
This method distributes the liquid in the source well into multiple destinations.
|
|
1433
|
+
It can accomplish this by either doing a multi-dispense (aspirate once and then
|
|
1434
|
+
dispense multiple times consecutively) or by doing multiple single-dispenses
|
|
1435
|
+
(going back to aspirate after each dispense). Whether it does a multi-dispense or
|
|
1436
|
+
multiple single dispenses is determined by whether multi-dispense properties
|
|
1437
|
+
are available in the liquid class and whether the tip in use can hold multiple
|
|
1438
|
+
volumes to be dispensed whithout having to refill.
|
|
1439
|
+
"""
|
|
1440
|
+
if not tip_racks:
|
|
1441
|
+
raise RuntimeError(
|
|
1442
|
+
"No tipracks found for pipette in order to perform transfer"
|
|
1443
|
+
)
|
|
1444
|
+
assert new_tip in [TransferTipPolicyV2.NEVER, TransferTipPolicyV2.ONCE]
|
|
1445
|
+
|
|
1446
|
+
tiprack_uri_for_transfer_props = tip_racks[0][1].get_uri()
|
|
1447
|
+
working_volume = min(
|
|
1448
|
+
self.get_max_volume(),
|
|
1449
|
+
self._engine_client.state.geometry.get_nominal_tip_geometry(
|
|
1450
|
+
pipette_id=self.pipette_id,
|
|
1451
|
+
labware_id=tip_racks[0][1].labware_id,
|
|
1452
|
+
well_name=None,
|
|
1453
|
+
).volume,
|
|
1454
|
+
)
|
|
1455
|
+
|
|
1456
|
+
try:
|
|
1457
|
+
transfer_props = liquid_class.get_for(
|
|
1458
|
+
pipette=self.get_pipette_name(), tip_rack=tiprack_uri_for_transfer_props
|
|
1459
|
+
)
|
|
1460
|
+
except NoLiquidClassPropertyError:
|
|
1461
|
+
if self._protocol_core.robot_type == "OT-2 Standard":
|
|
1462
|
+
raise NoLiquidClassPropertyError(
|
|
1463
|
+
"Default liquid classes are not supported with OT-2 pipettes and tip racks."
|
|
1464
|
+
) from None
|
|
1465
|
+
raise
|
|
1466
|
+
|
|
1467
|
+
# If the volume to dispense into a well is less than threashold for low volume mode,
|
|
1468
|
+
# then set the max working volume to the max volume of low volume mode.
|
|
1469
|
+
# NOTE: this logic will need to be updated once we support list of volumes
|
|
1470
|
+
# TODO (spp): refactor this to use the volume thresholds from shared data
|
|
1471
|
+
has_low_volume_mode = self.get_pipette_name() in [
|
|
1472
|
+
"flex_1channel_50",
|
|
1473
|
+
"flex_8channel_50",
|
|
1474
|
+
]
|
|
1475
|
+
if has_low_volume_mode and volume < 5:
|
|
1476
|
+
working_volume = 30
|
|
1477
|
+
# If there are no multi-dispense properties or if the volume to distribute
|
|
1478
|
+
# per destination well is so large that the tip cannot hold enough liquid
|
|
1479
|
+
# to consecutively distribute to at least two wells, then we resort to using
|
|
1480
|
+
# a regular, one-to-one transfer to carry out the distribution.
|
|
1481
|
+
min_asp_vol_for_multi_dispense = 2 * volume
|
|
1482
|
+
if transfer_props.multi_dispense is None or (
|
|
1483
|
+
transfer_props.multi_dispense is not None
|
|
1484
|
+
and not self._tip_can_hold_volume_for_multi_dispensing(
|
|
1485
|
+
transfer_volume=min_asp_vol_for_multi_dispense,
|
|
1486
|
+
multi_dispense_properties=transfer_props.multi_dispense,
|
|
1487
|
+
tip_working_volume=working_volume,
|
|
1488
|
+
)
|
|
1489
|
+
):
|
|
1490
|
+
self.transfer_with_liquid_class(
|
|
1491
|
+
liquid_class=liquid_class,
|
|
1492
|
+
volume=volume,
|
|
1493
|
+
source=[source for _ in range(len(dest))],
|
|
1494
|
+
dest=dest,
|
|
1495
|
+
new_tip=new_tip,
|
|
1496
|
+
tip_racks=tip_racks,
|
|
1497
|
+
starting_tip=starting_tip,
|
|
1498
|
+
trash_location=trash_location,
|
|
1499
|
+
return_tip=return_tip,
|
|
1500
|
+
)
|
|
1501
|
+
return
|
|
1502
|
+
|
|
1503
|
+
# TODO: use the ID returned by load_liquid_class in command annotations
|
|
1504
|
+
self.load_liquid_class(
|
|
1505
|
+
name=liquid_class.name,
|
|
1506
|
+
transfer_properties=transfer_props,
|
|
1507
|
+
tiprack_uri=tiprack_uri_for_transfer_props,
|
|
1508
|
+
)
|
|
1509
|
+
|
|
1510
|
+
# This will return a generator that provides pairs of destination well and
|
|
1511
|
+
# the volume to dispense into it
|
|
1512
|
+
dest_per_volume_step = (
|
|
1513
|
+
tx_commons.expand_for_volume_constraints_for_liquid_classes(
|
|
1514
|
+
volumes=[volume for _ in range(len(dest))],
|
|
1515
|
+
targets=dest,
|
|
1516
|
+
max_volume=working_volume,
|
|
1517
|
+
)
|
|
1518
|
+
)
|
|
1519
|
+
|
|
1520
|
+
last_tip_picked_up_from: Optional[WellCore] = None
|
|
1521
|
+
|
|
1522
|
+
def _drop_tip() -> None:
|
|
1523
|
+
if return_tip:
|
|
1524
|
+
assert last_tip_picked_up_from is not None
|
|
1525
|
+
self.drop_tip(
|
|
1526
|
+
location=None,
|
|
1527
|
+
well_core=last_tip_picked_up_from,
|
|
1528
|
+
home_after=False,
|
|
1529
|
+
alternate_drop_location=False,
|
|
1530
|
+
)
|
|
1531
|
+
elif isinstance(trash_location, (TrashBin, WasteChute)):
|
|
1532
|
+
self.drop_tip_in_disposal_location(
|
|
1533
|
+
disposal_location=trash_location,
|
|
1534
|
+
home_after=False,
|
|
1535
|
+
alternate_tip_drop=True,
|
|
1536
|
+
)
|
|
1537
|
+
elif isinstance(trash_location, Location):
|
|
1538
|
+
self.drop_tip(
|
|
1539
|
+
location=trash_location,
|
|
1540
|
+
well_core=trash_location.labware.as_well()._core, # type: ignore[arg-type]
|
|
1541
|
+
home_after=False,
|
|
1542
|
+
alternate_drop_location=True,
|
|
1543
|
+
)
|
|
1544
|
+
|
|
1545
|
+
def _pick_up_tip() -> WellCore:
|
|
1546
|
+
next_tip = self.get_next_tip(
|
|
1547
|
+
tip_racks=[core for loc, core in tip_racks],
|
|
1548
|
+
starting_well=starting_tip,
|
|
1549
|
+
)
|
|
1550
|
+
if next_tip is None:
|
|
1551
|
+
raise RuntimeError(
|
|
1552
|
+
f"No tip available among the tipracks assigned for {self.get_pipette_name()}:"
|
|
1553
|
+
f" {[f'{tip_rack[1].get_display_name()} in {tip_rack[1].get_deck_slot()}' for tip_rack in tip_racks]}"
|
|
1554
|
+
)
|
|
1555
|
+
(
|
|
1556
|
+
tiprack_loc,
|
|
1557
|
+
tiprack_uri,
|
|
1558
|
+
tip_well,
|
|
1559
|
+
) = self._get_location_and_well_core_from_next_tip_info(next_tip, tip_racks)
|
|
1560
|
+
if tiprack_uri != tiprack_uri_for_transfer_props:
|
|
1561
|
+
raise RuntimeError(
|
|
1562
|
+
f"Tiprack {tiprack_uri} does not match the tiprack designated "
|
|
1563
|
+
f"for this transfer- {tiprack_uri_for_transfer_props}."
|
|
1564
|
+
)
|
|
1565
|
+
self.pick_up_tip(
|
|
1566
|
+
location=tiprack_loc,
|
|
1567
|
+
well_core=tip_well,
|
|
1568
|
+
presses=None,
|
|
1569
|
+
increment=None,
|
|
1570
|
+
)
|
|
1571
|
+
return tip_well
|
|
1572
|
+
|
|
1573
|
+
if new_tip != TransferTipPolicyV2.NEVER:
|
|
1574
|
+
last_tip_picked_up_from = _pick_up_tip()
|
|
1575
|
+
|
|
1576
|
+
tip_contents = [
|
|
1577
|
+
tx_comps_executor.LiquidAndAirGapPair(
|
|
1578
|
+
liquid=0,
|
|
1579
|
+
air_gap=0,
|
|
1580
|
+
)
|
|
1581
|
+
]
|
|
1582
|
+
next_step_volume, next_dest = next(dest_per_volume_step)
|
|
1583
|
+
is_last_step = False
|
|
1584
|
+
is_first_step = True
|
|
1585
|
+
|
|
1586
|
+
# This loop will run until the last step has been executed
|
|
1587
|
+
while not is_last_step:
|
|
1588
|
+
total_aspirate_volume = 0.0
|
|
1589
|
+
vol_dest_combo = []
|
|
1590
|
+
|
|
1591
|
+
# This loop looks at the next volumes to dispense and calculates how many
|
|
1592
|
+
# dispense volumes plus their conditioning & disposal volumes can fit into
|
|
1593
|
+
# the tip. It then collects these volumes and their destinations in a list.
|
|
1594
|
+
while not is_last_step and self._tip_can_hold_volume_for_multi_dispensing(
|
|
1595
|
+
transfer_volume=total_aspirate_volume + next_step_volume,
|
|
1596
|
+
multi_dispense_properties=transfer_props.multi_dispense,
|
|
1597
|
+
tip_working_volume=working_volume,
|
|
1598
|
+
):
|
|
1599
|
+
total_aspirate_volume += next_step_volume
|
|
1600
|
+
vol_dest_combo.append((next_step_volume, next_dest))
|
|
1601
|
+
try:
|
|
1602
|
+
next_step_volume, next_dest = next(dest_per_volume_step)
|
|
1603
|
+
except StopIteration:
|
|
1604
|
+
is_last_step = True
|
|
1605
|
+
|
|
1606
|
+
conditioning_vol = (
|
|
1607
|
+
transfer_props.multi_dispense.conditioning_by_volume.get_for_volume(
|
|
1608
|
+
total_aspirate_volume
|
|
1609
|
+
)
|
|
1610
|
+
)
|
|
1611
|
+
disposal_vol = (
|
|
1612
|
+
transfer_props.multi_dispense.disposal_by_volume.get_for_volume(
|
|
1613
|
+
total_aspirate_volume
|
|
1614
|
+
)
|
|
1615
|
+
)
|
|
1616
|
+
|
|
1617
|
+
use_single_dispense = False
|
|
1618
|
+
if total_aspirate_volume == volume and len(vol_dest_combo) == 1:
|
|
1619
|
+
# We are only doing a single transfer. Either because this is the last
|
|
1620
|
+
# remaining volume to dispense or, once this function accepts a list of
|
|
1621
|
+
# volumes, the next pair of volumes is too large to be multi-dispensed.
|
|
1622
|
+
# So we won't use conditioning volume or disposal volume
|
|
1623
|
+
conditioning_vol = 0
|
|
1624
|
+
disposal_vol = 0
|
|
1625
|
+
use_single_dispense = True
|
|
1626
|
+
|
|
1627
|
+
if (
|
|
1628
|
+
not use_single_dispense
|
|
1629
|
+
and disposal_vol > 0
|
|
1630
|
+
and not transfer_props.multi_dispense.retract.blowout.enabled
|
|
1631
|
+
):
|
|
1632
|
+
raise RuntimeError(
|
|
1633
|
+
"Distribute uses a disposal volume but location for disposing of"
|
|
1634
|
+
" the disposal volume cannot be found when blowout is disabled."
|
|
1635
|
+
" Specify a blowout location and enable blowout when using a disposal volume."
|
|
1636
|
+
)
|
|
1637
|
+
|
|
1638
|
+
if (
|
|
1639
|
+
self.get_liquid_presence_detection()
|
|
1640
|
+
and new_tip != TransferTipPolicyV2.NEVER
|
|
1641
|
+
and is_first_step
|
|
1642
|
+
):
|
|
1643
|
+
enable_lpd = True
|
|
1644
|
+
else:
|
|
1645
|
+
enable_lpd = False
|
|
1646
|
+
with self.lpd_for_transfer(enable=enable_lpd):
|
|
1647
|
+
# Aspirate the total volume determined by the loop above
|
|
1648
|
+
tip_contents = self.aspirate_liquid_class(
|
|
1649
|
+
volume=total_aspirate_volume + conditioning_vol + disposal_vol,
|
|
1650
|
+
source=source,
|
|
1651
|
+
transfer_properties=transfer_props,
|
|
1652
|
+
transfer_type=tx_comps_executor.TransferType.ONE_TO_MANY,
|
|
1653
|
+
tip_contents=tip_contents,
|
|
1654
|
+
# We configure the mode based on the last dispense volume and disposal volume
|
|
1655
|
+
# since the mode is only used to determine the dispense push out volume
|
|
1656
|
+
# and we can do a push out only at the last dispense, that too if there is no disposal volume.
|
|
1657
|
+
volume_for_pipette_mode_configuration=vol_dest_combo[-1][0],
|
|
1658
|
+
conditioning_volume=conditioning_vol,
|
|
1659
|
+
)
|
|
1660
|
+
|
|
1661
|
+
# If the tip has volumes correspoinding to multiple destinations, then
|
|
1662
|
+
# multi-dispense in those destinations.
|
|
1663
|
+
# If the tip has a volume corresponding to a single destination, then
|
|
1664
|
+
# do a single-dispense into that destination.
|
|
1665
|
+
for next_vol, next_dest in vol_dest_combo:
|
|
1666
|
+
if use_single_dispense:
|
|
1667
|
+
tip_contents = self.dispense_liquid_class(
|
|
1668
|
+
volume=next_vol,
|
|
1669
|
+
dest=next_dest,
|
|
1670
|
+
source=source,
|
|
1671
|
+
transfer_properties=transfer_props,
|
|
1672
|
+
transfer_type=tx_comps_executor.TransferType.ONE_TO_MANY,
|
|
1673
|
+
tip_contents=tip_contents,
|
|
1674
|
+
add_final_air_gap=(
|
|
1675
|
+
False
|
|
1676
|
+
if is_last_step and new_tip == TransferTipPolicyV2.NEVER
|
|
1677
|
+
else True
|
|
1678
|
+
),
|
|
1679
|
+
trash_location=trash_location,
|
|
1680
|
+
)
|
|
1681
|
+
else:
|
|
1682
|
+
tip_contents = self.dispense_liquid_class_during_multi_dispense(
|
|
1683
|
+
volume=next_vol,
|
|
1684
|
+
dest=next_dest,
|
|
1685
|
+
source=source,
|
|
1686
|
+
transfer_properties=transfer_props,
|
|
1687
|
+
transfer_type=tx_comps_executor.TransferType.ONE_TO_MANY,
|
|
1688
|
+
tip_contents=tip_contents,
|
|
1689
|
+
add_final_air_gap=(
|
|
1690
|
+
False
|
|
1691
|
+
if is_last_step and new_tip == TransferTipPolicyV2.NEVER
|
|
1692
|
+
else True
|
|
1693
|
+
),
|
|
1694
|
+
trash_location=trash_location,
|
|
1695
|
+
conditioning_volume=conditioning_vol,
|
|
1696
|
+
disposal_volume=disposal_vol,
|
|
1697
|
+
)
|
|
1698
|
+
is_first_step = False
|
|
1699
|
+
|
|
1700
|
+
if new_tip != TransferTipPolicyV2.NEVER:
|
|
1701
|
+
_drop_tip()
|
|
1702
|
+
|
|
1703
|
+
def _tip_can_hold_volume_for_multi_dispensing(
|
|
1704
|
+
self,
|
|
1705
|
+
transfer_volume: float,
|
|
1706
|
+
multi_dispense_properties: MultiDispenseProperties,
|
|
1707
|
+
tip_working_volume: float,
|
|
1708
|
+
) -> bool:
|
|
1709
|
+
"""
|
|
1710
|
+
Whether the tip can hold the volume plus the conditioning and disposal volumes
|
|
1711
|
+
required for multi-dispensing.
|
|
1712
|
+
"""
|
|
1713
|
+
return (
|
|
1714
|
+
transfer_volume
|
|
1715
|
+
+ multi_dispense_properties.conditioning_by_volume.get_for_volume(
|
|
1716
|
+
transfer_volume
|
|
1717
|
+
)
|
|
1718
|
+
+ multi_dispense_properties.disposal_by_volume.get_for_volume(
|
|
1719
|
+
transfer_volume
|
|
1720
|
+
)
|
|
1721
|
+
<= tip_working_volume
|
|
1722
|
+
)
|
|
1723
|
+
|
|
1724
|
+
def consolidate_with_liquid_class( # noqa: C901
|
|
1725
|
+
self,
|
|
1726
|
+
liquid_class: LiquidClass,
|
|
1727
|
+
volume: float,
|
|
1728
|
+
source: List[Tuple[Location, WellCore]],
|
|
1729
|
+
dest: Tuple[Location, WellCore],
|
|
1730
|
+
new_tip: Literal[TransferTipPolicyV2.NEVER, TransferTipPolicyV2.ONCE],
|
|
1731
|
+
tip_racks: List[Tuple[Location, LabwareCore]],
|
|
1732
|
+
starting_tip: Optional[WellCore],
|
|
1733
|
+
trash_location: Union[Location, TrashBin, WasteChute],
|
|
1734
|
+
return_tip: bool,
|
|
1735
|
+
) -> None:
|
|
1736
|
+
if not tip_racks:
|
|
1737
|
+
raise RuntimeError(
|
|
1738
|
+
"No tipracks found for pipette in order to perform transfer"
|
|
1739
|
+
)
|
|
1740
|
+
assert new_tip in [TransferTipPolicyV2.NEVER, TransferTipPolicyV2.ONCE]
|
|
1741
|
+
tiprack_uri_for_transfer_props = tip_racks[0][1].get_uri()
|
|
1742
|
+
try:
|
|
1743
|
+
transfer_props = liquid_class.get_for(
|
|
1744
|
+
pipette=self.get_pipette_name(), tip_rack=tiprack_uri_for_transfer_props
|
|
1745
|
+
)
|
|
1746
|
+
except NoLiquidClassPropertyError:
|
|
1747
|
+
if self._protocol_core.robot_type == "OT-2 Standard":
|
|
1748
|
+
raise NoLiquidClassPropertyError(
|
|
1749
|
+
"Default liquid classes are not supported with OT-2 pipettes and tip racks."
|
|
1750
|
+
) from None
|
|
1751
|
+
raise
|
|
1752
|
+
|
|
1753
|
+
blow_out_properties = transfer_props.dispense.retract.blowout
|
|
1754
|
+
if (
|
|
1755
|
+
blow_out_properties.enabled
|
|
1756
|
+
and blow_out_properties.location == BlowoutLocation.SOURCE
|
|
1757
|
+
):
|
|
1758
|
+
raise RuntimeError(
|
|
1759
|
+
'Blowout location "source" incompatible with consolidate liquid.'
|
|
1760
|
+
' Please choose "destination" or "trash".'
|
|
1761
|
+
)
|
|
1762
|
+
|
|
1763
|
+
# TODO: use the ID returned by load_liquid_class in command annotations
|
|
1764
|
+
self.load_liquid_class(
|
|
1765
|
+
name=liquid_class.name,
|
|
1766
|
+
transfer_properties=transfer_props,
|
|
1767
|
+
tiprack_uri=tiprack_uri_for_transfer_props,
|
|
1768
|
+
)
|
|
1769
|
+
|
|
1770
|
+
max_volume = min(
|
|
1771
|
+
self.get_max_volume(),
|
|
1772
|
+
self._engine_client.state.geometry.get_nominal_tip_geometry(
|
|
1773
|
+
pipette_id=self.pipette_id,
|
|
1774
|
+
labware_id=tip_racks[0][1].labware_id,
|
|
1775
|
+
well_name=None,
|
|
1776
|
+
).volume,
|
|
1777
|
+
)
|
|
1778
|
+
|
|
1779
|
+
source_per_volume_step = (
|
|
1780
|
+
tx_commons.expand_for_volume_constraints_for_liquid_classes(
|
|
1781
|
+
volumes=[volume for _ in range(len(source))],
|
|
1782
|
+
targets=source,
|
|
1783
|
+
max_volume=max_volume,
|
|
1784
|
+
)
|
|
1785
|
+
)
|
|
1786
|
+
|
|
1787
|
+
last_tip_picked_up_from: Optional[WellCore] = None
|
|
1788
|
+
|
|
1789
|
+
def _drop_tip() -> None:
|
|
1790
|
+
if return_tip:
|
|
1791
|
+
assert last_tip_picked_up_from is not None
|
|
1792
|
+
self.drop_tip(
|
|
1793
|
+
location=None,
|
|
1794
|
+
well_core=last_tip_picked_up_from,
|
|
1795
|
+
home_after=False,
|
|
1796
|
+
alternate_drop_location=False,
|
|
1797
|
+
)
|
|
1798
|
+
elif isinstance(trash_location, (TrashBin, WasteChute)):
|
|
1799
|
+
self.drop_tip_in_disposal_location(
|
|
1800
|
+
disposal_location=trash_location,
|
|
1801
|
+
home_after=False,
|
|
1802
|
+
alternate_tip_drop=True,
|
|
1803
|
+
)
|
|
1804
|
+
elif isinstance(trash_location, Location):
|
|
1805
|
+
self.drop_tip(
|
|
1806
|
+
location=trash_location,
|
|
1807
|
+
well_core=trash_location.labware.as_well()._core, # type: ignore[arg-type]
|
|
1808
|
+
home_after=False,
|
|
1809
|
+
alternate_drop_location=True,
|
|
1810
|
+
)
|
|
1811
|
+
|
|
1812
|
+
def _pick_up_tip() -> WellCore:
|
|
1813
|
+
next_tip = self.get_next_tip(
|
|
1814
|
+
tip_racks=[core for loc, core in tip_racks],
|
|
1815
|
+
starting_well=starting_tip,
|
|
1816
|
+
)
|
|
1817
|
+
if next_tip is None:
|
|
1818
|
+
raise RuntimeError(
|
|
1819
|
+
f"No tip available among the tipracks assigned for {self.get_pipette_name()}:"
|
|
1820
|
+
f" {[ f'{tip_rack[1].get_display_name()} in {tip_rack[1].get_deck_slot()}' for tip_rack in tip_racks]}"
|
|
1821
|
+
)
|
|
1822
|
+
(
|
|
1823
|
+
tiprack_loc,
|
|
1824
|
+
tiprack_uri,
|
|
1825
|
+
tip_well,
|
|
1826
|
+
) = self._get_location_and_well_core_from_next_tip_info(next_tip, tip_racks)
|
|
1827
|
+
if tiprack_uri != tiprack_uri_for_transfer_props:
|
|
1828
|
+
raise RuntimeError(
|
|
1829
|
+
f"Tiprack {tiprack_uri} does not match the tiprack designated "
|
|
1830
|
+
f"for this transfer- {tiprack_uri_for_transfer_props}."
|
|
1831
|
+
)
|
|
1832
|
+
self.pick_up_tip(
|
|
1833
|
+
location=tiprack_loc,
|
|
1834
|
+
well_core=tip_well,
|
|
1835
|
+
presses=None,
|
|
1836
|
+
increment=None,
|
|
1837
|
+
)
|
|
1838
|
+
return tip_well
|
|
1839
|
+
|
|
1840
|
+
if new_tip == TransferTipPolicyV2.ONCE:
|
|
1841
|
+
last_tip_picked_up_from = _pick_up_tip()
|
|
1842
|
+
|
|
1843
|
+
tip_contents = [
|
|
1844
|
+
tx_comps_executor.LiquidAndAirGapPair(
|
|
1845
|
+
liquid=0,
|
|
1846
|
+
air_gap=0,
|
|
1847
|
+
)
|
|
1848
|
+
]
|
|
1849
|
+
next_step_volume, next_source = next(source_per_volume_step)
|
|
1850
|
+
is_first_step = True
|
|
1851
|
+
is_last_step = False
|
|
1852
|
+
while not is_last_step:
|
|
1853
|
+
total_dispense_volume = 0.0
|
|
1854
|
+
vol_aspirate_combo = []
|
|
1855
|
+
# Take air gap into account because there will be a final air gap before the dispense
|
|
1856
|
+
while total_dispense_volume + next_step_volume <= max_volume:
|
|
1857
|
+
total_dispense_volume += next_step_volume
|
|
1858
|
+
vol_aspirate_combo.append((next_step_volume, next_source))
|
|
1859
|
+
try:
|
|
1860
|
+
next_step_volume, next_source = next(source_per_volume_step)
|
|
1861
|
+
except StopIteration:
|
|
1862
|
+
is_last_step = True
|
|
1863
|
+
break
|
|
1864
|
+
|
|
1865
|
+
if (
|
|
1866
|
+
self.get_liquid_presence_detection()
|
|
1867
|
+
and new_tip != TransferTipPolicyV2.NEVER
|
|
1868
|
+
and is_first_step
|
|
1869
|
+
):
|
|
1870
|
+
enable_lpd = True
|
|
1871
|
+
else:
|
|
1872
|
+
enable_lpd = False
|
|
1873
|
+
|
|
1874
|
+
for step_num, (step_volume, step_source) in enumerate(vol_aspirate_combo):
|
|
1875
|
+
with self.lpd_for_transfer(enable=enable_lpd):
|
|
1876
|
+
tip_contents = self.aspirate_liquid_class(
|
|
1877
|
+
volume=step_volume,
|
|
1878
|
+
source=step_source,
|
|
1879
|
+
transfer_properties=transfer_props,
|
|
1880
|
+
transfer_type=tx_comps_executor.TransferType.MANY_TO_ONE,
|
|
1881
|
+
tip_contents=tip_contents,
|
|
1882
|
+
volume_for_pipette_mode_configuration=(
|
|
1883
|
+
total_dispense_volume if step_num == 0 else None
|
|
1884
|
+
),
|
|
1885
|
+
)
|
|
1886
|
+
is_first_step = False
|
|
1887
|
+
enable_lpd = False
|
|
1888
|
+
tip_contents = self.dispense_liquid_class(
|
|
1889
|
+
volume=total_dispense_volume,
|
|
1890
|
+
dest=dest,
|
|
1891
|
+
source=None, # Cannot have source as location for blowout so hardcoded to None
|
|
1892
|
+
transfer_properties=transfer_props,
|
|
1893
|
+
transfer_type=tx_comps_executor.TransferType.MANY_TO_ONE,
|
|
1894
|
+
tip_contents=tip_contents,
|
|
1895
|
+
add_final_air_gap=(
|
|
1896
|
+
False
|
|
1897
|
+
if is_last_step and new_tip == TransferTipPolicyV2.NEVER
|
|
1898
|
+
else True
|
|
1899
|
+
),
|
|
1900
|
+
trash_location=trash_location,
|
|
1901
|
+
)
|
|
1902
|
+
if new_tip != TransferTipPolicyV2.NEVER:
|
|
1903
|
+
_drop_tip()
|
|
1904
|
+
|
|
1905
|
+
def _get_location_and_well_core_from_next_tip_info(
|
|
1906
|
+
self,
|
|
1907
|
+
tip_info: NextTipInfo,
|
|
1908
|
+
tip_racks: List[Tuple[Location, LabwareCore]],
|
|
1909
|
+
) -> _TipInfo:
|
|
1910
|
+
tiprack_labware_core = self._protocol_core._labware_cores_by_id[
|
|
1911
|
+
tip_info.labwareId
|
|
1912
|
+
]
|
|
1913
|
+
tip_well = tiprack_labware_core.get_well_core(tip_info.tipStartingWell)
|
|
1914
|
+
|
|
1915
|
+
tiprack_loc = [
|
|
1916
|
+
loc for loc, lw_core in tip_racks if lw_core == tiprack_labware_core
|
|
1917
|
+
]
|
|
1918
|
+
|
|
1919
|
+
return _TipInfo(
|
|
1920
|
+
Location(tip_well.get_top(0), tiprack_loc[0].labware),
|
|
1921
|
+
tiprack_labware_core.get_uri(),
|
|
1922
|
+
tip_well,
|
|
1923
|
+
)
|
|
1924
|
+
|
|
1925
|
+
def aspirate_liquid_class(
|
|
1926
|
+
self,
|
|
1927
|
+
volume: float,
|
|
1928
|
+
source: Tuple[Location, WellCore],
|
|
1929
|
+
transfer_properties: TransferProperties,
|
|
1930
|
+
transfer_type: tx_comps_executor.TransferType,
|
|
1931
|
+
tip_contents: List[tx_comps_executor.LiquidAndAirGapPair],
|
|
1932
|
+
volume_for_pipette_mode_configuration: Optional[float],
|
|
1933
|
+
conditioning_volume: Optional[float] = None,
|
|
1934
|
+
) -> List[tx_comps_executor.LiquidAndAirGapPair]:
|
|
1935
|
+
"""Execute aspiration steps.
|
|
1936
|
+
|
|
1937
|
+
1. Submerge
|
|
1938
|
+
2. Mix
|
|
1939
|
+
3. pre-wet
|
|
1940
|
+
4. Aspirate
|
|
1941
|
+
5. Delay- wait inside the liquid
|
|
1942
|
+
6. Aspirate retract
|
|
1943
|
+
|
|
1944
|
+
Return: List of liquid and air gap pairs in tip.
|
|
1945
|
+
"""
|
|
1946
|
+
aspirate_props = transfer_properties.aspirate
|
|
1947
|
+
tx_commons.check_valid_liquid_class_volume_parameters(
|
|
1948
|
+
aspirate_volume=volume,
|
|
1949
|
+
air_gap=(
|
|
1950
|
+
aspirate_props.retract.air_gap_by_volume.get_for_volume(volume)
|
|
1951
|
+
if conditioning_volume is None
|
|
1952
|
+
else 0
|
|
1953
|
+
),
|
|
1954
|
+
disposal_volume=0, # Disposal volume is accounted for in aspirate vol
|
|
1955
|
+
max_volume=self.get_working_volume(),
|
|
1956
|
+
)
|
|
1957
|
+
source_loc, source_well = source
|
|
1958
|
+
aspirate_point = (
|
|
1959
|
+
tx_comps_executor.absolute_point_from_position_reference_and_offset(
|
|
1960
|
+
well=source_well,
|
|
1961
|
+
position_reference=aspirate_props.position_reference,
|
|
1962
|
+
offset=aspirate_props.offset,
|
|
1963
|
+
)
|
|
1964
|
+
)
|
|
1965
|
+
aspirate_location = Location(aspirate_point, labware=source_loc.labware)
|
|
1966
|
+
last_liquid_and_airgap_in_tip = (
|
|
1967
|
+
tip_contents[-1]
|
|
1968
|
+
if tip_contents
|
|
1969
|
+
else tx_comps_executor.LiquidAndAirGapPair(
|
|
1970
|
+
liquid=0,
|
|
1971
|
+
air_gap=0,
|
|
1972
|
+
)
|
|
1973
|
+
)
|
|
1974
|
+
components_executor = tx_comps_executor.TransferComponentsExecutor(
|
|
1975
|
+
instrument_core=self,
|
|
1976
|
+
transfer_properties=transfer_properties,
|
|
1977
|
+
target_location=aspirate_location,
|
|
1978
|
+
target_well=source_well,
|
|
1979
|
+
transfer_type=transfer_type,
|
|
1980
|
+
tip_state=tx_comps_executor.TipState(
|
|
1981
|
+
last_liquid_and_air_gap_in_tip=last_liquid_and_airgap_in_tip
|
|
1982
|
+
),
|
|
1983
|
+
)
|
|
1984
|
+
components_executor.submerge(
|
|
1985
|
+
submerge_properties=aspirate_props.submerge,
|
|
1986
|
+
post_submerge_action="aspirate",
|
|
1987
|
+
volume_for_pipette_mode_configuration=volume_for_pipette_mode_configuration,
|
|
1988
|
+
)
|
|
1989
|
+
# Do not do a pre-aspirate mix or pre-wet if consolidating
|
|
1990
|
+
if transfer_type != tx_comps_executor.TransferType.MANY_TO_ONE:
|
|
1991
|
+
# TODO: check if we want to do a mix only once when we're splitting a transfer
|
|
1992
|
+
# and coming back to the source multiple times.
|
|
1993
|
+
# We will have to do pre-wet always even for split volumes
|
|
1994
|
+
components_executor.mix(
|
|
1995
|
+
mix_properties=aspirate_props.mix, last_dispense_push_out=False
|
|
1996
|
+
)
|
|
1997
|
+
# TODO: check if pre-wet needs to be enabled for first well of consolidate
|
|
1998
|
+
components_executor.pre_wet(
|
|
1999
|
+
volume=volume,
|
|
2000
|
+
)
|
|
2001
|
+
components_executor.aspirate_and_wait(volume=volume)
|
|
2002
|
+
if (
|
|
2003
|
+
transfer_type == tx_comps_executor.TransferType.ONE_TO_MANY
|
|
2004
|
+
and conditioning_volume not in [None, 0.0]
|
|
2005
|
+
and transfer_properties.multi_dispense is not None
|
|
2006
|
+
):
|
|
2007
|
+
# Dispense the conditioning volume
|
|
2008
|
+
components_executor.dispense_and_wait(
|
|
2009
|
+
dispense_properties=transfer_properties.multi_dispense,
|
|
2010
|
+
volume=conditioning_volume or 0.0,
|
|
2011
|
+
push_out_override=0,
|
|
2012
|
+
)
|
|
2013
|
+
components_executor.retract_after_aspiration(
|
|
2014
|
+
volume=volume, add_air_gap=False
|
|
2015
|
+
)
|
|
2016
|
+
else:
|
|
2017
|
+
components_executor.retract_after_aspiration(
|
|
2018
|
+
volume=volume, add_air_gap=True
|
|
2019
|
+
)
|
|
2020
|
+
|
|
2021
|
+
# return copy of tip_contents with last entry replaced by tip state from executor
|
|
2022
|
+
last_contents = components_executor.tip_state.last_liquid_and_air_gap_in_tip
|
|
2023
|
+
new_tip_contents = tip_contents[0:-1] + [last_contents]
|
|
2024
|
+
return new_tip_contents
|
|
2025
|
+
|
|
2026
|
+
def dispense_liquid_class(
|
|
2027
|
+
self,
|
|
2028
|
+
volume: float,
|
|
2029
|
+
dest: Tuple[Location, WellCore],
|
|
2030
|
+
source: Optional[Tuple[Location, WellCore]],
|
|
2031
|
+
transfer_properties: TransferProperties,
|
|
2032
|
+
transfer_type: tx_comps_executor.TransferType,
|
|
2033
|
+
tip_contents: List[tx_comps_executor.LiquidAndAirGapPair],
|
|
2034
|
+
add_final_air_gap: bool,
|
|
2035
|
+
trash_location: Union[Location, TrashBin, WasteChute],
|
|
2036
|
+
) -> List[tx_comps_executor.LiquidAndAirGapPair]:
|
|
2037
|
+
"""Execute single-dispense steps.
|
|
2038
|
+
1. Move pipette to the ‘submerge’ position with normal speed.
|
|
2039
|
+
- The pipette will move in an arc- move to max z height of labware
|
|
2040
|
+
(if asp & disp are in same labware)
|
|
2041
|
+
or max z height of all labware (if asp & disp are in separate labware)
|
|
2042
|
+
2. Air gap removal:
|
|
2043
|
+
- If dispense location is above the meniscus, DO NOT remove air gap
|
|
2044
|
+
(it will be dispensed along with rest of the liquid later).
|
|
2045
|
+
All other scenarios, remove the air gap by doing a dispense
|
|
2046
|
+
- Flow rate = min(dispenseFlowRate, (airGapByVolume)/sec)
|
|
2047
|
+
- Use the post-dispense delay
|
|
2048
|
+
4. Move to the dispense position at the specified ‘submerge’ speed
|
|
2049
|
+
(even if we might not be moving into the liquid)
|
|
2050
|
+
- Do a delay (submerge delay)
|
|
2051
|
+
6. Dispense:
|
|
2052
|
+
- Dispense at the specified flow rate.
|
|
2053
|
+
- Do a push out as specified ONLY IF there is no mix following the dispense AND the tip is empty.
|
|
2054
|
+
Volume for push out is the volume being dispensed. So if we are dispensing 50uL, use pushOutByVolume[50] as push out volume.
|
|
2055
|
+
7. Delay
|
|
2056
|
+
8. Mix using the same flow rate and delays as specified for asp+disp,
|
|
2057
|
+
with the volume and the number of repetitions specified. Use the delays in asp & disp.
|
|
2058
|
+
- If the dispense position is outside the liquid, then raise error if mix is enabled.
|
|
2059
|
+
Can only be checked if using liquid level detection/ meniscus-based positioning.
|
|
2060
|
+
- If the user wants to perform a mix then they should specify a dispense position that’s inside the liquid OR do mix() on the wells after transfer.
|
|
2061
|
+
- Do push out at the last dispense.
|
|
2062
|
+
9. Retract
|
|
2063
|
+
|
|
2064
|
+
Return:
|
|
2065
|
+
List of liquid and air gap pairs in tip.
|
|
2066
|
+
"""
|
|
2067
|
+
dispense_props = transfer_properties.dispense
|
|
2068
|
+
dest_loc, dest_well = dest
|
|
2069
|
+
dispense_point = (
|
|
2070
|
+
tx_comps_executor.absolute_point_from_position_reference_and_offset(
|
|
2071
|
+
well=dest_well,
|
|
2072
|
+
position_reference=dispense_props.position_reference,
|
|
2073
|
+
offset=dispense_props.offset,
|
|
2074
|
+
)
|
|
2075
|
+
)
|
|
2076
|
+
dispense_location = Location(dispense_point, labware=dest_loc.labware)
|
|
2077
|
+
last_liquid_and_airgap_in_tip = (
|
|
2078
|
+
tip_contents[-1]
|
|
2079
|
+
if tip_contents
|
|
2080
|
+
else tx_comps_executor.LiquidAndAirGapPair(
|
|
2081
|
+
liquid=0,
|
|
2082
|
+
air_gap=0,
|
|
2083
|
+
)
|
|
2084
|
+
)
|
|
2085
|
+
components_executor = tx_comps_executor.TransferComponentsExecutor(
|
|
2086
|
+
instrument_core=self,
|
|
2087
|
+
transfer_properties=transfer_properties,
|
|
2088
|
+
target_location=dispense_location,
|
|
2089
|
+
target_well=dest_well,
|
|
2090
|
+
transfer_type=transfer_type,
|
|
2091
|
+
tip_state=tx_comps_executor.TipState(
|
|
2092
|
+
last_liquid_and_air_gap_in_tip=last_liquid_and_airgap_in_tip
|
|
2093
|
+
),
|
|
2094
|
+
)
|
|
2095
|
+
components_executor.submerge(
|
|
2096
|
+
submerge_properties=dispense_props.submerge,
|
|
2097
|
+
post_submerge_action="dispense",
|
|
2098
|
+
volume_for_pipette_mode_configuration=None,
|
|
2099
|
+
)
|
|
2100
|
+
push_out_vol = (
|
|
2101
|
+
0.0
|
|
2102
|
+
if dispense_props.mix.enabled
|
|
2103
|
+
else dispense_props.push_out_by_volume.get_for_volume(volume)
|
|
2104
|
+
)
|
|
2105
|
+
components_executor.dispense_and_wait(
|
|
2106
|
+
dispense_properties=dispense_props,
|
|
2107
|
+
volume=volume,
|
|
2108
|
+
push_out_override=push_out_vol,
|
|
2109
|
+
)
|
|
2110
|
+
components_executor.mix(
|
|
2111
|
+
mix_properties=dispense_props.mix,
|
|
2112
|
+
last_dispense_push_out=True,
|
|
2113
|
+
)
|
|
2114
|
+
components_executor.retract_after_dispensing(
|
|
2115
|
+
trash_location=trash_location,
|
|
2116
|
+
source_location=source[0] if source else None,
|
|
2117
|
+
source_well=source[1] if source else None,
|
|
2118
|
+
add_final_air_gap=add_final_air_gap,
|
|
2119
|
+
)
|
|
2120
|
+
last_contents = components_executor.tip_state.last_liquid_and_air_gap_in_tip
|
|
2121
|
+
new_tip_contents = tip_contents[0:-1] + [last_contents]
|
|
2122
|
+
return new_tip_contents
|
|
2123
|
+
|
|
2124
|
+
def dispense_liquid_class_during_multi_dispense(
|
|
2125
|
+
self,
|
|
2126
|
+
volume: float,
|
|
2127
|
+
dest: Tuple[Location, WellCore],
|
|
2128
|
+
source: Optional[Tuple[Location, WellCore]],
|
|
2129
|
+
transfer_properties: TransferProperties,
|
|
2130
|
+
transfer_type: tx_comps_executor.TransferType,
|
|
2131
|
+
tip_contents: List[tx_comps_executor.LiquidAndAirGapPair],
|
|
2132
|
+
add_final_air_gap: bool,
|
|
2133
|
+
trash_location: Union[Location, TrashBin, WasteChute],
|
|
2134
|
+
conditioning_volume: float,
|
|
2135
|
+
disposal_volume: float,
|
|
2136
|
+
) -> List[tx_comps_executor.LiquidAndAirGapPair]:
|
|
2137
|
+
"""Execute a dispense step that's part of a multi-dispense.
|
|
2138
|
+
|
|
2139
|
+
This executes a dispense step very similar to a single dispense except that:
|
|
2140
|
+
- it uses the multi-dispense properties from the liquid class
|
|
2141
|
+
- handles push-out based on disposal volume in addition to the existing conditions
|
|
2142
|
+
- delegates the retraction steps to a different, multi-dispense retract function
|
|
2143
|
+
|
|
2144
|
+
Return:
|
|
2145
|
+
List of liquid and air gap pairs in tip.
|
|
2146
|
+
"""
|
|
2147
|
+
assert transfer_properties.multi_dispense is not None
|
|
2148
|
+
dispense_props = transfer_properties.multi_dispense
|
|
2149
|
+
|
|
2150
|
+
dest_loc, dest_well = dest
|
|
2151
|
+
dispense_point = (
|
|
2152
|
+
tx_comps_executor.absolute_point_from_position_reference_and_offset(
|
|
2153
|
+
well=dest_well,
|
|
2154
|
+
position_reference=dispense_props.position_reference,
|
|
2155
|
+
offset=dispense_props.offset,
|
|
2156
|
+
)
|
|
2157
|
+
)
|
|
2158
|
+
dispense_location = Location(dispense_point, labware=dest_loc.labware)
|
|
2159
|
+
last_liquid_and_airgap_in_tip = (
|
|
2160
|
+
tip_contents[-1]
|
|
2161
|
+
if tip_contents
|
|
2162
|
+
else tx_comps_executor.LiquidAndAirGapPair(
|
|
2163
|
+
liquid=0,
|
|
2164
|
+
air_gap=0,
|
|
2165
|
+
)
|
|
2166
|
+
)
|
|
2167
|
+
components_executor = tx_comps_executor.TransferComponentsExecutor(
|
|
2168
|
+
instrument_core=self,
|
|
2169
|
+
transfer_properties=transfer_properties,
|
|
2170
|
+
target_location=dispense_location,
|
|
2171
|
+
target_well=dest_well,
|
|
2172
|
+
transfer_type=transfer_type,
|
|
2173
|
+
tip_state=tx_comps_executor.TipState(
|
|
2174
|
+
last_liquid_and_air_gap_in_tip=last_liquid_and_airgap_in_tip
|
|
2175
|
+
),
|
|
2176
|
+
)
|
|
2177
|
+
components_executor.submerge(
|
|
2178
|
+
submerge_properties=dispense_props.submerge,
|
|
2179
|
+
post_submerge_action="dispense",
|
|
2180
|
+
volume_for_pipette_mode_configuration=None,
|
|
2181
|
+
)
|
|
2182
|
+
tip_starting_volume = self.get_current_volume()
|
|
2183
|
+
is_last_dispense_without_disposal_vol = (
|
|
2184
|
+
disposal_volume == 0 and tip_starting_volume == volume
|
|
2185
|
+
)
|
|
2186
|
+
push_out_vol = (
|
|
2187
|
+
# TODO (spp): verify if it's okay to use push_out_by_volume of single dispense
|
|
2188
|
+
transfer_properties.dispense.push_out_by_volume.get_for_volume(volume)
|
|
2189
|
+
if is_last_dispense_without_disposal_vol
|
|
2190
|
+
else 0.0
|
|
2191
|
+
)
|
|
2192
|
+
|
|
2193
|
+
components_executor.dispense_and_wait(
|
|
2194
|
+
dispense_properties=dispense_props,
|
|
2195
|
+
volume=volume,
|
|
2196
|
+
push_out_override=push_out_vol,
|
|
2197
|
+
)
|
|
2198
|
+
components_executor.retract_during_multi_dispensing(
|
|
2199
|
+
trash_location=trash_location,
|
|
2200
|
+
source_location=source[0] if source else None,
|
|
2201
|
+
source_well=source[1] if source else None,
|
|
2202
|
+
conditioning_volume=conditioning_volume,
|
|
2203
|
+
add_final_air_gap=add_final_air_gap,
|
|
2204
|
+
is_last_retract=tip_starting_volume - volume == disposal_volume,
|
|
2205
|
+
)
|
|
2206
|
+
last_contents = components_executor.tip_state.last_liquid_and_air_gap_in_tip
|
|
2207
|
+
new_tip_contents = tip_contents[0:-1] + [last_contents]
|
|
2208
|
+
return new_tip_contents
|
|
1014
2209
|
|
|
1015
2210
|
def retract(self) -> None:
|
|
1016
2211
|
"""Retract this instrument to the top of the gantry."""
|
|
@@ -1060,11 +2255,40 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
1060
2255
|
|
|
1061
2256
|
return result.z_position is not None
|
|
1062
2257
|
|
|
2258
|
+
def get_minimum_liquid_sense_height(self) -> float:
|
|
2259
|
+
attached_tip = self._engine_client.state.pipettes.get_attached_tip(
|
|
2260
|
+
self._pipette_id
|
|
2261
|
+
)
|
|
2262
|
+
if attached_tip:
|
|
2263
|
+
tip_volume = attached_tip.volume
|
|
2264
|
+
else:
|
|
2265
|
+
raise TipNotAttachedError(
|
|
2266
|
+
"Need to have a tip attached for liquid-sense operations."
|
|
2267
|
+
)
|
|
2268
|
+
lld_settings = self._engine_client.state.pipettes.get_pipette_lld_settings(
|
|
2269
|
+
pipette_id=self.pipette_id
|
|
2270
|
+
)
|
|
2271
|
+
if lld_settings:
|
|
2272
|
+
lld_min_height_for_tip_attached = lld_settings[f"t{tip_volume}"][
|
|
2273
|
+
"minHeight"
|
|
2274
|
+
]
|
|
2275
|
+
return lld_min_height_for_tip_attached
|
|
2276
|
+
else:
|
|
2277
|
+
raise ValueError("liquid-level detection settings not found.")
|
|
2278
|
+
|
|
1063
2279
|
def liquid_probe_with_recovery(self, well_core: WellCore, loc: Location) -> None:
|
|
1064
2280
|
labware_id = well_core.labware_id
|
|
1065
2281
|
well_name = well_core.get_name()
|
|
2282
|
+
offset = LIQUID_PROBE_START_OFFSET_FROM_WELL_TOP
|
|
1066
2283
|
well_location = WellLocation(
|
|
1067
|
-
origin=WellOrigin.TOP, offset=WellOffset(x=
|
|
2284
|
+
origin=WellOrigin.TOP, offset=WellOffset(x=offset.x, y=offset.y, z=offset.z)
|
|
2285
|
+
)
|
|
2286
|
+
pipette_movement_conflict.check_safe_for_pipette_movement(
|
|
2287
|
+
engine_state=self._engine_client.state,
|
|
2288
|
+
pipette_id=self._pipette_id,
|
|
2289
|
+
labware_id=labware_id,
|
|
2290
|
+
well_name=well_name,
|
|
2291
|
+
well_location=well_location,
|
|
1068
2292
|
)
|
|
1069
2293
|
self._engine_client.execute_command(
|
|
1070
2294
|
cmd.LiquidProbeParams(
|
|
@@ -1077,14 +2301,22 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
1077
2301
|
|
|
1078
2302
|
self._protocol_core.set_last_location(location=loc, mount=self.get_mount())
|
|
1079
2303
|
|
|
2304
|
+
# TODO(cm, 3.4.25): decide whether to allow users to try and do math on a potential SimulatedProbeResult
|
|
1080
2305
|
def liquid_probe_without_recovery(
|
|
1081
2306
|
self, well_core: WellCore, loc: Location
|
|
1082
|
-
) ->
|
|
2307
|
+
) -> LiquidTrackingType:
|
|
1083
2308
|
labware_id = well_core.labware_id
|
|
1084
2309
|
well_name = well_core.get_name()
|
|
1085
2310
|
well_location = WellLocation(
|
|
1086
2311
|
origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=2)
|
|
1087
2312
|
)
|
|
2313
|
+
pipette_movement_conflict.check_safe_for_pipette_movement(
|
|
2314
|
+
engine_state=self._engine_client.state,
|
|
2315
|
+
pipette_id=self._pipette_id,
|
|
2316
|
+
labware_id=labware_id,
|
|
2317
|
+
well_name=well_name,
|
|
2318
|
+
well_location=well_location,
|
|
2319
|
+
)
|
|
1088
2320
|
result = self._engine_client.execute_command_without_recovery(
|
|
1089
2321
|
cmd.LiquidProbeParams(
|
|
1090
2322
|
labwareId=labware_id,
|
|
@@ -1095,7 +2327,6 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
1095
2327
|
)
|
|
1096
2328
|
|
|
1097
2329
|
self._protocol_core.set_last_location(location=loc, mount=self.get_mount())
|
|
1098
|
-
|
|
1099
2330
|
return result.z_position
|
|
1100
2331
|
|
|
1101
2332
|
def nozzle_configuration_valid_for_lld(self) -> bool:
|
|
@@ -1103,3 +2334,21 @@ class InstrumentCore(AbstractInstrument[WellCore]):
|
|
|
1103
2334
|
return self._engine_client.state.pipettes.get_nozzle_configuration_supports_lld(
|
|
1104
2335
|
self.pipette_id
|
|
1105
2336
|
)
|
|
2337
|
+
|
|
2338
|
+
def delay(self, seconds: float) -> None:
|
|
2339
|
+
"""Call a protocol delay."""
|
|
2340
|
+
self._protocol_core.delay(seconds=seconds, msg=None)
|
|
2341
|
+
|
|
2342
|
+
@contextmanager
|
|
2343
|
+
def lpd_for_transfer(self, enable: bool) -> Generator[None, None, None]:
|
|
2344
|
+
"""Context manager for the instrument's LPD state during a transfer."""
|
|
2345
|
+
global_lpd_enabled = self.get_liquid_presence_detection()
|
|
2346
|
+
self.set_liquid_presence_detection(enable=enable)
|
|
2347
|
+
yield
|
|
2348
|
+
self.set_liquid_presence_detection(enable=global_lpd_enabled)
|
|
2349
|
+
|
|
2350
|
+
|
|
2351
|
+
class _TipInfo(NamedTuple):
|
|
2352
|
+
tiprack_location: Location
|
|
2353
|
+
tiprack_uri: str
|
|
2354
|
+
tip_well: WellCore
|