diff --git a/test/resources/bottle_carrier.py b/test/resources/bottle_carrier.py new file mode 100644 index 00000000..162ead9e --- /dev/null +++ b/test/resources/bottle_carrier.py @@ -0,0 +1,49 @@ +import pytest + +from unilabos.resources.bioyond.bottle_carrier import BIOYOND_Electrolyte_6VialCarrier, BIOYOND_Electrolyte_1BottleCarrier +from unilabos.resources.bioyond.bottle import create_powder_bottle, create_solution_beaker, create_reagent_bottle + + +@pytest.fixture +def bottle_carrier() -> "BottleCarrier": + print("创建载架...") + + # 创建6瓶载架 + bottle_carrier = BIOYOND_Electrolyte_6VialCarrier("powder_carrier_01") + print(f"6瓶载架: {bottle_carrier.name}, 位置数: {len(bottle_carrier.sites)}") + + # 创建1烧杯载架 + beaker_carrier = BIOYOND_Electrolyte_1BottleCarrier("solution_carrier_01") + print(f"1烧杯载架: {beaker_carrier.name}, 位置数: {len(beaker_carrier.sites)}") + + # 创建瓶子和烧杯 + powder_bottle = create_powder_bottle("powder_bottle_01") + solution_beaker = create_solution_beaker("solution_beaker_01") + reagent_bottle = create_reagent_bottle("reagent_bottle_01") + + print(f"\n创建的物料:") + print(f"粉末瓶: {powder_bottle.name} - {powder_bottle.diameter}mm x {powder_bottle.height}mm, {powder_bottle.max_volume}μL") + print(f"溶液烧杯: {solution_beaker.name} - {solution_beaker.diameter}mm x {solution_beaker.height}mm, {solution_beaker.max_volume}μL") + print(f"试剂瓶: {reagent_bottle.name} - {reagent_bottle.diameter}mm x {reagent_bottle.height}mm, {reagent_bottle.max_volume}μL") + + # 测试放置容器 + print(f"\n测试放置容器...") + + # 通过载架的索引操作来放置容器 + bottle_carrier[0] = powder_bottle # 放置粉末瓶到第一个位置 + print(f"粉末瓶已放置到6瓶载架的位置 0") + + beaker_carrier[0] = solution_beaker # 放置烧杯到第一个位置 + print(f"溶液烧杯已放置到1烧杯载架的位置 0") + + # 验证放置结果 + print(f"\n验证放置结果:") + bottle_at_0 = bottle_carrier[0].resource + beaker_at_0 = beaker_carrier[0].resource + + if bottle_at_0: + print(f"位置 0 的瓶子: {bottle_at_0.name}") + if beaker_at_0: + print(f"位置 0 的烧杯: {beaker_at_0.name}") + + print("\n载架设置完成!") \ No newline at end of file diff --git a/unilabos/resources/bioyond/__init__.py b/unilabos/resources/bioyond/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/unilabos/resources/bioyond/bottle.py b/unilabos/resources/bioyond/bottle.py new file mode 100644 index 00000000..f642ca2e --- /dev/null +++ b/unilabos/resources/bioyond/bottle.py @@ -0,0 +1,50 @@ +from unilabos.resources.bottle_carrier import Bottle, BottleCarrier +# 工厂函数 + + +def create_powder_bottle( + name: str, + diameter: float = 30.0, + height: float = 50.0, + max_volume: float = 50000.0, # 50mL +) -> Bottle: + """创建粉末瓶""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + category="powder_bottle", + ) + + +def create_solution_beaker( + name: str, + diameter: float = 80.0, + height: float = 100.0, + max_volume: float = 500000.0, # 500mL +) -> Bottle: + """创建溶液烧杯""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + category="solution_beaker", + ) + + +def create_reagent_bottle( + name: str, + diameter: float = 20.0, + height: float = 40.0, + max_volume: float = 15000.0, # 15mL +) -> Bottle: + """创建试剂瓶""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + category="reagent_bottle", + ) \ No newline at end of file diff --git a/unilabos/resources/bioyond/bottle_carrier.py b/unilabos/resources/bioyond/bottle_carrier.py new file mode 100644 index 00000000..e7ac488e --- /dev/null +++ b/unilabos/resources/bioyond/bottle_carrier.py @@ -0,0 +1,78 @@ +from unilabos.resources.bottle_carrier import Bottle, BottleCarrier +from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder + + +# 命名约定:试剂瓶-Bottle,烧杯-Beaker,烧瓶-Flask,小瓶-Vial + +def BIOYOND_Electrolyte_6VialCarrier(name: str) -> BottleCarrier: + """6瓶载架 - 2x3布局""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 50.0 + + # 瓶位尺寸 + bottle_diameter = 30.0 + bottle_spacing_x = 42.0 # X方向间距 + bottle_spacing_y = 35.0 # Y方向间距 + + # 计算起始位置 (居中排列) + start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2 + start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2 + + # 创建6个位置坐标 (2行 x 3列) + locations = [] + for row in range(2): + for col in range(3): + x = start_x + col * bottle_spacing_x + y = start_y + row * bottle_spacing_y + z = 5.0 # 架位底部 + locations.append(Coordinate(x, y, z)) + + return BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=create_homogeneous_resources( + klass=ResourceHolder, + locations=locations, + resource_size_x=bottle_diameter, + resource_size_y=bottle_diameter, + name_prefix=name, + ), + model="BIOYOND_Electrolyte_6VialCarrier", + ) + + +def BIOYOND_Electrolyte_1BottleCarrier(name: str) -> BottleCarrier: + """1瓶载架 - 单个中央位置""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 100.0 + + # 烧杯尺寸 + beaker_diameter = 80.0 + + # 计算中央位置 + center_x = (carrier_size_x - beaker_diameter) / 2 + center_y = (carrier_size_y - beaker_diameter) / 2 + center_z = 5.0 + + return BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=create_homogeneous_resources( + klass=ResourceHolder, + locations=[Coordinate(center_x, center_y, center_z)], + resource_size_x=beaker_diameter, + resource_size_y=beaker_diameter, + name_prefix=name, + ), + model="BIOYOND_Electrolyte_1BottleCarrier", + ) \ No newline at end of file diff --git a/unilabos/resources/bioyond/warehouse.py b/unilabos/resources/bioyond/warehouse.py new file mode 100644 index 00000000..c468f875 --- /dev/null +++ b/unilabos/resources/bioyond/warehouse.py @@ -0,0 +1,54 @@ +from unilabos.resources.warehouse import WareHouse + + +def bioyond_warehouse_1x4x4(name: str) -> WareHouse: + """创建BioYond 4x1x4仓库""" + return WareHouse( + name=name, + num_items_x=1, + num_items_y=4, + num_items_z=4, + dx=137.0, + dy=96.0, + dz=120.0, + item_dx=10.0, + item_dy=10.0, + item_dz=10.0, + category="warehouse", + ) + + +def bioyond_warehouse_1x3x2(name: str) -> WareHouse: + """创建BioYond 3x1x2仓库""" + return WareHouse( + name=name, + num_items_x=1, + num_items_y=3, + num_items_z=2, + dx=137.0, + dy=96.0, + dz=120.0, + item_dx=10.0, + item_dy=10.0, + item_dz=10.0, + category="warehouse", + removed_positions=None + ) + + +def bioyond_warehouse_liquid_and_lid_handling(name: str) -> WareHouse: + """创建BioYond开关盖加液模块台面""" + return WareHouse( + name=name, + num_items_x=2, + num_items_y=5, + num_items_z=1, + dx=137.0, + dy=96.0, + dz=120.0, + item_dx=10.0, + item_dy=10.0, + item_dz=10.0, + category="warehouse", + removed_positions=None + ) \ No newline at end of file diff --git a/unilabos/resources/bottle_carrier.py b/unilabos/resources/bottle_carrier.py new file mode 100644 index 00000000..e680b26e --- /dev/null +++ b/unilabos/resources/bottle_carrier.py @@ -0,0 +1,72 @@ +""" +自动化液体处理工作站物料类定义 - 简化版 +Automated Liquid Handling Station Resource Classes - Simplified Version +""" + +from __future__ import annotations + +from typing import Dict, Optional + +from pylabrobot.resources.coordinate import Coordinate +from pylabrobot.resources.container import Container +from pylabrobot.resources.carrier import TubeCarrier +from pylabrobot.resources.resource_holder import ResourceHolder + + +class Bottle(Container): + """瓶子类 - 简化版,不追踪瓶盖""" + + def __init__( + self, + name: str, + diameter: float, + height: float, + max_volume: float, + barcode: Optional[str] = "", + category: str = "container", + model: Optional[str] = None, + ): + super().__init__( + name=name, + size_x=diameter, + size_y=diameter, + size_z=height, + max_volume=max_volume, + category=category, + model=model, + ) + self.diameter = diameter + self.height = height + self.barcode = barcode + + def serialize(self) -> dict: + return { + **super().serialize(), + "diameter": self.diameter, + "height": self.height, + "barcode": self.barcode, + } + + +class BottleCarrier(TubeCarrier): + """瓶载架 - 直接继承自 TubeCarrier""" + + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + sites: Optional[Dict[int, ResourceHolder]] = None, + category: str = "bottle_carrier", + model: Optional[str] = None, + ): + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + sites=sites, + category=category, + model=model, + ) diff --git a/unilabos/resources/warehouse.py b/unilabos/resources/warehouse.py new file mode 100644 index 00000000..5561b94e --- /dev/null +++ b/unilabos/resources/warehouse.py @@ -0,0 +1,79 @@ +import json +from typing import Optional, List +from pylabrobot.resources import Coordinate, Resource +from pylabrobot.resources.carrier import Carrier, PlateHolder, ResourceHolder, create_homogeneous_resources +from pylabrobot.resources.deck import Deck + + +class WareHouse(Carrier[ResourceHolder]): + """4x4x1堆栈载体类 - 可容纳16个板位的载体(4层x4行x1列)""" + + def __init__( + self, + name: str, + num_items_x: int = 4, + num_items_y: int = 1, + num_items_z: int = 4, + dx: float = 137.0, + dy: float = 96.0, + dz: float = 120.0, + item_dx: float = 10.0, + item_dy: float = 10.0, + item_dz: float = 10.0, + removed_positions: Optional[List[int]] = None, + category: str = "warehouse", + model: Optional[str] = None, + ): + # 创建16个板架位 (4层 x 4位置) + locations = [] + + for layer in range(num_items_z): # 4层 + for row in range(num_items_y): # 4行 + for col in range(num_items_x): # 1列 (每层4x1=4个位置) + # 计算位置 + x = dx + col * item_dx + y = dy + (num_items_y - row - 1) * item_dy + z = dz + (num_items_z - layer - 1) * item_dz + + locations.append(Coordinate(x, y, z)) + if removed_positions: + locations = [loc for i, loc in enumerate(locations) if i not in removed_positions] + + sites = create_homogeneous_resources( + klass=ResourceHolder, + locations=[ + Coordinate(4.0, 8.5, 86.15), + Coordinate(4.0, 104.5, 86.15), + Coordinate(4.0, 200.5, 86.15), + Coordinate(4.0, 296.5, 86.15), + Coordinate(4.0, 392.5, 86.15), + ], + resource_size_x=127.0, + resource_size_y=86.0, + name_prefix=name, + ) + + super().__init__( + name=name, + size_x=dx + item_dx * num_items_x, + size_y=dy + item_dy * num_items_y, + size_z=dz + item_dz * num_items_z, + sites=sites, + category=category, + model=model, + ) + + def get_site_by_layer_position(self, row: int, col: int, layer: int) -> PlateHolder: + if not (0 <= layer < 4 and 0 <= row < 4 and 0 <= col < 1): + raise ValueError("无效的位置: layer={}, row={}, col={}".format(layer, row, col)) + + site_index = layer * 4 + row * 1 + col + return self.sites[site_index] + + def add_rack_to_position(self, row: int, col: int, layer: int, rack) -> None: + site = self.get_site_by_layer_position(row, col, layer) + site.assign_child_resource(rack) + + def get_rack_at_position(self, row: int, col: int, layer: int): + site = self.get_site_by_layer_position(row, col, layer) + return site.resource \ No newline at end of file