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

feat(api): create instr ctx methods for checking liquid presence #15555

Merged
merged 15 commits into from
Jul 3, 2024

Conversation

aaron-kulkarni
Copy link
Contributor

@aaron-kulkarni aaron-kulkarni commented Jul 1, 2024

Added detect_liquid and require_liquid to instrument_context. The two methods both check for liquid in a specific well, but one returns true/false and the other raises exceptions.

Also adds measure_liquid_level to instrument context. This method probes the well to find the height of the liquid.
close EXEC-544, EXEC-545, EXEC-546

Overview

Test Plan

Changelog

Review requests

In find_liquid_level in the instrument core, I couldn't find a method that would execute the command with recovery and also return the result of the command. So I settled for execute_command_without_recovery but I'm not sure if that's the right way to go about it. I created a new method called execute_command_with_result to get around this.

Risk assessment

…ence

Added detect_liquid and require_liquid to instrument_context. The two methods both check for liquid
in a specific well, but one returns true/false and the other raises exceptions.

re EXEC-544, re EXEC-545
@aaron-kulkarni aaron-kulkarni requested a review from a team as a code owner July 1, 2024 15:55
@aaron-kulkarni aaron-kulkarni marked this pull request as draft July 1, 2024 15:55
@aaron-kulkarni
Copy link
Contributor Author

still need to test

aaron-kulkarni and others added 3 commits July 1, 2024 12:01
…ailure capture (#15556)

This PR is an automated snapshot update request. Please review the
changes and merge if they are acceptable or find your bug and fix it.

Co-authored-by: aaron-kulkarni <[email protected]>
@Opentrons Opentrons deleted a comment from github-actions bot Jul 1, 2024
@Opentrons Opentrons deleted a comment from github-actions bot Jul 1, 2024
@aaron-kulkarni aaron-kulkarni marked this pull request as ready for review July 1, 2024 18:40
@aaron-kulkarni aaron-kulkarni requested a review from a team as a code owner July 1, 2024 18:40
try:
height = self._core.find_liquid_level(well._core)
return float(height)
except Exception:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should only explicity catch LiquidNotFoundException if we're going to handle them. If there is some other issue that results in an error this could let that go undetected

Copy link
Member

@sfoster1 sfoster1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple changes we should make to the API. Also, if you merge edge into this PR it should get rid of the snapshot problem.

)
if (
result is None
): # this should probably only happen in testing with mock components
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we shouldn't do this. If we're using mock components, those mock components should return data in the same way the real things do even if they get it from somewhere else - this is why we have mocks.

:returns: None.
"""
if well is None:
raise WellDoesNotExistError("Well type was none.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
raise WellDoesNotExistError("Well type was none.")
raise WellDoesNotExistError("You must provide a well to check.")

api/src/opentrons/protocol_api/instrument_context.py Outdated Show resolved Hide resolved
api/src/opentrons/protocol_api/instrument_context.py Outdated Show resolved Hide resolved
:returns: A float representing the height of the liquid.
"""
if well is None:
raise WellDoesNotExistError()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this error should take the same form as the same check in the other methods

height = self._core.find_liquid_level(well._core)
return float(height)
except PipetteLiquidNotFoundError:
return 0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this really an appropriate return value? Remember, this function returns a height in absolute deck coordinates, in which z=0 is the level of the deck. So this function can sometimes return, without indicating an error, that the liquid level is at the height of the deck. That doesn't make much sense to me.

I think this function should just let the PipetteLiquidNotFoundError through.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree we should pass this through. The only api call that shouldn't raise errors is detect_liquid

