Skip to content

Conversation

@ryanthecoder
Copy link
Contributor

@ryanthecoder ryanthecoder commented Nov 24, 2025

Overview

This PR encompases a first pass implementation of allowing dynamic tracking with liquid classes. As is there is no super convenient way a new way for users to edit positions in transfer properties. There is no default selection for any current liquid classes and is defaulted to None everywhere.

But as a first pass this does add a few new features

  • TipPosition has split LIQUID_MENISCUS into LIQUID_MENISCUS_START and LIQUID_MENISCUS_END to match the implementation with the standard aspirate and dispense options
  • Adds a Schema 2 liquid class which adds aspirate_end_position and dispense_end_position as optional members of the liquid class, it also adds override start and end positions that can be used, and will fallback to the old positions if they throw an error caused by no liquid level detection data, or inner well geometry data.
  • Enables the use of dynamic tracking in liquid classes for the gravimetric protocol which will be part of another PR and the minimal implementation of that is enabled for testing
  • Adds a convenience method to the aspirate, single-dispense and multi-dispense objects within transfer properties to add an override tip position

@codecov
Copy link

codecov bot commented Nov 24, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 56.74%. Comparing base (652c8fd) to head (5cfb9cb).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             edge   #20232      +/-   ##
==========================================
- Coverage   56.74%   56.74%   -0.01%     
==========================================
  Files        3649     3649              
  Lines      303774   303770       -4     
  Branches    42724    42724              
==========================================
- Hits       172383   172379       -4     
  Misses     131177   131177              
  Partials      214      214              
Flag Coverage Δ
app 46.44% <ø> (ø)
protocol-designer 19.38% <ø> (ø)
step-generation 5.64% <ø> (-0.01%) ⬇️

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

Files with missing lines Coverage Δ
...n/opentrons_shared_data/liquid_classes/__init__.py 81.81% <ø> (-2.80%) ⬇️
...red_data/liquid_classes/liquid_class_definition.py 95.85% <ø> (-0.05%) ⬇️
...thon/opentrons_shared_data/liquid_classes/types.py 0.00% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ryanthecoder ryanthecoder marked this pull request as ready for review December 2, 2025 16:28
@ryanthecoder ryanthecoder requested review from a team as code owners December 2, 2025 16:28
@ryanthecoder ryanthecoder requested review from smb2268 and removed request for a team December 2, 2025 16:28
@ryanthecoder ryanthecoder changed the title feat(api, shared-data): First pass of dynamic pipetting into liquid classes feat(api, shared-data): add dynamic pipetting into liquid classes Dec 2, 2025
@ryanthecoder ryanthecoder force-pushed the EXEC-2074-dynamic-in-liquid-class branch from 3b83afa to f3c1a6c Compare December 2, 2025 18:33
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.

Some inline things; I think we should get naming consistent between defs and code, I think there's some stuff accidentally left in the pr.

In general I'm a little uneasy about the specificity of this implementation. There's a tip position and an override tip position, and the only way to go from override to backup is if specific exceptions about not having liquid height estimation data are thrown. That definitely works for the specific issue where maybe we don't have height data for a variety of reasons. But there's also more reasons we might want different tip positions:

  • Maybe we trust filtertips, or some future filter tips, less
  • Maybe we want to do different things depending on the volume in the well (for instance, maybe we find some wells where under a certain volume we know our estimations are unreliable)
    Adding different kinds of reasons to pick a different position is something that would either take a lot of refactoring from here or require more and more little checks and exceptions to be raised from lower layers and more and more specific named entries in the classes as fallbacks.

That said, I don't know if we need to go all the way to having an embedded conditional in the defs like previously proposed. I think we should give the new position groups some structure. Rather than having the flat entries aspiratePosition, aspirateEndPosition, overrideAspiratePosition, overrideAspirateEndPosition side by side, have aspiratePositions: [{name, start, end}, {name, start, end}] where name is some name like "dynamic" or "static". Then, the implied logic (and the specification in the class schema) is that the code will try and use these positions in order from the array, and if there's an exception go to the next.

That at least means that if we want to add more entries we won't need more schema changes; it avoids the thing I hate most in schemas which is flat entries with specific names, which is really just putting structure in element names when it could be in actual structure; and it gives some nicer specifiable names to things.

"additionalProperties": false,
"description": "Properties specific to the aspirate function.",
"properties": {
"aspirateEndPosition": {
Copy link
Member

Choose a reason for hiding this comment

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

this doesn't seem right? there's no corresponding change in the engine aspirate command or dispense command, did there used to be?

return self._override_dispense_position or self._dispense_position

@property
def dispense_end_position(self) -> Optional[TipPosition]:
Copy link
Member

Choose a reason for hiding this comment

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

it's a bit confusing that it's endPosition and overrideEndPosition in the class defs, but end_position and fallback_end_position in the code. can we make these the same?

# Will this cause more harm than good? Is there a better alternative to this?
reference_point = well.get_center()
case PositionReference.LIQUID_MENISCUS_END | PositionReference.LIQUID_MENISCUS:
estimated_liquid_height = well.estimate_liquid_height_after_pipetting(
Copy link
Member

Choose a reason for hiding this comment

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

this is weird, it duplicates the code that's in aspirateWhileTracking doesn't it? i thought we had the capability to call aspirateWhileTracking with end position meniscus-end AND a user-provided offset?

Copy link
Member

Choose a reason for hiding this comment

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

or is the intent to force an exception to raise here rather than in the command? if so, can we add an engine query function that lets us say "hey can you do meniscus-end for this well right now"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It does duplicate stuff but that is because everything in this file is a Point and not a Location. it needs the Point value for all of the move_to commands that its doing. So I just kept with the Point paradigm of this file

@ddcc4
Copy link
Collaborator

ddcc4 commented Dec 3, 2025

Note to self/@jbleon95: I haven't read through all of changes yet, but I don't see any edits the public transfer functions in instrument_context.py. I think (?) Ryan is proposing to add both static positions and meniscus-relative positions to the liquid class definitions. If both are present, how does the user choose which to use?

@ryanthecoder ryanthecoder force-pushed the EXEC-2074-dynamic-in-liquid-class branch from 8b58a4c to a536f65 Compare December 15, 2025 19:58
@ryanthecoder
Copy link
Contributor Author

Note to self/@jbleon95: I haven't read through all of changes yet, but I don't see any edits the public transfer functions in instrument_context.py. I think (?) Ryan is proposing to add both static positions and meniscus-relative positions to the liquid class definitions. If both are present, how does the user choose which to use?

This PR is aimed just at providing the ability to define a liquid class or edit an extant one to give this behavior. Adding defaults is its own whole project.

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