Skip to content

Commit

Permalink
feat(shared-data): Add the ability to handle sub wells in labware geo…
Browse files Browse the repository at this point in the history
…metry (#16658)

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

Some Labware, particularly larger reservoirs have wells that have "sub
well" geometry at the bottom, such as the Nest 1 well 195ml, which is
one well with 96 little pyramids at the bottom.
This PR lets there be a "count" field in the geometry stack up which
tells the system that there are "count" number of sub features in this
well.

For the Height->Volume, we do the normal height->volume of one of these
sub features and then multiple the resulting volume by "count"

For Volume-Height, we first divide the volume by "count" to get the
volume change in one sub well and then do the normal calculation.

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

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

## 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.
-->
  • Loading branch information
ryanthecoder authored Nov 1, 2024
1 parent f2d37b1 commit c9cda35
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 35 deletions.
89 changes: 57 additions & 32 deletions api/src/opentrons/protocol_engine/state/frustum_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,28 +220,40 @@ def _get_segment_capacity(segment: WellSegment) -> float:
section_height = segment.topHeight - segment.bottomHeight
match segment:
case SphericalSegment():
return _volume_from_height_spherical(
target_height=segment.topHeight,
radius_of_curvature=segment.radiusOfCurvature,
return (
_volume_from_height_spherical(
target_height=segment.topHeight,
radius_of_curvature=segment.radiusOfCurvature,
)
* segment.count
)
case CuboidalFrustum():
return _volume_from_height_rectangular(
target_height=section_height,
bottom_length=segment.bottomYDimension,
bottom_width=segment.bottomXDimension,
top_length=segment.topYDimension,
top_width=segment.topXDimension,
total_frustum_height=section_height,
return (
_volume_from_height_rectangular(
target_height=section_height,
bottom_length=segment.bottomYDimension,
bottom_width=segment.bottomXDimension,
top_length=segment.topYDimension,
top_width=segment.topXDimension,
total_frustum_height=section_height,
)
* segment.count
)
case ConicalFrustum():
return _volume_from_height_circular(
target_height=section_height,
total_frustum_height=section_height,
bottom_radius=(segment.bottomDiameter / 2),
top_radius=(segment.topDiameter / 2),
return (
_volume_from_height_circular(
target_height=section_height,
total_frustum_height=section_height,
bottom_radius=(segment.bottomDiameter / 2),
top_radius=(segment.topDiameter / 2),
)
* segment.count
)
case SquaredConeSegment():
return _volume_from_height_squared_cone(section_height, segment)
return (
_volume_from_height_squared_cone(section_height, segment)
* segment.count
)
case _:
# TODO: implement volume calculations for truncated circular and rounded rectangular segments
raise NotImplementedError(
Expand Down Expand Up @@ -272,6 +284,7 @@ def height_at_volume_within_section(
section_height: float,
) -> float:
"""Calculate a height within a bounded section according to geometry."""
target_volume_relative = target_volume_relative / section.count
match section:
case SphericalSegment():
return _height_from_volume_spherical(
Expand Down Expand Up @@ -311,28 +324,40 @@ def volume_at_height_within_section(
"""Calculate a volume within a bounded section according to geometry."""
match section:
case SphericalSegment():
return _volume_from_height_spherical(
target_height=target_height_relative,
radius_of_curvature=section.radiusOfCurvature,
return (
_volume_from_height_spherical(
target_height=target_height_relative,
radius_of_curvature=section.radiusOfCurvature,
)
* section.count
)
case ConicalFrustum():
return _volume_from_height_circular(
target_height=target_height_relative,
total_frustum_height=section_height,
bottom_radius=(section.bottomDiameter / 2),
top_radius=(section.topDiameter / 2),
return (
_volume_from_height_circular(
target_height=target_height_relative,
total_frustum_height=section_height,
bottom_radius=(section.bottomDiameter / 2),
top_radius=(section.topDiameter / 2),
)
* section.count
)
case CuboidalFrustum():
return _volume_from_height_rectangular(
target_height=target_height_relative,
total_frustum_height=section_height,
bottom_width=section.bottomXDimension,
bottom_length=section.bottomYDimension,
top_width=section.topXDimension,
top_length=section.topYDimension,
return (
_volume_from_height_rectangular(
target_height=target_height_relative,
total_frustum_height=section_height,
bottom_width=section.bottomXDimension,
bottom_length=section.bottomYDimension,
top_width=section.topXDimension,
top_length=section.topYDimension,
)
* section.count
)
case SquaredConeSegment():
return _volume_from_height_squared_cone(target_height_relative, section)
return (
_volume_from_height_squared_cone(target_height_relative, section)
* section.count
)
case _:
# TODO(cm): this would be the NEST-96 2uL wells referenced in EXEC-712
# we need to input the math attached to that issue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,27 @@
},
"innerLabwareGeometry": {
"cuboidalWell": {
"sections": []
"sections": [
{
"shape": "cuboidal",
"topXDimension": 107.25,
"topYDimension": 8,
"bottomXDimension": 101.25,
"bottomYDimension": 1.66,
"topHeight": 2,
"bottomHeight": 8,
"yCount": 8
},
{
"shape": "cuboidal",
"topXDimension": 107.5,
"topYDimension": 71.25,
"bottomXDimension": 107.25,
"bottomYDimension": 71.0,
"topHeight": 39.22,
"bottomHeight": 2
}
]
}
}
}
23 changes: 22 additions & 1 deletion shared-data/labware/definitions/3/nest_1_reservoir_195ml/3.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,28 @@
},
"innerLabwareGeometry": {
"cuboidalWell": {
"sections": []
"sections": [
{
"shape": "cuboidal",
"topXDimension": 9,
"topYDimension": 9,
"bottomXDimension": 1.93,
"bottomYDimension": 1.93,
"topHeight": 2,
"bottomHeight": 0,
"xCount": 12,
"yCount": 8
},
{
"shape": "cuboidal",
"topXDimension": 71.3,
"topYDimension": 70.6,
"bottomXDimension": 107.3,
"bottomYDimension": 106.8,
"topHeight": 26.85,
"bottomHeight": 2
}
]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,27 @@
},
"innerLabwareGeometry": {
"cuboidalWell": {
"sections": []
"sections": [
{
"shape": "squaredcone",
"bottomCrossSection": "circular",
"circleDiameter": 2.5,
"rectangleXDimension": 7.98,
"rectangleYDimension": 70.98,
"topHeight": 4.05,
"bottomHeight": 0.0,
"yCount": 8
},
{
"shape": "cuboidal",
"topXDimension": 8.34,
"topYDimension": 71.85,
"bottomXDimension": 7.98,
"bottomYDimension": 70.98,
"topHeight": 41.75,
"bottomHeight": 4.05
}
]
}
}
}
30 changes: 30 additions & 0 deletions shared-data/labware/schemas/3.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@
},
"bottomHeight": {
"type": "number"
},
"xCount": {
"type": "integer"
},
"yCount": {
"type": "integer"
}
}
},
Expand Down Expand Up @@ -112,6 +118,12 @@
},
"bottomHeight": {
"type": "number"
},
"xCount": {
"type": "integer"
},
"yCount": {
"type": "integer"
}
}
},
Expand Down Expand Up @@ -149,6 +161,12 @@
},
"bottomHeight": {
"type": "number"
},
"xCount": {
"type": "integer"
},
"yCount": {
"type": "integer"
}
}
},
Expand Down Expand Up @@ -187,6 +205,12 @@
},
"bottomHeight": {
"type": "number"
},
"xCount": {
"type": "integer"
},
"yCount": {
"type": "integer"
}
}
},
Expand Down Expand Up @@ -225,6 +249,12 @@
},
"bottomHeight": {
"type": "number"
},
"xCount": {
"type": "integer"
},
"yCount": {
"type": "integer"
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,21 @@ class SphericalSegment(BaseModel):
...,
description="Height of the bottom of the segment, must be 0.0",
)
xCount: _StrictNonNegativeInt = Field(
default=1,
description="Number of instances of this shape in the stackup, used for wells that have multiple sub-wells",
)
yCount: _StrictNonNegativeInt = Field(
default=1,
description="Number of instances of this shape in the stackup, used for wells that have multiple sub-wells",
)

