Skip to content

Commit

Permalink
fix(api): don't lpd for each step in a mix (#16310)
Browse files Browse the repository at this point in the history
<!--
Thanks for taking the time to open a Pull Request (PR)! Please make sure
you've read the "Opening Pull Requests" section of our Contributing
Guide:


https://github.com/Opentrons/opentrons/blob/edge/CONTRIBUTING.md#opening-pull-requests

GitHub provides robust markdown to format your PR. Links, diagrams,
pictures, and videos along with text formatting make it possible to
create a rich and informative PR. For more information on GitHub
markdown, see:


https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax

To ensure your code is reviewed quickly and thoroughly, please fill out
the sections below to the best of your ability!
-->

# Overview
If an instrument is set to have Liquid Presence Detection on and their
is a call to instrument.mix then the pipette will exit the liquid, do a
LPD and then re-enter for each cycle. This change makes it so that it
saves the state of the instrument, does LPD on the first aspirate and
then turns LPD off. Once Mix is complete then LPD is toggled back to
whatever state it was in before.
<!--
Describe your PR at a high level. State acceptance criteria and how this
PR fits into other work. Link issues, PRs, and other relevant resources.
-->

## Test Plan and Hands on Testing

<!--
Describe your testing of the PR. Emphasize testing not reflected in the
code. Attach protocols, logs, screenshots and any other assets that
support your testing.
-->

## Changelog

<!--
List changes introduced by this PR considering future developers and the
end user. Give careful thought and clear documentation to breaking
changes.
-->

## Review requests

<!--
- What do you need from reviewers to feel confident this PR is ready to
merge?
- Ask questions.
-->

## Risk assessment
Low: If an error somehow occurs during the mix cycle and the command is
bypassed then the instrument may be left with LPD off.
<!--
- Indicate the level of attention this PR needs.
- Provide context to guide reviewers.
- Discuss trade-offs, coupling, and side effects.
- Look for the possibility, even if you think it's small, that your
change may affect some other part of the system.
- For instance, changing return tip behavior may also change the
behavior of labware calibration.
- How do your unit tests and on hands on testing mitigate this PR's
risks and the risk of future regressions?
- Especially in high risk PRs, explain how you know your testing is
enough.
-->
  • Loading branch information
ryanthecoder authored Oct 1, 2024
1 parent 79b6d66 commit e799e9b
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 6 deletions.
28 changes: 22 additions & 6 deletions api/src/opentrons/protocol_api/instrument_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,12 +540,12 @@ def mix(
),
):
self.aspirate(volume, location, rate)
while repetitions - 1 > 0:
self.dispense(volume, rate=rate, **dispense_kwargs)
self.aspirate(volume, rate=rate)
repetitions -= 1
self.dispense(volume, rate=rate)

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)
Expand Down Expand Up @@ -2192,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, 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, 21):
self.instrument.liquid_presence_detection = self.auto_presence


def _raise_if_has_end_or_front_right_or_back_left(
style: NozzleLayout,
end: Optional[str],
Expand Down
115 changes: 115 additions & 0 deletions api/tests/opentrons/protocol_api/test_instrument_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,20 @@ 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
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 # type: ignore[attr-defined, no-any-return]

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


Expand Down Expand Up @@ -1476,3 +1490,104 @@ 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)
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(
mock_instrument_core.aspirate(), # type: ignore[call-arg]
ignore_extra_args=True,
times=10,
)
decoy.verify(
mock_instrument_core.dispense(), # type: ignore[call-arg]
ignore_extra_args=True,
times=10,
)

decoy.verify(
mock_instrument_core.liquid_probe_with_recovery(), # type: ignore[call-arg]
ignore_extra_args=True,
times=0,
)


@pytest.mark.ot3_only
@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)
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)
decoy.verify(
mock_instrument_core.aspirate(), # type: ignore[call-arg]
ignore_extra_args=True,
times=10,
)
decoy.verify(
mock_instrument_core.dispense(), # type: ignore[call-arg]
ignore_extra_args=True,
times=10,
)

decoy.verify(
mock_instrument_core.liquid_probe_with_recovery(), # type: ignore[call-arg]
ignore_extra_args=True,
times=1,
)

0 comments on commit e799e9b

Please sign in to comment.