from typing import Any, Callable, Dict, List, Optional, Tuple from pylabrobot.resources import Tube, Coordinate from pylabrobot.resources.well import Well, WellBottomType, CrossSectionType from pylabrobot.resources.tip import Tip, TipCreator from pylabrobot.resources.tip_rack import TipRack, TipSpot from pylabrobot.resources.utils import create_ordered_items_2d from pylabrobot.resources.height_volume_functions import ( compute_height_from_volume_rectangle, compute_volume_from_height_rectangle, ) from .prcxi import PRCXI9300Plate, PRCXI9300TipRack, PRCXI9300Trash, PRCXI9300TubeRack, PRCXI9300PlateAdapter def _make_tip_helper(volume: float, length: float, depth: float) -> Tip: """ PLR 的 Tip 类参数名为: maximal_volume, total_tip_length, fitting_depth """ return Tip( has_filter=False, # 默认无滤芯 maximal_volume=volume, total_tip_length=length, fitting_depth=depth ) # ========================================================================= # Plates # ========================================================================= def PRCXI_BioER_96_wellplate(name: str) -> PRCXI9300Plate: """ 对应 JSON Code: ZX-019-2.2 (2.2ml 深孔板) 原型: pylabrobot.resources.bioer.BioER_96_wellplate_Vb_2200uL """ return PRCXI9300Plate( name=name, size_x=127.1, size_y=85.0, size_z=44.2, model="PRCXI_BioER_96_wellplate", category="plate", material_info={"uuid": "ca877b8b114a4310b429d1de4aae96ee", "Code": "ZX-019-2.2", "Name": "2.2ml 深孔板", "materialEnum": 0, "SupplyType": 1}, ordered_items=create_ordered_items_2d( Well, num_items_x=12, num_items_y=8, dx=9.5, dy=7.5, dz=6.0, item_dx=9.0, item_dy=9.0, size_x=8.25, size_y=8.25, size_z=39.3, max_volume=2200.0, material_z_thickness=0.8, bottom_type=WellBottomType.V, cross_section_type=CrossSectionType.RECTANGLE, ), ) def PRCXI_nest_1_troughplate(name: str) -> PRCXI9300Plate: """ 对应 JSON Code: ZX-58-10000 (储液槽) 原型: pylabrobot.resources.nest.nest_1_troughplate_195000uL_Vb """ well_size_x = 108.0 well_size_y = 72.0 well_kwargs = { "size_x": well_size_x, "size_y": well_size_y, "size_z": 26.85, "bottom_type": WellBottomType.V, "compute_height_from_volume": lambda liquid_volume: compute_height_from_volume_rectangle( liquid_volume=liquid_volume, well_length=well_size_x, well_width=well_size_y ), "compute_volume_from_height": lambda liquid_height: compute_volume_from_height_rectangle( liquid_height=liquid_height, well_length=well_size_x, well_width=well_size_y ), "material_z_thickness": 0.9999999999999973, } return PRCXI9300Plate( name=name, size_x=127.76, size_y=85.48, size_z=31.4, lid=None, model="PRCXI_Nest_1_troughplate", category="plate", material_info={"uuid": "04211a2dc93547fe9bf6121eac533650", "Code": "ZX-58-10000", "Name": "储液槽", "materialEnum": 0, "SupplyType": 1}, ordered_items=create_ordered_items_2d( Well, num_items_x=1, num_items_y=1, dx=9.88, dy=6.74, dz=3.55, item_dx=9.0, item_dy=9.0, **well_kwargs, ), ) def PRCXI_BioRad_384_wellplate(name: str) -> PRCXI9300Plate: """ 对应 JSON Code: q3 (384板) 原型: pylabrobot.resources.biorad.BioRad_384_wellplate_50uL_Vb """ return PRCXI9300Plate( name=name, size_x=127.76, size_y=85.48, size_z=10.4, model="BioRad_384_wellplate_50uL_Vb", category="plate", material_info={"uuid": "853dcfb6226f476e8b23c250217dc7da", "Code": "q3", "Name": "384板", "SupplyType": 1}, ordered_items=create_ordered_items_2d( Well, num_items_x=24, num_items_y=16, dx=10.58, dy=7.44, dz=1.05, item_dx=4.5, item_dy=4.5, size_x=3.1, size_y=3.1, size_z=9.35, max_volume=50.0, material_z_thickness=1.0, bottom_type=WellBottomType.V, cross_section_type=CrossSectionType.CIRCLE, ), ) def PRCXI_AGenBio_4_troughplate(name: str) -> PRCXI9300Plate: """ 对应 JSON Code: sdfrth654 (4道储液槽) 原型: pylabrobot.resources.agenbio.AGenBio_4_troughplate_75000uL_Vb """ well_size_x = 26.0 well_size_y = 71.2 well_kwargs = { "size_x": well_size_x, "size_y": well_size_y, "size_z": 42.55, "bottom_type": WellBottomType.FLAT, "cross_section_type": CrossSectionType.RECTANGLE, "compute_height_from_volume": lambda liquid_volume: compute_height_from_volume_rectangle( liquid_volume=liquid_volume, well_length=well_size_x, well_width=well_size_y ), "compute_volume_from_height": lambda liquid_height: compute_volume_from_height_rectangle( liquid_height=liquid_height, well_length=well_size_x, well_width=well_size_y ), "material_z_thickness": 1.0, } return PRCXI9300Plate( name=name, size_x=127.76, size_y=85.48, size_z=43.8, lid=None, model="PRCXI_AGenBio_4_troughplate", category="plate", material_info={"uuid": "01953864f6f140ccaa8ddffd4f3e46f5", "Code": "sdfrth654", "Name": "4道储液槽", "materialEnum": 0, "SupplyType": 1}, ordered_items=create_ordered_items_2d( Well, num_items_x=4, num_items_y=1, dx=9.8, dy=7.2, dz=0.9, item_dx=27.1, item_dy=9.0, **well_kwargs, ), ) def PRCXI_nest_12_troughplate(name: str) -> PRCXI9300Plate: """ 对应 JSON Code: 12道储液槽 (12道储液槽) 原型: pylabrobot.resources.nest.nest_12_troughplate_15000uL_Vb """ well_size_x = 8.2 well_size_y = 71.2 well_kwargs = { "size_x": well_size_x, "size_y": well_size_y, "size_z": 26.85, "bottom_type": WellBottomType.V, "compute_height_from_volume": lambda liquid_volume: compute_height_from_volume_rectangle( liquid_volume=liquid_volume, well_length=well_size_x, well_width=well_size_y ), "compute_volume_from_height": lambda liquid_height: compute_volume_from_height_rectangle( liquid_height=liquid_height, well_length=well_size_x, well_width=well_size_y ), "material_z_thickness": 0.9999999999999973, } return PRCXI9300Plate( name=name, size_x=127.76, size_y=85.48, size_z=31.4, lid=None, model="PRCXI_nest_12_troughplate", category="plate", material_info={"uuid": "0f1639987b154e1fac78f4fb29a1f7c1", "Code": "12道储液槽", "Name": "12道储液槽", "materialEnum": 0, "SupplyType": 1}, ordered_items=create_ordered_items_2d( Well, num_items_x=12, num_items_y=1, dx=10.28, dy=7.14, dz=3.55, item_dx=9.0, item_dy=9.0, **well_kwargs, ), ) def PRCXI_CellTreat_96_wellplate(name: str) -> PRCXI9300Plate: """ 对应 JSON Code: ZX-78-096 (细菌培养皿) 原型: pylabrobot.resources.celltreat.CellTreat_96_wellplate_350ul_Fb """ return PRCXI9300Plate( name=name, size_x=127.61, size_y=85.24, size_z=14.3, model="PRCXI_CellTreat_96_wellplate", category="plate", material_info={"uuid": "b05b3b2aafd94ec38ea0cd3215ecea8f", "Code": "ZX-78-096", "Name": "细菌培养皿", "materialEnum": 4, "SupplyType": 1}, ordered_items=create_ordered_items_2d( Well, num_items_x=12, num_items_y=8, dx=10.83, dy=7.67, dz=4.05, item_dx=9.0, item_dy=9.0, size_x=6.96, size_y=6.96, size_z=10.04, max_volume=300.0, material_z_thickness=1.75, cross_section_type=CrossSectionType.CIRCLE, ), ) def PRCXI_PCR_Plate_200uL_nonskirted(name: str) -> PRCXI9300Plate: """ 对应 JSON Code: ZX-023-0.2 (0.2ml PCR 板) """ return PRCXI9300Plate( name=name, size_x=119.5, size_y=80.0, size_z=26.0, plate_type="non-skirted", model="PRCXI_PCR_Plate_200uL_nonskirted", category="plate", material_info={"uuid": "73bb9b10bc394978b70e027bf45ce2d3", "Code": "ZX-023-0.2", "Name": "0.2ml PCR 板", "materialEnum": 0, "SupplyType": 1}, ordered_items=create_ordered_items_2d( Well, num_items_x=12, num_items_y=8, dx=7.0, dy=5.0, dz=0.0, item_dx=9.0, item_dy=9.0, size_x=6.0, size_y=6.0, size_z=15.17, max_volume=428.92164499461444, bottom_type=WellBottomType.V, cross_section_type=CrossSectionType.CIRCLE, ), ) def PRCXI_PCR_Plate_200uL_semiskirted(name: str) -> PRCXI9300Plate: """ 对应 JSON Code: ZX-023-0.2 (0.2ml PCR 板) """ return PRCXI9300Plate( name=name, size_x=126.0, size_y=86.0, size_z=21.2, plate_type="semi-skirted", model="PRCXI_PCR_Plate_200uL_semiskirted", category="plate", material_info={"uuid": "73bb9b10bc394978b70e027bf45ce2d3", "Code": "ZX-023-0.2", "Name": "0.2ml PCR 板", "materialEnum": 0, "SupplyType": 1}, ordered_items=create_ordered_items_2d( Well, num_items_x=12, num_items_y=8, dx=11.0, dy=8.0, dz=0.0, item_dx=9.0, item_dy=9.0, size_x=6.0, size_y=6.0, size_z=15.17, max_volume=428.92164499461444, bottom_type=WellBottomType.V, cross_section_type=CrossSectionType.CIRCLE, ), ) def PRCXI_PCR_Plate_200uL_skirted(name: str) -> PRCXI9300Plate: """ 对应 JSON Code: ZX-023-0.2 (0.2ml PCR 板) """ return PRCXI9300Plate( name=name, size_x=127.76, size_y=86.0, size_z=16.1, plate_type="skirted", model="PRCXI_PCR_Plate_200uL_skirted", category="plate", material_info={"uuid": "73bb9b10bc394978b70e027bf45ce2d3", "Code": "ZX-023-0.2", "Name": "0.2ml PCR 板", "materialEnum": 0, "SupplyType": 1}, ordered_items=create_ordered_items_2d( Well, num_items_x=12, num_items_y=8, dx=11.0, dy=8.49, dz=0.8, item_dx=9.0, item_dy=9.0, size_x=6.0, size_y=6.0, size_z=15.1, max_volume=426.94244162285287, bottom_type=WellBottomType.V, cross_section_type=CrossSectionType.CIRCLE, ), ) def PRCXI_96_DeepWell(name: str) -> PRCXI9300Plate: """ 对应 JSON Code: q2 (96深孔板) """ return PRCXI9300Plate( name=name, size_x=127.3, size_y=85.35, size_z=45.0, model="PRCXI_96_DeepWell", category="plate", material_info={"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "Code": "q2", "Name": "96深孔板", "materialEnum": 0}, ordered_items=create_ordered_items_2d( Well, num_items_x=12, num_items_y=8, dx=10.9, dy=8.25, dz=2.0, item_dx=9.0, item_dy=9.0, size_x=8.2, size_y=8.2, size_z=42.0, max_volume=2200.0, bottom_type=WellBottomType.UNKNOWN, cross_section_type=CrossSectionType.CIRCLE, ), ) def PRCXI_48_DeepWell(name: str) -> PRCXI9300Plate: """ Code: 22 (48孔深孔板) """ return PRCXI9300Plate( name=name, size_x=127.0, size_y=85.0, size_z=44.0, model="PRCXI_48_DeepWell", category="plate", material_info={"uuid": "026c5d5cf3d94e56b4e16b7fb53a995b", "Code": "22", "Name": "48孔深孔板", "SupplyType": 1}, ordered_items=create_ordered_items_2d( Well, num_items_x=6, num_items_y=8, dx=10.0, dy=10.0, dz=1.0, item_dx=18.5, item_dy=9.0, size_x=8.0, size_y=8.0, size_z=40.0, max_volume=2010.6192982974676, bottom_type=WellBottomType.UNKNOWN, cross_section_type=CrossSectionType.CIRCLE, ), ) # ========================================================================= # Tip Racks # ========================================================================= def PRCXI_10ul_eTips(name: str) -> PRCXI9300TipRack: """ 对应 JSON Code: ZX-001-10+ """ return PRCXI9300TipRack( name=name, size_x=127.76, size_y=85.48, size_z=58.0, model="PRCXI_10ul_eTips", material_info={"uuid": "068b3815e36b4a72a59bae017011b29f", "Code": "ZX-001-10+", "Name": "10μL加长 Tip头", "SupplyType": 1}, ordered_items=create_ordered_items_2d( TipSpot, num_items_x=12, num_items_y=8, dx=10.63, dy=7.49, dz=14.6, item_dx=9.0, item_dy=9.0, size_x=7.5, size_y=7.5, size_z=0.0, make_tip=lambda: _make_tip_helper(volume=10.0, length=52.0, depth=8.2) ) ) def PRCXI_300ul_Tips(name: str) -> PRCXI9300TipRack: """ 对应 JSON Code: ZX-001-300 吸头盒通常比较特殊,需要定义 Tip 对象 """ return PRCXI9300TipRack( name=name, size_x=127.76, size_y=85.48, size_z=58.0, model="PRCXI_300ul_Tips", material_info={"uuid": "076250742950465b9d6ea29a225dfb00", "Code": "ZX-001-300", "Name": "300μL Tip头", "SupplyType": 1}, ordered_items=create_ordered_items_2d( TipSpot, num_items_x=12, num_items_y=8, dx=10.63, dy=7.49, dz=6.6, item_dx=9.0, item_dy=9.0, size_x=7.5, size_y=7.5, size_z=0.0, make_tip=lambda: _make_tip_helper(volume=300.0, length=60.0, depth=8.2) ) ) def PRCXI_1250uL_Tips(name: str) -> PRCXI9300TipRack: """ Code: ZX-001-1250 """ return PRCXI9300TipRack( name=name, size_x=127.76, size_y=85.48, size_z=98.0, model="PRCXI_1250uL_Tips", material_info={"uuid": "7960f49ddfe9448abadda89bd1556936", "Code": "ZX-001-1250", "Name": "1250μL Tip头", "SupplyType": 1}, ordered_items=create_ordered_items_2d( TipSpot, num_items_x=12, num_items_y=8, dx=10.63, dy=7.49, dz=6.6, item_dx=9.0, item_dy=9.0, size_x=7.5, size_y=7.5, size_z=100.0, make_tip=lambda: _make_tip_helper(volume=1250.0, length=100.0, depth=8.2) ) ) def PRCXI_10uL_Tips(name: str) -> PRCXI9300TipRack: """ Code: ZX-001-10 """ return PRCXI9300TipRack( name=name, size_x=127.76, size_y=85.48, size_z=58.0, model="PRCXI_10uL_Tips", material_info={"uuid": "45f2ed3ad925484d96463d675a0ebf66", "Code": "ZX-001-10", "Name": "10μL Tip头", "SupplyType": 1}, ordered_items=create_ordered_items_2d( TipSpot, num_items_x=12, num_items_y=8, dx=10.63, dy=7.49, dz=14.6, item_dx=9.0, item_dy=9.0, size_x=7.5, size_y=7.5, size_z=0.0, make_tip=lambda: _make_tip_helper(volume=10.0, length=52.0, depth=8.2) ) ) def PRCXI_1000uL_Tips(name: str) -> PRCXI9300TipRack: """ Code: ZX-001-1000 """ return PRCXI9300TipRack( name=name, size_x=127.76, size_y=85.48, size_z=98.0, model="PRCXI_1000uL_Tips", material_info={"uuid": "80652665f6a54402b2408d50b40398df", "Code": "ZX-001-1000", "Name": "1000μL Tip头", "SupplyType": 1}, ordered_items=create_ordered_items_2d( TipSpot, num_items_x=12, num_items_y=8, dx=10.63, dy=7.49, dz=6.6, item_dx=9.0, item_dy=9.0, size_x=7.5, size_y=7.5, size_z=100.0, make_tip=lambda: _make_tip_helper(volume=1000.0, length=100.0, depth=8.2) ) ) def PRCXI_200uL_Tips(name: str) -> PRCXI9300TipRack: """ Code: ZX-001-200 """ return PRCXI9300TipRack( name=name, size_x=120.98, size_y=82.12, size_z=66.9, model="PRCXI_200uL_Tips", material_info={"uuid": "7a73bb9e5c264515a8fcbe88aed0e6f7", "Code": "ZX-001-200", "Name": "200μL Tip头", "SupplyType": 1}, ordered_items=create_ordered_items_2d( TipSpot, num_items_x=12, num_items_y=8, dx=8.24, dy=6.81, dz=2.0, item_dx=9.0, item_dy=9.0, size_x=7.0, size_y=7.0, size_z=0.0, make_tip=lambda: _make_tip_helper(volume=300.0, length=60.0, depth=51.0) ) ) def PRCXI_50uL_tips(name: str) -> PRCXI9300TipRack: """ Code: """ return PRCXI9300TipRack( name=name, size_x=127.76, size_y=85.48, size_z=58.0, model="PRCXI_50uL_tips", material_info={"uuid": "", "Code": "", "Name": "", "SupplyType": 1}, ordered_items=create_ordered_items_2d( TipSpot, num_items_x=12, num_items_y=8, dx=10.63, dy=7.49, dz=13.6, item_dx=9.0, item_dy=9.0, size_x=7.5, size_y=7.5, size_z=0.0, make_tip=lambda: _make_tip_helper(volume=50.0, length=53.0, depth=8.2) ) ) # ========================================================================= # Trash # ========================================================================= def PRCXI_trash(name: str = "trash") -> PRCXI9300Trash: """ 对应 JSON Code: q1 (废弃槽) """ return PRCXI9300Trash( name="trash", size_x=126.59, size_y=84.87, size_z=89.5, category="trash", model="PRCXI_trash", material_info={"uuid": "730067cf07ae43849ddf4034299030e9", "Code": "q1", "Name": "废弃槽", "materialEnum": 0, "SupplyType": 1} ) # ========================================================================= # Tube Racks # ========================================================================= def PRCXI_EP_Adapter(name: str) -> PRCXI9300TubeRack: """ 对应 JSON Code: 1 (ep适配器) 这是一个 4x6 的 EP 管架,适配 1.5mL/2.0mL 离心管 """ return PRCXI9300TubeRack( name=name, size_x=128.04, size_y=85.8, size_z=42.66, model="PRCXI_EP_Adapter", category="tube_rack", material_info={"uuid": "e146697c395e4eabb3d6b74f0dd6aaf7", "Code": "1", "Name": "ep适配器", "materialEnum": 0, "SupplyType": 1}, ordered_items=create_ordered_items_2d( Tube, num_items_x=6, num_items_y=4, dx=3.54, dy=10.7, dz=4.58, item_dx=21.0, item_dy=18.0, size_x=10.6, size_y=10.6, size_z=40.0, max_volume=1500.0 ) ) # ========================================================================= # Plate Adapters # ========================================================================= def PRCXI_Tip1250_Adapter(name: str) -> PRCXI9300PlateAdapter: """ Code: ZX-58-1250 """ return PRCXI9300PlateAdapter( name=name, size_x=128.0, size_y=85.0, size_z=20.0, material_info={"uuid": "3b6f33ffbf734014bcc20e3c63e124d4", "Code": "ZX-58-1250", "Name": "Tip头适配器 1250uL", "SupplyType": 2} ) def PRCXI_Tip300_Adapter(name: str) -> PRCXI9300PlateAdapter: """ Code: ZX-58-300 """ return PRCXI9300PlateAdapter( name=name, size_x=127.0, size_y=85.0, size_z=81.0, material_info={"uuid": "7c822592b360451fb59690e49ac6b181", "Code": "ZX-58-300", "Name": "ZHONGXI 适配器 300uL", "SupplyType": 2} ) def PRCXI_Tip10_Adapter(name: str) -> PRCXI9300PlateAdapter: """ Code: ZX-58-10 """ return PRCXI9300PlateAdapter( name=name, size_x=128.0, size_y=85.0, size_z=72.3, material_info={"uuid": "8cc3dce884ac41c09f4570d0bcbfb01c", "Code": "ZX-58-10", "Name": "吸头10ul 适配器", "SupplyType": 2} ) def PRCXI_PCR_Adapter(name: str) -> PRCXI9300PlateAdapter: """ 对应 JSON Code: ZX-58-0001 (全裙边 PCR适配器) """ return PRCXI9300PlateAdapter( name=name, size_x=127.76, size_y=85.48, size_z=21.69, model="PRCXI_PCR_Adapter", material_info={"uuid": "4a043a07c65a4f9bb97745e1f129b165", "Code": "ZX-58-0001", "Name": "全裙边 PCR适配器", "materialEnum": 3, "SupplyType": 2} ) def PRCXI_Reservoir_Adapter(name: str) -> PRCXI9300PlateAdapter: """ Code: ZX-ADP-001 """ return PRCXI9300PlateAdapter( name=name, size_x=133.0, size_y=91.8, size_z=70.0, material_info={"uuid": "6bdfdd7069df453896b0806df50f2f4d", "Code": "ZX-ADP-001", "Name": "储液槽 适配器", "SupplyType": 2} ) def PRCXI_Deep300_Adapter(name: str) -> PRCXI9300PlateAdapter: """ Code: ZX-002-300 """ return PRCXI9300PlateAdapter( name=name, size_x=136.4, size_y=93.8, size_z=96.0, material_info={"uuid": "9a439bed8f3344549643d6b3bc5a5eb4", "Code": "ZX-002-300", "Name": "300ul深孔板适配器", "SupplyType": 2} ) def PRCXI_Deep10_Adapter(name: str) -> PRCXI9300PlateAdapter: """ Code: ZX-002-10 """ return PRCXI9300PlateAdapter( name=name, size_x=136.5, size_y=93.8, size_z=121.5, material_info={"uuid": "4dc8d6ecfd0449549683b8ef815a861b", "Code": "ZX-002-10", "Name": "10ul专用深孔板适配器", "SupplyType": 2} ) def PRCXI_Adapter(name: str) -> PRCXI9300PlateAdapter: """ Code: Fhh478 """ return PRCXI9300PlateAdapter( name=name, size_x=120.0, size_y=90.0, size_z=86.0, material_info={"uuid": "adfabfffa8f24af5abfbba67b8d0f973", "Code": "Fhh478", "Name": "适配器", "SupplyType": 2} ) def PRCXI_30mm_Adapter(name: str) -> PRCXI9300PlateAdapter: """ Code: ZX-58-30 """ return PRCXI9300PlateAdapter( name=name, size_x=132.0, size_y=93.5, size_z=30.0, material_info={"uuid": "a0757a90d8e44e81a68f306a608694f2", "Code": "ZX-58-30", "Name": "30mm适配器", "SupplyType": 2} ) PRCXI_TEMPLATE_FACTORY_KINDS: List[Tuple[Callable[..., Any], str]] = [ (PRCXI_BioER_96_wellplate, "plate"), (PRCXI_nest_1_troughplate, "plate"), (PRCXI_BioRad_384_wellplate, "plate"), (PRCXI_AGenBio_4_troughplate, "plate"), (PRCXI_nest_12_troughplate, "plate"), (PRCXI_CellTreat_96_wellplate, "plate"), (PRCXI_10ul_eTips, "tip_rack"), (PRCXI_300ul_Tips, "tip_rack"), (PRCXI_PCR_Plate_200uL_nonskirted, "plate"), (PRCXI_PCR_Plate_200uL_semiskirted, "plate"), (PRCXI_PCR_Plate_200uL_skirted, "plate"), (PRCXI_trash, "trash"), (PRCXI_96_DeepWell, "plate"), (PRCXI_EP_Adapter, "tube_rack"), (PRCXI_1250uL_Tips, "tip_rack"), (PRCXI_10uL_Tips, "tip_rack"), (PRCXI_1000uL_Tips, "tip_rack"), (PRCXI_200uL_Tips, "tip_rack"), (PRCXI_48_DeepWell, "plate"), ] # --------------------------------------------------------------------------- # 协议上传 / workflow 用:与设备端耗材字典字段对齐的模板描述(供 common 自动匹配) # --------------------------------------------------------------------------- _PRCXI_TEMPLATE_SPECS_CACHE: Optional[List[Dict[str, Any]]] = None def _probe_prcxi_resource(factory: Callable[..., Any]) -> Any: probe = "__unilab_template_probe__" if factory.__name__ == "PRCXI_trash": return factory() return factory(probe) def _first_child_capacity_for_match(resource: Any) -> float: """Well max_volume 或 Tip 的 maximal_volume,用于与设备端 Volume 类似的打分。""" ch = getattr(resource, "children", None) or [] if not ch: return 0.0 c0 = ch[0] mv = getattr(c0, "max_volume", None) if mv is not None: return float(mv) tip = getattr(c0, "tip", None) if tip is not None: mv2 = getattr(tip, "maximal_volume", None) if mv2 is not None: return float(mv2) return 0.0 def get_prcxi_labware_template_specs() -> List[Dict[str, Any]]: """返回与 ``prcxi._match_and_create_matrix`` 中耗材字段兼容的模板列表,用于按孔数+容量打分。""" global _PRCXI_TEMPLATE_SPECS_CACHE if _PRCXI_TEMPLATE_SPECS_CACHE is not None: return _PRCXI_TEMPLATE_SPECS_CACHE out: List[Dict[str, Any]] = [] for factory, kind in PRCXI_TEMPLATE_FACTORY_KINDS: try: r = _probe_prcxi_resource(factory) except Exception: continue nx = int(getattr(r, "num_items_x", None) or 0) ny = int(getattr(r, "num_items_y", None) or 0) nchild = len(getattr(r, "children", []) or []) hole_count = nx * ny if nx > 0 and ny > 0 else nchild hole_row = ny if nx > 0 and ny > 0 else 0 hole_col = nx if nx > 0 and ny > 0 else 0 mi = getattr(r, "material_info", None) or {} vol = _first_child_capacity_for_match(r) menum = mi.get("materialEnum") if menum is None and kind == "tip_rack": menum = 1 elif menum is None and kind == "trash": menum = 6 out.append( { "class_name": factory.__name__, "kind": kind, "materialEnum": menum, "HoleRow": hole_row, "HoleColum": hole_col, "Volume": vol, "hole_count": hole_count, "material_uuid": mi.get("uuid"), "material_code": mi.get("Code"), } ) _PRCXI_TEMPLATE_SPECS_CACHE = out return out