opentrons 8.4.1a2__py2.py3-none-any.whl → 8.5.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. opentrons/config/defaults_ot3.py +1 -1
  2. opentrons/hardware_control/backends/flex_protocol.py +25 -0
  3. opentrons/hardware_control/backends/ot3controller.py +76 -1
  4. opentrons/hardware_control/backends/ot3simulator.py +27 -0
  5. opentrons/hardware_control/instruments/ot3/pipette_handler.py +1 -0
  6. opentrons/hardware_control/ot3api.py +32 -0
  7. opentrons/legacy_commands/commands.py +16 -4
  8. opentrons/legacy_commands/robot_commands.py +51 -0
  9. opentrons/legacy_commands/types.py +91 -2
  10. opentrons/protocol_api/_liquid.py +60 -15
  11. opentrons/protocol_api/_liquid_properties.py +149 -90
  12. opentrons/protocol_api/_transfer_liquid_validation.py +43 -14
  13. opentrons/protocol_api/core/engine/instrument.py +367 -221
  14. opentrons/protocol_api/core/engine/protocol.py +14 -15
  15. opentrons/protocol_api/core/engine/robot.py +2 -2
  16. opentrons/protocol_api/core/engine/transfer_components_executor.py +275 -163
  17. opentrons/protocol_api/core/engine/well.py +16 -0
  18. opentrons/protocol_api/core/instrument.py +11 -5
  19. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +11 -5
  20. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +2 -2
  21. opentrons/protocol_api/core/legacy/legacy_well_core.py +8 -0
  22. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +11 -5
  23. opentrons/protocol_api/core/protocol.py +3 -3
  24. opentrons/protocol_api/core/well.py +8 -0
  25. opentrons/protocol_api/instrument_context.py +478 -111
  26. opentrons/protocol_api/labware.py +10 -0
  27. opentrons/protocol_api/module_contexts.py +5 -2
  28. opentrons/protocol_api/protocol_context.py +76 -11
  29. opentrons/protocol_api/robot_context.py +48 -6
  30. opentrons/protocol_api/validation.py +15 -8
  31. opentrons/protocol_engine/commands/command_unions.py +10 -10
  32. opentrons/protocol_engine/commands/generate_command_schema.py +1 -1
  33. opentrons/protocol_engine/commands/get_next_tip.py +2 -2
  34. opentrons/protocol_engine/commands/load_labware.py +0 -19
  35. opentrons/protocol_engine/commands/pick_up_tip.py +9 -3
  36. opentrons/protocol_engine/commands/robot/__init__.py +20 -20
  37. opentrons/protocol_engine/commands/robot/close_gripper_jaw.py +34 -24
  38. opentrons/protocol_engine/commands/robot/open_gripper_jaw.py +29 -20
  39. opentrons/protocol_engine/commands/seal_pipette_to_tip.py +1 -1
  40. opentrons/protocol_engine/commands/unsafe/__init__.py +17 -1
  41. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +1 -2
  42. opentrons/protocol_engine/execution/labware_movement.py +9 -2
  43. opentrons/protocol_engine/execution/movement.py +12 -9
  44. opentrons/protocol_engine/execution/queue_worker.py +8 -1
  45. opentrons/protocol_engine/execution/thermocycler_movement_flagger.py +52 -19
  46. opentrons/protocol_engine/resources/labware_validation.py +7 -1
  47. opentrons/protocol_engine/state/_well_math.py +2 -2
  48. opentrons/protocol_engine/state/commands.py +14 -28
  49. opentrons/protocol_engine/state/frustum_helpers.py +11 -7
  50. opentrons/protocol_engine/state/labware.py +12 -0
  51. opentrons/protocol_engine/state/modules.py +1 -1
  52. opentrons/protocol_engine/state/pipettes.py +8 -0
  53. opentrons/protocol_engine/state/tips.py +46 -83
  54. opentrons/protocol_engine/state/update_types.py +8 -23
  55. opentrons/protocol_engine/types/liquid_level_detection.py +68 -8
  56. opentrons/protocol_runner/legacy_command_mapper.py +12 -6
  57. opentrons/protocol_runner/run_orchestrator.py +1 -1
  58. opentrons/protocols/advanced_control/transfers/common.py +54 -11
  59. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +55 -28
  60. opentrons/protocols/api_support/definitions.py +1 -1
  61. opentrons/types.py +6 -6
  62. {opentrons-8.4.1a2.dist-info → opentrons-8.5.0.dist-info}/METADATA +4 -4
  63. {opentrons-8.4.1a2.dist-info → opentrons-8.5.0.dist-info}/RECORD +67 -66
  64. {opentrons-8.4.1a2.dist-info → opentrons-8.5.0.dist-info}/LICENSE +0 -0
  65. {opentrons-8.4.1a2.dist-info → opentrons-8.5.0.dist-info}/WHEEL +0 -0
  66. {opentrons-8.4.1a2.dist-info → opentrons-8.5.0.dist-info}/entry_points.txt +0 -0
  67. {opentrons-8.4.1a2.dist-info → opentrons-8.5.0.dist-info}/top_level.txt +0 -0
@@ -1,17 +1,17 @@
1
1
  """ProtocolEngine-based InstrumentContext core implementation."""
2
2
 
3
3
  from __future__ import annotations
4
- from contextlib import contextmanager
5
4
  from itertools import dropwhile
5
+ from copy import deepcopy
6
6
  from typing import (
7
7
  Optional,
8
8
  TYPE_CHECKING,
9
9
  cast,
10
10
  Union,
11
11
  List,
12
+ Sequence,
12
13
  Tuple,
13
14
  NamedTuple,
14
- Generator,
15
15
  Literal,
16
16
  )
17
17
  from opentrons.types import (
@@ -30,6 +30,9 @@ from opentrons.protocols.advanced_control.transfers.common import (
30
30
  NoLiquidClassPropertyError,
31
31
  )
32
32
  from opentrons.protocols.advanced_control.transfers import common as tx_commons
33
+ from opentrons.protocols.advanced_control.transfers.transfer_liquid_utils import (
34
+ check_current_volume_before_dispensing,
35
+ )
33
36
  from opentrons.protocol_engine import commands as cmd
34
37
  from opentrons.protocol_engine import (
35
38
  DeckPoint,
@@ -88,6 +91,7 @@ if TYPE_CHECKING:
88
91
  from opentrons.protocol_api._liquid_properties import (
89
92
  TransferProperties,
90
93
  MultiDispenseProperties,
94
+ SingleDispenseProperties,
91
95
  )
92
96
 
93
97
  _DISPENSE_VOLUME_VALIDATION_ADDED_IN = APIVersion(2, 17)
@@ -389,12 +393,7 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
389
393
  )
390
394
  )
391
395
 
