Skip to content

Commit

Permalink
feat(shared-data,api,robot-server): Use new OT-3 deck slot naming sty…
Browse files Browse the repository at this point in the history
…le (#12571)

Co-authored-by: Jeremy Leon <[email protected]>
  • Loading branch information
SyntaxColoring and jbleon95 authored Jun 2, 2023
1 parent 030b9a0 commit 3404c50
Show file tree
Hide file tree
Showing 35 changed files with 1,137 additions and 219 deletions.
15 changes: 14 additions & 1 deletion api/release-notes-internal.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,21 @@ This is internal release 0.11.0 for the Opentrons Flex robot software, involving

Some things are known not to work, and are listed below. Specific compatibility notes about peripheral hardware are also listed.

## Smaller Fun Features
## Update Notes

- ⚠️ After upgrading your robot to 0.11.0, you'll need to factory-reset its run history before you can use it.

1. From the robot's 3-dot menu (⋮), go to **Robot settings.**
2. Under **Advanced > Factory reset**, select **Choose reset settings.**
3. Choose **Clear protocol run history,** and then select **Clear data and restart robot.**

Note that this will remove all of your saved labware offsets.

You will need to follow these steps if you subsequently downgrade back to a prior release, too.

## New Stuff In This Release

- When interacting with an OT-3, the app will use the newer names for the deck slots, like "C2", instead of the names from the OT-2, like "5".
- The `requirements` dict in Python protocols can now have `"robotType": "Flex"` instead of `"robotType": "OT-3"`. `"OT-3"` will still work, but it's discouraged because it's not the customer-facing name.

# Internal Release 0.9.0
Expand Down
9 changes: 8 additions & 1 deletion api/src/opentrons/calibration_storage/ot3/models/v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,14 @@ class Config:
class ModuleOffsetModel(BaseModel):
offset: Point = Field(..., description="Module offset found from calibration.")
mount: OT3Mount = Field(..., description="The mount used to calibrate this module.")
slot: int = Field(..., description="The slot this module was calibrated in.")
slot: int = Field(
...,
description=(
"The slot this module was calibrated in."
" For historical reasons, this is specified as an OT-2-style integer like `5`,"
" not an OT-3-style string like `'C2'`."
),
)
module: ModuleType = Field(..., description="The module type of this module.")
module_id: str = Field(..., description="The unique id of this module.")
instrument_id: str = Field(
Expand Down
16 changes: 14 additions & 2 deletions api/src/opentrons/protocol_api/deck.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,47 +82,59 @@ def __len__(self) -> int:
"""Get the number of slots on the deck."""
return len(self._slot_definitions_by_name)

# todo(mm, 2023-05-08): This may be internal and removable from this public class. Jira RSS-236.
def right_of(self, slot: DeckLocation) -> Optional[DeckItem]:
"""Get the item directly to the right of the given slot, if any."""
slot_name = _get_slot_name(slot)
east_slot = adjacent_slots_getters.get_east_slot(slot_name.as_int())

return self[east_slot] if east_slot is not None else None

# todo(mm, 2023-05-08): This may be internal and removable from this public class. Jira RSS-236.
def left_of(self, slot: DeckLocation) -> Optional[DeckItem]:
"""Get the item directly to the left of the given slot, if any."""
slot_name = _get_slot_name(slot)
west_slot = adjacent_slots_getters.get_west_slot(slot_name.as_int())

return self[west_slot] if west_slot is not None else None

# todo(mm, 2023-05-08): This is undocumented in the public PAPI, but is used in some protocols
# written by Applications Engineering. Either officially document this, or decide it's internal
# and remove it from this class. Jira RSS-236.
def position_for(self, slot: DeckLocation) -> Location:
"""Get the absolute location of a deck slot's front-left corner."""
slot_definition = self.get_slot_definition(slot)
x, y, z = slot_definition["position"]

return Location(point=Point(x, y, z), labware=slot_definition["id"])

# todo(mm, 2023-05-08): This may be internal and removable from this public class. Jira RSS-236.
def get_slot_definition(self, slot: DeckLocation) -> SlotDefV3:
"""Get the geometric definition data of a slot."""
slot_name = _get_slot_name(slot)
return self._slot_definitions_by_name[slot_name.id]
slot_name = validation.ensure_deck_slot_string(
_get_slot_name(slot), self._protocol_core.robot_type
)
return self._slot_definitions_by_name[slot_name]

# todo(mm, 2023-05-08): This may be internal and removable from this public class. Jira RSS-236.
def get_slot_center(self, slot: DeckLocation) -> Point:
"""Get the absolute coordinates of a slot's center."""
slot_name = _get_slot_name(slot)
return self._protocol_core.get_slot_center(slot_name)

# todo(mm, 2023-05-08): This may be internal and removable from this public class. Jira RSS-236.
@property
def highest_z(self) -> float:
"""Get the height of the tallest known point on the deck."""
return self._protocol_core.get_highest_z()

# todo(mm, 2023-05-08): This appears internal. Remove it from this public class. Jira RSS-236.
@property
def slots(self) -> List[SlotDefV3]:
"""Get a list of all slot definitions."""
return list(self._slot_definitions_by_name.values())

# todo(mm, 2023-05-08): This appears internal. Remove it from this public class. Jira RSS-236.
@property
def calibration_positions(self) -> List[CalibrationPosition]:
"""Get a list of all calibration positions on the deck."""
Expand Down
5 changes: 1 addition & 4 deletions api/src/opentrons/protocol_api/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,7 @@ def ensure_deck_slot(deck_slot: Union[int, str]) -> DeckSlotName:


def ensure_deck_slot_string(slot_name: DeckSlotName, robot_type: RobotType) -> str:
if robot_type == "OT-2 Standard":
return str(slot_name)
else:
return slot_name.as_coordinate()
return slot_name.to_equivalent_for_robot_type(robot_type).id


def ensure_lowercase_name(name: str) -> str:
Expand Down
10 changes: 9 additions & 1 deletion api/src/opentrons/protocol_engine/protocol_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from opentrons.hardware_control.modules import AbstractModule as HardwareModuleAPI
from opentrons.hardware_control.types import PauseType as HardwarePauseType

from . import commands
from . import commands, slot_standardization
from .resources import ModelUtils, ModuleDataProvider
from .types import (
LabwareOffset,
Expand Down Expand Up @@ -148,6 +148,10 @@ def add_command(self, request: commands.CommandCreate) -> commands.Command:
RunStoppedError: the run has been stopped, so no new commands
may be added.
"""
request = slot_standardization.standardize_command(
request, self.state_view.config.robot_type
)

command_id = self._model_utils.generate_id()
request_hash = commands.hash_command_params(
create=request,
Expand Down Expand Up @@ -281,6 +285,10 @@ def add_labware_offset(self, request: LabwareOffsetCreate) -> LabwareOffset:
To retrieve offsets later, see `.state_view.labware`.
"""
request = slot_standardization.standardize_labware_offset(
request, self.state_view.config.robot_type
)

labware_offset_id = self._model_utils.generate_id()
created_at = self._model_utils.get_timestamp()
self._action_dispatcher.dispatch(
Expand Down
133 changes: 133 additions & 0 deletions api/src/opentrons/protocol_engine/slot_standardization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"""Convert deck slots into the preferred style for a robot.
The deck slots on an OT-2 are labeled like "1", "2", ..., and on an OT-3 they're labeled like
"D1," "D2", ....
When Protocol Engine takes a deck slot as input, we generally want it to accept either style
of label. This helps make protocols more portable across robot types.
But, Protocol Engine should then immediately convert it to the "correct" style for the robot that
it's controlling or simulating. This makes it simpler to consume the robot's HTTP API,
and it makes it easier for us to reason about Protocol Engine's internal state.
This module does that conversion, for any Protocol Engine input that contains a reference to a
deck slot.
"""


from typing import Any, Callable, Dict, Type

from opentrons_shared_data.robot.dev_types import RobotType

from . import commands
from .types import (
OFF_DECK_LOCATION,
DeckSlotLocation,
LabwareLocation,
LabwareOffsetCreate,
ModuleLocation,
)


def standardize_labware_offset(
original: LabwareOffsetCreate, robot_type: RobotType
) -> LabwareOffsetCreate:
"""Convert the deck slot in the given `LabwareOffsetCreate` to match the given robot type."""
return original.copy(
update={
"location": original.location.copy(
update={
"slotName": original.location.slotName.to_equivalent_for_robot_type(
robot_type
)
}
)
}
)


def standardize_command(
original: commands.CommandCreate, robot_type: RobotType
) -> commands.CommandCreate:
"""Convert any deck slots in the given `CommandCreate` to match the given robot type."""
try:
standardize = _standardize_command_functions[type(original)]
except KeyError:
return original
else:
return standardize(original, robot_type)


# Command-specific standardization:
#
# Our use of .copy(update=...) in these implementations, instead of .construct(...), is a tradeoff.
# .construct() would give us better type-checking for the fields that we set,
# but .copy(update=...) avoids the hazard of forgetting to set fields that have defaults.


def _standardize_load_labware(
original: commands.LoadLabwareCreate, robot_type: RobotType
) -> commands.LoadLabwareCreate:
params = original.params.copy(
update={
"location": _standardize_labware_location(
original.params.location, robot_type
)
}
)
return original.copy(update={"params": params})


def _standardize_load_module(
original: commands.LoadModuleCreate, robot_type: RobotType
) -> commands.LoadModuleCreate:
params = original.params.copy(
update={
"location": _standardize_deck_slot_location(
original.params.location, robot_type
)
}
)
return original.copy(update={"params": params})


def _standardize_move_labware(
original: commands.MoveLabwareCreate, robot_type: RobotType
) -> commands.MoveLabwareCreate:
params = original.params.copy(
update={
"newLocation": _standardize_labware_location(
original.params.newLocation, robot_type
)
}
)
return original.copy(update={"params": params})


_standardize_command_functions: Dict[
Type[commands.CommandCreate], Callable[[Any, RobotType], commands.CommandCreate]
] = {
commands.LoadLabwareCreate: _standardize_load_labware,
commands.LoadModuleCreate: _standardize_load_module,
commands.MoveLabwareCreate: _standardize_move_labware,
}


# Helpers:


def _standardize_labware_location(
original: LabwareLocation, robot_type: RobotType
) -> LabwareLocation:
if isinstance(original, DeckSlotLocation):
return _standardize_deck_slot_location(original, robot_type)
elif isinstance(original, ModuleLocation) or original == OFF_DECK_LOCATION:
return original


def _standardize_deck_slot_location(
original: DeckSlotLocation, robot_type: RobotType
) -> DeckSlotLocation:
return original.copy(
update={"slotName": original.slotName.to_equivalent_for_robot_type(robot_type)}
)
12 changes: 9 additions & 3 deletions api/src/opentrons/protocol_engine/state/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
CurrentWell,
TipGeometry,
)
from .config import Config
from .labware import LabwareView
from .modules import ModuleView
from .pipettes import PipetteView
Expand All @@ -34,11 +35,13 @@ class GeometryView:

def __init__(
self,
config: Config,
labware_view: LabwareView,
module_view: ModuleView,
pipette_view: PipetteView,
) -> None:
"""Initialize a GeometryView instance."""
self._config = config
self._labware = labware_view
self._modules = module_view
self._pipettes = pipette_view
Expand Down Expand Up @@ -374,10 +377,13 @@ def get_extra_waypoints(
from_slot=self.get_ancestor_slot_name(location.labware_id),
to_slot=self.get_ancestor_slot_name(labware_id),
):
slot_5_center = self._labware.get_slot_center_position(
slot=DeckSlotName.SLOT_5
middle_slot = DeckSlotName.SLOT_5.to_equivalent_for_robot_type(
self._config.robot_type
)
return [(slot_5_center.x, slot_5_center.y)]
middle_slot_center = self._labware.get_slot_center_position(
slot=middle_slot,
)
return [(middle_slot_center.x, middle_slot_center.y)]
return []

# TODO(mc, 2022-12-09): enforce data integrity (e.g. one module per slot)
Expand Down
38 changes: 24 additions & 14 deletions api/src/opentrons/protocol_engine/state/labware.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,20 @@
"opentrons/usascientific_96_wellplate_2.4ml_deep/1",
}

_INSTRUMENT_ATTACH_SLOT = DeckSlotName.SLOT_1
_OT3_INSTRUMENT_ATTACH_SLOT = DeckSlotName.SLOT_D1

_RIGHT_SIDE_SLOTS = {
# OT-2:
DeckSlotName.FIXED_TRASH,
DeckSlotName.SLOT_9,
DeckSlotName.SLOT_6,
DeckSlotName.SLOT_3,
# OT-3:
DeckSlotName.SLOT_A3,
DeckSlotName.SLOT_B3,
DeckSlotName.SLOT_C3,
DeckSlotName.SLOT_D3,
}


class LabwareLoadParams(NamedTuple):
Expand Down Expand Up @@ -269,11 +282,11 @@ def get_slot_definition(self, slot: DeckSlotName) -> SlotDefV3:
deck_def = self.get_deck_definition()

for slot_def in deck_def["locations"]["orderedSlots"]:
if slot_def["id"] == str(slot):
if slot_def["id"] == slot.id:
return slot_def

raise errors.SlotDoesNotExistError(
f"Slot ID {slot} does not exist in deck {deck_def['otId']}"
f"Slot ID {slot.id} does not exist in deck {deck_def['otId']}"
)

def get_slot_position(self, slot: DeckSlotName) -> Point:
Expand Down Expand Up @@ -408,12 +421,7 @@ def get_edge_path_type(

left_path_criteria = mount is MountType.RIGHT and well_name in left_column
right_path_criteria = mount is MountType.LEFT and well_name in right_column
labware_right_side = labware_slot in [
DeckSlotName.SLOT_3,
DeckSlotName.SLOT_6,
DeckSlotName.SLOT_9,
DeckSlotName.FIXED_TRASH,
]
labware_right_side = labware_slot in _RIGHT_SIDE_SLOTS

if left_path_criteria and (next_to_module or labware_right_side):
return EdgePathType.LEFT
Expand Down Expand Up @@ -580,10 +588,12 @@ def get_fixed_trash_id(self) -> str:
that is currently in use for the protocol run.
"""
for labware in self._state.labware_by_id.values():
if (
isinstance(labware.location, DeckSlotLocation)
and labware.location.slotName == DeckSlotName.FIXED_TRASH
):
if isinstance(
labware.location, DeckSlotLocation
) and labware.location.slotName in {
DeckSlotName.FIXED_TRASH,
DeckSlotName.SLOT_A3,
}:
return labware.id

raise errors.LabwareNotLoadedError(
Expand All @@ -606,7 +616,7 @@ def raise_if_labware_in_location(

def get_calibration_coordinates(self, offset: Point) -> Point:
"""Get calibration critical point and target position."""
target_center = self.get_slot_center_position(_INSTRUMENT_ATTACH_SLOT)
target_center = self.get_slot_center_position(_OT3_INSTRUMENT_ATTACH_SLOT)
# TODO (tz, 11-30-22): These coordinates wont work for OT-2. We will need to apply offsets after
# https://opentrons.atlassian.net/browse/RCORE-382

Expand Down
Loading

0 comments on commit 3404c50

Please sign in to comment.