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

fix(api): restrict the labware that can be moved to the plate reader + validate wavelengths. #16649

Merged
merged 5 commits into from
Nov 5, 2024
Merged
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
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
Loading