well_location = WellLocation(
origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=0)
)
result = self._engine_client.execute_command_without_recovery(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
result = self._engine_client.execute_command_without_recovery(
result = self._engine_client.execute_command(

we definitely want to recover from this command

Copy link
Contributor

@SyntaxColoring SyntaxColoring Jul 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the problem here is that one of this method's callers, InstrumentContext.get_liquid_height(), is returning the probed height. And if the liquidProbe command fails and we let the frontend drive error recovery, there isn't a probed height in the general case. The client can choose to skip to the next command without retrying the failed liquidProbe.

We can certainly solve this for InstrumentContext.require_liquid(). Just, yeah, call execute_command() (with recovery) and discard the liquidProbe result. A successful liquidProbe, or a failed-but-recovered liquidProbe, will return from execute_command(). A failed-and-not-recovered liquidProbe will raise from execute_command(), and we should let that propagate up.

But for InstrumentContext.get_liquid_height() and InstrumentContext.detect_liquid_presence(), it's unclear to me what we would even want to happen.

Copy link
Contributor

@SyntaxColoring SyntaxColoring Jul 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recap of in-person discussion. Looks like you've already done some of this by the time I'm posting this comment.

InstrumentContext.require_liquid_presence()

We want this method to give the client the opportunity for error recovery, but raise an exception if that ultimately fails. That's what SyncClient.execute_command(), so under the hood, this should be implemented using that. If that raises an exception, we should just let it propagate.

InstrumentContext.detect_liquid_presence()

We want this method to straight-up return True/False without giving the client the opportunity for error recovery. So, under the hood:

  • We should implement it via SyncClient.execute_command_without_recovery().

  • If the probe detects no liquid, execute_command_without_recovery() will raise that as an exception. We should look for that and translate it into a False return.

    We can catch ProtocolCommandFailedError, and then check if exception.original_error is an instance of LiquidNotFoundError. If so, we should translate it into a False return. Otherwise, we should re-raise the exception to let it propagate.

I think this will expose a bug where the robot will mistakenly go into error recovery mode (because "the command failed") and try to keep running the protocol (because we're catching the exception, translating it to False, and letting the protocol continue). We can take that on in a separate PR: EXEC-598.

InstrumentContext.measure_liquid_height()

We want to keep this internal and experimental for now and keep our options open for the future. So:

  • We should say in its docstring that it's internal and experimental, and exclude it from the public docs.opentrons.com website. Here is an example of how to exclude it from docs.opentrons.com.
  • We should implement this via SyncClient.execute_command_without_recovery(). So if no liquid is detected, it will raise an exception. We should just let that propagate.

The reason we want to keep this internal and experimental is that we haven't really fleshed out how to support error recovery on PAPI methods that return results. Does the UI flow need to have a step to retry the failed probe? Should we just raise some documented exception for the protocol author to catch and deal with how they please?

@aaron-kulkarni aaron-kulkarni marked this pull request as draft July 2, 2024 14:04
Copy link
Contributor

@SyntaxColoring SyntaxColoring left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is coming along! Agreed with the existing comments.

api/src/opentrons/protocol_api/instrument_context.py Outdated Show resolved Hide resolved
api/src/opentrons/protocol_api/instrument_context.py Outdated Show resolved Hide resolved
well_location = WellLocation(
origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=0)
)
result = self._engine_client.execute_command_without_recovery(
Copy link
Contributor

@SyntaxColoring SyntaxColoring Jul 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the problem here is that one of this method's callers, InstrumentContext.get_liquid_height(), is returning the probed height. And if the liquidProbe command fails and we let the frontend drive error recovery, there isn't a probed height in the general case. The client can choose to skip to the next command without retrying the failed liquidProbe.

We can certainly solve this for InstrumentContext.require_liquid(). Just, yeah, call execute_command() (with recovery) and discard the liquidProbe result. A successful liquidProbe, or a failed-but-recovered liquidProbe, will return from execute_command(). A failed-and-not-recovered liquidProbe will raise from execute_command(), and we should let that propagate up.

But for InstrumentContext.get_liquid_height() and InstrumentContext.detect_liquid_presence(), it's unclear to me what we would even want to happen.

api/src/opentrons/protocol_api/instrument_context.py Outdated Show resolved Hide resolved
api/src/opentrons/protocol_api/instrument_context.py Outdated Show resolved Hide resolved
@aaron-kulkarni aaron-kulkarni marked this pull request as ready for review July 2, 2024 20:49
@aaron-kulkarni aaron-kulkarni requested a review from sfoster1 July 3, 2024 13:17
@aaron-kulkarni
Copy link
Contributor Author

aaron-kulkarni commented Jul 3, 2024

I implemented the changes that we discussed in standup as I understood them. Probably forgot something or misunderstood it so if I did just let me know

Changes include:
In the bool only method, change to execute_command_without_recovery
In the bool or exception method, changed nothing
In the float or exception method, change to execute_command_without_recovery

Also edited the tests to reflect this

Copy link

codecov bot commented Jul 3, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 92.43%. Comparing base (5ce130b) to head (520058f).
Report is 125 commits behind head on edge.

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##             edge   #15555       +/-   ##
===========================================
+ Coverage   60.43%   92.43%   +32.00%     
===========================================
  Files         154       77       -77     
  Lines       12119     1283    -10836     
===========================================
- Hits         7324     1186     -6138     
+ Misses       4795       97     -4698     
Flag Coverage Δ
g-code-testing 92.43% <ø> (?)
hardware ?
shared-data ?

Flags with carried forward coverage won't be shown. Click here to find out more.

see 231 files with indirect coverage changes

@aaron-kulkarni aaron-kulkarni dismissed sfoster1’s stale review July 3, 2024 17:16

Requested changes have been implemented

Copy link
Contributor

@ryanthecoder ryanthecoder left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@aaron-kulkarni aaron-kulkarni merged commit 389c12d into edge Jul 3, 2024
25 checks passed
@aaron-kulkarni aaron-kulkarni deleted the exec-544-545-detect-liquid branch July 3, 2024 18:05
Comment on lines +93 to +112
def execute_command_with_result(
self, params: commands.CommandParams
) -> Optional[commands.CommandResult]:
"""Execute a ProtocolEngine command, including error recovery, and return a result.

See `ChildThreadTransport.execute_command_wait_for_recovery()` for exact
behavior.
"""
CreateType = CREATE_TYPES_BY_PARAMS_TYPE[type(params)]
create_request = CreateType(params=cast(Any, params))
result = self._transport.execute_command_wait_for_recovery(create_request)
if result.error is None:
return result.result
if isinstance(result.error, BaseException): # necessary to pass lint
raise result.error
raise ProtocolCommandFailedError(
original_error=result.error,
message=f"{result.error.errorType}: {result.error.detail}",
)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should remove this new execute_command_with_result() method for now. It seems like it's contributing to confusion about the tradeoff between returning a result and allowing for error recovery.

Thankfully, we shouldn't need it for the behavior that we want to implement right now. See my other comment.

well_location = WellLocation(
origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=0)
)
result = self._engine_client.execute_command_without_recovery(
Copy link
Contributor

@SyntaxColoring SyntaxColoring Jul 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recap of in-person discussion. Looks like you've already done some of this by the time I'm posting this comment.

InstrumentContext.require_liquid_presence()

We want this method to give the client the opportunity for error recovery, but raise an exception if that ultimately fails. That's what SyncClient.execute_command(), so under the hood, this should be implemented using that. If that raises an exception, we should just let it propagate.

InstrumentContext.detect_liquid_presence()

We want this method to straight-up return True/False without giving the client the opportunity for error recovery. So, under the hood:

  • We should implement it via SyncClient.execute_command_without_recovery().

  • If the probe detects no liquid, execute_command_without_recovery() will raise that as an exception. We should look for that and translate it into a False return.

    We can catch ProtocolCommandFailedError, and then check if exception.original_error is an instance of LiquidNotFoundError. If so, we should translate it into a False return. Otherwise, we should re-raise the exception to let it propagate.

I think this will expose a bug where the robot will mistakenly go into error recovery mode (because "the command failed") and try to keep running the protocol (because we're catching the exception, translating it to False, and letting the protocol continue). We can take that on in a separate PR: EXEC-598.

InstrumentContext.measure_liquid_height()

We want to keep this internal and experimental for now and keep our options open for the future. So:

  • We should say in its docstring that it's internal and experimental, and exclude it from the public docs.opentrons.com website. Here is an example of how to exclude it from docs.opentrons.com.
  • We should implement this via SyncClient.execute_command_without_recovery(). So if no liquid is detected, it will raise an exception. We should just let that propagate.

The reason we want to keep this internal and experimental is that we haven't really fleshed out how to support error recovery on PAPI methods that return results. Does the UI flow need to have a step to retry the failed probe? Should we just raise some documented exception for the protocol author to catch and deal with how they please?

def measure_liquid_height(self, well: labware.Well) -> float:
"""Check the height of the liquid within a well.

:returns: The height, in mm, of the liquid from the deck.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For API usability, I think we would ideally have this return the height from the bottom of the well, instead of from the deck. That seems more relevant because it wouldn't be coupled to the labware offsets that the operator happened to choose at the beginning of the run.

This doesn't need to happen right now as long as we're marking this method internal and experimental.

Comment on lines +876 to +879
# should never get here
raise PipetteLiquidNotFoundError(
"Error while trying to find liquid level.",
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this gets a lot cleaner if you split this into two methods:

  1. One that does SyncClient.execute_command().
  2. One that does SyncClient.execute_command_without_recovery().

SyncClient is drawing a distinction those two fundamentally modes of operation, representing a tradeoff between having a command result and allowing for error recovery, and representing that tradeoff with two different method signatures. Those map cleanly to the public InstrumentContext behaviors that we want to implement, if we maintain the two-method distinction through InstrumentCore.

@aaron-kulkarni aaron-kulkarni restored the exec-544-545-detect-liquid branch July 3, 2024 18:11
aaron-kulkarni added a commit that referenced this pull request Jul 8, 2024
<!--
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

Edit detect_liquid_presence, require_liquid_presence, and
measure_liquid_height as per the standup today.

Change instrument_context tests to check for the right
error(LiquidNotFoundError)

Split find_liquid_level into two functions: liquid_probe_with_recovery
and liquid_probe_without_recovery

<!--
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

-->

Added unit tests for liquid_probe_with_recovery and liquid_probe_without
recovery to test_instrument_core.py
Added unit tests for detect_liquid_presence, require_liquid_presence,
and measure_liquid_height to test_instrument_context.py

After the PR is merged, these API calls will be tested on the robot.

# 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.
-->

Accidentally merged #15555 too soon, this is a continuation of that PR.

# 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.
-->

Low: Since all the work was done in newly created functions, the risk
that it breaks existing code is minimal.
However, the new API calls still do need to be tested on a robot before
they can be used in other parts of the system.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants