Skip to content

Commit

Permalink
Merge branch 'EXEC-1078' into flex-stacker-frontend-basic
Browse files Browse the repository at this point in the history
  • Loading branch information
vegano1 committed Jan 17, 2025
2 parents f72b302 + 1c846f9 commit 057a705
Show file tree
Hide file tree
Showing 12 changed files with 204 additions and 56 deletions.
1 change: 0 additions & 1 deletion api/src/opentrons/protocol_api/core/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,4 +397,3 @@ def retrieve(self) -> None:
@abstractmethod
def store(self) -> None:
"""Store a labware at the bottom of the labware stack."""
pass
5 changes: 4 additions & 1 deletion api/src/opentrons/protocol_api/module_contexts.py
Original file line number Diff line number Diff line change
Expand Up @@ -1151,14 +1151,17 @@ def serial_number(self) -> str:
return self._core.get_serial_number()

@requires_version(2, 23)
def retrieve(self) -> None:
def retrieve(self) -> Labware:
"""Release and return a labware at the bottom of the labware stack."""
self._core.retrieve()
assert self.labware is not None
return self.labware

@requires_version(2, 23)
def store(self, labware: Labware) -> None:
"""Store a labware at the bottom of the labware stack.
:param labware: The labware object to store.
"""
assert labware._core is not None
self._core.store()
58 changes: 48 additions & 10 deletions api/src/opentrons/protocol_engine/commands/flex_stacker/retrieve.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@
from pydantic import BaseModel, Field

from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
from ...errors.error_occurrence import ErrorOccurrence
from ...errors import (
ErrorOccurrence,
CannotPerformModuleAction,
)
from ...state import update_types
from ...types import ModuleLocation

if TYPE_CHECKING:
from opentrons.protocol_engine.state.state import StateView
Expand All @@ -28,6 +32,11 @@ class RetrieveParams(BaseModel):
class RetrieveResult(BaseModel):
"""Result data from a labware retrieval command."""

labware_id: str = Field(
...,
description="The labware ID of the retrieved labware.",
)


