opentrons 8.4.1a1__py2.py3-none-any.whl → 8.5.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opentrons/config/defaults_ot3.py +1 -1
- opentrons/hardware_control/backends/flex_protocol.py +25 -0
- opentrons/hardware_control/backends/ot3controller.py +76 -1
- opentrons/hardware_control/backends/ot3simulator.py +27 -0
- opentrons/hardware_control/instruments/ot3/pipette_handler.py +1 -0
- opentrons/hardware_control/ot3api.py +32 -0
- opentrons/legacy_commands/commands.py +16 -4
- opentrons/legacy_commands/robot_commands.py +51 -0
- opentrons/legacy_commands/types.py +91 -2
- opentrons/protocol_api/_liquid.py +60 -15
- opentrons/protocol_api/_liquid_properties.py +149 -90
- opentrons/protocol_api/_transfer_liquid_validation.py +43 -14
- opentrons/protocol_api/core/engine/instrument.py +367 -221
- opentrons/protocol_api/core/engine/protocol.py +14 -15
- opentrons/protocol_api/core/engine/robot.py +2 -2
- opentrons/protocol_api/core/engine/transfer_components_executor.py +275 -163
- opentrons/protocol_api/core/engine/well.py +16 -0
- opentrons/protocol_api/core/instrument.py +11 -5
- opentrons/protocol_api/core/legacy/legacy_instrument_core.py +11 -5
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +2 -2
- opentrons/protocol_api/core/legacy/legacy_well_core.py +8 -0
- opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +11 -5
- opentrons/protocol_api/core/protocol.py +3 -3
- opentrons/protocol_api/core/well.py +8 -0
- opentrons/protocol_api/instrument_context.py +478 -111
- opentrons/protocol_api/labware.py +10 -0
- opentrons/protocol_api/module_contexts.py +5 -2
- opentrons/protocol_api/protocol_context.py +76 -11
- opentrons/protocol_api/robot_context.py +48 -6
- opentrons/protocol_api/validation.py +15 -8
- opentrons/protocol_engine/commands/command_unions.py +10 -10
- opentrons/protocol_engine/commands/generate_command_schema.py +1 -1
- opentrons/protocol_engine/commands/get_next_tip.py +2 -2
- opentrons/protocol_engine/commands/load_labware.py +0 -19
- opentrons/protocol_engine/commands/pick_up_tip.py +9 -3
- opentrons/protocol_engine/commands/robot/__init__.py +20 -20
- opentrons/protocol_engine/commands/robot/close_gripper_jaw.py +34 -24
- opentrons/protocol_engine/commands/robot/open_gripper_jaw.py +29 -20
- opentrons/protocol_engine/commands/seal_pipette_to_tip.py +1 -1
- opentrons/protocol_engine/commands/unsafe/__init__.py +17 -1
- opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +1 -2
- opentrons/protocol_engine/execution/labware_movement.py +9 -2
- opentrons/protocol_engine/execution/movement.py +12 -9
- opentrons/protocol_engine/execution/queue_worker.py +8 -1
- opentrons/protocol_engine/execution/thermocycler_movement_flagger.py +52 -19
- opentrons/protocol_engine/resources/labware_validation.py +7 -1
- opentrons/protocol_engine/state/_well_math.py +2 -2
- opentrons/protocol_engine/state/commands.py +14 -28
- opentrons/protocol_engine/state/frustum_helpers.py +11 -7
- opentrons/protocol_engine/state/labware.py +12 -0
- opentrons/protocol_engine/state/modules.py +1 -1
- opentrons/protocol_engine/state/pipettes.py +8 -0
- opentrons/protocol_engine/state/tips.py +46 -83
- opentrons/protocol_engine/state/update_types.py +8 -23
- opentrons/protocol_engine/types/liquid_level_detection.py +68 -8
- opentrons/protocol_runner/legacy_command_mapper.py +12 -6
- opentrons/protocol_runner/run_orchestrator.py +1 -1
- opentrons/protocols/advanced_control/transfers/common.py +54 -11
- opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +55 -28
- opentrons/protocols/api_support/definitions.py +1 -1
- opentrons/types.py +6 -6
- {opentrons-8.4.1a1.dist-info → opentrons-8.5.0.dist-info}/METADATA +4 -4
- {opentrons-8.4.1a1.dist-info → opentrons-8.5.0.dist-info}/RECORD +67 -66
- {opentrons-8.4.1a1.dist-info → opentrons-8.5.0.dist-info}/LICENSE +0 -0
- {opentrons-8.4.1a1.dist-info → opentrons-8.5.0.dist-info}/WHEEL +0 -0
- {opentrons-8.4.1a1.dist-info → opentrons-8.5.0.dist-info}/entry_points.txt +0 -0
- {opentrons-8.4.1a1.dist-info → opentrons-8.5.0.dist-info}/top_level.txt +0 -0
|
@@ -12,7 +12,6 @@ from opentrons_shared_data.liquid_classes.liquid_class_definition import (
|
|
|
12
12
|
Coordinate,
|
|
13
13
|
BlowoutLocation,
|
|
14
14
|
)
|
|
15
|
-
from opentrons_shared_data.pipette.types import LIQUID_PROBE_START_OFFSET_FROM_WELL_TOP
|
|
16
15
|
|
|
17
16
|
from opentrons.protocol_api._liquid_properties import (
|
|
18
17
|
Submerge,
|
|
@@ -23,9 +22,10 @@ from opentrons.protocol_api._liquid_properties import (
|
|
|
23
22
|
TouchTipProperties,
|
|
24
23
|
)
|
|
25
24
|
from opentrons.protocol_engine.errors import TouchTipDisabledError
|
|
26
|
-
from opentrons.types import Location, Point
|
|
25
|
+
from opentrons.types import Location, Point, Mount
|
|
27
26
|
from opentrons.protocols.advanced_control.transfers.transfer_liquid_utils import (
|
|
28
27
|
LocationCheckDescriptors,
|
|
28
|
+
check_current_volume_before_dispensing,
|
|
29
29
|
)
|
|
30
30
|
from opentrons.protocols.advanced_control.transfers import (
|
|
31
31
|
transfer_liquid_utils as tx_utils,
|
|
@@ -39,6 +39,9 @@ if TYPE_CHECKING:
|
|
|
39
39
|
log = logging.getLogger(__name__)
|
|
40
40
|
|
|
41
41
|
|
|
42
|
+
AIR_GAP_LOC_Z_OFFSET_FROM_WELL_TOP = 2
|
|
43
|
+
|
|
44
|
+
|
|
42
45
|
@dataclass
|
|
43
46
|
class LiquidAndAirGapPair:
|
|
44
47
|
"""Pairing of a liquid and air gap in a tip, with air gap below the liquid in a tip."""
|
|
@@ -119,11 +122,32 @@ class TransferComponentsExecutor:
|
|
|
119
122
|
self,
|
|
120
123
|
instrument_core: InstrumentCore,
|
|
121
124
|
transfer_properties: TransferProperties,
|
|
122
|
-
target_location: Location,
|
|
123
|
-
target_well: WellCore,
|
|
125
|
+
target_location: Union[Location, TrashBin, WasteChute],
|
|
126
|
+
target_well: Optional[WellCore],
|
|
124
127
|
tip_state: TipState,
|
|
125
128
|
transfer_type: TransferType,
|
|
126
129
|
) -> None:
|
|
130
|
+
"""Create a TransferComponentsExecutor instance.
|
|
131
|
+
|
|
132
|
+
One instance should be created to execute all the steps inside each of the
|
|
133
|
+
liquid class' transfer components- aspirate, dispense and multi-dispense.
|
|
134
|
+
The state of the TransferComponentsExecutor instance is expected to be valid
|
|
135
|
+
only for the component it was created.
|
|
136
|
+
|
|
137
|
+
For example, if we want to execute all the steps (submerge, dispense, retract, etc)
|
|
138
|
+
related to the 'dispense' component of a liquid-class based transfer, the class
|
|
139
|
+
will be used to initialize info about the dispense by assigning values
|
|
140
|
+
to class attributes as follows-
|
|
141
|
+
- target_location: the dispense location
|
|
142
|
+
- target_well: the well associated with dispense location, will be None when the
|
|
143
|
+
target_location argument is a TrashBin or WasteChute
|
|
144
|
+
- tip_state: the state of the tip before dispense component steps are executed
|
|
145
|
+
- transfer_type: whether the dispense component is being called as a part of a
|
|
146
|
+
1-to-1 transfer or a consolidation or a distribution
|
|
147
|
+
|
|
148
|
+
These attributes will remain the same throughout the component's execution,
|
|
149
|
+
except `tip_state`, which will keep updating as fluids are handled.
|
|
150
|
+
"""
|
|
127
151
|
self._instrument = instrument_core
|
|
128
152
|
self._transfer_properties = transfer_properties
|
|
129
153
|
self._target_location = target_location
|
|
@@ -140,7 +164,6 @@ class TransferComponentsExecutor:
|
|
|
140
164
|
self,
|
|
141
165
|
submerge_properties: Submerge,
|
|
142
166
|
post_submerge_action: Literal["aspirate", "dispense"],
|
|
143
|
-
volume_for_pipette_mode_configuration: Optional[float],
|
|
144
167
|
) -> None:
|
|
145
168
|
"""Execute submerge steps.
|
|
146
169
|
|
|
@@ -148,56 +171,37 @@ class TransferComponentsExecutor:
|
|
|
148
171
|
Should raise an error if this point is inside the liquid?
|
|
149
172
|
For liquid meniscus this is easy to tell. Can’t be below meniscus
|
|
150
173
|
For reference pos of anything else, do not allow submerge position to be below aspirate position
|
|
151
|
-
2. move to aspirate position at desired speed
|
|
174
|
+
2. move to aspirate/dispense position at desired speed
|
|
152
175
|
3. delay
|
|
176
|
+
|
|
177
|
+
If target location is a trash bin or waste chute, the pipette will move to the disposal location given,
|
|
178
|
+
remove air gap and delay
|
|
153
179
|
"""
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
location=Location(
|
|
170
|
-
point=self._target_well.get_top(
|
|
171
|
-
LIQUID_PROBE_START_OFFSET_FROM_WELL_TOP.z
|
|
172
|
-
),
|
|
173
|
-
labware=self._target_location.labware,
|
|
174
|
-
),
|
|
180
|
+
submerge_start_location: Union[Location, TrashBin, WasteChute]
|
|
181
|
+
if isinstance(self._target_location, Location):
|
|
182
|
+
assert self._target_well is not None
|
|
183
|
+
submerge_start_point = absolute_point_from_position_reference_and_offset(
|
|
184
|
+
well=self._target_well,
|
|
185
|
+
well_volume_difference=0,
|
|
186
|
+
position_reference=submerge_properties.start_position.position_reference,
|
|
187
|
+
offset=submerge_properties.start_position.offset,
|
|
188
|
+
mount=self._instrument.get_mount(),
|
|
189
|
+
)
|
|
190
|
+
submerge_start_location = Location(
|
|
191
|
+
point=submerge_start_point, labware=self._target_location.labware
|
|
192
|
+
)
|
|
193
|
+
tx_utils.raise_if_location_inside_liquid(
|
|
194
|
+
location=submerge_start_location,
|
|
175
195
|
well_core=self._target_well,
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
196
|
+
location_check_descriptors=LocationCheckDescriptors(
|
|
197
|
+
location_type="submerge start",
|
|
198
|
+
pipetting_action=post_submerge_action,
|
|
199
|
+
),
|
|
200
|
+
logger=log,
|
|
179
201
|
)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
and self._instrument.get_liquid_presence_detection()
|
|
184
|
-
):
|
|
185
|
-
self._instrument.liquid_probe_with_recovery(
|
|
186
|
-
well_core=self._target_well, loc=submerge_start_location
|
|
187
|
-
)
|
|
188
|
-
# TODO: do volume configuration + prepare for aspirate only if the mode needs to be changed
|
|
189
|
-
self._instrument.configure_for_volume(volume_for_pipette_mode_configuration) # type: ignore[arg-type]
|
|
190
|
-
self._instrument.prepare_to_aspirate()
|
|
191
|
-
tx_utils.raise_if_location_inside_liquid(
|
|
192
|
-
location=submerge_start_location,
|
|
193
|
-
well_location=self._target_location,
|
|
194
|
-
well_core=self._target_well,
|
|
195
|
-
location_check_descriptors=LocationCheckDescriptors(
|
|
196
|
-
location_type="submerge start",
|
|
197
|
-
pipetting_action=post_submerge_action,
|
|
198
|
-
),
|
|
199
|
-
logger=log,
|
|
200
|
-
)
|
|
202
|
+
else:
|
|
203
|
+
submerge_start_location = self._target_location
|
|
204
|
+
|
|
201
205
|
self._instrument.move_to(
|
|
202
206
|
location=submerge_start_location,
|
|
203
207
|
well_core=self._target_well,
|
|
@@ -205,23 +209,30 @@ class TransferComponentsExecutor:
|
|
|
205
209
|
minimum_z_height=None,
|
|
206
210
|
speed=None,
|
|
207
211
|
)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
212
|
+
self._remove_air_gap(location=submerge_start_location)
|
|
213
|
+
if isinstance(self._target_location, Location):
|
|
214
|
+
self._instrument.move_to(
|
|
215
|
+
location=self._target_location,
|
|
216
|
+
well_core=self._target_well,
|
|
217
|
+
force_direct=True,
|
|
218
|
+
minimum_z_height=None,
|
|
219
|
+
speed=submerge_properties.speed,
|
|
220
|
+
)
|
|
221
|
+
|
|
217
222
|
if submerge_properties.delay.enabled and submerge_properties.delay.duration:
|
|
218
223
|
self._instrument.delay(submerge_properties.delay.duration)
|
|
219
224
|
|
|
220
225
|
def aspirate_and_wait(self, volume: float) -> None:
|
|
221
226
|
"""Aspirate according to aspirate properties and wait if enabled."""
|
|
222
227
|
# TODO: handle volume correction
|
|
228
|
+
assert (
|
|
229
|
+
isinstance(self._target_location, Location)
|
|
230
|
+
and self._target_well is not None
|
|
231
|
+
)
|
|
223
232
|
aspirate_props = self._transfer_properties.aspirate
|
|
224
|
-
correction_volume = aspirate_props.correction_by_volume.get_for_volume(
|
|
233
|
+
correction_volume = aspirate_props.correction_by_volume.get_for_volume(
|
|
234
|
+
self._instrument.get_current_volume() + volume
|
|
235
|
+
)
|
|
225
236
|
self._instrument.aspirate(
|
|
226
237
|
location=self._target_location,
|
|
227
238
|
well_core=None,
|
|
@@ -243,8 +254,12 @@ class TransferComponentsExecutor:
|
|
|
243
254
|
push_out_override: Optional[float],
|
|
244
255
|
) -> None:
|
|
245
256
|
"""Dispense according to dispense properties and wait if enabled."""
|
|
257
|
+
current_vol = self._instrument.get_current_volume()
|
|
258
|
+
check_current_volume_before_dispensing(
|
|
259
|
+
current_volume=current_vol, dispense_volume=volume
|
|
260
|
+
)
|
|
246
261
|
correction_volume = dispense_properties.correction_by_volume.get_for_volume(
|
|
247
|
-
volume
|
|
262
|
+
current_vol - volume
|
|
248
263
|
)
|
|
249
264
|
self._instrument.dispense(
|
|
250
265
|
location=self._target_location,
|
|
@@ -275,11 +290,15 @@ class TransferComponentsExecutor:
|
|
|
275
290
|
NOTE: For most of our built-in definitions, we will keep _mix_ off because it is a very application specific thing.
|
|
276
291
|
We should mention in our docs that users should adjust this property according to their application.
|
|
277
292
|
"""
|
|
278
|
-
if not mix_properties.enabled
|
|
293
|
+
if not mix_properties.enabled or not isinstance(
|
|
294
|
+
self._target_location, Location
|
|
295
|
+
):
|
|
279
296
|
return
|
|
280
297
|
# Assertion only for mypy purposes
|
|
281
298
|
assert (
|
|
282
|
-
mix_properties.repetitions is not None
|
|
299
|
+
mix_properties.repetitions is not None
|
|
300
|
+
and mix_properties.volume is not None
|
|
301
|
+
and self._target_well is not None
|
|
283
302
|
)
|
|
284
303
|
push_out_vol = (
|
|
285
304
|
self._transfer_properties.dispense.push_out_by_volume.get_for_volume(
|
|
@@ -325,30 +344,40 @@ class TransferComponentsExecutor:
|
|
|
325
344
|
- Touch tip to the sides at the specified speed (tip moves back to the center as part of touch tip)
|
|
326
345
|
- Return back to the retract position
|
|
327
346
|
4. Air gap
|
|
328
|
-
-
|
|
329
|
-
|
|
330
|
-
|
|
347
|
+
- If the retract location is at or above the safe location of
|
|
348
|
+
AIR_GAP_LOC_Z_OFFSET_FROM_WELL_TOP, then add the air gap at the
|
|
349
|
+
retract location (where the pipette is already assumed to be).
|
|
350
|
+
- If the retract location is below the safe location, then move to
|
|
351
|
+
the safe location and then add the air gap.
|
|
352
|
+
- Air gap volume depends on the amount of liquid in the pipette.
|
|
353
|
+
So, if the total aspirated volume is 20, use the value for airGapByVolume[20]
|
|
354
|
+
Flow rate = max(aspirateFlowRate, (airGapByVolume)/sec)
|
|
331
355
|
- Use post-aspirate delay
|
|
332
356
|
|
|
333
357
|
Args:
|
|
334
358
|
volume: dispense volume
|
|
335
359
|
add_air_gap: whether to add an air gap before moving away from the current well.
|
|
336
360
|
This value is True for all retractions, except when retracting
|
|
337
|
-
during a multi-dispense.
|
|
361
|
+
during a multi-dispense. Value of add_air_gap during multi-dispense
|
|
362
|
+
will depend on whether a conditioning volume is used.
|
|
338
363
|
"""
|
|
339
|
-
|
|
364
|
+
assert (
|
|
365
|
+
isinstance(self._target_location, Location)
|
|
366
|
+
and self._target_well is not None
|
|
367
|
+
)
|
|
340
368
|
retract_props = self._transfer_properties.aspirate.retract
|
|
341
369
|
retract_point = absolute_point_from_position_reference_and_offset(
|
|
342
370
|
well=self._target_well,
|
|
343
|
-
|
|
344
|
-
|
|
371
|
+
well_volume_difference=0,
|
|
372
|
+
position_reference=retract_props.end_position.position_reference,
|
|
373
|
+
offset=retract_props.end_position.offset,
|
|
374
|
+
mount=self._instrument.get_mount(),
|
|
345
375
|
)
|
|
346
376
|
retract_location = Location(
|
|
347
377
|
retract_point, labware=self._target_location.labware
|
|
348
378
|
)
|
|
349
379
|
tx_utils.raise_if_location_inside_liquid(
|
|
350
380
|
location=retract_location,
|
|
351
|
-
well_location=self._target_location,
|
|
352
381
|
well_core=self._target_well,
|
|
353
382
|
location_check_descriptors=LocationCheckDescriptors(
|
|
354
383
|
location_type="retract end",
|
|
@@ -371,7 +400,7 @@ class TransferComponentsExecutor:
|
|
|
371
400
|
assert (
|
|
372
401
|
touch_tip_props.speed is not None
|
|
373
402
|
and touch_tip_props.z_offset is not None
|
|
374
|
-
and touch_tip_props.
|
|
403
|
+
and touch_tip_props.mm_from_edge is not None
|
|
375
404
|
)
|
|
376
405
|
self._instrument.touch_tip(
|
|
377
406
|
location=retract_location,
|
|
@@ -379,7 +408,7 @@ class TransferComponentsExecutor:
|
|
|
379
408
|
radius=1,
|
|
380
409
|
z_offset=touch_tip_props.z_offset,
|
|
381
410
|
speed=touch_tip_props.speed,
|
|
382
|
-
mm_from_edge=touch_tip_props.
|
|
411
|
+
mm_from_edge=touch_tip_props.mm_from_edge,
|
|
383
412
|
)
|
|
384
413
|
self._instrument.move_to(
|
|
385
414
|
location=retract_location,
|
|
@@ -396,6 +425,29 @@ class TransferComponentsExecutor:
|
|
|
396
425
|
else:
|
|
397
426
|
volume_for_air_gap = volume
|
|
398
427
|
if add_air_gap:
|
|
428
|
+
# If we need to add air gap, move to a safe location above the well if
|
|
429
|
+
# the retract location is not already at or above this safe location
|
|
430
|
+
if (
|
|
431
|
+
retract_location.point.z
|
|
432
|
+
< self._target_well.get_top(AIR_GAP_LOC_Z_OFFSET_FROM_WELL_TOP).z
|
|
433
|
+
):
|
|
434
|
+
self._instrument.move_to(
|
|
435
|
+
location=Location(
|
|
436
|
+
point=Point(
|
|
437
|
+
retract_location.point.x,
|
|
438
|
+
retract_location.point.y,
|
|
439
|
+
self._target_well.get_top(
|
|
440
|
+
AIR_GAP_LOC_Z_OFFSET_FROM_WELL_TOP
|
|
441
|
+
).z,
|
|
442
|
+
),
|
|
443
|
+
labware=retract_location.labware,
|
|
444
|
+
),
|
|
445
|
+
well_core=self._target_well,
|
|
446
|
+
force_direct=True,
|
|
447
|
+
minimum_z_height=None,
|
|
448
|
+
# Full speed because the tip will already be out of the liquid
|
|
449
|
+
speed=None,
|
|
450
|
+
)
|
|
399
451
|
self._add_air_gap(
|
|
400
452
|
air_gap_volume=self._transfer_properties.aspirate.retract.air_gap_by_volume.get_for_volume(
|
|
401
453
|
volume_for_air_gap
|
|
@@ -431,35 +483,45 @@ class TransferComponentsExecutor:
|
|
|
431
483
|
- Prepare-to-aspirate (top of well)
|
|
432
484
|
- Do air-gap (top of well)
|
|
433
485
|
7. If drop tip, move to drop tip location, drop tip
|
|
434
|
-
"""
|
|
435
|
-
# TODO: Raise error if retract is below the meniscus
|
|
436
486
|
|
|
487
|
+
If target location is a trash bin or waste chute, the retract movement step is skipped along with touch tip,
|
|
488
|
+
even if it is enabled.
|
|
489
|
+
"""
|
|
437
490
|
retract_props = self._transfer_properties.dispense.retract
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
491
|
+
|
|
492
|
+
retract_location: Union[Location, TrashBin, WasteChute]
|
|
493
|
+
if isinstance(self._target_location, Location):
|
|
494
|
+
assert self._target_well is not None
|
|
495
|
+
retract_point = absolute_point_from_position_reference_and_offset(
|
|
496
|
+
well=self._target_well,
|
|
497
|
+
well_volume_difference=0,
|
|
498
|
+
position_reference=retract_props.end_position.position_reference,
|
|
499
|
+
offset=retract_props.end_position.offset,
|
|
500
|
+
mount=self._instrument.get_mount(),
|
|
501
|
+
)
|
|
502
|
+
retract_location = Location(
|
|
503
|
+
retract_point, labware=self._target_location.labware
|
|
504
|
+
)
|
|
505
|
+
tx_utils.raise_if_location_inside_liquid(
|
|
506
|
+
location=retract_location,
|
|
507
|
+
well_core=self._target_well,
|
|
508
|
+
location_check_descriptors=LocationCheckDescriptors(
|
|
509
|
+
location_type="retract end",
|
|
510
|
+
pipetting_action="dispense",
|
|
511
|
+
),
|
|
512
|
+
logger=log,
|
|
513
|
+
)
|
|
514
|
+
self._instrument.move_to(
|
|
515
|
+
location=retract_location,
|
|
516
|
+
well_core=self._target_well,
|
|
517
|
+
force_direct=True,
|
|
518
|
+
minimum_z_height=None,
|
|
519
|
+
speed=retract_props.speed,
|
|
520
|
+
)
|
|
521
|
+
else:
|
|
522
|
+
retract_location = self._target_location
|
|
523
|
+
|
|
524
|
+
# TODO should we delay here for a trash despite not having a "retract"?
|
|
463
525
|
retract_delay = retract_props.delay
|
|
464
526
|
if retract_delay.enabled and retract_delay.duration:
|
|
465
527
|
self._instrument.delay(retract_delay.duration)
|
|
@@ -485,7 +547,7 @@ class TransferComponentsExecutor:
|
|
|
485
547
|
# when leaving the dispense well. If this will be the final air gap, i.e,
|
|
486
548
|
# we won't be moving to a Trash or a Source for Blowout after this air gap,
|
|
487
549
|
# then skip the final air gap if we have been told to do so.
|
|
488
|
-
self.
|
|
550
|
+
self._do_touch_tip_and_air_gap_after_dispense(
|
|
489
551
|
touch_tip_properties=retract_props.touch_tip,
|
|
490
552
|
location=retract_location,
|
|
491
553
|
well=self._target_well,
|
|
@@ -499,7 +561,7 @@ class TransferComponentsExecutor:
|
|
|
499
561
|
# TODO: no-op touch tip if touch tip is enabled and blowout is in trash/ reservoir/ any labware with touch-tip disabled
|
|
500
562
|
assert blowout_props.flow_rate is not None
|
|
501
563
|
self._instrument.set_flow_rate(blow_out=blowout_props.flow_rate)
|
|
502
|
-
touch_tip_and_air_gap_location:
|
|
564
|
+
touch_tip_and_air_gap_location: Union[Location, TrashBin, WasteChute]
|
|
503
565
|
if blowout_props.location == BlowoutLocation.SOURCE:
|
|
504
566
|
if source_location is None or source_well is None:
|
|
505
567
|
raise RuntimeError(
|
|
@@ -523,9 +585,7 @@ class TransferComponentsExecutor:
|
|
|
523
585
|
well_core=None,
|
|
524
586
|
in_place=False,
|
|
525
587
|
)
|
|
526
|
-
touch_tip_and_air_gap_location =
|
|
527
|
-
trash_location if isinstance(trash_location, Location) else None
|
|
528
|
-
)
|
|
588
|
+
touch_tip_and_air_gap_location = trash_location
|
|
529
589
|
touch_tip_and_air_gap_well = (
|
|
530
590
|
# We have already established that trash location of `Location` type
|
|
531
591
|
# has its `labware` as `Well` type.
|
|
@@ -540,7 +600,7 @@ class TransferComponentsExecutor:
|
|
|
540
600
|
self._tip_state.delete_air_gap(last_air_gap)
|
|
541
601
|
self._tip_state.ready_to_aspirate = False
|
|
542
602
|
# Do touch tip and air gap again after blowing out into source well or trash
|
|
543
|
-
self.
|
|
603
|
+
self._do_touch_tip_and_air_gap_after_dispense(
|
|
544
604
|
touch_tip_properties=retract_props.touch_tip,
|
|
545
605
|
location=touch_tip_and_air_gap_location,
|
|
546
606
|
well=touch_tip_and_air_gap_well,
|
|
@@ -572,22 +632,25 @@ class TransferComponentsExecutor:
|
|
|
572
632
|
that it handles air gaps differently based on the disposal volume, conditioning volume
|
|
573
633
|
and whether we are moving to another dispense or going back to the source.
|
|
574
634
|
"""
|
|
575
|
-
|
|
576
|
-
|
|
635
|
+
assert (
|
|
636
|
+
isinstance(self._target_location, Location)
|
|
637
|
+
and self._target_well is not None
|
|
638
|
+
)
|
|
577
639
|
assert self._transfer_properties.multi_dispense is not None
|
|
578
640
|
|
|
579
641
|
retract_props = self._transfer_properties.multi_dispense.retract
|
|
580
642
|
retract_point = absolute_point_from_position_reference_and_offset(
|
|
581
643
|
well=self._target_well,
|
|
582
|
-
|
|
583
|
-
|
|
644
|
+
well_volume_difference=0,
|
|
645
|
+
position_reference=retract_props.end_position.position_reference,
|
|
646
|
+
offset=retract_props.end_position.offset,
|
|
647
|
+
mount=self._instrument.get_mount(),
|
|
584
648
|
)
|
|
585
649
|
retract_location = Location(
|
|
586
650
|
retract_point, labware=self._target_location.labware
|
|
587
651
|
)
|
|
588
652
|
tx_utils.raise_if_location_inside_liquid(
|
|
589
653
|
location=retract_location,
|
|
590
|
-
well_location=self._target_location,
|
|
591
654
|
well_core=self._target_well,
|
|
592
655
|
location_check_descriptors=LocationCheckDescriptors(
|
|
593
656
|
location_type="retract end",
|
|
@@ -662,7 +725,7 @@ class TransferComponentsExecutor:
|
|
|
662
725
|
# Add an air gap depending on conditioning volume + whether this is
|
|
663
726
|
# the last step of a multi-dispense sequence + whether this is the last step
|
|
664
727
|
# of the entire liquid distribution.
|
|
665
|
-
self.
|
|
728
|
+
self._do_touch_tip_and_air_gap_after_dispense(
|
|
666
729
|
touch_tip_properties=retract_props.touch_tip,
|
|
667
730
|
location=retract_location,
|
|
668
731
|
well=self._target_well,
|
|
@@ -676,7 +739,7 @@ class TransferComponentsExecutor:
|
|
|
676
739
|
):
|
|
677
740
|
assert blowout_props.flow_rate is not None
|
|
678
741
|
self._instrument.set_flow_rate(blow_out=blowout_props.flow_rate)
|
|
679
|
-
touch_tip_and_air_gap_location:
|
|
742
|
+
touch_tip_and_air_gap_location: Union[Location, TrashBin, WasteChute]
|
|
680
743
|
if blowout_props.location == BlowoutLocation.SOURCE:
|
|
681
744
|
if source_location is None or source_well is None:
|
|
682
745
|
raise RuntimeError(
|
|
@@ -700,9 +763,7 @@ class TransferComponentsExecutor:
|
|
|
700
763
|
well_core=None,
|
|
701
764
|
in_place=False,
|
|
702
765
|
)
|
|
703
|
-
touch_tip_and_air_gap_location =
|
|
704
|
-
trash_location if isinstance(trash_location, Location) else None
|
|
705
|
-
)
|
|
766
|
+
touch_tip_and_air_gap_location = trash_location
|
|
706
767
|
touch_tip_and_air_gap_well = (
|
|
707
768
|
# We have already established that trash location of `Location` type
|
|
708
769
|
# has its `labware` as `Well` type.
|
|
@@ -716,7 +777,7 @@ class TransferComponentsExecutor:
|
|
|
716
777
|
self._tip_state.ready_to_aspirate = False
|
|
717
778
|
|
|
718
779
|
# Do touch tip and air gap again after blowing out into source well or trash
|
|
719
|
-
self.
|
|
780
|
+
self._do_touch_tip_and_air_gap_after_dispense(
|
|
720
781
|
touch_tip_properties=retract_props.touch_tip,
|
|
721
782
|
location=touch_tip_and_air_gap_location,
|
|
722
783
|
well=touch_tip_and_air_gap_well,
|
|
@@ -728,24 +789,35 @@ class TransferComponentsExecutor:
|
|
|
728
789
|
),
|
|
729
790
|
)
|
|
730
791
|
|
|
731
|
-
def
|
|
792
|
+
def _do_touch_tip_and_air_gap_after_dispense( # noqa: C901
|
|
732
793
|
self,
|
|
733
794
|
touch_tip_properties: TouchTipProperties,
|
|
734
|
-
location:
|
|
795
|
+
location: Union[Location, TrashBin, WasteChute],
|
|
735
796
|
well: Optional[WellCore],
|
|
736
797
|
add_air_gap: bool,
|
|
737
798
|
) -> None:
|
|
738
|
-
"""Perform touch tip and air gap as part of post-dispense retract.
|
|
799
|
+
"""Perform touch tip and air gap as part of post-dispense retract.
|
|
800
|
+
|
|
801
|
+
If the retract location is at or above the safe location of
|
|
802
|
+
AIR_GAP_LOC_Z_OFFSET_FROM_WELL_TOP, then add the air gap at the retract location
|
|
803
|
+
(where the pipette is already assumed to be at).
|
|
804
|
+
|
|
805
|
+
If the retract location is below the safe location, then move to the safe location
|
|
806
|
+
and then add the air gap.
|
|
807
|
+
|
|
808
|
+
Note: if the plunger needs to be adjusted to prepare for aspirate, it will be done
|
|
809
|
+
at the same location where the air gap will be added.
|
|
810
|
+
"""
|
|
739
811
|
if touch_tip_properties.enabled:
|
|
740
812
|
assert (
|
|
741
813
|
touch_tip_properties.speed is not None
|
|
742
814
|
and touch_tip_properties.z_offset is not None
|
|
743
|
-
and touch_tip_properties.
|
|
815
|
+
and touch_tip_properties.mm_from_edge is not None
|
|
744
816
|
)
|
|
745
817
|
# TODO:, check that when blow out is a non-dest-well,
|
|
746
818
|
# whether the touch tip params from transfer props should be used for
|
|
747
819
|
# both dest-well touch tip and non-dest-well touch tip.
|
|
748
|
-
if
|
|
820
|
+
if isinstance(location, Location) and well is not None:
|
|
749
821
|
try:
|
|
750
822
|
self._instrument.touch_tip(
|
|
751
823
|
location=location,
|
|
@@ -753,7 +825,7 @@ class TransferComponentsExecutor:
|
|
|
753
825
|
radius=1,
|
|
754
826
|
z_offset=touch_tip_properties.z_offset,
|
|
755
827
|
speed=touch_tip_properties.speed,
|
|
756
|
-
mm_from_edge=touch_tip_properties.
|
|
828
|
+
mm_from_edge=touch_tip_properties.mm_from_edge,
|
|
757
829
|
)
|
|
758
830
|
except TouchTipDisabledError:
|
|
759
831
|
# TODO: log a warning
|
|
@@ -768,25 +840,69 @@ class TransferComponentsExecutor:
|
|
|
768
840
|
# Full speed because the tip will already be out of the liquid
|
|
769
841
|
speed=None,
|
|
770
842
|
)
|
|
843
|
+
if add_air_gap or not self._tip_state.ready_to_aspirate:
|
|
844
|
+
# If we need to move the plunger up either to prepare for aspirate or to add air gap,
|
|
845
|
+
# move to a safe location above the well if the retract location is not already
|
|
846
|
+
# at or above this safe location
|
|
847
|
+
if isinstance(location, Location):
|
|
848
|
+
assert well is not None # For mypy purposes only
|
|
849
|
+
if (
|
|
850
|
+
location.point.z
|
|
851
|
+
< well.get_top(AIR_GAP_LOC_Z_OFFSET_FROM_WELL_TOP).z
|
|
852
|
+
):
|
|
853
|
+
self._instrument.move_to(
|
|
854
|
+
location=Location(
|
|
855
|
+
point=Point(
|
|
856
|
+
location.point.x,
|
|
857
|
+
location.point.y,
|
|
858
|
+
well.get_top(AIR_GAP_LOC_Z_OFFSET_FROM_WELL_TOP).z,
|
|
859
|
+
),
|
|
860
|
+
labware=location.labware,
|
|
861
|
+
),
|
|
862
|
+
well_core=well,
|
|
863
|
+
force_direct=True,
|
|
864
|
+
minimum_z_height=None,
|
|
865
|
+
speed=None,
|
|
866
|
+
)
|
|
867
|
+
else:
|
|
868
|
+
if (
|
|
869
|
+
location.offset.z
|
|
870
|
+
< location.top(
|
|
871
|
+
x=0, y=0, z=AIR_GAP_LOC_Z_OFFSET_FROM_WELL_TOP
|
|
872
|
+
).offset.z
|
|
873
|
+
):
|
|
874
|
+
self._instrument.move_to(
|
|
875
|
+
location=location.top(
|
|
876
|
+
x=location.offset.x,
|
|
877
|
+
y=location.offset.y,
|
|
878
|
+
z=AIR_GAP_LOC_Z_OFFSET_FROM_WELL_TOP,
|
|
879
|
+
),
|
|
880
|
+
well_core=None,
|
|
881
|
+
force_direct=True,
|
|
882
|
+
minimum_z_height=None,
|
|
883
|
+
speed=None,
|
|
884
|
+
)
|
|
771
885
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
886
|
+
if not self._tip_state.ready_to_aspirate:
|
|
887
|
+
self._instrument.prepare_to_aspirate()
|
|
888
|
+
self._tip_state.ready_to_aspirate = True
|
|
889
|
+
if add_air_gap:
|
|
890
|
+
self._add_air_gap(
|
|
891
|
+
air_gap_volume=self._transfer_properties.aspirate.retract.air_gap_by_volume.get_for_volume(
|
|
892
|
+
0
|
|
893
|
+
)
|
|
780
894
|
)
|
|
781
|
-
)
|
|
782
895
|
|
|
783
|
-
def _add_air_gap(
|
|
896
|
+
def _add_air_gap(
|
|
897
|
+
self,
|
|
898
|
+
air_gap_volume: float,
|
|
899
|
+
) -> None:
|
|
784
900
|
"""Add an air gap."""
|
|
785
901
|
if air_gap_volume == 0:
|
|
786
902
|
return
|
|
787
903
|
aspirate_props = self._transfer_properties.aspirate
|
|
788
904
|
correction_volume = aspirate_props.correction_by_volume.get_for_volume(
|
|
789
|
-
air_gap_volume
|
|
905
|
+
self._instrument.get_current_volume() + air_gap_volume
|
|
790
906
|
)
|
|
791
907
|
# The minimum flow rate should be air_gap_volume per second
|
|
792
908
|
flow_rate = max(
|
|
@@ -803,43 +919,33 @@ class TransferComponentsExecutor:
|
|
|
803
919
|
self._instrument.delay(delay_props.duration)
|
|
804
920
|
self._tip_state.append_air_gap(air_gap_volume)
|
|
805
921
|
|
|
806
|
-
def _remove_air_gap(self, location: Location) -> None:
|
|
922
|
+
def _remove_air_gap(self, location: Union[Location, TrashBin, WasteChute]) -> None:
|
|
807
923
|
"""Remove a previously added air gap."""
|
|
808
924
|
last_air_gap = self._tip_state.last_liquid_and_air_gap_in_tip.air_gap
|
|
809
|
-
if last_air_gap == 0:
|
|
810
|
-
return
|
|
811
|
-
|
|
812
925
|
dispense_props = self._transfer_properties.dispense
|
|
813
|
-
|
|
814
|
-
last_air_gap
|
|
815
|
-
|
|
816
|
-
# The minimum flow rate should be air_gap_volume per second
|
|
817
|
-
flow_rate = max(
|
|
818
|
-
dispense_props.flow_rate_by_volume.get_for_volume(last_air_gap),
|
|
819
|
-
last_air_gap,
|
|
820
|
-
)
|
|
821
|
-
self._instrument.dispense(
|
|
926
|
+
self._instrument.remove_air_gap_during_transfer_with_liquid_class(
|
|
927
|
+
last_air_gap=last_air_gap,
|
|
928
|
+
dispense_props=dispense_props,
|
|
822
929
|
location=location,
|
|
823
|
-
well_core=None,
|
|
824
|
-
volume=last_air_gap,
|
|
825
|
-
rate=1,
|
|
826
|
-
flow_rate=flow_rate,
|
|
827
|
-
in_place=True,
|
|
828
|
-
push_out=0,
|
|
829
|
-
correction_volume=correction_volume,
|
|
830
930
|
)
|
|
831
931
|
self._tip_state.delete_air_gap(last_air_gap)
|
|
832
|
-
dispense_delay = dispense_props.delay
|
|
833
|
-
if dispense_delay.enabled and dispense_delay.duration:
|
|
834
|
-
self._instrument.delay(dispense_delay.duration)
|
|
835
932
|
|
|
836
933
|
|
|
837
934
|
def absolute_point_from_position_reference_and_offset(
|
|
838
935
|
well: WellCore,
|
|
936
|
+
well_volume_difference: float,
|
|
839
937
|
position_reference: PositionReference,
|
|
840
938
|
offset: Coordinate,
|
|
939
|
+
mount: Mount,
|
|
841
940
|
) -> Point:
|
|
842
|
-
"""Return the absolute point, given the well, the position reference and offset.
|
|
941
|
+
"""Return the absolute point, given the well, the position reference and offset.
|
|
942
|
+
|
|
943
|
+
If using meniscus as the position reference, well_volume_difference should be specified.
|
|
944
|
+
`well_volume_difference` is the expected *difference* in well volume we want to consider
|
|
945
|
+
when estimating the height of the liquid meniscus after an aspirate/ dispense.
|
|
946
|
+
So, for liquid height estimation after an aspirate, well_volume_difference is
|
|
947
|
+
expected to be a -ve value while for a dispense, it will be a +ve value.
|
|
948
|
+
"""
|
|
843
949
|
match position_reference:
|
|
844
950
|
case PositionReference.WELL_TOP:
|
|
845
951
|
reference_point = well.get_top(0)
|
|
@@ -848,11 +954,17 @@ def absolute_point_from_position_reference_and_offset(
|
|
|
848
954
|
case PositionReference.WELL_CENTER:
|
|
849
955
|
reference_point = well.get_center()
|
|
850
956
|
case PositionReference.LIQUID_MENISCUS:
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
957
|
+
estimated_liquid_height = well.estimate_liquid_height_after_pipetting(
|
|
958
|
+
mount=mount,
|
|
959
|
+
operation_volume=well_volume_difference,
|
|
960
|
+
)
|
|
961
|
+
if isinstance(estimated_liquid_height, (float, int)):
|
|
962
|
+
reference_point = well.get_bottom(z_offset=estimated_liquid_height)
|
|
854
963
|
else:
|
|
855
|
-
|
|
964
|
+
# If estimated liquid height gives a SimulatedProbeResult then
|
|
965
|
+
# assume meniscus is at well center.
|
|
966
|
+
# Will this cause more harm than good? Is there a better alternative to this?
|
|
967
|
+
reference_point = well.get_center()
|
|
856
968
|
case _:
|
|
857
969
|
raise ValueError(f"Unknown position reference {position_reference}")
|
|
858
970
|
return reference_point + Point(offset.x, offset.y, offset.z)
|