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): add WellVolumeOffset to WellLocation #16302

Merged
merged 56 commits into from
Oct 17, 2024

Conversation

pmoegenburg
Copy link
Contributor

@pmoegenburg pmoegenburg commented Sep 19, 2024

Overview

This PR enables static meniscus-relative aspirate and dispense via the Protocol API. To enable this, WellVolumeOffset has been added to WellLocation. This is a volume of liquid to account for when executing commands with an origin of WellOrigin.MENISCUS. Specifying operationVolume results in this class acting as a sentinel and should be used when volume can be determined from the command parameters, for example commanding Aspirate. A volume should be specified when it cannot be determined from the command parameters, for example commanding MoveToWell prior to AspirateInPlace.

Test Plan and Hands on Testing

Successfully tested the following protocol on a robot multiple times in a row. Aspiration from meniscus was not tested due to not-yet-available InnerWellGeometry:

from opentrons.protocol_api import ProtocolContext
metadata = {"protocolName": "Test LLD"}
requirements = {"robotType": "Flex", "apiLevel":"2.21"}


def run(ctx: ProtocolContext) -> None:
    """Run."""
    tiprack = ctx.load_labware(f"opentrons_flex_96_tiprack_1000uL", "A3")
    source = ctx.load_labware("nest_12_reservoir_15ml", "C2")
    sink = ctx.load_labware("nest_96_wellplate_100ul_pcr_full_skirt", "D2")
    pipette = ctx.load_instrument("flex_1channel_1000", "left", liquid_presence_detection = True)

    pipette.pick_up_tip(tiprack)
    pipette.measure_liquid_height(sink["A1"])
    pipette.aspirate(10, source["A1"])
    pipette.dispense(10, sink["A1"].meniscus(-2))
    pipette.return_tip()

Changelog

Review requests

Risk assessment

@pmoegenburg pmoegenburg self-assigned this Sep 19, 2024
@pmoegenburg pmoegenburg requested review from a team as code owners September 19, 2024 17:36
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.

I think the logic isn't quite right and we're missing a function

api/src/opentrons/protocol_engine/commands/aspirate.py Outdated Show resolved Hide resolved
api/src/opentrons/protocol_engine/state/geometry.py Outdated Show resolved Hide resolved
api/src/opentrons/protocol_engine/state/geometry.py Outdated Show resolved Hide resolved
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.

Can get rid of another intermediate and remove a couple lines from the diff. looks good for now but holding off approving until that get_ending_height function exists

api/src/opentrons/protocol_engine/commands/aspirate.py Outdated Show resolved Hide resolved
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.

Thank you! Some questions about the outward-facing Protocol Engine API.

api/src/opentrons/protocol_engine/types.py Outdated Show resolved Hide resolved
api/src/opentrons/protocol_engine/types.py Outdated Show resolved Hide resolved
api/src/opentrons/protocol_engine/types.py Outdated Show resolved Hide resolved
Copy link

codecov bot commented Sep 20, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 79.43%. Comparing base (5658b8a) to head (05a075c).
Report is 306 commits behind head on edge.

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##             edge   #16302       +/-   ##
===========================================
+ Coverage   63.28%   79.43%   +16.14%     
===========================================
  Files         300      118      -182     
  Lines       15786     4283    -11503     
===========================================
- Hits         9990     3402     -6588     
+ Misses       5796      881     -4915     
Flag Coverage Δ
hardware ?
shared-data 73.86% <100.00%> (+0.53%) ⬆️
system-server ?
update-server ?
usb-bridge ?

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

Files with missing lines Coverage Δ
.../opentrons_shared_data/protocol/models/__init__.py 100.00% <ø> (ø)
...trons_shared_data/protocol/models/shared_models.py 100.00% <100.00%> (ø)

... and 187 files with indirect coverage changes

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 small things, and also let's remove the last of the temporary comments and commented out code

api/src/opentrons/protocol_engine/types.py Outdated Show resolved Hide resolved
@@ -239,8 +239,31 @@ class WellOffset(BaseModel):
z: float = 0


# get rid of operationVolume below and operation_volume parameter in methods?
# TODO(pm, 2024-09-23): PE should raise error if volumeOffset specified with a tip rack location
class WellLocation(BaseModel):
Copy link
Member

Choose a reason for hiding this comment

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

I think we should call this LiquidHandlingWellLocation, and then have WellLocation = LiquidHandlingWellLocation if we don't want to change all the papi code

api/src/opentrons/protocol_engine/types.py Outdated Show resolved Hide resolved
ending_volume = starting_volume + volume
ending_height = self.get_height_at_volume(labware_id=labware_id, well_id=well_id, target_volume=ending_volume)
# return ending_height
return starting_height # delete, use line above once sub-methods implemented
Copy link
Member

