mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-17 13:01:12 +00:00
add standardized BIOYOND resources: bottle_carrier, bottle
This commit is contained in:
49
test/resources/bottle_carrier.py
Normal file
49
test/resources/bottle_carrier.py
Normal file
@@ -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载架设置完成!")
|
||||||
0
unilabos/resources/bioyond/__init__.py
Normal file
0
unilabos/resources/bioyond/__init__.py
Normal file
50
unilabos/resources/bioyond/bottle.py
Normal file
50
unilabos/resources/bioyond/bottle.py
Normal file
@@ -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",
|
||||||
|
)
|
||||||
78
unilabos/resources/bioyond/bottle_carrier.py
Normal file
78
unilabos/resources/bioyond/bottle_carrier.py
Normal file
@@ -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",
|
||||||
|
)
|
||||||
54
unilabos/resources/bioyond/warehouse.py
Normal file
54
unilabos/resources/bioyond/warehouse.py
Normal file
@@ -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
|
||||||
|
)
|
||||||
72
unilabos/resources/bottle_carrier.py
Normal file
72
unilabos/resources/bottle_carrier.py
Normal file
@@ -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,
|
||||||
|
)
|
||||||
79
unilabos/resources/warehouse.py
Normal file
79
unilabos/resources/warehouse.py
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user