392
- if isinstance(location, (TrashBin, WasteChute)):
393
- self._protocol_core.set_last_location(location=None, mount=self.get_mount())
394
- else:
395
- self._protocol_core.set_last_location(
396
- location=location, mount=self.get_mount()
397
- )
396
+ self._protocol_core.set_last_location(location=location, mount=self.get_mount())
398
397
 
399
398
  def blow_out(
400
399
  self,
@@ -470,12 +469,7 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
470
469
  )
471
470
  )
472
471
 
473
- if isinstance(location, (TrashBin, WasteChute)):
474
- self._protocol_core.set_last_location(location=None, mount=self.get_mount())
475
- else:
476
- self._protocol_core.set_last_location(
477
- location=location, mount=self.get_mount()
478
- )
472
+ self._protocol_core.set_last_location(location=location, mount=self.get_mount())
479
473
 
480
474
  def touch_tip(
481
475
  self,
@@ -803,12 +797,8 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
803
797
  speed=speed,
804
798
  )
805
799
  )
806
- if isinstance(location, (TrashBin, WasteChute)):
807
- self._protocol_core.set_last_location(location=None, mount=self.get_mount())
808
- else:
809
- self._protocol_core.set_last_location(
810
- location=location, mount=self.get_mount()
811
- )
800
+
801
+ self._protocol_core.set_last_location(location=location, mount=self.get_mount())
812
802
 
813
803
  def resin_tip_seal(
814
804
  self, location: Location, well_core: WellCore, in_place: Optional[bool] = False
@@ -1010,15 +1000,15 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1010
1000
  return self._sync_hardware_api.get_attached_instrument(self.get_mount()) # type: ignore[no-any-return]
1011
1001
 
1012
1002
  def get_channels(self) -> int:
1013
- return self._engine_client.state.tips.get_pipette_channels(self._pipette_id)
1003
+ return self._engine_client.state.pipettes.get_channels(self._pipette_id)
1014
1004
 
1015
1005
  def get_active_channels(self) -> int:
1016
- return self._engine_client.state.tips.get_pipette_active_channels(
1017
- self._pipette_id
1018
- )
1006
+ return self._engine_client.state.pipettes.get_active_channels(self._pipette_id)
1019
1007
 
1020
1008
  def get_nozzle_map(self) -> NozzleMapInterface:
1021
- return self._engine_client.state.tips.get_pipette_nozzle_map(self._pipette_id)
1009
+ return self._engine_client.state.pipettes.get_nozzle_configuration(
1010
+ self._pipette_id
1011
+ )
1022
1012
 
1023
1013
  def has_tip(self) -> bool:
1024
1014
  return (
@@ -1210,13 +1200,15 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1210
1200
  liquid_class: LiquidClass,
1211
1201
  volume: float,
1212
1202
  source: List[Tuple[Location, WellCore]],
1213
- dest: List[Tuple[Location, WellCore]],
1203
+ dest: Union[List[Tuple[Location, WellCore]], TrashBin, WasteChute],
1214
1204
  new_tip: TransferTipPolicyV2,
1215
1205
  tip_racks: List[Tuple[Location, LabwareCore]],
1216
1206
  starting_tip: Optional[WellCore],
1217
1207
  trash_location: Union[Location, TrashBin, WasteChute],
1218
1208
  return_tip: bool,
1219
- ) -> None:
1209
+ keep_last_tip: bool,
1210
+ last_tip_location: Optional[Tuple[Location, WellCore]],
1211
+ ) -> Optional[Tuple[Location, WellCore]]:
1220
1212
  """Execute transfer using liquid class properties.
1221
1213
 
1222
1214
  Args:
@@ -1230,8 +1222,18 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1230
1222
  types.Location is only necessary for saving the last accessed location.
1231
1223
  new_tip: Whether the transfer should use a new tip 'once', 'never', 'always',
1232
1224
  or 'per source'.
1233
- tiprack_uri: The URI of the tiprack that the transfer settings are for.
1234
- tip_drop_location: Location where the tip will be dropped (if appropriate).
1225
+ tip_racks: List of tipracks that the transfer will pick up tips from, represented
1226
+ as tuples of types.Location and WellCore.
1227
+ starting_tip: The user-chosen starting tip to use when deciding what tip to pick
1228
+ up, if the user has set it.
1229
+ trash_location: The chosen trash container to drop tips in and dispose liquid in.
1230
+ return_tip: If `True`, return tips to the tip rack location they were picked up from,
1231
+ otherwise drop in `trash_location`
1232
+ keep_last_tip: When set to `True`, do not drop the final tip used in the transfer.
1233
+ last_tip_location: If a tip is already attached, this will be the tiprack and well it was
1234
+ picked up from, represented as a tuple of types.Location and WellCore.
1235
+ Used so a tip can be returned if it was picked up outside this function
1236
+ as could be the case for a new_tip of `never`.
1235
1237
  """
1236
1238
  if not tip_racks:
1237
1239
  raise RuntimeError(
@@ -1256,29 +1258,42 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1256
1258
  tiprack_uri=tiprack_uri_for_transfer_props,
1257
1259
  )
1258
1260
 
1261
+ target_destinations: Sequence[
1262
+ Union[Tuple[Location, WellCore], TrashBin, WasteChute]
1263
+ ]
1264
+ if isinstance(dest, (TrashBin, WasteChute)):
1265
+ target_destinations = [dest] * len(source)
1266
+ else:
1267
+ target_destinations = dest
1268
+
1269
+ max_volume = min(
1270
+ self.get_max_volume(),
1271
+ self._engine_client.state.geometry.get_nominal_tip_geometry(
1272
+ pipette_id=self.pipette_id,
1273
+ labware_id=tip_racks[0][1].labware_id,
1274
+ well_name=None,
1275
+ ).volume,
1276
+ )
1277
+
1278
+ aspirate_air_gap_by_volume = transfer_props.aspirate.retract.air_gap_by_volume
1259
1279
  source_dest_per_volume_step = (
1260
1280
  tx_commons.expand_for_volume_constraints_for_liquid_classes(
1261
1281
  volumes=[volume for _ in range(len(source))],
1262
- targets=zip(source, dest),
1263
- max_volume=min(
1264
- self.get_max_volume(),
1265
- self._engine_client.state.geometry.get_nominal_tip_geometry(
1266
- pipette_id=self.pipette_id,
1267
- labware_id=tip_racks[0][1].labware_id,
1268
- well_name=None,
1269
- ).volume,
1270
- ),
1282
+ targets=zip(source, target_destinations),
1283
+ max_volume=max_volume,
1284
+ air_gap=aspirate_air_gap_by_volume,
1271
1285
  )
1272
1286
  )
1273
1287
 
1274
- last_tip_picked_up_from: Optional[WellCore] = None
1288
+ last_tip = last_tip_location
1275
1289
 
1276
1290
  def _drop_tip() -> None:
