Shapes
Every container in the labware library, whether a plate well, a tube, a trough, or a tip, is built from one or more shape sections: Cylinder, Cuboid, Cone, ConicalFrustum, Pyramid, PyramidalFrustum, SphericalCap, HalfSphere, and SphericalSegment. A Container stacks them from top to bottom, and the library uses that model to turn a volume into a liquid level and back.
from unitelabs.labware import Container, ConicalFrustum, Cylinder
# The container of a 50 mL conical tube: a cylinder on top of a conical bottom
container = Container(
max_volume=50_000,
sections=[
Cylinder(radius=13.89, height=98.77), # top section first
ConicalFrustum(radius_lower=3.6, radius_upper=13.89, height=14.88),
],
)
Dimensions at Height
When an operation needs the width of a container at a specific height, for example to move a pipetting channel to the side of a well, the answer depends on the shape. Three methods give the same answer for every concrete shape class, so your code doesn't have to handle each shape type separately:
width_at_height(height)returns the x-axis extent of the cross-section at a given height.depth_at_height(height)returns the y-axis extent of the cross-section at a given height. For circular shapes this equals the width.area_at_height(height)returns the cross-sectional area at a given height.
For stacked containers (any Container with multiple sections), one more method is available:
section_at_height(height)returns atuple[Shape, Decimal]containing the shape section and the local height within that section.Containerdelegateswidth_at_height,depth_at_height, andarea_at_heightthrough this method.
Usage Example
Given the Standard50mLTube defined in Tubes and Tube Racks, a tube with a cylindrical upper section and a conical frustum at the bottom, you can query the width at any height without caring which section you are in:
from unitelabs.labware import Decimal
tube = Standard50mLTube()
# Width at the middle of the cylindrical section (e.g. 50 mm from bottom)
width_mid = tube.container.width_at_height(Decimal("50"))
print(f"Width at 50 mm: {width_mid} mm") # 27.78 mm (2 × 13.89)
# Width near the tip of the conical frustum (e.g. 5 mm from bottom)
width_tip = tube.container.width_at_height(Decimal("5"))
print(f"Width at 5 mm: {width_tip} mm") # interpolated between 7.2 mm and 27.78 mm
# Cross-sectional area at 50 mm
area = tube.container.area_at_height(Decimal("50"))
print(f"Area at 50 mm: {round(area, 3)} mm²") # π × 13.89²
# Inspect which section and local offset a given height falls into
section, local_height = tube.container.section_at_height(Decimal("5"))
print(f"Section: {section}") # ConicalFrustum(...)
print(f"Local height: {local_height} mm")
Shape Reference
The table below shows how each concrete shape implements the three methods, where h is the queried height and H is the total height of the shape:
| Shape | width_at_height(h) | depth_at_height(h) | area_at_height(h) |
|---|---|---|---|
| Cuboid | width (constant) | depth (constant) | width × depth |
| Pyramid | (h/H) × width | (h/H) × depth | (h/H)² × width × depth |
| PyramidalFrustum | linear interpolation between width_lower and width_upper | linear interpolation between depth_lower and depth_upper | w(h) × d(h) |
| Cylinder | 2 × radius (constant) | 2 × radius (constant) | π × r² |
| Cone | 2 × (h/H) × radius | same as width | π × ((h/H) × r)² |
| ConicalFrustum | 2 × r(h), linear interpolation between radius_lower and radius_upper | same as width | π × r(h)² |
| SphericalCap | 2 × √(2Rh − h²) | same as width | π × (2Rh − h²) |
| HalfSphere | inherits from SphericalCap | inherits from SphericalCap | inherits from SphericalCap |
| SphericalSegment | 2 × √(2R·h' − h'²) where h' is height from sphere bottom | same as width | π × (2R·h' − h'²) |
Container has multiple sections, width_at_height, depth_at_height, and area_at_height automatically resolve the correct section via section_at_height. The height passed is always measured from the bottom of the container, not from the bottom of the individual section.Last updated