From 14ef8840ad7df6e7ef71302532c17bd57d06146d Mon Sep 17 00:00:00 2001 From: CaseyBatten Date: Fri, 9 Aug 2024 17:08:22 -0400 Subject: [PATCH 01/10] error casing and tests for unsupported pipette configuration behavior --- .../protocol_api/instrument_context.py | 61 +++++++++++++------ .../protocol_api/test_instrument_context.py | 36 +++++++++++ 2 files changed, 77 insertions(+), 20 deletions(-) diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index c39a4aba2ac..4cf83593a1e 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,49 @@ 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 configuraiton is only supported on 96-Channel Pipettes." + ) + if style == NozzleLayout.PARTIAL_COLUMN: + if self.channels == 1 or self.channels == 96: + raise ValueError( + "Partial Column configuraiton 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( + "Parameter 'end' is required for Partial Column Nozzle Configuration Layout." + ) + if start[0] in end: + raise ValueError( + "When configuring in Partial Column the 'start' and 'end' parameters 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"When configuring in Partial Column with 'start'={start} the 'end' parameter cannot be in row A." + ) + back_left_resolved = end + elif start == "A1" or start == "A12": + if "H" in end: + raise ValueError( + f"When configuring in Partial Column with 'start'={start} the 'end' parameter cannot be in row H." + ) + 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 {style.value} Nozzle Configuration Layout." ) - # 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..1b3569e07d8 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -1135,6 +1135,7 @@ def test_prepare_to_aspirate_checks_volume( [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)], + [NozzleLayout.PARTIAL_COLUMN, "H1", None, "A1", pytest.raises(ValueError)], ], ) def test_configure_nozzle_layout( @@ -1152,6 +1153,41 @@ def test_configure_nozzle_layout( ) +@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 + ) + + @pytest.mark.parametrize("api_version", [APIVersion(2, 15)]) def test_dispense_0_volume_means_dispense_everything( decoy: Decoy, From ed45146dbc0e4db2da1cde982d6e4dec01ee8eb0 Mon Sep 17 00:00:00 2001 From: CaseyBatten Date: Tue, 13 Aug 2024 16:29:29 -0400 Subject: [PATCH 02/10] Update api/src/opentrons/protocol_api/instrument_context.py Co-authored-by: Ed Cormany --- api/src/opentrons/protocol_api/instrument_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 4cf83593a1e..a9f075d133c 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -2091,7 +2091,7 @@ def configure_nozzle_layout( # noqa: C901 if style == NozzleLayout.ROW: if self.channels != 96: raise ValueError( - "Row configuraiton is only supported on 96-Channel Pipettes." + "Row configuration is only supported on 96-Channel pipettes." ) if style == NozzleLayout.PARTIAL_COLUMN: if self.channels == 1 or self.channels == 96: From 0302aa7ba57ae711b3518f81df3ae1af90141e02 Mon Sep 17 00:00:00 2001 From: CaseyBatten Date: Tue, 13 Aug 2024 16:29:38 -0400 Subject: [PATCH 03/10] Update api/src/opentrons/protocol_api/instrument_context.py Co-authored-by: Ed Cormany --- api/src/opentrons/protocol_api/instrument_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index a9f075d133c..e2ca1f2aed6 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -2096,7 +2096,7 @@ def configure_nozzle_layout( # noqa: C901 if style == NozzleLayout.PARTIAL_COLUMN: if self.channels == 1 or self.channels == 96: raise ValueError( - "Partial Column configuraiton is only supported on 8-Channel Pipettes." + "Partial column configuration is only supported on 8-Channel pipettes." ) if end is None: From 900b9a63666ffb81bc47e6adcfb81e596eb83e9f Mon Sep 17 00:00:00 2001 From: CaseyBatten Date: Tue, 13 Aug 2024 16:29:49 -0400 Subject: [PATCH 04/10] Update api/src/opentrons/protocol_api/instrument_context.py Co-authored-by: Ed Cormany --- api/src/opentrons/protocol_api/instrument_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index e2ca1f2aed6..dbc8ed70680 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -2105,7 +2105,7 @@ def configure_nozzle_layout( # noqa: C901 ) if start[0] in end: raise ValueError( - "When configuring in Partial Column the 'start' and 'end' parameters cannot be in the same row." + "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": From 5b6b6e7e885f41b991ba7428abff80ed479e061e Mon Sep 17 00:00:00 2001 From: CaseyBatten Date: Tue, 13 Aug 2024 16:30:09 -0400 Subject: [PATCH 05/10] Update api/src/opentrons/protocol_api/instrument_context.py Co-authored-by: Ed Cormany --- api/src/opentrons/protocol_api/instrument_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index dbc8ed70680..430f72aeb2e 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -2101,7 +2101,7 @@ def configure_nozzle_layout( # noqa: C901 if end is None: raise ValueError( - "Parameter 'end' is required for Partial Column Nozzle Configuration Layout." + "Partial column configurations require the 'end' parameter." ) if start[0] in end: raise ValueError( From 95fb706cf9f3e74acafe9b17a87628f9340ab342 Mon Sep 17 00:00:00 2001 From: CaseyBatten Date: Tue, 13 Aug 2024 16:30:24 -0400 Subject: [PATCH 06/10] Update api/src/opentrons/protocol_api/instrument_context.py Co-authored-by: Ed Cormany --- api/src/opentrons/protocol_api/instrument_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 430f72aeb2e..55c8e417c56 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -2111,7 +2111,7 @@ def configure_nozzle_layout( # noqa: C901 if start == "H1" or start == "H12": if "A" in end: raise ValueError( - f"When configuring in Partial Column with 'start'={start} the 'end' parameter cannot be in row A." + f"A partial column configuration with 'start'={start} cannot have its 'end' parameter be in row A." ) back_left_resolved = end elif start == "A1" or start == "A12": From a1952c2e175fe6e77d5a1c29b6e8fbd677658277 Mon Sep 17 00:00:00 2001 From: CaseyBatten Date: Tue, 13 Aug 2024 16:30:31 -0400 Subject: [PATCH 07/10] Update api/src/opentrons/protocol_api/instrument_context.py Co-authored-by: Ed Cormany --- api/src/opentrons/protocol_api/instrument_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 55c8e417c56..b36e0cb01d9 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -2117,7 +2117,7 @@ def configure_nozzle_layout( # noqa: C901 elif start == "A1" or start == "A12": if "H" in end: raise ValueError( - f"When configuring in Partial Column with 'start'={start} the 'end' parameter cannot be in row H." + f"A partial column configuration with 'start'={start} cannot have its 'end' parameter be in row H." ) front_right_resolved = end From 165a8c63717c7f2ad1df5138007f2c213a7df295 Mon Sep 17 00:00:00 2001 From: CaseyBatten Date: Tue, 13 Aug 2024 16:30:54 -0400 Subject: [PATCH 08/10] Update api/src/opentrons/protocol_api/instrument_context.py Co-authored-by: Ed Cormany --- api/src/opentrons/protocol_api/instrument_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index b36e0cb01d9..240d5130e1f 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -2128,7 +2128,7 @@ def configure_nozzle_layout( # noqa: C901 ) 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." + f"Parameters 'front_right' and 'back_left' cannot be used with a {style.value} nozzle configuration." ) self._core.configure_nozzle_layout( From e92429d5bc58b809405d491d7814203c70351591 Mon Sep 17 00:00:00 2001 From: CaseyBatten Date: Wed, 14 Aug 2024 17:52:28 -0400 Subject: [PATCH 09/10] account for COLUMN configuration on 8ch --- api/src/opentrons/protocol_api/instrument_context.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 240d5130e1f..65d6cc63ff8 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -2093,6 +2093,11 @@ def configure_nozzle_layout( # noqa: C901 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( @@ -2111,13 +2116,13 @@ def configure_nozzle_layout( # noqa: C901 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." + 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." + 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 From d1970573c732c9ec68edf53033719210e23f357b Mon Sep 17 00:00:00 2001 From: CaseyBatten Date: Wed, 14 Aug 2024 17:58:32 -0400 Subject: [PATCH 10/10] test corrections --- .../protocol_api/test_instrument_context.py | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 1b3569e07d8..17d8a4e4004 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -1128,18 +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)], - [NozzleLayout.PARTIAL_COLUMN, "H1", None, "A1", 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], @@ -1147,6 +1157,7 @@ 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