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