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

596 lines
20 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"[ADD] {message}", flush=True)
logger.info(f"[ADD] {message}")
def parse_volume_input(volume_input: Union[str, float]) -> float:
"""
解析体积输入,支持带单位的字符串
Args:
volume_input: 体积输入(如 "2.7 mL", "2.67 mL", "?", 10.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 = 10.0 # 默认10mL
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}'使用默认值10mL")
return 10.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 parse_mass_input(mass_input: Union[str, float]) -> float:
"""
解析质量输入,支持带单位的字符串
Args:
mass_input: 质量输入(如 "19.3 g", "4.5 g", 2.5
Returns:
float: 质量(克)
"""
if isinstance(mass_input, (int, float)):
return float(mass_input)
if not mass_input or not str(mass_input).strip():
return 0.0
mass_str = str(mass_input).lower().strip()
debug_print(f"解析质量输入: '{mass_str}'")
# 移除空格并提取数字和单位
mass_clean = re.sub(r'\s+', '', mass_str)
# 匹配数字和单位的正则表达式
match = re.match(r'([0-9]*\.?[0-9]+)\s*(g|mg|kg|gram|milligram|kilogram)?', mass_clean)
if not match:
debug_print(f"⚠️ 无法解析质量: '{mass_str}'返回0.0g")
return 0.0
value = float(match.group(1))
unit = match.group(2) or 'g' # 默认单位为克
# 转换为克
if unit in ['mg', 'milligram']:
mass = value / 1000.0 # mg -> g
elif unit in ['kg', 'kilogram']:
mass = value * 1000.0 # kg -> g
else: # g, gram 或默认
mass = value # 已经是g
debug_print(f"质量转换: {value}{unit}{mass}g")
return mass
def parse_time_input(time_input: Union[str, float]) -> float:
"""
解析时间输入,支持带单位的字符串
Args:
time_input: 时间输入(如 "1 h", "20 min", "30 s", 60.0
Returns:
float: 时间(秒)
"""
if isinstance(time_input, (int, float)):
return float(time_input)
if not time_input or not str(time_input).strip():
return 0.0
time_str = str(time_input).lower().strip()
debug_print(f"解析时间输入: '{time_str}'")
# 处理未知时间
if time_str in ['?', 'unknown', 'tbd']:
default_time = 60.0 # 默认1分钟
debug_print(f"检测到未知时间,使用默认值: {default_time}s")
return default_time
# 移除空格并提取数字和单位
time_clean = re.sub(r'\s+', '', time_str)
# 匹配数字和单位的正则表达式
match = re.match(r'([0-9]*\.?[0-9]+)\s*(s|sec|second|min|minute|h|hr|hour|d|day)?', time_clean)
if not match:
debug_print(f"⚠️ 无法解析时间: '{time_str}'返回0s")
return 0.0
value = float(match.group(1))
unit = match.group(2) or 's' # 默认单位为秒
# 转换为秒
if unit in ['min', 'minute']:
time_sec = value * 60.0 # min -> s
elif unit in ['h', 'hr', 'hour']:
time_sec = value * 3600.0 # h -> s
elif unit in ['d', 'day']:
time_sec = value * 86400.0 # d -> s
else: # s, sec, second 或默认
time_sec = value # 已经是s
debug_print(f"时间转换: {value}{unit}{time_sec}s")
return time_sec
def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str:
"""增强版试剂容器查找,支持固体和液体"""
debug_print(f"查找试剂 '{reagent}' 的容器...")
# 🔧 方法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 == reagent.lower() or config_reagent == reagent.lower():
debug_print(f"✅ 通过reagent字段找到容器: {node}")
return node
# 模糊匹配
if (reagent.lower() in reagent_name and reagent_name) or \
(reagent.lower() in config_reagent and config_reagent):
debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node}")
return node
# 🔧 方法2常见的容器命名规则
reagent_clean = reagent.lower().replace(' ', '_').replace('-', '_')
possible_names = [
reagent_clean,
f"flask_{reagent_clean}",
f"bottle_{reagent_clean}",
f"vessel_{reagent_clean}",
f"{reagent_clean}_flask",
f"{reagent_clean}_bottle",
f"reagent_{reagent_clean}",
f"reagent_bottle_{reagent_clean}",
f"solid_reagent_bottle_{reagent_clean}",
f"reagent_bottle_1", # 通用试剂瓶
f"reagent_bottle_2",
f"reagent_bottle_3"
]
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':
# 检查节点名称是否包含试剂名称
if reagent_clean in node_id.lower():
debug_print(f"✅ 通过节点名称模糊匹配到容器: {node_id}")
return node_id
# 检查液体类型匹配
vessel_data = node_data.get('data', {})
liquids = vessel_data.get('liquid', [])
for liquid in liquids:
if isinstance(liquid, dict):
liquid_type = liquid.get('liquid_type') or liquid.get('name', '')
if liquid_type.lower() == reagent.lower():
debug_print(f"✅ 通过液体类型匹配到容器: {node_id}")
return node_id
# 🔧 方法4使用第一个试剂瓶作为备选
for node_id in G.nodes():
node_data = G.nodes[node_id]
if (node_data.get('type') == 'container' and
('reagent' in node_id.lower() or 'bottle' in node_id.lower())):
debug_print(f"⚠️ 未找到专用容器,使用备选试剂瓶: {node_id}")
return node_id
raise ValueError(f"找不到试剂 '{reagent}' 对应的容器")
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
"""查找连接到指定容器的搅拌器"""
stirrer_nodes = []
for node in G.nodes():
node_class = G.nodes[node].get('class', '').lower()
if 'stirrer' in node_class:
stirrer_nodes.append(node)
# 查找连接到容器的搅拌器
for stirrer in stirrer_nodes:
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
debug_print(f"找到连接的搅拌器: {stirrer}")
return stirrer
# 返回第一个搅拌器
if stirrer_nodes:
debug_print(f"使用第一个搅拌器: {stirrer_nodes[0]}")
return stirrer_nodes[0]
return ""
def find_solid_dispenser(G: nx.DiGraph) -> str:
"""查找固体加样器"""
for node in G.nodes():
node_class = G.nodes[node].get('class', '').lower()
if 'solid_dispenser' in node_class or 'dispenser' in node_class:
debug_print(f"找到固体加样器: {node}")
return node
debug_print("⚠️ 未找到固体加样器")
return ""
def generate_add_protocol(
G: nx.DiGraph,
vessel: str,
reagent: str,
# 🔧 修复:所有参数都用 Union 类型,支持字符串和数值
volume: Union[str, float] = 0.0,
mass: Union[str, float] = 0.0,
amount: str = "",
time: Union[str, float] = 0.0,
stir: bool = False,
stir_speed: float = 300.0,
viscous: bool = False,
purpose: str = "添加试剂",
# XDL扩展参数
mol: str = "",
event: str = "",
rate_spec: str = "",
equiv: str = "",
ratio: str = "",
**kwargs
) -> List[Dict[str, Any]]:
"""
生成添加试剂协议 - 修复版
支持所有XDL参数和单位
- volume: "2.7 mL", "2.67 mL", "?" 或数值
- mass: "19.3 g", "4.5 g" 或数值
- time: "1 h", "20 min" 或数值(秒)
- mol: "0.28 mol", "16.2 mmol", "25.2 mmol"
- rate_spec: "portionwise", "dropwise"
- event: "A", "B"
- equiv: "1.1"
- ratio: "?", "1:1"
"""
debug_print("=" * 60)
debug_print("开始生成添加试剂协议")
debug_print(f"原始参数:")
debug_print(f" - vessel: '{vessel}'")
debug_print(f" - reagent: '{reagent}'")
debug_print(f" - volume: {volume} (类型: {type(volume)})")
debug_print(f" - mass: {mass} (类型: {type(mass)})")
debug_print(f" - time: {time} (类型: {type(time)})")
debug_print(f" - mol: '{mol}'")
debug_print(f" - event: '{event}'")
debug_print(f" - rate_spec: '{rate_spec}'")
debug_print("=" * 60)
action_sequence = []
# === 参数验证 ===
debug_print("步骤1: 参数验证...")
if not vessel:
raise ValueError("vessel 参数不能为空")
if not reagent:
raise ValueError("reagent 参数不能为空")
if vessel not in G.nodes():
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
debug_print("✅ 基本参数验证通过")
# === 🔧 关键修复:参数解析 ===
debug_print("步骤2: 参数解析...")
# 解析各种参数为数值
final_volume = parse_volume_input(volume)
final_mass = parse_mass_input(mass)
final_time = parse_time_input(time)
debug_print(f"解析结果:")
debug_print(f" - 体积: {final_volume}mL")
debug_print(f" - 质量: {final_mass}g")
debug_print(f" - 时间: {final_time}s")
debug_print(f" - 摩尔: '{mol}'")
debug_print(f" - 事件: '{event}'")
debug_print(f" - 速率: '{rate_spec}'")
# === 判断添加类型 ===
debug_print("步骤3: 判断添加类型...")
# 🔧 修复:现在使用解析后的数值进行比较
is_solid = (final_mass > 0 or (mol and mol.strip() != ""))
is_liquid = (final_volume > 0)
if not is_solid and not is_liquid:
# 默认为液体10mL
is_liquid = True
final_volume = 10.0
debug_print("⚠️ 未指定体积或质量默认为10mL液体")
debug_print(f"添加类型: {'固体' if is_solid else '液体'}")
# === 执行添加流程 ===
debug_print("步骤4: 执行添加流程...")
try:
if is_solid:
# === 固体添加路径 ===
debug_print(f"使用固体添加路径")
solid_dispenser = find_solid_dispenser(G)
if solid_dispenser:
# 启动搅拌
if stir:
stirrer_id = find_connected_stirrer(G, vessel)
if stirrer_id:
action_sequence.append({
"device_id": stirrer_id,
"action_name": "start_stir",
"action_kwargs": {
"vessel": vessel,
"stir_speed": stir_speed,
"purpose": f"准备添加固体 {reagent}"
}
})
# 等待搅拌稳定
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 3}
})
# 固体加样
add_kwargs = {
"vessel": vessel,
"reagent": reagent,
"purpose": purpose,
"event": event,
"rate_spec": rate_spec
}
if final_mass > 0:
add_kwargs["mass"] = str(final_mass)
if mol and mol.strip():
add_kwargs["mol"] = mol
if equiv and equiv.strip():
add_kwargs["equiv"] = equiv
action_sequence.append({
"device_id": solid_dispenser,
"action_name": "add_solid",
"action_kwargs": add_kwargs
})
# 添加后等待
if final_time > 0:
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": final_time}
})
debug_print(f"✅ 固体添加完成")
else:
debug_print("⚠️ 未找到固体加样器,跳过固体添加")
else:
# === 液体添加路径 ===
debug_print(f"使用液体添加路径")
# 查找试剂容器
reagent_vessel = find_reagent_vessel(G, reagent)
# 启动搅拌
if stir:
stirrer_id = find_connected_stirrer(G, vessel)
if stirrer_id:
action_sequence.append({
"device_id": stirrer_id,
"action_name": "start_stir",
"action_kwargs": {
"vessel": vessel,
"stir_speed": stir_speed,
"purpose": f"准备添加液体 {reagent}"
}
})
# 等待搅拌稳定
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5}
})
# 计算流速
if final_time > 0:
flowrate = final_volume / final_time * 60 # mL/min
transfer_flowrate = flowrate
else:
if rate_spec == "dropwise":
flowrate = 0.5 # 滴加,很慢
transfer_flowrate = 0.2
elif viscous:
flowrate = 1.0 # 粘性液体
transfer_flowrate = 0.3
else:
flowrate = 2.5 # 正常流速
transfer_flowrate = 0.5
debug_print(f"流速设置: {flowrate} mL/min")
# 调用pump protocol
pump_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=reagent_vessel,
to_vessel=vessel,
volume=final_volume,
amount=amount,
time=final_time,
viscous=viscous,
rinsing_solvent="",
rinsing_volume=0.0,
rinsing_repeats=0,
solid=False,
flowrate=flowrate,
transfer_flowrate=transfer_flowrate,
rate_spec=rate_spec,
event=event,
through="",
**kwargs
)
action_sequence.extend(pump_actions)
debug_print(f"✅ 液体转移完成,添加了 {len(pump_actions)} 个动作")
except Exception as e:
debug_print(f"⚠️ 试剂添加失败: {str(e)}")
# 添加错误日志
action_sequence.append({
"device_id": "system",
"action_name": "log_message",
"action_kwargs": {
"message": f"试剂 '{reagent}' 添加失败: {str(e)}"
}
})
# === 最终结果 ===
debug_print("=" * 60)
debug_print(f"✅ 添加试剂协议生成完成")
debug_print(f"📊 总动作数: {len(action_sequence)}")
debug_print(f"📋 处理总结:")
debug_print(f" - 试剂: {reagent}")
debug_print(f" - 添加类型: {'固体' if is_solid else '液体'}")
debug_print(f" - 目标容器: {vessel}")
if is_liquid:
debug_print(f" - 体积: {final_volume}mL")
if is_solid:
debug_print(f" - 质量: {final_mass}g")
debug_print(f" - 摩尔: {mol}")
debug_print("=" * 60)
return action_sequence
# === 便捷函数 ===
def add_liquid_volume(G: nx.DiGraph, vessel: str, reagent: str, volume: Union[str, float],
time: Union[str, float] = 0.0, rate_spec: str = "") -> List[Dict[str, Any]]:
"""添加指定体积的液体试剂"""
return generate_add_protocol(
G, vessel, reagent,
volume=volume,
time=time,
rate_spec=rate_spec
)
def add_solid_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, float],
event: str = "") -> List[Dict[str, Any]]:
"""添加指定质量的固体试剂"""
return generate_add_protocol(
G, vessel, reagent,
mass=mass,
event=event
)
def add_solid_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str,
event: str = "") -> List[Dict[str, Any]]:
"""按摩尔数添加固体试剂"""
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],
time: Union[str, float] = "20 min", event: str = "") -> List[Dict[str, Any]]:
"""滴加液体试剂"""
return generate_add_protocol(
G, vessel, reagent,
volume=volume,
time=time,
rate_spec="dropwise",
event=event
)
def add_portionwise_solid(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, float],
time: Union[str, float] = "1 h", event: str = "") -> List[Dict[str, Any]]:
"""分批添加固体试剂"""
return generate_add_protocol(
G, vessel, reagent,
mass=mass,
time=time,
rate_spec="portionwise",
event=event
)
# 测试函数
def test_add_protocol():
"""测试添加协议的各种参数解析"""
print("=== ADD PROTOCOL 增强版测试 ===")
# 测试体积解析
volumes = ["2.7 mL", "2.67 mL", "?", 10.0, "1 L", "500 μL"]
for vol in volumes:
result = parse_volume_input(vol)
print(f"体积解析: {vol}{result}mL")
# 测试质量解析
masses = ["19.3 g", "4.5 g", 2.5, "500 mg", "1 kg"]
for mass in masses:
result = parse_mass_input(mass)
print(f"质量解析: {mass}{result}g")
# 测试时间解析
times = ["1 h", "20 min", "30 s", 60.0, "?"]
for time in times:
result = parse_time_input(time)
print(f"时间解析: {time}{result}s")
print("✅ 测试完成")
if __name__ == "__main__":
test_add_protocol()