1277
1291
  if return_tip:
1278
- assert last_tip_picked_up_from is not None
1292
+ assert last_tip is not None
1293
+ _, tip_well = last_tip
1279
1294
  self.drop_tip(
1280
1295
  location=None,
1281
- well_core=last_tip_picked_up_from,
1296
+ well_core=tip_well,
1282
1297
  home_after=False,
1283
1298
  alternate_drop_location=False,
1284
1299
  )
@@ -1296,7 +1311,7 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1296
1311
  alternate_drop_location=True,
1297
1312
  )
1298
1313
 
1299
- def _pick_up_tip() -> WellCore:
1314
+ def _pick_up_tip() -> Tuple[Location, WellCore]:
1300
1315
  next_tip = self.get_next_tip(
1301
1316
  tip_racks=[core for loc, core in tip_racks],
1302
1317
  starting_well=starting_tip,
@@ -1322,12 +1337,15 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1322
1337
  presses=None,
1323
1338
  increment=None,
1324
1339
  )
1325
- return tip_well
1340
+ return tiprack_loc, tip_well
1326
1341
 
1327
1342
  if new_tip == TransferTipPolicyV2.ONCE:
1328
- last_tip_picked_up_from = _pick_up_tip()
1343
+ last_tip = _pick_up_tip()
1329
1344
 
1330
1345
  prev_src: Optional[Tuple[Location, WellCore]] = None
