Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(api): don't lpd for each step in a mix #16310

Merged
merged 8 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
)
Loading