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): allow custom user offsets for deck configured trash bins and waste chute #14560

Merged
merged 16 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from 13 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
3 changes: 1 addition & 2 deletions api/src/opentrons/commands/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
from . import types as command_types

from opentrons.types import Location
from opentrons.protocol_api._trash_bin import TrashBin
from opentrons.protocol_api._waste_chute import WasteChute
from opentrons.protocol_api._disposal_locations import TrashBin, WasteChute

if TYPE_CHECKING:
from opentrons.protocol_api import InstrumentContext
Expand Down
3 changes: 1 addition & 2 deletions api/src/opentrons/commands/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

from opentrons.protocol_api.labware import Well, Labware
from opentrons.protocol_api.module_contexts import ModuleContext
from opentrons.protocol_api._trash_bin import TrashBin
from opentrons.protocol_api._waste_chute import WasteChute
from opentrons.protocol_api._disposal_locations import TrashBin, WasteChute
from opentrons.protocol_api._types import OffDeckType
from opentrons.types import Location, DeckLocation

Expand Down
3 changes: 1 addition & 2 deletions api/src/opentrons/commands/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
if TYPE_CHECKING:
from opentrons.protocol_api import InstrumentContext
from opentrons.protocol_api.labware import Well
from opentrons.protocol_api._trash_bin import TrashBin
from opentrons.protocol_api._waste_chute import WasteChute
from opentrons.protocol_api._disposal_locations import TrashBin, WasteChute

from opentrons.types import Location

Expand Down
5 changes: 2 additions & 3 deletions api/src/opentrons/motion_planning/deck_conflict.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class TrashBin:
"""A non-labware trash bin (loaded via api level 2.16 and above)."""

name_for_errors: str
highest_z: float


@dataclass
Expand Down Expand Up @@ -138,9 +139,7 @@ def is_allowed(self, item: DeckItem) -> bool:
elif isinstance(item, _Module):
return item.highest_z_including_labware < self.max_height
elif isinstance(item, TrashBin):
# Since this is a restriction for OT-2 only and OT-2 trashes exceeded the height limit, always return False
# TODO(jbl 2024-01-16) Include trash height and use that for check for more robustness
return False
return item.highest_z < self.max_height


