Refactor Bioyond resource handling: update warehouse mapping retrieval, add TipBox support, and improve liquid tracking logic. Migrate TipBox creation to bottle_carriers.py for better structure.

This commit is contained in:
ZiWei
2026-01-29 16:31:14 +08:00
parent 6bf57f18c1
commit 37ec49f318
7 changed files with 154 additions and 85 deletions

View File

@@ -258,7 +258,7 @@ class BioyondResourceSynchronizer(ResourceSynchronizer):
logger.info(f"[同步→Bioyond] 物料不存在于 Bioyond将创建新物料并入库") logger.info(f"[同步→Bioyond] 物料不存在于 Bioyond将创建新物料并入库")
# 第1步从配置中获取仓库配置 # 第1步从配置中获取仓库配置
warehouse_mapping = self.bioyond_config.get("warehouse_mapping", {}) warehouse_mapping = self.workstation.bioyond_config.get("warehouse_mapping", {})
# 确定目标仓库名称 # 确定目标仓库名称
parent_name = None parent_name = None

View File

@@ -46,3 +46,16 @@ BIOYOND_PolymerStation_8StockCarrier:
init_param_schema: {} init_param_schema: {}
registry_type: resource registry_type: resource
version: 1.0.0 version: 1.0.0
BIOYOND_PolymerStation_TipBox:
category:
- bottle_carriers
- tip_racks
class:
module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_TipBox
type: pylabrobot
description: BIOYOND_PolymerStation_TipBox (4x6布局24个枪头孔位)
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -82,14 +82,3 @@ BIOYOND_PolymerStation_Solution_Beaker:
icon: '' icon: ''
init_param_schema: {} init_param_schema: {}
version: 1.0.0 version: 1.0.0
BIOYOND_PolymerStation_TipBox:
category:
- bottles
- tip_boxes
class:
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_TipBox
type: pylabrobot
handles: []
icon: ''
init_param_schema: {}
version: 1.0.0

View File