1346
+ prev_dest: Optional[
1347
+ Union[Tuple[Location, WellCore], TrashBin, WasteChute]
1348
+ ] = None
1331
1349
  post_disp_tip_contents = [
1332
1350
  tx_comps_executor.LiquidAndAirGapPair(
1333
1351
  liquid=0,
@@ -1347,74 +1365,72 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1347
1365
  except StopIteration:
1348
1366
  is_last_step = True
1349
1367
 
1350
- if new_tip == TransferTipPolicyV2.ALWAYS or (
1351
- new_tip == TransferTipPolicyV2.PER_SOURCE and step_source != prev_src
1368
+ if (
1369
+ new_tip == TransferTipPolicyV2.ALWAYS
1370
+ or (
1371
+ new_tip == TransferTipPolicyV2.PER_SOURCE
1372
+ and step_source != prev_src
1373
+ )
1374
+ or (
1375
+ new_tip == TransferTipPolicyV2.PER_DESTINATION
1376
+ and step_destination != prev_dest
1377
+ )
1352
1378
  ):
1353
- if prev_src is not None:
1379
+ if prev_src is not None and prev_dest is not None:
1354
1380
  _drop_tip()
1355
- last_tip_picked_up_from = _pick_up_tip()
1381
+ last_tip = _pick_up_tip()
1356
1382
  post_disp_tip_contents = [
1357
1383
  tx_comps_executor.LiquidAndAirGapPair(
1358
1384
  liquid=0,
1359
1385
  air_gap=0,
1360
1386
  )
1361
1387
  ]
1362
- # Enable LPD only if all of these apply:
1363
- # - LPD is globally enabled for this pipette
1364
- # - it is the first time visiting this well
1365
- # - pipette tip is unused
1366
- enable_lpd = (
1367
- self.get_liquid_presence_detection() and step_source != prev_src
1368
- )
1369
- elif new_tip == TransferTipPolicyV2.ONCE:
1370
- # Enable LPD only if:
1371
- # - LPD is globally enabled for this pipette
1372
- # - this is the first source well of the entire transfer, which means
1373
- # that the current tip is unused
1374
- enable_lpd = self.get_liquid_presence_detection() and prev_src is None
1375
- else:
1376
- enable_lpd = False
1377
1388
 
1378
- with self.lpd_for_transfer(enable_lpd):
1379
- post_asp_tip_contents = self.aspirate_liquid_class(
1380
- volume=step_volume,
1381
- source=step_source,
1382
- transfer_properties=transfer_props,
1383
- transfer_type=tx_comps_executor.TransferType.ONE_TO_ONE,
1384
- tip_contents=post_disp_tip_contents,
1385
- volume_for_pipette_mode_configuration=step_volume,
1386
- )
1387
- post_disp_tip_contents = self.dispense_liquid_class(
1388
- volume=step_volume,
1389
- dest=step_destination,
1390
- source=step_source,
1391
- transfer_properties=transfer_props,
1392
- transfer_type=tx_comps_executor.TransferType.ONE_TO_ONE,
1393
- tip_contents=post_asp_tip_contents,
1394
- add_final_air_gap=(
1395
- False
1396
- if is_last_step and new_tip == TransferTipPolicyV2.NEVER
1397
- else True
1398
- ),
1399
- trash_location=trash_location,
1400
- )
1389
+ post_asp_tip_contents = self.aspirate_liquid_class(
1390
+ volume=step_volume,
1391
+ source=step_source,
1392
+ transfer_properties=transfer_props,
1393
+ transfer_type=tx_comps_executor.TransferType.ONE_TO_ONE,
1394
+ tip_contents=post_disp_tip_contents,
1395
+ volume_for_pipette_mode_configuration=step_volume,
1396
+ )
1397
+ post_disp_tip_contents = self.dispense_liquid_class(
1398
+ volume=step_volume,
1399
+ dest=step_destination,
1400
+ source=step_source,
1401
+ transfer_properties=transfer_props,
1402
+ transfer_type=tx_comps_executor.TransferType.ONE_TO_ONE,
1403
+ tip_contents=post_asp_tip_contents,
1404
+ add_final_air_gap=(False if is_last_step and keep_last_tip else True),
1405
+ trash_location=trash_location,
1406
+ )
1401
1407
  prev_src = step_source
1402
- if new_tip != TransferTipPolicyV2.NEVER:
1408
+ prev_dest = step_destination
1409
+
1410
+ if not keep_last_tip:
1403
1411
  _drop_tip()
1412
+ last_tip = None
1413
+
1414
+ return last_tip
1404
1415
 
1405
- # TODO(spp, 2025-02-25): wire up return tip
1406
1416
  def distribute_with_liquid_class( # noqa: C901
1407
1417
  self,
1408
1418
  liquid_class: LiquidClass,
1409
1419
  volume: float,
1410
1420
  source: Tuple[Location, WellCore],
1411
1421
  dest: List[Tuple[Location, WellCore]],
1412
- new_tip: Literal[TransferTipPolicyV2.NEVER, TransferTipPolicyV2.ONCE],
1422
+ new_tip: Literal[
1423
+ TransferTipPolicyV2.NEVER,
1424
+ TransferTipPolicyV2.ONCE,
1425
+ TransferTipPolicyV2.ALWAYS,
1426
+ ],
1413
1427
  tip_racks: List[Tuple[Location, LabwareCore]],
1414
1428
  starting_tip: Optional[WellCore],
1415
1429
  trash_location: Union[Location, TrashBin, WasteChute],
1416
1430
  return_tip: bool,
1417
- ) -> None:
1431
+ keep_last_tip: bool,
1432
+ last_tip_location: Optional[Tuple[Location, WellCore]],
1433
+ ) -> Optional[Tuple[Location, WellCore]]:
1418
1434
  """Execute a distribution using liquid class properties.
1419
1435
 
1420
1436
  Args:
@@ -1425,9 +1441,22 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1425
1441
  dest: List of destination wells, with each well represented as a tuple of
1426
1442
  types.Location and WellCore.
1427
1443
  types.Location is only necessary for saving the last accessed location.
1428
- new_tip: Whether the transfer should use a new tip 'once' or 'never'.
1429
- tiprack_uri: The URI of the tiprack that the transfer settings are for.
1430
- tip_drop_location: Location where the tip will be dropped (if appropriate).
1444
+ new_tip: Whether the transfer should use a new tip 'once', 'always' or 'never'.
1445
+ 'never': the transfer will never pick up a new tip
1446
+ 'once': the transfer will pick up a new tip once at the start of transfer
1447
+ 'always': the transfer will pick up a new tip before every aspirate
1448
+ tip_racks: List of tipracks that the transfer will pick up tips from, represented
1449
+ as tuples of types.Location and WellCore.
1450
+ starting_tip: The user-chosen starting tip to use when deciding what tip to pick
1451
+ up, if the user has set it.
1452
+ trash_location: The chosen trash container to drop tips in and dispose liquid in.
1453
+ return_tip: If `True`, return tips to the tip rack location they were picked up from,
1454
+ otherwise drop in `trash_location`
1455
+ keep_last_tip: When set to `True`, do not drop the final tip used in the distribute.
1456
+ last_tip_location: If a tip is already attached, this will be the tiprack and well it was
1457
+ picked up from, represented as a tuple of types.Location and WellCore.
1458
+ Used so a tip can be returned if it was picked up outside this function
1459
+ as could be the case for a new_tip of `never`
1431
1460
 
1432
1461
  This method distributes the liquid in the source well into multiple destinations.
1433
1462
  It can accomplish this by either doing a multi-dispense (aspirate once and then
@@ -1435,13 +1464,17 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1435
1464
  (going back to aspirate after each dispense). Whether it does a multi-dispense or
1436
1465
  multiple single dispenses is determined by whether multi-dispense properties
1437
1466
  are available in the liquid class and whether the tip in use can hold multiple
1438
- volumes to be dispensed whithout having to refill.
1467
+ volumes to be dispensed without having to refill.
1439
1468
  """
1440
1469
  if not tip_racks:
1441
1470
  raise RuntimeError(
1442
1471
  "No tipracks found for pipette in order to perform transfer"
1443
1472
  )
1444
- assert new_tip in [TransferTipPolicyV2.NEVER, TransferTipPolicyV2.ONCE]
1473
+ assert new_tip in [
1474
+ TransferTipPolicyV2.NEVER,
1475
+ TransferTipPolicyV2.ONCE,
1476
+ TransferTipPolicyV2.ALWAYS,
1477
+ ]
1445
1478
 
1446
1479
  tiprack_uri_for_transfer_props = tip_racks[0][1].get_uri()
1447
1480
  working_volume = min(
@@ -1487,7 +1520,7 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1487
1520
  tip_working_volume=working_volume,
1488
1521
  )
1489
1522
  ):
1490
- self.transfer_with_liquid_class(
1523
+ return self.transfer_with_liquid_class(
1491
1524
  liquid_class=liquid_class,
1492
1525
  volume=volume,
1493
1526
  source=[source for _ in range(len(dest))],
@@ -1497,8 +1530,9 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1497
1530
  starting_tip=starting_tip,
1498
1531
  trash_location=trash_location,
1499
1532
  return_tip=return_tip,
1533
+ keep_last_tip=keep_last_tip,
1534
+ last_tip_location=last_tip_location,
1500
1535
  )
1501
- return
1502
1536
 
1503
1537
  # TODO: use the ID returned by load_liquid_class in command annotations
1504
1538
  self.load_liquid_class(
@@ -1507,6 +1541,11 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1507
1541
  tiprack_uri=tiprack_uri_for_transfer_props,
1508
1542
  )
1509
1543
 
1544
+ aspirate_air_gap_by_volume = transfer_props.aspirate.retract.air_gap_by_volume
1545
+ disposal_vol_by_volume = transfer_props.multi_dispense.disposal_by_volume
1546
+ conditioning_vol_by_volume = (
1547
+ transfer_props.multi_dispense.conditioning_by_volume
1548
+ )
1510
1549
  # This will return a generator that provides pairs of destination well and
1511
1550
  # the volume to dispense into it
1512
1551
  dest_per_volume_step = (
@@ -1514,17 +1553,21 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1514
1553
  volumes=[volume for _ in range(len(dest))],
1515
1554
  targets=dest,
1516
1555
  max_volume=working_volume,
1556
+ air_gap=aspirate_air_gap_by_volume,
1557
+ disposal_vol=disposal_vol_by_volume,
1558
+ conditioning_vol=conditioning_vol_by_volume,
1517
1559
  )
1518
1560
  )
1519
1561
 
1520
- last_tip_picked_up_from: Optional[WellCore] = None
1562
+ last_tip = last_tip_location
1521
1563
 
1522
1564
  def _drop_tip() -> None:
1523
1565
  if return_tip:
1524
- assert last_tip_picked_up_from is not None
1566
+ assert last_tip is not None
1567
+ _, tip_well = last_tip
1525
1568
  self.drop_tip(
1526
1569
  location=None,
1527
- well_core=last_tip_picked_up_from,
1570
+ well_core=tip_well,
1528
1571
  home_after=False,
1529
1572
  alternate_drop_location=False,
1530
1573
  )
@@ -1542,7 +1585,7 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1542
1585
  alternate_drop_location=True,
1543
1586
  )
1544
1587
 
1545
- def _pick_up_tip() -> WellCore:
1588
+ def _pick_up_tip() -> Tuple[Location, WellCore]:
1546
1589
  next_tip = self.get_next_tip(
1547
1590
  tip_racks=[core for loc, core in tip_racks],
1548
1591
  starting_well=starting_tip,
@@ -1568,10 +1611,10 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1568
1611
  presses=None,
1569
1612
  increment=None,
1570
1613
  )
1571
- return tip_well
1614
+ return tiprack_loc, tip_well
1572
1615
 
1573
1616
  if new_tip != TransferTipPolicyV2.NEVER:
1574
- last_tip_picked_up_from = _pick_up_tip()
1617
+ last_tip = _pick_up_tip()
1575
1618
 
