opentrons 8.4.0a3__py2.py3-none-any.whl → 8.4.0a5__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of opentrons might be problematic. Click here for more details.

Files changed (42) hide show
  1. opentrons/legacy_commands/commands.py +83 -2
  2. opentrons/legacy_commands/helpers.py +59 -1
  3. opentrons/legacy_commands/types.py +30 -0
  4. opentrons/protocol_api/core/engine/instrument.py +182 -115
  5. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +6 -14
  6. opentrons/protocol_api/core/engine/transfer_components_executor.py +30 -25
  7. opentrons/protocol_api/core/instrument.py +8 -4
  8. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +9 -30
  9. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +8 -4
  10. opentrons/protocol_api/core/well.py +1 -1
  11. opentrons/protocol_api/instrument_context.py +144 -73
  12. opentrons/protocol_api/labware.py +26 -44
  13. opentrons/protocol_api/protocol_context.py +18 -16
  14. opentrons/protocol_engine/commands/__init__.py +38 -38
  15. opentrons/protocol_engine/commands/aspirate_while_tracking.py +38 -65
  16. opentrons/protocol_engine/commands/command_unions.py +33 -33
  17. opentrons/protocol_engine/commands/dispense_while_tracking.py +36 -72
  18. opentrons/protocol_engine/commands/labware_handling_common.py +6 -1
  19. opentrons/protocol_engine/commands/liquid_probe.py +1 -2
  20. opentrons/protocol_engine/commands/move_to_well.py +5 -11
  21. opentrons/protocol_engine/commands/{evotip_dispense.py → pressure_dispense.py} +27 -27
  22. opentrons/protocol_engine/commands/{evotip_seal_pipette.py → seal_pipette_to_tip.py} +32 -27
  23. opentrons/protocol_engine/commands/{evotip_unseal_pipette.py → unseal_pipette_from_tip.py} +22 -22
  24. opentrons/protocol_engine/execution/pipetting.py +1 -0
  25. opentrons/protocol_engine/labware_offset_standardization.py +22 -1
  26. opentrons/protocol_engine/resources/deck_configuration_provider.py +8 -4
  27. opentrons/protocol_engine/state/frustum_helpers.py +12 -4
  28. opentrons/protocol_engine/state/geometry.py +121 -72
  29. opentrons/protocol_engine/state/update_types.py +1 -1
  30. opentrons/protocol_engine/state/wells.py +1 -1
  31. opentrons/protocol_engine/types/__init__.py +6 -0
  32. opentrons/protocol_engine/types/well_position.py +18 -1
  33. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +1 -1
  34. opentrons/protocols/labware.py +23 -18
  35. opentrons/util/logging_config.py +94 -25
  36. opentrons/util/logging_queue_handler.py +61 -0
  37. {opentrons-8.4.0a3.dist-info → opentrons-8.4.0a5.dist-info}/METADATA +4 -4
  38. {opentrons-8.4.0a3.dist-info → opentrons-8.4.0a5.dist-info}/RECORD +42 -41
  39. {opentrons-8.4.0a3.dist-info → opentrons-8.4.0a5.dist-info}/LICENSE +0 -0
  40. {opentrons-8.4.0a3.dist-info → opentrons-8.4.0a5.dist-info}/WHEEL +0 -0
  41. {opentrons-8.4.0a3.dist-info → opentrons-8.4.0a5.dist-info}/entry_points.txt +0 -0
  42. {opentrons-8.4.0a3.dist-info → opentrons-8.4.0a5.dist-info}/top_level.txt +0 -0
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
  from contextlib import contextmanager
