Files
Uni-Lab-OS/unilabos/compile/dissolve_protocol.py
Xuwznln 9aeffebde1 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 commit 49354fcf39)

* remove runze multiple software obtainer

(cherry picked from commit 8bcc92a394)

* support multiple backbone

(cherry picked from commit 4771ff2347)

* 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 commit e5aa4d940a)

* 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 commit 172599adcf)

* 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>
2025-10-12 23:34:26 +08:00

965 lines
40 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from functools import partial
import networkx as nx
import re
import logging
from typing import List, Dict, Any, Union
from .utils.vessel_parser import get_vessel
from .utils.logger_util import action_log
from .pump_protocol import generate_pump_protocol_with_rinsing
logger = logging.getLogger(__name__)
def debug_print(message):
"""调试输出"""
logger.info(f"[DISSOLVE] {message}")
# 🆕 创建进度日志动作
create_action_log = partial(action_log, prefix="[DISSOLVE]")
def parse_volume_input(volume_input: Union[str, float]) -> float:
"""
解析体积输入,支持带单位的字符串
Args:
volume_input: 体积输入(如 "10 mL", "?", 10.0
Returns:
float: 体积(毫升)
"""
if isinstance(volume_input, (int, float)):
debug_print(f"📏 体积输入为数值: {volume_input}")
return float(volume_input)
if not volume_input or not str(volume_input).strip():
debug_print(f"⚠️ 体积输入为空返回0.0mL")
return 0.0
volume_str = str(volume_input).lower().strip()
debug_print(f"🔍 解析体积输入: '{volume_str}'")
# 处理未知体积
if volume_str in ['?', 'unknown', 'tbd', 'to be determined']:
default_volume = 50.0 # 默认50mL
debug_print(f"❓ 检测到未知体积,使用默认值: {default_volume}mL 🎯")
return default_volume
# 移除空格并提取数字和单位
volume_clean = re.sub(r'\s+', '', volume_str)
# 匹配数字和单位的正则表达式
match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
if not match:
debug_print(f"❌ 无法解析体积: '{volume_str}'使用默认值50mL")
return 50.0
value = float(match.group(1))
unit = match.group(2) or 'ml' # 默认单位为毫升
# 转换为毫升
if unit in ['l', 'liter']:
volume = value * 1000.0 # L -> mL
debug_print(f"🔄 体积转换: {value}L → {volume}mL")
elif unit in ['μl', 'ul', 'microliter']:
volume = value / 1000.0 # μL -> mL
debug_print(f"🔄 体积转换: {value}μL → {volume}mL")
else: # ml, milliliter 或默认
volume = value # 已经是mL
debug_print(f"✅ 体积已为mL: {volume}mL")
return volume
def parse_mass_input(mass_input: Union[str, float]) -> float:
"""
解析质量输入,支持带单位的字符串
Args:
mass_input: 质量输入(如 "2.9 g", "?", 2.5
Returns:
float: 质量(克)
"""
if isinstance(mass_input, (int, float)):
debug_print(f"⚖️ 质量输入为数值: {mass_input}g")
return float(mass_input)
if not mass_input or not str(mass_input).strip():
debug_print(f"⚠️ 质量输入为空返回0.0g")
return 0.0
mass_str = str(mass_input).lower().strip()
debug_print(f"🔍 解析质量输入: '{mass_str}'")
# 处理未知质量
if mass_str in ['?', 'unknown', 'tbd', 'to be determined']:
default_mass = 1.0 # 默认1g
debug_print(f"❓ 检测到未知质量,使用默认值: {default_mass}g 🎯")
return default_mass
# 移除空格并提取数字和单位
mass_clean = re.sub(r'\s+', '', mass_str)
# 匹配数字和单位的正则表达式
match = re.match(r'([0-9]*\.?[0-9]+)\s*(g|mg|kg|gram|milligram|kilogram)?', mass_clean)
if not match:
debug_print(f"❌ 无法解析质量: '{mass_str}'返回0.0g")
return 0.0
value = float(match.group(1))
unit = match.group(2) or 'g' # 默认单位为克
# 转换为克
if unit in ['mg', 'milligram']:
mass = value / 1000.0 # mg -> g
debug_print(f"🔄 质量转换: {value}mg → {mass}g")
elif unit in ['kg', 'kilogram']:
mass = value * 1000.0 # kg -> g
debug_print(f"🔄 质量转换: {value}kg → {mass}g")
else: # g, gram 或默认
mass = value # 已经是g
debug_print(f"✅ 质量已为g: {mass}g")
return mass
def parse_time_input(time_input: Union[str, float]) -> float:
"""
解析时间输入,支持带单位的字符串
Args:
time_input: 时间输入(如 "30 min", "1 h", "?", 60.0
Returns:
float: 时间(秒)
"""
if isinstance(time_input, (int, float)):
debug_print(f"⏱️ 时间输入为数值: {time_input}")
return float(time_input)
if not time_input or not str(time_input).strip():
debug_print(f"⚠️ 时间输入为空返回0秒")
return 0.0
time_str = str(time_input).lower().strip()
debug_print(f"🔍 解析时间输入: '{time_str}'")
# 处理未知时间
if time_str in ['?', 'unknown', 'tbd']:
default_time = 600.0 # 默认10分钟
debug_print(f"❓ 检测到未知时间,使用默认值: {default_time}s (10分钟) ⏰")
return default_time
# 移除空格并提取数字和单位
time_clean = re.sub(r'\s+', '', time_str)
# 匹配数字和单位的正则表达式
match = re.match(r'([0-9]*\.?[0-9]+)\s*(s|sec|second|min|minute|h|hr|hour|d|day)?', time_clean)
if not match:
debug_print(f"❌ 无法解析时间: '{time_str}'返回0s")
return 0.0
value = float(match.group(1))
unit = match.group(2) or 's' # 默认单位为秒
# 转换为秒
if unit in ['min', 'minute']:
time_sec = value * 60.0 # min -> s
debug_print(f"🔄 时间转换: {value}分钟 → {time_sec}")
elif unit in ['h', 'hr', 'hour']:
time_sec = value * 3600.0 # h -> s
debug_print(f"🔄 时间转换: {value}小时 → {time_sec}")
elif unit in ['d', 'day']:
time_sec = value * 86400.0 # d -> s
debug_print(f"🔄 时间转换: {value}天 → {time_sec}")
else: # s, sec, second 或默认
time_sec = value # 已经是s
debug_print(f"✅ 时间已为秒: {time_sec}")
return time_sec
def parse_temperature_input(temp_input: Union[str, float]) -> float:
"""
解析温度输入,支持带单位的字符串
Args:
temp_input: 温度输入(如 "60 °C", "room temperature", "?", 25.0
Returns:
float: 温度(摄氏度)
"""
if isinstance(temp_input, (int, float)):
debug_print(f"🌡️ 温度输入为数值: {temp_input}°C")
return float(temp_input)
if not temp_input or not str(temp_input).strip():
debug_print(f"⚠️ 温度输入为空使用默认室温25°C")
return 25.0 # 默认室温
temp_str = str(temp_input).lower().strip()
debug_print(f"🔍 解析温度输入: '{temp_str}'")
# 处理特殊温度描述
temp_aliases = {
'room temperature': 25.0,
'rt': 25.0,
'ambient': 25.0,
'cold': 4.0,
'ice': 0.0,
'reflux': 80.0, # 默认回流温度
'?': 25.0,
'unknown': 25.0
}
if temp_str in temp_aliases:
result = temp_aliases[temp_str]
debug_print(f"🏷️ 温度别名解析: '{temp_str}'{result}°C")
return result
# 移除空格并提取数字和单位
temp_clean = re.sub(r'\s+', '', temp_str)
# 匹配数字和单位的正则表达式
match = re.match(r'([0-9]*\.?[0-9]+)\s*(°c|c|celsius|°f|f|fahrenheit|k|kelvin)?', temp_clean)
if not match:
debug_print(f"❌ 无法解析温度: '{temp_str}'使用默认值25°C")
return 25.0
value = float(match.group(1))
unit = match.group(2) or 'c' # 默认单位为摄氏度
# 转换为摄氏度
if unit in ['°f', 'f', 'fahrenheit']:
temp_c = (value - 32) * 5/9 # F -> C
debug_print(f"🔄 温度转换: {value}°F → {temp_c:.1f}°C")
elif unit in ['k', 'kelvin']:
temp_c = value - 273.15 # K -> C
debug_print(f"🔄 温度转换: {value}K → {temp_c:.1f}°C")
else: # °c, c, celsius 或默认
temp_c = value # 已经是C
debug_print(f"✅ 温度已为°C: {temp_c}°C")
return temp_c
def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
"""增强版溶剂容器查找,支持多种匹配模式"""
debug_print(f"🔍 开始查找溶剂 '{solvent}' 的容器...")
# 🔧 方法1直接搜索 data.reagent_name 和 config.reagent
debug_print(f"📋 方法1: 搜索reagent字段...")
for node in G.nodes():
node_data = G.nodes[node].get('data', {})
node_type = G.nodes[node].get('type', '')
config_data = G.nodes[node].get('config', {})
# 只搜索容器类型的节点
if node_type == 'container':
reagent_name = node_data.get('reagent_name', '').lower()
config_reagent = config_data.get('reagent', '').lower()
# 精确匹配
if reagent_name == solvent.lower() or config_reagent == solvent.lower():
debug_print(f"✅ 通过reagent字段精确匹配到容器: {node} 🎯")
return node
# 模糊匹配
if (solvent.lower() in reagent_name and reagent_name) or \
(solvent.lower() in config_reagent and config_reagent):
debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node} 🔍")
return node
# 🔧 方法2常见的容器命名规则
debug_print(f"📋 方法2: 使用命名规则查找...")
solvent_clean = solvent.lower().replace(' ', '_').replace('-', '_')
possible_names = [
solvent_clean,
f"flask_{solvent_clean}",
f"bottle_{solvent_clean}",
f"vessel_{solvent_clean}",
f"{solvent_clean}_flask",
f"{solvent_clean}_bottle",
f"solvent_{solvent_clean}",
f"reagent_{solvent_clean}",
f"reagent_bottle_{solvent_clean}",
f"reagent_bottle_1", # 通用试剂瓶
f"reagent_bottle_2",
f"reagent_bottle_3"
]
debug_print(f"🔍 尝试的容器名称: {possible_names[:5]}... (共{len(possible_names)}个)")
for name in possible_names:
if name in G.nodes():
node_type = G.nodes[name].get('type', '')
if node_type == 'container':
debug_print(f"✅ 通过命名规则找到容器: {name} 📝")
return name
# 🔧 方法3节点名称模糊匹配
debug_print(f"📋 方法3: 节点名称模糊匹配...")
for node_id in G.nodes():
node_data = G.nodes[node_id]
if node_data.get('type') == 'container':
# 检查节点名称是否包含溶剂名称
if solvent_clean in node_id.lower():
debug_print(f"✅ 通过节点名称模糊匹配到容器: {node_id} 🔍")
return node_id
# 检查液体类型匹配
vessel_data = node_data.get('data', {})
liquids = vessel_data.get('liquid', [])
for liquid in liquids:
if isinstance(liquid, dict):
liquid_type = liquid.get('liquid_type') or liquid.get('name', '')
if liquid_type.lower() == solvent.lower():
debug_print(f"✅ 通过液体类型匹配到容器: {node_id} 💧")
return node_id
# 🔧 方法4使用第一个试剂瓶作为备选
debug_print(f"📋 方法4: 查找备选试剂瓶...")
for node_id in G.nodes():
node_data = G.nodes[node_id]
if (node_data.get('type') == 'container' and
('reagent' in node_id.lower() or 'bottle' in node_id.lower() or 'flask' in node_id.lower())):
debug_print(f"⚠️ 未找到专用容器,使用备选试剂瓶: {node_id} 🔄")
return node_id
debug_print(f"❌ 所有方法都失败了,无法找到容器!")
raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器")
def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str:
"""查找连接到指定容器的加热搅拌器"""
debug_print(f"🔍 查找连接到容器 '{vessel}' 的加热搅拌器...")
heatchill_nodes = []
for node in G.nodes():
node_class = G.nodes[node].get('class', '').lower()
if 'heatchill' in node_class:
heatchill_nodes.append(node)
debug_print(f"📋 发现加热搅拌器: {node}")
debug_print(f"📊 共找到 {len(heatchill_nodes)} 个加热搅拌器")
# 查找连接到容器的加热器
for heatchill in heatchill_nodes:
if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill):
debug_print(f"✅ 找到连接的加热搅拌器: {heatchill} 🔗")
return heatchill
# 返回第一个加热器
if heatchill_nodes:
debug_print(f"⚠️ 未找到直接连接的加热搅拌器,使用第一个: {heatchill_nodes[0]} 🔄")
return heatchill_nodes[0]
debug_print(f"❌ 未找到任何加热搅拌器")
return ""
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
"""查找连接到指定容器的搅拌器"""
debug_print(f"🔍 查找连接到容器 '{vessel}' 的搅拌器...")
stirrer_nodes = []
for node in G.nodes():
node_class = G.nodes[node].get('class', '').lower()
if 'stirrer' in node_class:
stirrer_nodes.append(node)
debug_print(f"📋 发现搅拌器: {node}")
debug_print(f"📊 共找到 {len(stirrer_nodes)} 个搅拌器")
# 查找连接到容器的搅拌器
for stirrer in stirrer_nodes:
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
debug_print(f"✅ 找到连接的搅拌器: {stirrer} 🔗")
return stirrer
# 返回第一个搅拌器
if stirrer_nodes:
debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个: {stirrer_nodes[0]} 🔄")
return stirrer_nodes[0]
debug_print(f"❌ 未找到任何搅拌器")
return ""
def find_solid_dispenser(G: nx.DiGraph) -> str:
"""查找固体加样器"""
debug_print(f"🔍 查找固体加样器...")
for node in G.nodes():
node_class = G.nodes[node].get('class', '').lower()
if 'solid_dispenser' in node_class or 'dispenser' in node_class:
debug_print(f"✅ 找到固体加样器: {node} 🥄")
return node
debug_print(f"❌ 未找到固体加样器")
return ""
def generate_dissolve_protocol(
G: nx.DiGraph,
vessel: dict, # 🔧 修改:从字符串改为字典类型
# 🔧 修复按照checklist.md的DissolveProtocol参数
solvent: str = "",
volume: Union[str, float] = 0.0,
amount: str = "",
temp: Union[str, float] = 25.0,
time: Union[str, float] = 0.0,
stir_speed: float = 300.0,
# 🔧 关键修复:添加缺失的参数,防止"unexpected keyword argument"错误
mass: Union[str, float] = 0.0, # 这个参数在action文件中存在必须包含
mol: str = "", # 这个参数在action文件中存在必须包含
reagent: str = "", # 这个参数在action文件中存在必须包含
event: str = "", # 这个参数在action文件中存在必须包含
**kwargs # 🔧 关键接受所有其他参数防止unexpected keyword错误
) -> List[Dict[str, Any]]:
"""
生成溶解操作的协议序列 - 增强版
🔧 修复要点:
1. 修改vessel参数类型为dict并提取vessel_id
2. 添加action文件中的所有参数mass, mol, reagent, event
3. 使用 **kwargs 接受所有额外参数,防止 unexpected keyword argument 错误
4. 支持固体溶解和液体溶解两种模式
5. 添加详细的体积运算逻辑
支持两种溶解模式:
1. 液体溶解:指定 solvent + volume使用pump protocol转移溶剂
2. 固体溶解:指定 mass/mol + reagent使用固体加样器添加固体试剂
支持所有XDL参数和单位
- volume: "10 mL", "?" 或数值
- mass: "2.9 g", "?" 或数值
- temp: "60 °C", "room temperature", "?" 或数值
- time: "30 min", "1 h", "?" 或数值
- mol: "0.12 mol", "16.2 mmol"
"""
# 🔧 核心修改从字典中提取容器ID
vessel_id, vessel_data = get_vessel(vessel)
debug_print("=" * 60)
debug_print("🧪 开始生成溶解协议")
debug_print(f"📋 原始参数:")
debug_print(f" 🥼 vessel: {vessel} (ID: {vessel_id})")
debug_print(f" 💧 solvent: '{solvent}'")
debug_print(f" 📏 volume: {volume} (类型: {type(volume)})")
debug_print(f" ⚖️ mass: {mass} (类型: {type(mass)})")
debug_print(f" 🌡️ temp: {temp} (类型: {type(temp)})")
debug_print(f" ⏱️ time: {time} (类型: {type(time)})")
debug_print(f" 🧪 reagent: '{reagent}'")
debug_print(f" 🧬 mol: '{mol}'")
debug_print(f" 🎯 event: '{event}'")
debug_print(f" 📦 kwargs: {kwargs}") # 显示额外参数
debug_print("=" * 60)
action_sequence = []
# === 参数验证 ===
debug_print("🔍 步骤1: 参数验证...")
action_sequence.append(create_action_log(f"开始溶解操作 - 容器: {vessel_id}", "🎬"))
if not vessel_id:
debug_print("❌ vessel 参数不能为空")
raise ValueError("vessel 参数不能为空")
if vessel_id not in G.nodes():
debug_print(f"❌ 容器 '{vessel_id}' 不存在于系统中")
raise ValueError(f"容器 '{vessel_id}' 不存在于系统中")
debug_print("✅ 基本参数验证通过")
action_sequence.append(create_action_log("参数验证通过", ""))
# 🔧 新增:记录溶解前的容器状态
debug_print("🔍 记录溶解前容器状态...")
original_liquid_volume = 0.0
if "data" in vessel and "liquid_volume" in vessel["data"]:
current_volume = vessel["data"]["liquid_volume"]
if isinstance(current_volume, list) and len(current_volume) > 0:
original_liquid_volume = current_volume[0]
elif isinstance(current_volume, (int, float)):
original_liquid_volume = current_volume
debug_print(f"📊 溶解前液体体积: {original_liquid_volume:.2f}mL")
# === 🔧 关键修复:参数解析 ===
debug_print("🔍 步骤2: 参数解析...")
action_sequence.append(create_action_log("正在解析溶解参数...", "🔍"))
# 解析各种参数为数值
final_volume = parse_volume_input(volume)
final_mass = parse_mass_input(mass)
final_temp = parse_temperature_input(temp)
final_time = parse_time_input(time)
debug_print(f"📊 解析结果:")
debug_print(f" 📏 体积: {final_volume}mL")
debug_print(f" ⚖️ 质量: {final_mass}g")
debug_print(f" 🌡️ 温度: {final_temp}°C")
debug_print(f" ⏱️ 时间: {final_time}s")
debug_print(f" 🧪 试剂: '{reagent}'")
debug_print(f" 🧬 摩尔: '{mol}'")
debug_print(f" 🎯 事件: '{event}'")
# === 判断溶解类型 ===
debug_print("🔍 步骤3: 判断溶解类型...")
action_sequence.append(create_action_log("正在判断溶解类型...", "🔍"))
# 判断是固体溶解还是液体溶解
is_solid_dissolve = (final_mass > 0 or (mol and mol.strip() != "") or (reagent and reagent.strip() != ""))
is_liquid_dissolve = (final_volume > 0 and solvent and solvent.strip() != "")
if not is_solid_dissolve and not is_liquid_dissolve:
# 默认为液体溶解50mL
is_liquid_dissolve = True
final_volume = 50.0
if not solvent:
solvent = "water" # 默认溶剂
debug_print("⚠️ 未明确指定溶解参数默认为50mL水溶解")
dissolve_type = "固体溶解" if is_solid_dissolve else "液体溶解"
dissolve_emoji = "🧂" if is_solid_dissolve else "💧"
debug_print(f"📋 溶解类型: {dissolve_type} {dissolve_emoji}")
action_sequence.append(create_action_log(f"确定溶解类型: {dissolve_type} {dissolve_emoji}", "📋"))
# === 查找设备 ===
debug_print("🔍 步骤4: 查找设备...")
action_sequence.append(create_action_log("正在查找相关设备...", "🔍"))
# 查找加热搅拌器
heatchill_id = find_connected_heatchill(G, vessel_id)
stirrer_id = find_connected_stirrer(G, vessel_id)
# 优先使用加热搅拌器,否则使用独立搅拌器
stir_device_id = heatchill_id or stirrer_id
debug_print(f"📊 设备映射:")
debug_print(f" 🔥 加热器: '{heatchill_id}'")
debug_print(f" 🌪️ 搅拌器: '{stirrer_id}'")
debug_print(f" 🎯 使用设备: '{stir_device_id}'")
if heatchill_id:
action_sequence.append(create_action_log(f"找到加热搅拌器: {heatchill_id}", "🔥"))
elif stirrer_id:
action_sequence.append(create_action_log(f"找到搅拌器: {stirrer_id}", "🌪️"))
else:
action_sequence.append(create_action_log("未找到搅拌设备,将跳过搅拌", "⚠️"))
# === 执行溶解流程 ===
debug_print("🔍 步骤5: 执行溶解流程...")
try:
# 步骤5.1: 启动加热搅拌(如果需要)
if stir_device_id and (final_temp > 25.0 or final_time > 0 or stir_speed > 0):
debug_print(f"🔍 5.1: 启动加热搅拌,温度: {final_temp}°C")
action_sequence.append(create_action_log(f"准备加热搅拌 (目标温度: {final_temp}°C)", "🔥"))
if heatchill_id and (final_temp > 25.0 or final_time > 0):
# 使用加热搅拌器
action_sequence.append(create_action_log(f"启动加热搅拌器 {heatchill_id}", "🔥"))
heatchill_action = {
"device_id": heatchill_id,
"action_name": "heat_chill_start",
"action_kwargs": {
"vessel": {"id": vessel_id},
"temp": final_temp,
"purpose": f"溶解准备 - {event}" if event else "溶解准备"
}
}
action_sequence.append(heatchill_action)
# 等待温度稳定
if final_temp > 25.0:
wait_time = min(60, abs(final_temp - 25.0) * 1.5)
action_sequence.append(create_action_log(f"等待温度稳定 ({wait_time:.0f}秒)", ""))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": wait_time}
})
elif stirrer_id:
# 使用独立搅拌器
action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {stir_speed}rpm)", "🌪️"))
stir_action = {
"device_id": stirrer_id,
"action_name": "start_stir",
"action_kwargs": {
"vessel": {"id": vessel_id},
"stir_speed": stir_speed,
"purpose": f"溶解搅拌 - {event}" if event else "溶解搅拌"
}
}
action_sequence.append(stir_action)
# 等待搅拌稳定
action_sequence.append(create_action_log("等待搅拌稳定...", ""))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5}
})
if is_solid_dissolve:
# === 固体溶解路径 ===
debug_print(f"🔍 5.2: 使用固体溶解路径")
action_sequence.append(create_action_log("开始固体溶解流程", "🧂"))
solid_dispenser = find_solid_dispenser(G)
if solid_dispenser:
action_sequence.append(create_action_log(f"找到固体加样器: {solid_dispenser}", "🥄"))
# 固体加样
add_kwargs = {
"vessel": {"id": vessel_id},
"reagent": reagent or amount or "solid reagent",
"purpose": f"溶解固体试剂 - {event}" if event else "溶解固体试剂",
"event": event
}
if final_mass > 0:
add_kwargs["mass"] = str(final_mass)
action_sequence.append(create_action_log(f"准备添加固体: {final_mass}g", "⚖️"))
if mol and mol.strip():
add_kwargs["mol"] = mol
action_sequence.append(create_action_log(f"按摩尔数添加: {mol}", "🧬"))
action_sequence.append(create_action_log("开始固体加样操作", "🥄"))
action_sequence.append({
"device_id": solid_dispenser,
"action_name": "add_solid",
"action_kwargs": add_kwargs
})
debug_print(f"✅ 固体加样完成")
action_sequence.append(create_action_log("固体加样完成", ""))
# 🔧 新增:固体溶解体积运算 - 固体本身不会显著增加体积,但可能有少量变化
debug_print(f"🔧 固体溶解 - 体积变化很小,主要是质量变化")
# 固体通常不会显著改变液体体积,这里只记录日志
action_sequence.append(create_action_log(f"固体已添加: {final_mass}g", "📊"))
else:
debug_print("⚠️ 未找到固体加样器,跳过固体添加")
action_sequence.append(create_action_log("未找到固体加样器,无法添加固体", ""))
elif is_liquid_dissolve:
# === 液体溶解路径 ===
debug_print(f"🔍 5.3: 使用液体溶解路径")
action_sequence.append(create_action_log("开始液体溶解流程", "💧"))
# 查找溶剂容器
action_sequence.append(create_action_log("正在查找溶剂容器...", "🔍"))
try:
solvent_vessel = find_solvent_vessel(G, solvent)
action_sequence.append(create_action_log(f"找到溶剂容器: {solvent_vessel}", "🧪"))
except ValueError as e:
debug_print(f"⚠️ {str(e)},跳过溶剂添加")
action_sequence.append(create_action_log(f"溶剂容器查找失败: {str(e)}", ""))
solvent_vessel = None
if solvent_vessel:
# 计算流速 - 溶解时通常用较慢的速度,避免飞溅
flowrate = 1.0 # 较慢的注入速度
transfer_flowrate = 0.5 # 较慢的转移速度
action_sequence.append(create_action_log(f"设置流速: {flowrate}mL/min (缓慢注入)", ""))
action_sequence.append(create_action_log(f"开始转移 {final_volume}mL {solvent}", "🚰"))
# 调用pump protocol
pump_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=solvent_vessel,
to_vessel=vessel_id,
volume=final_volume,
amount=amount,
time=0.0, # 不在pump level控制时间
viscous=False,
rinsing_solvent="",
rinsing_volume=0.0,
rinsing_repeats=0,
solid=False,
flowrate=flowrate,
transfer_flowrate=transfer_flowrate,
rate_spec="",
event=event,
through="",
**kwargs
)
action_sequence.extend(pump_actions)
debug_print(f"✅ 溶剂转移完成,添加了 {len(pump_actions)} 个动作")
action_sequence.append(create_action_log(f"溶剂转移完成 ({len(pump_actions)} 个操作)", ""))
# 🔧 新增:液体溶解体积运算 - 添加溶剂后更新容器体积
debug_print(f"🔧 更新容器液体体积 - 添加溶剂 {final_volume:.2f}mL")
# 确保vessel有data字段
if "data" not in vessel:
vessel["data"] = {}
if "liquid_volume" in vessel["data"]:
current_volume = vessel["data"]["liquid_volume"]
if isinstance(current_volume, list):
if len(current_volume) > 0:
vessel["data"]["liquid_volume"][0] += final_volume
debug_print(f"📊 添加溶剂后体积: {vessel['data']['liquid_volume'][0]:.2f}mL (+{final_volume:.2f}mL)")
else:
vessel["data"]["liquid_volume"] = [final_volume]
debug_print(f"📊 初始化溶解体积: {final_volume:.2f}mL")
elif isinstance(current_volume, (int, float)):
vessel["data"]["liquid_volume"] += final_volume
debug_print(f"📊 添加溶剂后体积: {vessel['data']['liquid_volume']:.2f}mL (+{final_volume:.2f}mL)")
else:
vessel["data"]["liquid_volume"] = final_volume
debug_print(f"📊 重置体积为: {final_volume:.2f}mL")
else:
vessel["data"]["liquid_volume"] = final_volume
debug_print(f"📊 创建新体积记录: {final_volume:.2f}mL")
# 🔧 同时更新图中的容器数据
if vessel_id in G.nodes():
if 'data' not in G.nodes[vessel_id]:
G.nodes[vessel_id]['data'] = {}
vessel_node_data = G.nodes[vessel_id]['data']
current_node_volume = vessel_node_data.get('liquid_volume', 0.0)
if isinstance(current_node_volume, list):
if len(current_node_volume) > 0:
G.nodes[vessel_id]['data']['liquid_volume'][0] += final_volume
else:
G.nodes[vessel_id]['data']['liquid_volume'] = [final_volume]
else:
G.nodes[vessel_id]['data']['liquid_volume'] = current_node_volume + final_volume
debug_print(f"✅ 图节点体积数据已更新")
action_sequence.append(create_action_log(f"容器体积已更新 (+{final_volume:.2f}mL)", "📊"))
# 溶剂添加后等待
action_sequence.append(create_action_log("溶剂添加后短暂等待...", ""))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5}
})
# 步骤5.4: 等待溶解完成
if final_time > 0:
debug_print(f"🔍 5.4: 等待溶解完成 - {final_time}s")
wait_minutes = final_time / 60
action_sequence.append(create_action_log(f"开始溶解等待 ({wait_minutes:.1f}分钟)", ""))
if heatchill_id:
# 使用定时加热搅拌
action_sequence.append(create_action_log(f"使用加热搅拌器进行定时溶解", "🔥"))
dissolve_action = {
"device_id": heatchill_id,
"action_name": "heat_chill",
"action_kwargs": {
"vessel": {"id": vessel_id},
"temp": final_temp,
"time": final_time,
"stir": True,
"stir_speed": stir_speed,
"purpose": f"溶解等待 - {event}" if event else "溶解等待"
}
}
action_sequence.append(dissolve_action)
elif stirrer_id:
# 使用定时搅拌
action_sequence.append(create_action_log(f"使用搅拌器进行定时溶解", "🌪️"))
stir_action = {
"device_id": stirrer_id,
"action_name": "stir",
"action_kwargs": {
"vessel": {"id": vessel_id},
"stir_time": final_time,
"stir_speed": stir_speed,
"settling_time": 0,
"purpose": f"溶解搅拌 - {event}" if event else "溶解搅拌"
}
}
action_sequence.append(stir_action)
else:
# 简单等待
action_sequence.append(create_action_log(f"简单等待溶解完成", ""))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": final_time}
})
# 步骤5.5: 停止加热搅拌(如果需要)
if heatchill_id and final_time == 0 and final_temp > 25.0:
debug_print(f"🔍 5.5: 停止加热器")
action_sequence.append(create_action_log("停止加热搅拌器", "🛑"))
stop_action = {
"device_id": heatchill_id,
"action_name": "heat_chill_stop",
"action_kwargs": {
"vessel": {"id": vessel_id},
}
}
action_sequence.append(stop_action)
except Exception as e:
debug_print(f"❌ 溶解流程执行失败: {str(e)}")
action_sequence.append(create_action_log(f"溶解流程失败: {str(e)}", ""))
# 添加错误日志
action_sequence.append({
"device_id": "system",
"action_name": "log_message",
"action_kwargs": {
"message": f"溶解失败: {str(e)}"
}
})
# 🔧 新增:溶解完成后的状态报告
final_liquid_volume = 0.0
if "data" in vessel and "liquid_volume" in vessel["data"]:
current_volume = vessel["data"]["liquid_volume"]
if isinstance(current_volume, list) and len(current_volume) > 0:
final_liquid_volume = current_volume[0]
elif isinstance(current_volume, (int, float)):
final_liquid_volume = current_volume
# === 最终结果 ===
debug_print("=" * 60)
debug_print(f"🎉 溶解协议生成完成")
debug_print(f"📊 协议统计:")
debug_print(f" 📋 总动作数: {len(action_sequence)}")
debug_print(f" 🥼 容器: {vessel_id}")
debug_print(f" {dissolve_emoji} 溶解类型: {dissolve_type}")
if is_liquid_dissolve:
debug_print(f" 💧 溶剂: {solvent} ({final_volume}mL)")
if is_solid_dissolve:
debug_print(f" 🧪 试剂: {reagent}")
debug_print(f" ⚖️ 质量: {final_mass}g")
debug_print(f" 🧬 摩尔: {mol}")
debug_print(f" 🌡️ 温度: {final_temp}°C")
debug_print(f" ⏱️ 时间: {final_time}s")
debug_print(f" 📊 溶解前体积: {original_liquid_volume:.2f}mL")
debug_print(f" 📊 溶解后体积: {final_liquid_volume:.2f}mL")
debug_print("=" * 60)
# 添加完成日志
summary_msg = f"溶解协议完成: {vessel_id}"
if is_liquid_dissolve:
summary_msg += f" (使用 {final_volume}mL {solvent})"
if is_solid_dissolve:
summary_msg += f" (溶解 {final_mass}g {reagent})"
action_sequence.append(create_action_log(summary_msg, "🎉"))
return action_sequence
# === 便捷函数 ===
# 🔧 修改便捷函数的参数类型
def dissolve_solid_by_mass(G: nx.DiGraph, vessel: dict, reagent: str, mass: Union[str, float],
temp: Union[str, float] = 25.0, time: Union[str, float] = "10 min") -> List[Dict[str, Any]]:
"""按质量溶解固体"""
vessel_id = vessel["id"]
debug_print(f"🧂 快速固体溶解: {reagent} ({mass}) → {vessel_id}")
return generate_dissolve_protocol(
G, vessel,
mass=mass,
reagent=reagent,
temp=temp,
time=time
)
def dissolve_solid_by_moles(G: nx.DiGraph, vessel: dict, reagent: str, mol: str,
temp: Union[str, float] = 25.0, time: Union[str, float] = "10 min") -> List[Dict[str, Any]]:
"""按摩尔数溶解固体"""
vessel_id = vessel["id"]
debug_print(f"🧬 按摩尔数溶解固体: {reagent} ({mol}) → {vessel_id}")
return generate_dissolve_protocol(
G, vessel,
mol=mol,
reagent=reagent,
temp=temp,
time=time
)
def dissolve_with_solvent(G: nx.DiGraph, vessel: dict, solvent: str, volume: Union[str, float],
temp: Union[str, float] = 25.0, time: Union[str, float] = "5 min") -> List[Dict[str, Any]]:
"""用溶剂溶解"""
vessel_id = vessel["id"]
debug_print(f"💧 溶剂溶解: {solvent} ({volume}) → {vessel_id}")
return generate_dissolve_protocol(
G, vessel,
solvent=solvent,
volume=volume,
temp=temp,
time=time
)
def dissolve_at_room_temp(G: nx.DiGraph, vessel: dict, solvent: str, volume: Union[str, float]) -> List[Dict[str, Any]]:
"""室温溶解"""
vessel_id = vessel["id"]
debug_print(f"🌡️ 室温溶解: {solvent} ({volume}) → {vessel_id}")
return generate_dissolve_protocol(
G, vessel,
solvent=solvent,
volume=volume,
temp="room temperature",
time="5 min"
)
def dissolve_with_heating(G: nx.DiGraph, vessel: dict, solvent: str, volume: Union[str, float],
temp: Union[str, float] = "60 °C", time: Union[str, float] = "15 min") -> List[Dict[str, Any]]:
"""加热溶解"""
vessel_id = vessel["id"]
debug_print(f"🔥 加热溶解: {solvent} ({volume}) → {vessel_id} @ {temp}")
return generate_dissolve_protocol(
G, vessel,
solvent=solvent,
volume=volume,
temp=temp,
time=time
)
# 测试函数
def test_dissolve_protocol():
"""测试溶解协议的各种参数解析"""
debug_print("=== DISSOLVE PROTOCOL 增强版测试 ===")
# 测试体积解析
debug_print("💧 测试体积解析...")
volumes = ["10 mL", "?", 10.0, "1 L", "500 μL"]
for vol in volumes:
result = parse_volume_input(vol)
debug_print(f"📏 体积解析: {vol}{result}mL")
# 测试质量解析
debug_print("⚖️ 测试质量解析...")
masses = ["2.9 g", "?", 2.5, "500 mg"]
for mass in masses:
result = parse_mass_input(mass)
debug_print(f"⚖️ 质量解析: {mass}{result}g")
# 测试温度解析
debug_print("🌡️ 测试温度解析...")
temps = ["60 °C", "room temperature", "?", 25.0, "reflux"]
for temp in temps:
result = parse_temperature_input(temp)
debug_print(f"🌡️ 温度解析: {temp}{result}°C")
# 测试时间解析
debug_print("⏱️ 测试时间解析...")
times = ["30 min", "1 h", "?", 60.0]
for time in times:
result = parse_time_input(time)
debug_print(f"⏱️ 时间解析: {time}{result}s")
debug_print("✅ 测试完成")
if __name__ == "__main__":
test_dissolve_protocol()