Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 90 additions & 1 deletion pylabrobot/resources/corning/plates.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
""" Corning plates. """

from pylabrobot.resources.height_volume_functions import (
calculate_liquid_height_in_container_2segments_round_vbottom,
calculate_liquid_height_in_container_2segments_square_vbottom,
calculate_liquid_volume_container_2segments_round_vbottom,
calculate_liquid_volume_container_2segments_square_vbottom,
compute_height_from_volume_conical_frustum_in_well,
compute_volume_from_height_conical_frustum_in_well,
)
from pylabrobot.resources.plate import Lid, Plate
from pylabrobot.resources.utils import create_ordered_items_2d
Expand Down Expand Up @@ -35,6 +39,12 @@ def Cor_96_wellplate_360ul_Fb(name: str, with_lid: bool = False) -> Plate:

# This used to be Cos_96_EZWash in the Esvelt lab

# Corning MicroplateDimensions96-384-1536.pdf:
# top internal diameter 6.86mm, bottom internal diameter 6.35mm, well depth 10.67mm
BOTTOM_RADIUS = 6.35 / 2
TOP_RADIUS = 6.86 / 2
WELL_DEPTH = 10.67

return Plate(
name=name,
size_x=127.76,
Expand All @@ -53,11 +63,21 @@ def Cor_96_wellplate_360ul_Fb(name: str, with_lid: bool = False) -> Plate:
item_dy=9.0,
size_x=6.86, # top
size_y=6.86, # top
size_z=10.67,
size_z=WELL_DEPTH,
material_z_thickness=0.5,
bottom_type=WellBottomType.FLAT,
cross_section_type=CrossSectionType.CIRCLE,
max_volume=360,
compute_volume_from_height=lambda liquid_height: (
compute_volume_from_height_conical_frustum_in_well(
liquid_height, BOTTOM_RADIUS, TOP_RADIUS, WELL_DEPTH
)
),
compute_height_from_volume=lambda liquid_volume: (
compute_height_from_volume_conical_frustum_in_well(
liquid_volume, BOTTOM_RADIUS, TOP_RADIUS, WELL_DEPTH
)
),
),
)

Expand All @@ -82,6 +102,75 @@ def Cos_96_EZWash(name: str, with_lid: bool = False) -> Plate:
raise ValueError("Deprecated. You probably want to use Cor_96_wellplate_360ul_Fb instead.")


# # # # # # # # # # Cor_96_wellplate_320ul_Vb # # # # # # # # # #


def Cor_96_wellplate_320ul_Vb(name: str, with_lid: bool = False) -> Plate:
"""
Corning cat. no.s: 3894, 3896, 3897, 3898, 3342
- manufacturer_link: https://ecatalog.corning.com/life-sciences/b2c/US/en/Microplates/
Assay-Microplates/96-Well-Microplates/Corning%C2%AE-96-well-Clear-Polystyrene-Microplates/p/3894
- brand: Corning / Costar
- material: Polystyrene
- notes:
- Standard 96-well V-bottom microplate.
- Well geometry: conical V-bottom section (cone height ~1.6mm) transitioning to a
cylindrical section. Dimensions from Corning MicroplateDimensions96-384-1536.pdf:
top internal diameter 6.86mm, bottom internal diameter 6.37mm, well depth 11.12mm,
total well volume 320uL.
"""

# Corning MicroplateDimensions96-384-1536.pdf:
# top internal diameter 6.86mm, bottom internal diameter 6.37mm, well depth 11.12mm
# The V-bottom is modeled as a cone (apex at bottom) transitioning to a cylinder.
# Cone height back-calculated from published total volume (320uL) using d=6.37mm.
WELL_DIAMETER = 6.37 # diameter at cone-to-cylinder transition
CONE_HEIGHT = 1.62 # mm, back-calculated to match 320uL total volume
CYLINDER_HEIGHT = 11.12 - CONE_HEIGHT # 9.50mm
WELL_DEPTH = 11.12

