Squash merge from dev

Update recipe.yaml

fix: figure_resource

use call_async in all service to avoid deadlock

fix: prcxi import error

临时兼容错误的driver写法

fix protocol node

fix filter protocol

bugfixes on organic protocols

fix and remove redundant info

feat: 新增use_remote_resource参数

fix all protocol_compilers and remove deprecated devices

feat: 优化protocol node节点运行日志

fix pumps and liquid_handler handle

feat: workstation example

add: prcxi res
fix: startup slow

fix: prcxi_res

fix: discard_tips

fix: discard_tips error

fix: drop_tips not using auto resource select

feat: 添加ChinWe设备控制类,支持串口通信和电机控制功能 (#79)

feat: add trace log level

modify default discovery_interval to 15s

fix: working dir error when input config path
feat: report publish topic when error

fix: workstation handlers and vessel_id parsing

Cleanup registry to be easy-understanding (#76)

* delete deprecated mock devices

* rename categories

* combine chromatographic devices

* rename rviz simulation nodes

* organic virtual devices

* parse vessel_id

* run registry completion before merge

---------

Co-authored-by: Xuwznln <18435084+Xuwznln@users.noreply.github.com>
This commit is contained in:
Junhan Chang
2025-08-03 11:21:37 +08:00
committed by Xuwznln
parent a555c59dc2
commit 0bfb52df00
97 changed files with 5033 additions and 164837 deletions

View File

@@ -1,8 +1,12 @@
from functools import partial
import networkx as nx
import re
import logging
import sys
from typing import List, Dict, Any, Union
from .utils.vessel_parser import get_vessel
from .utils.logger_util import action_log
from .pump_protocol import generate_pump_protocol_with_rinsing
logger = logging.getLogger(__name__)
@@ -20,48 +24,472 @@ def debug_print(message):
try:
# 确保消息是字符串格式
safe_message = str(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"🌀 [SEPARATE] {safe_message}", flush=True)
logger.info(f"[SEPARATE] {safe_message}")
except Exception as e:
# 最后的安全措施
fallback_message = f"日志输出错误: {repr(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"""
create_action_log = partial(action_log, prefix="[SEPARATE]")
def generate_separate_protocol(
G: nx.DiGraph,
# 🔧 基础参数支持XDL的vessel参数
vessel: dict = None, # 🔧 修改:从字符串改为字典类型
purpose: str = "separate", # 分离目的
product_phase: str = "top", # 产物相
# 🔧 可选的详细参数
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, # 溶剂体积
volume: Union[str, float] = 0.0, # XDL: 体积与solvent_volume同义
# 🔧 操作参数
through: str = "", # 通过材料
repeats: int = 1, # 重复次数
stir_time: float = 30.0, # 搅拌时间(秒)
stir_speed: float = 300.0, # 搅拌速度
settling_time: float = 300.0, # 沉降时间(秒)
**kwargs
) -> List[Dict[str, Any]]:
"""
生成分离操作的协议序列 - 支持vessel字典和体积运算
支持XDL参数格式
- vessel: 分离容器字典(必需)
- purpose: "wash", "extract", "separate"
- product_phase: "top", "bottom"
- product_vessel: 产物收集容器
- waste_vessel: 废液收集容器
- solvent: 溶剂名称
- volume: "200 mL", "?" 或数值
- repeats: 重复次数
分离流程:
1. (可选)添加溶剂到分离容器
2. 搅拌混合
3. 静置分层
4. 收集指定相到目标容器
5. 重复指定次数
"""
# 🔧 核心修改vessel参数兼容处理
if vessel is None:
if isinstance(separation_vessel, dict):
vessel = separation_vessel
else:
raise ValueError("必须提供vessel字典参数")
# 🔧 核心修改从字典中提取容器ID
vessel_id, vessel_data = get_vessel(vessel)
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}'")
debug_print(f" 📏 体积: {volume} (类型: {type(volume)})")
debug_print(f" 🔄 重复次数: {repeats}")
debug_print(f" 🎯 产物容器: '{product_vessel}'")
debug_print(f" 🗑️ 废液容器: '{waste_vessel}'")
debug_print(f" 📦 其他参数: {kwargs}")
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_id}", "🎬"))
action_sequence.append(create_action_log(f"分离目的: {purpose}", "🧪"))
action_sequence.append(create_action_log(f"产物相: {product_phase}", "📊"))
# 统一容器参数 - 支持字典和字符串
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_vessel_id, _ = vessel_id
final_to_vessel_id, _ = get_vessel(to_vessel) or get_vessel(product_vessel)
final_waste_vessel_id, _ = get_vessel(waste_phase_to_vessel) or get_vessel(waste_vessel)
# 统一体积参数
final_volume = parse_volume_input(volume or solvent_volume)
# 🔧 修复确保repeats至少为1
if repeats <= 0:
repeats = 1
debug_print(f"⚠️ 重复次数参数 <= 0自动设置为 1")
debug_print(f"🔧 标准化后的参数:")
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_id}", "🧪"))
action_sequence.append(create_action_log(f"溶剂体积: {final_volume}mL", "📏"))
action_sequence.append(create_action_log(f"重复次数: {repeats}", "🔄"))
# 验证必需参数
if not purpose:
purpose = "separate"
if not product_phase:
product_phase = "top"
if purpose not in ["wash", "extract", "separate"]:
debug_print(f"⚠️ 未知的分离目的 '{purpose}',使用默认值 'separate'")
purpose = "separate"
action_sequence.append(create_action_log(f"未知目的,使用: {purpose}", "⚠️"))
if product_phase not in ["top", "bottom"]:
debug_print(f"⚠️ 未知的产物相 '{product_phase}',使用默认值 'top'")
product_phase = "top"
action_sequence.append(create_action_log(f"未知相别,使用: {product_phase}", "⚠️"))
debug_print("✅ 参数验证通过")
action_sequence.append(create_action_log("参数验证通过", ""))
# === 查找设备 ===
debug_print("🔍 步骤2: 查找设备...")
action_sequence.append(create_action_log("正在查找相关设备...", "🔍"))
# 查找分离器设备
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:
debug_print("⚠️ 未找到分离器设备,可能无法执行分离")
action_sequence.append(create_action_log("未找到分离器设备", "⚠️"))
# 查找搅拌器
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:
action_sequence.append(create_action_log("未找到搅拌器", "⚠️"))
# 查找溶剂容器(如果需要)
solvent_vessel = ""
if solvent and solvent.strip():
solvent_vessel = find_solvent_vessel(G, solvent)
if solvent_vessel:
action_sequence.append(create_action_log(f"找到溶剂容器: {solvent_vessel}", "💧"))
else:
action_sequence.append(create_action_log(f"未找到溶剂容器: {solvent}", "⚠️"))
debug_print(f"📊 设备配置:")
debug_print(f" 🧪 分离器设备: '{separator_device}'")
debug_print(f" 🌪️ 搅拌器设备: '{stirrer_device}'")
debug_print(f" 💧 溶剂容器: '{solvent_vessel}'")
# === 执行分离流程 ===
debug_print("🔍 步骤3: 执行分离流程...")
action_sequence.append(create_action_log("开始分离工作流程", "🎯"))
# 🔧 新增:体积变化跟踪变量
current_volume = original_liquid_volume
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
}
}
for repeat_idx in range(repeats):
cycle_num = repeat_idx + 1
debug_print(f"🔄 第{cycle_num}轮: 开始分离循环 {cycle_num}/{repeats}")
action_sequence.append(create_action_log(f"分离循环 {cycle_num}/{repeats} 开始", "🔄"))
# 步骤3.1: 添加溶剂(如果需要)
if solvent_vessel and final_volume > 0:
debug_print(f"🔄 第{cycle_num}轮 步骤1: 添加溶剂 {solvent} ({final_volume}mL)")
action_sequence.append(create_action_log(f"向分离容器添加 {final_volume}mL {solvent}", "💧"))
try:
# 使用pump protocol添加溶剂
pump_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=solvent_vessel,
to_vessel=final_vessel_id, # 🔧 使用 final_vessel_id
volume=final_volume,
amount="",
time=0.0,
viscous=False,
rinsing_solvent="",
rinsing_volume=0.0,
rinsing_repeats=0,
solid=False,
flowrate=2.5,
transfer_flowrate=0.5,
rate_spec="",
event="",
through="",
**kwargs
)
action_sequence.extend(pump_actions)
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)}", ""))
else:
debug_print(f"🔄 第{cycle_num}轮 步骤1: 无需添加溶剂")
action_sequence.append(create_action_log("无需添加溶剂", "⏭️"))
# 步骤3.2: 启动搅拌(如果有搅拌器)
if stirrer_device and stir_time > 0:
debug_print(f"🔄 第{cycle_num}轮 步骤2: 开始搅拌 ({stir_speed}rpm持续 {stir_time}s)")
action_sequence.append(create_action_log(f"开始搅拌: {stir_speed}rpm持续 {stir_time}s", "🌪️"))
action_sequence.append({
"device_id": stirrer_device,
"action_name": "start_stir",
"action_kwargs": {
"vessel": final_vessel_id, # 🔧 使用 final_vessel_id
"stir_speed": stir_speed,
"purpose": f"分离混合 - {purpose}"
}
})
# 搅拌等待
stir_minutes = stir_time / 60
action_sequence.append(create_action_log(f"搅拌中,持续 {stir_minutes:.1f} 分钟", "⏱️"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": stir_time}
})
# 停止搅拌
action_sequence.append(create_action_log("停止搅拌器", "🛑"))
action_sequence.append({
"device_id": stirrer_device,
"action_name": "stop_stir",
"action_kwargs": {"vessel": final_vessel_id} # 🔧 使用 final_vessel_id
})
else:
debug_print(f"🔄 第{cycle_num}轮 步骤2: 无需搅拌")
action_sequence.append(create_action_log("无需搅拌", "⏭️"))
# 步骤3.3: 静置分层
if settling_time > 0:
debug_print(f"🔄 第{cycle_num}轮 步骤3: 静置分层 ({settling_time}s)")
settling_minutes = settling_time / 60
action_sequence.append(create_action_log(f"静置分层 ({settling_minutes:.1f} 分钟)", "⚖️"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": settling_time}
})
else:
debug_print(f"🔄 第{cycle_num}轮 步骤3: 未指定静置时间")
action_sequence.append(create_action_log("未指定静置时间", "⏭️"))
# 步骤3.4: 执行分离操作
if separator_device:
debug_print(f"🔄 第{cycle_num}轮 步骤4: 执行分离操作")
action_sequence.append(create_action_log(f"执行分离: 收集{product_phase}", "🧪"))
# 🔧 替换为具体的分离操作逻辑基于old版本
# 首先进行分液判断(电导突跃)
action_sequence.append({
"device_id": separator_device,
"action_name": "valve_open",
"action_kwargs": {
"command": "delta > 0.05"
}
})
# 估算每相的体积(假设大致平分)
phase_volume = current_volume / 2
# 智能查找分离容器底部
separation_vessel_bottom = find_separation_vessel_bottom(G, final_vessel_id) # ✅
if product_phase == "bottom":
debug_print(f"🔄 收集底相产物到 {final_to_vessel_id}")
action_sequence.append(create_action_log("收集底相产物", "📦"))
# 产物转移到目标瓶
if final_to_vessel_id:
pump_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=separation_vessel_bottom,
to_vessel=final_to_vessel_id,
volume=current_volume,
flowrate=2.5,
**kwargs
)
action_sequence.extend(pump_actions)
# 放出上面那一相60秒后关阀门
action_sequence.append({
"device_id": separator_device,
"action_name": "valve_open",
"action_kwargs": {
"command": "time > 60"
}
})
# 弃去上面那一相进废液
if final_waste_vessel_id:
pump_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=separation_vessel_bottom,
to_vessel=final_waste_vessel_id,
volume=current_volume,
flowrate=2.5,
**kwargs
)
action_sequence.extend(pump_actions)
elif product_phase == "top":
debug_print(f"🔄 收集上相产物到 {final_to_vessel_id}")
action_sequence.append(create_action_log("收集上相产物", "📦"))
# 弃去下面那一相进废液
if final_waste_vessel_id:
pump_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=separation_vessel_bottom,
to_vessel=final_waste_vessel_id,
volume=phase_volume,
flowrate=2.5,
**kwargs
)
action_sequence.extend(pump_actions)
# 放出上面那一相60秒后关阀门
action_sequence.append({
"device_id": separator_device,
"action_name": "valve_open",
"action_kwargs": {
"command": "time > 60"
}
})
# 产物转移到目标瓶
if final_to_vessel_id:
pump_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=separation_vessel_bottom,
to_vessel=final_to_vessel_id,
volume=phase_volume,
flowrate=2.5,
**kwargs
)
action_sequence.extend(pump_actions)
debug_print(f"✅ 分离操作已完成")
action_sequence.append(create_action_log("分离操作完成", ""))
# 🔧 新增:分离后体积估算
separated_volume = phase_volume * 0.95 # 假设5%损失,只保留产物相体积
update_vessel_volume(vessel, G, separated_volume, f"分离操作后(第{cycle_num}轮)")
current_volume = separated_volume
# 收集结果
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: 无分离器设备,跳过分离")
action_sequence.append(create_action_log("无分离器设备可用", ""))
# 添加等待时间模拟分离
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 10.0}
})
# 🔧 新增如果不是最后一次从中转瓶转移回分液漏斗基于old版本逻辑
if repeat_idx < repeats - 1 and final_to_vessel_id and final_to_vessel_id != final_vessel_id:
debug_print(f"🔄 第{cycle_num}轮: 产物转移回分离容器准备下一轮")
action_sequence.append(create_action_log("产物转回分离容器,准备下一轮", "🔄"))
pump_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=final_to_vessel_id,
to_vessel=final_vessel_id,
volume=current_volume,
flowrate=2.5,
**kwargs
)
action_sequence.extend(pump_actions)
# 更新体积回到分离容器
update_vessel_volume(vessel, G, current_volume, f"产物转回分离容器(第{cycle_num}轮后)")
# 循环间等待(除了最后一次)
if repeat_idx < repeats - 1:
debug_print(f"🔄 第{cycle_num}轮: 等待下一次循环...")
action_sequence.append(create_action_log("等待下一次循环...", ""))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5}
})
else:
action_sequence.append(create_action_log(f"分离循环 {cycle_num}/{repeats} 完成", "🌟"))
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
}
}
debug_print(f"❌ 分离工作流程执行失败: {str(e)}")
action_sequence.append(create_action_log(f"分离工作流程失败: {str(e)}", ""))
# 🔧 新增:分离完成后的最终状态报告
final_liquid_volume = get_vessel_liquid_volume(vessel)
# === 最终结果 ===
total_time = (stir_time + settling_time + 15) * repeats # 估算总时间
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_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 × {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_id} ({purpose}{repeats} 次循环)"
if solvent:
summary_msg += f",使用 {final_volume * repeats:.2f}mL {solvent}"
action_sequence.append(create_action_log(summary_msg, "🎉"))
return action_sequence
def parse_volume_input(volume_input: Union[str, float]) -> float:
"""
@@ -364,386 +792,54 @@ def update_vessel_volume(vessel: dict, G: nx.DiGraph, new_volume: float, descrip
debug_print(f"📊 容器 '{vessel_id}' 体积已更新为: {new_volume:.2f}mL")
def generate_separate_protocol(
G: nx.DiGraph,
# 🔧 基础参数支持XDL的vessel参数
vessel: dict = None, # 🔧 修改:从字符串改为字典类型
purpose: str = "separate", # 分离目的
product_phase: str = "top", # 产物相
# 🔧 可选的详细参数
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, # 溶剂体积
volume: Union[str, float] = 0.0, # XDL: 体积与solvent_volume同义
# 🔧 操作参数
through: str = "", # 通过材料
repeats: int = 1, # 重复次数
stir_time: float = 30.0, # 搅拌时间(秒)
stir_speed: float = 300.0, # 搅拌速度
settling_time: float = 300.0, # 沉降时间(秒)
**kwargs
) -> List[Dict[str, Any]]:
"""
生成分离操作的协议序列 - 支持vessel字典和体积运算
支持XDL参数格式
- vessel: 分离容器字典(必需)
- purpose: "wash", "extract", "separate"
- product_phase: "top", "bottom"
- product_vessel: 产物收集容器
- waste_vessel: 废液收集容器
- solvent: 溶剂名称
- volume: "200 mL", "?" 或数值
- repeats: 重复次数
分离流程:
1. (可选)添加溶剂到分离容器
2. 搅拌混合
3. 静置分层
4. 收集指定相到目标容器
5. 重复指定次数
"""
# 🔧 核心修改vessel参数兼容处理
if vessel is None:
if isinstance(separation_vessel, dict):
vessel = separation_vessel
else:
raise ValueError("必须提供vessel字典参数")
# 🔧 核心修改从字典中提取容器ID
# 统一处理vessel参数
if isinstance(vessel, dict):
if "id" not in vessel:
vessel_id = list(vessel.values())[0].get("id", "")
else:
vessel_id = vessel.get("id", "")
vessel_data = vessel.get("data", {})
else:
vessel_id = str(vessel)
vessel_data = G.nodes[vessel_id].get("data", {}) if vessel_id in G.nodes() else {}
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}'")
debug_print(f" 📏 体积: {volume} (类型: {type(volume)})")
debug_print(f" 🔄 重复次数: {repeats}")
debug_print(f" 🎯 产物容器: '{product_vessel}'")
debug_print(f" 🗑️ 废液容器: '{waste_vessel}'")
debug_print(f" 📦 其他参数: {kwargs}")
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_id}", "🎬"))
action_sequence.append(create_action_log(f"分离目的: {purpose}", "🧪"))
action_sequence.append(create_action_log(f"产物相: {product_phase}", "📊"))
# 统一容器参数 - 支持字典和字符串
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_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)
# 🔧 修复确保repeats至少为1
if repeats <= 0:
repeats = 1
debug_print(f"⚠️ 重复次数参数 <= 0自动设置为 1")
debug_print(f"🔧 标准化后的参数:")
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_id}", "🧪"))
action_sequence.append(create_action_log(f"溶剂体积: {final_volume}mL", "📏"))
action_sequence.append(create_action_log(f"重复次数: {repeats}", "🔄"))
# 验证必需参数
if not purpose:
purpose = "separate"
if not product_phase:
product_phase = "top"
if purpose not in ["wash", "extract", "separate"]:
debug_print(f"⚠️ 未知的分离目的 '{purpose}',使用默认值 'separate'")
purpose = "separate"
action_sequence.append(create_action_log(f"未知目的,使用: {purpose}", "⚠️"))
if product_phase not in ["top", "bottom"]:
debug_print(f"⚠️ 未知的产物相 '{product_phase}',使用默认值 'top'")
product_phase = "top"
action_sequence.append(create_action_log(f"未知相别,使用: {product_phase}", "⚠️"))
debug_print("✅ 参数验证通过")
action_sequence.append(create_action_log("参数验证通过", ""))
# === 查找设备 ===
debug_print("🔍 步骤2: 查找设备...")
action_sequence.append(create_action_log("正在查找相关设备...", "🔍"))
# 查找分离器设备
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:
debug_print("⚠️ 未找到分离器设备,可能无法执行分离")
action_sequence.append(create_action_log("未找到分离器设备", "⚠️"))
# 查找搅拌器
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:
action_sequence.append(create_action_log("未找到搅拌器", "⚠️"))
# 查找溶剂容器(如果需要)
solvent_vessel = ""
if solvent and solvent.strip():
solvent_vessel = find_solvent_vessel(G, solvent)
if solvent_vessel:
action_sequence.append(create_action_log(f"找到溶剂容器: {solvent_vessel}", "💧"))
else:
action_sequence.append(create_action_log(f"未找到溶剂容器: {solvent}", "⚠️"))
debug_print(f"📊 设备配置:")
debug_print(f" 🧪 分离器设备: '{separator_device}'")
debug_print(f" 🌪️ 搅拌器设备: '{stirrer_device}'")
debug_print(f" 💧 溶剂容器: '{solvent_vessel}'")
# === 执行分离流程 ===
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
debug_print(f"🔄 第{cycle_num}轮: 开始分离循环 {cycle_num}/{repeats}")
action_sequence.append(create_action_log(f"分离循环 {cycle_num}/{repeats} 开始", "🔄"))
# 步骤3.1: 添加溶剂(如果需要)
if solvent_vessel and final_volume > 0:
debug_print(f"🔄 第{cycle_num}轮 步骤1: 添加溶剂 {solvent} ({final_volume}mL)")
action_sequence.append(create_action_log(f"向分离容器添加 {final_volume}mL {solvent}", "💧"))
try:
# 使用pump protocol添加溶剂
pump_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=solvent_vessel,
to_vessel=final_vessel_id, # 🔧 使用 final_vessel_id
volume=final_volume,
amount="",
time=0.0,
viscous=False,
rinsing_solvent="",
rinsing_volume=0.0,
rinsing_repeats=0,
solid=False,
flowrate=2.5,
transfer_flowrate=0.5,
rate_spec="",
event="",
through="",
**kwargs
)
action_sequence.extend(pump_actions)
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)}", ""))
else:
debug_print(f"🔄 第{cycle_num}轮 步骤1: 无需添加溶剂")
action_sequence.append(create_action_log("无需添加溶剂", "⏭️"))
# 步骤3.2: 启动搅拌(如果有搅拌器)
if stirrer_device and stir_time > 0:
debug_print(f"🔄 第{cycle_num}轮 步骤2: 开始搅拌 ({stir_speed}rpm持续 {stir_time}s)")
action_sequence.append(create_action_log(f"开始搅拌: {stir_speed}rpm持续 {stir_time}s", "🌪️"))
action_sequence.append({
"device_id": stirrer_device,
"action_name": "start_stir",
"action_kwargs": {
"vessel": final_vessel_id, # 🔧 使用 final_vessel_id
"stir_speed": stir_speed,
"purpose": f"分离混合 - {purpose}"
}
})
# 搅拌等待
stir_minutes = stir_time / 60
action_sequence.append(create_action_log(f"搅拌中,持续 {stir_minutes:.1f} 分钟", "⏱️"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": stir_time}
})
# 停止搅拌
action_sequence.append(create_action_log("停止搅拌器", "🛑"))
action_sequence.append({
"device_id": stirrer_device,
"action_name": "stop_stir",
"action_kwargs": {"vessel": final_vessel_id} # 🔧 使用 final_vessel_id
})
else:
debug_print(f"🔄 第{cycle_num}轮 步骤2: 无需搅拌")
action_sequence.append(create_action_log("无需搅拌", "⏭️"))
# 步骤3.3: 静置分层
if settling_time > 0:
debug_print(f"🔄 第{cycle_num}轮 步骤3: 静置分层 ({settling_time}s)")
settling_minutes = settling_time / 60
action_sequence.append(create_action_log(f"静置分层 ({settling_minutes:.1f} 分钟)", "⚖️"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": settling_time}
})
else:
debug_print(f"🔄 第{cycle_num}轮 步骤3: 未指定静置时间")
action_sequence.append(create_action_log("未指定静置时间", "⏭️"))
# 步骤3.4: 执行分离操作
if separator_device:
debug_print(f"🔄 第{cycle_num}轮 步骤4: 执行分离操作")
action_sequence.append(create_action_log(f"执行分离: 收集{product_phase}", "🧪"))
# 调用分离器设备的separate方法
separate_action = {
"device_id": separator_device,
"action_name": "separate",
"action_kwargs": {
"purpose": purpose,
"product_phase": product_phase,
"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,
"repeats": 1, # 每次调用只做一次分离
"stir_time": 0, # 已经在上面完成
"stir_speed": stir_speed,
"settling_time": 0 # 已经在上面完成
}
}
action_sequence.append(separate_action)
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_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: 无分离器设备,跳过分离")
action_sequence.append(create_action_log("无分离器设备可用", ""))
# 添加等待时间模拟分离
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 10.0}
})
# 循环间等待(除了最后一次)
if repeat_idx < repeats - 1:
debug_print(f"🔄 第{cycle_num}轮: 等待下一次循环...")
action_sequence.append(create_action_log("等待下一次循环...", ""))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5}
})
else:
action_sequence.append(create_action_log(f"分离循环 {cycle_num}/{repeats} 完成", "🌟"))
except Exception as e:
debug_print(f"❌ 分离工作流程执行失败: {str(e)}")
action_sequence.append(create_action_log(f"分离工作流程失败: {str(e)}", ""))
# 添加错误日志
action_sequence.append({
"device_id": "system",
"action_name": "log_message",
"action_kwargs": {
"message": f"分离操作失败: {str(e)}"
}
})
# 🔧 新增:分离完成后的最终状态报告
final_liquid_volume = get_vessel_liquid_volume(vessel)
# === 最终结果 ===
total_time = (stir_time + settling_time + 15) * repeats # 估算总时间
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_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 × {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_id} ({purpose}{repeats} 次循环)"
if solvent:
summary_msg += f",使用 {final_volume * repeats:.2f}mL {solvent}"
action_sequence.append(create_action_log(summary_msg, "🎉"))
return action_sequence
def find_separation_vessel_bottom(G: nx.DiGraph, vessel_id: str) -> str:
"""
智能查找分离容器的底部容器假设为flask或vessel类型
Args:
G: 网络图
vessel_id: 分离容器ID
Returns:
str: 底部容器ID
"""
debug_print(f"🔍 查找分离容器 {vessel_id} 的底部容器...")
# 方法1根据命名规则推测
possible_bottoms = [
f"{vessel_id}_bottom",
f"flask_{vessel_id}",
f"vessel_{vessel_id}",
f"{vessel_id}_flask",
f"{vessel_id}_vessel"
]
debug_print(f"📋 尝试的底部容器名称: {possible_bottoms}")
for bottom_id in possible_bottoms:
if bottom_id in G.nodes():
node_type = G.nodes[bottom_id].get('type', '')
if node_type == 'container':
debug_print(f"✅ 通过命名规则找到底部容器: {bottom_id}")
return bottom_id
# 方法2查找与分离器相连的容器假设底部容器会与分离器相连
debug_print(f"📋 方法2: 查找连接的容器...")
for node in G.nodes():
node_data = G.nodes[node]
node_class = node_data.get('class', '') or ''
if 'separator' in node_class.lower():
# 检查分离器的输入端
if G.has_edge(node, vessel_id):
for neighbor in G.neighbors(node):
if neighbor != vessel_id:
neighbor_type = G.nodes[neighbor].get('type', '')
if neighbor_type == 'container':
debug_print(f"✅ 通过连接找到底部容器: {neighbor}")
return neighbor
debug_print(f"❌ 无法找到分离容器 {vessel_id} 的底部容器")
return ""