Skip to content

Commit

Permalink
feat(api): Add stacker fill, empy, set_stored_labware (#17570)
Browse files Browse the repository at this point in the history
Add a protocol API binding for fill, empty, and set_stored_labware.
These functions are used to configure the stacker labware pool and to
empty or fill the hopper interactively while a protocol is paused.


Also, add tests and a slight defaulting change to the engine
setStoredLabware command.

## testing
- [x] run a protocol and test that fill, empty, and set_stored_labware
run

Closes EXEC-1217
Closes EXEC-1216
  • Loading branch information
sfoster1 authored Feb 21, 2025
1 parent 71e76e2 commit 477ed2f
Show file tree
Hide file tree
Showing 10 changed files with 626 additions and 15 deletions.
87 changes: 86 additions & 1 deletion api/src/opentrons/protocol_api/core/engine/module_core.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Protocol API module implementation logic."""

from __future__ import annotations

from typing import Optional, List, Dict, Union
Expand All @@ -17,7 +18,7 @@
)

from opentrons.protocol_engine import commands as cmd
from opentrons.protocol_engine.types import ABSMeasureMode
from opentrons.protocol_engine.types import ABSMeasureMode, StackerFillEmptyStrategy
from opentrons.types import DeckSlotName
from opentrons.protocol_engine.clients import SyncClient as ProtocolEngineClient
from opentrons.protocol_engine.errors.exceptions import (
Expand All @@ -40,6 +41,7 @@
AbstractFlexStackerCore,
)
from .exceptions import InvalidMagnetEngageHeightError
from . import load_labware_params


# Valid wavelength range for absorbance reader
Expand Down Expand Up @@ -729,3 +731,86 @@ def store(self) -> None:
moduleId=self.module_id,
)
)

def fill(self, message: str, count: int | None) -> None:
"""Pause the protocol to add more labware to the Flex Stacker's hopper."""
self._engine_client.execute_command(
cmd.flex_stacker.FillParams(
moduleId=self.module_id,
strategy=StackerFillEmptyStrategy.MANUAL_WITH_PAUSE,
message=message,
count=count,
)
)

def empty(self, message: str) -> None:
"""Pause the protocol to remove labware from the Flex Stacker's hopper."""
self._engine_client.execute_command(
cmd.flex_stacker.EmptyParams(
moduleId=self.module_id,
strategy=StackerFillEmptyStrategy.MANUAL_WITH_PAUSE,
message=message,
count=0,
)
)

def set_stored_labware(
self,
main_load_name: str,
main_namespace: str | None,
main_version: int | None,
lid_load_name: str | None,
lid_namespace: str | None,
lid_version: int | None,
adapter_load_name: str | None,
adapter_namespace: str | None,
adapter_version: int | None,
count: int | None,
) -> None:
"""Configure the kind of labware that the stacker stores."""

custom_labware_params = (
self._engine_client.state.labware.find_custom_labware_load_params()
)

main_namespace, main_version = load_labware_params.resolve(
main_load_name, main_namespace, main_version, custom_labware_params
)
main_labware = cmd.flex_stacker.StackerStoredLabwareDetails(
loadName=main_load_name, namespace=main_namespace, version=main_version
)

lid_labware: cmd.flex_stacker.StackerStoredLabwareDetails | None = None

if lid_load_name:
lid_namespace, lid_version = load_labware_params.resolve(
lid_load_name, lid_namespace, lid_version, custom_labware_params
)
lid_labware = cmd.flex_stacker.StackerStoredLabwareDetails(
loadName=lid_load_name, namespace=lid_namespace, version=lid_version
)

adapter_labware: cmd.flex_stacker.StackerStoredLabwareDetails | None = None

if adapter_load_name:
adapter_namespace, adapter_version = load_labware_params.resolve(
adapter_load_name,
adapter_namespace,
adapter_version,
custom_labware_params,
)
adapter_labware = cmd.flex_stacker.StackerStoredLabwareDetails(
loadName=adapter_load_name,
namespace=adapter_namespace,
version=adapter_version,
)

self._engine_client.execute_command(
cmd.flex_stacker.SetStoredLabwareParams(
moduleId=self.module_id,
initialCount=count,
primaryLabware=main_labware,
lidLabware=lid_labware,
adapterLabware=adapter_labware,
)
)
25 changes: 25 additions & 0 deletions api/src/opentrons/protocol_api/core/module.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Core module control interfaces."""

from __future__ import annotations

from abc import ABC, abstractmethod
Expand Down Expand Up @@ -401,3 +402,27 @@ def retrieve(self) -> None:
@abstractmethod
def store(self) -> None:
"""Store a labware in the stacker hopper."""

@abstractmethod
def fill(self, message: str, count: int | None) -> None:
"""Pause the protocol to allow for filling the stacker."""

@abstractmethod
def empty(self, message: str) -> None:
"""Pause the protocol to allow for emptying the stacker."""

@abstractmethod
def set_stored_labware(
self,
main_load_name: str,
main_namespace: str | None,
main_version: int | None,
lid_load_name: str | None,
lid_namespace: str | None,
lid_version: int | None,
adapter_load_name: str | None,
adapter_namespace: str | None,
adapter_version: int | None,
count: int | None,
) -> None:
"""Configure the kind of labware that the stacker stores."""
75 changes: 75 additions & 0 deletions api/src/opentrons/protocol_api/module_contexts.py
Original file line number Diff line number Diff line change
Expand Up @@ -1188,3 +1188,78 @@ def store(self, labware: Labware) -> None:
"""
assert labware._core is not None
self._core.store()

@requires_version(2, 23)
def set_stored_labware(
self,
load_name: str,
namespace: str | None = None,
version: int | None = None,
adapter: str | None = None,
lid: str | None = None,
count: int | None = None,
) -> None:
"""Configure what kind of labware the Flex Stacker will store.
:param str load_name: A string to use for looking up a labware definition.
You can find the ``load_name`` for any Opentrons-verified labware on the
`Labware Library <https://labware.opentrons.com>`__.
:param str namespace: The namespace that the labware definition belongs to.
If unspecified, the API will automatically search two namespaces:
- ``"opentrons"``, to load standard Opentrons labware definitions.
- ``"custom_beta"``, to load custom labware definitions created with the
`Custom Labware Creator <https://labware.opentrons.com/create>`__.
You might need to specify an explicit ``namespace`` if you have a custom
definition whose ``load_name`` is the same as an Opentrons-verified
definition, and you want to explicitly choose one or the other.
:param version: The version of the labware definition. You should normally
leave this unspecified to let ``load_labware()`` choose a version
automatically.
:param adapter: An adapter to load the labware on top of. Accepts the same
values as the ``load_name`` parameter of :py:meth:`.load_adapter`. The
adapter will use the same namespace as the labware, and the API will
choose the adapter's version automatically.
:param lid: A lid to load the on top of the main labware. Accepts the same
values as the ``load_name`` parameter of :py:meth:`.load_lid_stack`. The
lid will use the same namespace as the labware, and the API will
choose the lid's version automatically.
:param count: The number of labware that the Flex Stacker should start the protocol
storing. If not specified, this will be the maximum amount of this kind of
labware that the Flex Stacker is capable of storing.
"""
self._core.set_stored_labware(
main_load_name=load_name,
main_namespace=namespace,
main_version=version,
lid_load_name=lid,
lid_namespace=namespace,
lid_version=version,
adapter_load_name=adapter,
adapter_namespace=namespace,
adapter_version=version,
count=count,
)

@requires_version(2, 23)
def fill(self, message: str, count: int | None = None) -> None:
"""Pause the protocol to add more labware to the Flex Stacker.
:param message: A message to display in the Opentrons App to note what kind of labware to add.
:param count: The amount of labware the Flex Stacker should hold after this command is executed.
If not specified, the Flex Stacker should be full after this command is executed.
"""
self._core.fill(message, count)

@requires_version(2, 23)
def empty(self, message: str) -> None:
"""Pause the protocol to remove labware from the Flex Stacker.
:param message: A message to display in the Opentrons App to note what should be removed from
the Flex Stacker.
"""
self._core.empty(
message,
)
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
SetStoredLabwareResult,
SetStoredLabware,
SetStoredLabwareCreate,
StackerStoredLabwareDetails,
)