5
+ from itertools import dropwhile
5
6
  from typing import (
6
7
  Optional,
7
8
  TYPE_CHECKING,
@@ -11,6 +12,7 @@ from typing import (
11
12
  Tuple,
12
13
  NamedTuple,
13
14
  Generator,
15
+ Literal,
14
16
  )
15
17
  from opentrons.types import (
16
18
  Location,
@@ -48,8 +50,17 @@ from opentrons.protocol_engine.types import (
48
50
  AddressableOffsetVector,
49
51
  LiquidClassRecord,
50
52
  NextTipInfo,
53
+ PickUpTipWellLocation,
54
+ LiquidHandlingWellLocation,
55
+ )
56
+ from opentrons.protocol_engine.types import (
57
+ LiquidTrackingType,
58
+ WellLocationFunction,
59
+ )
60
+ from opentrons.protocol_engine.types.automatic_tip_selection import (
61
+ NoTipAvailable,
62
+ NoTipReason,
51
63
  )
52
- from opentrons.protocol_engine.types.liquid_level_detection import LiquidTrackingType
53
64
  from opentrons.protocol_engine.errors.exceptions import TipNotAttachedError
54
65
  from opentrons.protocol_engine.clients import SyncClient as EngineClient
55
66
  from opentrons.protocols.api_support.definitions import MAX_SUPPORTED_VERSION
@@ -59,6 +70,7 @@ from opentrons_shared_data.pipette.types import (
59
70
  )
60
71
  from opentrons_shared_data.errors.exceptions import (
61
72
  UnsupportedHardwareCommand,
73
+ CommandPreconditionViolated,
62
74
  )
63
75
  from opentrons_shared_data.liquid_classes.liquid_class_definition import BlowoutLocation
64
76
  from opentrons.protocol_api._nozzle_layout import NozzleLayout
@@ -222,10 +234,11 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
222
234
  (
223
235
  well_location,
224
236
  dynamic_liquid_tracking,
225
- ) = self._engine_client.state.geometry.get_relative_liquid_handling_well_location(
237
+ ) = self._engine_client.state.geometry.get_relative_well_location(
226
238
  labware_id=labware_id,
227
239
  well_name=well_name,
228
240
  absolute_point=location.point,
241
+ location_type=WellLocationFunction.LIQUID_HANDLING,
229
242
  meniscus_tracking=meniscus_tracking,
230
243
  )
231
244
  pipette_movement_conflict.check_safe_for_pipette_movement(
@@ -235,8 +248,8 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
235
248
  well_name=well_name,
236
249
  well_location=well_location,
237
250
  )
251
+ assert isinstance(well_location, LiquidHandlingWellLocation)
238
252
  if dynamic_liquid_tracking:
239
-
240
253
  self._engine_client.execute_command(
241
254
  cmd.AspirateWhileTrackingParams(
242
255
  pipetteId=self._pipette_id,
@@ -334,12 +347,14 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
334
347
  (
335
348
  well_location,
336
349
  dynamic_liquid_tracking,
337
- ) = self._engine_client.state.geometry.get_relative_liquid_handling_well_location(
350
+ ) = self._engine_client.state.geometry.get_relative_well_location(
338
351
  labware_id=labware_id,
339
352
  well_name=well_name,
340
353
  absolute_point=location.point,
354
+ location_type=WellLocationFunction.LIQUID_HANDLING,
341
355
  meniscus_tracking=meniscus_tracking,
342
356
  )
357
+ assert isinstance(well_location, LiquidHandlingWellLocation)
343
358
  pipette_movement_conflict.check_safe_for_pipette_movement(
344
359
  engine_state=self._engine_client.state,
345
360
  pipette_id=self._pipette_id,
@@ -425,13 +440,16 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
425
440
  well_name = well_core.get_name()
426
441
  labware_id = well_core.labware_id
427
442
 
428
- well_location = (
429
- self._engine_client.state.geometry.get_relative_well_location(
430
- labware_id=labware_id,
431
- well_name=well_name,
432
- absolute_point=location.point,
433
- )
443
+ (
444
+ well_location,
445
+ _,
446
+ ) = self._engine_client.state.geometry.get_relative_well_location(
447
+ labware_id=labware_id,
448
+ well_name=well_name,
449
+ absolute_point=location.point,
450
+ location_type=WellLocationFunction.BASE,
434
451
  )
452
+
435
453
  pipette_movement_conflict.check_safe_for_pipette_movement(
436
454
  engine_state=self._engine_client.state,
437
455
  pipette_id=self._pipette_id,
@@ -439,6 +457,7 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
439
457
  well_name=well_name,
440
458
  well_location=well_location,
441
459
  )
460
+ assert isinstance(well_location, WellLocation)
442
461
  self._engine_client.execute_command(
443
462
  cmd.BlowOutParams(
444
463
  pipetteId=self._pipette_id,
@@ -533,12 +552,14 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
533
552
  well_name = well_core.get_name()
534
553
  labware_id = well_core.labware_id
535
554
 
536
- well_location = (
537
- self._engine_client.state.geometry.get_relative_pick_up_tip_well_location(
538
- labware_id=labware_id,
539
- well_name=well_name,
540
- absolute_point=location.point,
541
- )
555
+ (
556
+ well_location,
557
+ _,
558
+ ) = self._engine_client.state.geometry.get_relative_well_location(
559
+ labware_id=labware_id,
560
+ well_name=well_name,
561
+ absolute_point=location.point,
562
+ location_type=WellLocationFunction.PICK_UP_TIP,
542
563
  )
543
564
  pipette_movement_conflict.check_safe_for_tip_pickup_and_return(
544
565
  engine_state=self._engine_client.state,
@@ -552,7 +573,7 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
552
573
  well_name=well_name,
553
574
  well_location=well_location,
554
575
  )
555
-
576
+ assert isinstance(well_location, PickUpTipWellLocation)
556
577
  self._engine_client.execute_command(
557
578
  cmd.PickUpTipParams(
558
579
  pipetteId=self._pipette_id,
@@ -590,12 +611,14 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
590
611
  scrape_tips = False
591
612
 
592
613
  if location is not None:
593
- relative_well_location = (
594
- self._engine_client.state.geometry.get_relative_well_location(
595
- labware_id=labware_id,
596
- well_name=well_name,
597
- absolute_point=location.point,
598
- )
614
+ (
615
+ relative_well_location,
616
+ _,
617
+ ) = self._engine_client.state.geometry.get_relative_well_location(
618
+ labware_id=labware_id,
619
+ well_name=well_name,
620
+ absolute_point=location.point,
621
+ location_type=WellLocationFunction.DROP_TIP,
599
622
  )
600
623
 
601
624
  well_location = DropTipWellLocation(
@@ -725,20 +748,33 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
725
748
  force_direct: bool,
726
749
  minimum_z_height: Optional[float],
727
750
  speed: Optional[float],
751
+ check_for_movement_conflicts: bool = True,
728
752
  ) -> None:
729
753
  if well_core is not None:
730
754
  if isinstance(location, (TrashBin, WasteChute)):
731
755
  raise ValueError("Trash Bin and Waste Chute have no Wells.")
732
756
  labware_id = well_core.labware_id
733
757
  well_name = well_core.get_name()
734
- well_location = (
735
- self._engine_client.state.geometry.get_relative_well_location(
758
+ (
759
+ well_location,
760
+ _,
761
+ ) = self._engine_client.state.geometry.get_relative_well_location(
762
+ labware_id=labware_id,
763
+ well_name=well_name,
764
+ absolute_point=location.point,
765
+ location_type=WellLocationFunction.LIQUID_HANDLING,
766
+ )
767
+ assert isinstance(well_location, LiquidHandlingWellLocation)
768
+ if well_location.volumeOffset and well_location.volumeOffset != 0:
769
+ raise ValueError("volume offset not supported with move_to")
770
+ if check_for_movement_conflicts:
771
+ pipette_movement_conflict.check_safe_for_pipette_movement(
772
+ engine_state=self._engine_client.state,
773
+ pipette_id=self._pipette_id,
736
774
  labware_id=labware_id,
737
775
  well_name=well_name,
738
- absolute_point=location.point,
776
+ well_location=well_location,
739
777
  )
740
- )
741
-
742
778
  self._engine_client.execute_command(
743
779
  cmd.MoveToWellParams(
744
780
  pipetteId=self._pipette_id,
@@ -779,16 +815,18 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
779
815
  ) -> None:
780
816
  labware_id = well_core.labware_id
781
817
  well_name = well_core.get_name()
782
- well_location = (
783
- self._engine_client.state.geometry.get_relative_pick_up_tip_well_location(
784
- labware_id=labware_id,
785
- well_name=well_name,
786
- absolute_point=location.point,
787
- )
818
+ (
819
+ well_location,
820
+ _,
821
+ ) = self._engine_client.state.geometry.get_relative_well_location(
822
+ labware_id=labware_id,
823
+ well_name=well_name,
824
+ absolute_point=location.point,
825
+ location_type=WellLocationFunction.PICK_UP_TIP,
788
826
  )
789
-
827
+ assert isinstance(well_location, PickUpTipWellLocation)
790
828
  self._engine_client.execute_command(
791
- cmd.EvotipSealPipetteParams(
829
+ cmd.SealPipetteToTipParams(
792
830
  pipetteId=self._pipette_id,
793
831
  labwareId=labware_id,
794
832
  wellName=well_name,
@@ -801,12 +839,14 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
801
839
  labware_id = well_core.labware_id
802
840
 
803
841
  if location is not None:
804
- relative_well_location = (
805
- self._engine_client.state.geometry.get_relative_well_location(
806
- labware_id=labware_id,
807
- well_name=well_name,
808
- absolute_point=location.point,
809
- )
842
+ (
843
+ relative_well_location,
844
+ _,
845
+ ) = self._engine_client.state.geometry.get_relative_well_location(
846
+ labware_id=labware_id,
847
+ well_name=well_name,
848
+ absolute_point=location.point,
849
+ location_type=WellLocationFunction.BASE,
810
850
  )
811
851
 
812
852
  well_location = DropTipWellLocation(
@@ -824,7 +864,7 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
824
864
  well_location=well_location,
825
865
  )
826
866
  self._engine_client.execute_command(
827
- cmd.EvotipUnsealPipetteParams(
867
+ cmd.UnsealPipetteFromTipParams(
828
868
  pipetteId=self._pipette_id,
829
869
  labwareId=labware_id,
830
870
  wellName=well_name,
@@ -860,10 +900,11 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
860
900
  (
861
901
  well_location,
862
902
  dynamic_tracking,
863
- ) = self._engine_client.state.geometry.get_relative_liquid_handling_well_location(
903
+ ) = self._engine_client.state.geometry.get_relative_well_location(
864
904
  labware_id=labware_id,
865
905
  well_name=well_name,
866
906
  absolute_point=location.point,
907
+ location_type=WellLocationFunction.LIQUID_HANDLING,
867
908
  )
868
909
  pipette_movement_conflict.check_safe_for_pipette_movement(
869
910
  engine_state=self._engine_client.state,
@@ -872,8 +913,9 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
872
913
  well_name=well_name,
873
914
  well_location=well_location,
874
915
  )
916
+ assert isinstance(well_location, LiquidHandlingWellLocation)
875
917
  self._engine_client.execute_command(
876
- cmd.EvotipDispenseParams(
918
+ cmd.PressureDispenseParams(
877
919
  pipetteId=self._pipette_id,
878
920
  labwareId=labware_id,
879
921
  wellName=well_name,
@@ -1115,9 +1157,11 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1115
1157
  tiprack=tiprack_uri,
1116
1158
  aspirate=transfer_properties.aspirate.as_shared_data_model(),
1117
1159
  singleDispense=transfer_properties.dispense.as_shared_data_model(),
1118
- multiDispense=transfer_properties.multi_dispense.as_shared_data_model()
1119
- if transfer_properties.multi_dispense
1120
- else None,
1160
+ multiDispense=(
1161
+ transfer_properties.multi_dispense.as_shared_data_model()
1162
+ if transfer_properties.multi_dispense
1163
+ else None
1164
+ ),
1121
1165
  )
1122
1166
  result = self._engine_client.execute_command_without_recovery(
1123
1167
  cmd.LoadLiquidClassParams(
@@ -1127,19 +1171,39 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1127
1171
  return result.liquidClassId
1128
1172
 
1129
1173
  def get_next_tip(
1130
- self, tip_racks: List[LabwareCore], starting_well: Optional[str]
1174
+ self, tip_racks: List[LabwareCore], starting_well: Optional[WellCore]
1131
1175
  ) -> Optional[NextTipInfo]:
1132
1176
  """Get the next tip to pick up."""
1177
+ if starting_well is not None:
1178
+ # Drop tip racks until the one with the starting tip is reached (if any)
1179
+ valid_tip_racks = list(
1180
+ dropwhile(
1181
+ lambda tr: starting_well.labware_id != tr.labware_id, tip_racks
1182
+ )
1183
+ )
1184
+ else:
1185
+ valid_tip_racks = tip_racks
1186
+
1133
1187
  result = self._engine_client.execute_command_without_recovery(
1134
1188
  cmd.GetNextTipParams(
1135
1189
  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
- )
1190
+ labwareIds=[tip_rack.labware_id for tip_rack in valid_tip_racks],
1191
+ startingTipWell=starting_well.get_name()
1192
+ if starting_well is not None
1193
+ else None,
1194
+ )
1195
+ )
1196
+ next_tip_info = result.nextTipInfo
1197
+ if isinstance(next_tip_info, NoTipAvailable):
1198
+ if next_tip_info.noTipReason == NoTipReason.STARTING_TIP_WITH_PARTIAL:
1199
+ raise CommandPreconditionViolated(
1200
+ "Automatic tip tracking is not available when using a partial pipette"
1201
+ " nozzle configuration and InstrumentContext.starting_tip."
1202
+ " Switch to a full configuration or set starting_tip to None."
1203
+ )
1204
+ return None
1205
+ else:
1206
+ return next_tip_info
1143
1207
 
1144
1208
  def transfer_with_liquid_class( # noqa: C901
1145
1209
  self,
@@ -1149,6 +1213,7 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1149
1213
  dest: List[Tuple[Location, WellCore]],
1150
1214
  new_tip: TransferTipPolicyV2,
1151
1215
  tip_racks: List[Tuple[Location, LabwareCore]],
1216
+ starting_tip: Optional[WellCore],
1152
1217
  trash_location: Union[Location, TrashBin, WasteChute],
1153
1218
  return_tip: bool,
1154
1219
  ) -> None:
@@ -1234,11 +1299,12 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1234
1299
  def _pick_up_tip() -> WellCore:
1235
1300
  next_tip = self.get_next_tip(
1236
1301
  tip_racks=[core for loc, core in tip_racks],
1237
- starting_well=None,
1302
+ starting_well=starting_tip,
1238
1303
  )
1239
1304
  if next_tip is None:
1240
1305
  raise RuntimeError(
1241
- f"No tip available among {tip_racks} for this transfer."
1306
+ f"No tip available among the tipracks assigned for {self.get_pipette_name()}:"
1307
+ f" {[f'{tip_rack[1].get_display_name()} in {tip_rack[1].get_deck_slot()}' for tip_rack in tip_racks]}"
1242
1308
  )
1243
1309
  (
1244
1310
  tiprack_loc,
@@ -1325,9 +1391,11 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1325
1391
  transfer_properties=transfer_props,
1326
1392
  transfer_type=tx_comps_executor.TransferType.ONE_TO_ONE,
1327
1393
  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,
1394
+ add_final_air_gap=(
1395
+ False
1396
+ if is_last_step and new_tip == TransferTipPolicyV2.NEVER
1397
+ else True
1398
+ ),
1331
1399
  trash_location=trash_location,
1332
1400
  )
1333
1401
  prev_src = step_source
@@ -1341,8 +1409,9 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1341
1409
  volume: float,
1342
1410
  source: Tuple[Location, WellCore],
1343
1411
  dest: List[Tuple[Location, WellCore]],
1344
- new_tip: TransferTipPolicyV2,
1412
+ new_tip: Literal[TransferTipPolicyV2.NEVER, TransferTipPolicyV2.ONCE],
1345
1413
  tip_racks: List[Tuple[Location, LabwareCore]],
1414
+ starting_tip: Optional[WellCore],
1346
1415
  trash_location: Union[Location, TrashBin, WasteChute],
1347
1416
  return_tip: bool,
1348
1417
  ) -> None:
@@ -1356,8 +1425,7 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1356
1425
  dest: List of destination wells, with each well represented as a tuple of
1357
1426
  types.Location and WellCore.
1358
1427
  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'.
1428
+ new_tip: Whether the transfer should use a new tip 'once' or 'never'.
1361
1429
  tiprack_uri: The URI of the tiprack that the transfer settings are for.
1362
1430
  tip_drop_location: Location where the tip will be dropped (if appropriate).
1363
1431
 
@@ -1373,6 +1441,8 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1373
1441
  raise RuntimeError(
1374
1442
  "No tipracks found for pipette in order to perform transfer"
1375
1443
  )
1444
+ assert new_tip in [TransferTipPolicyV2.NEVER, TransferTipPolicyV2.ONCE]
1445
+
1376
1446
  tiprack_uri_for_transfer_props = tip_racks[0][1].get_uri()
1377
1447
  working_volume = min(
1378
1448
  self.get_max_volume(),
@@ -1424,6 +1494,7 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1424
1494
  dest=dest,
1425
1495
  new_tip=new_tip,
1426
1496
  tip_racks=tip_racks,
1497
+ starting_tip=starting_tip,
1427
1498
  trash_location=trash_location,
1428
1499
  return_tip=return_tip,
1429
1500
  )
@@ -1474,11 +1545,12 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1474
1545
  def _pick_up_tip() -> WellCore:
1475
1546
  next_tip = self.get_next_tip(
1476
1547
  tip_racks=[core for loc, core in tip_racks],
1477
- starting_well=None,
1548
+ starting_well=starting_tip,
1478
1549
  )
1479
1550
  if next_tip is None:
1480
1551
  raise RuntimeError(
1481
- f"No tip available among {tip_racks} for this transfer."
1552
+ f"No tip available among the tipracks assigned for {self.get_pipette_name()}:"
1553
+ f" {[f'{tip_rack[1].get_display_name()} in {tip_rack[1].get_deck_slot()}' for tip_rack in tip_racks]}"
1482
1554
  )
1483
1555
  (
1484
1556
  tiprack_loc,
@@ -1498,7 +1570,6 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1498
1570
  )
1499
1571
  return tip_well
1500
1572
 
1501
- tip_used = False
1502
1573
  if new_tip != TransferTipPolicyV2.NEVER:
1503
1574
  last_tip_picked_up_from = _pick_up_tip()
1504
1575
 
@@ -1543,16 +1614,6 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1543
1614
  )
1544
1615
  )
1545
1616
 
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
1617
  use_single_dispense = False
1557
1618
  if total_aspirate_volume == volume and len(vol_dest_combo) == 1:
1558
1619
  # We are only doing a single transfer. Either because this is the last
@@ -1569,7 +1630,7 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1569
1630
  and not transfer_props.multi_dispense.retract.blowout.enabled
1570
1631
  ):
1571
1632
  raise RuntimeError(
1572
- "Distribute liquid uses a disposal volume but location for disposing of"
1633
+ "Distribute uses a disposal volume but location for disposing of"
1573
1634
  " the disposal volume cannot be found when blowout is disabled."
1574
1635
  " Specify a blowout location and enable blowout when using a disposal volume."
1575
1636
  )
@@ -1610,9 +1671,11 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1610
1671
  transfer_properties=transfer_props,
1611
1672
  transfer_type=tx_comps_executor.TransferType.ONE_TO_MANY,
1612
1673
  tip_contents=tip_contents,
1613
- add_final_air_gap=False
1614
- if is_last_step and new_tip == TransferTipPolicyV2.NEVER
1615
- else True,
1674
+ add_final_air_gap=(
1675
+ False
1676
+ if is_last_step and new_tip == TransferTipPolicyV2.NEVER
1677
+ else True
1678
+ ),
1616
1679
  trash_location=trash_location,
1617
1680
  )
1618
1681
  else:
@@ -1623,15 +1686,16 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1623
1686
  transfer_properties=transfer_props,
1624
1687
  transfer_type=tx_comps_executor.TransferType.ONE_TO_MANY,
1625
1688
  tip_contents=tip_contents,
1626
- add_final_air_gap=False
1627
- if is_last_step and new_tip == TransferTipPolicyV2.NEVER
1628
- else True,
1689
+ add_final_air_gap=(
1690
+ False
1691
+ if is_last_step and new_tip == TransferTipPolicyV2.NEVER
1692
+ else True
1693
+ ),
1629
1694
  trash_location=trash_location,
1630
1695
  conditioning_volume=conditioning_vol,
1631
1696
  disposal_volume=disposal_vol,
1632
1697
  )
1633
1698
  is_first_step = False
1634
- tip_used = True
1635
1699
 
1636
1700
  if new_tip != TransferTipPolicyV2.NEVER:
1637
1701
  _drop_tip()
@@ -1663,8 +1727,9 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1663
1727
  volume: float,
1664
1728
  source: List[Tuple[Location, WellCore]],
1665
1729
  dest: Tuple[Location, WellCore],
1666
- new_tip: TransferTipPolicyV2,
1730
+ new_tip: Literal[TransferTipPolicyV2.NEVER, TransferTipPolicyV2.ONCE],
1667
1731
  tip_racks: List[Tuple[Location, LabwareCore]],
1732
+ starting_tip: Optional[WellCore],
1668
1733
  trash_location: Union[Location, TrashBin, WasteChute],
1669
1734
  return_tip: bool,
1670
1735
  ) -> None:
@@ -1672,7 +1737,7 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1672
1737
  raise RuntimeError(
1673
1738
  "No tipracks found for pipette in order to perform transfer"
1674
1739
  )
1675
-
1740
+ assert new_tip in [TransferTipPolicyV2.NEVER, TransferTipPolicyV2.ONCE]
1676
1741
  tiprack_uri_for_transfer_props = tip_racks[0][1].get_uri()
1677
1742
  try:
1678
1743
  transfer_props = liquid_class.get_for(
@@ -1747,11 +1812,12 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1747
1812
  def _pick_up_tip() -> WellCore:
1748
1813
  next_tip = self.get_next_tip(
1749
1814
  tip_racks=[core for loc, core in tip_racks],
1750
- starting_well=None,
1815
+ starting_well=starting_tip,
1751
1816
  )
1752
1817
  if next_tip is None:
1753
1818
  raise RuntimeError(
1754
- f"No tip available among {tip_racks} for this transfer."
1819
+ f"No tip available among the tipracks assigned for {self.get_pipette_name()}:"
1820
+ f" {[ f'{tip_rack[1].get_display_name()} in {tip_rack[1].get_deck_slot()}' for tip_rack in tip_racks]}"
1755
1821
  )
1756
1822
  (
1757
1823
  tiprack_loc,
@@ -1774,7 +1840,6 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1774
1840
  if new_tip == TransferTipPolicyV2.ONCE:
1775
1841
  last_tip_picked_up_from = _pick_up_tip()
1776
1842
 
1777
- prev_src: Optional[Tuple[Location, WellCore]] = None
1778
1843
  tip_contents = [
1779
1844
  tx_comps_executor.LiquidAndAirGapPair(
1780
1845
  liquid=0,
@@ -1782,6 +1847,7 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1782
1847
  )
1783
1848
  ]
1784
1849
  next_step_volume, next_source = next(source_per_volume_step)
1850
+ is_first_step = True
1785
1851
  is_last_step = False
1786
1852
  while not is_last_step:
1787
1853
  total_dispense_volume = 0.0
@@ -1796,31 +1862,29 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1796
1862
  is_last_step = True
1797
1863
  break
1798
1864
 
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
- ):
1865
+ if (
1866
+ self.get_liquid_presence_detection()
1867
+ and new_tip != TransferTipPolicyV2.NEVER
1868
+ and is_first_step
1869
+ ):
1870
+ enable_lpd = True
1871
+ else:
1872
+ enable_lpd = False
1873
+
1874
+ for step_num, (step_volume, step_source) in enumerate(vol_aspirate_combo):
1875
+ with self.lpd_for_transfer(enable=enable_lpd):
1814
1876
  tip_contents = self.aspirate_liquid_class(
1815
1877
  volume=step_volume,
1816
1878
  source=step_source,
1817
1879
  transfer_properties=transfer_props,
1818
1880
  transfer_type=tx_comps_executor.TransferType.MANY_TO_ONE,
1819
1881
  tip_contents=tip_contents,
1820
- volume_for_pipette_mode_configuration=total_dispense_volume
1821
- if step_num == 0
1822
- else None,
1882
+ volume_for_pipette_mode_configuration=(
1883
+ total_dispense_volume if step_num == 0 else None
1884
+ ),
1823
1885
  )
1886
+ is_first_step = False
1887
+ enable_lpd = False
1824
1888
  tip_contents = self.dispense_liquid_class(
1825
1889
  volume=total_dispense_volume,
1826
1890
  dest=dest,
@@ -1828,12 +1892,13 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1828
1892
  transfer_properties=transfer_props,
1829
1893
  transfer_type=tx_comps_executor.TransferType.MANY_TO_ONE,
1830
1894
  tip_contents=tip_contents,
1831
- add_final_air_gap=False
1832
- if is_last_step and new_tip == TransferTipPolicyV2.NEVER
1833
- else True,
1895
+ add_final_air_gap=(
1896
+ False
1897
+ if is_last_step and new_tip == TransferTipPolicyV2.NEVER
1898
+ else True
1899
+ ),
1834
1900
  trash_location=trash_location,
1835
1901
  )
1836
- prev_src = next_source
1837
1902
  if new_tip != TransferTipPolicyV2.NEVER:
1838
1903
  _drop_tip()
1839
1904
 
@@ -1881,9 +1946,11 @@ class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
1881
1946
  aspirate_props = transfer_properties.aspirate
1882
1947
  tx_commons.check_valid_liquid_class_volume_parameters(
1883
1948
  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,
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
+ ),
1887
1954
  disposal_volume=0, # Disposal volume is accounted for in aspirate vol
1888
1955
  max_volume=self.get_working_volume(),
1889
1956
  )
@@ -19,14 +19,9 @@ from opentrons.protocol_engine import (
19
19
  StateView,
20
20
  DeckSlotLocation,
21
21
  OnLabwareLocation,
22
- WellLocation,
23
- LiquidHandlingWellLocation,
24
- PickUpTipWellLocation,
25
22
  DropTipWellLocation,
26
23
  )
27
- from opentrons.protocol_engine.types import (
28
- StagingSlotLocation,
29
- )
24
+ from opentrons.protocol_engine.types import StagingSlotLocation, WellLocationType
30
25
  from opentrons.types import DeckSlotName, StagingSlotName, Point
31
26
  from . import point_calculations
32
27
 
@@ -69,12 +64,7 @@ def check_safe_for_pipette_movement( # noqa: C901
69
64
  pipette_id: str,
70
65
  labware_id: str,
71
66
  well_name: str,
72
- well_location: Union[
73
- WellLocation,
74
- LiquidHandlingWellLocation,
75
- PickUpTipWellLocation,
76
- DropTipWellLocation,
77
- ],
67
+ well_location: WellLocationType,
78
68
  ) -> None:
79
69
  """Check if the labware is safe to move to with a pipette in partial tip configuration.
80
70
 
@@ -100,12 +90,14 @@ def check_safe_for_pipette_movement( # noqa: C901
100
90
  partially_configured=True,
101
91
  )
102
92
  well_location_point = engine_state.geometry.get_well_position(
103
- labware_id=labware_id, well_name=well_name, well_location=well_location
93
+ labware_id=labware_id,
94
+ well_name=well_name,
95
+ well_location=well_location,
96
+ pipette_id=pipette_id,
104
97
  )
105
98
  primary_nozzle = engine_state.pipettes.get_primary_nozzle(pipette_id)
106
99
 
107
100
  destination_cp = _get_critical_point_to_use(engine_state, labware_id)
108
-
109
101
  pipette_bounds_at_well_location = (
110
102
  engine_state.pipettes.get_pipette_bounds_at_specified_move_to_position(
111
103
  pipette_id=pipette_id,