diff --git a/api/src/opentrons/protocol_api/core/engine/module_core.py b/api/src/opentrons/protocol_api/core/engine/module_core.py index 1e6d4e26b2f..d3cf8dca725 100644 --- a/api/src/opentrons/protocol_api/core/engine/module_core.py +++ b/api/src/opentrons/protocol_api/core/engine/module_core.py @@ -41,6 +41,11 @@ from .exceptions import InvalidMagnetEngageHeightError +# Valid wavelength range for absorbance reader +ABS_WAVELENGTH_MIN = 350 +ABS_WAVELENGTH_MAX = 1000 + + class ModuleCore(AbstractModuleCore): """Module core logic implementation for Python protocols. Args: @@ -581,7 +586,39 @@ def initialize( "Cannot perform Initialize action on Absorbance Reader without calling `.close_lid()` first." ) - # TODO: check that the wavelengths are within the supported wavelengths + wavelength_len = len(wavelengths) + if mode == "single" and wavelength_len != 1: + raise ValueError( + f"Single mode can only be initialized with 1 wavelength" + f" {wavelength_len} wavelengths provided instead." + ) + + if mode == "multi" and (wavelength_len < 1 or wavelength_len > 6): + raise ValueError( + f"Multi mode can only be initialized with 1 - 6 wavelengths." + f" {wavelength_len} wavelengths provided instead." + ) + + if reference_wavelength is not None and ( + reference_wavelength < ABS_WAVELENGTH_MIN + or reference_wavelength > ABS_WAVELENGTH_MAX + ): + raise ValueError( + f"Unsupported reference wavelength: ({reference_wavelength}) needs" + f" to between {ABS_WAVELENGTH_MIN} and {ABS_WAVELENGTH_MAX} nm." + ) + + for wavelength in wavelengths: + if ( + not isinstance(wavelength, int) + or wavelength < ABS_WAVELENGTH_MIN + or wavelength > ABS_WAVELENGTH_MAX + ): + raise ValueError( + f"Unsupported sample wavelength: ({wavelength}) needs" + f" to between {ABS_WAVELENGTH_MIN} and {ABS_WAVELENGTH_MAX} nm." + ) + self._engine_client.execute_command( cmd.absorbance_reader.InitializeParams( moduleId=self.module_id, diff --git a/api/src/opentrons/protocol_engine/commands/load_labware.py b/api/src/opentrons/protocol_engine/commands/load_labware.py index 05eccb95a7a..fb97f5d2c87 100644 --- a/api/src/opentrons/protocol_engine/commands/load_labware.py +++ b/api/src/opentrons/protocol_engine/commands/load_labware.py @@ -10,6 +10,8 @@ from ..resources import labware_validation, fixture_validation from ..types import ( LabwareLocation, + ModuleLocation, + ModuleModel, OnLabwareLocation, DeckSlotLocation, AddressableAreaLocation, @@ -160,6 +162,13 @@ async def execute( top_labware_definition=loaded_labware.definition, bottom_labware_id=verified_location.labwareId, ) + # Validate labware for the absorbance reader + elif isinstance(params.location, ModuleLocation): + module = self._state_view.modules.get(params.location.moduleId) + if module is not None and module.model == ModuleModel.ABSORBANCE_READER_V1: + self._state_view.labware.raise_if_labware_incompatible_with_plate_reader( + loaded_labware.definition + ) return SuccessData( public=LoadLabwareResult( diff --git a/api/src/opentrons/protocol_engine/commands/move_labware.py b/api/src/opentrons/protocol_engine/commands/move_labware.py index eb4b101e76c..09cdc08561c 100644 --- a/api/src/opentrons/protocol_engine/commands/move_labware.py +++ b/api/src/opentrons/protocol_engine/commands/move_labware.py @@ -13,9 +13,11 @@ from opentrons.protocol_engine.resources.model_utils import ModelUtils from opentrons.types import Point from ..types import ( + ModuleModel, CurrentWell, LabwareLocation, DeckSlotLocation, + ModuleLocation, OnLabwareLocation, AddressableAreaLocation, LabwareMovementStrategy, @@ -221,6 +223,13 @@ async def execute(self, params: MoveLabwareParams) -> _ExecuteReturn: # noqa: C raise LabwareMovementNotAllowedError( "Cannot move a labware onto itself." ) + # Validate labware for the absorbance reader + elif isinstance(available_new_location, ModuleLocation): + module = self._state_view.modules.get(available_new_location.moduleId) + if module is not None and module.model == ModuleModel.ABSORBANCE_READER_V1: + self._state_view.labware.raise_if_labware_incompatible_with_plate_reader( + current_labware_definition + ) # Allow propagation of ModuleNotLoadedError. new_offset_id = self._equipment.find_applicable_labware_offset_id( diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py index 0bbb8b3ab30..052ca1666ed 100644 --- a/api/src/opentrons/protocol_engine/state/labware.py +++ b/api/src/opentrons/protocol_engine/state/labware.py @@ -81,6 +81,10 @@ } +# The max height of the labware that can fit in a plate reader +_PLATE_READER_MAX_LABWARE_Z_MM = 16 + + class LabwareLoadParams(NamedTuple): """Parameters required to load a labware in Protocol Engine.""" @@ -818,6 +822,29 @@ def raise_if_labware_in_location( f"Labware {labware.loadName} is already present at {location}." ) + def raise_if_labware_incompatible_with_plate_reader( + self, + labware_definition: LabwareDefinition, + ) -> None: + """Raise an error if the labware is not compatible with the plate reader.""" + # TODO: (ba, 2024-11-1): the plate reader lid should not be a labware. + load_name = labware_definition.parameters.loadName + if load_name != "opentrons_flex_lid_absorbance_plate_reader_module": + number_of_wells = len(labware_definition.wells) + if number_of_wells != 96: + raise errors.LabwareMovementNotAllowedError( + f"Cannot move '{load_name}' into plate reader because the" + f" labware contains {number_of_wells} wells where 96 wells is expected." + ) + elif ( + labware_definition.dimensions.zDimension + > _PLATE_READER_MAX_LABWARE_Z_MM + ): + raise errors.LabwareMovementNotAllowedError( + f"Cannot move '{load_name}' into plate reader because the" + f" maximum allowed labware height is {_PLATE_READER_MAX_LABWARE_Z_MM}mm." + ) + 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_api/core/engine/test_absorbance_reader_core.py b/api/tests/opentrons/protocol_api/core/engine/test_absorbance_reader_core.py index fd537d4cad9..22b734a6024 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_absorbance_reader_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_absorbance_reader_core.py @@ -69,59 +69,67 @@ def test_initialize( ) -> None: """It should set the sample wavelength with the engine client.""" subject._ready_to_initialize = True - subject.initialize("single", [123]) + subject.initialize("single", [350]) decoy.verify( mock_engine_client.execute_command( cmd.absorbance_reader.InitializeParams( moduleId="1234", measureMode="single", - sampleWavelengths=[123], + sampleWavelengths=[350], referenceWavelength=None, ), ), times=1, ) - assert subject._initialized_value == [123] + assert subject._initialized_value == [350] # Test reference wavelength - subject.initialize("single", [124], 450) + subject.initialize("single", [350], 450) decoy.verify( mock_engine_client.execute_command( cmd.absorbance_reader.InitializeParams( moduleId="1234", measureMode="single", - sampleWavelengths=[124], + sampleWavelengths=[350], referenceWavelength=450, ), ), times=1, ) - assert subject._initialized_value == [124] + assert subject._initialized_value == [350] # Test initialize multi - subject.initialize("multi", [124, 125, 126]) + subject.initialize("multi", [350, 400, 450]) decoy.verify( mock_engine_client.execute_command( cmd.absorbance_reader.InitializeParams( moduleId="1234", measureMode="multi", - sampleWavelengths=[124, 125, 126], + sampleWavelengths=[350, 400, 450], referenceWavelength=None, ), ), times=1, ) - assert subject._initialized_value == [124, 125, 126] + assert subject._initialized_value == [350, 400, 450] def test_initialize_not_ready(subject: AbsorbanceReaderCore) -> None: """It should raise CannotPerformModuleAction if you dont call .close_lid() command.""" subject._ready_to_initialize = False with pytest.raises(CannotPerformModuleAction): - subject.initialize("single", [123]) + subject.initialize("single", [350]) + + +@pytest.mark.parametrize("wavelength", [-350, 0, 1200, "wda"]) +def test_invalid_wavelengths(wavelength: int, subject: AbsorbanceReaderCore) -> None: + """It should raise ValueError if you provide an invalid wavelengthi.""" + subject._ready_to_initialize = True + with pytest.raises(ValueError): + subject.initialize("single", [wavelength]) def test_read( @@ -129,7 +137,7 @@ def test_read( ) -> None: """It should call absorbance reader to read with the engine client.""" subject._ready_to_initialize = True - subject._initialized_value = [123] + subject._initialized_value = [350] substate = AbsorbanceReaderSubState( module_id=AbsorbanceReaderId(subject.module_id), configured=True, @@ -152,6 +160,7 @@ def test_read( mock_engine_client.execute_command( cmd.absorbance_reader.ReadAbsorbanceParams( moduleId="1234", + fileName=None, ), ), times=1,