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): enforce stack constraints in stacker #17572

Open
wants to merge 1 commit into
base: edge
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -125,23 +125,24 @@ async def execute(
namespace=params.primaryLabware.namespace,
version=params.primaryLabware.version,
)
definition_stack = [labware_def]
lid_def: LabwareDefinition | None = None
if params.lidLabware:
lid_def, _ = await self._equipment.load_definition_for_details(
load_name=params.lidLabware.loadName,
namespace=params.lidLabware.namespace,
version=params.lidLabware.version,
)
definition_stack.insert(0, lid_def)
adapter_def: LabwareDefinition | None = None
if params.adapterLabware:
adapter_def, _ = await self._equipment.load_definition_for_details(
load_name=params.adapterLabware.loadName,
namespace=params.adapterLabware.namespace,
version=params.adapterLabware.version,
)
definition_stack.insert(-1, adapter_def)

self._state_view.labware.raise_if_stacker_labware_pool_is_not_valid(
labware_def, lid_def, adapter_def
)

# TODO: propagate the limit on max height of the stacker
initial_count = params.initialCount if params.initialCount is not None else 5
Expand Down
35 changes: 35 additions & 0 deletions api/src/opentrons/protocol_engine/state/labware.py
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,41 @@ def raise_if_labware_incompatible_with_plate_reader(
f" maximum allowed labware height is {_PLATE_READER_MAX_LABWARE_Z_MM}mm."
)

def raise_if_stacker_labware_pool_is_not_valid(
self,
primary_labware_definition: LabwareDefinition,
lid_labware_definition: LabwareDefinition | None,
adapter_labware_definition: LabwareDefinition | None,
) -> None:
"""Raise if the primary, lid, and adapter do not go together."""
if lid_labware_definition:
if not labware_validation.validate_definition_is_lid(
lid_labware_definition
):
raise errors.LabwareCannotBeStackedError(
f"Labware {lid_labware_definition.parameters.loadName} cannot be used as a lid in the Flex Stacker."
)
if not labware_validation.validate_labware_can_be_stacked(
lid_labware_definition, primary_labware_definition.parameters.loadName
):
raise errors.LabwareCannotBeStackedError(
f"Labware {lid_labware_definition.parameters.loadName} cannot be used as a lid for {primary_labware_definition.parameters.loadName}"
)
if adapter_labware_definition:
if not labware_validation.validate_definition_is_adapter(
adapter_labware_definition
):
raise errors.LabwareCannotBeStackedError(
f"Labware {adapter_labware_definition.parameters.loadName} cannot be used as an adapter in the Flex Stacker."
)
if not labware_validation.validate_labware_can_be_stacked(
primary_labware_definition,
adapter_labware_definition.parameters.loadName,
):
raise errors.LabwareCannotBeStackedError(
f"Labware {adapter_labware_definition.parameters.loadName} cannot be used as an adapter for {primary_labware_definition.parameters.loadName}"
)

def raise_if_labware_cannot_be_stacked( # noqa: C901
self, top_labware_definition: LabwareDefinition, bottom_labware_id: str
) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ async def test_set_stored_labware_happypath(
).then_return((sentinel.adapter_definition, sentinel.unused))
adapter_definition = sentinel.adapter_definition
result = await subject.execute(params)
decoy.verify(
state_view.labware.raise_if_stacker_labware_pool_is_not_valid(
sentinel.primary_definition, lid_definition, adapter_definition
)
)