@@ -1,4 +1,4 @@
from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder, create_ordered_items_2d from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder, create_ordered_items_2d, Container
from unilabos.resources.itemized_carrier import BottleCarrier from unilabos.resources.itemized_carrier import BottleCarrier
from unilabos.resources.bioyond.bottles import ( from unilabos.resources.bioyond.bottles import (
@@ -9,6 +9,28 @@ from unilabos.resources.bioyond.bottles import (
BIOYOND_PolymerStation_Reagent_Bottle, BIOYOND_PolymerStation_Reagent_Bottle,
BIOYOND_PolymerStation_Flask, BIOYOND_PolymerStation_Flask,
) )
def BIOYOND_PolymerStation_Tip(name: str, size_x: float = 8.0, size_y: float = 8.0, size_z: float = 50.0) -> Container:
"""创建单个枪头资源
Args:
name: 枪头名称
size_x: 枪头宽度 (mm)
size_y: 枪头长度 (mm)
size_z: 枪头高度 (mm)
Returns:
Container: 枪头容器
"""
return Container(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
category="tip",
model="BIOYOND_PolymerStation_Tip",
)
# 命名约定:试剂瓶-Bottle烧杯-Beaker烧瓶-Flask,小瓶-Vial # 命名约定:试剂瓶-Bottle烧杯-Beaker烧瓶-Flask,小瓶-Vial
@@ -322,3 +344,88 @@ def BIOYOND_Electrolyte_1BottleCarrier(name: str) -> BottleCarrier:
carrier.num_items_z = 1 carrier.num_items_z = 1
carrier[0] = BIOYOND_PolymerStation_Solution_Beaker(f"{name}_beaker_1") carrier[0] = BIOYOND_PolymerStation_Solution_Beaker(f"{name}_beaker_1")
return carrier return carrier
def BIOYOND_PolymerStation_TipBox(
name: str,
size_x: float = 127.76, # 枪头盒宽度
size_y: float = 85.48, # 枪头盒长度
size_z: float = 100.0, # 枪头盒高度
barcode: str = None,
) -> BottleCarrier:
"""创建4×6枪头盒 (24个枪头) - 使用 BottleCarrier 结构
Args:
name: 枪头盒名称
size_x: 枪头盒宽度 (mm)
size_y: 枪头盒长度 (mm)
size_z: 枪头盒高度 (mm)
barcode: 条形码
Returns:
BottleCarrier: 包含24个枪头孔位的枪头盒载架
布局说明:
- 4行×6列 (A-D, 1-6)
- 枪头孔位间距: 18mm (x方向) × 18mm (y方向)
- 起始位置居中对齐
- 索引顺序: 列优先 (0=A1, 1=B1, 2=C1, 3=D1, 4=A2, ...)
"""
# 枪头孔位参数
num_cols = 6 # 1-6 (x方向)
num_rows = 4 # A-D (y方向)
tip_diameter = 8.0 # 枪头孔位直径
tip_spacing_x = 18.0 # 列间距 (增加到18mm更宽松)
tip_spacing_y = 18.0 # 行间距 (增加到18mm更宽松)
# 计算起始位置 (居中对齐)
total_width = (num_cols - 1) * tip_spacing_x + tip_diameter
total_height = (num_rows - 1) * tip_spacing_y + tip_diameter
start_x = (size_x - total_width) / 2
start_y = (size_y - total_height) / 2
# 使用 create_ordered_items_2d 创建孔位
# create_ordered_items_2d 返回的 key 是数字索引: 0, 1, 2, ...
# 顺序是列优先: 先y后x (即 0=A1, 1=B1, 2=C1, 3=D1, 4=A2, 5=B2, ...)
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=num_cols,
num_items_y=num_rows,
dx=start_x,
dy=start_y,
dz=5.0,
item_dx=tip_spacing_x,
item_dy=tip_spacing_y,
size_x=tip_diameter,
size_y=tip_diameter,
size_z=50.0, # 枪头深度
)
# 更新 sites 中每个 ResourceHolder 的名称
for k, v in sites.items():
v.name = f"{name}_{v.name}"
# 创建枪头盒载架
# 注意:不设置 category使用默认的 "bottle_carrier",这样前端会显示为完整的矩形载架
tip_box = BottleCarrier(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
sites=sites, # 直接使用数字索引的 sites
model="BIOYOND_PolymerStation_TipBox",
)
# 设置自定义属性
tip_box.barcode = barcode
tip_box.tip_count = 24 # 4行×6列
tip_box.num_items_x = num_cols
tip_box.num_items_y = num_rows
tip_box.num_items_z = 1
# ⭐ 枪头盒不需要放入子资源
# 与其他 carrier 不同,枪头盒在 Bioyond 中是一个整体
# 不需要追踪每个枪头的状态,保持为空的 ResourceHolder 即可
# 这样前端会显示24个空槽位可以用于放置枪头
return tip_box

View File

