Skip to content

Commit

Permalink
account for AddressableAreaLocation as valid load module location
Browse files Browse the repository at this point in the history
  • Loading branch information
CaseyBatten committed Mar 18, 2024
1 parent 7f110f6 commit 110c745
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 34 deletions.
30 changes: 26 additions & 4 deletions api/src/opentrons/protocol_api/core/engine/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,10 +412,32 @@ def load_module(
normalized_deck_slot = deck_slot.to_equivalent_for_robot_type(robot_type)
self._ensure_module_location(normalized_deck_slot, module_type)

result = self._engine_client.load_module(
model=EngineModuleModel(model),
location=DeckSlotLocation(slotName=normalized_deck_slot),
)
module_loaded = False
if robot_type == "OT-3 Standard":
if isinstance(deck_slot, DeckSlotName):
addressable_areas = deck_configuration_provider.get_provided_addressable_area_names(
cutout_fixture_id=ModuleType.to_module_fixture_id(module_type),
cutout_id=self._engine_client.state.addressable_areas.get_cutout_id_by_deck_slot_name(
normalized_deck_slot
),
deck_definition=self._engine_client.state.addressable_areas.state.deck_definition,
)
if len(addressable_areas) > 0:
# TODO (cb, 2023-03-18): Currently modules only supply one addressable area at most, so this is safe but not if we have multi AAs
# An example: For the waste chute we hardcode which addressable area is selected based on number of pipette channels
# If we make one module with many AAs, we will need a way to pick which one to use
result = self._engine_client.load_module(
model=EngineModuleModel(model),
location=AddressableAreaLocation(
addressableAreaName=addressable_areas[0]
),
)
module_loaded = True
if not module_loaded:
result = self._engine_client.load_module(
model=EngineModuleModel(model),
location=DeckSlotLocation(slotName=normalized_deck_slot),
)

module_core = self._get_module_core(load_module_result=result, model=model)

Expand Down
5 changes: 3 additions & 2 deletions api/src/opentrons/protocol_engine/clients/sync_client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Control a `ProtocolEngine` without async/await."""

from typing import cast, List, Optional, Dict
from typing import cast, List, Optional, Dict, Union

from opentrons_shared_data.pipette.dev_types import PipetteNameType
from opentrons_shared_data.labware.dev_types import LabwareUri
Expand All @@ -25,6 +25,7 @@
Liquid,
NozzleLayoutConfigurationType,
AddressableOffsetVector,
AddressableAreaLocation,
)
from .transports import ChildThreadTransport

Expand Down Expand Up @@ -264,7 +265,7 @@ def move_to_coordinates(
def load_module(
self,
model: ModuleModel,
location: DeckSlotLocation,
location: Union[DeckSlotLocation, AddressableAreaLocation],
) -> commands.LoadModuleResult:
"""Execute a LoadModule command and return the result."""
request = commands.LoadModuleCreate(
Expand Down
22 changes: 16 additions & 6 deletions api/src/opentrons/protocol_engine/commands/load_module.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
"""Implementation, request models, and response models for the load module command."""
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Type
from typing import TYPE_CHECKING, Optional, Type, Union
from typing_extensions import Literal
from pydantic import BaseModel, Field

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

if TYPE_CHECKING:
from ..state import StateView
Expand Down Expand Up @@ -37,7 +42,7 @@ class LoadModuleParams(BaseModel):
# single deck slot precludes loading a Thermocycler in its special "shifted slightly
# to the left" position. This is okay for now because neither the Python Protocol
# API nor Protocol Designer attempt to support it, either.
location: DeckSlotLocation = Field(
location: Union[DeckSlotLocation, AddressableAreaLocation] = Field(
...,
description=(
"The location into which this module should be loaded."
Expand Down Expand Up @@ -104,9 +109,14 @@ 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 isinstance(params.location, DeckSlotLocation):
self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration(
params.location.slotName.id
)
elif isinstance(params.location, AddressableAreaLocation):
self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration(
params.location.addressableAreaName
)

verified_location = self._state_view.geometry.ensure_location_not_occupied(
params.location
Expand Down
11 changes: 5 additions & 6 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 @@ -283,7 +284,7 @@ async def load_magnetic_block(
async def load_module(
self,
model: ModuleModel,
location: DeckSlotLocation,
location: Union[DeckSlotLocation, AddressableAreaLocation],
module_id: Optional[str],
) -> LoadedModuleData:
"""Ensure the required module is attached.
Expand Down Expand Up @@ -318,9 +319,7 @@ async def load_module(
]

attached_module = self._state_store.modules.select_hardware_module_to_load(
model=model,
location=location,
attached_modules=attached_modules,
model=model, location=location, attached_modules=attached_modules
)

else:
Expand Down
18 changes: 11 additions & 7 deletions api/src/opentrons/protocol_engine/slot_standardization.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,17 @@ def _standardize_load_labware(
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
)
}
)
if isinstance(original.params.location, DeckSlotLocation):
params = original.params.copy(
update={
"location": _standardize_deck_slot_location(
original.params.location, robot_type
)
}
)
else:
params = original.params

