Skip to content

Commit

Permalink
add flex_stacker.load_labware_to_hopper
Browse files Browse the repository at this point in the history
  • Loading branch information
ahiuchingau committed Jan 14, 2025
1 parent 8a6a0cc commit 5ffc25a
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 5 deletions.
2 changes: 2 additions & 0 deletions api/src/opentrons/protocol_api/core/engine/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
NonConnectedModuleCore,
MagneticBlockCore,
AbsorbanceReaderCore,
FlexStackerCore,
)
from .exceptions import InvalidModuleLocationError
from . import load_labware_params, deck_conflict, overlap_versions
Expand Down Expand Up @@ -527,6 +528,7 @@ def _create_module_core(
ModuleType.THERMOCYCLER: ThermocyclerModuleCore,
ModuleType.HEATER_SHAKER: HeaterShakerModuleCore,
ModuleType.ABSORBANCE_READER: AbsorbanceReaderCore,
ModuleType.FLEX_STACKER: FlexStackerCore,
}

module_type = load_module_result.model.as_type()
Expand Down
26 changes: 26 additions & 0 deletions api/src/opentrons/protocol_api/module_contexts.py
Original file line number Diff line number Diff line change
Expand Up @@ -1112,6 +1112,32 @@ class FlexStackerContext(ModuleContext):

_core: FlexStackerCore

@requires_version(2, 23)
def load_labware_to_hopper(
self,
load_name: str,
quantity: int,
label: Optional[str] = None,
namespace: Optional[str] = None,
version: Optional[int] = None,
lid: Optional[str] = None,
) -> None:
"""Load one or more labware onto the flex stacker.
The parameters of this function behave like those of
:py:obj:`ProtocolContext.load_labware` (which loads labware directly
onto the deck). Note that the parameter ``name`` here corresponds to
``load_name`` on the ``ProtocolContext`` function.
:returns: The initialized and loaded labware object.
.. versionadded:: 2.23
The *label,* *namespace,* and *version* parameters.
"""
load_name = validation.ensure_lowercase_name(load_name)
for _ in range(quantity):
self.load_labware(load_name, label, namespace, version, lid)

@property
@requires_version(2, 23)
def serial_number(self) -> str:
Expand Down
29 changes: 27 additions & 2 deletions api/src/opentrons/protocol_api/protocol_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from opentrons.hardware_control.modules.types import (
MagneticBlockModel,
AbsorbanceReaderModel,
FlexStackerModuleModel,
)
from opentrons.legacy_commands import protocol_commands as cmds, types as cmd_types
from opentrons.legacy_commands.helpers import stringify_labware_movement_command
Expand Down Expand Up @@ -58,6 +59,7 @@
AbstractHeaterShakerCore,
AbstractMagneticBlockCore,
AbstractAbsorbanceReaderCore,
AbstractFlexStackerCore,
)
from .robot_context import RobotContext, HardwareManager
from .core.engine import ENGINE_CORE_API_VERSION
Expand All @@ -76,6 +78,7 @@
HeaterShakerContext,
MagneticBlockContext,
AbsorbanceReaderContext,
FlexStackerContext,
ModuleContext,
)
from ._parameters import Parameters
Expand All @@ -91,6 +94,7 @@
HeaterShakerContext,
MagneticBlockContext,
AbsorbanceReaderContext,
FlexStackerContext,
]


