Skip to content

Commit

Permalink
fix(api): Error handling for unsupported pipette configuration behavi…
Browse files Browse the repository at this point in the history
…or with partial column and row (#15961)

Covers PLAT-457
Enforces pipette configuration rules on the 96ch and 8ch pipettes
  • Loading branch information
CaseyBatten authored Aug 14, 2024
1 parent 9c3929b commit 225251b
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 26 deletions.
66 changes: 46 additions & 20 deletions api/src/opentrons/protocol_api/instrument_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -2077,6 +2077,8 @@ def configure_nozzle_layout( # noqa: C901
f"Nozzle layout configuration of style {style.value} is unsupported in API Versions lower than {_PARTIAL_NOZZLE_CONFIGURATION_SINGLE_ROW_PARTIAL_COLUMN_ADDED_IN}."
)

front_right_resolved = front_right
back_left_resolved = back_left
if style != NozzleLayout.ALL:
if start is None:
raise ValueError(
Expand All @@ -2086,30 +2088,54 @@ def configure_nozzle_layout( # noqa: C901
raise ValueError(
f"Starting nozzle specified is not one of {types.ALLOWED_PRIMARY_NOZZLES}"
)
if style == NozzleLayout.QUADRANT:
if front_right is None and back_left is None:
raise ValueError(
"Cannot configure a QUADRANT layout without a front right or back left nozzle."
)
elif not (front_right is None and back_left is None):
raise ValueError(
f"Parameters 'front_right' and 'back_left' cannot be used with {style.value} Nozzle Configuration Layout."
)
if style == NozzleLayout.ROW:
if self.channels != 96:
raise ValueError(
"Row configuration is only supported on 96-Channel pipettes."
)
if style == NozzleLayout.COLUMN:
if self.channels != 96:
raise ValueError(
"Column configuration is only supported on 96-Channel pipettes."
)
if style == NozzleLayout.PARTIAL_COLUMN:
if self.channels == 1 or self.channels == 96:
raise ValueError(
"Partial column configuration is only supported on 8-Channel pipettes."
)

front_right_resolved = front_right
back_left_resolved = back_left
if style == NozzleLayout.PARTIAL_COLUMN:
if end is None:
if end is None:
raise ValueError(
"Partial column configurations require the 'end' parameter."
)
if start[0] in end:
raise ValueError(
"The 'start' and 'end' parameters of a partial column configuration cannot be in the same row."
)
# Determine if 'end' will be configured as front_right or back_left
if start == "H1" or start == "H12":
if "A" in end:
raise ValueError(
f"A partial column configuration with 'start'={start} cannot have its 'end' parameter be in row A. Use `ALL` configuration to utilize all nozzles."
)
back_left_resolved = end
elif start == "A1" or start == "A12":
if "H" in end:
raise ValueError(
f"A partial column configuration with 'start'={start} cannot have its 'end' parameter be in row H. Use `ALL` configuration to utilize all nozzles."
)
front_right_resolved = end

if style == NozzleLayout.QUADRANT:
if front_right is None and back_left is None:
raise ValueError(
"Cannot configure a QUADRANT layout without a front right or back left nozzle."
)
elif not (front_right is None and back_left is None):
raise ValueError(
"Parameter 'end' is required for Partial Column Nozzle Configuration Layout."
f"Parameters 'front_right' and 'back_left' cannot be used with a {style.value} nozzle configuration."
)

# Determine if 'end' will be configured as front_right or back_left
if start == "H1" or start == "H12":
back_left_resolved = end
elif start == "A1" or start == "A12":
front_right_resolved = end

self._core.configure_nozzle_layout(
style,
primary_nozzle=start,
Expand Down
59 changes: 53 additions & 6 deletions api/tests/opentrons/protocol_api/test_instrument_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -1128,24 +1128,71 @@ def test_prepare_to_aspirate_checks_volume(


@pytest.mark.parametrize(
argnames=["style", "primary_nozzle", "front_right_nozzle", "end", "exception"],
argnames=[
"pipette_channels",
"style",
"primary_nozzle",
"front_right_nozzle",
"end",
"exception",
],
argvalues=[
[NozzleLayout.COLUMN, "A1", None, None, does_not_raise()],
[NozzleLayout.SINGLE, None, None, None, pytest.raises(ValueError)],
[NozzleLayout.ROW, "E1", None, None, pytest.raises(ValueError)],
[NozzleLayout.PARTIAL_COLUMN, "H1", None, "G1", does_not_raise()],
[NozzleLayout.PARTIAL_COLUMN, "H1", "H1", "G1", pytest.raises(ValueError)],
[96, NozzleLayout.COLUMN, "A1", None, None, does_not_raise()],
[96, NozzleLayout.SINGLE, None, None, None, pytest.raises(ValueError)],
[96, NozzleLayout.ROW, "E1", None, None, pytest.raises(ValueError)],
[8, NozzleLayout.PARTIAL_COLUMN, "H1", None, "G1", does_not_raise()],
[8, NozzleLayout.PARTIAL_COLUMN, "H1", "H1", "G1", pytest.raises(ValueError)],
[8, NozzleLayout.PARTIAL_COLUMN, "H1", None, "A1", pytest.raises(ValueError)],
],
)
def test_configure_nozzle_layout(
subject: InstrumentContext,
decoy: Decoy,
mock_instrument_core: InstrumentCore,
pipette_channels: int,
style: NozzleLayout,
primary_nozzle: Optional[str],
front_right_nozzle: Optional[str],
end: Optional[str],
exception: ContextManager[None],
) -> None:
"""The correct model is passed to the engine client."""
decoy.when(mock_instrument_core.get_channels()).then_return(pipette_channels)
with exception:
subject.configure_nozzle_layout(
style=style, start=primary_nozzle, end=end, front_right=front_right_nozzle
)


@pytest.mark.parametrize(
argnames=[
"pipette_channels",
"style",
"primary_nozzle",
"front_right_nozzle",
"end",
"exception",
],
argvalues=[
[8, NozzleLayout.PARTIAL_COLUMN, "A1", None, "G1", does_not_raise()],
[96, NozzleLayout.PARTIAL_COLUMN, "H1", None, "G1", pytest.raises(ValueError)],
[8, NozzleLayout.ROW, "H1", None, None, pytest.raises(ValueError)],
[96, NozzleLayout.ROW, "H1", None, None, does_not_raise()],
],
)
def test_pipette_supports_nozzle_layout(
subject: InstrumentContext,
decoy: Decoy,
mock_instrument_core: InstrumentCore,
pipette_channels: int,
style: NozzleLayout,
primary_nozzle: Optional[str],
front_right_nozzle: Optional[str],
end: Optional[str],
exception: ContextManager[None],
) -> None:
"""Test that error is raised when a pipette attempts to use an unsupported layout."""
decoy.when(mock_instrument_core.get_channels()).then_return(pipette_channels)
with exception:
subject.configure_nozzle_layout(
style=style, start=primary_nozzle, end=end, front_right=front_right_nozzle
Expand Down

0 comments on commit 225251b

Please sign in to comment.