Skip to content

feat(shared-data): change schema 2 to include user-defined volumes #18815

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

Open
wants to merge 13 commits into
base: edge
Choose a base branch
from

Conversation

caila-marashaj
Copy link
Contributor

@caila-marashaj caila-marashaj commented Jul 2, 2025

Overview

Liquid-level detection is out now, and its main blind-spot is labware that don't already have InnerLabwareGeometry definitions in our shared-data repo. This is difficult for users to fill in on their own, as it comes from some pretty intensive measuring and modeling from our hardware department.

As an alternative, we can give users/support people the option to add UserDefinedVolumes to their labware's shared-data file, which will be a list of small dictionaries, each containing a height entry and a volume entry, like this:
"innerLabwareGeometry": { "userVolumes": { "heightToVolumeMap": [ { "height": 0.5, "volume": 3 }, { "height": 2.0, "volume": 10 }, { "height": 10.4, "volume": 70 }, { "height": 30.8, "volume": 100 }, { "height": 59.3, "volume": 300 } ] }

This pr enables this type of data to be used as input for the same liquid-level detection functionality that InnerWellGeometry definitions use, like labware.height_from_volume and labware.volume_from_height.

InnerWellGeometry will use the same geometric calculations we've been using before, and estimations using UserDefinedVolumes will use linear interpolations of the user-specified volumes and heights. This obviously won't be as accurate, but for a lot of labware, especially small labware, it won't matter a ton, so this is a good jumping off point I think.

Changelog

  • change labware schema 2 to allow InnerLabwareGeometry to be either InnerWellGeometry or UserDefinedVolumes
  • add this ^ to labware_definitions as well
  • create linear interpolation functions for UserDefinedVolumes in frustum_helpers
  • have geometry.py determine the type of InnerLabwareGeometry and call the right kind of calculation
  • add a fixture in there
  • some tests

Testing

Hardware testing is coming up with a protocol that will allow users to automatically generate these UserDefinedVolumes data by dispensing known volumes into a well, and probing the liquid height in the well after dispensing. The math itself hasn't been rigorously tested yet, but once we get some more data to work with, we're going to use this protocol to come up with UserDefinedVolumes data for labware with known InnerWellGeometry data, and compare the results of height_from_volume and volume_from_height a bunch of times to evaluate the efficacy.

Copy link

codecov bot commented Jul 3, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 25.47%. Comparing base (85a098f) to head (5a0ac5d).
Report is 4 commits behind head on edge.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             edge   #18815      +/-   ##
==========================================
+ Coverage   25.38%   25.47%   +0.08%     
==========================================
  Files        3333     3334       +1     
  Lines      287669   288127     +458     
  Branches    36482    36538      +56     
==========================================
+ Hits        73034    73390     +356     
- Misses     214612   214714     +102     
  Partials       23       23              
Flag Coverage Δ
protocol-designer 19.11% <ø> (+0.11%) ⬆️
step-generation 5.38% <ø> (+0.06%) ⬆️

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

Files with missing lines Coverage Δ
...pentrons_shared_data/labware/labware_definition.py 78.92% <ø> (-0.08%) ⬇️

... and 15 files with indirect coverage changes

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

@caila-marashaj caila-marashaj force-pushed the EXEC-762-user-defined-volumes branch from 8e03737 to 2eb5ef7 Compare July 14, 2025 18:56
@caila-marashaj caila-marashaj marked this pull request as ready for review July 14, 2025 21:34
@caila-marashaj caila-marashaj requested review from a team as code owners July 14, 2025 21:34
@caila-marashaj caila-marashaj requested review from smb2268 and removed request for a team July 14, 2025 21:34
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! This looks fundamentally right.

Just lots of small things, sorry for the wall of text. Only some of these are blocking change requests. Blocking change requests are prefixed with ⚠️.

Comment on lines 283 to 298
test('the last entry for UserDefinedVolumes should be the max volume of the well at its depth', () => {
for (const well of Object.values(labwareDef.wells)) {
const wellGeometryId = well.geometryDefinitionId

if (wellGeometryId === undefined) return
if (labwareDef.innerLabwareGeometry == null) return
if (!('sections' in labwareDef.innerLabwareGeometry)) return
const wellGeometry = labwareDef.innerLabwareGeometry[wellGeometryId]
if (wellGeometry === undefined) return

const wellDepth = well.depth
const sectionList = wellGeometry.sections
const last_entry = sectionList[sectionList.length - 1]
expect(last_entry.height).toStrictEqual(wellDepth)
}
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ This test is checking sections, but I think we want it to check the new heightToVolumeMap instead, don't we?

Also heads up that the if (!('sections' in labwareDef.innerLabwareGeometry)) check looks like it's missing one level of nesting. Like 'sections' in labwareDef.innerLabwareGeometry[wellGeometryId]. (Except that sections would change to heightToVolumeMap, per the above.

Copy link
Contributor

Choose a reason for hiding this comment

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

Separately, a stylistic suggestion: it looks like some of this null/undefined checking can be condensed with optional chaining and nullish coalescing.

@@ -4434,6 +4450,130 @@ def test_virtual_find_height_and_volume(
assert height_estimate == volume_estimate == target_height_volume


@pytest.mark.parametrize("use_mocks", [False])
@given(target_height_volume_st=st.data())
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture])
Copy link
Contributor

Choose a reason for hiding this comment

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

This HealthCheck that we're suppressing actually looks significant.

It's warning us that we're reusing the same labware_store and subject across Hypothesis examples, so they won't be isolated the way we usually expect.

We could probably get away with it here if we really needed to, but we ought to leave a comment in the test warning people not to do anything mutative.

Might be moot because of the larger points about this test, see below.

Comment on lines +4474 to +4494
def _expected_volume_result(
target_height: float, well_geometry: UserDefinedVolumes
) -> float:
prev_height = 0.0
prev_volume = 0.0
if target_height == 0.0:
return 0.0
for pair in well_geometry.heightToVolumeMap:
if target_height == pair.height:
return pair.volume
if target_height > prev_height and target_height < pair.height:
proportional_diff = (target_height - prev_height) / (
pair.height - prev_height
)
target_volume = (
prev_volume + (pair.volume - prev_height) * proportional_diff
)
return target_volume
prev_height = pair.height
prev_volume = pair.volume
raise ValueError("test function unable to find volume.")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Hm, this test is reimplementing, almost word-for-word, a dependency of its subject. This is a red flag that we ought to step back and examine what's going on here.

At a glance, we have:

  • Some Real Math in frustum_helpers.py.
  • Some simple steering logic in geometry.py that picks which Real Math in frustum_helpers to use.

So, some options:

  • Leaving all the production code as you have it, use Hypothesis to test the Real Math through the public interface of frustum_helpers.py, and use Decoy to test that geometry.py correctly calls a mock of frustum_helpers.py.
    • This is how things were originally supposed to work, which is why most of the other tests in test_geometry_view.py are written this way.
  • Leaving all of the production code as you have it, use Hypothesis to test the Real Math through the public interface of frustum_helpers.py, and leave the steering logic in geometry.py untested.
    • This is acceptable IMO as long as the steering logic in geometry.py is trivial. The existing isinstance() check seems fine. Any kind of math would probably not be fine.
  • Move the steering logic into frustum_helpers.py. So its existing functions would become private, and in their place it would expose a single height-to-volume function and a single volume-to-height function that would automatically steer towards the appropriate implementation. Then, use Hypothesis to test everything through the public interface of frustum_helpers.py.

Comment on lines +4470 to +4471
well_def = subject._labware.get_well_definition("labware-id-1", "A1")
well_geometry = subject._labware.get_well_geometry("labware-id-1", "A1")
Copy link
Contributor

Choose a reason for hiding this comment

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

We should generally try to avoid letting tests access private implementation details (_labware) of their subjects. Fortunately, it's easy to avoid in this case: you can just replace _labware with labware_store.

Might be moot because of the larger points about this test, see other comment.

@caila-marashaj caila-marashaj force-pushed the EXEC-762-user-defined-volumes branch from 1937c5c to 859286c Compare July 15, 2025 17:37
@caila-marashaj caila-marashaj force-pushed the EXEC-762-user-defined-volumes branch from 7a03bc8 to 75b42fc Compare July 15, 2025 18:41
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.

2 participants