assert result == SuccessData(
public=SetStoredLabwareResult.model_construct(
primaryLabwareDefinition=sentinel.primary_definition,
Expand Down
189 changes: 189 additions & 0 deletions api/tests/opentrons/protocol_engine/state/test_labware_view_old.py
Original file line number Diff line number Diff line change
Expand Up @@ -1510,6 +1510,195 @@ def test_raise_if_labware_cannot_be_stacked_on_labware_on_adapter() -> None:
)


@pytest.mark.parametrize(
argnames=["primary_def", "lid_def", "adapter_def", "exception"],
argvalues=[
pytest.param(
LabwareDefinition2.model_construct( # type: ignore[call-arg]
allowedRoles=[LabwareRole.labware],
parameters=Parameters2.model_construct( # type: ignore[call-arg]
loadName="primary"
),
stackingOffsetWithLabware={"adapter": Vector(x=0, y=0, z=0)},
),
LabwareDefinition2.model_construct( # type: ignore[call-arg]
allowedRoles=[LabwareRole.lid],
parameters=Parameters2.model_construct( # type: ignore[call-arg]
loadName="lid"
),
stackingOffsetWithLabware={"primary": Vector(x=0, y=0, z=0)},
),
LabwareDefinition2.model_construct( # type: ignore[call-arg]
allowedRoles=[LabwareRole.adapter],
parameters=Parameters2.model_construct( # type: ignore[call-arg]
loadName="adapter"
),
),
does_not_raise(),
id="all-valid-and-present",
),
pytest.param(
LabwareDefinition2.model_construct( # type: ignore[call-arg]
allowedRoles=[LabwareRole.labware],
parameters=Parameters2.model_construct( # type: ignore[call-arg]
loadName="primary"
),
stackingOffsetWithLabware={"adapter": Vector(x=0, y=0, z=0)},
),
None,
LabwareDefinition2.model_construct( # type: ignore[call-arg]
allowedRoles=[LabwareRole.adapter],
parameters=Parameters2.model_construct( # type: ignore[call-arg]
loadName="adapter"
),
),
does_not_raise(),
id="adapter-valid-and-present",
),
pytest.param(
LabwareDefinition2.model_construct( # type: ignore[call-arg]
allowedRoles=[LabwareRole.labware],
parameters=Parameters2.model_construct( # type: ignore[call-arg]
loadName="primary"
),
stackingOffsetWithLabware={"adapter": Vector(x=0, y=0, z=0)},
),
LabwareDefinition2.model_construct( # type: ignore[call-arg]
allowedRoles=[LabwareRole.lid],
parameters=Parameters2.model_construct( # type: ignore[call-arg]
loadName="lid"
),
stackingOffsetWithLabware={"primary": Vector(x=0, y=0, z=0)},
),
None,
does_not_raise(),
id="lid-valid-and-present",
),
pytest.param(
LabwareDefinition2.model_construct( # type: ignore[call-arg]
allowedRoles=[LabwareRole.labware],
parameters=Parameters2.model_construct( # type: ignore[call-arg]
loadName="primary"
),
stackingOffsetWithLabware={"adapter": Vector(x=0, y=0, z=0)},
),
None,
None,
does_not_raise(),
id="primary-only",
),
pytest.param(
LabwareDefinition2.model_construct( # type: ignore[call-arg]
allowedRoles=[LabwareRole.labware],
parameters=Parameters2.model_construct( # type: ignore[call-arg]
loadName="primary"
),
stackingOffsetWithLabware={"adapter": Vector(x=0, y=0, z=0)},
),
LabwareDefinition2.model_construct( # type: ignore[call-arg]
allowedRoles=[LabwareRole.lid],
parameters=Parameters2.model_construct( # type: ignore[call-arg]
loadName="lid"
),
stackingOffsetWithLabware={"uhoh": Vector(x=0, y=0, z=0)},
),
LabwareDefinition2.model_construct( # type: ignore[call-arg]
allowedRoles=[LabwareRole.adapter],
parameters=Parameters2.model_construct( # type: ignore[call-arg]
loadName="adapter"
),
),
pytest.raises(errors.LabwareCannotBeStackedError),
id="lid-may-not-stack-on-primary",
),
pytest.param(
LabwareDefinition2.model_construct( # type: ignore[call-arg]
allowedRoles=[LabwareRole.labware],
parameters=Parameters2.model_construct( # type: ignore[call-arg]
loadName="primary"
),
stackingOffsetWithLabware={"uhoh": Vector(x=0, y=0, z=0)},
),
LabwareDefinition2.model_construct( # type: ignore[call-arg]
allowedRoles=[LabwareRole.lid],
parameters=Parameters2.model_construct( # type: ignore[call-arg]
loadName="lid"
),
stackingOffsetWithLabware={"primary": Vector(x=0, y=0, z=0)},
),
LabwareDefinition2.model_construct( # type: ignore[call-arg]
allowedRoles=[LabwareRole.adapter],
parameters=Parameters2.model_construct( # type: ignore[call-arg]
loadName="adapter"
),
),
pytest.raises(errors.LabwareCannotBeStackedError),
id="primary-may-not-stack-on-adapter",
),
pytest.param(
LabwareDefinition2.model_construct( # type: ignore[call-arg]
allowedRoles=[LabwareRole.labware],
parameters=Parameters2.model_construct( # type: ignore[call-arg]
loadName="primary"
),
stackingOffsetWithLabware={"adapter": Vector(x=0, y=0, z=0)},
),
LabwareDefinition2.model_construct( # type: ignore[call-arg]
allowedRoles=[LabwareRole.lid],
parameters=Parameters2.model_construct( # type: ignore[call-arg]
loadName="lid"
),
stackingOffsetWithLabware={"primary": Vector(x=0, y=0, z=0)},
),
LabwareDefinition2.model_construct( # type: ignore[call-arg]
allowedRoles=[LabwareRole.labware],
parameters=Parameters2.model_construct( # type: ignore[call-arg]
loadName="adapter"
),
),
pytest.raises(errors.LabwareCannotBeStackedError),
id="adapter-wrong-role",
),
pytest.param(
LabwareDefinition2.model_construct( # type: ignore[call-arg]
allowedRoles=[LabwareRole.labware],
parameters=Parameters2.model_construct( # type: ignore[call-arg]
loadName="primary"
),
stackingOffsetWithLabware={"adapter": Vector(x=0, y=0, z=0)},
),
LabwareDefinition2.model_construct( # type: ignore[call-arg]
allowedRoles=[LabwareRole.labware],
parameters=Parameters2.model_construct( # type: ignore[call-arg]
loadName="lid"
),
stackingOffsetWithLabware={"primary": Vector(x=0, y=0, z=0)},
),
LabwareDefinition2.model_construct( # type: ignore[call-arg]
allowedRoles=[LabwareRole.adapter],
parameters=Parameters2.model_construct( # type: ignore[call-arg]
loadName="adapter"
),
),
pytest.raises(errors.LabwareCannotBeStackedError),
id="lid-wrong-role",
),
],
)
def test_stacker_labware_pool_passes_or_raises(
primary_def: LabwareDefinition,
lid_def: LabwareDefinition | None,
adapter_def: LabwareDefinition | None,
exception: ContextManager[None],
) -> None:
"""It should raise if a stacker labware pool configuration is invalid."""
subject = get_labware_view()
with exception:
subject.raise_if_stacker_labware_pool_is_not_valid(
primary_def, lid_def, adapter_def
)


@pytest.mark.parametrize(
argnames=[
"allowed_roles",
Expand Down
Loading