Skip to content

Commit

Permalink
feat(api): enforce stack constraints in stacker (#17572)
Browse files Browse the repository at this point in the history
setStoredLabware now applies the same kinds of checks that we use when
loading labware to the labware pool at the time of pool constraint to
make sure that it's valid to retrieve the labware that the pool defines.

Closes EXEC-1239
  • Loading branch information
sfoster1 authored Feb 24, 2025
1 parent 7c0a37c commit 7ca7915
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 3 deletions.
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

0 comments on commit 7ca7915

Please sign in to comment.