From c9da1b6f3883b5dab0d2ebd1a8b027dafeca437c Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Thu, 19 Sep 2024 17:09:40 -0400 Subject: [PATCH 1/8] don't lpd between mixes --- api/src/opentrons/protocol_api/instrument_context.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index e11ffcc78c2..84ec8873e6f 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -540,12 +540,14 @@ def mix( ), ): self.aspirate(volume, location, rate) + auto_presence = self.liquid_presence_detection + self.liquid_presence_detection = False while repetitions - 1 > 0: self.dispense(volume, rate=rate, **dispense_kwargs) self.aspirate(volume, rate=rate) repetitions -= 1 self.dispense(volume, rate=rate) - + self.liquid_presence_detection = auto_presence return self @requires_version(2, 0) From 8a7c9e934bd76c722d84f988b00d4a8f440057fd Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Thu, 19 Sep 2024 17:20:35 -0400 Subject: [PATCH 2/8] only do the toggle on > 2.20 --- api/src/opentrons/protocol_api/instrument_context.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 84ec8873e6f..153e6688e8e 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -540,14 +540,16 @@ def mix( ), ): self.aspirate(volume, location, rate) - auto_presence = self.liquid_presence_detection - self.liquid_presence_detection = False + if self.api_version >= APIVersion(2, 20): + auto_presence = self.liquid_presence_detection + self.liquid_presence_detection = False while repetitions - 1 > 0: self.dispense(volume, rate=rate, **dispense_kwargs) self.aspirate(volume, rate=rate) repetitions -= 1 self.dispense(volume, rate=rate) - self.liquid_presence_detection = auto_presence + if self.api_version >= APIVersion(2, 20): + self.liquid_presence_detection = auto_presence return self @requires_version(2, 0) From 24d61e8a6754164f644cc0f5ed1e218dfd0bdf5f Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Mon, 23 Sep 2024 11:19:01 -0400 Subject: [PATCH 3/8] switch to context manager --- .../protocol_api/instrument_context.py | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 153e6688e8e..1bef8324e1e 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -540,16 +540,12 @@ def mix( ), ): self.aspirate(volume, location, rate) - if self.api_version >= APIVersion(2, 20): - auto_presence = self.liquid_presence_detection - self.liquid_presence_detection = False - while repetitions - 1 > 0: - self.dispense(volume, rate=rate, **dispense_kwargs) - self.aspirate(volume, rate=rate) - repetitions -= 1 - self.dispense(volume, rate=rate) - if self.api_version >= APIVersion(2, 20): - self.liquid_presence_detection = auto_presence + with AutoProbeDisable(self): + while repetitions - 1 > 0: + self.dispense(volume, rate=rate, **dispense_kwargs) + self.aspirate(volume, rate=rate) + repetitions -= 1 + self.dispense(volume, rate=rate) return self @requires_version(2, 0) @@ -2196,6 +2192,22 @@ def _raise_if_configuration_not_supported_by_pipette( # SINGLE, QUADRANT and ALL are supported by all pipettes +class AutoProbeDisable: + """Use this class to temporarily disable automatic liquid presence detection.""" + + def __init__(self, instrument: InstrumentContext): + self.instrument = instrument + + def __enter__(self) -> None: + if self.instrument.api_version >= APIVersion(2, 20): + self.auto_presence = self.instrument.liquid_presence_detection + self.instrument.liquid_presence_detection = False + + def __exit__(self, *args: Any, **kwargs: Any) -> None: + if self.instrument.api_version >= APIVersion(2, 20): + self.instrument.liquid_presence_detection = self.auto_presence + + def _raise_if_has_end_or_front_right_or_back_left( style: NozzleLayout, end: Optional[str], From 8bea1143ed98f8530c883ba3e6e16243a03c9977 Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Tue, 24 Sep 2024 14:50:43 -0400 Subject: [PATCH 4/8] up the api version --- api/src/opentrons/protocol_api/instrument_context.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 1bef8324e1e..b158ff8c75f 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -2199,12 +2199,12 @@ def __init__(self, instrument: InstrumentContext): self.instrument = instrument def __enter__(self) -> None: - if self.instrument.api_version >= APIVersion(2, 20): + if self.instrument.api_version >= APIVersion(2, 21): self.auto_presence = self.instrument.liquid_presence_detection self.instrument.liquid_presence_detection = False def __exit__(self, *args: Any, **kwargs: Any) -> None: - if self.instrument.api_version >= APIVersion(2, 20): + if self.instrument.api_version >= APIVersion(2, 21): self.instrument.liquid_presence_detection = self.auto_presence From 98a0639c57f142b0c6194258d4318bc469773ed6 Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Fri, 27 Sep 2024 11:03:27 -0400 Subject: [PATCH 5/8] add tests for mix with and without lpd --- .../protocol_api/test_instrument_context.py | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 3478ceb9a86..60f2dd2d5dc 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -89,6 +89,18 @@ def mock_instrument_core(decoy: Decoy) -> InstrumentCore: """Get a mock instrument implementation core.""" instrument_core = decoy.mock(cls=InstrumentCore) decoy.when(instrument_core.get_mount()).then_return(Mount.LEFT) + + # we need to add this for the mock of liquid_presence detection to actually work + # this replaces the mock with a a property again + def _setter(enable) -> None: + instrument_core._liquid_presence_detection = enable + def _getter() -> bool: + return instrument_core._liquid_presence_detection + + instrument_core._liquid_presence_detection = False + instrument_core.get_liquid_presence_detection = _getter + instrument_core.set_liquid_presence_detection = _setter + return instrument_core @@ -1476,3 +1488,105 @@ def test_96_tip_config_invalid( decoy.when(mock_instrument_core.get_nozzle_map()).then_return(nozzle_map) decoy.when(mock_instrument_core.get_active_channels()).then_return(96) assert subject._96_tip_config_valid() is True + + +@pytest.mark.parametrize("api_version", [APIVersion(2, 21)]) +def test_mix_no_lpd( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + subject: InstrumentContext, + mock_protocol_core: ProtocolCore, +) -> None: + """It should aspirate/dispense to a well several times.""" + mock_well = decoy.mock(cls=Well) + + bottom_location = Location(point=Point(1, 2, 3), labware=mock_well) + input_location = Location(point=Point(2, 2, 2), labware=None) + last_location = Location(point=Point(9, 9, 9), labware=None) + + decoy.when(mock_protocol_core.get_last_location(Mount.LEFT)).then_return( + last_location + ) + decoy.when( + mock_validation.validate_location( + location=input_location, last_location=last_location + ) + ).then_return(WellTarget(well=mock_well, location=None, in_place=False)) + decoy.when( + mock_validation.validate_location( + location=None, last_location=last_location + ) + ).then_return(WellTarget(well=mock_well, location=None, in_place=False)) + decoy.when(mock_well.bottom(z=1.0)).then_return(bottom_location) + decoy.when(mock_instrument_core.get_aspirate_flow_rate(1.23)).then_return(5.67) + decoy.when(mock_instrument_core.get_dispense_flow_rate(1.23)).then_return(5.67) + decoy.when(mock_instrument_core.has_tip()).then_return(True) + + subject.mix(repetitions=10, volume=10.0, location=input_location, rate=1.23) + decoy.verify( + mock_instrument_core.aspirate(), + ignore_extra_args=True, + times=10, + ) + decoy.verify( + mock_instrument_core.dispense(), + ignore_extra_args=True, + times=10, + ) + + decoy.verify( + mock_instrument_core.liquid_probe_with_recovery(), + ignore_extra_args=True, + times=0, + ) + + +@pytest.mark.parametrize("api_version", [APIVersion(2, 21)]) +def test_mix_with_lpd( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + subject: InstrumentContext, + mock_protocol_core: ProtocolCore, +) -> None: + """It should aspirate/dispense to a well several times and do 1 lpd.""" + mock_well = decoy.mock(cls=Well) + bottom_location = Location(point=Point(1, 2, 3), labware=mock_well) + input_location = Location(point=Point(2, 2, 2), labware=None) + last_location = Location(point=Point(9, 9, 9), labware=None) + + decoy.when(mock_protocol_core.get_last_location(Mount.LEFT)).then_return( + last_location + ) + decoy.when( + mock_validation.validate_location( + location=input_location, last_location=last_location + ) + ).then_return(WellTarget(well=mock_well, location=None, in_place=False)) + decoy.when( + mock_validation.validate_location( + location=None, last_location=last_location + ) + ).then_return(WellTarget(well=mock_well, location=None, in_place=False)) + decoy.when(mock_well.bottom(z=1.0)).then_return(bottom_location) + decoy.when(mock_instrument_core.get_aspirate_flow_rate(1.23)).then_return(5.67) + decoy.when(mock_instrument_core.get_dispense_flow_rate(1.23)).then_return(5.67) + decoy.when(mock_instrument_core.has_tip()).then_return(True) + + subject.liquid_presence_detection = True + subject.mix(repetitions=10, volume=10.0, location=input_location, rate=1.23) + decoy.verify( + mock_instrument_core.aspirate(), + ignore_extra_args=True, + times=10, + ) + decoy.verify( + mock_instrument_core.dispense(), + ignore_extra_args=True, + times=10, + ) + + decoy.verify( + mock_instrument_core.liquid_probe_with_recovery(), + ignore_extra_args=True, + times=1, + ) From edb70a616a9f24f71735bbbc42e38589ec92e406 Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Fri, 27 Sep 2024 11:09:55 -0400 Subject: [PATCH 6/8] format lint --- .../protocol_api/test_instrument_context.py | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 60f2dd2d5dc..5f7c33c3149 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -92,14 +92,16 @@ def mock_instrument_core(decoy: Decoy) -> InstrumentCore: # we need to add this for the mock of liquid_presence detection to actually work # this replaces the mock with a a property again - def _setter(enable) -> None: - instrument_core._liquid_presence_detection = enable + instrument_core._liquid_presence_detection = False # type: ignore[attr-defined] + + def _setter(enable: bool) -> None: + instrument_core._liquid_presence_detection = enable # type: ignore[attr-defined] + def _getter() -> bool: - return instrument_core._liquid_presence_detection + return instrument_core._liquid_presence_detection # type: ignore[attr-defined, no-any-return] - instrument_core._liquid_presence_detection = False - instrument_core.get_liquid_presence_detection = _getter - instrument_core.set_liquid_presence_detection = _setter + instrument_core.get_liquid_presence_detection = _getter # type: ignore[method-assign] + instrument_core.set_liquid_presence_detection = _setter # type: ignore[method-assign] return instrument_core @@ -1513,9 +1515,7 @@ def test_mix_no_lpd( ) ).then_return(WellTarget(well=mock_well, location=None, in_place=False)) decoy.when( - mock_validation.validate_location( - location=None, last_location=last_location - ) + mock_validation.validate_location(location=None, last_location=last_location) ).then_return(WellTarget(well=mock_well, location=None, in_place=False)) decoy.when(mock_well.bottom(z=1.0)).then_return(bottom_location) decoy.when(mock_instrument_core.get_aspirate_flow_rate(1.23)).then_return(5.67) @@ -1524,18 +1524,18 @@ def test_mix_no_lpd( subject.mix(repetitions=10, volume=10.0, location=input_location, rate=1.23) decoy.verify( - mock_instrument_core.aspirate(), + mock_instrument_core.aspirate(), # type: ignore[call-arg] ignore_extra_args=True, times=10, ) decoy.verify( - mock_instrument_core.dispense(), + mock_instrument_core.dispense(), # type: ignore[call-arg] ignore_extra_args=True, times=10, ) decoy.verify( - mock_instrument_core.liquid_probe_with_recovery(), + mock_instrument_core.liquid_probe_with_recovery(), # type: ignore[call-arg] ignore_extra_args=True, times=0, ) @@ -1563,9 +1563,7 @@ def test_mix_with_lpd( ) ).then_return(WellTarget(well=mock_well, location=None, in_place=False)) decoy.when( - mock_validation.validate_location( - location=None, last_location=last_location - ) + mock_validation.validate_location(location=None, last_location=last_location) ).then_return(WellTarget(well=mock_well, location=None, in_place=False)) decoy.when(mock_well.bottom(z=1.0)).then_return(bottom_location) decoy.when(mock_instrument_core.get_aspirate_flow_rate(1.23)).then_return(5.67) @@ -1575,18 +1573,18 @@ def test_mix_with_lpd( subject.liquid_presence_detection = True subject.mix(repetitions=10, volume=10.0, location=input_location, rate=1.23) decoy.verify( - mock_instrument_core.aspirate(), + mock_instrument_core.aspirate(), # type: ignore[call-arg] ignore_extra_args=True, times=10, ) decoy.verify( - mock_instrument_core.dispense(), + mock_instrument_core.dispense(), # type: ignore[call-arg] ignore_extra_args=True, times=10, ) decoy.verify( - mock_instrument_core.liquid_probe_with_recovery(), + mock_instrument_core.liquid_probe_with_recovery(), # type: ignore[call-arg] ignore_extra_args=True, times=1, ) From 3878ce57cc3976550639f5723958704648f21f54 Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Fri, 27 Sep 2024 11:34:26 -0400 Subject: [PATCH 7/8] dont test lpd on ot2 --- api/tests/opentrons/protocol_api/test_instrument_context.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 5f7c33c3149..4e4a6fa09d0 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -1541,6 +1541,7 @@ def test_mix_no_lpd( ) +@pytest.mark.ot3_only @pytest.mark.parametrize("api_version", [APIVersion(2, 21)]) def test_mix_with_lpd( decoy: Decoy, From 7b3611abca4a96b4c34168123533d0d8eab042a4 Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Tue, 1 Oct 2024 11:45:27 -0400 Subject: [PATCH 8/8] fix the tests post mergeback --- api/tests/opentrons/protocol_api/test_instrument_context.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 4e4a6fa09d0..4478c250b8c 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -1521,6 +1521,7 @@ def test_mix_no_lpd( decoy.when(mock_instrument_core.get_aspirate_flow_rate(1.23)).then_return(5.67) decoy.when(mock_instrument_core.get_dispense_flow_rate(1.23)).then_return(5.67) decoy.when(mock_instrument_core.has_tip()).then_return(True) + decoy.when(mock_instrument_core.get_current_volume()).then_return(0.0) subject.mix(repetitions=10, volume=10.0, location=input_location, rate=1.23) decoy.verify( @@ -1570,6 +1571,7 @@ def test_mix_with_lpd( decoy.when(mock_instrument_core.get_aspirate_flow_rate(1.23)).then_return(5.67) decoy.when(mock_instrument_core.get_dispense_flow_rate(1.23)).then_return(5.67) decoy.when(mock_instrument_core.has_tip()).then_return(True) + decoy.when(mock_instrument_core.get_current_volume()).then_return(0.0) subject.liquid_presence_detection = True subject.mix(repetitions=10, volume=10.0, location=input_location, rate=1.23)