Skip to content

Commit

Permalink
feat(api): raise error if tip tracking is not available for current n…
Browse files Browse the repository at this point in the history
…ozzle configuration (#14231)

Raises error when pick_up_tip is called without a location when using pipette nozzle configurations that aren't supported in 7 1
sanni-t authored Dec 18, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 24310ce commit c45ed00
Showing 7 changed files with 101 additions and 2 deletions.
22 changes: 22 additions & 0 deletions api/src/opentrons/protocol_api/core/engine/instrument.py
Original file line number Diff line number Diff line change
@@ -681,6 +681,28 @@ def get_nozzle_configuration(self) -> NozzleConfigurationType:
self._pipette_id
)

def is_tip_tracking_available(self) -> bool:
primary_nozzle = self._engine_client.state.pipettes.get_primary_nozzle(
self._pipette_id
)
if self.get_nozzle_configuration() == NozzleConfigurationType.FULL:
return True
else:
if self.get_channels() == 96:
# SINGLE configuration with H12 nozzle is technically supported by the
# current tip tracking implementation but we don't do any deck conflict
# checks for it, so we won't provide full support for it yet.
return (
self.get_nozzle_configuration() == NozzleConfigurationType.COLUMN
and primary_nozzle == "A12"
)
if self.get_channels() == 8:
return (
self.get_nozzle_configuration() == NozzleConfigurationType.SINGLE
and primary_nozzle == "H1"
)
return False

def set_flow_rate(
self,
aspirate: Optional[float] = None,
4 changes: 4 additions & 0 deletions api/src/opentrons/protocol_api/core/instrument.py
Original file line number Diff line number Diff line change
@@ -274,5 +274,9 @@ def configure_nozzle_layout(
"""
...

def is_tip_tracking_available(self) -> bool:
"""Return whether auto tip tracking is available for the pipette's current nozzle configuration."""
...


InstrumentCoreType = TypeVar("InstrumentCoreType", bound=AbstractInstrument[Any])
Original file line number Diff line number Diff line change
@@ -547,3 +547,7 @@ def configure_nozzle_layout(
def get_active_channels(self) -> int:
"""This will never be called because it was added in API 2.16."""
assert False, "get_active_channels only supported in API 2.16 & later"

def is_tip_tracking_available(self) -> bool:
# Tip tracking is always available in legacy context
return True
Original file line number Diff line number Diff line change
@@ -465,3 +465,7 @@ def configure_nozzle_layout(
def get_active_channels(self) -> int:
"""This will never be called because it was added in API 2.16."""
assert False, "get_active_channels only supported in API 2.16 & later"

def is_tip_tracking_available(self) -> bool:
# Tip tracking is always available in legacy context
return True
10 changes: 9 additions & 1 deletion api/src/opentrons/protocol_api/instrument_context.py
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@
from opentrons_shared_data.errors.exceptions import (
CommandPreconditionViolated,
CommandParameterLimitViolated,
UnexpectedTipRemovalError,
)
from opentrons.legacy_broker import LegacyBroker
from opentrons.hardware_control.dev_types import PipetteDict
@@ -26,7 +27,6 @@
requires_version,
APIVersionError,
)
from opentrons_shared_data.errors.exceptions import UnexpectedTipRemovalError

from .core.common import InstrumentCore, ProtocolCore
from .core.engine import ENGINE_CORE_API_VERSION
@@ -860,6 +860,14 @@ def pick_up_tip(
)

if location is None:
if not self._core.is_tip_tracking_available():
raise CommandPreconditionViolated(
"Automatic tip tracking is not available for the current pipette"
" nozzle configuration. We suggest switching to a configuration"
" that supports automatic tip tracking or specifying the exact tip"
" to pick up."
)

tip_rack, well = labware.next_available_tip(
starting_tip=self.starting_tip,
tip_racks=self.tip_racks,
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Test for the ProtocolEngine-based instrument API core."""
from typing import cast, Optional
from typing import cast, Optional, Union

import pytest
from decoy import Decoy
@@ -8,6 +8,7 @@

from opentrons.hardware_control import SyncHardwareAPI
from opentrons.hardware_control.dev_types import PipetteDict
from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType
from opentrons.protocol_engine import (
DeckPoint,
LoadedPipette,
@@ -1011,3 +1012,42 @@ def test_configure_nozzle_layout(
decoy.verify(
mock_engine_client.configure_nozzle_layout(subject._pipette_id, expected_model)
)


@pytest.mark.parametrize(
argnames=["pipette_channels", "nozzle_layout", "primary_nozzle", "expected_result"],
argvalues=[
(96, NozzleConfigurationType.FULL, "A1", True),
(96, NozzleConfigurationType.FULL, None, True),
(96, NozzleConfigurationType.ROW, "A1", False),
(96, NozzleConfigurationType.COLUMN, "A1", False),
(96, NozzleConfigurationType.COLUMN, "A12", True),
(96, NozzleConfigurationType.SINGLE, "H12", False),
(96, NozzleConfigurationType.SINGLE, "A1", False),
(8, NozzleConfigurationType.FULL, "A1", True),
(8, NozzleConfigurationType.FULL, None, True),
(8, NozzleConfigurationType.SINGLE, "H1", True),
(8, NozzleConfigurationType.SINGLE, "A1", False),
(1, NozzleConfigurationType.FULL, None, True),
],
)
def test_is_tip_tracking_available(
decoy: Decoy,
mock_engine_client: EngineClient,
subject: InstrumentCore,
pipette_channels: int,
nozzle_layout: NozzleConfigurationType,
primary_nozzle: Union[str, None],
expected_result: bool,
) -> None:
"""It should return whether tip tracking is available based on nozzle configuration."""
decoy.when(
mock_engine_client.state.tips.get_pipette_channels(subject.pipette_id)
).then_return(pipette_channels)
decoy.when(
mock_engine_client.state.pipettes.get_nozzle_layout_type(subject.pipette_id)
).then_return(nozzle_layout)
decoy.when(
mock_engine_client.state.pipettes.get_primary_nozzle(subject.pipette_id)
).then_return(primary_nozzle)
assert subject.is_tip_tracking_available() == expected_result
17 changes: 17 additions & 0 deletions api/tests/opentrons/protocol_api/test_instrument_context.py
Original file line number Diff line number Diff line change
@@ -600,6 +600,7 @@ def test_pick_up_from_associated_tip_racks(
mock_well = decoy.mock(cls=Well)
top_location = Location(point=Point(1, 2, 3), labware=mock_well)

decoy.when(mock_instrument_core.is_tip_tracking_available()).then_return(True)
decoy.when(mock_instrument_core.get_active_channels()).then_return(123)
decoy.when(
labware.next_available_tip(
@@ -626,6 +627,22 @@ def test_pick_up_from_associated_tip_racks(
)


def test_pick_up_fails_when_tip_tracking_unavailable(
decoy: Decoy, mock_instrument_core: InstrumentCore, subject: InstrumentContext
) -> None:
"""It should raise an error if automatic tip tracking is not available.."""
mock_tip_rack_1 = decoy.mock(cls=Labware)

decoy.when(mock_instrument_core.is_tip_tracking_available()).then_return(False)
decoy.when(mock_instrument_core.get_active_channels()).then_return(123)

subject.tip_racks = [mock_tip_rack_1]
with pytest.raises(
CommandPreconditionViolated, match="Automatic tip tracking is not available"
):
subject.pick_up_tip()


def test_drop_tip_to_well(
decoy: Decoy, mock_instrument_core: InstrumentCore, subject: InstrumentContext
) -> None:

0 comments on commit c45ed00

Please sign in to comment.