opentrons 8.4.0a13__py2.py3-none-any.whl → 8.5.0a1__py2.py3-none-any.whl

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

Potentially problematic release.


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

Files changed (57) hide show
  1. opentrons/config/defaults_ot3.py +1 -1
  2. opentrons/legacy_commands/commands.py +16 -4
  3. opentrons/legacy_commands/robot_commands.py +51 -0
  4. opentrons/legacy_commands/types.py +91 -2
  5. opentrons/protocol_api/_liquid.py +60 -15
  6. opentrons/protocol_api/_liquid_properties.py +137 -90
  7. opentrons/protocol_api/_transfer_liquid_validation.py +10 -6
  8. opentrons/protocol_api/core/engine/instrument.py +172 -75
  9. opentrons/protocol_api/core/engine/protocol.py +13 -14
  10. opentrons/protocol_api/core/engine/robot.py +2 -2
  11. opentrons/protocol_api/core/engine/transfer_components_executor.py +157 -126
  12. opentrons/protocol_api/core/engine/well.py +16 -0
  13. opentrons/protocol_api/core/instrument.py +2 -2
  14. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +2 -2
  15. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +1 -1
  16. opentrons/protocol_api/core/legacy/legacy_well_core.py +8 -0
  17. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +2 -2
  18. opentrons/protocol_api/core/protocol.py +2 -2
  19. opentrons/protocol_api/core/well.py +8 -0
  20. opentrons/protocol_api/instrument_context.py +377 -86
  21. opentrons/protocol_api/labware.py +10 -0
  22. opentrons/protocol_api/protocol_context.py +79 -4
  23. opentrons/protocol_api/robot_context.py +48 -6
  24. opentrons/protocol_api/validation.py +15 -8
  25. opentrons/protocol_engine/commands/command_unions.py +10 -10
  26. opentrons/protocol_engine/commands/generate_command_schema.py +1 -1
  27. opentrons/protocol_engine/commands/get_next_tip.py +2 -2
  28. opentrons/protocol_engine/commands/pick_up_tip.py +9 -3
  29. opentrons/protocol_engine/commands/robot/__init__.py +20 -20
  30. opentrons/protocol_engine/commands/robot/close_gripper_jaw.py +34 -24
  31. opentrons/protocol_engine/commands/robot/open_gripper_jaw.py +29 -20
  32. opentrons/protocol_engine/commands/seal_pipette_to_tip.py +1 -1
  33. opentrons/protocol_engine/commands/unsafe/__init__.py +17 -1
  34. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +1 -2
  35. opentrons/protocol_engine/execution/labware_movement.py +9 -2
  36. opentrons/protocol_engine/execution/movement.py +12 -9
  37. opentrons/protocol_engine/execution/queue_worker.py +8 -1
  38. opentrons/protocol_engine/execution/thermocycler_movement_flagger.py +52 -19
  39. opentrons/protocol_engine/state/_well_math.py +2 -2
  40. opentrons/protocol_engine/state/commands.py +14 -28
  41. opentrons/protocol_engine/state/frustum_helpers.py +11 -7
  42. opentrons/protocol_engine/state/modules.py +1 -1
  43. opentrons/protocol_engine/state/pipettes.py +8 -0
  44. opentrons/protocol_engine/state/tips.py +46 -83
  45. opentrons/protocol_engine/state/update_types.py +8 -23
  46. opentrons/protocol_runner/legacy_command_mapper.py +11 -4
  47. opentrons/protocol_runner/run_orchestrator.py +1 -1
  48. opentrons/protocols/advanced_control/transfers/common.py +54 -11
  49. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +1 -1
  50. opentrons/protocols/api_support/definitions.py +1 -1
  51. opentrons/types.py +6 -6
  52. {opentrons-8.4.0a13.dist-info → opentrons-8.5.0a1.dist-info}/METADATA +4 -4
  53. {opentrons-8.4.0a13.dist-info → opentrons-8.5.0a1.dist-info}/RECORD +57 -56
  54. {opentrons-8.4.0a13.dist-info → opentrons-8.5.0a1.dist-info}/LICENSE +0 -0
  55. {opentrons-8.4.0a13.dist-info → opentrons-8.5.0a1.dist-info}/WHEEL +0 -0
  56. {opentrons-8.4.0a13.dist-info → opentrons-8.5.0a1.dist-info}/entry_points.txt +0 -0
  57. {opentrons-8.4.0a13.dist-info → opentrons-8.5.0a1.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
  import logging
3
3
  from contextlib import ExitStack
4
- from typing import Any, List, Optional, Sequence, Union, cast, Dict
4
+ from typing import Any, List, Optional, Sequence, Union, cast, Tuple
5
5
  from opentrons_shared_data.errors.exceptions import (
6
6
  CommandPreconditionViolated,
7
7
  CommandParameterLimitViolated,
@@ -12,7 +12,10 @@ from opentrons_shared_data.errors.exceptions import (
12
12
  from opentrons.legacy_broker import LegacyBroker
13
13
  from opentrons.hardware_control.dev_types import PipetteDict
14
14
  from opentrons import types
15
- from opentrons.legacy_commands import commands as cmds
15
+ from opentrons.legacy_commands import (
16
+ commands as cmds,
17
+ protocol_commands as protocol_cmds,
18
+ )
16
19
 
17
20
  from opentrons.legacy_commands import publisher
18
21
  from opentrons.protocols.advanced_control.mix import mix_from_kwargs
@@ -29,7 +32,7 @@ from opentrons.protocols.api_support.util import (
29
32
  UnsupportedAPIError,
30
33
  )
31
34
 
32
- from .core.common import InstrumentCore, ProtocolCore
35
+ from .core.common import InstrumentCore, ProtocolCore, WellCore
33
36
  from .core.engine import ENGINE_CORE_API_VERSION
34
37
  from .core.legacy.legacy_instrument_core import LegacyInstrumentCore
35
38
  from .config import Clearances
@@ -70,6 +73,16 @@ _AIR_GAP_TRACKING_ADDED_IN = APIVersion(2, 22)
70
73
  AdvancedLiquidHandling = v1_transfer.AdvancedLiquidHandling
71
74
 
72
75
 
76
+ class _Unset:
77
+ """A sentinel value when no value has been supplied for an argument.
78
+ User code should never use this explicitly."""
79
+
80
+ def __repr__(self) -> str:
81
+ # Without this, the generated docs render the argument as
82
+ # "<opentrons.protocol_api.instrument_context._Unset object at 0x1234>"
83
+ return self.__class__.__name__
84
+
85
+
73
86
  class InstrumentContext(publisher.CommandPublisher):
74
87
  """
75
88
  A context for a specific pipette or instrument.
@@ -173,11 +186,12 @@ class InstrumentContext(publisher.CommandPublisher):
173
186
  return self._core.get_minimum_liquid_sense_height()
174
187
 
175
188
  @requires_version(2, 0)
176
- def aspirate(
189
+ def aspirate( # noqa: C901
177
190
  self,
178
191
  volume: Optional[float] = None,
179
192
  location: Optional[Union[types.Location, labware.Well]] = None,
180
193
  rate: float = 1.0,
194
+ flow_rate: Optional[float] = None,
181
195
  ) -> InstrumentContext:
182
196
  """
183
197
  Draw liquid into a pipette tip.
@@ -214,6 +228,9 @@ class InstrumentContext(publisher.CommandPublisher):
214
228
  <flow_rate>`. If not specified, defaults to 1.0. See
215
229
  :ref:`new-plunger-flow-rates`.
216
230
  :type rate: float
231
+ :param flow_rate: The absolute flow rate in µL/s. If ``flow_rate`` is specified,
232
+ ``rate`` must not be set.
233
+ :type flow_rate: float
217
234
  :returns: This instance.
218
235
 
219
236
  .. note::
@@ -223,15 +240,30 @@ class InstrumentContext(publisher.CommandPublisher):
223
240
  ``location``, specify it as a keyword argument:
224
241
  ``pipette.aspirate(location=plate['A1'])``
225
242
 
243
+ .. versionchanged:: 2.24
244
+ Added the ``flow_rate`` parameter.
226
245
  """
246
+ if flow_rate is not None:
247
+ if self.api_version < APIVersion(2, 24):
248
+ raise APIVersionError(
249
+ api_element="flow_rate",
250
+ until_version="2.24",
251
+ current_version=f"{self.api_version}",
252
+ )
253
+ if rate != 1.0:
254
+ raise ValueError("rate must not be set if flow_rate is specified")
255
+ rate = flow_rate / self._core.get_aspirate_flow_rate()
256
+ else:
257
+ flow_rate = self._core.get_aspirate_flow_rate(rate)
258
+
227
259
  _log.debug(
228
- "aspirate {} from {} at {}".format(
229
- volume, location if location else "current position", rate
260
+ "aspirate {} from {} at {} µL/s".format(
261
+ volume, location if location else "current position", flow_rate
230
262
  )
231
263
  )
232
264
 
233
265
  move_to_location: types.Location
234
- well: Optional[labware.Well] = None
266
+ well: Optional[labware.Well]
235
267
  last_location = self._get_last_location_by_api_version()
236
268
  try:
237
269
  target = validation.validate_location(
@@ -245,7 +277,7 @@ class InstrumentContext(publisher.CommandPublisher):
245
277
  "knows where it is."
246
278
  ) from e
247
279
 
248
- if isinstance(target, (TrashBin, WasteChute)):
280
+ if isinstance(target, validation.DisposalTarget):
249
281
  raise ValueError(
250
282
  "Trash Bin and Waste Chute are not acceptable location parameters for Aspirate commands."
251
283
  )
@@ -263,7 +295,6 @@ class InstrumentContext(publisher.CommandPublisher):
263
295
  c_vol = self._core.get_available_volume() if volume is None else volume
264
296
  else:
265
297
  c_vol = self._core.get_available_volume() if not volume else volume
266
- flow_rate = self._core.get_aspirate_flow_rate(rate)
267
298
 
268
299
  if (
269
300
  self.api_version >= APIVersion(2, 20)
@@ -299,7 +330,7 @@ class InstrumentContext(publisher.CommandPublisher):
299
330
  return self
300
331
 
301
332
  @requires_version(2, 0)
302
- def dispense(
333
+ def dispense( # noqa: C901
303
334
  self,
304
335
  volume: Optional[float] = None,
305
336
  location: Optional[
@@ -307,6 +338,7 @@ class InstrumentContext(publisher.CommandPublisher):
307
338
  ] = None,
308
339
  rate: float = 1.0,
309
340
  push_out: Optional[float] = None,
341
+ flow_rate: Optional[float] = None,
310
342
  ) -> InstrumentContext:
311
343
  """
312
344
  Dispense liquid from a pipette tip.
@@ -363,15 +395,19 @@ class InstrumentContext(publisher.CommandPublisher):
363
395
  <flow_rate>`. If not specified, defaults to 1.0. See
364
396
  :ref:`new-plunger-flow-rates`.
365
397
  :type rate: float
398
+
366
399
  :param push_out: Continue past the plunger bottom to help ensure all liquid
367
400
  leaves the tip. Measured in µL. The default value is ``None``.
368
401
 
369
402
  When not specified or set to ``None``, the plunger moves by a non-zero default amount.
370
403
 
371
-
372
404
  For a table of default values, see :ref:`push-out-dispense`.
373
405
  :type push_out: float
374
406
 
407
+ :param flow_rate: The absolute flow rate in µL/s. If ``flow_rate`` is specified,
408
+ ``rate`` must not be set.
409
+ :type flow_rate: float
410
+
375
411
  :returns: This instance.
376
412
 
377
413
  .. note::
@@ -386,6 +422,13 @@ class InstrumentContext(publisher.CommandPublisher):
386
422
 
387
423
  .. versionchanged:: 2.17
388
424
  Behavior of the ``volume`` parameter.
425
+
426
+ .. versionchanged:: 2.24
427
+ Added the ``flow_rate`` parameter.
428
+
429
+ .. versionchanged:: 2.24
430
+ ``location`` is no longer required if the pipette just moved to, dispensed, or blew out
431
+ into a trash bin or waste chute.
389
432
  """
390
433
  if self.api_version < APIVersion(2, 15) and push_out:
391
434
  raise APIVersionError(
@@ -393,13 +436,27 @@ class InstrumentContext(publisher.CommandPublisher):
393
436
  until_version="2.15",
394
437
  current_version=f"{self.api_version}",
395
438
  )
439
+
440
+ if flow_rate is not None:
441
+ if self.api_version < APIVersion(2, 24):
442
+ raise APIVersionError(
443
+ api_element="flow_rate",
444
+ until_version="2.24",
445
+ current_version=f"{self.api_version}",
446
+ )
447
+ if rate != 1.0:
448
+ raise ValueError("rate must not be set if flow_rate is specified")
449
+ rate = flow_rate / self._core.get_dispense_flow_rate()
450
+ else:
451
+ flow_rate = self._core.get_dispense_flow_rate(rate)
452
+
396
453
  _log.debug(
397
- "dispense {} from {} at {}".format(
398
- volume, location if location else "current position", rate
454
+ "dispense {} from {} at {} µL/s".format(
455
+ volume, location if location else "current position", flow_rate
399
456
  )
400
457
  )
401
- last_location = self._get_last_location_by_api_version()
402
458
 
459
+ last_location = self._get_last_location_by_api_version()
403
460
  try:
404
461
  target = validation.validate_location(
405
462
  location=location, last_location=last_location
@@ -417,15 +474,13 @@ class InstrumentContext(publisher.CommandPublisher):
417
474
  else:
418
475
  c_vol = self._core.get_current_volume() if not volume else volume
419
476
 
420
- flow_rate = self._core.get_dispense_flow_rate(rate)
421
-
422
- if isinstance(target, (TrashBin, WasteChute)):
477
+ if isinstance(target, validation.DisposalTarget):
423
478
  with publisher.publish_context(
424
479
  broker=self.broker,
425
480
  command=cmds.dispense_in_disposal_location(
426
481
  instrument=self,
427
482
  volume=c_vol,
428
- location=target,
483
+ location=target.location,
429
484
  rate=rate,
430
485
  flow_rate=flow_rate,
431
486
  ),
@@ -433,10 +488,10 @@ class InstrumentContext(publisher.CommandPublisher):
433
488
  self._core.dispense(
434
489
  volume=c_vol,
435
490
  rate=rate,
436
- location=target,
491
+ location=target.location,
437
492
  well_core=None,
438
493
  flow_rate=flow_rate,
439
- in_place=False,
494
+ in_place=target.in_place,
440
495
  push_out=push_out,
441
496
  meniscus_tracking=None,
442
497
  )
@@ -477,12 +532,17 @@ class InstrumentContext(publisher.CommandPublisher):
477
532
  return self
478
533
 
479
534
  @requires_version(2, 0)
480
- def mix(
535
+ def mix( # noqa: C901
481
536
  self,
482
537
  repetitions: int = 1,
483
538
  volume: Optional[float] = None,
484
539
  location: Optional[Union[types.Location, labware.Well]] = None,
485
540
  rate: float = 1.0,
541
+ aspirate_flow_rate: Optional[float] = None,
542
+ dispense_flow_rate: Optional[float] = None,
543
+ aspirate_delay: Optional[float] = None,
544
+ dispense_delay: Optional[float] = None,
545
+ final_push_out: Optional[float] = None,
486
546
  ) -> InstrumentContext:
487
547
  """
488
548
  Mix a volume of liquid by repeatedly aspirating and dispensing it in a single location.
@@ -507,6 +567,16 @@ class InstrumentContext(publisher.CommandPublisher):
507
567
  dispensing flow rate is calculated as ``rate`` multiplied by
508
568
  :py:attr:`flow_rate.dispense <flow_rate>`. See
509
569
  :ref:`new-plunger-flow-rates`.
570
+ :param aspirate_flow_rate: The flow rate for each aspirate in the mix, in µL/s.
571
+ If this is specified, ``rate`` must not be set.
572
+ :param dispense_flow_rate: The flow rate for each dispense in the mix, in µL/s.
573
+ If this is specified, ``rate`` must not be set.
574
+ :param aspirate_delay: How long to wait after each aspirate in the mix, in seconds.
575
+ :param dispense_delay: How long to wait after each dispense in the mix, in seconds.
576
+ :param final_push_out: How much to push out after the final mix repetition. The
577
+ pipette will not push out after earlier repetitions. If
578
+ not specified or ``None``, the pipette will push out the
579
+ default non-zero amount. See :ref:`push-out-dispense`.
510
580
  :raises: ``UnexpectedTipRemovalError`` -- If no tip is attached to the pipette.
511
581
  :returns: This instance.
512
582
 
@@ -519,6 +589,9 @@ class InstrumentContext(publisher.CommandPublisher):
519
589
 
520
590
  .. versionchanged:: 2.21
521
591
  Does not repeatedly check for liquid presence.
592
+ .. versionchanged:: 2.24
593
+ Adds the ``aspirate_flow_rate``, ``dispense_flow_rate``, ``aspirate_delay``,
594
+ ``dispense_delay``, and ``final_push_out`` parameters.
522
595
  """
523
596
  _log.debug(
524
597
  "mixing {}uL with {} repetitions in {} at rate={}".format(
@@ -533,9 +606,69 @@ class InstrumentContext(publisher.CommandPublisher):
533
606
  else:
534
607
  c_vol = self._core.get_available_volume() if not volume else volume
535
608
 
536
- dispense_kwargs: Dict[str, Any] = {}
537
- if self.api_version >= APIVersion(2, 16):
538
- dispense_kwargs["push_out"] = 0.0
609
+ if aspirate_flow_rate:
610
+ if self.api_version < APIVersion(2, 24):
611
+ raise APIVersionError(
612
+ api_element="aspirate_flow_rate",
613
+ until_version="2.24",
614
+ current_version=f"{self._api_version}",
615
+ )
616
+ if rate != 1.0:
617
+ raise ValueError(
618
+ "rate must not be set if aspirate_flow_rate is specified"
619
+ )
620
+ if dispense_flow_rate:
621
+ if self.api_version < APIVersion(2, 24):
622
+ raise APIVersionError(
623
+ api_element="dispense_flow_rate",
624
+ until_version="2.24",
625
+ current_version=f"{self._api_version}",
626
+ )
627
+ if rate != 1.0:
628
+ raise ValueError(
629
+ "rate must not be set if dispense_flow_rate is specified"
630
+ )
631
+ if aspirate_delay and self.api_version < APIVersion(2, 24):
632
+ raise APIVersionError(
633
+ api_element="aspirate_delay",
634
+ until_version="2.24",
635
+ current_version=f"{self._api_version}",
636
+ )
637
+ if dispense_delay and self.api_version < APIVersion(2, 24):
638
+ raise APIVersionError(
639
+ api_element="dispense_delay",
640
+ until_version="2.24",
641
+ current_version=f"{self._api_version}",
642
+ )
643
+ if final_push_out and self.api_version < APIVersion(2, 24):
644
+ raise APIVersionError(
645
+ api_element="final_push_out",
646
+ until_version="2.24",
647
+ current_version=f"{self._api_version}",
648
+ )
649
+
650
+ def delay_with_publish(seconds: float) -> None:
651
+ # We don't have access to ProtocolContext.delay() which would automatically
652
+ # publish a message to the broker, so we have to do it manually:
653
+ with publisher.publish_context(
654
+ broker=self.broker,
655
+ command=protocol_cmds.delay(seconds=seconds, minutes=0, msg=None),
656
+ ):
657
+ self._protocol_core.delay(seconds=seconds, msg=None)
658
+
659
+ def aspirate_with_delay(
660
+ location: Optional[types.Location | labware.Well],
661
+ ) -> None:
662
+ self.aspirate(volume, location, rate, flow_rate=aspirate_flow_rate)
663
+ if aspirate_delay:
664
+ delay_with_publish(aspirate_delay)
665
+
666
+ def dispense_with_delay(push_out: Optional[float]) -> None:
667
+ self.dispense(
668
+ volume, None, rate, flow_rate=dispense_flow_rate, push_out=push_out
669
+ )
670
+ if dispense_delay:
671
+ delay_with_publish(dispense_delay)
539
672
 
540
673
  with publisher.publish_context(
541
674
  broker=self.broker,
@@ -546,13 +679,22 @@ class InstrumentContext(publisher.CommandPublisher):
546
679
  location=location,
547
680
  ),
548
681
  ):
549
- self.aspirate(volume, location, rate)
682
+ aspirate_with_delay(location=location)
550
683
  with AutoProbeDisable(self):
551
684
  while repetitions - 1 > 0:
552
- self.dispense(volume, rate=rate, **dispense_kwargs)
553
- self.aspirate(volume, rate=rate)
685
+ # starting in 2.16, we disable push_out on all but the last
686
+ # dispense() to prevent the tip from jumping out of the liquid
687
+ # during the mix (PR #14004):
688
+ dispense_with_delay(
689
+ push_out=0 if self.api_version >= APIVersion(2, 16) else None
690
+ )
691
+ # aspirate location was set above, do subsequent aspirates in-place:
692
+ aspirate_with_delay(location=None)
554
693
  repetitions -= 1
555
- self.dispense(volume, rate=rate)
694
+ if final_push_out is not None:
695
+ dispense_with_delay(push_out=final_push_out)
696
+ else:
697
+ dispense_with_delay(push_out=None)
556
698
  return self
557
699
 
558
700
  @requires_version(2, 0)
@@ -583,6 +725,10 @@ class InstrumentContext(publisher.CommandPublisher):
583
725
  without first calling a method that takes a location, like
584
726
  :py:meth:`.aspirate` or :py:meth:`dispense`.
585
727
  :returns: This instance.
728
+
729
+ .. versionchanged:: 2.24
730
+ ``location`` is no longer required if the pipette just moved to, dispensed, or blew out
731
+ into a trash bin or waste chute.
586
732
  """
587
733
  well: Optional[labware.Well] = None
588
734
  move_to_location: types.Location
@@ -623,17 +769,17 @@ class InstrumentContext(publisher.CommandPublisher):
623
769
  well = target.well
624
770
  elif isinstance(target, validation.PointTarget):
625
771
  move_to_location = target.location
626
- elif isinstance(target, (TrashBin, WasteChute)):
772
+ elif isinstance(target, validation.DisposalTarget):
627
773
  with publisher.publish_context(
628
774
  broker=self.broker,
629
775
  command=cmds.blow_out_in_disposal_location(
630
- instrument=self, location=target
776
+ instrument=self, location=target.location
631
777
  ),
632
778
  ):
633
779
  self._core.blow_out(
634
- location=target,
780
+ location=target.location,
635
781
  well_core=None,
636
- in_place=False,
782
+ in_place=target.in_place,
637
783
  )
638
784
  return self
639
785
 
@@ -657,12 +803,13 @@ class InstrumentContext(publisher.CommandPublisher):
657
803
 
658
804
  @publisher.publish(command=cmds.touch_tip)
659
805
  @requires_version(2, 0)
660
- def touch_tip(
806
+ def touch_tip( # noqa: C901
661
807
  self,
662
808
  location: Optional[labware.Well] = None,
663
809
  radius: float = 1.0,
664
810
  v_offset: float = -1.0,
665
811
  speed: float = 60.0,
812
+ mm_from_edge: Union[float, _Unset] = _Unset(),
666
813
  ) -> InstrumentContext:
667
814
  """
668
815
  Touch the pipette tip to the sides of a well, with the intent of removing leftover droplets.
@@ -688,12 +835,28 @@ class InstrumentContext(publisher.CommandPublisher):
688
835
  - Maximum: 80.0 mm/s
689
836
  - Minimum: 1.0 mm/s
690
837
  :type speed: float
838
+ :param mm_from_edge: How far to move inside the well, as a distance from the
839
+ well's edge.
840
+ When ``mm_from_edge=0``, the pipette tip will move all the
841
+ way to the edge of the target well. When ``mm_from_edge=1``,
842
+ the pipette tip will move to 1 mm from the well's edge.
843
+ Lower values will press the tip harder into the well's
844
+ walls; higher values will touch the well more lightly, or
845
+ not at all.
846
+ ``mm_from_edge`` and ``radius`` are mutually exclusive: to
847
+ use ``mm_from_edge``, ``radius`` must be unspecified (left
848
+ to its default value of 1.0).
849
+ :type mm_from_edge: float
691
850
  :raises: ``UnexpectedTipRemovalError`` -- If no tip is attached to the pipette.
692
851
  :raises RuntimeError: If no location is specified and the location cache is
693
852
  ``None``. This should happen if ``touch_tip`` is called
694
853
  without first calling a method that takes a location, like
695
854
  :py:meth:`.aspirate` or :py:meth:`dispense`.
855
+ :raises: ValueError: If both ``mm_from_edge`` and ``radius`` are specified.
696
856
  :returns: This instance.
857
+
858
+ .. versionchanged:: 2.24
859
+ Added the ``mm_from_edge`` parameter.
697
860
  """
698
861
  if not self._core.has_tip():
699
862
  raise UnexpectedTipRemovalError("touch_tip", self.name, self.mount)
@@ -703,8 +866,12 @@ class InstrumentContext(publisher.CommandPublisher):
703
866
  # If location is a valid well, move to the well first
704
867
  if location is None:
705
868
  last_location = self._protocol_core.get_last_location()
706
- if not last_location:
707
- raise RuntimeError("No valid current location cache present")
869
+ if last_location is None or isinstance(
870
+ last_location, (TrashBin, WasteChute)
871
+ ):
872
+ raise RuntimeError(
873
+ f"Cached location of {last_location} is not valid for touch tip."
874
+ )
708
875
  parent_labware, well = last_location.labware.get_parent_labware_and_well()
709
876
  if not well or not parent_labware:
710
877
  raise RuntimeError(
@@ -716,6 +883,18 @@ class InstrumentContext(publisher.CommandPublisher):
716
883
  else:
717
884
  raise TypeError(f"location should be a Well, but it is {location}")
718
885
 
886
+ if not isinstance(mm_from_edge, _Unset):
887
+ if self.api_version < APIVersion(2, 24):
888
+ raise APIVersionError(
889
+ api_element="mm_from_edge",
890
+ until_version="2.24",
891
+ current_version=f"{self.api_version}",
892
+ )
893
+ if radius != 1.0:
894
+ raise ValueError(
895
+ "radius must be set to 1.0 if mm_from_edge is specified"
896
+ )
897
+
719
898
  if "touchTipDisabled" in parent_labware.quirks:
720
899
  _log.info(f"Ignoring touch tip on labware {well}")
721
900
  return self
@@ -735,13 +914,19 @@ class InstrumentContext(publisher.CommandPublisher):
735
914
  radius=radius,
736
915
  z_offset=v_offset,
737
916
  speed=checked_speed,
917
+ mm_from_edge=mm_from_edge if not isinstance(mm_from_edge, _Unset) else None,
738
918
  )
739
919
  return self
740
920
 
741
921
  @publisher.publish(command=cmds.air_gap)
742
922
  @requires_version(2, 0)
743
- def air_gap(
744
- self, volume: Optional[float] = None, height: Optional[float] = None
923
+ def air_gap( # noqa: C901
924
+ self,
925
+ volume: Optional[float] = None,
926
+ height: Optional[float] = None,
927
+ in_place: Optional[bool] = None,
928
+ rate: Optional[float] = None,
929
+ flow_rate: Optional[float] = None,
745
930
  ) -> InstrumentContext:
746
931
  """
747
932
  Draw air into the pipette's tip at the current well.
@@ -756,12 +941,27 @@ class InstrumentContext(publisher.CommandPublisher):
756
941
  the air gap. The default is 5 mm above the current well.
757
942
  :type height: float
758
943
 
944
+ :param in_place: Air gap at the pipette's current position, without moving to
945
+ some height above the well. If ``in_place`` is specified,
946
+ ``height`` must be unset.
947
+ :type in_place: bool
948
+
949
+ :param rate: A multiplier for the default flow rate of the pipette. Calculated
950
+ as ``rate`` multiplied by :py:attr:`flow_rate.aspirate
951
+ <flow_rate>`. If neither rate nor flow_rate is specified, the pipette
952
+ will aspirate at a rate of 1.0 * InstrumentContext.flow_rate.aspirate. See
953
+ :ref:`new-plunger-flow-rates`.
954
+ :type rate: float
955
+
956
+ :param flow_rate: The rate, in µL/s, at which the pipette will draw in air.
957
+ :type flow_rate: float
958
+
759
959
  :raises: ``UnexpectedTipRemovalError`` -- If no tip is attached to the pipette.
760
960
 
761
- :raises RuntimeError: If location cache is ``None``. This should happen if
762
- ``air_gap()`` is called without first calling a method
763
- that takes a location (e.g., :py:meth:`.aspirate`,
764
- :py:meth:`dispense`)
961
+ :raises RuntimeError: If location cache is ``None`` and the air gap is not
962
+ ``in_place``. This would happen if ``air_gap()`` is called
963
+ without first calling a method that takes a location (e.g.,
964
+ :py:meth:`.aspirate`, :py:meth:`dispense`)
765
965
 
766
966
  :returns: This instance.
767
967
 
@@ -779,22 +979,75 @@ class InstrumentContext(publisher.CommandPublisher):
779
979
 
780
980
  .. versionchanged:: 2.22
781
981
  No longer implemented as an aspirate.
982
+ .. versionchanged:: 2.24
983
+ Added the ``in_place`` option.
984
+ .. versionchanged:: 2.24
985
+ Adds the ``rate`` and ``flow_rate`` parameter. You can only define one or the other. If
986
+ both are unspecified then ``rate`` is by default set to 1.0.
987
+ Can air gap over a trash bin or waste chute.
782
988
  """
783
989
  if not self._core.has_tip():
784
990
  raise UnexpectedTipRemovalError("air_gap", self.name, self.mount)
785
991
 
786
- if height is None:
787
- height = 5
788
- loc = self._protocol_core.get_last_location()
789
- if not loc or not loc.labware.is_well:
790
- raise RuntimeError("No previous Well cached to perform air gap")
791
- target = loc.labware.as_well().top(height)
792
- self.move_to(target, publish=False)
992
+ if rate is not None and self.api_version < APIVersion(2, 24):
993
+ raise APIVersionError(
994
+ api_element="rate",
995
+ until_version="2.24",
996
+ current_version=f"{self._api_version}",
997
+ )
998
+
999
+ if flow_rate is not None and self.api_version < APIVersion(2, 24):
1000
+ raise APIVersionError(
1001
+ api_element="flow_rate",
1002
+ until_version="2.24",
1003
+ current_version=f"{self._api_version}",
1004
+ )
1005
+
1006
+ if flow_rate is not None and rate is not None:
1007
+ raise ValueError("Cannot define both flow_rate and rate.")
1008
+
1009
+ if in_place:
1010
+ if self.api_version < APIVersion(2, 24):
1011
+ raise APIVersionError(
1012
+ api_element="in_place",
1013
+ until_version="2.24",
1014
+ current_version=f"{self._api_version}",
1015
+ )
1016
+ if height is not None:
1017
+ raise ValueError("height must be unset if air gapping in_place")
1018
+ else:
1019
+ if height is None:
1020
+ height = 5
1021
+ last_location = self._protocol_core.get_last_location()
1022
+ if self.api_version < APIVersion(2, 24) and isinstance(
1023
+ last_location, (TrashBin, WasteChute)
1024
+ ):
1025
+ last_location = None
1026
+ if last_location is None or (
1027
+ isinstance(last_location, types.Location)
1028
+ and not last_location.labware.is_well
1029
+ ):
1030
+ raise RuntimeError(
1031
+ f"Cached location of {last_location} is not valid for air gap."
1032
+ )
1033
+ target: Union[types.Location, TrashBin, WasteChute]
1034
+ if isinstance(last_location, types.Location):
1035
+ target = last_location.labware.as_well().top(height)
1036
+ else:
1037
+ target = last_location.top(height)
1038
+ self.move_to(target, publish=False)
1039
+
793
1040
  if self.api_version >= _AIR_GAP_TRACKING_ADDED_IN:
794
1041
  self._core.prepare_to_aspirate()
795
1042
  c_vol = self._core.get_available_volume() if volume is None else volume
796
- flow_rate = self._core.get_aspirate_flow_rate()
797
- self._core.air_gap_in_place(c_vol, flow_rate)
1043
+ if flow_rate is not None:
1044
+ calculated_rate = flow_rate
1045
+ elif rate is not None:
1046
+ calculated_rate = rate * self._core.get_aspirate_flow_rate()
1047
+ else:
1048
+ calculated_rate = self._core.get_aspirate_flow_rate()
1049
+
1050
+ self._core.air_gap_in_place(c_vol, calculated_rate)
798
1051
  else:
799
1052
  self.aspirate(volume)
800
1053
  return self
@@ -1530,7 +1783,11 @@ class InstrumentContext(publisher.CommandPublisher):
1530
1783
  labware.Well, Sequence[labware.Well], Sequence[Sequence[labware.Well]]
1531
1784
  ],
1532
1785
  dest: Union[
1533
- labware.Well, Sequence[labware.Well], Sequence[Sequence[labware.Well]]
1786
+ labware.Well,
1787
+ Sequence[labware.Well],
1788
+ Sequence[Sequence[labware.Well]],
1789
+ TrashBin,
1790
+ WasteChute,
1534
1791
  ],
1535
1792
  new_tip: TransferTipPolicyV2Type = "once",
1536
1793
  trash_location: Optional[
@@ -1552,7 +1809,7 @@ class InstrumentContext(publisher.CommandPublisher):
1552
1809
  :param volume: The amount, in µL, to aspirate from each source and dispense to
1553
1810
  each destination.
1554
1811
  :param source: A single well or a list of wells to aspirate liquid from.
1555
- :param dest: A single well or a list of wells to dispense liquid into.
1812
+ :param dest: A single well, list of wells, trash bin, or waste chute to dispense liquid into.
1556
1813
  :param new_tip: When to pick up and drop tips during the command.
1557
1814
  Defaults to ``"once"``.
1558
1815
 
@@ -1560,6 +1817,8 @@ class InstrumentContext(publisher.CommandPublisher):
1560
1817
  - ``"always"``: Use a new tip for each set of aspirate and dispense steps.
1561
1818
  - ``"per source"``: Use one tip for each source well, even if
1562
1819
  :ref:`tip refilling <complex-tip-refilling>` is required.
1820
+ - ``"per destination"``: Use one tip for each destination well, even if
1821
+ :ref:`tip refilling <complex-tip-refilling>` is required.
1563
1822
  - ``"never"``: Do not pick up or drop tips at all.
1564
1823
 
1565
1824
  See :ref:`param-tip-handling` for details.
@@ -1591,12 +1850,23 @@ class InstrumentContext(publisher.CommandPublisher):
1591
1850
  trash_location if trash_location is not None else self.trash_container
1592
1851
  ),
1593
1852
  )
1594
- if len(transfer_args.sources_list) != len(transfer_args.destinations_list):
1595
- raise ValueError(
1596
- "Sources and destinations should be of the same length in order to perform a transfer."
1597
- " To transfer liquid from one source to many destinations, use 'distribute_liquid',"
1598
- " to transfer liquid onto one destinations from many sources, use 'consolidate_liquid'."
1599
- )
1853
+
1854
+ verified_dest: Union[
1855
+ List[Tuple[types.Location, WellCore]], TrashBin, WasteChute
1856
+ ]
1857
+ if isinstance(transfer_args.dest, (TrashBin, WasteChute)):
1858
+ verified_dest = transfer_args.dest
1859
+ else:
1860
+ if len(transfer_args.source) != len(transfer_args.dest):
1861
+ raise ValueError(
1862
+ "Sources and destinations should be of the same length in order to perform a transfer."
1863
+ " To transfer liquid from one source to many destinations, use 'distribute_liquid',"
1864
+ " to transfer liquid to one destination from many sources, use 'consolidate_liquid'."
1865
+ )
1866
+ verified_dest = [
1867
+ (types.Location(types.Point(), labware=well), well._core)
1868
+ for well in transfer_args.dest
1869
+ ]
1600
1870
 
1601
1871
  with publisher.publish_context(
1602
1872
  broker=self.broker,
@@ -1613,12 +1883,9 @@ class InstrumentContext(publisher.CommandPublisher):
1613
1883
  volume=volume,
1614
1884
  source=[
1615
1885
  (types.Location(types.Point(), labware=well), well._core)
1616
- for well in transfer_args.sources_list
1617
- ],
1618
- dest=[
1619
- (types.Location(types.Point(), labware=well), well._core)
1620
- for well in transfer_args.destinations_list
1886
+ for well in transfer_args.source
1621
1887
  ],
1888
+ dest=verified_dest,
1622
1889
  new_tip=transfer_args.tip_policy,
1623
1890
  tip_racks=[
1624
1891
  (types.Location(types.Point(), labware=rack), rack._core)
@@ -1661,7 +1928,8 @@ class InstrumentContext(publisher.CommandPublisher):
1661
1928
 
1662
1929
  :param volume: The amount, in µL, to aspirate from the source and dispense to
1663
1930
  each destination.
1664
- :param source: A single well to aspirate liquid from.
1931
+ :param source: A single well for the pipette to target, or a group of wells to
1932
+ target in a single aspirate for a multi-channel pipette.
1665
1933
  :param dest: A list of wells to dispense liquid into.
1666
1934
  :param new_tip: When to pick up and drop tips during the command.
1667
1935
  Defaults to ``"once"``.
@@ -1698,10 +1966,15 @@ class InstrumentContext(publisher.CommandPublisher):
1698
1966
  trash_location if trash_location is not None else self.trash_container
1699
1967
  ),
1700
1968
  )
1701
- if len(transfer_args.sources_list) != 1:
1969
+ if isinstance(transfer_args.dest, (TrashBin, WasteChute)):
1970
+ raise ValueError(
1971
+ "distribute_with_liquid_class() does not support trash bin or waste chute"
1972
+ " as a destination."
1973
+ )
1974
+ if len(transfer_args.source) != 1:
1702
1975
  raise ValueError(
1703
1976
  f"Source should be a single well (or resolve to a single transfer for multi-channel) "
1704
- f"but received {transfer_args.sources_list}."
1977
+ f"but received {transfer_args.source}."
1705
1978
  )
1706
1979
  if transfer_args.tip_policy not in [
1707
1980
  TransferTipPolicyV2.ONCE,
@@ -1713,7 +1986,7 @@ class InstrumentContext(publisher.CommandPublisher):
1713
1986
  f" 'once' and 'never'."
1714
1987
  )
1715
1988
 
1716
- verified_source = transfer_args.sources_list[0]
1989
+ verified_source = transfer_args.source[0]
1717
1990
  with publisher.publish_context(
1718
1991
  broker=self.broker,
1719
1992
  command=cmds.distribute_with_liquid_class(
@@ -1733,7 +2006,7 @@ class InstrumentContext(publisher.CommandPublisher):
1733
2006
  ),
1734
2007
  dest=[
1735
2008
  (types.Location(types.Point(), labware=well), well._core)
1736
- for well in transfer_args.destinations_list
2009
+ for well in transfer_args.dest
1737
2010
  ],
1738
2011
  new_tip=transfer_args.tip_policy, # type: ignore[arg-type]
1739
2012
  tip_racks=[
@@ -1756,7 +2029,7 @@ class InstrumentContext(publisher.CommandPublisher):
1756
2029
  source: Union[
1757
2030
  labware.Well, Sequence[labware.Well], Sequence[Sequence[labware.Well]]
1758
2031
  ],
1759
- dest: Union[labware.Well, Sequence[labware.Well]],
2032
+ dest: Union[labware.Well, Sequence[labware.Well], TrashBin, WasteChute],
1760
2033
  new_tip: TransferTipPolicyV2Type = "once",
1761
2034
  trash_location: Optional[
1762
2035
  Union[types.Location, labware.Well, TrashBin, WasteChute]
@@ -1778,7 +2051,9 @@ class InstrumentContext(publisher.CommandPublisher):
1778
2051
  :param volume: The amount, in µL, to aspirate from the source and dispense to
1779
2052
  each destination.
1780
2053
  :param source: A list of wells to aspirate liquid from.
1781
- :param dest: A single well to dispense liquid into.
2054
+ :param dest: A single well, list of wells, trash bin, or waste chute to dispense liquid into.
2055
+ Multiple wells can only be given for multi-channel pipette configurations, and
2056
+ must be able to be dispensed to in a single dispense.
1782
2057
  :param new_tip: When to pick up and drop tips during the command.
1783
2058
  Defaults to ``"once"``.
1784
2059
 
@@ -1814,10 +2089,18 @@ class InstrumentContext(publisher.CommandPublisher):
1814
2089
  trash_location if trash_location is not None else self.trash_container
1815
2090
  ),
1816
2091
  )
1817
- if len(transfer_args.destinations_list) != 1:
1818
- raise ValueError(
1819
- f"Destination should be a single well (or resolve to a single transfer for multi-channel) "
1820
- f"but received {transfer_args.destinations_list}."
2092
+ verified_dest: Union[Tuple[types.Location, WellCore], TrashBin, WasteChute]
2093
+ if isinstance(transfer_args.dest, (TrashBin, WasteChute)):
2094
+ verified_dest = transfer_args.dest
2095
+ else:
2096
+ if len(transfer_args.dest) != 1:
2097
+ raise ValueError(
2098
+ f"Destination should be a single well (or resolve to a single transfer for multi-channel) "
2099
+ f"but received {transfer_args.dest}."
2100
+ )
2101
+ verified_dest = (
2102
+ types.Location(types.Point(), labware=transfer_args.dest[0]),
2103
+ transfer_args.dest[0]._core,
1821
2104
  )
1822
2105
  if transfer_args.tip_policy not in [
1823
2106
  TransferTipPolicyV2.ONCE,
@@ -1829,7 +2112,6 @@ class InstrumentContext(publisher.CommandPublisher):
1829
2112
  f" 'once' and 'never'."
1830
2113
  )
1831
2114
 
1832
- verified_dest = transfer_args.destinations_list[0]
1833
2115
  with publisher.publish_context(
1834
2116
  broker=self.broker,
1835
2117
  command=cmds.consolidate_with_liquid_class(
@@ -1845,12 +2127,9 @@ class InstrumentContext(publisher.CommandPublisher):
1845
2127
  volume=volume,
1846
2128
  source=[
1847
2129
  (types.Location(types.Point(), labware=well), well._core)
1848
- for well in transfer_args.sources_list
2130
+ for well in transfer_args.source
1849
2131
  ],
1850
- dest=(
1851
- types.Location(types.Point(), labware=verified_dest),
1852
- verified_dest._core,
1853
- ),
2132
+ dest=verified_dest,
1854
2133
  new_tip=transfer_args.tip_policy, # type: ignore[arg-type]
1855
2134
  tip_racks=[
1856
2135
  (types.Location(types.Point(), labware=rack), rack._core)
@@ -2413,14 +2692,22 @@ class InstrumentContext(publisher.CommandPublisher):
2413
2692
  """
2414
2693
  return self._well_bottom_clearances
2415
2694
 
2416
- def _get_last_location_by_api_version(self) -> Optional[types.Location]:
2695
+ def _get_last_location_by_api_version(
2696
+ self,
2697
+ ) -> Optional[Union[types.Location, TrashBin, WasteChute]]:
2417
2698
  """Get the last location accessed by this pipette, if any.
2418
2699
 
2419
2700
  In pre-engine Protocol API versions, this call omits the pipette mount.
2701
+ Between 2.14 (first engine PAPI version) and 2.23 this only returns None or a Location object.
2420
2702
  This is to preserve pre-existing, potentially buggy behavior.
2421
2703
  """
2422
- if self._api_version >= ENGINE_CORE_API_VERSION:
2704
+ if self._api_version >= APIVersion(2, 24):
2423
2705
  return self._protocol_core.get_last_location(mount=self._core.get_mount())
2706
+ elif self._api_version >= ENGINE_CORE_API_VERSION:
2707
+ last_location = self._protocol_core.get_last_location(
2708
+ mount=self._core.get_mount()
2709
+ )
2710
+ return last_location if isinstance(last_location, types.Location) else None
2424
2711
  else:
2425
2712
  return self._protocol_core.get_last_location()
2426
2713
 
@@ -2476,7 +2763,11 @@ class InstrumentContext(publisher.CommandPublisher):
2476
2763
  actual_value=str(volume),
2477
2764
  )
2478
2765
  last_location = self._get_last_location_by_api_version()
2479
- if last_location and isinstance(last_location.labware, labware.Well):
2766
+ if (
2767
+ last_location
2768
+ and isinstance(last_location, types.Location)
2769
+ and isinstance(last_location.labware, labware.Well)
2770
+ ):
2480
2771
  self.move_to(last_location.labware.top())
2481
2772
  self._core.configure_for_volume(volume)
2482
2773
 
@@ -2498,19 +2789,19 @@ class InstrumentContext(publisher.CommandPublisher):
2498
2789
  If the pipette is in a well, it will move out of the well, move the plunger,
2499
2790
  and then move back.
2500
2791
 
2501
- Use ``prepare_to_aspirate`` when you need to control exactly when the plunger
2792
+ Use ``prepare_to_aspirate()`` when you need to control exactly when the plunger
2502
2793
  motion will happen. A common use case is a pre-wetting routine, which requires
2503
2794
  preparing for aspiration, moving into a well, and then aspirating *without
2504
2795
  leaving the well*::
2505
2796
 
2506
2797
  pipette.move_to(well.bottom(z=2))
2507
- pipette.delay(5)
2798
+ protocol.delay(5)
2508
2799
  pipette.mix(10, 10)
2509
2800
  pipette.move_to(well.top(z=5))
2510
2801
  pipette.blow_out()
2511
2802
  pipette.prepare_to_aspirate()
2512
2803
  pipette.move_to(well.bottom(z=2))
2513
- pipette.delay(5)
2804
+ protocol.delay(5)
2514
2805
  pipette.aspirate(10, well.bottom(z=2))
2515
2806
 
2516
2807
  The call to ``prepare_to_aspirate()`` means that the plunger will be in the