From 8da6f22d3b40eb4b39226422140cb6e5002ea32a Mon Sep 17 00:00:00 2001 From: caila-marashaj Date: Mon, 30 Sep 2024 16:24:33 -0400 Subject: [PATCH 01/35] changes to types and frustum_helpers --- .../protocol_engine/state/frustum_helpers.py | 276 ++++++++---------- shared-data/labware/schemas/3.json | 111 +++++-- .../labware/labware_definition.py | 110 +++++-- .../opentrons_shared_data/labware/types.py | 72 +++-- 4 files changed, 358 insertions(+), 211 deletions(-) diff --git a/api/src/opentrons/protocol_engine/state/frustum_helpers.py b/api/src/opentrons/protocol_engine/state/frustum_helpers.py index 27e417aa8b4..85bf252c1b3 100644 --- a/api/src/opentrons/protocol_engine/state/frustum_helpers.py +++ b/api/src/opentrons/protocol_engine/state/frustum_helpers.py @@ -4,12 +4,9 @@ from math import isclose from ..errors.exceptions import InvalidLiquidHeightFound, InvalidWellDefinitionError -from opentrons_shared_data.labware.types import ( - is_circular_frusta_list, - is_rectangular_frusta_list, - CircularBoundedSection, - RectangularBoundedSection, -) + +# from opentrons_shared_data.labware.types import ( +# ) from opentrons_shared_data.labware.labware_definition import InnerWellGeometry @@ -33,9 +30,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: Any) -> 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"]) @@ -212,15 +207,6 @@ def height_from_volume_spherical( return height -def get_boundary_pairs(frusta: Sequence[Any]) -> Iterator[Tuple[Any, Any]]: - """Yield tuples representing two cross-section boundaries of a segment of a well.""" - iter_f = iter(frusta) - el = next(iter_f) - for next_el in iter_f: - yield el, next_el - el = next_el - - def get_well_volumetric_capacity( well_geometry: InnerWellGeometry, ) -> List[Tuple[float, float]]: @@ -228,107 +214,109 @@ def get_well_volumetric_capacity( # dictionary map of heights to volumetric capacities within their respective segment # {top_height_0: volume_0, top_height_1: volume_1, top_height_2: volume_2} well_volume = [] - if well_geometry.bottomShape is not None: - if well_geometry.bottomShape.shape == "spherical": - bottom_spherical_section_depth = well_geometry.bottomShape.depth - bottom_sphere_volume = volume_from_height_spherical( - radius_of_curvature=well_geometry.bottomShape.radiusOfCurvature, - target_height=bottom_spherical_section_depth, + + # get the well segments sorted in ascending order + sorted_well = sorted(well_geometry.sections, key=lambda section: section.topHeight) + + for segment in sorted_well: + section_volume: Optional[float] = None + if segment.shape == "spherical": + if sorted_well[0] != segment: + raise InvalidWellDefinitionError( + "spherical segment must only be at the bottom of a well." + ) + section_volume = volume_from_height_spherical( + target_height=segment.topHeight, + radius_of_curvature=segment.radiusOfCurvature, ) - well_volume.append((bottom_spherical_section_depth, bottom_sphere_volume)) - - # get the volume of remaining frusta sorted in ascending order - sorted_frusta = sorted(well_geometry.frusta, key=lambda section: section.topHeight) - - 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"] - frustum_volume = volume_from_height_rectangular( - target_height=frustum_height, - total_frustum_height=frustum_height, - bottom_length=bottom_cross_section_length, - bottom_width=bottom_cross_section_width, - top_length=top_cross_section_length, - top_width=top_cross_section_width, + elif segment.shape == "rectangular": + section_height = segment.topHeight - segment.bottomHeight + section_volume = 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, ) - - 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"] - 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, + elif segment.shape == "circular": + section_height = segment.topHeight - segment.bottomHeight + section_volume = volume_from_height_circular( + target_height=section_height, + total_frustum_height=section_height, + bottom_radius=(segment.bottomDiameter / 2), + top_radius=(segment.topDiameter / 2), ) - - well_volume.append((next_f["topHeight"], frustum_volume)) - else: - raise NotImplementedError( - "Well section with differing boundary shapes not yet implemented." - ) + # TODO: implement volume calculations for truncated circular and rounded rectangular segments + if not section_volume: + raise NotImplementedError( + f"volume calculation for shape: {segment.shape} not yet implemented." + ) + well_volume.append((segment.topHeight, section_volume)) return well_volume def height_at_volume_within_section( - top_cross_section: Union[CircularBoundedSection, RectangularBoundedSection], - bottom_cross_section: Union[CircularBoundedSection, RectangularBoundedSection], + section: Any, target_volume_relative: float, - frustum_height: float, + section_height: float, ) -> float: """Calculate a height within a bounded section according to geometry.""" - if top_cross_section["shape"] == bottom_cross_section["shape"] == "circular": - frustum_height = height_from_volume_circular( + if section["shape"] == "spherical": + partial_height = height_from_volume_spherical( volume=target_volume_relative, - top_radius=(top_cross_section["diameter"] / 2), - bottom_radius=(bottom_cross_section["diameter"] / 2), - total_frustum_height=frustum_height, + total_frustum_height=section_height, + radius_of_curvature=section.radiusOfCurvature, ) - elif top_cross_section["shape"] == bottom_cross_section["shape"] == "rectangular": - frustum_height = height_from_volume_rectangular( + elif section["shape"] == "circular": + partial_height = height_from_volume_circular( volume=target_volume_relative, - total_frustum_height=frustum_height, - bottom_width=bottom_cross_section["xDimension"], - bottom_length=bottom_cross_section["yDimension"], - top_width=top_cross_section["xDimension"], - top_length=top_cross_section["yDimension"], + top_radius=(section["bottomDiameter"] / 2), + bottom_radius=(section["topDiameter"] / 2), + total_frustum_height=section_height, + ) + elif section["shape"] == "rectangular": + partial_height = height_from_volume_rectangular( + volume=target_volume_relative, + total_frustum_height=section_height, + bottom_width=section["bottomXDimension"], + bottom_length=section["bottomYDimension"], + top_width=section["topXDimension"], + top_length=section["topYDimension"], ) else: raise NotImplementedError( "Height from volume calculation not yet implemented for this well shape." ) - return frustum_height + return partial_height def volume_at_height_within_section( - top_cross_section: Union[CircularBoundedSection, RectangularBoundedSection], - bottom_cross_section: Union[CircularBoundedSection, RectangularBoundedSection], + section: Any, target_height_relative: float, - frustum_height: float, + section_height: float, ) -> float: """Calculate a volume within a bounded section according to geometry.""" - if top_cross_section["shape"] == bottom_cross_section["shape"] == "circular": - frustum_volume = volume_from_height_circular( + if section["shape"] == "spherical": + partial_volume = volume_from_height_spherical( target_height=target_height_relative, - total_frustum_height=frustum_height, - bottom_radius=(bottom_cross_section["diameter"] / 2), - top_radius=(top_cross_section["diameter"] / 2), + radius_of_curvature=section["radiusOfCurvature"], ) - elif top_cross_section["shape"] == bottom_cross_section["shape"] == "rectangular": - frustum_volume = volume_from_height_rectangular( + elif section["shape"] == "circular": + partial_volume = volume_from_height_circular( target_height=target_height_relative, - total_frustum_height=frustum_height, - bottom_width=bottom_cross_section["xDimension"], - bottom_length=bottom_cross_section["yDimension"], - top_width=top_cross_section["xDimension"], - top_length=top_cross_section["yDimension"], + total_frustum_height=section_height, + bottom_radius=(section["bottomDiameter"] / 2), + top_radius=(section["topDiameter"] / 2), + ) + elif section["shape"] == "rectangular": + partial_volume = 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"], ) # TODO(cm): this would be the NEST-96 2uL wells referenced in EXEC-712 # we need to input the math attached to that issue @@ -336,30 +324,29 @@ def volume_at_height_within_section( raise NotImplementedError( "Height from volume calculation not yet implemented for this well shape." ) - return frustum_volume + return partial_volume def _find_volume_in_partial_frustum( - sorted_frusta: List[Any], + sorted_well: List[Any], target_height: float, ) -> Optional[float]: """Look through a sorted list of frusta for a target height, and find the volume at that height.""" + 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"] - ): - relative_target_height = target_height - bottom_cross_section["topHeight"] - frustum_height = ( - top_cross_section["topHeight"] - bottom_cross_section["topHeight"] - ) + for segment in sorted_well: + if segment["bottomHeight"] < target_height < segment["topHeight"]: + relative_target_height = target_height - segment["bottomHeight"] + section_height = segment["topHeight"] - segment["bottomHeight"] partial_volume = volume_at_height_within_section( - top_cross_section=top_cross_section, - bottom_cross_section=bottom_cross_section, + section=segment, target_height_relative=relative_target_height, - frustum_height=frustum_height, + section_height=section_height, + ) + if not partial_volume: + # if we've looked through all sections and can't find the target volume, raise an error + raise InvalidLiquidHeightFound( + f"Unable to find volume at given well-height {target_height}." ) return partial_volume @@ -385,20 +372,11 @@ def find_volume_at_well_height( return closed_section_volume # find the section the target height is in and compute the volume # since bottomShape is not in list of frusta, check here first - if well_geometry.bottomShape: - bottom_segment_height = volumetric_capacity[0][0] - if ( - target_height < bottom_segment_height - and well_geometry.bottomShape.shape == "spherical" - ): - return volume_from_height_spherical( - target_height=target_height, - radius_of_curvature=well_geometry.bottomShape.radiusOfCurvature, - ) - sorted_frusta = sorted(well_geometry.frusta, key=lambda section: section.topHeight) + + sorted_well = sorted(well_geometry.sections, key=lambda section: section.topHeight) # TODO(cm): handle non-frustum section that is not at the bottom. partial_volume = _find_volume_in_partial_frustum( - sorted_frusta=sorted_frusta, + sorted_well=sorted_well, target_height=target_height, ) if not partial_volume: @@ -407,30 +385,29 @@ def find_volume_at_well_height( def _find_height_in_partial_frustum( - sorted_frusta: List[Any], + sorted_well: List[Any], volumetric_capacity: List[Tuple[float, float]], target_volume: float, ) -> Optional[float]: """Look through a sorted list of frusta for a target volume, and find the height at that volume.""" - well_height: Optional[float] = None - for cross_sections, capacity in zip( - get_boundary_pairs(sorted_frusta), - get_boundary_pairs(volumetric_capacity), - ): - bottom_cross_section, top_cross_section = cross_sections - (bottom_height, bottom_volume), (top_height, top_volume) = capacity - - if bottom_volume < target_volume < top_volume: - relative_target_volume = target_volume - bottom_volume - frustum_height = top_height - bottom_height + + bottom_section_volume = 0.0 + height_within_well: Optional[float] = None + for section, capacity in zip(sorted_well, volumetric_capacity): + section_top_height, section_volume = capacity + if bottom_section_volume < target_volume < section_volume: + relative_target_volume = target_volume - bottom_section_volume + relative_section_height = section["topHeight"] - section["bottomHeight"] partial_height = height_at_volume_within_section( - top_cross_section=top_cross_section, - bottom_cross_section=bottom_cross_section, + section=section, target_volume_relative=relative_target_volume, - frustum_height=frustum_height, + section_height=relative_section_height, ) - well_height = partial_height + bottom_height - return well_height + height_within_well = partial_height + section["bottomHeight"] + # bottom section volume should always be the volume enclosed in the previously + # viewed section + bottom_section_volume = section_volume + return height_within_well def find_height_at_well_volume( @@ -441,30 +418,19 @@ def find_height_at_well_volume( max_volume = volumetric_capacity[-1][1] if target_volume < 0 or target_volume > max_volume: raise InvalidLiquidHeightFound("Invalid target volume.") + for section_height, section_volume in volumetric_capacity: + if target_volume == section_volume: + return section_height - sorted_frusta = sorted(well_geometry.frusta, key=lambda section: section.topHeight) + sorted_well = sorted(well_geometry.sections, key=lambda section: section.topHeight) # find the section the target volume is in and compute the height - # since bottomShape is not in list of frusta, check here first - if well_geometry.bottomShape: - volume_within_bottom_segment = volumetric_capacity[0][1] - if ( - target_volume < volume_within_bottom_segment - and well_geometry.bottomShape.shape == "spherical" - ): - return height_from_volume_spherical( - volume=target_volume, - radius_of_curvature=well_geometry.bottomShape.radiusOfCurvature, - total_frustum_height=well_geometry.bottomShape.depth, - ) - # if bottom shape is present but doesn't contain the target volume, - # then we need to look through the volumetric capacity list without the bottom shape - # so volumetric_capacity and sorted_frusta will be aligned - volumetric_capacity.pop(0) well_height = _find_height_in_partial_frustum( - sorted_frusta=sorted_frusta, + sorted_well=sorted_well, volumetric_capacity=volumetric_capacity, target_volume=target_volume, ) if not well_height: - raise InvalidLiquidHeightFound("Unable to find height at given well-volume.") + raise InvalidLiquidHeightFound( + f"Unable to find height at given well-volume {target_volume}." + ) return well_height diff --git a/shared-data/labware/schemas/3.json b/shared-data/labware/schemas/3.json index e03b1c8f064..c88b4a7317d 100644 --- a/shared-data/labware/schemas/3.json +++ b/shared-data/labware/schemas/3.json @@ -68,7 +68,7 @@ "SphericalSegment": { "type": "object", "additionalProperties": false, - "required": ["shape", "radiusOfCurvature", "depth"], + "required": ["shape", "radiusOfCurvature", "topHeight"], "properties": { "shape": { "type": "string", @@ -77,70 +77,137 @@ "radiusOfCurvature": { "type": "number" }, - "depth": { + "topHeight": { "type": "number" } } }, - "CircularBoundedSection": { + "CircularFrustum": { "type": "object", - "required": ["shape", "diameter", "topHeight"], + "required": ["shape", "bottomDiameter", "TopDiameter", "topHeight", "bottomHeight"], "properties": { "shape": { "type": "string", "enum": ["circular"] }, - "diameter": { + "bottomDiameter": { + "type": "number" + }, + "topDiameter": { "type": "number" }, "topHeight": { - "type": "number", - "description": "The height at the top of a bounded subsection of a well, relative to the bottom" + "type": "number" + }, + "bottomHeight": { + "type": "number" } } }, - "RectangularBoundedSection": { + "RectangularFrustum": { "type": "object", - "required": ["shape", "xDimension", "yDimension", "topHeight"], + "required": ["shape", "bottomXDimension", "bottomYDimensions", "topXDimension", "topYDimension", "topHeight", "bottomHeight"], "properties": { "shape": { "type": "string", "enum": ["rectangular"] }, - "xDimension": { + "bottomXDimension": { "type": "number" }, - "yDimension": { + "bottomYDimension": { + "type": "number" + }, + "topXDimension": { + "type": "number" + }, + "topYDimension": { "type": "number" }, "topHeight": { - "type": "number", - "description": "The height at the top of a bounded subsection of a well, relative to the bottom" + "type": "number" + }, + "bottomHeight": { + "type": "number" + } + } + }, + "TruncatedCircularSegment": { + "type": "object", + "required": ["shape", "circleDiameter", "rectangleXDimension", "rectangleYDimension", "topHeight", "bottomHeight"], + "properties": { + "shape": { + "type": "string", + "enum": ["truncatedcircular"] + }, + "circleDiameter": { + "type": "number" + }, + "rectangleXDimension": { + "type": "number" + }, + "rectangleYDimension": { + "type": "number" + }, + "topHeight": { + "type": "number" + }, + "bottomHeight": { + "type": "number" + } + } + }, + "RoundedRectangularSegment": { + "type": "object", + "required": ["shape", "circleDiameter", "rectangleXDimension", "rectangleYDimension", "topHeight", "bottomHeight"], + "properties": { + "shape": { + "type": "string", + "enum": ["roundedrectangular"] + }, + "circleDiameter": { + "type": "number" + }, + "rectangleXDimension": { + "type": "number" + }, + "rectangleYDimension": { + "type": "number" + }, + "topHeight": { + "type": "number" + }, + "bottomHeight": { + "type": "number" } } }, "InnerWellGeometry": { "type": "object", - "required": ["frusta"], + "required": ["sections"], "properties": { - "frusta": { + "sections": { "description": "A list of all of the sections of the well that have a contiguous shape", "type": "array", "items": { "oneOf": [ { - "$ref": "#/definitions/CircularBoundedSection" + "$ref": "#/definitions/CircularFrustum" + }, + { + "$ref": "#/definitions/RectangularFrustum" + }, + { + "$ref": "#/definitions/TruncatedCircularSegment" }, { - "$ref": "#/definitions/RectangularBoundedSection" + "$ref": "#/definitions/RoundedRectangularSegment" + }, + { + "$ref": "#/definitions/SphericalSegment" } ] } - }, - "bottomShape": { - "type": "object", - "description": "The shape at the bottom of the well: either a spherical segment or a cross-section", - "$ref": "#/definitions/SphericalSegment" } } } diff --git a/shared-data/python/opentrons_shared_data/labware/labware_definition.py b/shared-data/python/opentrons_shared_data/labware/labware_definition.py index a6ee1804cde..e289a22b3c1 100644 --- a/shared-data/python/opentrons_shared_data/labware/labware_definition.py +++ b/shared-data/python/opentrons_shared_data/labware/labware_definition.py @@ -233,39 +233,112 @@ class SphericalSegment(BaseModel): ..., description="radius of curvature of bottom subsection of wells", ) - depth: _NonNegativeNumber = Field( + topHeight: _NonNegativeNumber = Field( ..., description="The depth of a spherical bottom of a well" ) -class CircularBoundedSection(BaseModel): +class CircularFrustum(BaseModel): shape: Literal["circular"] = Field(..., description="Denote shape as circular") - diameter: _NonNegativeNumber = Field( - ..., description="The diameter of a circular cross section of a well" + bottomDiameter: _NonNegativeNumber = Field( + ..., description="The diameter at the bottom cross-section of a circular frustum" + ) + topDiameter: _NonNegativeNumber = Field( + ..., description="The diameter at the top cross-section of a circular frustum" ) topHeight: _NonNegativeNumber = Field( ..., description="The height at the top of a bounded subsection of a well, relative to the bottom" "of the well", ) + bottomHeight: _NonNegativeNumber = Field( + ..., + description="The height at the bottom of a bounded subsection of a well, relative to the bottom of the well" + ) -class RectangularBoundedSection(BaseModel): +class RectangularFrustum(BaseModel): shape: Literal["rectangular"] = Field( ..., description="Denote shape as rectangular" ) - xDimension: _NonNegativeNumber = Field( + bottomXDimension: _NonNegativeNumber = Field( + ..., + description="x dimension of the bottom cross-section of a rectangular frustum", + ) + bottomYDimension: _NonNegativeNumber = Field( ..., - description="x dimension of a subsection of wells", + description="y dimension of the bottom cross-section of a rectangular frustum", ) - yDimension: _NonNegativeNumber = Field( + topXDimension: _NonNegativeNumber = Field( ..., - description="y dimension of a subsection of wells", + description="x dimension of the top cross-section of a rectangular frustum", + ) + topYDimension: _NonNegativeNumber = Field( + ..., + description="y dimension of the top cross-section of a rectangular frustum", ) topHeight: _NonNegativeNumber = Field( ..., description="The height at the top of a bounded subsection of a well, relative to the bottom" - "of the well", + "of the well", + ) + bottomHeight: _NonNegativeNumber = Field( + ..., + description="The height at the bottom of a bounded subsection of a well, relative to the bottom of the well" + ) + + +class TruncatedCircularSegment(BaseModel): + shape: Literal["truncatedcircular"] = Field( + ..., description="Denote shape as a truncated circular segment" + ), + circleDiameter: _NonNegativeNumber = Field( + ..., + description="diameter of the circular face of a truncated circular segment" + ), + rectangleXDimension: _NonNegativeNumber = Field( + ..., + description="x dimension of the rectangular face of a truncated circular segment" + ) + rectangleYDimension: _NonNegativeNumber = Field( + ..., + description="y dimension of the rectangular face of a truncated circular segment" + ) + topHeight: _NonNegativeNumber = Field( + ..., + description="The height at the top of a bounded subsection of a well, relative to the bottom" + "of the well", + ) + bottomHeight: _NonNegativeNumber = Field( + ..., + description="The height at the bottom of a bounded subsection of a well, relative to the bottom of the well" + ) + + +class RoundedRectangularSegment(BaseModel): + shape: Literal["roundedrectangular"] = Field( + ..., description="Denote shape as a rounded rectangular segment" + ), + circleDiameter: _NonNegativeNumber = Field( + ..., + description="diameter of the circular face of a rounded rectangular segment" + ), + rectangleXDimension: _NonNegativeNumber = Field( + ..., + description="x dimension of the rectangular face of a rounded rectangular segment" + ) + rectangleYDimension: _NonNegativeNumber = Field( + ..., + description="y dimension of the rectangular face of a rounded rectangular segment" + ) + topHeight: _NonNegativeNumber = Field( + ..., + description="The height at the top of a bounded subsection of a well, relative to the bottom" + "of the well", + ) + bottomHeight: _NonNegativeNumber = Field( + ..., + description="The height at the bottom of a bounded subsection of a well, relative to the bottom of the well" ) @@ -297,17 +370,20 @@ class Group(BaseModel): ) +WellSegment = Union[ + CircularFrustum, + RectangularFrustum, + TruncatedCircularSegment, + RoundedRectangularSegment, + SphericalSegment +] + + class InnerWellGeometry(BaseModel): - frusta: Union[ - List[CircularBoundedSection], List[RectangularBoundedSection] - ] = Field( + sections: List[WellSegment] = Field( ..., description="A list of all of the sections of the well that have a contiguous shape", ) - bottomShape: Optional[SphericalSegment] = Field( - None, - description="The shape at the bottom of the well: either a spherical segment or a cross-section", - ) class LabwareDefinition(BaseModel): diff --git a/shared-data/python/opentrons_shared_data/labware/types.py b/shared-data/python/opentrons_shared_data/labware/types.py index 9ea7a83fb6b..4bcf9c248a6 100644 --- a/shared-data/python/opentrons_shared_data/labware/types.py +++ b/shared-data/python/opentrons_shared_data/labware/types.py @@ -37,6 +37,8 @@ Circular = Literal["circular"] Rectangular = Literal["rectangular"] +TruncatedCircular = Literal["truncatedcircular"] +RoundedRectangular = Literal["roundedrectangular"] Spherical = Literal["spherical"] WellShape = Union[Circular, Rectangular] @@ -123,37 +125,73 @@ class WellGroup(TypedDict, total=False): class SphericalSegment(TypedDict): shape: Spherical radiusOfCurvature: float - depth: float + topHeight: float + + +# class RectangularBoundedSection(TypedDict): +# shape: Rectangular +# xDimension: float +# yDimension: float +# topHeight: float +# +# +# class CircularBoundedSection(TypedDict): +# shape: Circular +# diameter: float +# topHeight: float + + +class CircularFrustum(TypedDict): + shape: Circular + bottomDiameter: float + topDiameter: float + topHeight: float + bottom_height: float -class RectangularBoundedSection(TypedDict): +class RectangularFrustum(TypedDict): shape: Rectangular - xDimension: float - yDimension: float + bottomXDimension: float + bottomYDimension: float + topXDimension: float + topYDimension: float topHeight: float + bottom_height: float -class CircularBoundedSection(TypedDict): - shape: Circular - diameter: float +# A truncated circle is a square that is trimmed at the corners by a smaller circle +# that is concentric with the square. +class TruncatedCircularSegment(TypedDict): + shape: TruncatedCircular + circleDiameter: float + rectangleXDimension: float + rectangleYDimension: float topHeight: float + bottom_height: float -def is_circular_frusta_list( - items: List[Any], -) -> TypeGuard[List[CircularBoundedSection]]: - return all(item.shape == "circular" for item in items) +# A rounded rectangle is a rectangle that is filleted by 4 circles +# centered somewhere along the diagonals of the rectangle +class RoundedRectangularSegment(TypedDict): + shape: RoundedRectangular + circleDiameter: float + rectangleXDimension: float + rectangleYDimension: float + topHeight: float + bottom_height: float -def is_rectangular_frusta_list( - items: List[Any], -) -> TypeGuard[List[RectangularBoundedSection]]: - return all(item.shape == "rectangular" for item in items) +WellSegment = Union[ + CircularFrustum, + RectangularFrustum, + TruncatedCircularSegment, + RoundedRectangularSegment, + SphericalSegment +] class InnerWellGeometry(TypedDict): - frusta: Union[List[CircularBoundedSection], List[RectangularBoundedSection]] - bottomShape: Optional[SphericalSegment] + sections: List[WellSegment] class LabwareDefinition(TypedDict): From a9f7507d6b9b3c39aa5fc2a78eda461bf83085df Mon Sep 17 00:00:00 2001 From: caila-marashaj Date: Mon, 30 Sep 2024 16:54:09 -0400 Subject: [PATCH 02/35] fix api tests --- .../geometry/test_frustum_helpers.py | 148 +++++++++++------- .../opentrons_shared_data/labware/types.py | 8 +- 2 files changed, 97 insertions(+), 59 deletions(-) diff --git a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py index 0bf74aae5b2..79f9cbee801 100644 --- a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py +++ b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py @@ -3,15 +3,14 @@ from typing import Any, List from opentrons_shared_data.labware.types import ( - RectangularBoundedSection, - CircularBoundedSection, + CircularFrustum, + RectangularFrustum, SphericalSegment, ) from opentrons.protocol_engine.state.frustum_helpers import ( cross_section_area_rectangular, cross_section_area_circular, reject_unacceptable_heights, - get_boundary_pairs, circular_frustum_polynomial_roots, rectangular_frustum_polynomial_roots, volume_from_height_rectangular, @@ -29,59 +28,108 @@ def fake_frusta() -> List[List[Any]]: frusta = [] frusta.append( [ - RectangularBoundedSection( - shape="rectangular", xDimension=9.0, yDimension=10.0, topHeight=10.0 + RectangularFrustum( + shape="rectangular", + topXDimension=9.0, + topYDimension=10.0, + bottomXDimension=8.0, + bottomYDimension=9.0, + topHeight=10.0, + bottomHeight=5.0, ), - RectangularBoundedSection( - shape="rectangular", xDimension=8.0, yDimension=9.0, topHeight=5.0 + RectangularFrustum( + shape="rectangular", + topXDimension=8.0, + topYDimension=9.0, + bottomXDimension=15.0, + bottomYDimension=18.0, + topHeight=5.0, + bottomHeight=1.0, ), - CircularBoundedSection(shape="circular", diameter=23.0, topHeight=1.0), - SphericalSegment(shape="spherical", radiusOfCurvature=4.0, depth=1.0), + CircularFrustum( + shape="circular", + topDiameter=23.0, + bottomDiameter=3.0, + topHeight=1.0, + bottomHeight=1.0, + ), + SphericalSegment(shape="spherical", radiusOfCurvature=4.0, topHeight=1.0), ] ) frusta.append( [ - RectangularBoundedSection( - shape="rectangular", xDimension=8.0, yDimension=70.0, topHeight=3.5 - ), - RectangularBoundedSection( - shape="rectangular", xDimension=8.0, yDimension=75.0, topHeight=2.0 - ), - RectangularBoundedSection( - shape="rectangular", xDimension=8.0, yDimension=80.0, topHeight=1.0 + RectangularFrustum( + shape="rectangular", + topXDimension=8.0, + topYDimension=70.0, + bottomXDimension=7.0, + bottomYDimension=75.0, + topHeight=3.5, + bottomHeight=2.0, ), - RectangularBoundedSection( - shape="rectangular", xDimension=8.0, yDimension=90.0, topHeight=0.0 + RectangularFrustum( + shape="rectangular", + topXDimension=8.0, + topYDimension=80.0, + bottomXDimension=8.0, + bottomYDimension=90.0, + topHeight=1.0, + bottomHeight=0.0, ), ] ) frusta.append( [ - CircularBoundedSection(shape="circular", diameter=23.0, topHeight=7.5), - CircularBoundedSection(shape="circular", diameter=11.5, topHeight=5.0), - CircularBoundedSection(shape="circular", diameter=23.0, topHeight=2.5), - CircularBoundedSection(shape="circular", diameter=11.5, topHeight=0.0), + CircularFrustum( + shape="circular", + topDiameter=23.0, + bottomDiameter=11.5, + topHeight=7.5, + bottomHeight=5.0, + ), + CircularFrustum( + shape="circular", + topDiameter=11.5, + bottomDiameter=23.0, + topHeight=5.0, + bottomHeight=2.5, + ), + CircularFrustum( + shape="circular", + topDiameter=23.0, + bottomDiameter=11.5, + topHeight=2.5, + bottomHeight=0.0, + ), ] ) frusta.append( [ - CircularBoundedSection(shape="circular", diameter=4.0, topHeight=3.0), - CircularBoundedSection(shape="circular", diameter=5.0, topHeight=2.0), - SphericalSegment(shape="spherical", radiusOfCurvature=3.5, depth=2.0), + CircularFrustum( + shape="circular", + topDiameter=4.0, + bottomDiameter=5.0, + topHeight=3.0, + bottomHeight=2.0, + ), + SphericalSegment(shape="spherical", radiusOfCurvature=3.5, topHeight=2.0), ] ) frusta.append( - [SphericalSegment(shape="spherical", radiusOfCurvature=4.0, depth=3.0)] + [SphericalSegment(shape="spherical", radiusOfCurvature=4.0, topHeight=3.0)] ) frusta.append( [ - RectangularBoundedSection( - shape="rectangular", xDimension=27.0, yDimension=36.0, topHeight=3.5 - ), - RectangularBoundedSection( - shape="rectangular", xDimension=36.0, yDimension=26.0, topHeight=1.5 + RectangularFrustum( + shape="rectangular", + topXDimension=27.0, + topYDimension=36.0, + bottomXDimension=36.0, + bottomYDimension=26.0, + topHeight=3.5, + bottomHeight=1.5, ), - SphericalSegment(shape="spherical", radiusOfCurvature=4.0, depth=1.5), + SphericalSegment(shape="spherical", radiusOfCurvature=4.0, topHeight=1.5), ] ) return frusta @@ -132,26 +180,16 @@ def test_cross_section_area_rectangular(x_dimension: float, y_dimension: float) ) -@pytest.mark.parametrize("well", fake_frusta()) -def test_get_cross_section_boundaries(well: List[List[Any]]) -> None: - """Make sure get_cross_section_boundaries returns the expected list indices.""" - i = 0 - for f, next_f in get_boundary_pairs(well): - assert f == well[i] - assert next_f == well[i + 1] - i += 1 - - @pytest.mark.parametrize("well", fake_frusta()) def test_volume_and_height_circular(well: List[Any]) -> None: """Test both volume and height calculations for circular frusta.""" if well[-1]["shape"] == "spherical": return total_height = well[0]["topHeight"] - for f, next_f in get_boundary_pairs(well): - if f["shape"] == next_f["shape"] == "circular": - top_radius = next_f["diameter"] / 2 - bottom_radius = f["diameter"] / 2 + for segment in well: + if segment["shape"] == "circular": + top_radius = segment["topDiameter"] / 2 + bottom_radius = segment["bottomDiameter"] / 2 a = pi * ((top_radius - bottom_radius) ** 2) / (3 * total_height**2) b = pi * bottom_radius * (top_radius - bottom_radius) / total_height c = pi * bottom_radius**2 @@ -190,12 +228,12 @@ def test_volume_and_height_rectangular(well: List[Any]) -> None: if well[-1]["shape"] == "spherical": return total_height = well[0]["topHeight"] - for f, next_f in get_boundary_pairs(well): - if f["shape"] == next_f["shape"] == "rectangular": - top_length = next_f["yDimension"] - top_width = next_f["xDimension"] - bottom_length = f["yDimension"] - bottom_width = f["xDimension"] + for segment in well: + if segment["shape"] == "rectangular": + top_length = segment["topYDimension"] + top_width = segment["topXDimension"] + bottom_length = segment["bottomYDimension"] + bottom_width = segment["bottomXDimension"] a = ( (top_length - bottom_length) * (top_width - bottom_width) @@ -245,7 +283,7 @@ def test_volume_and_height_rectangular(well: List[Any]) -> None: def test_volume_and_height_spherical(well: List[Any]) -> None: """Test both volume and height calculations for spherical segments.""" if well[0]["shape"] == "spherical": - for target_height in range(round(well[0]["depth"])): + for target_height in range(round(well[0]["topHeight"])): expected_volume = ( (1 / 3) * pi @@ -260,6 +298,6 @@ def test_volume_and_height_spherical(well: List[Any]) -> None: found_height = height_from_volume_spherical( volume=found_volume, radius_of_curvature=well[0]["radiusOfCurvature"], - total_frustum_height=well[0]["depth"], + total_frustum_height=well[0]["topHeight"], ) assert isclose(found_height, target_height) diff --git a/shared-data/python/opentrons_shared_data/labware/types.py b/shared-data/python/opentrons_shared_data/labware/types.py index 4bcf9c248a6..cadb3da59a0 100644 --- a/shared-data/python/opentrons_shared_data/labware/types.py +++ b/shared-data/python/opentrons_shared_data/labware/types.py @@ -146,7 +146,7 @@ class CircularFrustum(TypedDict): bottomDiameter: float topDiameter: float topHeight: float - bottom_height: float + bottomHeight: float class RectangularFrustum(TypedDict): @@ -156,7 +156,7 @@ class RectangularFrustum(TypedDict): topXDimension: float topYDimension: float topHeight: float - bottom_height: float + bottomHeight: float # A truncated circle is a square that is trimmed at the corners by a smaller circle @@ -167,7 +167,7 @@ class TruncatedCircularSegment(TypedDict): rectangleXDimension: float rectangleYDimension: float topHeight: float - bottom_height: float + bottomHeight: float # A rounded rectangle is a rectangle that is filleted by 4 circles @@ -178,7 +178,7 @@ class RoundedRectangularSegment(TypedDict): rectangleXDimension: float rectangleYDimension: float topHeight: float - bottom_height: float + bottomHeight: float WellSegment = Union[ From e441baf501cb54012a0e289ac932ed85b89bce35 Mon Sep 17 00:00:00 2001 From: caila-marashaj Date: Mon, 30 Sep 2024 17:11:40 -0400 Subject: [PATCH 03/35] remaining test fixes --- .../protocol_engine/state/frustum_helpers.py | 6 +--- .../protocol_runner/test_json_translator.py | 32 +++++++++++-------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/api/src/opentrons/protocol_engine/state/frustum_helpers.py b/api/src/opentrons/protocol_engine/state/frustum_helpers.py index 85bf252c1b3..7718288bd1f 100644 --- a/api/src/opentrons/protocol_engine/state/frustum_helpers.py +++ b/api/src/opentrons/protocol_engine/state/frustum_helpers.py @@ -1,12 +1,10 @@ """Helper functions for liquid-level related calculations inside a given frustum.""" -from typing import List, Tuple, Iterator, Sequence, Any, Union, Optional +from typing import List, Tuple, Any, Optional from numpy import pi, iscomplex, roots, real from math import isclose from ..errors.exceptions import InvalidLiquidHeightFound, InvalidWellDefinitionError -# from opentrons_shared_data.labware.types import ( -# ) from opentrons_shared_data.labware.labware_definition import InnerWellGeometry @@ -332,7 +330,6 @@ def _find_volume_in_partial_frustum( target_height: float, ) -> Optional[float]: """Look through a sorted list of frusta for a target height, and find the volume at that height.""" - partial_volume: Optional[float] = None for segment in sorted_well: if segment["bottomHeight"] < target_height < segment["topHeight"]: @@ -390,7 +387,6 @@ def _find_height_in_partial_frustum( target_volume: float, ) -> Optional[float]: """Look through a sorted list of frusta for a target volume, and find the height at that volume.""" - bottom_section_volume = 0.0 height_within_well: Optional[float] = None for section, capacity in zip(sorted_well, volumetric_capacity): diff --git a/api/tests/opentrons/protocol_runner/test_json_translator.py b/api/tests/opentrons/protocol_runner/test_json_translator.py index 62181880dbc..73ee39bd481 100644 --- a/api/tests/opentrons/protocol_runner/test_json_translator.py +++ b/api/tests/opentrons/protocol_runner/test_json_translator.py @@ -13,7 +13,7 @@ Group, Metadata1, WellDefinition, - RectangularBoundedSection, + RectangularFrustum, InnerWellGeometry, SphericalSegment, ) @@ -690,25 +690,31 @@ def _load_labware_definition_data() -> LabwareDefinition: cornerOffsetFromSlot=CornerOffsetFromSlot(x=0, y=0, z=0), innerLabwareGeometry={ "welldefinition1111": InnerWellGeometry( - frusta=[ - RectangularBoundedSection( + sections=[ + RectangularFrustum( shape="rectangular", - xDimension=7.6, - yDimension=8.5, + topXDimension=7.6, + topYDimension=8.5, + bottomXDimension=5.6, + bottomYDimension=6.5, topHeight=45, + bottomHeight=20, ), - RectangularBoundedSection( + RectangularFrustum( shape="rectangular", - xDimension=5.6, - yDimension=6.5, + topXDimension=5.6, + topYDimension=6.5, + bottomXDimension=4.5, + bottomYDimension=4.0, topHeight=20, + bottomHeight=10, + ), + SphericalSegment( + shape="spherical", + radiusOfCurvature=6, + topHeight=10, ), ], - bottomShape=SphericalSegment( - shape="spherical", - radiusOfCurvature=6, - depth=10, - ), ) }, brand=BrandData(brand="foo"), From 5ef80206af652062b5af7d8225a64c7bf05ada81 Mon Sep 17 00:00:00 2001 From: caila-marashaj Date: Mon, 30 Sep 2024 17:27:02 -0400 Subject: [PATCH 04/35] fix shared-data lint --- .../labware/labware_definition.py | 40 ++++++++++--------- .../opentrons_shared_data/labware/types.py | 19 ++------- 2 files changed, 24 insertions(+), 35 deletions(-) diff --git a/shared-data/python/opentrons_shared_data/labware/labware_definition.py b/shared-data/python/opentrons_shared_data/labware/labware_definition.py index e289a22b3c1..382141d2a25 100644 --- a/shared-data/python/opentrons_shared_data/labware/labware_definition.py +++ b/shared-data/python/opentrons_shared_data/labware/labware_definition.py @@ -241,7 +241,8 @@ class SphericalSegment(BaseModel): class CircularFrustum(BaseModel): shape: Literal["circular"] = Field(..., description="Denote shape as circular") bottomDiameter: _NonNegativeNumber = Field( - ..., description="The diameter at the bottom cross-section of a circular frustum" + ..., + description="The diameter at the bottom cross-section of a circular frustum", ) topDiameter: _NonNegativeNumber = Field( ..., description="The diameter at the top cross-section of a circular frustum" @@ -253,7 +254,7 @@ class CircularFrustum(BaseModel): ) bottomHeight: _NonNegativeNumber = Field( ..., - description="The height at the bottom of a bounded subsection of a well, relative to the bottom of the well" + description="The height at the bottom of a bounded subsection of a well, relative to the bottom of the well", ) @@ -280,65 +281,66 @@ class RectangularFrustum(BaseModel): topHeight: _NonNegativeNumber = Field( ..., description="The height at the top of a bounded subsection of a well, relative to the bottom" - "of the well", + "of the well", ) bottomHeight: _NonNegativeNumber = Field( ..., - description="The height at the bottom of a bounded subsection of a well, relative to the bottom of the well" + description="The height at the bottom of a bounded subsection of a well, relative to the bottom of the well", ) class TruncatedCircularSegment(BaseModel): shape: Literal["truncatedcircular"] = Field( ..., description="Denote shape as a truncated circular segment" - ), + ) circleDiameter: _NonNegativeNumber = Field( ..., - description="diameter of the circular face of a truncated circular segment" - ), + description="diameter of the circular face of a truncated circular segment", + ) + rectangleXDimension: _NonNegativeNumber = Field( ..., - description="x dimension of the rectangular face of a truncated circular segment" + description="x dimension of the rectangular face of a truncated circular segment", ) rectangleYDimension: _NonNegativeNumber = Field( ..., - description="y dimension of the rectangular face of a truncated circular segment" + description="y dimension of the rectangular face of a truncated circular segment", ) topHeight: _NonNegativeNumber = Field( ..., description="The height at the top of a bounded subsection of a well, relative to the bottom" - "of the well", + "of the well", ) bottomHeight: _NonNegativeNumber = Field( ..., - description="The height at the bottom of a bounded subsection of a well, relative to the bottom of the well" + description="The height at the bottom of a bounded subsection of a well, relative to the bottom of the well", ) class RoundedRectangularSegment(BaseModel): shape: Literal["roundedrectangular"] = Field( ..., description="Denote shape as a rounded rectangular segment" - ), + ) circleDiameter: _NonNegativeNumber = Field( ..., - description="diameter of the circular face of a rounded rectangular segment" - ), + description="diameter of the circular face of a rounded rectangular segment", + ) rectangleXDimension: _NonNegativeNumber = Field( ..., - description="x dimension of the rectangular face of a rounded rectangular segment" + description="x dimension of the rectangular face of a rounded rectangular segment", ) rectangleYDimension: _NonNegativeNumber = Field( ..., - description="y dimension of the rectangular face of a rounded rectangular segment" + description="y dimension of the rectangular face of a rounded rectangular segment", ) topHeight: _NonNegativeNumber = Field( ..., description="The height at the top of a bounded subsection of a well, relative to the bottom" - "of the well", + "of the well", ) bottomHeight: _NonNegativeNumber = Field( ..., - description="The height at the bottom of a bounded subsection of a well, relative to the bottom of the well" + description="The height at the bottom of a bounded subsection of a well, relative to the bottom of the well", ) @@ -375,7 +377,7 @@ class Group(BaseModel): RectangularFrustum, TruncatedCircularSegment, RoundedRectangularSegment, - SphericalSegment + SphericalSegment, ] diff --git a/shared-data/python/opentrons_shared_data/labware/types.py b/shared-data/python/opentrons_shared_data/labware/types.py index cadb3da59a0..499500e2313 100644 --- a/shared-data/python/opentrons_shared_data/labware/types.py +++ b/shared-data/python/opentrons_shared_data/labware/types.py @@ -3,8 +3,8 @@ types in this file by and large require the use of typing_extensions. this module shouldn't be imported unless typing.TYPE_CHECKING is true. """ -from typing import Dict, List, NewType, Union, Optional, Any -from typing_extensions import Literal, TypedDict, NotRequired, TypeGuard +from typing import Dict, List, NewType, Union +from typing_extensions import Literal, TypedDict, NotRequired LabwareUri = NewType("LabwareUri", str) @@ -128,19 +128,6 @@ class SphericalSegment(TypedDict): topHeight: float -# class RectangularBoundedSection(TypedDict): -# shape: Rectangular -# xDimension: float -# yDimension: float -# topHeight: float -# -# -# class CircularBoundedSection(TypedDict): -# shape: Circular -# diameter: float -# topHeight: float - - class CircularFrustum(TypedDict): shape: Circular bottomDiameter: float @@ -186,7 +173,7 @@ class RoundedRectangularSegment(TypedDict): RectangularFrustum, TruncatedCircularSegment, RoundedRectangularSegment, - SphericalSegment + SphericalSegment, ] From 90f6182fe0b5248ea2a1b1be230c9065cef4e11f Mon Sep 17 00:00:00 2001 From: caila-marashaj Date: Mon, 30 Sep 2024 18:34:13 -0400 Subject: [PATCH 05/35] some fixture changes, json schema test still failing --- .../labware/fixtures/3/fixture_2_plate.json | 38 ++++++++----------- .../fixtures/3/fixture_corning_24_plate.json | 13 +++---- 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/shared-data/labware/fixtures/3/fixture_2_plate.json b/shared-data/labware/fixtures/3/fixture_2_plate.json index a2e1bb5a3ea..b77a060dda6 100644 --- a/shared-data/labware/fixtures/3/fixture_2_plate.json +++ b/shared-data/labware/fixtures/3/fixture_2_plate.json @@ -62,39 +62,33 @@ }, "innerLabwareGeometry": { "daiwudhadfhiew": { - "frusta": [ + "sections": [ { "shape": "rectangular", - "xDimension": 127.76, - "yDimension": 85.8, - "topHeight": 42.16 - }, - { - "shape": "rectangular", - "xDimension": 70.0, - "yDimension": 50.0, - "topHeight": 20.0 + "topXDimension": 127.76, + "topYDimension": 85.8, + "bottomXDimension": 70.0, + "bottomYDimension": 50.0, + "topHeight": 42.16, + "bottomHeight": 20.0 } ] }, "iuweofiuwhfn": { - "frusta": [ + "sections": [ { "shape": "circular", - "diameter": 35.0, - "topHeight": 42.16 + "bottomDiameter": 35.0, + "topDiameter": 35.0, + "topHeight": 42.16, + "bottomHeight": 10.0 }, { - "shape": "circular", - "diameter": 35.0, - "topHeight": 20.0 + "shape": "spherical", + "radiusOfCurvature": 20.0, + "topHeight": 10.0 } - ], - "bottomShape": { - "shape": "spherical", - "radiusOfCurvature": 20.0, - "depth": 6.0 - } + ] } } } diff --git a/shared-data/labware/fixtures/3/fixture_corning_24_plate.json b/shared-data/labware/fixtures/3/fixture_corning_24_plate.json index d53a6f017ca..c912d66c7df 100644 --- a/shared-data/labware/fixtures/3/fixture_corning_24_plate.json +++ b/shared-data/labware/fixtures/3/fixture_corning_24_plate.json @@ -323,16 +323,13 @@ }, "innerLabwareGeometry": { "venirhgerug": { - "frusta": [ + "sections": [ { "shape": "circular", - "diameter": 16.26, - "topHeight": 17.4 - }, - { - "shape": "circular", - "diameter": 16.26, - "topHeight": 0.0 + "bottomDiameter": 16.26, + "topDiameter": 16.26, + "topHeight": 17.4, + "bottomHeight": 0.0 } ] } From e634f6268e4e2659fcd0dadd4d152081bac1fb0a Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Tue, 1 Oct 2024 17:23:12 -0400 Subject: [PATCH 06/35] provide method to cast labware defs and types to typeddict --- .../python/opentrons_shared_data/labware/types.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/shared-data/python/opentrons_shared_data/labware/types.py b/shared-data/python/opentrons_shared_data/labware/types.py index 499500e2313..a491b67bbc9 100644 --- a/shared-data/python/opentrons_shared_data/labware/types.py +++ b/shared-data/python/opentrons_shared_data/labware/types.py @@ -5,7 +5,7 @@ """ from typing import Dict, List, NewType, Union from typing_extensions import Literal, TypedDict, NotRequired - +from .labware_definition import WellSegment as WellSegmentDef LabwareUri = NewType("LabwareUri", str) @@ -176,10 +176,21 @@ class RoundedRectangularSegment(TypedDict): SphericalSegment, ] +@staticmethod +def ToWellSegmentDict(segment: Union[WellSegment,WellSegmentDef]) -> WellSegment: + if isinstance(segment, WellSegmentDef): + return typing.cast(WellSegment, segment.model_dump(exclude_none=True, exclude_unset=True)) + return segment + class InnerWellGeometry(TypedDict): sections: List[WellSegment] +@staticmethod +def ToInnerWellGeometryDict(inner_well_geometry: Union[InnerWellGeometry,InnerWellGeometryDef]) -> InnerWellGeometry: + return InnerWellGeometry([ToWellSegmentDict(section) for section in inner_well_geometry["sections"]]) + + class LabwareDefinition(TypedDict): schemaVersion: Literal[2] From 6ac6574d408f4d8644b7c496eba5c0ed482a51c2 Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Tue, 1 Oct 2024 17:26:53 -0400 Subject: [PATCH 07/35] Mark some methods as non-public --- .../protocol_engine/state/frustum_helpers.py | 54 +++++++++---------- .../state/test_geometry_view.py | 16 +++--- .../geometry/test_frustum_helpers.py | 48 +++++++++-------- 3 files changed, 60 insertions(+), 58 deletions(-) diff --git a/api/src/opentrons/protocol_engine/state/frustum_helpers.py b/api/src/opentrons/protocol_engine/state/frustum_helpers.py index 7718288bd1f..bdd92dae224 100644 --- a/api/src/opentrons/protocol_engine/state/frustum_helpers.py +++ b/api/src/opentrons/protocol_engine/state/frustum_helpers.py @@ -8,7 +8,7 @@ from opentrons_shared_data.labware.labware_definition import InnerWellGeometry -def reject_unacceptable_heights( +def _reject_unacceptable_heights( potential_heights: List[float], max_height: float ) -> float: """Reject any solutions to a polynomial equation that cannot be the height of a frustum.""" @@ -42,18 +42,18 @@ def get_cross_section_area(bounded_section: Any) -> float: return cross_section_area -def cross_section_area_circular(diameter: float) -> float: +def _cross_section_area_circular(diameter: float) -> float: """Get the area of a circular cross-section.""" radius = diameter / 2 return pi * (radius**2) -def cross_section_area_rectangular(x_dimension: float, y_dimension: float) -> float: +def _cross_section_area_rectangular(x_dimension: float, y_dimension: float) -> float: """Get the area of a rectangular cross-section.""" return x_dimension * y_dimension -def rectangular_frustum_polynomial_roots( +def _rectangular_frustum_polynomial_roots( bottom_length: float, bottom_width: float, top_length: float, @@ -75,7 +75,7 @@ def rectangular_frustum_polynomial_roots( return a, b, c -def circular_frustum_polynomial_roots( +def _circular_frustum_polynomial_roots( bottom_radius: float, top_radius: float, total_frustum_height: float, @@ -88,14 +88,14 @@ def circular_frustum_polynomial_roots( return a, b, c -def volume_from_height_circular( +def _volume_from_height_circular( target_height: float, total_frustum_height: float, bottom_radius: float, top_radius: float, ) -> float: """Find the volume given a height within a circular frustum.""" - a, b, c = circular_frustum_polynomial_roots( + a, b, c = _circular_frustum_polynomial_roots( bottom_radius=bottom_radius, top_radius=top_radius, total_frustum_height=total_frustum_height, @@ -104,7 +104,7 @@ def volume_from_height_circular( return volume -def volume_from_height_rectangular( +def _volume_from_height_rectangular( target_height: float, total_frustum_height: float, bottom_length: float, @@ -113,7 +113,7 @@ def volume_from_height_rectangular( top_width: float, ) -> float: """Find the volume given a height within a rectangular frustum.""" - a, b, c = rectangular_frustum_polynomial_roots( + a, b, c = _rectangular_frustum_polynomial_roots( bottom_length=bottom_length, bottom_width=bottom_width, top_length=top_length, @@ -124,7 +124,7 @@ def volume_from_height_rectangular( return volume -def volume_from_height_spherical( +def _volume_from_height_spherical( target_height: float, radius_of_curvature: float, ) -> float: @@ -135,14 +135,14 @@ def volume_from_height_spherical( return volume -def height_from_volume_circular( +def _height_from_volume_circular( volume: float, total_frustum_height: float, bottom_radius: float, top_radius: float, ) -> float: """Find the height given a volume within a circular frustum.""" - a, b, c = circular_frustum_polynomial_roots( + a, b, c = _circular_frustum_polynomial_roots( bottom_radius=bottom_radius, top_radius=top_radius, total_frustum_height=total_frustum_height, @@ -151,14 +151,14 @@ def height_from_volume_circular( x_intercept_roots = (a, b, c, d) height_from_volume_roots = roots(x_intercept_roots) - height = reject_unacceptable_heights( + height = _reject_unacceptable_heights( potential_heights=list(height_from_volume_roots), max_height=total_frustum_height, ) return height -def height_from_volume_rectangular( +def _height_from_volume_rectangular( volume: float, total_frustum_height: float, bottom_length: float, @@ -167,7 +167,7 @@ def height_from_volume_rectangular( top_width: float, ) -> float: """Find the height given a volume within a rectangular frustum.""" - a, b, c = rectangular_frustum_polynomial_roots( + a, b, c = _rectangular_frustum_polynomial_roots( bottom_length=bottom_length, bottom_width=bottom_width, top_length=top_length, @@ -178,14 +178,14 @@ def height_from_volume_rectangular( x_intercept_roots = (a, b, c, d) height_from_volume_roots = roots(x_intercept_roots) - height = reject_unacceptable_heights( + height = _reject_unacceptable_heights( potential_heights=list(height_from_volume_roots), max_height=total_frustum_height, ) return height -def height_from_volume_spherical( +def _height_from_volume_spherical( volume: float, radius_of_curvature: float, total_frustum_height: float, @@ -198,7 +198,7 @@ def height_from_volume_spherical( x_intercept_roots = (a, b, c, d) height_from_volume_roots = roots(x_intercept_roots) - height = reject_unacceptable_heights( + height = _reject_unacceptable_heights( potential_heights=list(height_from_volume_roots), max_height=total_frustum_height, ) @@ -223,13 +223,13 @@ def get_well_volumetric_capacity( raise InvalidWellDefinitionError( "spherical segment must only be at the bottom of a well." ) - section_volume = volume_from_height_spherical( target_height=segment.topHeight, radius_of_curvature=segment.radiusOfCurvature, + section_volume = _volume_from_height_spherical( ) elif segment.shape == "rectangular": section_height = segment.topHeight - segment.bottomHeight - section_volume = volume_from_height_rectangular( + section_volume = _volume_from_height_rectangular( target_height=section_height, bottom_length=segment.bottomYDimension, bottom_width=segment.bottomXDimension, @@ -239,7 +239,7 @@ def get_well_volumetric_capacity( ) elif segment.shape == "circular": section_height = segment.topHeight - segment.bottomHeight - section_volume = volume_from_height_circular( + section_volume = _volume_from_height_circular( target_height=section_height, total_frustum_height=section_height, bottom_radius=(segment.bottomDiameter / 2), @@ -261,20 +261,20 @@ def height_at_volume_within_section( ) -> float: """Calculate a height within a bounded section according to geometry.""" if section["shape"] == "spherical": - partial_height = height_from_volume_spherical( + partial_height = _height_from_volume_spherical( volume=target_volume_relative, total_frustum_height=section_height, radius_of_curvature=section.radiusOfCurvature, ) elif section["shape"] == "circular": - partial_height = height_from_volume_circular( + partial_height = _height_from_volume_circular( volume=target_volume_relative, top_radius=(section["bottomDiameter"] / 2), bottom_radius=(section["topDiameter"] / 2), total_frustum_height=section_height, ) elif section["shape"] == "rectangular": - partial_height = height_from_volume_rectangular( + partial_height = _height_from_volume_rectangular( volume=target_volume_relative, total_frustum_height=section_height, bottom_width=section["bottomXDimension"], @@ -296,19 +296,19 @@ def volume_at_height_within_section( ) -> float: """Calculate a volume within a bounded section according to geometry.""" if section["shape"] == "spherical": - partial_volume = volume_from_height_spherical( + partial_volume = _volume_from_height_spherical( target_height=target_height_relative, radius_of_curvature=section["radiusOfCurvature"], ) elif section["shape"] == "circular": - partial_volume = volume_from_height_circular( + partial_volume = _volume_from_height_circular( target_height=target_height_relative, total_frustum_height=section_height, bottom_radius=(section["bottomDiameter"] / 2), top_radius=(section["topDiameter"] / 2), ) elif section["shape"] == "rectangular": - partial_volume = volume_from_height_rectangular( + partial_volume = _volume_from_height_rectangular( target_height=target_height_relative, total_frustum_height=section_height, bottom_width=section["BottomXDimension"], diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index 6bbd13c5e25..427dececa7b 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -83,10 +83,10 @@ ) from opentrons.protocol_engine.state.geometry import GeometryView, _GripperMoveType from opentrons.protocol_engine.state.frustum_helpers import ( - height_from_volume_circular, - height_from_volume_rectangular, - volume_from_height_circular, - volume_from_height_rectangular, + _height_from_volume_circular, + _height_from_volume_rectangular, + _volume_from_height_circular, + _volume_from_height_rectangular, ) from ..pipette_fixtures import get_default_nozzle_map from ..mock_circular_frusta import TEST_EXAMPLES as CIRCULAR_TEST_EXAMPLES @@ -2776,7 +2776,7 @@ def _find_volume_from_height_(index: int) -> None: top_width = frustum["width"][index] target_height = frustum["height"][index] - found_volume = volume_from_height_rectangular( + found_volume = _volume_from_height_rectangular( target_height=target_height, total_frustum_height=total_frustum_height, top_length=top_length, @@ -2785,7 +2785,7 @@ def _find_volume_from_height_(index: int) -> None: bottom_width=bottom_width, ) - found_height = height_from_volume_rectangular( + found_height = _height_from_volume_rectangular( volume=found_volume, total_frustum_height=total_frustum_height, top_length=top_length, @@ -2815,14 +2815,14 @@ def _find_volume_from_height_(index: int) -> None: top_radius = frustum["radius"][index] target_height = frustum["height"][index] - found_volume = volume_from_height_circular( + found_volume = _volume_from_height_circular( target_height=target_height, total_frustum_height=total_frustum_height, top_radius=top_radius, bottom_radius=bottom_radius, ) - found_height = height_from_volume_circular( + found_height = _height_from_volume_circular( volume=found_volume, total_frustum_height=total_frustum_height, top_radius=top_radius, diff --git a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py index 79f9cbee801..98a5f4e9100 100644 --- a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py +++ b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py @@ -8,17 +8,17 @@ SphericalSegment, ) from opentrons.protocol_engine.state.frustum_helpers import ( - cross_section_area_rectangular, - cross_section_area_circular, - reject_unacceptable_heights, - circular_frustum_polynomial_roots, - rectangular_frustum_polynomial_roots, - volume_from_height_rectangular, - volume_from_height_circular, - volume_from_height_spherical, - height_from_volume_circular, - height_from_volume_rectangular, - height_from_volume_spherical, + _cross_section_area_rectangular, + _cross_section_area_circular, + _reject_unacceptable_heights, + _circular_frustum_polynomial_roots, + _rectangular_frustum_polynomial_roots, + _volume_from_height_rectangular, + _volume_from_height_circular, + _volume_from_height_spherical, + _height_from_volume_circular, + _height_from_volume_rectangular, + _height_from_volume_spherical, ) from opentrons.protocol_engine.errors.exceptions import InvalidLiquidHeightFound @@ -151,11 +151,11 @@ def test_reject_unacceptable_heights( """Make sure we reject all mathematical solutions that are physically not possible.""" if len(expected_heights) != 1: with pytest.raises(InvalidLiquidHeightFound): - reject_unacceptable_heights( + _reject_unacceptable_heights( max_height=max_height, potential_heights=potential_heights ) else: - found_heights = reject_unacceptable_heights( + found_heights = _reject_unacceptable_heights( max_height=max_height, potential_heights=potential_heights ) assert found_heights == expected_heights[0] @@ -165,7 +165,7 @@ def test_reject_unacceptable_heights( def test_cross_section_area_circular(diameter: float) -> None: """Test circular area calculation.""" expected_area = pi * (diameter / 2) ** 2 - assert cross_section_area_circular(diameter) == expected_area + assert _cross_section_area_circular(diameter) == expected_area @pytest.mark.parametrize( @@ -175,7 +175,9 @@ def test_cross_section_area_rectangular(x_dimension: float, y_dimension: float) """Test rectangular area calculation.""" expected_area = x_dimension * y_dimension assert ( - cross_section_area_rectangular(x_dimension=x_dimension, y_dimension=y_dimension) + _cross_section_area_rectangular( + x_dimension=x_dimension, y_dimension=y_dimension + ) == expected_area ) @@ -193,7 +195,7 @@ def test_volume_and_height_circular(well: List[Any]) -> None: a = pi * ((top_radius - bottom_radius) ** 2) / (3 * total_height**2) b = pi * bottom_radius * (top_radius - bottom_radius) / total_height c = pi * bottom_radius**2 - assert circular_frustum_polynomial_roots( + assert _circular_frustum_polynomial_roots( top_radius=top_radius, bottom_radius=bottom_radius, total_frustum_height=total_height, @@ -205,7 +207,7 @@ def test_volume_and_height_circular(well: List[Any]) -> None: + b * (target_height**2) + c * target_height ) - found_volume = volume_from_height_circular( + found_volume = _volume_from_height_circular( target_height=target_height, total_frustum_height=total_height, bottom_radius=bottom_radius, @@ -213,7 +215,7 @@ def test_volume_and_height_circular(well: List[Any]) -> None: ) assert found_volume == expected_volume # test going backwards to get height back - found_height = height_from_volume_circular( + found_height = _height_from_volume_circular( volume=found_volume, total_frustum_height=total_height, bottom_radius=bottom_radius, @@ -244,7 +246,7 @@ def test_volume_and_height_rectangular(well: List[Any]) -> None: + (bottom_width * (top_length - bottom_length)) ) / (2 * total_height) c = bottom_length * bottom_width - assert rectangular_frustum_polynomial_roots( + assert _rectangular_frustum_polynomial_roots( top_length=top_length, bottom_length=bottom_length, top_width=top_width, @@ -258,7 +260,7 @@ def test_volume_and_height_rectangular(well: List[Any]) -> None: + b * (target_height**2) + c * target_height ) - found_volume = volume_from_height_rectangular( + found_volume = _volume_from_height_rectangular( target_height=target_height, total_frustum_height=total_height, bottom_length=bottom_length, @@ -268,7 +270,7 @@ def test_volume_and_height_rectangular(well: List[Any]) -> None: ) assert found_volume == expected_volume # test going backwards to get height back - found_height = height_from_volume_rectangular( + found_height = _height_from_volume_rectangular( volume=found_volume, total_frustum_height=total_height, bottom_length=bottom_length, @@ -290,12 +292,12 @@ def test_volume_and_height_spherical(well: List[Any]) -> None: * (target_height**2) * (3 * well[0]["radiusOfCurvature"] - target_height) ) - found_volume = volume_from_height_spherical( + found_volume = _volume_from_height_spherical( target_height=target_height, radius_of_curvature=well[0]["radiusOfCurvature"], ) assert found_volume == expected_volume - found_height = height_from_volume_spherical( + found_height = _height_from_volume_spherical( volume=found_volume, radius_of_curvature=well[0]["radiusOfCurvature"], total_frustum_height=well[0]["topHeight"], From d5ad5b7de9566e360cc740ee8c29eefa6ce8335e Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Tue, 1 Oct 2024 17:30:10 -0400 Subject: [PATCH 08/35] Enforce typing and type checking --- .../protocol_engine/state/frustum_helpers.py | 88 ++++++++++++------- 1 file changed, 54 insertions(+), 34 deletions(-) diff --git a/api/src/opentrons/protocol_engine/state/frustum_helpers.py b/api/src/opentrons/protocol_engine/state/frustum_helpers.py index bdd92dae224..95753e4f4e7 100644 --- a/api/src/opentrons/protocol_engine/state/frustum_helpers.py +++ b/api/src/opentrons/protocol_engine/state/frustum_helpers.py @@ -1,11 +1,20 @@ """Helper functions for liquid-level related calculations inside a given frustum.""" -from typing import List, Tuple, Any, Optional +from typing import List, Tuple, Optional, Union from numpy import pi, iscomplex, roots, real from math import isclose from ..errors.exceptions import InvalidLiquidHeightFound, InvalidWellDefinitionError -from opentrons_shared_data.labware.labware_definition import InnerWellGeometry +from opentrons_shared_data.labware.types import ( + InnerWellGeometry, + WellSegment, + ToWellSegmentDict, + ToInnerWellGeometryDict, +) +from opentrons_shared_data.labware.labware_definition import ( + InnerWellGeometry as InnerWellGeometryDef, + WellSegment as WellSegmentDef, +) def _reject_unacceptable_heights( @@ -206,15 +215,18 @@ def _height_from_volume_spherical( def get_well_volumetric_capacity( - well_geometry: InnerWellGeometry, + well_geometry: Union[InnerWellGeometry, InnerWellGeometryDef], ) -> List[Tuple[float, float]]: """Return the total volumetric capacity of a well as a map of height borders to volume.""" + checked_geometry = ToInnerWellGeometryDict(well_geometry) # dictionary map of heights to volumetric capacities within their respective segment # {top_height_0: volume_0, top_height_1: volume_1, top_height_2: volume_2} well_volume = [] # get the well segments sorted in ascending order - sorted_well = sorted(well_geometry.sections, key=lambda section: section.topHeight) + sorted_well = sorted( + checked_geometry["sections"], key=lambda section: section["topHeight"] + ) for segment in sorted_well: section_volume: Optional[float] = None @@ -255,32 +267,33 @@ def get_well_volumetric_capacity( def height_at_volume_within_section( - section: Any, + section: Union[WellSegment, WellSegmentDef], target_volume_relative: float, section_height: float, ) -> float: """Calculate a height within a bounded section according to geometry.""" - if section["shape"] == "spherical": + checked_section = ToWellSegmentDict(section) + if checked_section["shape"] == "spherical": partial_height = _height_from_volume_spherical( volume=target_volume_relative, total_frustum_height=section_height, - radius_of_curvature=section.radiusOfCurvature, + radius_of_curvature=checked_section["radiusOfCurvature"], ) - elif section["shape"] == "circular": + elif checked_section["shape"] == "circular": partial_height = _height_from_volume_circular( volume=target_volume_relative, - top_radius=(section["bottomDiameter"] / 2), - bottom_radius=(section["topDiameter"] / 2), + top_radius=(checked_section["bottomDiameter"] / 2), + bottom_radius=(checked_section["topDiameter"] / 2), total_frustum_height=section_height, ) - elif section["shape"] == "rectangular": + elif checked_section["shape"] == "rectangular": partial_height = _height_from_volume_rectangular( volume=target_volume_relative, total_frustum_height=section_height, - bottom_width=section["bottomXDimension"], - bottom_length=section["bottomYDimension"], - top_width=section["topXDimension"], - top_length=section["topYDimension"], + bottom_width=checked_section["bottomXDimension"], + bottom_length=checked_section["bottomYDimension"], + top_width=checked_section["topXDimension"], + top_length=checked_section["topYDimension"], ) else: raise NotImplementedError( @@ -290,31 +303,32 @@ def height_at_volume_within_section( def volume_at_height_within_section( - section: Any, + section: Union[WellSegment, WellSegmentDef], target_height_relative: float, section_height: float, ) -> float: """Calculate a volume within a bounded section according to geometry.""" - if section["shape"] == "spherical": + checked_section = ToWellSegmentDict(section) + if checked_section["shape"] == "spherical": partial_volume = _volume_from_height_spherical( target_height=target_height_relative, - radius_of_curvature=section["radiusOfCurvature"], + radius_of_curvature=checked_section["radiusOfCurvature"], ) - elif section["shape"] == "circular": + elif checked_section["shape"] == "circular": partial_volume = _volume_from_height_circular( target_height=target_height_relative, total_frustum_height=section_height, - bottom_radius=(section["bottomDiameter"] / 2), - top_radius=(section["topDiameter"] / 2), + bottom_radius=(checked_section["bottomDiameter"] / 2), + top_radius=(checked_section["topDiameter"] / 2), ) - elif section["shape"] == "rectangular": + elif checked_section["shape"] == "rectangular": partial_volume = _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"], + bottom_width=checked_section["bottomXDimension"], + bottom_length=checked_section["bottomYDimension"], + top_width=checked_section["topXDimension"], + top_length=checked_section["topYDimension"], ) # TODO(cm): this would be the NEST-96 2uL wells referenced in EXEC-712 # we need to input the math attached to that issue @@ -326,7 +340,7 @@ def volume_at_height_within_section( def _find_volume_in_partial_frustum( - sorted_well: List[Any], + sorted_well: List[WellSegment], target_height: float, ) -> Optional[float]: """Look through a sorted list of frusta for a target height, and find the volume at that height.""" @@ -349,10 +363,11 @@ def _find_volume_in_partial_frustum( def find_volume_at_well_height( - target_height: float, well_geometry: InnerWellGeometry + target_height: float, well_geometry: Union[InnerWellGeometry, InnerWellGeometryDef] ) -> float: """Find the volume within a well, at a known height.""" - volumetric_capacity = get_well_volumetric_capacity(well_geometry) + checked_geometry = ToInnerWellGeometryDict(well_geometry) + volumetric_capacity = get_well_volumetric_capacity(checked_geometry) max_height = volumetric_capacity[-1][0] if target_height < 0 or target_height > max_height: raise InvalidLiquidHeightFound("Invalid target height.") @@ -370,7 +385,9 @@ def find_volume_at_well_height( # find the section the target height is in and compute the volume # since bottomShape is not in list of frusta, check here first - sorted_well = sorted(well_geometry.sections, key=lambda section: section.topHeight) + sorted_well = sorted( + checked_geometry["sections"], key=lambda section: section["topHeight"] + ) # TODO(cm): handle non-frustum section that is not at the bottom. partial_volume = _find_volume_in_partial_frustum( sorted_well=sorted_well, @@ -382,7 +399,7 @@ def find_volume_at_well_height( def _find_height_in_partial_frustum( - sorted_well: List[Any], + sorted_well: List[WellSegment], volumetric_capacity: List[Tuple[float, float]], target_volume: float, ) -> Optional[float]: @@ -407,10 +424,11 @@ def _find_height_in_partial_frustum( def find_height_at_well_volume( - target_volume: float, well_geometry: InnerWellGeometry + target_volume: float, well_geometry: Union[InnerWellGeometry, InnerWellGeometryDef] ) -> float: """Find the height within a well, at a known volume.""" - volumetric_capacity = get_well_volumetric_capacity(well_geometry) + checked_geometry = ToInnerWellGeometryDict(well_geometry) + volumetric_capacity = get_well_volumetric_capacity(checked_geometry) max_volume = volumetric_capacity[-1][1] if target_volume < 0 or target_volume > max_volume: raise InvalidLiquidHeightFound("Invalid target volume.") @@ -418,7 +436,9 @@ def find_height_at_well_volume( if target_volume == section_volume: return section_height - sorted_well = sorted(well_geometry.sections, key=lambda section: section.topHeight) + sorted_well = sorted( + checked_geometry["sections"], key=lambda section: section["topHeight"] + ) # find the section the target volume is in and compute the height well_height = _find_height_in_partial_frustum( sorted_well=sorted_well, From 8a1e02a207af1d67864861a53ad62f0d97c00a50 Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Tue, 1 Oct 2024 17:31:35 -0400 Subject: [PATCH 09/35] fix all the things using typing caught --- .../protocol_engine/state/frustum_helpers.py | 30 +++++++++---------- .../geometry/test_frustum_helpers.py | 30 ++++++++++++++++--- .../opentrons_shared_data/labware/types.py | 1 + 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/api/src/opentrons/protocol_engine/state/frustum_helpers.py b/api/src/opentrons/protocol_engine/state/frustum_helpers.py index 95753e4f4e7..604db4bfcf5 100644 --- a/api/src/opentrons/protocol_engine/state/frustum_helpers.py +++ b/api/src/opentrons/protocol_engine/state/frustum_helpers.py @@ -230,39 +230,39 @@ def get_well_volumetric_capacity( for segment in sorted_well: section_volume: Optional[float] = None - if segment.shape == "spherical": + if segment["shape"] == "spherical": if sorted_well[0] != segment: raise InvalidWellDefinitionError( "spherical segment must only be at the bottom of a well." ) - target_height=segment.topHeight, - radius_of_curvature=segment.radiusOfCurvature, section_volume = _volume_from_height_spherical( + target_height=segment["topHeight"], + radius_of_curvature=segment["radiusOfCurvature"], ) - elif segment.shape == "rectangular": - section_height = segment.topHeight - segment.bottomHeight + elif segment["shape"] == "rectangular": + section_height = segment["topHeight"] - segment["bottomHeight"] section_volume = _volume_from_height_rectangular( target_height=section_height, - bottom_length=segment.bottomYDimension, - bottom_width=segment.bottomXDimension, - top_length=segment.topYDimension, - top_width=segment.topXDimension, + bottom_length=segment["bottomYDimension"], + bottom_width=segment["bottomXDimension"], + top_length=segment["topYDimension"], + top_width=segment["topXDimension"], total_frustum_height=section_height, ) - elif segment.shape == "circular": - section_height = segment.topHeight - segment.bottomHeight + elif segment["shape"] == "circular": + section_height = segment["topHeight"] - segment["bottomHeight"] section_volume = _volume_from_height_circular( target_height=section_height, total_frustum_height=section_height, - bottom_radius=(segment.bottomDiameter / 2), - top_radius=(segment.topDiameter / 2), + bottom_radius=(segment["bottomDiameter"] / 2), + top_radius=(segment["topDiameter"] / 2), ) # TODO: implement volume calculations for truncated circular and rounded rectangular segments if not section_volume: raise NotImplementedError( - f"volume calculation for shape: {segment.shape} not yet implemented." + f"volume calculation for shape: {segment['shape']} not yet implemented." ) - well_volume.append((segment.topHeight, section_volume)) + well_volume.append((segment["topHeight"], section_volume)) return well_volume diff --git a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py index 98a5f4e9100..48763fa9975 100644 --- a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py +++ b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py @@ -53,7 +53,12 @@ def fake_frusta() -> List[List[Any]]: topHeight=1.0, bottomHeight=1.0, ), - SphericalSegment(shape="spherical", radiusOfCurvature=4.0, topHeight=1.0), + SphericalSegment( + shape="spherical", + radiusOfCurvature=4.0, + topHeight=1.0, + bottomHeight=0.0, + ), ] ) frusta.append( @@ -112,11 +117,23 @@ def fake_frusta() -> List[List[Any]]: topHeight=3.0, bottomHeight=2.0, ), - SphericalSegment(shape="spherical", radiusOfCurvature=3.5, topHeight=2.0), + SphericalSegment( + shape="spherical", + radiusOfCurvature=3.5, + topHeight=2.0, + bottomHeight=0.0, + ), ] ) frusta.append( - [SphericalSegment(shape="spherical", radiusOfCurvature=4.0, topHeight=3.0)] + [ + SphericalSegment( + shape="spherical", + radiusOfCurvature=4.0, + topHeight=3.0, + bottomHeight=0.0, + ) + ] ) frusta.append( [ @@ -129,7 +146,12 @@ def fake_frusta() -> List[List[Any]]: topHeight=3.5, bottomHeight=1.5, ), - SphericalSegment(shape="spherical", radiusOfCurvature=4.0, topHeight=1.5), + SphericalSegment( + shape="spherical", + radiusOfCurvature=4.0, + topHeight=1.5, + bottomHeight=0.0, + ), ] ) return frusta diff --git a/shared-data/python/opentrons_shared_data/labware/types.py b/shared-data/python/opentrons_shared_data/labware/types.py index a491b67bbc9..d975640cc98 100644 --- a/shared-data/python/opentrons_shared_data/labware/types.py +++ b/shared-data/python/opentrons_shared_data/labware/types.py @@ -126,6 +126,7 @@ class SphericalSegment(TypedDict): shape: Spherical radiusOfCurvature: float topHeight: float + bottomHeight: Literal[0.0] = 0.0 class CircularFrustum(TypedDict): From 4fa13387fcfd7305429722a6c09ad8b9c2a51600 Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Tue, 1 Oct 2024 17:31:47 -0400 Subject: [PATCH 10/35] remove this method that we no longer need/want --- .../protocol_engine/state/frustum_helpers.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/api/src/opentrons/protocol_engine/state/frustum_helpers.py b/api/src/opentrons/protocol_engine/state/frustum_helpers.py index 604db4bfcf5..c3bdec6625e 100644 --- a/api/src/opentrons/protocol_engine/state/frustum_helpers.py +++ b/api/src/opentrons/protocol_engine/state/frustum_helpers.py @@ -37,20 +37,6 @@ def _reject_unacceptable_heights( return valid_heights[0] -def get_cross_section_area(bounded_section: Any) -> 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"]) - elif bounded_section["shape"] == "rectangular": - cross_section_area = cross_section_area_rectangular( - bounded_section["xDimension"], - bounded_section["yDimension"], - ) - else: - raise InvalidWellDefinitionError(message="Invalid well volume components.") - return cross_section_area - - def _cross_section_area_circular(diameter: float) -> float: """Get the area of a circular cross-section.""" radius = diameter / 2 From e144c0c4fb267e93b0d2abd44afffc48fc24bbf1 Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Wed, 2 Oct 2024 13:54:12 -0400 Subject: [PATCH 11/35] consolidate constants --- .../labware/constants.py | 7 +++++++ .../labware/labware_definition.py | 20 ++++++++++++------- .../opentrons_shared_data/labware/types.py | 12 ++++++----- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/shared-data/python/opentrons_shared_data/labware/constants.py b/shared-data/python/opentrons_shared_data/labware/constants.py index 00fbef3c160..293f922f35f 100644 --- a/shared-data/python/opentrons_shared_data/labware/constants.py +++ b/shared-data/python/opentrons_shared_data/labware/constants.py @@ -1,7 +1,14 @@ import re from typing_extensions import Final +from typing import Literal # Regular expression to validate and extract row, column from well name # (ie A3, C1) WELL_NAME_PATTERN: Final["re.Pattern[str]"] = re.compile(r"^([A-Z]+)([0-9]+)$", re.X) + +Circular = Literal["circular"] +Rectangular = Literal["rectangular"] +TruncatedCircular = Literal["truncatedcircular"] +RoundedRectangular = Literal["roundedrectangular"] +Spherical = Literal["spherical"] diff --git a/shared-data/python/opentrons_shared_data/labware/labware_definition.py b/shared-data/python/opentrons_shared_data/labware/labware_definition.py index 382141d2a25..623d515248c 100644 --- a/shared-data/python/opentrons_shared_data/labware/labware_definition.py +++ b/shared-data/python/opentrons_shared_data/labware/labware_definition.py @@ -19,6 +19,14 @@ ) from typing_extensions import Literal +from .constants import ( + Circular, + Rectangular, + RoundedRectangular, + TruncatedCircular, + Spherical, +) + SAFE_STRING_REGEX = "^[a-z0-9._]+$" @@ -228,7 +236,7 @@ class Config: class SphericalSegment(BaseModel): - shape: Literal["spherical"] = Field(..., description="Denote shape as spherical") + shape: Spherical = Field(..., description="Denote shape as spherical") radiusOfCurvature: _NonNegativeNumber = Field( ..., description="radius of curvature of bottom subsection of wells", @@ -239,7 +247,7 @@ class SphericalSegment(BaseModel): class CircularFrustum(BaseModel): - shape: Literal["circular"] = Field(..., description="Denote shape as circular") + shape: Circular = Field(..., description="Denote shape as circular") bottomDiameter: _NonNegativeNumber = Field( ..., description="The diameter at the bottom cross-section of a circular frustum", @@ -259,9 +267,7 @@ class CircularFrustum(BaseModel): class RectangularFrustum(BaseModel): - shape: Literal["rectangular"] = Field( - ..., description="Denote shape as rectangular" - ) + shape: Rectangular = Field(..., description="Denote shape as rectangular") bottomXDimension: _NonNegativeNumber = Field( ..., description="x dimension of the bottom cross-section of a rectangular frustum", @@ -290,7 +296,7 @@ class RectangularFrustum(BaseModel): class TruncatedCircularSegment(BaseModel): - shape: Literal["truncatedcircular"] = Field( + shape: TruncatedCircular = Field( ..., description="Denote shape as a truncated circular segment" ) circleDiameter: _NonNegativeNumber = Field( @@ -318,7 +324,7 @@ class TruncatedCircularSegment(BaseModel): class RoundedRectangularSegment(BaseModel): - shape: Literal["roundedrectangular"] = Field( + shape: RoundedRectangular = Field( ..., description="Denote shape as a rounded rectangular segment" ) circleDiameter: _NonNegativeNumber = Field( diff --git a/shared-data/python/opentrons_shared_data/labware/types.py b/shared-data/python/opentrons_shared_data/labware/types.py index d975640cc98..d12aaf9724c 100644 --- a/shared-data/python/opentrons_shared_data/labware/types.py +++ b/shared-data/python/opentrons_shared_data/labware/types.py @@ -6,6 +6,13 @@ from typing import Dict, List, NewType, Union from typing_extensions import Literal, TypedDict, NotRequired from .labware_definition import WellSegment as WellSegmentDef +from .constants import ( + Circular, + Rectangular, + TruncatedCircular, + RoundedRectangular, + Spherical, +) LabwareUri = NewType("LabwareUri", str) @@ -35,11 +42,6 @@ Literal["maintenance"], ] -Circular = Literal["circular"] -Rectangular = Literal["rectangular"] -TruncatedCircular = Literal["truncatedcircular"] -RoundedRectangular = Literal["roundedrectangular"] -Spherical = Literal["spherical"] WellShape = Union[Circular, Rectangular] From b6dfda7190614654a31ca3e39a5d08076ace17e6 Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Wed, 2 Oct 2024 13:54:57 -0400 Subject: [PATCH 12/35] spherecal segments need a bottom height for the sorting to work --- api/tests/opentrons/protocol_runner/test_json_translator.py | 1 + .../opentrons_shared_data/labware/labware_definition.py | 4 ++++ shared-data/python/opentrons_shared_data/labware/types.py | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/api/tests/opentrons/protocol_runner/test_json_translator.py b/api/tests/opentrons/protocol_runner/test_json_translator.py index 73ee39bd481..4e1adcc528b 100644 --- a/api/tests/opentrons/protocol_runner/test_json_translator.py +++ b/api/tests/opentrons/protocol_runner/test_json_translator.py @@ -713,6 +713,7 @@ def _load_labware_definition_data() -> LabwareDefinition: shape="spherical", radiusOfCurvature=6, topHeight=10, + bottomHeight=0.0, ), ], ) diff --git a/shared-data/python/opentrons_shared_data/labware/labware_definition.py b/shared-data/python/opentrons_shared_data/labware/labware_definition.py index 623d515248c..12b4fb3a726 100644 --- a/shared-data/python/opentrons_shared_data/labware/labware_definition.py +++ b/shared-data/python/opentrons_shared_data/labware/labware_definition.py @@ -244,6 +244,10 @@ class SphericalSegment(BaseModel): topHeight: _NonNegativeNumber = Field( ..., description="The depth of a spherical bottom of a well" ) + bottomHeight: _NonNegativeNumber = Field( + ..., + description="Hight of the bottom of the segment, must be 0.0", + ) class CircularFrustum(BaseModel): diff --git a/shared-data/python/opentrons_shared_data/labware/types.py b/shared-data/python/opentrons_shared_data/labware/types.py index d12aaf9724c..22322f9fd18 100644 --- a/shared-data/python/opentrons_shared_data/labware/types.py +++ b/shared-data/python/opentrons_shared_data/labware/types.py @@ -128,7 +128,7 @@ class SphericalSegment(TypedDict): shape: Spherical radiusOfCurvature: float topHeight: float - bottomHeight: Literal[0.0] = 0.0 + bottomHeight: float class CircularFrustum(TypedDict): From 1cd588c90602fefeedaed21c8b14709ad84b022d Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Wed, 2 Oct 2024 13:55:15 -0400 Subject: [PATCH 13/35] fixup the casting methods --- .../opentrons_shared_data/labware/types.py | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/shared-data/python/opentrons_shared_data/labware/types.py b/shared-data/python/opentrons_shared_data/labware/types.py index 22322f9fd18..54f2bc64eae 100644 --- a/shared-data/python/opentrons_shared_data/labware/types.py +++ b/shared-data/python/opentrons_shared_data/labware/types.py @@ -3,9 +3,12 @@ types in this file by and large require the use of typing_extensions. this module shouldn't be imported unless typing.TYPE_CHECKING is true. """ -from typing import Dict, List, NewType, Union +from typing import Dict, List, NewType, Union, cast from typing_extensions import Literal, TypedDict, NotRequired -from .labware_definition import WellSegment as WellSegmentDef +from .labware_definition import ( + WellSegment as WellSegmentDef, + InnerWellGeometry as InnerWellGeometryDef, +) from .constants import ( Circular, Rectangular, @@ -179,20 +182,30 @@ class RoundedRectangularSegment(TypedDict): SphericalSegment, ] -@staticmethod -def ToWellSegmentDict(segment: Union[WellSegment,WellSegmentDef]) -> WellSegment: - if isinstance(segment, WellSegmentDef): - return typing.cast(WellSegment, segment.model_dump(exclude_none=True, exclude_unset=True)) + +def ToWellSegmentDict(segment: Union[WellSegment, WellSegmentDef]) -> WellSegment: + if not isinstance(segment, dict): + return cast( + WellSegment, segment.model_dump(exclude_none=True, exclude_unset=True) # type: ignore[union-attr] + ) return segment class InnerWellGeometry(TypedDict): sections: List[WellSegment] -@staticmethod -def ToInnerWellGeometryDict(inner_well_geometry: Union[InnerWellGeometry,InnerWellGeometryDef]) -> InnerWellGeometry: - return InnerWellGeometry([ToWellSegmentDict(section) for section in inner_well_geometry["sections"]]) +def ToInnerWellGeometryDict( + inner_well_geometry: Union[InnerWellGeometry, InnerWellGeometryDef] +) -> InnerWellGeometry: + if not isinstance(inner_well_geometry, dict): + geometry_dict: InnerWellGeometry = { + "sections": [ + ToWellSegmentDict(section) for section in inner_well_geometry.sections + ] + } + return geometry_dict + return inner_well_geometry class LabwareDefinition(TypedDict): From f7e6a4d69fc6bf2961633c4fc776dd7914255e50 Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Wed, 2 Oct 2024 15:18:11 -0400 Subject: [PATCH 14/35] change 2d shape definitions to 3d shape definitions --- .../protocol_engine/state/frustum_helpers.py | 12 ++--- .../protocol_runner/test_json_translator.py | 12 ++--- .../geometry/test_frustum_helpers.py | 48 +++++++++---------- .../labware/constants.py | 9 +++- .../labware/labware_definition.py | 32 ++++++------- .../opentrons_shared_data/labware/types.py | 32 +++++++------ 6 files changed, 76 insertions(+), 69 deletions(-) diff --git a/api/src/opentrons/protocol_engine/state/frustum_helpers.py b/api/src/opentrons/protocol_engine/state/frustum_helpers.py index c3bdec6625e..13ec3adc3ac 100644 --- a/api/src/opentrons/protocol_engine/state/frustum_helpers.py +++ b/api/src/opentrons/protocol_engine/state/frustum_helpers.py @@ -225,7 +225,7 @@ def get_well_volumetric_capacity( target_height=segment["topHeight"], radius_of_curvature=segment["radiusOfCurvature"], ) - elif segment["shape"] == "rectangular": + elif segment["shape"] == "pyramidal": section_height = segment["topHeight"] - segment["bottomHeight"] section_volume = _volume_from_height_rectangular( target_height=section_height, @@ -235,7 +235,7 @@ def get_well_volumetric_capacity( top_width=segment["topXDimension"], total_frustum_height=section_height, ) - elif segment["shape"] == "circular": + elif segment["shape"] == "conical": section_height = segment["topHeight"] - segment["bottomHeight"] section_volume = _volume_from_height_circular( target_height=section_height, @@ -265,14 +265,14 @@ def height_at_volume_within_section( total_frustum_height=section_height, radius_of_curvature=checked_section["radiusOfCurvature"], ) - elif checked_section["shape"] == "circular": + elif checked_section["shape"] == "conical": partial_height = _height_from_volume_circular( volume=target_volume_relative, top_radius=(checked_section["bottomDiameter"] / 2), bottom_radius=(checked_section["topDiameter"] / 2), total_frustum_height=section_height, ) - elif checked_section["shape"] == "rectangular": + elif checked_section["shape"] == "pyramidal": partial_height = _height_from_volume_rectangular( volume=target_volume_relative, total_frustum_height=section_height, @@ -300,14 +300,14 @@ def volume_at_height_within_section( target_height=target_height_relative, radius_of_curvature=checked_section["radiusOfCurvature"], ) - elif checked_section["shape"] == "circular": + elif checked_section["shape"] == "conical": partial_volume = _volume_from_height_circular( target_height=target_height_relative, total_frustum_height=section_height, bottom_radius=(checked_section["bottomDiameter"] / 2), top_radius=(checked_section["topDiameter"] / 2), ) - elif checked_section["shape"] == "rectangular": + elif checked_section["shape"] == "pyramidal": partial_volume = _volume_from_height_rectangular( target_height=target_height_relative, total_frustum_height=section_height, diff --git a/api/tests/opentrons/protocol_runner/test_json_translator.py b/api/tests/opentrons/protocol_runner/test_json_translator.py index 4e1adcc528b..4fe366d4582 100644 --- a/api/tests/opentrons/protocol_runner/test_json_translator.py +++ b/api/tests/opentrons/protocol_runner/test_json_translator.py @@ -13,7 +13,7 @@ Group, Metadata1, WellDefinition, - RectangularFrustum, + PyramidalFrustum, InnerWellGeometry, SphericalSegment, ) @@ -683,7 +683,7 @@ def _load_labware_definition_data() -> LabwareDefinition: y=75.43, z=75, totalLiquidVolume=1100000, - shape="rectangular", + shape="circular", ) }, dimensions=Dimensions(yDimension=85.5, zDimension=100, xDimension=127.75), @@ -691,8 +691,8 @@ def _load_labware_definition_data() -> LabwareDefinition: innerLabwareGeometry={ "welldefinition1111": InnerWellGeometry( sections=[ - RectangularFrustum( - shape="rectangular", + PyramidalFrustum( + shape="pyramidal", topXDimension=7.6, topYDimension=8.5, bottomXDimension=5.6, @@ -700,8 +700,8 @@ def _load_labware_definition_data() -> LabwareDefinition: topHeight=45, bottomHeight=20, ), - RectangularFrustum( - shape="rectangular", + PyramidalFrustum( + shape="pyramidal", topXDimension=5.6, topYDimension=6.5, bottomXDimension=4.5, diff --git a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py index 48763fa9975..5c984f8227a 100644 --- a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py +++ b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py @@ -3,8 +3,8 @@ from typing import Any, List from opentrons_shared_data.labware.types import ( - CircularFrustum, - RectangularFrustum, + ConicalFrustum, + PyramidalFrustum, SphericalSegment, ) from opentrons.protocol_engine.state.frustum_helpers import ( @@ -28,8 +28,8 @@ def fake_frusta() -> List[List[Any]]: frusta = [] frusta.append( [ - RectangularFrustum( - shape="rectangular", + PyramidalFrustum( + shape="pyramidal", topXDimension=9.0, topYDimension=10.0, bottomXDimension=8.0, @@ -37,8 +37,8 @@ def fake_frusta() -> List[List[Any]]: topHeight=10.0, bottomHeight=5.0, ), - RectangularFrustum( - shape="rectangular", + PyramidalFrustum( + shape="pyramidal", topXDimension=8.0, topYDimension=9.0, bottomXDimension=15.0, @@ -46,8 +46,8 @@ def fake_frusta() -> List[List[Any]]: topHeight=5.0, bottomHeight=1.0, ), - CircularFrustum( - shape="circular", + ConicalFrustum( + shape="conical", topDiameter=23.0, bottomDiameter=3.0, topHeight=1.0, @@ -63,8 +63,8 @@ def fake_frusta() -> List[List[Any]]: ) frusta.append( [ - RectangularFrustum( - shape="rectangular", + PyramidalFrustum( + shape="pyramidal", topXDimension=8.0, topYDimension=70.0, bottomXDimension=7.0, @@ -72,8 +72,8 @@ def fake_frusta() -> List[List[Any]]: topHeight=3.5, bottomHeight=2.0, ), - RectangularFrustum( - shape="rectangular", + PyramidalFrustum( + shape="pyramidal", topXDimension=8.0, topYDimension=80.0, bottomXDimension=8.0, @@ -85,22 +85,22 @@ def fake_frusta() -> List[List[Any]]: ) frusta.append( [ - CircularFrustum( - shape="circular", + ConicalFrustum( + shape="conical", topDiameter=23.0, bottomDiameter=11.5, topHeight=7.5, bottomHeight=5.0, ), - CircularFrustum( - shape="circular", + ConicalFrustum( + shape="conical", topDiameter=11.5, bottomDiameter=23.0, topHeight=5.0, bottomHeight=2.5, ), - CircularFrustum( - shape="circular", + ConicalFrustum( + shape="conical", topDiameter=23.0, bottomDiameter=11.5, topHeight=2.5, @@ -110,8 +110,8 @@ def fake_frusta() -> List[List[Any]]: ) frusta.append( [ - CircularFrustum( - shape="circular", + ConicalFrustum( + shape="conical", topDiameter=4.0, bottomDiameter=5.0, topHeight=3.0, @@ -137,8 +137,8 @@ def fake_frusta() -> List[List[Any]]: ) frusta.append( [ - RectangularFrustum( - shape="rectangular", + PyramidalFrustum( + shape="pyramidal", topXDimension=27.0, topYDimension=36.0, bottomXDimension=36.0, @@ -211,7 +211,7 @@ def test_volume_and_height_circular(well: List[Any]) -> None: return total_height = well[0]["topHeight"] for segment in well: - if segment["shape"] == "circular": + if segment["shape"] == "conical": top_radius = segment["topDiameter"] / 2 bottom_radius = segment["bottomDiameter"] / 2 a = pi * ((top_radius - bottom_radius) ** 2) / (3 * total_height**2) @@ -253,7 +253,7 @@ def test_volume_and_height_rectangular(well: List[Any]) -> None: return total_height = well[0]["topHeight"] for segment in well: - if segment["shape"] == "rectangular": + if segment["shape"] == "pyramidal": top_length = segment["topYDimension"] top_width = segment["topXDimension"] bottom_length = segment["bottomYDimension"] diff --git a/shared-data/python/opentrons_shared_data/labware/constants.py b/shared-data/python/opentrons_shared_data/labware/constants.py index 293f922f35f..ae16fa2c4d2 100644 --- a/shared-data/python/opentrons_shared_data/labware/constants.py +++ b/shared-data/python/opentrons_shared_data/labware/constants.py @@ -7,8 +7,13 @@ # (ie A3, C1) WELL_NAME_PATTERN: Final["re.Pattern[str]"] = re.compile(r"^([A-Z]+)([0-9]+)$", re.X) +# These shapes are for wellshape definitions and describe the top of the well Circular = Literal["circular"] Rectangular = Literal["rectangular"] -TruncatedCircular = Literal["truncatedcircular"] -RoundedRectangular = Literal["roundedrectangular"] + +# These shapes are used to describe the 3D primatives used to build wells +Conical = Literal["conical"] +Pyramidal = Literal["pyramidal"] +SquaredCone = Literal["squaredcone"] +RoundedPyramid = Literal["roundedpyramid"] Spherical = Literal["spherical"] diff --git a/shared-data/python/opentrons_shared_data/labware/labware_definition.py b/shared-data/python/opentrons_shared_data/labware/labware_definition.py index 12b4fb3a726..b149378d880 100644 --- a/shared-data/python/opentrons_shared_data/labware/labware_definition.py +++ b/shared-data/python/opentrons_shared_data/labware/labware_definition.py @@ -20,10 +20,10 @@ from typing_extensions import Literal from .constants import ( - Circular, - Rectangular, - RoundedRectangular, - TruncatedCircular, + Conical, + Pyramidal, + RoundedPyramid, + SquaredCone, Spherical, ) @@ -250,8 +250,8 @@ class SphericalSegment(BaseModel): ) -class CircularFrustum(BaseModel): - shape: Circular = Field(..., description="Denote shape as circular") +class ConicalFrustum(BaseModel): + shape: Conical = Field(..., description="Denote shape as circular") bottomDiameter: _NonNegativeNumber = Field( ..., description="The diameter at the bottom cross-section of a circular frustum", @@ -270,8 +270,8 @@ class CircularFrustum(BaseModel): ) -class RectangularFrustum(BaseModel): - shape: Rectangular = Field(..., description="Denote shape as rectangular") +class PyramidalFrustum(BaseModel): + shape: Pyramidal = Field(..., description="Denote shape as rectangular") bottomXDimension: _NonNegativeNumber = Field( ..., description="x dimension of the bottom cross-section of a rectangular frustum", @@ -299,8 +299,8 @@ class RectangularFrustum(BaseModel): ) -class TruncatedCircularSegment(BaseModel): - shape: TruncatedCircular = Field( +class SquaredConeSegment(BaseModel): + shape: SquaredCone = Field( ..., description="Denote shape as a truncated circular segment" ) circleDiameter: _NonNegativeNumber = Field( @@ -327,8 +327,8 @@ class TruncatedCircularSegment(BaseModel): ) -class RoundedRectangularSegment(BaseModel): - shape: RoundedRectangular = Field( +class RoundedPyramidSegment(BaseModel): + shape: RoundedPyramid = Field( ..., description="Denote shape as a rounded rectangular segment" ) circleDiameter: _NonNegativeNumber = Field( @@ -383,10 +383,10 @@ class Group(BaseModel): WellSegment = Union[ - CircularFrustum, - RectangularFrustum, - TruncatedCircularSegment, - RoundedRectangularSegment, + ConicalFrustum, + PyramidalFrustum, + SquaredConeSegment, + RoundedPyramidSegment, SphericalSegment, ] diff --git a/shared-data/python/opentrons_shared_data/labware/types.py b/shared-data/python/opentrons_shared_data/labware/types.py index 54f2bc64eae..1e3865010cb 100644 --- a/shared-data/python/opentrons_shared_data/labware/types.py +++ b/shared-data/python/opentrons_shared_data/labware/types.py @@ -10,11 +10,13 @@ InnerWellGeometry as InnerWellGeometryDef, ) from .constants import ( + Conical, + Pyramidal, + SquaredCone, + RoundedPyramid, + Spherical, Circular, Rectangular, - TruncatedCircular, - RoundedRectangular, - Spherical, ) LabwareUri = NewType("LabwareUri", str) @@ -134,16 +136,16 @@ class SphericalSegment(TypedDict): bottomHeight: float -class CircularFrustum(TypedDict): - shape: Circular +class ConicalFrustum(TypedDict): + shape: Conical bottomDiameter: float topDiameter: float topHeight: float bottomHeight: float -class RectangularFrustum(TypedDict): - shape: Rectangular +class PyramidalFrustum(TypedDict): + shape: Pyramidal bottomXDimension: float bottomYDimension: float topXDimension: float @@ -154,8 +156,8 @@ class RectangularFrustum(TypedDict): # A truncated circle is a square that is trimmed at the corners by a smaller circle # that is concentric with the square. -class TruncatedCircularSegment(TypedDict): - shape: TruncatedCircular +class SquaredConeSegment(TypedDict): + shape: SquaredCone circleDiameter: float rectangleXDimension: float rectangleYDimension: float @@ -165,8 +167,8 @@ class TruncatedCircularSegment(TypedDict): # A rounded rectangle is a rectangle that is filleted by 4 circles # centered somewhere along the diagonals of the rectangle -class RoundedRectangularSegment(TypedDict): - shape: RoundedRectangular +class RoundedPyramidSegment(TypedDict): + shape: RoundedPyramid circleDiameter: float rectangleXDimension: float rectangleYDimension: float @@ -175,10 +177,10 @@ class RoundedRectangularSegment(TypedDict): WellSegment = Union[ - CircularFrustum, - RectangularFrustum, - TruncatedCircularSegment, - RoundedRectangularSegment, + ConicalFrustum, + PyramidalFrustum, + SquaredConeSegment, + RoundedPyramidSegment, SphericalSegment, ] From 1048a7d64e87d05f252a0709c49d4899beb79ba9 Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Wed, 2 Oct 2024 15:37:37 -0400 Subject: [PATCH 15/35] Flesh out shape descriptions --- .../python/opentrons_shared_data/labware/types.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/shared-data/python/opentrons_shared_data/labware/types.py b/shared-data/python/opentrons_shared_data/labware/types.py index 1e3865010cb..f41cbc89410 100644 --- a/shared-data/python/opentrons_shared_data/labware/types.py +++ b/shared-data/python/opentrons_shared_data/labware/types.py @@ -154,8 +154,8 @@ class PyramidalFrustum(TypedDict): bottomHeight: float -# A truncated circle is a square that is trimmed at the corners by a smaller circle -# that is concentric with the square. +# A squared cone is the intersection of a cube and a cone that both +# share a central axis, and is a transitional shape between a cone and pyramid class SquaredConeSegment(TypedDict): shape: SquaredCone circleDiameter: float @@ -165,8 +165,15 @@ class SquaredConeSegment(TypedDict): bottomHeight: float -# A rounded rectangle is a rectangle that is filleted by 4 circles -# centered somewhere along the diagonals of the rectangle +# A rounded pyramid is a pyramid that is filleted on each corner with the following: +# for each cross section the shape is a rectangle that has its corners rounded off +# the rounding for the corner is done by taking the intersection of the rectangle and +# a circle who's center is 1 radius away in both x and y from the edge of the rectangle +# which means the two angles where the circle meets the rectangle are exactly 90 degrees +# on the "circular" side of the shape all 4 filleting circles share a common center +# at the center of the rectangle +# on the "rectangular" side of the shape the 4 circles are 0 radius +# and their centers are the exact corner of the rectangle class RoundedPyramidSegment(TypedDict): shape: RoundedPyramid circleDiameter: float From fc78771fdcd56e464c4024f7e43db90a89ee6998 Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Wed, 2 Oct 2024 15:53:17 -0400 Subject: [PATCH 16/35] update the schema definitions --- shared-data/labware/schemas/3.json | 65 ++++++++++++++----- .../labware/labware_definition.py | 8 +-- 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/shared-data/labware/schemas/3.json b/shared-data/labware/schemas/3.json index c88b4a7317d..865ea94f7b0 100644 --- a/shared-data/labware/schemas/3.json +++ b/shared-data/labware/schemas/3.json @@ -70,6 +70,7 @@ "additionalProperties": false, "required": ["shape", "radiusOfCurvature", "topHeight"], "properties": { + "description": "A list of all of the sections of the well that have a contiguous shape", "shape": { "type": "string", "enum": ["spherical"] @@ -82,13 +83,20 @@ } } }, - "CircularFrustum": { + "ConicalFrustum": { "type": "object", - "required": ["shape", "bottomDiameter", "TopDiameter", "topHeight", "bottomHeight"], + "required": [ + "shape", + "bottomDiameter", + "TopDiameter", + "topHeight", + "bottomHeight" + ], "properties": { + "description": "A list of all of the sections of the well that have a contiguous shape", "shape": { "type": "string", - "enum": ["circular"] + "enum": ["conical"] }, "bottomDiameter": { "type": "number" @@ -104,13 +112,22 @@ } } }, - "RectangularFrustum": { + "PyramidalFrustum": { "type": "object", - "required": ["shape", "bottomXDimension", "bottomYDimensions", "topXDimension", "topYDimension", "topHeight", "bottomHeight"], + "required": [ + "shape", + "bottomXDimension", + "bottomYDimensions", + "topXDimension", + "topYDimension", + "topHeight", + "bottomHeight" + ], "properties": { + "description": "A list of all of the sections of the well that have a contiguous shape", "shape": { "type": "string", - "enum": ["rectangular"] + "enum": ["pyramidal"] }, "bottomXDimension": { "type": "number" @@ -132,13 +149,21 @@ } } }, - "TruncatedCircularSegment": { + "SquaredConeSegment": { "type": "object", - "required": ["shape", "circleDiameter", "rectangleXDimension", "rectangleYDimension", "topHeight", "bottomHeight"], + "required": [ + "shape", + "circleDiameter", + "rectangleXDimension", + "rectangleYDimension", + "topHeight", + "bottomHeight" + ], "properties": { + "description": "The intersection of a pyramid and a cone that both share a central axis where one face is a circle and one face is a rectangle", "shape": { "type": "string", - "enum": ["truncatedcircular"] + "enum": ["squaredcone"] }, "circleDiameter": { "type": "number" @@ -157,13 +182,21 @@ } } }, - "RoundedRectangularSegment": { + "RoundedPyramidSegment": { "type": "object", - "required": ["shape", "circleDiameter", "rectangleXDimension", "rectangleYDimension", "topHeight", "bottomHeight"], + "description": "A pyramidal frustum where each corner is filleted out by circles with centers on the diagonals between opposite corners", + "required": [ + "shape", + "circleDiameter", + "rectangleXDimension", + "rectangleYDimension", + "topHeight", + "bottomHeight" + ], "properties": { "shape": { "type": "string", - "enum": ["roundedrectangular"] + "enum": ["roundedpyramid"] }, "circleDiameter": { "type": "number" @@ -192,16 +225,16 @@ "items": { "oneOf": [ { - "$ref": "#/definitions/CircularFrustum" + "$ref": "#/definitions/ConicalFrustum" }, { - "$ref": "#/definitions/RectangularFrustum" + "$ref": "#/definitions/PyramidalFrustum" }, { - "$ref": "#/definitions/TruncatedCircularSegment" + "$ref": "#/definitions/SquaredConeSegment" }, { - "$ref": "#/definitions/RoundedRectangularSegment" + "$ref": "#/definitions/RoundedPyramidSegment" }, { "$ref": "#/definitions/SphericalSegment" diff --git a/shared-data/python/opentrons_shared_data/labware/labware_definition.py b/shared-data/python/opentrons_shared_data/labware/labware_definition.py index b149378d880..7f685d03ec9 100644 --- a/shared-data/python/opentrons_shared_data/labware/labware_definition.py +++ b/shared-data/python/opentrons_shared_data/labware/labware_definition.py @@ -251,7 +251,7 @@ class SphericalSegment(BaseModel): class ConicalFrustum(BaseModel): - shape: Conical = Field(..., description="Denote shape as circular") + shape: Conical = Field(..., description="Denote shape as conical") bottomDiameter: _NonNegativeNumber = Field( ..., description="The diameter at the bottom cross-section of a circular frustum", @@ -271,7 +271,7 @@ class ConicalFrustum(BaseModel): class PyramidalFrustum(BaseModel): - shape: Pyramidal = Field(..., description="Denote shape as rectangular") + shape: Pyramidal = Field(..., description="Denote shape as pyramidal") bottomXDimension: _NonNegativeNumber = Field( ..., description="x dimension of the bottom cross-section of a rectangular frustum", @@ -301,7 +301,7 @@ class PyramidalFrustum(BaseModel): class SquaredConeSegment(BaseModel): shape: SquaredCone = Field( - ..., description="Denote shape as a truncated circular segment" + ..., description="Denote shape as a squared conical segment" ) circleDiameter: _NonNegativeNumber = Field( ..., @@ -329,7 +329,7 @@ class SquaredConeSegment(BaseModel): class RoundedPyramidSegment(BaseModel): shape: RoundedPyramid = Field( - ..., description="Denote shape as a rounded rectangular segment" + ..., description="Denote shape as a rounded pyramidal segment" ) circleDiameter: _NonNegativeNumber = Field( ..., From 02bbb77492916b3108939425cc75f2c77ba8d4ef Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Wed, 2 Oct 2024 16:02:06 -0400 Subject: [PATCH 17/35] add required bottom cross section to definitions --- shared-data/labware/schemas/3.json | 10 ++++++++++ .../python/opentrons_shared_data/labware/constants.py | 3 ++- .../labware/labware_definition.py | 9 +++++++++ .../python/opentrons_shared_data/labware/types.py | 5 +++-- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/shared-data/labware/schemas/3.json b/shared-data/labware/schemas/3.json index 865ea94f7b0..7ae401b7427 100644 --- a/shared-data/labware/schemas/3.json +++ b/shared-data/labware/schemas/3.json @@ -153,6 +153,7 @@ "type": "object", "required": [ "shape", + "bottomCrossSection", "circleDiameter", "rectangleXDimension", "rectangleYDimension", @@ -165,6 +166,10 @@ "type": "string", "enum": ["squaredcone"] }, + "bottomCrossSection": { + "type": "string", + "enum": ["circular", "rectangular"] + }, "circleDiameter": { "type": "number" }, @@ -187,6 +192,7 @@ "description": "A pyramidal frustum where each corner is filleted out by circles with centers on the diagonals between opposite corners", "required": [ "shape", + "bottomCrossSection", "circleDiameter", "rectangleXDimension", "rectangleYDimension", @@ -198,6 +204,10 @@ "type": "string", "enum": ["roundedpyramid"] }, + "bottomCrossSection": { + "type": "string", + "enum": ["circular", "rectangular"] + }, "circleDiameter": { "type": "number" }, diff --git a/shared-data/python/opentrons_shared_data/labware/constants.py b/shared-data/python/opentrons_shared_data/labware/constants.py index ae16fa2c4d2..3b12a88b821 100644 --- a/shared-data/python/opentrons_shared_data/labware/constants.py +++ b/shared-data/python/opentrons_shared_data/labware/constants.py @@ -1,6 +1,6 @@ import re from typing_extensions import Final -from typing import Literal +from typing import Literal, Union # Regular expression to validate and extract row, column from well name @@ -10,6 +10,7 @@ # These shapes are for wellshape definitions and describe the top of the well Circular = Literal["circular"] Rectangular = Literal["rectangular"] +WellShape = Union[Circular, Rectangular] # These shapes are used to describe the 3D primatives used to build wells Conical = Literal["conical"] diff --git a/shared-data/python/opentrons_shared_data/labware/labware_definition.py b/shared-data/python/opentrons_shared_data/labware/labware_definition.py index 7f685d03ec9..557b86d02a2 100644 --- a/shared-data/python/opentrons_shared_data/labware/labware_definition.py +++ b/shared-data/python/opentrons_shared_data/labware/labware_definition.py @@ -25,6 +25,7 @@ RoundedPyramid, SquaredCone, Spherical, + WellShape, ) SAFE_STRING_REGEX = "^[a-z0-9._]+$" @@ -303,6 +304,10 @@ class SquaredConeSegment(BaseModel): shape: SquaredCone = Field( ..., description="Denote shape as a squared conical segment" ) + bottomCrossSection: WellShape = Field( + ..., + description="Denote if the shape is going from circular to rectangular or vise versa", + ) circleDiameter: _NonNegativeNumber = Field( ..., description="diameter of the circular face of a truncated circular segment", @@ -331,6 +336,10 @@ class RoundedPyramidSegment(BaseModel): shape: RoundedPyramid = Field( ..., description="Denote shape as a rounded pyramidal segment" ) + bottomCrossSection: WellShape = Field( + ..., + description="Denote if the shape is going from circular to rectangular or vise versa", + ) circleDiameter: _NonNegativeNumber = Field( ..., description="diameter of the circular face of a rounded rectangular segment", diff --git a/shared-data/python/opentrons_shared_data/labware/types.py b/shared-data/python/opentrons_shared_data/labware/types.py index f41cbc89410..4a5b378dfc1 100644 --- a/shared-data/python/opentrons_shared_data/labware/types.py +++ b/shared-data/python/opentrons_shared_data/labware/types.py @@ -17,6 +17,7 @@ Spherical, Circular, Rectangular, + WellShape, ) LabwareUri = NewType("LabwareUri", str) @@ -47,8 +48,6 @@ Literal["maintenance"], ] -WellShape = Union[Circular, Rectangular] - class NamedOffset(TypedDict): x: float @@ -158,6 +157,7 @@ class PyramidalFrustum(TypedDict): # share a central axis, and is a transitional shape between a cone and pyramid class SquaredConeSegment(TypedDict): shape: SquaredCone + bottomCrossSection: WellShape circleDiameter: float rectangleXDimension: float rectangleYDimension: float @@ -176,6 +176,7 @@ class SquaredConeSegment(TypedDict): # and their centers are the exact corner of the rectangle class RoundedPyramidSegment(TypedDict): shape: RoundedPyramid + bottomCrossSection: WellShape circleDiameter: float rectangleXDimension: float rectangleYDimension: float From eaaee958abb388898795395778cfdc4e773bfa40 Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Wed, 2 Oct 2024 17:03:35 -0400 Subject: [PATCH 18/35] fix schema and tests --- .../js/__tests__/labwareDefSchemaV3.test.ts | 6 +----- .../labware/fixtures/3/fixture_2_plate.json | 7 ++++--- .../fixtures/3/fixture_corning_24_plate.json | 2 +- shared-data/labware/schemas/3.json | 17 ++++++++++------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/shared-data/js/__tests__/labwareDefSchemaV3.test.ts b/shared-data/js/__tests__/labwareDefSchemaV3.test.ts index 8416e8b60c5..14d0c4bf968 100644 --- a/shared-data/js/__tests__/labwareDefSchemaV3.test.ts +++ b/shared-data/js/__tests__/labwareDefSchemaV3.test.ts @@ -33,14 +33,10 @@ const checkGeometryDefinitions = ( expect(wellGeometryId in labwareDef.innerLabwareGeometry).toBe(true) const wellDepth = labwareDef.wells[wellName].depth - const wellShape = labwareDef.wells[wellName].shape const topFrustumHeight = - labwareDef.innerLabwareGeometry[wellGeometryId].frusta[0].topHeight - const topFrustumShape = - labwareDef.innerLabwareGeometry[wellGeometryId].frusta[0].shape + labwareDef.innerLabwareGeometry[wellGeometryId].sections[0].topHeight expect(wellDepth).toEqual(topFrustumHeight) - expect(wellShape).toEqual(topFrustumShape) } }) } diff --git a/shared-data/labware/fixtures/3/fixture_2_plate.json b/shared-data/labware/fixtures/3/fixture_2_plate.json index b77a060dda6..a9d740b42ff 100644 --- a/shared-data/labware/fixtures/3/fixture_2_plate.json +++ b/shared-data/labware/fixtures/3/fixture_2_plate.json @@ -64,7 +64,7 @@ "daiwudhadfhiew": { "sections": [ { - "shape": "rectangular", + "shape": "pyramidal", "topXDimension": 127.76, "topYDimension": 85.8, "bottomXDimension": 70.0, @@ -77,7 +77,7 @@ "iuweofiuwhfn": { "sections": [ { - "shape": "circular", + "shape": "conical", "bottomDiameter": 35.0, "topDiameter": 35.0, "topHeight": 42.16, @@ -86,7 +86,8 @@ { "shape": "spherical", "radiusOfCurvature": 20.0, - "topHeight": 10.0 + "topHeight": 10.0, + "bottomHeight": 0.0 } ] } diff --git a/shared-data/labware/fixtures/3/fixture_corning_24_plate.json b/shared-data/labware/fixtures/3/fixture_corning_24_plate.json index c912d66c7df..679f8916377 100644 --- a/shared-data/labware/fixtures/3/fixture_corning_24_plate.json +++ b/shared-data/labware/fixtures/3/fixture_corning_24_plate.json @@ -325,7 +325,7 @@ "venirhgerug": { "sections": [ { - "shape": "circular", + "shape": "conical", "bottomDiameter": 16.26, "topDiameter": 16.26, "topHeight": 17.4, diff --git a/shared-data/labware/schemas/3.json b/shared-data/labware/schemas/3.json index 7ae401b7427..6e6db94342c 100644 --- a/shared-data/labware/schemas/3.json +++ b/shared-data/labware/schemas/3.json @@ -67,10 +67,10 @@ }, "SphericalSegment": { "type": "object", + "description": "A list of all of the sections of the well that have a contiguous shape", "additionalProperties": false, "required": ["shape", "radiusOfCurvature", "topHeight"], "properties": { - "description": "A list of all of the sections of the well that have a contiguous shape", "shape": { "type": "string", "enum": ["spherical"] @@ -80,20 +80,23 @@ }, "topHeight": { "type": "number" + }, + "bottomHeight": { + "type": "number" } } }, "ConicalFrustum": { "type": "object", + "description": "A list of all of the sections of the well that have a contiguous shape", "required": [ "shape", "bottomDiameter", - "TopDiameter", + "topDiameter", "topHeight", "bottomHeight" ], "properties": { - "description": "A list of all of the sections of the well that have a contiguous shape", "shape": { "type": "string", "enum": ["conical"] @@ -114,18 +117,18 @@ }, "PyramidalFrustum": { "type": "object", + "description": "A list of all of the sections of the well that have a contiguous shape", "required": [ "shape", "bottomXDimension", - "bottomYDimensions", + "bottomYDimension", "topXDimension", "topYDimension", "topHeight", "bottomHeight" ], "properties": { - "description": "A list of all of the sections of the well that have a contiguous shape", - "shape": { + "shape": { "type": "string", "enum": ["pyramidal"] }, @@ -151,6 +154,7 @@ }, "SquaredConeSegment": { "type": "object", + "description": "The intersection of a pyramid and a cone that both share a central axis where one face is a circle and one face is a rectangle", "required": [ "shape", "bottomCrossSection", @@ -161,7 +165,6 @@ "bottomHeight" ], "properties": { - "description": "The intersection of a pyramid and a cone that both share a central axis where one face is a circle and one face is a rectangle", "shape": { "type": "string", "enum": ["squaredcone"] From 43dfa7cd56c23df6220d3f82f96e5e2203f64a1d Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Thu, 3 Oct 2024 17:22:34 -0400 Subject: [PATCH 19/35] strip out redundant typedict implimentation --- .../protocol_engine/state/frustum_helpers.py | 121 ++++++++---------- .../geometry/test_frustum_helpers.py | 2 +- .../labware/labware_definition.py | 17 +++ .../opentrons_shared_data/labware/types.py | 103 +-------------- 4 files changed, 72 insertions(+), 171 deletions(-) diff --git a/api/src/opentrons/protocol_engine/state/frustum_helpers.py b/api/src/opentrons/protocol_engine/state/frustum_helpers.py index 13ec3adc3ac..d90f8dbccf9 100644 --- a/api/src/opentrons/protocol_engine/state/frustum_helpers.py +++ b/api/src/opentrons/protocol_engine/state/frustum_helpers.py @@ -1,19 +1,13 @@ """Helper functions for liquid-level related calculations inside a given frustum.""" -from typing import List, Tuple, Optional, Union +from typing import List, Tuple, Optional from numpy import pi, iscomplex, roots, real from math import isclose from ..errors.exceptions import InvalidLiquidHeightFound, InvalidWellDefinitionError -from opentrons_shared_data.labware.types import ( +from opentrons_shared_data.labware.labware_definition import ( InnerWellGeometry, WellSegment, - ToWellSegmentDict, - ToInnerWellGeometryDict, -) -from opentrons_shared_data.labware.labware_definition import ( - InnerWellGeometry as InnerWellGeometryDef, - WellSegment as WellSegmentDef, ) @@ -201,85 +195,81 @@ def _height_from_volume_spherical( def get_well_volumetric_capacity( - well_geometry: Union[InnerWellGeometry, InnerWellGeometryDef], + well_geometry: InnerWellGeometry, ) -> List[Tuple[float, float]]: """Return the total volumetric capacity of a well as a map of height borders to volume.""" - checked_geometry = ToInnerWellGeometryDict(well_geometry) # dictionary map of heights to volumetric capacities within their respective segment # {top_height_0: volume_0, top_height_1: volume_1, top_height_2: volume_2} well_volume = [] # get the well segments sorted in ascending order - sorted_well = sorted( - checked_geometry["sections"], key=lambda section: section["topHeight"] - ) + sorted_well = sorted(well_geometry.sections, key=lambda section: section.topHeight) for segment in sorted_well: section_volume: Optional[float] = None - if segment["shape"] == "spherical": + if segment.shape == "spherical": if sorted_well[0] != segment: raise InvalidWellDefinitionError( "spherical segment must only be at the bottom of a well." ) section_volume = _volume_from_height_spherical( - target_height=segment["topHeight"], - radius_of_curvature=segment["radiusOfCurvature"], + target_height=segment.topHeight, + radius_of_curvature=segment.radiusOfCurvature, ) - elif segment["shape"] == "pyramidal": - section_height = segment["topHeight"] - segment["bottomHeight"] + elif segment.shape == "pyramidal": + section_height = segment.topHeight - segment.bottomHeight section_volume = _volume_from_height_rectangular( target_height=section_height, - bottom_length=segment["bottomYDimension"], - bottom_width=segment["bottomXDimension"], - top_length=segment["topYDimension"], - top_width=segment["topXDimension"], + bottom_length=segment.bottomYDimension, + bottom_width=segment.bottomXDimension, + top_length=segment.topYDimension, + top_width=segment.topXDimension, total_frustum_height=section_height, ) - elif segment["shape"] == "conical": - section_height = segment["topHeight"] - segment["bottomHeight"] + elif segment.shape == "conical": + section_height = segment.topHeight - segment.bottomHeight section_volume = _volume_from_height_circular( target_height=section_height, total_frustum_height=section_height, - bottom_radius=(segment["bottomDiameter"] / 2), - top_radius=(segment["topDiameter"] / 2), + bottom_radius=(segment.bottomDiameter / 2), + top_radius=(segment.topDiameter / 2), ) # TODO: implement volume calculations for truncated circular and rounded rectangular segments if not section_volume: raise NotImplementedError( - f"volume calculation for shape: {segment['shape']} not yet implemented." + f"volume calculation for shape: {segment.shape} not yet implemented." ) - well_volume.append((segment["topHeight"], section_volume)) + well_volume.append((segment.topHeight, section_volume)) return well_volume def height_at_volume_within_section( - section: Union[WellSegment, WellSegmentDef], + section: WellSegment, target_volume_relative: float, section_height: float, ) -> float: """Calculate a height within a bounded section according to geometry.""" - checked_section = ToWellSegmentDict(section) - if checked_section["shape"] == "spherical": + if section.shape == "spherical": partial_height = _height_from_volume_spherical( volume=target_volume_relative, total_frustum_height=section_height, - radius_of_curvature=checked_section["radiusOfCurvature"], + radius_of_curvature=section.radiusOfCurvature, ) - elif checked_section["shape"] == "conical": + elif section.shape == "conical": partial_height = _height_from_volume_circular( volume=target_volume_relative, - top_radius=(checked_section["bottomDiameter"] / 2), - bottom_radius=(checked_section["topDiameter"] / 2), + top_radius=(section.bottomDiameter / 2), + bottom_radius=(section.topDiameter / 2), total_frustum_height=section_height, ) - elif checked_section["shape"] == "pyramidal": + elif section.shape == "pyramidal": partial_height = _height_from_volume_rectangular( volume=target_volume_relative, total_frustum_height=section_height, - bottom_width=checked_section["bottomXDimension"], - bottom_length=checked_section["bottomYDimension"], - top_width=checked_section["topXDimension"], - top_length=checked_section["topYDimension"], + bottom_width=section.bottomXDimension, + bottom_length=section.bottomYDimension, + top_width=section.topXDimension, + top_length=section.topYDimension, ) else: raise NotImplementedError( @@ -289,32 +279,31 @@ def height_at_volume_within_section( def volume_at_height_within_section( - section: Union[WellSegment, WellSegmentDef], + section: WellSegment, target_height_relative: float, section_height: float, ) -> float: """Calculate a volume within a bounded section according to geometry.""" - checked_section = ToWellSegmentDict(section) - if checked_section["shape"] == "spherical": + if section.shape == "spherical": partial_volume = _volume_from_height_spherical( target_height=target_height_relative, - radius_of_curvature=checked_section["radiusOfCurvature"], + radius_of_curvature=section.radiusOfCurvature, ) - elif checked_section["shape"] == "conical": + elif section.shape == "conical": partial_volume = _volume_from_height_circular( target_height=target_height_relative, total_frustum_height=section_height, - bottom_radius=(checked_section["bottomDiameter"] / 2), - top_radius=(checked_section["topDiameter"] / 2), + bottom_radius=(section.bottomDiameter / 2), + top_radius=(section.topDiameter / 2), ) - elif checked_section["shape"] == "pyramidal": + elif section.shape == "pyramidal": partial_volume = _volume_from_height_rectangular( target_height=target_height_relative, total_frustum_height=section_height, - bottom_width=checked_section["bottomXDimension"], - bottom_length=checked_section["bottomYDimension"], - top_width=checked_section["topXDimension"], - top_length=checked_section["topYDimension"], + bottom_width=section.bottomXDimension, + bottom_length=section.bottomYDimension, + top_width=section.topXDimension, + top_length=section.topYDimension, ) # TODO(cm): this would be the NEST-96 2uL wells referenced in EXEC-712 # we need to input the math attached to that issue @@ -332,9 +321,9 @@ def _find_volume_in_partial_frustum( """Look through a sorted list of frusta for a target height, and find the volume at that height.""" partial_volume: Optional[float] = None for segment in sorted_well: - if segment["bottomHeight"] < target_height < segment["topHeight"]: - relative_target_height = target_height - segment["bottomHeight"] - section_height = segment["topHeight"] - segment["bottomHeight"] + if segment.bottomHeight < target_height < segment.topHeight: + relative_target_height = target_height - segment.bottomHeight + section_height = segment.topHeight - segment.bottomHeight partial_volume = volume_at_height_within_section( section=segment, target_height_relative=relative_target_height, @@ -349,11 +338,10 @@ def _find_volume_in_partial_frustum( def find_volume_at_well_height( - target_height: float, well_geometry: Union[InnerWellGeometry, InnerWellGeometryDef] + target_height: float, well_geometry: InnerWellGeometry ) -> float: """Find the volume within a well, at a known height.""" - checked_geometry = ToInnerWellGeometryDict(well_geometry) - volumetric_capacity = get_well_volumetric_capacity(checked_geometry) + volumetric_capacity = get_well_volumetric_capacity(well_geometry) max_height = volumetric_capacity[-1][0] if target_height < 0 or target_height > max_height: raise InvalidLiquidHeightFound("Invalid target height.") @@ -371,9 +359,7 @@ def find_volume_at_well_height( # find the section the target height is in and compute the volume # since bottomShape is not in list of frusta, check here first - sorted_well = sorted( - checked_geometry["sections"], key=lambda section: section["topHeight"] - ) + sorted_well = sorted(well_geometry.sections, key=lambda section: section.topHeight) # TODO(cm): handle non-frustum section that is not at the bottom. partial_volume = _find_volume_in_partial_frustum( sorted_well=sorted_well, @@ -396,13 +382,13 @@ def _find_height_in_partial_frustum( section_top_height, section_volume = capacity if bottom_section_volume < target_volume < section_volume: relative_target_volume = target_volume - bottom_section_volume - relative_section_height = section["topHeight"] - section["bottomHeight"] + relative_section_height = section.topHeight - section.bottomHeight partial_height = height_at_volume_within_section( section=section, target_volume_relative=relative_target_volume, section_height=relative_section_height, ) - height_within_well = partial_height + section["bottomHeight"] + height_within_well = partial_height + section.bottomHeight # bottom section volume should always be the volume enclosed in the previously # viewed section bottom_section_volume = section_volume @@ -410,11 +396,10 @@ def _find_height_in_partial_frustum( def find_height_at_well_volume( - target_volume: float, well_geometry: Union[InnerWellGeometry, InnerWellGeometryDef] + target_volume: float, well_geometry: InnerWellGeometry ) -> float: """Find the height within a well, at a known volume.""" - checked_geometry = ToInnerWellGeometryDict(well_geometry) - volumetric_capacity = get_well_volumetric_capacity(checked_geometry) + volumetric_capacity = get_well_volumetric_capacity(well_geometry) max_volume = volumetric_capacity[-1][1] if target_volume < 0 or target_volume > max_volume: raise InvalidLiquidHeightFound("Invalid target volume.") @@ -422,9 +407,7 @@ def find_height_at_well_volume( if target_volume == section_volume: return section_height - sorted_well = sorted( - checked_geometry["sections"], key=lambda section: section["topHeight"] - ) + sorted_well = sorted(well_geometry.sections, key=lambda section: section.topHeight) # find the section the target volume is in and compute the height well_height = _find_height_in_partial_frustum( sorted_well=sorted_well, diff --git a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py index 5c984f8227a..dbb9fa1e4cb 100644 --- a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py +++ b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py @@ -2,7 +2,7 @@ from math import pi, isclose from typing import Any, List -from opentrons_shared_data.labware.types import ( +from opentrons_shared_data.labware.labware_definition import ( ConicalFrustum, PyramidalFrustum, SphericalSegment, diff --git a/shared-data/python/opentrons_shared_data/labware/labware_definition.py b/shared-data/python/opentrons_shared_data/labware/labware_definition.py index 557b86d02a2..36c4fedd26a 100644 --- a/shared-data/python/opentrons_shared_data/labware/labware_definition.py +++ b/shared-data/python/opentrons_shared_data/labware/labware_definition.py @@ -300,6 +300,23 @@ class PyramidalFrustum(BaseModel): ) +# A squared cone is the intersection of a cube and a cone that both +# share a central axis, and is a transitional shape between a cone and pyramid +""" +module RectangularPrismToCone(bottom_shape, diameter, x, y, z) { + circle_radius = diameter/2; + r1 = sqrt(x*x + y*y)/2; + r2 = circle_radius/2; + top_r = bottom_shape == "square" ? r1 : r2; + bottom_r = bottom_shape == "square" ? r2 : r1; + intersection() { + cylinder(z,top_r,bottom_r,$fn=100); + translate([0,0,z/2])cube([x, y, z], center=true); + } +} +""" + + class SquaredConeSegment(BaseModel): shape: SquaredCone = Field( ..., description="Denote shape as a squared conical segment" diff --git a/shared-data/python/opentrons_shared_data/labware/types.py b/shared-data/python/opentrons_shared_data/labware/types.py index 4a5b378dfc1..d3f6599848c 100644 --- a/shared-data/python/opentrons_shared_data/labware/types.py +++ b/shared-data/python/opentrons_shared_data/labware/types.py @@ -3,21 +3,12 @@ types in this file by and large require the use of typing_extensions. this module shouldn't be imported unless typing.TYPE_CHECKING is true. """ -from typing import Dict, List, NewType, Union, cast +from typing import Dict, List, NewType, Union from typing_extensions import Literal, TypedDict, NotRequired -from .labware_definition import ( - WellSegment as WellSegmentDef, - InnerWellGeometry as InnerWellGeometryDef, -) +from .labware_definition import InnerWellGeometry from .constants import ( - Conical, - Pyramidal, - SquaredCone, - RoundedPyramid, - Spherical, Circular, Rectangular, - WellShape, ) LabwareUri = NewType("LabwareUri", str) @@ -128,96 +119,6 @@ class WellGroup(TypedDict, total=False): brand: LabwareBrandData -class SphericalSegment(TypedDict): - shape: Spherical - radiusOfCurvature: float - topHeight: float - bottomHeight: float - - -class ConicalFrustum(TypedDict): - shape: Conical - bottomDiameter: float - topDiameter: float - topHeight: float - bottomHeight: float - - -class PyramidalFrustum(TypedDict): - shape: Pyramidal - bottomXDimension: float - bottomYDimension: float - topXDimension: float - topYDimension: float - topHeight: float - bottomHeight: float - - -# A squared cone is the intersection of a cube and a cone that both -# share a central axis, and is a transitional shape between a cone and pyramid -class SquaredConeSegment(TypedDict): - shape: SquaredCone - bottomCrossSection: WellShape - circleDiameter: float - rectangleXDimension: float - rectangleYDimension: float - topHeight: float - bottomHeight: float - - -# A rounded pyramid is a pyramid that is filleted on each corner with the following: -# for each cross section the shape is a rectangle that has its corners rounded off -# the rounding for the corner is done by taking the intersection of the rectangle and -# a circle who's center is 1 radius away in both x and y from the edge of the rectangle -# which means the two angles where the circle meets the rectangle are exactly 90 degrees -# on the "circular" side of the shape all 4 filleting circles share a common center -# at the center of the rectangle -# on the "rectangular" side of the shape the 4 circles are 0 radius -# and their centers are the exact corner of the rectangle -class RoundedPyramidSegment(TypedDict): - shape: RoundedPyramid - bottomCrossSection: WellShape - circleDiameter: float - rectangleXDimension: float - rectangleYDimension: float - topHeight: float - bottomHeight: float - - -WellSegment = Union[ - ConicalFrustum, - PyramidalFrustum, - SquaredConeSegment, - RoundedPyramidSegment, - SphericalSegment, -] - - -def ToWellSegmentDict(segment: Union[WellSegment, WellSegmentDef]) -> WellSegment: - if not isinstance(segment, dict): - return cast( - WellSegment, segment.model_dump(exclude_none=True, exclude_unset=True) # type: ignore[union-attr] - ) - return segment - - -class InnerWellGeometry(TypedDict): - sections: List[WellSegment] - - -def ToInnerWellGeometryDict( - inner_well_geometry: Union[InnerWellGeometry, InnerWellGeometryDef] -) -> InnerWellGeometry: - if not isinstance(inner_well_geometry, dict): - geometry_dict: InnerWellGeometry = { - "sections": [ - ToWellSegmentDict(section) for section in inner_well_geometry.sections - ] - } - return geometry_dict - return inner_well_geometry - - class LabwareDefinition(TypedDict): schemaVersion: Literal[2] version: int From 7e8be6f2ce8882a4b070809fb754c9f2fc70f428 Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Thu, 3 Oct 2024 17:25:58 -0400 Subject: [PATCH 20/35] openscad for fillited shape, we may need to completely re-think this --- .../labware/labware_definition.py | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/shared-data/python/opentrons_shared_data/labware/labware_definition.py b/shared-data/python/opentrons_shared_data/labware/labware_definition.py index 36c4fedd26a..e1b83632913 100644 --- a/shared-data/python/opentrons_shared_data/labware/labware_definition.py +++ b/shared-data/python/opentrons_shared_data/labware/labware_definition.py @@ -349,6 +349,86 @@ class SquaredConeSegment(BaseModel): ) +""" +module filitedPyramidSquare(bottom_shape, diameter, width, length, height, steps) { + module _slice(depth, x, y, r) { + echo("called with: ", depth, x, y, r); + circle_centers = [ + [(x/2)-r, (y/2)-r, 0], + [(-x/2)+r, (y/2)-r, 0], + [(x/2)-r, (-y/2)+r, 0], + [(-x/2)+r, (-y/2)+r, 0] + + ]; + translate([0,0,depth/2])cube([x-2*r,y,depth], center=true); + translate([0,0,depth/2])cube([x,y-2*r,depth], center=true); + for (center = circle_centers) { + translate(center) cylinder(depth, r, r, $fn=100); + } + } + for (slice_height = [0:height/steps:height]) { + r = (diameter) * (slice_height/height); + translate([0,0,slice_height]) { + _slice(height/steps , width, length, r/2); + } + } +} +module filitedPyramidForce(bottom_shape, diameter, width, length, height, steps) { + module single_cone(r,x,y,z) { + r = diameter/2; + circle_face = [[ for (i = [0:1: steps]) i ]]; + theta = 360/steps; + circle_points = [for (step = [0:1:steps]) [r*cos(theta*step), r*sin(theta*step), z]]; + final_points = [[x,y,0]]; + all_points = concat(circle_points, final_points); + triangles = [for (step = [0:1:steps-1]) [step, step+1, steps+1]]; + faces = concat(circle_face, triangles); + polyhedron(all_points, faces); + } + module square_section(r, x, y, z) { + points = [ + [x,y,0], + [-x,y,0], + [-x,-y,0], + [x,-y,0], + [r,0,z], + [0,r,z], + [-r,0,z], + [0,-r,z], + ]; + faces = [ + [0,1,2,3], + [4,5,6,7], + [4, 0, 3], + [5, 0, 1], + [6, 1, 2], + [7, 2, 3], + ]; + polyhedron(points, faces); + } + circle_height = bottom_shape == "square" ? height : -height; + translate_height = bottom_shape == "square" ? 0 : height; + translate ([0,0, translate_height]) { + union() { + single_cone(diameter/2, width/2, length/2, circle_height); + single_cone(diameter/2, -width/2, length/2, circle_height); + single_cone(diameter/2, width/2, -length/2, circle_height); + single_cone(diameter/2, -width/2, -length/2, circle_height); + square_section(diameter/2, width/2, length/2, circle_height); + } + } +} + +module filitedPyramid(bottom_shape, diameter, width, length, height) { + if (width == length && width == diameter) { + filitedPyramidSquare(bottom_shape, diameter, width, length, height, 100); + } + else { + filitedPyramidForce(bottom_shape, diameter, width, length, height, 100); + } +}""" + + class RoundedPyramidSegment(BaseModel): shape: RoundedPyramid = Field( ..., description="Denote shape as a rounded pyramidal segment" From 4aaffc66453901ec90615bcdaaf745ef120f3cd2 Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Fri, 4 Oct 2024 10:35:02 -0400 Subject: [PATCH 21/35] fix the typing changes in the tests --- .../geometry/test_frustum_helpers.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py index dbb9fa1e4cb..0a93141bb88 100644 --- a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py +++ b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py @@ -207,13 +207,13 @@ def test_cross_section_area_rectangular(x_dimension: float, y_dimension: float) @pytest.mark.parametrize("well", fake_frusta()) def test_volume_and_height_circular(well: List[Any]) -> None: """Test both volume and height calculations for circular frusta.""" - if well[-1]["shape"] == "spherical": + if well[-1].shape == "spherical": return - total_height = well[0]["topHeight"] + total_height = well[0].topHeight for segment in well: - if segment["shape"] == "conical": - top_radius = segment["topDiameter"] / 2 - bottom_radius = segment["bottomDiameter"] / 2 + if segment.shape == "conical": + top_radius = segment.topDiameter / 2 + bottom_radius = segment.bottomDiameter / 2 a = pi * ((top_radius - bottom_radius) ** 2) / (3 * total_height**2) b = pi * bottom_radius * (top_radius - bottom_radius) / total_height c = pi * bottom_radius**2 @@ -249,15 +249,15 @@ def test_volume_and_height_circular(well: List[Any]) -> None: @pytest.mark.parametrize("well", fake_frusta()) def test_volume_and_height_rectangular(well: List[Any]) -> None: """Test both volume and height calculations for rectangular frusta.""" - if well[-1]["shape"] == "spherical": + if well[-1].shape == "spherical": return - total_height = well[0]["topHeight"] + total_height = well[0].topHeight for segment in well: - if segment["shape"] == "pyramidal": - top_length = segment["topYDimension"] - top_width = segment["topXDimension"] - bottom_length = segment["bottomYDimension"] - bottom_width = segment["bottomXDimension"] + if segment.shape == "pyramidal": + top_length = segment.topYDimension + top_width = segment.topXDimension + bottom_length = segment.bottomYDimension + bottom_width = segment.bottomXDimension a = ( (top_length - bottom_length) * (top_width - bottom_width) @@ -306,22 +306,22 @@ def test_volume_and_height_rectangular(well: List[Any]) -> None: @pytest.mark.parametrize("well", fake_frusta()) def test_volume_and_height_spherical(well: List[Any]) -> None: """Test both volume and height calculations for spherical segments.""" - if well[0]["shape"] == "spherical": - for target_height in range(round(well[0]["topHeight"])): + if well[0].shape == "spherical": + for target_height in range(round(well[0].topHeight)): expected_volume = ( (1 / 3) * pi * (target_height**2) - * (3 * well[0]["radiusOfCurvature"] - target_height) + * (3 * well[0].radiusOfCurvature - target_height) ) found_volume = _volume_from_height_spherical( target_height=target_height, - radius_of_curvature=well[0]["radiusOfCurvature"], + radius_of_curvature=well[0].radiusOfCurvature, ) assert found_volume == expected_volume found_height = _height_from_volume_spherical( volume=found_volume, - radius_of_curvature=well[0]["radiusOfCurvature"], - total_frustum_height=well[0]["topHeight"], + radius_of_curvature=well[0].radiusOfCurvature, + total_frustum_height=well[0].topHeight, ) assert isclose(found_height, target_height) From 608320ae045ef46e5a2de5b31014849f42f61780 Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Fri, 4 Oct 2024 11:06:07 -0400 Subject: [PATCH 22/35] typo --- .../python/opentrons_shared_data/labware/labware_definition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-data/python/opentrons_shared_data/labware/labware_definition.py b/shared-data/python/opentrons_shared_data/labware/labware_definition.py index e1b83632913..3e52d4d26cb 100644 --- a/shared-data/python/opentrons_shared_data/labware/labware_definition.py +++ b/shared-data/python/opentrons_shared_data/labware/labware_definition.py @@ -247,7 +247,7 @@ class SphericalSegment(BaseModel): ) bottomHeight: _NonNegativeNumber = Field( ..., - description="Hight of the bottom of the segment, must be 0.0", + description="Height of the bottom of the segment, must be 0.0", ) From 9cf5ef14915c8cf2a70f9694a94956218d505a76 Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Fri, 4 Oct 2024 11:07:19 -0400 Subject: [PATCH 23/35] fancy *matching* you here --- .../protocol_engine/state/frustum_helpers.py | 176 +++++++++--------- 1 file changed, 90 insertions(+), 86 deletions(-) diff --git a/api/src/opentrons/protocol_engine/state/frustum_helpers.py b/api/src/opentrons/protocol_engine/state/frustum_helpers.py index d90f8dbccf9..f19cd61958b 100644 --- a/api/src/opentrons/protocol_engine/state/frustum_helpers.py +++ b/api/src/opentrons/protocol_engine/state/frustum_helpers.py @@ -8,6 +8,9 @@ from opentrons_shared_data.labware.labware_definition import ( InnerWellGeometry, WellSegment, + SphericalSegment, + ConicalFrustum, + PyramidalFrustum, ) @@ -207,38 +210,39 @@ def get_well_volumetric_capacity( for segment in sorted_well: section_volume: Optional[float] = None - if segment.shape == "spherical": - if sorted_well[0] != segment: - raise InvalidWellDefinitionError( - "spherical segment must only be at the bottom of a well." + match segment: + case SphericalSegment: + if sorted_well[0] != segment: + raise InvalidWellDefinitionError( + "spherical segment must only be at the bottom of a well." + ) + section_volume = _volume_from_height_spherical( + target_height=segment.topHeight, + radius_of_curvature=segment.radiusOfCurvature, + ) + case PyramidalFrustum: + section_height = segment.topHeight - segment.bottomHeight + section_volume = _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, + ) + case ConicalFrustum: + section_height = segment.topHeight - segment.bottomHeight + section_volume = _volume_from_height_circular( + target_height=section_height, + total_frustum_height=section_height, + bottom_radius=(segment.bottomDiameter / 2), + top_radius=(segment.topDiameter / 2), + ) + case _: + # TODO: implement volume calculations for truncated circular and rounded rectangular segments + raise NotImplementedError( + f"volume calculation for shape: {segment.shape} not yet implemented." ) - section_volume = _volume_from_height_spherical( - target_height=segment.topHeight, - radius_of_curvature=segment.radiusOfCurvature, - ) - elif segment.shape == "pyramidal": - section_height = segment.topHeight - segment.bottomHeight - section_volume = _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, - ) - elif segment.shape == "conical": - section_height = segment.topHeight - segment.bottomHeight - section_volume = _volume_from_height_circular( - target_height=section_height, - total_frustum_height=section_height, - bottom_radius=(segment.bottomDiameter / 2), - top_radius=(segment.topDiameter / 2), - ) - # TODO: implement volume calculations for truncated circular and rounded rectangular segments - if not section_volume: - raise NotImplementedError( - f"volume calculation for shape: {segment.shape} not yet implemented." - ) well_volume.append((segment.topHeight, section_volume)) return well_volume @@ -249,33 +253,33 @@ def height_at_volume_within_section( section_height: float, ) -> float: """Calculate a height within a bounded section according to geometry.""" - if section.shape == "spherical": - partial_height = _height_from_volume_spherical( - volume=target_volume_relative, - total_frustum_height=section_height, - radius_of_curvature=section.radiusOfCurvature, - ) - elif section.shape == "conical": - partial_height = _height_from_volume_circular( - volume=target_volume_relative, - top_radius=(section.bottomDiameter / 2), - bottom_radius=(section.topDiameter / 2), - total_frustum_height=section_height, - ) - elif section.shape == "pyramidal": - partial_height = _height_from_volume_rectangular( - volume=target_volume_relative, - total_frustum_height=section_height, - bottom_width=section.bottomXDimension, - bottom_length=section.bottomYDimension, - top_width=section.topXDimension, - top_length=section.topYDimension, - ) - else: - raise NotImplementedError( - "Height from volume calculation not yet implemented for this well shape." - ) - return partial_height + match section: + case SphericalSegment: + return _height_from_volume_spherical( + volume=target_volume_relative, + total_frustum_height=section_height, + radius_of_curvature=section.radiusOfCurvature, + ) + case ConicalFrustum: + return _height_from_volume_circular( + volume=target_volume_relative, + top_radius=(section.bottomDiameter / 2), + bottom_radius=(section.topDiameter / 2), + total_frustum_height=section_height, + ) + case PyramidalFrustum: + return _height_from_volume_rectangular( + volume=target_volume_relative, + total_frustum_height=section_height, + bottom_width=section.bottomXDimension, + bottom_length=section.bottomYDimension, + top_width=section.topXDimension, + top_length=section.topYDimension, + ) + case _: + raise NotImplementedError( + "Height from volume calculation not yet implemented for this well shape." + ) def volume_at_height_within_section( @@ -284,34 +288,34 @@ def volume_at_height_within_section( section_height: float, ) -> float: """Calculate a volume within a bounded section according to geometry.""" - if section.shape == "spherical": - partial_volume = _volume_from_height_spherical( - target_height=target_height_relative, - radius_of_curvature=section.radiusOfCurvature, - ) - elif section.shape == "conical": - partial_volume = _volume_from_height_circular( - target_height=target_height_relative, - total_frustum_height=section_height, - bottom_radius=(section.bottomDiameter / 2), - top_radius=(section.topDiameter / 2), - ) - elif section.shape == "pyramidal": - partial_volume = _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, - ) - # TODO(cm): this would be the NEST-96 2uL wells referenced in EXEC-712 - # we need to input the math attached to that issue - else: - raise NotImplementedError( - "Height from volume calculation not yet implemented for this well shape." - ) - return partial_volume + match section: + case SphericalSegment: + return _volume_from_height_spherical( + target_height=target_height_relative, + radius_of_curvature=section.radiusOfCurvature, + ) + 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), + ) + case PyramidalFrustum: + 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, + ) + 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 + raise NotImplementedError( + "Height from volume calculation not yet implemented for this well shape." + ) def _find_volume_in_partial_frustum( From c09c4efef40a2b896eab6fe1df8c9e1937130e87 Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Fri, 4 Oct 2024 11:35:16 -0400 Subject: [PATCH 24/35] factor out some things so we can get rid of optionals --- .../protocol_engine/state/frustum_helpers.py | 106 ++++++++---------- 1 file changed, 48 insertions(+), 58 deletions(-) diff --git a/api/src/opentrons/protocol_engine/state/frustum_helpers.py b/api/src/opentrons/protocol_engine/state/frustum_helpers.py index f19cd61958b..b1f0e4163dd 100644 --- a/api/src/opentrons/protocol_engine/state/frustum_helpers.py +++ b/api/src/opentrons/protocol_engine/state/frustum_helpers.py @@ -1,5 +1,5 @@ """Helper functions for liquid-level related calculations inside a given frustum.""" -from typing import List, Tuple, Optional +from typing import List, Tuple from numpy import pi, iscomplex, roots, real from math import isclose @@ -197,6 +197,38 @@ def _height_from_volume_spherical( return height +def _get_segment_capacity(segment: WellSegment) -> float: + match segment: + case SphericalSegment: + return _volume_from_height_spherical( + target_height=segment.topHeight, + radius_of_curvature=segment.radiusOfCurvature, + ) + case PyramidalFrustum: + section_height = segment.topHeight - segment.bottomHeight + 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, + ) + case ConicalFrustum: + section_height = segment.topHeight - segment.bottomHeight + return _volume_from_height_circular( + target_height=section_height, + total_frustum_height=section_height, + bottom_radius=(segment.bottomDiameter / 2), + top_radius=(segment.topDiameter / 2), + ) + case _: + # TODO: implement volume calculations for truncated circular and rounded rectangular segments + raise NotImplementedError( + f"volume calculation for shape: {segment.shape} not yet implemented." + ) + + def get_well_volumetric_capacity( well_geometry: InnerWellGeometry, ) -> List[Tuple[float, float]]: @@ -209,40 +241,7 @@ def get_well_volumetric_capacity( sorted_well = sorted(well_geometry.sections, key=lambda section: section.topHeight) for segment in sorted_well: - section_volume: Optional[float] = None - match segment: - case SphericalSegment: - if sorted_well[0] != segment: - raise InvalidWellDefinitionError( - "spherical segment must only be at the bottom of a well." - ) - section_volume = _volume_from_height_spherical( - target_height=segment.topHeight, - radius_of_curvature=segment.radiusOfCurvature, - ) - case PyramidalFrustum: - section_height = segment.topHeight - segment.bottomHeight - section_volume = _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, - ) - case ConicalFrustum: - section_height = segment.topHeight - segment.bottomHeight - section_volume = _volume_from_height_circular( - target_height=section_height, - total_frustum_height=section_height, - bottom_radius=(segment.bottomDiameter / 2), - top_radius=(segment.topDiameter / 2), - ) - case _: - # TODO: implement volume calculations for truncated circular and rounded rectangular segments - raise NotImplementedError( - f"volume calculation for shape: {segment.shape} not yet implemented." - ) + section_volume = _get_segment_capacity(segment) well_volume.append((segment.topHeight, section_volume)) return well_volume @@ -321,24 +320,21 @@ def volume_at_height_within_section( def _find_volume_in_partial_frustum( sorted_well: List[WellSegment], target_height: float, -) -> Optional[float]: +) -> float: """Look through a sorted list of frusta for a target height, and find the volume at that height.""" - partial_volume: Optional[float] = None for segment in sorted_well: if segment.bottomHeight < target_height < segment.topHeight: relative_target_height = target_height - segment.bottomHeight section_height = segment.topHeight - segment.bottomHeight - partial_volume = volume_at_height_within_section( + return volume_at_height_within_section( section=segment, target_height_relative=relative_target_height, section_height=section_height, ) - if not partial_volume: - # if we've looked through all sections and can't find the target volume, raise an error - raise InvalidLiquidHeightFound( - f"Unable to find volume at given well-height {target_height}." - ) - return partial_volume + # if we've looked through all sections and can't find the target volume, raise an error + raise InvalidLiquidHeightFound( + f"Unable to find volume at given well-height {target_height}." + ) def find_volume_at_well_height( @@ -361,16 +357,12 @@ def find_volume_at_well_height( if target_height == boundary_height: return closed_section_volume # find the section the target height is in and compute the volume - # since bottomShape is not in list of frusta, check here first sorted_well = sorted(well_geometry.sections, key=lambda section: section.topHeight) - # TODO(cm): handle non-frustum section that is not at the bottom. partial_volume = _find_volume_in_partial_frustum( sorted_well=sorted_well, target_height=target_height, ) - if not partial_volume: - raise InvalidLiquidHeightFound("Unable to find volume at given well-height.") return partial_volume + closed_section_volume @@ -378,10 +370,9 @@ def _find_height_in_partial_frustum( sorted_well: List[WellSegment], volumetric_capacity: List[Tuple[float, float]], target_volume: float, -) -> Optional[float]: +) -> float: """Look through a sorted list of frusta for a target volume, and find the height at that volume.""" bottom_section_volume = 0.0 - height_within_well: Optional[float] = None for section, capacity in zip(sorted_well, volumetric_capacity): section_top_height, section_volume = capacity if bottom_section_volume < target_volume < section_volume: @@ -392,11 +383,15 @@ def _find_height_in_partial_frustum( target_volume_relative=relative_target_volume, section_height=relative_section_height, ) - height_within_well = partial_height + section.bottomHeight + return partial_height + section.bottomHeight # bottom section volume should always be the volume enclosed in the previously # viewed section bottom_section_volume = section_volume - return height_within_well + + # if we've looked through all sections and can't find the target volume, raise an error + raise InvalidLiquidHeightFound( + f"Unable to find height at given volume {target_volume}." + ) def find_height_at_well_volume( @@ -413,13 +408,8 @@ def find_height_at_well_volume( sorted_well = sorted(well_geometry.sections, key=lambda section: section.topHeight) # find the section the target volume is in and compute the height - well_height = _find_height_in_partial_frustum( + return _find_height_in_partial_frustum( sorted_well=sorted_well, volumetric_capacity=volumetric_capacity, target_volume=target_volume, ) - if not well_height: - raise InvalidLiquidHeightFound( - f"Unable to find height at given well-volume {target_volume}." - ) - return well_height From 965ccb4332c1c50d1cfaaaffe9d85bbada1ccdbd Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Fri, 4 Oct 2024 11:46:58 -0400 Subject: [PATCH 25/35] lint --- .../protocol_engine/state/frustum_helpers.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/api/src/opentrons/protocol_engine/state/frustum_helpers.py b/api/src/opentrons/protocol_engine/state/frustum_helpers.py index b1f0e4163dd..51b00c4b2ad 100644 --- a/api/src/opentrons/protocol_engine/state/frustum_helpers.py +++ b/api/src/opentrons/protocol_engine/state/frustum_helpers.py @@ -3,7 +3,7 @@ from numpy import pi, iscomplex, roots, real from math import isclose -from ..errors.exceptions import InvalidLiquidHeightFound, InvalidWellDefinitionError +from ..errors.exceptions import InvalidLiquidHeightFound from opentrons_shared_data.labware.labware_definition import ( InnerWellGeometry, @@ -199,12 +199,12 @@ def _height_from_volume_spherical( def _get_segment_capacity(segment: WellSegment) -> float: match segment: - case SphericalSegment: + case SphericalSegment(): return _volume_from_height_spherical( target_height=segment.topHeight, radius_of_curvature=segment.radiusOfCurvature, ) - case PyramidalFrustum: + case PyramidalFrustum(): section_height = segment.topHeight - segment.bottomHeight return _volume_from_height_rectangular( target_height=section_height, @@ -214,7 +214,7 @@ def _get_segment_capacity(segment: WellSegment) -> float: top_width=segment.topXDimension, total_frustum_height=section_height, ) - case ConicalFrustum: + case ConicalFrustum(): section_height = segment.topHeight - segment.bottomHeight return _volume_from_height_circular( target_height=section_height, @@ -253,20 +253,20 @@ def height_at_volume_within_section( ) -> float: """Calculate a height within a bounded section according to geometry.""" match section: - case SphericalSegment: + case SphericalSegment(): return _height_from_volume_spherical( volume=target_volume_relative, total_frustum_height=section_height, radius_of_curvature=section.radiusOfCurvature, ) - case ConicalFrustum: + case ConicalFrustum(): return _height_from_volume_circular( volume=target_volume_relative, top_radius=(section.bottomDiameter / 2), bottom_radius=(section.topDiameter / 2), total_frustum_height=section_height, ) - case PyramidalFrustum: + case PyramidalFrustum(): return _height_from_volume_rectangular( volume=target_volume_relative, total_frustum_height=section_height, @@ -288,19 +288,19 @@ def volume_at_height_within_section( ) -> float: """Calculate a volume within a bounded section according to geometry.""" match section: - case SphericalSegment: + case SphericalSegment(): return _volume_from_height_spherical( target_height=target_height_relative, radius_of_curvature=section.radiusOfCurvature, ) - case ConicalFrustum: + 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), ) - case PyramidalFrustum: + case PyramidalFrustum(): return _volume_from_height_rectangular( target_height=target_height_relative, total_frustum_height=section_height, From cc9e6f190424d0b305c7c8a004e717b2532b1baf Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Fri, 4 Oct 2024 12:38:51 -0400 Subject: [PATCH 26/35] fix impossible shape --- api/tests/opentrons/protocols/geometry/test_frustum_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py index 0a93141bb88..5bd31a19132 100644 --- a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py +++ b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py @@ -50,7 +50,7 @@ def fake_frusta() -> List[List[Any]]: shape="conical", topDiameter=23.0, bottomDiameter=3.0, - topHeight=1.0, + topHeight=2.0, bottomHeight=1.0, ), SphericalSegment( From 9d50757739ba64fe1ec6eb2b2dbb48e98a9bcf17 Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Fri, 4 Oct 2024 12:39:48 -0400 Subject: [PATCH 27/35] remove iffy float check and add test to prove it works as intended anyway --- .../protocol_engine/state/frustum_helpers.py | 3 --- .../protocols/geometry/test_frustum_helpers.py | 13 +++++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/api/src/opentrons/protocol_engine/state/frustum_helpers.py b/api/src/opentrons/protocol_engine/state/frustum_helpers.py index 51b00c4b2ad..7786b16384d 100644 --- a/api/src/opentrons/protocol_engine/state/frustum_helpers.py +++ b/api/src/opentrons/protocol_engine/state/frustum_helpers.py @@ -402,9 +402,6 @@ def find_height_at_well_volume( max_volume = volumetric_capacity[-1][1] if target_volume < 0 or target_volume > max_volume: raise InvalidLiquidHeightFound("Invalid target volume.") - for section_height, section_volume in volumetric_capacity: - if target_volume == section_volume: - return section_height sorted_well = sorted(well_geometry.sections, key=lambda section: section.topHeight) # find the section the target volume is in and compute the height diff --git a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py index 5bd31a19132..4b86f4fac09 100644 --- a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py +++ b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py @@ -19,6 +19,8 @@ _height_from_volume_circular, _height_from_volume_rectangular, _height_from_volume_spherical, + height_at_volume_within_section, + _get_segment_capacity, ) from opentrons.protocol_engine.errors.exceptions import InvalidLiquidHeightFound @@ -325,3 +327,14 @@ def test_volume_and_height_spherical(well: List[Any]) -> None: total_frustum_height=well[0].topHeight, ) assert isclose(found_height, target_height) + + +@pytest.mark.parametrize("well", fake_frusta()) +def test_height_at_volume_within_section(well: List[Any]) -> None: + """Test that finding the height when volume ~= capacity works.""" + for segment in well: + print(segment) + segment_height = segment.topHeight - segment.bottomHeight + height = height_at_volume_within_section(segment, _get_segment_capacity(segment), segment_height) + assert isclose(height, segment_height) + From 05fb103d23908372d50403718761a8369fb0314b Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Fri, 4 Oct 2024 12:53:16 -0400 Subject: [PATCH 28/35] format --- .../opentrons/protocols/geometry/test_frustum_helpers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py index 4b86f4fac09..9420e61d06c 100644 --- a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py +++ b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py @@ -335,6 +335,7 @@ def test_height_at_volume_within_section(well: List[Any]) -> None: for segment in well: print(segment) segment_height = segment.topHeight - segment.bottomHeight - height = height_at_volume_within_section(segment, _get_segment_capacity(segment), segment_height) + height = height_at_volume_within_section( + segment, _get_segment_capacity(segment), segment_height + ) assert isclose(height, segment_height) - From 22a0fe506b4cfe5b09f57b7414a2d15c3af5cb06 Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Fri, 4 Oct 2024 13:22:50 -0400 Subject: [PATCH 29/35] format js --- shared-data/labware/schemas/3.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-data/labware/schemas/3.json b/shared-data/labware/schemas/3.json index 6e6db94342c..59de59db3a4 100644 --- a/shared-data/labware/schemas/3.json +++ b/shared-data/labware/schemas/3.json @@ -128,7 +128,7 @@ "bottomHeight" ], "properties": { - "shape": { + "shape": { "type": "string", "enum": ["pyramidal"] }, From 4efb26c6154db69060e7d18b81939a7a61de3cc5 Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Fri, 4 Oct 2024 13:23:54 -0400 Subject: [PATCH 30/35] errant print --- api/tests/opentrons/protocols/geometry/test_frustum_helpers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py index 9420e61d06c..b6e6ad3ebb1 100644 --- a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py +++ b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py @@ -333,7 +333,6 @@ def test_volume_and_height_spherical(well: List[Any]) -> None: def test_height_at_volume_within_section(well: List[Any]) -> None: """Test that finding the height when volume ~= capacity works.""" for segment in well: - print(segment) segment_height = segment.topHeight - segment.bottomHeight height = height_at_volume_within_section( segment, _get_segment_capacity(segment), segment_height From 4d2bab0309b11fc72800b1148eea9d11fe8b4c79 Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Mon, 7 Oct 2024 10:29:18 -0400 Subject: [PATCH 31/35] fix schema descriptions --- shared-data/labware/schemas/3.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shared-data/labware/schemas/3.json b/shared-data/labware/schemas/3.json index 59de59db3a4..b00e45404a9 100644 --- a/shared-data/labware/schemas/3.json +++ b/shared-data/labware/schemas/3.json @@ -67,7 +67,7 @@ }, "SphericalSegment": { "type": "object", - "description": "A list of all of the sections of the well that have a contiguous shape", + "description": "A partial sphere shaped section at the bottom of the well.", "additionalProperties": false, "required": ["shape", "radiusOfCurvature", "topHeight"], "properties": { @@ -88,7 +88,7 @@ }, "ConicalFrustum": { "type": "object", - "description": "A list of all of the sections of the well that have a contiguous shape", + "description": "A cone or conical segment, bounded by two circles on the top and bottom.", "required": [ "shape", "bottomDiameter", @@ -117,7 +117,7 @@ }, "PyramidalFrustum": { "type": "object", - "description": "A list of all of the sections of the well that have a contiguous shape", + "description": "A pyramidal shape bounded by two rectangles on the top and bottom", "required": [ "shape", "bottomXDimension", From 7eec633ca8bc3092a4a29938ccefc2d484cc8e29 Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Mon, 7 Oct 2024 10:29:35 -0400 Subject: [PATCH 32/35] make bottom hieght required for a spherical segment --- shared-data/labware/schemas/3.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-data/labware/schemas/3.json b/shared-data/labware/schemas/3.json index b00e45404a9..28999ca348f 100644 --- a/shared-data/labware/schemas/3.json +++ b/shared-data/labware/schemas/3.json @@ -69,7 +69,7 @@ "type": "object", "description": "A partial sphere shaped section at the bottom of the well.", "additionalProperties": false, - "required": ["shape", "radiusOfCurvature", "topHeight"], + "required": ["shape", "radiusOfCurvature", "topHeight", "bottomHeight"], "properties": { "shape": { "type": "string", From 070e119a098c46144ab1e3fbd6849dd225688684 Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Mon, 7 Oct 2024 10:51:57 -0400 Subject: [PATCH 33/35] My symantics knows no bounds other than two parallel 4 sided polygons. --- .../protocol_engine/state/frustum_helpers.py | 8 +++--- .../protocol_runner/test_json_translator.py | 10 +++---- .../geometry/test_frustum_helpers.py | 24 ++++++++-------- shared-data/labware/schemas/3.json | 14 +++++----- .../labware/constants.py | 4 +-- .../labware/labware_definition.py | 28 +++++++++---------- 6 files changed, 44 insertions(+), 44 deletions(-) diff --git a/api/src/opentrons/protocol_engine/state/frustum_helpers.py b/api/src/opentrons/protocol_engine/state/frustum_helpers.py index 7786b16384d..4f132ac3b40 100644 --- a/api/src/opentrons/protocol_engine/state/frustum_helpers.py +++ b/api/src/opentrons/protocol_engine/state/frustum_helpers.py @@ -10,7 +10,7 @@ WellSegment, SphericalSegment, ConicalFrustum, - PyramidalFrustum, + CuboidalFrustum, ) @@ -204,7 +204,7 @@ def _get_segment_capacity(segment: WellSegment) -> float: target_height=segment.topHeight, radius_of_curvature=segment.radiusOfCurvature, ) - case PyramidalFrustum(): + case CuboidalFrustum(): section_height = segment.topHeight - segment.bottomHeight return _volume_from_height_rectangular( target_height=section_height, @@ -266,7 +266,7 @@ def height_at_volume_within_section( bottom_radius=(section.topDiameter / 2), total_frustum_height=section_height, ) - case PyramidalFrustum(): + case CuboidalFrustum(): return _height_from_volume_rectangular( volume=target_volume_relative, total_frustum_height=section_height, @@ -300,7 +300,7 @@ def volume_at_height_within_section( bottom_radius=(section.bottomDiameter / 2), top_radius=(section.topDiameter / 2), ) - case PyramidalFrustum(): + case CuboidalFrustum(): return _volume_from_height_rectangular( target_height=target_height_relative, total_frustum_height=section_height, diff --git a/api/tests/opentrons/protocol_runner/test_json_translator.py b/api/tests/opentrons/protocol_runner/test_json_translator.py index 4fe366d4582..69edd3c1445 100644 --- a/api/tests/opentrons/protocol_runner/test_json_translator.py +++ b/api/tests/opentrons/protocol_runner/test_json_translator.py @@ -13,7 +13,7 @@ Group, Metadata1, WellDefinition, - PyramidalFrustum, + CuboidalFrustum, InnerWellGeometry, SphericalSegment, ) @@ -691,8 +691,8 @@ def _load_labware_definition_data() -> LabwareDefinition: innerLabwareGeometry={ "welldefinition1111": InnerWellGeometry( sections=[ - PyramidalFrustum( - shape="pyramidal", + CuboidalFrustum( + shape="cuboidal", topXDimension=7.6, topYDimension=8.5, bottomXDimension=5.6, @@ -700,8 +700,8 @@ def _load_labware_definition_data() -> LabwareDefinition: topHeight=45, bottomHeight=20, ), - PyramidalFrustum( - shape="pyramidal", + CuboidalFrustum( + shape="cuboidal", topXDimension=5.6, topYDimension=6.5, bottomXDimension=4.5, diff --git a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py index b6e6ad3ebb1..0b8d3429527 100644 --- a/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py +++ b/api/tests/opentrons/protocols/geometry/test_frustum_helpers.py @@ -4,7 +4,7 @@ from opentrons_shared_data.labware.labware_definition import ( ConicalFrustum, - PyramidalFrustum, + CuboidalFrustum, SphericalSegment, ) from opentrons.protocol_engine.state.frustum_helpers import ( @@ -30,8 +30,8 @@ def fake_frusta() -> List[List[Any]]: frusta = [] frusta.append( [ - PyramidalFrustum( - shape="pyramidal", + CuboidalFrustum( + shape="cuboidal", topXDimension=9.0, topYDimension=10.0, bottomXDimension=8.0, @@ -39,8 +39,8 @@ def fake_frusta() -> List[List[Any]]: topHeight=10.0, bottomHeight=5.0, ), - PyramidalFrustum( - shape="pyramidal", + CuboidalFrustum( + shape="cuboidal", topXDimension=8.0, topYDimension=9.0, bottomXDimension=15.0, @@ -65,8 +65,8 @@ def fake_frusta() -> List[List[Any]]: ) frusta.append( [ - PyramidalFrustum( - shape="pyramidal", + CuboidalFrustum( + shape="cuboidal", topXDimension=8.0, topYDimension=70.0, bottomXDimension=7.0, @@ -74,8 +74,8 @@ def fake_frusta() -> List[List[Any]]: topHeight=3.5, bottomHeight=2.0, ), - PyramidalFrustum( - shape="pyramidal", + CuboidalFrustum( + shape="cuboidal", topXDimension=8.0, topYDimension=80.0, bottomXDimension=8.0, @@ -139,8 +139,8 @@ def fake_frusta() -> List[List[Any]]: ) frusta.append( [ - PyramidalFrustum( - shape="pyramidal", + CuboidalFrustum( + shape="cuboidal", topXDimension=27.0, topYDimension=36.0, bottomXDimension=36.0, @@ -255,7 +255,7 @@ def test_volume_and_height_rectangular(well: List[Any]) -> None: return total_height = well[0].topHeight for segment in well: - if segment.shape == "pyramidal": + if segment.shape == "cuboidal": top_length = segment.topYDimension top_width = segment.topXDimension bottom_length = segment.bottomYDimension diff --git a/shared-data/labware/schemas/3.json b/shared-data/labware/schemas/3.json index 28999ca348f..51db0e80746 100644 --- a/shared-data/labware/schemas/3.json +++ b/shared-data/labware/schemas/3.json @@ -115,9 +115,9 @@ } } }, - "PyramidalFrustum": { + "CuboidalFrustum": { "type": "object", - "description": "A pyramidal shape bounded by two rectangles on the top and bottom", + "description": "A cuboidal shape bounded by two rectangles on the top and bottom", "required": [ "shape", "bottomXDimension", @@ -130,7 +130,7 @@ "properties": { "shape": { "type": "string", - "enum": ["pyramidal"] + "enum": ["cuboidal"] }, "bottomXDimension": { "type": "number" @@ -190,9 +190,9 @@ } } }, - "RoundedPyramidSegment": { + "RoundedCuboidSegment": { "type": "object", - "description": "A pyramidal frustum where each corner is filleted out by circles with centers on the diagonals between opposite corners", + "description": "A cuboidal frustum where each corner is filleted out by circles with centers on the diagonals between opposite corners", "required": [ "shape", "bottomCrossSection", @@ -241,13 +241,13 @@ "$ref": "#/definitions/ConicalFrustum" }, { - "$ref": "#/definitions/PyramidalFrustum" + "$ref": "#/definitions/CuboidalFrustum" }, { "$ref": "#/definitions/SquaredConeSegment" }, { - "$ref": "#/definitions/RoundedPyramidSegment" + "$ref": "#/definitions/RoundedCuboidSegment" }, { "$ref": "#/definitions/SphericalSegment" diff --git a/shared-data/python/opentrons_shared_data/labware/constants.py b/shared-data/python/opentrons_shared_data/labware/constants.py index 3b12a88b821..9973604937b 100644 --- a/shared-data/python/opentrons_shared_data/labware/constants.py +++ b/shared-data/python/opentrons_shared_data/labware/constants.py @@ -14,7 +14,7 @@ # These shapes are used to describe the 3D primatives used to build wells Conical = Literal["conical"] -Pyramidal = Literal["pyramidal"] +Cuboidal = Literal["cuboidal"] SquaredCone = Literal["squaredcone"] -RoundedPyramid = Literal["roundedpyramid"] +RoundedCuboid = Literal["roundedcuboid"] Spherical = Literal["spherical"] diff --git a/shared-data/python/opentrons_shared_data/labware/labware_definition.py b/shared-data/python/opentrons_shared_data/labware/labware_definition.py index 3e52d4d26cb..a818afc106a 100644 --- a/shared-data/python/opentrons_shared_data/labware/labware_definition.py +++ b/shared-data/python/opentrons_shared_data/labware/labware_definition.py @@ -21,8 +21,8 @@ from .constants import ( Conical, - Pyramidal, - RoundedPyramid, + Cuboidal, + RoundedCuboid, SquaredCone, Spherical, WellShape, @@ -271,8 +271,8 @@ class ConicalFrustum(BaseModel): ) -class PyramidalFrustum(BaseModel): - shape: Pyramidal = Field(..., description="Denote shape as pyramidal") +class CuboidalFrustum(BaseModel): + shape: Cuboidal = Field(..., description="Denote shape as cuboidal") bottomXDimension: _NonNegativeNumber = Field( ..., description="x dimension of the bottom cross-section of a rectangular frustum", @@ -350,7 +350,7 @@ class SquaredConeSegment(BaseModel): """ -module filitedPyramidSquare(bottom_shape, diameter, width, length, height, steps) { +module filitedCuboidSquare(bottom_shape, diameter, width, length, height, steps) { module _slice(depth, x, y, r) { echo("called with: ", depth, x, y, r); circle_centers = [ @@ -373,7 +373,7 @@ class SquaredConeSegment(BaseModel): } } } -module filitedPyramidForce(bottom_shape, diameter, width, length, height, steps) { +module filitedCuboidForce(bottom_shape, diameter, width, length, height, steps) { module single_cone(r,x,y,z) { r = diameter/2; circle_face = [[ for (i = [0:1: steps]) i ]]; @@ -419,19 +419,19 @@ class SquaredConeSegment(BaseModel): } } -module filitedPyramid(bottom_shape, diameter, width, length, height) { +module filitedCuboid(bottom_shape, diameter, width, length, height) { if (width == length && width == diameter) { - filitedPyramidSquare(bottom_shape, diameter, width, length, height, 100); + filitedCuboidSquare(bottom_shape, diameter, width, length, height, 100); } else { - filitedPyramidForce(bottom_shape, diameter, width, length, height, 100); + filitedCuboidForce(bottom_shape, diameter, width, length, height, 100); } }""" -class RoundedPyramidSegment(BaseModel): - shape: RoundedPyramid = Field( - ..., description="Denote shape as a rounded pyramidal segment" +class RoundedCuboidSegment(BaseModel): + shape: RoundedCuboid = Field( + ..., description="Denote shape as a rounded cuboidal segment" ) bottomCrossSection: WellShape = Field( ..., @@ -490,9 +490,9 @@ class Group(BaseModel): WellSegment = Union[ ConicalFrustum, - PyramidalFrustum, + CuboidalFrustum, SquaredConeSegment, - RoundedPyramidSegment, + RoundedCuboidSegment, SphericalSegment, ] From 59e4130f502d197f9a0057c38cd9ae24f1957c9c Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Mon, 7 Oct 2024 11:53:06 -0400 Subject: [PATCH 34/35] ohh javascript --- shared-data/js/types.ts | 49 ++++++++++++++----- .../labware/fixtures/3/fixture_2_plate.json | 2 +- shared-data/labware/schemas/3.json | 2 +- 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index dbd8c7f59c7..4406aadf64c 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -162,25 +162,52 @@ export type LabwareWell = LabwareWellProperties & { export interface SphericalSegment { shape: 'spherical' radiusOfCurvature: number - depth: number + topHeight: number + bottomHeight: number } -export interface CircularBoundedSection { - shape: 'circular' - diameter: number +export interface ConicalFrustum { + shape: 'conical' + bottomDiameter: number + topDiameter: number topHeight: number + bottomHeight: number } -export interface RectangularBoundedSection { - shape: 'rectangular' - xDimension: number - yDimension: number +export interface CuboidalFrustum { + shape: 'cuboidal' + bottomXDimension: number + bottomYDimension: number + topXDimension: number + topYDimension: number topHeight: number + bottomHeight: number } -export interface InnerWellGeometry { - frusta: CircularBoundedSection[] | RectangularBoundedSection[] - bottomShape?: SphericalSegment | null +export interface SquaredConeSegment { + shape: 'squaredcone' + bottomCrossSection: string + circleDiameter: number + rectangleXDimension: number + rectangleYDimension: number + topHeight: number + bottomHeight: number +} + +export interface RoundedCuboidSegment { + shape: 'roundedcuboid' + bottomCrossSection: string + circleDiameter: number + rectangleXDimension: number + rectangleYDimension: number + topHeight: number + bottomHeight: number +} + +export type WellSegment = CuboidalFrustum | ConicalFrustum | SquaredConeSegment | SphericalSegment | RoundedCuboidSegment + +export type InnerWellGeometry = { + sections: WellSegment[] } // TODO(mc, 2019-03-21): exact object is tough to use with the initial value in diff --git a/shared-data/labware/fixtures/3/fixture_2_plate.json b/shared-data/labware/fixtures/3/fixture_2_plate.json index a9d740b42ff..19ea2f82ffc 100644 --- a/shared-data/labware/fixtures/3/fixture_2_plate.json +++ b/shared-data/labware/fixtures/3/fixture_2_plate.json @@ -64,7 +64,7 @@ "daiwudhadfhiew": { "sections": [ { - "shape": "pyramidal", + "shape": "cuboidal", "topXDimension": 127.76, "topYDimension": 85.8, "bottomXDimension": 70.0, diff --git a/shared-data/labware/schemas/3.json b/shared-data/labware/schemas/3.json index 51db0e80746..ecd285c554a 100644 --- a/shared-data/labware/schemas/3.json +++ b/shared-data/labware/schemas/3.json @@ -205,7 +205,7 @@ "properties": { "shape": { "type": "string", - "enum": ["roundedpyramid"] + "enum": ["roundedcuboid"] }, "bottomCrossSection": { "type": "string", From e17af4fec04fd88a4276ea17553e4c1fde98287e Mon Sep 17 00:00:00 2001 From: Ryan howard Date: Mon, 7 Oct 2024 14:08:26 -0400 Subject: [PATCH 35/35] format js --- shared-data/js/types.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index 4406aadf64c..0ffb3f7a649 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -204,9 +204,14 @@ export interface RoundedCuboidSegment { bottomHeight: number } -export type WellSegment = CuboidalFrustum | ConicalFrustum | SquaredConeSegment | SphericalSegment | RoundedCuboidSegment - -export type InnerWellGeometry = { +export type WellSegment = + | CuboidalFrustum + | ConicalFrustum + | SquaredConeSegment + | SphericalSegment + | RoundedCuboidSegment + +export interface InnerWellGeometry { sections: WellSegment[] }