mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-17 04:51:10 +00:00
0.10.7 Update (#101)
* Cleanup registry to be easy-understanding (#76) * delete deprecated mock devices * rename categories * combine chromatographic devices * rename rviz simulation nodes * organic virtual devices * parse vessel_id * run registry completion before merge --------- Co-authored-by: Xuwznln <18435084+Xuwznln@users.noreply.github.com> * fix: workstation handlers and vessel_id parsing * fix: working dir error when input config path feat: report publish topic when error * modify default discovery_interval to 15s * feat: add trace log level * feat: 添加ChinWe设备控制类,支持串口通信和电机控制功能 (#79) * fix: drop_tips not using auto resource select * fix: discard_tips error * fix: discard_tips * fix: prcxi_res * add: prcxi res fix: startup slow * feat: workstation example * fix pumps and liquid_handler handle * feat: 优化protocol node节点运行日志 * fix all protocol_compilers and remove deprecated devices * feat: 新增use_remote_resource参数 * fix and remove redundant info * bugfixes on organic protocols * fix filter protocol * fix protocol node * 临时兼容错误的driver写法 * fix: prcxi import error * use call_async in all service to avoid deadlock * fix: figure_resource * Update recipe.yaml * add workstation template and battery example * feat: add sk & ak * update workstation base * Create workstation_architecture.md * refactor: workstation_base 重构为仅含业务逻辑,通信和子设备管理交给 ProtocolNode * refactor: ProtocolNode→WorkstationNode * Add:msgs.action (#83) * update: Workstation dev 将版本号从 0.10.3 更新为 0.10.4 (#84) * Add:msgs.action * update: 将版本号从 0.10.3 更新为 0.10.4 * simplify resource system * uncompleted refactor * example for use WorkstationBase * feat: websocket * feat: websocket test * feat: workstation example * feat: action status * fix: station自己的方法注册错误 * fix: 还原protocol node处理方法 * fix: build * fix: missing job_id key * ws test version 1 * ws test version 2 * ws protocol * 增加物料关系上传日志 * 增加物料关系上传日志 * 修正物料关系上传 * 修复工站的tracker实例追踪失效问题 * 增加handle检测,增加material edge关系上传 * 修复event loop错误 * 修复edge上报错误 * 修复async错误 * 更新schema的title字段 * 主机节点信息等支持自动刷新 * 注册表编辑器 * 修复status密集发送时,消息出错 * 增加addr参数 * fix: addr param * fix: addr param * 取消labid 和 强制config输入 * Add action definitions for LiquidHandlerSetGroup and LiquidHandlerTransferGroup - Created LiquidHandlerSetGroup.action with fields for group name, wells, and volumes. - Created LiquidHandlerTransferGroup.action with fields for source and target group names and unit volume. - Both actions include response fields for return information and success status. * Add LiquidHandlerSetGroup and LiquidHandlerTransferGroup actions to CMakeLists * Add set_group and transfer_group methods to PRCXI9300Handler and update liquid_handler.yaml * result_info改为字典类型 * 新增uat的地址替换 * runze multiple pump support (cherry picked from commit49354fcf39) * remove runze multiple software obtainer (cherry picked from commit8bcc92a394) * support multiple backbone (cherry picked from commit4771ff2347) * Update runze pump format * Correct runze multiple backbone * Update runze_multiple_backbone * Correct runze pump multiple receive method. * Correct runze pump multiple receive method. * 对于PRCXI9320的transfer_group,一对多和多对多 * 移除MQTT,更新launch文档,提供注册表示例文件,更新到0.10.5 * fix import error * fix dupe upload registry * refactor ws client * add server timeout * Fix: run-column with correct vessel id (#86) * fix run_column * Update run_column_protocol.py (cherry picked from commite5aa4d940a) * resource_update use resource_add * 新增版位推荐功能 * 重新规定了版位推荐的入参 * update registry with nested obj * fix protocol node log_message, added create_resource return value * fix protocol node log_message, added create_resource return value * try fix add protocol * fix resource_add * 修复移液站错误的aspirate注册表 * Feature/xprbalance-zhida (#80) * feat(devices): add Zhida GC/MS pretreatment automation workstation * feat(devices): add mettler_toledo xpr balance * balance * 重新补全zhida注册表 * PRCXI9320 json * PRCXI9320 json * PRCXI9320 json * fix resource download * remove class for resource * bump version to 0.10.6 * 更新所有注册表 * 修复protocolnode的兼容性 * 修复protocolnode的兼容性 * Update install md * Add Defaultlayout * 更新物料接口 * fix dict to tree/nested-dict converter * coin_cell_station draft * refactor: rename "station_resource" to "deck" * add standardized BIOYOND resources: bottle_carrier, bottle * refactor and add BIOYOND resources tests * add BIOYOND deck assignment and pass all tests * fix: update resource with correct structure; remove deprecated liquid_handler set_group action * feat: 将新威电池测试系统驱动与配置文件并入 workstation_dev_YB2 (#92) * feat: 新威电池测试系统驱动与注册文件 * feat: bring neware driver & battery.json into workstation_dev_YB2 * add bioyond studio draft * bioyond station with communication init and resource sync * fix bioyond station and registry * fix: update resource with correct structure; remove deprecated liquid_handler set_group action * frontend_docs * create/update resources with POST/PUT for big amount/ small amount data * create/update resources with POST/PUT for big amount/ small amount data * refactor: add itemized_carrier instead of carrier consists of ResourceHolder * create warehouse by factory func * update bioyond launch json * add child_size for itemized_carrier * fix bioyond resource io * Workstation templates: Resources and its CRUD, and workstation tasks (#95) * coin_cell_station draft * refactor: rename "station_resource" to "deck" * add standardized BIOYOND resources: bottle_carrier, bottle * refactor and add BIOYOND resources tests * add BIOYOND deck assignment and pass all tests * fix: update resource with correct structure; remove deprecated liquid_handler set_group action * feat: 将新威电池测试系统驱动与配置文件并入 workstation_dev_YB2 (#92) * feat: 新威电池测试系统驱动与注册文件 * feat: bring neware driver & battery.json into workstation_dev_YB2 * add bioyond studio draft * bioyond station with communication init and resource sync * fix bioyond station and registry * create/update resources with POST/PUT for big amount/ small amount data * refactor: add itemized_carrier instead of carrier consists of ResourceHolder * create warehouse by factory func * update bioyond launch json * add child_size for itemized_carrier * fix bioyond resource io --------- Co-authored-by: h840473807 <47357934+h840473807@users.noreply.github.com> Co-authored-by: Xie Qiming <97236197+Andy6M@users.noreply.github.com> * 更新物料接口 * Workstation dev yb2 (#100) * Refactor and extend reaction station action messages * Refactor dispensing station tasks to enhance parameter clarity and add batch processing capabilities - Updated `create_90_10_vial_feeding_task` to include detailed parameters for 90%/10% vial feeding, improving clarity and usability. - Introduced `create_batch_90_10_vial_feeding_task` for batch processing of 90%/10% vial feeding tasks with JSON formatted input. - Added `create_batch_diamine_solution_task` for batch preparation of diamine solution, also utilizing JSON formatted input. - Refined `create_diamine_solution_task` to include additional parameters for better task configuration. - Enhanced schema descriptions and default values for improved user guidance. * 修复to_plr_resources * add update remove * 支持选择器注册表自动生成 支持转运物料 * 修复资源添加 * 修复transfer_resource_to_another生成 * 更新transfer_resource_to_another参数,支持spot入参 * 新增test_resource动作 * fix host_node error * fix host_node test_resource error * fix host_node test_resource error * 过滤本地动作 * 移动内部action以兼容host node * 修复同步任务报错不显示的bug * feat: 允许返回非本节点物料,后面可以通过decoration进行区分,就不进行warning了 * update todo * modify bioyond/plr converter, bioyond resource registry, and tests * pass the tests * update todo * add conda-pack-build.yml * add auto install script for conda-pack-build.yml (cherry picked from commit172599adcf) * update conda-pack-build.yml * update conda-pack-build.yml * update conda-pack-build.yml * update conda-pack-build.yml * update conda-pack-build.yml * Add version in __init__.py Update conda-pack-build.yml Add create_zip_archive.py * Update conda-pack-build.yml * Update conda-pack-build.yml (with mamba) * Update conda-pack-build.yml * Fix FileNotFoundError * Try fix 'charmap' codec can't encode characters in position 16-23: character maps to <undefined> * Fix unilabos msgs search error * Fix environment_check.py * Update recipe.yaml * Update registry. Update uuid loop figure method. Update install docs. * Fix nested conda pack * Fix one-key installation path error * Bump version to 0.10.7 * Workshop bj (#99) * Add LaiYu Liquid device integration and tests Introduce LaiYu Liquid device implementation, including backend, controllers, drivers, configuration, and resource files. Add hardware connection, tip pickup, and simplified test scripts, as well as experiment and registry configuration for LaiYu Liquid. Documentation and .gitignore for the device are also included. * feat(LaiYu_Liquid): 重构设备模块结构并添加硬件文档 refactor: 重新组织LaiYu_Liquid模块目录结构 docs: 添加SOPA移液器和步进电机控制指令文档 fix: 修正设备配置中的最大体积默认值 test: 新增工作台配置测试用例 chore: 删除过时的测试脚本和配置文件 * add * 重构: 将 LaiYu_Liquid.py 重命名为 laiyu_liquid_main.py 并更新所有导入引用 - 使用 git mv 将 LaiYu_Liquid.py 重命名为 laiyu_liquid_main.py - 更新所有相关文件中的导入引用 - 保持代码功能不变,仅改善命名一致性 - 测试确认所有导入正常工作 * 修复: 在 core/__init__.py 中添加 LaiYuLiquidBackend 导出 - 添加 LaiYuLiquidBackend 到导入列表 - 添加 LaiYuLiquidBackend 到 __all__ 导出列表 - 确保所有主要类都可以正确导入 * 修复大小写文件夹名字 * 电池装配工站二次开发教程(带目录)上传至dev (#94) * 电池装配工站二次开发教程 * Update intro.md * 物料教程 * 更新物料教程,json格式注释 * Update prcxi driver & fix transfer_liquid mix_times (#90) * Update prcxi driver & fix transfer_liquid mix_times * fix: correct mix_times type * Update liquid_handler registry * test: prcxi.py * Update registry from pr * fix ony-key script not exist * clean files --------- Co-authored-by: Junhan Chang <changjh@dp.tech> Co-authored-by: ZiWei <131428629+ZiWei09@users.noreply.github.com> Co-authored-by: Guangxin Zhang <guangxin.zhang.bio@gmail.com> Co-authored-by: Xie Qiming <97236197+Andy6M@users.noreply.github.com> Co-authored-by: h840473807 <47357934+h840473807@users.noreply.github.com> Co-authored-by: LccLink <1951855008@qq.com> Co-authored-by: lixinyu1011 <61094742+lixinyu1011@users.noreply.github.com> Co-authored-by: shiyubo0410 <shiyubo@dp.tech>
This commit is contained in:
0
unilabos/resources/bioyond/__init__.py
Normal file
0
unilabos/resources/bioyond/__init__.py
Normal file
276
unilabos/resources/bioyond/bottle_carriers.py
Normal file
276
unilabos/resources/bioyond/bottle_carriers.py
Normal file
@@ -0,0 +1,276 @@
|
||||
from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder, create_ordered_items_2d
|
||||
|
||||
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
|
||||
from unilabos.resources.bioyond.bottles import (
|
||||
BIOYOND_PolymerStation_Solid_Stock,
|
||||
BIOYOND_PolymerStation_Solid_Vial,
|
||||
BIOYOND_PolymerStation_Liquid_Vial,
|
||||
BIOYOND_PolymerStation_Solution_Beaker,
|
||||
BIOYOND_PolymerStation_Reagent_Bottle
|
||||
)
|
||||
# 命名约定:试剂瓶-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
|
||||
|
||||
sites = create_ordered_items_2d(
|
||||
klass=ResourceHolder,
|
||||
num_items_x=3,
|
||||
num_items_y=2,
|
||||
dx=start_x,
|
||||
dy=start_y,
|
||||
dz=5.0,
|
||||
item_dx=bottle_spacing_x,
|
||||
item_dy=bottle_spacing_y,
|
||||
|
||||
size_x=bottle_diameter,
|
||||
size_y=bottle_diameter,
|
||||
size_z=carrier_size_z,
|
||||
)
|
||||
for k, v in sites.items():
|
||||
v.name = f"{name}_{v.name}"
|
||||
|
||||
carrier = BottleCarrier(
|
||||
name=name,
|
||||
size_x=carrier_size_x,
|
||||
size_y=carrier_size_y,
|
||||
size_z=carrier_size_z,
|
||||
sites=sites,
|
||||
model="BIOYOND_Electrolyte_6VialCarrier",
|
||||
)
|
||||
carrier.num_items_x = 3
|
||||
carrier.num_items_y = 2
|
||||
carrier.num_items_z = 1
|
||||
for i in range(6):
|
||||
carrier[i] = BIOYOND_PolymerStation_Solid_Vial(f"{name}_vial_{i+1}")
|
||||
return carrier
|
||||
|
||||
|
||||
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
|
||||
|
||||
carrier = 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",
|
||||
)
|
||||
carrier.num_items_x = 1
|
||||
carrier.num_items_y = 1
|
||||
carrier.num_items_z = 1
|
||||
carrier[0] = BIOYOND_PolymerStation_Solution_Beaker(f"{name}_beaker_1")
|
||||
return carrier
|
||||
|
||||
|
||||
def BIOYOND_PolymerStation_6StockCarrier(name: str) -> BottleCarrier:
|
||||
"""6瓶载架 - 2x3布局"""
|
||||
|
||||
# 载架尺寸 (mm)
|
||||
carrier_size_x = 127.8
|
||||
carrier_size_y = 85.5
|
||||
carrier_size_z = 50.0
|
||||
|
||||
# 瓶位尺寸
|
||||
bottle_diameter = 20.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
|
||||
|
||||
sites = create_ordered_items_2d(
|
||||
klass=ResourceHolder,
|
||||
num_items_x=3,
|
||||
num_items_y=2,
|
||||
dx=start_x,
|
||||
dy=start_y,
|
||||
dz=5.0,
|
||||
item_dx=bottle_spacing_x,
|
||||
item_dy=bottle_spacing_y,
|
||||
|
||||
size_x=bottle_diameter,
|
||||
size_y=bottle_diameter,
|
||||
size_z=carrier_size_z,
|
||||
)
|
||||
for k, v in sites.items():
|
||||
v.name = f"{name}_{v.name}"
|
||||
|
||||
carrier = BottleCarrier(
|
||||
name=name,
|
||||
size_x=carrier_size_x,
|
||||
size_y=carrier_size_y,
|
||||
size_z=carrier_size_z,
|
||||
sites=sites,
|
||||
model="BIOYOND_PolymerStation_6VialCarrier",
|
||||
)
|
||||
carrier.num_items_x = 3
|
||||
carrier.num_items_y = 2
|
||||
carrier.num_items_z = 1
|
||||
ordering = ["A1", "A2", "A3", "B1", "B2", "B3"] # 自定义顺序
|
||||
for i in range(6):
|
||||
carrier[i] = BIOYOND_PolymerStation_Solid_Stock(f"{name}_vial_{ordering[i]}")
|
||||
return carrier
|
||||
|
||||
|
||||
def BIOYOND_PolymerStation_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
|
||||
|
||||
sites = create_ordered_items_2d(
|
||||
klass=ResourceHolder,
|
||||
num_items_x=3,
|
||||
num_items_y=2,
|
||||
dx=start_x,
|
||||
dy=start_y,
|
||||
dz=5.0,
|
||||
item_dx=bottle_spacing_x,
|
||||
item_dy=bottle_spacing_y,
|
||||
|
||||
size_x=bottle_diameter,
|
||||
size_y=bottle_diameter,
|
||||
size_z=carrier_size_z,
|
||||
)
|
||||
for k, v in sites.items():
|
||||
v.name = f"{name}_{v.name}"
|
||||
|
||||
carrier = BottleCarrier(
|
||||
name=name,
|
||||
size_x=carrier_size_x,
|
||||
size_y=carrier_size_y,
|
||||
size_z=carrier_size_z,
|
||||
sites=sites,
|
||||
model="BIOYOND_PolymerStation_6VialCarrier",
|
||||
)
|
||||
carrier.num_items_x = 3
|
||||
carrier.num_items_y = 2
|
||||
carrier.num_items_z = 1
|
||||
ordering = ["A1", "A2", "A3", "B1", "B2", "B3"] # 自定义顺序
|
||||
for i in range(3):
|
||||
carrier[i] = BIOYOND_PolymerStation_Solid_Vial(f"{name}_solidvial_{ordering[i]}")
|
||||
for i in range(3, 6):
|
||||
carrier[i] = BIOYOND_PolymerStation_Liquid_Vial(f"{name}_liquidvial_{ordering[i]}")
|
||||
return carrier
|
||||
|
||||
|
||||
def BIOYOND_PolymerStation_1BottleCarrier(name: str) -> BottleCarrier:
|
||||
"""1瓶载架 - 单个中央位置"""
|
||||
|
||||
# 载架尺寸 (mm)
|
||||
carrier_size_x = 127.8
|
||||
carrier_size_y = 85.5
|
||||
carrier_size_z = 20.0
|
||||
|
||||
# 烧杯尺寸
|
||||
beaker_diameter = 60.0
|
||||
|
||||
# 计算中央位置
|
||||
center_x = (carrier_size_x - beaker_diameter) / 2
|
||||
center_y = (carrier_size_y - beaker_diameter) / 2
|
||||
center_z = 5.0
|
||||
|
||||
carrier = 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_PolymerStation_1BottleCarrier",
|
||||
)
|
||||
carrier.num_items_x = 1
|
||||
carrier.num_items_y = 1
|
||||
carrier.num_items_z = 1
|
||||
carrier[0] = BIOYOND_PolymerStation_Reagent_Bottle(f"{name}_flask_1")
|
||||
return carrier
|
||||
|
||||
|
||||
def BIOYOND_PolymerStation_1FlaskCarrier(name: str) -> BottleCarrier:
|
||||
"""1瓶载架 - 单个中央位置"""
|
||||
|
||||
# 载架尺寸 (mm)
|
||||
carrier_size_x = 127.8
|
||||
carrier_size_y = 85.5
|
||||
carrier_size_z = 20.0
|
||||
|
||||
# 烧杯尺寸
|
||||
beaker_diameter = 70.0
|
||||
|
||||
# 计算中央位置
|
||||
center_x = (carrier_size_x - beaker_diameter) / 2
|
||||
center_y = (carrier_size_y - beaker_diameter) / 2
|
||||
center_z = 5.0
|
||||
|
||||
carrier = 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_PolymerStation_1FlaskCarrier",
|
||||
)
|
||||
carrier.num_items_x = 1
|
||||
carrier.num_items_y = 1
|
||||
carrier.num_items_z = 1
|
||||
carrier[0] = BIOYOND_PolymerStation_Reagent_Bottle(f"{name}_bottle_1")
|
||||
return carrier
|
||||
92
unilabos/resources/bioyond/bottles.py
Normal file
92
unilabos/resources/bioyond/bottles.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
|
||||
# 工厂函数
|
||||
|
||||
|
||||
def BIOYOND_PolymerStation_Solid_Stock(
|
||||
name: str,
|
||||
diameter: float = 20.0,
|
||||
height: float = 100.0,
|
||||
max_volume: float = 30000.0, # 30mL
|
||||
barcode: str = None,
|
||||
) -> Bottle:
|
||||
"""创建粉末瓶"""
|
||||
return Bottle(
|
||||
name=name,
|
||||
diameter=diameter,
|
||||
height=height,
|
||||
max_volume=max_volume,
|
||||
barcode=barcode,
|
||||
model="BIOYOND_PolymerStation_Solid_Stock",
|
||||
)
|
||||
|
||||
|
||||
def BIOYOND_PolymerStation_Solid_Vial(
|
||||
name: str,
|
||||
diameter: float = 25.0,
|
||||
height: float = 60.0,
|
||||
max_volume: float = 30000.0, # 30mL
|
||||
barcode: str = None,
|
||||
) -> Bottle:
|
||||
"""创建粉末瓶"""
|
||||
return Bottle(
|
||||
name=name,
|
||||
diameter=diameter,
|
||||
height=height,
|
||||
max_volume=max_volume,
|
||||
barcode=barcode,
|
||||
model="BIOYOND_PolymerStation_Solid_Vial",
|
||||
)
|
||||
|
||||
|
||||
def BIOYOND_PolymerStation_Liquid_Vial(
|
||||
name: str,
|
||||
diameter: float = 25.0,
|
||||
height: float = 60.0,
|
||||
max_volume: float = 30000.0, # 30mL
|
||||
barcode: str = None,
|
||||
) -> Bottle:
|
||||
"""创建滴定液瓶"""
|
||||
return Bottle(
|
||||
name=name,
|
||||
diameter=diameter,
|
||||
height=height,
|
||||
max_volume=max_volume,
|
||||
barcode=barcode,
|
||||
model="BIOYOND_PolymerStation_Liquid_Vial",
|
||||
)
|
||||
|
||||
|
||||
def BIOYOND_PolymerStation_Solution_Beaker(
|
||||
name: str,
|
||||
diameter: float = 60.0,
|
||||
height: float = 70.0,
|
||||
max_volume: float = 200000.0, # 200mL
|
||||
barcode: str = None,
|
||||
) -> Bottle:
|
||||
"""创建溶液烧杯"""
|
||||
return Bottle(
|
||||
name=name,
|
||||
diameter=diameter,
|
||||
height=height,
|
||||
max_volume=max_volume,
|
||||
barcode=barcode,
|
||||
model="BIOYOND_PolymerStation_Solution_Beaker",
|
||||
)
|
||||
|
||||
|
||||
def BIOYOND_PolymerStation_Reagent_Bottle(
|
||||
name: str,
|
||||
diameter: float = 70.0,
|
||||
height: float = 120.0,
|
||||
max_volume: float = 500000.0, # 500mL
|
||||
barcode: str = None,
|
||||
) -> Bottle:
|
||||
"""创建试剂瓶"""
|
||||
return Bottle(
|
||||
name=name,
|
||||
diameter=diameter,
|
||||
height=height,
|
||||
max_volume=max_volume,
|
||||
barcode=barcode,
|
||||
model="BIOYOND_PolymerStation_Reagent_Bottle",
|
||||
)
|
||||
68
unilabos/resources/bioyond/decks.py
Normal file
68
unilabos/resources/bioyond/decks.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from pylabrobot.resources import Deck, Coordinate, Rotation
|
||||
|
||||
from unilabos.resources.bioyond.warehouses import bioyond_warehouse_1x4x4, bioyond_warehouse_1x4x2, bioyond_warehouse_liquid_and_lid_handling
|
||||
|
||||
|
||||
class BIOYOND_PolymerReactionStation_Deck(Deck):
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "PolymerReactionStation_Deck",
|
||||
size_x: float = 2700.0,
|
||||
size_y: float = 1080.0,
|
||||
size_z: float = 1500.0,
|
||||
category: str = "deck",
|
||||
setup: bool = False
|
||||
) -> None:
|
||||
super().__init__(name=name, size_x=2700.0, size_y=1080.0, size_z=1500.0)
|
||||
if setup:
|
||||
self.setup()
|
||||
|
||||
def setup(self) -> None:
|
||||
# 添加仓库
|
||||
self.warehouses = {
|
||||
"堆栈1": bioyond_warehouse_1x4x4("堆栈1"),
|
||||
"堆栈2": bioyond_warehouse_1x4x4("堆栈2"),
|
||||
"站内试剂存放堆栈": bioyond_warehouse_liquid_and_lid_handling("站内试剂存放堆栈"),
|
||||
}
|
||||
self.warehouse_locations = {
|
||||
"堆栈1": Coordinate(0.0, 430.0, 0.0),
|
||||
"堆栈2": Coordinate(2550.0, 430.0, 0.0),
|
||||
"站内试剂存放堆栈": Coordinate(800.0, 475.0, 0.0),
|
||||
}
|
||||
self.warehouses["站内试剂存放堆栈"].rotation = Rotation(z=90)
|
||||
|
||||
for warehouse_name, warehouse in self.warehouses.items():
|
||||
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
|
||||
|
||||
|
||||
class BIOYOND_PolymerPreparationStation_Deck(Deck):
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "PolymerPreparationStation_Deck",
|
||||
size_x: float = 2700.0,
|
||||
size_y: float = 1080.0,
|
||||
size_z: float = 1500.0,
|
||||
category: str = "deck",
|
||||
setup: bool = False
|
||||
) -> None:
|
||||
super().__init__(name=name, size_x=2700.0, size_y=1080.0, size_z=1500.0)
|
||||
if setup:
|
||||
self.setup()
|
||||
|
||||
def setup(self) -> None:
|
||||
# 添加仓库
|
||||
self.warehouses = {
|
||||
"io_warehouse_left": bioyond_warehouse_1x4x4("io_warehouse_left"),
|
||||
"io_warehouse_right": bioyond_warehouse_1x4x4("io_warehouse_right"),
|
||||
"solutions": bioyond_warehouse_1x4x2("warehouse_solutions"),
|
||||
"liquid_and_lid_handling": bioyond_warehouse_liquid_and_lid_handling("warehouse_liquid_and_lid_handling"),
|
||||
}
|
||||
self.warehouse_locations = {
|
||||
"io_warehouse_left": Coordinate(0.0, 650.0, 0.0),
|
||||
"io_warehouse_right": Coordinate(2550.0, 650.0, 0.0),
|
||||
"solutions": Coordinate(1915.0, 900.0, 0.0),
|
||||
"liquid_and_lid_handling": Coordinate(1330.0, 490.0, 0.0),
|
||||
}
|
||||
|
||||
for warehouse_name, warehouse in self.warehouses.items():
|
||||
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
|
||||
54
unilabos/resources/bioyond/warehouses.py
Normal file
54
unilabos/resources/bioyond/warehouses.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from unilabos.resources.warehouse import WareHouse, warehouse_factory
|
||||
|
||||
|
||||
def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
|
||||
"""创建BioYond 4x1x4仓库"""
|
||||
return warehouse_factory(
|
||||
name=name,
|
||||
num_items_x=1,
|
||||
num_items_y=4,
|
||||
num_items_z=4,
|
||||
dx=10.0,
|
||||
dy=10.0,
|
||||
dz=10.0,
|
||||
item_dx=137.0,
|
||||
item_dy=96.0,
|
||||
item_dz=120.0,
|
||||
category="warehouse",
|
||||
)
|
||||
|
||||
|
||||
def bioyond_warehouse_1x4x2(name: str) -> WareHouse:
|
||||
"""创建BioYond 4x1x2仓库"""
|
||||
return warehouse_factory(
|
||||
name=name,
|
||||
num_items_x=1,
|
||||
num_items_y=4,
|
||||
num_items_z=2,
|
||||
dx=10.0,
|
||||
dy=10.0,
|
||||
dz=10.0,
|
||||
item_dx=137.0,
|
||||
item_dy=96.0,
|
||||
item_dz=120.0,
|
||||
category="warehouse",
|
||||
removed_positions=None
|
||||
)
|
||||
|
||||
|
||||
def bioyond_warehouse_liquid_and_lid_handling(name: str) -> WareHouse:
|
||||
"""创建BioYond开关盖加液模块台面"""
|
||||
return warehouse_factory(
|
||||
name=name,
|
||||
num_items_x=2,
|
||||
num_items_y=5,
|
||||
num_items_z=1,
|
||||
dx=10.0,
|
||||
dy=10.0,
|
||||
dz=10.0,
|
||||
item_dx=137.0,
|
||||
item_dy=96.0,
|
||||
item_dz=120.0,
|
||||
category="warehouse",
|
||||
removed_positions=None
|
||||
)
|
||||
@@ -1,88 +1,125 @@
|
||||
import importlib
|
||||
import inspect
|
||||
import json
|
||||
from typing import Union, Any, Dict
|
||||
import numpy as np
|
||||
import traceback
|
||||
from typing import Union, Any, Dict, List
|
||||
import networkx as nx
|
||||
from pylabrobot.resources import ResourceHolder
|
||||
from unilabos_msgs.msg import Resource
|
||||
|
||||
from unilabos.resources.container import RegularContainer
|
||||
from unilabos.ros.msgs.message_converter import convert_from_ros_msg_with_mapping, convert_to_ros_msg
|
||||
from unilabos.ros.msgs.message_converter import convert_to_ros_msg
|
||||
from unilabos.ros.nodes.resource_tracker import (
|
||||
ResourceDictInstance,
|
||||
ResourceTreeSet,
|
||||
)
|
||||
from unilabos.utils.banner_print import print_status
|
||||
|
||||
try:
|
||||
from pylabrobot.resources.resource import Resource as ResourcePLR
|
||||
except ImportError:
|
||||
pass
|
||||
from typing import Union, get_origin, get_args
|
||||
from typing import get_origin
|
||||
|
||||
physical_setup_graph: nx.Graph = None
|
||||
|
||||
|
||||
def canonicalize_nodes_data(data: dict, parent_relation: dict = {}) -> dict:
|
||||
for node in data.get("nodes", []):
|
||||
def canonicalize_nodes_data(
|
||||
nodes: List[Dict[str, Any]], parent_relation: Dict[str, List[str]] = {}
|
||||
) -> ResourceTreeSet:
|
||||
"""
|
||||
标准化节点数据,使用 ResourceInstanceDictFlatten 进行规范化并创建 ResourceTreeSet
|
||||
|
||||
Args:
|
||||
nodes: 原始节点列表
|
||||
parent_relation: 父子关系映射 {parent_id: [child_id1, child_id2, ...]}
|
||||
|
||||
Returns:
|
||||
ResourceTreeSet: 标准化后的资源树集合
|
||||
"""
|
||||
print_status(f"{len(nodes)} Resources loaded:", "info")
|
||||
|
||||
# 第一步:基本预处理(处理graphml的label字段)
|
||||
for node in nodes:
|
||||
if node.get("label") is not None:
|
||||
id = node.pop("label")
|
||||
node["id"] = node["name"] = id
|
||||
if "id" not in node:
|
||||
node["id"] = node.get("name", "NaN")
|
||||
if "name" not in node:
|
||||
node["name"] = node["id"]
|
||||
if node.get("position") is None:
|
||||
node["position"] = {
|
||||
"x": node.pop("x", 0.0),
|
||||
"y": node.pop("y", 0.0),
|
||||
"z": node.pop("z", 0.0),
|
||||
}
|
||||
if node.get("config") is None:
|
||||
node["config"] = {}
|
||||
node["data"] = {}
|
||||
for k in list(node.keys()):
|
||||
if k not in [
|
||||
"id",
|
||||
"name",
|
||||
"class",
|
||||
"type",
|
||||
"position",
|
||||
"children",
|
||||
"parent",
|
||||
"config",
|
||||
"data",
|
||||
]:
|
||||
if k in ["chemical", "current_volume"]:
|
||||
if node["data"].get("liquids") is None:
|
||||
node["data"]["liquids"] = [{}]
|
||||
if k == "chemical":
|
||||
node["data"]["liquids"][0]["liquid_name"] = node.pop(k)
|
||||
elif k == "current_volume":
|
||||
node["data"]["liquids"][0]["liquid_volume"] = node.pop(k)
|
||||
elif k == "max_volume":
|
||||
node["data"]["max_volume"] = node.pop(k)
|
||||
elif k == "url":
|
||||
node.pop(k)
|
||||
else:
|
||||
node["config"][k] = node.pop(k)
|
||||
if "class" not in node:
|
||||
node["class"] = None
|
||||
if "type" not in node:
|
||||
node["type"] = (
|
||||
"container"
|
||||
if node["class"] is None
|
||||
else "device" if node["class"] not in ["container", "plate"] else node["class"]
|
||||
)
|
||||
if "children" not in node:
|
||||
node["children"] = []
|
||||
node_id = node.pop("label")
|
||||
node["id"] = node["name"] = node_id
|
||||
|
||||
id2idx = {node_data["id"]: idx for idx, node_data in enumerate(data["nodes"])}
|
||||
# 第二步:处理parent_relation
|
||||
id2idx = {node["id"]: idx for idx, node in enumerate(nodes)}
|
||||
for parent, children in parent_relation.items():
|
||||
data["nodes"][id2idx[parent]]["children"] = children
|
||||
for child in children:
|
||||
data["nodes"][id2idx[child]]["parent"] = parent
|
||||
return data
|
||||
if parent in id2idx:
|
||||
nodes[id2idx[parent]]["children"] = children
|
||||
for child in children:
|
||||
if child in id2idx:
|
||||
nodes[id2idx[child]]["parent"] = parent
|
||||
|
||||
# 第三步:使用 ResourceInstanceDictFlatten 标准化每个节点
|
||||
standardized_instances = []
|
||||
known_nodes: Dict[str, ResourceDictInstance] = {} # {node_id: ResourceDictInstance}
|
||||
uuid_to_instance: Dict[str, ResourceDictInstance] = {} # {uuid: ResourceDictInstance}
|
||||
|
||||
for node in nodes:
|
||||
try:
|
||||
print_status(f"DeviceId: {node['id']}, Class: {node['class']}", "info")
|
||||
# 使用标准化方法
|
||||
resource_instance = ResourceDictInstance.get_resource_instance_from_dict(node)
|
||||
known_nodes[node["id"]] = resource_instance
|
||||
uuid_to_instance[resource_instance.res_content.uuid] = resource_instance
|
||||
standardized_instances.append(resource_instance)
|
||||
except Exception as e:
|
||||
print_status(f"Failed to standardize node {node.get('id', 'unknown')}:\n{traceback.format_exc()}", "error")
|
||||
continue
|
||||
|
||||
# 第四步:建立 parent 和 children 关系
|
||||
for node in nodes:
|
||||
node_id = node["id"]
|
||||
if node_id not in known_nodes:
|
||||
continue
|
||||
|
||||
current_instance = known_nodes[node_id]
|
||||
|
||||
# 优先使用 parent_uuid 进行匹配,如果不存在则使用 parent
|
||||
parent_uuid = node.get("parent_uuid")
|
||||
parent_id = node.get("parent")
|
||||
parent_instance = None
|
||||
|
||||
# 优先用 parent_uuid 匹配
|
||||
if parent_uuid and parent_uuid in uuid_to_instance:
|
||||
parent_instance = uuid_to_instance[parent_uuid]
|
||||
# 否则用 parent_id 匹配
|
||||
elif parent_id and parent_id in known_nodes:
|
||||
parent_instance = known_nodes[parent_id]
|
||||
|
||||
# 设置 parent 引用
|
||||
if parent_instance:
|
||||
current_instance.res_content.parent = parent_instance.res_content
|
||||
# 将当前节点添加到父节点的 children 列表
|
||||
parent_instance.children.append(current_instance)
|
||||
|
||||
# 第五步:创建 ResourceTreeSet
|
||||
resource_tree_set = ResourceTreeSet.from_nested_list(standardized_instances)
|
||||
return resource_tree_set
|
||||
|
||||
|
||||
def canonicalize_links_ports(data: dict) -> dict:
|
||||
def canonicalize_links_ports(links: List[Dict[str, Any]], resource_tree_set: ResourceTreeSet) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
标准化边/连接的端口信息
|
||||
|
||||
Args:
|
||||
links: 原始连接列表
|
||||
resource_tree_set: 资源树集合,用于获取节点的UUID信息
|
||||
|
||||
Returns:
|
||||
标准化后的连接列表
|
||||
"""
|
||||
# 构建 id 到 uuid 的映射
|
||||
id_to_uuid: Dict[str, str] = {}
|
||||
for node in resource_tree_set.all_nodes:
|
||||
id_to_uuid[node.res_content.id] = node.res_content.uuid
|
||||
|
||||
# 第一遍处理:将字符串类型的port转换为字典格式
|
||||
for link in data.get("links", []):
|
||||
for link in links:
|
||||
port = link.get("port")
|
||||
if link.get("type", "physical") == "physical":
|
||||
link["type"] = "fluid"
|
||||
@@ -106,11 +143,11 @@ def canonicalize_links_ports(data: dict) -> dict:
|
||||
link["port"] = {link["source"]: None, link["target"]: None}
|
||||
|
||||
# 构建边字典,键为(source节点, target节点),值为对应的port信息
|
||||
edges = {(link["source"], link["target"]): link["port"] for link in data.get("links", [])}
|
||||
edges = {(link["source"], link["target"]): link["port"] for link in links}
|
||||
|
||||
# 第二遍处理:填充反向边的dest信息
|
||||
delete_reverses = []
|
||||
for i, link in enumerate(data.get("links", [])):
|
||||
for i, link in enumerate(links):
|
||||
s, t = link["source"], link["target"]
|
||||
current_port = link["port"]
|
||||
if current_port.get(t) is None:
|
||||
@@ -126,9 +163,22 @@ def canonicalize_links_ports(data: dict) -> dict:
|
||||
# 若不存在反向边,初始化为空结构
|
||||
current_port[t] = current_port[s]
|
||||
# 删除已被使用反向端口信息的反向边
|
||||
data["links"] = [link for i, link in enumerate(data.get("links", [])) if i not in delete_reverses]
|
||||
standardized_links = [link for i, link in enumerate(links) if i not in delete_reverses]
|
||||
|
||||
return data
|
||||
# 第三遍处理:为每个 link 添加 source_uuid 和 target_uuid
|
||||
for link in standardized_links:
|
||||
source_id = link.get("source")
|
||||
target_id = link.get("target")
|
||||
|
||||
# 添加 source_uuid
|
||||
if source_id and source_id in id_to_uuid:
|
||||
link["source_uuid"] = id_to_uuid[source_id]
|
||||
|
||||
# 添加 target_uuid
|
||||
if target_id and target_id in id_to_uuid:
|
||||
link["target_uuid"] = id_to_uuid[target_id]
|
||||
|
||||
return standardized_links
|
||||
|
||||
|
||||
def handle_communications(G: nx.Graph):
|
||||
@@ -150,18 +200,43 @@ def handle_communications(G: nx.Graph):
|
||||
G.nodes[device]["config"]["io_device_port"] = int(edata["port"][device_comm])
|
||||
|
||||
|
||||
def read_node_link_json(json_info: Union[str, Dict[str, Any]]) -> tuple[nx.Graph, dict]:
|
||||
def read_node_link_json(
|
||||
json_info: Union[str, Dict[str, Any]],
|
||||
) -> tuple[nx.Graph, ResourceTreeSet, List[Dict[str, Any]]]:
|
||||
"""
|
||||
读取节点-边的JSON数据并构建图
|
||||
|
||||
Args:
|
||||
json_info: JSON文件路径或字典数据
|
||||
|
||||
Returns:
|
||||
tuple[nx.Graph, ResourceTreeSet, List[Dict[str, Any]]]:
|
||||
返回NetworkX图对象、资源树集合和标准化后的连接列表
|
||||
"""
|
||||
global physical_setup_graph
|
||||
if isinstance(json_info, str):
|
||||
data = json.load(open(json_info, encoding="utf-8"))
|
||||
else:
|
||||
data = json_info
|
||||
data = canonicalize_nodes_data(data)
|
||||
data = canonicalize_links_ports(data)
|
||||
|
||||
physical_setup_graph = nx.node_link_graph(data, multigraph=False) # edges="links" 3.6 warning
|
||||
# 标准化节点数据并创建 ResourceTreeSet
|
||||
nodes = data.get("nodes", [])
|
||||
resource_tree_set = canonicalize_nodes_data(nodes)
|
||||
|
||||
# 标准化边数据
|
||||
links = data.get("links", [])
|
||||
standardized_links = canonicalize_links_ports(links, resource_tree_set)
|
||||
|
||||
# 构建 NetworkX 图(需要转换回 dict 格式)
|
||||
# 从 ResourceTreeSet 获取所有节点
|
||||
graph_data = {
|
||||
"nodes": [node.res_content.model_dump(by_alias=True) for node in resource_tree_set.all_nodes],
|
||||
"links": standardized_links,
|
||||
}
|
||||
physical_setup_graph = nx.node_link_graph(graph_data, edges="links", multigraph=False)
|
||||
handle_communications(physical_setup_graph)
|
||||
return physical_setup_graph, data
|
||||
|
||||
return physical_setup_graph, resource_tree_set, standardized_links
|
||||
|
||||
|
||||
def modify_to_backend_format(data: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
@@ -184,7 +259,17 @@ def modify_to_backend_format(data: list[dict[str, Any]]) -> list[dict[str, Any]]
|
||||
return data
|
||||
|
||||
|
||||
def read_graphml(graphml_file):
|
||||
def read_graphml(graphml_file: str) -> tuple[nx.Graph, ResourceTreeSet, List[Dict[str, Any]]]:
|
||||
"""
|
||||
读取GraphML文件并构建图
|
||||
|
||||
Args:
|
||||
graphml_file: GraphML文件路径
|
||||
|
||||
Returns:
|
||||
tuple[nx.Graph, ResourceTreeSet, List[Dict[str, Any]]]:
|
||||
返回NetworkX图对象、资源树集合和标准化后的连接列表
|
||||
"""
|
||||
global physical_setup_graph
|
||||
|
||||
G = nx.read_graphml(graphml_file)
|
||||
@@ -201,12 +286,25 @@ def read_graphml(graphml_file):
|
||||
|
||||
G2 = nx.relabel_nodes(G, mapping)
|
||||
data = nx.node_link_data(G2)
|
||||
data = canonicalize_nodes_data(data, parent_relation=parent_relation)
|
||||
data = canonicalize_links_ports(data)
|
||||
|
||||
physical_setup_graph = nx.node_link_graph(data, edges="links", multigraph=False) # edges="links" 3.6 warning
|
||||
# 标准化节点数据并创建 ResourceTreeSet
|
||||
nodes = data.get("nodes", [])
|
||||
resource_tree_set = canonicalize_nodes_data(nodes, parent_relation=parent_relation)
|
||||
|
||||
# 标准化边数据
|
||||
links = data.get("links", [])
|
||||
standardized_links = canonicalize_links_ports(links, resource_tree_set)
|
||||
|
||||
# 构建 NetworkX 图(需要转换回 dict 格式)
|
||||
# 从 ResourceTreeSet 获取所有节点
|
||||
graph_data = {
|
||||
"nodes": [node.res_content.model_dump(by_alias=True) for node in resource_tree_set.all_nodes],
|
||||
"links": standardized_links,
|
||||
}
|
||||
physical_setup_graph = nx.node_link_graph(graph_data, link="links", multigraph=False)
|
||||
handle_communications(physical_setup_graph)
|
||||
return physical_setup_graph, data
|
||||
|
||||
return physical_setup_graph, resource_tree_set, standardized_links
|
||||
|
||||
|
||||
def dict_from_graph(graph: nx.Graph) -> dict:
|
||||
@@ -218,15 +316,17 @@ def dict_to_tree(nodes: dict, devices_only: bool = False) -> list[dict]:
|
||||
# 将节点转换为字典,以便通过 ID 快速查找
|
||||
nodes_list = [node for node in nodes.values() if node.get("type") == "device" or not devices_only]
|
||||
id_list = [node["id"] for node in nodes_list]
|
||||
is_root = {node["id"]: True for node in nodes_list}
|
||||
|
||||
# 初始化每个节点的 children 为包含节点字典的列表
|
||||
for node in nodes_list:
|
||||
node["children"] = [nodes[child_id] for child_id in node.get("children", [])]
|
||||
for child_id in node.get("children", []):
|
||||
if child_id in is_root:
|
||||
is_root[child_id] = False
|
||||
|
||||
# 找到根节点并返回
|
||||
root_nodes = [
|
||||
node for node in nodes_list if len(nodes_list) == 1 or node.get("parent", node.get("parent_name")) in [None, "", "None", np.nan] or node.get("parent", node.get("parent_name")) not in id_list
|
||||
]
|
||||
root_nodes = [node for node in nodes_list if is_root.get(node["id"], False) or len(nodes_list) == 1]
|
||||
|
||||
# 如果存在多个根节点,返回所有根节点
|
||||
return root_nodes
|
||||
@@ -235,6 +335,7 @@ def dict_to_tree(nodes: dict, devices_only: bool = False) -> list[dict]:
|
||||
def dict_to_nested_dict(nodes: dict, devices_only: bool = False) -> dict:
|
||||
# 将节点转换为字典,以便通过 ID 快速查找
|
||||
nodes_list = [node for node in nodes.values() if node.get("type") == "device" or not devices_only]
|
||||
is_root = {node["id"]: True for node in nodes_list}
|
||||
|
||||
# 初始化每个节点的 children 为包含节点字典的列表
|
||||
for node in nodes_list:
|
||||
@@ -243,15 +344,14 @@ def dict_to_nested_dict(nodes: dict, devices_only: bool = False) -> dict:
|
||||
for child_id in node.get("children", [])
|
||||
if nodes[child_id].get("type") == "device" or not devices_only
|
||||
}
|
||||
if len(node["children"]) > 0 and node["type"].lower() == "device" and devices_only:
|
||||
for child_id in node.get("children", []):
|
||||
if child_id in is_root:
|
||||
is_root[child_id] = False
|
||||
if len(node["children"]) > 0 and node["type"].lower() == "device":
|
||||
node["config"]["children"] = node["children"]
|
||||
|
||||
# 找到根节点并返回
|
||||
root_nodes = {
|
||||
node["id"]: node
|
||||
for node in nodes_list
|
||||
if node.get("parent", node.get("parent_name")) in [None, "", "None", np.nan] or len(nodes_list) == 1
|
||||
}
|
||||
root_nodes = {node["id"]: node for node in nodes_list if is_root.get(node["id"], False) or len(nodes_list) == 1}
|
||||
|
||||
# 如果存在多个根节点,返回所有根节点
|
||||
return root_nodes
|
||||
@@ -326,8 +426,9 @@ def nested_dict_to_list(nested_dict: dict) -> list[dict]: # FIXME 是tree?
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def convert_resources_to_type(
|
||||
resources_list: list[dict], resource_type: type, *, plr_model: bool = False
|
||||
resources_list: list[dict], resource_type: Union[type, list[type]], *, plr_model: bool = False
|
||||
) -> Union[list[dict], dict, None, "ResourcePLR"]:
|
||||
"""
|
||||
Convert resources to a given type (PyLabRobot or NestedDict) from flattened list of dictionaries.
|
||||
@@ -358,7 +459,9 @@ def convert_resources_to_type(
|
||||
return None
|
||||
|
||||
|
||||
def convert_resources_from_type(resources_list, resource_type: type) -> Union[list[dict], dict, None, "ResourcePLR"]:
|
||||
def convert_resources_from_type(
|
||||
resources_list, resource_type: Union[type, list[type]], *, is_plr: bool = False
|
||||
) -> Union[list[dict], dict, None, "ResourcePLR"]:
|
||||
"""
|
||||
Convert resources from a given type (PyLabRobot or NestedDict) to flattened list of dictionaries.
|
||||
|
||||
@@ -374,11 +477,11 @@ def convert_resources_from_type(resources_list, resource_type: type) -> Union[li
|
||||
elif isinstance(resource_type, type) and issubclass(resource_type, ResourcePLR):
|
||||
resources_tree = [resource_plr_to_ulab(resources_list)]
|
||||
return tree_to_list(resources_tree)
|
||||
elif isinstance(resource_type, list) :
|
||||
elif isinstance(resource_type, list):
|
||||
if all((get_origin(t) is Union) for t in resource_type):
|
||||
resources_tree = [resource_plr_to_ulab(r) for r in resources_list]
|
||||
return tree_to_list(resources_tree)
|
||||
elif all(issubclass(t, ResourcePLR) for t in resource_type):
|
||||
elif is_plr or all(issubclass(t, ResourcePLR) for t in resource_type):
|
||||
resources_tree = [resource_plr_to_ulab(r) for r in resources_list]
|
||||
return tree_to_list(resources_tree)
|
||||
else:
|
||||
@@ -421,6 +524,7 @@ def resource_ulab_to_plr(resource: dict, plr_model=False) -> "ResourcePLR":
|
||||
d = resource_ulab_to_plr_inner(resource)
|
||||
"""无法通过Resource进行反序列化,例如TipSpot必须内部序列化好,直接用TipSpot序列化会多参数,导致出错"""
|
||||
from pylabrobot.utils.object_parsing import find_subclass
|
||||
|
||||
sub_cls = find_subclass(d["type"], ResourcePLR)
|
||||
spect = inspect.signature(sub_cls)
|
||||
if "category" not in spect.parameters:
|
||||
@@ -430,7 +534,7 @@ def resource_ulab_to_plr(resource: dict, plr_model=False) -> "ResourcePLR":
|
||||
return resource_plr
|
||||
|
||||
|
||||
def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None):
|
||||
def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None, with_children=True):
|
||||
def replace_plr_type_to_ulab(source: str):
|
||||
replace_info = {
|
||||
"plate": "plate",
|
||||
@@ -445,12 +549,13 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None):
|
||||
else:
|
||||
print("转换pylabrobot的时候,出现未知类型", source)
|
||||
return "container"
|
||||
def resource_plr_to_ulab_inner(d: dict, all_states: dict) -> dict:
|
||||
|
||||
def resource_plr_to_ulab_inner(d: dict, all_states: dict, child=True) -> dict:
|
||||
r = {
|
||||
"id": d["name"],
|
||||
"name": d["name"],
|
||||
"sample_id": None,
|
||||
"children": [resource_plr_to_ulab_inner(child, all_states) for child in d["children"]],
|
||||
"children": [resource_plr_to_ulab_inner(child, all_states) for child in d["children"]] if child else [],
|
||||
"parent": d["parent_name"] if d["parent_name"] else parent_name if parent_name else None,
|
||||
"type": replace_plr_type_to_ulab(d.get("category")), # FIXME plr自带的type是python class name
|
||||
"class": d.get("class", ""),
|
||||
@@ -463,14 +568,114 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None):
|
||||
"data": all_states[d["name"]],
|
||||
}
|
||||
return r
|
||||
|
||||
d = resource_plr.serialize()
|
||||
all_states = resource_plr.serialize_all_state()
|
||||
r = resource_plr_to_ulab_inner(d, all_states)
|
||||
r = resource_plr_to_ulab_inner(d, all_states, with_children)
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def initialize_resource(resource_config: dict) -> list[dict]:
|
||||
def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict = {}, deck: Any = None) -> list[dict]:
|
||||
"""
|
||||
将 bioyond 物料格式转换为 ulab 物料格式
|
||||
|
||||
Args:
|
||||
bioyond_materials: bioyond 系统的物料查询结果列表
|
||||
type_mapping: 物料类型映射字典,格式 {bioyond_type: plr_class_name}
|
||||
location_id_mapping: 库位 ID 到名称的映射字典,格式 {location_id: location_name}
|
||||
|
||||
Returns:
|
||||
pylabrobot 格式的物料列表
|
||||
"""
|
||||
plr_materials = []
|
||||
|
||||
for material in bioyond_materials:
|
||||
className = (
|
||||
type_mapping.get(material.get("typeName"), "RegularContainer") if type_mapping else "RegularContainer"
|
||||
)
|
||||
|
||||
plr_material: ResourcePLR = initialize_resource(
|
||||
{"name": material["name"], "class": className}, resource_type=ResourcePLR
|
||||
)
|
||||
plr_material.code = material.get("code", "") and material.get("barCode", "") or ""
|
||||
|
||||
# 处理子物料(detail)
|
||||
if material.get("detail") and len(material["detail"]) > 0:
|
||||
child_ids = []
|
||||
for detail in material["detail"]:
|
||||
number = (
|
||||
(detail.get("z", 0) - 1) * plr_material.num_items_x * plr_material.num_items_y
|
||||
+ (detail.get("x", 0) - 1) * plr_material.num_items_x
|
||||
+ (detail.get("y", 0) - 1)
|
||||
)
|
||||
bottle = plr_material[number]
|
||||
if detail["name"] in type_mapping:
|
||||
# plr_material.unassign_child_resource(bottle)
|
||||
plr_material.sites[number] = None
|
||||
plr_material[number] = initialize_resource(
|
||||
{"name": f'{detail["name"]}_{number}', "class": type_mapping[detail["name"]]}, resource_type=ResourcePLR
|
||||
)
|
||||
else:
|
||||
bottle.tracker.liquids = [
|
||||
(detail["name"], float(detail.get("quantity", 0)) if detail.get("quantity") else 0)
|
||||
]
|
||||
bottle.code = detail.get("code", "")
|
||||
else:
|
||||
bottle = plr_material[0] if plr_material.capacity > 0 else plr_material
|
||||
bottle.tracker.liquids = [
|
||||
(material["name"], float(material.get("quantity", 0)) if material.get("quantity") else 0)
|
||||
]
|
||||
|
||||
plr_materials.append(plr_material)
|
||||
|
||||
if deck and hasattr(deck, "warehouses"):
|
||||
for loc in material.get("locations", []):
|
||||
if hasattr(deck, "warehouses") and loc.get("whName") in deck.warehouses:
|
||||
warehouse = deck.warehouses[loc["whName"]]
|
||||
idx = (
|
||||
(loc.get("y", 0) - 1) * warehouse.num_items_x * warehouse.num_items_y
|
||||
+ (loc.get("x", 0) - 1) * warehouse.num_items_x
|
||||
+ (loc.get("z", 0) - 1)
|
||||
)
|
||||
if 0 <= idx < warehouse.capacity:
|
||||
if warehouse[idx] is None or isinstance(warehouse[idx], ResourceHolder):
|
||||
warehouse[idx] = plr_material
|
||||
|
||||
return plr_materials
|
||||
|
||||
|
||||
def resource_plr_to_bioyond(plr_materials: list[ResourcePLR], type_mapping: dict = {}, warehouse_mapping: dict = {}) -> list[dict]:
|
||||
bioyond_materials = []
|
||||
for plr_material in plr_materials:
|
||||
material = {
|
||||
"name": plr_material.name,
|
||||
"typeName": plr_material.__class__.__name__,
|
||||
"code": plr_material.code,
|
||||
"quantity": 0,
|
||||
"detail": [],
|
||||
"locations": [],
|
||||
}
|
||||
if hasattr(plr_material, "capacity") and plr_material.capacity > 1:
|
||||
for idx in range(plr_material.capacity):
|
||||
bottle = plr_material[idx]
|
||||
detail = {
|
||||
"x": (idx // (plr_material.num_items_x * plr_material.num_items_y)) + 1,
|
||||
"y": ((idx % (plr_material.num_items_x * plr_material.num_items_y)) // plr_material.num_items_x) + 1,
|
||||
"z": (idx % plr_material.num_items_x) + 1,
|
||||
"code": bottle.code if hasattr(bottle, "code") else "",
|
||||
"quantity": sum(qty for _, qty in bottle.tracker.liquids) if hasattr(bottle, "tracker") else 0,
|
||||
}
|
||||
material["detail"].append(detail)
|
||||
material["quantity"] = 1.0
|
||||
else:
|
||||
bottle = plr_material[0] if plr_material.capacity > 0 else plr_material
|
||||
material["quantity"] = sum(qty for _, qty in bottle.tracker.liquids) if hasattr(plr_material, "tracker") else 0
|
||||
bioyond_materials.append(material)
|
||||
return bioyond_materials
|
||||
|
||||
|
||||
def initialize_resource(resource_config: dict, resource_type: Any = None) -> Union[list[dict], ResourcePLR]:
|
||||
"""Initializes a resource based on its configuration.
|
||||
|
||||
If the config is detailed, then do nothing;
|
||||
@@ -483,6 +688,7 @@ def initialize_resource(resource_config: dict) -> list[dict]:
|
||||
None
|
||||
"""
|
||||
from unilabos.registry.registry import lab_registry
|
||||
|
||||
resource_class_config = resource_config.get("class", None)
|
||||
if resource_class_config is None:
|
||||
return [resource_config]
|
||||
@@ -502,14 +708,19 @@ def initialize_resource(resource_config: dict) -> list[dict]:
|
||||
|
||||
if resource_class_config["type"] == "pylabrobot":
|
||||
resource_plr = RESOURCE(name=resource_config["name"])
|
||||
r = resource_plr_to_ulab(resource_plr=resource_plr, parent_name=resource_config.get("parent", None))
|
||||
# r = resource_plr_to_ulab(resource_plr=resource_plr)
|
||||
if resource_config.get("position") is not None:
|
||||
r["position"] = resource_config["position"]
|
||||
r = tree_to_list([r])
|
||||
if resource_type != ResourcePLR:
|
||||
r = resource_plr_to_ulab(resource_plr=resource_plr, parent_name=resource_config.get("parent", None))
|
||||
# r = resource_plr_to_ulab(resource_plr=resource_plr)
|
||||
if resource_config.get("position") is not None:
|
||||
r["position"] = resource_config["position"]
|
||||
r = tree_to_list([r])
|
||||
else:
|
||||
r = resource_plr
|
||||
elif resource_class_config["type"] == "unilabos":
|
||||
res_instance: RegularContainer = RESOURCE(id=resource_config["name"])
|
||||
res_instance.ulr_resource = convert_to_ros_msg(Resource, {k:v for k,v in resource_config.items() if k != "class"})
|
||||
res_instance.ulr_resource = convert_to_ros_msg(
|
||||
Resource, {k: v for k, v in resource_config.items() if k != "class"}
|
||||
)
|
||||
r = [res_instance.get_ulr_resource_as_dict()]
|
||||
elif isinstance(RESOURCE, dict):
|
||||
r = [RESOURCE.copy()]
|
||||
|
||||
356
unilabos/resources/itemized_carrier.py
Normal file
356
unilabos/resources/itemized_carrier.py
Normal file
@@ -0,0 +1,356 @@
|
||||
"""
|
||||
自动化液体处理工作站物料类定义 - 简化版
|
||||
Automated Liquid Handling Station Resource Classes - Simplified Version
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, List, Optional, TypeVar, Union, Sequence, Tuple
|
||||
|
||||
import pylabrobot
|
||||
|
||||
from pylabrobot.resources import Resource as ResourcePLR
|
||||
from pylabrobot.resources import Well, ResourceHolder
|
||||
from pylabrobot.resources.coordinate import Coordinate
|
||||
|
||||
|
||||
LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
|
||||
class Bottle(Well):
|
||||
"""瓶子类 - 简化版,不追踪瓶盖"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
diameter: float,
|
||||
height: float,
|
||||
max_volume: float,
|
||||
size_x: float = 0.0,
|
||||
size_y: float = 0.0,
|
||||
size_z: float = 0.0,
|
||||
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,
|
||||
bottom_type="flat",
|
||||
cross_section_type="circle"
|
||||
)
|
||||
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,
|
||||
}
|
||||
|
||||
T = TypeVar("T", bound=ResourceHolder)
|
||||
|
||||
S = TypeVar("S", bound=ResourceHolder)
|
||||
|
||||
|
||||
class ItemizedCarrier(ResourcePLR):
|
||||
"""Base class for all carriers."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
size_x: float,
|
||||
size_y: float,
|
||||
size_z: float,
|
||||
num_items_x: int = 0,
|
||||
num_items_y: int = 0,
|
||||
num_items_z: int = 0,
|
||||
sites: Optional[Dict[Union[int, str], Optional[ResourcePLR]]] = None,
|
||||
category: Optional[str] = "carrier",
|
||||
model: Optional[str] = None,
|
||||
):
|
||||
super().__init__(
|
||||
name=name,
|
||||
size_x=size_x,
|
||||
size_y=size_y,
|
||||
size_z=size_z,
|
||||
category=category,
|
||||
model=model,
|
||||
)
|
||||
self.num_items = len(sites)
|
||||
self.num_items_x, self.num_items_y, self.num_items_z = num_items_x, num_items_y, num_items_z
|
||||
if isinstance(sites, dict):
|
||||
sites = sites or {}
|
||||
self.sites: List[Optional[ResourcePLR]] = list(sites.values())
|
||||
self._ordering = sites
|
||||
self.child_locations: Dict[str, Coordinate] = {}
|
||||
self.child_size: Dict[str, dict] = {}
|
||||
for spot, resource in sites.items():
|
||||
if resource is not None and getattr(resource, "location", None) is None:
|
||||
raise ValueError(f"resource {resource} has no location")
|
||||
if resource is not None:
|
||||
self.child_locations[spot] = resource.location
|
||||
self.child_size[spot] = {"width": resource._size_x, "height": resource._size_y, "depth": resource._size_z}
|
||||
else:
|
||||
self.child_locations[spot] = Coordinate.zero()
|
||||
self.child_size[spot] = {"width": 0, "height": 0, "depth": 0}
|
||||
elif isinstance(sites, list):
|
||||
# deserialize时走这里;还需要根据 self.sites 索引children
|
||||
self.child_locations = {site["label"]: Coordinate(**site["position"]) for site in sites}
|
||||
self.child_size = {site["label"]: site["size"] for site in sites}
|
||||
self.sites = [site["occupied_by"] for site in sites]
|
||||
self._ordering = {site["label"]: site["position"] for site in sites}
|
||||
else:
|
||||
print("sites:", sites)
|
||||
|
||||
@property
|
||||
def capacity(self):
|
||||
"""The number of sites on this carrier."""
|
||||
return len(self.sites)
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Return the number of sites on this carrier."""
|
||||
return len(self.sites)
|
||||
|
||||
def assign_child_resource(
|
||||
self,
|
||||
resource: ResourcePLR,
|
||||
location: Optional[Coordinate],
|
||||
reassign: bool = True,
|
||||
spot: Optional[int] = None,
|
||||
):
|
||||
idx = spot
|
||||
# 如果只给 location,根据坐标和 deserialize 后的 self.sites(持有names)来寻找 resource 该摆放的位置
|
||||
if spot is not None:
|
||||
idx = spot
|
||||
else:
|
||||
for i, site in enumerate(self.sites):
|
||||
site_location = list(self.child_locations.values())[i]
|
||||
if type(site) == str and site == resource.name:
|
||||
idx = i
|
||||
break
|
||||
if site_location == location:
|
||||
idx = i
|
||||
break
|
||||
|
||||
if not reassign and self.sites[idx] is not None:
|
||||
raise ValueError(f"a site with index {idx} already exists")
|
||||
super().assign_child_resource(resource, location=location, reassign=reassign)
|
||||
self.sites[idx] = resource
|
||||
|
||||
def assign_resource_to_site(self, resource: ResourcePLR, spot: int):
|
||||
if self.sites[spot] is not None and not isinstance(self.sites[spot], ResourceHolder):
|
||||
raise ValueError(f"spot {spot} already has a resource, {resource}")
|
||||
self.assign_child_resource(resource, location=self.child_locations.get(str(spot)), spot=spot)
|
||||
|
||||
def unassign_child_resource(self, resource: ResourcePLR):
|
||||
found = False
|
||||
for spot, res in enumerate(self.sites):
|
||||
if res == resource:
|
||||
self.sites[spot] = None
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
raise ValueError(f"Resource {resource} is not assigned to this carrier")
|
||||
if hasattr(resource, "unassign"):
|
||||
resource.unassign()
|
||||
|
||||
def __getitem__(
|
||||
self,
|
||||
identifier: Union[str, int, Sequence[int], Sequence[str], slice, range],
|
||||
) -> Union[List[T], T]:
|
||||
"""Get the items with the given identifier.
|
||||
|
||||
This is a convenience method for getting the items with the given identifier. It is equivalent
|
||||
to :meth:`get_items`, but adds support for slicing and supports single items in the same
|
||||
functional call. Note that the return type will always be a list, even if a single item is
|
||||
requested.
|
||||
|
||||
Examples:
|
||||
Getting the items with identifiers "A1" through "E1":
|
||||
|
||||
>>> items["A1:E1"]
|
||||
|
||||
[<Item A1>, <Item B1>, <Item C1>, <Item D1>, <Item E1>]
|
||||
|
||||
Getting the items with identifiers 0 through 4 (note that this is the same as above):
|
||||
|
||||
>>> items[range(5)]
|
||||
|
||||
[<Item A1>, <Item B1>, <Item C1>, <Item D1>, <Item E1>]
|
||||
|
||||
Getting items with a slice (note that this is the same as above):
|
||||
|
||||
>>> items[0:5]
|
||||
|
||||
[<Item A1>, <Item B1>, <Item C1>, <Item D1>, <Item E1>]
|
||||
|
||||
Getting a single item:
|
||||
|
||||
>>> items[0]
|
||||
|
||||
[<Item A1>]
|
||||
"""
|
||||
|
||||
if isinstance(identifier, str):
|
||||
if ":" in identifier: # multiple # TODO: deprecate this, use `"A1":"E1"` instead (slice)
|
||||
return self.get_items(identifier)
|
||||
|
||||
return self.get_item(identifier) # single
|
||||
|
||||
if isinstance(identifier, int):
|
||||
return self.get_item(identifier)
|
||||
|
||||
if isinstance(identifier, (slice, range)):
|
||||
start, stop = identifier.start, identifier.stop
|
||||
if isinstance(identifier.start, str):
|
||||
start = list(self._ordering.keys()).index(identifier.start)
|
||||
elif identifier.start is None:
|
||||
start = 0
|
||||
if isinstance(identifier.stop, str):
|
||||
stop = list(self._ordering.keys()).index(identifier.stop)
|
||||
elif identifier.stop is None:
|
||||
stop = self.num_items
|
||||
identifier = list(range(start, stop, identifier.step or 1))
|
||||
return self.get_items(identifier)
|
||||
|
||||
if isinstance(identifier, (list, tuple)):
|
||||
return self.get_items(identifier)
|
||||
|
||||
raise TypeError(f"Invalid identifier type: {type(identifier)}")
|
||||
|
||||
def get_item(self, identifier: Union[str, int, Tuple[int, int]]) -> T:
|
||||
"""Get the item with the given identifier.
|
||||
|
||||
Args:
|
||||
identifier: The identifier of the item. Either a string, an integer, or a tuple. If an
|
||||
integer, it is the index of the item in the list of items (counted from 0, top to bottom, left
|
||||
to right). If a string, it uses transposed MS Excel style notation, e.g. "A1" for the first
|
||||
item, "B1" for the item below that, etc. If a tuple, it is (row, column).
|
||||
|
||||
Raises:
|
||||
IndexError: If the identifier is out of range. The range is 0 to self.num_items-1 (inclusive).
|
||||
"""
|
||||
|
||||
if isinstance(identifier, tuple):
|
||||
row, column = identifier
|
||||
identifier = LETTERS[row] + str(column + 1) # standard transposed-Excel style notation
|
||||
if isinstance(identifier, str):
|
||||
try:
|
||||
identifier = list(self._ordering.keys()).index(identifier)
|
||||
except ValueError as e:
|
||||
raise IndexError(
|
||||
f"Item with identifier '{identifier}' does not exist on " f"resource '{self.name}'."
|
||||
) from e
|
||||
|
||||
if not 0 <= identifier < self.capacity:
|
||||
raise IndexError(
|
||||
f"Item with identifier '{identifier}' does not exist on " f"resource '{self.name}'."
|
||||
)
|
||||
|
||||
# Cast child to item type. Children will always be `T`, but the type checker doesn't know that.
|
||||
return self.sites[identifier]
|
||||
|
||||
def get_items(self, identifiers: Union[str, Sequence[int], Sequence[str]]) -> List[T]:
|
||||
"""Get the items with the given identifier.
|
||||
|
||||
Args:
|
||||
identifier: Deprecated. Use `identifiers` instead. # TODO(deprecate-ordered-items)
|
||||
identifiers: The identifiers of the items. Either a string range or a list of integers. If a
|
||||
string, it uses transposed MS Excel style notation. Regions of items can be specified using
|
||||
a colon, e.g. "A1:H1" for the first column. If a list of integers, it is the indices of the
|
||||
items in the list of items (counted from 0, top to bottom, left to right).
|
||||
|
||||
Examples:
|
||||
Getting the items with identifiers "A1" through "E1":
|
||||
|
||||
>>> items.get_items("A1:E1")
|
||||
|
||||
[<Item A1>, <Item B1>, <Item C1>, <Item D1>, <Item E1>]
|
||||
|
||||
Getting the items with identifiers 0 through 4:
|
||||
|
||||
>>> items.get_items(range(5))
|
||||
|
||||
[<Item A1>, <Item B1>, <Item C1>, <Item D1>, <Item E1>]
|
||||
"""
|
||||
|
||||
if isinstance(identifiers, str):
|
||||
identifiers = pylabrobot.utils.expand_string_range(identifiers)
|
||||
return [self.get_item(i) for i in identifiers]
|
||||
|
||||
def __setitem__(self, idx: Union[int, str], resource: Optional[ResourcePLR]):
|
||||
"""Assign a resource to this carrier."""
|
||||
if resource is None: # setting to None
|
||||
assigned_resource = self[idx]
|
||||
if assigned_resource is not None:
|
||||
self.unassign_child_resource(assigned_resource)
|
||||
else:
|
||||
idx = list(self._ordering.keys()).index(idx) if isinstance(idx, str) else idx
|
||||
self.assign_resource_to_site(resource, spot=idx)
|
||||
|
||||
def __delitem__(self, idx: int):
|
||||
"""Unassign a resource from this carrier."""
|
||||
assigned_resource = self[idx]
|
||||
if assigned_resource is not None:
|
||||
self.unassign_child_resource(assigned_resource)
|
||||
|
||||
def get_resources(self) -> List[ResourcePLR]:
|
||||
"""Get all resources assigned to this carrier."""
|
||||
return [resource for resource in self.sites.values() if resource is not None]
|
||||
|
||||
def __eq__(self, other):
|
||||
return super().__eq__(other) and self.sites == other.sites
|
||||
|
||||
def get_free_sites(self) -> List[int]:
|
||||
return [spot for spot, resource in self.sites.items() if resource is None]
|
||||
|
||||
def serialize(self):
|
||||
return {
|
||||
**super().serialize(),
|
||||
"num_items_x": self.num_items_x,
|
||||
"num_items_y": self.num_items_y,
|
||||
"num_items_z": self.num_items_z,
|
||||
"sites": [{
|
||||
"label": str(identifier),
|
||||
"visible": True if self[identifier] is not None else False,
|
||||
"occupied_by": self[identifier].name
|
||||
if isinstance(self[identifier], ResourcePLR) and not isinstance(self[identifier], ResourceHolder) else
|
||||
self[identifier] if isinstance(self[identifier], str) else None,
|
||||
"position": {"x": location.x, "y": location.y, "z": location.z},
|
||||
"size": self.child_size[identifier],
|
||||
"content_type": ["bottle", "container", "tube", "bottle_carrier", "tip_rack"]
|
||||
} for identifier, location in self.child_locations.items()]
|
||||
}
|
||||
|
||||
|
||||
class BottleCarrier(ItemizedCarrier):
|
||||
"""瓶载架 - 直接继承自 TubeCarrier"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
size_x: float,
|
||||
size_y: float,
|
||||
size_z: float,
|
||||
sites: Optional[Dict[Union[int, str], 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,
|
||||
)
|
||||
@@ -6,4 +6,6 @@ def register():
|
||||
# noinspection PyUnresolvedReferences
|
||||
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300Container
|
||||
# noinspection PyUnresolvedReferences
|
||||
from unilabos.ros.nodes.presets.workstation import WorkStationContainer
|
||||
from unilabos.devices.workstation.workstation_base import WorkStationContainer
|
||||
|
||||
from unilabos.devices.liquid_handling.rviz_backend import UniLiquidHandlerRvizBackend
|
||||
|
||||
104
unilabos/resources/warehouse.py
Normal file
104
unilabos/resources/warehouse.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from typing import Dict, Optional, List, Union
|
||||
from pylabrobot.resources import Coordinate
|
||||
from pylabrobot.resources.carrier import ResourceHolder, create_homogeneous_resources
|
||||
|
||||
from unilabos.resources.itemized_carrier import ItemizedCarrier, ResourcePLR
|
||||
|
||||
|
||||
def warehouse_factory(
|
||||
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,
|
||||
empty: bool = False,
|
||||
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=locations,
|
||||
resource_size_x=127.0,
|
||||
resource_size_y=86.0,
|
||||
name_prefix=name,
|
||||
)
|
||||
|
||||
return WareHouse(
|
||||
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,
|
||||
num_items_x = num_items_x,
|
||||
num_items_y = num_items_y,
|
||||
num_items_z = num_items_z,
|
||||
# ordered_items=ordered_items,
|
||||
# ordering=ordering,
|
||||
sites=sites,
|
||||
category=category,
|
||||
model=model,
|
||||
)
|
||||
|
||||
|
||||
class WareHouse(ItemizedCarrier):
|
||||
"""堆栈载体类 - 可容纳16个板位的载体(4层x4行x1列)"""
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
size_x: float,
|
||||
size_y: float,
|
||||
size_z: float,
|
||||
num_items_x: int,
|
||||
num_items_y: int,
|
||||
num_items_z: int,
|
||||
sites: Optional[Dict[Union[int, str], Optional[ResourcePLR]]] = None,
|
||||
category: str = "warehouse",
|
||||
model: Optional[str] = None,
|
||||
):
|
||||
|
||||
super().__init__(
|
||||
name=name,
|
||||
size_x=size_x,
|
||||
size_y=size_y,
|
||||
size_z=size_z,
|
||||
# ordered_items=ordered_items,
|
||||
# ordering=ordering,
|
||||
num_items_x=num_items_x,
|
||||
num_items_y=num_items_y,
|
||||
num_items_z=num_items_z,
|
||||
sites=sites,
|
||||
category=category,
|
||||
model=model,
|
||||
)
|
||||
|
||||
def get_site_by_layer_position(self, row: int, col: int, layer: int) -> ResourceHolder:
|
||||
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