class RetrieveImpl(AbstractCommandImpl[RetrieveParams, SuccessData[RetrieveResult]]):
"""Implementation of a labware retrieval command."""
Expand All @@ -43,19 +52,48 @@ def __init__(

async def execute(self, params: RetrieveParams) -> SuccessData[RetrieveResult]:
"""Execute the labware retrieval command."""
state_update = update_types.StateUpdate()
stacker_substate = self._state_view.modules.get_flex_stacker_substate(
module_id=params.moduleId
stacker_state = self._state_view.modules.get_flex_stacker_substate(
params.moduleId
)

stacker_loc = ModuleLocation(moduleId=params.moduleId)
# Allow propagation of ModuleNotAttachedError.
stacker = self._equipment.get_module_hardware_api(stacker_substate.module_id)
stacker_hw = self._equipment.get_module_hardware_api(stacker_state.module_id)

if not stacker_state.hopper_labware_ids:
raise CannotPerformModuleAction(
f"Flex Stacker {params.moduleId} has no labware to retrieve"
)

if stacker is not None:
# TODO: get labware height from labware state view
await stacker.dispense_labware(labware_height=50.0)
try:
self._state_view.labware.raise_if_labware_in_location(stacker_loc)
except Exception as e:
raise CannotPerformModuleAction(
f"Cannot retrieve a labware from Flex Stacker if the carriage is occupied: {e}"
)

return SuccessData(public=RetrieveResult(), state_update=state_update)
state_update = update_types.StateUpdate()

# Get the labware dimensions for the labware being retrieved,
# which is the first one in the hopper labware id list
lw_id = stacker_state.hopper_labware_ids[0]
lw_dim = self._state_view.labware.get_dimensions(labware_id=lw_id)

if stacker_hw is not None:
# Dispense the labware from the Flex Stacker using the labware height
await stacker_hw.dispense_labware(labware_height=lw_dim.z)

# update the state to reflect the labware is now in the flex stacker slot
state_update.set_labware_location(
labware_id=lw_id,
new_location=ModuleLocation(moduleId=params.moduleId),
new_offset_id=None,
)
state_update.retrieve_flex_stacker_labware(
module_id=params.moduleId, labware_id=lw_id
)
return SuccessData(
public=RetrieveResult(labware_id=lw_id), state_update=state_update
)


class Retrieve(BaseCommand[RetrieveParams, RetrieveResult, ErrorOccurrence]):
Expand Down
38 changes: 29 additions & 9 deletions api/src/opentrons/protocol_engine/commands/flex_stacker/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
from pydantic import BaseModel, Field

from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
from ...errors.error_occurrence import ErrorOccurrence
from ...errors import ErrorOccurrence, CannotPerformModuleAction
from ...state import update_types
from ...types import OFF_DECK_LOCATION


if TYPE_CHECKING:
from opentrons.protocol_engine.state.state import StateView
Expand Down Expand Up @@ -44,17 +46,35 @@ def __init__(

async def execute(self, params: StoreParams) -> SuccessData[StoreResult]:
"""Execute the labware storage command."""
state_update = update_types.StateUpdate()
stacker_substate = self._state_view.modules.get_flex_stacker_substate(
module_id=params.moduleId
stacker_state = self._state_view.modules.get_flex_stacker_substate(
params.moduleId
)

# Allow propagation of ModuleNotAttachedError.
stacker = self._equipment.get_module_hardware_api(stacker_substate.module_id)
stacker_hw = self._equipment.get_module_hardware_api(stacker_state.module_id)

if stacker is not None:
# TODO: get labware height from labware state view
await stacker.store_labware(labware_height=50.0)
try:
lw_id = self._state_view.labware.get_id_by_module(params.moduleId)
except Exception:
raise CannotPerformModuleAction(
"Cannot store labware if Flex Stacker carriage is empty"
)

lw_dim = self._state_view.labware.get_dimensions(labware_id=lw_id)
# TODO: check the type of the labware should match that already in the stack
state_update = update_types.StateUpdate()

if stacker_hw is not None:
await stacker_hw.store_labware(labware_height=lw_dim.z)

# update the state to reflect the labware is store in the stack
state_update.set_labware_location(
labware_id=lw_id,
new_location=OFF_DECK_LOCATION,
new_offset_id=None,
)
state_update.store_flex_stacker_labware(
module_id=params.moduleId, labware_id=lw_id
)

return SuccessData(public=StoreResult(), state_update=state_update)

Expand Down
2 changes: 1 addition & 1 deletion api/src/opentrons/protocol_engine/commands/load_labware.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ async def execute( # noqa: C901
if self._is_loading_to_module(
params.location, ModuleModel.FLEX_STACKER_MODULE_V1
):
state_update.add_flex_stacker_hopper_labware(
state_update.load_flex_stacker_hopper_labware(
module_id=params.location.moduleId,
labware_id=loaded_labware.labware_id,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
from typing import NewType, List
from opentrons.protocol_engine.state.update_types import (
FlexStackerStateUpdate,
FlexStackerAddHopperLabware,
FlexStackerRemoveHopperLabware,
FlexStackerLoadHopperLabware,
FlexStackerRetrieveLabware,
FlexStackerStoreLabware,
)


Expand All @@ -28,10 +29,17 @@ def new_from_state_change(
"""Return a new state with the given update applied."""
lw_change = update.hopper_labware_update
new_labware_ids = self.hopper_labware_ids.copy()
if isinstance(lw_change, FlexStackerAddHopperLabware):

# TODO the labware stack needs to be handled more elegantly
# this is a temporary solution to enable evt testing
if isinstance(lw_change, FlexStackerLoadHopperLabware):
# for manually loading labware in the stacker
new_labware_ids.append(lw_change.labware_id)
elif isinstance(lw_change, FlexStackerRemoveHopperLabware):
elif isinstance(lw_change, FlexStackerRetrieveLabware):
new_labware_ids.remove(lw_change.labware_id)
elif isinstance(lw_change, FlexStackerStoreLabware):
# automatically store labware at the bottom of the stack
new_labware_ids.insert(0, lw_change.labware_id)

return FlexStackerSubState(
module_id=self.module_id,
Expand Down
41 changes: 36 additions & 5 deletions api/src/opentrons/protocol_engine/state/update_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,14 +317,21 @@ class AbsorbanceReaderStateUpdate:


@dataclasses.dataclass
class FlexStackerAddHopperLabware:
class FlexStackerLoadHopperLabware:
"""An update to the Flex Stacker module static state."""

labware_id: str


@dataclasses.dataclass
class FlexStackerRemoveHopperLabware:
class FlexStackerRetrieveLabware:
"""An update to the Flex Stacker module static state."""

labware_id: str


@dataclasses.dataclass
class FlexStackerStoreLabware:
"""An update to the Flex Stacker module static state."""

labware_id: str
Expand All @@ -335,7 +342,7 @@ class FlexStackerStateUpdate:
"""An update to the Flex Stacker module state."""

module_id: str
hopper_labware_update: FlexStackerAddHopperLabware | FlexStackerRemoveHopperLabware | NoChangeType = (
hopper_labware_update: FlexStackerLoadHopperLabware | FlexStackerRetrieveLabware | FlexStackerStoreLabware | NoChangeType = (
NO_CHANGE
)

Expand Down Expand Up @@ -744,14 +751,38 @@ def set_addressable_area_used(self: Self, addressable_area_name: str) -> Self:
)
return self

def add_flex_stacker_hopper_labware(
def load_flex_stacker_hopper_labware(
self,
module_id: str,
labware_id: str,
) -> Self:
"""Add a labware definition to the engine."""
self.flex_stacker_state_update = FlexStackerStateUpdate(
module_id=module_id,
hopper_labware_update=FlexStackerLoadHopperLabware(labware_id=labware_id),
)
return self

def retrieve_flex_stacker_labware(
self,
module_id: str,
labware_id: str,
) -> Self:
"""Add a labware definition to the engine."""
self.flex_stacker_state_update = FlexStackerStateUpdate(
module_id=module_id,
hopper_labware_update=FlexStackerRetrieveLabware(labware_id=labware_id),
)
return self

def store_flex_stacker_labware(
self,
module_id: str,
labware_id: str,
) -> Self:
"""Add a labware definition to the engine."""
self.flex_stacker_state_update = FlexStackerStateUpdate(
module_id=module_id,
hopper_labware_update=FlexStackerAddHopperLabware(labware_id=labware_id),
hopper_labware_update=FlexStackerStoreLabware(labware_id=labware_id),
)
return self
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from opentrons.hardware_control.modules.types import (
ModuleType,
)
from opentrons.protocol_engine import commands as cmd
from opentrons.protocol_engine.clients import SyncClient as EngineClient
from opentrons.protocol_api.core.engine.module_core import FlexStackerCore
from opentrons.protocol_api import MAX_SUPPORTED_VERSION
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
from opentrons.hardware_control.modules import FlexStacker

from opentrons.protocol_engine.state.state import StateView
from opentrons.protocol_engine.state.update_types import (
StateUpdate,
FlexStackerStateUpdate,
FlexStackerRetrieveLabware,
LabwareLocationUpdate,
)
from opentrons.protocol_engine.state.module_substates import (
FlexStackerSubState,
FlexStackerId,
Expand All @@ -12,6 +18,7 @@
from opentrons.protocol_engine.commands import flex_stacker
from opentrons.protocol_engine.commands.command import SuccessData
from opentrons.protocol_engine.commands.flex_stacker.retrieve import RetrieveImpl
from opentrons.protocol_engine.types import Dimensions, ModuleLocation


async def test_retrieve(
Expand All @@ -33,13 +40,31 @@ async def test_retrieve(
decoy.when(fs_module_substate.module_id).then_return(
FlexStackerId("flex-stacker-id")
)
decoy.when(fs_module_substate.hopper_labware_ids).then_return(["labware-id"])
decoy.when(state_view.labware.get_dimensions(labware_id="labware-id")).then_return(
Dimensions(x=1, y=1, z=1)
)

decoy.when(
equipment.get_module_hardware_api(FlexStackerId("flex-stacker-id"))
).then_return(fs_hardware)

result = await subject.execute(data)
decoy.verify(await fs_hardware.dispense_labware(labware_height=50.0), times=1)
decoy.verify(await fs_hardware.dispense_labware(labware_height=1), times=1)

assert result == SuccessData(
public=flex_stacker.RetrieveResult(),
public=flex_stacker.RetrieveResult(labware_id="labware-id"),
state_update=StateUpdate(
labware_location=LabwareLocationUpdate(
labware_id="labware-id",
new_location=ModuleLocation(moduleId="flex-stacker-id"),
offset_id=None,
),
flex_stacker_state_update=FlexStackerStateUpdate(
module_id="flex-stacker-id",
hopper_labware_update=FlexStackerRetrieveLabware(
labware_id="labware-id"
),
),
),
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@

from opentrons.hardware_control.modules import FlexStacker

from opentrons.protocol_engine.state.update_types import (
StateUpdate,
FlexStackerStateUpdate,
FlexStackerStoreLabware,
LabwareLocationUpdate,
)

from opentrons.protocol_engine.state.state import StateView
from opentrons.protocol_engine.state.module_substates import (
FlexStackerSubState,
Expand All @@ -12,6 +19,7 @@
from opentrons.protocol_engine.commands import flex_stacker
from opentrons.protocol_engine.commands.command import SuccessData
from opentrons.protocol_engine.commands.flex_stacker.store import StoreImpl
from opentrons.protocol_engine.types import Dimensions, OFF_DECK_LOCATION


async def test_store(
Expand All @@ -34,12 +42,31 @@ async def test_store(
FlexStackerId("flex-stacker-id")
)

decoy.when(
state_view.labware.get_id_by_module(module_id="flex-stacker-id")
).then_return("labware-id")

decoy.when(state_view.labware.get_dimensions(labware_id="labware-id")).then_return(
Dimensions(x=1, y=1, z=1)
)

decoy.when(
equipment.get_module_hardware_api(FlexStackerId("flex-stacker-id"))
).then_return(fs_hardware)

result = await subject.execute(data)
decoy.verify(await fs_hardware.store_labware(labware_height=50.0), times=1)
decoy.verify(await fs_hardware.store_labware(labware_height=1), times=1)
assert result == SuccessData(
public=flex_stacker.StoreResult(),
state_update=StateUpdate(
labware_location=LabwareLocationUpdate(
labware_id="labware-id",
new_location=OFF_DECK_LOCATION,
offset_id=None,
),
flex_stacker_state_update=FlexStackerStateUpdate(
module_id="flex-stacker-id",
hopper_labware_update=FlexStackerStoreLabware(labware_id="labware-id"),
),
),
)
Loading

0 comments on commit 057a705

Please sign in to comment.