1576
1619
  tip_contents = [
1577
1620
  tx_comps_executor.LiquidAndAirGapPair(
@@ -1635,61 +1678,57 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1635
1678
  " Specify a blowout location and enable blowout when using a disposal volume."
1636
1679
  )
1637
1680
 
1638
- if (
1639
- self.get_liquid_presence_detection()
1640
- and new_tip != TransferTipPolicyV2.NEVER
1641
- and is_first_step
1642
- ):
1643
- enable_lpd = True
1644
- else:
1645
- enable_lpd = False
1646
- with self.lpd_for_transfer(enable=enable_lpd):
1647
- # Aspirate the total volume determined by the loop above
1648
- tip_contents = self.aspirate_liquid_class(
1649
- volume=total_aspirate_volume + conditioning_vol + disposal_vol,
1650
- source=source,
1651
- transfer_properties=transfer_props,
1652
- transfer_type=tx_comps_executor.TransferType.ONE_TO_MANY,
1653
- tip_contents=tip_contents,
1654
- # We configure the mode based on the last dispense volume and disposal volume
1655
- # since the mode is only used to determine the dispense push out volume
1656
- # and we can do a push out only at the last dispense, that too if there is no disposal volume.
1657
- volume_for_pipette_mode_configuration=vol_dest_combo[-1][0],
1658
- conditioning_volume=conditioning_vol,
1659
- )
1681
+ if not is_first_step and new_tip == TransferTipPolicyV2.ALWAYS:
1682
+ _drop_tip()
1683
+ last_tip = _pick_up_tip()
1684
+ tip_contents = [
1685
+ tx_comps_executor.LiquidAndAirGapPair(
1686
+ liquid=0,
1687
+ air_gap=0,
1688
+ )
1689
+ ]
1690
+ # Aspirate the total volume determined by the loop above
1691
+ tip_contents = self.aspirate_liquid_class(
1692
+ volume=total_aspirate_volume + conditioning_vol + disposal_vol,
1693
+ source=source,
1694
+ transfer_properties=transfer_props,
1695
+ transfer_type=tx_comps_executor.TransferType.ONE_TO_MANY,
1696
+ tip_contents=tip_contents,
1697
+ # We configure the mode based on the last dispense volume and disposal volume
1698
+ # since the mode is only used to determine the dispense push out volume
1699
+ # and we can do a push out only at the last dispense, that too if there is no disposal volume.
1700
+ volume_for_pipette_mode_configuration=vol_dest_combo[-1][0],
1701
+ conditioning_volume=conditioning_vol,
1702
+ )
1660
1703
 
1661
- # If the tip has volumes correspoinding to multiple destinations, then
1704
+ # If the tip has volumes corresponding to multiple destinations, then
1662
1705
  # multi-dispense in those destinations.
1663
1706
  # If the tip has a volume corresponding to a single destination, then
1664
1707
  # do a single-dispense into that destination.
1665
- for next_vol, next_dest in vol_dest_combo:
1708
+ for dispense_vol, dispense_dest in vol_dest_combo:
1666
1709
  if use_single_dispense:
1667
1710
  tip_contents = self.dispense_liquid_class(
1668
- volume=next_vol,
1669
- dest=next_dest,
1711
+ volume=dispense_vol,
1712
+ dest=dispense_dest,
1670
1713
  source=source,
1671
1714
  transfer_properties=transfer_props,
1672
1715
  transfer_type=tx_comps_executor.TransferType.ONE_TO_MANY,
1673
1716
  tip_contents=tip_contents,
1674
1717
  add_final_air_gap=(
1675
- False
1676
- if is_last_step and new_tip == TransferTipPolicyV2.NEVER
1677
- else True
1718
+ False if is_last_step and keep_last_tip else True
1678
1719
  ),
1679
1720
  trash_location=trash_location,
1680
1721
  )
1681
1722
  else:
1682
1723
  tip_contents = self.dispense_liquid_class_during_multi_dispense(
1683
- volume=next_vol,
1684
- dest=next_dest,
1724
+ volume=dispense_vol,
1725
+ dest=dispense_dest,
1685
1726
  source=source,
1686
1727
  transfer_properties=transfer_props,
1687
1728
  transfer_type=tx_comps_executor.TransferType.ONE_TO_MANY,
1688
1729
  tip_contents=tip_contents,
1689
1730
  add_final_air_gap=(
1690
- False
1691
- if is_last_step and new_tip == TransferTipPolicyV2.NEVER
1692
- else True
1731
+ False if is_last_step and keep_last_tip else True
1693
1732
  ),
1694
1733
  trash_location=trash_location,
1695
1734
  conditioning_volume=conditioning_vol,
@@ -1697,8 +1736,11 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1697
1736
  )
1698
1737
  is_first_step = False
1699
1738
 
1700
- if new_tip != TransferTipPolicyV2.NEVER:
1739
+ if not keep_last_tip:
1701
1740
  _drop_tip()
1741
+ last_tip = None
1742
+
1743
+ return last_tip
1702
1744
 
1703
1745
  def _tip_can_hold_volume_for_multi_dispensing(
1704
1746
  self,
@@ -1726,18 +1768,58 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1726
1768
  liquid_class: LiquidClass,
1727
1769
  volume: float,
1728
1770
  source: List[Tuple[Location, WellCore]],
1729
- dest: Tuple[Location, WellCore],
1730
- new_tip: Literal[TransferTipPolicyV2.NEVER, TransferTipPolicyV2.ONCE],
1771
+ dest: Union[Tuple[Location, WellCore], TrashBin, WasteChute],
1772
+ new_tip: Literal[
1773
+ TransferTipPolicyV2.NEVER,
1774
+ TransferTipPolicyV2.ONCE,
1775
+ TransferTipPolicyV2.ALWAYS,
1776
+ ],
1731
1777
  tip_racks: List[Tuple[Location, LabwareCore]],
1732
1778
  starting_tip: Optional[WellCore],
1733
1779
  trash_location: Union[Location, TrashBin, WasteChute],
1734
1780
  return_tip: bool,
1735
- ) -> None:
1781
+ keep_last_tip: bool,
1782
+ last_tip_location: Optional[Tuple[Location, WellCore]],
1783
+ ) -> Optional[Tuple[Location, WellCore]]:
1784
+ """Execute consolidate using liquid class properties.
1785
+
1786
+ Args:
1787
+ liquid_class: The liquid class to use for transfer properties.
1788
+ volume: Volume to transfer per well.
1789
+ source: List of source wells, with each well represented as a tuple of
1790
+ types.Location and WellCore.
1791
+ types.Location is only necessary for saving the last accessed location.
1792
+ dest: List of destination wells, with each well represented as a tuple of
1793
+ types.Location and WellCore.
1794
+ types.Location is only necessary for saving the last accessed location.
1795
+ new_tip: Whether the transfer should use a new tip 'once', 'always' or 'never'.
1796
+ 'never': the transfer will never pick up a new tip
1797
+ 'once': the transfer will pick up a new tip once at the start of transfer
1798
+ 'always': the transfer will pick up a new tip after every dispense
1799
+ tip_racks: List of tipracks that the transfer will pick up tips from, represented
1800
+ as tuples of types.Location and WellCore.
1801
+ starting_tip: The user-chosen starting tip to use when deciding what tip to pick
1802
+ up, if the user has set it.
1803
+ trash_location: The chosen trash container to drop tips in and dispose liquid in.
1804
+ return_tip: If `True`, return tips to the tip rack location they were picked up from,
1805
+ otherwise drop in `trash_location`
1806
+ keep_last_tip: When set to `True`, do not drop the final tip used in the consolidate.
1807
+ last_tip_location: If a tip is already attached, this will be the tiprack and well it was
1808
+ picked up from, represented as a tuple of types.Location and WellCore.
1809
+ Used so a tip can be returned if it was picked up outside this function
1810
+ as could be the case for a new_tip of `never`.
1811
+ """
1736
1812
  if not tip_racks:
1737
1813
  raise RuntimeError(
1738
1814
  "No tipracks found for pipette in order to perform transfer"
1739
1815
  )
1740
- assert new_tip in [TransferTipPolicyV2.NEVER, TransferTipPolicyV2.ONCE]
1816
+ # NOTE: Tip option of "always" in consolidate is equivalent to "after every dispense",
1817
+ # or more specifically, "before the next chunk of aspirates".
1818
+ assert new_tip in [
1819
+ TransferTipPolicyV2.NEVER,
1820
+ TransferTipPolicyV2.ONCE,
1821
+ TransferTipPolicyV2.ALWAYS,
1822
+ ]
1741
1823
  tiprack_uri_for_transfer_props = tip_racks[0][1].get_uri()
1742
1824
  try:
1743
1825
  transfer_props = liquid_class.get_for(
@@ -1776,22 +1858,25 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1776
1858
  ).volume,
1777
1859
  )
1778
1860
 
1861
+ aspirate_air_gap_by_volume = transfer_props.aspirate.retract.air_gap_by_volume
1779
1862
  source_per_volume_step = (
1780
1863
  tx_commons.expand_for_volume_constraints_for_liquid_classes(
1781
1864
  volumes=[volume for _ in range(len(source))],
1782
1865
  targets=source,
1783
1866
  max_volume=max_volume,
1867
+ air_gap=aspirate_air_gap_by_volume,
1784
1868
  )
1785
1869
  )
1786
1870
 
1787
- last_tip_picked_up_from: Optional[WellCore] = None
1871
+ last_tip = last_tip_location
1788
1872
 
1789
1873
  def _drop_tip() -> None:
1790
1874
  if return_tip:
1791
- assert last_tip_picked_up_from is not None
1875
+ assert last_tip is not None
1876
+ _, tip_well = last_tip
1792
1877
  self.drop_tip(
1793
1878
  location=None,
1794
- well_core=last_tip_picked_up_from,
1879
+ well_core=tip_well,
1795
1880
  home_after=False,
1796
1881
  alternate_drop_location=False,
1797
1882
  )
@@ -1809,7 +1894,7 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1809
1894
  alternate_drop_location=True,
1810
1895
  )
1811
1896
 
1812
- def _pick_up_tip() -> WellCore:
1897
+ def _pick_up_tip() -> Tuple[Location, WellCore]:
1813
1898
  next_tip = self.get_next_tip(
1814
1899
  tip_racks=[core for loc, core in tip_racks],
1815
1900
  starting_well=starting_tip,
@@ -1835,10 +1920,10 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1835
1920
  presses=None,
1836
1921
  increment=None,
1837
1922
  )
1838
- return tip_well
1923
+ return tiprack_loc, tip_well
1839
1924
 
1840
- if new_tip == TransferTipPolicyV2.ONCE:
1841
- last_tip_picked_up_from = _pick_up_tip()
1925
+ if new_tip in [TransferTipPolicyV2.ONCE, TransferTipPolicyV2.ALWAYS]:
1926
+ last_tip = _pick_up_tip()
1842
1927
 
