Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 42 additions & 23 deletions api/src/opentrons/protocol_api/instrument_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,7 @@ def air_gap( # noqa: C901
self,
volume: Optional[float] = None,
height: Optional[float] = None,
in_place: Optional[bool] = None,
rate: Optional[float] = None,
flow_rate: Optional[float] = None,
) -> InstrumentContext:
Expand All @@ -875,6 +876,11 @@ def air_gap( # noqa: C901
the air gap. The default is 5 mm above the current well.
:type height: float

:param in_place: Air gap at the pipette's current position, without moving to
some height above the well. If ``in_place`` is specified,
``height`` must be unset.
:type in_place: bool

:param rate: A multiplier for the default flow rate of the pipette. Calculated
as ``rate`` multiplied by :py:attr:`flow_rate.aspirate
<flow_rate>`. If neither rate nor flow_rate is specified, the pipette
Expand All @@ -887,10 +893,10 @@ def air_gap( # noqa: C901

:raises: ``UnexpectedTipRemovalError`` -- If no tip is attached to the pipette.

:raises RuntimeError: If location cache is ``None``. This should happen if
``air_gap()`` is called without first calling a method
that takes a location (e.g., :py:meth:`.aspirate`,
:py:meth:`dispense`)
:raises RuntimeError: If location cache is ``None`` and the air gap is not
``in_place``. This would happen if ``air_gap()`` is called
without first calling a method that takes a location (e.g.,
:py:meth:`.aspirate`, :py:meth:`dispense`)

:returns: This instance.

Expand All @@ -908,6 +914,8 @@ def air_gap( # noqa: C901

.. versionchanged:: 2.22
No longer implemented as an aspirate.
.. versionchanged:: 2.24
Added the ``in_place`` option.
.. versionchanged:: 2.24
Adds the ``rate`` and ``flow_rate`` parameter. You can only define one or the other. If
both are unspecified then ``rate`` is by default set to 1.0.
Expand All @@ -933,26 +941,37 @@ def air_gap( # noqa: C901
if flow_rate is not None and rate is not None:
raise ValueError("Cannot define both flow_rate and rate.")

if height is None:
height = 5
last_location = self._protocol_core.get_last_location()
if self.api_version < APIVersion(2, 24) and isinstance(
last_location, (TrashBin, WasteChute)
):
last_location = None
if last_location is None or (
isinstance(last_location, types.Location)
and not last_location.labware.is_well
):
raise RuntimeError(
f"Cached location of {last_location} is not valid for air gap."
)
target: Union[types.Location, TrashBin, WasteChute]
if isinstance(last_location, types.Location):
target = last_location.labware.as_well().top(height)
if in_place:
if self.api_version < APIVersion(2, 24):
raise APIVersionError(
api_element="in_place",
until_version="2.24",
current_version=f"{self._api_version}",
)
if height is not None:
raise ValueError("height must be unset if air gapping in_place")
else:
target = last_location.top(height)
self.move_to(target, publish=False)
if height is None:
height = 5
last_location = self._protocol_core.get_last_location()
if self.api_version < APIVersion(2, 24) and isinstance(
last_location, (TrashBin, WasteChute)
):
last_location = None
if last_location is None or (
isinstance(last_location, types.Location)
and not last_location.labware.is_well
):
raise RuntimeError(
f"Cached location of {last_location} is not valid for air gap."
)
target: Union[types.Location, TrashBin, WasteChute]
if isinstance(last_location, types.Location):
target = last_location.labware.as_well().top(height)
else:
target = last_location.top(height)
self.move_to(target, publish=False)

if self.api_version >= _AIR_GAP_TRACKING_ADDED_IN:
self._core.prepare_to_aspirate()
c_vol = self._core.get_available_volume() if volume is None else volume
Expand Down
23 changes: 23 additions & 0 deletions api/tests/opentrons/protocol_api/test_instrument_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -1966,6 +1966,29 @@ def test_air_gap_uses_air_gap(
decoy.verify(mock_instrument_core.air_gap_in_place(10, 11))


def test_air_gap_in_place(
decoy: Decoy,
mock_instrument_core: InstrumentCore,
subject: InstrumentContext,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""It should air gap in place when in_place=True."""
decoy.when(mock_instrument_core.has_tip()).then_return(True)
decoy.when(mock_instrument_core.get_aspirate_flow_rate()).then_return(11)
monkeypatch.setattr(subject, "move_to", None) # pipette should not move

subject.air_gap(volume=10, in_place=True)

decoy.verify(mock_instrument_core.air_gap_in_place(10, 11))

# Should not allow height if in_place=True is specified.
with pytest.raises(ValueError):
subject.air_gap(volume=10, height=2, in_place=True)
# height=0 is also not allowed when in_place=True.
with pytest.raises(ValueError):
subject.air_gap(volume=10, height=0, in_place=True)


@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 24)))
def test_air_gap_has_rate(
decoy: Decoy,
Expand Down
Loading