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): Experimental, barebones support for dropping stuff in the waste chute #13828

Merged
merged 13 commits into from
Oct 25, 2023
14 changes: 14 additions & 0 deletions api/drop_labware_in_waste_chute.py
SyntaxColoring marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from opentrons import protocol_api
SyntaxColoring marked this conversation as resolved.
Show resolved Hide resolved


requirements = {
"apiLevel": "2.16",
"robotType": "Flex",
}


def run(protocol: protocol_api.ProtocolContext) -> None:
waste_chute = protocol.load_waste_chute(orifice="wide_open")

labware = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "C2")
protocol.move_labware(labware, waste_chute, use_gripper=True)
19 changes: 19 additions & 0 deletions api/drop_tips_in_waste_chute.py
SyntaxColoring marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from opentrons import protocol_api


requirements = {
"apiLevel": "2.16",
"robotType": "Flex",
}


def run(protocol: protocol_api.ProtocolContext) -> None:
tip_rack = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "C2")

waste_chute = protocol.load_waste_chute(orifice="columnar_slit")

pipette = protocol.load_instrument("flex_8channel_1000", mount="left")

for column in tip_rack.columns():
pipette.pick_up_tip(column[0])
pipette.drop_tip(waste_chute)
4 changes: 3 additions & 1 deletion api/src/opentrons/protocol_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from .deck import Deck
from .instrument_context import InstrumentContext
from .labware import Labware, Well
from ._types import OFF_DECK
from .module_contexts import (
ModuleContext,
ThermocyclerContext,
Expand All @@ -24,6 +23,8 @@
MagneticBlockContext,
)
from ._liquid import Liquid
from ._types import OFF_DECK
from ._waste_chute import WasteChute

