Skip to content

Commit

Permalink
fix(api): restrict the labware that can be moved to the plate reader …
Browse files Browse the repository at this point in the history
…+ validate wavelengths. (#16649)
  • Loading branch information
vegano1 authored Nov 5, 2024
1 parent 3cf6f34 commit 01c06d5
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 12 deletions.
39 changes: 38 additions & 1 deletion api/src/opentrons/protocol_api/core/engine/module_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions api/src/opentrons/protocol_engine/commands/load_labware.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from ..resources import labware_validation, fixture_validation
from ..types import (
LabwareLocation,
ModuleLocation,
ModuleModel,
OnLabwareLocation,
DeckSlotLocation,
AddressableAreaLocation,
Expand Down Expand Up @@ -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(
Expand Down
9 changes: 9 additions & 0 deletions api/src/opentrons/protocol_engine/commands/move_labware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
27 changes: 27 additions & 0 deletions api/src/opentrons/protocol_engine/state/labware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,67 +69,75 @@ 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(
decoy: Decoy, mock_engine_client: EngineClient, subject: AbsorbanceReaderCore
) -> 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,
Expand All @@ -152,6 +160,7 @@ def test_read(
mock_engine_client.execute_command(
cmd.absorbance_reader.ReadAbsorbanceParams(
moduleId="1234",
fileName=None,
),
),
times=1,
Expand Down

0 comments on commit 01c06d5

Please sign in to comment.