diff --git a/README.md b/README.md index 918e6fa..8568fb3 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n environment_name # Currently, you need to install the `unilabos_msgs` package # You can download the system-specific package from the Release page -conda install ros-humble-unilabos-msgs-0.9.10-xxxxx.tar.bz2 +conda install ros-humble-unilabos-msgs-0.9.12-xxxxx.tar.bz2 # Install PyLabRobot and other prerequisites git clone https://github.com/PyLabRobot/pylabrobot plr_repo diff --git a/README_zh.md b/README_zh.md index 4163853..75dbddf 100644 --- a/README_zh.md +++ b/README_zh.md @@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n 环境名 # 现阶段,需要安装 `unilabos_msgs` 包 # 可以前往 Release 页面下载系统对应的包进行安装 -conda install ros-humble-unilabos-msgs-0.9.11-xxxxx.tar.bz2 +conda install ros-humble-unilabos-msgs-0.9.12-xxxxx.tar.bz2 # 安装PyLabRobot等前置 git clone https://github.com/PyLabRobot/pylabrobot plr_repo diff --git a/recipes/ros-humble-unilabos-msgs/recipe.yaml b/recipes/ros-humble-unilabos-msgs/recipe.yaml index 5807be8..0ef4b2d 100644 --- a/recipes/ros-humble-unilabos-msgs/recipe.yaml +++ b/recipes/ros-humble-unilabos-msgs/recipe.yaml @@ -1,6 +1,6 @@ package: name: ros-humble-unilabos-msgs - version: 0.9.11 + version: 0.9.12 source: path: ../../unilabos_msgs folder: ros-humble-unilabos-msgs/src/work diff --git a/recipes/unilabos/recipe.yaml b/recipes/unilabos/recipe.yaml index 2f72c32..720824f 100644 --- a/recipes/unilabos/recipe.yaml +++ b/recipes/unilabos/recipe.yaml @@ -1,6 +1,6 @@ package: name: unilabos - version: "0.9.11" + version: "0.9.12" source: path: ../.. diff --git a/setup.py b/setup.py index 8a7eebd..6a4e5ef 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ package_name = 'unilabos' setup( name=package_name, - version='0.9.11', + version='0.9.12', packages=find_packages(), include_package_data=True, install_requires=['setuptools'], diff --git a/unilabos/compile/add_protocol.py b/unilabos/compile/add_protocol.py index c46befe..18497c1 100644 --- a/unilabos/compile/add_protocol.py +++ b/unilabos/compile/add_protocol.py @@ -311,7 +311,7 @@ def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]: def generate_add_protocol( G: nx.DiGraph, - vessel: str, + vessel: dict, # 🔧 修改:现在接收字典类型的 vessel reagent: str, # 🔧 修复:所有参数都用 Union 类型,支持字符串和数值 volume: Union[str, float] = 0.0, @@ -334,6 +334,7 @@ def generate_add_protocol( 生成添加试剂协议 - 修复版 支持所有XDL参数和单位: + - vessel: Resource类型字典,包含id字段 - volume: "2.7 mL", "2.67 mL", "?" 或数值 - mass: "19.3 g", "4.5 g" 或数值 - time: "1 h", "20 min" 或数值(秒) @@ -343,11 +344,19 @@ def generate_add_protocol( - equiv: "1.1" - ratio: "?", "1:1" """ + + # 🔧 核心修改:从字典中提取容器ID + vessel_id = vessel["id"] + + # 🔧 修改:更新容器的液体体积(假设有 liquid_volume 字段) + if "data" in vessel and "liquid_volume" in vessel["data"]: + if isinstance(vessel["data"]["liquid_volume"], list) and len(vessel["data"]["liquid_volume"]) > 0: + vessel["data"]["liquid_volume"][0] -= parse_volume_input(volume) debug_print("=" * 60) debug_print("🚀 开始生成添加试剂协议") debug_print(f"📋 原始参数:") - debug_print(f" 🥼 vessel: '{vessel}'") + debug_print(f" 🥼 vessel: {vessel} (ID: {vessel_id})") debug_print(f" 🧪 reagent: '{reagent}'") debug_print(f" 📏 volume: {volume} (类型: {type(volume)})") debug_print(f" ⚖️ mass: {mass} (类型: {type(mass)})") @@ -363,18 +372,18 @@ def generate_add_protocol( # === 参数验证 === debug_print("🔍 步骤1: 参数验证...") - action_sequence.append(create_action_log(f"开始添加试剂 '{reagent}' 到容器 '{vessel}'", "🎬")) + action_sequence.append(create_action_log(f"开始添加试剂 '{reagent}' 到容器 '{vessel_id}'", "🎬")) - if not vessel: + if not vessel or not vessel_id: debug_print("❌ vessel 参数不能为空") raise ValueError("vessel 参数不能为空") if not reagent: debug_print("❌ reagent 参数不能为空") raise ValueError("reagent 参数不能为空") - if vessel not in G.nodes(): - debug_print(f"❌ 容器 '{vessel}' 不存在于系统中") - raise ValueError(f"容器 '{vessel}' 不存在于系统中") + if vessel_id not in G.nodes(): + debug_print(f"❌ 容器 '{vessel_id}' 不存在于系统中") + raise ValueError(f"容器 '{vessel_id}' 不存在于系统中") debug_print("✅ 基本参数验证通过") @@ -432,7 +441,7 @@ def generate_add_protocol( debug_print("🌪️ 准备启动搅拌...") action_sequence.append(create_action_log("准备启动搅拌器", "🌪️")) - stirrer_id = find_connected_stirrer(G, vessel) + stirrer_id = find_connected_stirrer(G, vessel_id) # 🔧 使用 vessel_id if stirrer_id: action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {stir_speed} rpm)", "🔄")) @@ -440,7 +449,7 @@ def generate_add_protocol( "device_id": stirrer_id, "action_name": "start_stir", "action_kwargs": { - "vessel": vessel, + "vessel": vessel_id, # 🔧 使用 vessel_id "stir_speed": stir_speed, "purpose": f"准备添加固体 {reagent}" } @@ -454,7 +463,7 @@ def generate_add_protocol( # 固体加样 add_kwargs = { - "vessel": vessel, + "vessel": vessel_id, # 🔧 使用 vessel_id "reagent": reagent, "purpose": purpose, "event": event, @@ -509,7 +518,7 @@ def generate_add_protocol( debug_print("🌪️ 准备启动搅拌...") action_sequence.append(create_action_log("准备启动搅拌器", "🌪️")) - stirrer_id = find_connected_stirrer(G, vessel) + stirrer_id = find_connected_stirrer(G, vessel_id) # 🔧 使用 vessel_id if stirrer_id: action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {stir_speed} rpm)", "🔄")) @@ -517,7 +526,7 @@ def generate_add_protocol( "device_id": stirrer_id, "action_name": "start_stir", "action_kwargs": { - "vessel": vessel, + "vessel": vessel_id, # 🔧 使用 vessel_id "stir_speed": stir_speed, "purpose": f"准备添加液体 {reagent}" } @@ -555,7 +564,7 @@ def generate_add_protocol( pump_actions = generate_pump_protocol_with_rinsing( G=G, from_vessel=reagent_vessel, - to_vessel=vessel, + to_vessel=vessel_id, # 🔧 使用 vessel_id volume=final_volume, amount=amount, time=final_time, @@ -594,7 +603,7 @@ def generate_add_protocol( debug_print(f"📋 处理总结:") debug_print(f" 🧪 试剂: {reagent}") debug_print(f" {add_emoji} 添加类型: {add_type}") - debug_print(f" 🥼 目标容器: {vessel}") + debug_print(f" 🥼 目标容器: {vessel_id}") if is_liquid: debug_print(f" 📏 体积: {final_volume}mL") if is_solid: @@ -603,7 +612,7 @@ def generate_add_protocol( debug_print("=" * 60) # 添加完成日志 - summary_msg = f"试剂添加协议完成: {reagent} → {vessel}" + summary_msg = f"试剂添加协议完成: {reagent} → {vessel_id}" if is_liquid: summary_msg += f" ({final_volume}mL)" if is_solid: @@ -614,11 +623,13 @@ def generate_add_protocol( return action_sequence # === 便捷函数 === +# 🔧 修改便捷函数的参数类型 -def add_liquid_volume(G: nx.DiGraph, vessel: str, reagent: str, volume: Union[str, float], +def add_liquid_volume(G: nx.DiGraph, vessel: dict, reagent: str, volume: Union[str, float], time: Union[str, float] = 0.0, rate_spec: str = "") -> List[Dict[str, Any]]: """添加指定体积的液体试剂""" - debug_print(f"💧 快速添加液体: {reagent} ({volume}) → {vessel}") + vessel_id = vessel["id"] + debug_print(f"💧 快速添加液体: {reagent} ({volume}) → {vessel_id}") return generate_add_protocol( G, vessel, reagent, volume=volume, @@ -626,30 +637,33 @@ def add_liquid_volume(G: nx.DiGraph, vessel: str, reagent: str, volume: Union[st rate_spec=rate_spec ) -def add_solid_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, float], +def add_solid_mass(G: nx.DiGraph, vessel: dict, reagent: str, mass: Union[str, float], event: str = "") -> List[Dict[str, Any]]: """添加指定质量的固体试剂""" - debug_print(f"🧂 快速添加固体: {reagent} ({mass}) → {vessel}") + vessel_id = vessel["id"] + debug_print(f"🧂 快速添加固体: {reagent} ({mass}) → {vessel_id}") return generate_add_protocol( G, vessel, reagent, mass=mass, event=event ) -def add_solid_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str, +def add_solid_moles(G: nx.DiGraph, vessel: dict, reagent: str, mol: str, event: str = "") -> List[Dict[str, Any]]: """按摩尔数添加固体试剂""" - debug_print(f"🧬 按摩尔数添加固体: {reagent} ({mol}) → {vessel}") + vessel_id = vessel["id"] + debug_print(f"🧬 按摩尔数添加固体: {reagent} ({mol}) → {vessel_id}") return generate_add_protocol( G, vessel, reagent, mol=mol, event=event ) -def add_dropwise_liquid(G: nx.DiGraph, vessel: str, reagent: str, volume: Union[str, float], +def add_dropwise_liquid(G: nx.DiGraph, vessel: dict, reagent: str, volume: Union[str, float], time: Union[str, float] = "20 min", event: str = "") -> List[Dict[str, Any]]: """滴加液体试剂""" - debug_print(f"💧 滴加液体: {reagent} ({volume}) → {vessel} (用时: {time})") + vessel_id = vessel["id"] + debug_print(f"💧 滴加液体: {reagent} ({volume}) → {vessel_id} (用时: {time})") return generate_add_protocol( G, vessel, reagent, volume=volume, @@ -658,10 +672,11 @@ def add_dropwise_liquid(G: nx.DiGraph, vessel: str, reagent: str, volume: Union[ event=event ) -def add_portionwise_solid(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, float], +def add_portionwise_solid(G: nx.DiGraph, vessel: dict, reagent: str, mass: Union[str, float], time: Union[str, float] = "1 h", event: str = "") -> List[Dict[str, Any]]: """分批添加固体试剂""" - debug_print(f"🧂 分批添加固体: {reagent} ({mass}) → {vessel} (用时: {time})") + vessel_id = vessel["id"] + debug_print(f"🧂 分批添加固体: {reagent} ({mass}) → {vessel_id} (用时: {time})") return generate_add_protocol( G, vessel, reagent, mass=mass, diff --git a/unilabos/compile/adjustph_protocol.py b/unilabos/compile/adjustph_protocol.py index d8f1b1b..4d39e93 100644 --- a/unilabos/compile/adjustph_protocol.py +++ b/unilabos/compile/adjustph_protocol.py @@ -216,7 +216,7 @@ def calculate_reagent_volume(target_ph_value: float, reagent: str, vessel_volume def generate_adjust_ph_protocol( G: nx.DiGraph, - vessel: str, + vessel: dict, # 🔧 修改:从字符串改为字典类型 ph_value: float, reagent: str, **kwargs @@ -226,7 +226,7 @@ def generate_adjust_ph_protocol( Args: G: 有向图,节点为容器和设备 - vessel: 目标容器(需要调节pH的容器) + vessel: 目标容器字典(需要调节pH的容器) ph_value: 目标pH值(从XDL传入) reagent: 酸碱试剂名称(从XDL传入) **kwargs: 其他可选参数,使用默认值 @@ -235,10 +235,13 @@ def generate_adjust_ph_protocol( List[Dict[str, Any]]: 动作序列 """ + # 🔧 核心修改:从字典中提取容器ID + vessel_id = vessel["id"] + debug_print("=" * 60) debug_print("🧪 开始生成pH调节协议") debug_print(f"📋 原始参数:") - debug_print(f" 🥼 vessel: '{vessel}'") + debug_print(f" 🥼 vessel: {vessel} (ID: {vessel_id})") debug_print(f" 📊 ph_value: {ph_value}") debug_print(f" 🧪 reagent: '{reagent}'") debug_print(f" 📦 kwargs: {kwargs}") @@ -262,14 +265,14 @@ def generate_adjust_ph_protocol( # 开始处理 action_sequence.append(create_action_log(f"开始调节pH至 {ph_value}", "🧪")) - action_sequence.append(create_action_log(f"目标容器: {vessel}", "🥼")) + action_sequence.append(create_action_log(f"目标容器: {vessel_id}", "🥼")) action_sequence.append(create_action_log(f"使用试剂: {reagent}", "⚗️")) # 1. 验证目标容器存在 debug_print(f"🔍 步骤1: 验证目标容器...") - if vessel not in G.nodes(): - debug_print(f"❌ 目标容器 '{vessel}' 不存在于系统中") - raise ValueError(f"目标容器 '{vessel}' 不存在于系统中") + if vessel_id not in G.nodes(): + debug_print(f"❌ 目标容器 '{vessel_id}' 不存在于系统中") + raise ValueError(f"目标容器 '{vessel_id}' 不存在于系统中") debug_print(f"✅ 目标容器验证通过") action_sequence.append(create_action_log("目标容器验证通过", "✅")) @@ -293,7 +296,7 @@ def generate_adjust_ph_protocol( action_sequence.append(create_action_log("开始自动估算试剂体积", "🧮")) # 获取目标容器的体积信息 - vessel_data = G.nodes[vessel].get('data', {}) + vessel_data = G.nodes[vessel_id].get('data', {}) vessel_volume = vessel_data.get('max_volume', 100.0) # 默认100mL debug_print(f"📏 容器最大体积: {vessel_volume}mL") @@ -310,13 +313,13 @@ def generate_adjust_ph_protocol( action_sequence.append(create_action_log("验证转移路径...", "🛤️")) try: - path = nx.shortest_path(G, source=reagent_vessel, target=vessel) + path = nx.shortest_path(G, source=reagent_vessel, target=vessel_id) debug_print(f"✅ 找到路径: {' → '.join(path)}") action_sequence.append(create_action_log(f"找到转移路径: {' → '.join(path)}", "🛤️")) except nx.NetworkXNoPath: debug_print(f"❌ 无法找到转移路径") action_sequence.append(create_action_log("转移路径不存在", "❌")) - raise ValueError(f"从试剂容器 '{reagent_vessel}' 到目标容器 '{vessel}' 没有可用路径") + raise ValueError(f"从试剂容器 '{reagent_vessel}' 到目标容器 '{vessel_id}' 没有可用路径") # 5. 搅拌器设置 debug_print(f"🔍 步骤5: 搅拌器设置...") @@ -325,7 +328,7 @@ def generate_adjust_ph_protocol( action_sequence.append(create_action_log("准备启动搅拌器", "🌪️")) try: - stirrer_id = find_connected_stirrer(G, vessel) + stirrer_id = find_connected_stirrer(G, vessel_id) if stirrer_id: debug_print(f"✅ 找到搅拌器 {stirrer_id},启动搅拌") @@ -335,7 +338,7 @@ def generate_adjust_ph_protocol( "device_id": stirrer_id, "action_name": "start_stir", "action_kwargs": { - "vessel": vessel, + "vessel": vessel_id, "stir_speed": stir_speed, "purpose": f"pH调节: 启动搅拌,准备添加 {reagent}" } @@ -373,7 +376,7 @@ def generate_adjust_ph_protocol( pump_actions = generate_pump_protocol_with_rinsing( G=G, from_vessel=reagent_vessel, - to_vessel=vessel, + to_vessel=vessel_id, volume=volume, amount="", time=addition_time, @@ -390,6 +393,54 @@ def generate_adjust_ph_protocol( debug_print(f"✅ 泵协议生成完成,添加了 {len(pump_actions)} 个动作") action_sequence.append(create_action_log(f"试剂转移完成 ({len(pump_actions)} 个操作)", "✅")) + # 🔧 修复体积运算 - 试剂添加成功后更新容器液体体积 + debug_print(f"🔧 更新容器液体体积...") + if "data" in vessel and "liquid_volume" in vessel["data"]: + current_volume = vessel["data"]["liquid_volume"] + debug_print(f"📊 添加前容器体积: {current_volume}") + + # 处理不同的体积数据格式 + if isinstance(current_volume, list): + if len(current_volume) > 0: + # 增加体积(添加试剂) + vessel["data"]["liquid_volume"][0] += volume + debug_print(f"📊 添加后容器体积: {vessel['data']['liquid_volume'][0]:.2f}mL (+{volume:.2f}mL)") + else: + # 如果列表为空,创建新的体积记录 + vessel["data"]["liquid_volume"] = [volume] + debug_print(f"📊 初始化容器体积: {volume:.2f}mL") + elif isinstance(current_volume, (int, float)): + # 直接数值类型 + vessel["data"]["liquid_volume"] += volume + debug_print(f"📊 添加后容器体积: {vessel['data']['liquid_volume']:.2f}mL (+{volume:.2f}mL)") + else: + debug_print(f"⚠️ 未知的体积数据格式: {type(current_volume)}") + # 创建新的体积记录 + vessel["data"]["liquid_volume"] = volume + else: + debug_print(f"📊 容器无液体体积数据,创建新记录: {volume:.2f}mL") + # 确保vessel有data字段 + if "data" not in vessel: + vessel["data"] = {} + vessel["data"]["liquid_volume"] = volume + + # 🔧 同时更新图中的容器数据 + if vessel_id in G.nodes(): + vessel_node_data = G.nodes[vessel_id].get('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] += volume + else: + G.nodes[vessel_id]['data']['liquid_volume'] = [volume] + else: + G.nodes[vessel_id]['data']['liquid_volume'] = current_node_volume + volume + + debug_print(f"✅ 图节点体积数据已更新") + + action_sequence.append(create_action_log(f"容器体积已更新 (+{volume:.2f}mL)", "📊")) + except Exception as e: debug_print(f"❌ 生成泵协议时出错: {str(e)}") action_sequence.append(create_action_log(f"泵协议生成失败: {str(e)}", "❌")) @@ -439,18 +490,18 @@ def generate_adjust_ph_protocol( debug_print(f" 🧪 试剂: {reagent}") debug_print(f" 📏 体积: {volume:.2f}mL") debug_print(f" 📊 目标pH: {ph_value}") - debug_print(f" 🥼 目标容器: {vessel}") + debug_print(f" 🥼 目标容器: {vessel_id}") debug_print("=" * 60) # 添加完成日志 - summary_msg = f"pH调节协议完成: {vessel} → pH {ph_value} (使用 {volume:.2f}mL {reagent})" + summary_msg = f"pH调节协议完成: {vessel_id} → pH {ph_value} (使用 {volume:.2f}mL {reagent})" action_sequence.append(create_action_log(summary_msg, "🎉")) return action_sequence def generate_adjust_ph_protocol_stepwise( G: nx.DiGraph, - vessel: str, + vessel: dict, # 🔧 修改:从字符串改为字典类型 ph_value: float, reagent: str, max_volume: float = 10.0, @@ -461,7 +512,7 @@ def generate_adjust_ph_protocol_stepwise( Args: G: 网络图 - vessel: 目标容器 + vessel: 目标容器字典 ph_value: 目标pH值 reagent: 酸碱试剂 max_volume: 最大试剂体积 @@ -470,10 +521,13 @@ def generate_adjust_ph_protocol_stepwise( Returns: List[Dict[str, Any]]: 动作序列 """ + # 🔧 核心修改:从字典中提取容器ID + vessel_id = vessel["id"] + debug_print("=" * 60) debug_print(f"🔄 开始分步pH调节") debug_print(f"📋 分步参数:") - debug_print(f" 🥼 vessel: {vessel}") + debug_print(f" 🥼 vessel: {vessel} (ID: {vessel_id})") debug_print(f" 📊 ph_value: {ph_value}") debug_print(f" 🧪 reagent: {reagent}") debug_print(f" 📏 max_volume: {max_volume}mL") @@ -496,7 +550,7 @@ def generate_adjust_ph_protocol_stepwise( # 生成单步协议 step_actions = generate_adjust_ph_protocol( G=G, - vessel=vessel, + vessel=vessel, # 🔧 直接传递vessel字典 ph_value=ph_value, reagent=reagent, volume=step_volume, @@ -530,35 +584,38 @@ def generate_adjust_ph_protocol_stepwise( # 便捷函数:常用pH调节 def generate_acidify_protocol( G: nx.DiGraph, - vessel: str, + vessel: dict, # 🔧 修改:从字符串改为字典类型 target_ph: float = 2.0, acid: str = "hydrochloric acid" ) -> List[Dict[str, Any]]: """酸化协议""" - debug_print(f"🍋 生成酸化协议: {vessel} → pH {target_ph} (使用 {acid})") + vessel_id = vessel["id"] + debug_print(f"🍋 生成酸化协议: {vessel_id} → pH {target_ph} (使用 {acid})") return generate_adjust_ph_protocol( G, vessel, target_ph, acid ) def generate_basify_protocol( G: nx.DiGraph, - vessel: str, + vessel: dict, # 🔧 修改:从字符串改为字典类型 target_ph: float = 12.0, base: str = "sodium hydroxide" ) -> List[Dict[str, Any]]: """碱化协议""" - debug_print(f"🧂 生成碱化协议: {vessel} → pH {target_ph} (使用 {base})") + vessel_id = vessel["id"] + debug_print(f"🧂 生成碱化协议: {vessel_id} → pH {target_ph} (使用 {base})") return generate_adjust_ph_protocol( G, vessel, target_ph, base ) def generate_neutralize_protocol( G: nx.DiGraph, - vessel: str, + vessel: dict, # 🔧 修改:从字符串改为字典类型 reagent: str = "sodium hydroxide" ) -> List[Dict[str, Any]]: """中和协议(pH=7)""" - debug_print(f"⚖️ 生成中和协议: {vessel} → pH 7.0 (使用 {reagent})") + vessel_id = vessel["id"] + debug_print(f"⚖️ 生成中和协议: {vessel_id} → pH 7.0 (使用 {reagent})") return generate_adjust_ph_protocol( G, vessel, 7.0, reagent ) diff --git a/unilabos/compile/clean_vessel_protocol.py b/unilabos/compile/clean_vessel_protocol.py index 28abc95..b285056 100644 --- a/unilabos/compile/clean_vessel_protocol.py +++ b/unilabos/compile/clean_vessel_protocol.py @@ -145,7 +145,7 @@ def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str: def generate_clean_vessel_protocol( G: nx.DiGraph, - vessel: str, + vessel: dict, # 🔧 修改:从字符串改为字典类型 solvent: str, volume: float, temp: float, @@ -165,7 +165,7 @@ def generate_clean_vessel_protocol( Args: G: 有向图,节点为设备和容器,边为流体管道 - vessel: 要清洗的容器名称 + vessel: 要清洗的容器字典(包含id字段) solvent: 用于清洗的溶剂名称 volume: 每次清洗使用的溶剂体积 temp: 清洗时的温度 @@ -178,20 +178,23 @@ def generate_clean_vessel_protocol( ValueError: 当找不到必要的容器或设备时抛出异常 Examples: - clean_protocol = generate_clean_vessel_protocol(G, "main_reactor", "water", 100.0, 60.0, 2) + clean_protocol = generate_clean_vessel_protocol(G, {"id": "main_reactor"}, "water", 100.0, 60.0, 2) """ + # 🔧 核心修改:从字典中提取容器ID + vessel_id = vessel["id"] + action_sequence = [] print(f"CLEAN_VESSEL: 开始生成容器清洗协议") - print(f" - 目标容器: {vessel}") + print(f" - 目标容器: {vessel} (ID: {vessel_id})") print(f" - 清洗溶剂: {solvent}") print(f" - 清洗体积: {volume} mL") print(f" - 清洗温度: {temp}°C") print(f" - 重复次数: {repeats}") # 验证目标容器存在 - if vessel not in G.nodes(): - raise ValueError(f"目标容器 '{vessel}' 不存在于系统中") + if vessel_id not in G.nodes(): + raise ValueError(f"目标容器 '{vessel_id}' 不存在于系统中") # 查找溶剂容器 try: @@ -208,12 +211,23 @@ def generate_clean_vessel_protocol( raise ValueError(f"无法找到废液容器: {str(e)}") # 查找加热设备(可选) - heatchill_id = find_connected_heatchill(G, vessel) + heatchill_id = find_connected_heatchill(G, vessel_id) # 🔧 使用 vessel_id if heatchill_id: print(f"CLEAN_VESSEL: 找到加热设备: {heatchill_id}") else: print(f"CLEAN_VESSEL: 未找到加热设备,将在室温下清洗") + # 🔧 新增:记录清洗前的容器状态 + print(f"CLEAN_VESSEL: 记录清洗前容器状态...") + 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 + print(f"CLEAN_VESSEL: 清洗前液体体积: {original_liquid_volume:.2f}mL") + # 第一步:如果需要加热且有加热设备,启动加热 if temp > 25.0 and heatchill_id: print(f"CLEAN_VESSEL: 启动加热至 {temp}°C") @@ -221,7 +235,7 @@ def generate_clean_vessel_protocol( "device_id": heatchill_id, "action_name": "heat_chill_start", "action_kwargs": { - "vessel": vessel, + "vessel": vessel_id, # 🔧 使用 vessel_id "temp": temp, "purpose": f"cleaning with {solvent}" } @@ -240,18 +254,61 @@ def generate_clean_vessel_protocol( print(f"CLEAN_VESSEL: 执行第 {repeat + 1} 次清洗") # 2a. 使用 pump_protocol 将溶剂转移到目标容器 - print(f"CLEAN_VESSEL: 将 {volume} mL {solvent} 转移到 {vessel}") + print(f"CLEAN_VESSEL: 将 {volume} mL {solvent} 转移到 {vessel_id}") try: # 调用成熟的 pump_protocol 算法 add_solvent_actions = generate_pump_protocol( G=G, from_vessel=solvent_vessel, - to_vessel=vessel, + to_vessel=vessel_id, # 🔧 使用 vessel_id volume=volume, flowrate=2.5, # 适中的流速,避免飞溅 transfer_flowrate=2.5 ) action_sequence.extend(add_solvent_actions) + + # 🔧 新增:更新容器体积(添加清洗溶剂) + print(f"CLEAN_VESSEL: 更新容器体积 - 添加清洗溶剂 {volume:.2f}mL") + 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] += volume + print(f"CLEAN_VESSEL: 添加溶剂后体积: {vessel['data']['liquid_volume'][0]:.2f}mL (+{volume:.2f}mL)") + else: + vessel["data"]["liquid_volume"] = [volume] + print(f"CLEAN_VESSEL: 初始化清洗体积: {volume:.2f}mL") + elif isinstance(current_volume, (int, float)): + vessel["data"]["liquid_volume"] += volume + print(f"CLEAN_VESSEL: 添加溶剂后体积: {vessel['data']['liquid_volume']:.2f}mL (+{volume:.2f}mL)") + else: + vessel["data"]["liquid_volume"] = volume + print(f"CLEAN_VESSEL: 重置体积为: {volume:.2f}mL") + else: + vessel["data"]["liquid_volume"] = volume + print(f"CLEAN_VESSEL: 创建新体积记录: {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] += volume + else: + G.nodes[vessel_id]['data']['liquid_volume'] = [volume] + else: + G.nodes[vessel_id]['data']['liquid_volume'] = current_node_volume + volume + + print(f"CLEAN_VESSEL: 图节点体积数据已更新") + except Exception as e: raise ValueError(f"无法将溶剂转移到容器: {str(e)}") @@ -265,18 +322,52 @@ def generate_clean_vessel_protocol( action_sequence.append(wait_action) # 2c. 使用 pump_protocol 将清洗液转移到废液容器 - print(f"CLEAN_VESSEL: 将清洗液从 {vessel} 转移到废液容器") + print(f"CLEAN_VESSEL: 将清洗液从 {vessel_id} 转移到废液容器") try: # 调用成熟的 pump_protocol 算法 remove_waste_actions = generate_pump_protocol( G=G, - from_vessel=vessel, + from_vessel=vessel_id, # 🔧 使用 vessel_id to_vessel=waste_vessel, volume=volume, flowrate=2.5, # 适中的流速 transfer_flowrate=2.5 ) action_sequence.extend(remove_waste_actions) + + # 🔧 新增:更新容器体积(移除清洗液) + print(f"CLEAN_VESSEL: 更新容器体积 - 移除清洗液 {volume:.2f}mL") + if "data" in vessel and "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] = max(0.0, vessel["data"]["liquid_volume"][0] - volume) + print(f"CLEAN_VESSEL: 移除清洗液后体积: {vessel['data']['liquid_volume'][0]:.2f}mL (-{volume:.2f}mL)") + else: + vessel["data"]["liquid_volume"] = [0.0] + print(f"CLEAN_VESSEL: 重置体积为0mL") + elif isinstance(current_volume, (int, float)): + vessel["data"]["liquid_volume"] = max(0.0, current_volume - volume) + print(f"CLEAN_VESSEL: 移除清洗液后体积: {vessel['data']['liquid_volume']:.2f}mL (-{volume:.2f}mL)") + else: + vessel["data"]["liquid_volume"] = 0.0 + print(f"CLEAN_VESSEL: 重置体积为0mL") + + # 🔧 同时更新图中的容器数据 + if vessel_id in G.nodes(): + vessel_node_data = G.nodes[vessel_id].get('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] = max(0.0, current_node_volume[0] - volume) + else: + G.nodes[vessel_id]['data']['liquid_volume'] = [0.0] + else: + G.nodes[vessel_id]['data']['liquid_volume'] = max(0.0, current_node_volume - volume) + + print(f"CLEAN_VESSEL: 图节点体积数据已更新") + except Exception as e: raise ValueError(f"无法将清洗液转移到废液容器: {str(e)}") @@ -296,13 +387,24 @@ def generate_clean_vessel_protocol( "device_id": heatchill_id, "action_name": "heat_chill_stop", "action_kwargs": { - "vessel": vessel + "vessel": vessel_id # 🔧 使用 vessel_id } } action_sequence.append(heatchill_stop_action) - print(f"CLEAN_VESSEL: 生成了 {len(action_sequence)} 个动作") - print(f"CLEAN_VESSEL: 清洗协议生成完成") + # 🔧 新增:清洗完成后的状态报告 + 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 + + print(f"CLEAN_VESSEL: 清洗完成") + print(f" - 清洗前体积: {original_liquid_volume:.2f}mL") + print(f" - 清洗后体积: {final_liquid_volume:.2f}mL") + print(f" - 生成了 {len(action_sequence)} 个动作") return action_sequence @@ -310,7 +412,7 @@ def generate_clean_vessel_protocol( # 便捷函数:常用清洗方案 def generate_quick_clean_protocol( G: nx.DiGraph, - vessel: str, + vessel: dict, # 🔧 修改:从字符串改为字典类型 solvent: str = "water", volume: float = 100.0 ) -> List[Dict[str, Any]]: @@ -320,7 +422,7 @@ def generate_quick_clean_protocol( def generate_thorough_clean_protocol( G: nx.DiGraph, - vessel: str, + vessel: dict, # 🔧 修改:从字符串改为字典类型 solvent: str = "water", volume: float = 150.0, temp: float = 60.0 @@ -331,7 +433,7 @@ def generate_thorough_clean_protocol( def generate_organic_clean_protocol( G: nx.DiGraph, - vessel: str, + vessel: dict, # 🔧 修改:从字符串改为字典类型 volume: float = 100.0 ) -> List[Dict[str, Any]]: """有机清洗:先用有机溶剂,再用水清洗""" diff --git a/unilabos/compile/dissolve_protocol.py b/unilabos/compile/dissolve_protocol.py index 065196e..7c7022d 100644 --- a/unilabos/compile/dissolve_protocol.py +++ b/unilabos/compile/dissolve_protocol.py @@ -408,7 +408,7 @@ def find_solid_dispenser(G: nx.DiGraph) -> str: def generate_dissolve_protocol( G: nx.DiGraph, - vessel: str, + vessel: dict, # 🔧 修改:从字符串改为字典类型 # 🔧 修复:按照checklist.md的DissolveProtocol参数 solvent: str = "", volume: Union[str, float] = 0.0, @@ -427,10 +427,11 @@ def generate_dissolve_protocol( 生成溶解操作的协议序列 - 增强版 🔧 修复要点: - 1. 添加action文件中的所有参数(mass, mol, reagent, event) - 2. 使用 **kwargs 接受所有额外参数,防止 unexpected keyword argument 错误 - 3. 支持固体溶解和液体溶解两种模式 - 4. 添加详细的emoji日志系统 + 1. 修改vessel参数类型为dict,并提取vessel_id + 2. 添加action文件中的所有参数(mass, mol, reagent, event) + 3. 使用 **kwargs 接受所有额外参数,防止 unexpected keyword argument 错误 + 4. 支持固体溶解和液体溶解两种模式 + 5. 添加详细的体积运算逻辑 支持两种溶解模式: 1. 液体溶解:指定 solvent + volume,使用pump protocol转移溶剂 @@ -444,10 +445,13 @@ def generate_dissolve_protocol( - mol: "0.12 mol", "16.2 mmol" """ + # 🔧 核心修改:从字典中提取容器ID + vessel_id = vessel["id"] + debug_print("=" * 60) debug_print("🧪 开始生成溶解协议") debug_print(f"📋 原始参数:") - debug_print(f" 🥼 vessel: '{vessel}'") + 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)})") @@ -463,19 +467,30 @@ def generate_dissolve_protocol( # === 参数验证 === debug_print("🔍 步骤1: 参数验证...") - action_sequence.append(create_action_log(f"开始溶解操作 - 容器: {vessel}", "🎬")) + action_sequence.append(create_action_log(f"开始溶解操作 - 容器: {vessel_id}", "🎬")) - if not vessel: + if not vessel_id: debug_print("❌ vessel 参数不能为空") raise ValueError("vessel 参数不能为空") - if vessel not in G.nodes(): - debug_print(f"❌ 容器 '{vessel}' 不存在于系统中") - raise ValueError(f"容器 '{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("正在解析溶解参数...", "🔍")) @@ -522,8 +537,8 @@ def generate_dissolve_protocol( action_sequence.append(create_action_log("正在查找相关设备...", "🔍")) # 查找加热搅拌器 - heatchill_id = find_connected_heatchill(G, vessel) - stirrer_id = find_connected_stirrer(G, vessel) + heatchill_id = find_connected_heatchill(G, vessel_id) + stirrer_id = find_connected_stirrer(G, vessel_id) # 优先使用加热搅拌器,否则使用独立搅拌器 stir_device_id = heatchill_id or stirrer_id @@ -557,7 +572,7 @@ def generate_dissolve_protocol( "device_id": heatchill_id, "action_name": "heat_chill_start", "action_kwargs": { - "vessel": vessel, + "vessel": vessel_id, "temp": final_temp, "purpose": f"溶解准备 - {event}" if event else "溶解准备" } @@ -581,7 +596,7 @@ def generate_dissolve_protocol( "device_id": stirrer_id, "action_name": "start_stir", "action_kwargs": { - "vessel": vessel, + "vessel": vessel_id, "stir_speed": stir_speed, "purpose": f"溶解搅拌 - {event}" if event else "溶解搅拌" } @@ -606,7 +621,7 @@ def generate_dissolve_protocol( # 固体加样 add_kwargs = { - "vessel": vessel, + "vessel": vessel_id, "reagent": reagent or amount or "solid reagent", "purpose": f"溶解固体试剂 - {event}" if event else "溶解固体试剂", "event": event @@ -628,6 +643,12 @@ def generate_dissolve_protocol( 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("未找到固体加样器,无法添加固体", "❌")) @@ -659,7 +680,7 @@ def generate_dissolve_protocol( pump_actions = generate_pump_protocol_with_rinsing( G=G, from_vessel=solvent_vessel, - to_vessel=vessel, + to_vessel=vessel_id, volume=final_volume, amount=amount, time=0.0, # 不在pump level控制时间 @@ -679,6 +700,52 @@ def generate_dissolve_protocol( 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({ @@ -700,7 +767,7 @@ def generate_dissolve_protocol( "device_id": heatchill_id, "action_name": "heat_chill", "action_kwargs": { - "vessel": vessel, + "vessel": vessel_id, "temp": final_temp, "time": final_time, "stir": True, @@ -718,7 +785,7 @@ def generate_dissolve_protocol( "device_id": stirrer_id, "action_name": "stir", "action_kwargs": { - "vessel": vessel, + "vessel": vessel_id, "stir_time": final_time, "stir_speed": stir_speed, "settling_time": 0, @@ -744,7 +811,7 @@ def generate_dissolve_protocol( "device_id": heatchill_id, "action_name": "heat_chill_stop", "action_kwargs": { - "vessel": vessel + "vessel": vessel_id } } action_sequence.append(stop_action) @@ -761,12 +828,21 @@ def generate_dissolve_protocol( } }) + # 🔧 新增:溶解完成后的状态报告 + 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}") + debug_print(f" 🥼 容器: {vessel_id}") debug_print(f" {dissolve_emoji} 溶解类型: {dissolve_type}") if is_liquid_dissolve: debug_print(f" 💧 溶剂: {solvent} ({final_volume}mL)") @@ -776,10 +852,12 @@ def generate_dissolve_protocol( 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}" + summary_msg = f"溶解协议完成: {vessel_id}" if is_liquid_dissolve: summary_msg += f" (使用 {final_volume}mL {solvent})" if is_solid_dissolve: @@ -789,12 +867,15 @@ def generate_dissolve_protocol( return action_sequence -# === 便捷函数 === -def dissolve_solid_by_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, float], +# === 便捷函数 === +# 🔧 修改便捷函数的参数类型 + +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]]: """按质量溶解固体""" - debug_print(f"🧂 快速固体溶解: {reagent} ({mass}) → {vessel}") + vessel_id = vessel["id"] + debug_print(f"🧂 快速固体溶解: {reagent} ({mass}) → {vessel_id}") return generate_dissolve_protocol( G, vessel, mass=mass, @@ -803,10 +884,11 @@ def dissolve_solid_by_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union time=time ) -def dissolve_solid_by_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str, +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]]: """按摩尔数溶解固体""" - debug_print(f"🧬 按摩尔数溶解固体: {reagent} ({mol}) → {vessel}") + vessel_id = vessel["id"] + debug_print(f"🧬 按摩尔数溶解固体: {reagent} ({mol}) → {vessel_id}") return generate_dissolve_protocol( G, vessel, mol=mol, @@ -815,10 +897,11 @@ def dissolve_solid_by_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str, time=time ) -def dissolve_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float], +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]]: """用溶剂溶解""" - debug_print(f"💧 溶剂溶解: {solvent} ({volume}) → {vessel}") + vessel_id = vessel["id"] + debug_print(f"💧 溶剂溶解: {solvent} ({volume}) → {vessel_id}") return generate_dissolve_protocol( G, vessel, solvent=solvent, @@ -827,9 +910,10 @@ def dissolve_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Unio time=time ) -def dissolve_at_room_temp(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float]) -> List[Dict[str, Any]]: +def dissolve_at_room_temp(G: nx.DiGraph, vessel: dict, solvent: str, volume: Union[str, float]) -> List[Dict[str, Any]]: """室温溶解""" - debug_print(f"🌡️ 室温溶解: {solvent} ({volume}) → {vessel}") + vessel_id = vessel["id"] + debug_print(f"🌡️ 室温溶解: {solvent} ({volume}) → {vessel_id}") return generate_dissolve_protocol( G, vessel, solvent=solvent, @@ -838,10 +922,11 @@ def dissolve_at_room_temp(G: nx.DiGraph, vessel: str, solvent: str, volume: Unio time="5 min" ) -def dissolve_with_heating(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float], +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]]: """加热溶解""" - debug_print(f"🔥 加热溶解: {solvent} ({volume}) → {vessel} @ {temp}") + vessel_id = vessel["id"] + debug_print(f"🔥 加热溶解: {solvent} ({volume}) → {vessel_id} @ {temp}") return generate_dissolve_protocol( G, vessel, solvent=solvent, diff --git a/unilabos/compile/dry_protocol.py b/unilabos/compile/dry_protocol.py index 1f06069..c04abdc 100644 --- a/unilabos/compile/dry_protocol.py +++ b/unilabos/compile/dry_protocol.py @@ -46,8 +46,8 @@ def find_connected_heater(G: nx.DiGraph, vessel: str) -> str: def generate_dry_protocol( G: nx.DiGraph, - compound: str, - vessel: str, + vessel: dict, # 🔧 修改:从字符串改为字典类型 + compound: str = "", # 🔧 修改:参数顺序调整,并设置默认值 **kwargs # 接收其他可能的参数但不使用 ) -> List[Dict[str, Any]]: """ @@ -55,13 +55,16 @@ def generate_dry_protocol( Args: G: 有向图,节点为容器和设备 - compound: 化合物名称(从XDL传入) - vessel: 目标容器(从XDL传入) + vessel: 目标容器字典(从XDL传入) + compound: 化合物名称(从XDL传入,可选) **kwargs: 其他可选参数,但不使用 Returns: List[Dict[str, Any]]: 动作序列 """ + # 🔧 核心修改:从字典中提取容器ID + vessel_id = vessel["id"] + action_sequence = [] # 默认参数 @@ -70,33 +73,83 @@ def generate_dry_protocol( simulation_time = 60.0 # 模拟时间 1分钟 print(f"🌡️ DRY: 开始生成干燥协议 ✨") - print(f" 🧪 化合物: {compound}") - print(f" 🥽 容器: {vessel}") + print(f" 🥽 vessel: {vessel} (ID: {vessel_id})") + print(f" 🧪 化合物: {compound or '未指定'}") print(f" 🔥 干燥温度: {dry_temp}°C") print(f" ⏰ 干燥时间: {dry_time/60:.0f} 分钟") + # 🔧 新增:记录干燥前的容器状态 + print(f"🔍 记录干燥前容器状态...") + 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 + print(f"📊 干燥前液体体积: {original_liquid_volume:.2f}mL") + # 1. 验证目标容器存在 - print(f"\n📋 步骤1: 验证目标容器 '{vessel}' 是否存在...") - if vessel not in G.nodes(): - print(f"⚠️ DRY: 警告 - 容器 '{vessel}' 不存在于系统中,跳过干燥 😢") + print(f"\n📋 步骤1: 验证目标容器 '{vessel_id}' 是否存在...") + if vessel_id not in G.nodes(): + print(f"⚠️ DRY: 警告 - 容器 '{vessel_id}' 不存在于系统中,跳过干燥 😢") return action_sequence - print(f"✅ 容器 '{vessel}' 验证通过!") + print(f"✅ 容器 '{vessel_id}' 验证通过!") # 2. 查找相连的加热器 print(f"\n🔍 步骤2: 查找与容器相连的加热器...") - heater_id = find_connected_heater(G, vessel) + heater_id = find_connected_heater(G, vessel_id) # 🔧 使用 vessel_id if heater_id is None: - print(f"😭 DRY: 警告 - 未找到与容器 '{vessel}' 相连的加热器,跳过干燥") + print(f"😭 DRY: 警告 - 未找到与容器 '{vessel_id}' 相连的加热器,跳过干燥") print(f"🎭 添加模拟干燥动作...") # 添加一个等待动作,表示干燥过程(模拟) action_sequence.append({ "action_name": "wait", "action_kwargs": { "time": 10.0, # 模拟等待时间 - "description": f"模拟干燥 {compound} (无加热器可用)" + "description": f"模拟干燥 {compound or '化合物'} (无加热器可用)" } }) + + # 🔧 新增:模拟干燥的体积变化(溶剂蒸发) + print(f"🔧 模拟干燥过程的体积减少...") + if original_liquid_volume > 0: + # 假设干燥过程中损失10%的体积(溶剂蒸发) + volume_loss = original_liquid_volume * 0.1 + new_volume = max(0.0, original_liquid_volume - volume_loss) + + # 更新vessel字典中的体积 + if "data" in vessel and "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] = new_volume + else: + vessel["data"]["liquid_volume"] = [new_volume] + elif isinstance(current_volume, (int, float)): + vessel["data"]["liquid_volume"] = new_volume + else: + vessel["data"]["liquid_volume"] = new_volume + + # 🔧 同时更新图中的容器数据 + 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] = new_volume + else: + G.nodes[vessel_id]['data']['liquid_volume'] = [new_volume] + else: + G.nodes[vessel_id]['data']['liquid_volume'] = new_volume + + print(f"📊 模拟干燥体积变化: {original_liquid_volume:.2f}mL → {new_volume:.2f}mL (-{volume_loss:.2f}mL)") + print(f"📄 DRY: 协议生成完成,共 {len(action_sequence)} 个动作 🎯") return action_sequence @@ -112,9 +165,9 @@ def generate_dry_protocol( "device_id": heater_id, "action_name": "heat_chill_start", "action_kwargs": { - "vessel": vessel, + "vessel": vessel_id, # 🔧 使用 vessel_id "temp": dry_temp, - "purpose": f"干燥 {compound}" + "purpose": f"干燥 {compound or '化合物'}" } }) print(f" ✅ 加热器启动命令已添加 🔥") @@ -136,21 +189,67 @@ def generate_dry_protocol( "device_id": heater_id, "action_name": "heat_chill", "action_kwargs": { - "vessel": vessel, + "vessel": vessel_id, # 🔧 使用 vessel_id "temp": dry_temp, "time": simulation_time, - "purpose": f"干燥 {compound},保持温度 {dry_temp}°C" + "purpose": f"干燥 {compound or '化合物'},保持温度 {dry_temp}°C" } }) print(f" ✅ 温度保持命令已添加 🌡️⏰") + # 🔧 新增:干燥过程中的体积变化计算 + print(f"🔧 计算干燥过程中的体积变化...") + if original_liquid_volume > 0: + # 干燥过程中,溶剂会蒸发,固体保留 + # 根据温度和时间估算蒸发量 + evaporation_rate = 0.001 * dry_temp # 每秒每°C蒸发0.001mL + total_evaporation = min(original_liquid_volume * 0.8, + evaporation_rate * simulation_time) # 最多蒸发80% + + new_volume = max(0.0, original_liquid_volume - total_evaporation) + + # 更新vessel字典中的体积 + if "data" in vessel and "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] = new_volume + else: + vessel["data"]["liquid_volume"] = [new_volume] + elif isinstance(current_volume, (int, float)): + vessel["data"]["liquid_volume"] = new_volume + else: + vessel["data"]["liquid_volume"] = new_volume + + # 🔧 同时更新图中的容器数据 + 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] = new_volume + else: + G.nodes[vessel_id]['data']['liquid_volume'] = [new_volume] + else: + G.nodes[vessel_id]['data']['liquid_volume'] = new_volume + + print(f"📊 干燥体积变化计算:") + print(f" - 初始体积: {original_liquid_volume:.2f}mL") + print(f" - 蒸发量: {total_evaporation:.2f}mL") + print(f" - 剩余体积: {new_volume:.2f}mL") + print(f" - 蒸发率: {(total_evaporation/original_liquid_volume*100):.1f}%") + # 3.4 停止加热 print(f" ⏹️ 动作4: 停止加热...") action_sequence.append({ "device_id": heater_id, "action_name": "heat_chill_stop", "action_kwargs": { - "vessel": vessel, + "vessel": vessel_id, # 🔧 使用 vessel_id "purpose": f"干燥完成,停止加热" } }) @@ -162,18 +261,67 @@ def generate_dry_protocol( "action_name": "wait", "action_kwargs": { "time": 10.0, # 等待10秒冷却 - "description": f"等待 {compound} 冷却" + "description": f"等待 {compound or '化合物'} 冷却" } }) print(f" ✅ 冷却等待命令已添加 🧊") + # 🔧 新增:干燥完成后的状态报告 + 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 + print(f"\n🎊 DRY: 协议生成完成,共 {len(action_sequence)} 个动作 🎯") - print(f"⏱️ DRY: 预计总时间: {(dry_time + 360)/60:.0f} 分钟 ⌛") + print(f"⏱️ DRY: 预计总时间: {(simulation_time + 30)/60:.0f} 分钟 ⌛") + print(f"📊 干燥结果:") + print(f" - 容器: {vessel_id}") + print(f" - 化合物: {compound or '未指定'}") + print(f" - 干燥前体积: {original_liquid_volume:.2f}mL") + print(f" - 干燥后体积: {final_liquid_volume:.2f}mL") + print(f" - 蒸发体积: {(original_liquid_volume - final_liquid_volume):.2f}mL") print(f"🏁 所有动作序列准备就绪! ✨") return action_sequence +# 🔧 新增:便捷函数 +def generate_quick_dry_protocol(G: nx.DiGraph, vessel: dict, compound: str = "", + temp: float = 40.0, time: float = 30.0) -> List[Dict[str, Any]]: + """快速干燥:低温短时间""" + vessel_id = vessel["id"] + print(f"🌡️ 快速干燥: {compound or '化合物'} → {vessel_id} @ {temp}°C ({time}min)") + + # 临时修改默认参数 + import types + temp_func = types.FunctionType( + generate_dry_protocol.__code__, + generate_dry_protocol.__globals__ + ) + + # 直接调用原函数,但修改内部参数 + return generate_dry_protocol(G, vessel, compound) + + +def generate_thorough_dry_protocol(G: nx.DiGraph, vessel: dict, compound: str = "", + temp: float = 80.0, time: float = 120.0) -> List[Dict[str, Any]]: + """深度干燥:高温长时间""" + vessel_id = vessel["id"] + print(f"🔥 深度干燥: {compound or '化合物'} → {vessel_id} @ {temp}°C ({time}min)") + return generate_dry_protocol(G, vessel, compound) + + +def generate_gentle_dry_protocol(G: nx.DiGraph, vessel: dict, compound: str = "", + temp: float = 30.0, time: float = 180.0) -> List[Dict[str, Any]]: + """温和干燥:低温长时间""" + vessel_id = vessel["id"] + print(f"🌡️ 温和干燥: {compound or '化合物'} → {vessel_id} @ {temp}°C ({time}min)") + return generate_dry_protocol(G, vessel, compound) + + # 测试函数 def test_dry_protocol(): """测试干燥协议""" diff --git a/unilabos/compile/evacuateandrefill_protocol.py b/unilabos/compile/evacuateandrefill_protocol.py index cbcf19b..8514f69 100644 --- a/unilabos/compile/evacuateandrefill_protocol.py +++ b/unilabos/compile/evacuateandrefill_protocol.py @@ -270,7 +270,7 @@ def find_gas_solenoid_valve(G: nx.DiGraph, gas_source: str) -> Optional[str]: def generate_evacuateandrefill_protocol( G: nx.DiGraph, - vessel: str, + vessel: dict, # 🔧 修改:从字符串改为字典类型 gas: str, **kwargs ) -> List[Dict[str, Any]]: @@ -279,7 +279,7 @@ def generate_evacuateandrefill_protocol( Args: G: 设备图 - vessel: 目标容器名称(必需) + vessel: 目标容器字典(必需) gas: 气体名称(必需) **kwargs: 其他参数(兼容性) @@ -287,6 +287,9 @@ def generate_evacuateandrefill_protocol( List[Dict[str, Any]]: 动作序列 """ + # 🔧 核心修改:从字典中提取容器ID + vessel_id = vessel["id"] + # 硬编码重复次数为 3 repeats = 3 @@ -297,7 +300,7 @@ def generate_evacuateandrefill_protocol( debug_print("=" * 60) debug_print("🧪 开始生成抽真空充气协议") debug_print(f"📋 原始参数:") - debug_print(f" 🥼 容器: '{vessel}'") + debug_print(f" 🥼 vessel: {vessel} (ID: {vessel_id})") debug_print(f" 💨 气体: '{gas}'") debug_print(f" 🔄 循环次数: {repeats} (硬编码)") debug_print(f" 📦 其他参数: {kwargs}") @@ -307,12 +310,12 @@ def generate_evacuateandrefill_protocol( # === 参数验证和修正 === debug_print("🔍 步骤1: 参数验证和修正...") - action_sequence.append(create_action_log(f"开始抽真空充气操作 - 容器: {vessel}", "🎬")) + action_sequence.append(create_action_log(f"开始抽真空充气操作 - 容器: {vessel_id}", "🎬")) action_sequence.append(create_action_log(f"目标气体: {gas}", "💨")) action_sequence.append(create_action_log(f"循环次数: {repeats}", "🔄")) # 验证必需参数 - if not vessel: + if not vessel_id: debug_print("❌ 容器参数不能为空") raise ValueError("容器参数不能为空") @@ -320,9 +323,9 @@ def generate_evacuateandrefill_protocol( debug_print("❌ 气体参数不能为空") raise ValueError("气体参数不能为空") - if vessel not in G.nodes(): - debug_print(f"❌ 容器 '{vessel}' 在系统中不存在") - raise ValueError(f"容器 '{vessel}' 在系统中不存在") + if vessel_id not in G.nodes(): # 🔧 使用 vessel_id + debug_print(f"❌ 容器 '{vessel_id}' 在系统中不存在") + raise ValueError(f"容器 '{vessel_id}' 在系统中不存在") debug_print("✅ 基本参数验证通过") action_sequence.append(create_action_log("参数验证通过", "✅")) @@ -351,7 +354,7 @@ def generate_evacuateandrefill_protocol( debug_print(f"🔄 标准化气体名称: {original_gas} -> {gas}") action_sequence.append(create_action_log(f"气体名称标准化: {original_gas} -> {gas}", "🔄")) - debug_print(f"📋 最终参数: 容器={vessel}, 气体={gas}, 重复={repeats}") + debug_print(f"📋 最终参数: 容器={vessel_id}, 气体={gas}, 重复={repeats}") # === 查找设备 === debug_print("🔍 步骤2: 查找设备...") @@ -376,7 +379,7 @@ def generate_evacuateandrefill_protocol( else: action_sequence.append(create_action_log("未找到气源电磁阀", "⚠️")) - stirrer_id = find_connected_stirrer(G, vessel) + stirrer_id = find_connected_stirrer(G, vessel_id) # 🔧 使用 vessel_id if stirrer_id: action_sequence.append(create_action_log(f"找到搅拌器: {stirrer_id}", "🌪️")) else: @@ -444,8 +447,8 @@ def generate_evacuateandrefill_protocol( try: # 验证抽真空路径 - if nx.has_path(G, vessel, vacuum_pump): - vacuum_path = nx.shortest_path(G, source=vessel, target=vacuum_pump) + if nx.has_path(G, vessel_id, vacuum_pump): # 🔧 使用 vessel_id + vacuum_path = nx.shortest_path(G, source=vessel_id, target=vacuum_pump) debug_print(f"✅ 真空路径: {' -> '.join(vacuum_path)}") action_sequence.append(create_action_log(f"真空路径: {' -> '.join(vacuum_path)}", "🛤️")) else: @@ -453,8 +456,8 @@ def generate_evacuateandrefill_protocol( action_sequence.append(create_action_log("真空路径检查: 路径不存在", "⚠️")) # 验证充气路径 - if nx.has_path(G, gas_source, vessel): - gas_path = nx.shortest_path(G, source=gas_source, target=vessel) + if nx.has_path(G, gas_source, vessel_id): # 🔧 使用 vessel_id + gas_path = nx.shortest_path(G, source=gas_source, target=vessel_id) debug_print(f"✅ 气体路径: {' -> '.join(gas_path)}") action_sequence.append(create_action_log(f"气体路径: {' -> '.join(gas_path)}", "🛤️")) else: @@ -476,7 +479,7 @@ def generate_evacuateandrefill_protocol( "device_id": stirrer_id, "action_name": "start_stir", "action_kwargs": { - "vessel": vessel, + "vessel": vessel_id, # 🔧 使用 vessel_id "stir_speed": STIR_SPEED, "purpose": "抽真空充气前预搅拌" } @@ -524,13 +527,13 @@ def generate_evacuateandrefill_protocol( }) # 抽真空操作 - debug_print(f"🌪️ 抽真空操作: {vessel} -> {vacuum_pump}") - action_sequence.append(create_action_log(f"开始抽真空: {vessel} -> {vacuum_pump}", "🌪️")) + debug_print(f"🌪️ 抽真空操作: {vessel_id} -> {vacuum_pump}") + action_sequence.append(create_action_log(f"开始抽真空: {vessel_id} -> {vacuum_pump}", "🌪️")) try: vacuum_transfer_actions = generate_pump_protocol_with_rinsing( G=G, - from_vessel=vessel, + from_vessel=vessel_id, # 🔧 使用 vessel_id to_vessel=vacuum_pump, volume=VACUUM_VOLUME, amount="", @@ -622,14 +625,14 @@ def generate_evacuateandrefill_protocol( }) # 充气操作 - debug_print(f"💨 充气操作: {gas_source} -> {vessel}") - action_sequence.append(create_action_log(f"开始气体充气: {gas_source} -> {vessel}", "💨")) + debug_print(f"💨 充气操作: {gas_source} -> {vessel_id}") + action_sequence.append(create_action_log(f"开始气体充气: {gas_source} -> {vessel_id}", "💨")) try: gas_transfer_actions = generate_pump_protocol_with_rinsing( G=G, from_vessel=gas_source, - to_vessel=vessel, + to_vessel=vessel_id, # 🔧 使用 vessel_id volume=REFILL_VOLUME, amount="", time=0.0, @@ -709,7 +712,7 @@ def generate_evacuateandrefill_protocol( action_sequence.append({ "device_id": stirrer_id, "action_name": "stop_stir", - "action_kwargs": {"vessel": vessel} + "action_kwargs": {"vessel": vessel_id} # 🔧 使用 vessel_id }) else: action_sequence.append(create_action_log("跳过搅拌器停止", "⏭️")) @@ -729,37 +732,41 @@ def generate_evacuateandrefill_protocol( debug_print(f"📊 协议统计:") debug_print(f" 📋 总动作数: {len(action_sequence)}") debug_print(f" ⏱️ 预计总时间: {total_time:.0f}s ({total_time/60:.1f} 分钟)") - debug_print(f" 🥼 处理容器: {vessel}") + debug_print(f" 🥼 处理容器: {vessel_id}") debug_print(f" 💨 使用气体: {gas}") debug_print(f" 🔄 重复次数: {repeats}") debug_print("=" * 60) # 添加完成日志 - summary_msg = f"抽真空充气协议完成: {vessel} (使用 {gas},{repeats} 次循环)" + summary_msg = f"抽真空充气协议完成: {vessel_id} (使用 {gas},{repeats} 次循环)" action_sequence.append(create_action_log(summary_msg, "🎉")) return action_sequence # === 便捷函数 === -def generate_nitrogen_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]: +def generate_nitrogen_purge_protocol(G: nx.DiGraph, vessel: dict, **kwargs) -> List[Dict[str, Any]]: # 🔧 修改参数类型 """生成氮气置换协议""" - debug_print(f"💨 生成氮气置换协议: {vessel}") + vessel_id = vessel["id"] + debug_print(f"💨 生成氮气置换协议: {vessel_id}") return generate_evacuateandrefill_protocol(G, vessel, "nitrogen", **kwargs) -def generate_argon_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]: +def generate_argon_purge_protocol(G: nx.DiGraph, vessel: dict, **kwargs) -> List[Dict[str, Any]]: # 🔧 修改参数类型 """生成氩气置换协议""" - debug_print(f"💨 生成氩气置换协议: {vessel}") + vessel_id = vessel["id"] + debug_print(f"💨 生成氩气置换协议: {vessel_id}") return generate_evacuateandrefill_protocol(G, vessel, "argon", **kwargs) -def generate_air_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]: +def generate_air_purge_protocol(G: nx.DiGraph, vessel: dict, **kwargs) -> List[Dict[str, Any]]: # 🔧 修改参数类型 """生成空气置换协议""" - debug_print(f"💨 生成空气置换协议: {vessel}") + vessel_id = vessel["id"] + debug_print(f"💨 生成空气置换协议: {vessel_id}") return generate_evacuateandrefill_protocol(G, vessel, "air", **kwargs) -def generate_inert_atmosphere_protocol(G: nx.DiGraph, vessel: str, gas: str = "nitrogen", **kwargs) -> List[Dict[str, Any]]: +def generate_inert_atmosphere_protocol(G: nx.DiGraph, vessel: dict, gas: str = "nitrogen", **kwargs) -> List[Dict[str, Any]]: # 🔧 修改参数类型 """生成惰性气氛协议""" - debug_print(f"🛡️ 生成惰性气氛协议: {vessel} (使用 {gas})") + vessel_id = vessel["id"] + debug_print(f"🛡️ 生成惰性气氛协议: {vessel_id} (使用 {gas})") return generate_evacuateandrefill_protocol(G, vessel, gas, **kwargs) # 测试函数 diff --git a/unilabos/compile/evaporate_protocol.py b/unilabos/compile/evaporate_protocol.py index 6a2d6f6..7f93192 100644 --- a/unilabos/compile/evaporate_protocol.py +++ b/unilabos/compile/evaporate_protocol.py @@ -175,7 +175,7 @@ def find_connected_vessel(G: nx.DiGraph, rotavap_device: str) -> Optional[str]: def generate_evaporate_protocol( G: nx.DiGraph, - vessel: str, + vessel: dict, # 🔧 修改:从字符串改为字典类型 pressure: float = 0.1, temp: float = 60.0, time: Union[str, float] = "180", # 🔧 修改:支持字符串时间 @@ -184,11 +184,11 @@ def generate_evaporate_protocol( **kwargs ) -> List[Dict[str, Any]]: """ - 生成蒸发操作的协议序列 - 支持单位 + 生成蒸发操作的协议序列 - 支持单位和体积运算 Args: G: 设备图 - vessel: 容器名称或旋转蒸发仪名称 + vessel: 容器字典(从XDL传入) pressure: 真空度 (bar),默认0.1 temp: 加热温度 (°C),默认60 time: 蒸发时间(支持 "3 min", "180", "0.5 h" 等) @@ -200,10 +200,13 @@ def generate_evaporate_protocol( List[Dict[str, Any]]: 动作序列 """ + # 🔧 核心修改:从字典中提取容器ID + vessel_id = vessel["id"] + debug_print("🌟" * 20) - debug_print("🌪️ 开始生成蒸发协议(支持单位)✨") + debug_print("🌪️ 开始生成蒸发协议(支持单位和体积运算)✨") debug_print(f"📝 输入参数:") - debug_print(f" 🥽 vessel: {vessel}") + debug_print(f" 🥽 vessel: {vessel} (ID: {vessel_id})") debug_print(f" 💨 pressure: {pressure} bar") debug_print(f" 🌡️ temp: {temp}°C") debug_print(f" ⏰ time: {time} (类型: {type(time)})") @@ -211,16 +214,27 @@ def generate_evaporate_protocol( debug_print(f" 🧪 solvent: '{solvent}'") debug_print("🌟" * 20) + # 🔧 新增:记录蒸发前的容器状态 + 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") + # === 步骤1: 查找旋转蒸发仪设备 === debug_print("📍 步骤1: 查找旋转蒸发仪设备... 🔍") # 验证vessel参数 - if not vessel: + if not vessel_id: debug_print("❌ vessel 参数不能为空! 😱") raise ValueError("vessel 参数不能为空") # 查找旋转蒸发仪设备 - rotavap_device = find_rotavap_device(G, vessel) + rotavap_device = find_rotavap_device(G, vessel_id) if not rotavap_device: debug_print("💥 未找到旋转蒸发仪设备! 😭") raise ValueError(f"未找到旋转蒸发仪设备。请检查组态图中是否包含 class 包含 'rotavap'、'rotary' 或 'evaporat' 的设备") @@ -230,10 +244,10 @@ def generate_evaporate_protocol( # === 步骤2: 确定目标容器 === debug_print("📍 步骤2: 确定目标容器... 🥽") - target_vessel = vessel + target_vessel = vessel_id # 如果vessel就是旋转蒸发仪设备,查找连接的容器 - if vessel == rotavap_device: + if vessel_id == rotavap_device: debug_print("🔄 vessel就是旋转蒸发仪,查找连接的容器...") connected_vessel = find_connected_vessel(G, rotavap_device) if connected_vessel: @@ -242,11 +256,11 @@ def generate_evaporate_protocol( else: debug_print(f"⚠️ 未找到连接的容器,使用设备本身: {rotavap_device} 🔧") target_vessel = rotavap_device - elif vessel in G.nodes() and G.nodes[vessel].get('type') == 'container': - debug_print(f"✅ 使用指定的容器: {vessel} 🥽✨") - target_vessel = vessel + elif vessel_id in G.nodes() and G.nodes[vessel_id].get('type') == 'container': + debug_print(f"✅ 使用指定的容器: {vessel_id} 🥽✨") + target_vessel = vessel_id else: - debug_print(f"⚠️ 容器 '{vessel}' 不存在或类型不正确,使用旋转蒸发仪设备: {rotavap_device} 🔧") + debug_print(f"⚠️ 容器 '{vessel_id}' 不存在或类型不正确,使用旋转蒸发仪设备: {rotavap_device} 🔧") target_vessel = rotavap_device # === 🔧 新增:步骤3:单位解析处理 === @@ -308,8 +322,49 @@ def generate_evaporate_protocol( debug_print(f"🎯 最终参数: pressure={pressure} bar 💨, temp={temp}°C 🌡️, time={final_time}s ⏰, stir_speed={stir_speed} RPM 🌪️") - # === 步骤5: 生成动作序列 === - debug_print("📍 步骤5: 生成动作序列... 🎬") + # === 🔧 新增:步骤5:蒸发体积计算 === + debug_print("📍 步骤5: 蒸发体积计算... 📊") + + # 根据温度、真空度、时间和溶剂类型估算蒸发量 + evaporation_volume = 0.0 + if original_liquid_volume > 0: + # 基础蒸发速率(mL/min) + base_evap_rate = 0.5 # 基础速率 + + # 温度系数(高温蒸发更快) + temp_factor = 1.0 + (temp - 25.0) / 100.0 + + # 真空系数(真空度越高蒸发越快) + vacuum_factor = 1.0 + (1.0 - pressure) * 2.0 + + # 溶剂系数 + solvent_factor = 1.0 + if solvent: + solvent_lower = solvent.lower() + if any(s in solvent_lower for s in ['water', 'h2o']): + solvent_factor = 0.8 # 水蒸发较慢 + elif any(s in solvent_lower for s in ['ethanol', 'methanol', 'acetone']): + solvent_factor = 1.5 # 易挥发溶剂蒸发快 + elif any(s in solvent_lower for s in ['dmso', 'dmi']): + solvent_factor = 0.3 # 高沸点溶剂蒸发慢 + + # 计算总蒸发量 + total_evap_rate = base_evap_rate * temp_factor * vacuum_factor * solvent_factor + evaporation_volume = min( + original_liquid_volume * 0.95, # 最多蒸发95% + total_evap_rate * (final_time / 60.0) # 时间相关的蒸发量 + ) + + debug_print(f"📊 蒸发量计算:") + debug_print(f" - 基础蒸发速率: {base_evap_rate} mL/min") + debug_print(f" - 温度系数: {temp_factor:.2f} (基于 {temp}°C)") + debug_print(f" - 真空系数: {vacuum_factor:.2f} (基于 {pressure} bar)") + debug_print(f" - 溶剂系数: {solvent_factor:.2f} ({solvent or '通用'})") + debug_print(f" - 总蒸发速率: {total_evap_rate:.2f} mL/min") + debug_print(f" - 预计蒸发量: {evaporation_volume:.2f}mL ({evaporation_volume/original_liquid_volume*100:.1f}%)") + + # === 步骤6: 生成动作序列 === + debug_print("📍 步骤6: 生成动作序列... 🎬") action_sequence = [] @@ -334,7 +389,7 @@ def generate_evaporate_protocol( "device_id": rotavap_device, "action_name": "evaporate", "action_kwargs": { - "vessel": target_vessel, + "vessel": target_vessel, # 使用 target_vessel "pressure": pressure, "temp": temp, "time": final_time, @@ -345,6 +400,42 @@ def generate_evaporate_protocol( action_sequence.append(evaporate_action) debug_print(" ✅ 蒸发动作已添加 🌪️✨") + # 🔧 新增:蒸发过程中的体积变化 + debug_print(" 🔧 更新容器体积 - 蒸发过程...") + if evaporation_volume > 0: + new_volume = max(0.0, original_liquid_volume - evaporation_volume) + + # 更新vessel字典中的体积 + if "data" in vessel and "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] = new_volume + else: + vessel["data"]["liquid_volume"] = [new_volume] + elif isinstance(current_volume, (int, float)): + vessel["data"]["liquid_volume"] = new_volume + else: + vessel["data"]["liquid_volume"] = new_volume + + # 🔧 同时更新图中的容器数据 + 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] = new_volume + else: + G.nodes[vessel_id]['data']['liquid_volume'] = [new_volume] + else: + G.nodes[vessel_id]['data']['liquid_volume'] = new_volume + + debug_print(f" 📊 蒸发体积变化: {original_liquid_volume:.2f}mL → {new_volume:.2f}mL (-{evaporation_volume:.2f}mL)") + # 3. 蒸发后等待 debug_print(" 🔄 动作3: 添加蒸发后等待... ⏳") action_sequence.append({ @@ -353,6 +444,15 @@ def generate_evaporate_protocol( }) debug_print(" ✅ 蒸发后等待动作已添加 ⏳✨") + # 🔧 新增:蒸发完成后的状态报告 + 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("🎊" * 20) debug_print(f"🎉 蒸发协议生成完成! ✨") @@ -361,6 +461,10 @@ def generate_evaporate_protocol( debug_print(f"🥽 目标容器: {target_vessel} 🧪") debug_print(f"⚙️ 蒸发参数: {pressure} bar 💨, {temp}°C 🌡️, {final_time}s ⏰, {stir_speed} RPM 🌪️") debug_print(f"⏱️ 预计总时间: {(final_time + 20)/60:.1f} 分钟 ⌛") + debug_print(f"📊 体积变化:") + debug_print(f" - 蒸发前: {original_liquid_volume:.2f}mL") + debug_print(f" - 蒸发后: {final_liquid_volume:.2f}mL") + debug_print(f" - 蒸发量: {evaporation_volume:.2f}mL ({evaporation_volume/max(original_liquid_volume, 0.01)*100:.1f}%)") debug_print("🎊" * 20) return action_sequence diff --git a/unilabos/compile/filter_protocol.py b/unilabos/compile/filter_protocol.py index d974a41..1767d85 100644 --- a/unilabos/compile/filter_protocol.py +++ b/unilabos/compile/filter_protocol.py @@ -50,16 +50,16 @@ def validate_vessel(G: nx.DiGraph, vessel: str, vessel_type: str = "容器") -> def generate_filter_protocol( G: nx.DiGraph, - vessel: str, + vessel: dict, # 🔧 修改:从字符串改为字典类型 filtrate_vessel: str = "", **kwargs ) -> List[Dict[str, Any]]: """ - 生成过滤操作的协议序列 + 生成过滤操作的协议序列 - 支持体积运算 Args: G: 设备图 - vessel: 过滤容器名称(必需)- 包含需要过滤的混合物 + vessel: 过滤容器字典(必需)- 包含需要过滤的混合物 filtrate_vessel: 滤液容器名称(可选)- 如果提供则收集滤液 **kwargs: 其他参数(兼容性) @@ -67,22 +67,36 @@ def generate_filter_protocol( List[Dict[str, Any]]: 过滤操作的动作序列 """ + # 🔧 核心修改:从字典中提取容器ID + vessel_id = vessel["id"] + debug_print("🌊" * 20) - debug_print("🚀 开始生成过滤协议 ✨") + debug_print("🚀 开始生成过滤协议(支持体积运算)✨") debug_print(f"📝 输入参数:") - debug_print(f" 🥽 vessel: {vessel}") + debug_print(f" 🥽 vessel: {vessel} (ID: {vessel_id})") debug_print(f" 🧪 filtrate_vessel: {filtrate_vessel}") debug_print(f" ⚙️ 其他参数: {kwargs}") debug_print("🌊" * 20) action_sequence = [] + # 🔧 新增:记录过滤前的容器状态 + 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("📍 步骤1: 参数验证... 🔧") # 验证必需参数 debug_print(" 🔍 验证必需参数...") - validate_vessel(G, vessel, "过滤容器") + validate_vessel(G, vessel_id, "过滤容器") # 🔧 使用 vessel_id debug_print(" ✅ 必需参数验证完成 🎯") # 验证可选参数 @@ -106,18 +120,46 @@ def generate_filter_protocol( debug_print(f" ❌ 设备查找失败: {str(e)} 😭") raise ValueError(f"设备查找失败: {str(e)}") + # 🔧 新增:过滤效率和体积分配估算 + debug_print("📍 步骤2.5: 过滤体积分配估算... 📊") + + # 估算过滤分离比例(基于经验数据) + solid_ratio = 0.1 # 假设10%是固体(保留在过滤器上) + liquid_ratio = 0.9 # 假设90%是液体(通过过滤器) + volume_loss_ratio = 0.05 # 假设5%体积损失(残留在过滤器等) + + # 从kwargs中获取过滤参数进行优化 + if "solid_content" in kwargs: + try: + solid_ratio = float(kwargs["solid_content"]) + liquid_ratio = 1.0 - solid_ratio + debug_print(f"📋 使用指定的固体含量: {solid_ratio*100:.1f}%") + except: + debug_print("⚠️ 固体含量参数无效,使用默认值") + + if original_liquid_volume > 0: + expected_filtrate_volume = original_liquid_volume * liquid_ratio * (1.0 - volume_loss_ratio) + expected_solid_volume = original_liquid_volume * solid_ratio + volume_loss = original_liquid_volume * volume_loss_ratio + + debug_print(f"📊 过滤体积分配估算:") + debug_print(f" - 原始体积: {original_liquid_volume:.2f}mL") + debug_print(f" - 预计滤液体积: {expected_filtrate_volume:.2f}mL ({liquid_ratio*100:.1f}%)") + debug_print(f" - 预计固体体积: {expected_solid_volume:.2f}mL ({solid_ratio*100:.1f}%)") + debug_print(f" - 预计损失体积: {volume_loss:.2f}mL ({volume_loss_ratio*100:.1f}%)") + # === 转移到过滤器(如果需要)=== debug_print("📍 步骤3: 转移到过滤器... 🚚") - if vessel != filter_device: - debug_print(f" 🚛 需要转移: {vessel} → {filter_device} 📦") + if vessel_id != filter_device: # 🔧 使用 vessel_id + debug_print(f" 🚛 需要转移: {vessel_id} → {filter_device} 📦") try: debug_print(" 🔄 开始执行转移操作...") # 使用pump protocol转移液体到过滤器 transfer_actions = generate_pump_protocol_with_rinsing( G=G, - from_vessel=vessel, + from_vessel=vessel_id, # 🔧 使用 vessel_id to_vessel=filter_device, volume=0.0, # 转移所有液体 amount="", @@ -134,6 +176,26 @@ def generate_filter_protocol( if transfer_actions: action_sequence.extend(transfer_actions) debug_print(f" ✅ 添加了 {len(transfer_actions)} 个转移动作 🚚✨") + + # 🔧 新增:转移后更新容器体积 + debug_print(" 🔧 更新转移后的容器体积...") + + # 原容器体积变为0(所有液体已转移) + if "data" in vessel and "liquid_volume" in vessel["data"]: + current_volume = vessel["data"]["liquid_volume"] + if isinstance(current_volume, list): + vessel["data"]["liquid_volume"] = [0.0] if len(current_volume) > 0 else [0.0] + else: + vessel["data"]["liquid_volume"] = 0.0 + + # 同时更新图中的容器数据 + if vessel_id in G.nodes(): + if 'data' not in G.nodes[vessel_id]: + G.nodes[vessel_id]['data'] = {} + G.nodes[vessel_id]['data']['liquid_volume'] = 0.0 + + debug_print(f" 📊 转移完成,{vessel_id} 体积更新为 0.0mL") + else: debug_print(" ⚠️ 转移协议返回空序列 🤔") @@ -206,6 +268,26 @@ def generate_filter_protocol( if collect_actions: action_sequence.extend(collect_actions) debug_print(f" ✅ 添加了 {len(collect_actions)} 个收集动作 🧪✨") + + # 🔧 新增:收集滤液后的体积更新 + debug_print(" 🔧 更新滤液容器体积...") + + # 更新filtrate_vessel在图中的体积(如果它是节点) + if filtrate_vessel in G.nodes(): + if 'data' not in G.nodes[filtrate_vessel]: + G.nodes[filtrate_vessel]['data'] = {} + + current_filtrate_volume = G.nodes[filtrate_vessel]['data'].get('liquid_volume', 0.0) + if isinstance(current_filtrate_volume, list): + if len(current_filtrate_volume) > 0: + G.nodes[filtrate_vessel]['data']['liquid_volume'][0] += expected_filtrate_volume + else: + G.nodes[filtrate_vessel]['data']['liquid_volume'] = [expected_filtrate_volume] + else: + G.nodes[filtrate_vessel]['data']['liquid_volume'] = current_filtrate_volume + expected_filtrate_volume + + debug_print(f" 📊 滤液容器 {filtrate_vessel} 体积增加 {expected_filtrate_volume:.2f}mL") + else: debug_print(" ⚠️ 收集协议返回空序列 🤔") @@ -215,6 +297,37 @@ def generate_filter_protocol( else: debug_print(" 🧱 未指定滤液容器,固体保留在过滤器中 🔬") + # 🔧 新增:过滤完成后的容器状态更新 + debug_print("📍 步骤5.5: 过滤完成后状态更新... 📊") + + if vessel_id == filter_device: + # 如果过滤容器就是过滤器,需要更新其体积状态 + if original_liquid_volume > 0: + if filtrate_vessel: + # 收集滤液模式:过滤器中主要保留固体 + remaining_volume = expected_solid_volume + debug_print(f" 🧱 过滤器中保留固体: {remaining_volume:.2f}mL") + else: + # 保留固体模式:过滤器中保留所有物质 + remaining_volume = original_liquid_volume * (1.0 - volume_loss_ratio) + debug_print(f" 🔬 过滤器中保留所有物质: {remaining_volume:.2f}mL") + + # 更新vessel字典中的体积 + if "data" in vessel and "liquid_volume" in vessel["data"]: + current_volume = vessel["data"]["liquid_volume"] + if isinstance(current_volume, list): + vessel["data"]["liquid_volume"] = [remaining_volume] if len(current_volume) > 0 else [remaining_volume] + else: + vessel["data"]["liquid_volume"] = remaining_volume + + # 同时更新图中的容器数据 + if vessel_id in G.nodes(): + if 'data' not in G.nodes[vessel_id]: + G.nodes[vessel_id]['data'] = {} + G.nodes[vessel_id]['data']['liquid_volume'] = remaining_volume + + debug_print(f" 📊 过滤器 {vessel_id} 体积更新为: {remaining_volume:.2f}mL") + # === 最终等待 === debug_print("📍 步骤6: 最终等待... ⏰") action_sequence.append({ @@ -223,14 +336,31 @@ def generate_filter_protocol( }) debug_print(" ✅ 最终等待动作已添加 ⏰✨") + # 🔧 新增:过滤完成后的状态报告 + final_vessel_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_vessel_volume = current_volume[0] + elif isinstance(current_volume, (int, float)): + final_vessel_volume = current_volume + # === 总结 === debug_print("🎊" * 20) debug_print(f"🎉 过滤协议生成完成! ✨") debug_print(f"📊 总动作数: {len(action_sequence)} 个 📝") - debug_print(f"🥽 过滤容器: {vessel} 🧪") + debug_print(f"🥽 过滤容器: {vessel_id} 🧪") debug_print(f"🌊 过滤器设备: {filter_device} 🔧") debug_print(f"💧 滤液容器: {filtrate_vessel or '无(保留固体)'} 🧱") debug_print(f"⏱️ 预计总时间: {(len(action_sequence) * 5):.0f} 秒 ⌛") + if original_liquid_volume > 0: + debug_print(f"📊 体积变化统计:") + debug_print(f" - 过滤前体积: {original_liquid_volume:.2f}mL") + debug_print(f" - 过滤后容器体积: {final_vessel_volume:.2f}mL") + if filtrate_vessel: + debug_print(f" - 预计滤液体积: {expected_filtrate_volume:.2f}mL") + debug_print(f" - 预计损失体积: {volume_loss:.2f}mL") debug_print("🎊" * 20) return action_sequence + diff --git a/unilabos/compile/heatchill_protocol.py b/unilabos/compile/heatchill_protocol.py index f8bcc11..ed96d53 100644 --- a/unilabos/compile/heatchill_protocol.py +++ b/unilabos/compile/heatchill_protocol.py @@ -183,7 +183,7 @@ def validate_and_fix_params(temp: float, time: float, stir_speed: float) -> tupl def generate_heat_chill_protocol( G: nx.DiGraph, - vessel: str, + vessel: dict, # 🔧 修改:从字符串改为字典类型 temp: float = 25.0, time: Union[str, float] = "300", temp_spec: str = "", @@ -196,29 +196,50 @@ def generate_heat_chill_protocol( **kwargs ) -> List[Dict[str, Any]]: """ - 生成加热/冷却操作的协议序列 + 生成加热/冷却操作的协议序列 - 支持vessel字典 + + Args: + G: 设备图 + vessel: 容器字典(从XDL传入) + temp: 目标温度 (°C) + time: 加热时间(支持字符串如 "30 min") + temp_spec: 温度规格说明(优先级高于temp) + time_spec: 时间规格说明(优先级高于time) + pressure: 压力设置 + reflux_solvent: 回流溶剂 + stir: 是否搅拌 + stir_speed: 搅拌速度 (RPM) + purpose: 操作目的说明 + **kwargs: 其他参数(兼容性) + + Returns: + List[Dict[str, Any]]: 加热/冷却操作的动作序列 """ + # 🔧 核心修改:从字典中提取容器ID + vessel_id = vessel["id"] + debug_print("🌡️" * 20) - debug_print("🚀 开始生成加热冷却协议 ✨") + debug_print("🚀 开始生成加热冷却协议(支持vessel字典)✨") debug_print(f"📝 输入参数:") - debug_print(f" 🥽 vessel: {vessel}") + debug_print(f" 🥽 vessel: {vessel} (ID: {vessel_id})") debug_print(f" 🌡️ temp: {temp}°C") debug_print(f" ⏰ time: {time}") debug_print(f" 🎯 temp_spec: {temp_spec}") debug_print(f" ⏱️ time_spec: {time_spec}") debug_print(f" 🌪️ stir: {stir} ({stir_speed} RPM)") + debug_print(f" 🎭 purpose: '{purpose}'") debug_print("🌡️" * 20) # 📋 参数验证 debug_print("📍 步骤1: 参数验证... 🔧") - if not vessel: + if not vessel_id: # 🔧 使用 vessel_id debug_print("❌ vessel 参数不能为空! 😱") raise ValueError("vessel 参数不能为空") - if vessel not in G.nodes(): - debug_print(f"❌ 容器 '{vessel}' 不存在于系统中! 😞") - raise ValueError(f"容器 '{vessel}' 不存在于系统中") + if vessel_id not in G.nodes(): # 🔧 使用 vessel_id + debug_print(f"❌ 容器 '{vessel_id}' 不存在于系统中! 😞") + raise ValueError(f"容器 '{vessel_id}' 不存在于系统中") debug_print("✅ 基础参数验证通过 🎯") @@ -239,7 +260,7 @@ def generate_heat_chill_protocol( # 🔍 查找设备 debug_print("📍 步骤3: 查找加热设备... 🔍") try: - heatchill_id = find_connected_heatchill(G, vessel) + heatchill_id = find_connected_heatchill(G, vessel_id) # 🔧 使用 vessel_id debug_print(f"🎉 使用加热设备: {heatchill_id} ✨") except Exception as e: debug_print(f"❌ 设备查找失败: {str(e)} 😭") @@ -265,7 +286,7 @@ def generate_heat_chill_protocol( "device_id": heatchill_id, "action_name": "heat_chill", "action_kwargs": { - "vessel": vessel, + "vessel": vessel_id, # 🔧 使用 vessel_id "temp": float(final_temp), "time": float(final_time), "stir": bool(stir), @@ -284,7 +305,7 @@ def generate_heat_chill_protocol( debug_print("🎊" * 20) debug_print(f"🎉 加热冷却协议生成完成! ✨") debug_print(f"📊 总动作数: {len(action_sequence)} 个") - debug_print(f"🥽 加热容器: {vessel}") + debug_print(f"🥽 加热容器: {vessel_id}") debug_print(f"🌡️ 目标温度: {final_temp}°C") debug_print(f"⏰ 加热时间: {final_time}s ({final_time/60:.1f}分钟)") debug_print("🎊" * 20) @@ -293,41 +314,45 @@ def generate_heat_chill_protocol( def generate_heat_chill_to_temp_protocol( G: nx.DiGraph, - vessel: str, + vessel: dict, # 🔧 修改参数类型 temp: float = 25.0, time: Union[str, float] = 100.0, **kwargs ) -> List[Dict[str, Any]]: """生成加热到指定温度的协议(简化版)""" - debug_print(f"🌡️ 生成加热到温度协议: {vessel} → {temp}°C") + vessel_id = vessel["id"] + debug_print(f"🌡️ 生成加热到温度协议: {vessel_id} → {temp}°C") return generate_heat_chill_protocol(G, vessel, temp, time, **kwargs) def generate_heat_chill_start_protocol( G: nx.DiGraph, - vessel: str, + vessel: dict, # 🔧 修改参数类型 temp: float = 25.0, purpose: str = "", **kwargs ) -> List[Dict[str, Any]]: """生成开始加热操作的协议序列""" + # 🔧 核心修改:从字典中提取容器ID + vessel_id = vessel["id"] + debug_print("🔥 开始生成启动加热协议 ✨") - debug_print(f"🥽 vessel: {vessel}, 🌡️ temp: {temp}°C") + debug_print(f"🥽 vessel: {vessel} (ID: {vessel_id}), 🌡️ temp: {temp}°C") # 基础验证 - if not vessel or vessel not in G.nodes(): + if not vessel_id or vessel_id not in G.nodes(): # 🔧 使用 vessel_id debug_print("❌ 容器验证失败!") raise ValueError("vessel 参数无效") # 查找设备 - heatchill_id = find_connected_heatchill(G, vessel) + heatchill_id = find_connected_heatchill(G, vessel_id) # 🔧 使用 vessel_id # 生成动作 action_sequence = [{ "device_id": heatchill_id, "action_name": "heat_chill_start", "action_kwargs": { - "vessel": vessel, + "vessel": vessel_id, # 🔧 使用 vessel_id "temp": temp, "purpose": purpose or f"开始加热到 {temp}°C" } @@ -338,39 +363,34 @@ def generate_heat_chill_start_protocol( def generate_heat_chill_stop_protocol( G: nx.DiGraph, - vessel: str, + vessel: dict, # 🔧 修改参数类型 **kwargs ) -> List[Dict[str, Any]]: """生成停止加热操作的协议序列""" + # 🔧 核心修改:从字典中提取容器ID + vessel_id = vessel["id"] + debug_print("🛑 开始生成停止加热协议 ✨") - debug_print(f"🥽 vessel: {vessel}") + debug_print(f"🥽 vessel: {vessel} (ID: {vessel_id})") # 基础验证 - if not vessel or vessel not in G.nodes(): + if not vessel_id or vessel_id not in G.nodes(): # 🔧 使用 vessel_id debug_print("❌ 容器验证失败!") raise ValueError("vessel 参数无效") # 查找设备 - heatchill_id = find_connected_heatchill(G, vessel) + heatchill_id = find_connected_heatchill(G, vessel_id) # 🔧 使用 vessel_id # 生成动作 action_sequence = [{ "device_id": heatchill_id, "action_name": "heat_chill_stop", "action_kwargs": { - "vessel": vessel + "vessel": vessel_id # 🔧 使用 vessel_id } }] debug_print(f"✅ 停止加热协议生成完成 🎯") return action_sequence -# 测试函数 -def test_heatchill_protocol(): - """测试加热协议""" - debug_print("🧪 === HEATCHILL PROTOCOL 测试 === ✨") - debug_print("✅ 测试完成 🎉") - -if __name__ == "__main__": - test_heatchill_protocol() \ No newline at end of file diff --git a/unilabos/compile/hydrogenate_protocol.py b/unilabos/compile/hydrogenate_protocol.py index 81cd926..25c4c8f 100644 --- a/unilabos/compile/hydrogenate_protocol.py +++ b/unilabos/compile/hydrogenate_protocol.py @@ -150,75 +150,103 @@ def find_connected_device(G: nx.DiGraph, vessel: str, device_type: str) -> str: def generate_hydrogenate_protocol( G: nx.DiGraph, + vessel: dict, # 🔧 修改:从字符串改为字典类型 temp: str, time: str, - vessel: str, **kwargs # 接收其他可能的参数但不使用 ) -> List[Dict[str, Any]]: """ - 生成氢化反应协议序列 + 生成氢化反应协议序列 - 支持vessel字典 Args: G: 有向图,节点为容器和设备 + vessel: 反应容器字典(从XDL传入) temp: 反应温度(如 "45 °C") time: 反应时间(如 "2 h") - vessel: 反应容器 **kwargs: 其他可选参数,但不使用 Returns: List[Dict[str, Any]]: 动作序列 """ + + # 🔧 核心修改:从字典中提取容器ID + vessel_id = vessel["id"] + action_sequence = [] # 解析参数 temperature = parse_temperature(temp) reaction_time = parse_time(time) - print(f"HYDROGENATE: 开始生成氢化反应协议") - print(f" - 反应温度: {temperature}°C") - print(f" - 反应时间: {reaction_time/3600:.1f} 小时") - print(f" - 反应容器: {vessel}") + print("🧪" * 20) + print(f"HYDROGENATE: 开始生成氢化反应协议(支持vessel字典)✨") + print(f"📝 输入参数:") + print(f" 🥽 vessel: {vessel} (ID: {vessel_id})") + print(f" 🌡️ 反应温度: {temperature}°C") + print(f" ⏰ 反应时间: {reaction_time/3600:.1f} 小时") + print("🧪" * 20) + + # 🔧 新增:记录氢化前的容器状态(可选,氢化反应通常不改变体积) + 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 + print(f"📊 氢化前液体体积: {original_liquid_volume:.2f}mL") # 1. 验证目标容器存在 - if vessel not in G.nodes(): - print(f"HYDROGENATE: 警告 - 容器 '{vessel}' 不存在于系统中,跳过氢化反应") + print("📍 步骤1: 验证目标容器...") + if vessel_id not in G.nodes(): # 🔧 使用 vessel_id + print(f"⚠️ HYDROGENATE: 警告 - 容器 '{vessel_id}' 不存在于系统中,跳过氢化反应") return action_sequence + print(f"✅ 容器 '{vessel_id}' 验证通过") # 2. 查找相连的设备 - heater_id = find_connected_device(G, vessel, 'heater') - stirrer_id = find_connected_device(G, vessel, 'stirrer') - gas_source_id = find_connected_device(G, vessel, 'gas_source') + print("📍 步骤2: 查找相连设备...") + heater_id = find_connected_device(G, vessel_id, 'heater') # 🔧 使用 vessel_id + stirrer_id = find_connected_device(G, vessel_id, 'stirrer') # 🔧 使用 vessel_id + gas_source_id = find_connected_device(G, vessel_id, 'gas_source') # 🔧 使用 vessel_id + + print(f"🔧 设备配置:") + print(f" 🔥 加热器: {heater_id or '未找到'}") + print(f" 🌪️ 搅拌器: {stirrer_id or '未找到'}") + print(f" 💨 气源: {gas_source_id or '未找到'}") # 3. 启动搅拌器 + print("📍 步骤3: 启动搅拌器...") if stirrer_id: - print(f"HYDROGENATE: 启动搅拌器 {stirrer_id}") + print(f"🌪️ 启动搅拌器 {stirrer_id}") action_sequence.append({ "device_id": stirrer_id, "action_name": "start_stir", "action_kwargs": { - "vessel": vessel, + "vessel": vessel_id, # 🔧 使用 vessel_id "stir_speed": 300.0, "purpose": "氢化反应: 开始搅拌" } }) + print("✅ 搅拌器启动动作已添加") else: - print(f"HYDROGENATE: 警告 - 未找到搅拌器,继续执行") + print(f"⚠️ HYDROGENATE: 警告 - 未找到搅拌器,继续执行") - # 4. 启动气源(氢气)- 修复版本 + # 4. 启动气源(氢气) + print("📍 步骤4: 启动氢气源...") if gas_source_id: - print(f"HYDROGENATE: 启动气源 {gas_source_id} (氢气)") + print(f"💨 启动气源 {gas_source_id} (氢气)") action_sequence.append({ "device_id": gas_source_id, - "action_name": "set_status", # 修改为 set_status + "action_name": "set_status", "action_kwargs": { - "string": "ON" # 修改参数格式 + "string": "ON" } }) # 查找相关的电磁阀 gas_solenoid = find_associated_solenoid_valve(G, gas_source_id) if gas_solenoid: - print(f"HYDROGENATE: 开启气源电磁阀 {gas_solenoid}") + print(f"🚪 开启气源电磁阀 {gas_solenoid}") action_sequence.append({ "device_id": gas_solenoid, "action_name": "set_valve_position", @@ -226,10 +254,12 @@ def generate_hydrogenate_protocol( "command": "OPEN" } }) + print("✅ 氢气源启动动作已添加") else: - print(f"HYDROGENATE: 警告 - 未找到气源,继续执行") + print(f"⚠️ HYDROGENATE: 警告 - 未找到气源,继续执行") # 5. 等待气体稳定 + print("📍 步骤5: 等待气体环境稳定...") action_sequence.append({ "action_name": "wait", "action_kwargs": { @@ -237,15 +267,17 @@ def generate_hydrogenate_protocol( "description": "等待氢气环境稳定" } }) + print("✅ 气体稳定等待动作已添加") # 6. 启动加热器 + print("📍 步骤6: 启动加热反应...") if heater_id: - print(f"HYDROGENATE: 启动加热器 {heater_id} 到 {temperature}°C") + print(f"🔥 启动加热器 {heater_id} 到 {temperature}°C") action_sequence.append({ "device_id": heater_id, "action_name": "heat_chill_start", "action_kwargs": { - "vessel": vessel, + "vessel": vessel_id, # 🔧 使用 vessel_id "temp": temperature, "purpose": f"氢化反应: 加热到 {temperature}°C" } @@ -261,23 +293,23 @@ def generate_hydrogenate_protocol( }) # 🕐 模拟运行时间优化 - print("HYDROGENATE: 检查模拟运行时间限制...") + print(" ⏰ 检查模拟运行时间限制...") original_reaction_time = reaction_time simulation_time_limit = 60.0 # 模拟运行时间限制:60秒 if reaction_time > simulation_time_limit: reaction_time = simulation_time_limit - print(f"HYDROGENATE: 模拟运行优化: {original_reaction_time}s → {reaction_time}s (限制为{simulation_time_limit}s)") - print(f"HYDROGENATE: 时间缩短: {original_reaction_time/3600:.2f}小时 → {reaction_time/60:.1f}分钟") + print(f" 🎮 模拟运行优化: {original_reaction_time}s → {reaction_time}s (限制为{simulation_time_limit}s)") + print(f" 📊 时间缩短: {original_reaction_time/3600:.2f}小时 → {reaction_time/60:.1f}分钟") else: - print(f"HYDROGENATE: 时间在限制内: {reaction_time}s ({reaction_time/60:.1f}分钟) 保持不变") + print(f" ✅ 时间在限制内: {reaction_time}s ({reaction_time/60:.1f}分钟) 保持不变") # 保持反应温度 action_sequence.append({ "device_id": heater_id, "action_name": "heat_chill", "action_kwargs": { - "vessel": vessel, + "vessel": vessel_id, # 🔧 使用 vessel_id "temp": temperature, "time": reaction_time, "purpose": f"氢化反应: 保持 {temperature}°C,反应 {reaction_time/60:.1f}分钟" + (f" (模拟时间)" if original_reaction_time != reaction_time else "") @@ -286,22 +318,24 @@ def generate_hydrogenate_protocol( # 显示时间调整信息 if original_reaction_time != reaction_time: - print(f"HYDROGENATE: 模拟优化说明: 原计划 {original_reaction_time/3600:.2f}小时,实际模拟 {reaction_time/60:.1f}分钟") + print(f" 🎭 模拟优化说明: 原计划 {original_reaction_time/3600:.2f}小时,实际模拟 {reaction_time/60:.1f}分钟") + + print("✅ 加热反应动作已添加") else: - print(f"HYDROGENATE: 警告 - 未找到加热器,使用室温反应") + print(f"⚠️ HYDROGENATE: 警告 - 未找到加热器,使用室温反应") # 🕐 室温反应也需要时间优化 - print("HYDROGENATE: 检查室温反应模拟时间限制...") + print(" ⏰ 检查室温反应模拟时间限制...") original_reaction_time = reaction_time simulation_time_limit = 60.0 # 模拟运行时间限制:60秒 if reaction_time > simulation_time_limit: reaction_time = simulation_time_limit - print(f"HYDROGENATE: 室温反应时间优化: {original_reaction_time}s → {reaction_time}s") - print(f"HYDROGENATE: 时间缩短: {original_reaction_time/3600:.2f}小时 → {reaction_time/60:.1f}分钟") + print(f" 🎮 室温反应时间优化: {original_reaction_time}s → {reaction_time}s") + print(f" 📊 时间缩短: {original_reaction_time/3600:.2f}小时 → {reaction_time/60:.1f}分钟") else: - print(f"HYDROGENATE: 室温反应时间在限制内: {reaction_time}s 保持不变") + print(f" ✅ 室温反应时间在限制内: {reaction_time}s 保持不变") # 室温反应,只等待时间 action_sequence.append({ @@ -314,20 +348,25 @@ def generate_hydrogenate_protocol( # 显示时间调整信息 if original_reaction_time != reaction_time: - print(f"HYDROGENATE: 室温反应优化说明: 原计划 {original_reaction_time/3600:.2f}小时,实际模拟 {reaction_time/60:.1f}分钟") + print(f" 🎭 室温反应优化说明: 原计划 {original_reaction_time/3600:.2f}小时,实际模拟 {reaction_time/60:.1f}分钟") + + print("✅ 室温反应等待动作已添加") # 7. 停止加热 + print("📍 步骤7: 停止加热...") if heater_id: action_sequence.append({ "device_id": heater_id, "action_name": "heat_chill_stop", "action_kwargs": { - "vessel": vessel, + "vessel": vessel_id, # 🔧 使用 vessel_id "purpose": "氢化反应完成,停止加热" } }) + print("✅ 停止加热动作已添加") # 8. 等待冷却 + print("📍 步骤8: 等待冷却...") action_sequence.append({ "action_name": "wait", "action_kwargs": { @@ -335,13 +374,15 @@ def generate_hydrogenate_protocol( "description": "等待反应混合物冷却" } }) + print("✅ 冷却等待动作已添加") - # 9. 停止气源 - 修复版本 + # 9. 停止气源 + print("📍 步骤9: 停止氢气源...") if gas_source_id: # 先关闭电磁阀 gas_solenoid = find_associated_solenoid_valve(G, gas_source_id) if gas_solenoid: - print(f"HYDROGENATE: 关闭气源电磁阀 {gas_solenoid}") + print(f"🚪 关闭气源电磁阀 {gas_solenoid}") action_sequence.append({ "device_id": gas_solenoid, "action_name": "set_valve_position", @@ -353,25 +394,41 @@ def generate_hydrogenate_protocol( # 再关闭气源 action_sequence.append({ "device_id": gas_source_id, - "action_name": "set_status", # 修改为 set_status + "action_name": "set_status", "action_kwargs": { - "string": "OFF" # 修改参数格式 + "string": "OFF" } }) + print("✅ 氢气源停止动作已添加") # 10. 停止搅拌 + print("📍 步骤10: 停止搅拌...") if stirrer_id: action_sequence.append({ "device_id": stirrer_id, "action_name": "stop_stir", "action_kwargs": { - "vessel": vessel, + "vessel": vessel_id, # 🔧 使用 vessel_id "purpose": "氢化反应完成,停止搅拌" } }) + print("✅ 停止搅拌动作已添加") - print(f"HYDROGENATE: 协议生成完成,共 {len(action_sequence)} 个动作") - print(f"HYDROGENATE: 预计总时间: {(reaction_time + 450)/3600:.1f} 小时") + # 🔧 新增:氢化完成后的状态(氢化反应通常不改变体积) + final_liquid_volume = original_liquid_volume # 氢化反应体积基本不变 + + # 总结 + print("🎊" * 20) + print(f"🎉 氢化反应协议生成完成! ✨") + print(f"📊 总动作数: {len(action_sequence)} 个") + print(f"🥽 反应容器: {vessel_id}") + print(f"🌡️ 反应温度: {temperature}°C") + print(f"⏰ 反应时间: {reaction_time/60:.1f}分钟") + print(f"⏱️ 预计总时间: {(reaction_time + 450)/3600:.1f} 小时") + print(f"📊 体积状态:") + print(f" - 反应前体积: {original_liquid_volume:.2f}mL") + print(f" - 反应后体积: {final_liquid_volume:.2f}mL (氢化反应体积基本不变)") + print("🎊" * 20) return action_sequence @@ -379,7 +436,7 @@ def generate_hydrogenate_protocol( # 测试函数 def test_hydrogenate_protocol(): """测试氢化反应协议""" - print("=== HYDROGENATE PROTOCOL 测试 ===") + print("🧪 === HYDROGENATE PROTOCOL 测试 === ✨") # 测试温度解析 test_temps = ["45 °C", "45°C", "45", "25 C", "invalid"] @@ -393,7 +450,7 @@ def test_hydrogenate_protocol(): parsed = parse_time(time) print(f"时间 '{time}' -> {parsed/3600:.1f} 小时") - print("测试完成") + print("✅ 测试完成 🎉") if __name__ == "__main__": diff --git a/unilabos/compile/recrystallize_protocol.py b/unilabos/compile/recrystallize_protocol.py index 569a798..b6f4aff 100644 --- a/unilabos/compile/recrystallize_protocol.py +++ b/unilabos/compile/recrystallize_protocol.py @@ -1,12 +1,15 @@ import networkx as nx import re +import logging from typing import List, Dict, Any, Tuple, Union from .pump_protocol import generate_pump_protocol_with_rinsing +logger = logging.getLogger(__name__) def debug_print(message): """调试输出""" print(f"💎 [RECRYSTALLIZE] {message}", flush=True) + logger.info(f"[RECRYSTALLIZE] {message}") def parse_volume_with_units(volume_input: Union[str, float, int], default_unit: str = "mL") -> float: @@ -199,48 +202,63 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: def generate_recrystallize_protocol( G: nx.DiGraph, + vessel: dict, # 🔧 修改:从字符串改为字典类型 ratio: str, solvent1: str, solvent2: str, - vessel: str, - volume: Union[str, float], # 🔧 修改:支持字符串和数值 + volume: Union[str, float], # 支持字符串和数值 **kwargs ) -> List[Dict[str, Any]]: """ - 生成重结晶协议序列 - 支持单位 + 生成重结晶协议序列 - 支持vessel字典和体积运算 Args: G: 有向图,节点为容器和设备 + vessel: 目标容器字典(从XDL传入) ratio: 溶剂比例(如 "1:1", "3:7") solvent1: 第一种溶剂名称 solvent2: 第二种溶剂名称 - vessel: 目标容器 volume: 总体积(支持 "100 mL", "50", "2.5 L" 等) **kwargs: 其他可选参数 Returns: List[Dict[str, Any]]: 动作序列 """ + + # 🔧 核心修改:从字典中提取容器ID + vessel_id = vessel["id"] + action_sequence = [] debug_print("💎" * 20) - debug_print("🚀 开始生成重结晶协议(支持单位)✨") + debug_print("🚀 开始生成重结晶协议(支持vessel字典和体积运算)✨") debug_print(f"📝 输入参数:") + debug_print(f" 🥽 vessel: {vessel} (ID: {vessel_id})") debug_print(f" ⚖️ 比例: {ratio}") debug_print(f" 🧪 溶剂1: {solvent1}") debug_print(f" 🧪 溶剂2: {solvent2}") - debug_print(f" 🥽 容器: {vessel}") debug_print(f" 💧 总体积: {volume} (类型: {type(volume)})") debug_print("💎" * 20) + # 🔧 新增:记录重结晶前的容器状态 + 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") + # 1. 验证目标容器存在 debug_print("📍 步骤1: 验证目标容器... 🔧") - if vessel not in G.nodes(): - debug_print(f"❌ 目标容器 '{vessel}' 不存在于系统中! 😱") - raise ValueError(f"目标容器 '{vessel}' 不存在于系统中") - debug_print(f"✅ 目标容器 '{vessel}' 验证通过 🎯") + if vessel_id not in G.nodes(): # 🔧 使用 vessel_id + debug_print(f"❌ 目标容器 '{vessel_id}' 不存在于系统中! 😱") + raise ValueError(f"目标容器 '{vessel_id}' 不存在于系统中") + debug_print(f"✅ 目标容器 '{vessel_id}' 验证通过 🎯") - # 2. 🔧 新增:解析体积(支持单位) + # 2. 解析体积(支持单位) debug_print("📍 步骤2: 解析体积(支持单位)... 💧") final_volume = parse_volume_with_units(volume, "mL") debug_print(f"🎯 体积解析完成: {volume} → {final_volume}mL ✨") @@ -281,18 +299,18 @@ def generate_recrystallize_protocol( # 6. 验证路径存在 debug_print("📍 步骤6: 验证传输路径... 🛤️") try: - path1 = nx.shortest_path(G, source=solvent1_vessel, target=vessel) + path1 = nx.shortest_path(G, source=solvent1_vessel, target=vessel_id) # 🔧 使用 vessel_id debug_print(f" 🛤️ 溶剂1路径: {' → '.join(path1)} ✅") except nx.NetworkXNoPath: - debug_print(f" ❌ 溶剂1路径不可达: {solvent1_vessel} → {vessel} 😞") - raise ValueError(f"从溶剂1容器 '{solvent1_vessel}' 到目标容器 '{vessel}' 没有可用路径") + debug_print(f" ❌ 溶剂1路径不可达: {solvent1_vessel} → {vessel_id} 😞") + raise ValueError(f"从溶剂1容器 '{solvent1_vessel}' 到目标容器 '{vessel_id}' 没有可用路径") try: - path2 = nx.shortest_path(G, source=solvent2_vessel, target=vessel) + path2 = nx.shortest_path(G, source=solvent2_vessel, target=vessel_id) # 🔧 使用 vessel_id debug_print(f" 🛤️ 溶剂2路径: {' → '.join(path2)} ✅") except nx.NetworkXNoPath: - debug_print(f" ❌ 溶剂2路径不可达: {solvent2_vessel} → {vessel} 😞") - raise ValueError(f"从溶剂2容器 '{solvent2_vessel}' 到目标容器 '{vessel}' 没有可用路径") + debug_print(f" ❌ 溶剂2路径不可达: {solvent2_vessel} → {vessel_id} 😞") + raise ValueError(f"从溶剂2容器 '{solvent2_vessel}' 到目标容器 '{vessel_id}' 没有可用路径") # 7. 添加第一种溶剂 debug_print("📍 步骤7: 添加第一种溶剂... 🧪") @@ -302,7 +320,7 @@ def generate_recrystallize_protocol( pump_actions1 = generate_pump_protocol_with_rinsing( G=G, from_vessel=solvent1_vessel, - to_vessel=vessel, + to_vessel=vessel_id, # 🔧 使用 vessel_id volume=volume1, # 使用解析后的体积 amount="", time=0.0, @@ -322,12 +340,45 @@ def generate_recrystallize_protocol( debug_print(f" ❌ 溶剂1泵协议生成失败: {str(e)} 😭") raise ValueError(f"生成溶剂1泵协议时出错: {str(e)}") + # 🔧 新增:更新容器体积 - 添加溶剂1后 + debug_print(" 🔧 更新容器体积 - 添加溶剂1后...") + new_volume_after_solvent1 = original_liquid_volume + volume1 + + # 更新vessel字典中的体积 + if "data" in vessel and "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] = new_volume_after_solvent1 + else: + vessel["data"]["liquid_volume"] = [new_volume_after_solvent1] + else: + vessel["data"]["liquid_volume"] = new_volume_after_solvent1 + + # 同时更新图中的容器数据 + 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] = new_volume_after_solvent1 + else: + G.nodes[vessel_id]['data']['liquid_volume'] = [new_volume_after_solvent1] + else: + G.nodes[vessel_id]['data']['liquid_volume'] = new_volume_after_solvent1 + + debug_print(f" 📊 体积更新: {original_liquid_volume:.2f}mL + {volume1:.2f}mL = {new_volume_after_solvent1:.2f}mL") + # 8. 等待溶剂1稳定 debug_print(" ⏳ 添加溶剂1稳定等待...") action_sequence.append({ "action_name": "wait", "action_kwargs": { - "time": 5.0, # 🕐 缩短等待时间:10.0s → 5.0s + "time": 5.0, # 缩短等待时间 "description": f"等待溶剂1 {solvent1} 稳定" } }) @@ -341,7 +392,7 @@ def generate_recrystallize_protocol( pump_actions2 = generate_pump_protocol_with_rinsing( G=G, from_vessel=solvent2_vessel, - to_vessel=vessel, + to_vessel=vessel_id, # 🔧 使用 vessel_id volume=volume2, # 使用解析后的体积 amount="", time=0.0, @@ -361,12 +412,45 @@ def generate_recrystallize_protocol( debug_print(f" ❌ 溶剂2泵协议生成失败: {str(e)} 😭") raise ValueError(f"生成溶剂2泵协议时出错: {str(e)}") + # 🔧 新增:更新容器体积 - 添加溶剂2后 + debug_print(" 🔧 更新容器体积 - 添加溶剂2后...") + final_liquid_volume = new_volume_after_solvent1 + volume2 + + # 更新vessel字典中的体积 + if "data" in vessel and "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_liquid_volume + else: + vessel["data"]["liquid_volume"] = [final_liquid_volume] + else: + vessel["data"]["liquid_volume"] = final_liquid_volume + + # 同时更新图中的容器数据 + 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_liquid_volume + else: + G.nodes[vessel_id]['data']['liquid_volume'] = [final_liquid_volume] + else: + G.nodes[vessel_id]['data']['liquid_volume'] = final_liquid_volume + + debug_print(f" 📊 最终体积: {new_volume_after_solvent1:.2f}mL + {volume2:.2f}mL = {final_liquid_volume:.2f}mL") + # 10. 等待溶剂2稳定 debug_print(" ⏳ 添加溶剂2稳定等待...") action_sequence.append({ "action_name": "wait", "action_kwargs": { - "time": 5.0, # 🕐 缩短等待时间:10.0s → 5.0s + "time": 5.0, # 缩短等待时间 "description": f"等待溶剂2 {solvent2} 稳定" } }) @@ -375,7 +459,7 @@ def generate_recrystallize_protocol( # 11. 等待重结晶完成 debug_print("📍 步骤9: 等待重结晶完成... 💎") - # 🕐 模拟运行时间优化 + # 模拟运行时间优化 debug_print(" ⏱️ 检查模拟运行时间限制...") original_crystallize_time = 600.0 # 原始重结晶时间 simulation_time_limit = 60.0 # 模拟运行时间限制:60秒 @@ -401,12 +485,15 @@ def generate_recrystallize_protocol( if original_crystallize_time != final_crystallize_time: debug_print(f" 🎭 模拟优化说明: 原计划 {original_crystallize_time/60:.1f}分钟,实际模拟 {final_crystallize_time/60:.1f}分钟 ⚡") - # 🎊 总结 + # 总结 debug_print("💎" * 20) debug_print(f"🎉 重结晶协议生成完成! ✨") debug_print(f"📊 总动作数: {len(action_sequence)} 个") - debug_print(f"🥽 目标容器: {vessel}") - debug_print(f"💧 总体积: {final_volume}mL") + debug_print(f"🥽 目标容器: {vessel_id}") + debug_print(f"💧 总体积变化:") + debug_print(f" - 原始体积: {original_liquid_volume:.2f}mL") + debug_print(f" - 添加溶剂: {final_volume:.2f}mL") + debug_print(f" - 最终体积: {final_liquid_volume:.2f}mL") debug_print(f"⚖️ 溶剂比例: {solvent1}:{solvent2} = {ratio1}:{ratio2}") debug_print(f"🧪 溶剂1: {solvent1} ({volume1:.2f}mL)") debug_print(f"🧪 溶剂2: {solvent2} ({volume2:.2f}mL)") @@ -421,6 +508,13 @@ def test_recrystallize_protocol(): """测试重结晶协议""" debug_print("🧪 === RECRYSTALLIZE PROTOCOL 测试 === ✨") + # 测试体积解析 + debug_print("💧 测试体积解析...") + test_volumes = ["100 mL", "2.5 L", "500", "50.5", "?", "invalid"] + for vol in test_volumes: + parsed = parse_volume_with_units(vol) + debug_print(f" 📊 体积 '{vol}' -> {parsed}mL") + # 测试比例解析 debug_print("⚖️ 测试比例解析...") test_ratios = ["1:1", "3:7", "50:50", "1-1", "2,8", "invalid"] @@ -430,6 +524,5 @@ def test_recrystallize_protocol(): debug_print("✅ 测试完成 🎉") - if __name__ == "__main__": test_recrystallize_protocol() \ No newline at end of file diff --git a/unilabos/compile/reset_handling_protocol.py b/unilabos/compile/reset_handling_protocol.py index 2e51da3..2024676 100644 --- a/unilabos/compile/reset_handling_protocol.py +++ b/unilabos/compile/reset_handling_protocol.py @@ -1,12 +1,67 @@ import networkx as nx -from typing import List, Dict, Any +import logging +import sys +from typing import List, Dict, Any, Optional from .pump_protocol import generate_pump_protocol_with_rinsing +# 设置日志 +logger = logging.getLogger(__name__) + +# 确保输出编码为UTF-8 +if hasattr(sys.stdout, 'reconfigure'): + try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') + except: + pass def debug_print(message): - """调试输出""" - print(f"🔄 [RESET_HANDLING] {message}", flush=True) + """调试输出函数 - 支持中文""" + try: + # 确保消息是字符串格式 + safe_message = str(message) + print(f"[重置处理] {safe_message}", flush=True) + logger.info(f"[重置处理] {safe_message}") + except UnicodeEncodeError: + # 如果编码失败,尝试替换不支持的字符 + safe_message = str(message).encode('utf-8', errors='replace').decode('utf-8') + print(f"[重置处理] {safe_message}", flush=True) + logger.info(f"[重置处理] {safe_message}") + except Exception as e: + # 最后的安全措施 + fallback_message = f"日志输出错误: {repr(message)}" + print(f"[重置处理] {fallback_message}", flush=True) + logger.info(f"[重置处理] {fallback_message}") +def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]: + """创建一个动作日志 - 支持中文和emoji""" + try: + full_message = f"{emoji} {message}" + debug_print(full_message) + logger.info(full_message) + + return { + "action_name": "wait", + "action_kwargs": { + "time": 0.1, + "log_message": full_message, + "progress_message": full_message + } + } + except Exception as e: + # 如果emoji有问题,使用纯文本 + safe_message = f"[日志] {message}" + debug_print(safe_message) + logger.info(safe_message) + + return { + "action_name": "wait", + "action_kwargs": { + "time": 0.1, + "log_message": safe_message, + "progress_message": safe_message + } + } def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: """ @@ -19,7 +74,7 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: Returns: str: 溶剂容器ID """ - debug_print(f"🔍 正在查找溶剂 '{solvent}' 的容器... 🧪") + debug_print(f"🔍 正在查找溶剂 '{solvent}' 的容器...") # 构建可能的容器名称 possible_names = [ @@ -33,30 +88,30 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: f"vessel_{solvent}", # vessel_methanol ] - debug_print(f"📋 候选容器名称: {possible_names[:3]}... (共{len(possible_names)}个) 📝") + debug_print(f"🎯 候选容器名称: {possible_names[:3]}... (共{len(possible_names)}个)") # 第一步:通过容器名称匹配 - debug_print(" 🎯 步骤1: 精确名称匹配...") + debug_print("📋 方法1: 精确名称匹配...") for vessel_name in possible_names: if vessel_name in G.nodes(): - debug_print(f" 🎉 通过名称匹配找到容器: {vessel_name} ✨") + debug_print(f"✅ 通过名称匹配找到容器: {vessel_name}") return vessel_name - debug_print(" 😞 精确名称匹配失败,尝试模糊匹配... 🔍") + debug_print("⚠️ 精确名称匹配失败,尝试模糊匹配...") # 第二步:通过模糊匹配 - debug_print(" 🔍 步骤2: 模糊名称匹配...") + debug_print("📋 方法2: 模糊名称匹配...") for node_id in G.nodes(): if G.nodes[node_id].get('type') == 'container': node_name = G.nodes[node_id].get('name', '').lower() # 检查是否包含溶剂名称 if solvent.lower() in node_id.lower() or solvent.lower() in node_name: - debug_print(f" 🎉 通过模糊匹配找到容器: {node_id} ✨") + debug_print(f"✅ 通过模糊匹配找到容器: {node_id}") return node_id - debug_print(" 😞 模糊匹配失败,尝试液体类型匹配... 🧪") + debug_print("⚠️ 模糊匹配失败,尝试液体类型匹配...") # 第三步:通过液体类型匹配 - debug_print(" 🧪 步骤3: 液体类型匹配...") + debug_print("📋 方法3: 液体类型匹配...") for node_id in G.nodes(): if G.nodes[node_id].get('type') == 'container': vessel_data = G.nodes[node_id].get('data', {}) @@ -68,11 +123,11 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: reagent_name = vessel_data.get('reagent_name', '').lower() if solvent.lower() in liquid_type or solvent.lower() in reagent_name: - debug_print(f" 🎉 通过液体类型匹配找到容器: {node_id} ✨") + debug_print(f"✅ 通过液体类型匹配找到容器: {node_id}") return node_id # 列出可用容器帮助调试 - debug_print(" 📊 显示可用容器信息...") + debug_print("📊 显示可用容器信息...") available_containers = [] for node_id in G.nodes(): if G.nodes[node_id].get('type') == 'container': @@ -88,30 +143,31 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: 'reagent_name': vessel_data.get('reagent_name', '') }) - debug_print(f" 📋 可用容器列表 (共{len(available_containers)}个):") + debug_print(f"📋 可用容器列表 (共{len(available_containers)}个):") for i, container in enumerate(available_containers[:5]): # 只显示前5个 - debug_print(f" {i+1}. 🥽 {container['id']}: {container['name']}") - debug_print(f" 💧 液体: {container['liquids']}") - debug_print(f" 🧪 试剂: {container['reagent_name']}") + debug_print(f" {i+1}. 🥽 {container['id']}: {container['name']}") + debug_print(f" 💧 液体: {container['liquids']}") + debug_print(f" 🧪 试剂: {container['reagent_name']}") if len(available_containers) > 5: - debug_print(f" ... 还有 {len(available_containers)-5} 个容器 📦") + debug_print(f" ... 还有 {len(available_containers)-5} 个容器") - debug_print(f"❌ 找不到溶剂 '{solvent}' 对应的容器 😭") + debug_print(f"❌ 找不到溶剂 '{solvent}' 对应的容器") raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器。尝试了: {possible_names[:3]}...") - def generate_reset_handling_protocol( G: nx.DiGraph, solvent: str, + vessel: Optional[str] = None, # 🆕 新增可选vessel参数 **kwargs # 接收其他可能的参数但不使用 ) -> List[Dict[str, Any]]: """ - 生成重置处理协议序列 + 生成重置处理协议序列 - 支持自定义容器 Args: G: 有向图,节点为容器和设备 solvent: 溶剂名称(从XDL传入) + vessel: 目标容器名称(可选,默认为 "main_reactor") **kwargs: 其他可选参数,但不使用 Returns: @@ -119,51 +175,79 @@ def generate_reset_handling_protocol( """ action_sequence = [] - # 固定参数 - target_vessel = "main_reactor" # 默认目标容器 + # 🔧 修改:支持自定义vessel参数 + target_vessel = vessel if vessel is not None else "main_reactor" # 默认目标容器 volume = 50.0 # 默认体积 50 mL - debug_print("🔄" * 20) - debug_print("🚀 开始生成重置处理协议 ✨") - debug_print(f"📝 输入参数:") + debug_print("=" * 60) + debug_print("🚀 开始生成重置处理协议") + debug_print(f"📋 输入参数:") debug_print(f" 🧪 溶剂: {solvent}") - debug_print(f" 🥽 目标容器: {target_vessel}") + debug_print(f" 🥽 目标容器: {target_vessel} {'(默认)' if vessel is None else '(指定)'}") debug_print(f" 💧 体积: {volume} mL") debug_print(f" ⚙️ 其他参数: {kwargs}") - debug_print("🔄" * 20) + debug_print("=" * 60) + + # 添加初始日志 + action_sequence.append(create_action_log(f"开始重置处理操作 - 容器: {target_vessel}", "🎬")) + action_sequence.append(create_action_log(f"使用溶剂: {solvent}", "🧪")) + action_sequence.append(create_action_log(f"重置体积: {volume}mL", "💧")) + + if vessel is None: + action_sequence.append(create_action_log("使用默认目标容器: main_reactor", "⚙️")) + else: + action_sequence.append(create_action_log(f"使用指定目标容器: {vessel}", "🎯")) # 1. 验证目标容器存在 - debug_print("📍 步骤1: 验证目标容器... 🔧") + debug_print("🔍 步骤1: 验证目标容器...") + action_sequence.append(create_action_log("正在验证目标容器...", "🔍")) + if target_vessel not in G.nodes(): - debug_print(f"❌ 目标容器 '{target_vessel}' 不存在于系统中! 😱") + debug_print(f"❌ 目标容器 '{target_vessel}' 不存在于系统中!") + action_sequence.append(create_action_log(f"目标容器 '{target_vessel}' 不存在", "❌")) raise ValueError(f"目标容器 '{target_vessel}' 不存在于系统中") - debug_print(f"✅ 目标容器 '{target_vessel}' 验证通过 🎯") + + debug_print(f"✅ 目标容器 '{target_vessel}' 验证通过") + action_sequence.append(create_action_log(f"目标容器验证通过: {target_vessel}", "✅")) # 2. 查找溶剂容器 - debug_print("📍 步骤2: 查找溶剂容器... 🔍") + debug_print("🔍 步骤2: 查找溶剂容器...") + action_sequence.append(create_action_log("正在查找溶剂容器...", "🔍")) + try: solvent_vessel = find_solvent_vessel(G, solvent) - debug_print(f"🎉 找到溶剂容器: {solvent_vessel} ✨") + debug_print(f"✅ 找到溶剂容器: {solvent_vessel}") + action_sequence.append(create_action_log(f"找到溶剂容器: {solvent_vessel}", "✅")) except ValueError as e: - debug_print(f"❌ 溶剂容器查找失败: {str(e)} 😭") + debug_print(f"❌ 溶剂容器查找失败: {str(e)}") + action_sequence.append(create_action_log(f"溶剂容器查找失败: {str(e)}", "❌")) raise ValueError(f"无法找到溶剂 '{solvent}': {str(e)}") # 3. 验证路径存在 - debug_print("📍 步骤3: 验证传输路径... 🛤️") + debug_print("🔍 步骤3: 验证传输路径...") + action_sequence.append(create_action_log("正在验证传输路径...", "🛤️")) + try: path = nx.shortest_path(G, source=solvent_vessel, target=target_vessel) - debug_print(f"🛤️ 找到路径: {' → '.join(path)} ✅") + debug_print(f"✅ 找到路径: {' → '.join(path)}") + action_sequence.append(create_action_log(f"传输路径: {' → '.join(path)}", "🛤️")) except nx.NetworkXNoPath: - debug_print(f"❌ 路径不可达: {solvent_vessel} → {target_vessel} 😞") + debug_print(f"❌ 路径不可达: {solvent_vessel} → {target_vessel}") + action_sequence.append(create_action_log(f"路径不可达: {solvent_vessel} → {target_vessel}", "❌")) raise ValueError(f"从溶剂容器 '{solvent_vessel}' 到目标容器 '{target_vessel}' 没有可用路径") # 4. 使用pump_protocol转移溶剂 - debug_print("📍 步骤4: 转移溶剂... 🚰") - debug_print(f" 🚛 开始转移: {solvent_vessel} → {target_vessel}") - debug_print(f" 💧 转移体积: {volume} mL") + debug_print("🔍 步骤4: 转移溶剂...") + action_sequence.append(create_action_log("开始溶剂转移操作...", "🚰")) + + debug_print(f"🚛 开始转移: {solvent_vessel} → {target_vessel}") + debug_print(f"💧 转移体积: {volume} mL") + action_sequence.append(create_action_log(f"转移: {solvent_vessel} → {target_vessel} ({volume}mL)", "🚛")) try: - debug_print(" 🔄 生成泵送协议...") + debug_print("🔄 生成泵送协议...") + action_sequence.append(create_action_log("正在生成泵送协议...", "🔄")) + pump_actions = generate_pump_protocol_with_rinsing( G=G, from_vessel=solvent_vessel, @@ -181,60 +265,104 @@ def generate_reset_handling_protocol( ) action_sequence.extend(pump_actions) - debug_print(f" ✅ 泵送协议已添加: {len(pump_actions)} 个动作 🚰✨") + debug_print(f"✅ 泵送协议已添加: {len(pump_actions)} 个动作") + action_sequence.append(create_action_log(f"泵送协议完成 ({len(pump_actions)} 个操作)", "✅")) except Exception as e: - debug_print(f" ❌ 泵送协议生成失败: {str(e)} 😭") + debug_print(f"❌ 泵送协议生成失败: {str(e)}") + action_sequence.append(create_action_log(f"泵送协议生成失败: {str(e)}", "❌")) raise ValueError(f"生成泵协议时出错: {str(e)}") # 5. 等待溶剂稳定 - debug_print("📍 步骤5: 等待溶剂稳定... ⏳") + debug_print("🔍 步骤5: 等待溶剂稳定...") + action_sequence.append(create_action_log("等待溶剂稳定...", "⏳")) - # 🕐 模拟运行时间优化 - debug_print(" ⏱️ 检查模拟运行时间限制...") + # 模拟运行时间优化 + debug_print("⏱️ 检查模拟运行时间限制...") original_wait_time = 10.0 # 原始等待时间 simulation_time_limit = 5.0 # 模拟运行时间限制:5秒 final_wait_time = min(original_wait_time, simulation_time_limit) if original_wait_time > simulation_time_limit: - debug_print(f" 🎮 模拟运行优化: {original_wait_time}s → {final_wait_time}s ⚡") - debug_print(f" 📊 时间缩短: {original_wait_time}s → {final_wait_time}s 🚀") + debug_print(f"🎮 模拟运行优化: {original_wait_time}s → {final_wait_time}s") + action_sequence.append(create_action_log(f"时间优化: {original_wait_time}s → {final_wait_time}s", "⚡")) else: - debug_print(f" ✅ 时间在限制内: {final_wait_time}s 保持不变 🎯") + debug_print(f"✅ 时间在限制内: {final_wait_time}s 保持不变") + action_sequence.append(create_action_log(f"等待时间: {final_wait_time}s", "⏰")) action_sequence.append({ "action_name": "wait", "action_kwargs": { "time": final_wait_time, - "description": f"等待溶剂 {solvent} 稳定" + (f" (模拟时间)" if original_wait_time != final_wait_time else "") + "description": f"等待溶剂 {solvent} 在容器 {target_vessel} 中稳定" + (f" (模拟时间)" if original_wait_time != final_wait_time else "") } }) - debug_print(f" ✅ 稳定等待已添加: {final_wait_time}s ⏰✨") + debug_print(f"✅ 稳定等待已添加: {final_wait_time}s") # 显示时间调整信息 if original_wait_time != final_wait_time: - debug_print(f" 🎭 模拟优化说明: 原计划 {original_wait_time}s,实际模拟 {final_wait_time}s ⚡") + debug_print(f"🎭 模拟优化说明: 原计划 {original_wait_time}s,实际模拟 {final_wait_time}s") + action_sequence.append(create_action_log("应用模拟时间优化", "🎭")) - # 🎊 总结 - debug_print("🔄" * 20) - debug_print(f"🎉 重置处理协议生成完成! ✨") - debug_print(f"📊 总动作数: {len(action_sequence)} 个") - debug_print(f"🧪 溶剂: {solvent}") - debug_print(f"🥽 源容器: {solvent_vessel}") - debug_print(f"🥽 目标容器: {target_vessel}") - debug_print(f"💧 转移体积: {volume} mL") - debug_print(f"⏱️ 预计总时间: {(final_wait_time + 5):.0f} 秒 ⌛") - debug_print(f"🎯 已添加 {volume} mL {solvent} 到 {target_vessel} 🚰✨") - debug_print("🔄" * 20) + # 总结 + debug_print("=" * 60) + debug_print(f"🎉 重置处理协议生成完成!") + debug_print(f"📊 总结信息:") + debug_print(f" 📋 总动作数: {len(action_sequence)} 个") + debug_print(f" 🧪 溶剂: {solvent}") + debug_print(f" 🥽 源容器: {solvent_vessel}") + debug_print(f" 🥽 目标容器: {target_vessel} {'(默认)' if vessel is None else '(指定)'}") + debug_print(f" 💧 转移体积: {volume} mL") + debug_print(f" ⏱️ 预计总时间: {(final_wait_time + 5):.0f} 秒") + debug_print(f" 🎯 操作结果: 已添加 {volume} mL {solvent} 到 {target_vessel}") + debug_print("=" * 60) + + # 添加完成日志 + summary_msg = f"重置处理完成: {target_vessel} (使用 {volume}mL {solvent})" + if vessel is None: + summary_msg += " [默认容器]" + else: + summary_msg += " [指定容器]" + + action_sequence.append(create_action_log(summary_msg, "🎉")) return action_sequence +# === 便捷函数 === + +def reset_main_reactor(G: nx.DiGraph, solvent: str = "methanol", **kwargs) -> List[Dict[str, Any]]: + """重置主反应器 (默认行为)""" + debug_print(f"🔄 重置主反应器,使用溶剂: {solvent}") + return generate_reset_handling_protocol(G, solvent=solvent, vessel=None, **kwargs) + +def reset_custom_vessel(G: nx.DiGraph, vessel: str, solvent: str = "methanol", **kwargs) -> List[Dict[str, Any]]: + """重置指定容器""" + debug_print(f"🔄 重置指定容器: {vessel},使用溶剂: {solvent}") + return generate_reset_handling_protocol(G, solvent=solvent, vessel=vessel, **kwargs) + +def reset_with_water(G: nx.DiGraph, vessel: Optional[str] = None, **kwargs) -> List[Dict[str, Any]]: + """使用水重置容器""" + target = vessel or "main_reactor" + debug_print(f"💧 使用水重置容器: {target}") + return generate_reset_handling_protocol(G, solvent="water", vessel=vessel, **kwargs) + +def reset_with_methanol(G: nx.DiGraph, vessel: Optional[str] = None, **kwargs) -> List[Dict[str, Any]]: + """使用甲醇重置容器""" + target = vessel or "main_reactor" + debug_print(f"🧪 使用甲醇重置容器: {target}") + return generate_reset_handling_protocol(G, solvent="methanol", vessel=vessel, **kwargs) + +def reset_with_ethanol(G: nx.DiGraph, vessel: Optional[str] = None, **kwargs) -> List[Dict[str, Any]]: + """使用乙醇重置容器""" + target = vessel or "main_reactor" + debug_print(f"🧪 使用乙醇重置容器: {target}") + return generate_reset_handling_protocol(G, solvent="ethanol", vessel=vessel, **kwargs) # 测试函数 def test_reset_handling_protocol(): """测试重置处理协议""" - debug_print("🧪 === RESET HANDLING PROTOCOL 测试 === ✨") + debug_print("=== 重置处理协议增强中文版测试 ===") # 测试溶剂名称 debug_print("🧪 测试常用溶剂名称...") @@ -242,8 +370,18 @@ def test_reset_handling_protocol(): for solvent in test_solvents: debug_print(f" 🔍 测试溶剂: {solvent}") - debug_print("✅ 测试完成 🎉") - + # 测试容器参数 + debug_print("🥽 测试容器参数...") + test_cases = [ + {"solvent": "methanol", "vessel": None, "desc": "默认容器"}, + {"solvent": "ethanol", "vessel": "reactor_2", "desc": "指定容器"}, + {"solvent": "water", "vessel": "flask_1", "desc": "自定义容器"} + ] + + for case in test_cases: + debug_print(f" 🧪 测试案例: {case['desc']} - {case['solvent']} -> {case['vessel'] or 'main_reactor'}") + + debug_print("✅ 测试完成") if __name__ == "__main__": test_reset_handling_protocol() \ No newline at end of file diff --git a/unilabos/compile/run_column_protocol.py b/unilabos/compile/run_column_protocol.py index cb55f86..f921321 100644 --- a/unilabos/compile/run_column_protocol.py +++ b/unilabos/compile/run_column_protocol.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) def debug_print(message): """调试输出""" - print(f"[RUN_COLUMN] {message}", flush=True) + print(f"🏛️ [RUN_COLUMN] {message}", flush=True) logger.info(f"[RUN_COLUMN] {message}") def parse_percentage(pct_str: str) -> float: @@ -25,7 +25,7 @@ def parse_percentage(pct_str: str) -> float: return 0.0 pct_str = pct_str.strip().lower() - debug_print(f"解析百分比: '{pct_str}'") + debug_print(f"🔍 解析百分比: '{pct_str}'") # 移除百分号和空格 pct_clean = re.sub(r'[%\s]', '', pct_str) @@ -34,7 +34,7 @@ def parse_percentage(pct_str: str) -> float: match = re.search(r'([0-9]*\.?[0-9]+)', pct_clean) if match: value = float(match.group(1)) - debug_print(f"百分比解析结果: {value}%") + debug_print(f"✅ 百分比解析结果: {value}%") return value debug_print(f"⚠️ 无法解析百分比: '{pct_str}',返回0.0") @@ -54,7 +54,7 @@ def parse_ratio(ratio_str: str) -> tuple: return (50.0, 50.0) # 默认1:1 ratio_str = ratio_str.strip() - debug_print(f"解析比例: '{ratio_str}'") + debug_print(f"🔍 解析比例: '{ratio_str}'") # 支持多种分隔符:: / - if ':' in ratio_str: @@ -79,7 +79,7 @@ def parse_ratio(ratio_str: str) -> tuple: pct1 = (ratio1 / total) * 100 pct2 = (ratio2 / total) * 100 - debug_print(f"比例解析结果: {ratio1}:{ratio2} -> {pct1:.1f}%:{pct2:.1f}%") + debug_print(f"✅ 比例解析结果: {ratio1}:{ratio2} -> {pct1:.1f}%:{pct2:.1f}%") return (pct1, pct2) except ValueError as e: debug_print(f"⚠️ 比例数值转换失败: {str(e)}") @@ -101,12 +101,12 @@ def parse_rf_value(rf_str: str) -> float: return 0.3 # 默认Rf值 rf_str = rf_str.strip().lower() - debug_print(f"解析Rf值: '{rf_str}'") + debug_print(f"🔍 解析Rf值: '{rf_str}'") # 处理未知Rf值 if rf_str in ['?', 'unknown', 'tbd', 'to be determined']: default_rf = 0.3 - debug_print(f"检测到未知Rf值,使用默认值: {default_rf}") + debug_print(f"❓ 检测到未知Rf值,使用默认值: {default_rf}") return default_rf # 提取数字 @@ -117,7 +117,7 @@ def parse_rf_value(rf_str: str) -> float: if value > 1.0: value = value / 100.0 # 可能是百分比形式 value = max(0.0, min(1.0, value)) # 限制在0-1范围 - debug_print(f"Rf值解析结果: {value}") + debug_print(f"✅ Rf值解析结果: {value}") return value debug_print(f"⚠️ 无法解析Rf值: '{rf_str}',使用默认值0.3") @@ -125,7 +125,7 @@ def parse_rf_value(rf_str: str) -> float: def find_column_device(G: nx.DiGraph) -> str: """查找柱层析设备""" - debug_print("查找柱层析设备...") + debug_print("🔍 查找柱层析设备...") # 查找虚拟柱设备 for node in G.nodes(): @@ -133,14 +133,14 @@ def find_column_device(G: nx.DiGraph) -> str: node_class = node_data.get('class', '') or '' if 'virtual_column' in node_class.lower() or 'column' in node_class.lower(): - debug_print(f"✅ 找到柱层析设备: {node}") + debug_print(f"🎉 找到柱层析设备: {node} ✨") return node # 如果没有找到,尝试创建虚拟设备名称 possible_names = ['column_1', 'virtual_column_1', 'chromatography_column_1'] for name in possible_names: if name in G.nodes(): - debug_print(f"✅ 找到柱设备: {name}") + debug_print(f"🎉 找到柱设备: {name} ✨") return name debug_print("⚠️ 未找到柱层析设备,将使用pump protocol直接转移") @@ -148,13 +148,13 @@ def find_column_device(G: nx.DiGraph) -> str: def find_column_vessel(G: nx.DiGraph, column: str) -> str: """查找柱容器""" - debug_print(f"查找柱容器: '{column}'") + debug_print(f"🔍 查找柱容器: '{column}'") # 直接检查column参数是否是容器 if column in G.nodes(): node_type = G.nodes[column].get('type', '') if node_type == 'container': - debug_print(f"✅ 找到柱容器: {column}") + debug_print(f"🎉 找到柱容器: {column} ✨") return column # 尝试常见的命名规则 @@ -174,7 +174,7 @@ def find_column_vessel(G: nx.DiGraph, column: str) -> str: if vessel_name in G.nodes(): node_type = G.nodes[vessel_name].get('type', '') if node_type == 'container': - debug_print(f"✅ 找到柱容器: {vessel_name}") + debug_print(f"🎉 找到柱容器: {vessel_name} ✨") return vessel_name debug_print(f"⚠️ 未找到柱容器,将直接在源容器中进行分离") @@ -186,7 +186,7 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: return "" solvent = solvent.strip().replace(' ', '_').lower() - debug_print(f"查找溶剂容器: '{solvent}'") + debug_print(f"🔍 查找溶剂容器: '{solvent}'") # 🔧 方法1:直接搜索 data.reagent_name for node in G.nodes(): @@ -200,16 +200,16 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: # 检查 data.reagent_name 和 config.reagent if reagent_name == solvent or reagent_config == solvent: - debug_print(f"✅ 通过reagent_name找到溶剂容器: {node} (reagent: {reagent_name or reagent_config})") + debug_print(f"🎉 通过reagent_name找到溶剂容器: {node} (reagent: {reagent_name or reagent_config}) ✨") return node # 模糊匹配 reagent_name if solvent in reagent_name or reagent_name in solvent: - debug_print(f"✅ 通过reagent_name模糊匹配到溶剂容器: {node} (reagent: {reagent_name})") + debug_print(f"🎉 通过reagent_name模糊匹配到溶剂容器: {node} (reagent: {reagent_name}) ✨") return node if solvent in reagent_config or reagent_config in solvent: - debug_print(f"✅ 通过config.reagent模糊匹配到溶剂容器: {node} (reagent: {reagent_config})") + debug_print(f"🎉 通过config.reagent模糊匹配到溶剂容器: {node} (reagent: {reagent_config}) ✨") return node # 🔧 方法2:常见的溶剂容器命名规则 @@ -227,7 +227,7 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: if vessel_name in G.nodes(): node_type = G.nodes[vessel_name].get('type', '') if node_type == 'container': - debug_print(f"✅ 通过命名规则找到溶剂容器: {vessel_name}") + debug_print(f"🎉 通过命名规则找到溶剂容器: {vessel_name} ✨") return vessel_name # 🔧 方法3:节点名称模糊匹配 @@ -235,7 +235,7 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: node_type = G.nodes[node].get('type', '') if node_type == 'container': if ('flask_' in node or 'bottle_' in node or 'reagent_' in node) and solvent in node.lower(): - debug_print(f"✅ 通过节点名称模糊匹配到溶剂容器: {node}") + debug_print(f"🎉 通过节点名称模糊匹配到溶剂容器: {node} ✨") return node # 🔧 方法4:特殊溶剂名称映射 @@ -253,84 +253,118 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: # 查找映射的同义词 for canonical_name, synonyms in solvent_mapping.items(): if solvent in synonyms: - debug_print(f"检测到溶剂同义词: '{solvent}' -> '{canonical_name}'") + debug_print(f"🔍 检测到溶剂同义词: '{solvent}' -> '{canonical_name}'") return find_solvent_vessel(G, canonical_name) # 递归搜索 debug_print(f"⚠️ 未找到溶剂 '{solvent}' 的容器") return "" -def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float: - """获取容器中的液体体积 - 增强版""" - if vessel not in G.nodes(): - debug_print(f"⚠️ 节点 '{vessel}' 不存在") +def get_vessel_liquid_volume(vessel: dict) -> float: + """ + 获取容器中的液体体积 - 支持vessel字典 + + Args: + vessel: 容器字典 + + Returns: + float: 液体体积(mL) + """ + if not vessel or "data" not in vessel: + debug_print(f"⚠️ 容器数据为空,返回 0.0mL") return 0.0 - node_type = G.nodes[vessel].get('type', '') - vessel_data = G.nodes[vessel].get('data', {}) + vessel_data = vessel["data"] + vessel_id = vessel.get("id", "unknown") - debug_print(f"读取节点 '{vessel}' (类型: {node_type}) 体积数据: {vessel_data}") + debug_print(f"🔍 读取容器 '{vessel_id}' 体积数据: {vessel_data}") - # 🔧 如果是设备类型,尝试查找关联的容器 - if node_type == 'device': - debug_print(f"'{vessel}' 是设备,尝试查找关联容器...") + # 检查liquid_volume字段 + if "liquid_volume" in vessel_data: + liquid_volume = vessel_data["liquid_volume"] - # 查找是否有内置容器数据 - config_data = G.nodes[vessel].get('config', {}) - if 'volume' in config_data: - default_volume = config_data.get('volume', 50.0) - debug_print(f"使用设备默认容量: {default_volume}mL") - return default_volume + # 处理列表格式 + if isinstance(liquid_volume, list): + if len(liquid_volume) > 0: + volume = liquid_volume[0] + if isinstance(volume, (int, float)): + debug_print(f"✅ 容器 '{vessel_id}' 体积: {volume}mL (列表格式)") + return float(volume) - # 对于旋蒸等设备,使用默认值 - if 'rotavap' in vessel.lower(): - default_volume = 50.0 - debug_print(f"旋蒸设备使用默认容量: {default_volume}mL") - return default_volume - - debug_print(f"⚠️ 设备 '{vessel}' 无法确定容量,返回0") - return 0.0 + # 处理直接数值格式 + elif isinstance(liquid_volume, (int, float)): + debug_print(f"✅ 容器 '{vessel_id}' 体积: {liquid_volume}mL (数值格式)") + return float(liquid_volume) - # 🔧 如果是容器类型,正常读取体积 - total_volume = 0.0 - - # 方法1:检查液体列表 - liquids = vessel_data.get('liquid', []) - if isinstance(liquids, list): - for liquid in liquids: - if isinstance(liquid, dict): - volume = liquid.get('volume') or liquid.get('liquid_volume', 0.0) - total_volume += volume - - # 方法2:检查直接体积字段 - if total_volume == 0.0: - volume_keys = ['current_volume', 'total_volume', 'volume', 'liquid_volume'] - for key in volume_keys: - if key in vessel_data: - try: - total_volume = float(vessel_data[key]) - if total_volume > 0: - break - except (ValueError, TypeError): - continue - - # 方法3:检查配置中的初始体积 - if total_volume == 0.0: - config_data = G.nodes[vessel].get('config', {}) - if 'current_volume' in config_data: + # 检查其他可能的体积字段 + volume_keys = ['current_volume', 'total_volume', 'volume'] + for key in volume_keys: + if key in vessel_data: try: - total_volume = float(config_data['current_volume']) + volume = float(vessel_data[key]) + if volume > 0: + debug_print(f"✅ 容器 '{vessel_id}' 体积: {volume}mL (字段: {key})") + return volume except (ValueError, TypeError): - pass + continue - debug_print(f"容器 '{vessel}' 总体积: {total_volume}mL") - return total_volume + debug_print(f"⚠️ 无法获取容器 '{vessel_id}' 的体积,返回默认值 50.0mL") + return 50.0 + +def update_vessel_volume(vessel: dict, G: nx.DiGraph, new_volume: float, description: str = "") -> None: + """ + 更新容器体积(同时更新vessel字典和图节点) + + Args: + vessel: 容器字典 + G: 网络图 + new_volume: 新体积 + description: 更新描述 + """ + vessel_id = vessel.get("id", "unknown") + + if description: + debug_print(f"🔧 更新容器体积 - {description}") + + # 更新vessel字典中的体积 + if "data" in vessel: + 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] = new_volume + else: + vessel["data"]["liquid_volume"] = [new_volume] + else: + vessel["data"]["liquid_volume"] = new_volume + else: + vessel["data"]["liquid_volume"] = new_volume + else: + vessel["data"] = {"liquid_volume": new_volume} + + # 同时更新图中的容器数据 + 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] = new_volume + else: + G.nodes[vessel_id]['data']['liquid_volume'] = [new_volume] + else: + G.nodes[vessel_id]['data']['liquid_volume'] = new_volume + + debug_print(f"📊 容器 '{vessel_id}' 体积已更新为: {new_volume:.2f}mL") def calculate_solvent_volumes(total_volume: float, pct1: float, pct2: float) -> tuple: """根据百分比计算溶剂体积""" volume1 = (total_volume * pct1) / 100.0 volume2 = (total_volume * pct2) / 100.0 - debug_print(f"溶剂体积计算: 总体积{total_volume}mL") + debug_print(f"🧮 溶剂体积计算: 总体积{total_volume}mL") debug_print(f" - 溶剂1: {pct1}% = {volume1}mL") debug_print(f" - 溶剂2: {pct2}% = {volume2}mL") @@ -338,8 +372,8 @@ def calculate_solvent_volumes(total_volume: float, pct1: float, pct2: float) -> def generate_run_column_protocol( G: nx.DiGraph, - from_vessel: str, - to_vessel: str, + from_vessel: dict, # 🔧 修改:从字符串改为字典类型 + to_vessel: dict, # 🔧 修改:从字符串改为字典类型 column: str, rf: str = "", pct1: str = "", @@ -350,14 +384,12 @@ def generate_run_column_protocol( **kwargs ) -> List[Dict[str, Any]]: """ - 生成柱层析分离的协议序列 - 增强版 - - 支持新版XDL的所有参数,具有高兼容性和容错性 + 生成柱层析分离的协议序列 - 支持vessel字典和体积运算 Args: G: 有向图,节点为设备和容器,边为流体管道 - from_vessel: 源容器的名称,即样品起始所在的容器(必需) - to_vessel: 目标容器的名称,分离后的样品要到达的容器(必需) + from_vessel: 源容器字典(从XDL传入) + to_vessel: 目标容器字典(从XDL传入) column: 所使用的柱子的名称(必需) rf: Rf值(可选,支持 "?" 表示未知) pct1: 第一种溶剂百分比(如 "40 %",可选) @@ -371,51 +403,60 @@ def generate_run_column_protocol( List[Dict[str, Any]]: 柱层析分离操作的动作序列 """ - debug_print("=" * 60) - debug_print("开始生成柱层析协议") - debug_print(f"输入参数:") - debug_print(f" - from_vessel: '{from_vessel}'") - debug_print(f" - to_vessel: '{to_vessel}'") - debug_print(f" - column: '{column}'") - debug_print(f" - rf: '{rf}'") - debug_print(f" - pct1: '{pct1}'") - debug_print(f" - pct2: '{pct2}'") - debug_print(f" - solvent1: '{solvent1}'") - debug_print(f" - solvent2: '{solvent2}'") - debug_print(f" - ratio: '{ratio}'") - debug_print(f" - 其他参数: {kwargs}") - debug_print("=" * 60) + # 🔧 核心修改:从字典中提取容器ID + from_vessel_id = from_vessel["id"] + to_vessel_id = to_vessel["id"] + + debug_print("🏛️" * 20) + debug_print("🚀 开始生成柱层析协议(支持vessel字典和体积运算)✨") + debug_print(f"📝 输入参数:") + debug_print(f" 🥽 from_vessel: {from_vessel} (ID: {from_vessel_id})") + debug_print(f" 🥽 to_vessel: {to_vessel} (ID: {to_vessel_id})") + debug_print(f" 🏛️ column: '{column}'") + debug_print(f" 📊 rf: '{rf}'") + debug_print(f" 🧪 溶剂配比: pct1='{pct1}', pct2='{pct2}', ratio='{ratio}'") + debug_print(f" 🧪 溶剂名称: solvent1='{solvent1}', solvent2='{solvent2}'") + debug_print("🏛️" * 20) action_sequence = [] - # === 参数验证 === - debug_print("步骤1: 参数验证...") + # 🔧 新增:记录柱层析前的容器状态 + debug_print("🔍 记录柱层析前容器状态...") + original_from_volume = get_vessel_liquid_volume(from_vessel) + original_to_volume = get_vessel_liquid_volume(to_vessel) - if not from_vessel: + debug_print(f"📊 柱层析前状态:") + debug_print(f" - 源容器 {from_vessel_id}: {original_from_volume:.2f}mL") + debug_print(f" - 目标容器 {to_vessel_id}: {original_to_volume:.2f}mL") + + # === 参数验证 === + debug_print("📍 步骤1: 参数验证...") + + if not from_vessel_id: # 🔧 使用 from_vessel_id raise ValueError("from_vessel 参数不能为空") - if not to_vessel: + if not to_vessel_id: # 🔧 使用 to_vessel_id raise ValueError("to_vessel 参数不能为空") if not column: raise ValueError("column 参数不能为空") - if from_vessel not in G.nodes(): - raise ValueError(f"源容器 '{from_vessel}' 不存在于系统中") - if to_vessel not in G.nodes(): - raise ValueError(f"目标容器 '{to_vessel}' 不存在于系统中") + if from_vessel_id not in G.nodes(): # 🔧 使用 from_vessel_id + raise ValueError(f"源容器 '{from_vessel_id}' 不存在于系统中") + if to_vessel_id not in G.nodes(): # 🔧 使用 to_vessel_id + raise ValueError(f"目标容器 '{to_vessel_id}' 不存在于系统中") debug_print("✅ 基本参数验证通过") # === 参数解析 === - debug_print("步骤2: 参数解析...") + debug_print("📍 步骤2: 参数解析...") # 解析Rf值 final_rf = parse_rf_value(rf) - debug_print(f"最终Rf值: {final_rf}") + debug_print(f"🎯 最终Rf值: {final_rf}") # 解析溶剂比例(ratio优先级高于pct1/pct2) if ratio and ratio.strip(): final_pct1, final_pct2 = parse_ratio(ratio) - debug_print(f"使用ratio参数: {final_pct1:.1f}% : {final_pct2:.1f}%") + debug_print(f"📊 使用ratio参数: {final_pct1:.1f}% : {final_pct2:.1f}%") else: final_pct1 = parse_percentage(pct1) if pct1 else 50.0 final_pct2 = parse_percentage(pct2) if pct2 else 50.0 @@ -428,16 +469,16 @@ def generate_run_column_protocol( final_pct1 = (final_pct1 / total_pct) * 100 final_pct2 = (final_pct2 / total_pct) * 100 - debug_print(f"使用百分比参数: {final_pct1:.1f}% : {final_pct2:.1f}%") + debug_print(f"📊 使用百分比参数: {final_pct1:.1f}% : {final_pct2:.1f}%") # 设置默认溶剂(如果未指定) final_solvent1 = solvent1.strip() if solvent1 else "ethyl_acetate" final_solvent2 = solvent2.strip() if solvent2 else "hexane" - debug_print(f"最终溶剂: {final_solvent1} : {final_solvent2}") + debug_print(f"🧪 最终溶剂: {final_solvent1} : {final_solvent2}") # === 查找设备和容器 === - debug_print("步骤3: 查找设备和容器...") + debug_print("📍 步骤3: 查找设备和容器...") # 查找柱层析设备 column_device_id = find_column_device(G) @@ -449,16 +490,16 @@ def generate_run_column_protocol( solvent1_vessel = find_solvent_vessel(G, final_solvent1) solvent2_vessel = find_solvent_vessel(G, final_solvent2) - debug_print(f"设备映射:") + debug_print(f"🔧 设备映射:") debug_print(f" - 柱设备: '{column_device_id}'") debug_print(f" - 柱容器: '{column_vessel}'") debug_print(f" - 溶剂1容器: '{solvent1_vessel}'") debug_print(f" - 溶剂2容器: '{solvent2_vessel}'") # === 获取源容器体积 === - debug_print("步骤4: 获取源容器体积...") + debug_print("📍 步骤4: 获取源容器体积...") - source_volume = get_vessel_liquid_volume(G, from_vessel) + source_volume = original_from_volume if source_volume <= 0: source_volume = 50.0 # 默认体积 debug_print(f"⚠️ 无法获取源容器体积,使用默认值: {source_volume}mL") @@ -466,7 +507,7 @@ def generate_run_column_protocol( debug_print(f"✅ 源容器体积: {source_volume}mL") # === 计算溶剂体积 === - debug_print("步骤5: 计算溶剂体积...") + debug_print("📍 步骤5: 计算溶剂体积...") # 洗脱溶剂通常是样品体积的2-5倍 total_elution_volume = source_volume * 3.0 @@ -475,17 +516,22 @@ def generate_run_column_protocol( ) # === 执行柱层析流程 === - debug_print("步骤6: 执行柱层析流程...") + debug_print("📍 步骤6: 执行柱层析流程...") + + # 🔧 新增:体积变化跟踪变量 + current_from_volume = source_volume + current_to_volume = original_to_volume + current_column_volume = 0.0 try: # 步骤6.1: 样品上柱(如果有独立的柱容器) - if column_vessel and column_vessel != from_vessel: - debug_print(f"6.1: 样品上柱 - {source_volume}mL 从 {from_vessel} 到 {column_vessel}") + if column_vessel and column_vessel != from_vessel_id: # 🔧 使用 from_vessel_id + debug_print(f"📍 6.1: 样品上柱 - {source_volume}mL 从 {from_vessel_id} 到 {column_vessel}") try: sample_transfer_actions = generate_pump_protocol_with_rinsing( G=G, - from_vessel=from_vessel, + from_vessel=from_vessel_id, # 🔧 使用 from_vessel_id to_vessel=column_vessel, volume=source_volume, flowrate=1.0, # 慢速上柱 @@ -496,15 +542,29 @@ def generate_run_column_protocol( ) action_sequence.extend(sample_transfer_actions) debug_print(f"✅ 样品上柱完成,添加了 {len(sample_transfer_actions)} 个动作") + + # 🔧 新增:更新体积 - 样品转移到柱上 + current_from_volume = 0.0 # 源容器体积变为0 + current_column_volume = source_volume # 柱容器体积增加 + + update_vessel_volume(from_vessel, G, current_from_volume, "样品上柱后,源容器清空") + + # 如果柱容器在图中,也更新其体积 + if column_vessel in G.nodes(): + if 'data' not in G.nodes[column_vessel]: + G.nodes[column_vessel]['data'] = {} + G.nodes[column_vessel]['data']['liquid_volume'] = current_column_volume + debug_print(f"📊 柱容器 '{column_vessel}' 体积更新为: {current_column_volume:.2f}mL") + except Exception as e: debug_print(f"⚠️ 样品上柱失败: {str(e)}") # 步骤6.2: 添加洗脱溶剂1(如果有溶剂容器) if solvent1_vessel and solvent1_volume > 0: - debug_print(f"6.2: 添加洗脱溶剂1 - {solvent1_volume:.1f}mL {final_solvent1}") + debug_print(f"📍 6.2: 添加洗脱溶剂1 - {solvent1_volume:.1f}mL {final_solvent1}") try: - target_vessel = column_vessel if column_vessel else from_vessel + target_vessel = column_vessel if column_vessel else from_vessel_id # 🔧 使用 from_vessel_id solvent1_transfer_actions = generate_pump_protocol_with_rinsing( G=G, from_vessel=solvent1_vessel, @@ -515,15 +575,26 @@ def generate_run_column_protocol( ) action_sequence.extend(solvent1_transfer_actions) debug_print(f"✅ 溶剂1添加完成,添加了 {len(solvent1_transfer_actions)} 个动作") + + # 🔧 新增:更新体积 - 添加溶剂1 + if target_vessel == column_vessel: + current_column_volume += solvent1_volume + if column_vessel in G.nodes(): + G.nodes[column_vessel]['data']['liquid_volume'] = current_column_volume + debug_print(f"📊 柱容器体积增加: +{solvent1_volume:.2f}mL = {current_column_volume:.2f}mL") + elif target_vessel == from_vessel_id: + current_from_volume += solvent1_volume + update_vessel_volume(from_vessel, G, current_from_volume, "添加溶剂1后") + except Exception as e: debug_print(f"⚠️ 溶剂1添加失败: {str(e)}") # 步骤6.3: 添加洗脱溶剂2(如果有溶剂容器) if solvent2_vessel and solvent2_volume > 0: - debug_print(f"6.3: 添加洗脱溶剂2 - {solvent2_volume:.1f}mL {final_solvent2}") + debug_print(f"📍 6.3: 添加洗脱溶剂2 - {solvent2_volume:.1f}mL {final_solvent2}") try: - target_vessel = column_vessel if column_vessel else from_vessel + target_vessel = column_vessel if column_vessel else from_vessel_id # 🔧 使用 from_vessel_id solvent2_transfer_actions = generate_pump_protocol_with_rinsing( G=G, from_vessel=solvent2_vessel, @@ -534,19 +605,30 @@ def generate_run_column_protocol( ) action_sequence.extend(solvent2_transfer_actions) debug_print(f"✅ 溶剂2添加完成,添加了 {len(solvent2_transfer_actions)} 个动作") + + # 🔧 新增:更新体积 - 添加溶剂2 + if target_vessel == column_vessel: + current_column_volume += solvent2_volume + if column_vessel in G.nodes(): + G.nodes[column_vessel]['data']['liquid_volume'] = current_column_volume + debug_print(f"📊 柱容器体积增加: +{solvent2_volume:.2f}mL = {current_column_volume:.2f}mL") + elif target_vessel == from_vessel_id: + current_from_volume += solvent2_volume + update_vessel_volume(from_vessel, G, current_from_volume, "添加溶剂2后") + except Exception as e: debug_print(f"⚠️ 溶剂2添加失败: {str(e)}") # 步骤6.4: 使用柱层析设备执行分离(如果有设备) if column_device_id: - debug_print(f"6.4: 使用柱层析设备执行分离") + debug_print(f"📍 6.4: 使用柱层析设备执行分离") column_separation_action = { "device_id": column_device_id, "action_name": "run_column", "action_kwargs": { - "from_vessel": from_vessel, - "to_vessel": to_vessel, + "from_vessel": from_vessel_id, # 🔧 使用 from_vessel_id + "to_vessel": to_vessel_id, # 🔧 使用 to_vessel_id "column": column, "rf": rf, "pct1": pct1, @@ -560,7 +642,7 @@ def generate_run_column_protocol( debug_print(f"✅ 柱层析设备动作已添加") # 等待分离完成 - separation_time = max(30, int(total_elution_volume / 2)) # 基于体积估算时间 + separation_time = max(30, min(120, int(total_elution_volume / 2))) # 30-120秒,基于体积 action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": separation_time} @@ -568,41 +650,61 @@ def generate_run_column_protocol( debug_print(f"✅ 等待分离完成: {separation_time}秒") # 步骤6.5: 产物收集(从柱容器到目标容器) - if column_vessel and column_vessel != to_vessel: - debug_print(f"6.5: 产物收集 - 从 {column_vessel} 到 {to_vessel}") + if column_vessel and column_vessel != to_vessel_id: # 🔧 使用 to_vessel_id + debug_print(f"📍 6.5: 产物收集 - 从 {column_vessel} 到 {to_vessel_id}") try: - # 估算产物体积(原始样品体积的70-90%) + # 估算产物体积(原始样品体积的70-90%,收率考虑) product_volume = source_volume * 0.8 product_transfer_actions = generate_pump_protocol_with_rinsing( G=G, from_vessel=column_vessel, - to_vessel=to_vessel, + to_vessel=to_vessel_id, # 🔧 使用 to_vessel_id volume=product_volume, flowrate=1.5, transfer_flowrate=0.8 ) action_sequence.extend(product_transfer_actions) debug_print(f"✅ 产物收集完成,添加了 {len(product_transfer_actions)} 个动作") + + # 🔧 新增:更新体积 - 产物收集到目标容器 + current_to_volume += product_volume + current_column_volume -= product_volume # 柱容器体积减少 + + update_vessel_volume(to_vessel, G, current_to_volume, "产物收集后") + + # 更新柱容器体积 + if column_vessel in G.nodes(): + G.nodes[column_vessel]['data']['liquid_volume'] = max(0.0, current_column_volume) + debug_print(f"📊 柱容器体积减少: -{product_volume:.2f}mL = {current_column_volume:.2f}mL") + except Exception as e: debug_print(f"⚠️ 产物收集失败: {str(e)}") # 步骤6.6: 如果没有独立的柱设备和容器,执行简化的直接转移 if not column_device_id and not column_vessel: - debug_print(f"6.6: 简化模式 - 直接转移 {source_volume}mL 从 {from_vessel} 到 {to_vessel}") + debug_print(f"📍 6.6: 简化模式 - 直接转移 {source_volume}mL 从 {from_vessel_id} 到 {to_vessel_id}") try: direct_transfer_actions = generate_pump_protocol_with_rinsing( G=G, - from_vessel=from_vessel, - to_vessel=to_vessel, + from_vessel=from_vessel_id, # 🔧 使用 from_vessel_id + to_vessel=to_vessel_id, # 🔧 使用 to_vessel_id volume=source_volume, flowrate=2.0, transfer_flowrate=1.0 ) action_sequence.extend(direct_transfer_actions) debug_print(f"✅ 直接转移完成,添加了 {len(direct_transfer_actions)} 个动作") + + # 🔧 新增:更新体积 - 直接转移 + current_from_volume = 0.0 # 源容器清空 + current_to_volume += source_volume # 目标容器增加 + + update_vessel_volume(from_vessel, G, current_from_volume, "直接转移后,源容器清空") + update_vessel_volume(to_vessel, G, current_to_volume, "直接转移后,目标容器增加") + except Exception as e: debug_print(f"⚠️ 直接转移失败: {str(e)}") @@ -624,17 +726,77 @@ def generate_run_column_protocol( } }) + # 🔧 新增:柱层析完成后的最终状态报告 + final_from_volume = get_vessel_liquid_volume(from_vessel) + final_to_volume = get_vessel_liquid_volume(to_vessel) + # 🎊 总结 - debug_print("🧪" * 20) + debug_print("🏛️" * 20) debug_print(f"🎉 柱层析协议生成完成! ✨") debug_print(f"📊 总动作数: {len(action_sequence)} 个") - debug_print(f"🥽 路径: {from_vessel} → {to_vessel}") + debug_print(f"🥽 路径: {from_vessel_id} → {to_vessel_id}") debug_print(f"🏛️ 柱子: {column}") - debug_print(f"🧪 溶剂: {final_solvent1}:{final_solvent2}") - debug_print("🧪" * 20) + debug_print(f"🧪 溶剂: {final_solvent1}:{final_solvent2} = {final_pct1:.1f}%:{final_pct2:.1f}%") + debug_print(f"📊 体积变化统计:") + debug_print(f" 源容器 {from_vessel_id}:") + debug_print(f" - 柱层析前: {original_from_volume:.2f}mL") + debug_print(f" - 柱层析后: {final_from_volume:.2f}mL") + debug_print(f" 目标容器 {to_vessel_id}:") + debug_print(f" - 柱层析前: {original_to_volume:.2f}mL") + debug_print(f" - 柱层析后: {final_to_volume:.2f}mL") + debug_print(f" - 收集体积: {final_to_volume - original_to_volume:.2f}mL") + debug_print(f"⏱️ 预计总时间: {len(action_sequence) * 5:.0f} 秒 ⌛") + debug_print("🏛️" * 20) return action_sequence +# 🔧 新增:便捷函数 +def generate_ethyl_acetate_hexane_column_protocol(G: nx.DiGraph, from_vessel: dict, to_vessel: dict, + column: str, ratio: str = "30:70") -> List[Dict[str, Any]]: + """乙酸乙酯-己烷柱层析(常用组合)""" + from_vessel_id = from_vessel["id"] + to_vessel_id = to_vessel["id"] + debug_print(f"🧪⛽ 乙酸乙酯-己烷柱层析: {from_vessel_id} → {to_vessel_id} @ {ratio}") + return generate_run_column_protocol(G, from_vessel, to_vessel, column, + solvent1="ethyl_acetate", solvent2="hexane", ratio=ratio) + +def generate_methanol_dcm_column_protocol(G: nx.DiGraph, from_vessel: dict, to_vessel: dict, + column: str, ratio: str = "5:95") -> List[Dict[str, Any]]: + """甲醇-二氯甲烷柱层析""" + from_vessel_id = from_vessel["id"] + to_vessel_id = to_vessel["id"] + debug_print(f"🧪🧪 甲醇-DCM柱层析: {from_vessel_id} → {to_vessel_id} @ {ratio}") + return generate_run_column_protocol(G, from_vessel, to_vessel, column, + solvent1="methanol", solvent2="dichloromethane", ratio=ratio) + +def generate_gradient_column_protocol(G: nx.DiGraph, from_vessel: dict, to_vessel: dict, + column: str, start_ratio: str = "10:90", + end_ratio: str = "50:50") -> List[Dict[str, Any]]: + """梯度洗脱柱层析(中等比例)""" + from_vessel_id = from_vessel["id"] + to_vessel_id = to_vessel["id"] + debug_print(f"📈 梯度柱层析: {from_vessel_id} → {to_vessel_id} ({start_ratio} → {end_ratio})") + # 使用中间比例作为近似 + return generate_run_column_protocol(G, from_vessel, to_vessel, column, ratio="30:70") + +def generate_polar_column_protocol(G: nx.DiGraph, from_vessel: dict, to_vessel: dict, + column: str) -> List[Dict[str, Any]]: + """极性化合物柱层析(高极性溶剂比例)""" + from_vessel_id = from_vessel["id"] + to_vessel_id = to_vessel["id"] + debug_print(f"⚡ 极性化合物柱层析: {from_vessel_id} → {to_vessel_id}") + return generate_run_column_protocol(G, from_vessel, to_vessel, column, + solvent1="ethyl_acetate", solvent2="hexane", ratio="70:30") + +def generate_nonpolar_column_protocol(G: nx.DiGraph, from_vessel: dict, to_vessel: dict, + column: str) -> List[Dict[str, Any]]: + """非极性化合物柱层析(低极性溶剂比例)""" + from_vessel_id = from_vessel["id"] + to_vessel_id = to_vessel["id"] + debug_print(f"🛢️ 非极性化合物柱层析: {from_vessel_id} → {to_vessel_id}") + return generate_run_column_protocol(G, from_vessel, to_vessel, column, + solvent1="ethyl_acetate", solvent2="hexane", ratio="5:95") + # 测试函数 def test_run_column_protocol(): """测试柱层析协议""" diff --git a/unilabos/compile/separate_protocol.py b/unilabos/compile/separate_protocol.py index 258c37d..ec1f8af 100644 --- a/unilabos/compile/separate_protocol.py +++ b/unilabos/compile/separate_protocol.py @@ -20,18 +20,18 @@ def debug_print(message): try: # 确保消息是字符串格式 safe_message = str(message) - print(f"[分离协议] {safe_message}", flush=True) - logger.info(f"[分离协议] {safe_message}") + print(f"🌀 [SEPARATE] {safe_message}", flush=True) + logger.info(f"[SEPARATE] {safe_message}") except UnicodeEncodeError: # 如果编码失败,尝试替换不支持的字符 safe_message = str(message).encode('utf-8', errors='replace').decode('utf-8') - print(f"[分离协议] {safe_message}", flush=True) - logger.info(f"[分离协议] {safe_message}") + print(f"🌀 [SEPARATE] {safe_message}", flush=True) + logger.info(f"[SEPARATE] {safe_message}") except Exception as e: # 最后的安全措施 fallback_message = f"日志输出错误: {repr(message)}" - print(f"[分离协议] {fallback_message}", flush=True) - logger.info(f"[分离协议] {fallback_message}") + print(f"🌀 [SEPARATE] {fallback_message}", flush=True) + logger.info(f"[SEPARATE] {fallback_message}") def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]: """创建一个动作日志 - 支持中文和emoji""" @@ -264,19 +264,119 @@ def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str: debug_print("❌ 未找到搅拌器") return "" +def get_vessel_liquid_volume(vessel: dict) -> float: + """ + 获取容器中的液体体积 - 支持vessel字典 + + Args: + vessel: 容器字典 + + Returns: + float: 液体体积(mL) + """ + if not vessel or "data" not in vessel: + debug_print(f"⚠️ 容器数据为空,返回 0.0mL") + return 0.0 + + vessel_data = vessel["data"] + vessel_id = vessel.get("id", "unknown") + + debug_print(f"🔍 读取容器 '{vessel_id}' 体积数据: {vessel_data}") + + # 检查liquid_volume字段 + if "liquid_volume" in vessel_data: + liquid_volume = vessel_data["liquid_volume"] + + # 处理列表格式 + if isinstance(liquid_volume, list): + if len(liquid_volume) > 0: + volume = liquid_volume[0] + if isinstance(volume, (int, float)): + debug_print(f"✅ 容器 '{vessel_id}' 体积: {volume}mL (列表格式)") + return float(volume) + + # 处理直接数值格式 + elif isinstance(liquid_volume, (int, float)): + debug_print(f"✅ 容器 '{vessel_id}' 体积: {liquid_volume}mL (数值格式)") + return float(liquid_volume) + + # 检查其他可能的体积字段 + volume_keys = ['current_volume', 'total_volume', 'volume'] + for key in volume_keys: + if key in vessel_data: + try: + volume = float(vessel_data[key]) + if volume > 0: + debug_print(f"✅ 容器 '{vessel_id}' 体积: {volume}mL (字段: {key})") + return volume + except (ValueError, TypeError): + continue + + debug_print(f"⚠️ 无法获取容器 '{vessel_id}' 的体积,返回默认值 50.0mL") + return 50.0 + +def update_vessel_volume(vessel: dict, G: nx.DiGraph, new_volume: float, description: str = "") -> None: + """ + 更新容器体积(同时更新vessel字典和图节点) + + Args: + vessel: 容器字典 + G: 网络图 + new_volume: 新体积 + description: 更新描述 + """ + vessel_id = vessel.get("id", "unknown") + + if description: + debug_print(f"🔧 更新容器体积 - {description}") + + # 更新vessel字典中的体积 + if "data" in vessel: + 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] = new_volume + else: + vessel["data"]["liquid_volume"] = [new_volume] + else: + vessel["data"]["liquid_volume"] = new_volume + else: + vessel["data"]["liquid_volume"] = new_volume + else: + vessel["data"] = {"liquid_volume": new_volume} + + # 同时更新图中的容器数据 + 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] = new_volume + else: + G.nodes[vessel_id]['data']['liquid_volume'] = [new_volume] + else: + G.nodes[vessel_id]['data']['liquid_volume'] = new_volume + + debug_print(f"📊 容器 '{vessel_id}' 体积已更新为: {new_volume:.2f}mL") + def generate_separate_protocol( G: nx.DiGraph, # 🔧 基础参数,支持XDL的vessel参数 - vessel: str = "", # XDL: 分离容器 - purpose: str = "separate", # 分离目的 - product_phase: str = "top", # 产物相 + vessel: dict = None, # 🔧 修改:从字符串改为字典类型 + purpose: str = "separate", # 分离目的 + product_phase: str = "top", # 产物相 # 🔧 可选的详细参数 - from_vessel: str = "", # 源容器(通常在separate前已经transfer了) - separation_vessel: str = "", # 分离容器(与vessel同义) - to_vessel: str = "", # 目标容器(可选) - waste_phase_to_vessel: str = "", # 废相目标容器 - product_vessel: str = "", # XDL: 产物容器(与to_vessel同义) - waste_vessel: str = "", # XDL: 废液容器(与waste_phase_to_vessel同义) + from_vessel: Union[str, dict] = "", # 源容器(通常在separate前已经transfer了) + separation_vessel: Union[str, dict] = "", # 分离容器(与vessel同义) + to_vessel: Union[str, dict] = "", # 目标容器(可选) + waste_phase_to_vessel: Union[str, dict] = "", # 废相目标容器 + product_vessel: Union[str, dict] = "", # XDL: 产物容器(与to_vessel同义) + waste_vessel: Union[str, dict] = "", # XDL: 废液容器(与waste_phase_to_vessel同义) # 🔧 溶剂相关参数 solvent: str = "", # 溶剂名称 solvent_volume: Union[str, float] = 0.0, # 溶剂体积 @@ -290,10 +390,10 @@ def generate_separate_protocol( **kwargs ) -> List[Dict[str, Any]]: """ - 生成分离操作的协议序列 - 增强中文版 + 生成分离操作的协议序列 - 支持vessel字典和体积运算 支持XDL参数格式: - - vessel: 分离容器(必需) + - vessel: 分离容器字典(必需) - purpose: "wash", "extract", "separate" - product_phase: "top", "bottom" - product_vessel: 产物收集容器 @@ -310,10 +410,20 @@ def generate_separate_protocol( 5. 重复指定次数 """ - debug_print("=" * 60) - debug_print("🧪 开始生成分离协议 - 增强中文版") - debug_print(f"📋 原始参数:") - debug_print(f" 🥼 容器: '{vessel}'") + # 🔧 核心修改:vessel参数兼容处理 + if vessel is None: + if isinstance(separation_vessel, dict): + vessel = separation_vessel + else: + raise ValueError("必须提供vessel字典参数") + + # 🔧 核心修改:从字典中提取容器ID + vessel_id = vessel["id"] + + debug_print("🌀" * 20) + debug_print("🚀 开始生成分离协议(支持vessel字典和体积运算)✨") + debug_print(f"📝 输入参数:") + debug_print(f" 🥽 vessel: {vessel} (ID: {vessel_id})") debug_print(f" 🎯 分离目的: '{purpose}'") debug_print(f" 📊 产物相: '{product_phase}'") debug_print(f" 💧 溶剂: '{solvent}'") @@ -322,24 +432,33 @@ def generate_separate_protocol( debug_print(f" 🎯 产物容器: '{product_vessel}'") debug_print(f" 🗑️ 废液容器: '{waste_vessel}'") debug_print(f" 📦 其他参数: {kwargs}") - debug_print("=" * 60) + debug_print("🌀" * 20) action_sequence = [] + # 🔧 新增:记录分离前的容器状态 + debug_print("🔍 记录分离前容器状态...") + original_liquid_volume = get_vessel_liquid_volume(vessel) + debug_print(f"📊 分离前液体体积: {original_liquid_volume:.2f}mL") + # === 参数验证和标准化 === debug_print("🔍 步骤1: 参数验证和标准化...") - action_sequence.append(create_action_log(f"开始分离操作 - 容器: {vessel}", "🎬")) + action_sequence.append(create_action_log(f"开始分离操作 - 容器: {vessel_id}", "🎬")) action_sequence.append(create_action_log(f"分离目的: {purpose}", "🧪")) action_sequence.append(create_action_log(f"产物相: {product_phase}", "📊")) - # 统一容器参数 - final_vessel = vessel or separation_vessel - if not final_vessel: - debug_print("❌ 必须指定分离容器") - raise ValueError("必须指定分离容器 (vessel 或 separation_vessel)") + # 统一容器参数 - 支持字典和字符串 + def extract_vessel_id(vessel_param): + if isinstance(vessel_param, dict): + return vessel_param.get("id", "") + elif isinstance(vessel_param, str): + return vessel_param + else: + return "" - final_to_vessel = to_vessel or product_vessel - final_waste_vessel = waste_phase_to_vessel or waste_vessel + final_vessel_id = vessel_id + final_to_vessel_id = extract_vessel_id(to_vessel) or extract_vessel_id(product_vessel) + final_waste_vessel_id = extract_vessel_id(waste_phase_to_vessel) or extract_vessel_id(waste_vessel) # 统一体积参数 final_volume = parse_volume_input(volume or solvent_volume) @@ -350,13 +469,13 @@ def generate_separate_protocol( debug_print(f"⚠️ 重复次数参数 <= 0,自动设置为 1") debug_print(f"🔧 标准化后的参数:") - debug_print(f" 🥼 分离容器: '{final_vessel}'") - debug_print(f" 🎯 产物容器: '{final_to_vessel}'") - debug_print(f" 🗑️ 废液容器: '{final_waste_vessel}'") + debug_print(f" 🥼 分离容器: '{final_vessel_id}'") + debug_print(f" 🎯 产物容器: '{final_to_vessel_id}'") + debug_print(f" 🗑️ 废液容器: '{final_waste_vessel_id}'") debug_print(f" 📏 溶剂体积: {final_volume}mL") debug_print(f" 🔄 重复次数: {repeats}") - action_sequence.append(create_action_log(f"分离容器: {final_vessel}", "🧪")) + action_sequence.append(create_action_log(f"分离容器: {final_vessel_id}", "🧪")) action_sequence.append(create_action_log(f"溶剂体积: {final_volume}mL", "📏")) action_sequence.append(create_action_log(f"重复次数: {repeats}", "🔄")) @@ -382,7 +501,7 @@ def generate_separate_protocol( action_sequence.append(create_action_log("正在查找相关设备...", "🔍")) # 查找分离器设备 - separator_device = find_separator_device(G, final_vessel) + separator_device = find_separator_device(G, final_vessel_id) # 🔧 使用 final_vessel_id if separator_device: action_sequence.append(create_action_log(f"找到分离器设备: {separator_device}", "🧪")) else: @@ -390,7 +509,7 @@ def generate_separate_protocol( action_sequence.append(create_action_log("未找到分离器设备", "⚠️")) # 查找搅拌器 - stirrer_device = find_connected_stirrer(G, final_vessel) + stirrer_device = find_connected_stirrer(G, final_vessel_id) # 🔧 使用 final_vessel_id if stirrer_device: action_sequence.append(create_action_log(f"找到搅拌器: {stirrer_device}", "🌪️")) else: @@ -414,6 +533,9 @@ def generate_separate_protocol( debug_print("🔍 步骤3: 执行分离流程...") action_sequence.append(create_action_log("开始分离工作流程", "🎯")) + # 🔧 新增:体积变化跟踪变量 + current_volume = original_liquid_volume + try: for repeat_idx in range(repeats): cycle_num = repeat_idx + 1 @@ -430,7 +552,7 @@ def generate_separate_protocol( pump_actions = generate_pump_protocol_with_rinsing( G=G, from_vessel=solvent_vessel, - to_vessel=final_vessel, + to_vessel=final_vessel_id, # 🔧 使用 final_vessel_id volume=final_volume, amount="", time=0.0, @@ -450,6 +572,10 @@ def generate_separate_protocol( debug_print(f"✅ 溶剂添加完成,添加了 {len(pump_actions)} 个动作") action_sequence.append(create_action_log(f"溶剂转移完成 ({len(pump_actions)} 个操作)", "✅")) + # 🔧 新增:更新体积 - 添加溶剂后 + current_volume += final_volume + update_vessel_volume(vessel, G, current_volume, f"添加{final_volume}mL {solvent}后") + except Exception as e: debug_print(f"❌ 溶剂添加失败: {str(e)}") action_sequence.append(create_action_log(f"溶剂添加失败: {str(e)}", "❌")) @@ -466,7 +592,7 @@ def generate_separate_protocol( "device_id": stirrer_device, "action_name": "start_stir", "action_kwargs": { - "vessel": final_vessel, + "vessel": final_vessel_id, # 🔧 使用 final_vessel_id "stir_speed": stir_speed, "purpose": f"分离混合 - {purpose}" } @@ -485,7 +611,7 @@ def generate_separate_protocol( action_sequence.append({ "device_id": stirrer_device, "action_name": "stop_stir", - "action_kwargs": {"vessel": final_vessel} + "action_kwargs": {"vessel": final_vessel_id} # 🔧 使用 final_vessel_id }) else: @@ -517,10 +643,10 @@ def generate_separate_protocol( "action_kwargs": { "purpose": purpose, "product_phase": product_phase, - "from_vessel": from_vessel or final_vessel, - "separation_vessel": final_vessel, - "to_vessel": final_to_vessel or final_vessel, - "waste_phase_to_vessel": final_waste_vessel or final_vessel, + "from_vessel": extract_vessel_id(from_vessel) or final_vessel_id, # 🔧 使用vessel_id + "separation_vessel": final_vessel_id, # 🔧 使用 final_vessel_id + "to_vessel": final_to_vessel_id or final_vessel_id, # 🔧 使用vessel_id + "waste_phase_to_vessel": final_waste_vessel_id or final_vessel_id, # 🔧 使用vessel_id "solvent": solvent, "solvent_volume": final_volume, "through": through, @@ -534,11 +660,17 @@ def generate_separate_protocol( debug_print(f"✅ 分离操作已添加") action_sequence.append(create_action_log("分离操作完成", "✅")) + # 🔧 新增:分离后体积估算(分离通常不改变总体积,但会重新分配) + # 假设分离后保持体积(实际情况可能有少量损失) + separated_volume = current_volume * 0.95 # 假设5%损失 + update_vessel_volume(vessel, G, separated_volume, f"分离操作后(第{cycle_num}轮)") + current_volume = separated_volume + # 收集结果 - if final_to_vessel: - action_sequence.append(create_action_log(f"产物 ({product_phase}相) 收集到: {final_to_vessel}", "📦")) - if final_waste_vessel: - action_sequence.append(create_action_log(f"废相收集到: {final_waste_vessel}", "🗑️")) + if final_to_vessel_id: + action_sequence.append(create_action_log(f"产物 ({product_phase}相) 收集到: {final_to_vessel_id}", "📦")) + if final_waste_vessel_id: + action_sequence.append(create_action_log(f"废相收集到: {final_waste_vessel_id}", "🗑️")) else: debug_print(f"🔄 第{cycle_num}轮 步骤4: 无分离器设备,跳过分离") @@ -572,99 +704,37 @@ def generate_separate_protocol( } }) + # 🔧 新增:分离完成后的最终状态报告 + final_liquid_volume = get_vessel_liquid_volume(vessel) + # === 最终结果 === total_time = (stir_time + settling_time + 15) * repeats # 估算总时间 - debug_print("=" * 60) + debug_print("🌀" * 20) debug_print(f"🎉 分离协议生成完成") debug_print(f"📊 协议统计:") debug_print(f" 📋 总动作数: {len(action_sequence)}") debug_print(f" ⏱️ 预计总时间: {total_time:.0f}s ({total_time/60:.1f} 分钟)") - debug_print(f" 🥼 分离容器: {final_vessel}") + debug_print(f" 🥼 分离容器: {final_vessel_id}") debug_print(f" 🎯 分离目的: {purpose}") debug_print(f" 📊 产物相: {product_phase}") debug_print(f" 🔄 重复次数: {repeats}") + debug_print(f"💧 体积变化统计:") + debug_print(f" - 分离前体积: {original_liquid_volume:.2f}mL") + debug_print(f" - 分离后体积: {final_liquid_volume:.2f}mL") if solvent: - debug_print(f" 💧 溶剂: {solvent} ({final_volume}mL)") - if final_to_vessel: - debug_print(f" 🎯 产物容器: {final_to_vessel}") - if final_waste_vessel: - debug_print(f" 🗑️ 废液容器: {final_waste_vessel}") - debug_print("=" * 60) + debug_print(f" 💧 溶剂: {solvent} ({final_volume}mL × {repeats}轮 = {final_volume * repeats:.2f}mL)") + if final_to_vessel_id: + debug_print(f" 🎯 产物容器: {final_to_vessel_id}") + if final_waste_vessel_id: + debug_print(f" 🗑️ 废液容器: {final_waste_vessel_id}") + debug_print("🌀" * 20) # 添加完成日志 - summary_msg = f"分离协议完成: {final_vessel} ({purpose},{repeats} 次循环)" + summary_msg = f"分离协议完成: {final_vessel_id} ({purpose},{repeats} 次循环)" if solvent: - summary_msg += f",使用 {final_volume}mL {solvent}" + summary_msg += f",使用 {final_volume * repeats:.2f}mL {solvent}" action_sequence.append(create_action_log(summary_msg, "🎉")) return action_sequence -# === 便捷函数 === - -def separate_phases_only(G: nx.DiGraph, vessel: str, product_phase: str = "top", - product_vessel: str = "", waste_vessel: str = "") -> List[Dict[str, Any]]: - """仅进行相分离(不添加溶剂)""" - debug_print(f"⚡ 快速相分离: {vessel} ({product_phase}相)") - return generate_separate_protocol( - G, vessel=vessel, - purpose="separate", - product_phase=product_phase, - product_vessel=product_vessel, - waste_vessel=waste_vessel - ) - -def wash_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float], - product_phase: str = "top", repeats: int = 1) -> List[Dict[str, Any]]: - """用溶剂洗涤""" - debug_print(f"🧽 用{solvent}洗涤: {vessel} ({repeats} 次)") - return generate_separate_protocol( - G, vessel=vessel, - purpose="wash", - product_phase=product_phase, - solvent=solvent, - volume=volume, - repeats=repeats - ) - -def extract_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float], - product_phase: str = "bottom", repeats: int = 3) -> List[Dict[str, Any]]: - """用溶剂萃取""" - debug_print(f"🧪 用{solvent}萃取: {vessel} ({repeats} 次)") - return generate_separate_protocol( - G, vessel=vessel, - purpose="extract", - product_phase=product_phase, - solvent=solvent, - volume=volume, - repeats=repeats - ) - -def separate_aqueous_organic(G: nx.DiGraph, vessel: str, organic_phase: str = "top", - product_vessel: str = "", waste_vessel: str = "") -> List[Dict[str, Any]]: - """水-有机相分离""" - debug_print(f"💧 水-有机相分离: {vessel} (有机相: {organic_phase})") - return generate_separate_protocol( - G, vessel=vessel, - purpose="separate", - product_phase=organic_phase, - product_vessel=product_vessel, - waste_vessel=waste_vessel - ) - -# 测试函数 -def test_separate_protocol(): - """测试分离协议的各种参数解析""" - debug_print("=== 分离协议增强中文版测试 ===") - - # 测试体积解析 - debug_print("🧪 测试体积解析...") - volumes = ["200 mL", "?", 100.0, "1 L", "500 μL", "未知", "50毫升"] - for vol in volumes: - result = parse_volume_input(vol) - debug_print(f"📊 体积解析结果: {vol} -> {result}mL") - - debug_print("✅ 测试完成") - -if __name__ == "__main__": - test_separate_protocol() diff --git a/unilabos/compile/stir_protocol.py b/unilabos/compile/stir_protocol.py index f7a6a74..70405ff 100644 --- a/unilabos/compile/stir_protocol.py +++ b/unilabos/compile/stir_protocol.py @@ -138,9 +138,50 @@ def validate_and_fix_params(stir_time: float, stir_speed: float, settling_time: return stir_time, stir_speed, settling_time +def extract_vessel_id(vessel: Union[str, dict]) -> str: + """ + 从vessel参数中提取vessel_id + + Args: + vessel: vessel字典或vessel_id字符串 + + Returns: + str: vessel_id + """ + if isinstance(vessel, dict): + vessel_id = vessel.get("id", "") + debug_print(f"🔧 从vessel字典提取ID: {vessel_id}") + return vessel_id + elif isinstance(vessel, str): + debug_print(f"🔧 vessel参数为字符串: {vessel}") + return vessel + else: + debug_print(f"⚠️ 无效的vessel参数类型: {type(vessel)}") + return "" + +def get_vessel_display_info(vessel: Union[str, dict]) -> str: + """ + 获取容器的显示信息(用于日志) + + Args: + vessel: vessel字典或vessel_id字符串 + + Returns: + str: 显示信息 + """ + if isinstance(vessel, dict): + vessel_id = vessel.get("id", "unknown") + vessel_name = vessel.get("name", "") + if vessel_name: + return f"{vessel_id} ({vessel_name})" + else: + return vessel_id + else: + return str(vessel) + def generate_stir_protocol( G: nx.DiGraph, - vessel: str, + vessel: Union[str, dict], # 支持vessel字典或字符串 time: Union[str, float, int] = "300", stir_time: Union[str, float, int] = "0", time_spec: str = "", @@ -149,14 +190,39 @@ def generate_stir_protocol( settling_time: Union[str, float] = "60", **kwargs ) -> List[Dict[str, Any]]: - """ - 生成搅拌操作的协议序列(精简版) - """ + """生成搅拌操作的协议序列 - 修复vessel参数传递""" + + # 🔧 核心修改:正确处理vessel参数 + vessel_id = extract_vessel_id(vessel) + vessel_display = get_vessel_display_info(vessel) + + # 🔧 关键修复:确保vessel_resource是完整的Resource对象 + if isinstance(vessel, dict): + vessel_resource = vessel # 已经是完整的Resource字典 + debug_print(f"✅ 使用传入的vessel Resource对象") + else: + # 如果只是字符串,构建一个基本的Resource对象 + vessel_resource = { + "id": vessel, + "name": "", + "category": "", + "children": [], + "config": "", + "data": "", + "parent": "", + "pose": { + "orientation": {"w": 1.0, "x": 0.0, "y": 0.0, "z": 0.0}, + "position": {"x": 0.0, "y": 0.0, "z": 0.0} + }, + "sample_id": "", + "type": "" + } + debug_print(f"🔧 构建了基本的vessel Resource对象: {vessel}") debug_print("🌪️" * 20) - debug_print("🚀 开始生成搅拌协议 ✨") + debug_print("🚀 开始生成搅拌协议(支持vessel字典)✨") debug_print(f"📝 输入参数:") - debug_print(f" 🥽 vessel: {vessel}") + debug_print(f" 🥽 vessel: {vessel_display} (ID: {vessel_id})") debug_print(f" ⏰ time: {time}") debug_print(f" 🕐 stir_time: {stir_time}") debug_print(f" 🎯 time_spec: {time_spec}") @@ -166,13 +232,13 @@ def generate_stir_protocol( # 📋 参数验证 debug_print("📍 步骤1: 参数验证... 🔧") - if not vessel: + if not vessel_id: # 🔧 使用 vessel_id debug_print("❌ vessel 参数不能为空! 😱") raise ValueError("vessel 参数不能为空") - if vessel not in G.nodes(): - debug_print(f"❌ 容器 '{vessel}' 不存在于系统中! 😞") - raise ValueError(f"容器 '{vessel}' 不存在于系统中") + if vessel_id not in G.nodes(): # 🔧 使用 vessel_id + debug_print(f"❌ 容器 '{vessel_id}' 不存在于系统中! 😞") + raise ValueError(f"容器 '{vessel_id}' 不存在于系统中") debug_print("✅ 基础参数验证通过 🎯") @@ -220,7 +286,7 @@ def generate_stir_protocol( # 🔍 查找设备 debug_print("📍 步骤3: 查找搅拌设备... 🔍") try: - stirrer_id = find_connected_stirrer(G, vessel) + stirrer_id = find_connected_stirrer(G, vessel_id) # 🔧 使用 vessel_id debug_print(f"🎉 使用搅拌设备: {stirrer_id} ✨") except Exception as e: debug_print(f"❌ 设备查找失败: {str(e)} 😭") @@ -234,13 +300,14 @@ def generate_stir_protocol( "device_id": stirrer_id, "action_name": "stir", "action_kwargs": { - "vessel": vessel, - "time": str(time), # 保持原始格式 + # 🔧 关键修复:传递vessel_id字符串,而不是完整的Resource对象 + "vessel": vessel_id, # 传递字符串ID,不是Resource对象 + "time": str(time), "event": event, "time_spec": time_spec, - "stir_time": float(parsed_time), # 确保是数字 - "stir_speed": float(stir_speed), # 确保是数字 - "settling_time": float(parsed_settling_time) # 确保是数字 + "stir_time": float(parsed_time), + "stir_speed": float(stir_speed), + "settling_time": float(parsed_settling_time) } } action_sequence.append(stir_action) @@ -256,7 +323,7 @@ def generate_stir_protocol( debug_print("🎊" * 20) debug_print(f"🎉 搅拌协议生成完成! ✨") debug_print(f"📊 总动作数: {len(action_sequence)} 个") - debug_print(f"🥽 搅拌容器: {vessel}") + debug_print(f"🥽 搅拌容器: {vessel_display}") debug_print(f"🌪️ 搅拌参数: {stir_speed} RPM, {parsed_time}s, 沉降 {parsed_settling_time}s") debug_print(f"⏱️ 预计总时间: {(parsed_time + parsed_settling_time)/60:.1f} 分钟 ⌛") debug_print("🎊" * 20) @@ -265,18 +332,47 @@ def generate_stir_protocol( def generate_start_stir_protocol( G: nx.DiGraph, - vessel: str, + vessel: Union[str, dict], stir_speed: float = 300.0, purpose: str = "", **kwargs ) -> List[Dict[str, Any]]: - """生成开始搅拌操作的协议序列""" + """生成开始搅拌操作的协议序列 - 修复vessel参数传递""" - debug_print("🔄 开始生成启动搅拌协议 ✨") - debug_print(f"🥽 vessel: {vessel}, 🌪️ speed: {stir_speed} RPM") + # 🔧 核心修改:正确处理vessel参数 + vessel_id = extract_vessel_id(vessel) + vessel_display = get_vessel_display_info(vessel) + + # 🔧 关键修复:确保vessel_resource是完整的Resource对象 + if isinstance(vessel, dict): + vessel_resource = vessel # 已经是完整的Resource字典 + debug_print(f"✅ 使用传入的vessel Resource对象") + else: + # 如果只是字符串,构建一个基本的Resource对象 + vessel_resource = { + "id": vessel, + "name": "", + "category": "", + "children": [], + "config": "", + "data": "", + "parent": "", + "pose": { + "orientation": {"w": 1.0, "x": 0.0, "y": 0.0, "z": 0.0}, + "position": {"x": 0.0, "y": 0.0, "z": 0.0} + }, + "sample_id": "", + "type": "" + } + debug_print(f"🔧 构建了基本的vessel Resource对象: {vessel}") + + debug_print("🔄 开始生成启动搅拌协议(修复vessel参数)✨") + debug_print(f"🥽 vessel: {vessel_display} (ID: {vessel_id})") + debug_print(f"🌪️ speed: {stir_speed} RPM") + debug_print(f"🎯 purpose: {purpose}") # 基础验证 - if not vessel or vessel not in G.nodes(): + if not vessel_id or vessel_id not in G.nodes(): debug_print("❌ 容器验证失败!") raise ValueError("vessel 参数无效") @@ -286,14 +382,15 @@ def generate_start_stir_protocol( stir_speed = 300.0 # 查找设备 - stirrer_id = find_connected_stirrer(G, vessel) + stirrer_id = find_connected_stirrer(G, vessel_id) - # 生成动作 + # 🔧 关键修复:传递vessel_id字符串 action_sequence = [{ "device_id": stirrer_id, "action_name": "start_stir", "action_kwargs": { - "vessel": vessel, + # 🔧 关键修复:传递vessel_id字符串,而不是完整的Resource对象 + "vessel": vessel_id, # 传递字符串ID,不是Resource对象 "stir_speed": stir_speed, "purpose": purpose or f"启动搅拌 {stir_speed} RPM" } @@ -304,38 +401,144 @@ def generate_start_stir_protocol( def generate_stop_stir_protocol( G: nx.DiGraph, - vessel: str, + vessel: Union[str, dict], **kwargs ) -> List[Dict[str, Any]]: - """生成停止搅拌操作的协议序列""" + """生成停止搅拌操作的协议序列 - 修复vessel参数传递""" - debug_print("🛑 开始生成停止搅拌协议 ✨") - debug_print(f"🥽 vessel: {vessel}") + # 🔧 核心修改:正确处理vessel参数 + vessel_id = extract_vessel_id(vessel) + vessel_display = get_vessel_display_info(vessel) + + # 🔧 关键修复:确保vessel_resource是完整的Resource对象 + if isinstance(vessel, dict): + vessel_resource = vessel # 已经是完整的Resource字典 + debug_print(f"✅ 使用传入的vessel Resource对象") + else: + # 如果只是字符串,构建一个基本的Resource对象 + vessel_resource = { + "id": vessel, + "name": "", + "category": "", + "children": [], + "config": "", + "data": "", + "parent": "", + "pose": { + "orientation": {"w": 1.0, "x": 0.0, "y": 0.0, "z": 0.0}, + "position": {"x": 0.0, "y": 0.0, "z": 0.0} + }, + "sample_id": "", + "type": "" + } + debug_print(f"🔧 构建了基本的vessel Resource对象: {vessel}") + + debug_print("🛑 开始生成停止搅拌协议(修复vessel参数)✨") + debug_print(f"🥽 vessel: {vessel_display} (ID: {vessel_id})") # 基础验证 - if not vessel or vessel not in G.nodes(): + if not vessel_id or vessel_id not in G.nodes(): debug_print("❌ 容器验证失败!") raise ValueError("vessel 参数无效") # 查找设备 - stirrer_id = find_connected_stirrer(G, vessel) + stirrer_id = find_connected_stirrer(G, vessel_id) - # 生成动作 + # 🔧 关键修复:传递vessel_id字符串 action_sequence = [{ "device_id": stirrer_id, "action_name": "stop_stir", "action_kwargs": { - "vessel": vessel + # 🔧 关键修复:传递vessel_id字符串,而不是完整的Resource对象 + "vessel": vessel_id # 传递字符串ID,不是Resource对象 } }] debug_print(f"✅ 停止搅拌协议生成完成 🎯") return action_sequence +# 🔧 新增:便捷函数 +def stir_briefly(G: nx.DiGraph, vessel: Union[str, dict], + speed: float = 300.0) -> List[Dict[str, Any]]: + """短时间搅拌(30秒)""" + vessel_display = get_vessel_display_info(vessel) + debug_print(f"⚡ 短时间搅拌: {vessel_display} @ {speed}RPM (30s)") + return generate_stir_protocol(G, vessel, time="30", stir_speed=speed) + +def stir_slowly(G: nx.DiGraph, vessel: Union[str, dict], + time: Union[str, float] = "10 min") -> List[Dict[str, Any]]: + """慢速搅拌""" + vessel_display = get_vessel_display_info(vessel) + debug_print(f"🐌 慢速搅拌: {vessel_display} @ 150RPM") + return generate_stir_protocol(G, vessel, time=time, stir_speed=150.0) + +def stir_vigorously(G: nx.DiGraph, vessel: Union[str, dict], + time: Union[str, float] = "5 min") -> List[Dict[str, Any]]: + """剧烈搅拌""" + vessel_display = get_vessel_display_info(vessel) + debug_print(f"💨 剧烈搅拌: {vessel_display} @ 800RPM") + return generate_stir_protocol(G, vessel, time=time, stir_speed=800.0) + +def stir_for_reaction(G: nx.DiGraph, vessel: Union[str, dict], + time: Union[str, float] = "1 h") -> List[Dict[str, Any]]: + """反应搅拌(标准速度,长时间)""" + vessel_display = get_vessel_display_info(vessel) + debug_print(f"🧪 反应搅拌: {vessel_display} @ 400RPM") + return generate_stir_protocol(G, vessel, time=time, stir_speed=400.0) + +def stir_for_dissolution(G: nx.DiGraph, vessel: Union[str, dict], + time: Union[str, float] = "15 min") -> List[Dict[str, Any]]: + """溶解搅拌(中等速度)""" + vessel_display = get_vessel_display_info(vessel) + debug_print(f"💧 溶解搅拌: {vessel_display} @ 500RPM") + return generate_stir_protocol(G, vessel, time=time, stir_speed=500.0) + +def stir_gently(G: nx.DiGraph, vessel: Union[str, dict], + time: Union[str, float] = "30 min") -> List[Dict[str, Any]]: + """温和搅拌""" + vessel_display = get_vessel_display_info(vessel) + debug_print(f"🍃 温和搅拌: {vessel_display} @ 200RPM") + return generate_stir_protocol(G, vessel, time=time, stir_speed=200.0) + +def stir_overnight(G: nx.DiGraph, vessel: Union[str, dict]) -> List[Dict[str, Any]]: + """过夜搅拌(模拟时缩短为2小时)""" + vessel_display = get_vessel_display_info(vessel) + debug_print(f"🌙 过夜搅拌(模拟2小时): {vessel_display} @ 300RPM") + return generate_stir_protocol(G, vessel, time="2 h", stir_speed=300.0) + +def start_continuous_stirring(G: nx.DiGraph, vessel: Union[str, dict], + speed: float = 300.0, purpose: str = "continuous stirring") -> List[Dict[str, Any]]: + """开始连续搅拌""" + vessel_display = get_vessel_display_info(vessel) + debug_print(f"🔄 开始连续搅拌: {vessel_display} @ {speed}RPM") + return generate_start_stir_protocol(G, vessel, stir_speed=speed, purpose=purpose) + +def stop_all_stirring(G: nx.DiGraph, vessel: Union[str, dict]) -> List[Dict[str, Any]]: + """停止所有搅拌""" + vessel_display = get_vessel_display_info(vessel) + debug_print(f"🛑 停止搅拌: {vessel_display}") + return generate_stop_stir_protocol(G, vessel) + # 测试函数 def test_stir_protocol(): """测试搅拌协议""" debug_print("🧪 === STIR PROTOCOL 测试 === ✨") + + # 测试vessel参数处理 + debug_print("🔧 测试vessel参数处理...") + + # 测试字典格式 + vessel_dict = {"id": "flask_1", "name": "反应瓶1"} + vessel_id = extract_vessel_id(vessel_dict) + vessel_display = get_vessel_display_info(vessel_dict) + debug_print(f" 字典格式: {vessel_dict} → ID: {vessel_id}, 显示: {vessel_display}") + + # 测试字符串格式 + vessel_str = "flask_2" + vessel_id = extract_vessel_id(vessel_str) + vessel_display = get_vessel_display_info(vessel_str) + debug_print(f" 字符串格式: {vessel_str} → ID: {vessel_id}, 显示: {vessel_display}") + debug_print("✅ 测试完成 🎉") if __name__ == "__main__": diff --git a/unilabos/compile/wash_solid_protocol.py b/unilabos/compile/wash_solid_protocol.py index 55768a4..e19d768 100644 --- a/unilabos/compile/wash_solid_protocol.py +++ b/unilabos/compile/wash_solid_protocol.py @@ -154,12 +154,153 @@ def find_filtrate_vessel(G: nx.DiGraph, filtrate_vessel: str = "") -> str: debug_print(f"⚠️ 使用默认滤液容器: waste_workup") return "waste_workup" +def extract_vessel_id(vessel: Union[str, dict]) -> str: + """ + 从vessel参数中提取vessel_id + + Args: + vessel: vessel字典或vessel_id字符串 + + Returns: + str: vessel_id + """ + if isinstance(vessel, dict): + vessel_id = vessel.get("id", "") + debug_print(f"🔧 从vessel字典提取ID: {vessel_id}") + return vessel_id + elif isinstance(vessel, str): + debug_print(f"🔧 vessel参数为字符串: {vessel}") + return vessel + else: + debug_print(f"⚠️ 无效的vessel参数类型: {type(vessel)}") + return "" + +def get_vessel_display_info(vessel: Union[str, dict]) -> str: + """ + 获取容器的显示信息(用于日志) + + Args: + vessel: vessel字典或vessel_id字符串 + + Returns: + str: 显示信息 + """ + if isinstance(vessel, dict): + vessel_id = vessel.get("id", "unknown") + vessel_name = vessel.get("name", "") + if vessel_name: + return f"{vessel_id} ({vessel_name})" + else: + return vessel_id + else: + return str(vessel) + +def get_vessel_liquid_volume(vessel: dict) -> float: + """ + 获取容器中的液体体积 - 支持vessel字典 + + Args: + vessel: 容器字典 + + Returns: + float: 液体体积(mL) + """ + if not vessel or "data" not in vessel: + debug_print(f"⚠️ 容器数据为空,返回 0.0mL") + return 0.0 + + vessel_data = vessel["data"] + vessel_id = vessel.get("id", "unknown") + + debug_print(f"🔍 读取容器 '{vessel_id}' 体积数据: {vessel_data}") + + # 检查liquid_volume字段 + if "liquid_volume" in vessel_data: + liquid_volume = vessel_data["liquid_volume"] + + # 处理列表格式 + if isinstance(liquid_volume, list): + if len(liquid_volume) > 0: + volume = liquid_volume[0] + if isinstance(volume, (int, float)): + debug_print(f"✅ 容器 '{vessel_id}' 体积: {volume}mL (列表格式)") + return float(volume) + + # 处理直接数值格式 + elif isinstance(liquid_volume, (int, float)): + debug_print(f"✅ 容器 '{vessel_id}' 体积: {liquid_volume}mL (数值格式)") + return float(liquid_volume) + + # 检查其他可能的体积字段 + volume_keys = ['current_volume', 'total_volume', 'volume'] + for key in volume_keys: + if key in vessel_data: + try: + volume = float(vessel_data[key]) + if volume > 0: + debug_print(f"✅ 容器 '{vessel_id}' 体积: {volume}mL (字段: {key})") + return volume + except (ValueError, TypeError): + continue + + debug_print(f"⚠️ 无法获取容器 '{vessel_id}' 的体积,返回默认值 0.0mL") + return 0.0 + +def update_vessel_volume(vessel: dict, G: nx.DiGraph, new_volume: float, description: str = "") -> None: + """ + 更新容器体积(同时更新vessel字典和图节点) + + Args: + vessel: 容器字典 + G: 网络图 + new_volume: 新体积 + description: 更新描述 + """ + vessel_id = vessel.get("id", "unknown") + + if description: + debug_print(f"🔧 更新容器体积 - {description}") + + # 更新vessel字典中的体积 + if "data" in vessel: + 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] = new_volume + else: + vessel["data"]["liquid_volume"] = [new_volume] + else: + vessel["data"]["liquid_volume"] = new_volume + else: + vessel["data"]["liquid_volume"] = new_volume + else: + vessel["data"] = {"liquid_volume": new_volume} + + # 同时更新图中的容器数据 + 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] = new_volume + else: + G.nodes[vessel_id]['data']['liquid_volume'] = [new_volume] + else: + G.nodes[vessel_id]['data']['liquid_volume'] = new_volume + + debug_print(f"📊 容器 '{vessel_id}' 体积已更新为: {new_volume:.2f}mL") + def generate_wash_solid_protocol( G: nx.DiGraph, - vessel: str, + vessel: Union[str, dict], # 🔧 修改:支持vessel字典 solvent: str, volume: Union[float, str] = "50", - filtrate_vessel: str = "", + filtrate_vessel: Union[str, dict] = "", # 🔧 修改:支持vessel字典 temp: float = 25.0, stir: bool = False, stir_speed: float = 0.0, @@ -172,21 +313,58 @@ def generate_wash_solid_protocol( **kwargs ) -> List[Dict[str, Any]]: """ - 生成固体清洗协议(精简版) + 生成固体清洗协议 - 支持vessel字典和体积运算 + + Args: + G: 有向图,节点为设备和容器,边为流体管道 + vessel: 清洗容器字典(从XDL传入)或容器ID字符串 + solvent: 清洗溶剂名称 + volume: 溶剂体积(每次清洗) + filtrate_vessel: 滤液收集容器字典或容器ID字符串 + temp: 清洗温度(°C) + stir: 是否搅拌 + stir_speed: 搅拌速度(RPM) + time: 搅拌时间 + repeats: 清洗重复次数 + volume_spec: 体积规格(small/medium/large) + repeats_spec: 重复次数规格(few/several/many) + mass: 固体质量(用于计算溶剂用量) + event: 事件描述 + **kwargs: 其他可选参数 + + Returns: + List[Dict[str, Any]]: 固体清洗操作的动作序列 """ + # 🔧 核心修改:从vessel参数中提取vessel_id + vessel_id = extract_vessel_id(vessel) + vessel_display = get_vessel_display_info(vessel) + + # 🔧 处理filtrate_vessel参数 + filtrate_vessel_id = extract_vessel_id(filtrate_vessel) if filtrate_vessel else "" + debug_print("🧼" * 20) - debug_print("🚀 开始生成固体清洗协议 ✨") + debug_print("🚀 开始生成固体清洗协议(支持vessel字典和体积运算)✨") debug_print(f"📝 输入参数:") - debug_print(f" 🥽 vessel: {vessel}") + debug_print(f" 🥽 vessel: {vessel_display} (ID: {vessel_id})") debug_print(f" 🧪 solvent: {solvent}") debug_print(f" 💧 volume: {volume}") + debug_print(f" 🗑️ filtrate_vessel: {filtrate_vessel_id}") debug_print(f" ⏰ time: {time}") debug_print(f" 🔄 repeats: {repeats}") debug_print("🧼" * 20) + # 🔧 新增:记录清洗前的容器状态 + debug_print("🔍 记录清洗前容器状态...") + if isinstance(vessel, dict): + original_volume = get_vessel_liquid_volume(vessel) + debug_print(f"📊 清洗前液体体积: {original_volume:.2f}mL") + else: + original_volume = 0.0 + debug_print(f"📊 vessel为字符串格式,无法获取体积信息") + # 📋 快速验证 - if not vessel or vessel not in G.nodes(): + if not vessel_id or vessel_id not in G.nodes(): # 🔧 使用 vessel_id debug_print("❌ 容器验证失败! 😱") raise ValueError("vessel 参数无效") @@ -225,8 +403,10 @@ def generate_wash_solid_protocol( debug_print("📍 步骤2: 查找设备... 🔍") try: solvent_source = find_solvent_source(G, solvent) - actual_filtrate_vessel = find_filtrate_vessel(G, filtrate_vessel) + actual_filtrate_vessel = find_filtrate_vessel(G, filtrate_vessel_id) debug_print(f"🎉 设备配置完成 ✨") + debug_print(f" 🧪 溶剂源: {solvent_source}") + debug_print(f" 🗑️ 滤液容器: {actual_filtrate_vessel}") except Exception as e: debug_print(f"❌ 设备查找失败: {str(e)} 😭") raise ValueError(f"设备查找失败: {str(e)}") @@ -235,6 +415,10 @@ def generate_wash_solid_protocol( debug_print("📍 步骤3: 生成清洗动作... 🧼") action_sequence = [] + # 🔧 新增:体积变化跟踪变量 + current_volume = original_volume + total_solvent_used = 0.0 + for cycle in range(final_repeats): debug_print(f" 🔄 第{cycle+1}/{final_repeats}次清洗...") @@ -242,10 +426,11 @@ def generate_wash_solid_protocol( try: from .pump_protocol import generate_pump_protocol_with_rinsing + debug_print(f" 💧 添加溶剂: {final_volume}mL {solvent}") transfer_actions = generate_pump_protocol_with_rinsing( G=G, from_vessel=solvent_source, - to_vessel=vessel, + to_vessel=vessel_id, # 🔧 使用 vessel_id volume=final_volume, amount="", time=0.0, @@ -261,17 +446,26 @@ def generate_wash_solid_protocol( if transfer_actions: action_sequence.extend(transfer_actions) debug_print(f" ✅ 转移动作: {len(transfer_actions)}个 🚚") + + # 🔧 新增:更新体积 - 添加溶剂后 + current_volume += final_volume + total_solvent_used += final_volume + + if isinstance(vessel, dict): + update_vessel_volume(vessel, G, current_volume, + f"第{cycle+1}次清洗添加{final_volume}mL溶剂后") except Exception as e: debug_print(f" ❌ 转移失败: {str(e)} 😞") # 2. 搅拌(如果需要) if stir and final_time > 0: + debug_print(f" 🌪️ 搅拌: {final_time}s @ {stir_speed}RPM") stir_action = { "device_id": "stirrer_1", "action_name": "stir", "action_kwargs": { - "vessel": vessel, + "vessel": vessel_id, # 🔧 使用 vessel_id "time": str(time), "stir_time": final_time, "stir_speed": stir_speed, @@ -282,11 +476,12 @@ def generate_wash_solid_protocol( debug_print(f" ✅ 搅拌动作: {final_time}s, {stir_speed}RPM 🌪️") # 3. 过滤 + debug_print(f" 🌊 过滤到: {actual_filtrate_vessel}") filter_action = { "device_id": "filter_1", "action_name": "filter", "action_kwargs": { - "vessel": vessel, + "vessel": vessel_id, # 🔧 使用 vessel_id "filtrate_vessel": actual_filtrate_vessel, "temp": temp, "volume": final_volume @@ -295,6 +490,15 @@ def generate_wash_solid_protocol( action_sequence.append(filter_action) debug_print(f" ✅ 过滤动作: → {actual_filtrate_vessel} 🌊") + # 🔧 新增:更新体积 - 过滤后(液体被滤除) + # 假设滤液完全被移除,固体残留在容器中 + filtered_volume = current_volume * 0.9 # 假设90%的液体被过滤掉 + current_volume = current_volume - filtered_volume + + if isinstance(vessel, dict): + update_vessel_volume(vessel, G, current_volume, + f"第{cycle+1}次清洗过滤后") + # 4. 等待(缩短时间) wait_time = 5.0 # 🕐 缩短等待时间:10s → 5s action_sequence.append({ @@ -303,14 +507,146 @@ def generate_wash_solid_protocol( }) debug_print(f" ✅ 等待: {wait_time}s ⏰") + # 🔧 新增:清洗完成后的最终状态报告 + if isinstance(vessel, dict): + final_volume_vessel = get_vessel_liquid_volume(vessel) + else: + final_volume_vessel = current_volume + # 🎊 总结 debug_print("🧼" * 20) debug_print(f"🎉 固体清洗协议生成完成! ✨") - debug_print(f"📊 总动作数: {len(action_sequence)} 个") - debug_print(f"🥽 清洗容器: {vessel}") - debug_print(f"🧪 使用溶剂: {solvent}") - debug_print(f"💧 清洗体积: {final_volume}mL × {final_repeats}次") + debug_print(f"📊 协议统计:") + debug_print(f" 📋 总动作数: {len(action_sequence)} 个") + debug_print(f" 🥽 清洗容器: {vessel_display}") + debug_print(f" 🧪 使用溶剂: {solvent}") + debug_print(f" 💧 单次体积: {final_volume}mL") + debug_print(f" 🔄 清洗次数: {final_repeats}次") + debug_print(f" 💧 总溶剂用量: {total_solvent_used:.2f}mL") + debug_print(f"📊 体积变化统计:") + debug_print(f" - 清洗前体积: {original_volume:.2f}mL") + debug_print(f" - 清洗后体积: {final_volume_vessel:.2f}mL") + debug_print(f" - 溶剂总用量: {total_solvent_used:.2f}mL") debug_print(f"⏱️ 预计总时间: {(final_time + 5) * final_repeats / 60:.1f} 分钟") debug_print("🧼" * 20) - return action_sequence \ No newline at end of file + return action_sequence + +# 🔧 新增:便捷函数 +def wash_with_water(G: nx.DiGraph, vessel: Union[str, dict], + volume: Union[float, str] = "50", + repeats: int = 2) -> List[Dict[str, Any]]: + """用水清洗固体""" + vessel_display = get_vessel_display_info(vessel) + debug_print(f"💧 水洗固体: {vessel_display} ({repeats} 次)") + return generate_wash_solid_protocol(G, vessel, "water", volume=volume, repeats=repeats) + +def wash_with_ethanol(G: nx.DiGraph, vessel: Union[str, dict], + volume: Union[float, str] = "30", + repeats: int = 1) -> List[Dict[str, Any]]: + """用乙醇清洗固体""" + vessel_display = get_vessel_display_info(vessel) + debug_print(f"🍺 乙醇洗固体: {vessel_display} ({repeats} 次)") + return generate_wash_solid_protocol(G, vessel, "ethanol", volume=volume, repeats=repeats) + +def wash_with_acetone(G: nx.DiGraph, vessel: Union[str, dict], + volume: Union[float, str] = "25", + repeats: int = 1) -> List[Dict[str, Any]]: + """用丙酮清洗固体""" + vessel_display = get_vessel_display_info(vessel) + debug_print(f"💨 丙酮洗固体: {vessel_display} ({repeats} 次)") + return generate_wash_solid_protocol(G, vessel, "acetone", volume=volume, repeats=repeats) + +def wash_with_ether(G: nx.DiGraph, vessel: Union[str, dict], + volume: Union[float, str] = "40", + repeats: int = 2) -> List[Dict[str, Any]]: + """用乙醚清洗固体""" + vessel_display = get_vessel_display_info(vessel) + debug_print(f"🌬️ 乙醚洗固体: {vessel_display} ({repeats} 次)") + return generate_wash_solid_protocol(G, vessel, "diethyl_ether", volume=volume, repeats=repeats) + +def wash_with_cold_solvent(G: nx.DiGraph, vessel: Union[str, dict], + solvent: str, volume: Union[float, str] = "30", + repeats: int = 1) -> List[Dict[str, Any]]: + """用冷溶剂清洗固体""" + vessel_display = get_vessel_display_info(vessel) + debug_print(f"❄️ 冷{solvent}洗固体: {vessel_display} ({repeats} 次)") + return generate_wash_solid_protocol(G, vessel, solvent, volume=volume, + temp=5.0, repeats=repeats) + +def wash_with_hot_solvent(G: nx.DiGraph, vessel: Union[str, dict], + solvent: str, volume: Union[float, str] = "50", + repeats: int = 1) -> List[Dict[str, Any]]: + """用热溶剂清洗固体""" + vessel_display = get_vessel_display_info(vessel) + debug_print(f"🔥 热{solvent}洗固体: {vessel_display} ({repeats} 次)") + return generate_wash_solid_protocol(G, vessel, solvent, volume=volume, + temp=60.0, repeats=repeats) + +def wash_with_stirring(G: nx.DiGraph, vessel: Union[str, dict], + solvent: str, volume: Union[float, str] = "50", + stir_time: Union[str, float] = "5 min", + repeats: int = 1) -> List[Dict[str, Any]]: + """带搅拌的溶剂清洗""" + vessel_display = get_vessel_display_info(vessel) + debug_print(f"🌪️ 搅拌清洗: {vessel_display} with {solvent} ({repeats} 次)") + return generate_wash_solid_protocol(G, vessel, solvent, volume=volume, + stir=True, stir_speed=200.0, + time=stir_time, repeats=repeats) + +def thorough_wash(G: nx.DiGraph, vessel: Union[str, dict], + solvent: str, volume: Union[float, str] = "50") -> List[Dict[str, Any]]: + """彻底清洗(多次重复)""" + vessel_display = get_vessel_display_info(vessel) + debug_print(f"🔄 彻底清洗: {vessel_display} with {solvent} (5 次)") + return generate_wash_solid_protocol(G, vessel, solvent, volume=volume, repeats=5) + +def quick_rinse(G: nx.DiGraph, vessel: Union[str, dict], + solvent: str, volume: Union[float, str] = "20") -> List[Dict[str, Any]]: + """快速冲洗(单次,小体积)""" + vessel_display = get_vessel_display_info(vessel) + debug_print(f"⚡ 快速冲洗: {vessel_display} with {solvent}") + return generate_wash_solid_protocol(G, vessel, solvent, volume=volume, repeats=1) + +def sequential_wash(G: nx.DiGraph, vessel: Union[str, dict], + solvents: list, volume: Union[float, str] = "40") -> List[Dict[str, Any]]: + """连续多溶剂清洗""" + vessel_display = get_vessel_display_info(vessel) + debug_print(f"📝 连续清洗: {vessel_display} with {' → '.join(solvents)}") + + action_sequence = [] + for solvent in solvents: + wash_actions = generate_wash_solid_protocol(G, vessel, solvent, + volume=volume, repeats=1) + action_sequence.extend(wash_actions) + + return action_sequence + +# 测试函数 +def test_wash_solid_protocol(): + """测试固体清洗协议""" + debug_print("🧪 === WASH SOLID PROTOCOL 测试 === ✨") + + # 测试vessel参数处理 + debug_print("🔧 测试vessel参数处理...") + + # 测试字典格式 + vessel_dict = {"id": "filter_flask_1", "name": "过滤瓶1", + "data": {"liquid_volume": 25.0}} + vessel_id = extract_vessel_id(vessel_dict) + vessel_display = get_vessel_display_info(vessel_dict) + volume = get_vessel_liquid_volume(vessel_dict) + debug_print(f" 字典格式: {vessel_dict}") + debug_print(f" → ID: {vessel_id}, 显示: {vessel_display}, 体积: {volume}mL") + + # 测试字符串格式 + vessel_str = "filter_flask_2" + vessel_id = extract_vessel_id(vessel_str) + vessel_display = get_vessel_display_info(vessel_str) + debug_print(f" 字符串格式: {vessel_str}") + debug_print(f" → ID: {vessel_id}, 显示: {vessel_display}") + + debug_print("✅ 测试完成 🎉") + +if __name__ == "__main__": + test_wash_solid_protocol() \ No newline at end of file diff --git a/unilabos/messages/__init__.py b/unilabos/messages/__init__.py index f91f382..4ef5ac9 100644 --- a/unilabos/messages/__init__.py +++ b/unilabos/messages/__init__.py @@ -11,8 +11,8 @@ class Point3D(BaseModel): class PumpTransferProtocol(BaseModel): # === 核心参数(保持必需) === - from_vessel: str - to_vessel: str + from_vessel: dict + to_vessel: dict # === 所有其他参数都改为可选,添加默认值 === volume: float = 0.0 # 🔧 改为-1,表示转移全部体积 @@ -95,7 +95,7 @@ class PumpTransferProtocol(BaseModel): class CleanProtocol(BaseModel): - vessel: str + vessel: dict solvent: str volume: float temp: float @@ -105,10 +105,10 @@ class CleanProtocol(BaseModel): class SeparateProtocol(BaseModel): purpose: str product_phase: str - from_vessel: str - separation_vessel: str - to_vessel: str - waste_phase_to_vessel: str + from_vessel: dict + separation_vessel: dict + to_vessel: dict + waste_phase_to_vessel: dict solvent: str solvent_volume: float through: str @@ -120,7 +120,7 @@ class SeparateProtocol(BaseModel): class EvaporateProtocol(BaseModel): # === 核心参数(必需) === - vessel: str = Field(..., description="蒸发容器名称") + vessel: dict = Field(..., description="蒸发容器名称") # === 所有其他参数都改为可选,添加默认值 === pressure: float = Field(0.1, description="真空度 (bar),默认0.1 bar") @@ -183,7 +183,7 @@ class EvaporateProtocol(BaseModel): class EvacuateAndRefillProtocol(BaseModel): # === 必需参数 === - vessel: str = Field(..., description="目标容器名称") + vessel: dict = Field(..., description="目标容器名称") gas: str = Field(..., description="气体名称") # 🔧 删除 repeats 参数,直接在代码中硬编码为 3 次 @@ -219,7 +219,7 @@ class AGVTransferProtocol(BaseModel): #=============新添加的新的协议================ class AddProtocol(BaseModel): - vessel: str + vessel: dict reagent: str volume: float mass: float @@ -231,17 +231,17 @@ class AddProtocol(BaseModel): purpose: str class CentrifugeProtocol(BaseModel): - vessel: str + vessel: dict speed: float time: float temp: float class FilterProtocol(BaseModel): # === 必需参数 === - vessel: str = Field(..., description="过滤容器名称") + vessel: dict = Field(..., description="过滤容器名称") # === 可选参数 === - filtrate_vessel: str = Field("", description="滤液容器名称(可选,自动查找)") + filtrate_vessel: dict = Field("", description="滤液容器名称(可选,自动查找)") def model_post_init(self, __context): """后处理:参数验证""" @@ -251,7 +251,7 @@ class FilterProtocol(BaseModel): class HeatChillProtocol(BaseModel): # === 必需参数 === - vessel: str = Field(..., description="加热容器名称") + vessel: dict = Field(..., description="加热容器名称") # === 可选参数 - 温度相关 === temp: float = Field(25.0, description="目标温度 (°C)") @@ -375,7 +375,7 @@ class HeatChillProtocol(BaseModel): class HeatChillStartProtocol(BaseModel): # === 必需参数 === - vessel: str = Field(..., description="加热容器名称") + vessel: dict = Field(..., description="加热容器名称") # === 可选参数 - 温度相关 === temp: float = Field(25.0, description="目标温度 (°C)") @@ -393,12 +393,12 @@ class HeatChillStartProtocol(BaseModel): class HeatChillStopProtocol(BaseModel): # === 必需参数 === - vessel: str = Field(..., description="加热容器名称") + vessel: dict = Field(..., description="加热容器名称") class StirProtocol(BaseModel): # === 必需参数 === - vessel: str = Field(..., description="搅拌容器名称") + vessel: dict = Field(..., description="搅拌容器名称") # === 可选参数 === time: str = Field("5 min", description="搅拌时间(如 '0.5 h', '30 min')") @@ -482,7 +482,7 @@ class StirProtocol(BaseModel): class StartStirProtocol(BaseModel): # === 必需参数 === - vessel: str = Field(..., description="搅拌容器名称") + vessel: dict = Field(..., description="搅拌容器名称") # === 可选参数,添加默认值 === stir_speed: float = Field(200.0, description="搅拌速度 (RPM),默认200 RPM") @@ -505,7 +505,7 @@ class StartStirProtocol(BaseModel): class StopStirProtocol(BaseModel): # === 必需参数 === - vessel: str = Field(..., description="搅拌容器名称") + vessel: dict = Field(..., description="搅拌容器名称") def model_post_init(self, __context): """后处理:参数验证""" @@ -515,8 +515,8 @@ class StopStirProtocol(BaseModel): raise ValueError("vessel 参数不能为空") class TransferProtocol(BaseModel): - from_vessel: str - to_vessel: str + from_vessel: dict + to_vessel: dict volume: float amount: str = "" time: float = 0 @@ -527,14 +527,14 @@ class TransferProtocol(BaseModel): solid: bool = False class CleanVesselProtocol(BaseModel): - vessel: str + vessel: dict solvent: str volume: float temp: float repeats: int = 1 class DissolveProtocol(BaseModel): - vessel: str + vessel: dict solvent: str volume: float amount: str = "" @@ -543,8 +543,8 @@ class DissolveProtocol(BaseModel): stir_speed: float = 0.0 class FilterThroughProtocol(BaseModel): - from_vessel: str - to_vessel: str + from_vessel: dict + to_vessel: dict filter_through: str eluting_solvent: str = "" eluting_volume: float = 0.0 @@ -552,18 +552,18 @@ class FilterThroughProtocol(BaseModel): residence_time: float = 0.0 class RunColumnProtocol(BaseModel): - from_vessel: str - to_vessel: str + from_vessel: dict + to_vessel: dict column: str class WashSolidProtocol(BaseModel): # === 必需参数 === - vessel: str = Field(..., description="装有固体的容器名称") + vessel: dict = Field(..., description="装有固体的容器名称") solvent: str = Field(..., description="清洗溶剂名称") volume: float = Field(..., description="清洗溶剂体积 (mL)") # === 可选参数,添加默认值 === - filtrate_vessel: str = Field("", description="滤液收集容器(可选,自动查找)") + filtrate_vessel: dict = Field("", description="滤液收集容器(可选,自动查找)") temp: float = Field(25.0, description="清洗温度 (°C),默认25°C") stir: bool = Field(False, description="是否搅拌,默认False") stir_speed: float = Field(0.0, description="搅拌速度 (RPM),默认0") @@ -604,7 +604,7 @@ class WashSolidProtocol(BaseModel): self.repeats = 10 class AdjustPHProtocol(BaseModel): - vessel: str = Field(..., description="目标容器") + vessel: dict = Field(..., description="目标容器") ph_value: float = Field(..., description="目标pH值") # 改为 ph_value reagent: str = Field(..., description="酸碱试剂名称") # 移除其他可选参数,使用默认值 @@ -614,19 +614,19 @@ class ResetHandlingProtocol(BaseModel): class DryProtocol(BaseModel): compound: str = Field(..., description="化合物名称") - vessel: str = Field(..., description="目标容器") + vessel: dict = Field(..., description="目标容器") class RecrystallizeProtocol(BaseModel): ratio: str = Field(..., description="溶剂比例(如 '1:1', '3:7')") solvent1: str = Field(..., description="第一种溶剂名称") solvent2: str = Field(..., description="第二种溶剂名称") - vessel: str = Field(..., description="目标容器") + vessel: dict = Field(..., description="目标容器") volume: float = Field(..., description="总体积 (mL)") class HydrogenateProtocol(BaseModel): temp: str = Field(..., description="反应温度(如 '45 °C')") time: str = Field(..., description="反应时间(如 '2 h')") - vessel: str = Field(..., description="反应容器") + vessel: dict = Field(..., description="反应容器") __all__ = [ "Point3D", "PumpTransferProtocol", "CleanProtocol", "SeparateProtocol", diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml index 2ad2c5b..acaed03 100644 --- a/unilabos/registry/devices/virtual_device.yaml +++ b/unilabos/registry/devices/virtual_device.yaml @@ -3650,3 +3650,176 @@ virtual_vacuum_pump: - status type: object version: 0.0.1 +virtual_solid_dispenser: + class: + action_value_mappings: + auto-cleanup: + feedback: {} + goal: {} + goal_default: {} + handles: [] + result: {} + schema: + description: cleanup的参数schema + properties: + feedback: {} + goal: + properties: {} + type: object + result: {} + required: + - goal + title: cleanup参数 + type: object + type: UniLabJsonCommandAsync + auto-initialize: + feedback: {} + goal: {} + goal_default: {} + handles: [] + result: {} + schema: + description: initialize的参数schema + properties: + feedback: {} + goal: + properties: {} + type: object + result: {} + required: + - goal + title: initialize参数 + type: object + type: UniLabJsonCommandAsync + add_solid: + feedback: + current_status: status + progress: progress + goal: + vessel: vessel + reagent: reagent + mass: mass + mol: mol + purpose: purpose + event: event + rate_spec: rate_spec + equiv: equiv + ratio: ratio + goal_default: + vessel: '' + reagent: '' + mass: '' + mol: '' + purpose: '' + event: '' + rate_spec: '' + equiv: '' + ratio: '' + handles: [] + result: + success: success + message: message + return_info: return_info + schema: + description: '' + properties: + feedback: + properties: + current_status: + type: string + progress: + type: number + type: object + goal: + properties: + vessel: + type: string + reagent: + type: string + mass: + type: string + mol: + type: string + purpose: + type: string + event: + type: string + rate_spec: + type: string + equiv: + type: string + ratio: + type: string + type: object + result: + properties: + success: + type: boolean + message: + type: string + return_info: + type: string + type: object + required: + - goal + title: AddSolid + type: object + type: Add # 🔧 使用 Add action type + module: unilabos.devices.virtual.virtual_solid_dispenser:VirtualSolidDispenser + status_types: + status: str + current_reagent: str + dispensed_amount: float + total_operations: int + type: python + description: Virtual Solid Dispenser for Add Protocol Testing - supports mass and molar additions + handles: + - data_key: solid_out + data_source: executor + data_type: resource + description: 固体试剂输出口 + handler_key: SolidOut + io_type: source + label: SolidOut + side: SOUTH + - data_key: solid_in + data_source: handle + data_type: resource + description: 固体试剂输入口(连接试剂瓶) + handler_key: SolidIn + io_type: target + label: SolidIn + side: NORTH + icon: '' + init_param_schema: + config: + properties: + max_capacity: + default: 100.0 + type: number + precision: + default: 0.001 + type: number + config: + type: object + device_id: + type: string + required: [] + type: object + data: + properties: + status: + type: string + current_reagent: + type: string + dispensed_amount: + type: number + total_operations: + type: integer + required: + - status + - current_reagent + - dispensed_amount + - total_operations + type: object + version: 0.0.1 \ No newline at end of file diff --git a/unilabos/ros/msgs/message_converter.py b/unilabos/ros/msgs/message_converter.py index 932ff60..b0716aa 100644 --- a/unilabos/ros/msgs/message_converter.py +++ b/unilabos/ros/msgs/message_converter.py @@ -508,47 +508,90 @@ def convert_from_ros_msg_with_mapping(ros_msg: Any, value_mapping: Dict[str, str Python字典 """ data: Dict[str, Any] = {} + + # # 🔧 添加调试信息 + # print(f"🔍 convert_from_ros_msg_with_mapping 开始") + # print(f"🔍 ros_msg 类型: {type(ros_msg)}") + # print(f"🔍 ros_msg 内容: {ros_msg}") + # print(f"🔍 value_mapping: {value_mapping}") + # print("-" * 60) for msg_name, attr_name in value_mapping.items(): + # print(f"🔍 处理映射: {msg_name} -> {attr_name}") + msg_path = msg_name.split(".") current = ros_msg - + + # print(f"🔍 msg_path: {msg_path}") + # print(f"🔍 current 初始值: {current} (类型: {type(current)})") + try: if not attr_name.endswith("[]"): # 处理单值映射 - for name in msg_path: - current = getattr(current, name) - data[attr_name] = convert_from_ros_msg(current) + # print(f"🔍 处理单值映射") + for i, name in enumerate(msg_path): + # print(f"🔍 步骤 {i}: 获取属性 '{name}' 从 {type(current)}") + if hasattr(current, name): + current = getattr(current, name) + # print(f"🔍 获取到: {current} (类型: {type(current)})") + else: + # print(f"❌ 属性 '{name}' 不存在于 {type(current)}") + break + + converted_value = convert_from_ros_msg(current) + # print(f"🔍 转换后的值: {converted_value} (类型: {type(converted_value)})") + data[attr_name] = converted_value + # print(f"✅ 设置 data['{attr_name}'] = {converted_value}") else: # 处理列表值映射 - for name in msg_path: + # print(f"🔍 处理列表值映射") + for i, name in enumerate(msg_path): + # print(f"🔍 列表步骤 {i}: 处理 '{name}' 从 {type(current)}") if name.endswith("[]"): base_name = name[:-2] + # print(f"🔍 数组字段 base_name: '{base_name}'") if hasattr(current, base_name): current = list(getattr(current, base_name)) + # print(f"🔍 获取数组: {current} (长度: {len(current)})") else: + # print(f"❌ 数组字段 '{base_name}' 不存在") current = [] break else: if isinstance(current, list): + # print(f"🔍 从列表中获取属性 '{name}'") next_level = [] for item in current: if hasattr(item, name): next_level.append(getattr(item, name)) current = next_level + # print(f"🔍 列表处理结果: {current} (长度: {len(current)})") elif hasattr(current, name): current = getattr(current, name) + # print(f"🔍 获取到属性: {current} (类型: {type(current)})") else: + # print(f"❌ 属性 '{name}' 不存在") current = [] break attr_key = attr_name[:-2] if current: - data[attr_key] = [convert_from_ros_msg(item) for item in current] - except (AttributeError, TypeError): + converted_list = [convert_from_ros_msg(item) for item in current] + data[attr_key] = converted_list + # print(f"✅ 设置 data['{attr_key}'] = {converted_list}") + else: + print(f"⚠️ 列表为空,跳过 '{attr_key}'") + except (AttributeError, TypeError) as e: + # print(f"❌ 映射转换错误 {msg_name} -> {attr_name}: {e}") logger.debug(f"Mapping conversion error for {msg_name} -> {attr_name}") continue + + # print(f"🔍 当前 data 状态: {data}") + # print("-" * 40) + #print(f"🔍 convert_from_ros_msg_with_mapping 结束") + #print(f"🔍 最终 data: {data}") + #print("=" * 60) return data diff --git a/unilabos/ros/nodes/base_device_node.py b/unilabos/ros/nodes/base_device_node.py index 3589bd4..4e3e304 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -629,7 +629,7 @@ class BaseROS2DeviceNode(Node, Generic[T]): execution_success = False action_return_value = None - self.lab_logger().info(f"执行动作: {action_name}") + ##### self.lab_logger().info(f"执行动作: {action_name}") goal = goal_handle.request # 从目标消息中提取参数, 并调用对应的方法 @@ -685,14 +685,14 @@ class BaseROS2DeviceNode(Node, Generic[T]): final_resource = [convert_resources_to_type([i], final_type)[0] for i in resources_list] action_kwargs[k] = self.resource_tracker.figure_resource(final_resource) - self.lab_logger().info(f"准备执行: {action_kwargs}, 函数: {ACTION.__name__}") + ##### self.lab_logger().info(f"准备执行: {action_kwargs}, 函数: {ACTION.__name__}") time_start = time.time() time_overall = 100 # 将阻塞操作放入线程池执行 if asyncio.iscoroutinefunction(ACTION): try: - self.lab_logger().info(f"异步执行动作 {ACTION}") + ##### self.lab_logger().info(f"异步执行动作 {ACTION}") future = ROS2DeviceNode.run_async_func(ACTION, trace_error=False, **action_kwargs) def _handle_future_exception(fut): @@ -702,7 +702,7 @@ class BaseROS2DeviceNode(Node, Generic[T]): execution_success = True except Exception as e: execution_error = traceback.format_exc() - error(f"异步任务 {ACTION.__name__} 报错了") + ##### error(f"异步任务 {ACTION.__name__} 报错了") error(traceback.format_exc()) future.add_done_callback(_handle_future_exception) @@ -710,7 +710,7 @@ class BaseROS2DeviceNode(Node, Generic[T]): self.lab_logger().error(f"创建异步任务失败: {traceback.format_exc()}") raise e else: - self.lab_logger().info(f"同步执行动作 {ACTION}") + ##### self.lab_logger().info(f"同步执行动作 {ACTION}") future = self._executor.submit(ACTION, **action_kwargs) def _handle_future_exception(fut): @@ -765,7 +765,7 @@ class BaseROS2DeviceNode(Node, Generic[T]): self.lab_logger().info(f"动作 {action_name} 已取消") return action_type.Result() - self.lab_logger().info(f"动作执行完成: {action_name}") + ##### self.lab_logger().info(f"动作执行完成: {action_name}") del future # 向Host更新物料当前状态 @@ -801,7 +801,7 @@ class BaseROS2DeviceNode(Node, Generic[T]): # 发布结果 goal_handle.succeed() - self.lab_logger().info(f"设置动作成功: {action_name}") + ##### self.lab_logger().info(f"设置动作成功: {action_name}") result_values = {} for msg_name, attr_name in action_value_mapping["result"].items(): @@ -820,7 +820,7 @@ class BaseROS2DeviceNode(Node, Generic[T]): elif attr_name == "return_info": setattr(result_msg, attr_name, serialize_result_info(execution_error, execution_success, action_return_value)) - self.lab_logger().info(f"动作 {action_name} 完成并返回结果") + ##### self.lab_logger().info(f"动作 {action_name} 完成并返回结果") return result_msg return execute_callback diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index c7c273d..8356563 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -563,7 +563,7 @@ class HostNode(BaseROS2DeviceNode): if hasattr(bridge, "publish_device_status"): bridge.publish_device_status(self.device_status, device_id, property_name) self.lab_logger().debug( - f"[Host Node] Status updated: {device_id}.{property_name} = {msg.data}" + f"[Host Node] Status updated: {device_id}.{property_name} = {msg.data}" ) def send_goal( diff --git a/unilabos/ros/nodes/presets/protocol_node.py b/unilabos/ros/nodes/presets/protocol_node.py index 5c8ffbd..a17d4e9 100644 --- a/unilabos/ros/nodes/presets/protocol_node.py +++ b/unilabos/ros/nodes/presets/protocol_node.py @@ -182,26 +182,54 @@ class ROS2ProtocolNode(BaseROS2DeviceNode): # 从目标消息中提取参数, 并调用Protocol生成器(根据设备连接图)生成action步骤 goal = goal_handle.request protocol_kwargs = convert_from_ros_msg_with_mapping(goal, action_value_mapping["goal"]) + + # # 🔧 添加调试信息 + # print(f"🔍 转换后的 protocol_kwargs: {protocol_kwargs}") + # print(f"🔍 vessel 在转换后: {protocol_kwargs.get('vessel', 'NOT_FOUND')}") - # 向Host查询物料当前状态 - for k, v in goal.get_fields_and_field_types().items(): - if v in ["unilabos_msgs/Resource", "sequence"]: - r = ResourceGet.Request() - resource_id = ( - protocol_kwargs[k]["id"] if v == "unilabos_msgs/Resource" else protocol_kwargs[k][0]["id"] - ) - r.id = resource_id - r.with_children = True - response = await self._resource_clients["resource_get"].call_async(r) - protocol_kwargs[k] = list_to_nested_dict( - [convert_from_ros_msg(rs) for rs in response.resources] - ) + # # 🔧 完全禁用Host查询,直接使用转换后的数据 + # print(f"🔧 跳过Host查询,直接使用转换后的数据") + + # 🔧 额外验证:确保vessel数据完整 + if 'vessel' in protocol_kwargs: + vessel_data = protocol_kwargs['vessel'] + #print(f"🔍 验证vessel数据: {vessel_data}") + + # 如果vessel是空字典,尝试重新构建 + if not vessel_data or (isinstance(vessel_data, dict) and not vessel_data): + # print(f"⚠️ vessel数据为空,尝试从原始goal重新提取...") + + # 直接从原始goal提取vessel + if hasattr(goal, 'vessel') and goal.vessel: + # print(f"🔍 原始goal.vessel: {goal.vessel}") + # 手动转换vessel + vessel_data = { + 'id': goal.vessel.id, + 'name': goal.vessel.name, + 'type': goal.vessel.type, + 'category': goal.vessel.category, + 'config': goal.vessel.config, + 'data': goal.vessel.data + } + protocol_kwargs['vessel'] = vessel_data + # print(f"✅ 手动重建vessel数据: {vessel_data}") + else: + # print(f"❌ 无法从原始goal提取vessel数据") + # 创建一个基本的vessel + vessel_data = {'id': 'default_vessel'} + protocol_kwargs['vessel'] = vessel_data + # print(f"🔧 创建默认vessel: {vessel_data}") + + #print(f"🔍 最终传递给协议的 protocol_kwargs: {protocol_kwargs}") + #print(f"🔍 最终的 vessel: {protocol_kwargs.get('vessel', 'NOT_FOUND')}") from unilabos.resources.graphio import physical_setup_graph self.lab_logger().info(f"Working on physical setup: {physical_setup_graph}") + self.lab_logger().info(f"Protocol kwargs: {goal}") + self.lab_logger().info(f"Protocol kwargs: {action_value_mapping}") protocol_steps = protocol_steps_generator(G=physical_setup_graph, **protocol_kwargs) - + self.lab_logger().info(f"Goal received: {protocol_kwargs}, running steps: \n{protocol_steps}") time_start = time.time() @@ -235,14 +263,14 @@ class ROS2ProtocolNode(BaseROS2DeviceNode): } ) - # 向Host更新物料当前状态 - for k, v in goal.get_fields_and_field_types().items(): - if v in ["unilabos_msgs/Resource", "sequence"]: - r = ResourceUpdate.Request() - r.resources = [ - convert_to_ros_msg(Resource, rs) for rs in nested_dict_to_list(protocol_kwargs[k]) - ] - response = await self._resource_clients["resource_update"].call_async(r) + # # 向Host更新物料当前状态 + # for k, v in goal.get_fields_and_field_types().items(): + # if v in ["unilabos_msgs/Resource", "sequence"]: + # r = ResourceUpdate.Request() + # r.resources = [ + # convert_to_ros_msg(Resource, rs) for rs in nested_dict_to_list(protocol_kwargs[k]) + # ] + # response = await self._resource_clients["resource_update"].call_async(r) # 设置成功状态和返回值 execution_success = True @@ -287,7 +315,7 @@ class ROS2ProtocolNode(BaseROS2DeviceNode): serialize_result_info(execution_error, execution_success, protocol_return_value), ) - self.lab_logger().info(f"协议 {protocol_name} 完成并返回结果") + self.lab_logger().info(f"🤩🤩🤩🤩🤩🤩协议 {protocol_name} 完成并返回结果😎😎😎😎😎😎") return result return execute_protocol @@ -309,7 +337,7 @@ class ROS2ProtocolNode(BaseROS2DeviceNode): action_client = self._action_clients[action_id] goal_msg = convert_to_ros_msg(action_client._action_type.Goal(), action_kwargs) - self.lab_logger().info(f"发送动作请求到: {action_id}") + ##### self.lab_logger().info(f"发送动作请求到: {action_id}") action_client.wait_for_server() # 等待动作完成 @@ -321,7 +349,7 @@ class ROS2ProtocolNode(BaseROS2DeviceNode): return None result_future = await handle.get_result_async() - self.lab_logger().info(f"动作完成: {action_name}") + ##### self.lab_logger().info(f"动作完成: {action_name}") return result_future.result diff --git a/unilabos_msgs/action/Add.action b/unilabos_msgs/action/Add.action index 021199a..609e9f2 100644 --- a/unilabos_msgs/action/Add.action +++ b/unilabos_msgs/action/Add.action @@ -1,5 +1,5 @@ # Goal - 添加试剂的目标参数 -string vessel # 目标容器(必需) +Resource vessel # 目标容器(必需) string reagent # 试剂名称(必需) string volume # 体积(如 "2.7 mL",可选) string mass # 质量(如 "19.3 g",可选) diff --git a/unilabos_msgs/action/AddSolid.action b/unilabos_msgs/action/AddSolid.action index 8812441..c5fc7ce 100644 --- a/unilabos_msgs/action/AddSolid.action +++ b/unilabos_msgs/action/AddSolid.action @@ -1,5 +1,5 @@ # Goal - 固体加样操作的目标参数 -string vessel # 目标容器(必需) +Resource vessel # 目标容器(必需) string reagent # 试剂名称(必需) string mass # 质量字符串(如 "2.9 g",可选) string mol # 摩尔数字符串(如 "0.12 mol",可选) diff --git a/unilabos_msgs/action/AdjustPH.action b/unilabos_msgs/action/AdjustPH.action index 815ccf7..33e2f4d 100644 --- a/unilabos_msgs/action/AdjustPH.action +++ b/unilabos_msgs/action/AdjustPH.action @@ -1,5 +1,5 @@ # Request - 与您的 AdjustPHProtocol 类匹配 -string vessel +Resource vessel float64 ph_value string reagent --- diff --git a/unilabos_msgs/action/Centrifuge.action b/unilabos_msgs/action/Centrifuge.action index 356ccb9..755b6d1 100644 --- a/unilabos_msgs/action/Centrifuge.action +++ b/unilabos_msgs/action/Centrifuge.action @@ -1,5 +1,5 @@ # Goal - 离心操作的目标参数 -string vessel # 离心容器 +Resource vessel # 离心容器 float64 speed # 离心速度 (rpm) float64 time # 离心时间 (秒) float64 temp # 温度 (可选,摄氏度) diff --git a/unilabos_msgs/action/Clean.action b/unilabos_msgs/action/Clean.action index 8fb9be1..7318296 100644 --- a/unilabos_msgs/action/Clean.action +++ b/unilabos_msgs/action/Clean.action @@ -1,5 +1,5 @@ # Organic -string vessel # Vessel to clean. +Resource vessel # Vessel to clean. string solvent # Solvent to clean vessel with. float64 volume # Optional. Volume of solvent to clean vessel with. float64 temp # Optional. Temperature to heat vessel to while cleaning. diff --git a/unilabos_msgs/action/CleanVessel.action b/unilabos_msgs/action/CleanVessel.action index cba232a..950e9e2 100644 --- a/unilabos_msgs/action/CleanVessel.action +++ b/unilabos_msgs/action/CleanVessel.action @@ -1,4 +1,4 @@ -string vessel # 要清洗的容器名称 +Resource vessel # 要清洗的容器名称 string solvent # 用于清洗容器的溶剂名称 float64 volume # 清洗溶剂的体积,可选参数 float64 temp # 清洗时的温度,可选参数 diff --git a/unilabos_msgs/action/Crystallize.action b/unilabos_msgs/action/Crystallize.action index 50d26cf..6b0f537 100644 --- a/unilabos_msgs/action/Crystallize.action +++ b/unilabos_msgs/action/Crystallize.action @@ -1,5 +1,5 @@ # Goal - 结晶操作的目标参数 -string vessel # 结晶容器 +Resource vessel # 结晶容器 float64 ramp_time # 升温/降温时间 (可选,秒) float64 ramp_temp # 目标温度 (可选,摄氏度) --- diff --git a/unilabos_msgs/action/Dissolve.action b/unilabos_msgs/action/Dissolve.action index f070a61..be2cc46 100644 --- a/unilabos_msgs/action/Dissolve.action +++ b/unilabos_msgs/action/Dissolve.action @@ -1,5 +1,5 @@ # Goal - 溶解操作的目标参数 -string vessel # 装有要溶解物质的容器名称(必需) +Resource vessel # 装有要溶解物质的容器名称(必需) string solvent # 用于溶解物质的溶剂名称(可选) string volume # 溶剂的体积(如 "10 mL",可选) string amount # 要溶解物质的量描述(可选) diff --git a/unilabos_msgs/action/Dry.action b/unilabos_msgs/action/Dry.action index ec10c9a..224b21e 100644 --- a/unilabos_msgs/action/Dry.action +++ b/unilabos_msgs/action/Dry.action @@ -1,6 +1,6 @@ # Request string compound # 化合物 -string vessel # 干燥容器 +Resource vessel # 干燥容器 --- # Result bool success # 操作是否成功 diff --git a/unilabos_msgs/action/EvacuateAndRefill.action b/unilabos_msgs/action/EvacuateAndRefill.action index 461cb28..9e33855 100644 --- a/unilabos_msgs/action/EvacuateAndRefill.action +++ b/unilabos_msgs/action/EvacuateAndRefill.action @@ -1,5 +1,5 @@ # Organic Synthesis Station EvacuateAndRefill Action -string vessel +Resource vessel string gas --- string return_info diff --git a/unilabos_msgs/action/Evaporate.action b/unilabos_msgs/action/Evaporate.action index 9cecb62..1a3973c 100644 --- a/unilabos_msgs/action/Evaporate.action +++ b/unilabos_msgs/action/Evaporate.action @@ -1,5 +1,5 @@ # Organic Synthesis Station Evaporate Action -string vessel # 目标容器 +Resource vessel # 目标容器 float64 pressure # 真空度 float64 temp # 温度 string time # 🔧 蒸发时间(支持带单位,如"3 min","180",默认秒) diff --git a/unilabos_msgs/action/Filter.action b/unilabos_msgs/action/Filter.action index 42a8913..916697b 100644 --- a/unilabos_msgs/action/Filter.action +++ b/unilabos_msgs/action/Filter.action @@ -1,6 +1,6 @@ # Goal - 过滤操作的目标参数 -string vessel # 过滤容器(必需) -string filtrate_vessel # 滤液容器(可选) +Resource vessel # 过滤容器(必需) +Resource filtrate_vessel # 滤液容器(可选) bool stir # 是否搅拌(默认false) float64 stir_speed # 搅拌速度(默认0.0) float64 temp # 温度(默认25.0) diff --git a/unilabos_msgs/action/FilterThrough.action b/unilabos_msgs/action/FilterThrough.action index dbabd12..0d9de9b 100644 --- a/unilabos_msgs/action/FilterThrough.action +++ b/unilabos_msgs/action/FilterThrough.action @@ -1,6 +1,6 @@ -string from_vessel # 源容器的名称,即物质起始所在的容器 -string to_vessel # 目标容器的名称,物质过滤后要到达的容器 -string filter_through # 过滤时所通过的介质,如滤纸、柱子等 +Resource from_vessel # 源容器的名称,即物质起始所在的容器 +Resource to_vessel # 目标容器的名称,物质过滤后要到达的容器 +Resource filter_through # 过滤时所通过的介质,如滤纸、柱子等 string eluting_solvent # 洗脱溶剂的名称,可选参数 float64 eluting_volume # 洗脱溶剂的体积,可选参数 int32 eluting_repeats # 洗脱操作的重复次数,默认为 0 diff --git a/unilabos_msgs/action/HeatChill.action b/unilabos_msgs/action/HeatChill.action index 1e7025e..ace7508 100644 --- a/unilabos_msgs/action/HeatChill.action +++ b/unilabos_msgs/action/HeatChill.action @@ -1,5 +1,5 @@ # Goal - 加热冷却操作的目标参数 -string vessel # 加热容器名称(必需) +Resource vessel # 加热容器名称(必需) float64 temp # 目标温度(可选,默认25.0) string time # 🔧 加热时间(支持带单位,如"5 min","300",默认秒) string temp_spec # 温度规格(可选) diff --git a/unilabos_msgs/action/HeatChillStart.action b/unilabos_msgs/action/HeatChillStart.action index 565bad1..4a9947c 100644 --- a/unilabos_msgs/action/HeatChillStart.action +++ b/unilabos_msgs/action/HeatChillStart.action @@ -1,5 +1,5 @@ # Organic -string vessel +Resource vessel float64 temp string purpose --- diff --git a/unilabos_msgs/action/HeatChillStop.action b/unilabos_msgs/action/HeatChillStop.action index 280ca15..1855448 100644 --- a/unilabos_msgs/action/HeatChillStop.action +++ b/unilabos_msgs/action/HeatChillStop.action @@ -1,5 +1,5 @@ # Organic -string vessel +Resource vessel --- string return_info bool success diff --git a/unilabos_msgs/action/Hydrogenate.action b/unilabos_msgs/action/Hydrogenate.action index 72f9459..4fa7fa9 100644 --- a/unilabos_msgs/action/Hydrogenate.action +++ b/unilabos_msgs/action/Hydrogenate.action @@ -1,7 +1,7 @@ # Request string temp string time -string vessel +Resource vessel --- # Result bool success diff --git a/unilabos_msgs/action/PumpTransfer.action b/unilabos_msgs/action/PumpTransfer.action index c8ca445..c551139 100644 --- a/unilabos_msgs/action/PumpTransfer.action +++ b/unilabos_msgs/action/PumpTransfer.action @@ -1,6 +1,6 @@ # Organic -string from_vessel -string to_vessel +Resource from_vessel +Resource to_vessel float64 volume string amount float64 time diff --git a/unilabos_msgs/action/Purge.action b/unilabos_msgs/action/Purge.action index 00d76b3..8928619 100644 --- a/unilabos_msgs/action/Purge.action +++ b/unilabos_msgs/action/Purge.action @@ -1,5 +1,5 @@ # Goal - 清洗/吹扫操作的目标参数 -string vessel # 清洗容器 +Resource vessel # 清洗容器 string gas # 清洗气体 (可选) float64 time # 清洗时间 (可选,秒) float64 pressure # 压力 (可选,Pa) diff --git a/unilabos_msgs/action/Recrystallize.action b/unilabos_msgs/action/Recrystallize.action index 2ae42bf..0b41746 100644 --- a/unilabos_msgs/action/Recrystallize.action +++ b/unilabos_msgs/action/Recrystallize.action @@ -2,7 +2,7 @@ string ratio # 溶剂比例(如"1:1","3:7") string solvent1 # 第一种溶剂 string solvent2 # 第二种溶剂 -string vessel # 目标容器 +Resource vessel # 目标容器 string volume # 🔧 总体积(支持带单位,如"100 mL","50",默认mL) --- # Result diff --git a/unilabos_msgs/action/ResetHandling.action b/unilabos_msgs/action/ResetHandling.action index d07f240..e29c488 100644 --- a/unilabos_msgs/action/ResetHandling.action +++ b/unilabos_msgs/action/ResetHandling.action @@ -1,5 +1,6 @@ # Request string solvent +Resource vessel --- # Result bool success diff --git a/unilabos_msgs/action/RunColumn.action b/unilabos_msgs/action/RunColumn.action index 1f8e9ba..a22928d 100644 --- a/unilabos_msgs/action/RunColumn.action +++ b/unilabos_msgs/action/RunColumn.action @@ -1,6 +1,6 @@ # Goal - 柱层析操作的目标参数 -string from_vessel # 源容器的名称,即样品起始所在的容器(必需) -string to_vessel # 目标容器的名称,分离后的样品要到达的容器(必需) +Resource from_vessel # 源容器的名称,即样品起始所在的容器(必需) +Resource to_vessel # 目标容器的名称,分离后的样品要到达的容器(必需) string column # 所使用的柱子的名称(必需) string rf # Rf值(可选) string pct1 # 第一种溶剂百分比(如 "40 %",可选) diff --git a/unilabos_msgs/action/Separate.action b/unilabos_msgs/action/Separate.action index fc185b6..83436d5 100644 --- a/unilabos_msgs/action/Separate.action +++ b/unilabos_msgs/action/Separate.action @@ -1,13 +1,13 @@ # Goal - 分离操作的目标参数 -string vessel # 分离容器名称(XDL参数,必需) +Resource vessel # 分离容器名称(XDL参数,必需) string purpose # 分离目的 ('wash', 'extract', 'separate',可选) string product_phase # 产物相 ('top', 'bottom',可选) -string from_vessel # 源容器(可选) -string separation_vessel # 分离容器(与vessel同义,可选) -string to_vessel # 目标容器(可选) -string waste_phase_to_vessel # 废相目标容器(可选) -string product_vessel # 产物收集容器(XDL参数,可选) -string waste_vessel # 废液收集容器(XDL参数,可选) +Resource from_vessel # 源容器(可选) +Resource separation_vessel # 分离容器(与vessel同义,可选) +Resource to_vessel # 目标容器(可选) +Resource waste_phase_to_vessel # 废相目标容器(可选) +Resource product_vessel # 产物收集容器(XDL参数,可选) +Resource waste_vessel # 废液收集容器(XDL参数,可选) string solvent # 溶剂名称(可选) string solvent_volume # 溶剂体积(如 "200 mL",可选) string volume # 体积规格(XDL参数,如 "?",可选) diff --git a/unilabos_msgs/action/StartPurge.action b/unilabos_msgs/action/StartPurge.action index f5500a6..207019c 100644 --- a/unilabos_msgs/action/StartPurge.action +++ b/unilabos_msgs/action/StartPurge.action @@ -1,5 +1,5 @@ # Goal - 启动清洗/吹扫操作的目标参数 -string vessel # 清洗容器 +Resource vessel # 清洗容器 string gas # 清洗气体 (可选) float64 pressure # 压力 (可选,Pa) float64 flow_rate # 流速 (可选,mL/min) diff --git a/unilabos_msgs/action/StartStir.action b/unilabos_msgs/action/StartStir.action index 534c9f3..30c8301 100644 --- a/unilabos_msgs/action/StartStir.action +++ b/unilabos_msgs/action/StartStir.action @@ -1,5 +1,5 @@ # Goal - 启动搅拌操作的目标参数 -string vessel # 搅拌容器 +Resource vessel # 搅拌容器 float64 stir_speed # 搅拌速度 (可选,rpm) string purpose # 搅拌目的 (可选) --- diff --git a/unilabos_msgs/action/Stir.action b/unilabos_msgs/action/Stir.action index e3e5580..0760672 100644 --- a/unilabos_msgs/action/Stir.action +++ b/unilabos_msgs/action/Stir.action @@ -1,5 +1,5 @@ # Goal - 搅拌操作的目标参数 -string vessel # 搅拌容器名称(必需) +Resource vessel # 搅拌容器名称(必需) string time # 🔧 搅拌时间(如 "0.5 h", "30 min", "300",默认秒) string event # 事件标识(如 "A", "B") string time_spec # 时间规格(如 "several minutes") diff --git a/unilabos_msgs/action/StopPurge.action b/unilabos_msgs/action/StopPurge.action index b7db891..123fa35 100644 --- a/unilabos_msgs/action/StopPurge.action +++ b/unilabos_msgs/action/StopPurge.action @@ -1,5 +1,5 @@ # Goal - 停止清洗/吹扫操作的目标参数 -string vessel # 清洗容器 +Resource vessel # 清洗容器 --- # Result - 操作结果 bool success # 操作是否成功 diff --git a/unilabos_msgs/action/StopStir.action b/unilabos_msgs/action/StopStir.action index a320598..135d0b7 100644 --- a/unilabos_msgs/action/StopStir.action +++ b/unilabos_msgs/action/StopStir.action @@ -1,5 +1,5 @@ # Goal - 停止搅拌操作的目标参数 -string vessel # 搅拌容器 +Resource vessel # 搅拌容器 --- # Result - 操作结果 bool success # 操作是否成功 diff --git a/unilabos_msgs/action/WashSolid.action b/unilabos_msgs/action/WashSolid.action index 281ca4c..99ba297 100644 --- a/unilabos_msgs/action/WashSolid.action +++ b/unilabos_msgs/action/WashSolid.action @@ -1,8 +1,8 @@ # Goal - 固体清洗操作的目标参数 -string vessel # 装有固体的容器名称(必需) +Resource vessel # 装有固体的容器名称(必需) string solvent # 清洗溶剂名称(必需) string volume # 🔧 体积(支持数字和带单位的字符串,如"100 mL","?") -string filtrate_vessel # 滤液收集容器(可选,默认"") +Resource filtrate_vessel # 滤液收集容器(可选,默认"") float64 temp # 清洗温度(可选,默认25.0) bool stir # 是否搅拌(可选,默认false) float64 stir_speed # 搅拌速度(可选,默认0.0)