Files
Uni-Lab-OS/unilabos/compile/run_column_protocol.py
Junhan Chang 0ab4027de7 refactor: 14个协议编译器去重精简,删除死代码
- 统一debug_print为共享import,移除14个本地定义
- 移除重复工具函数(find_connected_stirrer, get_vessel_liquid_volume等)
- 精简装饰性日志(emoji分隔线、进度提示),保留关键决策点
- 删除evacuateandrefill_protocol_old.py死代码
- 涉及文件:add, adjustph, clean_vessel, dissolve, dry, evacuateandrefill,
  evaporate, filter, pump, recrystallize, reset_handling, run_column,
  stir, wash_solid

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 13:12:10 +08:00

473 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.
from typing import List, Dict, Any, Union
import networkx as nx
import logging
import re
from .utils.vessel_parser import get_vessel, find_solvent_vessel
from .utils.resource_helper import get_resource_id, get_resource_data, get_resource_liquid_volume, update_vessel_volume
from .utils.logger_util import debug_print
from .pump_protocol import generate_pump_protocol_with_rinsing
logger = logging.getLogger(__name__)
def parse_percentage(pct_str: str) -> float:
"""
解析百分比字符串为数值
Args:
pct_str: 百分比字符串(如 "40 %", "40%", "40"
Returns:
float: 百分比数值0-100
"""
if not pct_str or not pct_str.strip():
return 0.0
pct_str = pct_str.strip().lower()
# 移除百分号和空格
pct_clean = re.sub(r'[%\s]', '', pct_str)
match = re.search(r'([0-9]*\.?[0-9]+)', pct_clean)
if match:
value = float(match.group(1))
return value
debug_print(f"无法解析百分比: '{pct_str}'返回0.0")
return 0.0
def parse_ratio(ratio_str: str) -> tuple:
"""
解析比例字符串为两个数值
Args:
ratio_str: 比例字符串(如 "5:95", "1:1", "40:60"
Returns:
tuple: (ratio1, ratio2) 两个比例值(百分比)
"""
if not ratio_str or not ratio_str.strip():
return (50.0, 50.0)
ratio_str = ratio_str.strip()
# 支持多种分隔符:: / -
if ':' in ratio_str:
parts = ratio_str.split(':')
elif '/' in ratio_str:
parts = ratio_str.split('/')
elif '-' in ratio_str:
parts = ratio_str.split('-')
elif 'to' in ratio_str.lower():
parts = ratio_str.lower().split('to')
else:
debug_print(f"无法解析比例格式: '{ratio_str}'使用默认1:1")
return (50.0, 50.0)
if len(parts) >= 2:
try:
ratio1 = float(parts[0].strip())
ratio2 = float(parts[1].strip())
total = ratio1 + ratio2
pct1 = (ratio1 / total) * 100
pct2 = (ratio2 / total) * 100
return (pct1, pct2)
except ValueError as e:
debug_print(f"比例数值转换失败: {str(e)}")
debug_print(f"比例解析失败使用默认1:1")
return (50.0, 50.0)
def parse_rf_value(rf_str: str) -> float:
"""
解析Rf值字符串
Args:
rf_str: Rf值字符串"0.3", "0.45", "?"
Returns:
float: Rf值0-1
"""
if not rf_str or not rf_str.strip():
return 0.3
rf_str = rf_str.strip().lower()
if rf_str in ['?', 'unknown', 'tbd', 'to be determined']:
return 0.3
match = re.search(r'([0-9]*\.?[0-9]+)', rf_str)
if match:
value = float(match.group(1))
if value > 1.0:
value = value / 100.0
value = max(0.0, min(1.0, value))
return value
return 0.3
def find_column_device(G: nx.DiGraph) -> str:
"""查找柱层析设备"""
for node in G.nodes():
node_data = G.nodes[node]
node_class = node_data.get('class', '') or ''
if 'virtual_column' in node_class.lower() or 'column' in node_class.lower():
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}")
return name
debug_print("未找到柱层析设备将使用pump protocol直接转移")
return ""
def find_column_vessel(G: nx.DiGraph, column: str) -> str:
"""查找柱容器"""
if column in G.nodes():
node_type = G.nodes[column].get('type', '')
if node_type == 'container':
return column
possible_names = [
f"column_{column}",
f"{column}_column",
f"vessel_{column}",
f"{column}_vessel",
"column_vessel",
"chromatography_column",
"silica_column",
"preparative_column",
"column"
]
for vessel_name in possible_names:
if vessel_name in G.nodes():
node_type = G.nodes[vessel_name].get('type', '')
if node_type == 'container':
return vessel_name
return ""
def calculate_solvent_volumes(total_volume: float, pct1: float, pct2: float) -> tuple:
"""根据百分比计算溶剂体积"""
volume1 = (total_volume * pct1) / 100.0
volume2 = (total_volume * pct2) / 100.0
return (volume1, volume2)
def generate_run_column_protocol(
G: nx.DiGraph,
from_vessel: dict,
to_vessel: dict,
column: str,
rf: str = "",
pct1: str = "",
pct2: str = "",
solvent1: str = "",
solvent2: str = "",
ratio: str = "",
**kwargs
) -> List[Dict[str, Any]]:
"""
生成柱层析分离的协议序列 - 支持vessel字典和体积运算
Args:
G: 有向图,节点为设备和容器,边为流体管道
from_vessel: 源容器字典从XDL传入
to_vessel: 目标容器字典从XDL传入
column: 所使用的柱子的名称(必需)
rf: Rf值可选支持 "?" 表示未知)
pct1: 第一种溶剂百分比(如 "40 %",可选)
pct2: 第二种溶剂百分比(如 "50 %",可选)
solvent1: 第一种溶剂名称(可选)
solvent2: 第二种溶剂名称(可选)
ratio: 溶剂比例(如 "5:95"可选优先级高于pct1/pct2
**kwargs: 其他可选参数
Returns:
List[Dict[str, Any]]: 柱层析分离操作的动作序列
"""
from_vessel_id, _ = get_vessel(from_vessel)
to_vessel_id, _ = get_vessel(to_vessel)
debug_print(f"开始生成柱层析协议: {from_vessel_id} -> {to_vessel_id}, column={column}")
action_sequence = []
# 记录柱层析前的容器状态
original_from_volume = get_resource_liquid_volume(from_vessel)
original_to_volume = get_resource_liquid_volume(to_vessel)
# === 参数验证 ===
if not from_vessel_id:
raise ValueError("from_vessel 参数不能为空")
if not to_vessel_id:
raise ValueError("to_vessel 参数不能为空")
if not column:
raise ValueError("column 参数不能为空")
if from_vessel_id not in G.nodes():
raise ValueError(f"源容器 '{from_vessel_id}' 不存在于系统中")
if to_vessel_id not in G.nodes():
raise ValueError(f"目标容器 '{to_vessel_id}' 不存在于系统中")
# === 参数解析 ===
final_rf = parse_rf_value(rf)
if ratio and ratio.strip():
final_pct1, final_pct2 = parse_ratio(ratio)
else:
final_pct1 = parse_percentage(pct1) if pct1 else 50.0
final_pct2 = parse_percentage(pct2) if pct2 else 50.0
total_pct = final_pct1 + final_pct2
if total_pct == 0:
final_pct1, final_pct2 = 50.0, 50.0
elif total_pct != 100.0:
final_pct1 = (final_pct1 / total_pct) * 100
final_pct2 = (final_pct2 / total_pct) * 100
final_solvent1 = solvent1.strip() if solvent1 else "ethyl_acetate"
final_solvent2 = solvent2.strip() if solvent2 else "hexane"
debug_print(f"参数: rf={final_rf}, 溶剂={final_solvent1}:{final_solvent2} = {final_pct1:.1f}%:{final_pct2:.1f}%")
# === 查找设备和容器 ===
column_device_id = find_column_device(G)
column_vessel = find_column_vessel(G, column)
solvent1_vessel = find_solvent_vessel(G, final_solvent1)
solvent2_vessel = find_solvent_vessel(G, final_solvent2)
# === 获取源容器体积 ===
source_volume = original_from_volume
if source_volume <= 0:
source_volume = 50.0
# === 计算溶剂体积 ===
total_elution_volume = source_volume * 3.0
solvent1_volume, solvent2_volume = calculate_solvent_volumes(
total_elution_volume, final_pct1, final_pct2
)
# === 执行柱层析流程 ===
current_from_volume = source_volume
current_to_volume = original_to_volume
current_column_volume = 0.0
try:
# 步骤1: 样品上柱
if column_vessel and column_vessel != from_vessel_id:
try:
sample_transfer_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=from_vessel_id,
to_vessel=column_vessel,
volume=source_volume,
flowrate=1.0,
transfer_flowrate=0.5,
rinsing_solvent="",
rinsing_volume=0.0,
rinsing_repeats=0
)
action_sequence.extend(sample_transfer_actions)
current_from_volume = 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
except Exception as e:
debug_print(f"样品上柱失败: {str(e)}")
# 步骤2: 添加洗脱溶剂1
if solvent1_vessel and solvent1_volume > 0:
try:
target_vessel = column_vessel if column_vessel else from_vessel_id
solvent1_transfer_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=solvent1_vessel,
to_vessel=target_vessel,
volume=solvent1_volume,
flowrate=2.0,
transfer_flowrate=1.0
)
action_sequence.extend(solvent1_transfer_actions)
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
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)}")
# 步骤3: 添加洗脱溶剂2
if solvent2_vessel and solvent2_volume > 0:
try:
target_vessel = column_vessel if column_vessel else from_vessel_id
solvent2_transfer_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=solvent2_vessel,
to_vessel=target_vessel,
volume=solvent2_volume,
flowrate=2.0,
transfer_flowrate=1.0
)
action_sequence.extend(solvent2_transfer_actions)
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
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)}")
# 步骤4: 使用柱层析设备执行分离
if column_device_id:
column_separation_action = {
"device_id": column_device_id,
"action_name": "run_column",
"action_kwargs": {
"from_vessel": from_vessel_id,
"to_vessel": to_vessel_id,
"column": column,
"rf": rf,
"pct1": pct1,
"pct2": pct2,
"solvent1": solvent1,
"solvent2": solvent2,
"ratio": ratio
}
}
action_sequence.append(column_separation_action)
separation_time = max(30, min(120, int(total_elution_volume / 2)))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": separation_time}
})
# 步骤5: 产物收集
if column_vessel and column_vessel != to_vessel_id:
try:
product_volume = source_volume * 0.8
product_transfer_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=column_vessel,
to_vessel=to_vessel_id,
volume=product_volume,
flowrate=1.5,
transfer_flowrate=0.8
)
action_sequence.extend(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)
except Exception as e:
debug_print(f"产物收集失败: {str(e)}")
# 步骤6: 简化模式 - 直接转移
if not column_device_id and not column_vessel:
try:
direct_transfer_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=from_vessel_id,
to_vessel=to_vessel_id,
volume=source_volume,
flowrate=2.0,
transfer_flowrate=1.0
)
action_sequence.extend(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)}")
except Exception as e:
debug_print(f"协议生成失败: {str(e)}")
if not action_sequence:
action_sequence.append({
"action_name": "wait",
"action_kwargs": {
"time": 1.0,
"description": "柱层析协议执行完成"
}
})
final_from_volume = get_resource_liquid_volume(from_vessel)
final_to_volume = get_resource_liquid_volume(to_vessel)
debug_print(f"柱层析协议生成完成: {len(action_sequence)} 个动作, {from_vessel_id} -> {to_vessel_id}, 收集={final_to_volume - original_to_volume:.2f}mL")
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]]:
"""乙酸乙酯-己烷柱层析(常用组合)"""
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]]:
"""甲醇-二氯甲烷柱层析"""
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]]:
"""梯度洗脱柱层析(中等比例)"""
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]]:
"""极性化合物柱层析(高极性溶剂比例)"""
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]]:
"""非极性化合物柱层析(低极性溶剂比例)"""
return generate_run_column_protocol(G, from_vessel, to_vessel, column,
solvent1="ethyl_acetate", solvent2="hexane", ratio="5:95")
# 测试函数
def test_run_column_protocol():
"""测试柱层析协议"""
debug_print("=== RUN COLUMN PROTOCOL 测试 ===")
debug_print("测试完成")
if __name__ == "__main__":
test_run_column_protocol()