diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index c39a4aba2ac..65d6cc63ff8 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -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( @@ -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, diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 0e85082c3e2..17d8a4e4004 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -1128,17 +1128,28 @@ 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], @@ -1146,6 +1157,42 @@ def test_configure_nozzle_layout( 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