Skip to content

Commit

Permalink
feat(app, api, shared-data, robot-server): Add module fixtures to dec…
Browse files Browse the repository at this point in the history
…k configuration (#14684)

Build out the backend and frontend to support loading modules into the
deck configuration, including tracking modules by serial number in the
persistent deck configuration directory.
Closes RESC-209, PLAT-247, PLAT-248, PLAT-249, PLAT-250, PLAT-251, PLAT-252, PLAT-254

---------

Co-authored-by: Brian Cooper <[email protected]>
Co-authored-by: ahiuchingau <[email protected]>
  • Loading branch information
3 people authored Apr 15, 2024
1 parent bf69ad8 commit 4e895d4
Show file tree
Hide file tree
Showing 118 changed files with 6,032 additions and 1,361 deletions.
9 changes: 7 additions & 2 deletions api/src/opentrons/calibration_storage/deck_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
class _CutoutFixturePlacementModel(pydantic.BaseModel):
cutoutId: str
cutoutFixtureId: str
opentronsModuleSerialNumber: Optional[str]


class _DeckConfigurationModel(pydantic.BaseModel):
Expand All @@ -26,7 +27,9 @@ def serialize_deck_configuration(
data = _DeckConfigurationModel.construct(
cutoutFixtures=[
_CutoutFixturePlacementModel.construct(
cutoutId=e.cutout_id, cutoutFixtureId=e.cutout_fixture_id
cutoutId=e.cutout_id,
cutoutFixtureId=e.cutout_fixture_id,
opentronsModuleSerialNumber=e.opentrons_module_serial_number,
)
for e in cutout_fixture_placements
],
Expand All @@ -50,7 +53,9 @@ def deserialize_deck_configuration(
else:
cutout_fixture_placements = [
CutoutFixturePlacement(
cutout_id=e.cutoutId, cutout_fixture_id=e.cutoutFixtureId
cutout_id=e.cutoutId,
cutout_fixture_id=e.cutoutFixtureId,
opentrons_module_serial_number=e.opentronsModuleSerialNumber,
)
for e in parsed.cutoutFixtures
]
Expand Down
1 change: 1 addition & 0 deletions api/src/opentrons/calibration_storage/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ class UriDetails:
class CutoutFixturePlacement:
cutout_fixture_id: str
cutout_id: str
opentrons_module_serial_number: typing.Optional[str]
16 changes: 16 additions & 0 deletions api/src/opentrons/hardware_control/modules/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,22 @@ def from_model(cls, model: ModuleModel) -> ModuleType:
if isinstance(model, MagneticBlockModel):
return cls.MAGNETIC_BLOCK

@classmethod
def to_module_fixture_id(cls, module_type: ModuleType) -> str:
if module_type == ModuleType.THERMOCYCLER:
# Thermocyclers are "loaded" in B1 only
return "thermocyclerModuleV2Front"
if module_type == ModuleType.TEMPERATURE:
return "temperatureModuleV2"
if module_type == ModuleType.HEATER_SHAKER:
return "heaterShakerModuleV1"
if module_type == ModuleType.MAGNETIC_BLOCK:
return "magneticBlockV1"
else:
raise ValueError(
f"Module Type {module_type} does not have a related fixture ID."
)


class MagneticModuleModel(str, Enum):
MAGNETIC_V1: str = "magneticModuleV1"
Expand Down
29 changes: 23 additions & 6 deletions api/src/opentrons/protocol_api/core/engine/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from typing import Dict, Optional, Type, Union, List, Tuple, TYPE_CHECKING

from opentrons.protocol_engine.commands import LoadModuleResult
from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, SlotDefV3
from opentrons_shared_data.deck.dev_types import DeckDefinitionV5, SlotDefV3
from opentrons.protocol_engine.resources import deck_configuration_provider
from opentrons_shared_data.labware.labware_definition import LabwareDefinition
from opentrons_shared_data.labware.dev_types import LabwareDefinition as LabwareDefDict
from opentrons_shared_data.pipette.dev_types import PipetteNameType
Expand Down Expand Up @@ -602,7 +603,7 @@ def set_last_location(
self._last_location = location
self._last_mount = mount

def get_deck_definition(self) -> DeckDefinitionV4:
def get_deck_definition(self) -> DeckDefinitionV5:
"""Get the geometry definition of the robot's deck."""
return self._engine_client.state.labware.get_deck_definition()

Expand All @@ -625,10 +626,26 @@ def get_staging_slot_definitions(self) -> Dict[str, SlotDefV3]:
def _ensure_module_location(
self, slot: DeckSlotName, module_type: ModuleType
) -> None:
slot_def = self.get_slot_definition(slot)
compatible_modules = slot_def["compatibleModuleTypes"]
if module_type.value not in compatible_modules:
raise ValueError(f"A {module_type.value} cannot be loaded into slot {slot}")
if self._engine_client.state.config.robot_type == "OT-2 Standard":
slot_def = self.get_slot_definition(slot)
compatible_modules = slot_def["compatibleModuleTypes"]
if module_type.value not in compatible_modules:
raise ValueError(
f"A {module_type.value} cannot be loaded into slot {slot}"
)
else:
cutout_fixture_id = ModuleType.to_module_fixture_id(module_type)
module_fixture = deck_configuration_provider.get_cutout_fixture(
cutout_fixture_id,
self._engine_client.state.addressable_areas.state.deck_definition,
)
cutout_id = self._engine_client.state.addressable_areas.get_cutout_id_by_deck_slot_name(
slot
)
if cutout_id not in module_fixture["mayMountTo"]:
raise ValueError(
f"A {module_type.value} cannot be loaded into slot {slot}"
)

def get_slot_item(
self, slot_name: Union[DeckSlotName, StagingSlotName]
Expand Down
5 changes: 5 additions & 0 deletions api/src/opentrons/protocol_api/core/legacy/deck.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,11 @@ def resolve_module_location(
compatible_modules = slot_def["compatibleModuleTypes"]
if module_type.value in compatible_modules:
return location
elif (
self._definition["robot"]["model"] == "OT-3 Standard"
and ModuleType.to_module_fixture_id(module_type) == slot_def["id"]
):
return location
else:
raise ValueError(
f"A {dn_from_type[module_type]} cannot be loaded"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging
from typing import Dict, List, Optional, Set, Union, cast, Tuple

from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, SlotDefV3
from opentrons_shared_data.deck.dev_types import DeckDefinitionV5, SlotDefV3
from opentrons_shared_data.labware.dev_types import LabwareDefinition
from opentrons_shared_data.pipette.dev_types import PipetteNameType
from opentrons_shared_data.robot.dev_types import RobotType
Expand Down Expand Up @@ -491,7 +491,7 @@ def get_labware_on_labware(
) -> Optional[LegacyLabwareCore]:
assert False, "get_labware_on_labware only supported on engine core"

def get_deck_definition(self) -> DeckDefinitionV4:
def get_deck_definition(self) -> DeckDefinitionV5:
"""Get the geometry definition of the robot's deck."""
assert False, "get_deck_definition only supported on engine core"

Expand Down
4 changes: 2 additions & 2 deletions api/src/opentrons/protocol_api/core/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from abc import abstractmethod, ABC
from typing import Generic, List, Optional, Union, Tuple, Dict, TYPE_CHECKING

from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, SlotDefV3
from opentrons_shared_data.deck.dev_types import DeckDefinitionV5, SlotDefV3
from opentrons_shared_data.pipette.dev_types import PipetteNameType
from opentrons_shared_data.labware.dev_types import LabwareDefinition
from opentrons_shared_data.robot.dev_types import RobotType
Expand Down Expand Up @@ -188,7 +188,7 @@ def set_last_location(
...

@abstractmethod
def get_deck_definition(self) -> DeckDefinitionV4:
def get_deck_definition(self) -> DeckDefinitionV5:
"""Get the geometry definition of the robot's deck."""

@abstractmethod
Expand Down
4 changes: 4 additions & 0 deletions api/src/opentrons/protocol_api/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ class InvalidTrashBinLocationError(ValueError):
"""An error raised when attempting to load trash bins in invalid slots."""


class InvalidFixtureLocationError(ValueError):
"""An error raised when attempting to load a fixture in an invalid cutout."""


def ensure_mount_for_pipette(
mount: Union[str, Mount, None], pipette: PipetteNameType
) -> Mount:
Expand Down
22 changes: 18 additions & 4 deletions api/src/opentrons/protocol_engine/commands/load_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
from pydantic import BaseModel, Field

from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate
from ..types import DeckSlotLocation, ModuleModel, ModuleDefinition
from ..types import (
DeckSlotLocation,
ModuleModel,
ModuleDefinition,
)

if TYPE_CHECKING:
from ..state import StateView
Expand Down Expand Up @@ -104,9 +108,19 @@ def __init__(

async def execute(self, params: LoadModuleParams) -> LoadModuleResult:
"""Check that the requested module is attached and assign its identifier."""
self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration(
params.location.slotName.id
)
if self._state_view.config.robot_type == "OT-2 Standard":
self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration(
params.location.slotName.id
)
else:
addressable_area = self._state_view.geometry._modules.ensure_and_convert_module_fixture_location(
deck_slot=params.location.slotName,
deck_type=self._state_view.config.deck_type,
model=params.model,
)
self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration(
addressable_area
)

verified_location = self._state_view.geometry.ensure_location_not_occupied(
params.location
Expand Down
9 changes: 7 additions & 2 deletions api/src/opentrons/protocol_engine/execution/equipment.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Equipment command side-effect logic."""
from dataclasses import dataclass
from typing import Optional, overload
from typing import Optional, overload, Union

from opentrons_shared_data.pipette.dev_types import PipetteNameType

Expand Down Expand Up @@ -44,6 +44,7 @@
LabwareOffsetLocation,
ModuleModel,
ModuleDefinition,
AddressableAreaLocation,
)


Expand Down Expand Up @@ -252,7 +253,7 @@ async def load_pipette(
async def load_magnetic_block(
self,
model: ModuleModel,
location: DeckSlotLocation,
location: Union[DeckSlotLocation, AddressableAreaLocation],
module_id: Optional[str],
) -> LoadedModuleData:
"""Ensure the required magnetic block is attached.
Expand Down Expand Up @@ -317,10 +318,14 @@ async def load_module(
for hw_mod in self._hardware_api.attached_modules
]

serial_number_at_locaiton = self._state_store.geometry._addressable_areas.get_fixture_serial_from_deck_configuration_by_deck_slot(
location.slotName
)
attached_module = self._state_store.modules.select_hardware_module_to_load(
model=model,
location=location,
attached_modules=attached_modules,
expected_serial_number=serial_number_at_locaiton,
)

else:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Deck configuration resource provider."""
from typing import List, Set, Tuple

from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, CutoutFixture
from opentrons_shared_data.deck.dev_types import DeckDefinitionV5, CutoutFixture

from opentrons.types import DeckSlotName

Expand All @@ -17,11 +17,10 @@
CutoutDoesNotExistError,
FixtureDoesNotExistError,
AddressableAreaDoesNotExistError,
FixtureDoesNotProvideAreasError,
)


def get_cutout_position(cutout_id: str, deck_definition: DeckDefinitionV4) -> DeckPoint:
def get_cutout_position(cutout_id: str, deck_definition: DeckDefinitionV5) -> DeckPoint:
"""Get the base position of a cutout on the deck."""
for cutout in deck_definition["locations"]["cutouts"]:
if cutout_id == cutout["id"]:
Expand All @@ -32,7 +31,7 @@ def get_cutout_position(cutout_id: str, deck_definition: DeckDefinitionV4) -> De


def get_cutout_fixture(
cutout_fixture_id: str, deck_definition: DeckDefinitionV4
cutout_fixture_id: str, deck_definition: DeckDefinitionV5
) -> CutoutFixture:
"""Gets cutout fixture from deck that matches the cutout fixture ID provided."""
for cutout_fixture in deck_definition["cutoutFixtures"]:
Expand All @@ -44,20 +43,18 @@ def get_cutout_fixture(


def get_provided_addressable_area_names(
cutout_fixture_id: str, cutout_id: str, deck_definition: DeckDefinitionV4
cutout_fixture_id: str, cutout_id: str, deck_definition: DeckDefinitionV5
) -> List[str]:
"""Gets a list of the addressable areas provided by the cutout fixture on the cutout."""
cutout_fixture = get_cutout_fixture(cutout_fixture_id, deck_definition)
try:
return cutout_fixture["providesAddressableAreas"][cutout_id]
except KeyError as exception:
raise FixtureDoesNotProvideAreasError(
f"Cutout fixture {cutout_fixture['id']} does not provide addressable areas for {cutout_id}"
) from exception
except KeyError:
return []


def get_addressable_area_display_name(
addressable_area_name: str, deck_definition: DeckDefinitionV4
addressable_area_name: str, deck_definition: DeckDefinitionV5
) -> str:
"""Get the display name for an addressable area name."""
for addressable_area in deck_definition["locations"]["addressableAreas"]:
Expand All @@ -69,7 +66,7 @@ def get_addressable_area_display_name(


def get_potential_cutout_fixtures(
addressable_area_name: str, deck_definition: DeckDefinitionV4
addressable_area_name: str, deck_definition: DeckDefinitionV5
) -> Tuple[str, Set[PotentialCutoutFixture]]:
"""Given an addressable area name, gets the cutout ID associated with it and a set of potential fixtures."""
potential_fixtures = []
Expand Down Expand Up @@ -102,7 +99,7 @@ def get_addressable_area_from_name(
addressable_area_name: str,
cutout_position: DeckPoint,
base_slot: DeckSlotName,
deck_definition: DeckDefinitionV4,
deck_definition: DeckDefinitionV5,
) -> AddressableArea:
"""Given a name and a cutout position, get an addressable area on the deck."""
for addressable_area in deck_definition["locations"]["addressableAreas"]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
load as load_deck,
DEFAULT_DECK_DEFINITION_VERSION,
)
from opentrons_shared_data.deck.dev_types import DeckDefinitionV4
from opentrons_shared_data.deck.dev_types import DeckDefinitionV5
from opentrons.protocols.models import LabwareDefinition
from opentrons.types import DeckSlotName

Expand Down Expand Up @@ -39,10 +39,10 @@ def __init__(
self._deck_type = deck_type
self._labware_data = labware_data or LabwareDataProvider()

async def get_deck_definition(self) -> DeckDefinitionV4:
async def get_deck_definition(self) -> DeckDefinitionV5:
"""Get a labware definition given the labware's identification."""

def sync() -> DeckDefinitionV4:
def sync() -> DeckDefinitionV5:
return load_deck(
name=self._deck_type.value, version=DEFAULT_DECK_DEFINITION_VERSION
)
Expand All @@ -51,7 +51,7 @@ def sync() -> DeckDefinitionV4:

async def get_deck_fixed_labware(
self,
deck_definition: DeckDefinitionV4,
deck_definition: DeckDefinitionV5,
) -> List[DeckFixedLabware]:
"""Get a list of all labware fixtures from a given deck definition."""
labware: List[DeckFixedLabware] = []
Expand Down
Loading

0 comments on commit 4e895d4

Please sign in to comment.