From ab26d9e3540766d37dd632843d49e6025c881d8e Mon Sep 17 00:00:00 2001 From: Harley King Date: Tue, 4 Nov 2025 14:36:51 -0500 Subject: [PATCH 1/2] add volume to height conversion calcs --- pylabrobot/resources/bioer/plates.py | 89 ++++++++++++++++++++++------ 1 file changed, 72 insertions(+), 17 deletions(-) diff --git a/pylabrobot/resources/bioer/plates.py b/pylabrobot/resources/bioer/plates.py index fa0bf127091..2d2655e592f 100644 --- a/pylabrobot/resources/bioer/plates.py +++ b/pylabrobot/resources/bioer/plates.py @@ -1,3 +1,6 @@ +# BioER 2.2 mL deepwell with polynomial height<->volume mapping +# Uses fit: h(V) = a3*V^3 + a2*V^2 + a1*V + a0 (V in µL, h in mm) + from pylabrobot.resources.plate import Plate from pylabrobot.resources.utils import create_ordered_items_2d from pylabrobot.resources.well import ( @@ -6,33 +9,85 @@ WellBottomType, ) +# Measured the height of vols in the plate. Graphed and then fitted polynomial. +# Polynomial more accurate than circular conical frustum. + +# ---- Polynomial coefficients from your fit (units: µL -> mm) ---- +_A3 = 2.34770904e-09 +_A2 = -9.12279010e-06 +_A1 = 2.68872240e-02 +_A0 = 2.06530412e+00 + +# ---- Geometry / limits (from your earlier spec & measurements) ---- +_WELL_TOP_SIDE_MM = 8.25 # inner opening (square), mm +_WELL_DEPTH_MM = 42.4 # well depth, mm +_MAX_VOL_UL = 2200.0 # vendor spec, µL + +# Monotone cubic on [0, MAX_VOL] from your data — use binary search for inversion +def _height_from_volume_poly(vol_ul: float) -> float: + """Height (mm) from volume (µL) using the fitted cubic.""" + v = max(0.0, min(float(vol_ul), _MAX_VOL_UL)) + h = ((_A3 * v + _A2) * v + _A1) * v + _A0 + # Clamp to physical depth + if h < 0.0: + return 0.0 + if h > _WELL_DEPTH_MM: + return _WELL_DEPTH_MM + return h + +def _volume_from_height_poly(h_mm: float, *, tol: float = 1e-6, max_iter: int = 64) -> float: + """Volume (µL) from height (mm) by inverting the cubic with binary search.""" + h_target = max(0.0, min(float(h_mm), _WELL_DEPTH_MM)) + lo, hi = 0.0, _MAX_VOL_UL + # Quick outs + if h_target <= _height_from_volume_poly(lo): + return 0.0 + if h_target >= _height_from_volume_poly(hi): + return _MAX_VOL_UL + for _ in range(max_iter): + mid = 0.5 * (lo + hi) + h_mid = _height_from_volume_poly(mid) + if abs(h_mid - h_target) <= tol: + return mid + if h_mid < h_target: + lo = mid + else: + hi = mid + return 0.5 * (lo + hi) def BioER_96_wellplate_Vb_2200uL(name: str) -> Plate: """BioER Cat. No. BSH06M1T-A (KingFisher-compatible) Spec: https://en.bioer.com/uploadfiles/2024/05/20240513165756879.pdf """ + well_kwargs = { + "size_x": _WELL_TOP_SIDE_MM, + "size_y": _WELL_TOP_SIDE_MM, + "size_z": _WELL_DEPTH_MM, + "bottom_type": WellBottomType.V, # physical bottom shape + "cross_section_type": CrossSectionType.RECTANGLE, + "material_z_thickness": 0.8, # measured + "max_volume": _MAX_VOL_UL, + # ---- height<->volume mapping used by PLR ---- + "compute_height_from_volume": lambda vol_ul: _height_from_volume_poly(vol_ul), + "compute_volume_from_height": lambda h_mm: _volume_from_height_poly(h_mm), + } + return Plate( name=name, - size_x=127.1, # from spec - size_y=85.0, # from spec - size_z=44.2, # from spec + size_x=127.1, # spec + size_y=85.0, # spec + size_z=44.2, # spec lid=None, model=BioER_96_wellplate_Vb_2200uL.__name__, ordered_items=create_ordered_items_2d( Well, - size_x=8.25, # from spec (inner well width) - size_y=8.25, # from spec (inner well length) - size_z=42.4, # measured (well depth) - dx=9.5, # measured (column pitch) - dy=7.5, # measured (row pitch) - dz=6, # measured (expected to be 44.2-42.4-0.8=1, but 6 optimal on Hamilton_MFX_plateholder_DWP_metal_tapped ) - material_z_thickness=0.8, # measured - item_dx=9.0, # measured - item_dy=9.0, # measured - num_items_x=12, # from spec - num_items_y=8, # from spec - cross_section_type=CrossSectionType.RECTANGLE, - bottom_type=WellBottomType.V, - max_volume=2200, # from spec (2.2 mL) + num_items_x=12, # spec + num_items_y=8, # spec + dx=9.5, # measured (column pitch) + dy=7.5, # measured (row pitch) + dz=6.0, # calibrated (mounting offset for your deck) + item_dx=9.0, # measured + item_dy=9.0, # measured + **well_kwargs, ), ) From 6d6fdc1e2ebf2d9b325634d1248e0625c4b4a0df Mon Sep 17 00:00:00 2001 From: Harley King Date: Fri, 13 Feb 2026 16:46:36 -0500 Subject: [PATCH 2/2] refactor: clean up formatting and spacing in bioER height-volume calculations --- pylabrobot/resources/bioer/plates.py | 35 +++++++++++++++------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/pylabrobot/resources/bioer/plates.py b/pylabrobot/resources/bioer/plates.py index 2d2655e592f..79056f72168 100644 --- a/pylabrobot/resources/bioer/plates.py +++ b/pylabrobot/resources/bioer/plates.py @@ -16,12 +16,13 @@ _A3 = 2.34770904e-09 _A2 = -9.12279010e-06 _A1 = 2.68872240e-02 -_A0 = 2.06530412e+00 +_A0 = 2.06530412e00 # ---- Geometry / limits (from your earlier spec & measurements) ---- -_WELL_TOP_SIDE_MM = 8.25 # inner opening (square), mm -_WELL_DEPTH_MM = 42.4 # well depth, mm -_MAX_VOL_UL = 2200.0 # vendor spec, µL +_WELL_TOP_SIDE_MM = 8.25 # inner opening (square), mm +_WELL_DEPTH_MM = 42.4 # well depth, mm +_MAX_VOL_UL = 2200.0 # vendor spec, µL + # Monotone cubic on [0, MAX_VOL] from your data — use binary search for inversion def _height_from_volume_poly(vol_ul: float) -> float: @@ -35,6 +36,7 @@ def _height_from_volume_poly(vol_ul: float) -> float: return _WELL_DEPTH_MM return h + def _volume_from_height_poly(h_mm: float, *, tol: float = 1e-6, max_iter: int = 64) -> float: """Volume (µL) from height (mm) by inverting the cubic with binary search.""" h_target = max(0.0, min(float(h_mm), _WELL_DEPTH_MM)) @@ -55,6 +57,7 @@ def _volume_from_height_poly(h_mm: float, *, tol: float = 1e-6, max_iter: int = hi = mid return 0.5 * (lo + hi) + def BioER_96_wellplate_Vb_2200uL(name: str) -> Plate: """BioER Cat. No. BSH06M1T-A (KingFisher-compatible) Spec: https://en.bioer.com/uploadfiles/2024/05/20240513165756879.pdf @@ -63,9 +66,9 @@ def BioER_96_wellplate_Vb_2200uL(name: str) -> Plate: "size_x": _WELL_TOP_SIDE_MM, "size_y": _WELL_TOP_SIDE_MM, "size_z": _WELL_DEPTH_MM, - "bottom_type": WellBottomType.V, # physical bottom shape + "bottom_type": WellBottomType.V, # physical bottom shape "cross_section_type": CrossSectionType.RECTANGLE, - "material_z_thickness": 0.8, # measured + "material_z_thickness": 0.8, # measured "max_volume": _MAX_VOL_UL, # ---- height<->volume mapping used by PLR ---- "compute_height_from_volume": lambda vol_ul: _height_from_volume_poly(vol_ul), @@ -74,20 +77,20 @@ def BioER_96_wellplate_Vb_2200uL(name: str) -> Plate: return Plate( name=name, - size_x=127.1, # spec - size_y=85.0, # spec - size_z=44.2, # spec + size_x=127.1, # spec + size_y=85.0, # spec + size_z=44.2, # spec lid=None, model=BioER_96_wellplate_Vb_2200uL.__name__, ordered_items=create_ordered_items_2d( Well, - num_items_x=12, # spec - num_items_y=8, # spec - dx=9.5, # measured (column pitch) - dy=7.5, # measured (row pitch) - dz=6.0, # calibrated (mounting offset for your deck) - item_dx=9.0, # measured - item_dy=9.0, # measured + num_items_x=12, # spec + num_items_y=8, # spec + dx=9.5, # measured (column pitch) + dy=7.5, # measured (row pitch) + dz=6.0, # calibrated (mounting offset for your deck) + item_dx=9.0, # measured + item_dy=9.0, # measured **well_kwargs, ), )