Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): add a reload-labware command #14963

Merged
merged 10 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions api/src/opentrons/protocol_engine/clients/sync_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,27 @@ def load_labware(

return cast(commands.LoadLabwareResult, result)

def reload_labware(
self,
labware_id: str,
load_name: str,
namespace: str,
version: int,
display_name: Optional[str] = None,
) -> commands.ReloadLabwareResult:
"""Execute a ReloadLabware command and return the result."""
request = commands.ReloadLabwareCreate(
params=commands.ReloadLabwareParams(
labwareId=labware_id,
loadName=load_name,
namespace=namespace,
version=version,
displayName=display_name,
)
)
result = self._transport.execute_command(request=request)
return cast(commands.ReloadLabwareResult, result)

# TODO (spp, 2022-12-14): https://opentrons.atlassian.net/browse/RLAB-237
def move_labware(
self,
Expand Down
14 changes: 14 additions & 0 deletions api/src/opentrons/protocol_engine/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,14 @@
LoadLabwareCommandType,
)

from .reload_labware import (
ReloadLabware,
ReloadLabwareParams,
ReloadLabwareCreate,
ReloadLabwareResult,
ReloadLabwareCommandType,
)

from .load_liquid import (
LoadLiquid,
LoadLiquidParams,
Expand Down Expand Up @@ -402,6 +410,12 @@
"LoadLabwareParams",
"LoadLabwareResult",
"LoadLabwareCommandType",
# reload labware command models
"ReloadLabware",
"ReloadLabwareCreate",
"ReloadLabwareParams",
"ReloadLabwareResult",
"ReloadLabwareCommandType",
# load module command models
"LoadModule",
"LoadModuleCreate",
Expand Down
13 changes: 13 additions & 0 deletions api/src/opentrons/protocol_engine/commands/command_unions.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@
LoadLabwareCommandType,
)

from .reload_labware import (
ReloadLabware,
ReloadLabwareParams,
ReloadLabwareCreate,
ReloadLabwareResult,
ReloadLabwareCommandType,
)

from .load_liquid import (
LoadLiquid,
LoadLiquidParams,
Expand Down Expand Up @@ -304,6 +312,7 @@
Home,
RetractAxis,
LoadLabware,
ReloadLabware,
LoadLiquid,
LoadModule,
LoadPipette,
Expand Down Expand Up @@ -368,6 +377,7 @@
HomeParams,
RetractAxisParams,
LoadLabwareParams,
ReloadLabwareParams,
LoadLiquidParams,
LoadModuleParams,
LoadPipetteParams,
Expand Down Expand Up @@ -431,6 +441,7 @@
HomeCommandType,
RetractAxisCommandType,
LoadLabwareCommandType,
ReloadLabwareCommandType,
LoadLiquidCommandType,
LoadModuleCommandType,
LoadPipetteCommandType,
Expand Down Expand Up @@ -494,6 +505,7 @@
HomeCreate,
RetractAxisCreate,
LoadLabwareCreate,
ReloadLabwareCreate,
LoadLiquidCreate,
LoadModuleCreate,
LoadPipetteCreate,
Expand Down Expand Up @@ -558,6 +570,7 @@
HomeResult,
RetractAxisResult,
LoadLabwareResult,
ReloadLabwareResult,
LoadLiquidResult,
LoadModuleResult,
LoadPipetteResult,
Expand Down
139 changes: 139 additions & 0 deletions api/src/opentrons/protocol_engine/commands/reload_labware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"""Reload labware command request, result, and implementation models."""
from __future__ import annotations
from pydantic import BaseModel, Field
from typing import TYPE_CHECKING, Optional, Type
from typing_extensions import Literal

from opentrons_shared_data.labware.labware_definition import LabwareDefinition
from ..resources import labware_validation

from ..errors import LabwareIsNotAllowedInLocationError
from ..types import (
OnLabwareLocation,
DeckSlotLocation,
)

from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate

if TYPE_CHECKING:
from ..state import StateView
from ..execution import EquipmentHandler


ReloadLabwareCommandType = Literal["reloadLabware"]


class ReloadLabwareParams(BaseModel):
"""Payload required to load a labware into a slot."""

labwareId: str = Field(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ty!

..., description="The already-loaded labware instance to update."
)
loadName: str = Field(
...,
description="Name used to reference a labware definition.",
)
namespace: str = Field(
...,
description="The namespace the labware definition belongs to.",
)
version: int = Field(
...,
description="The labware definition version.",
)
displayName: Optional[str] = Field(
None,
description="An optional user-specified display name "
"or label for this labware.",
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have expected this to only take an id. What do loadName/namespace/version do if they're different from the original? Replace the labware with a different one, but keep the ID?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct! Honestly I don't know why someone would want to do this, but it does work, and it leads to better symmetry with the load command

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. Changing the loadName/namespace/version makes me nervous. No concrete reason, but it feels pretty good to me that a labware ID immutably points to a single thing. How do you feel about leaving this out until we have a use case for it?

I think loadLabware does let you do this, but it's always kind of been by accident.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @SyntaxColoring. maybe we should leave this out for now?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Okay, though I think it's going to do so little at that point that it'll look weird. I'm not so nervous about the changing-the-definition thing, I think this part of the code is really well tested and generally capable.



class ReloadLabwareResult(BaseModel):
"""Result data from the execution of a LoadLabware command."""

labwareId: str = Field(
...,
description="An ID to reference this labware in subsequent commands. Same as the one in the parameters.",
)
definition: LabwareDefinition = Field(
...,
description="The full definition data for this labware.",
)
offsetId: Optional[str] = Field(
# Default `None` instead of `...` so this field shows up as non-required in
# OpenAPI. The server is allowed to omit it or make it null.
None,
description=(
"An ID referencing the labware offset that will apply"
" to the reloaded labware."
" This offset will be in effect until the labware is moved"
" with a `moveLabware` command."
" Null or undefined means no offset applies,"
" so the default of (0, 0, 0) will be used."
),
)


class ReloadLabwareImplementation(
AbstractCommandImpl[ReloadLabwareParams, ReloadLabwareResult]
):
"""Load labware command implementation."""

def __init__(
self, equipment: EquipmentHandler, state_view: StateView, **kwargs: object
) -> None:
self._equipment = equipment
self._state_view = state_view

async def execute(self, params: ReloadLabwareParams) -> ReloadLabwareResult:
"""Reload the definition and calibration data for a specific labware."""
reloaded_labware = await self._equipment.reload_labware(
labware_id=params.labwareId,
load_name=params.loadName,
namespace=params.namespace,
version=params.version,
)

# note: this check must be kept because somebody might specify the trash loadName
if (
labware_validation.is_flex_trash(params.loadName)
and isinstance(reloaded_labware.location, DeckSlotLocation)
and self._state_view.geometry.get_slot_column(
reloaded_labware.location.slotName
)
!= 3
):
raise LabwareIsNotAllowedInLocationError(
f"{params.loadName} is not allowed in slot {reloaded_labware.location.slotName}"
)

if isinstance(reloaded_labware.location, OnLabwareLocation):
self._state_view.labware.raise_if_labware_cannot_be_stacked(
top_labware_definition=reloaded_labware.definition,
bottom_labware_id=reloaded_labware.location.labwareId,
)

return ReloadLabwareResult(
labwareId=params.labwareId,
definition=reloaded_labware.definition,
offsetId=reloaded_labware.offsetId,
)


class ReloadLabware(BaseCommand[ReloadLabwareParams, ReloadLabwareResult]):
"""Load labware command resource model."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reload?


commandType: ReloadLabwareCommandType = "reloadLabware"
params: ReloadLabwareParams
result: Optional[ReloadLabwareResult]

_ImplementationCls: Type[ReloadLabwareImplementation] = ReloadLabwareImplementation


class ReloadLabwareCreate(BaseCommandCreate[ReloadLabwareParams]):
"""Load labware command creation request."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reload?


commandType: ReloadLabwareCommandType = "reloadLabware"
params: ReloadLabwareParams

_CommandCls: Type[ReloadLabware] = ReloadLabware
2 changes: 2 additions & 0 deletions api/src/opentrons/protocol_engine/execution/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
LoadedPipetteData,
LoadedModuleData,
LoadedConfigureForVolumeData,
ReloadedLabwareData,
)
from .movement import MovementHandler
from .gantry_mover import GantryMover
Expand All @@ -29,6 +30,7 @@
"create_queue_worker",
"EquipmentHandler",
"LoadedLabwareData",
"ReloadedLabwareData",
"LoadedPipetteData",
"LoadedModuleData",
"LoadedConfigureForVolumeData",
Expand Down
51 changes: 51 additions & 0 deletions api/src/opentrons/protocol_engine/execution/equipment.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ class LoadedLabwareData:
offsetId: Optional[str]


@dataclass(frozen=True)
class ReloadedLabwareData:
"""The result of a reload labware procedure."""

definition: LabwareDefinition
location: LabwareLocation
offsetId: Optional[str]


@dataclass(frozen=True)
class LoadedPipetteData:
"""The result of a load pipette procedure."""
Expand Down Expand Up @@ -171,6 +180,48 @@ async def load_labware(
labware_id=labware_id, definition=definition, offsetId=offset_id
)

async def reload_labware(
self, labware_id: str, load_name: str, namespace: str, version: int
) -> ReloadedLabwareData:
"""Reload an already-loaded labware. This cannot change the labware location.

Args:
labware_id: The ID of the already-loaded labware.
load_name: The labware's load name.
namespace: The labware's namespace.
version: The labware's version.

Raises:
LabwareNotLoadedError: If `labware_id` does not reference a loaded labware.

"""
definition_uri = uri_from_details(
load_name=load_name,
namespace=namespace,
version=version,
)
location = self._state_store.labware.get_location(labware_id)

try:
# Try to use existing definition in state.
definition = self._state_store.labware.get_definition_by_uri(definition_uri)
except LabwareDefinitionDoesNotExistError:
definition = await self._labware_data_provider.get_labware_definition(
load_name=load_name,
namespace=namespace,
version=version,
)

# Allow propagation of ModuleNotLoadedError.
offset_id = self.find_applicable_labware_offset_id(
labware_definition_uri=definition_uri,
labware_location=location,
)

return ReloadedLabwareData(
definition=definition, location=location, offsetId=offset_id
)

async def load_pipette(
self,
pipette_name: PipetteNameType,
Expand Down
9 changes: 7 additions & 2 deletions api/src/opentrons/protocol_engine/state/labware.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
Command,
LoadLabwareResult,
MoveLabwareResult,
ReloadLabwareResult,
)
from ..types import (
DeckSlotLocation,
Expand Down Expand Up @@ -175,7 +176,7 @@ def handle_action(self, action: Action) -> None:

def _handle_command(self, command: Command) -> None:
"""Modify state in reaction to a command."""
if isinstance(command.result, LoadLabwareResult):
if isinstance(command.result, (LoadLabwareResult, ReloadLabwareResult)):
# If the labware load refers to an offset, that offset must actually exist.
if command.result.offsetId is not None:
assert command.result.offsetId in self._state.labware_offsets_by_id
Expand All @@ -187,12 +188,16 @@ def _handle_command(self, command: Command) -> None:
)

self._state.definitions_by_uri[definition_uri] = command.result.definition
if isinstance(command.result, LoadLabwareResult):
location = command.params.location
else:
location = self._state.labware_by_id[command.result.labwareId].location

self._state.labware_by_id[
command.result.labwareId
] = LoadedLabware.construct(
id=command.result.labwareId,
location=command.params.location,
location=location,
loadName=command.result.definition.parameters.loadName,
definitionUri=definition_uri,
offsetId=command.result.offsetId,
Expand Down
Loading
Loading