Expand Down Expand Up @@ -859,7 +863,11 @@ def load_module(
.. versionchanged:: 2.15
Added ``MagneticBlockContext`` return value.
.. versionchanged:: 2.23
Added ``FlexStackerModuleContext`` return value.
"""
flex_stacker_valid_since = APIVersion(2, 23)
if configuration:
if self._api_version < APIVersion(2, 4):
raise APIVersionError(
Expand Down Expand Up @@ -887,7 +895,18 @@ def load_module(
requested_model, AbsorbanceReaderModel
) and self._api_version < APIVersion(2, 21):
raise APIVersionError(
f"Module of type {module_name} is only available in versions 2.21 and above."
api_element=f"Module of type {module_name}",
until_version="2.21",
current_version=f"{self._api_version}",
)
if (
isinstance(requested_model, FlexStackerModuleModel)
and self._api_version < flex_stacker_valid_since
):
raise APIVersionError(
api_element=f"Module of type {module_name}",
until_version=str(flex_stacker_valid_since),
current_version=f"{self._api_version}",
)

deck_slot = (
Expand All @@ -898,7 +917,11 @@ def load_module(
)
)
if isinstance(deck_slot, StagingSlotName):
raise ValueError("Cannot load a module onto a staging slot.")
# flex stacker modules can only be loaded into staging slot inside a protocol
if isinstance(requested_model, FlexStackerModuleModel):
deck_slot = validation.convert_flex_stacker_load_slot(deck_slot)
else:
raise ValueError(f"Cannot load {module_name} onto a staging slot.")

module_core = self._core.load_module(
model=requested_model,
Expand Down Expand Up @@ -1462,6 +1485,8 @@ def _create_module_context(
module_cls = MagneticBlockContext
elif isinstance(module_core, AbstractAbsorbanceReaderCore):
module_cls = AbsorbanceReaderContext
elif isinstance(module_core, AbstractFlexStackerCore):
module_cls = FlexStackerContext
else:
assert False, "Unsupported module type"

Expand Down
16 changes: 16 additions & 0 deletions api/src/opentrons/protocol_api/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from opentrons_shared_data.pipette.types import PipetteNameType
from opentrons_shared_data.robot.types import RobotType

from opentrons.motion_planning.adjacent_slots_getters import get_west_of_staging_slot
from opentrons.protocols.api_support.types import APIVersion, ThermocyclerStep
from opentrons.protocols.api_support.util import APIVersionError
from opentrons.protocols.advanced_control.transfers.common import TransferTipPolicyV2
Expand All @@ -41,6 +42,7 @@
HeaterShakerModuleModel,
MagneticBlockModel,
AbsorbanceReaderModel,
FlexStackerModuleModel,
)

from .disposal_locations import TrashBin, WasteChute
Expand Down Expand Up @@ -413,6 +415,7 @@ def ensure_definition_is_not_lid_after_api_version(
"heaterShakerModuleV1": HeaterShakerModuleModel.HEATER_SHAKER_V1,
"magneticBlockV1": MagneticBlockModel.MAGNETIC_BLOCK_V1,
"absorbanceReaderV1": AbsorbanceReaderModel.ABSORBANCE_READER_V1,
"flexStackerModuleV1": FlexStackerModuleModel.FLEX_STACKER_V1,
}


Expand Down Expand Up @@ -744,3 +747,16 @@ def ensure_valid_tip_drop_location_for_transfer_v2(
f" or `Well` (e.g. `reservoir.wells()[0]`) or an instance of `TrashBin` or `WasteChute`."
f" However, it is '{tip_drop_location}'."
)


def convert_flex_stacker_load_slot(slot_name: StagingSlotName) -> DeckSlotName:
"""
Ensure a Flex Stacker load location to a deck slot location.
Args:
slot_name: The input staging slot location.
Returns:
A `DeckSlotName` on the deck.
"""
return get_west_of_staging_slot(slot_name)
2 changes: 1 addition & 1 deletion api/src/opentrons/protocols/api_support/definitions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from .types import APIVersion

MAX_SUPPORTED_VERSION = APIVersion(2, 22)
MAX_SUPPORTED_VERSION = APIVersion(2, 23)
"""The maximum supported protocol API version in this release."""

MIN_SUPPORTED_VERSION = APIVersion(2, 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
ThermocyclerModuleModel,
HeaterShakerModuleModel,
MagneticBlockModel,
FlexStackerModuleModel,
)
from opentrons.protocol_engine import (
ModuleModel as EngineModuleModel,
Expand Down Expand Up @@ -1613,6 +1614,7 @@ def test_load_module_thermocycler_with_no_location(
MagneticModuleModel.MAGNETIC_V2,
TemperatureModuleModel.TEMPERATURE_V1,
TemperatureModuleModel.TEMPERATURE_V2,
FlexStackerModuleModel.FLEX_STACKER_V1,
],
)
def test_load_module_no_location(
Expand Down
52 changes: 50 additions & 2 deletions api/tests/opentrons/protocol_api/test_protocol_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
from opentrons.config import feature_flags as ff
from opentrons.protocol_api import OFF_DECK
from opentrons.legacy_broker import LegacyBroker
from opentrons.hardware_control.modules.types import ModuleType, TemperatureModuleModel
from opentrons.hardware_control.modules.types import (
ModuleType,
TemperatureModuleModel,
FlexStackerModuleModel,
)
from opentrons.protocols.api_support import instrument as mock_instrument_support
from opentrons.protocols.api_support.types import APIVersion
from opentrons.protocols.api_support.util import (
Expand Down Expand Up @@ -44,6 +48,7 @@
TemperatureModuleCore,
MagneticModuleCore,
MagneticBlockCore,
FlexStackerCore,
)
from opentrons.protocol_api.disposal_locations import TrashBin, WasteChute
from opentrons.protocols.api_support.deck_type import (
Expand Down Expand Up @@ -1297,10 +1302,53 @@ def test_load_module_on_staging_slot_raises(
mock_validation.ensure_and_convert_deck_slot(42, api_version, "OT-3 Standard")
).then_return(StagingSlotName.SLOT_B4)

with pytest.raises(ValueError, match="Cannot load a module onto a staging slot."):
with pytest.raises(
ValueError, match="Cannot load spline reticulator onto a staging slot."
):
subject.load_module(module_name="spline reticulator", location=42)


def test_load_flex_stacker_on_staging_slot(
decoy: Decoy,
mock_core: ProtocolCore,
mock_core_map: LoadedCoreMap,
api_version: APIVersion,
subject: ProtocolContext,
) -> None:
"""It should load a module."""
mock_module_core: FlexStackerCore = decoy.mock(cls=FlexStackerCore)

decoy.when(mock_core.robot_type).then_return("OT-3 Standard")
decoy.when(mock_validation.ensure_module_model("flexStackerModuleV1")).then_return(
FlexStackerModuleModel.FLEX_STACKER_V1
)
decoy.when(
mock_validation.ensure_and_convert_deck_slot("B4", api_version, "OT-3 Standard")
).then_return(StagingSlotName.SLOT_B4)
decoy.when(
mock_validation.convert_flex_stacker_load_slot(StagingSlotName.SLOT_B4)
).then_return(DeckSlotName.SLOT_B3)

decoy.when(
mock_core.load_module(
model=FlexStackerModuleModel.FLEX_STACKER_V1,
deck_slot=DeckSlotName.SLOT_B3,
configuration=None,
)
).then_return(mock_module_core)

decoy.when(mock_module_core.get_model()).then_return(
FlexStackerModuleModel.FLEX_STACKER_V1
)
decoy.when(mock_module_core.get_serial_number()).then_return("cap'n crunch")
decoy.when(mock_module_core.get_deck_slot()).then_return(DeckSlotName.SLOT_B3)

result = subject.load_module(module_name="flexStackerModuleV1", location="B4")

assert isinstance(result, ModuleContext)
decoy.verify(mock_core_map.add(mock_module_core, result), times=1)


def test_loaded_modules(
decoy: Decoy,
mock_core_map: LoadedCoreMap,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
MagneticModuleModel,
ThermocyclerModuleModel,
HeaterShakerModuleModel,
FlexStackerModuleModel,
)
from opentrons_shared_data.deck.types import (
DeckDefinitionV5,
Expand Down Expand Up @@ -220,6 +221,13 @@ async def test_load_module_raises_if_location_occupied(
DeckSlotName.SLOT_A2,
"OT-3 Standard",
),
(
FlexStackerModuleModel.FLEX_STACKER_V1,
EngineModuleModel.FLEX_STACKER_MODULE_V1,
load_deck(STANDARD_OT3_DECK, 5),
DeckSlotName.SLOT_A2,
"OT-3 Standard",
),
],
)
async def test_load_module_raises_wrong_location(
Expand Down

0 comments on commit 5ffc25a

Please sign in to comment.