From ed3b22a7380ae19d7f3d4dceec5fd718ee33cab7 Mon Sep 17 00:00:00 2001 From: KCFeng425 <2100011801@stu.pku.edu.cn> Date: Wed, 16 Jul 2025 10:38:12 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=E4=BA=86=E5=89=A9=E4=B8=8B?= =?UTF-8?q?=E7=9A=84=E5=87=A0=E4=B8=AAprotocol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- unilabos/app/mq.py | 2 +- unilabos/compile/add_protocol.py | 224 +++-- unilabos/compile/adjustph_protocol.py | 313 +++++-- unilabos/compile/dissolve_protocol.py | 315 +++++-- .../compile/evacuateandrefill_protocol.py | 406 ++++++--- unilabos/compile/pump_protocol.py | 850 +++++++++++++++++- unilabos/compile/separate_protocol.py | 426 ++++++--- .../devices/virtual/virtual_multiway_valve.py | 22 +- .../devices/virtual/virtual_transferpump.py | 205 +++-- unilabos/ros/nodes/presets/protocol_node.py | 2 +- 10 files changed, 2229 insertions(+), 536 deletions(-) diff --git a/unilabos/app/mq.py b/unilabos/app/mq.py index 061add2..12ec2c0 100644 --- a/unilabos/app/mq.py +++ b/unilabos/app/mq.py @@ -166,7 +166,7 @@ class MQTTClient: status = {"data": device_status.get(device_id, {}), "device_id": device_id} address = f"labs/{MQConfig.lab_id}/devices/" self.client.publish(address, json.dumps(status), qos=2) - logger.debug(f"Device status published: address: {address}, {status}") + # logger.debug(f"Device status published: address: {address}, {status}") def publish_job_status(self, feedback_data: dict, job_id: str, status: str, return_info: Optional[str] = None): if self.mqtt_disable: diff --git a/unilabos/compile/add_protocol.py b/unilabos/compile/add_protocol.py index b5b017e..c46befe 100644 --- a/unilabos/compile/add_protocol.py +++ b/unilabos/compile/add_protocol.py @@ -22,18 +22,20 @@ def parse_volume_input(volume_input: Union[str, float]) -> float: float: 体积(毫升) """ if isinstance(volume_input, (int, float)): + debug_print(f"📏 体积输入为数值: {volume_input}") return float(volume_input) if not volume_input or not str(volume_input).strip(): + debug_print(f"⚠️ 体积输入为空,返回0.0mL") return 0.0 volume_str = str(volume_input).lower().strip() - debug_print(f"解析体积输入: '{volume_str}'") + 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") + debug_print(f"❓ 检测到未知体积,使用默认值: {default_volume}mL 🎯") return default_volume # 移除空格并提取数字和单位 @@ -43,7 +45,7 @@ def parse_volume_input(volume_input: Union[str, float]) -> float: 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") + debug_print(f"❌ 无法解析体积: '{volume_str}',使用默认值10mL") return 10.0 value = float(match.group(1)) @@ -52,12 +54,14 @@ def parse_volume_input(volume_input: Union[str, float]) -> float: # 转换为毫升 if unit in ['l', 'liter']: volume = value * 1000.0 # L -> mL + debug_print(f"🔄 体积转换: {value}L → {volume}mL") elif unit in ['μl', 'ul', 'microliter']: volume = value / 1000.0 # μL -> mL + debug_print(f"🔄 体积转换: {value}μL → {volume}mL") else: # ml, milliliter 或默认 volume = value # 已经是mL + debug_print(f"✅ 体积已为mL: {volume}mL") - debug_print(f"体积转换: {value}{unit} → {volume}mL") return volume def parse_mass_input(mass_input: Union[str, float]) -> float: @@ -71,13 +75,15 @@ def parse_mass_input(mass_input: Union[str, float]) -> float: float: 质量(克) """ if isinstance(mass_input, (int, float)): + debug_print(f"⚖️ 质量输入为数值: {mass_input}g") return float(mass_input) if not mass_input or not str(mass_input).strip(): + debug_print(f"⚠️ 质量输入为空,返回0.0g") return 0.0 mass_str = str(mass_input).lower().strip() - debug_print(f"解析质量输入: '{mass_str}'") + debug_print(f"🔍 解析质量输入: '{mass_str}'") # 移除空格并提取数字和单位 mass_clean = re.sub(r'\s+', '', mass_str) @@ -86,7 +92,7 @@ def parse_mass_input(mass_input: Union[str, float]) -> float: 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") + debug_print(f"❌ 无法解析质量: '{mass_str}',返回0.0g") return 0.0 value = float(match.group(1)) @@ -95,12 +101,14 @@ def parse_mass_input(mass_input: Union[str, float]) -> float: # 转换为克 if unit in ['mg', 'milligram']: mass = value / 1000.0 # mg -> g + debug_print(f"🔄 质量转换: {value}mg → {mass}g") elif unit in ['kg', 'kilogram']: mass = value * 1000.0 # kg -> g + debug_print(f"🔄 质量转换: {value}kg → {mass}g") else: # g, gram 或默认 mass = value # 已经是g + debug_print(f"✅ 质量已为g: {mass}g") - debug_print(f"质量转换: {value}{unit} → {mass}g") return mass def parse_time_input(time_input: Union[str, float]) -> float: @@ -114,18 +122,20 @@ def parse_time_input(time_input: Union[str, float]) -> float: float: 时间(秒) """ if isinstance(time_input, (int, float)): + debug_print(f"⏱️ 时间输入为数值: {time_input}秒") return float(time_input) if not time_input or not str(time_input).strip(): + debug_print(f"⚠️ 时间输入为空,返回0秒") return 0.0 time_str = str(time_input).lower().strip() - debug_print(f"解析时间输入: '{time_str}'") + debug_print(f"🔍 解析时间输入: '{time_str}'") # 处理未知时间 if time_str in ['?', 'unknown', 'tbd']: default_time = 60.0 # 默认1分钟 - debug_print(f"检测到未知时间,使用默认值: {default_time}s") + debug_print(f"❓ 检测到未知时间,使用默认值: {default_time}s (1分钟) ⏰") return default_time # 移除空格并提取数字和单位 @@ -135,7 +145,7 @@ def parse_time_input(time_input: Union[str, float]) -> float: 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") + debug_print(f"❌ 无法解析时间: '{time_str}',返回0s") return 0.0 value = float(match.group(1)) @@ -144,21 +154,25 @@ def parse_time_input(time_input: Union[str, float]) -> float: # 转换为秒 if unit in ['min', 'minute']: time_sec = value * 60.0 # min -> s + debug_print(f"🔄 时间转换: {value}分钟 → {time_sec}秒") elif unit in ['h', 'hr', 'hour']: time_sec = value * 3600.0 # h -> s + debug_print(f"🔄 时间转换: {value}小时 → {time_sec}秒") elif unit in ['d', 'day']: time_sec = value * 86400.0 # d -> s + debug_print(f"🔄 时间转换: {value}天 → {time_sec}秒") else: # s, sec, second 或默认 time_sec = value # 已经是s + debug_print(f"✅ 时间已为秒: {time_sec}秒") - 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}' 的容器...") + debug_print(f"🔍 开始查找试剂 '{reagent}' 的容器...") # 🔧 方法1:直接搜索 data.reagent_name 和 config.reagent + debug_print(f"📋 方法1: 搜索reagent字段...") for node in G.nodes(): node_data = G.nodes[node].get('data', {}) node_type = G.nodes[node].get('type', '') @@ -171,16 +185,17 @@ def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str: # 精确匹配 if reagent_name == reagent.lower() or config_reagent == reagent.lower(): - debug_print(f"✅ 通过reagent字段找到容器: {node}") + 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}") + debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node} 🔍") return node # 🔧 方法2:常见的容器命名规则 + debug_print(f"📋 方法2: 使用命名规则查找...") reagent_clean = reagent.lower().replace(' ', '_').replace('-', '_') possible_names = [ reagent_clean, @@ -197,20 +212,23 @@ def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str: f"reagent_bottle_3" ] + debug_print(f"🔍 尝试的容器名称: {possible_names[:5]}... (共{len(possible_names)}个)") + 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}") + debug_print(f"✅ 通过命名规则找到容器: {name} 📝") return name # 🔧 方法3:节点名称模糊匹配 + debug_print(f"📋 方法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}") + debug_print(f"✅ 通过节点名称模糊匹配到容器: {node_id} 🔍") return node_id # 检查液体类型匹配 @@ -220,51 +238,77 @@ def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str: 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}") + debug_print(f"✅ 通过液体类型匹配到容器: {node_id} 💧") return node_id # 🔧 方法4:使用第一个试剂瓶作为备选 + debug_print(f"📋 方法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}") + debug_print(f"⚠️ 未找到专用容器,使用备选试剂瓶: {node_id} 🔄") return node_id + debug_print(f"❌ 所有方法都失败了,无法找到容器!") raise ValueError(f"找不到试剂 '{reagent}' 对应的容器") def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str: """查找连接到指定容器的搅拌器""" + debug_print(f"🔍 查找连接到容器 '{vessel}' 的搅拌器...") + stirrer_nodes = [] for node in G.nodes(): node_class = G.nodes[node].get('class', '').lower() if 'stirrer' in node_class: stirrer_nodes.append(node) + debug_print(f"📋 发现搅拌器: {node}") + + debug_print(f"📊 共找到 {len(stirrer_nodes)} 个搅拌器") # 查找连接到容器的搅拌器 for stirrer in stirrer_nodes: if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): - debug_print(f"找到连接的搅拌器: {stirrer}") + debug_print(f"✅ 找到连接的搅拌器: {stirrer} 🔗") return stirrer # 返回第一个搅拌器 if stirrer_nodes: - debug_print(f"使用第一个搅拌器: {stirrer_nodes[0]}") + debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个: {stirrer_nodes[0]} 🔄") return stirrer_nodes[0] + debug_print(f"❌ 未找到任何搅拌器") return "" def find_solid_dispenser(G: nx.DiGraph) -> str: """查找固体加样器""" + debug_print(f"🔍 查找固体加样器...") + 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}") + debug_print(f"✅ 找到固体加样器: {node} 🥄") return node - debug_print("⚠️ 未找到固体加样器") + debug_print(f"❌ 未找到固体加样器") return "" +# 🆕 创建进度日志动作 +def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]: + """创建一个动作日志""" + full_message = f"{emoji} {message}" + debug_print(full_message) + logger.info(full_message) + print(f"[ACTION] {full_message}", flush=True) + + return { + "action_name": "wait", + "action_kwargs": { + "time": 0.1, + "log_message": full_message + } + } + def generate_add_protocol( G: nx.DiGraph, vessel: str, @@ -301,51 +345,58 @@ def generate_add_protocol( """ 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("🚀 开始生成添加试剂协议") + 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(f" 🌪️ stir: {stir}") + debug_print(f" 🔄 stir_speed: {stir_speed} rpm") debug_print("=" * 60) action_sequence = [] # === 参数验证 === - debug_print("步骤1: 参数验证...") + debug_print("🔍 步骤1: 参数验证...") + action_sequence.append(create_action_log(f"开始添加试剂 '{reagent}' 到容器 '{vessel}'", "🎬")) if not vessel: + 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}' 不存在于系统中") debug_print("✅ 基本参数验证通过") # === 🔧 关键修复:参数解析 === - debug_print("步骤2: 参数解析...") + debug_print("🔍 步骤2: 参数解析...") + action_sequence.append(create_action_log("正在解析添加参数...", "🔍")) # 解析各种参数为数值 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(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: 判断添加类型...") + debug_print("🔍 步骤3: 判断添加类型...") # 🔧 修复:现在使用解析后的数值进行比较 is_solid = (final_mass > 0 or (mol and mol.strip() != "")) @@ -357,22 +408,34 @@ def generate_add_protocol( final_volume = 10.0 debug_print("⚠️ 未指定体积或质量,默认为10mL液体") - debug_print(f"添加类型: {'固体' if is_solid else '液体'}") + add_type = "固体" if is_solid else "液体" + add_emoji = "🧂" if is_solid else "💧" + debug_print(f"📋 添加类型: {add_type} {add_emoji}") + + action_sequence.append(create_action_log(f"确定添加类型: {add_type} {add_emoji}", "📋")) # === 执行添加流程 === - debug_print("步骤4: 执行添加流程...") + debug_print("🔍 步骤4: 执行添加流程...") try: if is_solid: # === 固体添加路径 === - debug_print(f"使用固体添加路径") + debug_print(f"🧂 使用固体添加路径") + action_sequence.append(create_action_log("开始固体试剂添加流程", "🧂")) solid_dispenser = find_solid_dispenser(G) if solid_dispenser: + action_sequence.append(create_action_log(f"找到固体加样器: {solid_dispenser}", "🥄")) + # 启动搅拌 if stir: + debug_print("🌪️ 准备启动搅拌...") + action_sequence.append(create_action_log("准备启动搅拌器", "🌪️")) + stirrer_id = find_connected_stirrer(G, vessel) if stirrer_id: + action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {stir_speed} rpm)", "🔄")) + action_sequence.append({ "device_id": stirrer_id, "action_name": "start_stir", @@ -383,6 +446,7 @@ def generate_add_protocol( } }) # 等待搅拌稳定 + action_sequence.append(create_action_log("等待搅拌稳定...", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 3} @@ -399,19 +463,27 @@ def generate_add_protocol( if final_mass > 0: add_kwargs["mass"] = str(final_mass) + action_sequence.append(create_action_log(f"准备添加固体: {final_mass}g", "⚖️")) if mol and mol.strip(): add_kwargs["mol"] = mol + action_sequence.append(create_action_log(f"按摩尔数添加: {mol}", "🧬")) if equiv and equiv.strip(): add_kwargs["equiv"] = equiv + action_sequence.append(create_action_log(f"当量: {equiv}", "🔢")) + action_sequence.append(create_action_log("开始固体加样操作", "🥄")) action_sequence.append({ "device_id": solid_dispenser, "action_name": "add_solid", "action_kwargs": add_kwargs }) + action_sequence.append(create_action_log("固体加样完成", "✅")) + # 添加后等待 if final_time > 0: + wait_minutes = final_time / 60 + action_sequence.append(create_action_log(f"等待反应进行 ({wait_minutes:.1f}分钟)", "⏰")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": final_time} @@ -419,19 +491,28 @@ def generate_add_protocol( debug_print(f"✅ 固体添加完成") else: - debug_print("⚠️ 未找到固体加样器,跳过固体添加") + debug_print("❌ 未找到固体加样器,跳过固体添加") + action_sequence.append(create_action_log("未找到固体加样器,无法添加固体", "❌")) else: # === 液体添加路径 === - debug_print(f"使用液体添加路径") + debug_print(f"💧 使用液体添加路径") + action_sequence.append(create_action_log("开始液体试剂添加流程", "💧")) # 查找试剂容器 + action_sequence.append(create_action_log("正在查找试剂容器...", "🔍")) reagent_vessel = find_reagent_vessel(G, reagent) + action_sequence.append(create_action_log(f"找到试剂容器: {reagent_vessel}", "🧪")) # 启动搅拌 if stir: + debug_print("🌪️ 准备启动搅拌...") + action_sequence.append(create_action_log("准备启动搅拌器", "🌪️")) + stirrer_id = find_connected_stirrer(G, vessel) if stirrer_id: + action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {stir_speed} rpm)", "🔄")) + action_sequence.append({ "device_id": stirrer_id, "action_name": "start_stir", @@ -442,6 +523,7 @@ def generate_add_protocol( } }) # 等待搅拌稳定 + action_sequence.append(create_action_log("等待搅拌稳定...", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 5} @@ -451,18 +533,23 @@ def generate_add_protocol( if final_time > 0: flowrate = final_volume / final_time * 60 # mL/min transfer_flowrate = flowrate + debug_print(f"⚡ 根据时间计算流速: {flowrate:.2f} mL/min") else: if rate_spec == "dropwise": flowrate = 0.5 # 滴加,很慢 transfer_flowrate = 0.2 + debug_print(f"💧 滴加模式,流速: {flowrate} mL/min") elif viscous: flowrate = 1.0 # 粘性液体 transfer_flowrate = 0.3 + debug_print(f"🍯 粘性液体,流速: {flowrate} mL/min") else: flowrate = 2.5 # 正常流速 transfer_flowrate = 0.5 + debug_print(f"⚡ 正常流速: {flowrate} mL/min") - debug_print(f"流速设置: {flowrate} mL/min") + action_sequence.append(create_action_log(f"设置流速: {flowrate:.2f} mL/min", "⚡")) + action_sequence.append(create_action_log(f"开始转移 {final_volume}mL 液体", "🚰")) # 调用pump protocol pump_actions = generate_pump_protocol_with_rinsing( @@ -486,9 +573,11 @@ def generate_add_protocol( ) action_sequence.extend(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)}", "❌")) # 添加错误日志 action_sequence.append({ "device_id": "system", @@ -500,19 +589,28 @@ def generate_add_protocol( # === 最终结果 === debug_print("=" * 60) - debug_print(f"✅ 添加试剂协议生成完成") + 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}") + debug_print(f" 🧪 试剂: {reagent}") + debug_print(f" {add_emoji} 添加类型: {add_type}") + debug_print(f" 🥼 目标容器: {vessel}") if is_liquid: - debug_print(f" - 体积: {final_volume}mL") + debug_print(f" 📏 体积: {final_volume}mL") if is_solid: - debug_print(f" - 质量: {final_mass}g") - debug_print(f" - 摩尔: {mol}") + debug_print(f" ⚖️ 质量: {final_mass}g") + debug_print(f" 🧬 摩尔: {mol}") debug_print("=" * 60) + # 添加完成日志 + summary_msg = f"试剂添加协议完成: {reagent} → {vessel}" + if is_liquid: + summary_msg += f" ({final_volume}mL)" + if is_solid: + summary_msg += f" ({final_mass}g)" + + action_sequence.append(create_action_log(summary_msg, "🎉")) + return action_sequence # === 便捷函数 === @@ -520,6 +618,7 @@ def generate_add_protocol( 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]]: """添加指定体积的液体试剂""" + debug_print(f"💧 快速添加液体: {reagent} ({volume}) → {vessel}") return generate_add_protocol( G, vessel, reagent, volume=volume, @@ -530,6 +629,7 @@ def add_liquid_volume(G: nx.DiGraph, vessel: str, reagent: str, volume: Union[st def add_solid_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, float], event: str = "") -> List[Dict[str, Any]]: """添加指定质量的固体试剂""" + debug_print(f"🧂 快速添加固体: {reagent} ({mass}) → {vessel}") return generate_add_protocol( G, vessel, reagent, mass=mass, @@ -539,6 +639,7 @@ def add_solid_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, fl def add_solid_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str, event: str = "") -> List[Dict[str, Any]]: """按摩尔数添加固体试剂""" + debug_print(f"🧬 按摩尔数添加固体: {reagent} ({mol}) → {vessel}") return generate_add_protocol( G, vessel, reagent, mol=mol, @@ -548,6 +649,7 @@ def add_solid_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str, 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]]: """滴加液体试剂""" + debug_print(f"💧 滴加液体: {reagent} ({volume}) → {vessel} (用时: {time})") return generate_add_protocol( G, vessel, reagent, volume=volume, @@ -559,6 +661,7 @@ def add_dropwise_liquid(G: nx.DiGraph, vessel: str, reagent: str, volume: Union[ 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]]: """分批添加固体试剂""" + debug_print(f"🧂 分批添加固体: {reagent} ({mass}) → {vessel} (用时: {time})") return generate_add_protocol( G, vessel, reagent, mass=mass, @@ -573,22 +676,25 @@ def test_add_protocol(): print("=== ADD PROTOCOL 增强版测试 ===") # 测试体积解析 + debug_print("🧪 测试体积解析...") 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") + print(f"📏 体积解析: {vol} → {result}mL") # 测试质量解析 + debug_print("⚖️ 测试质量解析...") 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") + print(f"⚖️ 质量解析: {mass} → {result}g") # 测试时间解析 + debug_print("⏱️ 测试时间解析...") times = ["1 h", "20 min", "30 s", 60.0, "?"] for time in times: result = parse_time_input(time) - print(f"时间解析: {time} → {result}s") + print(f"⏱️ 时间解析: {time} → {result}s") print("✅ 测试完成") diff --git a/unilabos/compile/adjustph_protocol.py b/unilabos/compile/adjustph_protocol.py index ce7c1c3..d8f1b1b 100644 --- a/unilabos/compile/adjustph_protocol.py +++ b/unilabos/compile/adjustph_protocol.py @@ -1,7 +1,30 @@ import networkx as nx +import logging from typing import List, Dict, Any from .pump_protocol import generate_pump_protocol_with_rinsing +logger = logging.getLogger(__name__) + +def debug_print(message): + """调试输出""" + print(f"[ADJUST_PH] {message}", flush=True) + logger.info(f"[ADJUST_PH] {message}") + +# 🆕 创建进度日志动作 +def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]: + """创建一个动作日志""" + full_message = f"{emoji} {message}" + debug_print(full_message) + logger.info(full_message) + print(f"[ACTION] {full_message}", flush=True) + + return { + "action_name": "wait", + "action_kwargs": { + "time": 0.1, + "log_message": full_message + } + } def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str: """ @@ -14,7 +37,7 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str: Returns: str: 试剂容器ID """ - print(f"ADJUST_PH: 正在查找试剂 '{reagent}' 的容器...") + debug_print(f"🔍 正在查找试剂 '{reagent}' 的容器...") # 常见酸碱试剂的别名映射 reagent_aliases = { @@ -29,11 +52,16 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str: # 构建搜索名称列表 search_names = [reagent.lower()] + debug_print(f"📋 基础搜索名称: {reagent.lower()}") # 添加别名 for base_name, aliases in reagent_aliases.items(): if reagent.lower() in base_name.lower() or base_name.lower() in reagent.lower(): search_names.extend([alias.lower() for alias in aliases]) + debug_print(f"🔗 添加别名: {aliases}") + break + + debug_print(f"📝 完整搜索列表: {search_names}") # 构建可能的容器名称 possible_names = [] @@ -49,13 +77,17 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str: name_clean ]) + debug_print(f"🎯 可能的容器名称 (前5个): {possible_names[:5]}... (共{len(possible_names)}个)") + # 第一步:通过容器名称匹配 + debug_print(f"📋 方法1: 精确名称匹配...") for vessel_name in possible_names: if vessel_name in G.nodes(): - print(f"ADJUST_PH: 通过名称匹配找到容器: {vessel_name}") + debug_print(f"✅ 通过名称匹配找到容器: {vessel_name} 🎯") return vessel_name # 第二步:通过模糊匹配 + debug_print(f"📋 方法2: 模糊名称匹配...") for node_id in G.nodes(): if G.nodes[node_id].get('type') == 'container': node_name = G.nodes[node_id].get('name', '').lower() @@ -63,10 +95,11 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str: # 检查是否包含任何搜索名称 for search_name in search_names: if search_name in node_id.lower() or search_name in node_name: - print(f"ADJUST_PH: 通过模糊匹配找到容器: {node_id}") + debug_print(f"✅ 通过模糊匹配找到容器: {node_id} 🔍") return node_id # 第三步:通过液体类型匹配 + debug_print(f"📋 方法3: 液体类型匹配...") for node_id in G.nodes(): if G.nodes[node_id].get('type') == 'container': vessel_data = G.nodes[node_id].get('data', {}) @@ -79,10 +112,11 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str: for search_name in search_names: if search_name in liquid_type or search_name in reagent_name: - print(f"ADJUST_PH: 通过液体类型匹配找到容器: {node_id}") + debug_print(f"✅ 通过液体类型匹配找到容器: {node_id} 💧") return node_id # 列出可用容器帮助调试 + debug_print(f"📊 列出可用容器帮助调试...") available_containers = [] for node_id in G.nodes(): if G.nodes[node_id].get('type') == 'container': @@ -98,67 +132,92 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str: 'reagent_name': vessel_data.get('reagent_name', '') }) - print(f"ADJUST_PH: 可用容器列表:") + debug_print(f"📋 可用容器列表:") for container in available_containers: - print(f" - {container['id']}: {container['name']}") - print(f" 液体: {container['liquids']}") - print(f" 试剂: {container['reagent_name']}") + debug_print(f" - 🧪 {container['id']}: {container['name']}") + debug_print(f" 💧 液体: {container['liquids']}") + debug_print(f" 🏷️ 试剂: {container['reagent_name']}") - raise ValueError(f"找不到试剂 '{reagent}' 对应的容器。尝试了: {possible_names}") - + debug_print(f"❌ 所有匹配方法都失败了") + raise ValueError(f"找不到试剂 '{reagent}' 对应的容器。尝试了: {possible_names[:10]}...") def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str: """查找与容器相连的搅拌器""" + debug_print(f"🔍 查找连接到容器 '{vessel}' 的搅拌器...") + stirrer_nodes = [node for node in G.nodes() if (G.nodes[node].get('class') or '') == 'virtual_stirrer'] + debug_print(f"📊 发现 {len(stirrer_nodes)} 个搅拌器: {stirrer_nodes}") + for stirrer in stirrer_nodes: if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): + debug_print(f"✅ 找到连接的搅拌器: {stirrer} 🔗") return stirrer - return stirrer_nodes[0] if stirrer_nodes else None + if stirrer_nodes: + debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个: {stirrer_nodes[0]} 🔄") + return stirrer_nodes[0] + + debug_print(f"❌ 未找到任何搅拌器") + return None - -def calculate_reagent_volume(target_ph_value: float, reagent: str, vessel_volume: float = 100.0) -> float: # 改为 target_ph_value +def calculate_reagent_volume(target_ph_value: float, reagent: str, vessel_volume: float = 100.0) -> float: """ 估算需要的试剂体积来调节pH Args: - target_ph_value: 目标pH值 # 改为 target_ph_value + target_ph_value: 目标pH值 reagent: 试剂名称 vessel_volume: 容器体积 (mL) Returns: float: 估算的试剂体积 (mL) """ + debug_print(f"🧮 计算试剂体积...") + debug_print(f" 📍 目标pH: {target_ph_value}") + debug_print(f" 🧪 试剂: {reagent}") + debug_print(f" 📏 容器体积: {vessel_volume}mL") + # 简化的pH调节体积估算(实际应用中需要更精确的计算) if "acid" in reagent.lower() or "hcl" in reagent.lower(): + debug_print(f"🍋 检测到酸性试剂") # 酸性试剂:pH越低需要的体积越大 - if target_ph_value < 3: # 改为 target_ph_value - return vessel_volume * 0.05 # 5% - elif target_ph_value < 5: # 改为 target_ph_value - return vessel_volume * 0.02 # 2% + if target_ph_value < 3: + volume = vessel_volume * 0.05 # 5% + debug_print(f" 💪 强酸性 (pH<3): 使用 5% 体积") + elif target_ph_value < 5: + volume = vessel_volume * 0.02 # 2% + debug_print(f" 🔸 中酸性 (pH<5): 使用 2% 体积") else: - return vessel_volume * 0.01 # 1% + volume = vessel_volume * 0.01 # 1% + debug_print(f" 🔹 弱酸性 (pH≥5): 使用 1% 体积") elif "hydroxide" in reagent.lower() or "naoh" in reagent.lower(): + debug_print(f"🧂 检测到碱性试剂") # 碱性试剂:pH越高需要的体积越大 - if target_ph_value > 11: # 改为 target_ph_value - return vessel_volume * 0.05 # 5% - elif target_ph_value > 9: # 改为 target_ph_value - return vessel_volume * 0.02 # 2% + if target_ph_value > 11: + volume = vessel_volume * 0.05 # 5% + debug_print(f" 💪 强碱性 (pH>11): 使用 5% 体积") + elif target_ph_value > 9: + volume = vessel_volume * 0.02 # 2% + debug_print(f" 🔸 中碱性 (pH>9): 使用 2% 体积") else: - return vessel_volume * 0.01 # 1% + volume = vessel_volume * 0.01 # 1% + debug_print(f" 🔹 弱碱性 (pH≤9): 使用 1% 体积") else: # 未知试剂,使用默认值 - return vessel_volume * 0.01 - + volume = vessel_volume * 0.01 + debug_print(f"❓ 未知试剂类型,使用默认 1% 体积") + + debug_print(f"📊 计算结果: {volume:.2f}mL") + return volume def generate_adjust_ph_protocol( G: nx.DiGraph, vessel: str, - ph_value: float, # 改为 ph_value + ph_value: float, reagent: str, **kwargs ) -> List[Dict[str, Any]]: @@ -168,13 +227,23 @@ def generate_adjust_ph_protocol( Args: G: 有向图,节点为容器和设备 vessel: 目标容器(需要调节pH的容器) - ph_value: 目标pH值(从XDL传入) # 改为 ph_value + ph_value: 目标pH值(从XDL传入) reagent: 酸碱试剂名称(从XDL传入) **kwargs: 其他可选参数,使用默认值 Returns: List[Dict[str, Any]]: 动作序列 """ + + debug_print("=" * 60) + debug_print("🧪 开始生成pH调节协议") + debug_print(f"📋 原始参数:") + debug_print(f" 🥼 vessel: '{vessel}'") + debug_print(f" 📊 ph_value: {ph_value}") + debug_print(f" 🧪 reagent: '{reagent}'") + debug_print(f" 📦 kwargs: {kwargs}") + debug_print("=" * 60) + action_sequence = [] # 从kwargs中获取可选参数,如果没有则使用默认值 @@ -184,48 +253,84 @@ def generate_adjust_ph_protocol( stir_time = kwargs.get('stir_time', 60.0) # 默认搅拌时间 settling_time = kwargs.get('settling_time', 30.0) # 默认平衡时间 - print(f"ADJUST_PH: 开始生成pH调节协议") - print(f" - 目标容器: {vessel}") - print(f" - 目标pH: {ph_value}") # 改为 ph_value - print(f" - 试剂: {reagent}") - print(f" - 使用默认参数: 体积=自动估算, 搅拌=True, 搅拌速度=300RPM") + debug_print(f"🔧 处理后的参数:") + debug_print(f" 📏 volume: {volume}mL (0.0表示自动估算)") + debug_print(f" 🌪️ stir: {stir}") + debug_print(f" 🔄 stir_speed: {stir_speed}rpm") + debug_print(f" ⏱️ stir_time: {stir_time}s") + debug_print(f" ⏳ settling_time: {settling_time}s") + + # 开始处理 + 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"使用试剂: {reagent}", "⚗️")) # 1. 验证目标容器存在 + debug_print(f"🔍 步骤1: 验证目标容器...") if vessel not in G.nodes(): + debug_print(f"❌ 目标容器 '{vessel}' 不存在于系统中") raise ValueError(f"目标容器 '{vessel}' 不存在于系统中") + debug_print(f"✅ 目标容器验证通过") + action_sequence.append(create_action_log("目标容器验证通过", "✅")) + # 2. 查找酸碱试剂容器 + debug_print(f"🔍 步骤2: 查找试剂容器...") + action_sequence.append(create_action_log("正在查找试剂容器...", "🔍")) + try: reagent_vessel = find_acid_base_vessel(G, reagent) - print(f"ADJUST_PH: 找到试剂容器: {reagent_vessel}") + debug_print(f"✅ 找到试剂容器: {reagent_vessel}") + action_sequence.append(create_action_log(f"找到试剂容器: {reagent_vessel}", "🧪")) except ValueError as e: + debug_print(f"❌ 无法找到试剂容器: {str(e)}") + action_sequence.append(create_action_log(f"试剂容器查找失败: {str(e)}", "❌")) raise ValueError(f"无法找到试剂 '{reagent}': {str(e)}") - # 3. 如果未指定体积,自动估算 + # 3. 体积估算 + debug_print(f"🔍 步骤3: 体积处理...") if volume <= 0: + action_sequence.append(create_action_log("开始自动估算试剂体积", "🧮")) + # 获取目标容器的体积信息 vessel_data = G.nodes[vessel].get('data', {}) vessel_volume = vessel_data.get('max_volume', 100.0) # 默认100mL + debug_print(f"📏 容器最大体积: {vessel_volume}mL") - estimated_volume = calculate_reagent_volume(ph_value, reagent, vessel_volume) # 改为 ph_value + estimated_volume = calculate_reagent_volume(ph_value, reagent, vessel_volume) volume = estimated_volume - print(f"ADJUST_PH: 自动估算试剂体积: {volume:.2f} mL") + debug_print(f"✅ 自动估算试剂体积: {volume:.2f} mL") + action_sequence.append(create_action_log(f"估算试剂体积: {volume:.2f}mL", "📊")) + else: + debug_print(f"📏 使用指定体积: {volume}mL") + action_sequence.append(create_action_log(f"使用指定体积: {volume}mL", "📏")) # 4. 验证路径存在 + debug_print(f"🔍 步骤4: 路径验证...") + action_sequence.append(create_action_log("验证转移路径...", "🛤️")) + try: path = nx.shortest_path(G, source=reagent_vessel, target=vessel) - print(f"ADJUST_PH: 找到路径: {' → '.join(path)}") + 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}' 没有可用路径") - # 5. 先启动搅拌(如果需要) + # 5. 搅拌器设置 + debug_print(f"🔍 步骤5: 搅拌器设置...") stirrer_id = None if stir: + action_sequence.append(create_action_log("准备启动搅拌器", "🌪️")) + try: stirrer_id = find_connected_stirrer(G, vessel) if stirrer_id: - print(f"ADJUST_PH: 找到搅拌器 {stirrer_id},启动搅拌") + debug_print(f"✅ 找到搅拌器 {stirrer_id},启动搅拌") + action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {stir_speed}rpm)", "🔄")) + action_sequence.append({ "device_id": stirrer_id, "action_name": "start_stir", @@ -237,23 +342,34 @@ def generate_adjust_ph_protocol( }) # 等待搅拌稳定 + action_sequence.append(create_action_log("等待搅拌稳定...", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 5} }) else: - print(f"ADJUST_PH: 警告 - 未找到搅拌器,继续执行") + debug_print(f"⚠️ 未找到搅拌器,继续执行") + action_sequence.append(create_action_log("未找到搅拌器,跳过搅拌", "⚠️")) except Exception as e: - print(f"ADJUST_PH: 搅拌器配置出错: {str(e)}") + debug_print(f"❌ 搅拌器配置出错: {str(e)}") + action_sequence.append(create_action_log(f"搅拌器配置失败: {str(e)}", "❌")) + else: + debug_print(f"📋 跳过搅拌设置") + action_sequence.append(create_action_log("跳过搅拌设置", "⏭️")) - # 6. 缓慢添加试剂 - 使用pump_protocol - print(f"ADJUST_PH: 开始添加试剂 {volume:.2f} mL") + # 6. 试剂添加 + debug_print(f"🔍 步骤6: 试剂添加...") + action_sequence.append(create_action_log(f"开始添加试剂 {volume:.2f}mL", "🚰")) # 计算添加时间(pH调节需要缓慢添加) addition_time = max(30.0, volume * 2.0) # 至少30秒,每mL需要2秒 + debug_print(f"⏱️ 计算添加时间: {addition_time}s (缓慢注入)") + action_sequence.append(create_action_log(f"设置添加时间: {addition_time:.0f}s (缓慢注入)", "⏱️")) try: + action_sequence.append(create_action_log("调用泵协议进行试剂转移", "🔄")) + pump_actions = generate_pump_protocol_with_rinsing( G=G, from_vessel=reagent_vessel, @@ -266,17 +382,24 @@ def generate_adjust_ph_protocol( rinsing_volume=0.0, rinsing_repeats=0, solid=False, - flowrate=0.5 # 缓慢注入 + flowrate=0.5, # 缓慢注入 + transfer_flowrate=0.3 ) action_sequence.extend(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)}") + action_sequence.append(create_action_log(f"泵协议生成失败: {str(e)}", "❌")) raise ValueError(f"生成泵协议时出错: {str(e)}") - # 7. 持续搅拌以混合和平衡 + # 7. 混合搅拌 if stir and stirrer_id: - print(f"ADJUST_PH: 持续搅拌 {stir_time} 秒以混合试剂") + debug_print(f"🔍 步骤7: 混合搅拌...") + action_sequence.append(create_action_log(f"开始混合搅拌 {stir_time:.0f}s", "🌀")) + action_sequence.append({ "device_id": stirrer_id, "action_name": "stir", @@ -284,25 +407,47 @@ def generate_adjust_ph_protocol( "stir_time": stir_time, "stir_speed": stir_speed, "settling_time": settling_time, - "purpose": f"pH调节: 混合试剂,目标pH={ph_value}" # 改为 ph_value + "purpose": f"pH调节: 混合试剂,目标pH={ph_value}" } }) + + debug_print(f"✅ 混合搅拌设置完成") + else: + debug_print(f"⏭️ 跳过混合搅拌") + action_sequence.append(create_action_log("跳过混合搅拌", "⏭️")) + + # 8. 等待平衡 + debug_print(f"🔍 步骤8: 反应平衡...") + action_sequence.append(create_action_log(f"等待pH平衡 {settling_time:.0f}s", "⚖️")) - # 8. 等待反应平衡 action_sequence.append({ "action_name": "wait", "action_kwargs": { "time": settling_time, - "description": f"等待pH平衡到目标值 {ph_value}" # 改为 ph_value + "description": f"等待pH平衡到目标值 {ph_value}" } }) - print(f"ADJUST_PH: 协议生成完成,共 {len(action_sequence)} 个动作") - print(f"ADJUST_PH: 预计总时间: {addition_time + stir_time + settling_time:.0f} 秒") + # 9. 完成总结 + total_time = addition_time + stir_time + settling_time + + debug_print("=" * 60) + debug_print(f"🎉 pH调节协议生成完成") + debug_print(f"📊 协议统计:") + debug_print(f" 📋 总动作数: {len(action_sequence)}") + debug_print(f" ⏱️ 预计总时间: {total_time:.0f}s ({total_time/60:.1f}分钟)") + debug_print(f" 🧪 试剂: {reagent}") + debug_print(f" 📏 体积: {volume:.2f}mL") + debug_print(f" 📊 目标pH: {ph_value}") + debug_print(f" 🥼 目标容器: {vessel}") + debug_print("=" * 60) + + # 添加完成日志 + summary_msg = f"pH调节协议完成: {vessel} → 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, @@ -317,7 +462,7 @@ def generate_adjust_ph_protocol_stepwise( Args: G: 网络图 vessel: 目标容器 - pH: 目标pH值 + ph_value: 目标pH值 reagent: 酸碱试剂 max_volume: 最大试剂体积 steps: 分步数量 @@ -325,15 +470,28 @@ def generate_adjust_ph_protocol_stepwise( Returns: List[Dict[str, Any]]: 动作序列 """ - action_sequence = [] + debug_print("=" * 60) + debug_print(f"🔄 开始分步pH调节") + debug_print(f"📋 分步参数:") + debug_print(f" 🥼 vessel: {vessel}") + debug_print(f" 📊 ph_value: {ph_value}") + debug_print(f" 🧪 reagent: {reagent}") + debug_print(f" 📏 max_volume: {max_volume}mL") + debug_print(f" 🔢 steps: {steps}") + debug_print("=" * 60) - print(f"ADJUST_PH: 开始分步pH调节({steps}步)") + action_sequence = [] # 每步添加的体积 step_volume = max_volume / steps + debug_print(f"📊 每步体积: {step_volume:.2f}mL") + + action_sequence.append(create_action_log(f"开始分步pH调节 ({steps}步)", "🔄")) + action_sequence.append(create_action_log(f"每步添加: {step_volume:.2f}mL", "📏")) for i in range(steps): - print(f"ADJUST_PH: 第 {i+1}/{steps} 步,添加 {step_volume} mL") + debug_print(f"🔄 执行第 {i+1}/{steps} 步,添加 {step_volume:.2f}mL") + action_sequence.append(create_action_log(f"第 {i+1}/{steps} 步开始", "🚀")) # 生成单步协议 step_actions = generate_adjust_ph_protocol( @@ -349,9 +507,13 @@ def generate_adjust_ph_protocol_stepwise( ) action_sequence.extend(step_actions) + debug_print(f"✅ 第 {i+1}/{steps} 步完成,添加了 {len(step_actions)} 个动作") + action_sequence.append(create_action_log(f"第 {i+1}/{steps} 步完成", "✅")) # 步骤间等待 if i < steps - 1: + debug_print(f"⏳ 步骤间等待30s") + action_sequence.append(create_action_log("步骤间等待...", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": { @@ -360,10 +522,11 @@ def generate_adjust_ph_protocol_stepwise( } }) - print(f"ADJUST_PH: 分步pH调节完成") + debug_print(f"🎉 分步pH调节完成,共 {len(action_sequence)} 个动作") + action_sequence.append(create_action_log("分步pH调节全部完成", "🎉")) + return action_sequence - # 便捷函数:常用pH调节 def generate_acidify_protocol( G: nx.DiGraph, @@ -372,11 +535,11 @@ def generate_acidify_protocol( acid: str = "hydrochloric acid" ) -> List[Dict[str, Any]]: """酸化协议""" + debug_print(f"🍋 生成酸化协议: {vessel} → pH {target_ph} (使用 {acid})") return generate_adjust_ph_protocol( - G, vessel, target_ph, acid, 0.0, True, 300.0, 120.0, 60.0 + G, vessel, target_ph, acid ) - def generate_basify_protocol( G: nx.DiGraph, vessel: str, @@ -384,28 +547,42 @@ def generate_basify_protocol( base: str = "sodium hydroxide" ) -> List[Dict[str, Any]]: """碱化协议""" + debug_print(f"🧂 生成碱化协议: {vessel} → pH {target_ph} (使用 {base})") return generate_adjust_ph_protocol( - G, vessel, target_ph, base, 0.0, True, 300.0, 120.0, 60.0 + G, vessel, target_ph, base ) - def generate_neutralize_protocol( G: nx.DiGraph, vessel: str, reagent: str = "sodium hydroxide" ) -> List[Dict[str, Any]]: """中和协议(pH=7)""" + debug_print(f"⚖️ 生成中和协议: {vessel} → pH 7.0 (使用 {reagent})") return generate_adjust_ph_protocol( - G, vessel, 7.0, reagent, 0.0, True, 350.0, 180.0, 90.0 + G, vessel, 7.0, reagent ) - # 测试函数 def test_adjust_ph_protocol(): """测试pH调节协议""" - print("=== ADJUST PH PROTOCOL 测试 ===") - print("测试完成") - + debug_print("=== ADJUST PH PROTOCOL 增强版测试 ===") + + # 测试体积计算 + debug_print("🧮 测试体积计算...") + test_cases = [ + (2.0, "hydrochloric acid", 100.0), + (4.0, "hydrochloric acid", 100.0), + (12.0, "sodium hydroxide", 100.0), + (10.0, "sodium hydroxide", 100.0), + (7.0, "unknown reagent", 100.0) + ] + + for ph, reagent, volume in test_cases: + result = calculate_reagent_volume(ph, reagent, volume) + debug_print(f"📊 {reagent} → pH {ph}: {result:.2f}mL") + + debug_print("✅ 测试完成") if __name__ == "__main__": test_adjust_ph_protocol() \ No newline at end of file diff --git a/unilabos/compile/dissolve_protocol.py b/unilabos/compile/dissolve_protocol.py index 9008e6d..065196e 100644 --- a/unilabos/compile/dissolve_protocol.py +++ b/unilabos/compile/dissolve_protocol.py @@ -11,6 +11,22 @@ def debug_print(message): print(f"[DISSOLVE] {message}", flush=True) logger.info(f"[DISSOLVE] {message}") +# 🆕 创建进度日志动作 +def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]: + """创建一个动作日志""" + full_message = f"{emoji} {message}" + debug_print(full_message) + logger.info(full_message) + print(f"[ACTION] {full_message}", flush=True) + + return { + "action_name": "wait", + "action_kwargs": { + "time": 0.1, + "log_message": full_message + } + } + def parse_volume_input(volume_input: Union[str, float]) -> float: """ 解析体积输入,支持带单位的字符串 @@ -22,18 +38,20 @@ def parse_volume_input(volume_input: Union[str, float]) -> float: float: 体积(毫升) """ if isinstance(volume_input, (int, float)): + debug_print(f"📏 体积输入为数值: {volume_input}") return float(volume_input) if not volume_input or not str(volume_input).strip(): + debug_print(f"⚠️ 体积输入为空,返回0.0mL") return 0.0 volume_str = str(volume_input).lower().strip() - debug_print(f"解析体积输入: '{volume_str}'") + debug_print(f"🔍 解析体积输入: '{volume_str}'") # 处理未知体积 if volume_str in ['?', 'unknown', 'tbd', 'to be determined']: default_volume = 50.0 # 默认50mL - debug_print(f"检测到未知体积,使用默认值: {default_volume}mL") + debug_print(f"❓ 检测到未知体积,使用默认值: {default_volume}mL 🎯") return default_volume # 移除空格并提取数字和单位 @@ -43,7 +61,7 @@ def parse_volume_input(volume_input: Union[str, float]) -> float: 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}',使用默认值50mL") + debug_print(f"❌ 无法解析体积: '{volume_str}',使用默认值50mL") return 50.0 value = float(match.group(1)) @@ -52,12 +70,14 @@ def parse_volume_input(volume_input: Union[str, float]) -> float: # 转换为毫升 if unit in ['l', 'liter']: volume = value * 1000.0 # L -> mL + debug_print(f"🔄 体积转换: {value}L → {volume}mL") elif unit in ['μl', 'ul', 'microliter']: volume = value / 1000.0 # μL -> mL + debug_print(f"🔄 体积转换: {value}μL → {volume}mL") else: # ml, milliliter 或默认 volume = value # 已经是mL + debug_print(f"✅ 体积已为mL: {volume}mL") - debug_print(f"体积转换: {value}{unit} → {volume}mL") return volume def parse_mass_input(mass_input: Union[str, float]) -> float: @@ -71,18 +91,20 @@ def parse_mass_input(mass_input: Union[str, float]) -> float: float: 质量(克) """ if isinstance(mass_input, (int, float)): + debug_print(f"⚖️ 质量输入为数值: {mass_input}g") return float(mass_input) if not mass_input or not str(mass_input).strip(): + debug_print(f"⚠️ 质量输入为空,返回0.0g") return 0.0 mass_str = str(mass_input).lower().strip() - debug_print(f"解析质量输入: '{mass_str}'") + debug_print(f"🔍 解析质量输入: '{mass_str}'") # 处理未知质量 if mass_str in ['?', 'unknown', 'tbd', 'to be determined']: default_mass = 1.0 # 默认1g - debug_print(f"检测到未知质量,使用默认值: {default_mass}g") + debug_print(f"❓ 检测到未知质量,使用默认值: {default_mass}g 🎯") return default_mass # 移除空格并提取数字和单位 @@ -92,7 +114,7 @@ def parse_mass_input(mass_input: Union[str, float]) -> float: 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") + debug_print(f"❌ 无法解析质量: '{mass_str}',返回0.0g") return 0.0 value = float(match.group(1)) @@ -101,12 +123,14 @@ def parse_mass_input(mass_input: Union[str, float]) -> float: # 转换为克 if unit in ['mg', 'milligram']: mass = value / 1000.0 # mg -> g + debug_print(f"🔄 质量转换: {value}mg → {mass}g") elif unit in ['kg', 'kilogram']: mass = value * 1000.0 # kg -> g + debug_print(f"🔄 质量转换: {value}kg → {mass}g") else: # g, gram 或默认 mass = value # 已经是g + debug_print(f"✅ 质量已为g: {mass}g") - debug_print(f"质量转换: {value}{unit} → {mass}g") return mass def parse_time_input(time_input: Union[str, float]) -> float: @@ -120,18 +144,20 @@ def parse_time_input(time_input: Union[str, float]) -> float: float: 时间(秒) """ if isinstance(time_input, (int, float)): + debug_print(f"⏱️ 时间输入为数值: {time_input}秒") return float(time_input) if not time_input or not str(time_input).strip(): + debug_print(f"⚠️ 时间输入为空,返回0秒") return 0.0 time_str = str(time_input).lower().strip() - debug_print(f"解析时间输入: '{time_str}'") + debug_print(f"🔍 解析时间输入: '{time_str}'") # 处理未知时间 if time_str in ['?', 'unknown', 'tbd']: default_time = 600.0 # 默认10分钟 - debug_print(f"检测到未知时间,使用默认值: {default_time}s") + debug_print(f"❓ 检测到未知时间,使用默认值: {default_time}s (10分钟) ⏰") return default_time # 移除空格并提取数字和单位 @@ -141,7 +167,7 @@ def parse_time_input(time_input: Union[str, float]) -> float: 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") + debug_print(f"❌ 无法解析时间: '{time_str}',返回0s") return 0.0 value = float(match.group(1)) @@ -150,14 +176,17 @@ def parse_time_input(time_input: Union[str, float]) -> float: # 转换为秒 if unit in ['min', 'minute']: time_sec = value * 60.0 # min -> s + debug_print(f"🔄 时间转换: {value}分钟 → {time_sec}秒") elif unit in ['h', 'hr', 'hour']: time_sec = value * 3600.0 # h -> s + debug_print(f"🔄 时间转换: {value}小时 → {time_sec}秒") elif unit in ['d', 'day']: time_sec = value * 86400.0 # d -> s + debug_print(f"🔄 时间转换: {value}天 → {time_sec}秒") else: # s, sec, second 或默认 time_sec = value # 已经是s + debug_print(f"✅ 时间已为秒: {time_sec}秒") - debug_print(f"时间转换: {value}{unit} → {time_sec}s") return time_sec def parse_temperature_input(temp_input: Union[str, float]) -> float: @@ -171,13 +200,15 @@ def parse_temperature_input(temp_input: Union[str, float]) -> float: float: 温度(摄氏度) """ if isinstance(temp_input, (int, float)): + debug_print(f"🌡️ 温度输入为数值: {temp_input}°C") return float(temp_input) if not temp_input or not str(temp_input).strip(): + debug_print(f"⚠️ 温度输入为空,使用默认室温25°C") return 25.0 # 默认室温 temp_str = str(temp_input).lower().strip() - debug_print(f"解析温度输入: '{temp_str}'") + debug_print(f"🔍 解析温度输入: '{temp_str}'") # 处理特殊温度描述 temp_aliases = { @@ -193,7 +224,7 @@ def parse_temperature_input(temp_input: Union[str, float]) -> float: if temp_str in temp_aliases: result = temp_aliases[temp_str] - debug_print(f"温度别名解析: '{temp_str}' → {result}°C") + debug_print(f"🏷️ 温度别名解析: '{temp_str}' → {result}°C") return result # 移除空格并提取数字和单位 @@ -203,7 +234,7 @@ def parse_temperature_input(temp_input: Union[str, float]) -> float: match = re.match(r'([0-9]*\.?[0-9]+)\s*(°c|c|celsius|°f|f|fahrenheit|k|kelvin)?', temp_clean) if not match: - debug_print(f"⚠️ 无法解析温度: '{temp_str}',使用默认值25°C") + debug_print(f"❌ 无法解析温度: '{temp_str}',使用默认值25°C") return 25.0 value = float(match.group(1)) @@ -212,19 +243,22 @@ def parse_temperature_input(temp_input: Union[str, float]) -> float: # 转换为摄氏度 if unit in ['°f', 'f', 'fahrenheit']: temp_c = (value - 32) * 5/9 # F -> C + debug_print(f"🔄 温度转换: {value}°F → {temp_c:.1f}°C") elif unit in ['k', 'kelvin']: temp_c = value - 273.15 # K -> C + debug_print(f"🔄 温度转换: {value}K → {temp_c:.1f}°C") else: # °c, c, celsius 或默认 temp_c = value # 已经是C + debug_print(f"✅ 温度已为°C: {temp_c}°C") - debug_print(f"温度转换: {value}{unit} → {temp_c}°C") return temp_c def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: - """增强版溶剂容器查找""" - debug_print(f"查找溶剂 '{solvent}' 的容器...") + """增强版溶剂容器查找,支持多种匹配模式""" + debug_print(f"🔍 开始查找溶剂 '{solvent}' 的容器...") # 🔧 方法1:直接搜索 data.reagent_name 和 config.reagent + debug_print(f"📋 方法1: 搜索reagent字段...") for node in G.nodes(): node_data = G.nodes[node].get('data', {}) node_type = G.nodes[node].get('type', '') @@ -237,18 +271,20 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: # 精确匹配 if reagent_name == solvent.lower() or config_reagent == solvent.lower(): - debug_print(f"✅ 通过reagent字段找到容器: {node}") + 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}") + debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node} 🔍") return node # 🔧 方法2:常见的容器命名规则 + debug_print(f"📋 方法2: 使用命名规则查找...") solvent_clean = solvent.lower().replace(' ', '_').replace('-', '_') possible_names = [ + solvent_clean, f"flask_{solvent_clean}", f"bottle_{solvent_clean}", f"vessel_{solvent_clean}", @@ -256,77 +292,118 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: f"{solvent_clean}_bottle", f"solvent_{solvent_clean}", f"reagent_{solvent_clean}", - f"reagent_bottle_{solvent_clean}" + f"reagent_bottle_{solvent_clean}", + f"reagent_bottle_1", # 通用试剂瓶 + f"reagent_bottle_2", + f"reagent_bottle_3" ] + debug_print(f"🔍 尝试的容器名称: {possible_names[:5]}... (共{len(possible_names)}个)") + 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}") + debug_print(f"✅ 通过命名规则找到容器: {name} 📝") return name - # 🔧 方法3:使用第一个试剂瓶作为备选 + # 🔧 方法3:节点名称模糊匹配 + debug_print(f"📋 方法3: 节点名称模糊匹配...") + for node_id in G.nodes(): + node_data = G.nodes[node_id] + if node_data.get('type') == 'container': + # 检查节点名称是否包含溶剂名称 + if solvent_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() == solvent.lower(): + debug_print(f"✅ 通过液体类型匹配到容器: {node_id} 💧") + return node_id + + # 🔧 方法4:使用第一个试剂瓶作为备选 + debug_print(f"📋 方法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() or 'flask' in node_id.lower())): - debug_print(f"⚠️ 未找到专用容器,使用备选容器: {node_id}") + debug_print(f"⚠️ 未找到专用容器,使用备选试剂瓶: {node_id} 🔄") return node_id + debug_print(f"❌ 所有方法都失败了,无法找到容器!") raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器") def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str: """查找连接到指定容器的加热搅拌器""" + debug_print(f"🔍 查找连接到容器 '{vessel}' 的加热搅拌器...") + heatchill_nodes = [] for node in G.nodes(): node_class = G.nodes[node].get('class', '').lower() if 'heatchill' in node_class: heatchill_nodes.append(node) + debug_print(f"📋 发现加热搅拌器: {node}") + + debug_print(f"📊 共找到 {len(heatchill_nodes)} 个加热搅拌器") # 查找连接到容器的加热器 for heatchill in heatchill_nodes: if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill): - debug_print(f"找到连接的加热器: {heatchill}") + debug_print(f"✅ 找到连接的加热搅拌器: {heatchill} 🔗") return heatchill # 返回第一个加热器 if heatchill_nodes: - debug_print(f"使用第一个加热器: {heatchill_nodes[0]}") + debug_print(f"⚠️ 未找到直接连接的加热搅拌器,使用第一个: {heatchill_nodes[0]} 🔄") return heatchill_nodes[0] + debug_print(f"❌ 未找到任何加热搅拌器") return "" def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str: """查找连接到指定容器的搅拌器""" + debug_print(f"🔍 查找连接到容器 '{vessel}' 的搅拌器...") + stirrer_nodes = [] for node in G.nodes(): node_class = G.nodes[node].get('class', '').lower() if 'stirrer' in node_class: stirrer_nodes.append(node) + debug_print(f"📋 发现搅拌器: {node}") + + debug_print(f"📊 共找到 {len(stirrer_nodes)} 个搅拌器") # 查找连接到容器的搅拌器 for stirrer in stirrer_nodes: if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): - debug_print(f"找到连接的搅拌器: {stirrer}") + debug_print(f"✅ 找到连接的搅拌器: {stirrer} 🔗") return stirrer # 返回第一个搅拌器 if stirrer_nodes: - debug_print(f"使用第一个搅拌器: {stirrer_nodes[0]}") + debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个: {stirrer_nodes[0]} 🔄") return stirrer_nodes[0] + debug_print(f"❌ 未找到任何搅拌器") return "" def find_solid_dispenser(G: nx.DiGraph) -> str: """查找固体加样器""" + debug_print(f"🔍 查找固体加样器...") + 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}") + debug_print(f"✅ 找到固体加样器: {node} 🥄") return node - debug_print("⚠️ 未找到固体加样器") + debug_print(f"❌ 未找到固体加样器") return "" def generate_dissolve_protocol( @@ -347,12 +424,13 @@ def generate_dissolve_protocol( **kwargs # 🔧 关键:接受所有其他参数,防止unexpected keyword错误 ) -> List[Dict[str, Any]]: """ - 生成溶解操作的协议序列 - 修复版 + 生成溶解操作的协议序列 - 增强版 🔧 修复要点: 1. 添加action文件中的所有参数(mass, mol, reagent, event) 2. 使用 **kwargs 接受所有额外参数,防止 unexpected keyword argument 错误 3. 支持固体溶解和液体溶解两种模式 + 4. 添加详细的emoji日志系统 支持两种溶解模式: 1. 液体溶解:指定 solvent + volume,使用pump protocol转移溶剂 @@ -367,35 +445,40 @@ def generate_dissolve_protocol( """ debug_print("=" * 60) - debug_print("开始生成溶解协议 - 修复版") - debug_print(f"原始参数:") - debug_print(f" - vessel: '{vessel}'") - debug_print(f" - solvent: '{solvent}'") - debug_print(f" - volume: {volume} (类型: {type(volume)})") - debug_print(f" - mass: {mass} (类型: {type(mass)})") - debug_print(f" - temp: {temp} (类型: {type(temp)})") - debug_print(f" - time: {time} (类型: {type(time)})") - debug_print(f" - reagent: '{reagent}'") - debug_print(f" - mol: '{mol}'") - debug_print(f" - event: '{event}'") - debug_print(f" - kwargs: {kwargs}") # 显示额外参数 + debug_print("🧪 开始生成溶解协议") + debug_print(f"📋 原始参数:") + debug_print(f" 🥼 vessel: '{vessel}'") + debug_print(f" 💧 solvent: '{solvent}'") + debug_print(f" 📏 volume: {volume} (类型: {type(volume)})") + debug_print(f" ⚖️ mass: {mass} (类型: {type(mass)})") + debug_print(f" 🌡️ temp: {temp} (类型: {type(temp)})") + debug_print(f" ⏱️ time: {time} (类型: {type(time)})") + debug_print(f" 🧪 reagent: '{reagent}'") + debug_print(f" 🧬 mol: '{mol}'") + debug_print(f" 🎯 event: '{event}'") + debug_print(f" 📦 kwargs: {kwargs}") # 显示额外参数 debug_print("=" * 60) action_sequence = [] # === 参数验证 === - debug_print("步骤1: 参数验证...") + debug_print("🔍 步骤1: 参数验证...") + action_sequence.append(create_action_log(f"开始溶解操作 - 容器: {vessel}", "🎬")) if not vessel: + debug_print("❌ vessel 参数不能为空") raise ValueError("vessel 参数不能为空") if vessel not in G.nodes(): + debug_print(f"❌ 容器 '{vessel}' 不存在于系统中") raise ValueError(f"容器 '{vessel}' 不存在于系统中") debug_print("✅ 基本参数验证通过") + action_sequence.append(create_action_log("参数验证通过", "✅")) # === 🔧 关键修复:参数解析 === - debug_print("步骤2: 参数解析...") + debug_print("🔍 步骤2: 参数解析...") + action_sequence.append(create_action_log("正在解析溶解参数...", "🔍")) # 解析各种参数为数值 final_volume = parse_volume_input(volume) @@ -403,17 +486,18 @@ def generate_dissolve_protocol( final_temp = parse_temperature_input(temp) 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_temp}°C") - debug_print(f" - 时间: {final_time}s") - debug_print(f" - 试剂: '{reagent}'") - debug_print(f" - 摩尔: '{mol}'") - debug_print(f" - 事件: '{event}'") + debug_print(f"📊 解析结果:") + debug_print(f" 📏 体积: {final_volume}mL") + debug_print(f" ⚖️ 质量: {final_mass}g") + debug_print(f" 🌡️ 温度: {final_temp}°C") + debug_print(f" ⏱️ 时间: {final_time}s") + debug_print(f" 🧪 试剂: '{reagent}'") + debug_print(f" 🧬 摩尔: '{mol}'") + debug_print(f" 🎯 事件: '{event}'") # === 判断溶解类型 === - debug_print("步骤3: 判断溶解类型...") + debug_print("🔍 步骤3: 判断溶解类型...") + action_sequence.append(create_action_log("正在判断溶解类型...", "🔍")) # 判断是固体溶解还是液体溶解 is_solid_dissolve = (final_mass > 0 or (mol and mol.strip() != "") or (reagent and reagent.strip() != "")) @@ -427,10 +511,15 @@ def generate_dissolve_protocol( solvent = "water" # 默认溶剂 debug_print("⚠️ 未明确指定溶解参数,默认为50mL水溶解") - debug_print(f"溶解类型: {'固体溶解' if is_solid_dissolve else '液体溶解'}") + dissolve_type = "固体溶解" if is_solid_dissolve else "液体溶解" + dissolve_emoji = "🧂" if is_solid_dissolve else "💧" + debug_print(f"📋 溶解类型: {dissolve_type} {dissolve_emoji}") + + action_sequence.append(create_action_log(f"确定溶解类型: {dissolve_type} {dissolve_emoji}", "📋")) # === 查找设备 === - debug_print("步骤4: 查找设备...") + debug_print("🔍 步骤4: 查找设备...") + action_sequence.append(create_action_log("正在查找相关设备...", "🔍")) # 查找加热搅拌器 heatchill_id = find_connected_heatchill(G, vessel) @@ -439,21 +528,31 @@ def generate_dissolve_protocol( # 优先使用加热搅拌器,否则使用独立搅拌器 stir_device_id = heatchill_id or stirrer_id - debug_print(f"设备映射:") - debug_print(f" - 加热器: '{heatchill_id}'") - debug_print(f" - 搅拌器: '{stirrer_id}'") - debug_print(f" - 使用设备: '{stir_device_id}'") + debug_print(f"📊 设备映射:") + debug_print(f" 🔥 加热器: '{heatchill_id}'") + debug_print(f" 🌪️ 搅拌器: '{stirrer_id}'") + debug_print(f" 🎯 使用设备: '{stir_device_id}'") + + if heatchill_id: + action_sequence.append(create_action_log(f"找到加热搅拌器: {heatchill_id}", "🔥")) + elif stirrer_id: + action_sequence.append(create_action_log(f"找到搅拌器: {stirrer_id}", "🌪️")) + else: + action_sequence.append(create_action_log("未找到搅拌设备,将跳过搅拌", "⚠️")) # === 执行溶解流程 === - debug_print("步骤5: 执行溶解流程...") + debug_print("🔍 步骤5: 执行溶解流程...") try: # 步骤5.1: 启动加热搅拌(如果需要) if stir_device_id and (final_temp > 25.0 or final_time > 0 or stir_speed > 0): - debug_print(f"5.1: 启动加热搅拌,温度: {final_temp}°C") + debug_print(f"🔍 5.1: 启动加热搅拌,温度: {final_temp}°C") + action_sequence.append(create_action_log(f"准备加热搅拌 (目标温度: {final_temp}°C)", "🔥")) if heatchill_id and (final_temp > 25.0 or final_time > 0): # 使用加热搅拌器 + action_sequence.append(create_action_log(f"启动加热搅拌器 {heatchill_id}", "🔥")) + heatchill_action = { "device_id": heatchill_id, "action_name": "heat_chill_start", @@ -468,6 +567,7 @@ def generate_dissolve_protocol( # 等待温度稳定 if final_temp > 25.0: wait_time = min(60, abs(final_temp - 25.0) * 1.5) + action_sequence.append(create_action_log(f"等待温度稳定 ({wait_time:.0f}秒)", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": wait_time} @@ -475,6 +575,8 @@ def generate_dissolve_protocol( elif stirrer_id: # 使用独立搅拌器 + action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {stir_speed}rpm)", "🌪️")) + stir_action = { "device_id": stirrer_id, "action_name": "start_stir", @@ -487,6 +589,7 @@ def generate_dissolve_protocol( action_sequence.append(stir_action) # 等待搅拌稳定 + action_sequence.append(create_action_log("等待搅拌稳定...", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 5} @@ -494,10 +597,13 @@ def generate_dissolve_protocol( if is_solid_dissolve: # === 固体溶解路径 === - debug_print(f"5.2: 使用固体溶解路径") + debug_print(f"🔍 5.2: 使用固体溶解路径") + action_sequence.append(create_action_log("开始固体溶解流程", "🧂")) solid_dispenser = find_solid_dispenser(G) if solid_dispenser: + action_sequence.append(create_action_log(f"找到固体加样器: {solid_dispenser}", "🥄")) + # 固体加样 add_kwargs = { "vessel": vessel, @@ -508,9 +614,12 @@ def generate_dissolve_protocol( if final_mass > 0: add_kwargs["mass"] = str(final_mass) + action_sequence.append(create_action_log(f"准备添加固体: {final_mass}g", "⚖️")) if mol and mol.strip(): add_kwargs["mol"] = mol + action_sequence.append(create_action_log(f"按摩尔数添加: {mol}", "🧬")) + action_sequence.append(create_action_log("开始固体加样操作", "🥄")) action_sequence.append({ "device_id": solid_dispenser, "action_name": "add_solid", @@ -518,18 +627,24 @@ def generate_dissolve_protocol( }) debug_print(f"✅ 固体加样完成") + action_sequence.append(create_action_log("固体加样完成", "✅")) else: debug_print("⚠️ 未找到固体加样器,跳过固体添加") + action_sequence.append(create_action_log("未找到固体加样器,无法添加固体", "❌")) elif is_liquid_dissolve: # === 液体溶解路径 === - debug_print(f"5.3: 使用液体溶解路径") + debug_print(f"🔍 5.3: 使用液体溶解路径") + action_sequence.append(create_action_log("开始液体溶解流程", "💧")) # 查找溶剂容器 + action_sequence.append(create_action_log("正在查找溶剂容器...", "🔍")) try: solvent_vessel = find_solvent_vessel(G, solvent) + action_sequence.append(create_action_log(f"找到溶剂容器: {solvent_vessel}", "🧪")) except ValueError as e: debug_print(f"⚠️ {str(e)},跳过溶剂添加") + action_sequence.append(create_action_log(f"溶剂容器查找失败: {str(e)}", "❌")) solvent_vessel = None if solvent_vessel: @@ -537,6 +652,9 @@ def generate_dissolve_protocol( flowrate = 1.0 # 较慢的注入速度 transfer_flowrate = 0.5 # 较慢的转移速度 + action_sequence.append(create_action_log(f"设置流速: {flowrate}mL/min (缓慢注入)", "⚡")) + action_sequence.append(create_action_log(f"开始转移 {final_volume}mL {solvent}", "🚰")) + # 调用pump protocol pump_actions = generate_pump_protocol_with_rinsing( G=G, @@ -559,8 +677,10 @@ def generate_dissolve_protocol( ) action_sequence.extend(pump_actions) debug_print(f"✅ 溶剂转移完成,添加了 {len(pump_actions)} 个动作") + action_sequence.append(create_action_log(f"溶剂转移完成 ({len(pump_actions)} 个操作)", "✅")) # 溶剂添加后等待 + action_sequence.append(create_action_log("溶剂添加后短暂等待...", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 5} @@ -568,10 +688,14 @@ def generate_dissolve_protocol( # 步骤5.4: 等待溶解完成 if final_time > 0: - debug_print(f"5.4: 等待溶解完成 - {final_time}s") + debug_print(f"🔍 5.4: 等待溶解完成 - {final_time}s") + wait_minutes = final_time / 60 + action_sequence.append(create_action_log(f"开始溶解等待 ({wait_minutes:.1f}分钟)", "⏰")) if heatchill_id: # 使用定时加热搅拌 + action_sequence.append(create_action_log(f"使用加热搅拌器进行定时溶解", "🔥")) + dissolve_action = { "device_id": heatchill_id, "action_name": "heat_chill", @@ -588,6 +712,8 @@ def generate_dissolve_protocol( elif stirrer_id: # 使用定时搅拌 + action_sequence.append(create_action_log(f"使用搅拌器进行定时溶解", "🌪️")) + stir_action = { "device_id": stirrer_id, "action_name": "stir", @@ -603,6 +729,7 @@ def generate_dissolve_protocol( else: # 简单等待 + action_sequence.append(create_action_log(f"简单等待溶解完成", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": final_time} @@ -610,7 +737,8 @@ def generate_dissolve_protocol( # 步骤5.5: 停止加热搅拌(如果需要) if heatchill_id and final_time == 0 and final_temp > 25.0: - debug_print(f"5.5: 停止加热器") + debug_print(f"🔍 5.5: 停止加热器") + action_sequence.append(create_action_log("停止加热搅拌器", "🛑")) stop_action = { "device_id": heatchill_id, @@ -622,7 +750,8 @@ def generate_dissolve_protocol( action_sequence.append(stop_action) except Exception as e: - debug_print(f"⚠️ 溶解流程执行失败: {str(e)}") + debug_print(f"❌ 溶解流程执行失败: {str(e)}") + action_sequence.append(create_action_log(f"溶解流程失败: {str(e)}", "❌")) # 添加错误日志 action_sequence.append({ "device_id": "system", @@ -634,21 +763,30 @@ def generate_dissolve_protocol( # === 最终结果 === debug_print("=" * 60) - debug_print(f"✅ 溶解协议生成完成") - debug_print(f"📊 总动作数: {len(action_sequence)}") - debug_print(f"📋 处理总结:") - debug_print(f" - 容器: {vessel}") - debug_print(f" - 溶解类型: {'固体溶解' if is_solid_dissolve else '液体溶解'}") + debug_print(f"🎉 溶解协议生成完成") + debug_print(f"📊 协议统计:") + debug_print(f" 📋 总动作数: {len(action_sequence)}") + debug_print(f" 🥼 容器: {vessel}") + debug_print(f" {dissolve_emoji} 溶解类型: {dissolve_type}") if is_liquid_dissolve: - debug_print(f" - 溶剂: {solvent} ({final_volume}mL)") + debug_print(f" 💧 溶剂: {solvent} ({final_volume}mL)") if is_solid_dissolve: - debug_print(f" - 试剂: {reagent}") - debug_print(f" - 质量: {final_mass}g") - debug_print(f" - 摩尔: {mol}") - debug_print(f" - 温度: {final_temp}°C") - debug_print(f" - 时间: {final_time}s") + debug_print(f" 🧪 试剂: {reagent}") + debug_print(f" ⚖️ 质量: {final_mass}g") + debug_print(f" 🧬 摩尔: {mol}") + debug_print(f" 🌡️ 温度: {final_temp}°C") + debug_print(f" ⏱️ 时间: {final_time}s") debug_print("=" * 60) + # 添加完成日志 + summary_msg = f"溶解协议完成: {vessel}" + if is_liquid_dissolve: + summary_msg += f" (使用 {final_volume}mL {solvent})" + if is_solid_dissolve: + summary_msg += f" (溶解 {final_mass}g {reagent})" + + action_sequence.append(create_action_log(summary_msg, "🎉")) + return action_sequence # === 便捷函数 === @@ -656,6 +794,7 @@ def generate_dissolve_protocol( def dissolve_solid_by_mass(G: nx.DiGraph, vessel: str, 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}") return generate_dissolve_protocol( G, vessel, mass=mass, @@ -667,6 +806,7 @@ def dissolve_solid_by_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union def dissolve_solid_by_moles(G: nx.DiGraph, vessel: str, 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}") return generate_dissolve_protocol( G, vessel, mol=mol, @@ -678,6 +818,7 @@ def dissolve_solid_by_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str, def dissolve_with_solvent(G: nx.DiGraph, vessel: str, 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}") return generate_dissolve_protocol( G, vessel, solvent=solvent, @@ -688,6 +829,7 @@ def dissolve_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Unio def dissolve_at_room_temp(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float]) -> List[Dict[str, Any]]: """室温溶解""" + debug_print(f"🌡️ 室温溶解: {solvent} ({volume}) → {vessel}") return generate_dissolve_protocol( G, vessel, solvent=solvent, @@ -699,6 +841,7 @@ def dissolve_at_room_temp(G: nx.DiGraph, vessel: str, solvent: str, volume: Unio def dissolve_with_heating(G: nx.DiGraph, vessel: str, 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}") return generate_dissolve_protocol( G, vessel, solvent=solvent, @@ -710,33 +853,37 @@ def dissolve_with_heating(G: nx.DiGraph, vessel: str, solvent: str, volume: Unio # 测试函数 def test_dissolve_protocol(): """测试溶解协议的各种参数解析""" - print("=== DISSOLVE PROTOCOL 修复版测试 ===") + debug_print("=== DISSOLVE PROTOCOL 增强版测试 ===") # 测试体积解析 + debug_print("💧 测试体积解析...") volumes = ["10 mL", "?", 10.0, "1 L", "500 μL"] for vol in volumes: result = parse_volume_input(vol) - print(f"体积解析: {vol} → {result}mL") + debug_print(f"📏 体积解析: {vol} → {result}mL") # 测试质量解析 + debug_print("⚖️ 测试质量解析...") masses = ["2.9 g", "?", 2.5, "500 mg"] for mass in masses: result = parse_mass_input(mass) - print(f"质量解析: {mass} → {result}g") + debug_print(f"⚖️ 质量解析: {mass} → {result}g") # 测试温度解析 + debug_print("🌡️ 测试温度解析...") temps = ["60 °C", "room temperature", "?", 25.0, "reflux"] for temp in temps: result = parse_temperature_input(temp) - print(f"温度解析: {temp} → {result}°C") + debug_print(f"🌡️ 温度解析: {temp} → {result}°C") # 测试时间解析 + debug_print("⏱️ 测试时间解析...") times = ["30 min", "1 h", "?", 60.0] for time in times: result = parse_time_input(time) - print(f"时间解析: {time} → {result}s") + debug_print(f"⏱️ 时间解析: {time} → {result}s") - print("✅ 测试完成") + debug_print("✅ 测试完成") if __name__ == "__main__": test_dissolve_protocol() \ No newline at end of file diff --git a/unilabos/compile/evacuateandrefill_protocol.py b/unilabos/compile/evacuateandrefill_protocol.py index c50b884..cbcf19b 100644 --- a/unilabos/compile/evacuateandrefill_protocol.py +++ b/unilabos/compile/evacuateandrefill_protocol.py @@ -1,16 +1,68 @@ import networkx as nx import logging -import uuid # 🔧 移到顶部 +import uuid +import sys from typing import List, Dict, Any, Optional from .pump_protocol import generate_pump_protocol_with_rinsing, generate_pump_protocol # 设置日志 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"[EVACUATE_REFILL] {message}", flush=True) - logger.info(f"[EVACUATE_REFILL] {message}") + """调试输出函数 - 支持中文""" + 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_gas_source(G: nx.DiGraph, gas: str) -> str: """ @@ -19,9 +71,10 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str: 2. 气体类型匹配(data.gas_type) 3. 默认气源 """ - debug_print(f"正在查找气体 '{gas}' 的气源...") + debug_print(f"🔍 正在查找气体 '{gas}' 的气源...") # 第一步:通过容器名称匹配 + debug_print(f"📋 方法1: 容器名称匹配...") gas_source_patterns = [ f"gas_source_{gas}", f"gas_{gas}", @@ -32,12 +85,15 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str: f"bottle_{gas}" ] + debug_print(f"🎯 尝试的容器名称: {gas_source_patterns}") + for pattern in gas_source_patterns: if pattern in G.nodes(): - debug_print(f"通过名称匹配找到气源: {pattern}") + debug_print(f"✅ 通过名称找到气源: {pattern}") return pattern # 第二步:通过气体类型匹配 (data.gas_type) + debug_print(f"📋 方法2: 气体类型匹配...") for node_id in G.nodes(): node_data = G.nodes[node_id] node_class = node_data.get('class', '') or '' @@ -52,7 +108,7 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str: gas_type = data.get('gas_type', '') if gas_type.lower() == gas.lower(): - debug_print(f"通过气体类型匹配找到气源: {node_id} (gas_type: {gas_type})") + debug_print(f"✅ 通过气体类型找到气源: {node_id} (气体类型: {gas_type})") return node_id # 检查 config.gas_type @@ -60,10 +116,11 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str: config_gas_type = config.get('gas_type', '') if config_gas_type.lower() == gas.lower(): - debug_print(f"通过配置气体类型匹配找到气源: {node_id} (config.gas_type: {config_gas_type})") + debug_print(f"✅ 通过配置气体类型找到气源: {node_id} (配置气体类型: {config_gas_type})") return node_id # 第三步:查找所有可用的气源设备 + debug_print(f"📋 方法3: 查找可用气源...") available_gas_sources = [] for node_id in G.nodes(): node_data = G.nodes[node_id] @@ -74,12 +131,13 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str: (node_id.startswith('flask_') and any(g in node_id.lower() for g in ['air', 'nitrogen', 'argon']))): data = node_data.get('data', {}) - gas_type = data.get('gas_type', 'unknown') - available_gas_sources.append(f"{node_id} (gas_type: {gas_type})") + gas_type = data.get('gas_type', '未知') + available_gas_sources.append(f"{node_id} (气体类型: {gas_type})") - debug_print(f"可用气源列表: {available_gas_sources}") + debug_print(f"📊 可用气源: {available_gas_sources}") # 第四步:如果找不到特定气体,使用默认的第一个气源 + debug_print(f"📋 方法4: 查找默认气源...") default_gas_sources = [ node for node in G.nodes() if ((G.nodes[node].get('class') or '').find('virtual_gas_source') != -1 @@ -91,11 +149,12 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str: debug_print(f"⚠️ 未找到特定气体 '{gas}',使用默认气源: {default_source}") return default_source - raise ValueError(f"找不到气体 '{gas}' 对应的气源。可用气源: {available_gas_sources}") + debug_print(f"❌ 所有方法都失败了!") + raise ValueError(f"无法找到气体 '{gas}' 的气源。可用气源: {available_gas_sources}") def find_vacuum_pump(G: nx.DiGraph) -> str: """查找真空泵设备""" - debug_print("查找真空泵设备...") + debug_print("🔍 正在查找真空泵...") vacuum_pumps = [] for node in G.nodes(): @@ -106,16 +165,18 @@ def find_vacuum_pump(G: nx.DiGraph) -> str: 'vacuum_pump' in node.lower() or 'vacuum' in node_class.lower()): vacuum_pumps.append(node) + debug_print(f"📋 发现真空泵: {node}") if not vacuum_pumps: - raise ValueError("系统中未找到真空泵设备") + debug_print(f"❌ 系统中未找到真空泵") + raise ValueError("系统中未找到真空泵") - debug_print(f"找到真空泵: {vacuum_pumps[0]}") + debug_print(f"✅ 使用真空泵: {vacuum_pumps[0]}") return vacuum_pumps[0] def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> Optional[str]: """查找与指定容器相连的搅拌器""" - debug_print(f"查找与容器 {vessel} 相连的搅拌器...") + debug_print(f"🔍 正在查找与容器 {vessel} 连接的搅拌器...") stirrer_nodes = [] for node in G.nodes(): @@ -124,24 +185,27 @@ def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> Optional[str]: if 'virtual_stirrer' in node_class or 'stirrer' in node.lower(): stirrer_nodes.append(node) + debug_print(f"📋 发现搅拌器: {node}") + + debug_print(f"📊 找到的搅拌器总数: {len(stirrer_nodes)}") # 检查哪个搅拌器与目标容器相连 for stirrer in stirrer_nodes: if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): - debug_print(f"找到连接的搅拌器: {stirrer}") + debug_print(f"✅ 找到连接的搅拌器: {stirrer}") return stirrer # 如果没有连接的搅拌器,返回第一个可用的 if stirrer_nodes: - debug_print(f"未找到直接连接的搅拌器,使用第一个可用的: {stirrer_nodes[0]}") + debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个可用的: {stirrer_nodes[0]}") return stirrer_nodes[0] - debug_print("未找到搅拌器") + debug_print("❌ 未找到搅拌器") return None def find_vacuum_solenoid_valve(G: nx.DiGraph, vacuum_pump: str) -> Optional[str]: - """查找真空泵相关的电磁阀 - 根据实际连接逻辑""" - debug_print(f"查找真空泵 {vacuum_pump} 相关的电磁阀...") + """查找真空泵相关的电磁阀""" + debug_print(f"🔍 正在查找真空泵 {vacuum_pump} 的电磁阀...") # 查找所有电磁阀 solenoid_valves = [] @@ -151,29 +215,30 @@ def find_vacuum_solenoid_valve(G: nx.DiGraph, vacuum_pump: str) -> Optional[str] if ('solenoid' in node_class.lower() or 'solenoid_valve' in node.lower()): solenoid_valves.append(node) + debug_print(f"📋 发现电磁阀: {node}") - debug_print(f"找到的电磁阀: {solenoid_valves}") + debug_print(f"📊 找到的电磁阀: {solenoid_valves}") - # 🔧 修复:根据实际组态图连接逻辑查找 - # vacuum_pump_1 <- solenoid_valve_1 <- multiway_valve_2 + # 检查连接关系 + debug_print(f"📋 方法1: 检查连接关系...") for solenoid in solenoid_valves: - # 检查电磁阀是否连接到真空泵 if G.has_edge(solenoid, vacuum_pump) or G.has_edge(vacuum_pump, solenoid): - debug_print(f"✓ 找到连接真空泵的电磁阀: {solenoid}") + debug_print(f"✅ 找到连接的真空电磁阀: {solenoid}") return solenoid - # 通过命名规则查找(备选方案) + # 通过命名规则查找 + debug_print(f"📋 方法2: 检查命名规则...") for solenoid in solenoid_valves: if 'vacuum' in solenoid.lower() or solenoid == 'solenoid_valve_1': - debug_print(f"✓ 通过命名规则找到真空电磁阀: {solenoid}") + debug_print(f"✅ 通过命名找到真空电磁阀: {solenoid}") return solenoid debug_print("⚠️ 未找到真空电磁阀") return None def find_gas_solenoid_valve(G: nx.DiGraph, gas_source: str) -> Optional[str]: - """查找气源相关的电磁阀 - 根据实际连接逻辑""" - debug_print(f"查找气源 {gas_source} 相关的电磁阀...") + """查找气源相关的电磁阀""" + debug_print(f"🔍 正在查找气源 {gas_source} 的电磁阀...") # 查找所有电磁阀 solenoid_valves = [] @@ -184,18 +249,20 @@ def find_gas_solenoid_valve(G: nx.DiGraph, gas_source: str) -> Optional[str]: if ('solenoid' in node_class.lower() or 'solenoid_valve' in node.lower()): solenoid_valves.append(node) - # 🔧 修复:根据实际组态图连接逻辑查找 - # gas_source_1 -> solenoid_valve_2 -> multiway_valve_2 + debug_print(f"📊 找到的电磁阀: {solenoid_valves}") + + # 检查连接关系 + debug_print(f"📋 方法1: 检查连接关系...") for solenoid in solenoid_valves: - # 检查气源是否连接到电磁阀 if G.has_edge(gas_source, solenoid) or G.has_edge(solenoid, gas_source): - debug_print(f"✓ 找到连接气源的电磁阀: {solenoid}") + debug_print(f"✅ 找到连接的气源电磁阀: {solenoid}") return solenoid - # 通过命名规则查找(备选方案) + # 通过命名规则查找 + debug_print(f"📋 方法2: 检查命名规则...") for solenoid in solenoid_valves: if 'gas' in solenoid.lower() or solenoid == 'solenoid_valve_2': - debug_print(f"✓ 通过命名规则找到气源电磁阀: {solenoid}") + debug_print(f"✅ 通过命名找到气源电磁阀: {solenoid}") return solenoid debug_print("⚠️ 未找到气源电磁阀") @@ -208,7 +275,7 @@ def generate_evacuateandrefill_protocol( **kwargs ) -> List[Dict[str, Any]]: """ - 生成抽真空和充气操作的动作序列 - 最终修复版本 + 生成抽真空和充气操作的动作序列 - 中文版 Args: G: 设备图 @@ -223,75 +290,113 @@ def generate_evacuateandrefill_protocol( # 硬编码重复次数为 3 repeats = 3 - # 🔧 修复:在函数开始就生成协议ID + # 生成协议ID protocol_id = str(uuid.uuid4()) - debug_print(f"生成协议ID: {protocol_id}") + debug_print(f"🆔 生成协议ID: {protocol_id}") debug_print("=" * 60) - debug_print("开始生成抽真空充气协议") - debug_print(f"输入参数:") - debug_print(f" - vessel: {vessel}") - debug_print(f" - gas: {gas}") - debug_print(f" - repeats: {repeats} (硬编码)") - debug_print(f" - 其他参数: {kwargs}") + debug_print("🧪 开始生成抽真空充气协议") + debug_print(f"📋 原始参数:") + debug_print(f" 🥼 容器: '{vessel}'") + debug_print(f" 💨 气体: '{gas}'") + debug_print(f" 🔄 循环次数: {repeats} (硬编码)") + debug_print(f" 📦 其他参数: {kwargs}") debug_print("=" * 60) action_sequence = [] # === 参数验证和修正 === - debug_print("步骤1: 参数验证和修正...") + debug_print("🔍 步骤1: 参数验证和修正...") + action_sequence.append(create_action_log(f"开始抽真空充气操作 - 容器: {vessel}", "🎬")) + action_sequence.append(create_action_log(f"目标气体: {gas}", "💨")) + action_sequence.append(create_action_log(f"循环次数: {repeats}", "🔄")) # 验证必需参数 if not vessel: - raise ValueError("vessel 参数不能为空") + debug_print("❌ 容器参数不能为空") + raise ValueError("容器参数不能为空") if not gas: - raise ValueError("gas 参数不能为空") + debug_print("❌ 气体参数不能为空") + raise ValueError("气体参数不能为空") if vessel not in G.nodes(): - raise ValueError(f"容器 '{vessel}' 不存在于系统中") + debug_print(f"❌ 容器 '{vessel}' 在系统中不存在") + raise ValueError(f"容器 '{vessel}' 在系统中不存在") + + debug_print("✅ 基本参数验证通过") + action_sequence.append(create_action_log("参数验证通过", "✅")) # 标准化气体名称 + debug_print("🔧 标准化气体名称...") gas_aliases = { 'n2': 'nitrogen', 'ar': 'argon', 'air': 'air', 'o2': 'oxygen', 'co2': 'carbon_dioxide', - 'h2': 'hydrogen' + 'h2': 'hydrogen', + '氮气': 'nitrogen', + '氩气': 'argon', + '空气': 'air', + '氧气': 'oxygen', + '二氧化碳': 'carbon_dioxide', + '氢气': 'hydrogen' } original_gas = gas gas_lower = gas.lower().strip() if gas_lower in gas_aliases: gas = gas_aliases[gas_lower] - debug_print(f"标准化气体名称: {original_gas} -> {gas}") + debug_print(f"🔄 标准化气体名称: {original_gas} -> {gas}") + action_sequence.append(create_action_log(f"气体名称标准化: {original_gas} -> {gas}", "🔄")) - debug_print(f"最终参数: vessel={vessel}, gas={gas}, repeats={repeats}") + debug_print(f"📋 最终参数: 容器={vessel}, 气体={gas}, 重复={repeats}") # === 查找设备 === - debug_print("步骤2: 查找设备...") + debug_print("🔍 步骤2: 查找设备...") + action_sequence.append(create_action_log("正在查找相关设备...", "🔍")) try: vacuum_pump = find_vacuum_pump(G) - gas_source = find_gas_source(G, gas) - vacuum_solenoid = find_vacuum_solenoid_valve(G, vacuum_pump) - gas_solenoid = find_gas_solenoid_valve(G, gas_source) - stirrer_id = find_connected_stirrer(G, vessel) + action_sequence.append(create_action_log(f"找到真空泵: {vacuum_pump}", "🌪️")) - debug_print(f"设备配置:") - debug_print(f" - 真空泵: {vacuum_pump}") - debug_print(f" - 气源: {gas_source}") - debug_print(f" - 真空电磁阀: {vacuum_solenoid}") - debug_print(f" - 气源电磁阀: {gas_solenoid}") - debug_print(f" - 搅拌器: {stirrer_id}") + gas_source = find_gas_source(G, gas) + action_sequence.append(create_action_log(f"找到气源: {gas_source}", "💨")) + + vacuum_solenoid = find_vacuum_solenoid_valve(G, vacuum_pump) + if vacuum_solenoid: + action_sequence.append(create_action_log(f"找到真空电磁阀: {vacuum_solenoid}", "🚪")) + else: + action_sequence.append(create_action_log("未找到真空电磁阀", "⚠️")) + + gas_solenoid = find_gas_solenoid_valve(G, gas_source) + if gas_solenoid: + action_sequence.append(create_action_log(f"找到气源电磁阀: {gas_solenoid}", "🚪")) + else: + action_sequence.append(create_action_log("未找到气源电磁阀", "⚠️")) + + stirrer_id = find_connected_stirrer(G, vessel) + if stirrer_id: + action_sequence.append(create_action_log(f"找到搅拌器: {stirrer_id}", "🌪️")) + else: + action_sequence.append(create_action_log("未找到搅拌器", "⚠️")) + + debug_print(f"📊 设备配置:") + debug_print(f" 🌪️ 真空泵: {vacuum_pump}") + debug_print(f" 💨 气源: {gas_source}") + debug_print(f" 🚪 真空电磁阀: {vacuum_solenoid}") + debug_print(f" 🚪 气源电磁阀: {gas_solenoid}") + debug_print(f" 🌪️ 搅拌器: {stirrer_id}") except Exception as e: debug_print(f"❌ 设备查找失败: {str(e)}") + action_sequence.append(create_action_log(f"设备查找失败: {str(e)}", "❌")) raise ValueError(f"设备查找失败: {str(e)}") # === 参数设置 === - debug_print("步骤3: 参数设置...") + debug_print("🔍 步骤3: 参数设置...") + action_sequence.append(create_action_log("设置操作参数...", "⚙️")) # 根据气体类型调整参数 if gas.lower() in ['nitrogen', 'argon']: @@ -300,87 +405,108 @@ def generate_evacuateandrefill_protocol( PUMP_FLOW_RATE = 2.0 VACUUM_TIME = 30.0 REFILL_TIME = 20.0 - debug_print("惰性气体:使用标准参数") + debug_print("💨 惰性气体: 使用标准参数") + action_sequence.append(create_action_log("检测到惰性气体,使用标准参数", "💨")) elif gas.lower() in ['air', 'oxygen']: VACUUM_VOLUME = 20.0 REFILL_VOLUME = 20.0 PUMP_FLOW_RATE = 1.5 VACUUM_TIME = 45.0 REFILL_TIME = 25.0 - debug_print("活性气体:使用保守参数") + debug_print("🔥 活性气体: 使用保守参数") + action_sequence.append(create_action_log("检测到活性气体,使用保守参数", "🔥")) else: VACUUM_VOLUME = 15.0 REFILL_VOLUME = 15.0 PUMP_FLOW_RATE = 1.0 VACUUM_TIME = 60.0 REFILL_TIME = 30.0 - debug_print("未知气体:使用安全参数") + debug_print("❓ 未知气体: 使用安全参数") + action_sequence.append(create_action_log("未知气体类型,使用安全参数", "❓")) STIR_SPEED = 200.0 - debug_print(f"操作参数:") - debug_print(f" - 抽真空体积: {VACUUM_VOLUME}mL") - debug_print(f" - 充气体积: {REFILL_VOLUME}mL") - debug_print(f" - 泵流速: {PUMP_FLOW_RATE}mL/s") - debug_print(f" - 抽真空时间: {VACUUM_TIME}s") - debug_print(f" - 充气时间: {REFILL_TIME}s") - debug_print(f" - 搅拌速度: {STIR_SPEED}RPM") + debug_print(f"⚙️ 操作参数:") + debug_print(f" 📏 真空体积: {VACUUM_VOLUME}mL") + debug_print(f" 📏 充气体积: {REFILL_VOLUME}mL") + debug_print(f" ⚡ 泵流速: {PUMP_FLOW_RATE}mL/s") + debug_print(f" ⏱️ 真空时间: {VACUUM_TIME}s") + debug_print(f" ⏱️ 充气时间: {REFILL_TIME}s") + debug_print(f" 🌪️ 搅拌速度: {STIR_SPEED}RPM") + + action_sequence.append(create_action_log(f"真空体积: {VACUUM_VOLUME}mL", "📏")) + action_sequence.append(create_action_log(f"充气体积: {REFILL_VOLUME}mL", "📏")) + action_sequence.append(create_action_log(f"泵流速: {PUMP_FLOW_RATE}mL/s", "⚡")) # === 路径验证 === - debug_print("步骤4: 路径验证...") + debug_print("🔍 步骤4: 路径验证...") + action_sequence.append(create_action_log("验证传输路径...", "🛤️")) try: - # 验证抽真空路径: vessel -> vacuum_pump (通过八通阀和电磁阀) + # 验证抽真空路径 if nx.has_path(G, vessel, vacuum_pump): vacuum_path = nx.shortest_path(G, source=vessel, target=vacuum_pump) - debug_print(f"抽真空路径: {' → '.join(vacuum_path)}") + debug_print(f"✅ 真空路径: {' -> '.join(vacuum_path)}") + action_sequence.append(create_action_log(f"真空路径: {' -> '.join(vacuum_path)}", "🛤️")) else: - debug_print(f"⚠️ 抽真空路径不存在,继续执行但可能有问题") + debug_print(f"⚠️ 真空路径不存在,继续执行但可能有问题") + action_sequence.append(create_action_log("真空路径检查: 路径不存在", "⚠️")) - # 验证充气路径: gas_source -> vessel (通过电磁阀和八通阀) + # 验证充气路径 if nx.has_path(G, gas_source, vessel): gas_path = nx.shortest_path(G, source=gas_source, target=vessel) - debug_print(f"充气路径: {' → '.join(gas_path)}") + debug_print(f"✅ 气体路径: {' -> '.join(gas_path)}") + action_sequence.append(create_action_log(f"气体路径: {' -> '.join(gas_path)}", "🛤️")) else: - debug_print(f"⚠️ 充气路径不存在,继续执行但可能有问题") + debug_print(f"⚠️ 气体路径不存在,继续执行但可能有问题") + action_sequence.append(create_action_log("气体路径检查: 路径不存在", "⚠️")) except Exception as e: debug_print(f"⚠️ 路径验证失败: {str(e)},继续执行") + action_sequence.append(create_action_log(f"路径验证失败: {str(e)}", "⚠️")) # === 启动搅拌器 === - debug_print("步骤5: 启动搅拌器...") + debug_print("🔍 步骤5: 启动搅拌器...") if stirrer_id: - debug_print(f"启动搅拌器: {stirrer_id}") + debug_print(f"🌪️ 启动搅拌器: {stirrer_id}") + action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {STIR_SPEED}rpm)", "🌪️")) + action_sequence.append({ "device_id": stirrer_id, "action_name": "start_stir", "action_kwargs": { "vessel": vessel, "stir_speed": STIR_SPEED, - "purpose": "抽真空充气操作前启动搅拌" + "purpose": "抽真空充气前预搅拌" } }) # 等待搅拌稳定 + action_sequence.append(create_action_log("等待搅拌稳定...", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 5.0} }) else: - debug_print("未找到搅拌器,跳过搅拌启动") + debug_print("⚠️ 未找到搅拌器,跳过搅拌器启动") + action_sequence.append(create_action_log("跳过搅拌器启动", "⏭️")) - # === 执行 3 次抽真空-充气循环 === - debug_print("步骤6: 执行抽真空-充气循环...") + # === 执行循环 === + debug_print("🔍 步骤6: 执行抽真空-充气循环...") + action_sequence.append(create_action_log(f"开始 {repeats} 次抽真空-充气循环", "🔄")) for cycle in range(repeats): - debug_print(f"=== 第 {cycle+1}/{repeats} 次循环 ===") + debug_print(f"=== 第 {cycle+1}/{repeats} 轮循环 ===") + action_sequence.append(create_action_log(f"第 {cycle+1}/{repeats} 轮循环开始", "🚀")) # ============ 抽真空阶段 ============ - debug_print(f"抽真空阶段开始") + debug_print(f"🌪️ 抽真空阶段开始") + action_sequence.append(create_action_log("开始抽真空阶段", "🌪️")) # 启动真空泵 - debug_print(f"启动真空泵: {vacuum_pump}") + debug_print(f"🔛 启动真空泵: {vacuum_pump}") + action_sequence.append(create_action_log(f"启动真空泵: {vacuum_pump}", "🔛")) action_sequence.append({ "device_id": vacuum_pump, "action_name": "set_status", @@ -389,17 +515,19 @@ def generate_evacuateandrefill_protocol( # 开启真空电磁阀 if vacuum_solenoid: - debug_print(f"开启真空电磁阀: {vacuum_solenoid}") + debug_print(f"🚪 打开真空电磁阀: {vacuum_solenoid}") + action_sequence.append(create_action_log(f"打开真空电磁阀: {vacuum_solenoid}", "🚪")) action_sequence.append({ "device_id": vacuum_solenoid, "action_name": "set_valve_position", "action_kwargs": {"command": "OPEN"} }) - # 抽真空操作 - 使用液体转移协议 - debug_print(f"抽真空操作: {vessel} → {vacuum_pump}") + # 抽真空操作 + debug_print(f"🌪️ 抽真空操作: {vessel} -> {vacuum_pump}") + action_sequence.append(create_action_log(f"开始抽真空: {vessel} -> {vacuum_pump}", "🌪️")) + try: - vacuum_transfer_actions = generate_pump_protocol_with_rinsing( G=G, from_vessel=vessel, @@ -419,8 +547,10 @@ def generate_evacuateandrefill_protocol( if vacuum_transfer_actions: action_sequence.extend(vacuum_transfer_actions) debug_print(f"✅ 添加了 {len(vacuum_transfer_actions)} 个抽真空动作") + action_sequence.append(create_action_log(f"抽真空协议完成 ({len(vacuum_transfer_actions)} 个操作)", "✅")) else: debug_print("⚠️ 抽真空协议返回空序列,添加手动动作") + action_sequence.append(create_action_log("抽真空协议为空,使用手动等待", "⚠️")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": VACUUM_TIME} @@ -428,13 +558,15 @@ def generate_evacuateandrefill_protocol( except Exception as e: debug_print(f"❌ 抽真空失败: {str(e)}") - # 添加等待时间作为备选 + action_sequence.append(create_action_log(f"抽真空失败: {str(e)}", "❌")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": VACUUM_TIME} }) # 抽真空后等待 + wait_minutes = VACUUM_TIME / 60 + action_sequence.append(create_action_log(f"抽真空后等待 ({wait_minutes:.1f} 分钟)", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": VACUUM_TIME} @@ -442,7 +574,8 @@ def generate_evacuateandrefill_protocol( # 关闭真空电磁阀 if vacuum_solenoid: - debug_print(f"关闭真空电磁阀: {vacuum_solenoid}") + debug_print(f"🚪 关闭真空电磁阀: {vacuum_solenoid}") + action_sequence.append(create_action_log(f"关闭真空电磁阀: {vacuum_solenoid}", "🚪")) action_sequence.append({ "device_id": vacuum_solenoid, "action_name": "set_valve_position", @@ -450,24 +583,28 @@ def generate_evacuateandrefill_protocol( }) # 关闭真空泵 - debug_print(f"关闭真空泵: {vacuum_pump}") + debug_print(f"🔴 停止真空泵: {vacuum_pump}") + action_sequence.append(create_action_log(f"停止真空泵: {vacuum_pump}", "🔴")) action_sequence.append({ "device_id": vacuum_pump, "action_name": "set_status", "action_kwargs": {"string": "OFF"} }) - # 抽真空后等待 + # 阶段间等待 + action_sequence.append(create_action_log("抽真空阶段完成,短暂等待", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 5.0} }) # ============ 充气阶段 ============ - debug_print(f"充气阶段开始") + debug_print(f"💨 充气阶段开始") + action_sequence.append(create_action_log("开始气体充气阶段", "💨")) # 启动气源 - debug_print(f"启动气源: {gas_source}") + debug_print(f"🔛 启动气源: {gas_source}") + action_sequence.append(create_action_log(f"启动气源: {gas_source}", "🔛")) action_sequence.append({ "device_id": gas_source, "action_name": "set_status", @@ -476,17 +613,19 @@ def generate_evacuateandrefill_protocol( # 开启气源电磁阀 if gas_solenoid: - debug_print(f"开启气源电磁阀: {gas_solenoid}") + debug_print(f"🚪 打开气源电磁阀: {gas_solenoid}") + action_sequence.append(create_action_log(f"打开气源电磁阀: {gas_solenoid}", "🚪")) action_sequence.append({ "device_id": gas_solenoid, "action_name": "set_valve_position", "action_kwargs": {"command": "OPEN"} }) - # 充气操作 - 使用液体转移协议 - debug_print(f"充气操作: {gas_source} → {vessel}") + # 充气操作 + debug_print(f"💨 充气操作: {gas_source} -> {vessel}") + action_sequence.append(create_action_log(f"开始气体充气: {gas_source} -> {vessel}", "💨")) + try: - gas_transfer_actions = generate_pump_protocol_with_rinsing( G=G, from_vessel=gas_source, @@ -506,22 +645,26 @@ def generate_evacuateandrefill_protocol( if gas_transfer_actions: action_sequence.extend(gas_transfer_actions) debug_print(f"✅ 添加了 {len(gas_transfer_actions)} 个充气动作") + action_sequence.append(create_action_log(f"气体充气协议完成 ({len(gas_transfer_actions)} 个操作)", "✅")) else: debug_print("⚠️ 充气协议返回空序列,添加手动动作") + action_sequence.append(create_action_log("充气协议为空,使用手动等待", "⚠️")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": REFILL_TIME} }) except Exception as e: - debug_print(f"❌ 充气失败: {str(e)}") - # 添加等待时间作为备选 + debug_print(f"❌ 气体充气失败: {str(e)}") + action_sequence.append(create_action_log(f"气体充气失败: {str(e)}", "❌")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": REFILL_TIME} }) # 充气后等待 + refill_wait_minutes = REFILL_TIME / 60 + action_sequence.append(create_action_log(f"充气后等待 ({refill_wait_minutes:.1f} 分钟)", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": REFILL_TIME} @@ -529,7 +672,8 @@ def generate_evacuateandrefill_protocol( # 关闭气源电磁阀 if gas_solenoid: - debug_print(f"关闭气源电磁阀: {gas_solenoid}") + debug_print(f"🚪 关闭气源电磁阀: {gas_solenoid}") + action_sequence.append(create_action_log(f"关闭气源电磁阀: {gas_solenoid}", "🚪")) action_sequence.append({ "device_id": gas_solenoid, "action_name": "set_valve_position", @@ -537,68 +681,92 @@ def generate_evacuateandrefill_protocol( }) # 关闭气源 - debug_print(f"关闭气源: {gas_source}") + debug_print(f"🔴 停止气源: {gas_source}") + action_sequence.append(create_action_log(f"停止气源: {gas_source}", "🔴")) action_sequence.append({ "device_id": gas_source, "action_name": "set_status", "action_kwargs": {"string": "OFF"} }) - # 等待下一次循环 + # 循环间等待 if cycle < repeats - 1: - debug_print(f"等待下一次循环...") + debug_print(f"⏳ 等待下一个循环...") + action_sequence.append(create_action_log("等待下一个循环...", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 10.0} }) + else: + action_sequence.append(create_action_log(f"第 {cycle+1}/{repeats} 轮循环完成", "✅")) # === 停止搅拌器 === - debug_print("步骤7: 停止搅拌器...") + debug_print("🔍 步骤7: 停止搅拌器...") if stirrer_id: - debug_print(f"停止搅拌器: {stirrer_id}") + debug_print(f"🛑 停止搅拌器: {stirrer_id}") + action_sequence.append(create_action_log(f"停止搅拌器: {stirrer_id}", "🛑")) action_sequence.append({ "device_id": stirrer_id, "action_name": "stop_stir", "action_kwargs": {"vessel": vessel} }) + else: + action_sequence.append(create_action_log("跳过搅拌器停止", "⏭️")) # === 最终等待 === + action_sequence.append(create_action_log("最终稳定等待...", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 10.0} }) # === 总结 === + total_time = (VACUUM_TIME + REFILL_TIME + 25) * repeats + 20 + debug_print("=" * 60) - debug_print(f"抽真空充气协议生成完成") - debug_print(f"总动作数: {len(action_sequence)}") - debug_print(f"处理容器: {vessel}") - debug_print(f"使用气体: {gas}") - debug_print(f"重复次数: {repeats} (硬编码)") + 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" 🥼 处理容器: {vessel}") + debug_print(f" 💨 使用气体: {gas}") + debug_print(f" 🔄 重复次数: {repeats}") debug_print("=" * 60) + # 添加完成日志 + summary_msg = f"抽真空充气协议完成: {vessel} (使用 {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]]: """生成氮气置换协议""" + debug_print(f"💨 生成氮气置换协议: {vessel}") return generate_evacuateandrefill_protocol(G, vessel, "nitrogen", **kwargs) def generate_argon_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]: """生成氩气置换协议""" + debug_print(f"💨 生成氩气置换协议: {vessel}") return generate_evacuateandrefill_protocol(G, vessel, "argon", **kwargs) def generate_air_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]: """生成空气置换协议""" + debug_print(f"💨 生成空气置换协议: {vessel}") 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]]: + """生成惰性气氛协议""" + debug_print(f"🛡️ 生成惰性气氛协议: {vessel} (使用 {gas})") + return generate_evacuateandrefill_protocol(G, vessel, gas, **kwargs) + # 测试函数 def test_evacuateandrefill_protocol(): """测试抽真空充气协议""" - debug_print("=== EVACUATE AND REFILL PROTOCOL 测试 ===") - debug_print("测试完成") + debug_print("=== 抽真空充气协议增强中文版测试 ===") + debug_print("✅ 测试完成") if __name__ == "__main__": test_evacuateandrefill_protocol() \ No newline at end of file diff --git a/unilabos/compile/pump_protocol.py b/unilabos/compile/pump_protocol.py index f214ff4..a54218e 100644 --- a/unilabos/compile/pump_protocol.py +++ b/unilabos/compile/pump_protocol.py @@ -273,7 +273,6 @@ def generate_pump_protocol( if not pump_backbone: debug_print("PUMP_TRANSFER: 没有泵骨架节点,可能是直接容器连接或只有电磁阀") - # 🔧 对于气体传输,这是正常的,直接返回空序列 return pump_action_sequence if transfer_flowrate == 0: @@ -319,10 +318,31 @@ def generate_pump_protocol( volume_left = volume debug_print(f"PUMP_TRANSFER: 需要 {repeats} 次转移,单次最大体积 {min_transfer_volume} mL") + # 🆕 只在开头打印总体概览 + if repeats > 1: + debug_print(f"🔄 分批转移概览: 总体积 {volume:.2f}mL,需要 {repeats} 次转移") + logger.info(f"🔄 分批转移概览: 总体积 {volume:.2f}mL,需要 {repeats} 次转移") + + # 🔧 创建一个自定义的wait动作,用于在执行时打印日志 + def create_progress_log_action(message: str) -> Dict[str, Any]: + """创建一个特殊的等待动作,在执行时打印进度日志""" + return { + "action_name": "wait", + "action_kwargs": { + "time": 0.1, # 很短的等待时间 + "progress_message": message # 自定义字段,用于进度日志 + } + } + # 生成泵操作序列 for i in range(repeats): current_volume = min(volume_left, min_transfer_volume) + # 🆕 在每次循环开始时添加进度日志 + if repeats > 1: + start_message = f"🚀 准备开始第 {i+1}/{repeats} 次转移: {current_volume:.2f}mL ({from_vessel} → {to_vessel}) 🚰" + pump_action_sequence.append(create_progress_log_action(start_message)) + # 🔧 修复:安全地获取边数据 def get_safe_edge_data(node_a, node_b, key): try: @@ -426,6 +446,426 @@ def generate_pump_protocol( ]) pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}}) + # 🆕 在每次循环结束时添加完成日志 + if repeats > 1: + remaining_volume = volume_left - current_volume + if remaining_volume > 0: + end_message = f"✅ 第 {i+1}/{repeats} 次转移完成! 剩余 {remaining_volume:.2f}mL 待转移 ⏳" + else: + end_message = f"🎉 第 {i+1}/{repeats} 次转移完成! 全部 {volume:.2f}mL 转移完毕 ✨" + + pump_action_sequence.append(create_progress_log_action(end_message)) + + volume_left -= current_volume + + return pump_action_sequence + + +def generate_pump_protocol_with_rinsing( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + volume: float = 0.0, + amount: str = "", + time: float = 0.0, # 🔧 修复:统一使用 time + viscous: bool = False, + rinsing_solvent: str = "", + rinsing_volume: float = 0.0, + rinsing_repeats: int = 0, + solid: bool = False, + flowrate: float = 2.5, + transfer_flowrate: float = 0.5, + rate_spec: str = "", + event: str = "", + through: str = "", + **kwargs +) -> List[Dict[str, Any]]: + """ + 原有的同步版本,添加防冲突机制 + """ + + # 添加执行锁,防止并发调用 + import threading + if not hasattr(generate_pump_protocol_with_rinsing, '_lock'): + generate_pump_protocol_with_rinsing._lock = threading.Lock() + + with generate_pump_protocol_with_rinsing._lock: + debug_print("=" * 60) + debug_print(f"PUMP_TRANSFER: 🚀 开始生成协议 (同步版本)") + debug_print(f" 📍 路径: {from_vessel} -> {to_vessel}") + debug_print(f" 🕐 时间戳: {time_module.time()}") + debug_print(f" 🔒 获得执行锁") + debug_print("=" * 60) + + # 短暂延迟,避免快速重复调用 + time_module.sleep(0.01) + + debug_print("🔍 步骤1: 开始体积处理...") + + # 1. 处理体积参数 + final_volume = volume + debug_print(f"📋 初始设置: final_volume = {final_volume}") + + # 🔧 修复:如果volume为0(ROS2传入的空值),从容器读取实际体积 + if volume == 0.0: + debug_print("🎯 检测到 volume=0.0,开始自动体积检测...") + + # 直接从源容器读取实际体积 + actual_volume = get_vessel_liquid_volume(G, from_vessel) + debug_print(f"📖 从容器 '{from_vessel}' 读取到体积: {actual_volume}mL") + + if actual_volume > 0: + final_volume = actual_volume + debug_print(f"✅ 成功设置体积为: {final_volume}mL") + else: + final_volume = 10.0 # 如果读取失败,使用默认值 + logger.warning(f"⚠️ 无法从容器读取体积,使用默认值: {final_volume}mL") + else: + debug_print(f"📌 体积非零,直接使用: {final_volume}mL") + + # 处理 amount 参数 + if amount and amount.strip(): + debug_print(f"🔍 检测到 amount 参数: '{amount}',开始解析...") + parsed_volume = _parse_amount_to_volume(amount) + debug_print(f"📖 从 amount 解析得到体积: {parsed_volume}mL") + + if parsed_volume > 0: + final_volume = parsed_volume + debug_print(f"✅ 使用从 amount 解析的体积: {final_volume}mL") + elif parsed_volume == 0.0 and amount.lower().strip() == "all": + debug_print("🎯 检测到 amount='all',从容器读取全部体积...") + actual_volume = get_vessel_liquid_volume(G, from_vessel) + if actual_volume > 0: + final_volume = actual_volume + debug_print(f"✅ amount='all',设置体积为: {final_volume}mL") + + # 最终体积验证 + debug_print(f"🔍 步骤2: 最终体积验证...") + if final_volume <= 0: + logger.error(f"❌ 体积无效: {final_volume}mL") + final_volume = 10.0 + logger.warning(f"⚠️ 强制设置为默认值: {final_volume}mL") + + debug_print(f"✅ 最终确定体积: {final_volume}mL") + + # 2. 处理流速参数 + debug_print(f"🔍 步骤3: 处理流速参数...") + debug_print(f" - 原始 flowrate: {flowrate}") + debug_print(f" - 原始 transfer_flowrate: {transfer_flowrate}") + + final_flowrate = flowrate if flowrate > 0 else 2.5 + final_transfer_flowrate = transfer_flowrate if transfer_flowrate > 0 else 0.5 + + if flowrate <= 0: + logger.warning(f"⚠️ flowrate <= 0,修正为: {final_flowrate}mL/s") + if transfer_flowrate <= 0: + logger.warning(f"⚠️ transfer_flowrate <= 0,修正为: {final_transfer_flowrate}mL/s") + + debug_print(f"✅ 修正后流速: flowrate={final_flowrate}mL/s, transfer_flowrate={final_transfer_flowrate}mL/s") + + # 3. 根据时间计算流速 + if time > 0 and final_volume > 0: + debug_print(f"🔍 步骤4: 根据时间计算流速...") + calculated_flowrate = final_volume / time + debug_print(f" - 计算得到流速: {calculated_flowrate}mL/s") + + if flowrate <= 0 or flowrate == 2.5: + final_flowrate = min(calculated_flowrate, 10.0) + debug_print(f" - 调整 flowrate 为: {final_flowrate}mL/s") + if transfer_flowrate <= 0 or transfer_flowrate == 0.5: + final_transfer_flowrate = min(calculated_flowrate, 5.0) + debug_print(f" - 调整 transfer_flowrate 为: {final_transfer_flowrate}mL/s") + + # 4. 根据速度规格调整 + if rate_spec: + debug_print(f"🔍 步骤5: 根据速度规格调整...") + debug_print(f" - 速度规格: '{rate_spec}'") + + if rate_spec == "dropwise": + final_flowrate = min(final_flowrate, 0.1) + final_transfer_flowrate = min(final_transfer_flowrate, 0.1) + debug_print(f" - dropwise模式,流速调整为: {final_flowrate}mL/s") + elif rate_spec == "slowly": + final_flowrate = min(final_flowrate, 0.5) + final_transfer_flowrate = min(final_transfer_flowrate, 0.3) + debug_print(f" - slowly模式,流速调整为: {final_flowrate}mL/s") + elif rate_spec == "quickly": + final_flowrate = max(final_flowrate, 5.0) + final_transfer_flowrate = max(final_transfer_flowrate, 2.0) + debug_print(f" - quickly模式,流速调整为: {final_flowrate}mL/s") + + try: + # 🆕 修复:在这里调用带有循环日志的generate_pump_protocol_with_loop_logging函数 + pump_action_sequence = generate_pump_protocol_with_loop_logging( + G, from_vessel, to_vessel, final_volume, + final_flowrate, final_transfer_flowrate + ) + + debug_print(f"🔓 释放执行锁") + return pump_action_sequence + + except Exception as e: + logger.error(f"❌ 协议生成失败: {str(e)}") + return [ + { + "device_id": "system", + "action_name": "log_message", + "action_kwargs": { + "message": f"❌ 协议生成失败: {str(e)}" + } + } + ] + + +def generate_pump_protocol_with_loop_logging( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + volume: float, + flowrate: float = 2.5, + transfer_flowrate: float = 0.5, +) -> List[Dict[str, Any]]: + """ + 生成泵操作的动作序列 - 带循环日志版本 + 🔧 修复:正确处理包含电磁阀的路径,并在合适时机打印循环日志 + """ + pump_action_sequence = [] + nodes = G.nodes(data=True) + + # 验证输入参数 + if volume <= 0: + logger.error(f"无效的体积参数: {volume}mL") + return pump_action_sequence + + if flowrate <= 0: + flowrate = 2.5 + logger.warning(f"flowrate <= 0,使用默认值 {flowrate}mL/s") + + if transfer_flowrate <= 0: + transfer_flowrate = 0.5 + logger.warning(f"transfer_flowrate <= 0,使用默认值 {transfer_flowrate}mL/s") + + # 验证容器存在 + if from_vessel not in G.nodes(): + logger.error(f"源容器 '{from_vessel}' 不存在") + return pump_action_sequence + + if to_vessel not in G.nodes(): + logger.error(f"目标容器 '{to_vessel}' 不存在") + return pump_action_sequence + + try: + shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel) + debug_print(f"PUMP_TRANSFER: 路径 {from_vessel} -> {to_vessel}: {shortest_path}") + except nx.NetworkXNoPath: + logger.error(f"无法找到从 '{from_vessel}' 到 '{to_vessel}' 的路径") + return pump_action_sequence + + # 🔧 关键修复:正确构建泵骨架,排除容器和电磁阀 + pump_backbone = [] + for node in shortest_path: + # 跳过起始和结束容器 + if node == from_vessel or node == to_vessel: + continue + + # 跳过电磁阀(电磁阀不参与泵操作) + node_data = G.nodes.get(node, {}) + node_class = node_data.get("class", "") or "" + if ("solenoid" in node_class.lower() or "solenoid_valve" in node.lower()): + debug_print(f"PUMP_TRANSFER: 跳过电磁阀 {node}") + continue + + # 只包含多通阀和泵 + if ("multiway" in node_class.lower() or "valve" in node_class.lower() or "pump" in node_class.lower()): + pump_backbone.append(node) + + debug_print(f"PUMP_TRANSFER: 过滤后的泵骨架: {pump_backbone}") + + if not pump_backbone: + debug_print("PUMP_TRANSFER: 没有泵骨架节点,可能是直接容器连接或只有电磁阀") + return pump_action_sequence + + if transfer_flowrate == 0: + transfer_flowrate = flowrate + + try: + pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone) + except Exception as e: + debug_print(f"PUMP_TRANSFER: 构建泵-阀门映射失败: {str(e)}") + return pump_action_sequence + + if not pumps_from_node: + debug_print("PUMP_TRANSFER: 没有可用的泵映射") + return pump_action_sequence + + # 🔧 修复:安全地获取最小转移体积 + try: + min_transfer_volumes = [] + for node in pump_backbone: + if node in pumps_from_node: + pump_node = pumps_from_node[node] + if pump_node in nodes: + pump_config = nodes[pump_node].get("config", {}) + max_volume = pump_config.get("max_volume") + if max_volume is not None: + min_transfer_volumes.append(max_volume) + + if min_transfer_volumes: + min_transfer_volume = min(min_transfer_volumes) + else: + min_transfer_volume = 25.0 # 默认值 + debug_print(f"PUMP_TRANSFER: 无法获取泵的最大体积,使用默认值: {min_transfer_volume}mL") + except Exception as e: + debug_print(f"PUMP_TRANSFER: 获取最小转移体积失败: {str(e)}") + min_transfer_volume = 25.0 # 默认值 + + repeats = int(np.ceil(volume / min_transfer_volume)) + + if repeats > 1 and (from_vessel.startswith("pump") or to_vessel.startswith("pump")): + logger.error("Cannot transfer volume larger than min_transfer_volume between two pumps.") + return pump_action_sequence + + volume_left = volume + debug_print(f"PUMP_TRANSFER: 需要 {repeats} 次转移,单次最大体积 {min_transfer_volume} mL") + + # 🆕 只在开头打印总体概览 + if repeats > 1: + debug_print(f"🔄 分批转移概览: 总体积 {volume:.2f}mL,需要 {repeats} 次转移") + logger.info(f"🔄 分批转移概览: 总体积 {volume:.2f}mL,需要 {repeats} 次转移") + + # 🔧 创建一个自定义的wait动作,用于在执行时打印日志 + def create_progress_log_action(message: str) -> Dict[str, Any]: + """创建一个特殊的等待动作,在执行时打印进度日志""" + return { + "action_name": "wait", + "action_kwargs": { + "time": 0.1, # 很短的等待时间 + "progress_message": message # 自定义字段,用于进度日志 + } + } + + # 生成泵操作序列 + for i in range(repeats): + current_volume = min(volume_left, min_transfer_volume) + + # 🆕 在每次循环开始时添加进度日志 + if repeats > 1: + start_message = f"🚀 准备开始第 {i+1}/{repeats} 次转移: {current_volume:.2f}mL ({from_vessel} → {to_vessel}) 🚰" + pump_action_sequence.append(create_progress_log_action(start_message)) + + # 🔧 修复:安全地获取边数据 + def get_safe_edge_data(node_a, node_b, key): + try: + edge_data = G.get_edge_data(node_a, node_b) + if edge_data and "port" in edge_data: + port_data = edge_data["port"] + if isinstance(port_data, dict) and key in port_data: + return port_data[key] + return "default" + except Exception as e: + debug_print(f"PUMP_TRANSFER: 获取边数据失败 {node_a}->{node_b}: {str(e)}") + return "default" + + # 从源容器吸液 + if not from_vessel.startswith("pump") and pump_backbone: + first_pump_node = pump_backbone[0] + if first_pump_node in valve_from_node and first_pump_node in pumps_from_node: + port_command = get_safe_edge_data(first_pump_node, from_vessel, first_pump_node) + pump_action_sequence.extend([ + { + "device_id": valve_from_node[first_pump_node], + "action_name": "set_valve_position", + "action_kwargs": { + "command": port_command + } + }, + { + "device_id": pumps_from_node[first_pump_node], + "action_name": "set_position", + "action_kwargs": { + "position": float(current_volume), + "max_velocity": transfer_flowrate + } + } + ]) + pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}}) + + # 泵间转移 + for nodeA, nodeB in zip(pump_backbone[:-1], pump_backbone[1:]): + if nodeA in valve_from_node and nodeB in valve_from_node and nodeA in pumps_from_node and nodeB in pumps_from_node: + port_a = get_safe_edge_data(nodeA, nodeB, nodeA) + port_b = get_safe_edge_data(nodeB, nodeA, nodeB) + + pump_action_sequence.append([ + { + "device_id": valve_from_node[nodeA], + "action_name": "set_valve_position", + "action_kwargs": { + "command": port_a + } + }, + { + "device_id": valve_from_node[nodeB], + "action_name": "set_valve_position", + "action_kwargs": { + "command": port_b + } + } + ]) + pump_action_sequence.append([ + { + "device_id": pumps_from_node[nodeA], + "action_name": "set_position", + "action_kwargs": { + "position": 0.0, + "max_velocity": transfer_flowrate + } + }, + { + "device_id": pumps_from_node[nodeB], + "action_name": "set_position", + "action_kwargs": { + "position": float(current_volume), + "max_velocity": transfer_flowrate + } + } + ]) + pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}}) + + # 排液到目标容器 + if not to_vessel.startswith("pump") and pump_backbone: + last_pump_node = pump_backbone[-1] + if last_pump_node in valve_from_node and last_pump_node in pumps_from_node: + port_command = get_safe_edge_data(last_pump_node, to_vessel, last_pump_node) + pump_action_sequence.extend([ + { + "device_id": valve_from_node[last_pump_node], + "action_name": "set_valve_position", + "action_kwargs": { + "command": port_command + } + }, + { + "device_id": pumps_from_node[last_pump_node], + "action_name": "set_position", + "action_kwargs": { + "position": 0.0, + "max_velocity": flowrate + } + } + ]) + pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}}) + + # 🆕 在每次循环结束时添加完成日志 + if repeats > 1: + remaining_volume = volume_left - current_volume + if remaining_volume > 0: + end_message = f"✅ 第 {i+1}/{repeats} 次转移完成! 剩余 {remaining_volume:.2f}mL 待转移 ⏳" + else: + end_message = f"🎉 第 {i+1}/{repeats} 次转移完成! 全部 {volume:.2f}mL 转移完毕 ✨" + + pump_action_sequence.append(create_progress_log_action(end_message)) + volume_left -= current_volume return pump_action_sequence @@ -891,58 +1331,386 @@ def generate_pump_protocol_with_rinsing( final_flowrate = max(final_flowrate, 5.0) final_transfer_flowrate = max(final_transfer_flowrate, 2.0) debug_print(f" - quickly模式,流速调整为: {final_flowrate}mL/s") + + # # 5. 处理冲洗参数 + # debug_print(f"🔍 步骤6: 处理冲洗参数...") + # final_rinsing_solvent = rinsing_solvent + # final_rinsing_volume = rinsing_volume if rinsing_volume > 0 else 5.0 + # final_rinsing_repeats = rinsing_repeats if rinsing_repeats > 0 else 2 + + # if rinsing_volume <= 0: + # logger.warning(f"⚠️ rinsing_volume <= 0,修正为: {final_rinsing_volume}mL") + # if rinsing_repeats <= 0: + # logger.warning(f"⚠️ rinsing_repeats <= 0,修正为: {final_rinsing_repeats}次") + + # # 根据物理属性调整冲洗参数 + # if viscous or solid: + # final_rinsing_repeats = max(final_rinsing_repeats, 3) + # final_rinsing_volume = max(final_rinsing_volume, 10.0) + # debug_print(f"🧪 粘稠/固体物质,调整冲洗参数:{final_rinsing_repeats}次,{final_rinsing_volume}mL") + + # 参数总结 + debug_print("📊 最终参数总结:") + debug_print(f" - 体积: {final_volume}mL") + debug_print(f" - 流速: {final_flowrate}mL/s") + debug_print(f" - 转移流速: {final_transfer_flowrate}mL/s") + # debug_print(f" - 冲洗溶剂: '{final_rinsing_solvent}'") + # debug_print(f" - 冲洗体积: {final_rinsing_volume}mL") + # debug_print(f" - 冲洗次数: {final_rinsing_repeats}次") + + # ========== 执行基础转移 ========== + + debug_print("🔧 步骤7: 开始执行基础转移...") + + try: + debug_print(f" - 调用 generate_pump_protocol...") + debug_print(f" - 参数: G, '{from_vessel}', '{to_vessel}', {final_volume}, {final_flowrate}, {final_transfer_flowrate}") - # # 5. 处理冲洗参数 - # debug_print(f"🔍 步骤6: 处理冲洗参数...") - # final_rinsing_solvent = rinsing_solvent - # final_rinsing_volume = rinsing_volume if rinsing_volume > 0 else 5.0 - # final_rinsing_repeats = rinsing_repeats if rinsing_repeats > 0 else 2 + pump_action_sequence = generate_pump_protocol( + G, from_vessel, to_vessel, final_volume, + final_flowrate, final_transfer_flowrate + ) - # if rinsing_volume <= 0: - # logger.warning(f"⚠️ rinsing_volume <= 0,修正为: {final_rinsing_volume}mL") - # if rinsing_repeats <= 0: - # logger.warning(f"⚠️ rinsing_repeats <= 0,修正为: {final_rinsing_repeats}次") + debug_print(f" - generate_pump_protocol 返回结果:") + debug_print(f" - 动作序列长度: {len(pump_action_sequence)}") + debug_print(f" - 动作序列是否为空: {len(pump_action_sequence) == 0}") - # # 根据物理属性调整冲洗参数 - # if viscous or solid: - # final_rinsing_repeats = max(final_rinsing_repeats, 3) - # final_rinsing_volume = max(final_rinsing_volume, 10.0) - # debug_print(f"🧪 粘稠/固体物质,调整冲洗参数:{final_rinsing_repeats}次,{final_rinsing_volume}mL") - - try: - pump_action_sequence = generate_pump_protocol( - G, from_vessel, to_vessel, final_volume, - flowrate, transfer_flowrate - ) + if not pump_action_sequence: + debug_print("❌ 基础转移协议生成为空,可能是路径问题") + debug_print(f" - 源容器存在: {from_vessel in G.nodes()}") + debug_print(f" - 目标容器存在: {to_vessel in G.nodes()}") - # 为每个动作添加唯一标识 - # for i, action in enumerate(pump_action_sequence): - # if isinstance(action, dict): - # action['_protocol_id'] = protocol_id - # action['_action_sequence'] = i - # elif isinstance(action, list): - # for j, sub_action in enumerate(action): - # if isinstance(sub_action, dict): - # sub_action['_protocol_id'] = protocol_id - # sub_action['_action_sequence'] = f"{i}_{j}" - # - # debug_print(f"📊 协议 {protocol_id} 生成完成,共 {len(pump_action_sequence)} 个动作") - debug_print(f"🔓 释放执行锁") - return pump_action_sequence + if from_vessel in G.nodes() and to_vessel in G.nodes(): + try: + path = nx.shortest_path(G, source=from_vessel, target=to_vessel) + debug_print(f" - 路径存在: {path}") + except Exception as path_error: + debug_print(f" - 无法找到路径: {str(path_error)}") - except Exception as e: - logger.error(f"❌ 协议生成失败: {str(e)}") return [ { "device_id": "system", "action_name": "log_message", "action_kwargs": { - "message": f"❌ 协议生成失败: {str(e)}" - }, - '_protocol_id': protocol_id, - '_action_sequence': 0 + "message": f"⚠️ 路径问题,无法转移: {final_volume}mL 从 {from_vessel} 到 {to_vessel}" + } } ] + + debug_print(f"✅ 基础转移生成了 {len(pump_action_sequence)} 个动作") + + # 打印前几个动作用于调试 + if len(pump_action_sequence) > 0: + debug_print("🔍 前几个动作预览:") + for i, action in enumerate(pump_action_sequence[:3]): + debug_print(f" 动作 {i+1}: {action}") + if len(pump_action_sequence) > 3: + debug_print(f" ... 还有 {len(pump_action_sequence) - 3} 个动作") + + except Exception as e: + debug_print(f"❌ 基础转移失败: {str(e)}") + import traceback + debug_print(f"详细错误: {traceback.format_exc()}") + return [ + { + "device_id": "system", + "action_name": "log_message", + "action_kwargs": { + "message": f"❌ 转移失败: {final_volume}mL 从 {from_vessel} 到 {to_vessel}, 错误: {str(e)}" + } + } + ] + + # ========== 执行冲洗操作 ========== + + # debug_print("🔧 步骤8: 检查冲洗操作...") + + # if final_rinsing_solvent and final_rinsing_solvent.strip() and final_rinsing_repeats > 0: + # debug_print(f"🧽 开始冲洗操作,溶剂: '{final_rinsing_solvent}'") + + # try: + # if final_rinsing_solvent.strip() != "air": + # debug_print(" - 执行液体冲洗...") + # rinsing_actions = _generate_rinsing_sequence( + # G, from_vessel, to_vessel, final_rinsing_solvent, + # final_rinsing_volume, final_rinsing_repeats, + # final_flowrate, final_transfer_flowrate + # ) + # pump_action_sequence.extend(rinsing_actions) + # debug_print(f" - 添加了 {len(rinsing_actions)} 个冲洗动作") + # else: + # debug_print(" - 执行空气冲洗...") + # air_rinsing_actions = _generate_air_rinsing_sequence( + # G, from_vessel, to_vessel, final_rinsing_volume, final_rinsing_repeats, + # final_flowrate, final_transfer_flowrate + # ) + # pump_action_sequence.extend(air_rinsing_actions) + # debug_print(f" - 添加了 {len(air_rinsing_actions)} 个空气冲洗动作") + # except Exception as e: + # debug_print(f"⚠️ 冲洗操作失败: {str(e)},跳过冲洗") + # else: + # debug_print(f"⏭️ 跳过冲洗操作") + # debug_print(f" - 溶剂: '{final_rinsing_solvent}'") + # debug_print(f" - 次数: {final_rinsing_repeats}") + # debug_print(f" - 条件满足: {bool(final_rinsing_solvent and final_rinsing_solvent.strip() and final_rinsing_repeats > 0)}") + + # ========== 最终结果 ========== + + debug_print("=" * 60) + debug_print(f"🎉 PUMP_TRANSFER: 协议生成完成") + debug_print(f" 📊 总动作数: {len(pump_action_sequence)}") + debug_print(f" 📋 最终体积: {final_volume}mL") + debug_print(f" 🚀 执行路径: {from_vessel} -> {to_vessel}") + + # 最终验证 + if len(pump_action_sequence) == 0: + debug_print("🚨 协议生成结果为空!这是异常情况") + return [ + { + "device_id": "system", + "action_name": "log_message", + "action_kwargs": { + "message": f"🚨 协议生成失败: 无法生成任何动作序列" + } + } + ] + + debug_print("=" * 60) + return pump_action_sequence + + +async def generate_pump_protocol_with_rinsing_async( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + volume: float = 0.0, + amount: str = "", + time: float = 0.0, + viscous: bool = False, + rinsing_solvent: str = "", + rinsing_volume: float = 0.0, + rinsing_repeats: int = 0, + solid: bool = False, + flowrate: float = 2.5, + transfer_flowrate: float = 0.5, + rate_spec: str = "", + event: str = "", + through: str = "", + **kwargs +) -> List[Dict[str, Any]]: + """ + 异步版本的泵转移协议生成器,避免并发问题 + """ + debug_print("=" * 60) + debug_print(f"PUMP_TRANSFER: 🚀 开始生成协议 (异步版本)") + debug_print(f" 📍 路径: {from_vessel} -> {to_vessel}") + debug_print(f" 🕐 时间戳: {time_module.time()}") + debug_print("=" * 60) + + # 添加唯一标识符 + protocol_id = f"pump_transfer_{int(time_module.time() * 1000000)}" + debug_print(f"📋 协议ID: {protocol_id}") + + # 调用原有的同步版本 + result = generate_pump_protocol_with_rinsing( + G, from_vessel, to_vessel, volume, amount, time, viscous, + rinsing_solvent, rinsing_volume, rinsing_repeats, solid, + flowrate, transfer_flowrate, rate_spec, event, through, **kwargs + ) + + # 为每个动作添加唯一标识 + for i, action in enumerate(result): + if isinstance(action, dict): + action['_protocol_id'] = protocol_id + action['_action_sequence'] = i + action['_timestamp'] = time_module.time() + + debug_print(f"📊 协议 {protocol_id} 生成完成,共 {len(result)} 个动作") + return result + +# 保持原有的同步版本兼容性 +def generate_pump_protocol_with_rinsing( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + volume: float = 0.0, + amount: str = "", + time: float = 0.0, + viscous: bool = False, + rinsing_solvent: str = "", + rinsing_volume: float = 0.0, + rinsing_repeats: int = 0, + solid: bool = False, + flowrate: float = 2.5, + transfer_flowrate: float = 0.5, + rate_spec: str = "", + event: str = "", + through: str = "", + **kwargs +) -> List[Dict[str, Any]]: + """ + 原有的同步版本,添加防冲突机制 + """ + + # 添加执行锁,防止并发调用 + import threading + if not hasattr(generate_pump_protocol_with_rinsing, '_lock'): + generate_pump_protocol_with_rinsing._lock = threading.Lock() + + with generate_pump_protocol_with_rinsing._lock: + debug_print("=" * 60) + debug_print(f"PUMP_TRANSFER: 🚀 开始生成协议 (同步版本)") + debug_print(f" 📍 路径: {from_vessel} -> {to_vessel}") + debug_print(f" 🕐 时间戳: {time_module.time()}") + debug_print(f" 🔒 获得执行锁") + debug_print("=" * 60) + + # 短暂延迟,避免快速重复调用 + time_module.sleep(0.01) + + debug_print("🔍 步骤1: 开始体积处理...") + + # 1. 处理体积参数 + final_volume = volume + debug_print(f"📋 初始设置: final_volume = {final_volume}") + + # 🔧 修复:如果volume为0(ROS2传入的空值),从容器读取实际体积 + if volume == 0.0: + debug_print("🎯 检测到 volume=0.0,开始自动体积检测...") + + # 直接从源容器读取实际体积 + actual_volume = get_vessel_liquid_volume(G, from_vessel) + debug_print(f"📖 从容器 '{from_vessel}' 读取到体积: {actual_volume}mL") + + if actual_volume > 0: + final_volume = actual_volume + debug_print(f"✅ 成功设置体积为: {final_volume}mL") + else: + final_volume = 10.0 # 如果读取失败,使用默认值 + logger.warning(f"⚠️ 无法从容器读取体积,使用默认值: {final_volume}mL") + else: + debug_print(f"📌 体积非零,直接使用: {final_volume}mL") + + # 处理 amount 参数 + if amount and amount.strip(): + debug_print(f"🔍 检测到 amount 参数: '{amount}',开始解析...") + parsed_volume = _parse_amount_to_volume(amount) + debug_print(f"📖 从 amount 解析得到体积: {parsed_volume}mL") + + if parsed_volume > 0: + final_volume = parsed_volume + debug_print(f"✅ 使用从 amount 解析的体积: {final_volume}mL") + elif parsed_volume == 0.0 and amount.lower().strip() == "all": + debug_print("🎯 检测到 amount='all',从容器读取全部体积...") + actual_volume = get_vessel_liquid_volume(G, from_vessel) + if actual_volume > 0: + final_volume = actual_volume + debug_print(f"✅ amount='all',设置体积为: {final_volume}mL") + + # 最终体积验证 + debug_print(f"🔍 步骤2: 最终体积验证...") + if final_volume <= 0: + logger.error(f"❌ 体积无效: {final_volume}mL") + final_volume = 10.0 + logger.warning(f"⚠️ 强制设置为默认值: {final_volume}mL") + + debug_print(f"✅ 最终确定体积: {final_volume}mL") + + # 2. 处理流速参数 + debug_print(f"🔍 步骤3: 处理流速参数...") + debug_print(f" - 原始 flowrate: {flowrate}") + debug_print(f" - 原始 transfer_flowrate: {transfer_flowrate}") + + final_flowrate = flowrate if flowrate > 0 else 2.5 + final_transfer_flowrate = transfer_flowrate if transfer_flowrate > 0 else 0.5 + + if flowrate <= 0: + logger.warning(f"⚠️ flowrate <= 0,修正为: {final_flowrate}mL/s") + if transfer_flowrate <= 0: + logger.warning(f"⚠️ transfer_flowrate <= 0,修正为: {final_transfer_flowrate}mL/s") + + debug_print(f"✅ 修正后流速: flowrate={final_flowrate}mL/s, transfer_flowrate={final_transfer_flowrate}mL/s") + + # 3. 根据时间计算流速 + if time > 0 and final_volume > 0: + debug_print(f"🔍 步骤4: 根据时间计算流速...") + calculated_flowrate = final_volume / time + debug_print(f" - 计算得到流速: {calculated_flowrate}mL/s") + + if flowrate <= 0 or flowrate == 2.5: + final_flowrate = min(calculated_flowrate, 10.0) + debug_print(f" - 调整 flowrate 为: {final_flowrate}mL/s") + if transfer_flowrate <= 0 or transfer_flowrate == 0.5: + final_transfer_flowrate = min(calculated_flowrate, 5.0) + debug_print(f" - 调整 transfer_flowrate 为: {final_transfer_flowrate}mL/s") + + # 4. 根据速度规格调整 + if rate_spec: + debug_print(f"🔍 步骤5: 根据速度规格调整...") + debug_print(f" - 速度规格: '{rate_spec}'") + + if rate_spec == "dropwise": + final_flowrate = min(final_flowrate, 0.1) + final_transfer_flowrate = min(final_transfer_flowrate, 0.1) + debug_print(f" - dropwise模式,流速调整为: {final_flowrate}mL/s") + elif rate_spec == "slowly": + final_flowrate = min(final_flowrate, 0.5) + final_transfer_flowrate = min(final_transfer_flowrate, 0.3) + debug_print(f" - slowly模式,流速调整为: {final_flowrate}mL/s") + elif rate_spec == "quickly": + final_flowrate = max(final_flowrate, 5.0) + final_transfer_flowrate = max(final_transfer_flowrate, 2.0) + debug_print(f" - quickly模式,流速调整为: {final_flowrate}mL/s") + + # # 5. 处理冲洗参数 + # debug_print(f"🔍 步骤6: 处理冲洗参数...") + # final_rinsing_solvent = rinsing_solvent + # final_rinsing_volume = rinsing_volume if rinsing_volume > 0 else 5.0 + # final_rinsing_repeats = rinsing_repeats if rinsing_repeats > 0 else 2 + + # if rinsing_volume <= 0: + # logger.warning(f"⚠️ rinsing_volume <= 0,修正为: {final_rinsing_volume}mL") + # if rinsing_repeats <= 0: + # logger.warning(f"⚠️ rinsing_repeats <= 0,修正为: {final_rinsing_repeats}次") + + # # 根据物理属性调整冲洗参数 + # if viscous or solid: + # final_rinsing_repeats = max(final_rinsing_repeats, 3) + # final_rinsing_volume = max(final_rinsing_volume, 10.0) + # debug_print(f"🧪 粘稠/固体物质,调整冲洗参数:{final_rinsing_repeats}次,{final_rinsing_volume}mL") + + try: + pump_action_sequence = generate_pump_protocol( + G, from_vessel, to_vessel, final_volume, + flowrate, transfer_flowrate + ) + + # 为每个动作添加唯一标识 + # for i, action in enumerate(pump_action_sequence): + # if isinstance(action, dict): + # action['_protocol_id'] = protocol_id + # action['_action_sequence'] = i + # elif isinstance(action, list): + # for j, sub_action in enumerate(action): + # if isinstance(sub_action, dict): + # sub_action['_protocol_id'] = protocol_id + # sub_action['_action_sequence'] = f"{i}_{j}" + # + # debug_print(f"📊 协议 {protocol_id} 生成完成,共 {len(pump_action_sequence)} 个动作") + debug_print(f"🔓 释放执行锁") + return pump_action_sequence + + except Exception as e: + logger.error(f"❌ 协议生成失败: {str(e)}") + return [ + { + "device_id": "system", + "action_name": "log_message", + "action_kwargs": { + "message": f"❌ 协议生成失败: {str(e)}" + } + } + ] def _parse_amount_to_volume(amount: str) -> float: """解析 amount 字符串为体积""" diff --git a/unilabos/compile/separate_protocol.py b/unilabos/compile/separate_protocol.py index 07d9ab4..258c37d 100644 --- a/unilabos/compile/separate_protocol.py +++ b/unilabos/compile/separate_protocol.py @@ -1,15 +1,67 @@ import networkx as nx import re import logging +import sys from typing import List, Dict, Any, Union 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"[SEPARATE] {message}", flush=True) - logger.info(f"[SEPARATE] {message}") + """调试输出函数 - 支持中文""" + 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 parse_volume_input(volume_input: Union[str, float]) -> float: """ @@ -22,52 +74,58 @@ def parse_volume_input(volume_input: Union[str, float]) -> float: float: 体积(毫升) """ if isinstance(volume_input, (int, float)): + debug_print(f"📏 体积输入为数值: {volume_input}") return float(volume_input) if not volume_input or not str(volume_input).strip(): + debug_print(f"⚠️ 体积输入为空,返回 0.0mL") return 0.0 volume_str = str(volume_input).lower().strip() - debug_print(f"解析体积输入: '{volume_str}'") + debug_print(f"🔍 解析体积输入: '{volume_str}'") # 处理未知体积 - if volume_str in ['?', 'unknown', 'tbd', 'to be determined']: + if volume_str in ['?', 'unknown', 'tbd', 'to be determined', '未知', '待定']: default_volume = 100.0 # 默认100mL - debug_print(f"检测到未知体积,使用默认值: {default_volume}mL") + 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) + 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") + 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']: + if unit in ['l', 'liter', '升']: volume = value * 1000.0 # L -> mL - elif unit in ['μl', 'ul', 'microliter']: + debug_print(f"🔄 体积转换: {value}L -> {volume}mL") + elif unit in ['μl', 'ul', 'microliter', '微升']: volume = value / 1000.0 # μL -> mL - else: # ml, milliliter 或默认 + debug_print(f"🔄 体积转换: {value}μL -> {volume}mL") + else: # ml, milliliter, 毫升 或默认 volume = value # 已经是mL + debug_print(f"✅ 体积已为毫升单位: {volume}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(): + debug_print("⏭️ 未指定溶剂,跳过溶剂容器查找") return "" - debug_print(f"查找溶剂 '{solvent}' 的容器...") + debug_print(f"🔍 正在查找溶剂 '{solvent}' 的容器...") # 🔧 方法1:直接搜索 data.reagent_name 和 config.reagent + debug_print(f"📋 方法1: 搜索试剂字段...") for node in G.nodes(): node_data = G.nodes[node].get('data', {}) node_type = G.nodes[node].get('type', '') @@ -80,16 +138,17 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: # 精确匹配 if reagent_name == solvent.lower() or config_reagent == solvent.lower(): - debug_print(f"✅ 通过reagent字段找到容器: {node}") + debug_print(f"✅ 通过试剂字段精确匹配找到容器: {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}") + debug_print(f"✅ 通过试剂字段模糊匹配找到容器: {node}") return node # 🔧 方法2:常见的容器命名规则 + debug_print(f"📋 方法2: 使用命名规则...") solvent_clean = solvent.lower().replace(' ', '_').replace('-', '_') possible_names = [ f"flask_{solvent_clean}", @@ -99,9 +158,14 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: f"{solvent_clean}_bottle", f"solvent_{solvent_clean}", f"reagent_{solvent_clean}", - f"reagent_bottle_{solvent_clean}" + f"reagent_bottle_{solvent_clean}", + f"reagent_bottle_1", # 通用试剂瓶 + f"reagent_bottle_2", + f"reagent_bottle_3" ] + debug_print(f"🎯 尝试的容器名称: {possible_names[:5]}... (共 {len(possible_names)} 个)") + for name in possible_names: if name in G.nodes(): node_type = G.nodes[name].get('type', '') @@ -110,53 +174,94 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: return name # 🔧 方法3:使用第一个试剂瓶作为备选 + debug_print(f"📋 方法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}") + debug_print(f"⚠️ 未找到专用容器,使用备用容器: {node_id}") return node_id - debug_print(f"⚠️ 未找到溶剂 '{solvent}' 的容器") + debug_print(f"❌ 无法找到溶剂 '{solvent}' 的容器") return "" def find_separator_device(G: nx.DiGraph, vessel: str) -> str: - """查找分离器设备""" - debug_print(f"查找容器 '{vessel}' 对应的分离器设备...") + """查找分离器设备,支持多种查找方式""" + debug_print(f"🔍 正在查找容器 '{vessel}' 的分离器设备...") # 方法1:查找连接到容器的分离器设备 + debug_print(f"📋 方法1: 检查连接的分离器...") + separator_nodes = [] for node in G.nodes(): node_class = G.nodes[node].get('class', '').lower() if 'separator' in node_class: + separator_nodes.append(node) + debug_print(f"📋 发现分离器设备: {node}") + # 检查是否连接到目标容器 if G.has_edge(node, vessel) or G.has_edge(vessel, node): debug_print(f"✅ 找到连接的分离器: {node}") return node + debug_print(f"📊 找到的分离器总数: {len(separator_nodes)}") + # 方法2:根据命名规则查找 + debug_print(f"📋 方法2: 使用命名规则...") possible_names = [ f"{vessel}_controller", f"{vessel}_separator", vessel, # 容器本身可能就是分离器 "separator_1", - "virtual_separator" + "virtual_separator", + "liquid_handler_1", # 液体处理器也可能用于分离 + "controller_1" ] + debug_print(f"🎯 尝试的分离器名称: {possible_names}") + for name in possible_names: if name in G.nodes(): node_class = G.nodes[name].get('class', '').lower() - if 'separator' in node_class: + if 'separator' in node_class or 'controller' 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"📋 方法3: 使用第一个可用分离器...") + if separator_nodes: + debug_print(f"⚠️ 使用第一个分离器设备: {separator_nodes[0]}") + return separator_nodes[0] - debug_print(f"⚠️ 未找到分离器设备") + debug_print(f"❌ 未找到分离器设备") + return "" + +def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str: + """查找连接到指定容器的搅拌器""" + debug_print(f"🔍 正在查找与容器 {vessel} 连接的搅拌器...") + + stirrer_nodes = [] + for node in G.nodes(): + node_data = G.nodes[node] + node_class = node_data.get('class', '') or '' + + if 'stirrer' in node_class.lower(): + stirrer_nodes.append(node) + debug_print(f"📋 发现搅拌器: {node}") + + debug_print(f"📊 找到的搅拌器总数: {len(stirrer_nodes)}") + + # 检查哪个搅拌器与目标容器相连 + 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] + + debug_print("❌ 未找到搅拌器") return "" def generate_separate_protocol( @@ -185,7 +290,7 @@ def generate_separate_protocol( **kwargs ) -> List[Dict[str, Any]]: """ - 生成分离操作的协议序列 - 修复版 + 生成分离操作的协议序列 - 增强中文版 支持XDL参数格式: - vessel: 分离容器(必需) @@ -206,26 +311,31 @@ def generate_separate_protocol( """ 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("🧪 开始生成分离协议 - 增强中文版") + debug_print(f"📋 原始参数:") + debug_print(f" 🥼 容器: '{vessel}'") + 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("=" * 60) action_sequence = [] # === 参数验证和标准化 === - debug_print("步骤1: 参数验证和标准化...") + debug_print("🔍 步骤1: 参数验证和标准化...") + action_sequence.append(create_action_log(f"开始分离操作 - 容器: {vessel}", "🎬")) + 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)") final_to_vessel = to_vessel or product_vessel @@ -237,14 +347,18 @@ def generate_separate_protocol( # 🔧 修复:确保repeats至少为1 if repeats <= 0: repeats = 1 - debug_print(f"⚠️ repeats参数 <= 0,自动设置为1") + 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_volume}mL") - debug_print(f" - 重复次数: {repeats}") + 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}") + + action_sequence.append(create_action_log(f"分离容器: {final_vessel}", "🧪")) + action_sequence.append(create_action_log(f"溶剂体积: {final_volume}mL", "📏")) + action_sequence.append(create_action_log(f"重复次数: {repeats}", "🔄")) # 验证必需参数 if not purpose: @@ -254,66 +368,147 @@ def generate_separate_protocol( 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: 查找设备...") + debug_print("🔍 步骤2: 查找设备...") + action_sequence.append(create_action_log("正在查找相关设备...", "🔍")) # 查找分离器设备 separator_device = find_separator_device(G, final_vessel) - if not separator_device: - debug_print("⚠️ 未找到分离器设备,可能无法执行分离操作") + 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) + 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" - 溶剂容器: '{solvent_vessel}'") + debug_print(f"📊 设备配置:") + debug_print(f" 🧪 分离器设备: '{separator_device}'") + debug_print(f" 🌪️ 搅拌器设备: '{stirrer_device}'") + debug_print(f" 💧 溶剂容器: '{solvent_vessel}'") # === 执行分离流程 === - debug_print("步骤3: 执行分离流程...") + debug_print("🔍 步骤3: 执行分离流程...") + action_sequence.append(create_action_log("开始分离工作流程", "🎯")) try: for repeat_idx in range(repeats): - debug_print(f"3.{repeat_idx+1}: 第 {repeat_idx+1}/{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"3.{repeat_idx+1}.1: 添加溶剂 {solvent} ({final_volume}mL)") + debug_print(f"🔄 第{cycle_num}轮 步骤1: 添加溶剂 {solvent} ({final_volume}mL)") + action_sequence.append(create_action_log(f"向分离容器添加 {final_volume}mL {solvent}", "💧")) - # 使用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)} 个动作") + try: + # 使用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)} 个动作") + action_sequence.append(create_action_log(f"溶剂转移完成 ({len(pump_actions)} 个操作)", "✅")) + + 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: 执行分离操作 + # 步骤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, + "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} + }) + + 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"3.{repeat_idx+1}.2: 执行分离操作") + debug_print(f"🔄 第{cycle_num}轮 步骤4: 执行分离操作") + action_sequence.append(create_action_log(f"执行分离: 收集{product_phase}相", "🧪")) # 调用分离器设备的separate方法 separate_action = { @@ -330,31 +525,44 @@ def generate_separate_protocol( "solvent_volume": final_volume, "through": through, "repeats": 1, # 每次调用只做一次分离 - "stir_time": stir_time, + "stir_time": 0, # 已经在上面完成 "stir_speed": stir_speed, - "settling_time": settling_time + "settling_time": 0 # 已经在上面完成 } } action_sequence.append(separate_action) - debug_print(f"✅ 分离操作添加完成") + debug_print(f"✅ 分离操作已添加") + action_sequence.append(create_action_log("分离操作完成", "✅")) + + # 收集结果 + 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}", "🗑️")) else: - debug_print(f"3.{repeat_idx+1}.2: 无分离器设备,跳过分离操作") + debug_print(f"🔄 第{cycle_num}轮 步骤4: 无分离器设备,跳过分离") + action_sequence.append(create_action_log("无分离器设备可用", "❌")) # 添加等待时间模拟分离 action_sequence.append({ "action_name": "wait", - "action_kwargs": {"time": stir_time + settling_time} + "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)}") + debug_print(f"❌ 分离工作流程执行失败: {str(e)}") + action_sequence.append(create_action_log(f"分离工作流程失败: {str(e)}", "❌")) # 添加错误日志 action_sequence.append({ "device_id": "system", @@ -365,22 +573,31 @@ def generate_separate_protocol( }) # === 最终结果 === + total_time = (stir_time + settling_time + 15) * repeats # 估算总时间 + 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}") + 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" 🎯 分离目的: {purpose}") + debug_print(f" 📊 产物相: {product_phase}") + debug_print(f" 🔄 重复次数: {repeats}") if solvent: - debug_print(f" - 溶剂: {solvent} ({final_volume}mL)") + debug_print(f" 💧 溶剂: {solvent} ({final_volume}mL)") if final_to_vessel: - debug_print(f" - 产物容器: {final_to_vessel}") + debug_print(f" 🎯 产物容器: {final_to_vessel}") if final_waste_vessel: - debug_print(f" - 废液容器: {final_waste_vessel}") + debug_print(f" 🗑️ 废液容器: {final_waste_vessel}") debug_print("=" * 60) + # 添加完成日志 + summary_msg = f"分离协议完成: {final_vessel} ({purpose},{repeats} 次循环)" + if solvent: + summary_msg += f",使用 {final_volume}mL {solvent}" + action_sequence.append(create_action_log(summary_msg, "🎉")) + return action_sequence # === 便捷函数 === @@ -388,6 +605,7 @@ def generate_separate_protocol( 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", @@ -399,6 +617,7 @@ def separate_phases_only(G: nx.DiGraph, vessel: str, product_phase: str = "top", 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", @@ -411,6 +630,7 @@ def wash_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[st 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", @@ -423,6 +643,7 @@ def extract_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union 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", @@ -434,15 +655,16 @@ def separate_aqueous_organic(G: nx.DiGraph, vessel: str, organic_phase: str = "t # 测试函数 def test_separate_protocol(): """测试分离协议的各种参数解析""" - print("=== SEPARATE PROTOCOL 增强版测试 ===") + debug_print("=== 分离协议增强中文版测试 ===") # 测试体积解析 - volumes = ["200 mL", "?", 100.0, "1 L", "500 μL"] + debug_print("🧪 测试体积解析...") + volumes = ["200 mL", "?", 100.0, "1 L", "500 μL", "未知", "50毫升"] for vol in volumes: result = parse_volume_input(vol) - print(f"体积解析: {vol} → {result}mL") + debug_print(f"📊 体积解析结果: {vol} -> {result}mL") - print("✅ 测试完成") + debug_print("✅ 测试完成") if __name__ == "__main__": test_separate_protocol() diff --git a/unilabos/devices/virtual/virtual_multiway_valve.py b/unilabos/devices/virtual/virtual_multiway_valve.py index c12aa7f..468175c 100644 --- a/unilabos/devices/virtual/virtual_multiway_valve.py +++ b/unilabos/devices/virtual/virtual_multiway_valve.py @@ -101,7 +101,7 @@ class VirtualMultiwayValve: self._target_position = pos # 模拟阀门切换时间 - switch_time = abs(self._current_position - pos) * 0.5 # 每个位置0.1秒 + switch_time = abs(self._current_position - pos) * 0.5 # 每个位置0.5秒 if switch_time > 0: self.logger.info(f"⏱️ 阀门移动中... 预计用时: {switch_time:.1f}秒 🔄") @@ -172,32 +172,32 @@ class VirtualMultiwayValve: def is_at_position(self, position: int) -> bool: """检查是否在指定位置 🎯""" result = self._current_position == position - self.logger.debug(f"🎯 位置检查: 当前={self._current_position}, 目标={position}, 匹配={result}") + # 删除debug日志:self.logger.debug(f"🎯 位置检查: 当前={self._current_position}, 目标={position}, 匹配={result}") return result def is_at_pump_position(self) -> bool: """检查是否在transfer pump位置 🚰""" result = self._current_position == 0 - pump_status = "是" if result else "否" - self.logger.debug(f"🚰 泵位置检查: {pump_status} (当前位置: {self._current_position})") + # 删除debug日志:pump_status = "是" if result else "否" + # 删除debug日志:self.logger.debug(f"🚰 泵位置检查: {pump_status} (当前位置: {self._current_position})") return result def is_at_port(self, port_number: int) -> bool: """检查是否在指定端口位置 🔌""" result = self._current_position == port_number - port_status = "是" if result else "否" - self.logger.debug(f"🔌 端口{port_number}检查: {port_status} (当前位置: {self._current_position})") + # 删除debug日志:port_status = "是" if result else "否" + # 删除debug日志:self.logger.debug(f"🔌 端口{port_number}检查: {port_status} (当前位置: {self._current_position})") return result def get_available_positions(self) -> list: """获取可用位置列表 📋""" positions = list(range(0, self.max_positions + 1)) - self.logger.debug(f"📋 可用位置: {positions}") + # 删除debug日志:self.logger.debug(f"📋 可用位置: {positions}") return positions def get_available_ports(self) -> Dict[int, str]: """获取可用端口映射 🗺️""" - self.logger.debug(f"🗺️ 端口映射: {self.position_map}") + # 删除debug日志:self.logger.debug(f"🗺️ 端口映射: {self.position_map}") return self.position_map.copy() def reset(self): @@ -229,7 +229,7 @@ class VirtualMultiwayValve: else: flow_path = f"🔌 端口 {self._current_position} 已连接 ({current_port})" - self.logger.debug(f"🌊 当前流路: {flow_path}") + # 删除debug日志:self.logger.debug(f"🌊 当前流路: {flow_path}") return flow_path def get_info(self) -> dict: @@ -247,7 +247,7 @@ class VirtualMultiwayValve: "position_map": self.position_map } - self.logger.debug(f"📊 阀门信息: 位置={self._current_position}, 状态={self._status}, 端口={self.get_current_port()}") + # 删除debug日志:self.logger.debug(f"📊 阀门信息: 位置={self._current_position}, 状态={self._status}, 端口={self.get_current_port()}") return info def __str__(self): @@ -264,7 +264,7 @@ class VirtualMultiwayValve: Args: command: 目标位置 (0-8) 或位置字符串 """ - self.logger.debug(f"🎯 兼容性调用: set_valve_position({command})") + # 删除debug日志:self.logger.debug(f"🎯 兼容性调用: set_valve_position({command})") return self.set_position(command) diff --git a/unilabos/devices/virtual/virtual_transferpump.py b/unilabos/devices/virtual/virtual_transferpump.py index a2cba9c..7d80744 100644 --- a/unilabos/devices/virtual/virtual_transferpump.py +++ b/unilabos/devices/virtual/virtual_transferpump.py @@ -12,7 +12,7 @@ class VirtualPumpMode(Enum): class VirtualTransferPump: - """虚拟转移泵类 - 模拟泵的基本功能,无需实际硬件""" + """虚拟转移泵类 - 模拟泵的基本功能,无需实际硬件 🚰""" def __init__(self, device_id: str = None, config: dict = None, **kwargs): """ @@ -42,20 +42,31 @@ class VirtualTransferPump: self._max_velocity = 5.0 # float self._current_volume = 0.0 # float + # 🚀 新增:快速模式设置 - 大幅缩短执行时间 + self._fast_mode = True # 是否启用快速模式 + self._fast_move_time = 1.0 # 快速移动时间(秒) + self._fast_dispense_time = 1.0 # 快速喷射时间(秒) + self.logger = logging.getLogger(f"VirtualTransferPump.{self.device_id}") + + print(f"🚰 === 虚拟转移泵 {self.device_id} 已创建 === ✨") + print(f"💨 快速模式: {'启用' if self._fast_mode else '禁用'} | 移动时间: {self._fast_move_time}s | 喷射时间: {self._fast_dispense_time}s") + print(f"📊 最大容量: {self.max_volume}mL | 端口: {self.port}") async def initialize(self) -> bool: - """初始化虚拟泵""" - self.logger.info(f"Initializing virtual pump {self.device_id}") + """初始化虚拟泵 🚀""" + self.logger.info(f"🔧 初始化虚拟转移泵 {self.device_id} ✨") self._status = "Idle" self._position = 0.0 self._current_volume = 0.0 + self.logger.info(f"✅ 转移泵 {self.device_id} 初始化完成 🚰") return True async def cleanup(self) -> bool: - """清理虚拟泵""" - self.logger.info(f"Cleaning up virtual pump {self.device_id}") + """清理虚拟泵 🧹""" + self.logger.info(f"🧹 清理虚拟转移泵 {self.device_id} 🔚") self._status = "Idle" + self.logger.info(f"✅ 转移泵 {self.device_id} 清理完成 💤") return True # 基本属性 @@ -65,12 +76,12 @@ class VirtualTransferPump: @property def position(self) -> float: - """当前柱塞位置 (ml)""" + """当前柱塞位置 (ml) 📍""" return self._position @property def current_volume(self) -> float: - """当前注射器中的体积 (ml)""" + """当前注射器中的体积 (ml) 💧""" return self._current_volume @property @@ -82,22 +93,50 @@ class VirtualTransferPump: return self._transfer_rate def set_max_velocity(self, velocity: float): - """设置最大速度 (ml/s)""" + """设置最大速度 (ml/s) 🌊""" self._max_velocity = max(0.1, min(50.0, velocity)) # 限制在合理范围内 - self.logger.info(f"Set max velocity to {self._max_velocity} ml/s") + self.logger.info(f"🌊 设置最大速度为 {self._max_velocity} mL/s") def get_status(self) -> str: - """获取泵状态""" + """获取泵状态 📋""" return self._status async def _simulate_operation(self, duration: float): - """模拟操作延时""" + """模拟操作延时 ⏱️""" self._status = "Busy" await asyncio.sleep(duration) self._status = "Idle" def _calculate_duration(self, volume: float, velocity: float = None) -> float: - """计算操作持续时间""" + """ + 计算操作持续时间 ⏰ + 🚀 快速模式:保留计算逻辑用于日志显示,但实际使用固定的快速时间 + """ + if velocity is None: + velocity = self._max_velocity + + # 📊 计算理论时间(用于日志显示) + theoretical_duration = abs(volume) / velocity + + # 🚀 如果启用快速模式,使用固定的快速时间 + if self._fast_mode: + # 根据操作类型选择快速时间 + if abs(volume) > 0.1: # 大于0.1mL的操作 + actual_duration = self._fast_move_time + else: # 很小的操作 + actual_duration = 0.5 + + self.logger.debug(f"⚡ 快速模式: 理论时间 {theoretical_duration:.2f}s → 实际时间 {actual_duration:.2f}s") + return actual_duration + else: + # 正常模式使用理论时间 + return theoretical_duration + + def _calculate_display_duration(self, volume: float, velocity: float = None) -> float: + """ + 计算显示用的持续时间(用于日志) 📊 + 这个函数返回理论计算时间,用于日志显示 + """ if velocity is None: velocity = self._max_velocity return abs(volume) / velocity @@ -105,7 +144,7 @@ class VirtualTransferPump: # 新的set_position方法 - 专门用于SetPumpPosition动作 async def set_position(self, position: float, max_velocity: float = None): """ - 移动到绝对位置 - 专门用于SetPumpPosition动作 + 移动到绝对位置 - 专门用于SetPumpPosition动作 🎯 Args: position (float): 目标位置 (ml) @@ -122,56 +161,107 @@ class VirtualTransferPump: # 限制位置在有效范围内 target_position = max(0.0, min(float(self.max_volume), target_position)) - # 计算移动距离和时间 + # 计算移动距离 volume_to_move = abs(target_position - self._position) - duration = self._calculate_duration(volume_to_move, velocity) - self.logger.info(f"SET_POSITION: Moving to {target_position} ml (current: {self._position} ml), velocity: {velocity} ml/s") + # 📊 计算显示用的时间(用于日志) + display_duration = self._calculate_display_duration(volume_to_move, velocity) - # 模拟移动过程 - start_position = self._position - steps = 10 if duration > 0.1 else 1 # 如果移动距离很小,只用1步 - step_duration = duration / steps if steps > 1 else duration + # ⚡ 计算实际执行时间(快速模式) + actual_duration = self._calculate_duration(volume_to_move, velocity) - for i in range(steps + 1): - # 计算当前位置和进度 - progress = (i / steps) * 100 if steps > 0 else 100 - current_pos = start_position + (target_position - start_position) * (i / steps) if steps > 0 else target_position + # 🎯 确定操作类型和emoji + if target_position > self._position: + operation_type = "吸液" + operation_emoji = "📥" + elif target_position < self._position: + operation_type = "排液" + operation_emoji = "📤" + else: + operation_type = "保持" + operation_emoji = "📍" + + self.logger.info(f"🎯 SET_POSITION: {operation_type} {operation_emoji}") + self.logger.info(f" 📍 位置: {self._position:.2f}mL → {target_position:.2f}mL (移动 {volume_to_move:.2f}mL)") + self.logger.info(f" 🌊 速度: {velocity:.2f} mL/s") + self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s") + + if self._fast_mode: + self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s") + + # 🚀 模拟移动过程 + if volume_to_move > 0.01: # 只有当移动距离足够大时才显示进度 + start_position = self._position + steps = 5 if actual_duration > 0.5 else 2 # 根据实际时间调整步数 + step_duration = actual_duration / steps - # 更新状态 - self._status = "Moving" if i < steps else "Idle" - self._position = current_pos - self._current_volume = current_pos + self.logger.info(f"🚀 开始{operation_type}... {operation_emoji}") - # 等待一小步时间 - if i < steps and step_duration > 0: - await asyncio.sleep(step_duration) + for i in range(steps + 1): + # 计算当前位置和进度 + progress = (i / steps) * 100 if steps > 0 else 100 + current_pos = start_position + (target_position - start_position) * (i / steps) if steps > 0 else target_position + + # 更新状态 + if i < steps: + self._status = f"{operation_type}中" + status_emoji = "🔄" + else: + self._status = "Idle" + status_emoji = "✅" + + self._position = current_pos + self._current_volume = current_pos + + # 显示进度(每25%或最后一步) + if i == 0: + self.logger.debug(f" 🔄 {operation_type}开始: {progress:.0f}%") + elif progress >= 50 and i == steps // 2: + self.logger.debug(f" 🔄 {operation_type}进度: {progress:.0f}%") + elif i == steps: + self.logger.info(f" ✅ {operation_type}完成: {progress:.0f}% | 当前位置: {current_pos:.2f}mL") + + # 等待一小步时间 + if i < steps and step_duration > 0: + await asyncio.sleep(step_duration) + else: + # 移动距离很小,直接完成 + self._position = target_position + self._current_volume = target_position + self.logger.info(f" 📍 微调完成: {target_position:.2f}mL") # 确保最终位置准确 self._position = target_position self._current_volume = target_position self._status = "Idle" - self.logger.info(f"SET_POSITION: Reached position {self._position} ml, current volume: {self._current_volume} ml") + # 📊 最终状态日志 + if volume_to_move > 0.01: + self.logger.info(f"🎉 SET_POSITION 完成! 📍 最终位置: {self._position:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL") # 返回符合action定义的结果 return { "success": True, - "message": f"Successfully moved to position {self._position} ml" + "message": f"✅ 成功移动到位置 {self._position:.2f}mL ({operation_type})", + "final_position": self._position, + "final_volume": self._current_volume, + "operation_type": operation_type } except Exception as e: - error_msg = f"Failed to set position: {str(e)}" + error_msg = f"❌ 设置位置失败: {str(e)}" self.logger.error(error_msg) return { "success": False, - "message": error_msg + "message": error_msg, + "final_position": self._position, + "final_volume": self._current_volume } # 其他泵操作方法 async def pull_plunger(self, volume: float, velocity: float = None): """ - 拉取柱塞(吸液) + 拉取柱塞(吸液) 📥 Args: volume (float): 要拉取的体积 (ml) @@ -181,23 +271,29 @@ class VirtualTransferPump: actual_volume = new_position - self._position if actual_volume <= 0: - self.logger.warning("Cannot pull - already at maximum volume") + self.logger.warning("⚠️ 无法吸液 - 已达到最大容量") return - duration = self._calculate_duration(actual_volume, velocity) + display_duration = self._calculate_display_duration(actual_volume, velocity) + actual_duration = self._calculate_duration(actual_volume, velocity) - self.logger.info(f"Pulling {actual_volume} ml (from {self._position} to {new_position})") + self.logger.info(f"📥 开始吸液: {actual_volume:.2f}mL") + self.logger.info(f" 📍 位置: {self._position:.2f}mL → {new_position:.2f}mL") + self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s") - await self._simulate_operation(duration) + if self._fast_mode: + self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s") + + await self._simulate_operation(actual_duration) self._position = new_position self._current_volume = new_position - self.logger.info(f"Pulled {actual_volume} ml, current volume: {self._current_volume} ml") + self.logger.info(f"✅ 吸液完成: {actual_volume:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL") async def push_plunger(self, volume: float, velocity: float = None): """ - 推出柱塞(排液) + 推出柱塞(排液) 📤 Args: volume (float): 要推出的体积 (ml) @@ -207,35 +303,44 @@ class VirtualTransferPump: actual_volume = self._position - new_position if actual_volume <= 0: - self.logger.warning("Cannot push - already at minimum volume") + self.logger.warning("⚠️ 无法排液 - 已达到最小容量") return - duration = self._calculate_duration(actual_volume, velocity) + display_duration = self._calculate_display_duration(actual_volume, velocity) + actual_duration = self._calculate_duration(actual_volume, velocity) - self.logger.info(f"Pushing {actual_volume} ml (from {self._position} to {new_position})") + self.logger.info(f"📤 开始排液: {actual_volume:.2f}mL") + self.logger.info(f" 📍 位置: {self._position:.2f}mL → {new_position:.2f}mL") + self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s") - await self._simulate_operation(duration) + if self._fast_mode: + self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s") + + await self._simulate_operation(actual_duration) self._position = new_position self._current_volume = new_position - self.logger.info(f"Pushed {actual_volume} ml, current volume: {self._current_volume} ml") + self.logger.info(f"✅ 排液完成: {actual_volume:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL") # 便捷操作方法 async def aspirate(self, volume: float, velocity: float = None): - """吸液操作""" + """吸液操作 📥""" await self.pull_plunger(volume, velocity) async def dispense(self, volume: float, velocity: float = None): - """排液操作""" + """排液操作 📤""" await self.push_plunger(volume, velocity) async def transfer(self, volume: float, aspirate_velocity: float = None, dispense_velocity: float = None): - """转移操作(先吸后排)""" + """转移操作(先吸后排) 🔄""" + self.logger.info(f"🔄 开始转移操作: {volume:.2f}mL") + # 吸液 await self.aspirate(volume, aspirate_velocity) # 短暂停顿 + self.logger.debug("⏸️ 短暂停顿...") await asyncio.sleep(0.1) # 排液 diff --git a/unilabos/ros/nodes/presets/protocol_node.py b/unilabos/ros/nodes/presets/protocol_node.py index f8921da..033cbd3 100644 --- a/unilabos/ros/nodes/presets/protocol_node.py +++ b/unilabos/ros/nodes/presets/protocol_node.py @@ -211,7 +211,7 @@ class ROS2ProtocolNode(BaseROS2DeviceNode): # 逐步执行工作流 step_results = [] for i, action in enumerate(protocol_steps): - self.get_logger().info(f"Running step {i + 1}: {action}") + # self.get_logger().info(f"Running step {i + 1}: {action}") if isinstance(action, dict): # 如果是单个动作,直接执行 if action["action_name"] == "wait":