diff --git a/api/src/opentrons/protocol_engine/state/frustum_helpers.py b/api/src/opentrons/protocol_engine/state/frustum_helpers.py index 27e417aa8b4..3502f7ef96d 100644 --- a/api/src/opentrons/protocol_engine/state/frustum_helpers.py +++ b/api/src/opentrons/protocol_engine/state/frustum_helpers.py @@ -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: @@ -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"]) @@ -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, @@ -256,12 +257,12 @@ 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, @@ -269,7 +270,7 @@ def get_well_volumetric_capacity( 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." @@ -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, @@ -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, @@ -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, @@ -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( diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py index 0db6b310e1e..e2d7d8c7fc2 100644 --- a/api/src/opentrons/protocol_engine/state/labware.py +++ b/api/src/opentrons/protocol_engine/state/labware.py @@ -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 @@ -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]: diff --git a/api/tests/opentrons/protocol_engine/state/test_frustum_helpers.py b/api/tests/opentrons/protocol_engine/state/test_frustum_helpers.py new file mode 100644 index 00000000000..04dca79d764 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_frustum_helpers.py @@ -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