from .create_protocol_context import (
create_protocol_context,
Expand All @@ -44,6 +45,7 @@
"HeaterShakerContext",
"MagneticBlockContext",
"Labware",
"WasteChute",
"Well",
"Liquid",
"OFF_DECK",
Expand Down
16 changes: 16 additions & 0 deletions api/src/opentrons/protocol_api/_waste_chute.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from typing_extensions import Literal


class WasteChute:
"""Represents a Flex waste chute.

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

def __init__(
self,
with_staging_area_slot_d4: bool,
orifice: Literal["wide_open", "columnar_slit"],
) -> None:
self._with_staging_area_slot_d4 = with_staging_area_slot_d4
self._orifice = orifice
20 changes: 20 additions & 0 deletions api/src/opentrons/protocol_api/_waste_chute_dimensions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Constants for the dimensions of the Flex waste chute.

https://docs.google.com/spreadsheets/d/1yGRN-5QOa45p-jLsi4tL_PbHmKSK-QsXqHws6oBK7do/edit#gid=258725851
SyntaxColoring marked this conversation as resolved.
Show resolved Hide resolved

These should be moved into shared-data and interpreted by Protocol Engine.
"""


from opentrons.types import Point


SLOT_ORIGIN_TO_1_OR_8_TIP_A1 = Point(64, 21.91, 144)
SLOT_ORIGIN_TO_96_TIP_A1 = Point(14.445, 42.085, 115)

# TODO: This z-coord is misleading. We need to account for the labware height and the paddle height;
# we can't define this as a single coordinate.
SLOT_ORIGIN_TO_GRIPPER_JAW_CENTER = Point(64, 29, 136.5)

# This includes the height of the optional lid.
ENVELOPE_HEIGHT = 154
68 changes: 67 additions & 1 deletion api/src/opentrons/protocol_api/core/engine/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from __future__ import annotations

from typing import Optional, TYPE_CHECKING
from opentrons.motion_planning.waypoints import DEFAULT_GENERAL_ARC_Z_MARGIN

from opentrons.types import Location, Mount
from opentrons.hardware_control import SyncHardwareAPI
Expand All @@ -18,12 +19,15 @@
from opentrons.protocol_engine.errors.exceptions import TipNotAttachedError
from opentrons.protocol_engine.clients import SyncClient as EngineClient
from opentrons.protocols.api_support.definitions import MAX_SUPPORTED_VERSION
from opentrons.types import Point

from opentrons_shared_data.pipette.dev_types import PipetteNameType

from ..instrument import AbstractInstrument
from .well import WellCore

from ... import _waste_chute_dimensions

if TYPE_CHECKING:
from .protocol import ProtocolCore

Expand Down Expand Up @@ -373,6 +377,69 @@ def drop_tip(

self._protocol_core.set_last_location(location=location, mount=self.get_mount())

def _drop_tip_in_place(self, home_after: Optional[bool]) -> None:
self._engine_client.drop_tip_in_place(
pipette_id=self._pipette_id,
home_after=home_after,
)

def drop_tip_in_waste_chute(
self, waste_chute_core: WasteChuteCore, home_after: Optional[bool]
) -> None:
# TODO: Can we get away with implementing this in two steps like this,
# or does drop_tip() need to take the waste chute location because the z-height
# depends on the intent of dropping tip? How would Protocol Designer want to implement
# this?
self._move_to_waste_chute(
waste_chute_load_name=waste_chute_core.load_name,
force_direct=False,
speed=None,
)
self._drop_tip_in_place(home_after=home_after)

def _move_to_waste_chute(
self,
waste_chute_load_name: str,
force_direct: bool,
speed: Optional[float],
) -> None:
if self.get_channels() == 96:
if waste_chute_load_name not in {
"stagingAreaSlotWithWasteChuteRightAdapterNoCover",
"WasteChuteRightAdapterNoCover",
}:
# TODO: Instead of hard-coding load names, we should see whether the addressable area "96ChannelWasteChute" has
# been provided by any loaded fixture."
raise ValueError("Wrong waste chute, you silly goose.")
slot_origin_to_tip_a1 = _waste_chute_dimensions.SLOT_ORIGIN_TO_96_TIP_A1
else:
slot_origin_to_tip_a1 = _waste_chute_dimensions.SLOT_ORIGIN_TO_1_OR_8_TIP_A1

# TODO: All of this logic to compute the destination coordinate belongs in Protocol Engine.
slot_d3 = next(
s
for s in self._protocol_core.get_deck_definition()["locations"][
"orderedSlots"
]
if s["id"] == "D3"
)
slot_d3_origin = Point(*slot_d3["position"])
destination_point = slot_d3_origin + slot_origin_to_tip_a1
minimum_z = (
# TODO: Why isn't DEFAULT_GENERAL_ARC_Z_MARGIN valid?
_waste_chute_dimensions.ENVELOPE_HEIGHT
+ 7.5
)

self.move_to(
Location(destination_point, labware=None),
well_core=None,
force_direct=force_direct,
minimum_z_height=minimum_z,
# minimum_z_height=minimum_z,
speed=speed,
)

def home(self) -> None:
z_axis = self._engine_client.state.pipettes.get_z_axis(self._pipette_id)
plunger_axis = self._engine_client.state.pipettes.get_plunger_axis(
Expand All @@ -394,7 +461,6 @@ def move_to(
minimum_z_height: Optional[float],
speed: Optional[float],
) -> None:

if well_core is not None:
labware_id = well_core.labware_id
well_name = well_core.get_name()
Expand Down
60 changes: 52 additions & 8 deletions api/src/opentrons/protocol_api/core/engine/protocol.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
"""ProtocolEngine-based Protocol API core implementation."""
from typing import Dict, Optional, Type, Union, List, Tuple
from typing_extensions import Literal

from opentrons.protocol_api import _waste_chute_dimensions

from opentrons.protocol_engine.commands import LoadModuleResult
from opentrons_shared_data.deck.dev_types import DeckDefinitionV3, SlotDefV3
Expand Down Expand Up @@ -145,6 +148,7 @@ def load_labware(
) -> LabwareCore:
"""Load a labware using its identifying parameters."""
load_location = self._convert_labware_location(location=location)
assert not isinstance(load_location, WasteChuteLocation)

custom_labware_params = (
self._engine_client.state.labware.find_custom_labware_load_params()
Expand Down Expand Up @@ -204,6 +208,7 @@ def load_adapter(
) -> LabwareCore:
"""Load an adapter using its identifying parameters"""
load_location = self._get_non_stacked_location(location=location)
assert not isinstance(load_location, WasteChuteLocation)

custom_labware_params = (
self._engine_client.state.labware.find_custom_labware_load_params()
Expand Down Expand Up @@ -248,16 +253,19 @@ def move_labware(
self,
labware_core: LabwareCore,
new_location: Union[
DeckSlotName, LabwareCore, ModuleCore, NonConnectedModuleCore, OffDeckType
DeckSlotName,
LabwareCore,
ModuleCore,
NonConnectedModuleCore,
OffDeckType,
WasteChuteCore,
],
use_gripper: bool,
pause_for_manual_move: bool,
pick_up_offset: Optional[Tuple[float, float, float]],
drop_offset: Optional[Tuple[float, float, float]],
) -> None:
"""Move the given labware to a new location."""
to_location = self._convert_labware_location(location=new_location)

if use_gripper:
strategy = LabwareMovementStrategy.USING_GRIPPER
elif pause_for_manual_move:
Expand All @@ -272,11 +280,29 @@ def move_labware(
if pick_up_offset
else None
)
_drop_offset = (
LabwareOffsetVector(x=drop_offset[0], y=drop_offset[1], z=drop_offset[2])
if drop_offset
else None
)

if isinstance(new_location, WasteChuteCore):
# lol
to_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_D3)
slot_width = 128
slot_height = 86
drop_offset_point = (
_waste_chute_dimensions.SLOT_ORIGIN_TO_GRIPPER_JAW_CENTER
- Point(x=slot_width / 2, y=slot_height / 2)
)
_drop_offset = LabwareOffsetVector(
x=drop_offset_point.x, y=drop_offset_point.y, z=drop_offset_point.z
)

else:
to_location = self._convert_labware_location(location=new_location)
_drop_offset = (
LabwareOffsetVector(
x=drop_offset[0], y=drop_offset[1], z=drop_offset[2]
)
if drop_offset
else None
)

# TODO(mm, 2023-02-23): Check for conflicts with other items on the deck,
# when move_labware() support is no longer experimental.
Expand Down Expand Up @@ -606,3 +632,21 @@ def _get_non_stacked_location(
return OFF_DECK_LOCATION
elif isinstance(location, DeckSlotName):
return DeckSlotLocation(slotName=location)


def _waste_chute_fixture_id(
with_staging_area_slot_d4: bool, orifice: Literal["wide_open", "columnar_slit"]
) -> str:
if orifice not in {"wide_open", "columnar_slit"}:
raise ValueError(
f"orifice must be 'wide_open' or 'columnar_slit', not {repr(orifice)}."
)

ids: Dict[Tuple[bool, Literal["wide_open", "columnar_slit"]], str] = {
(False, "wide_open"): "wasteChuteRightAdapterNoCover",
(False, "columnar_slit"): "wasteChuteRightAdapterCovered",
(True, "wide_open"): "stagingAreaSlotWithWasteChuteRightAdapterNoCover",
(True, "columnar_slit"): "stagingAreaSlotWithWasteChuteRightAdapterCovered",
}

return ids[with_staging_area_slot_d4, orifice]
8 changes: 7 additions & 1 deletion api/src/opentrons/protocol_api/core/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ def drop_tip(
"""
...

@abstractmethod
def drop_tip_in_waste_chute(
self, waste_chute_core: WasteChuteCoreType, home_after: Optional[bool]
) -> None:
...

@abstractmethod
def home(self) -> None:
...
Expand Down Expand Up @@ -237,4 +243,4 @@ def configure_for_volume(self, volume: float) -> None:
...


InstrumentCoreType = TypeVar("InstrumentCoreType", bound=AbstractInstrument[Any])
InstrumentCoreType = TypeVar("InstrumentCoreType", bound=AbstractInstrument[Any, Any])
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,13 @@ def drop_tip(
f"Could not return tip to {labware_core.get_display_name()}"
)

def drop_tip_in_waste_chute(
self, waste_chute_core: LegacyWasteChuteCore, home_after: Optional[bool]
) -> None:
raise APIVersionError(
"Dropping tips in a waste chute is not supported in this API Version."
)

def home(self) -> None:
"""Home the mount"""
self._protocol_interface.get_hardware().home_z(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
from typing import Dict, List, Optional, Set, Union, cast, Tuple
from typing_extensions import Literal

from opentrons_shared_data.deck.dev_types import DeckDefinitionV3
from opentrons_shared_data.labware.dev_types import LabwareDefinition
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,9 @@ def drop_tip(
f"Could not return tip to {labware_core.get_display_name()}"
)

def drop_tip_in_waste_chute(self, waste_chute_core: LegacyWasteChuteCore) -> None:
raise APIVersionError("Waste chutes are not supported in this PAPI version.")

def home(self) -> None:
self._protocol_interface.set_last_location(None)

Expand Down
8 changes: 7 additions & 1 deletion api/src/opentrons/protocol_api/core/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,13 @@ def load_adapter(
def move_labware(
self,
labware_core: LabwareCoreType,
new_location: Union[DeckSlotName, LabwareCoreType, ModuleCoreType, OffDeckType],
new_location: Union[
DeckSlotName,
LabwareCoreType,
ModuleCoreType,
WasteChuteCoreType,
OffDeckType,
],
use_gripper: bool,
pause_for_manual_move: bool,
pick_up_offset: Optional[Tuple[float, float, float]],
Expand Down
Loading