Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): add extension mount and axes to top level types #12671

Merged
10 changes: 9 additions & 1 deletion api/src/opentrons/hardware_control/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -571,9 +571,17 @@ async def home_plunger(self, mount: top_types.Mount) -> None:
@ExecutionManagerProvider.wait_for_running
async def home(self, axes: Optional[List[Axis]] = None) -> None:
"""Home the entire robot and initialize current position."""
# Should we assert/ raise an error or just remove non-ot2 axes and log warning?
# No internal code passes OT3 axes as arguments on an OT2. But a user/ client
# can still explicitly specify an OT3 axis even when working on an OT2.
# Adding this check in order to prevent misuse of axes types.
if axes:
assert all(
axis in Axis.ot2_axes() for axis in axes
), "Received a non-OT2 axis for homing."
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is correct modulo using a more specific exception than AssertionError - I think we want like a NotSupportedByHardware error

self._reset_last_mount()
# Initialize/update current_position
checked_axes = axes or [ax for ax in Axis]
checked_axes = axes or [ax for ax in Axis.ot2_axes()]
gantry = [ax for ax in checked_axes if ax in Axis.gantry_axes()]
smoothie_gantry = [ax.name.upper() for ax in gantry]
smoothie_pos = {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ async def get_attached_instruments(
"""
return {
mount: await self._query_mount(mount, expected.get(mount))
for mount in Mount
for mount in Mount.ot2_mounts()
}

def set_active_current(self, axis_currents: Dict[Axis, float]) -> None:
Expand Down
4 changes: 2 additions & 2 deletions api/src/opentrons/hardware_control/backends/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def _sanitize_attached_instrument(

self._attached_instruments = {
m: _sanitize_attached_instrument(attached_instruments.get(m))
for m in types.Mount
for m in types.Mount # Check if addition of extension Mount creates issues for this
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feels like we should just use ot2_mounts here?

}
self._stubbed_attached_modules = attached_modules
self._position = copy.copy(self._smoothie_driver.homed_position)
Expand Down Expand Up @@ -284,7 +284,7 @@ async def get_attached_instruments(
"""
return {
mount: self._attached_to_mount(mount, expected.get(mount))
for mount in types.Mount
for mount in types.Mount.ot2_mounts()
}

def set_active_current(self, axis_currents: Dict[Axis, float]) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ def reset_instrument(self, mount: Optional[MountType] = None) -> None:
"""

def _reset(m: MountType) -> None:
if isinstance(m, top_types.Mount) and m not in top_types.Mount.ot2_mounts():
self._ihp_log.warning(
"Received a non OT2 mount for resetting. Skipping"
)
return
self._ihp_log.info(f"Resetting configuration for {m}")
p = self._attached_instruments[m]
if not p:
Expand Down
31 changes: 24 additions & 7 deletions api/src/opentrons/hardware_control/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,21 @@ class MotionChecks(enum.Enum):


class Axis(enum.Enum):
X = 0
Y = 1
Z = 2
A = 3
B = 4
C = 5
X = 0 # Gantry X
Y = 1 # Gantry Y
Z = 2 # left pipette mount Z
A = 3 # right pipette mount Z
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to change the ot3 stuff to use Z/A/B/C. I think we should handle Z_L, Z_R, P_L, P_R. I think this can be handled in one of two ways:

  • Just add them. Add per-machine iterators ot2_axes and flex_axes like we have for the mounts. It's non-unique now, that's fine.
  • Make it two types again but now they're top-level and have some sort of equivalence relationship

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, not sure which change you are talking about here. There are still two different axis types- Axis and OT3Axis, and OT3Axis uses Z_L, Z_R, etc. Here I've only added comments and Gripper Z & Jaw to the existing Axis type.

B = 4 # left pipette plunger
C = 5 # right pipette plunger
Z_G = 6 # Gripper Z
G = 7 # Gripper Jaws

@classmethod
def by_mount(cls, mount: top_types.Mount) -> "Axis":
bm = {top_types.Mount.LEFT: cls.Z, top_types.Mount.RIGHT: cls.A}
return bm[mount]

# TODO (spp, 2023-5-4): deprecate this method & create a replacement called 'pipette_mount_axes'
@classmethod
def mount_axes(cls) -> Tuple["Axis", "Axis"]:
"""The axes which are used for moving pipettes up and down."""
Expand All @@ -66,12 +69,19 @@ def to_mount(cls, inst: "Axis") -> top_types.Mount:
cls.A: top_types.Mount.RIGHT,
cls.B: top_types.Mount.LEFT,
cls.C: top_types.Mount.RIGHT,
cls.Z_G: top_types.Mount.EXTENSION,
cls.G: top_types.Mount.EXTENSION,
}[inst]

@classmethod
def pipette_axes(cls) -> Tuple["Axis", "Axis"]:
return cls.B, cls.C

@classmethod
def ot2_axes(cls) -> List["Axis"]:
"""Returns only OT2 axes."""
return [axis for axis in Axis if axis not in [Axis.Z_G, Axis.G]]

def __str__(self) -> str:
return self.name

Expand All @@ -88,11 +98,13 @@ def from_mount(
top_types.Mount, top_types.MountType, top_types.OT3MountType, "OT3Mount"
],
) -> "OT3Mount":
if mount == top_types.Mount.EXTENSION or mount == top_types.MountType.EXTENSION:
return OT3Mount.GRIPPER
return cls[mount.name]

def to_mount(self) -> top_types.Mount:
if self.value == self.GRIPPER.value:
raise KeyError("Gripper mount is not representable")
return top_types.Mount.EXTENSION
return top_types.Mount[self.name]


Expand Down Expand Up @@ -141,6 +153,7 @@ def by_mount(cls, mount: Union[top_types.Mount, OT3Mount]) -> "OT3Axis":
bm = {
top_types.Mount.LEFT: cls.Z_L,
top_types.Mount.RIGHT: cls.Z_R,
top_types.Mount.EXTENSION: cls.Z_G,
OT3Mount.LEFT: cls.Z_L,
OT3Mount.RIGHT: cls.Z_R,
OT3Mount.GRIPPER: cls.Z_G,
Expand All @@ -156,6 +169,8 @@ def from_axis(cls, axis: Union[Axis, "OT3Axis"]) -> "OT3Axis":
Axis.A: cls.Z_R,
Axis.B: cls.P_L,
Axis.C: cls.P_R,
Axis.Z_G: cls.Z_G,
Axis.G: cls.G,
}
try:
return am[axis] # type: ignore
Expand All @@ -170,6 +185,8 @@ def to_axis(self) -> Axis:
OT3Axis.Z_R: Axis.A,
OT3Axis.P_L: Axis.B,
OT3Axis.P_R: Axis.C,
OT3Axis.Z_G: Axis.Z_G,
OT3Axis.G: Axis.G,
}
return am[self]

Expand Down
4 changes: 4 additions & 0 deletions api/src/opentrons/protocol_api/core/engine/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ def move_labware(
pick_up_offset=_pick_up_offset,
drop_offset=_drop_offset,
)
if strategy == LabwareMovementStrategy.USING_GRIPPER:
# Clear out last location since it is not relevant to pipetting
# and we only use last location for in-place pipetting commands
self.set_last_location(location=None, mount=Mount.EXTENSION)

def _resolve_module_hardware(
self, serial_number: str, model: ModuleModel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def __init__(
self._deck_layout = Deck() if deck_layout is None else deck_layout

self._instruments: Dict[Mount, Optional[LegacyInstrumentCore]] = {
mount: None for mount in Mount
mount: None for mount in Mount.ot2_mounts() # Legacy core works only on OT2
}
self._bundled_labware = bundled_labware
self._extra_labware = extra_labware or {}
Expand Down
5 changes: 5 additions & 0 deletions api/src/opentrons/protocol_api/protocol_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ def __init__(
self._core = core
self._core_map = core_map or LoadedCoreMap()
self._deck = deck or Deck(protocol_core=core, core_map=self._core_map)

# With the introduction of Extension mount type, this dict initializes to include
# the extension mount, for both ot2 & 3. While it doesn't seem like it would
# create an issue in the current PAPI context, it would be much safer to
# only use mounts available on the robot.
self._instruments: Dict[Mount, Optional[InstrumentContext]] = {
mount: None for mount in Mount
}
Expand Down
29 changes: 25 additions & 4 deletions api/src/opentrons/protocol_api/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,44 @@
from .labware import Well


class InvalidPipetteMountError(ValueError):
"""An error raised when attempting to load pipettes on an invalid mount."""


class PipetteMountTypeError(TypeError):
"""An error raised when an invalid mount type is used for loading pipettes."""


def ensure_mount(mount: Union[str, Mount]) -> Mount:
"""Ensure that an input value represents a valid Mount."""
if mount in [Mount.EXTENSION, "extension"]:
# This would cause existing protocols that might be iterating over mount types
# for loading pipettes to raise an error because Mount now includes Extension mount.
# For example, this would raise error-
# ```
# for i, mount in enumerate(Mount):
# pipette[i] = ctx.load_instrument("p300_single", mount)
# ```
# But this is a very rare use case and none of the protocols in protocol library
# or protocols seen/ built by support/ science/ apps engg do this so it might be
# safe to raise this error now?
raise InvalidPipetteMountError(
f"Loading pipettes on {mount} is not allowed."
f"Use the left or right mounts instead."
)
if isinstance(mount, Mount):
return mount

if isinstance(mount, str):
try:
return Mount[mount.upper()]
except KeyError as e:
# TODO(mc, 2022-08-25): create specific exception type
raise ValueError(
raise InvalidPipetteMountError(
"If mount is specified as a string, it must be 'left' or 'right';"
f" instead, {mount} was given."
) from e

# TODO(mc, 2022-08-25): create specific exception type
raise TypeError(
raise PipetteMountTypeError(
"Instrument mount should be 'left', 'right', or an opentrons.types.Mount;"
f" instead, {mount} was given."
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from ...state import StateView


# Question (spp): Does this offset work for gripper mount too?
# These offsets are based on testing attach flows with 8/1 channel pipettes
_INSTRUMENT_ATTACH_OFFSET = Point(y=10, z=400)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ async def execute(self, params: OpenLabwareLatchParams) -> OpenLabwareLatchResul
[
MotorAxis.RIGHT_Z,
MotorAxis.LEFT_Z,
MotorAxis.EXTENSION_Z,
]
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ async def execute(
[
MotorAxis.RIGHT_Z,
MotorAxis.LEFT_Z,
MotorAxis.EXTENSION_Z,
]
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ async def execute(self, params: CloseLidParams) -> CloseLidResult:
MotorAxis.Y,
MotorAxis.RIGHT_Z,
MotorAxis.LEFT_Z,
MotorAxis.EXTENSION_Z,
]
)

Expand Down
3 changes: 2 additions & 1 deletion api/src/opentrons/protocol_engine/errors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
HardwareNotSupportedError,
LabwareMovementNotAllowedError,
LocationIsOccupiedError,
InvalidAxisForRobotType,
)

from .error_occurrence import ErrorOccurrence
Expand Down Expand Up @@ -95,7 +96,7 @@
"HardwareNotSupportedError",
"LabwareMovementNotAllowedError",
"LocationIsOccupiedError",
"FirmwareUpdateRequired",
"InvalidAxisForRobotType",
# error occurrence models
"ErrorOccurrence",
]
4 changes: 4 additions & 0 deletions api/src/opentrons/protocol_engine/errors/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,7 @@ class PipetteNotReadyToAspirateError(ProtocolEngineError):

class InvalidPipettingVolumeError(ProtocolEngineError):
"""Raised when pipetting a volume larger than the pipette volume."""


class InvalidAxisForRobotType(ProtocolEngineError):
"""Raised when attempting to use an axis that is not present on the given type of robot."""
3 changes: 3 additions & 0 deletions api/src/opentrons/protocol_engine/execution/equipment.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ async def load_pipette(
Returns:
A LoadedPipetteData object.
"""
# TODO (spp, 2023-05-10): either raise error if using MountType.EXTENSION in
# load pipettes command, or change the mount type used to be a restricted
# PipetteMountType which has only pipette mounts and not the extension mount.
use_virtual_pipettes = self._state_store.config.use_virtual_pipettes

pipette_name_value = (
Expand Down
11 changes: 10 additions & 1 deletion api/src/opentrons/protocol_engine/execution/gantry_mover.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from ..state import StateView
from ..types import MotorAxis, CurrentWell
from ..errors import MustHomeError
from ..errors import MustHomeError, InvalidAxisForRobotType


_MOTOR_AXIS_TO_HARDWARE_AXIS: Dict[MotorAxis, HardwareAxis] = {
Expand All @@ -22,6 +22,8 @@
MotorAxis.RIGHT_Z: HardwareAxis.A,
MotorAxis.LEFT_PLUNGER: HardwareAxis.B,
MotorAxis.RIGHT_PLUNGER: HardwareAxis.C,
MotorAxis.EXTENSION_Z: HardwareAxis.Z_G,
MotorAxis.EXTENSION_JAW: HardwareAxis.G,
}

# The height of the bottom of the pipette nozzle at home position without any tips.
Expand Down Expand Up @@ -185,6 +187,13 @@ async def home(self, axes: Optional[List[MotorAxis]]) -> None:
await self._hardware_api.home_plunger(Mount.RIGHT)
else:
hardware_axes = [_MOTOR_AXIS_TO_HARDWARE_AXIS[a] for a in axes]
if self._state_view.config.robot_type == "OT-2 Standard" and any(
axis not in HardwareAxis.ot2_axes() for axis in hardware_axes
):
raise InvalidAxisForRobotType(
f"{axes} includes axes that are not valid for OT-2 Standard robot type"
)
# Hardware API will raise error if invalid axes are passed for the type of robot
await self._hardware_api.home(axes=hardware_axes)


Expand Down
2 changes: 2 additions & 0 deletions api/src/opentrons/protocol_engine/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ class MotorAxis(str, Enum):
RIGHT_Z = "rightZ"
LEFT_PLUNGER = "leftPlunger"
RIGHT_PLUNGER = "rightPlunger"
EXTENSION_Z = "extensionZ"
EXTENSION_JAW = "extensionJaw"


# TODO(mc, 2022-01-18): use opentrons_shared_data.module.dev_types.ModuleModel
Expand Down
Loading