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

fix(api): fix and test frustum_helpers.py #16406

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 39 additions & 27 deletions api/src/opentrons/protocol_engine/state/frustum_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
from opentrons_shared_data.labware.labware_definition import InnerWellGeometry


SectionsType = Union[CircularBoundedSection, RectangularBoundedSection]


def reject_unacceptable_heights(
potential_heights: List[float], max_height: float
) -> float:
Expand All @@ -33,9 +36,7 @@ def reject_unacceptable_heights(
return valid_heights[0]


def get_cross_section_area(
bounded_section: Union[CircularBoundedSection, RectangularBoundedSection]
) -> float:
def get_cross_section_area(bounded_section: SectionsType) -> float:
"""Find the shape of a cross-section and calculate the area appropriately."""
if bounded_section["shape"] == "circular":
cross_section_area = cross_section_area_circular(bounded_section["diameter"])
Expand Down Expand Up @@ -242,11 +243,11 @@ def get_well_volumetric_capacity(

if is_rectangular_frusta_list(sorted_frusta):
for f, next_f in get_boundary_pairs(sorted_frusta):
top_cross_section_width = next_f["xDimension"]
top_cross_section_length = next_f["yDimension"]
bottom_cross_section_width = f["xDimension"]
bottom_cross_section_length = f["yDimension"]
frustum_height = next_f["topHeight"] - f["topHeight"]
top_cross_section_width = next_f.xDimension
top_cross_section_length = next_f.yDimension
bottom_cross_section_width = f.xDimension
bottom_cross_section_length = f.yDimension
frustum_height = next_f.topHeight - f.topHeight
frustum_volume = volume_from_height_rectangular(
target_height=frustum_height,
total_frustum_height=frustum_height,
Expand All @@ -256,20 +257,20 @@ def get_well_volumetric_capacity(
top_width=top_cross_section_width,
)

well_volume.append((next_f["topHeight"], frustum_volume))
well_volume.append((next_f.topHeight, frustum_volume))
elif is_circular_frusta_list(sorted_frusta):
for f, next_f in get_boundary_pairs(sorted_frusta):
top_cross_section_radius = next_f["diameter"] / 2.0
bottom_cross_section_radius = f["diameter"] / 2.0
frustum_height = next_f["topHeight"] - f["topHeight"]
top_cross_section_radius = next_f.diameter / 2.0
bottom_cross_section_radius = f.diameter / 2.0
frustum_height = next_f.topHeight - f.topHeight
frustum_volume = volume_from_height_circular(
target_height=frustum_height,
total_frustum_height=frustum_height,
bottom_radius=bottom_cross_section_radius,
top_radius=top_cross_section_radius,
)

well_volume.append((next_f["topHeight"], frustum_volume))
well_volume.append((next_f.topHeight, frustum_volume))
else:
raise NotImplementedError(
"Well section with differing boundary shapes not yet implemented."
Expand All @@ -278,20 +279,26 @@ def get_well_volumetric_capacity(


def height_at_volume_within_section(
top_cross_section: Union[CircularBoundedSection, RectangularBoundedSection],
bottom_cross_section: Union[CircularBoundedSection, RectangularBoundedSection],
top_cross_section: SectionsType,
bottom_cross_section: SectionsType,
target_volume_relative: float,
frustum_height: float,
) -> float:
"""Calculate a height within a bounded section according to geometry."""
if top_cross_section["shape"] == bottom_cross_section["shape"] == "circular":
if (
top_cross_section["shape"] == "circular"
and bottom_cross_section["shape"] == "circular"
):
frustum_height = height_from_volume_circular(
volume=target_volume_relative,
top_radius=(top_cross_section["diameter"] / 2),
bottom_radius=(bottom_cross_section["diameter"] / 2),
total_frustum_height=frustum_height,
)
elif top_cross_section["shape"] == bottom_cross_section["shape"] == "rectangular":
elif (
top_cross_section["shape"] == "rectangular"
and bottom_cross_section["shape"] == "rectangular"
):
frustum_height = height_from_volume_rectangular(
volume=target_volume_relative,
total_frustum_height=frustum_height,
Expand All @@ -308,20 +315,26 @@ def height_at_volume_within_section(


def volume_at_height_within_section(
top_cross_section: Union[CircularBoundedSection, RectangularBoundedSection],
bottom_cross_section: Union[CircularBoundedSection, RectangularBoundedSection],
top_cross_section: SectionsType,
bottom_cross_section: SectionsType,
target_height_relative: float,
frustum_height: float,
) -> float:
"""Calculate a volume within a bounded section according to geometry."""
if top_cross_section["shape"] == bottom_cross_section["shape"] == "circular":
if (
top_cross_section["shape"] == "circular"
and bottom_cross_section["shape"] == "circular"
):
frustum_volume = volume_from_height_circular(
target_height=target_height_relative,
total_frustum_height=frustum_height,
bottom_radius=(bottom_cross_section["diameter"] / 2),
top_radius=(top_cross_section["diameter"] / 2),
)
elif top_cross_section["shape"] == bottom_cross_section["shape"] == "rectangular":
elif (
top_cross_section["shape"] == "rectangular"
and bottom_cross_section["shape"] == "rectangular"
):
frustum_volume = volume_from_height_rectangular(
target_height=target_height_relative,
total_frustum_height=frustum_height,
Expand All @@ -347,13 +360,12 @@ def _find_volume_in_partial_frustum(
partial_volume: Optional[float] = None
for bottom_cross_section, top_cross_section in get_boundary_pairs(sorted_frusta):
if (
bottom_cross_section["topHeight"]
< target_height
< top_cross_section["targetHeight"]
bottom_cross_section.topHeight < target_height
and target_height < top_cross_section.topHeight
):
relative_target_height = target_height - bottom_cross_section["topHeight"]
relative_target_height = target_height - bottom_cross_section.topHeight
frustum_height = (
top_cross_section["topHeight"] - bottom_cross_section["topHeight"]
top_cross_section.topHeight - bottom_cross_section.topHeight
)
partial_volume = volume_at_height_within_section(
top_cross_section=top_cross_section,
Expand Down Expand Up @@ -420,7 +432,7 @@ def _find_height_in_partial_frustum(
bottom_cross_section, top_cross_section = cross_sections
(bottom_height, bottom_volume), (top_height, top_volume) = capacity

if bottom_volume < target_volume < top_volume:
if bottom_volume < target_volume and target_volume < top_volume:
relative_target_volume = target_volume - bottom_volume
frustum_height = top_height - bottom_height
partial_height = height_at_volume_within_section(
Expand Down
28 changes: 27 additions & 1 deletion api/src/opentrons/protocol_engine/state/labware.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
from opentrons.protocol_engine.state import update_types
from opentrons_shared_data.deck.types import DeckDefinitionV5
from opentrons_shared_data.gripper.constants import LABWARE_GRIP_FORCE
from opentrons_shared_data.labware.labware_definition import LabwareRole
from opentrons_shared_data.labware.labware_definition import (
LabwareRole,
InnerWellGeometry,
)
from opentrons_shared_data.pipette.types import LabwareUri

from opentrons.types import DeckSlotName, StagingSlotName, MountType
Expand Down Expand Up @@ -462,6 +465,29 @@ def get_well_definition(
f"{well_name} does not exist in {labware_id}."
) from e

def get_well_geometry(
self, labware_id: str, well_name: Optional[str] = None
) -> InnerWellGeometry:
"""Get a well's inner geometry by labware and well name."""
labware_def = self.get_definition(labware_id)
if labware_def.innerLabwareGeometry is None:
raise errors.InvalidWellDefinitionError(
message=f"No innerLabwareGeometry found for labware_id: {labware_id}."
)
well_def = self.get_well_definition(labware_id, well_name)
well_id = well_def.geometryDefinitionId
if well_id is None:
raise errors.InvalidWellDefinitionError(
message=f"No geometryDefinitionId found for well: {well_name} in labware_id: {labware_id}"
)
else:
well_geometry = labware_def.innerLabwareGeometry.get(well_id)
if well_geometry is None:
raise errors.InvalidWellDefinitionError(
message=f"No innerLabwareGeometry found for well_id: {well_id} in labware_id: {labware_id}"
)
return well_geometry

def get_well_size(
self, labware_id: str, well_name: str
) -> Tuple[float, float, float]:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Test methods that translate well heights and volumes for GeometryView."""
from opentrons.protocol_engine.state.frustum_helpers import (
find_volume_at_well_height,
find_height_at_well_volume,
)
from ...protocol_runner.test_json_translator import _load_labware_definition_data


def test_find_volume_at_well_height() -> None:
"""Test find_volume_at_well_height."""
labware_def = _load_labware_definition_data()
assert labware_def.innerLabwareGeometry is not None
inner_well_def = labware_def.innerLabwareGeometry["welldefinition1111"]
result = find_volume_at_well_height(40.0, inner_well_def)
assert result == 1245.833 # use isclose() or something


def test_find_height_at_well_volume() -> None:
"""Test find_height_at_well_volume."""
labware_def = _load_labware_definition_data()
assert labware_def.innerLabwareGeometry is not None
inner_well_def = labware_def.innerLabwareGeometry["welldefinition1111"]
result = find_height_at_well_volume(1245.833, inner_well_def)
assert result == 40.0 # use isclose() or something
Loading