Skip to content

Commit

Permalink
fix(api): throw error if no valid nozzle has tip (#15721)
Browse files Browse the repository at this point in the history
Provide a check in all LLD functions(called manually or automatically
during aspirate) that makes sure that if it's a 96-channel pipette, that
at least one of the nozzles with a pressure sensor has a tip on it.

<!--
Thanks for taking the time to open a pull request! 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

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

# Overview

<!--
Use this section to describe your pull-request at a high level. If the
PR addresses any open issues, please tag the issues here.
-->

# Test Plan

<!--
Use this section to describe the steps that you took to test your Pull
Request.
If you did not perform any testing provide justification why.

OT-3 Developers: You should default to testing on actual physical
hardware.
Once again, if you did not perform testing against hardware, justify
why.

Note: It can be helpful to write a test plan before doing development

Example Test Plan (HTTP API Change)

- Verified that new optional argument `dance-party` causes the robot to
flash its lights, move the pipettes,
then home.
- Verified that when you omit the `dance-party` option the robot homes
normally
- Added protocol that uses `dance-party` argument to G-Code Testing
Suite
- Ran protocol that did not use `dance-party` argument and everything
was successful
- Added unit tests to validate that changes to pydantic model are
correct

-->

# Changelog

<!--
List out the changes to the code in this PR. Please try your best to
categorize your changes and describe what has changed and why.

Example changelog:
- Fixed app crash when trying to calibrate an illegal pipette
- Added state to API to track pipette usage
- Updated API docs to mention only two pipettes are supported

IMPORTANT: MAKE SURE ANY BREAKING CHANGES ARE PROPERLY COMMUNICATED
-->

# Review requests

<!--
Describe any requests for your reviewers here.
-->

# Risk assessment

<!--
Carefully go over your pull request and look at the other parts of the
codebase it may affect. 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 in protocol may also change the
behavior of labware calibration.

Identify the other parts of the system your codebase may affect, so that
in addition to your own review and testing, other people who may not
have the system internalized as much as you can focus their attention
and testing there.
-->
  • Loading branch information
aaron-kulkarni authored Jul 23, 2024
1 parent 8fe39ab commit ea31e34
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 0 deletions.
19 changes: 19 additions & 0 deletions api/src/opentrons/protocol_api/instrument_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging
from contextlib import ExitStack
from typing import Any, List, Optional, Sequence, Union, cast, Dict
from opentrons.protocol_engine.errors.exceptions import TipNotAttachedError
from opentrons_shared_data.errors.exceptions import (
CommandPreconditionViolated,
CommandParameterLimitViolated,
Expand Down Expand Up @@ -264,6 +265,7 @@ def aspirate(
self.api_version >= APIVersion(2, 20)
and well is not None
and self.liquid_presence_detection
and self._96_tip_config_valid()
):
self.require_liquid_presence(well=well)
self.prepare_to_aspirate()
Expand Down Expand Up @@ -1874,6 +1876,19 @@ def _get_last_location_by_api_version(self) -> Optional[types.Location]:
else:
return self._protocol_core.get_last_location()

def _96_tip_config_valid(self) -> bool:
n_map = self._core.get_nozzle_map()
channels = self._core.get_active_channels()
if channels == 96:
if (
n_map.back_left != n_map.full_instrument_back_left
and n_map.front_right != n_map.full_instrument_front_right
):
raise TipNotAttachedError(
"Either the front right or the back left nozzle must have a tip attached to do LLD."
)
return True

def __repr__(self) -> str:
return "<{}: {} in {}>".format(
self.__class__.__name__,
Expand Down Expand Up @@ -2110,6 +2125,7 @@ def detect_liquid_presence(self, well: labware.Well) -> bool:
:returns: A boolean.
"""
loc = well.top()
self._96_tip_config_valid()
return self._core.detect_liquid_presence(well._core, loc)

@requires_version(2, 20)
Expand All @@ -2119,6 +2135,7 @@ def require_liquid_presence(self, well: labware.Well) -> None:
:returns: None.
"""
loc = well.top()
self._96_tip_config_valid()
self._core.liquid_probe_with_recovery(well._core, loc)

@requires_version(2, 20)
Expand All @@ -2131,6 +2148,8 @@ def measure_liquid_height(self, well: labware.Well) -> float:
This is intended for Opentrons internal use only and is not a guaranteed API.
"""

loc = well.top()
self._96_tip_config_valid()
height = self._core.liquid_probe_without_recovery(well._core, loc)
return height
59 changes: 59 additions & 0 deletions api/tests/opentrons/protocol_api/test_instrument_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@

from opentrons.legacy_broker import LegacyBroker

from opentrons.protocol_engine.errors.exceptions import TipNotAttachedError
from tests.opentrons.protocol_engine.pipette_fixtures import (
NINETY_SIX_COLS,
NINETY_SIX_MAP,
NINETY_SIX_ROWS,
)
from opentrons.protocols.api_support import instrument as mock_instrument_support
from opentrons.protocols.api_support.types import APIVersion
from opentrons.protocols.api_support.util import (
Expand Down Expand Up @@ -1351,3 +1357,56 @@ def test_measure_liquid_height(
with pytest.raises(ProtocolCommandFailedError) as pcfe:
subject.measure_liquid_height(mock_well)
assert pcfe.value is errorToRaise


def test_96_tip_config_valid(
decoy: Decoy, mock_instrument_core: InstrumentCore, subject: InstrumentContext
) -> None:
"""It should error when there's no tips on the correct corner nozzles."""
nozzle_map = NozzleMap.build(
physical_nozzles=NINETY_SIX_MAP,
physical_rows=NINETY_SIX_ROWS,
physical_columns=NINETY_SIX_COLS,
starting_nozzle="A5",
back_left_nozzle="A5",
front_right_nozzle="H5",
valid_nozzle_maps=ValidNozzleMaps(maps={"Column12": NINETY_SIX_COLS["5"]}),
)
decoy.when(mock_instrument_core.get_nozzle_map()).then_return(nozzle_map)
decoy.when(mock_instrument_core.get_active_channels()).then_return(96)
with pytest.raises(TipNotAttachedError):
subject._96_tip_config_valid()


def test_96_tip_config_invalid(
decoy: Decoy, mock_instrument_core: InstrumentCore, subject: InstrumentContext
) -> None:
"""It should return True when there are tips on the correct corner nozzles."""
nozzle_map = NozzleMap.build(
physical_nozzles=NINETY_SIX_MAP,
physical_rows=NINETY_SIX_ROWS,
physical_columns=NINETY_SIX_COLS,
starting_nozzle="A1",
back_left_nozzle="A1",
front_right_nozzle="H12",
valid_nozzle_maps=ValidNozzleMaps(
maps={
"Full": sum(
[
NINETY_SIX_ROWS["A"],
NINETY_SIX_ROWS["B"],
NINETY_SIX_ROWS["C"],
NINETY_SIX_ROWS["D"],
NINETY_SIX_ROWS["E"],
NINETY_SIX_ROWS["F"],
NINETY_SIX_ROWS["G"],
NINETY_SIX_ROWS["H"],
],
[],
)
}
),
)
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

0 comments on commit ea31e34

Please sign in to comment.