diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 0357adfbfb5..880626b53c9 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -217,9 +217,9 @@ def aspirate( ) ) - is_meniscus: Optional[bool] = None - well: Optional[labware.Well] = None move_to_location: types.Location + well: Optional[labware.Well] = None + is_meniscus: Optional[bool] = None last_location = self._get_last_location_by_api_version() try: target = validation.validate_location( @@ -233,18 +233,13 @@ def aspirate( "knows where it is." ) from e - if isinstance(target, validation.WellTarget): - move_to_location = target.location or target.well.bottom( - z=self._well_bottom_clearances.aspirate - ) - well = target.well - is_meniscus = target.is_meniscus - if isinstance(target, validation.PointTarget): - move_to_location = target.location if isinstance(target, (TrashBin, WasteChute)): raise ValueError( "Trash Bin and Waste Chute are not acceptable location parameters for Aspirate commands." ) + move_to_location, well, is_meniscus = self._handle_aspirate_target( + target=target + ) if self.api_version >= APIVersion(2, 11): instrument.validate_takes_liquid( location=move_to_location, @@ -284,7 +279,7 @@ def aspirate( rate=rate, flow_rate=flow_rate, in_place=target.in_place, - is_meniscus=is_meniscus if is_meniscus is not None else None, + is_meniscus=is_meniscus, ) return self @@ -406,7 +401,7 @@ def dispense( # noqa: C901 well = target.well if target.location: move_to_location = target.location - is_meniscus = target.is_meniscus + is_meniscus = target.location.is_meniscus elif well.parent._core.is_fixed_trash(): move_to_location = target.well.top() else: @@ -451,7 +446,6 @@ def dispense( # noqa: C901 flow_rate=flow_rate, in_place=False, push_out=push_out, - is_meniscus=is_meniscus if is_meniscus is not None else None, ) return self @@ -473,6 +467,7 @@ def dispense( # noqa: C901 flow_rate=flow_rate, in_place=target.in_place, push_out=push_out, + is_meniscus=is_meniscus, ) return self @@ -2197,6 +2192,26 @@ def _raise_if_configuration_not_supported_by_pipette( ) # SINGLE, QUADRANT and ALL are supported by all pipettes + def _handle_aspirate_target( + self, target: validation.ValidTarget + ) -> tuple[types.Location, Optional[labware.Well], Optional[bool]]: + move_to_location: types.Location + well: Optional[labware.Well] = None + is_meniscus: Optional[bool] = None + if isinstance(target, validation.WellTarget): + well = target.well + if target.location: + move_to_location = target.location + is_meniscus = target.location.is_meniscus + + else: + move_to_location = target.well.bottom( + z=self._well_bottom_clearances.aspirate + ) + if isinstance(target, validation.PointTarget): + move_to_location = target.location + return (move_to_location, well, is_meniscus) + class AutoProbeDisable: """Use this class to temporarily disable automatic liquid presence detection.""" diff --git a/api/src/opentrons/protocol_api/validation.py b/api/src/opentrons/protocol_api/validation.py index 79e895424c5..dc12165dced 100644 --- a/api/src/opentrons/protocol_api/validation.py +++ b/api/src/opentrons/protocol_api/validation.py @@ -418,7 +418,6 @@ class WellTarget(NamedTuple): well: Well location: Optional[Location] in_place: bool - is_meniscus: Optional[bool] = None class PointTarget(NamedTuple): @@ -436,10 +435,13 @@ class LocationTypeError(TypeError): """Error representing that the location supplied is of different expected type.""" +ValidTarget = Union[WellTarget, PointTarget, TrashBin, WasteChute] + + def validate_location( location: Union[Location, Well, TrashBin, WasteChute, None], last_location: Optional[Location], -) -> Union[WellTarget, PointTarget, TrashBin, WasteChute]: +) -> ValidTarget: """Validate a given location for a liquid handling command. Args: @@ -481,7 +483,6 @@ def validate_location( well=well, location=target_location, in_place=in_place, - is_meniscus=target_location.is_meniscus, ) if well is not None else PointTarget(location=target_location, in_place=in_place) diff --git a/api/src/opentrons/types.py b/api/src/opentrons/types.py index 66e670b0386..bcb0d543d51 100644 --- a/api/src/opentrons/types.py +++ b/api/src/opentrons/types.py @@ -154,6 +154,7 @@ def __eq__(self, other: object) -> bool: isinstance(other, Location) and other._point == self._point and other._labware == self._labware + and other._is_meniscus == self._is_meniscus ) def move(self, point: Point) -> "Location": @@ -179,7 +180,7 @@ def move(self, point: Point) -> "Location": return Location(point=self.point + point, labware=self._given_labware) def __repr__(self) -> str: - return f"Location(point={repr(self._point)}, labware={self._labware})" + return f"Location(point={repr(self._point)}, labware={self._labware}, is_meniscus={self._is_meniscus if self._is_meniscus is not None else False})" # TODO(mc, 2020-10-22): use MountType implementation for Mount diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index e4960d2c26b..0697b2ddc8a 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -402,11 +402,7 @@ def test_aspirate_meniscus_well_location( mock_validation.validate_location( location=input_location, last_location=last_location ) - ).then_return( - WellTarget( - well=mock_well, location=input_location, in_place=False, is_meniscus=True - ) - ) + ).then_return(WellTarget(well=mock_well, location=input_location, in_place=False)) decoy.when(mock_instrument_core.get_aspirate_flow_rate(1.23)).then_return(5.67) subject.aspirate(volume=42.0, location=input_location, rate=1.23) @@ -970,6 +966,7 @@ def test_dispense_with_location( rate=1.23, flow_rate=5.67, push_out=None, + is_meniscus=None, ), times=1, ) @@ -1008,6 +1005,7 @@ def test_dispense_with_well_location( rate=1.23, flow_rate=3.0, push_out=7, + is_meniscus=None, ), times=1, ) @@ -1048,6 +1046,7 @@ def test_dispense_with_well( rate=1.23, flow_rate=5.67, push_out=None, + is_meniscus=None, ), times=1, ) @@ -1302,6 +1301,7 @@ def test_dispense_0_volume_means_dispense_everything( rate=1.23, flow_rate=5.67, push_out=None, + is_meniscus=None, ), times=1, ) @@ -1331,6 +1331,7 @@ def test_dispense_0_volume_means_dispense_nothing( rate=1.23, flow_rate=5.67, push_out=None, + is_meniscus=None, ), times=1, ) diff --git a/api/tests/opentrons/protocol_api/test_validation.py b/api/tests/opentrons/protocol_api/test_validation.py index 969a66accf2..2a2ed6375b0 100644 --- a/api/tests/opentrons/protocol_api/test_validation.py +++ b/api/tests/opentrons/protocol_api/test_validation.py @@ -489,21 +489,6 @@ def test_validate_location_with_well(decoy: Decoy) -> None: assert result == expected_result -def test_validate_location_with_meniscus_well(decoy: Decoy) -> None: - """Should return a WellTarget with location.""" - mock_well = decoy.mock(cls=Well) - input_location = Location( - point=Point(x=1, y=1, z=1), labware=mock_well, is_meniscus=True - ) - expected_result = subject.WellTarget( - well=mock_well, location=input_location, in_place=False, is_meniscus=True - ) - - result = subject.validate_location(location=input_location, last_location=None) - - assert result == expected_result - - def test_validate_last_location(decoy: Decoy) -> None: """Should return a WellTarget with location.""" mock_well = decoy.mock(cls=Well) diff --git a/api/tests/opentrons/test_types.py b/api/tests/opentrons/test_types.py index 6cd93dce125..77249fa0492 100644 --- a/api/tests/opentrons/test_types.py +++ b/api/tests/opentrons/test_types.py @@ -29,7 +29,7 @@ def test_location_repr_labware(min_lw: Labware) -> None: loc = Location(point=Point(x=1.1, y=2.1, z=3.5), labware=min_lw) assert ( f"{loc}" - == "Location(point=Point(x=1.1, y=2.1, z=3.5), labware=minimal labware on deck)" + == "Location(point=Point(x=1.1, y=2.1, z=3.5), labware=minimal labware on deck, is_meniscus=False)" ) @@ -38,14 +38,17 @@ def test_location_repr_well(min_lw: Labware) -> None: loc = Location(point=Point(x=1, y=2, z=3), labware=min_lw.wells()[0]) assert ( f"{loc}" - == "Location(point=Point(x=1, y=2, z=3), labware=A1 of minimal labware on deck)" + == "Location(point=Point(x=1, y=2, z=3), labware=A1 of minimal labware on deck, is_meniscus=False)" ) def test_location_repr_slot() -> None: """It should represent labware as a slot""" loc = Location(point=Point(x=-1, y=2, z=3), labware="1") - assert f"{loc}" == "Location(point=Point(x=-1, y=2, z=3), labware=1)" + assert ( + f"{loc}" + == "Location(point=Point(x=-1, y=2, z=3), labware=1, is_meniscus=False)" + ) @pytest.mark.parametrize(