@cached_property
def count(self) -> int:
return self.xCount * self.yCount

class Config:
keep_untouched = (cached_property,)


class ConicalFrustum(BaseModel):
Expand All @@ -276,6 +291,21 @@ class ConicalFrustum(BaseModel):
...,
description="The height at the bottom of a bounded subsection of a well, relative to the bottom of the well",
)
xCount: _StrictNonNegativeInt = Field(
default=1,
description="Number of instances of this shape in the stackup, used for wells that have multiple sub-wells",
)
yCount: _StrictNonNegativeInt = Field(
default=1,
description="Number of instances of this shape in the stackup, used for wells that have multiple sub-wells",
)

@cached_property
def count(self) -> int:
return self.xCount * self.yCount

class Config:
keep_untouched = (cached_property,)


class CuboidalFrustum(BaseModel):
Expand Down Expand Up @@ -305,6 +335,21 @@ class CuboidalFrustum(BaseModel):
...,
description="The height at the bottom of a bounded subsection of a well, relative to the bottom of the well",
)
xCount: _StrictNonNegativeInt = Field(
default=1,
description="Number of instances of this shape in the stackup, used for wells that have multiple sub-wells",
)
yCount: _StrictNonNegativeInt = Field(
default=1,
description="Number of instances of this shape in the stackup, used for wells that have multiple sub-wells",
)

@cached_property
def count(self) -> int:
return self.xCount * self.yCount

class Config:
keep_untouched = (cached_property,)


# A squared cone is the intersection of a cube and a cone that both
Expand Down Expand Up @@ -354,6 +399,14 @@ class SquaredConeSegment(BaseModel):
...,
description="The height at the bottom of a bounded subsection of a well, relative to the bottom of the well",
)
xCount: _StrictNonNegativeInt = Field(
default=1,
description="Number of instances of this shape in the stackup, used for wells that have multiple sub-wells",
)
yCount: _StrictNonNegativeInt = Field(
default=1,
description="Number of instances of this shape in the stackup, used for wells that have multiple sub-wells",
)

@staticmethod
def _area_trap_points(
Expand Down Expand Up @@ -433,6 +486,10 @@ def height_to_volume_table(self) -> Dict[float, float]:
def volume_to_height_table(self) -> Dict[float, float]:
return dict((v, k) for k, v in self.height_to_volume_table.items())

@cached_property
def count(self) -> int:
return self.xCount * self.yCount

class Config:
keep_untouched = (cached_property,)

Expand Down Expand Up @@ -546,6 +603,21 @@ class RoundedCuboidSegment(BaseModel):
...,
description="The height at the bottom of a bounded subsection of a well, relative to the bottom of the well",
)
xCount: _StrictNonNegativeInt = Field(
default=1,
description="Number of instances of this shape in the stackup, used for wells that have multiple sub-wells",
)
yCount: _StrictNonNegativeInt = Field(
default=1,
description="Number of instances of this shape in the stackup, used for wells that have multiple sub-wells",
)

@cached_property
def count(self) -> int:
return self.xCount * self.yCount

class Config:
keep_untouched = (cached_property,)


class Metadata1(BaseModel):
Expand Down

0 comments on commit c9cda35

Please sign in to comment.