Files
Uni-Lab-OS/unilabos/compile/separate_protocol.py
2025-07-10 16:48:09 +08:00

449 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

import networkx as nx
import re
import logging
from typing import List, Dict, Any, Union
from .pump_protocol import generate_pump_protocol_with_rinsing
logger = logging.getLogger(__name__)
def debug_print(message):
"""调试输出"""
print(f"[SEPARATE] {message}", flush=True)
logger.info(f"[SEPARATE] {message}")
def parse_volume_input(volume_input: Union[str, float]) -> float:
"""
解析体积输入,支持带单位的字符串
Args:
volume_input: 体积输入(如 "200 mL", "?", 50.0
Returns:
float: 体积(毫升)
"""
if isinstance(volume_input, (int, float)):
return float(volume_input)
if not volume_input or not str(volume_input).strip():
return 0.0
volume_str = str(volume_input).lower().strip()
debug_print(f"解析体积输入: '{volume_str}'")
# 处理未知体积
if volume_str in ['?', 'unknown', 'tbd', 'to be determined']:
default_volume = 100.0 # 默认100mL
debug_print(f"检测到未知体积,使用默认值: {default_volume}mL")
return default_volume
# 移除空格并提取数字和单位
volume_clean = re.sub(r'\s+', '', volume_str)
# 匹配数字和单位的正则表达式
match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
if not match:
debug_print(f"⚠️ 无法解析体积: '{volume_str}'使用默认值100mL")
return 100.0
value = float(match.group(1))
unit = match.group(2) or 'ml' # 默认单位为毫升
# 转换为毫升
if unit in ['l', 'liter']:
volume = value * 1000.0 # L -> mL
elif unit in ['μl', 'ul', 'microliter']:
volume = value / 1000.0 # μL -> mL
else: # ml, milliliter 或默认
volume = value # 已经是mL
debug_print(f"体积转换: {value}{unit}{volume}mL")
return volume
def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
"""查找溶剂容器"""
if not solvent or not solvent.strip():
return ""
debug_print(f"查找溶剂 '{solvent}' 的容器...")
# 🔧 方法1直接搜索 data.reagent_name 和 config.reagent
for node in G.nodes():
node_data = G.nodes[node].get('data', {})
node_type = G.nodes[node].get('type', '')
config_data = G.nodes[node].get('config', {})
# 只搜索容器类型的节点
if node_type == 'container':
reagent_name = node_data.get('reagent_name', '').lower()
config_reagent = config_data.get('reagent', '').lower()
# 精确匹配
if reagent_name == solvent.lower() or config_reagent == solvent.lower():
debug_print(f"✅ 通过reagent字段找到容器: {node}")
return node
# 模糊匹配
if (solvent.lower() in reagent_name and reagent_name) or \
(solvent.lower() in config_reagent and config_reagent):
debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node}")
return node
# 🔧 方法2常见的容器命名规则
solvent_clean = solvent.lower().replace(' ', '_').replace('-', '_')
possible_names = [
f"flask_{solvent_clean}",
f"bottle_{solvent_clean}",
f"vessel_{solvent_clean}",
f"{solvent_clean}_flask",
f"{solvent_clean}_bottle",
f"solvent_{solvent_clean}",
f"reagent_{solvent_clean}",
f"reagent_bottle_{solvent_clean}"
]
for name in possible_names:
if name in G.nodes():
node_type = G.nodes[name].get('type', '')
if node_type == 'container':
debug_print(f"✅ 通过命名规则找到容器: {name}")
return name
# 🔧 方法3使用第一个试剂瓶作为备选
for node_id in G.nodes():
node_data = G.nodes[node_id]
if (node_data.get('type') == 'container' and
('reagent' in node_id.lower() or 'bottle' in node_id.lower())):
debug_print(f"⚠️ 未找到专用容器,使用备选容器: {node_id}")
return node_id
debug_print(f"⚠️ 未找到溶剂 '{solvent}' 的容器")
return ""
def find_separator_device(G: nx.DiGraph, vessel: str) -> str:
"""查找分离器设备"""
debug_print(f"查找容器 '{vessel}' 对应的分离器设备...")
# 方法1查找连接到容器的分离器设备
for node in G.nodes():
node_class = G.nodes[node].get('class', '').lower()
if 'separator' in node_class:
# 检查是否连接到目标容器
if G.has_edge(node, vessel) or G.has_edge(vessel, node):
debug_print(f"✅ 找到连接的分离器: {node}")
return node
# 方法2根据命名规则查找
possible_names = [
f"{vessel}_controller",
f"{vessel}_separator",
vessel, # 容器本身可能就是分离器
"separator_1",
"virtual_separator"
]
for name in possible_names:
if name in G.nodes():
node_class = G.nodes[name].get('class', '').lower()
if 'separator' in node_class:
debug_print(f"✅ 通过命名规则找到分离器: {name}")
return name
# 方法3查找第一个分离器设备
for node in G.nodes():
node_class = G.nodes[node].get('class', '').lower()
if 'separator' in node_class:
debug_print(f"⚠️ 使用第一个分离器设备: {node}")
return node
debug_print(f"⚠️ 未找到分离器设备")
return ""
def generate_separate_protocol(
G: nx.DiGraph,
# 🔧 基础参数支持XDL的vessel参数
vessel: str = "", # XDL: 分离容器
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同义
# 🔧 溶剂相关参数
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]]:
"""
生成分离操作的协议序列 - 修复版
支持XDL参数格式
- vessel: 分离容器(必需)
- purpose: "wash", "extract", "separate"
- product_phase: "top", "bottom"
- product_vessel: 产物收集容器
- waste_vessel: 废液收集容器
- solvent: 溶剂名称
- volume: "200 mL", "?" 或数值
- repeats: 重复次数
分离流程:
1. (可选)添加溶剂到分离容器
2. 搅拌混合
3. 静置分层
4. 收集指定相到目标容器
5. 重复指定次数
"""
debug_print("=" * 60)
debug_print("开始生成分离协议 - 修复版")
debug_print(f"原始参数:")
debug_print(f" - vessel: '{vessel}'")
debug_print(f" - purpose: '{purpose}'")
debug_print(f" - product_phase: '{product_phase}'")
debug_print(f" - solvent: '{solvent}'")
debug_print(f" - volume: {volume} (类型: {type(volume)})")
debug_print(f" - repeats: {repeats}")
debug_print(f" - product_vessel: '{product_vessel}'")
debug_print(f" - waste_vessel: '{waste_vessel}'")
debug_print("=" * 60)
action_sequence = []
# === 参数验证和标准化 ===
debug_print("步骤1: 参数验证和标准化...")
# 统一容器参数
final_vessel = vessel or separation_vessel
if not final_vessel:
raise ValueError("必须指定分离容器 (vessel 或 separation_vessel)")
final_to_vessel = to_vessel or product_vessel
final_waste_vessel = waste_phase_to_vessel or waste_vessel
# 统一体积参数
final_volume = parse_volume_input(volume or solvent_volume)
# 🔧 修复确保repeats至少为1
if repeats <= 0:
repeats = 1
debug_print(f"⚠️ repeats参数 <= 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_volume}mL")
debug_print(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"
if product_phase not in ["top", "bottom"]:
debug_print(f"⚠️ 未知的产物相 '{product_phase}',使用默认值 'top'")
product_phase = "top"
debug_print("✅ 参数验证通过")
# === 查找设备 ===
debug_print("步骤2: 查找设备...")
# 查找分离器设备
separator_device = find_separator_device(G, final_vessel)
if not separator_device:
debug_print("⚠️ 未找到分离器设备,可能无法执行分离操作")
# 查找溶剂容器(如果需要)
solvent_vessel = ""
if solvent and solvent.strip():
solvent_vessel = find_solvent_vessel(G, solvent)
debug_print(f"设备映射:")
debug_print(f" - 分离器设备: '{separator_device}'")
debug_print(f" - 溶剂容器: '{solvent_vessel}'")
# === 执行分离流程 ===
debug_print("步骤3: 执行分离流程...")
try:
for repeat_idx in range(repeats):
debug_print(f"3.{repeat_idx+1}: 第 {repeat_idx+1}/{repeats} 次分离")
# 步骤3.1: 添加溶剂(如果需要)
if solvent_vessel and final_volume > 0:
debug_print(f"3.{repeat_idx+1}.1: 添加溶剂 {solvent} ({final_volume}mL)")
# 使用pump protocol添加溶剂
pump_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=solvent_vessel,
to_vessel=final_vessel,
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)} 个动作")
# 步骤3.2: 执行分离操作
if separator_device:
debug_print(f"3.{repeat_idx+1}.2: 执行分离操作")
# 调用分离器设备的separate方法
separate_action = {
"device_id": separator_device,
"action_name": "separate",
"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,
"solvent": solvent,
"solvent_volume": final_volume,
"through": through,
"repeats": 1, # 每次调用只做一次分离
"stir_time": stir_time,
"stir_speed": stir_speed,
"settling_time": settling_time
}
}
action_sequence.append(separate_action)
debug_print(f"✅ 分离操作添加完成")
else:
debug_print(f"3.{repeat_idx+1}.2: 无分离器设备,跳过分离操作")
# 添加等待时间模拟分离
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": stir_time + settling_time}
})
# 等待间隔(除了最后一次)
if repeat_idx < repeats - 1:
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5}
})
except Exception as e:
debug_print(f"⚠️ 分离流程执行失败: {str(e)}")
# 添加错误日志
action_sequence.append({
"device_id": "system",
"action_name": "log_message",
"action_kwargs": {
"message": f"分离操作失败: {str(e)}"
}
})
# === 最终结果 ===
debug_print("=" * 60)
debug_print(f"✅ 分离协议生成完成")
debug_print(f"📊 总动作数: {len(action_sequence)}")
debug_print(f"📋 处理总结:")
debug_print(f" - 分离容器: {final_vessel}")
debug_print(f" - 分离目的: {purpose}")
debug_print(f" - 产物相: {product_phase}")
debug_print(f" - 重复次数: {repeats}")
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)
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]]:
"""仅进行相分离(不添加溶剂)"""
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]]:
"""用溶剂洗涤"""
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]]:
"""用溶剂萃取"""
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]]:
"""水-有机相分离"""
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():
"""测试分离协议的各种参数解析"""
print("=== SEPARATE PROTOCOL 增强版测试 ===")
# 测试体积解析
volumes = ["200 mL", "?", 100.0, "1 L", "500 μL"]
for vol in volumes:
result = parse_volume_input(vol)
print(f"体积解析: {vol}{result}mL")
print("✅ 测试完成")
if __name__ == "__main__":
test_separate_protocol()