diff --git a/api/src/opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py b/api/src/opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py index 79a49ec9c20..843367fea16 100644 --- a/api/src/opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +++ b/api/src/opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py @@ -125,7 +125,6 @@ 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( @@ -133,7 +132,6 @@ async def execute( 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( @@ -141,7 +139,10 @@ async def execute( 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 diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py index 7b803882a01..b603089729d 100644 --- a/api/src/opentrons/protocol_engine/state/labware.py +++ b/api/src/opentrons/protocol_engine/state/labware.py @@ -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: diff --git a/api/tests/opentrons/protocol_engine/commands/flex_stacker/test_set_stored_labware.py b/api/tests/opentrons/protocol_engine/commands/flex_stacker/test_set_stored_labware.py index cc500a56afd..bdf27711f03 100644 --- a/api/tests/opentrons/protocol_engine/commands/flex_stacker/test_set_stored_labware.py +++ b/api/tests/opentrons/protocol_engine/commands/flex_stacker/test_set_stored_labware.py @@ -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, diff --git a/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py b/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py index 446dfb672c5..9414cc8bb4d 100644 --- a/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py @@ -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",