class _NoModule(NamedTuple):
Expand Down
3 changes: 1 addition & 2 deletions api/src/opentrons/protocol_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@
)
from ._liquid import Liquid
from ._types import OFF_DECK
from ._trash_bin import TrashBin
from ._waste_chute import WasteChute
from ._disposal_locations import TrashBin, WasteChute
from ._nozzle_layout import (
COLUMN,
ALL,
Expand Down
225 changes: 225 additions & 0 deletions api/src/opentrons/protocol_api/_disposal_locations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
from __future__ import annotations

from dataclasses import dataclass
from typing_extensions import Protocol as TypingProtocol

from opentrons.types import DeckSlotName
from opentrons.protocols.api_support.types import APIVersion
from opentrons.protocol_engine.clients import SyncClient


# TODO(jbl 2024-02-26) these are hardcoded here since there is a 1 to many relationship going from
# addressable area names to cutout fixture ids. Currently for trash and waste chute this would not be
# an issue (trash has only one fixture that provides it, all waste chute fixtures are the same height).
# The ultimate fix for this is a multiple pass analysis, so for now these are being hardcoded to avoid
# writing cumbersome guessing logic for area name -> fixture name while still providing a direct link to
# the numbers in shared data.
_TRASH_BIN_CUTOUT_FIXTURE = "trashBinAdapter"
_TRASH_BIN_OT2_CUTOUT_FIXTURE = "fixedTrashSlot"
_WASTE_CHUTE_CUTOUT_FIXTURE = "wasteChuteRightAdapterCovered"


@dataclass(frozen=True)
class DisposalOffset:
x: float
y: float
z: float


class DisposalLocation(TypingProtocol):
"""Abstract class for disposal location."""

def top(self, x: float = 0, y: float = 0, z: float = 0) -> DisposalLocation:
"""Returns a disposal location with a user set offset."""
Copy link
Contributor

Choose a reason for hiding this comment

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

can we change this name? maybe disposale_location?

...

@property
def offset(self) -> DisposalOffset:
"""Offset of the disposal location.

:meta private:

This is intended for Opentrons internal use only and is not a guaranteed API.
"""
...

@property
def location(self) -> DeckSlotName:
"""Location of the disposal location.

:meta private:

This is intended for Opentrons internal use only and is not a guaranteed API.
"""
...

@property
def area_name(self) -> str:
"""Addressable area name of the disposal location.

:meta private:

This is intended for Opentrons internal use only and is not a guaranteed API.
"""
...

@property
def height(self) -> float:
"""Height of the disposal location.

:meta private:

This is intended for Opentrons internal use only and is not a guaranteed API.
"""
...


class TrashBin(DisposalLocation):
"""Represents a Flex or OT-2 trash bin.

See :py:meth:`.ProtocolContext.load_trash_bin`.
"""

def __init__(
self,
location: DeckSlotName,
addressable_area_name: str,
engine_client: SyncClient,
api_version: APIVersion,
offset: DisposalOffset = DisposalOffset(x=0, y=0, z=0),
) -> None:
self._location = location
self._addressable_area_name = addressable_area_name
self._offset = offset
self._api_version = api_version
self._engine_client = engine_client
if self._engine_client.state.config.robot_type == "OT-2 Standard":
self._cutout_fixture_name = _TRASH_BIN_OT2_CUTOUT_FIXTURE
else:
self._cutout_fixture_name = _TRASH_BIN_CUTOUT_FIXTURE

def top(self, x: float = 0, y: float = 0, z: float = 0) -> TrashBin:
"""Returns a trash bin with a user set offset."""
return TrashBin(
location=self._location,
addressable_area_name=self._addressable_area_name,
engine_client=self._engine_client,
api_version=self._api_version,
offset=DisposalOffset(x=x, y=y, z=z),
)

@property
def offset(self) -> DisposalOffset:
"""Current offset of the trash bin..

:meta private:

This is intended for Opentrons internal use only and is not a guaranteed API.
"""
return self._offset

@property
def location(self) -> DeckSlotName:
"""Location of the trash bin.

:meta private:

This is intended for Opentrons internal use only and is not a guaranteed API.
"""
return self._location

@property
def area_name(self) -> str:
"""Addressable area name of the trash bin.

:meta private:

This is intended for Opentrons internal use only and is not a guaranteed API.
"""
return self._addressable_area_name

@property
def height(self) -> float:
"""Height of the trash bin.

:meta private:

This is intended for Opentrons internal use only and is not a guaranteed API.
"""
return self._engine_client.state.addressable_areas.get_fixture_height(
self._cutout_fixture_name
)


class WasteChute(DisposalLocation):
"""Represents a Flex waste chute.

See :py:meth:`.ProtocolContext.load_waste_chute`.
"""

def __init__(
self,
engine_client: SyncClient,
api_version: APIVersion,
offset: DisposalOffset = DisposalOffset(x=0, y=0, z=0),
) -> None:
self._engine_client = engine_client
self._api_version = api_version
self._offset = offset

def top(self, x: float = 0, y: float = 0, z: float = 0) -> WasteChute:
"""Returns a waste chute with a user set offset."""
return WasteChute(
engine_client=self._engine_client,
api_version=self._api_version,
offset=DisposalOffset(x=x, y=y, z=z),
)

@property
def offset(self) -> DisposalOffset:
"""Current offset of the waste chute.

:meta private:

This is intended for Opentrons internal use only and is not a guaranteed API.
"""
return self._offset

@property
def location(self) -> DeckSlotName:
"""Location of the waste chute.

:meta private:

This is intended for Opentrons internal use only and is not a guaranteed API.
"""
return DeckSlotName.SLOT_D3

@property
def area_name(self) -> str:
"""Addressable area name of the waste chute.

:meta private:

This is intended for Opentrons internal use only and is not a guaranteed API.
"""
# TODO(jbl 2024-02-06) this is hardcoded here and should be removed when a multiple pass analysis exists
#
# We want to tell Protocol Engine that there's a waste chute in the waste chute location when it's loaded,
# so analysis can prevent the user from doing anything that would collide with it. At the same time, we
# do not want to create a false negative when it comes to addressable area conflict. We therefore use the
# addressable area `1ChannelWasteChute` because every waste chute cutout fixture provides it and it will
# provide the engine with the information it needs.
return "1ChannelWasteChute"

@property
def height(self) -> float:
"""Height of the waste chute.

:meta private:

This is intended for Opentrons internal use only and is not a guaranteed API.
"""
return self._engine_client.state.addressable_areas.get_fixture_height(
_WASTE_CHUTE_CUTOUT_FIXTURE
)
32 changes: 0 additions & 32 deletions api/src/opentrons/protocol_api/_trash_bin.py

This file was deleted.

5 changes: 0 additions & 5 deletions api/src/opentrons/protocol_api/_waste_chute.py

This file was deleted.

7 changes: 4 additions & 3 deletions api/src/opentrons/protocol_api/core/engine/deck_conflict.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@
from opentrons.protocol_engine.errors.exceptions import LabwareNotLoadedOnModuleError
from opentrons.protocol_engine.types import StagingSlotLocation, Dimensions
from opentrons.types import DeckSlotName, StagingSlotName, Point
from ..._trash_bin import TrashBin
from ..._waste_chute import WasteChute
from ..._disposal_locations import TrashBin, WasteChute

if TYPE_CHECKING:
from ...labware import Labware
Expand Down Expand Up @@ -521,7 +520,9 @@ def _map_disposal_location(
if isinstance(disposal_location, TrashBin):
return (
disposal_location.location,
wrapped_deck_conflict.TrashBin(name_for_errors="trash bin"),
wrapped_deck_conflict.TrashBin(
name_for_errors="trash bin", highest_z=disposal_location.height
),
)
else:
return None
Expand Down
18 changes: 12 additions & 6 deletions api/src/opentrons/protocol_api/core/engine/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@
from ..instrument import AbstractInstrument
from .well import WellCore

from ..._trash_bin import TrashBin
from ..._waste_chute import WasteChute
from ..._disposal_locations import TrashBin, WasteChute

if TYPE_CHECKING:
from .protocol import ProtocolCore
Expand Down Expand Up @@ -478,13 +477,16 @@ def drop_tip(
self._protocol_core.set_last_location(location=location, mount=self.get_mount())

def drop_tip_in_disposal_location(
self, disposal_location: Union[TrashBin, WasteChute], home_after: Optional[bool]
self,
disposal_location: Union[TrashBin, WasteChute],
home_after: Optional[bool],
alternate_tip_drop: bool = False,
) -> None:
self._move_to_disposal_location(
disposal_location,
force_direct=False,
speed=None,
alternate_tip_drop=True,
alternate_tip_drop=alternate_tip_drop,
)
self._drop_tip_in_place(home_after=home_after)
self._protocol_core.set_last_location(location=None, mount=self.get_mount())
Expand All @@ -498,10 +500,14 @@ def _move_to_disposal_location(
) -> None:
# TODO (nd, 2023-11-30): give appropriate offset when finalized
# https://opentrons.atlassian.net/browse/RSS-391
offset = AddressableOffsetVector(x=0, y=0, z=0)

disposal_offset = disposal_location.offset
offset = AddressableOffsetVector(
x=disposal_offset.x, y=disposal_offset.y, z=disposal_offset.z
)

if isinstance(disposal_location, TrashBin):
addressable_area_name = disposal_location._addressable_area_name
addressable_area_name = disposal_location.area_name
self._engine_client.move_to_addressable_area_for_drop_tip(
pipette_id=self._pipette_id,
addressable_area_name=addressable_area_name,
Expand Down
Loading
Loading