Choose a reason for hiding this comment

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

if we merge this, we should merge the return ending_height, and not this comment

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.

Looking great and almost there! Inline comments for change requests.

Generally, I do not think we should be validating scientific reasonableness in the engine - that should happen in the input layers like PD and PAPI. Specifically that's things like forcing dispense volumeOffset to 0, and requiring aspirate z offsets to be <0. Let the engine execute what it's given as long as it won't cause an outright error, like a collision.

api/src/opentrons/protocol_engine/commands/dispense.py Outdated Show resolved Hide resolved
api/src/opentrons/protocol_engine/errors/exceptions.py Outdated Show resolved Hide resolved
api/src/opentrons/protocol_engine/commands/aspirate.py Outdated Show resolved Hide resolved
if invalid:
if isinstance(well_location, PickUpTipWellLocation):
raise OperationLocationNotInWellError(
f"Specifying {well_location.origin} with an offset of {well_location.offset} results in an operation location below the bottom of the well"
Copy link
Member

Choose a reason for hiding this comment

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

not always true that this is below the well, right? it could also be below the minimum height. Might be worth raising these exceptions from inside the logic above so we can take that into account.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated docstring

api/src/opentrons/protocol_engine/state/labware.py Outdated Show resolved Hide resolved
@@ -244,6 +244,34 @@ class WellLocation(BaseModel):

origin: WellOrigin = WellOrigin.TOP
offset: WellOffset = Field(default_factory=WellOffset)
volumeOffset: float = Field(
Copy link
Member

Choose a reason for hiding this comment

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

are we keeping WellLocation around for backwards compatibility? If so, maybe we can avoid adding volumeOffset to it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

volumeOffset was added to WellLocation at your suggestion to enable more flexibility. Specifically for MoveToWell and TouchTip commands

Copy link
Member

Choose a reason for hiding this comment

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

okay, cool! sorry for forgetting :)

shared-data/command/schemas/9.json Outdated Show resolved Hide resolved
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.

Looks good to me pending on robot testing!

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.

Types mostly look good, just a couple places that I think need to be fixed.

Feel free to merge without fixing these, if you need to unblock something. But I do think we should fix these before release.

I found these by running a dev server (make -C robot-server dev), opening the HTTP API documentation (http://localhost:31950/redoc), and clicking through the params fields of the many commands in the GET /runs/{runId}/commands/{commandId} documentation. Another way of doing this would have been to feed the command schema into a viewer like https://json-schema.app/start.

I haven't looked closely at the opentrons.protocol_engine logic part of this since it seems like @sfoster1 already has, but I'm happy to if anyone thinks it's worthwhile.

api/src/opentrons/protocol_engine/commands/blow_out.py Outdated Show resolved Hide resolved
api/src/opentrons/protocol_engine/types.py Show resolved Hide resolved
api/src/opentrons/types.py Show resolved Hide resolved
api/src/opentrons/types.py Outdated Show resolved Hide resolved
@pmoegenburg pmoegenburg merged commit 9797d74 into edge Oct 17, 2024
41 of 42 checks passed
TamarZanzouri pushed a commit that referenced this pull request Oct 18, 2024
<!--
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

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

This PR enables static meniscus-relative aspirate and dispense via the
Protocol API. To enable this, WellVolumeOffset has been added to
WellLocation. This is a volume of liquid to account for when executing
commands with an origin of WellOrigin.MENISCUS. Specifying
`operationVolume` results in this class acting as a sentinel and should
be used when volume can be determined from the command parameters, for
example commanding Aspirate. A volume should be specified when it cannot
be determined from the command parameters, for example commanding
MoveToWell prior to AspirateInPlace.

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

Successfully tested the following protocol on a robot multiple times in
a row. Aspiration from meniscus was not tested due to not-yet-available
InnerWellGeometry:
```
from opentrons.protocol_api import ProtocolContext
metadata = {"protocolName": "Test LLD"}
requirements = {"robotType": "Flex", "apiLevel":"2.21"}


def run(ctx: ProtocolContext) -> None:
    """Run."""
    tiprack = ctx.load_labware(f"opentrons_flex_96_tiprack_1000uL", "A3")
    source = ctx.load_labware("nest_12_reservoir_15ml", "C2")
    sink = ctx.load_labware("nest_96_wellplate_100ul_pcr_full_skirt", "D2")
    pipette = ctx.load_instrument("flex_1channel_1000", "left", liquid_presence_detection = True)

    pipette.pick_up_tip(tiprack)
    pipette.measure_liquid_height(sink["A1"])
    pipette.aspirate(10, source["A1"])
    pipette.dispense(10, sink["A1"].meniscus(-2))
    pipette.return_tip()
```

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

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

3 participants