return original.copy(update={"params": params})


Expand Down
49 changes: 40 additions & 9 deletions api/src/opentrons/protocol_engine/state/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@
HeaterShakerMovementRestrictors,
DeckType,
LabwareMovementOffsetData,
AddressableAreaLocation,
)
from .addressable_areas import AddressableAreaView
from .. import errors
from ..commands import (
Command,
Expand Down Expand Up @@ -170,6 +172,9 @@ class ModuleState:
deck_type: DeckType
"""Type of deck that the modules are on."""

addressable_area_view: AddressableAreaView
"""Read-only view of the deck's addressable area state."""


class ModuleStore(HasState[ModuleState], HandlesActions):
"""Module state container."""
Expand All @@ -179,6 +184,7 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
def __init__(
self,
config: Config,
addressable_area_view: AddressableAreaView,
module_calibration_offsets: Optional[Dict[str, ModuleOffsetData]] = None,
) -> None:
"""Initialize a ModuleStore and its state."""
Expand All @@ -190,6 +196,7 @@ def __init__(
substate_by_module_id={},
module_offset_by_serial=module_calibration_offsets or {},
deck_type=config.deck_type,
addressable_area_view=addressable_area_view,
)
self._robot_type = config.robot_type

Expand All @@ -210,11 +217,19 @@ def handle_action(self, action: Action) -> None:

def _handle_command(self, command: Command) -> None:
if isinstance(command.result, LoadModuleResult):
if isinstance(command.params.location, AddressableAreaLocation):
slot_name = (
self._state.addressable_area_view.get_addressable_area_base_slot(
command.params.location.addressableAreaName
)
)
else:
slot_name = command.params.location.slotName
self._add_module_substate(
module_id=command.result.moduleId,
serial_number=command.result.serialNumber,
definition=command.result.definition,
slot_name=command.params.location.slotName,
slot_name=slot_name,
requested_model=command.params.model,
module_live_data=None,
)
Expand Down Expand Up @@ -946,7 +961,7 @@ def is_edge_move_unsafe(self, mount: MountType, target_slot: DeckSlotName) -> bo
def select_hardware_module_to_load(
self,
model: ModuleModel,
location: DeckSlotLocation,
location: Union[DeckSlotLocation, AddressableAreaLocation],
attached_modules: Sequence[HardwareModule],
) -> HardwareModule:
"""Get the next matching hardware module for the given model and location.
Expand All @@ -973,9 +988,19 @@ def select_hardware_module_to_load(
existing_mod_in_slot = None

for mod_id, slot in self._state.slot_by_module_id.items():
if slot == location.slotName:
existing_mod_in_slot = self._state.hardware_by_module_id.get(mod_id)
break
if isinstance(location, DeckSlotLocation):
if slot == location.slotName:
existing_mod_in_slot = self._state.hardware_by_module_id.get(mod_id)
break
elif isinstance(location, AddressableAreaLocation):
if (
slot
== self._state.addressable_area_view.get_addressable_area_base_slot(
location.addressableAreaName
)
):
existing_mod_in_slot = self._state.hardware_by_module_id.get(mod_id)
break

if existing_mod_in_slot:
existing_def = existing_mod_in_slot.definition
Expand All @@ -984,10 +1009,16 @@ def select_hardware_module_to_load(
return existing_mod_in_slot

else:
raise errors.ModuleAlreadyPresentError(
f"A {existing_def.model.value} is already"
f" present in {location.slotName.value}"
)
if isinstance(location, AddressableAreaLocation):
raise errors.ModuleAlreadyPresentError(
f"A {existing_def.model.value} is already"
f" present in {location.addressableAreaName}"
)
else:
raise errors.ModuleAlreadyPresentError(
f"A {existing_def.model.value} is already"
f" present in {location.slotName.value}"
)

for m in attached_modules:
if m not in self._state.hardware_by_module_id.values():
Expand Down
3 changes: 3 additions & 0 deletions api/src/opentrons/protocol_engine/state/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ def __init__(
)
self._module_store = ModuleStore(
config=config,
addressable_area_view=AddressableAreaView(
self._addressable_area_store.state
),
module_calibration_offsets=module_calibration_offsets,
)
self._liquid_store = LiquidStore()
Expand Down
1 change: 1 addition & 0 deletions api/src/opentrons/protocol_engine/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,7 @@ class AreaType(Enum):
THERMOCYCLER = "thermocycler"
HEATER_SHAKER = "heaterShaker"
TEMPERATURE = "temperatureModule"
MAGNETICBLOCK = "magneticBlock"


@dataclass(frozen=True)
Expand Down

0 comments on commit 110c745

Please sign in to comment.