@@ -116,7 +116,9 @@ def BIOYOND_PolymerStation_TipBox(
size_z: float = 100.0, # 枪头盒高度 size_z: float = 100.0, # 枪头盒高度
barcode: str = None, barcode: str = None,
): ):
"""创建4×6枪头盒 (24个枪头) """创建4×6枪头盒 (24个枪头) - 使用 BottleCarrier 结构
注意:此函数已弃用,请使用 bottle_carriers.py 中的版本
Args: Args:
name: 枪头盒名称 name: 枪头盒名称
@@ -126,55 +128,11 @@ def BIOYOND_PolymerStation_TipBox(
barcode: 条形码 barcode: 条形码
Returns: Returns:
TipBoxCarrier: 包含24个枪头孔位的枪头盒 BottleCarrier: 包含24个枪头孔位的枪头盒载架
""" """
from pylabrobot.resources import Container, Coordinate # 重定向到 bottle_carriers.py 中的实现
from unilabos.resources.bioyond.bottle_carriers import BIOYOND_PolymerStation_TipBox as TipBox_Carrier
# 创建枪头盒容器 return TipBox_Carrier(name=name, size_x=size_x, size_y=size_y, size_z=size_z, barcode=barcode)
tip_box = Container(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
category="tip_rack",
model="BIOYOND_PolymerStation_TipBox_4x6",
)
# 设置自定义属性
tip_box.barcode = barcode
tip_box.tip_count = 24 # 4行×6列
tip_box.num_items_x = 6 # 6列
tip_box.num_items_y = 4 # 4行
# 创建24个枪头孔位 (4行×6列)
# 假设孔位间距为 9mm
tip_spacing_x = 9.0 # 列间距
tip_spacing_y = 9.0 # 行间距
start_x = 14.38 # 第一个孔位的x偏移
start_y = 11.24 # 第一个孔位的y偏移
for row in range(4): # A, B, C, D
for col in range(6): # 1-6
spot_name = f"{chr(65 + row)}{col + 1}" # A1, A2, ..., D6
x = start_x + col * tip_spacing_x
y = start_y + row * tip_spacing_y
# 创建枪头孔位容器
tip_spot = Container(
name=spot_name,
size_x=8.0, # 单个枪头孔位大小
size_y=8.0,
size_z=size_z - 10.0, # 略低于盒子高度
category="tip_spot",
)
# 添加到枪头盒
tip_box.assign_child_resource(
tip_spot,
location=Coordinate(x=x, y=y, z=0)
)
return tip_box
def BIOYOND_PolymerStation_Flask( def BIOYOND_PolymerStation_Flask(

View File

@@ -759,6 +759,9 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
bottle = plr_material[number] = initialize_resource( bottle = plr_material[number] = initialize_resource(
{"name": f'{detail["name"]}_{number}', "class": reverse_type_mapping[typeName][0]}, resource_type=ResourcePLR {"name": f'{detail["name"]}_{number}', "class": reverse_type_mapping[typeName][0]}, resource_type=ResourcePLR
) )
# 只有具有 tracker 属性的容器才设置液体信息(如 Bottle, Well
# ResourceHolder 等不支持液体追踪的容器跳过
if hasattr(bottle, "tracker"):
bottle.tracker.liquids = [ bottle.tracker.liquids = [
(detail["name"], float(detail.get("quantity", 0)) if detail.get("quantity") else 0) (detail["name"], float(detail.get("quantity", 0)) if detail.get("quantity") else 0)
] ]
@@ -770,6 +773,8 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
# 只对有 capacity 属性的容器(液体容器)处理液体追踪 # 只对有 capacity 属性的容器(液体容器)处理液体追踪
if hasattr(plr_material, 'capacity'): if hasattr(plr_material, 'capacity'):
bottle = plr_material[0] if plr_material.capacity > 0 else plr_material bottle = plr_material[0] if plr_material.capacity > 0 else plr_material
# 确保 bottle 有 tracker 属性才设置液体信息
if hasattr(bottle, "tracker"):
bottle.tracker.liquids = [ bottle.tracker.liquids = [
(material["name"], float(material.get("quantity", 0)) if material.get("quantity") else 0) (material["name"], float(material.get("quantity", 0)) if material.get("quantity") else 0)
] ]
@@ -801,24 +806,29 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
wh_name = loc.get("whName") wh_name = loc.get("whName")
logger.debug(f"[物料位置] {unique_name} 尝试放置到 warehouse: {wh_name} (Bioyond坐标: x={loc.get('x')}, y={loc.get('y')}, z={loc.get('z')})") logger.debug(f"[物料位置] {unique_name} 尝试放置到 warehouse: {wh_name} (Bioyond坐标: x={loc.get('x')}, y={loc.get('y')}, z={loc.get('z')})")
# Bioyond坐标映射 (重要!): x→行(1=A,2=B...), y→列(1=01,2=02...), z→层(通常=1)
# 必须在warehouse映射之前先获取坐标以便后续调整
x = loc.get("x", 1) # 行号 (1-based: 1=A, 2=B, 3=C, 4=D)
y = loc.get("y", 1) # 列号 (1-based: 1=01, 2=02, 3=03...)
z = loc.get("z", 1) # 层号 (1-based, 通常为1)
# 特殊处理: Bioyond的"堆栈1"需要映射到"堆栈1左"或"堆栈1右" # 特殊处理: Bioyond的"堆栈1"需要映射到"堆栈1左"或"堆栈1右"
# 根据列号(x)判断: 1-4映射到左侧, 5-8映射到右侧 # 根据列号(y)判断: 1-4映射到左侧, 5-8映射到右侧
if wh_name == "堆栈1": if wh_name == "堆栈1":
x_val = loc.get("x", 1) if 1 <= y <= 4:
if 1 <= x_val <= 4:
wh_name = "堆栈1左" wh_name = "堆栈1左"
elif 5 <= x_val <= 8: elif 5 <= y <= 8:
wh_name = "堆栈1右" wh_name = "堆栈1右"
y = y - 4 # 调整列号: 5-8映射到1-4
else: else:
logger.warning(f"物料 {material['name']} 的列号 x={x_val} 超出范围无法映射到堆栈1左或堆栈1右") logger.warning(f"物料 {material['name']} 的列号 y={y} 超出范围无法映射到堆栈1左或堆栈1右")
continue continue
# 特殊处理: Bioyond的"站内Tip盒堆栈"也需要进行拆分映射 # 特殊处理: Bioyond的"站内Tip盒堆栈"也需要进行拆分映射
if wh_name == "站内Tip盒堆栈": if wh_name == "站内Tip盒堆栈":
y_val = loc.get("y", 1) if y == 1:
if y_val == 1:
wh_name = "站内Tip盒堆栈(右)" wh_name = "站内Tip盒堆栈(右)"
elif y_val in [2, 3]: elif y in [2, 3]:
wh_name = "站内Tip盒堆栈(左)" wh_name = "站内Tip盒堆栈(左)"
y = y - 1 # 调整列号,因为左侧仓库对应的 Bioyond y=2 实际上是它的第1列 y = y - 1 # 调整列号,因为左侧仓库对应的 Bioyond y=2 实际上是它的第1列
@@ -826,15 +836,6 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
warehouse = deck.warehouses[wh_name] warehouse = deck.warehouses[wh_name]
logger.debug(f"[Warehouse匹配] 找到warehouse: {wh_name} (容量: {warehouse.capacity}, 行×列: {warehouse.num_items_x}×{warehouse.num_items_y})") logger.debug(f"[Warehouse匹配] 找到warehouse: {wh_name} (容量: {warehouse.capacity}, 行×列: {warehouse.num_items_x}×{warehouse.num_items_y})")
# Bioyond坐标映射 (重要!): x→行(1=A,2=B...), y→列(1=01,2=02...), z→层(通常=1)
x = loc.get("x", 1) # 行号 (1-based: 1=A, 2=B, 3=C, 4=D)
y = loc.get("y", 1) # 列号 (1-based: 1=01, 2=02, 3=03...)
z = loc.get("z", 1) # 层号 (1-based, 通常为1)
# 如果是右侧堆栈,需要调整列号 (5→1, 6→2, 7→3, 8→4)
if wh_name == "堆栈1右":
y = y - 4 # 将5-8映射到1-4
# 特殊处理竖向warehouse站内试剂存放堆栈、测量小瓶仓库 # 特殊处理竖向warehouse站内试剂存放堆栈、测量小瓶仓库
# 这些warehouse使用 vertical-col-major 布局 # 这些warehouse使用 vertical-col-major 布局
if wh_name in ["站内试剂存放堆栈", "测量小瓶仓库(测密度)"]: if wh_name in ["站内试剂存放堆栈", "测量小瓶仓库(测密度)"]:

View File

@@ -338,6 +338,7 @@ class ResourceTreeSet(object):
"deck": "deck", "deck": "deck",
"tip_rack": "tip_rack", "tip_rack": "tip_rack",
"tip_spot": "tip_spot", "tip_spot": "tip_spot",
"tip": "tip", # 添加 tip 类型支持
"tube": "tube", "tube": "tube",
"bottle_carrier": "bottle_carrier", "bottle_carrier": "bottle_carrier",
} }