from .fill import FillCommandType, FillParams, FillResult, Fill, FillCreate
Expand Down Expand Up @@ -62,6 +63,7 @@
"SetStoredLabwareResult",
"SetStoredLabware",
"SetStoredLabwareCreate",
"StackerStoredLabwareDetails",
# flexStacker/fill
"FillCommandType",
"FillParams",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,6 @@ class SetStoredLabwareParams(BaseModel):
...,
description="Unique ID of the Flex Stacker.",
)
initialCount: int = Field(
...,
description=(
"The number of labware that should be initially stored in the stacker. This number will be silently clamped to "
"the maximum number of labware that will fit; do not rely on the parameter to know how many labware are in the stacker."
),
ge=0,
)
primaryLabware: StackerStoredLabwareDetails = Field(
...,
description="The details of the primary labware (i.e. not the lid or adapter, if any) stored in the stacker.",
Expand All @@ -61,6 +53,14 @@ class SetStoredLabwareParams(BaseModel):
default=None,
description="The details of the adapter under the primary labware, if any.",
)
initialCount: int | None = Field(
None,
description=(
"The number of labware that should be initially stored in the stacker. This number will be silently clamped to "
"the maximum number of labware that will fit; do not rely on the parameter to know how many labware are in the stacker."
),
ge=0,
)


class SetStoredLabwareResult(BaseModel):
Expand Down Expand Up @@ -144,7 +144,8 @@ async def execute(
definition_stack.insert(-1, adapter_def)

# TODO: propagate the limit on max height of the stacker
count = min(params.initialCount, 5)
initial_count = params.initialCount if params.initialCount is not None else 5
count = min(initial_count, 5)

state_update = (
update_types.StateUpdate()
Expand All @@ -154,7 +155,7 @@ async def execute(
.update_flex_stacker_labware_pool_count(params.moduleId, count)
)
return SuccessData(
public=SetStoredLabwareResult(
public=SetStoredLabwareResult.model_construct(
primaryLabwareDefinition=labware_def,
lidLabwareDefinition=lid_def,
adapterLabwareDefinition=adapter_def,
Expand Down
2 changes: 2 additions & 0 deletions api/src/opentrons/protocol_engine/errors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
LiquidClassRedefinitionError,
OffsetLocationInvalidError,
FlexStackerLabwarePoolNotYetDefinedError,
FlexStackerNotLogicallyEmptyError,
)

from .error_occurrence import ErrorOccurrence, ProtocolCommandFailedError
Expand Down Expand Up @@ -169,6 +170,7 @@
"NotSupportedOnRobotType",
"OffsetLocationInvalidError",
"FlexStackerLabwarePoolNotYetDefinedError",
"FlexStackerNotLogicallyEmptyError",
# error occurrence models
"ErrorOccurrence",
"CommandNotAllowedError",
Expand Down
Loading

0 comments on commit 477ed2f

Please sign in to comment.