1843
1928
  tip_contents = [
1844
1929
  tx_comps_executor.LiquidAndAirGapPair(
@@ -1852,39 +1937,45 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1852
1937
  while not is_last_step:
1853
1938
  total_dispense_volume = 0.0
1854
1939
  vol_aspirate_combo = []
1940
+ air_gap = aspirate_air_gap_by_volume.get_for_volume(next_step_volume)
1855
1941
  # Take air gap into account because there will be a final air gap before the dispense
1856
- while total_dispense_volume + next_step_volume <= max_volume:
1942
+ while total_dispense_volume + next_step_volume <= max_volume - air_gap:
1857
1943
  total_dispense_volume += next_step_volume
1858
1944
  vol_aspirate_combo.append((next_step_volume, next_source))
1859
1945
  try:
1860
1946
  next_step_volume, next_source = next(source_per_volume_step)
1947
+ air_gap = aspirate_air_gap_by_volume.get_for_volume(
1948
+ next_step_volume + total_dispense_volume
1949
+ )
1861
1950
  except StopIteration:
1862
1951
  is_last_step = True
1863
1952
  break
1864
1953
 
1865
- if (
1866
- self.get_liquid_presence_detection()
1867
- and new_tip != TransferTipPolicyV2.NEVER
1868
- and is_first_step
1869
- ):
1870
- enable_lpd = True
1871
- else:
1872
- enable_lpd = False
1954
+ if not is_first_step and new_tip == TransferTipPolicyV2.ALWAYS:
1955
+ _drop_tip()
1956
+ last_tip = _pick_up_tip()
1957
+ tip_contents = [
1958
+ tx_comps_executor.LiquidAndAirGapPair(
1959
+ liquid=0,
1960
+ air_gap=0,
1961
+ )
1962
+ ]
1873
1963
 
1964
+ total_aspirated_volume = 0.0
1874
1965
  for step_num, (step_volume, step_source) in enumerate(vol_aspirate_combo):
1875
- with self.lpd_for_transfer(enable=enable_lpd):
1876
- tip_contents = self.aspirate_liquid_class(
1877
- volume=step_volume,
1878
- source=step_source,
1879
- transfer_properties=transfer_props,
1880
- transfer_type=tx_comps_executor.TransferType.MANY_TO_ONE,
1881
- tip_contents=tip_contents,
1882
- volume_for_pipette_mode_configuration=(
1883
- total_dispense_volume if step_num == 0 else None
1884
- ),
1885
- )
1966
+ tip_contents = self.aspirate_liquid_class(
1967
+ volume=step_volume,
1968
+ source=step_source,
1969
+ transfer_properties=transfer_props,
1970
+ transfer_type=tx_comps_executor.TransferType.MANY_TO_ONE,
1971
+ tip_contents=tip_contents,
1972
+ volume_for_pipette_mode_configuration=(
1973
+ total_dispense_volume if step_num == 0 else None
1974
+ ),
1975
+ current_volume=total_aspirated_volume,
1976
+ )
1977
+ total_aspirated_volume += step_volume
1886
1978
  is_first_step = False
1887
- enable_lpd = False
1888
1979
  tip_contents = self.dispense_liquid_class(
1889
1980
  volume=total_dispense_volume,
1890
1981
  dest=dest,
@@ -1892,15 +1983,15 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1892
1983
  transfer_properties=transfer_props,
1893
1984
  transfer_type=tx_comps_executor.TransferType.MANY_TO_ONE,
1894
1985
  tip_contents=tip_contents,
1895
- add_final_air_gap=(
1896
- False
1897
- if is_last_step and new_tip == TransferTipPolicyV2.NEVER
1898
- else True
1899
- ),
1986
+ add_final_air_gap=(False if is_last_step and keep_last_tip else True),
1900
1987
  trash_location=trash_location,
1901
1988
  )
1902
- if new_tip != TransferTipPolicyV2.NEVER:
1989
+
1990
+ if not keep_last_tip:
1903
1991
  _drop_tip()
1992
+ last_tip = None
1993
+
1994
+ return last_tip
1904
1995
 
1905
1996
  def _get_location_and_well_core_from_next_tip_info(
1906
1997
  self,
@@ -1931,6 +2022,7 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1931
2022
  tip_contents: List[tx_comps_executor.LiquidAndAirGapPair],
1932
2023
  volume_for_pipette_mode_configuration: Optional[float],
1933
2024
  conditioning_volume: Optional[float] = None,
2025
+ current_volume: float = 0.0,
1934
2026
  ) -> List[tx_comps_executor.LiquidAndAirGapPair]:
1935
2027
  """Execute aspiration steps.
1936
2028
 
@@ -1944,33 +2036,57 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1944
2036
  Return: List of liquid and air gap pairs in tip.
1945
2037
  """
1946
2038
  aspirate_props = transfer_properties.aspirate
2039
+ volume_for_air_gap = aspirate_props.retract.air_gap_by_volume.get_for_volume(
2040
+ volume + current_volume
2041
+ )
1947
2042
  tx_commons.check_valid_liquid_class_volume_parameters(
1948
2043
  aspirate_volume=volume,
1949
- air_gap=(
1950
- aspirate_props.retract.air_gap_by_volume.get_for_volume(volume)
1951
- if conditioning_volume is None
1952
- else 0
1953
- ),
1954
- disposal_volume=0, # Disposal volume is accounted for in aspirate vol
2044
+ air_gap=volume_for_air_gap if conditioning_volume is None else 0,
1955
2045
  max_volume=self.get_working_volume(),
2046
+ current_volume=current_volume,
1956
2047
  )
1957
2048
  source_loc, source_well = source
1958
- aspirate_point = (
1959
- tx_comps_executor.absolute_point_from_position_reference_and_offset(
1960
- well=source_well,
1961
- position_reference=aspirate_props.position_reference,
1962
- offset=aspirate_props.offset,
1963
- )
1964
- )
1965
- aspirate_location = Location(aspirate_point, labware=source_loc.labware)
1966
2049
  last_liquid_and_airgap_in_tip = (
1967
- tip_contents[-1]
2050
+ deepcopy(tip_contents[-1]) # don't modify caller's object
1968
2051
  if tip_contents
1969
2052
  else tx_comps_executor.LiquidAndAirGapPair(
1970
2053
  liquid=0,
1971
2054
  air_gap=0,
1972
2055
  )
1973
2056
  )
2057
+ if volume_for_pipette_mode_configuration is not None:
2058
+ prep_location = Location(
2059
+ point=source_well.get_top(LIQUID_PROBE_START_OFFSET_FROM_WELL_TOP.z),
2060
+ labware=source_loc.labware,
2061
+ )
2062
+ self.move_to(
2063
+ location=prep_location,
2064
+ well_core=source_well,
2065
+ force_direct=False,
2066
+ minimum_z_height=None,
2067
+ speed=None,
2068
+ )
2069
+ self.remove_air_gap_during_transfer_with_liquid_class(
2070
+ last_air_gap=last_liquid_and_airgap_in_tip.air_gap,
2071
+ dispense_props=transfer_properties.dispense,
2072
+ location=prep_location,
2073
+ )
2074
+ last_liquid_and_airgap_in_tip.air_gap = 0
2075
+ # TODO: do volume configuration + prepare for aspirate only if the mode needs to be changed
2076
+ self.configure_for_volume(volume_for_pipette_mode_configuration)
2077
+ self.prepare_to_aspirate()
2078
+
2079
+ aspirate_point = (
2080
+ tx_comps_executor.absolute_point_from_position_reference_and_offset(
2081
+ well=source_well,
2082
+ well_volume_difference=-volume,
2083
+ position_reference=aspirate_props.aspirate_position.position_reference,
2084
+ offset=aspirate_props.aspirate_position.offset,
2085
+ mount=self.get_mount(),
2086
+ )
2087
+ )
2088
+ aspirate_location = Location(aspirate_point, labware=source_loc.labware)
2089
+
1974
2090
  components_executor = tx_comps_executor.TransferComponentsExecutor(
1975
2091
  instrument_core=self,
1976
2092
  transfer_properties=transfer_properties,
@@ -1982,9 +2098,7 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1982
2098
  ),
1983
2099
  )
1984
2100
  components_executor.submerge(
1985
- submerge_properties=aspirate_props.submerge,
1986
- post_submerge_action="aspirate",
1987
- volume_for_pipette_mode_configuration=volume_for_pipette_mode_configuration,
2101
+ submerge_properties=aspirate_props.submerge, post_submerge_action="aspirate"
1988
2102
  )
1989
2103
  # Do not do a pre-aspirate mix or pre-wet if consolidating
1990
2104
  if transfer_type != tx_comps_executor.TransferType.MANY_TO_ONE:
@@ -2023,10 +2137,45 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
2023
2137
  new_tip_contents = tip_contents[0:-1] + [last_contents]
2024
2138
  return new_tip_contents
2025
2139
 
2140
+ def remove_air_gap_during_transfer_with_liquid_class(
2141
+ self,
2142
+ last_air_gap: float,
2143
+ dispense_props: SingleDispenseProperties,
2144
+ location: Union[Location, TrashBin, WasteChute],
2145
+ ) -> None:
2146
+ """Remove an air gap that was previously added during a transfer."""
2147
+ if last_air_gap == 0:
2148
+ return
2149
+ current_vol = self.get_current_volume()
2150
+ check_current_volume_before_dispensing(
2151
+ current_volume=current_vol, dispense_volume=last_air_gap
2152
+ )
2153
+ correction_volume = dispense_props.correction_by_volume.get_for_volume(
2154
+ current_vol - last_air_gap
2155
+ )
2156
+ # The minimum flow rate should be air_gap_volume per second
2157
+ flow_rate = max(
2158
+ dispense_props.flow_rate_by_volume.get_for_volume(last_air_gap),
2159
+ last_air_gap,
2160
+ )
2161
+ self.dispense(
2162
+ location=location,
2163
+ well_core=None,
2164
+ volume=last_air_gap,
2165
+ rate=1,
2166
+ flow_rate=flow_rate,
2167
+ in_place=True,
2168
+ push_out=0,
2169
+ correction_volume=correction_volume,
2170
+ )
2171
+ dispense_delay = dispense_props.delay
2172
+ if dispense_delay.enabled and dispense_delay.duration:
2173
+ self.delay(dispense_delay.duration)
2174
+
2026
2175
  def dispense_liquid_class(
2027
2176
  self,
2028
2177
  volume: float,
2029
- dest: Tuple[Location, WellCore],
2178
+ dest: Union[Tuple[Location, WellCore], TrashBin, WasteChute],
2030
2179
  source: Optional[Tuple[Location, WellCore]],
2031
2180
  transfer_properties: TransferProperties,
2032
2181
  transfer_type: tx_comps_executor.TransferType,
@@ -2065,15 +2214,21 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
2065
2214
  List of liquid and air gap pairs in tip.
2066
2215
  """
2067
2216
  dispense_props = transfer_properties.dispense
2068
- dest_loc, dest_well = dest
2069
- dispense_point = (
2070
- tx_comps_executor.absolute_point_from_position_reference_and_offset(
2217
+ dispense_location: Union[Location, TrashBin, WasteChute]
2218
+ if isinstance(dest, tuple):
2219
+ dest_loc, dest_well = dest
2220
+ dispense_point = tx_comps_executor.absolute_point_from_position_reference_and_offset(
2071
2221
  well=dest_well,
2072
- position_reference=dispense_props.position_reference,
2073
- offset=dispense_props.offset,
2222
+ well_volume_difference=volume,
2223
+ position_reference=dispense_props.dispense_position.position_reference,
2224
+ offset=dispense_props.dispense_position.offset,
2225
+ mount=self.get_mount(),
2074
2226
  )
2075
- )
2076
- dispense_location = Location(dispense_point, labware=dest_loc.labware)
2227
+ dispense_location = Location(dispense_point, labware=dest_loc.labware)
2228
+ else:
2229
+ dispense_location = dest
2230
+ dest_well = None
2231
+
2077
2232
  last_liquid_and_airgap_in_tip = (
2078
2233
  tip_contents[-1]
2079
2234
  if tip_contents
@@ -2093,9 +2248,7 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
2093
2248
  ),
2094
2249
  )
2095
2250
  components_executor.submerge(
2096
- submerge_properties=dispense_props.submerge,
2097
- post_submerge_action="dispense",
2098
- volume_for_pipette_mode_configuration=None,
2251
+ submerge_properties=dispense_props.submerge, post_submerge_action="dispense"
2099
2252
  )
2100
2253
  push_out_vol = (
2101
2254
  0.0
@@ -2151,8 +2304,10 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
2151
2304
  dispense_point = (
2152
2305
  tx_comps_executor.absolute_point_from_position_reference_and_offset(
2153
2306
  well=dest_well,
2154
- position_reference=dispense_props.position_reference,
2155
- offset=dispense_props.offset,
2307
+ well_volume_difference=volume,
2308
+ position_reference=dispense_props.dispense_position.position_reference,
2309
+ offset=dispense_props.dispense_position.offset,
2310
+ mount=self.get_mount(),
2156
2311
  )
2157
2312
  )
2158
2313
  dispense_location = Location(dispense_point, labware=dest_loc.labware)
@@ -2175,9 +2330,7 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
2175
2330
  ),
2176
2331
  )
2177
2332
  components_executor.submerge(
2178
- submerge_properties=dispense_props.submerge,
2179
- post_submerge_action="dispense",
2180
- volume_for_pipette_mode_configuration=None,
2333
+ submerge_properties=dispense_props.submerge, post_submerge_action="dispense"
2181
2334
  )
2182
2335
  tip_starting_volume = self.get_current_volume()
2183
2336
  is_last_dispense_without_disposal_vol = (
@@ -2220,8 +2373,9 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
2220
2373
  def detect_liquid_presence(self, well_core: WellCore, loc: Location) -> bool:
2221
2374
  labware_id = well_core.labware_id
2222
2375
  well_name = well_core.get_name()
2376
+ offset = LIQUID_PROBE_START_OFFSET_FROM_WELL_TOP
2223
2377
  well_location = WellLocation(
2224
- origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=0)
2378
+ origin=WellOrigin.TOP, offset=WellOffset(x=offset.x, y=offset.y, z=offset.z)
2225
2379
  )
2226
2380
 
2227
2381
  # The error handling here is a bit nuanced and also a bit broken:
@@ -2301,14 +2455,14 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
2301
2455
 
2302
2456
  self._protocol_core.set_last_location(location=loc, mount=self.get_mount())
2303
2457
 
2304
- # TODO(cm, 3.4.25): decide whether to allow users to try and do math on a potential SimulatedProbeResult
2305
2458
  def liquid_probe_without_recovery(
2306
2459
  self, well_core: WellCore, loc: Location
2307
2460
  ) -> LiquidTrackingType:
2308
2461
  labware_id = well_core.labware_id
2309
2462
  well_name = well_core.get_name()
2463
+ offset = LIQUID_PROBE_START_OFFSET_FROM_WELL_TOP
2310
2464
  well_location = WellLocation(
2311
- origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=2)
2465
+ origin=WellOrigin.TOP, offset=WellOffset(x=offset.x, y=offset.y, z=offset.z)
2312
2466
  )
2313
2467
  pipette_movement_conflict.check_safe_for_pipette_movement(
2314
2468
  engine_state=self._engine_client.state,
@@ -2339,14 +2493,6 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
2339
2493
  """Call a protocol delay."""
2340
2494
  self._protocol_core.delay(seconds=seconds, msg=None)
2341
2495
 
2342
- @contextmanager
2343
- def lpd_for_transfer(self, enable: bool) -> Generator[None, None, None]:
2344
- """Context manager for the instrument's LPD state during a transfer."""
2345
- global_lpd_enabled = self.get_liquid_presence_detection()
2346
- self.set_liquid_presence_detection(enable=enable)
2347
- yield
2348
- self.set_liquid_presence_detection(enable=global_lpd_enabled)
2349
-
2350
2496
 
2351
2497
  class _TipInfo(NamedTuple):
2352
2498
  tiprack_location: Location