Skip to content

Commit

Permalink
feat(api): add api function for partial tip configurations
Browse files Browse the repository at this point in the history
  • Loading branch information
Laura-Danielle committed Oct 24, 2023
1 parent 695b4b4 commit 7353369
Show file tree
Hide file tree
Showing 10 changed files with 251 additions and 1 deletion.
9 changes: 9 additions & 0 deletions api/src/opentrons/protocol_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@
MagneticBlockContext,
)
from ._liquid import Liquid
from ._nozzle_layout import (
NozzleLayoutBase,
SingleNozzleLayout,
RowNozzleLayout,
ColumnNozzleLayout,
QuadrantNozzleLayout,
)

from .create_protocol_context import (
create_protocol_context,
Expand All @@ -46,6 +53,8 @@
"Labware",
"Well",
"Liquid",
"SingleNozzleLayout",
"ColumnNozzleLayout",
"OFF_DECK",
# For internal Opentrons use only:
"create_protocol_context",
Expand Down
47 changes: 47 additions & 0 deletions api/src/opentrons/protocol_api/_nozzle_layout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import re
from dataclasses import dataclass

# TODO MEOW THIS STRING TYPE TO SHARED PLACE
ACCEPTABLE_NOZZLE_KEY_RE = re.compile("[A-Z][0-100]")


@dataclass
class NozzleLayoutBase:
primary_nozzle: str

def __post_init__(self):
if not ACCEPTABLE_NOZZLE_KEY_RE.match(self.primary_nozzle):
raise ValueError(
f"{self.primary_nozzle} does not fit the standard nozzle naming convention. Please ensure the nozzle is labled <LETTER><NUMBER> and that your letter is capitalized."
)


@dataclass
class SingleNozzleLayout(NozzleLayoutBase):
pass


@dataclass
class RowNozzleLayout(NozzleLayoutBase):
pass


@dataclass
class ColumnNozzleLayout(NozzleLayoutBase):
pass


@dataclass
class QuadrantNozzleLayout(NozzleLayoutBase):
back_left_nozzle: str
front_right_nozzle: str

def __post_init__(self):
if not ACCEPTABLE_NOZZLE_KEY_RE.match(self.back_left_nozzle):
raise ValueError(
f"{self.back_left_nozzle} does not fit the standard nozzle naming convention. Please ensure the nozzle is labled <LETTER><NUMBER> and that your letter is capitalized."
)
if not ACCEPTABLE_NOZZLE_KEY_RE.match(self.front_right_nozzle):
raise ValueError(
f"{self.front_right_nozzle} does not fit the standard nozzle naming convention. Please ensure the nozzle is labled <LETTER><NUMBER> and that your letter is capitalized."
)
13 changes: 13 additions & 0 deletions api/src/opentrons/protocol_api/core/engine/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -531,3 +531,16 @@ def configure_for_volume(self, volume: float) -> None:
self._engine_client.configure_for_volume(
pipette_id=self._pipette_id, volume=volume
)

def configure_nozzle_layout(
self,
primary_nozzle: Optional[str],
back_left_nozzle: Optional[str],
front_right_nozzle: Optional[str],
) -> None:
self._engine_client.configure_nozzle_layout(
pipette_id=self._pipette_id,
primary_nozzle=primary_nozzle,
back_left_nozzle=back_left_nozzle,
front_right_nozzle=front_right_nozzle,
)
16 changes: 16 additions & 0 deletions api/src/opentrons/protocol_api/core/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from opentrons import types
from opentrons.hardware_control.dev_types import PipetteDict
from opentrons.protocols.api_support.util import FlowRates
from opentrons.protocol_api._nozzle_layout import NozzleLayoutBase

from .well import WellCoreType

Expand Down Expand Up @@ -236,5 +237,20 @@ def configure_for_volume(self, volume: float) -> None:
"""
...

def configure_nozzle_layout(
self,
primary_nozzle: Optional[str],
back_left_nozzle: Optional[str],
front_right_nozzle: Optional[str],
) -> None:
"""Configure the pipette to a specific nozzle layout.
Args:
primary_nozzle: The nozzle that will determine a pipettes critical point.
back_left_nozzle: The back left most nozzle in the requested layout.
front_right_nozzle: The front right most nozzle in the requested layout.
"""
...


InstrumentCoreType = TypeVar("InstrumentCoreType", bound=AbstractInstrument[Any])
Original file line number Diff line number Diff line change
Expand Up @@ -511,3 +511,12 @@ def flag_unsafe_move(self, location: types.Location) -> None:
def configure_for_volume(self, volume: float) -> None:
"""This will never be called because it was added in API 2.15."""
pass

def configure_nozzle_layout(
self,
primary_nozzle: Optional[str],
back_left_nozzle: Optional[str],
front_right_nozzle: Optional[str],
) -> None:
"""This will never be called because it was added in API 2.15."""
pass
Original file line number Diff line number Diff line change
Expand Up @@ -421,3 +421,12 @@ def _raise_if_tip(self, action: str) -> None:
def configure_for_volume(self, volume: float) -> None:
"""This will never be called because it was added in API 2.15."""
pass

def configure_nozzle_layout(
self,
primary_nozzle: Optional[str],
back_left_nozzle: Optional[str],
front_right_nozzle: Optional[str],
) -> None:
"""This will never be called because it was added in API 2.15."""
pass
104 changes: 104 additions & 0 deletions api/src/opentrons/protocol_api/instrument_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@
from .core.engine import ENGINE_CORE_API_VERSION
from .core.legacy.legacy_instrument_core import LegacyInstrumentCore
from .config import Clearances
from ._nozzle_layout import (
NozzleLayoutBase,
SingleNozzleLayout,
RowNozzleLayout,
ColumnNozzleLayout,
QuadrantNozzleLayout,
)
from . import labware, validation


Expand Down Expand Up @@ -1626,3 +1633,100 @@ def configure_for_volume(self, volume: float) -> None:
if last_location and isinstance(last_location.labware, labware.Well):
self.move_to(last_location.labware.top())
self._core.configure_for_volume(volume)

@requires_version(2, 16)
def configure_nozzle_layout(
self, requested_nozzle_layout: Optional[NozzleLayoutBase]
) -> None:
"""Configure a pipette to pick up less than the maximum tip capacity. The pipette
will remain in its partial state until this function is called again without any inputs. All subsequent
pipetting calls will execute with the new nozzle layout meaning that the pipette will perform
robot moves in the set nozzle layout.
:param requested_nozzle_layout: The requested nozzle layout should specify the shape that you
wish to configure your pipette to. Certain pipettes are restricted to a subset of `nozzle_layout`
types. See the note below on the different `nozzle_layout` types.
:type requested_nozzle_layout: `NozzleLayoutBase` or None.
.. note ::
Unlike many other API function calls, you will need to import specialized
objects to utilize this function. All of the specialized objects will
require at least one parameter - the primary nozzle. A primary nozzle
signifies the point in space that the robot will use to determine how to
perform moves to different locations on the deck.
The current available nozzle layouts are:
`SingleNozzleLayout` and `ColumnNozzleLayout`
`SingleNozzleLayout` will set the pipette to single pick up mode using
the specified primary nozzle. **Note** that only the outer nozzles of Opentrons
pipettes are available for this configuration.
`ColumnNozzleLayout` will set the pipette to full column pick up mode using
the specified primary nozzle. The full column will only
.. code-block:: python
from opentrons.protocol_api import SingleNozzleLayout, ColumnNozzleLayout
# Sets a pipette to single tip pick up mode using "A1" as the primary nozzle.
instr.configure_nozzle_layout(SingleNozzleLayout(primary_nozzle="A1")
# Sets a pipette to a full column pickup using "A1" as the primary nozzle.
instr.configure_nozzle_layout(ColumnNozzleLayout(primary_nozzle="A1"))
# Resets the pipette configuration to default
instr.configure_nozzle_layout()
"""
if self._core.has_tip():
raise CommandPreconditionViolated(
message=f"Cannot configure nozzle layout of {str(self)} while it has tips attached."
)

primary_nozzle = requested_nozzle_layout.primary_nozzle
if isinstance(requested_nozzle_layout, RowNozzleLayout):
if self.channels <= 8:
raise CommandParameterLimitViolated(
command_name="configure_nozzle_layout",
parameter_name="RowNozzleLayout",
limit_statement="RowNozzleLayout is incompatible with {self.channels} physical channels.",
actual_value=str(primary_nozzle),
)
# Not sure where to store these constants
if primary_nozzle not in ["A1", "H12"]:
raise CommandParameterLimitViolated(
command_name="configure_nozzle_layout",
parameter_name="RowNozzleLayout.primary_nozzle",
limit_statement="The primary nozzle can only be 'A1' or 'H12.",
actual_value=str(primary_nozzle),
)
# TODO need to switch ending nozzle based on primary nozzle
self._core.configure_nozzle_layout(
primary_nozzle=primary_nozzle, front_right_nozzle="A12"
)
elif isinstance(requested_nozzle_layout, ColumnNozzleLayout):
# TODO need to add additional primary nozzles for 96 channel
if primary_nozzle not in ["A1", "H1"]:
raise CommandParameterLimitViolated(
command_name="configure_nozzle_layout",
parameter_name="ColumnNozzleLayout.primary_nozzle",
limit_statement="The primary nozzle can only be 'A1' or 'H1.",
actual_value=str(primary_nozzle),
)
# TODO need to switch ending nozzle based on primary nozzle
self._core.configure_nozzle_layout(
primary_nozzle=primary_nozzle, front_right_nozzle="H1"
)
elif isinstance(requested_nozzle_layout, QuadrantNozzleLayout):
self._core.configure_nozzle_layout(
primary_nozzle=primary_nozzle,
back_left_nozzle=requested_nozzle_layout.back_left_nozzle,
front_right_nozzle=requested_nozzle_layout.front_right_nozzle,
)
elif isinstance(requested_nozzle_layout, SingleNozzleLayout):
self._core.configure_nozzle_layout(requested_nozzle_layout.primary_nozzle)
else:
# Default to a normal pickup
self._core.configure_nozzle_layout()
2 changes: 1 addition & 1 deletion api/src/opentrons/protocols/api_support/definitions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from .types import APIVersion

MAX_SUPPORTED_VERSION = APIVersion(2, 15)
MAX_SUPPORTED_VERSION = APIVersion(2, 16)
"""The maximum supported protocol API version in this release."""

MIN_SUPPORTED_VERSION = APIVersion(2, 0)
Expand Down
42 changes: 42 additions & 0 deletions api/tests/opentrons/protocol_api/test_instrument_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@
from opentrons.protocol_api.core.legacy.legacy_instrument_core import (
LegacyInstrumentCore,
)
from opentrons.protocol_api._nozzle_layout import (
SingleNozzleLayout,
RowNozzleLayout,
ColumnNozzleLayout,
QuadrantNozzleLayout,
)
from opentrons_shared_data.errors.exceptions import (
CommandPreconditionViolated,
CommandParameterLimitViolated,
)
from opentrons.types import Location, Mount, Point


Expand Down Expand Up @@ -912,3 +922,35 @@ def test_plunger_speed_removed(subject: InstrumentContext) -> None:
"""It should raise an error on PAPI >= v2.14."""
with pytest.raises(APIVersionError):
subject.speed


@pytest.mark.parametrize("channels", [8, 96, 1])
def test_configure_nozzle_layout(
decoy: Decoy,
subject: InstrumentContext,
mock_instrument_core: InstrumentCore,
channels: int,
) -> None:
"""Test configure nozzle layout API function"""
decoy.when(mock_instrument_core.get_channels()).then_return(channels)

decoy.when(
mock_instrument_core.configure_nozzle_layout(primary_nozzle="A1", front_right_nozzle="A12")
).then_return(None)
if channels == 8 or channels == 1:
with pytest.raises(CommandParameterLimitViolated):
subject.configure_nozzle_layout(RowNozzleLayout(primary_nozzle="A1"))
else:
subject.configure_nozzle_layout(RowNozzleLayout(primary_nozzle="A1"))

decoy.when(
mock_instrument_core.configure_nozzle_layout(primary_nozzle="A1")
).then_return(None)
subject.configure_nozzle_layout(SingleNozzleLayout(primary_nozzle="A1"))


subject.configure_nozzle_layout(
QuadrantNozzleLayout(
primary_nozzle="A1", back_left_nozzle="A1", front_right_nozzle="H12"
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ def test_load_virtual_pipette_nozzle_layout(
subject_instance: VirtualPipetteDataProvider,
) -> None:
"""It should return a NozzleMap object."""
# TODO create a real test
result = subject_instance.configure_virtual_pipette_nozzle_layout("my-pipette", "p300_multi_v2.1", "A1", "E1", "A1")
assert result

Expand Down

0 comments on commit 7353369

Please sign in to comment.