return Plate(
name=name,
size_x=127.76,
size_y=85.48,
size_z=14.2,
lid=Cor_96_wellplate_320ul_Vb_Lid(name=name + "_lid") if with_lid else None,
model="Cor_96_wellplate_320ul_Vb",
ordered_items=create_ordered_items_2d(
Well,
num_items_x=12,
num_items_y=8,
dx=10.87, # 14.3-6.86/2
dy=7.77, # 11.2-6.86/2
dz=2.58, # 14.2 - 11.12 - 0.5
item_dx=9.0,
item_dy=9.0,
size_x=6.86,
size_y=6.86,
size_z=WELL_DEPTH,
material_z_thickness=0.5,
bottom_type=WellBottomType.V,
cross_section_type=CrossSectionType.CIRCLE,
compute_volume_from_height=lambda liquid_height: (
calculate_liquid_volume_container_2segments_round_vbottom(
d=WELL_DIAMETER, h_cone=CONE_HEIGHT, h_cylinder=CYLINDER_HEIGHT,
liquid_height=liquid_height,
)
),
compute_height_from_volume=lambda liquid_volume: (
calculate_liquid_height_in_container_2segments_round_vbottom(
d=WELL_DIAMETER, h_cone=CONE_HEIGHT, h_cylinder=CYLINDER_HEIGHT,
liquid_volume=liquid_volume,
)
),
),
)


def Cor_96_wellplate_320ul_Vb_Lid(name: str) -> Lid:
raise NotImplementedError("This lid is not currently defined.")


# # # # # # # # # # Cor_96_wellplate_2mL_Vb # # # # # # # # # #


Expand Down
66 changes: 66 additions & 0 deletions pylabrobot/resources/height_volume_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,72 @@ def compute_height_from_volume_conical_frustum(
)


def compute_volume_from_height_conical_frustum_in_well(
liquid_height: float, bottom_radius: float, top_radius: float, total_height: float
) -> float:
"""Compute volume (uL) from height (mm) for liquid in a conical-frustum-shaped well.

Unlike :func:`compute_volume_from_height_conical_frustum`, this correctly interpolates the
radius at the liquid surface based on the fill height. In a tapered well, the cross-sectional
radius varies linearly from ``bottom_radius`` at h=0 to ``top_radius`` at h=total_height.

Args:
liquid_height: Height of the liquid in mm.
bottom_radius: Inner radius at the bottom of the well in mm.
top_radius: Inner radius at the top of the well in mm.
total_height: Total internal height of the well in mm.

Returns:
Volume of liquid in uL (= mm^3).
"""
if liquid_height <= 0:
return 0.0
h = min(liquid_height, total_height)
r_at_h = bottom_radius + (top_radius - bottom_radius) * h / total_height
return (math.pi * h / 3.0) * (r_at_h**2 + r_at_h * bottom_radius + bottom_radius**2)


def compute_height_from_volume_conical_frustum_in_well(
liquid_volume: float, bottom_radius: float, top_radius: float, total_height: float
) -> float:
"""Compute height (mm) from volume (uL) for liquid in a conical-frustum-shaped well.

Inverse of :func:`compute_volume_from_height_conical_frustum_in_well`. Uses binary search
because the volume-height relationship is cubic (not analytically invertible in closed form).

Args:
liquid_volume: Volume of the liquid in uL (= mm^3).
bottom_radius: Inner radius at the bottom of the well in mm.
top_radius: Inner radius at the top of the well in mm.
total_height: Total internal height of the well in mm.

Returns:
Height of the liquid in mm.
"""
if liquid_volume <= 0:
return 0.0

max_volume = compute_volume_from_height_conical_frustum_in_well(
total_height, bottom_radius, top_radius, total_height
)
if liquid_volume > max_volume:
raise ValueError(
f"Volume {liquid_volume} exceeds maximum well volume {max_volume:.2f}."
)

low, high = 0.0, total_height
tolerance = 1e-6
while high - low > tolerance:
mid = (low + high) / 2
if compute_volume_from_height_conical_frustum_in_well(
mid, bottom_radius, top_radius, total_height
) < liquid_volume:
low = mid
else:
high = mid
return (low + high) / 2


def compute_volume_from_height_square(liquid_height: float, well_side_length: float) -> float:
"""Compute volume (uL) from height (mm) for a square well."""
return liquid_height * (well_side_length**2)
Expand Down