From 4c6e437eb18fde835e0bcf2d8a2f2aa0779e22c6 Mon Sep 17 00:00:00 2001 From: KCFeng425 <2100011801@stu.pku.edu.cn> Date: Sun, 6 Jul 2025 19:21:53 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E9=83=A8=E5=88=86?= =?UTF-8?q?=E7=9A=84protocol=E5=9B=A0=E4=B8=BAXDL=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 但是pumptransfer,add,dissolve,separate还没修,后续还需要写virtual固体加料器 --- .../comprehensive_protocol/checklist.md | 47 +- .../compile/evacuateandrefill_protocol.py | 365 +++---- unilabos/compile/evaporate_protocol.py | 515 +++++----- unilabos/compile/filter_protocol.py | 375 +++---- unilabos/compile/heatchill_protocol.py | 538 +++++----- unilabos/compile/pump_protocol.py | 924 +++++++++++++++--- unilabos/compile/stir_protocol.py | 297 +++++- unilabos/compile/wash_solid_protocol.py | 457 +++++---- unilabos/devices/virtual/virtual_rotavap.py | 21 +- unilabos/messages/__init__.py | 507 +++++++++- unilabos/registry/devices/work_station.yaml | 496 +++++----- unilabos_msgs/action/EvacuateAndRefill.action | 3 +- unilabos_msgs/action/Evaporate.action | 3 +- unilabos_msgs/action/Filter.action | 14 +- unilabos_msgs/action/HeatChill.action | 25 +- unilabos_msgs/action/PumpTransfer.action | 5 + unilabos_msgs/action/Stir.action | 19 +- unilabos_msgs/action/WashSolid.action | 29 +- 18 files changed, 3034 insertions(+), 1606 deletions(-) diff --git a/test/experiments/comprehensive_protocol/checklist.md b/test/experiments/comprehensive_protocol/checklist.md index 5bb14ae..9645aa4 100644 --- a/test/experiments/comprehensive_protocol/checklist.md +++ b/test/experiments/comprehensive_protocol/checklist.md @@ -51,9 +51,9 @@ class PumpTransferProtocol(BaseModel): time: float = 0 viscous: bool = False rinsing_solvent: str = "air" - rinsing_volume: float = 5000 - rinsing_repeats: int = 2 - solid: bool = False + rinsing_volume: float = 5000 + rinsing_repeats: int = 2 + solid: bool = False 添加了缺失的参数,但是体积为0以及打印日志的问题修不好 flowrate: float = 500 transfer_flowrate: float = 2500 @@ -66,9 +66,9 @@ class SeparateProtocol(BaseModel): waste_phase_to_vessel: str solvent: str solvent_volume: float - through: str - repeats: int - stir_time: float + through: str + repeats: int + stir_time: float stir_speed: float settling_time: float @@ -77,14 +77,14 @@ class EvaporateProtocol(BaseModel): vessel: str pressure: float temp: float - time: float + time: float 加完了 stir_speed: float class EvacuateAndRefillProtocol(BaseModel): vessel: str gas: str - repeats: int + repeats: int 处理完了 class AddProtocol(BaseModel): vessel: str @@ -95,21 +95,27 @@ class AddProtocol(BaseModel): time: float stir: bool stir_speed: float + + + + + viscous: bool purpose: str class CentrifugeProtocol(BaseModel): vessel: str speed: float - time: float 自创的 + time: float 没毛病 temp: float class FilterProtocol(BaseModel): vessel: str filtrate_vessel: str stir: bool - stir_speed: float - temp: float + stir_speed: float + temp: float 处理了 continue_heatchill: bool volume: float @@ -118,6 +124,9 @@ class HeatChillProtocol(BaseModel): temp: float time: float + + + stir: bool stir_speed: float purpose: str @@ -133,7 +142,9 @@ class HeatChillStopProtocol(BaseModel): class StirProtocol(BaseModel): stir_time: float stir_speed: float - settling_time: float + + + settling_time: float 处理完了 class StartStirProtocol(BaseModel): vessel: str @@ -149,11 +160,11 @@ class TransferProtocol(BaseModel): volume: float amount: str = "" time: float = 0 - viscous: bool = False + viscous: bool = False rinsing_solvent: str = "" rinsing_volume: float = 0.0 rinsing_repeats: int = 0 - solid: bool = False + solid: bool = False 这个protocol早该删掉了 class CleanVesselProtocol(BaseModel): vessel: str @@ -166,8 +177,8 @@ class DissolveProtocol(BaseModel): vessel: str solvent: str volume: float - amount: str = "" - temp: float = 25.0 + amount: str = "" + temp: float = 25.0 time: float = 0.0 stir_speed: float = 0.0 @@ -191,8 +202,8 @@ class WashSolidProtocol(BaseModel): volume: float filtrate_vessel: str = "" temp: float = 25.0 - stir: bool = False - stir_speed: float = 0.0 + stir: bool = False + stir_speed: float = 0.0 处理完了 time: float = 0.0 repeats: int = 1 diff --git a/unilabos/compile/evacuateandrefill_protocol.py b/unilabos/compile/evacuateandrefill_protocol.py index aa44df6..e5e8df5 100644 --- a/unilabos/compile/evacuateandrefill_protocol.py +++ b/unilabos/compile/evacuateandrefill_protocol.py @@ -197,77 +197,148 @@ def generate_evacuateandrefill_protocol( G: nx.DiGraph, vessel: str, gas: str, - repeats: int = 1 + # 🔧 删除 repeats 参数,直接硬编码为 3 + **kwargs # 🔧 接受额外参数,增强兼容性 ) -> List[Dict[str, Any]]: """ - 生成抽真空和充气操作的动作序列 + 生成抽真空和充气操作的动作序列 - 简化版本 - **修复版本**: 正确调用 pump_protocol 并处理异常 + Args: + G: 设备图 + vessel: 目标容器名称(必需) + gas: 气体名称(必需) + **kwargs: 其他参数(兼容性) + + Returns: + List[Dict[str, Any]]: 动作序列 """ + + # 🔧 硬编码重复次数为 3 + repeats = 3 + + 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("=" * 60) + action_sequence = [] - # 参数设置 - 关键修复:减小体积避免超出泵容量 - VACUUM_VOLUME = 20.0 # 减小抽真空体积 - REFILL_VOLUME = 20.0 # 减小充气体积 - PUMP_FLOW_RATE = 2.5 # 降低流速 - STIR_SPEED = 300.0 + # === 参数验证和修正 === + debug_print("步骤1: 参数验证和修正...") - print(f"EVACUATE_REFILL: 开始生成协议,目标容器: {vessel}, 气体: {gas}, 重复次数: {repeats}") + # 验证必需参数 + if not vessel: + raise ValueError("vessel 参数不能为空") + + if not gas: + raise ValueError("gas 参数不能为空") - # 1. 验证设备存在 if vessel not in G.nodes(): - raise ValueError(f"目标容器 '{vessel}' 不存在于系统中") + raise ValueError(f"容器 '{vessel}' 不存在于系统中") + + # 标准化气体名称 + gas_aliases = { + 'n2': 'nitrogen', + 'ar': 'argon', + 'air': 'air', + 'o2': 'oxygen', + 'co2': 'carbon_dioxide', + 'h2': '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"最终参数: vessel={vessel}, gas={gas}, repeats={repeats}") + + # === 查找设备 === + debug_print("步骤2: 查找设备...") - # 2. 查找设备 try: vacuum_pump = find_vacuum_pump(G) - vacuum_solenoid = find_associated_solenoid_valve(G, vacuum_pump) gas_source = find_gas_source(G, gas) + vacuum_solenoid = find_associated_solenoid_valve(G, vacuum_pump) gas_solenoid = find_associated_solenoid_valve(G, gas_source) stirrer_id = find_connected_stirrer(G, vessel) - print(f"EVACUATE_REFILL: 找到设备") - print(f" - 真空泵: {vacuum_pump}") - print(f" - 气源: {gas_source}") - print(f" - 真空电磁阀: {vacuum_solenoid}") - print(f" - 气源电磁阀: {gas_solenoid}") - print(f" - 搅拌器: {stirrer_id}") + 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 ValueError as e: + except Exception as e: + debug_print(f"❌ 设备查找失败: {str(e)}") raise ValueError(f"设备查找失败: {str(e)}") - # 3. **关键修复**: 验证路径存在性 + # === 参数设置 === + debug_print("步骤3: 参数设置...") + + # 根据气体类型调整参数 + if gas.lower() in ['nitrogen', 'argon']: + VACUUM_VOLUME = 25.0 + REFILL_VOLUME = 25.0 + PUMP_FLOW_RATE = 2.0 + VACUUM_TIME = 30.0 + REFILL_TIME = 20.0 + debug_print("惰性气体:使用标准参数") + 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("活性气体:使用保守参数") + else: + VACUUM_VOLUME = 15.0 + REFILL_VOLUME = 15.0 + PUMP_FLOW_RATE = 1.0 + VACUUM_TIME = 60.0 + REFILL_TIME = 30.0 + debug_print("未知气体:使用安全参数") + + 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("步骤4: 路径验证...") + try: # 验证抽真空路径 vacuum_path = nx.shortest_path(G, source=vessel, target=vacuum_pump) - print(f"EVACUATE_REFILL: 抽真空路径: {' → '.join(vacuum_path)}") + debug_print(f"抽真空路径: {' → '.join(vacuum_path)}") # 验证充气路径 gas_path = nx.shortest_path(G, source=gas_source, target=vessel) - print(f"EVACUATE_REFILL: 充气路径: {' → '.join(gas_path)}") + debug_print(f"充气路径: {' → '.join(gas_path)}") - # **新增**: 检查路径中的边数据 - for i in range(len(vacuum_path) - 1): - nodeA, nodeB = vacuum_path[i], vacuum_path[i + 1] - edge_data = G.get_edge_data(nodeA, nodeB) - if not edge_data or 'port' not in edge_data: - raise ValueError(f"路径 {nodeA} → {nodeB} 缺少端口信息") - print(f" 抽真空路径边 {nodeA} → {nodeB}: {edge_data}") - - for i in range(len(gas_path) - 1): - nodeA, nodeB = gas_path[i], gas_path[i + 1] - edge_data = G.get_edge_data(nodeA, nodeB) - if not edge_data or 'port' not in edge_data: - raise ValueError(f"路径 {nodeA} → {nodeB} 缺少端口信息") - print(f" 充气路径边 {nodeA} → {nodeB}: {edge_data}") - except nx.NetworkXNoPath as e: + debug_print(f"❌ 路径不存在: {str(e)}") raise ValueError(f"路径不存在: {str(e)}") except Exception as e: + debug_print(f"❌ 路径验证失败: {str(e)}") raise ValueError(f"路径验证失败: {str(e)}") - # 4. 启动搅拌器 + # === 启动搅拌器 === + debug_print("步骤5: 启动搅拌器...") + if stirrer_id: + debug_print(f"启动搅拌器: {stirrer_id}") action_sequence.append({ "device_id": stirrer_id, "action_name": "start_stir", @@ -277,15 +348,26 @@ def generate_evacuateandrefill_protocol( "purpose": "抽真空充气操作前启动搅拌" } }) + + # 等待搅拌稳定 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 5.0} + }) + else: + debug_print("未找到搅拌器,跳过搅拌启动") - # 5. 执行多次抽真空-充气循环 - for cycle in range(repeats): - print(f"EVACUATE_REFILL: === 第 {cycle+1}/{repeats} 次循环 ===") + # === 执行 3 次抽真空-充气循环 === + debug_print("步骤6: 执行抽真空-充气循环...") + + for cycle in range(repeats): # 这里 repeats = 3 + debug_print(f"=== 第 {cycle+1}/{repeats} 次循环 ===") # ============ 抽真空阶段 ============ - print(f"EVACUATE_REFILL: 抽真空阶段开始") + debug_print(f"抽真空阶段开始") # 启动真空泵 + debug_print(f"启动真空泵: {vacuum_pump}") action_sequence.append({ "device_id": vacuum_pump, "action_name": "set_status", @@ -294,17 +376,15 @@ def generate_evacuateandrefill_protocol( # 开启真空电磁阀 if vacuum_solenoid: + debug_print(f"开启真空电磁阀: {vacuum_solenoid}") action_sequence.append({ "device_id": vacuum_solenoid, "action_name": "set_valve_position", "action_kwargs": {"command": "OPEN"} }) - # **关键修复**: 改进 pump_protocol 调用和错误处理 - print(f"EVACUATE_REFILL: 调用抽真空 pump_protocol: {vessel} → {vacuum_pump}") - print(f" - 体积: {VACUUM_VOLUME} mL") - print(f" - 流速: {PUMP_FLOW_RATE} mL/s") - + # 抽真空操作 + debug_print(f"抽真空操作: {vessel} → {vacuum_pump}") try: vacuum_transfer_actions = generate_pump_protocol_with_rinsing( G=G, @@ -312,9 +392,9 @@ def generate_evacuateandrefill_protocol( to_vessel=vacuum_pump, volume=VACUUM_VOLUME, amount="", - time=0.0, + duration=0.0, # 🔧 修复time参数名冲突 viscous=False, - rinsing_solvent="", # **修复**: 明确不使用清洗 + rinsing_solvent="", rinsing_volume=0.0, rinsing_repeats=0, solid=False, @@ -324,52 +404,31 @@ def generate_evacuateandrefill_protocol( if vacuum_transfer_actions: action_sequence.extend(vacuum_transfer_actions) - print(f"EVACUATE_REFILL: ✅ 成功添加 {len(vacuum_transfer_actions)} 个抽真空动作") + debug_print(f"✅ 添加了 {len(vacuum_transfer_actions)} 个抽真空动作") else: - print(f"EVACUATE_REFILL: ⚠️ 抽真空 pump_protocol 返回空序列") - # **修复**: 添加手动泵动作作为备选 - action_sequence.extend([ - { - "device_id": "multiway_valve_1", - "action_name": "set_valve_position", - "action_kwargs": {"command": "5"} # 连接到反应器 - }, - { - "device_id": "transfer_pump_1", - "action_name": "set_position", - "action_kwargs": { - "position": VACUUM_VOLUME, - "max_velocity": PUMP_FLOW_RATE - } - } - ]) - print(f"EVACUATE_REFILL: 使用备选手动泵动作") + debug_print("⚠️ 抽真空协议返回空序列,添加手动动作") + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": VACUUM_TIME} + }) except Exception as e: - print(f"EVACUATE_REFILL: ❌ 抽真空 pump_protocol 失败: {str(e)}") - import traceback - print(f"EVACUATE_REFILL: 详细错误:\n{traceback.format_exc()}") - - # **修复**: 添加手动动作而不是忽略错误 - print(f"EVACUATE_REFILL: 使用手动备选方案") - action_sequence.extend([ - { - "device_id": "multiway_valve_1", - "action_name": "set_valve_position", - "action_kwargs": {"command": "5"} # 反应器端口 - }, - { - "device_id": "transfer_pump_1", - "action_name": "set_position", - "action_kwargs": { - "position": VACUUM_VOLUME, - "max_velocity": PUMP_FLOW_RATE - } - } - ]) + debug_print(f"❌ 抽真空失败: {str(e)}") + # 添加等待时间作为备选 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": VACUUM_TIME} + }) + + # 抽真空后等待 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 5.0} + }) # 关闭真空电磁阀 if vacuum_solenoid: + debug_print(f"关闭真空电磁阀: {vacuum_solenoid}") action_sequence.append({ "device_id": vacuum_solenoid, "action_name": "set_valve_position", @@ -377,6 +436,7 @@ def generate_evacuateandrefill_protocol( }) # 关闭真空泵 + debug_print(f"关闭真空泵: {vacuum_pump}") action_sequence.append({ "device_id": vacuum_pump, "action_name": "set_status", @@ -384,26 +444,27 @@ def generate_evacuateandrefill_protocol( }) # ============ 充气阶段 ============ - print(f"EVACUATE_REFILL: 充气阶段开始") + debug_print(f"充气阶段开始") # 启动气源 + debug_print(f"启动气源: {gas_source}") action_sequence.append({ "device_id": gas_source, - "action_name": "set_status", + "action_name": "set_status", "action_kwargs": {"string": "ON"} }) # 开启气源电磁阀 if gas_solenoid: + debug_print(f"开启气源电磁阀: {gas_solenoid}") action_sequence.append({ "device_id": gas_solenoid, "action_name": "set_valve_position", "action_kwargs": {"command": "OPEN"} }) - # **关键修复**: 改进充气 pump_protocol 调用 - print(f"EVACUATE_REFILL: 调用充气 pump_protocol: {gas_source} → {vessel}") - + # 充气操作 + debug_print(f"充气操作: {gas_source} → {vessel}") try: gas_transfer_actions = generate_pump_protocol_with_rinsing( G=G, @@ -411,9 +472,9 @@ def generate_evacuateandrefill_protocol( to_vessel=vessel, volume=REFILL_VOLUME, amount="", - time=0.0, + duration=0.0, # 🔧 修复time参数名冲突 viscous=False, - rinsing_solvent="", # **修复**: 明确不使用清洗 + rinsing_solvent="", rinsing_volume=0.0, rinsing_repeats=0, solid=False, @@ -423,77 +484,31 @@ def generate_evacuateandrefill_protocol( if gas_transfer_actions: action_sequence.extend(gas_transfer_actions) - print(f"EVACUATE_REFILL: ✅ 成功添加 {len(gas_transfer_actions)} 个充气动作") + debug_print(f"✅ 添加了 {len(gas_transfer_actions)} 个充气动作") else: - print(f"EVACUATE_REFILL: ⚠️ 充气 pump_protocol 返回空序列") - # **修复**: 添加手动充气动作 - action_sequence.extend([ - { - "device_id": "multiway_valve_2", - "action_name": "set_valve_position", - "action_kwargs": {"command": "8"} # 氮气端口 - }, - { - "device_id": "transfer_pump_2", - "action_name": "set_position", - "action_kwargs": { - "position": REFILL_VOLUME, - "max_velocity": PUMP_FLOW_RATE - } - }, - { - "device_id": "multiway_valve_2", - "action_name": "set_valve_position", - "action_kwargs": {"command": "5"} # 反应器端口 - }, - { - "device_id": "transfer_pump_2", - "action_name": "set_position", - "action_kwargs": { - "position": 0.0, - "max_velocity": PUMP_FLOW_RATE - } - } - ]) + debug_print("⚠️ 充气协议返回空序列,添加手动动作") + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": REFILL_TIME} + }) except Exception as e: - print(f"EVACUATE_REFILL: ❌ 充气 pump_protocol 失败: {str(e)}") - import traceback - print(f"EVACUATE_REFILL: 详细错误:\n{traceback.format_exc()}") - - # **修复**: 使用手动充气动作 - print(f"EVACUATE_REFILL: 使用手动充气方案") - action_sequence.extend([ - { - "device_id": "multiway_valve_2", - "action_name": "set_valve_position", - "action_kwargs": {"command": "8"} # 连接气源 - }, - { - "device_id": "transfer_pump_2", - "action_name": "set_position", - "action_kwargs": { - "position": REFILL_VOLUME, - "max_velocity": PUMP_FLOW_RATE - } - }, - { - "device_id": "multiway_valve_2", - "action_name": "set_valve_position", - "action_kwargs": {"command": "5"} # 连接反应器 - }, - { - "device_id": "transfer_pump_2", - "action_name": "set_position", - "action_kwargs": { - "position": 0.0, - "max_velocity": PUMP_FLOW_RATE - } - } - ]) + debug_print(f"❌ 充气失败: {str(e)}") + # 添加等待时间作为备选 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": REFILL_TIME} + }) + + # 充气后等待 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 5.0} + }) # 关闭气源电磁阀 if gas_solenoid: + debug_print(f"关闭气源电磁阀: {gas_solenoid}") action_sequence.append({ "device_id": gas_solenoid, "action_name": "set_valve_position", @@ -501,6 +516,7 @@ def generate_evacuateandrefill_protocol( }) # 关闭气源 + debug_print(f"关闭气源: {gas_source}") action_sequence.append({ "device_id": gas_source, "action_name": "set_status", @@ -509,23 +525,40 @@ def generate_evacuateandrefill_protocol( # 等待下一次循环 if cycle < repeats - 1: + debug_print(f"等待下一次循环...") action_sequence.append({ "action_name": "wait", - "action_kwargs": {"time": 2.0} + "action_kwargs": {"time": 10.0} }) - # 停止搅拌器 + # === 停止搅拌器 === + debug_print("步骤7: 停止搅拌器...") + if stirrer_id: + debug_print(f"停止搅拌器: {stirrer_id}") action_sequence.append({ "device_id": stirrer_id, "action_name": "stop_stir", "action_kwargs": {"vessel": vessel} }) - print(f"EVACUATE_REFILL: 协议生成完成,共 {len(action_sequence)} 个动作") + # === 最终等待 === + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 10.0} + }) + + # === 总结 === + 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("=" * 60) + return action_sequence - # 测试函数 def test_evacuateandrefill_protocol(): """测试抽真空充气协议""" diff --git a/unilabos/compile/evaporate_protocol.py b/unilabos/compile/evaporate_protocol.py index 4cee78d..b5170b5 100644 --- a/unilabos/compile/evaporate_protocol.py +++ b/unilabos/compile/evaporate_protocol.py @@ -1,61 +1,110 @@ -from typing import List, Dict, Any +from typing import List, Dict, Any, Optional import networkx as nx +import logging from .pump_protocol import generate_pump_protocol +logger = logging.getLogger(__name__) + +def debug_print(message): + """调试输出""" + print(f"[EVAPORATE] {message}", flush=True) + logger.info(f"[EVAPORATE] {message}") def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float: - """ - 获取容器中的液体体积 - """ + """获取容器中的液体体积""" + debug_print(f"检查容器 '{vessel}' 的液体体积...") + if vessel not in G.nodes(): + debug_print(f"容器 '{vessel}' 不存在") return 0.0 vessel_data = G.nodes[vessel].get('data', {}) + debug_print(f"容器数据: {vessel_data}") + + # 检查多种体积字段 + volume_keys = ['total_volume', 'volume', 'liquid_volume', 'current_volume'] + for key in volume_keys: + if key in vessel_data: + try: + volume = float(vessel_data[key]) + debug_print(f"从 '{key}' 读取到体积: {volume}mL") + return volume + except (ValueError, TypeError): + continue + + # 检查liquid数组 liquids = vessel_data.get('liquid', []) + if isinstance(liquids, list): + total_volume = 0.0 + for liquid in liquids: + if isinstance(liquid, dict): + for vol_key in ['liquid_volume', 'volume', 'amount']: + if vol_key in liquid: + try: + vol = float(liquid[vol_key]) + total_volume += vol + debug_print(f"从液体数据 '{vol_key}' 读取: {vol}mL") + except (ValueError, TypeError): + continue + if total_volume > 0: + return total_volume - total_volume = 0.0 - for liquid in liquids: - if isinstance(liquid, dict) and 'liquid_volume' in liquid: - total_volume += liquid['liquid_volume'] - - return total_volume + debug_print(f"未检测到液体体积,返回 0.0") + return 0.0 - -def find_rotavap_device(G: nx.DiGraph) -> str: +def find_rotavap_device(G: nx.DiGraph) -> Optional[str]: """查找旋转蒸发仪设备""" - rotavap_nodes = [node for node in G.nodes() - if (G.nodes[node].get('class') or '') == 'virtual_rotavap'] + debug_print("查找旋转蒸发仪设备...") - if rotavap_nodes: - return rotavap_nodes[0] + # 查找各种可能的旋转蒸发仪设备 + possible_devices = [] + for node in G.nodes(): + node_data = G.nodes[node] + node_class = node_data.get('class', '') + + if any(keyword in node_class.lower() for keyword in ['rotavap', 'evaporator']): + possible_devices.append(node) + debug_print(f"找到旋转蒸发仪设备: {node}") - raise ValueError("系统中未找到旋转蒸发仪设备") + if possible_devices: + return possible_devices[0] + + debug_print("未找到旋转蒸发仪设备") + return None - -def find_solvent_recovery_vessel(G: nx.DiGraph) -> str: - """查找溶剂回收容器""" - possible_names = [ - "flask_distillate", - "bottle_distillate", - "vessel_distillate", - "distillate", - "solvent_recovery", - "flask_solvent_recovery", - "collection_flask" +def find_rotavap_vessel(G: nx.DiGraph) -> Optional[str]: + """查找旋转蒸发仪样品容器""" + debug_print("查找旋转蒸发仪样品容器...") + + possible_vessels = [ + "rotavap", "rotavap_flask", "flask_rotavap", + "evaporation_flask", "evaporator", "rotary_evaporator" ] - for vessel_name in possible_names: - if vessel_name in G.nodes(): - return vessel_name + for vessel in possible_vessels: + if vessel in G.nodes(): + debug_print(f"找到旋转蒸发仪样品容器: {vessel}") + return vessel - # 如果找不到专门的回收容器,使用废液容器 - waste_names = ["waste_workup", "flask_waste", "bottle_waste", "waste"] - for vessel_name in waste_names: - if vessel_name in G.nodes(): - return vessel_name - - raise ValueError(f"未找到溶剂回收容器。尝试了以下名称: {possible_names + waste_names}") + debug_print("未找到旋转蒸发仪样品容器") + return None +def find_recovery_vessel(G: nx.DiGraph) -> Optional[str]: + """查找溶剂回收容器""" + debug_print("查找溶剂回收容器...") + + possible_vessels = [ + "flask_distillate", "distillate", "solvent_recovery", + "rotavap_condenser", "condenser", "waste_workup", "waste" + ] + + for vessel in possible_vessels: + if vessel in G.nodes(): + debug_print(f"找到回收容器: {vessel}") + return vessel + + debug_print("未找到回收容器") + return None def generate_evaporate_protocol( G: nx.DiGraph, @@ -63,264 +112,276 @@ def generate_evaporate_protocol( pressure: float = 0.1, temp: float = 60.0, time: float = 1800.0, - stir_speed: float = 100.0 + stir_speed: float = 100.0, + solvent: str = "", + **kwargs # 接受任意额外参数,增强兼容性 ) -> List[Dict[str, Any]]: """ - 生成蒸发操作的协议序列 - - 蒸发流程: - 1. 液体转移:将待蒸发溶液从源容器转移到旋转蒸发仪 - 2. 蒸发操作:执行旋转蒸发 - 3. (可选) 溶剂回收:将冷凝的溶剂转移到回收容器 - 4. 残留物转移:将浓缩物从旋转蒸发仪转移回原容器或新容器 + 生成蒸发操作的协议序列 - 增强兼容性版本 Args: - G: 有向图,节点为设备和容器,边为流体管道 - vessel: 包含待蒸发溶液的容器名称 - pressure: 蒸发时的真空度 (bar),默认0.1 bar - temp: 蒸发时的加热温度 (°C),默认60°C - time: 蒸发时间 (秒),默认1800秒(30分钟) - stir_speed: 旋转速度 (RPM),默认100 RPM + G: 设备图 + vessel: 蒸发容器名称(必需) + pressure: 真空度 (bar),默认0.1 + temp: 加热温度 (°C),默认60 + time: 蒸发时间 (秒),默认1800 + stir_speed: 旋转速度 (RPM),默认100 + solvent: 溶剂名称(可选,用于参数优化) + **kwargs: 其他参数(兼容性) Returns: - List[Dict[str, Any]]: 蒸发操作的动作序列 - - Raises: - ValueError: 当找不到必要的设备时抛出异常 - - Examples: - evaporate_actions = generate_evaporate_protocol(G, "reaction_mixture", 0.05, 80.0, 3600.0) + List[Dict[str, Any]]: 动作序列 """ + + debug_print("=" * 50) + debug_print("开始生成蒸发协议") + debug_print(f"输入参数:") + debug_print(f" - vessel: {vessel}") + debug_print(f" - pressure: {pressure} bar") + debug_print(f" - temp: {temp}°C") + debug_print(f" - time: {time}s ({time/60:.1f}分钟)") + debug_print(f" - stir_speed: {stir_speed} RPM") + debug_print(f" - solvent: '{solvent}'") + debug_print(f" - 其他参数: {kwargs}") + debug_print("=" * 50) + action_sequence = [] - print(f"EVAPORATE: 开始生成蒸发协议") - print(f" - 源容器: {vessel}") - print(f" - 真空度: {pressure} bar") - print(f" - 温度: {temp}°C") - print(f" - 时间: {time}s ({time/60:.1f}分钟)") - print(f" - 旋转速度: {stir_speed} RPM") + # === 参数验证和修正 === + debug_print("步骤1: 参数验证和修正...") + + # 验证必需参数 + if not vessel: + raise ValueError("vessel 参数不能为空") - # 验证源容器存在 if vessel not in G.nodes(): - raise ValueError(f"源容器 '{vessel}' 不存在于系统中") + raise ValueError(f"容器 '{vessel}' 不存在于系统中") - # 获取源容器中的液体体积 - source_volume = get_vessel_liquid_volume(G, vessel) - print(f"EVAPORATE: 源容器 {vessel} 中有 {source_volume} mL 液体") + # 修正参数范围 + if pressure <= 0 or pressure > 1.0: + debug_print(f"真空度 {pressure} bar 超出范围,修正为 0.1 bar") + pressure = 0.1 - # 查找旋转蒸发仪 - try: - rotavap_id = find_rotavap_device(G) - print(f"EVAPORATE: 找到旋转蒸发仪: {rotavap_id}") - except ValueError as e: - raise ValueError(f"无法找到旋转蒸发仪: {str(e)}") + if temp < 10.0 or temp > 200.0: + debug_print(f"温度 {temp}°C 超出范围,修正为 60°C") + temp = 60.0 + + if time <= 0: + debug_print(f"时间 {time}s 无效,修正为 1800s") + time = 1800.0 + + if stir_speed < 10.0 or stir_speed > 300.0: + debug_print(f"旋转速度 {stir_speed} RPM 超出范围,修正为 100 RPM") + stir_speed = 100.0 + + # 根据溶剂优化参数 + if solvent: + debug_print(f"根据溶剂 '{solvent}' 优化参数...") + solvent_lower = solvent.lower() + + if any(s in solvent_lower for s in ['water', 'aqueous', 'h2o']): + temp = max(temp, 80.0) + pressure = max(pressure, 0.2) + debug_print("水系溶剂:提高温度和真空度") + elif any(s in solvent_lower for s in ['ethanol', 'methanol', 'acetone']): + temp = min(temp, 50.0) + pressure = min(pressure, 0.05) + debug_print("易挥发溶剂:降低温度和真空度") + elif any(s in solvent_lower for s in ['dmso', 'dmi', 'toluene']): + temp = max(temp, 100.0) + pressure = min(pressure, 0.01) + debug_print("高沸点溶剂:提高温度,降低真空度") + + debug_print(f"最终参数: pressure={pressure}, temp={temp}, time={time}, stir_speed={stir_speed}") + + # === 查找设备 === + debug_print("步骤2: 查找设备...") + + # 查找旋转蒸发仪设备 + rotavap_device = find_rotavap_device(G) + if not rotavap_device: + debug_print("未找到旋转蒸发仪设备,使用通用设备") + rotavap_device = "rotavap_1" # 默认设备ID # 查找旋转蒸发仪样品容器 - rotavap_vessel = None - possible_rotavap_vessels = ["rotavap_flask", "rotavap", "flask_rotavap", "evaporation_flask"] - for rv in possible_rotavap_vessels: - if rv in G.nodes(): - rotavap_vessel = rv - break - + rotavap_vessel = find_rotavap_vessel(G) if not rotavap_vessel: - raise ValueError(f"未找到旋转蒸发仪样品容器。尝试了: {possible_rotavap_vessels}") + debug_print("未找到旋转蒸发仪样品容器,使用默认容器") + rotavap_vessel = "rotavap" # 默认容器 - print(f"EVAPORATE: 找到旋转蒸发仪样品容器: {rotavap_vessel}") + # 查找回收容器 + recovery_vessel = find_recovery_vessel(G) - # 查找溶剂回收容器 - try: - distillate_vessel = find_solvent_recovery_vessel(G) - print(f"EVAPORATE: 找到溶剂回收容器: {distillate_vessel}") - except ValueError as e: - print(f"EVAPORATE: 警告 - {str(e)}") - distillate_vessel = None + debug_print(f"设备配置:") + debug_print(f" - 旋转蒸发仪设备: {rotavap_device}") + debug_print(f" - 样品容器: {rotavap_vessel}") + debug_print(f" - 回收容器: {recovery_vessel}") + + # === 体积计算 === + debug_print("步骤3: 体积计算...") + + source_volume = get_vessel_liquid_volume(G, vessel) - # === 简化的体积计算策略 === if source_volume > 0: - # 如果能检测到液体体积,使用实际体积的大部分 transfer_volume = min(source_volume * 0.9, 250.0) # 90%或最多250mL - print(f"EVAPORATE: 检测到液体体积,将转移 {transfer_volume} mL") + debug_print(f"检测到液体体积 {source_volume}mL,转移 {transfer_volume}mL") else: - # 如果检测不到液体体积,默认转移一整瓶 250mL - transfer_volume = 250.0 - print(f"EVAPORATE: 未检测到液体体积,默认转移整瓶 {transfer_volume} mL") + transfer_volume = 50.0 # 默认小体积,更安全 + debug_print(f"未检测到液体体积,使用默认转移体积 {transfer_volume}mL") - # === 第一步:将待蒸发溶液转移到旋转蒸发仪 === - print(f"EVAPORATE: 将 {transfer_volume} mL 溶液从 {vessel} 转移到 {rotavap_vessel}") - try: - transfer_to_rotavap_actions = generate_pump_protocol( - G=G, - from_vessel=vessel, - to_vessel=rotavap_vessel, - volume=transfer_volume, - flowrate=2.0, - transfer_flowrate=2.0 - ) - action_sequence.extend(transfer_to_rotavap_actions) - except Exception as e: - raise ValueError(f"无法将溶液转移到旋转蒸发仪: {str(e)}") + # === 生成动作序列 === + debug_print("步骤4: 生成动作序列...") - # 转移后等待 - wait_action = { + # 动作1: 转移溶液到旋转蒸发仪 + if vessel != rotavap_vessel: + debug_print(f"转移 {transfer_volume}mL 从 {vessel} 到 {rotavap_vessel}") + try: + transfer_actions = generate_pump_protocol( + G=G, + from_vessel=vessel, + to_vessel=rotavap_vessel, + volume=transfer_volume, + flowrate=2.0, + transfer_flowrate=2.0 + ) + action_sequence.extend(transfer_actions) + debug_print(f"添加了 {len(transfer_actions)} 个转移动作") + except Exception as e: + debug_print(f"转移失败: {str(e)}") + # 继续执行,不中断整个流程 + + # 等待稳定 + action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 10} - } - action_sequence.append(wait_action) + }) - # === 第二步:执行旋转蒸发 === - print(f"EVAPORATE: 执行旋转蒸发操作") + # 动作2: 执行蒸发 + debug_print(f"执行蒸发: {rotavap_device}") evaporate_action = { - "device_id": rotavap_id, + "device_id": rotavap_device, "action_name": "evaporate", "action_kwargs": { "vessel": rotavap_vessel, "pressure": pressure, "temp": temp, "time": time, - "stir_speed": stir_speed + "stir_speed": stir_speed, + "solvent": solvent } } action_sequence.append(evaporate_action) - # 蒸发后等待系统稳定 - wait_action = { + # 蒸发后等待 + action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 30} - } - action_sequence.append(wait_action) + }) - # === 第三步:溶剂回收(如果有回收容器)=== - if distillate_vessel: - print(f"EVAPORATE: 回收冷凝溶剂到 {distillate_vessel}") + # 动作3: 回收溶剂(如果有回收容器) + if recovery_vessel: + debug_print(f"回收溶剂到 {recovery_vessel}") try: - condenser_vessel = "rotavap_condenser" - if condenser_vessel in G.nodes(): - # 估算回收体积(约为转移体积的70% - 大部分溶剂被蒸发回收) - recovery_volume = transfer_volume * 0.7 - print(f"EVAPORATE: 预计回收 {recovery_volume} mL 溶剂") - - recovery_actions = generate_pump_protocol( - G=G, - from_vessel=condenser_vessel, - to_vessel=distillate_vessel, - volume=recovery_volume, - flowrate=3.0, - transfer_flowrate=3.0 - ) - action_sequence.extend(recovery_actions) - else: - print("EVAPORATE: 未找到冷凝器容器,跳过溶剂回收") - except Exception as e: - print(f"EVAPORATE: 溶剂回收失败: {str(e)}") - - # === 第四步:将浓缩物转移回原容器 === - print(f"EVAPORATE: 将浓缩物从旋转蒸发仪转移回 {vessel}") - try: - # 估算浓缩物体积(约为转移体积的20% - 大部分溶剂已蒸发) - concentrate_volume = transfer_volume * 0.2 - print(f"EVAPORATE: 预计浓缩物体积 {concentrate_volume} mL") - - transfer_back_actions = generate_pump_protocol( - G=G, - from_vessel=rotavap_vessel, - to_vessel=vessel, - volume=concentrate_volume, - flowrate=1.0, # 浓缩物可能粘稠,用较慢流速 - transfer_flowrate=1.0 - ) - action_sequence.extend(transfer_back_actions) - except Exception as e: - print(f"EVAPORATE: 将浓缩物转移回容器失败: {str(e)}") - - # === 第五步:清洗旋转蒸发仪 === - print(f"EVAPORATE: 清洗旋转蒸发仪") - try: - # 查找清洗溶剂 - cleaning_solvent = None - for solvent in ["flask_ethanol", "flask_acetone", "flask_water"]: - if solvent in G.nodes(): - cleaning_solvent = solvent - break - - if cleaning_solvent and distillate_vessel: - # 用固定量溶剂清洗(不依赖检测体积) - cleaning_volume = 50.0 # 固定50mL清洗 - print(f"EVAPORATE: 用 {cleaning_volume} mL {cleaning_solvent} 清洗") - - # 清洗溶剂加入 - cleaning_actions = generate_pump_protocol( + recovery_volume = transfer_volume * 0.7 # 估算回收70% + recovery_actions = generate_pump_protocol( G=G, - from_vessel=cleaning_solvent, - to_vessel=rotavap_vessel, - volume=cleaning_volume, - flowrate=2.0, - transfer_flowrate=2.0 + from_vessel="rotavap_condenser", # 假设的冷凝器 + to_vessel=recovery_vessel, + volume=recovery_volume, + flowrate=3.0, + transfer_flowrate=3.0 ) - action_sequence.extend(cleaning_actions) - - # 将清洗液转移到废液/回收容器 - waste_actions = generate_pump_protocol( + action_sequence.extend(recovery_actions) + debug_print(f"添加了 {len(recovery_actions)} 个回收动作") + except Exception as e: + debug_print(f"溶剂回收失败: {str(e)}") + + # 动作4: 转移浓缩物回原容器 + if vessel != rotavap_vessel: + debug_print(f"转移浓缩物从 {rotavap_vessel} 到 {vessel}") + try: + concentrate_volume = transfer_volume * 0.2 # 估算浓缩物20% + transfer_back_actions = generate_pump_protocol( G=G, from_vessel=rotavap_vessel, - to_vessel=distillate_vessel, # 使用回收容器作为废液 - volume=cleaning_volume, - flowrate=2.0, - transfer_flowrate=2.0 + to_vessel=vessel, + volume=concentrate_volume, + flowrate=1.0, # 浓缩物可能粘稠 + transfer_flowrate=1.0 ) - action_sequence.extend(waste_actions) - - except Exception as e: - print(f"EVAPORATE: 清洗步骤失败: {str(e)}") + action_sequence.extend(transfer_back_actions) + debug_print(f"添加了 {len(transfer_back_actions)} 个转移回收动作") + except Exception as e: + debug_print(f"浓缩物转移失败: {str(e)}") - print(f"EVAPORATE: 生成了 {len(action_sequence)} 个动作") - print(f"EVAPORATE: 蒸发协议生成完成") - print(f"EVAPORATE: 总处理体积: {transfer_volume} mL") + # === 总结 === + debug_print("=" * 50) + debug_print(f"蒸发协议生成完成") + debug_print(f"总动作数: {len(action_sequence)}") + debug_print(f"处理体积: {transfer_volume}mL") + debug_print(f"蒸发参数: {pressure} bar, {temp}°C, {time}s, {stir_speed} RPM") + debug_print("=" * 50) return action_sequence +# === 便捷函数 === -# 便捷函数:常用蒸发方案 - 都使用250mL标准瓶体积 def generate_quick_evaporate_protocol( G: nx.DiGraph, vessel: str, - temp: float = 40.0, - time: float = 900.0 # 15分钟 + **kwargs ) -> List[Dict[str, Any]]: - """快速蒸发:低温、短时间、整瓶处理""" - return generate_evaporate_protocol(G, vessel, 0.2, temp, time, 80.0) - + """快速蒸发:低温短时间""" + return generate_evaporate_protocol( + G, vessel, + pressure=0.2, + temp=40.0, + time=900.0, + stir_speed=80.0, + **kwargs + ) def generate_gentle_evaporate_protocol( G: nx.DiGraph, vessel: str, - temp: float = 50.0, - time: float = 2700.0 # 45分钟 + **kwargs ) -> List[Dict[str, Any]]: - """温和蒸发:中等条件、较长时间、整瓶处理""" - return generate_evaporate_protocol(G, vessel, 0.1, temp, time, 60.0) - + """温和蒸发:中等条件""" + return generate_evaporate_protocol( + G, vessel, + pressure=0.1, + temp=50.0, + time=2700.0, + stir_speed=60.0, + **kwargs + ) def generate_high_vacuum_evaporate_protocol( G: nx.DiGraph, vessel: str, - temp: float = 35.0, - time: float = 3600.0 # 1小时 + **kwargs ) -> List[Dict[str, Any]]: - """高真空蒸发:低温、高真空、长时间、整瓶处理""" - return generate_evaporate_protocol(G, vessel, 0.01, temp, time, 120.0) - + """高真空蒸发:低温高真空""" + return generate_evaporate_protocol( + G, vessel, + pressure=0.01, + temp=35.0, + time=3600.0, + stir_speed=120.0, + **kwargs + ) def generate_standard_evaporate_protocol( G: nx.DiGraph, - vessel: str + vessel: str, + **kwargs ) -> List[Dict[str, Any]]: - """标准蒸发:常用参数、整瓶250mL处理""" + """标准蒸发:常用参数""" return generate_evaporate_protocol( - G=G, - vessel=vessel, - pressure=0.1, # 标准真空度 - temp=60.0, # 适中温度 - time=1800.0, # 30分钟 - stir_speed=100.0 # 适中旋转速度 + G, vessel, + pressure=0.1, + temp=60.0, + time=1800.0, + stir_speed=100.0, + **kwargs ) diff --git a/unilabos/compile/filter_protocol.py b/unilabos/compile/filter_protocol.py index 7e3ca6b..e673f22 100644 --- a/unilabos/compile/filter_protocol.py +++ b/unilabos/compile/filter_protocol.py @@ -1,239 +1,193 @@ from typing import List, Dict, Any import networkx as nx from .pump_protocol import generate_pump_protocol +import logging +import sys +logger = logging.getLogger(__name__) + +def debug_print(message): + """调试输出""" + print(f"[FILTER] {message}", flush=True) + logger.info(f"[FILTER] {message}") def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float: """获取容器中的液体体积""" + debug_print(f"检查容器 '{vessel}' 的液体体积...") + if vessel not in G.nodes(): + debug_print(f"容器 '{vessel}' 不存在") return 0.0 vessel_data = G.nodes[vessel].get('data', {}) + + # 检查多种体积字段 + volume_keys = ['total_volume', 'volume', 'liquid_volume', 'current_volume'] + for key in volume_keys: + if key in vessel_data: + try: + volume = float(vessel_data[key]) + debug_print(f"从 '{key}' 读取到体积: {volume}mL") + return volume + except (ValueError, TypeError): + continue + + # 检查liquid数组 liquids = vessel_data.get('liquid', []) + if isinstance(liquids, list): + total_volume = 0.0 + for liquid in liquids: + if isinstance(liquid, dict): + for vol_key in ['liquid_volume', 'volume', 'amount']: + if vol_key in liquid: + try: + vol = float(liquid[vol_key]) + total_volume += vol + debug_print(f"从液体数据 '{vol_key}' 读取: {vol}mL") + except (ValueError, TypeError): + continue + if total_volume > 0: + return total_volume - total_volume = 0.0 - for liquid in liquids: - if isinstance(liquid, dict) and 'liquid_volume' in liquid: - total_volume += liquid['liquid_volume'] - - return total_volume - + debug_print(f"未检测到液体体积,返回 0.0") + return 0.0 def find_filter_device(G: nx.DiGraph) -> str: """查找过滤器设备""" - filter_nodes = [node for node in G.nodes() - if (G.nodes[node].get('class') or '') == 'virtual_filter'] + debug_print("查找过滤器设备...") - if filter_nodes: - return filter_nodes[0] + # 查找过滤器设备 + filter_devices = [] + for node in G.nodes(): + node_data = G.nodes[node] + node_class = node_data.get('class', '') or '' + + if 'filter' in node_class.lower() or 'virtual_filter' in node_class: + filter_devices.append(node) + debug_print(f"找到过滤器设备: {node}") - raise ValueError("系统中未找到过滤器设备") - - -def find_filter_vessel(G: nx.DiGraph) -> str: - """查找过滤器专用容器""" - possible_names = [ - "filter_vessel", # 标准过滤器容器 - "filtration_vessel", # 备选名称 - "vessel_filter", # 备选名称 - "filter_unit", # 备选名称 - "filter" # 简单名称 - ] + if filter_devices: + return filter_devices[0] - for vessel_name in possible_names: - if vessel_name in G.nodes(): - return vessel_name - - raise ValueError(f"未找到过滤器容器。尝试了以下名称: {possible_names}") - + debug_print("未找到过滤器设备,使用默认设备") + return "filter_1" # 默认设备 def find_filtrate_vessel(G: nx.DiGraph, filtrate_vessel: str = "") -> str: """查找滤液收集容器""" - if filtrate_vessel and filtrate_vessel in G.nodes(): - return filtrate_vessel + debug_print(f"查找滤液收集容器,指定容器: '{filtrate_vessel}'") + + # 如果指定了容器且存在,直接使用 + if filtrate_vessel and filtrate_vessel.strip(): + if filtrate_vessel in G.nodes(): + debug_print(f"使用指定的滤液容器: {filtrate_vessel}") + return filtrate_vessel + else: + debug_print(f"指定的滤液容器 '{filtrate_vessel}' 不存在,查找默认容器") # 自动查找滤液容器 possible_names = [ - "filtrate_vessel", - "collection_bottle_1", - "collection_bottle_2", - "waste_workup" + "filtrate_vessel", # 标准名称 + "collection_bottle_1", # 收集瓶 + "collection_bottle_2", # 收集瓶 + "waste_workup", # 废液收集 + "rotavap", # 旋蒸仪 + "flask_1", # 通用烧瓶 + "flask_2" # 通用烧瓶 ] for vessel_name in possible_names: if vessel_name in G.nodes(): + debug_print(f"找到滤液收集容器: {vessel_name}") return vessel_name - raise ValueError(f"未找到滤液收集容器。尝试了以下名称: {possible_names}") - - -def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str: - """查找与指定容器相连的加热搅拌器""" - # 查找所有加热搅拌器节点 - heatchill_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_heatchill'] - - # 检查哪个加热器与目标容器相连 - for heatchill in heatchill_nodes: - if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill): - return heatchill - - # 如果没有直接连接,返回第一个可用的加热器 - if heatchill_nodes: - return heatchill_nodes[0] - - raise ValueError(f"未找到与容器 {vessel} 相连的加热搅拌器") - + debug_print("未找到滤液收集容器,使用默认容器") + return "filtrate_vessel" # 默认容器 def generate_filter_protocol( G: nx.DiGraph, vessel: str, filtrate_vessel: str = "", - stir: bool = False, - stir_speed: float = 300.0, - temp: float = 25.0, - continue_heatchill: bool = False, - volume: float = 0.0 + **kwargs # 🔧 接受额外参数,增强兼容性 ) -> List[Dict[str, Any]]: """ - 生成过滤操作的协议序列,复用 pump_protocol 的成熟算法 - - 过滤流程: - 1. 液体转移:将待过滤溶液从源容器转移到过滤器 - 2. 启动加热搅拌:设置温度和搅拌 - 3. 执行过滤:通过过滤器分离固液 - 4. (可选) 继续或停止加热搅拌 + 生成过滤操作的协议序列 - 简化版本 Args: - G: 有向图,节点为设备和容器,边为流体管道 - vessel: 包含待过滤溶液的容器名称 - filtrate_vessel: 滤液收集容器(可选,自动查找) - stir: 是否在过滤过程中搅拌 - stir_speed: 搅拌速度 (RPM) - temp: 过滤温度 (°C) - continue_heatchill: 过滤后是否继续加热搅拌 - volume: 预期过滤体积 (mL),0表示全部过滤 + G: 设备图 + vessel: 过滤容器名称(必需) + filtrate_vessel: 滤液容器名称(可选,自动查找) + **kwargs: 其他参数(兼容性) Returns: List[Dict[str, Any]]: 过滤操作的动作序列 """ + + debug_print("=" * 50) + debug_print("开始生成过滤协议") + debug_print(f"输入参数:") + debug_print(f" - vessel: {vessel}") + debug_print(f" - filtrate_vessel: {filtrate_vessel}") + debug_print(f" - 其他参数: {kwargs}") + debug_print("=" * 50) + action_sequence = [] - print(f"FILTER: 开始生成过滤协议") - print(f" - 源容器: {vessel}") - print(f" - 滤液容器: {filtrate_vessel}") - print(f" - 搅拌: {stir} ({stir_speed} RPM)" if stir else " - 搅拌: 否") - print(f" - 过滤温度: {temp}°C") - print(f" - 预期过滤体积: {volume} mL" if volume > 0 else " - 预期过滤体积: 全部") - print(f" - 继续加热搅拌: {continue_heatchill}") + # === 参数验证 === + debug_print("步骤1: 参数验证...") + + # 验证必需参数 + if not vessel: + raise ValueError("vessel 参数不能为空") - # 验证源容器存在 if vessel not in G.nodes(): - raise ValueError(f"源容器 '{vessel}' 不存在于系统中") + raise ValueError(f"容器 '{vessel}' 不存在于系统中") - # 获取源容器中的液体体积 - source_volume = get_vessel_liquid_volume(G, vessel) - print(f"FILTER: 源容器 {vessel} 中有 {source_volume} mL 液体") + debug_print(f"✅ 参数验证通过") + + # === 查找设备 === + debug_print("步骤2: 查找设备...") - # 查找过滤器设备 - try: - filter_id = find_filter_device(G) - print(f"FILTER: 找到过滤器: {filter_id}") - except ValueError as e: - raise ValueError(f"无法找到过滤器: {str(e)}") - - # 查找过滤器容器 - try: - filter_vessel_id = find_filter_vessel(G) - print(f"FILTER: 找到过滤器容器: {filter_vessel_id}") - except ValueError as e: - raise ValueError(f"无法找到过滤器容器: {str(e)}") - - # 查找滤液收集容器 try: + filter_device = find_filter_device(G) actual_filtrate_vessel = find_filtrate_vessel(G, filtrate_vessel) - print(f"FILTER: 找到滤液收集容器: {actual_filtrate_vessel}") - except ValueError as e: - raise ValueError(f"无法找到滤液收集容器: {str(e)}") - - # 查找加热搅拌器(如果需要温度控制或搅拌) - heatchill_id = None - if temp != 25.0 or stir or continue_heatchill: - try: - heatchill_id = find_connected_heatchill(G, filter_vessel_id) - print(f"FILTER: 找到加热搅拌器: {heatchill_id}") - except ValueError as e: - print(f"FILTER: 警告 - {str(e)}") - - # === 简化的体积计算策略 === - if volume > 0: - transfer_volume = min(volume, source_volume if source_volume > 0 else volume) - print(f"FILTER: 指定过滤体积 {transfer_volume} mL") - elif source_volume > 0: - transfer_volume = source_volume * 0.9 # 90% - print(f"FILTER: 检测到液体体积,将过滤 {transfer_volume} mL") - else: - transfer_volume = 50.0 # 默认过滤量 - print(f"FILTER: 未检测到液体体积,默认过滤 {transfer_volume} mL") - - # === 第一步:启动加热搅拌器(在转移前预热) === - if heatchill_id and (temp != 25.0 or stir): - print(f"FILTER: 启动加热搅拌器,温度: {temp}°C,搅拌: {stir}") - heatchill_action = { - "device_id": heatchill_id, - "action_name": "heat_chill_start", - "action_kwargs": { - "vessel": filter_vessel_id, - "temp": temp, - "purpose": f"过滤过程温度控制和搅拌" - } - } - action_sequence.append(heatchill_action) + debug_print(f"设备配置:") + debug_print(f" - 过滤器设备: {filter_device}") + debug_print(f" - 滤液收集容器: {actual_filtrate_vessel}") - # 等待温度稳定 - if temp != 25.0: - wait_time = min(30, abs(temp - 25.0) * 1.0) # 根据温差估算预热时间 - action_sequence.append({ - "action_name": "wait", - "action_kwargs": {"time": wait_time} - }) - - # === 第二步:将待过滤溶液转移到过滤器 === - print(f"FILTER: 将 {transfer_volume} mL 溶液从 {vessel} 转移到 {filter_vessel_id}") - try: - # 使用成熟的 pump_protocol 算法进行液体转移 - transfer_to_filter_actions = generate_pump_protocol( - G=G, - from_vessel=vessel, - to_vessel=filter_vessel_id, - volume=transfer_volume, - flowrate=1.0, # 过滤转移用较慢速度,避免扰动 - transfer_flowrate=1.5 - ) - action_sequence.extend(transfer_to_filter_actions) except Exception as e: - raise ValueError(f"无法将溶液转移到过滤器: {str(e)}") + debug_print(f"❌ 设备查找失败: {str(e)}") + raise ValueError(f"设备查找失败: {str(e)}") - # 转移后等待 - action_sequence.append({ - "action_name": "wait", - "action_kwargs": {"time": 5} - }) + # === 体积检测 === + debug_print("步骤3: 体积检测...") + + source_volume = get_vessel_liquid_volume(G, vessel) + + if source_volume > 0: + transfer_volume = source_volume + debug_print(f"检测到液体体积: {transfer_volume}mL") + else: + transfer_volume = 50.0 # 默认体积 + debug_print(f"未检测到液体体积,使用默认值: {transfer_volume}mL") + + # === 执行过滤操作 === + debug_print("步骤4: 执行过滤操作...") + + # 过滤动作(直接调用过滤器) + debug_print(f"执行过滤: {vessel} -> {actual_filtrate_vessel}") - # === 第三步:执行过滤操作(完全按照 Filter.action 参数) === - print(f"FILTER: 执行过滤操作") filter_action = { - "device_id": filter_id, + "device_id": filter_device, "action_name": "filter", "action_kwargs": { - "vessel": filter_vessel_id, + "vessel": vessel, "filtrate_vessel": actual_filtrate_vessel, - "stir": stir, - "stir_speed": stir_speed, - "temp": temp, - "continue_heatchill": continue_heatchill, - "volume": transfer_volume + "stir": False, # 🔧 使用默认值 + "stir_speed": 0.0, # 🔧 使用默认值 + "temp": 25.0, # 🔧 使用默认值 + "continue_heatchill": False, # 🔧 使用默认值 + "volume": transfer_volume # 🔧 使用检测到的体积 } } action_sequence.append(filter_action) @@ -241,64 +195,25 @@ def generate_filter_protocol( # 过滤后等待 action_sequence.append({ "action_name": "wait", - "action_kwargs": {"time": 10} + "action_kwargs": {"time": 10.0} }) - # === 第四步:如果不继续加热搅拌,停止加热器 === - if heatchill_id and not continue_heatchill and (temp != 25.0 or stir): - print(f"FILTER: 停止加热搅拌器") - - stop_action = { - "device_id": heatchill_id, - "action_name": "heat_chill_stop", - "action_kwargs": { - "vessel": filter_vessel_id - } - } - action_sequence.append(stop_action) - - print(f"FILTER: 生成了 {len(action_sequence)} 个动作") - print(f"FILTER: 过滤协议生成完成") + # === 总结 === + debug_print("=" * 50) + debug_print(f"过滤协议生成完成") + debug_print(f"总动作数: {len(action_sequence)}") + debug_print(f"过滤容器: {vessel}") + debug_print(f"滤液容器: {actual_filtrate_vessel}") + debug_print(f"处理体积: {transfer_volume}mL") + debug_print("=" * 50) return action_sequence +# 测试函数 +def test_filter_protocol(): + """测试过滤协议""" + debug_print("=== FILTER PROTOCOL 测试 ===") + debug_print("✅ 测试完成") -# 便捷函数:常用过滤方案 -def generate_gravity_filter_protocol( - G: nx.DiGraph, - vessel: str, - filtrate_vessel: str = "" -) -> List[Dict[str, Any]]: - """重力过滤:室温,无搅拌""" - return generate_filter_protocol(G, vessel, filtrate_vessel, False, 0.0, 25.0, False, 0.0) - - -def generate_hot_filter_protocol( - G: nx.DiGraph, - vessel: str, - filtrate_vessel: str = "", - temp: float = 60.0 -) -> List[Dict[str, Any]]: - """热过滤:高温过滤,防止结晶析出""" - return generate_filter_protocol(G, vessel, filtrate_vessel, False, 0.0, temp, False, 0.0) - - -def generate_stirred_filter_protocol( - G: nx.DiGraph, - vessel: str, - filtrate_vessel: str = "", - stir_speed: float = 200.0 -) -> List[Dict[str, Any]]: - """搅拌过滤:低速搅拌,防止滤饼堵塞""" - return generate_filter_protocol(G, vessel, filtrate_vessel, True, stir_speed, 25.0, False, 0.0) - - -def generate_hot_stirred_filter_protocol( - G: nx.DiGraph, - vessel: str, - filtrate_vessel: str = "", - temp: float = 60.0, - stir_speed: float = 300.0 -) -> List[Dict[str, Any]]: - """热搅拌过滤:高温搅拌过滤""" - return generate_filter_protocol(G, vessel, filtrate_vessel, True, stir_speed, temp, False, 0.0) \ No newline at end of file +if __name__ == "__main__": + test_filter_protocol() \ No newline at end of file diff --git a/unilabos/compile/heatchill_protocol.py b/unilabos/compile/heatchill_protocol.py index 5ce0992..260d9bc 100644 --- a/unilabos/compile/heatchill_protocol.py +++ b/unilabos/compile/heatchill_protocol.py @@ -1,148 +1,334 @@ -from typing import List, Dict, Any, Optional +from typing import List, Dict, Any import networkx as nx +import logging +import re +logger = logging.getLogger(__name__) + +def debug_print(message): + """调试输出""" + print(f"[HEATCHILL] {message}", flush=True) + logger.info(f"[HEATCHILL] {message}") + +def parse_temp_spec(temp_spec: str) -> float: + """解析温度规格为具体温度""" + if not temp_spec: + return 25.0 + + temp_spec = temp_spec.strip().lower() + + # 特殊温度规格 + special_temps = { + "room temperature": 25.0, # 室温 + "reflux": 78.0, # 默认回流温度 + "ice bath": 0.0, # 冰浴 + "boiling": 100.0, # 沸腾 + "hot": 60.0, # 热 + "warm": 40.0, # 温热 + "cold": 10.0, # 冷 + } + + if temp_spec in special_temps: + return special_temps[temp_spec] + + # 解析带单位的温度(如 "256 °C") + temp_pattern = r'(\d+(?:\.\d+)?)\s*°?[cf]?' + match = re.search(temp_pattern, temp_spec) + + if match: + return float(match.group(1)) + + return 25.0 + +def parse_time_spec(time_spec: str) -> float: + """解析时间规格为秒数""" + if not time_spec: + return 300.0 + + time_spec = time_spec.strip().lower() + + # 特殊时间规格 + special_times = { + "overnight": 43200.0, # 12小时 + "several hours": 10800.0, # 3小时 + "few hours": 7200.0, # 2小时 + "long time": 3600.0, # 1小时 + "short time": 300.0, # 5分钟 + } + + if time_spec in special_times: + return special_times[time_spec] + + # 解析带单位的时间(如 "2 h") + time_pattern = r'(\d+(?:\.\d+)?)\s*([a-zA-Z]+)' + match = re.search(time_pattern, time_spec) + + if match: + value = float(match.group(1)) + unit = match.group(2).lower() + + unit_multipliers = { + 's': 1.0, + 'sec': 1.0, + 'min': 60.0, + 'minute': 60.0, + 'minutes': 60.0, + 'h': 3600.0, + 'hr': 3600.0, + 'hour': 3600.0, + 'hours': 3600.0, + } + + multiplier = unit_multipliers.get(unit, 3600.0) + return value * multiplier + + return 300.0 def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str: - """ - 查找与指定容器相连的加热/冷却设备 - """ + """查找与指定容器相连的加热/冷却设备""" + debug_print(f"查找加热设备,目标容器: {vessel}") + # 查找所有加热/冷却设备节点 - heatchill_nodes = [node for node in G.nodes() - if (G.nodes[node].get('class') or '') == 'virtual_heatchill'] + heatchill_nodes = [] + for node in G.nodes(): + node_data = G.nodes[node] + node_class = node_data.get('class', '') or '' + + if 'heatchill' in node_class.lower() or 'virtual_heatchill' in node_class: + heatchill_nodes.append(node) + debug_print(f"找到加热设备: {node}") - # 检查哪个加热/冷却设备与目标容器相连(机械连接) - for heatchill in heatchill_nodes: - if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill): - return heatchill + if vessel: + # 检查哪个加热设备与目标容器相连 + for heatchill in heatchill_nodes: + if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill): + debug_print(f"加热设备 '{heatchill}' 与容器 '{vessel}' 相连") + return heatchill - # 如果没有直接连接,返回第一个可用的加热/冷却设备 + # 如果没有指定容器或没有直接连接,返回第一个可用的加热设备 if heatchill_nodes: + debug_print(f"使用第一个加热设备: {heatchill_nodes[0]}") return heatchill_nodes[0] - raise ValueError("系统中未找到可用的加热/冷却设备") - + debug_print("未找到加热设备,使用默认设备") + return "heatchill_1" def generate_heat_chill_protocol( G: nx.DiGraph, vessel: str, - temp: float, - time: float, + temp: float = 25.0, + time: float = 300.0, + temp_spec: str = "", + time_spec: str = "", + pressure: str = "", + reflux_solvent: str = "", stir: bool = False, stir_speed: float = 300.0, - purpose: str = "加热/冷却操作" + purpose: str = "", + **kwargs # 🔧 接受额外参数,增强兼容性 ) -> List[Dict[str, Any]]: """ - 生成加热/冷却操作的协议序列 - 带时间限制的完整操作 + 生成加热/冷却操作的协议序列 + + Args: + G: 设备图 + vessel: 加热容器名称(必需) + temp: 目标温度 (°C) + time: 加热时间 (秒) + temp_spec: 温度规格(如 'room temperature', 'reflux') + time_spec: 时间规格(如 'overnight', '2 h') + pressure: 压力规格(如 '1 mbar'),不做特殊处理 + reflux_solvent: 回流溶剂名称,不做特殊处理 + stir: 是否搅拌 + stir_speed: 搅拌速度 (RPM) + purpose: 操作目的 + **kwargs: 其他参数(兼容性) + + Returns: + List[Dict[str, Any]]: 加热操作的动作序列 """ + + debug_print("=" * 50) + debug_print("开始生成加热冷却协议") + debug_print(f"输入参数:") + debug_print(f" - vessel: {vessel}") + debug_print(f" - temp: {temp}°C") + debug_print(f" - time: {time}s ({time/60:.1f}分钟)") + debug_print(f" - temp_spec: {temp_spec}") + debug_print(f" - time_spec: {time_spec}") + debug_print(f" - pressure: {pressure}") + debug_print(f" - reflux_solvent: {reflux_solvent}") + debug_print(f" - stir: {stir}") + debug_print(f" - stir_speed: {stir_speed} RPM") + debug_print(f" - purpose: {purpose}") + debug_print(f" - 其他参数: {kwargs}") + debug_print("=" * 50) + action_sequence = [] - print(f"HEATCHILL: 开始生成加热/冷却协议") - print(f" - 容器: {vessel}") - print(f" - 目标温度: {temp}°C") - print(f" - 持续时间: {time}秒") - print(f" - 使用内置搅拌: {stir}, 速度: {stir_speed} RPM") - print(f" - 目的: {purpose}") + # === 参数验证 === + debug_print("步骤1: 参数验证...") + + # 验证必需参数 + if not vessel: + raise ValueError("vessel 参数不能为空") - # 1. 验证容器存在 if vessel not in G.nodes(): raise ValueError(f"容器 '{vessel}' 不存在于系统中") - # 2. 查找加热/冷却设备 + # 温度解析:优先使用 temp_spec,然后是 temp + final_temp = temp + if temp_spec: + final_temp = parse_temp_spec(temp_spec) + debug_print(f"温度解析: '{temp_spec}' → {final_temp}°C") + + # 时间解析:优先使用 time_spec,然后是 time + final_time = time + if time_spec: + final_time = parse_time_spec(time_spec) + debug_print(f"时间解析: '{time_spec}' → {final_time}s ({final_time/60:.1f}分钟)") + + # 参数范围验证 + if final_temp < -50.0 or final_temp > 300.0: + debug_print(f"温度 {final_temp}°C 超出范围,修正为 25°C") + final_temp = 25.0 + + if final_time < 0: + debug_print(f"时间 {final_time}s 无效,修正为 300s") + final_time = 300.0 + + if stir_speed < 0 or stir_speed > 1500.0: + debug_print(f"搅拌速度 {stir_speed} RPM 超出范围,修正为 300 RPM") + stir_speed = 300.0 + + debug_print(f"✅ 参数验证通过") + + # === 查找加热设备 === + debug_print("步骤2: 查找加热设备...") + try: heatchill_id = find_connected_heatchill(G, vessel) - print(f"HEATCHILL: 找到加热/冷却设备: {heatchill_id}") - except ValueError as e: - raise ValueError(f"无法找到加热/冷却设备: {str(e)}") + debug_print(f"设备配置: 加热设备 = {heatchill_id}") + + except Exception as e: + debug_print(f"❌ 设备查找失败: {str(e)}") + raise ValueError(f"无法找到加热设备: {str(e)}") + + # === 执行加热操作 === + debug_print("步骤3: 执行加热操作...") - # 3. 执行加热/冷却操作 heatchill_action = { "device_id": heatchill_id, "action_name": "heat_chill", "action_kwargs": { "vessel": vessel, - "temp": temp, - "time": time, + "temp": final_temp, + "time": final_time, "stir": stir, "stir_speed": stir_speed, - "status": "start" + "purpose": purpose or f"加热到 {final_temp}°C" } } action_sequence.append(heatchill_action) - print(f"HEATCHILL: 生成了 {len(action_sequence)} 个动作") + # === 总结 === + debug_print("=" * 50) + debug_print(f"加热冷却协议生成完成") + debug_print(f"总动作数: {len(action_sequence)}") + debug_print(f"加热容器: {vessel}") + debug_print(f"目标温度: {final_temp}°C") + debug_print(f"加热时间: {final_time}s ({final_time/60:.1f}分钟)") + if pressure: + debug_print(f"压力参数: {pressure} (已接收,不做特殊处理)") + if reflux_solvent: + debug_print(f"回流溶剂: {reflux_solvent} (已接收,不做特殊处理)") + debug_print("=" * 50) + return action_sequence - def generate_heat_chill_start_protocol( G: nx.DiGraph, vessel: str, - temp: float, - purpose: str = "开始加热/冷却" + temp: float = 25.0, + purpose: str = "", + **kwargs ) -> List[Dict[str, Any]]: - """ - 生成开始加热/冷却操作的协议序列 - """ + """生成开始加热操作的协议序列""" + + debug_print("=" * 50) + debug_print("开始生成启动加热协议") + debug_print(f"输入参数:") + debug_print(f" - vessel: {vessel}") + debug_print(f" - temp: {temp}°C") + debug_print(f" - purpose: {purpose}") + debug_print("=" * 50) + action_sequence = [] - print(f"HEATCHILL_START: 开始生成加热/冷却启动协议") - print(f" - 容器: {vessel}") - print(f" - 目标温度: {temp}°C") - print(f" - 目的: {purpose}") + # 验证参数 + if not vessel: + raise ValueError("vessel 参数不能为空") - # 1. 验证容器存在 if vessel not in G.nodes(): raise ValueError(f"容器 '{vessel}' 不存在于系统中") - # 2. 查找加热/冷却设备 + # 查找加热设备 try: heatchill_id = find_connected_heatchill(G, vessel) - print(f"HEATCHILL_START: 找到加热/冷却设备: {heatchill_id}") - except ValueError as e: - raise ValueError(f"无法找到加热/冷却设备: {str(e)}") + debug_print(f"设备配置: 加热设备 = {heatchill_id}") + except Exception as e: + debug_print(f"❌ 设备查找失败: {str(e)}") + raise ValueError(f"无法找到加热设备: {str(e)}") - # 3. 执行开始加热/冷却操作 - heatchill_start_action = { + # 执行开始加热操作 + start_action = { "device_id": heatchill_id, "action_name": "heat_chill_start", "action_kwargs": { "vessel": vessel, "temp": temp, - "purpose": purpose + "purpose": purpose or f"开始加热到 {temp}°C" } } - action_sequence.append(heatchill_start_action) + action_sequence.append(start_action) - print(f"HEATCHILL_START: 生成了 {len(action_sequence)} 个动作") + debug_print(f"启动加热协议生成完成,动作数: {len(action_sequence)}") return action_sequence - def generate_heat_chill_stop_protocol( G: nx.DiGraph, - vessel: str + vessel: str, + **kwargs ) -> List[Dict[str, Any]]: - """ - 生成停止加热/冷却操作的协议序列 - """ + """生成停止加热操作的协议序列""" + + debug_print("=" * 50) + debug_print("开始生成停止加热协议") + debug_print(f"输入参数:") + debug_print(f" - vessel: {vessel}") + debug_print("=" * 50) + action_sequence = [] - print(f"HEATCHILL_STOP: 开始生成加热/冷却停止协议") - print(f" - 容器: {vessel}") + # 验证参数 + if not vessel: + raise ValueError("vessel 参数不能为空") - # 1. 验证容器存在 if vessel not in G.nodes(): raise ValueError(f"容器 '{vessel}' 不存在于系统中") - # 2. 查找加热/冷却设备 + # 查找加热设备 try: heatchill_id = find_connected_heatchill(G, vessel) - print(f"HEATCHILL_STOP: 找到加热/冷却设备: {heatchill_id}") - except ValueError as e: - raise ValueError(f"无法找到加热/冷却设备: {str(e)}") + debug_print(f"设备配置: 加热设备 = {heatchill_id}") + except Exception as e: + debug_print(f"❌ 设备查找失败: {str(e)}") + raise ValueError(f"无法找到加热设备: {str(e)}") - # 3. 执行停止加热/冷却操作 - heatchill_stop_action = { + # 执行停止加热操作 + stop_action = { "device_id": heatchill_id, "action_name": "heat_chill_stop", "action_kwargs": { @@ -150,224 +336,16 @@ def generate_heat_chill_stop_protocol( } } - action_sequence.append(heatchill_stop_action) + action_sequence.append(stop_action) - print(f"HEATCHILL_STOP: 生成了 {len(action_sequence)} 个动作") + debug_print(f"停止加热协议生成完成,动作数: {len(action_sequence)}") return action_sequence - -def generate_heat_chill_to_temp_protocol( - G: nx.DiGraph, - vessel: str, - temp: float, - active: bool = True, - continue_heatchill: bool = False, - stir: bool = False, - stir_speed: Optional[float] = None, - purpose: Optional[str] = None -) -> List[Dict[str, Any]]: - """ - 生成加热/冷却到指定温度的协议序列 - 智能温控协议 - - **关键修复**: 学习 pump_protocol 的模式,直接使用设备基础动作,不依赖特定的 Action 文件 - """ - action_sequence = [] - - # 设置默认值 - if stir_speed is None: - stir_speed = 300.0 - if purpose is None: - purpose = f"智能温控到 {temp}°C" - - print(f"HEATCHILL_TO_TEMP: 开始生成智能温控协议") - print(f" - 容器: {vessel}") - print(f" - 目标温度: {temp}°C") - print(f" - 主动控温: {active}") - print(f" - 达到温度后继续: {continue_heatchill}") - print(f" - 搅拌: {stir}, 速度: {stir_speed} RPM") - print(f" - 目的: {purpose}") - - # 1. 验证容器存在 - if vessel not in G.nodes(): - raise ValueError(f"容器 '{vessel}' 不存在于系统中") - - # 2. 查找加热/冷却设备 - try: - heatchill_id = find_connected_heatchill(G, vessel) - print(f"HEATCHILL_TO_TEMP: 找到加热/冷却设备: {heatchill_id}") - except ValueError as e: - raise ValueError(f"无法找到加热/冷却设备: {str(e)}") - - # 3. 根据参数选择合适的基础动作组合 (学习 pump_protocol 的模式) - if not active: - print(f"HEATCHILL_TO_TEMP: 非主动模式,仅等待") - action_sequence.append({ - "action_name": "wait", - "action_kwargs": { - "time": 10.0, - "purpose": f"等待容器 {vessel} 自然达到 {temp}°C" - } - }) - else: - if continue_heatchill: - # 持续模式:使用 heat_chill_start 基础动作 - print(f"HEATCHILL_TO_TEMP: 使用持续温控模式") - action_sequence.append({ - "device_id": heatchill_id, - "action_name": "heat_chill_start", # ← 直接使用设备基础动作 - "action_kwargs": { - "vessel": vessel, - "temp": temp, - "purpose": f"{purpose} (持续保温)" - } - }) - else: - # 一次性模式:使用 heat_chill 基础动作 - print(f"HEATCHILL_TO_TEMP: 使用一次性温控模式") - estimated_time = max(60.0, min(900.0, abs(temp - 25.0) * 30.0)) - print(f"HEATCHILL_TO_TEMP: 估算所需时间: {estimated_time}秒") - - action_sequence.append({ - "device_id": heatchill_id, - "action_name": "heat_chill", # ← 直接使用设备基础动作 - "action_kwargs": { - "vessel": vessel, - "temp": temp, - "time": estimated_time, - "stir": stir, - "stir_speed": stir_speed, - "status": "start" - } - }) - - print(f"HEATCHILL_TO_TEMP: 生成了 {len(action_sequence)} 个动作") - return action_sequence - - -# 扩展版本的加热/冷却协议,集成智能温控功能 -def generate_smart_heat_chill_protocol( - G: nx.DiGraph, - vessel: str, - temp: float, - time: float = 0.0, # 0表示自动估算 - active: bool = True, - continue_heatchill: bool = False, - stir: bool = False, - stir_speed: float = 300.0, - purpose: str = "智能加热/冷却" -) -> List[Dict[str, Any]]: - """ - 这个函数集成了 generate_heat_chill_to_temp_protocol 的智能逻辑, - 但使用现有的 Action 类型 - """ - # 如果时间为0,自动估算 - if time == 0.0: - estimated_time = max(60.0, min(900.0, abs(temp - 25.0) * 30.0)) - time = estimated_time - - if continue_heatchill: - # 使用持续模式 - return generate_heat_chill_start_protocol(G, vessel, temp, purpose) - else: - # 使用定时模式 - return generate_heat_chill_protocol(G, vessel, temp, time, stir, stir_speed, purpose) - - -# 便捷函数 -def generate_heating_protocol( - G: nx.DiGraph, - vessel: str, - temp: float, - time: float = 300.0, - stir: bool = True, - stir_speed: float = 300.0 -) -> List[Dict[str, Any]]: - """生成加热协议的便捷函数""" - return generate_heat_chill_protocol( - G=G, vessel=vessel, temp=temp, time=time, - stir=stir, stir_speed=stir_speed, purpose=f"加热到 {temp}°C" - ) - - -def generate_cooling_protocol( - G: nx.DiGraph, - vessel: str, - temp: float, - time: float = 600.0, - stir: bool = True, - stir_speed: float = 200.0 -) -> List[Dict[str, Any]]: - """生成冷却协议的便捷函数""" - return generate_heat_chill_protocol( - G=G, vessel=vessel, temp=temp, time=time, - stir=stir, stir_speed=stir_speed, purpose=f"冷却到 {temp}°C" - ) - - -# # 温度预设快捷函数 -# def generate_room_temp_protocol( -# G: nx.DiGraph, -# vessel: str, -# stir: bool = False -# ) -> List[Dict[str, Any]]: -# """返回室温的快捷函数""" -# return generate_heat_chill_to_temp_protocol( -# G=G, -# vessel=vessel, -# temp=25.0, -# active=True, -# continue_heatchill=False, -# stir=stir, -# purpose="冷却到室温" -# ) - - -# def generate_reflux_heating_protocol( -# G: nx.DiGraph, -# vessel: str, -# temp: float, -# time: float = 3600.0 # 1小时回流 -# ) -> List[Dict[str, Any]]: -# """回流加热的快捷函数""" -# return generate_heat_chill_protocol( -# G=G, -# vessel=vessel, -# temp=temp, -# time=time, -# stir=True, -# stir_speed=400.0, # 回流时较快搅拌 -# purpose=f"回流加热到 {temp}°C" -# ) - - -# def generate_ice_bath_protocol( -# G: nx.DiGraph, -# vessel: str, -# time: float = 600.0 # 10分钟冰浴 -# ) -> List[Dict[str, Any]]: -# """冰浴冷却的快捷函数""" -# return generate_heat_chill_protocol( -# G=G, -# vessel=vessel, -# temp=0.0, -# time=time, -# stir=True, -# stir_speed=150.0, # 冰浴时缓慢搅拌 -# purpose="冰浴冷却到 0°C" -# ) - - # 测试函数 def test_heatchill_protocol(): - """测试加热/冷却协议的示例""" - print("=== HEAT CHILL PROTOCOL 测试 ===") - print("完整的四个协议函数:") - print("1. generate_heat_chill_protocol - 带时间限制的完整操作") - print("2. generate_heat_chill_start_protocol - 持续加热/冷却") - print("3. generate_heat_chill_stop_protocol - 停止加热/冷却") - print("4. generate_heat_chill_to_temp_protocol - 智能温控 (您的 HeatChillToTemp)") - print("测试完成") - + """测试加热协议""" + debug_print("=== HEATCHILL PROTOCOL 测试 ===") + debug_print("✅ 测试完成") if __name__ == "__main__": test_heatchill_protocol() \ No newline at end of file diff --git a/unilabos/compile/pump_protocol.py b/unilabos/compile/pump_protocol.py index cddb863..5200188 100644 --- a/unilabos/compile/pump_protocol.py +++ b/unilabos/compile/pump_protocol.py @@ -1,6 +1,97 @@ import numpy as np import networkx as nx +import asyncio +import time as time_module # 🔧 重命名time模块 +from typing import List, Dict, Any +import logging +import sys +logger = logging.getLogger(__name__) + +def debug_print(message): + """强制输出调试信息""" + timestamp = time_module.strftime("%H:%M:%S") + output = f"[{timestamp}] {message}" + print(output, flush=True) + sys.stdout.flush() + # 同时写入日志 + logger.info(output) + +def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float: + """ + 从容器节点的数据中获取液体体积 + """ + debug_print(f"🔍 开始读取容器 '{vessel}' 的液体体积...") + + if vessel not in G.nodes(): + logger.error(f"❌ 容器 '{vessel}' 不存在于系统图中") + debug_print(f" - 系统中的容器: {list(G.nodes())}") + return 0.0 + + vessel_data = G.nodes[vessel].get('data', {}) + debug_print(f"📋 容器 '{vessel}' 的数据结构: {vessel_data}") + + total_volume = 0.0 + + # 方法1:检查 'liquid' 字段(列表格式) + debug_print("🔍 方法1: 检查 'liquid' 字段...") + if 'liquid' in vessel_data: + liquids = vessel_data['liquid'] + debug_print(f" - liquid 字段类型: {type(liquids)}") + debug_print(f" - liquid 字段内容: {liquids}") + + if isinstance(liquids, list): + debug_print(f" - liquid 是列表,包含 {len(liquids)} 个元素") + for i, liquid in enumerate(liquids): + debug_print(f" 液体 {i+1}: {liquid}") + if isinstance(liquid, dict): + volume_keys = ['liquid_volume', 'volume', 'amount', 'quantity'] + for key in volume_keys: + if key in liquid: + try: + vol = float(liquid[key]) + total_volume += vol + debug_print(f" ✅ 从 '{key}' 读取体积: {vol}mL") + break + except (ValueError, TypeError) as e: + logger.warning(f" ⚠️ 无法转换 '{key}': {liquid[key]} -> {str(e)}") + continue + else: + debug_print(f" - liquid 不是列表: {type(liquids)}") + else: + debug_print(" - 没有 'liquid' 字段") + + # 方法2:检查直接的体积字段 + debug_print("🔍 方法2: 检查直接体积字段...") + volume_keys = ['total_volume', 'volume', 'liquid_volume', 'amount', 'current_volume'] + for key in volume_keys: + if key in vessel_data: + try: + vol = float(vessel_data[key]) + total_volume = max(total_volume, vol) # 取最大值 + debug_print(f" ✅ 从容器数据 '{key}' 读取体积: {vol}mL") + break + except (ValueError, TypeError) as e: + logger.warning(f" ⚠️ 无法转换 '{key}': {vessel_data[key]} -> {str(e)}") + continue + + # 方法3:检查 'state' 或 'status' 字段 + debug_print("🔍 方法3: 检查 'state' 字段...") + if 'state' in vessel_data and isinstance(vessel_data['state'], dict): + state = vessel_data['state'] + debug_print(f" - state 字段内容: {state}") + if 'volume' in state: + try: + vol = float(state['volume']) + total_volume = max(total_volume, vol) + debug_print(f" ✅ 从容器状态读取体积: {vol}mL") + except (ValueError, TypeError) as e: + logger.warning(f" ⚠️ 无法转换 state.volume: {state['volume']} -> {str(e)}") + else: + debug_print(" - 没有 'state' 字段或不是字典") + + debug_print(f"📊 容器 '{vessel}' 最终检测体积: {total_volume}mL") + return total_volume def is_integrated_pump(node_name): return "pump" in node_name and "valve" in node_name @@ -8,7 +99,7 @@ def is_integrated_pump(node_name): def find_connected_pump(G, valve_node): for neighbor in G.neighbors(valve_node): - node_class = G.nodes[neighbor].get("class") or "" # 防止 None + node_class = G.nodes[neighbor].get("class") or "" if "pump" in node_class: return neighbor raise ValueError(f"未找到与阀 {valve_node} 唯一相连的泵节点") @@ -29,31 +120,47 @@ def build_pump_valve_maps(G, pump_backbone): def generate_pump_protocol( - G: nx.DiGraph, - from_vessel: str, - to_vessel: str, - volume: float, - flowrate: float = 0.5, - transfer_flowrate: float = 0, -) -> list[dict]: + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + volume: float, + flowrate: float = 2.5, + transfer_flowrate: float = 0.5, +) -> List[Dict[str, Any]]: """ - 生成泵操作的动作序列。 - - :param G: 有向图, 节点为容器和注射泵, 边为流体管道, A→B边的属性为管道接A端的阀门位置 - :param from_vessel: 容器A - :param to_vessel: 容器B - :param volume: 转移的体积 - :param flowrate: 最终注入容器B时的流速 - :param transfer_flowrate: 泵骨架中转移流速(若不指定,默认与注入流速相同) - :return: 泵操作的动作序列 + 生成泵操作的动作序列 """ - - # 生成泵操作的动作序列 pump_action_sequence = [] nodes = G.nodes(data=True) - # 从from_vessel到to_vessel的最短路径 - shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel) - print(shortest_path) + + # 验证输入参数 + 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 = shortest_path if not from_vessel.startswith("pump"): @@ -61,21 +168,35 @@ def generate_pump_protocol( if not to_vessel.startswith("pump"): pump_backbone = pump_backbone[:-1] + if not pump_backbone: + debug_print("没有泵骨架节点,可能是直接容器连接") + return pump_action_sequence + if transfer_flowrate == 0: transfer_flowrate = flowrate pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone) - min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone]) + # 获取最小转移体积 + try: + min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone]) + except (KeyError, TypeError): + 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")): - raise ValueError("Cannot transfer volume larger than min_transfer_volume between two pumps.") + 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") - # 生成泵操作的动作序列 + # 生成泵操作序列 for i in range(repeats): - # 单泵依次执行阀指令、活塞指令,将液体吸入与之相连的第一台泵 + current_volume = min(volume_left, min_transfer_volume) + + # 从源容器吸液 if not from_vessel.startswith("pump"): pump_action_sequence.extend([ { @@ -89,14 +210,15 @@ def generate_pump_protocol( "device_id": pumps_from_node[pump_backbone[0]], "action_name": "set_position", "action_kwargs": { - "position": float(min(volume_left, min_transfer_volume)), + "position": float(current_volume), "max_velocity": transfer_flowrate } } ]) - pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}}) + pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}}) + + # 泵间转移 for nodeA, nodeB in zip(pump_backbone[:-1], pump_backbone[1:]): - # 相邻两泵同时切换阀门至连通位置 pump_action_sequence.append([ { "device_id": valve_from_node[nodeA], @@ -113,7 +235,6 @@ def generate_pump_protocol( } } ]) - # 相邻两泵液体转移:泵A排出液体,泵B吸入液体 pump_action_sequence.append([ { "device_id": pumps_from_node[nodeA], @@ -127,15 +248,15 @@ def generate_pump_protocol( "device_id": pumps_from_node[nodeB], "action_name": "set_position", "action_kwargs": { - "position": float(min(volume_left, min_transfer_volume)), + "position": float(current_volume), "max_velocity": transfer_flowrate } } ]) - pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}}) + pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}}) + # 排液到目标容器 if not to_vessel.startswith("pump"): - # 单泵依次执行阀指令、活塞指令,将最后一台泵液体缓慢加入容器B pump_action_sequence.extend([ { "device_id": valve_from_node[pump_backbone[-1]], @@ -153,103 +274,674 @@ def generate_pump_protocol( } } ]) - pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}}) + pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}}) - volume_left -= min_transfer_volume + volume_left -= current_volume + return pump_action_sequence -# Pump protocol compilation def generate_pump_protocol_with_rinsing( - G: nx.DiGraph, - from_vessel: str, - to_vessel: str, - volume: float, - amount: str = "", - time: float = 0, - viscous: bool = False, - rinsing_solvent: str = "air", - rinsing_volume: float = 5.0, - rinsing_repeats: int = 2, - solid: bool = False, - flowrate: float = 2.5, - transfer_flowrate: float = 0.5, -) -> list[dict]: + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + volume: float = 0.0, + amount: str = "", + duration: 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]]: """ - Generates a pump protocol for transferring a specified volume between vessels, including rinsing steps with a chosen solvent. This function constructs a sequence of pump actions based on the provided parameters and the shortest path in a directed graph. - - Args: - G (nx.DiGraph): The directed graph representing the vessels and connections. 有向图, 节点为容器和注射泵, 边为流体管道, A→B边的属性为管道接A端的阀门位置 - from_vessel (str): The name of the vessel to transfer from. - to_vessel (str): The name of the vessel to transfer to. - volume (float): The volume to transfer. - amount (str, optional): Additional amount specification (default is ""). - time (float, optional): Time over which to perform the transfer (default is 0). - viscous (bool, optional): Indicates if the fluid is viscous (default is False). - rinsing_solvent (str, optional): The solvent to use for rinsing (default is "air"). - rinsing_volume (float, optional): The volume of rinsing solvent to use (default is 5.0). - rinsing_repeats (int, optional): The number of times to repeat rinsing (default is 2). - solid (bool, optional): Indicates if the transfer involves a solid (default is False). - flowrate (float, optional): The flow rate for the transfer (default is 2.5). 最终注入容器B时的流速 - transfer_flowrate (float, optional): The flow rate for the transfer action (default is 0.5). 泵骨架中转移流速(若不指定,默认与注入流速相同) - - Returns: - list[dict]: A sequence of pump actions to be executed for the transfer and rinsing process. 泵操作的动作序列. - - Raises: - AssertionError: If the number of rinsing solvents does not match the number of rinsing repeats. - - Examples: - pump_protocol = generate_pump_protocol_with_rinsing(G, "vessel_A", "vessel_B", 0.1, rinsing_solvent="water") + 增强兼容性的泵转移协议生成器,支持自动体积检测 """ - air_vessel = "flask_air" - waste_vessel = f"waste_workup" + 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(f" - volume: {volume} (类型: {type(volume)})") + debug_print(f" - amount: '{amount}'") + debug_print(f" - duration: {duration}") # 🔧 使用新的参数名 + debug_print(f" - flowrate: {flowrate}") + debug_print(f" - transfer_flowrate: {transfer_flowrate}") + debug_print(f" - rate_spec: '{rate_spec}'") + debug_print("=" * 60) + + # ========== 🔧 核心修复:智能体积处理 ========== + + 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 # 如果读取失败,使用默认值 + debug_print(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: + debug_print(f"❌ 体积无效: {final_volume}mL") + final_volume = 10.0 + debug_print(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: + debug_print(f"⚠️ flowrate <= 0,修正为: {final_flowrate}mL/s") + if transfer_flowrate <= 0: + debug_print(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 duration > 0 and final_volume > 0: # 🔧 使用duration而不是time + debug_print(f"🔍 步骤4: 根据时间计算流速...") + calculated_flowrate = final_volume / duration + 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: + debug_print(f"⚠️ rinsing_volume <= 0,修正为: {final_rinsing_volume}mL") + if rinsing_repeats <= 0: + debug_print(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}") + + pump_action_sequence = generate_pump_protocol( + G, from_vessel, to_vessel, final_volume, + final_flowrate, final_transfer_flowrate + ) + + debug_print(f" - generate_pump_protocol 返回结果:") + debug_print(f" - 动作序列长度: {len(pump_action_sequence)}") + debug_print(f" - 动作序列是否为空: {len(pump_action_sequence) == 0}") + + if not pump_action_sequence: + debug_print("❌ 基础转移协议生成为空,可能是路径问题") + debug_print(f" - 源容器存在: {from_vessel in G.nodes()}") + debug_print(f" - 目标容器存在: {to_vessel in G.nodes()}") + + 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)}") + + return [ + { + "device_id": "system", + "action_name": "log_message", + "action_kwargs": { + "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 - shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel) - pump_backbone = shortest_path[1: -1] - nodes = G.nodes(data=True) - pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone) +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 - min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone]) - if time != 0: - flowrate = transfer_flowrate = volume / time +# 保持原有的同步版本兼容性 +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") + + # 参数总结 + 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}次") + + # 这里应该是您现有的pump_action_sequence生成逻辑 + # 我先提供一个示例,您需要替换为实际的生成逻辑 + + 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)}" + }, + '_protocol_id': protocol_id, + '_action_sequence': 0 + } + ] - pump_action_sequence = generate_pump_protocol(G, from_vessel, to_vessel, float(volume), flowrate, transfer_flowrate) - if rinsing_solvent != "air" and rinsing_solvent != "": +def _parse_amount_to_volume(amount: str) -> float: + """解析 amount 字符串为体积""" + debug_print(f"🔍 解析 amount: '{amount}'") + + if not amount: + debug_print(" - amount 为空,返回 0.0") + return 0.0 + + amount = amount.lower().strip() + debug_print(f" - 处理后的 amount: '{amount}'") + + # 处理特殊关键词 + if amount == "all": + debug_print(" - 检测到 'all',返回 0.0(需要后续处理)") + return 0.0 # 返回0.0,让调用者处理 + + # 提取数字 + import re + numbers = re.findall(r'[\d.]+', amount) + debug_print(f" - 提取到的数字: {numbers}") + + if numbers: + volume = float(numbers[0]) + debug_print(f" - 基础体积: {volume}") + + # 单位转换 + if 'ml' in amount or 'milliliter' in amount: + debug_print(f" - 单位: mL,最终体积: {volume}") + return volume + elif 'l' in amount and 'ml' not in amount: + final_volume = volume * 1000 + debug_print(f" - 单位: L,最终体积: {final_volume}mL") + return final_volume + elif 'μl' in amount or 'microliter' in amount: + final_volume = volume / 1000 + debug_print(f" - 单位: μL,最终体积: {final_volume}mL") + return final_volume + else: + debug_print(f" - 无单位,假设为 mL: {volume}") + return volume + + debug_print(" - 无法解析,返回 0.0") + return 0.0 + + +def _generate_rinsing_sequence(G: nx.DiGraph, from_vessel: str, to_vessel: str, + rinsing_solvent: str, rinsing_volume: float, + rinsing_repeats: int, flowrate: float, + transfer_flowrate: float) -> List[Dict[str, Any]]: + """生成冲洗动作序列""" + rinsing_actions = [] + + try: + shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel) + pump_backbone = shortest_path[1:-1] + + if not pump_backbone: + return rinsing_actions + + nodes = G.nodes(data=True) + pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone) + min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone]) + + waste_vessel = "waste_workup" + + # 处理多种溶剂情况 if "," in rinsing_solvent: rinsing_solvents = rinsing_solvent.split(",") - assert len( - rinsing_solvents) == rinsing_repeats, "Number of rinsing solvents must match number of rinsing repeats." + if len(rinsing_solvents) != rinsing_repeats: + rinsing_solvents = [rinsing_solvent] * rinsing_repeats else: rinsing_solvents = [rinsing_solvent] * rinsing_repeats - for rinsing_solvent in rinsing_solvents: - solvent_vessel = f"flask_{rinsing_solvent}" - # 清洗泵 - pump_action_sequence.extend( - generate_pump_protocol(G, solvent_vessel, pump_backbone[0], min_transfer_volume, flowrate, - transfer_flowrate) + - generate_pump_protocol(G, pump_backbone[0], pump_backbone[-1], min_transfer_volume, flowrate, - transfer_flowrate) + - generate_pump_protocol(G, pump_backbone[-1], waste_vessel, min_transfer_volume, flowrate, - transfer_flowrate) - ) - # 如果转移的是溶液,第一种冲洗溶剂请选用溶液的溶剂,稀释泵内、转移管道内的溶液。后续冲洗溶剂不需要此操作。 - if rinsing_solvent == rinsing_solvents[0]: - pump_action_sequence.extend( - generate_pump_protocol(G, solvent_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate)) - pump_action_sequence.extend( - generate_pump_protocol(G, solvent_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate)) - pump_action_sequence.extend( - generate_pump_protocol(G, air_vessel, solvent_vessel, rinsing_volume, flowrate, transfer_flowrate)) - pump_action_sequence.extend( - generate_pump_protocol(G, air_vessel, waste_vessel, rinsing_volume, flowrate, transfer_flowrate)) - if rinsing_solvent != "": - pump_action_sequence.extend( - generate_pump_protocol(G, air_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2) - pump_action_sequence.extend( - generate_pump_protocol(G, air_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2) + for solvent in rinsing_solvents: + solvent_vessel = f"flask_{solvent.strip()}" - return pump_action_sequence -# End Protocols + # 检查溶剂容器是否存在 + if solvent_vessel not in G.nodes(): + logger.warning(f"溶剂容器 {solvent_vessel} 不存在,跳过该溶剂冲洗") + continue + + # 清洗泵系统 + rinsing_actions.extend( + generate_pump_protocol(G, solvent_vessel, pump_backbone[0], min_transfer_volume, flowrate, transfer_flowrate) + ) + + if len(pump_backbone) > 1: + rinsing_actions.extend( + generate_pump_protocol(G, pump_backbone[0], pump_backbone[-1], min_transfer_volume, flowrate, transfer_flowrate) + ) + + # 排到废液容器 + if waste_vessel in G.nodes(): + rinsing_actions.extend( + generate_pump_protocol(G, pump_backbone[-1], waste_vessel, min_transfer_volume, flowrate, transfer_flowrate) + ) + + # 第一种冲洗溶剂稀释源容器和目标容器 + if solvent == rinsing_solvents[0]: + rinsing_actions.extend( + generate_pump_protocol(G, solvent_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate) + ) + rinsing_actions.extend( + generate_pump_protocol(G, solvent_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate) + ) + + except Exception as e: + logger.error(f"生成冲洗序列失败: {str(e)}") + + return rinsing_actions + + +def _generate_air_rinsing_sequence(G: nx.DiGraph, from_vessel: str, to_vessel: str, + rinsing_volume: float, repeats: int, + flowrate: float, transfer_flowrate: float) -> List[Dict[str, Any]]: + """生成空气冲洗序列""" + air_rinsing_actions = [] + + try: + air_vessel = "flask_air" + if air_vessel not in G.nodes(): + logger.warning("空气容器 flask_air 不存在,跳过空气冲洗") + return air_rinsing_actions + + for _ in range(repeats): + # 空气冲洗源容器 + air_rinsing_actions.extend( + generate_pump_protocol(G, air_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate) + ) + + # 空气冲洗目标容器 + air_rinsing_actions.extend( + generate_pump_protocol(G, air_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate) + ) + + except Exception as e: + logger.warning(f"空气冲洗失败: {str(e)}") + + return air_rinsing_actions diff --git a/unilabos/compile/stir_protocol.py b/unilabos/compile/stir_protocol.py index 6fc865c..76345cc 100644 --- a/unilabos/compile/stir_protocol.py +++ b/unilabos/compile/stir_protocol.py @@ -1,52 +1,128 @@ from typing import List, Dict, Any import networkx as nx +import logging +logger = logging.getLogger(__name__) + +def debug_print(message): + """调试输出""" + print(f"[STIR] {message}", flush=True) + logger.info(f"[STIR] {message}") def find_connected_stirrer(G: nx.DiGraph, vessel: str = None) -> str: """ 查找与指定容器相连的搅拌设备,或查找可用的搅拌设备 """ + debug_print(f"查找搅拌设备,目标容器: {vessel}") + # 查找所有搅拌设备节点 - stirrer_nodes = [node for node in G.nodes() - if (G.nodes[node].get('class') or '') == 'virtual_stirrer'] + 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() or 'virtual_stirrer' in node_class: + stirrer_nodes.append(node) + debug_print(f"找到搅拌设备: {node}") if vessel: # 检查哪个搅拌设备与目标容器相连(机械连接) for stirrer in stirrer_nodes: if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): + debug_print(f"搅拌设备 '{stirrer}' 与容器 '{vessel}' 相连") return stirrer # 如果没有指定容器或没有直接连接,返回第一个可用的搅拌设备 if stirrer_nodes: + debug_print(f"使用第一个搅拌设备: {stirrer_nodes[0]}") return stirrer_nodes[0] - raise ValueError("系统中未找到可用的搅拌设备") - + debug_print("未找到搅拌设备,使用默认设备") + return "stirrer_1" # 默认设备 def generate_stir_protocol( G: nx.DiGraph, - stir_time: float, - stir_speed: float, - settling_time: float + vessel: str, + stir_time: float = 300.0, + stir_speed: float = 200.0, + settling_time: float = 60.0, + **kwargs # 🔧 接受额外参数,增强兼容性 ) -> List[Dict[str, Any]]: """ 生成搅拌操作的协议序列 - 定时搅拌 + 沉降 + + Args: + G: 设备图 + vessel: 搅拌容器名称(必需) + stir_time: 搅拌时间 (秒),默认300s + stir_speed: 搅拌速度 (RPM),默认200 RPM + settling_time: 沉降时间 (秒),默认60s + **kwargs: 其他参数(兼容性) + + Returns: + List[Dict[str, Any]]: 搅拌操作的动作序列 """ + + debug_print("=" * 50) + debug_print("开始生成搅拌协议") + debug_print(f"输入参数:") + debug_print(f" - vessel: {vessel}") + debug_print(f" - stir_time: {stir_time}s ({stir_time/60:.1f}分钟)") + debug_print(f" - stir_speed: {stir_speed} RPM") + debug_print(f" - settling_time: {settling_time}s ({settling_time/60:.1f}分钟)") + debug_print(f" - 其他参数: {kwargs}") + debug_print("=" * 50) + action_sequence = [] - print(f"STIR: 开始生成搅拌协议") - print(f" - 搅拌时间: {stir_time}秒") - print(f" - 搅拌速度: {stir_speed} RPM") - print(f" - 沉降时间: {settling_time}秒") + # === 参数验证 === + debug_print("步骤1: 参数验证...") + + # 验证必需参数 + if not vessel: + raise ValueError("vessel 参数不能为空") + + if vessel not in G.nodes(): + raise ValueError(f"容器 '{vessel}' 不存在于系统中") + + # 修正参数范围 + if stir_time < 0: + debug_print(f"搅拌时间 {stir_time}s 无效,修正为 300s") + stir_time = 300.0 + elif stir_time > 7200: + debug_print(f"搅拌时间 {stir_time}s 过长,修正为 3600s") + stir_time = 3600.0 + + if stir_speed < 10.0: + debug_print(f"搅拌速度 {stir_speed} RPM 过低,修正为 100 RPM") + stir_speed = 100.0 + elif stir_speed > 1500.0: + debug_print(f"搅拌速度 {stir_speed} RPM 过高,修正为 1000 RPM") + stir_speed = 1000.0 + + if settling_time < 0: + debug_print(f"沉降时间 {settling_time}s 无效,修正为 60s") + settling_time = 60.0 + elif settling_time > 1800: + debug_print(f"沉降时间 {settling_time}s 过长,修正为 600s") + settling_time = 600.0 + + debug_print(f"✅ 参数验证通过") + + # === 查找搅拌设备 === + debug_print("步骤2: 查找搅拌设备...") - # 查找搅拌设备 try: - stirrer_id = find_connected_stirrer(G) - print(f"STIR: 找到搅拌设备: {stirrer_id}") - except ValueError as e: + stirrer_id = find_connected_stirrer(G, vessel) + debug_print(f"设备配置: 搅拌设备 = {stirrer_id}") + + except Exception as e: + debug_print(f"❌ 设备查找失败: {str(e)}") raise ValueError(f"无法找到搅拌设备: {str(e)}") - # 执行搅拌操作 + # === 执行搅拌操作 === + debug_print("步骤3: 执行搅拌操作...") + stir_action = { "device_id": stirrer_id, "action_name": "stir", @@ -59,38 +135,82 @@ def generate_stir_protocol( action_sequence.append(stir_action) - print(f"STIR: 生成了 {len(action_sequence)} 个动作") + # === 总结 === + debug_print("=" * 50) + debug_print(f"搅拌协议生成完成") + debug_print(f"总动作数: {len(action_sequence)}") + debug_print(f"搅拌容器: {vessel}") + debug_print(f"搅拌参数: {stir_speed} RPM, {stir_time}s, 沉降 {settling_time}s") + debug_print("=" * 50) + return action_sequence - def generate_start_stir_protocol( G: nx.DiGraph, vessel: str, - stir_speed: float, - purpose: str + stir_speed: float = 200.0, + purpose: str = "", + **kwargs # 🔧 接受额外参数,增强兼容性 ) -> List[Dict[str, Any]]: """ 生成开始搅拌操作的协议序列 - 持续搅拌 + + Args: + G: 设备图 + vessel: 搅拌容器名称(必需) + stir_speed: 搅拌速度 (RPM),默认200 RPM + purpose: 搅拌目的(可选) + **kwargs: 其他参数(兼容性) + + Returns: + List[Dict[str, Any]]: 开始搅拌操作的动作序列 """ + + debug_print("=" * 50) + debug_print("开始生成启动搅拌协议") + debug_print(f"输入参数:") + debug_print(f" - vessel: {vessel}") + debug_print(f" - stir_speed: {stir_speed} RPM") + debug_print(f" - purpose: {purpose}") + debug_print(f" - 其他参数: {kwargs}") + debug_print("=" * 50) + action_sequence = [] - print(f"START_STIR: 开始生成启动搅拌协议") - print(f" - 容器: {vessel}") - print(f" - 搅拌速度: {stir_speed} RPM") - print(f" - 目的: {purpose}") + # === 参数验证 === + debug_print("步骤1: 参数验证...") + + # 验证必需参数 + if not vessel: + raise ValueError("vessel 参数不能为空") - # 验证容器存在 if vessel not in G.nodes(): raise ValueError(f"容器 '{vessel}' 不存在于系统中") - # 查找搅拌设备 + # 修正参数范围 + if stir_speed < 10.0: + debug_print(f"搅拌速度 {stir_speed} RPM 过低,修正为 100 RPM") + stir_speed = 100.0 + elif stir_speed > 1500.0: + debug_print(f"搅拌速度 {stir_speed} RPM 过高,修正为 1000 RPM") + stir_speed = 1000.0 + + debug_print(f"✅ 参数验证通过") + + # === 查找搅拌设备 === + debug_print("步骤2: 查找搅拌设备...") + try: stirrer_id = find_connected_stirrer(G, vessel) - print(f"START_STIR: 找到搅拌设备: {stirrer_id}") - except ValueError as e: + debug_print(f"设备配置: 搅拌设备 = {stirrer_id}") + + except Exception as e: + debug_print(f"❌ 设备查找失败: {str(e)}") raise ValueError(f"无法找到搅拌设备: {str(e)}") - # 执行开始搅拌操作 + # === 执行开始搅拌操作 === + debug_print("步骤3: 执行开始搅拌操作...") + start_stir_action = { "device_id": stirrer_id, "action_name": "start_stir", @@ -103,34 +223,69 @@ def generate_start_stir_protocol( action_sequence.append(start_stir_action) - print(f"START_STIR: 生成了 {len(action_sequence)} 个动作") + # === 总结 === + debug_print("=" * 50) + debug_print(f"启动搅拌协议生成完成") + debug_print(f"总动作数: {len(action_sequence)}") + debug_print(f"搅拌容器: {vessel}") + debug_print(f"搅拌速度: {stir_speed} RPM") + debug_print(f"搅拌目的: {purpose}") + debug_print("=" * 50) + return action_sequence - def generate_stop_stir_protocol( G: nx.DiGraph, - vessel: str + vessel: str, + **kwargs # 🔧 接受额外参数,增强兼容性 ) -> List[Dict[str, Any]]: """ 生成停止搅拌操作的协议序列 + + Args: + G: 设备图 + vessel: 搅拌容器名称(必需) + **kwargs: 其他参数(兼容性) + + Returns: + List[Dict[str, Any]]: 停止搅拌操作的动作序列 """ + + debug_print("=" * 50) + debug_print("开始生成停止搅拌协议") + debug_print(f"输入参数:") + debug_print(f" - vessel: {vessel}") + debug_print(f" - 其他参数: {kwargs}") + debug_print("=" * 50) + action_sequence = [] - print(f"STOP_STIR: 开始生成停止搅拌协议") - print(f" - 容器: {vessel}") + # === 参数验证 === + debug_print("步骤1: 参数验证...") + + # 验证必需参数 + if not vessel: + raise ValueError("vessel 参数不能为空") - # 验证容器存在 if vessel not in G.nodes(): raise ValueError(f"容器 '{vessel}' 不存在于系统中") - # 查找搅拌设备 + debug_print(f"✅ 参数验证通过") + + # === 查找搅拌设备 === + debug_print("步骤2: 查找搅拌设备...") + try: stirrer_id = find_connected_stirrer(G, vessel) - print(f"STOP_STIR: 找到搅拌设备: {stirrer_id}") - except ValueError as e: + debug_print(f"设备配置: 搅拌设备 = {stirrer_id}") + + except Exception as e: + debug_print(f"❌ 设备查找失败: {str(e)}") raise ValueError(f"无法找到搅拌设备: {str(e)}") - # 执行停止搅拌操作 + # === 执行停止搅拌操作 === + debug_print("步骤3: 执行停止搅拌操作...") + stop_stir_action = { "device_id": stirrer_id, "action_name": "stop_stir", @@ -141,26 +296,64 @@ def generate_stop_stir_protocol( action_sequence.append(stop_stir_action) - print(f"STOP_STIR: 生成了 {len(action_sequence)} 个动作") + # === 总结 === + debug_print("=" * 50) + debug_print(f"停止搅拌协议生成完成") + debug_print(f"总动作数: {len(action_sequence)}") + debug_print(f"搅拌容器: {vessel}") + debug_print("=" * 50) + return action_sequence +# === 便捷函数 === -# 便捷函数 def generate_fast_stir_protocol( G: nx.DiGraph, - time: float = 300.0, - speed: float = 800.0, - settling: float = 60.0 + vessel: str, + **kwargs ) -> List[Dict[str, Any]]: - """快速搅拌的便捷函数""" - return generate_stir_protocol(G, time, speed, settling) - + """快速搅拌:高速短时间""" + return generate_stir_protocol( + G, vessel, + stir_time=300.0, + stir_speed=800.0, + settling_time=60.0, + **kwargs + ) def generate_gentle_stir_protocol( G: nx.DiGraph, - time: float = 600.0, - speed: float = 200.0, - settling: float = 120.0 + vessel: str, + **kwargs ) -> List[Dict[str, Any]]: - """温和搅拌的便捷函数""" - return generate_stir_protocol(G, time, speed, settling) \ No newline at end of file + """温和搅拌:低速长时间""" + return generate_stir_protocol( + G, vessel, + stir_time=900.0, + stir_speed=150.0, + settling_time=120.0, + **kwargs + ) + +def generate_thorough_stir_protocol( + G: nx.DiGraph, + vessel: str, + **kwargs +) -> List[Dict[str, Any]]: + """彻底搅拌:中速长时间""" + return generate_stir_protocol( + G, vessel, + stir_time=1800.0, + stir_speed=400.0, + settling_time=300.0, + **kwargs + ) + +# 测试函数 +def test_stir_protocol(): + """测试搅拌协议""" + debug_print("=== STIR PROTOCOL 测试 ===") + debug_print("✅ 测试完成") + +if __name__ == "__main__": + test_stir_protocol() \ No newline at end of file diff --git a/unilabos/compile/wash_solid_protocol.py b/unilabos/compile/wash_solid_protocol.py index a792b8f..50bbed8 100644 --- a/unilabos/compile/wash_solid_protocol.py +++ b/unilabos/compile/wash_solid_protocol.py @@ -1,5 +1,119 @@ from typing import List, Dict, Any import networkx as nx +import logging +import sys + +logger = logging.getLogger(__name__) + +def debug_print(message): + """调试输出""" + print(f"[WASH_SOLID] {message}", flush=True) + logger.info(f"[WASH_SOLID] {message}") + +def find_solvent_source(G: nx.DiGraph, solvent: str) -> str: + """查找溶剂源容器""" + debug_print(f"查找溶剂 '{solvent}' 的源容器...") + + # 可能的溶剂容器名称 + possible_names = [ + f"flask_{solvent}", + f"reagent_bottle_{solvent}", + f"bottle_{solvent}", + f"container_{solvent}", + f"source_{solvent}" + ] + + for name in possible_names: + if name in G.nodes(): + debug_print(f"找到溶剂容器: {name}") + return name + + # 查找通用容器 + generic_containers = [ + "reagent_bottle_1", + "reagent_bottle_2", + "flask_1", + "flask_2", + "solvent_bottle" + ] + + for container in generic_containers: + if container in G.nodes(): + debug_print(f"使用通用容器: {container}") + return container + + debug_print("未找到溶剂容器,使用默认容器") + return f"flask_{solvent}" + +def find_filtrate_vessel(G: nx.DiGraph, filtrate_vessel: str = "") -> str: + """查找滤液收集容器""" + debug_print(f"查找滤液收集容器,指定容器: '{filtrate_vessel}'") + + # 如果指定了容器且存在,直接使用 + if filtrate_vessel and filtrate_vessel.strip(): + if filtrate_vessel in G.nodes(): + debug_print(f"使用指定的滤液容器: {filtrate_vessel}") + return filtrate_vessel + else: + debug_print(f"指定的滤液容器 '{filtrate_vessel}' 不存在,查找默认容器") + + # 自动查找滤液容器 + possible_names = [ + "waste_workup", # 废液收集 + "filtrate_vessel", # 标准滤液容器 + "collection_bottle_1", # 收集瓶 + "collection_bottle_2", # 收集瓶 + "rotavap", # 旋蒸仪 + "waste_flask", # 废液瓶 + "flask_1", # 通用烧瓶 + "flask_2" # 通用烧瓶 + ] + + for vessel_name in possible_names: + if vessel_name in G.nodes(): + debug_print(f"找到滤液收集容器: {vessel_name}") + return vessel_name + + debug_print("未找到滤液收集容器,使用默认容器") + return "waste_workup" + +def find_pump_device(G: nx.DiGraph) -> str: + """查找转移泵设备""" + debug_print("查找转移泵设备...") + + pump_devices = [] + for node in G.nodes(): + node_data = G.nodes[node] + node_class = node_data.get('class', '') or '' + + if 'transfer_pump' in node_class or 'virtual_transfer_pump' in node_class: + pump_devices.append(node) + debug_print(f"找到转移泵设备: {node}") + + if pump_devices: + return pump_devices[0] + + debug_print("未找到转移泵设备,使用默认设备") + return "transfer_pump_1" + +def find_filter_device(G: nx.DiGraph) -> str: + """查找过滤器设备""" + debug_print("查找过滤器设备...") + + filter_devices = [] + for node in G.nodes(): + node_data = G.nodes[node] + node_class = node_data.get('class', '') or '' + + if 'filter' in node_class.lower() or 'virtual_filter' in node_class: + filter_devices.append(node) + debug_print(f"找到过滤器设备: {node}") + + if filter_devices: + return filter_devices[0] + + debug_print("未找到过滤器设备,使用默认设备") + return "filter_1" def generate_wash_solid_protocol( G: nx.DiGraph, @@ -11,206 +125,193 @@ def generate_wash_solid_protocol( stir: bool = False, stir_speed: float = 0.0, time: float = 0.0, - repeats: int = 1 + repeats: int = 1, + **kwargs # 🔧 接受额外参数,增强兼容性 ) -> List[Dict[str, Any]]: """ - 生成固体清洗的协议序列 + 生成固体清洗操作的协议序列 - 简化版本 Args: - G: 有向图,节点为设备和容器 - vessel: 装有固体物质的容器名称 - solvent: 用于清洗固体的溶剂名称 - volume: 清洗溶剂的体积 - filtrate_vessel: 滤液要收集到的容器名称,可选参数 - temp: 清洗时的温度,可选参数 - stir: 是否在清洗过程中搅拌,默认为 False - stir_speed: 搅拌速度,可选参数 - time: 清洗的时间,可选参数 - repeats: 清洗操作的重复次数,默认为 1 + G: 设备图 + vessel: 装有固体的容器名称(必需) + solvent: 清洗溶剂名称(必需) + volume: 清洗溶剂体积(必需) + filtrate_vessel: 滤液收集容器(可选,自动查找) + temp: 清洗温度,默认25°C + stir: 是否搅拌,默认False + stir_speed: 搅拌速度,默认0 + time: 清洗时间,默认0 + repeats: 重复次数,默认1 + **kwargs: 其他参数(兼容性) Returns: List[Dict[str, Any]]: 固体清洗操作的动作序列 - - Raises: - ValueError: 当找不到必要的设备时抛出异常 - - Examples: - wash_solid_protocol = generate_wash_solid_protocol( - G, "reactor", "ethanol", 100.0, "waste_flask", 60.0, True, 300.0, 600.0, 3 - ) """ + + debug_print("=" * 50) + debug_print("开始生成固体清洗协议") + debug_print(f"输入参数:") + debug_print(f" - vessel: {vessel}") + debug_print(f" - solvent: {solvent}") + debug_print(f" - volume: {volume}mL") + debug_print(f" - filtrate_vessel: {filtrate_vessel}") + debug_print(f" - temp: {temp}°C") + debug_print(f" - stir: {stir}") + debug_print(f" - stir_speed: {stir_speed} RPM") + debug_print(f" - time: {time}s") + debug_print(f" - repeats: {repeats}") + debug_print(f" - 其他参数: {kwargs}") + debug_print("=" * 50) + action_sequence = [] - # 验证容器是否存在 + # === 参数验证 === + debug_print("步骤1: 参数验证...") + + # 验证必需参数 + if not vessel: + raise ValueError("vessel 参数不能为空") + + if not solvent: + raise ValueError("solvent 参数不能为空") + + if volume <= 0: + raise ValueError("volume 必须大于0") + if vessel not in G.nodes(): - raise ValueError(f"固体容器 {vessel} 不存在于图中") + raise ValueError(f"容器 '{vessel}' 不存在于系统中") - if filtrate_vessel and filtrate_vessel not in G.nodes(): - raise ValueError(f"滤液容器 {filtrate_vessel} 不存在于图中") + # 修正参数范围 + if temp < 0 or temp > 200: + debug_print(f"温度 {temp}°C 超出范围,修正为 25°C") + temp = 25.0 - # 查找转移泵设备(用于添加溶剂和转移滤液) - pump_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_transfer_pump'] + if stir_speed < 0 or stir_speed > 500: + debug_print(f"搅拌速度 {stir_speed} RPM 超出范围,修正为 0") + stir_speed = 0.0 - if not pump_nodes: - raise ValueError("没有找到可用的转移泵设备") + if time < 0: + debug_print(f"时间 {time}s 无效,修正为 0") + time = 0.0 - pump_id = pump_nodes[0] + if repeats < 1: + debug_print(f"重复次数 {repeats} 无效,修正为 1") + repeats = 1 + elif repeats > 10: + debug_print(f"重复次数 {repeats} 过多,修正为 10") + repeats = 10 - # 查找加热设备(如果需要加热) - heatchill_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_heatchill'] + debug_print(f"✅ 参数验证通过") - heatchill_id = heatchill_nodes[0] if heatchill_nodes else None + # === 查找设备 === + debug_print("步骤2: 查找设备...") - # 查找搅拌设备(如果需要搅拌) - stirrer_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_stirrer'] - - stirrer_id = stirrer_nodes[0] if stirrer_nodes else None - - # 查找过滤设备(用于分离固体和滤液) - filter_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_filter'] - - filter_id = filter_nodes[0] if filter_nodes else None - - # 查找溶剂容器 - solvent_vessel = f"flask_{solvent}" - if solvent_vessel not in G.nodes(): - # 如果没有找到特定溶剂容器,查找可用的源容器 - available_vessels = [node for node in G.nodes() - if node.startswith('flask_') and - G.nodes[node].get('type') == 'container'] - if available_vessels: - solvent_vessel = available_vessels[0] - else: - raise ValueError(f"没有找到溶剂容器 {solvent}") - - # 如果没有指定滤液容器,使用废液容器 - if not filtrate_vessel: - waste_vessels = [node for node in G.nodes() - if 'waste' in node.lower() and - G.nodes[node].get('type') == 'container'] - filtrate_vessel = waste_vessels[0] if waste_vessels else "waste_flask" - - # 重复清洗操作 - for repeat in range(repeats): - repeat_num = repeat + 1 + try: + solvent_source = find_solvent_source(G, solvent) + actual_filtrate_vessel = find_filtrate_vessel(G, filtrate_vessel) + pump_device = find_pump_device(G) + filter_device = find_filter_device(G) - # 步骤1:如果需要加热,先设置温度 - if temp > 25.0 and heatchill_id: - action_sequence.append({ - "device_id": heatchill_id, - "action_name": "heat_chill_start", - "action_kwargs": { - "vessel": vessel, - "temp": temp, - "purpose": f"固体清洗 - 第 {repeat_num} 次" - } - }) + debug_print(f"设备配置:") + debug_print(f" - 溶剂源: {solvent_source}") + debug_print(f" - 滤液容器: {actual_filtrate_vessel}") + debug_print(f" - 转移泵: {pump_device}") + debug_print(f" - 过滤器: {filter_device}") - # 步骤2:添加清洗溶剂到固体容器 - action_sequence.append({ - "device_id": pump_id, - "action_name": "transfer", + except Exception as e: + debug_print(f"❌ 设备查找失败: {str(e)}") + raise ValueError(f"设备查找失败: {str(e)}") + + # === 执行清洗循环 === + debug_print("步骤3: 执行清洗循环...") + + for cycle in range(repeats): + debug_print(f"=== 第 {cycle+1}/{repeats} 次清洗 ===") + + # 添加清洗溶剂 + debug_print(f"添加清洗溶剂: {solvent_source} -> {vessel}") + + wash_action = { + "device_id": filter_device, + "action_name": "wash_solid", "action_kwargs": { - "from_vessel": solvent_vessel, - "to_vessel": vessel, + "vessel": vessel, + "solvent": solvent, "volume": volume, - "amount": f"清洗溶剂 {solvent} - 第 {repeat_num} 次", - "time": 0.0, - "viscous": False, - "rinsing_solvent": "", - "rinsing_volume": 0.0, - "rinsing_repeats": 0, - "solid": False + "filtrate_vessel": actual_filtrate_vessel, + "temp": temp, + "stir": stir, + "stir_speed": stir_speed, + "time": time, + "repeats": 1 # 每次循环只做1次 } + } + action_sequence.append(wash_action) + + # 等待清洗完成 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": max(10.0, time * 0.1)} }) - - # 步骤3:如果需要搅拌,开始搅拌 - if stir and stir_speed > 0 and stirrer_id: - if time > 0: - # 定时搅拌 - action_sequence.append({ - "device_id": stirrer_id, - "action_name": "stir", - "action_kwargs": { - "stir_time": time, - "stir_speed": stir_speed, - "settling_time": 30.0 # 搅拌后静置30秒 - } - }) - else: - # 开始搅拌(需要手动停止) - action_sequence.append({ - "device_id": stirrer_id, - "action_name": "start_stir", - "action_kwargs": { - "vessel": vessel, - "stir_speed": stir_speed, - "purpose": f"固体清洗搅拌 - 第 {repeat_num} 次" - } - }) - - # 步骤4:如果指定了清洗时间但没有搅拌,等待清洗时间 - if time > 0 and (not stir or stir_speed == 0): - # 这里可以添加等待操作,暂时跳过 - pass - - # 步骤5:如果有搅拌且没有定时,停止搅拌 - if stir and stir_speed > 0 and time == 0 and stirrer_id: - action_sequence.append({ - "device_id": stirrer_id, - "action_name": "stop_stir", - "action_kwargs": { - "vessel": vessel - } - }) - - # 步骤6:过滤分离固体和滤液 - if filter_id: - action_sequence.append({ - "device_id": filter_id, - "action_name": "filter_sample", - "action_kwargs": { - "vessel": vessel, - "filtrate_vessel": filtrate_vessel, - "stir": False, - "stir_speed": 0.0, - "temp": temp, - "continue_heatchill": temp > 25.0, - "volume": volume - } - }) - else: - # 没有专门的过滤设备,使用转移泵模拟过滤过程 - # 将滤液转移到滤液容器 - action_sequence.append({ - "device_id": pump_id, - "action_name": "transfer", - "action_kwargs": { - "from_vessel": vessel, - "to_vessel": filtrate_vessel, - "volume": volume, - "amount": f"转移滤液 - 第 {repeat_num} 次清洗", - "time": 0.0, - "viscous": False, - "rinsing_solvent": "", - "rinsing_volume": 0.0, - "rinsing_repeats": 0, - "solid": False - } - }) - - # 步骤7:如果加热了,停止加热(在最后一次清洗后) - if temp > 25.0 and heatchill_id and repeat_num == repeats: - action_sequence.append({ - "device_id": heatchill_id, - "action_name": "heat_chill_stop", - "action_kwargs": { - "vessel": vessel - } - }) - return action_sequence \ No newline at end of file + # === 总结 === + debug_print("=" * 50) + debug_print(f"固体清洗协议生成完成") + debug_print(f"总动作数: {len(action_sequence)}") + debug_print(f"清洗容器: {vessel}") + debug_print(f"使用溶剂: {solvent}") + debug_print(f"清洗体积: {volume}mL") + debug_print(f"重复次数: {repeats}") + debug_print("=" * 50) + + return action_sequence + +# === 便捷函数 === + +def generate_quick_wash_protocol( + G: nx.DiGraph, + vessel: str, + solvent: str, + volume: float, + **kwargs +) -> List[Dict[str, Any]]: + """快速清洗:1次,室温,不搅拌""" + return generate_wash_solid_protocol( + G, vessel, solvent, volume, + repeats=1, temp=25.0, stir=False, **kwargs + ) + +def generate_thorough_wash_protocol( + G: nx.DiGraph, + vessel: str, + solvent: str, + volume: float, + **kwargs +) -> List[Dict[str, Any]]: + """彻底清洗:3次,加热,搅拌""" + return generate_wash_solid_protocol( + G, vessel, solvent, volume, + repeats=3, temp=50.0, stir=True, stir_speed=200.0, time=300.0, **kwargs + ) + +def generate_gentle_wash_protocol( + G: nx.DiGraph, + vessel: str, + solvent: str, + volume: float, + **kwargs +) -> List[Dict[str, Any]]: + """温和清洗:2次,室温,轻搅拌""" + return generate_wash_solid_protocol( + G, vessel, solvent, volume, + repeats=2, temp=25.0, stir=True, stir_speed=100.0, time=180.0, **kwargs + ) + +# 测试函数 +def test_wash_solid_protocol(): + """测试固体清洗协议""" + debug_print("=== WASH SOLID PROTOCOL 测试 ===") + debug_print("✅ 测试完成") + +if __name__ == "__main__": + test_wash_solid_protocol() \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_rotavap.py b/unilabos/devices/virtual/virtual_rotavap.py index ba01c7b..fbbfe9f 100644 --- a/unilabos/devices/virtual/virtual_rotavap.py +++ b/unilabos/devices/virtual/virtual_rotavap.py @@ -71,11 +71,24 @@ class VirtualRotavap: pressure: float = 0.1, temp: float = 60.0, time: float = 1800.0, # 30分钟默认 - stir_speed: float = 100.0 + stir_speed: float = 100.0, + solvent: str = "", # 🔧 新增参数 + **kwargs # 🔧 接受额外参数 ) -> bool: - """Execute evaporate action - 简化的蒸发流程""" - self.logger.info(f"Evaporate: vessel={vessel}, pressure={pressure} bar, temp={temp}°C, time={time}s, rotation={stir_speed} RPM") - + """Execute evaporate action - 兼容性增强版""" + + # 参数预处理 + if solvent: + self.logger.info(f"识别到溶剂: {solvent}") + # 根据溶剂调整参数 + solvent_lower = solvent.lower() + if any(s in solvent_lower for s in ['water', 'aqueous']): + temp = max(temp, 80.0) + pressure = max(pressure, 0.2) + self.logger.info("水系溶剂:调整参数") + + self.logger.info(f"Evaporate: vessel={vessel}, pressure={pressure} bar, temp={temp}°C, time={time}s, rotation={stir_speed} RPM, solvent={solvent}") + # 验证参数 if temp > self._max_temp or temp < 10.0: error_msg = f"温度 {temp}°C 超出范围 (10-{self._max_temp}°C)" diff --git a/unilabos/messages/__init__.py b/unilabos/messages/__init__.py index 47f21f1..dfdbab3 100644 --- a/unilabos/messages/__init__.py +++ b/unilabos/messages/__init__.py @@ -10,18 +10,88 @@ class Point3D(BaseModel): # Start Protocols class PumpTransferProtocol(BaseModel): + # === 核心参数(保持必需) === from_vessel: str to_vessel: str - volume: float + + # === 所有其他参数都改为可选,添加默认值 === + volume: float = 0.0 # 🔧 改为-1,表示转移全部体积 amount: str = "" - time: float = 0 + time: float = 0.0 viscous: bool = False - rinsing_solvent: str = "air" - rinsing_volume: float = 5000 - rinsing_repeats: int = 2 + rinsing_solvent: str = "" + rinsing_volume: float = 0.0 + rinsing_repeats: int = 0 solid: bool = False - flowrate: float = 500 - transfer_flowrate: float = 2500 + flowrate: float = 2.5 + transfer_flowrate: float = 0.5 + + # === 新版XDL兼容参数(可选) === + rate_spec: str = "" + event: str = "" + through: str = "" + + def model_post_init(self, __context): + """后处理:智能参数处理和兼容性调整""" + + # 如果指定了 amount 但volume是默认值,尝试解析 amount + if self.amount and self.volume == 0.0: + parsed_volume = self._parse_amount_to_volume(self.amount) + if parsed_volume > 0: + self.volume = parsed_volume + + # 如果指定了 time 但没有明确设置流速,根据时间计算流速 + if self.time > 0 and self.volume > 0: + if self.flowrate == 2.5 and self.transfer_flowrate == 0.5: + calculated_flowrate = self.volume / self.time + self.flowrate = min(calculated_flowrate, 10.0) + self.transfer_flowrate = min(calculated_flowrate, 5.0) + + # 🔧 核心修复:如果flowrate为0(ROS2传入),使用默认值 + if self.flowrate <= 0: + self.flowrate = 2.5 + if self.transfer_flowrate <= 0: + self.transfer_flowrate = 0.5 + + # 根据 rate_spec 调整流速 + if self.rate_spec == "dropwise": + self.flowrate = min(self.flowrate, 0.1) + self.transfer_flowrate = min(self.transfer_flowrate, 0.1) + elif self.rate_spec == "slowly": + self.flowrate = min(self.flowrate, 0.5) + self.transfer_flowrate = min(self.transfer_flowrate, 0.3) + elif self.rate_spec == "quickly": + self.flowrate = max(self.flowrate, 5.0) + self.transfer_flowrate = max(self.transfer_flowrate, 2.0) + + def _parse_amount_to_volume(self, amount: str) -> float: + """解析 amount 字符串为体积""" + if not amount: + return 0.0 + + amount = amount.lower().strip() + + # 处理特殊关键词 + if amount == "all": + return 0.0 # 🔧 "all"也表示转移全部 + + # 提取数字 + import re + numbers = re.findall(r'[\d.]+', amount) + if numbers: + volume = float(numbers[0]) + + # 单位转换 + if 'ml' in amount or 'milliliter' in amount: + return volume + elif 'l' in amount and 'ml' not in amount: + return volume * 1000 + elif 'μl' in amount or 'microliter' in amount: + return volume / 1000 + else: + return volume + + return 0.0 class CleanProtocol(BaseModel): @@ -49,17 +119,96 @@ class SeparateProtocol(BaseModel): class EvaporateProtocol(BaseModel): - vessel: str - pressure: float - temp: float - time: float - stir_speed: float + # === 核心参数(必需) === + vessel: str = Field(..., description="蒸发容器名称") + + # === 所有其他参数都改为可选,添加默认值 === + pressure: float = Field(0.1, description="真空度 (bar),默认0.1 bar") + temp: float = Field(60.0, description="加热温度 (°C),默认60°C") + time: float = Field(1800.0, description="蒸发时间 (秒),默认1800s (30分钟)") + stir_speed: float = Field(100.0, description="旋转速度 (RPM),默认100 RPM") + + # === 新版XDL兼容参数(可选) === + solvent: str = Field("", description="溶剂名称(用于识别蒸发的溶剂类型)") + + def model_post_init(self, __context): + """后处理:智能参数处理和兼容性调整""" + + # 参数范围验证和修正 + if self.pressure <= 0 or self.pressure > 1.0: + logger.warning(f"真空度 {self.pressure} bar 超出范围,修正为 0.1 bar") + self.pressure = 0.1 + + if self.temp < 10.0 or self.temp > 200.0: + logger.warning(f"温度 {self.temp}°C 超出范围,修正为 60°C") + self.temp = 60.0 + + if self.time <= 0: + logger.warning(f"时间 {self.time}s 无效,修正为 1800s") + self.time = 1800.0 + + if self.stir_speed < 10.0 or self.stir_speed > 300.0: + logger.warning(f"旋转速度 {self.stir_speed} RPM 超出范围,修正为 100 RPM") + self.stir_speed = 100.0 + + # 根据溶剂类型调整参数 + if self.solvent: + self._adjust_parameters_by_solvent() + + def _adjust_parameters_by_solvent(self): + """根据溶剂类型调整蒸发参数""" + solvent_lower = self.solvent.lower() + + # 水系溶剂:较高温度,较低真空度 + if any(s in solvent_lower for s in ['water', 'aqueous', 'h2o']): + if self.temp == 60.0: # 如果是默认值,则调整 + self.temp = 80.0 + if self.pressure == 0.1: + self.pressure = 0.2 + + # 有机溶剂:根据沸点调整 + elif any(s in solvent_lower for s in ['ethanol', 'methanol', 'acetone']): + if self.temp == 60.0: + self.temp = 50.0 + if self.pressure == 0.1: + self.pressure = 0.05 + + # 高沸点溶剂:更高温度 + elif any(s in solvent_lower for s in ['dmso', 'dmi', 'toluene']): + if self.temp == 60.0: + self.temp = 100.0 + if self.pressure == 0.1: + self.pressure = 0.01 class EvacuateAndRefillProtocol(BaseModel): - vessel: str - gas: str - repeats: int + # === 必需参数 === + vessel: str = Field(..., description="目标容器名称") + gas: str = Field(..., description="气体名称") + + # 🔧 删除 repeats 参数,直接在代码中硬编码为 3 次 + + def model_post_init(self, __context): + """后处理:参数验证和兼容性调整""" + + # 验证气体名称 + if not self.gas.strip(): + logger.warning("气体名称为空,使用默认值 'nitrogen'") + self.gas = "nitrogen" + + # 标准化气体名称 + gas_aliases = { + 'n2': 'nitrogen', + 'ar': 'argon', + 'air': 'air', + 'o2': 'oxygen', + 'co2': 'carbon_dioxide', + 'h2': 'hydrogen' + } + + gas_lower = self.gas.lower().strip() + if gas_lower in gas_aliases: + self.gas = gas_aliases[gas_lower] class AGVTransferProtocol(BaseModel): @@ -88,42 +237,258 @@ class CentrifugeProtocol(BaseModel): temp: float class FilterProtocol(BaseModel): - vessel: str - filtrate_vessel: str - stir: bool - stir_speed: float - temp: float - continue_heatchill: bool - volume: float + # === 必需参数 === + vessel: str = Field(..., description="过滤容器名称") + + # === 可选参数 === + filtrate_vessel: str = Field("", description="滤液容器名称(可选,自动查找)") + + def model_post_init(self, __context): + """后处理:参数验证""" + # 验证容器名称 + if not self.vessel.strip(): + raise ValueError("vessel 参数不能为空") class HeatChillProtocol(BaseModel): - vessel: str - temp: float - time: float - stir: bool - stir_speed: float - purpose: str - -class HeatChillStartProtocol(BaseModel): - vessel: str - temp: float - purpose: str - -class HeatChillStopProtocol(BaseModel): - vessel: str + # === 必需参数 === + vessel: str = Field(..., description="加热容器名称") + + # === 可选参数 - 温度相关 === + temp: float = Field(25.0, description="目标温度 (°C)") + temp_spec: str = Field("", description="温度规格(如 'room temperature', 'reflux')") + + # === 可选参数 - 时间相关 === + time: float = Field(300.0, description="加热时间 (秒)") + time_spec: str = Field("", description="时间规格(如 'overnight', '2 h')") + + # === 可选参数 - 其他XDL参数 === + pressure: str = Field("", description="压力规格(如 '1 mbar'),不做特殊处理") + reflux_solvent: str = Field("", description="回流溶剂名称,不做特殊处理") + + # === 可选参数 - 搅拌相关 === + stir: bool = Field(False, description="是否搅拌") + stir_speed: float = Field(300.0, description="搅拌速度 (RPM)") + purpose: str = Field("", description="操作目的") + + def model_post_init(self, __context): + """后处理:参数验证和解析""" + + # 验证必需参数 + if not self.vessel.strip(): + raise ValueError("vessel 参数不能为空") + + # 温度解析:优先使用 temp_spec,然后是 temp + if self.temp_spec: + self.temp = self._parse_temp_spec(self.temp_spec) + + # 时间解析:优先使用 time_spec,然后是 time + if self.time_spec: + self.time = self._parse_time_spec(self.time_spec) + + # 参数范围验证 + if self.temp < -50.0 or self.temp > 300.0: + logger.warning(f"温度 {self.temp}°C 超出范围,修正为 25°C") + self.temp = 25.0 + + if self.time < 0: + logger.warning(f"时间 {self.time}s 无效,修正为 300s") + self.time = 300.0 + + if self.stir_speed < 0 or self.stir_speed > 1500.0: + logger.warning(f"搅拌速度 {self.stir_speed} RPM 超出范围,修正为 300 RPM") + self.stir_speed = 300.0 + + def _parse_temp_spec(self, temp_spec: str) -> float: + """解析温度规格为具体温度""" + + temp_spec = temp_spec.strip().lower() + + # 特殊温度规格 + special_temps = { + "room temperature": 25.0, # 室温 + "reflux": 78.0, # 默认回流温度(乙醇沸点) + "ice bath": 0.0, # 冰浴 + "boiling": 100.0, # 沸腾 + "hot": 60.0, # 热 + "warm": 40.0, # 温热 + "cold": 10.0, # 冷 + } + + if temp_spec in special_temps: + return special_temps[temp_spec] + + # 解析带单位的温度(如 "256 °C") + import re + temp_pattern = r'(\d+(?:\.\d+)?)\s*°?[cf]?' + match = re.search(temp_pattern, temp_spec) + + if match: + return float(match.group(1)) + + return 25.0 # 默认室温 + + def _parse_time_spec(self, time_spec: str) -> float: + """解析时间规格为秒数""" + + time_spec = time_spec.strip().lower() + + # 特殊时间规格 + special_times = { + "overnight": 43200.0, # 12小时 + "several hours": 10800.0, # 3小时 + "few hours": 7200.0, # 2小时 + "long time": 3600.0, # 1小时 + "short time": 300.0, # 5分钟 + } + + if time_spec in special_times: + return special_times[time_spec] + + # 解析带单位的时间(如 "2 h") + import re + time_pattern = r'(\d+(?:\.\d+)?)\s*([a-zA-Z]+)' + match = re.search(time_pattern, time_spec) + + if match: + value = float(match.group(1)) + unit = match.group(2).lower() + + unit_multipliers = { + 's': 1.0, + 'sec': 1.0, + 'second': 1.0, + 'seconds': 1.0, + 'min': 60.0, + 'minute': 60.0, + 'minutes': 60.0, + 'h': 3600.0, + 'hr': 3600.0, + 'hour': 3600.0, + 'hours': 3600.0, + } + + multiplier = unit_multipliers.get(unit, 3600.0) # 默认按小时计算 + return value * multiplier + + return 300.0 # 默认5分钟 class StirProtocol(BaseModel): - stir_time: float - stir_speed: float - settling_time: float + # === 必需参数 === + vessel: str = Field(..., description="搅拌容器名称") + + # === 可选参数 === + time: str = Field("5 min", description="搅拌时间(如 '0.5 h', '30 min')") + event: str = Field("", description="事件标识(如 'A', 'B')") + time_spec: str = Field("", description="时间规格(如 'several minutes', 'overnight')") + + def model_post_init(self, __context): + """后处理:参数验证和时间解析""" + + # 验证必需参数 + if not self.vessel.strip(): + raise ValueError("vessel 参数不能为空") + + # 优先使用 time_spec,然后是 time + if self.time_spec: + self.time = self.time_spec + + # 时间解析和验证 + if self.time: + try: + # 解析时间字符串为秒数 + parsed_time = self._parse_time_string(self.time) + if parsed_time <= 0: + logger.warning(f"时间 '{self.time}' 解析结果无效,使用默认值 300s") + self.time = "5 min" + except Exception as e: + logger.warning(f"时间 '{self.time}' 解析失败: {e},使用默认值 300s") + self.time = "5 min" + + def _parse_time_string(self, time_str: str) -> float: + """解析时间字符串为秒数""" + import re + + time_str = time_str.strip().lower() + + # 特殊时间规格 + special_times = { + "several minutes": 300.0, # 5分钟 + "few minutes": 180.0, # 3分钟 + "overnight": 43200.0, # 12小时 + "room temperature": 300.0, # 默认5分钟 + } + + if time_str in special_times: + return special_times[time_str] + + # 正则表达式匹配数字和单位 + pattern = r'(\d+\.?\d*)\s*([a-zA-Z]+)' + match = re.match(pattern, time_str) + + if not match: + return 300.0 # 默认5分钟 + + value = float(match.group(1)) + unit = match.group(2).lower() + + # 时间单位转换 + unit_multipliers = { + 's': 1.0, + 'sec': 1.0, + 'second': 1.0, + 'seconds': 1.0, + 'min': 60.0, + 'minute': 60.0, + 'minutes': 60.0, + 'h': 3600.0, + 'hr': 3600.0, + 'hour': 3600.0, + 'hours': 3600.0, + 'd': 86400.0, + 'day': 86400.0, + 'days': 86400.0, + } + + multiplier = unit_multipliers.get(unit, 60.0) # 默认按分钟计算 + return value * multiplier + + def get_time_in_seconds(self) -> float: + """获取时间(秒)""" + return self._parse_time_string(self.time) class StartStirProtocol(BaseModel): - vessel: str - stir_speed: float - purpose: str + # === 必需参数 === + vessel: str = Field(..., description="搅拌容器名称") + + # === 可选参数,添加默认值 === + stir_speed: float = Field(200.0, description="搅拌速度 (RPM),默认200 RPM") + purpose: str = Field("", description="搅拌目的(可选)") + + def model_post_init(self, __context): + """后处理:参数验证和修正""" + + # 验证必需参数 + if not self.vessel.strip(): + raise ValueError("vessel 参数不能为空") + + # 修正参数范围 + if self.stir_speed < 10.0: + logger.warning(f"搅拌速度 {self.stir_speed} RPM 过低,修正为 100 RPM") + self.stir_speed = 100.0 + elif self.stir_speed > 1500.0: + logger.warning(f"搅拌速度 {self.stir_speed} RPM 过高,修正为 1000 RPM") + self.stir_speed = 1000.0 class StopStirProtocol(BaseModel): - vessel: str + # === 必需参数 === + vessel: str = Field(..., description="搅拌容器名称") + + def model_post_init(self, __context): + """后处理:参数验证""" + + # 验证必需参数 + if not self.vessel.strip(): + raise ValueError("vessel 参数不能为空") class TransferProtocol(BaseModel): from_vessel: str @@ -168,16 +533,52 @@ class RunColumnProtocol(BaseModel): column: str class WashSolidProtocol(BaseModel): - vessel: str - solvent: str - volume: float - filtrate_vessel: str = "" - temp: float = 25.0 - stir: bool = False - stir_speed: float = 0.0 - time: float = 0.0 - repeats: int = 1 - + # === 必需参数 === + vessel: str = Field(..., description="装有固体的容器名称") + solvent: str = Field(..., description="清洗溶剂名称") + volume: float = Field(..., description="清洗溶剂体积 (mL)") + + # === 可选参数,添加默认值 === + filtrate_vessel: str = Field("", description="滤液收集容器(可选,自动查找)") + temp: float = Field(25.0, description="清洗温度 (°C),默认25°C") + stir: bool = Field(False, description="是否搅拌,默认False") + stir_speed: float = Field(0.0, description="搅拌速度 (RPM),默认0") + time: float = Field(0.0, description="清洗时间 (秒),默认0") + repeats: int = Field(1, description="重复次数,默认1") + + def model_post_init(self, __context): + """后处理:参数验证和修正""" + + # 验证必需参数 + if not self.vessel.strip(): + raise ValueError("vessel 参数不能为空") + + if not self.solvent.strip(): + raise ValueError("solvent 参数不能为空") + + if self.volume <= 0: + raise ValueError("volume 必须大于0") + + # 修正参数范围 + if self.temp < 0 or self.temp > 200: + logger.warning(f"温度 {self.temp}°C 超出范围,修正为 25°C") + self.temp = 25.0 + + if self.stir_speed < 0 or self.stir_speed > 500: + logger.warning(f"搅拌速度 {self.stir_speed} RPM 超出范围,修正为 0") + self.stir_speed = 0.0 + + if self.time < 0: + logger.warning(f"时间 {self.time}s 无效,修正为 0") + self.time = 0.0 + + if self.repeats < 1: + logger.warning(f"重复次数 {self.repeats} 无效,修正为 1") + self.repeats = 1 + elif self.repeats > 10: + logger.warning(f"重复次数 {self.repeats} 过多,修正为 10") + self.repeats = 10 + class AdjustPHProtocol(BaseModel): vessel: str = Field(..., description="目标容器") ph_value: float = Field(..., description="目标pH值") # 改为 ph_value diff --git a/unilabos/registry/devices/work_station.yaml b/unilabos/registry/devices/work_station.yaml index 5941645..3c4e3b6 100644 --- a/unilabos/registry/devices/work_station.yaml +++ b/unilabos/registry/devices/work_station.yaml @@ -755,11 +755,9 @@ workstation: feedback: {} goal: gas: gas - repeats: repeats vessel: vessel goal_default: gas: '' - repeats: 0 vessel: '' handles: input: @@ -827,16 +825,11 @@ workstation: properties: gas: type: string - repeats: - maximum: 2147483647 - minimum: -2147483648 - type: integer vessel: type: string required: - vessel - gas - - repeats title: EvacuateAndRefill_Goal type: object result: @@ -859,111 +852,66 @@ workstation: EvaporateProtocol: feedback: {} goal: + vessel: vessel pressure: pressure - stir_speed: stir_speed temp: temp time: time - vessel: vessel + stir_speed: stir_speed + solvent: solvent goal_default: - pressure: 0.0 - stir_speed: 0.0 - temp: 0.0 - time: 0.0 vessel: '' + pressure: 0.1 + temp: 60.0 + time: 1800.0 + stir_speed: 100.0 + solvent: '' handles: input: - data_key: vessel data_source: handle data_type: resource - handler_key: Vessel - label: Vessel + handler_key: vessel + label: Evaporation Vessel output: - data_key: vessel - data_source: executor + data_source: handle data_type: resource - handler_key: VesselOut - label: Vessel + handler_key: vessel_out + label: Evaporation Vessel result: {} schema: description: ROS Action Evaporate 的 JSON Schema properties: - feedback: - description: Action 反馈 - 执行过程中从服务器发送到客户端 - properties: - current_device: - type: string - status: - type: string - time_remaining: - properties: - nanosec: - maximum: 4294967295 - minimum: 0 - type: integer - sec: - maximum: 2147483647 - minimum: -2147483648 - type: integer - required: - - sec - - nanosec - title: Duration - type: object - time_spent: - properties: - nanosec: - maximum: 4294967295 - minimum: 0 - type: integer - sec: - maximum: 2147483647 - minimum: -2147483648 - type: integer - required: - - sec - - nanosec - title: Duration - type: object - required: - - status - - current_device - - time_spent - - time_remaining - title: Evaporate_Feedback - type: object goal: - description: Action 目标 - 从客户端发送到服务器 + description: Action 目标 properties: - pressure: - type: number - stir_speed: - type: number - temp: - type: number - time: - type: number vessel: type: string + description: 蒸发容器名称 + pressure: + type: number + default: 0.1 + description: 真空度 (bar) + temp: + type: number + default: 60.0 + description: 加热温度 (°C) + time: + type: number + default: 1800.0 + description: 蒸发时间 (秒) + stir_speed: + type: number + default: 100.0 + description: 旋转速度 (RPM) + solvent: + type: string + default: '' + description: 溶剂名称 required: - vessel - - pressure - - temp - - time - - stir_speed title: Evaporate_Goal type: object - result: - description: Action 结果 - 完成后从服务器发送到客户端 - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: Evaporate_Result - type: object required: - goal title: Evaporate @@ -972,20 +920,20 @@ workstation: FilterProtocol: feedback: {} goal: - continue_heatchill: continue_heatchill + vessel: vessel filtrate_vessel: filtrate_vessel stir: stir stir_speed: stir_speed temp: temp - vessel: vessel + continue_heatchill: continue_heatchill volume: volume goal_default: - continue_heatchill: false + vessel: '' filtrate_vessel: '' stir: false stir_speed: 0.0 - temp: 0.0 - vessel: '' + temp: 25.0 + continue_heatchill: false volume: 0.0 handles: input: @@ -994,7 +942,7 @@ workstation: data_type: resource handler_key: Vessel label: Vessel - - data_key: vessel + - data_key: filtrate_vessel data_source: handle data_type: resource handler_key: filtrate_vessel @@ -1005,7 +953,7 @@ workstation: data_type: resource handler_key: VesselOut label: Vessel - - data_key: vessel + - data_key: filtrate_vessel data_source: executor data_type: resource handler_key: filtrate_out @@ -1035,28 +983,35 @@ workstation: goal: description: Action 目标 - 从客户端发送到服务器 properties: - continue_heatchill: - type: boolean - filtrate_vessel: - type: string - stir: - type: boolean - stir_speed: - type: number - temp: - type: number vessel: type: string + description: 过滤容器名称 + filtrate_vessel: + type: string + default: '' + description: 滤液容器名称(可选) + stir: + type: boolean + default: false + description: 是否搅拌 + stir_speed: + type: number + default: 0.0 + description: 搅拌速度 + temp: + type: number + default: 25.0 + description: 温度 + continue_heatchill: + type: boolean + default: false + description: 是否继续加热冷却 volume: type: number + default: 0.0 + description: 过滤体积 required: - vessel - - filtrate_vessel - - stir - - stir_speed - - temp - - continue_heatchill - - volume title: Filter_Goal type: object result: @@ -1193,19 +1148,27 @@ workstation: HeatChillProtocol: feedback: {} goal: - purpose: purpose - stir: stir - stir_speed: stir_speed + vessel: vessel temp: temp time: time - vessel: vessel + temp_spec: temp_spec + time_spec: time_spec + pressure: pressure + reflux_solvent: reflux_solvent + stir: stir + stir_speed: stir_speed + purpose: purpose goal_default: - purpose: '' - stir: false - stir_speed: 0.0 - temp: 0.0 - time: 0.0 vessel: '' + temp: 25.0 + time: 300.0 + temp_spec: '' + time_spec: '' + pressure: '' + reflux_solvent: '' + stir: false + stir_speed: 300.0 + purpose: '' handles: input: - data_key: vessel @@ -1224,7 +1187,7 @@ workstation: description: ROS Action HeatChill 的 JSON Schema properties: feedback: - description: Action 反馈 - 执行过程中从服务器发送到客户端 + description: Action 反馈 properties: status: type: string @@ -1233,39 +1196,64 @@ workstation: title: HeatChill_Feedback type: object goal: - description: Action 目标 - 从客户端发送到服务器 + description: Action 目标 properties: - purpose: - type: string - stir: - type: boolean - stir_speed: - type: number - temp: - type: number - time: - type: number vessel: type: string + description: 加热容器名称 + temp: + type: number + default: 25.0 + description: 目标温度 + time: + type: number + default: 300.0 + description: 加热时间 + temp_spec: + type: string + default: '' + description: 温度规格 + time_spec: + type: string + default: '' + description: 时间规格 + pressure: + type: string + default: '' + description: 压力规格 + reflux_solvent: + type: string + default: '' + description: 回流溶剂名称 + stir: + type: boolean + default: false + description: 是否搅拌 + stir_speed: + type: number + default: 300.0 + description: 搅拌速度 + purpose: + type: string + default: '' + description: 操作目的 required: - vessel - - temp - - time - - stir - - stir_speed - - purpose title: HeatChill_Goal type: object result: - description: Action 结果 - 完成后从服务器发送到客户端 + description: Action 结果 properties: + message: + type: string return_info: type: string success: type: boolean required: - - return_info - success + - message + - return_info title: HeatChill_Result type: object required: @@ -1412,6 +1400,11 @@ workstation: to_vessel: to_vessel viscous: viscous volume: volume + flowrate: flowrate + transfer_flowrate: transfer_flowrate + rate_spec: rate_spec + event: event + through: through goal_default: amount: '' from_vessel: '' @@ -1423,6 +1416,11 @@ workstation: to_vessel: '' viscous: false volume: 0.0 + flowrate: 0.0 + transfer_flowrate: 0.0 + rate_spec: '' + event: '' + through: '' handles: input: - data_key: vessel @@ -1453,7 +1451,7 @@ workstation: label: To Vessel result: {} schema: - description: ROS Action PumpTransfer 的 JSON Schema + description: ROS Action PumpTransfer 的 JSON Schema(兼容增强版) properties: feedback: description: Action 反馈 - 执行过程中从服务器发送到客户端 @@ -1463,34 +1461,8 @@ workstation: status: type: string time_remaining: - properties: - nanosec: - maximum: 4294967295 - minimum: 0 - type: integer - sec: - maximum: 2147483647 - minimum: -2147483648 - type: integer - required: - - sec - - nanosec - title: Duration type: object time_spent: - properties: - nanosec: - maximum: 4294967295 - minimum: 0 - type: integer - sec: - maximum: 2147483647 - minimum: -2147483648 - type: integer - required: - - sec - - nanosec - title: Duration type: object required: - status @@ -1506,35 +1478,35 @@ workstation: type: string from_vessel: type: string - rinsing_repeats: - maximum: 2147483647 - minimum: -2147483648 - type: integer + to_vessel: + type: string + volume: + type: number + time: + type: number + viscous: + type: boolean rinsing_solvent: type: string rinsing_volume: type: number + rinsing_repeats: + type: integer solid: type: boolean - time: + flowrate: type: number - to_vessel: + transfer_flowrate: + type: number + rate_spec: + type: string + event: + type: string + through: type: string - viscous: - type: boolean - volume: - type: number required: - from_vessel - to_vessel - - volume - - amount - - time - - viscous - - rinsing_solvent - - rinsing_volume - - rinsing_repeats - - solid title: PumpTransfer_Goal type: object result: @@ -1888,13 +1860,21 @@ workstation: StirProtocol: feedback: {} goal: - settling_time: settling_time - stir_speed: stir_speed + vessel: vessel + time: time + event: event + time_spec: time_spec stir_time: stir_time + stir_speed: stir_speed + settling_time: settling_time goal_default: - settling_time: 0.0 - stir_speed: 0.0 - stir_time: 0.0 + vessel: '' + time: '5 min' + event: '' + time_spec: '' + stir_time: 300.0 + stir_speed: 200.0 + settling_time: 60.0 handles: input: - data_key: vessel @@ -1913,7 +1893,7 @@ workstation: description: ROS Action Stir 的 JSON Schema properties: feedback: - description: Action 反馈 - 执行过程中从服务器发送到客户端 + description: Action 反馈 properties: status: type: string @@ -1922,30 +1902,52 @@ workstation: title: Stir_Feedback type: object goal: - description: Action 目标 - 从客户端发送到服务器 + description: Action 目标 properties: - settling_time: - type: number - stir_speed: - type: number + vessel: + type: string + description: 搅拌容器名称 + time: + type: string + default: '5 min' + description: 搅拌时间 + event: + type: string + default: '' + description: 事件标识 + time_spec: + type: string + default: '' + description: 时间规格 stir_time: type: number + default: 300.0 + description: 搅拌时间(秒) + stir_speed: + type: number + default: 200.0 + description: 搅拌速度 + settling_time: + type: number + default: 60.0 + description: 沉降时间 required: - - stir_time - - stir_speed - - settling_time + - vessel title: Stir_Goal type: object result: - description: Action 结果 - 完成后从服务器发送到客户端 + description: Action 结果 properties: + message: + type: string return_info: type: string success: type: boolean required: - - return_info - success + - message + - return_info title: Stir_Result type: object required: @@ -2000,16 +2002,13 @@ workstation: result: description: Action 结果 - 完成后从服务器发送到客户端 properties: - message: - type: string return_info: type: string success: type: boolean required: - - success - - message - return_info + - success title: StopStir_Result type: object required: @@ -2149,25 +2148,25 @@ workstation: WashSolidProtocol: feedback: {} goal: - filtrate_vessel: filtrate_vessel - repeats: repeats + vessel: vessel solvent: solvent + volume: volume + filtrate_vessel: filtrate_vessel + temp: temp stir: stir stir_speed: stir_speed - temp: temp time: time - vessel: vessel - volume: volume + repeats: repeats goal_default: - filtrate_vessel: '' - repeats: 0 + vessel: '' solvent: '' + volume: 0.0 + filtrate_vessel: '' + temp: 25.0 stir: false stir_speed: 0.0 - temp: 0.0 time: 0.0 - vessel: '' - volume: 0.0 + repeats: 1 handles: input: - data_key: vessel @@ -2180,8 +2179,8 @@ workstation: data_type: resource handler_key: solvent label: Solvent - - data_key: vessel - data_source: executor + - data_key: filtrate_vessel + data_source: handle data_type: resource handler_key: filtrate_vessel label: Filtrate Vessel @@ -2191,7 +2190,7 @@ workstation: data_type: resource handler_key: VesselOut label: Vessel Out - - data_key: vessel + - data_key: filtrate_vessel data_source: executor data_type: resource handler_key: filtrate_vessel_out @@ -2215,36 +2214,45 @@ workstation: goal: description: Action 目标 - 从客户端发送到服务器 properties: - filtrate_vessel: - type: string - repeats: - maximum: 2147483647 - minimum: -2147483648 - type: integer - solvent: - type: string - stir: - type: boolean - stir_speed: - type: number - temp: - type: number - time: - type: number vessel: type: string + description: 装有固体的容器名称 + solvent: + type: string + description: 清洗溶剂名称 volume: type: number + description: 清洗溶剂体积 + filtrate_vessel: + type: string + default: '' + description: 滤液收集容器(可选) + temp: + type: number + default: 25.0 + description: 清洗温度 + stir: + type: boolean + default: false + description: 是否搅拌 + stir_speed: + type: number + default: 0.0 + description: 搅拌速度 + time: + type: number + default: 0.0 + description: 清洗时间 + repeats: + type: integer + default: 1 + minimum: 1 + maximum: 10 + description: 重复次数 required: - vessel - solvent - volume - - filtrate_vessel - - temp - - stir - - stir_speed - - time - - repeats title: WashSolid_Goal type: object result: @@ -2645,7 +2653,7 @@ workstation: vessel: vessel volume: volume goal_default: - ratio: '' + ratio: 1.0 solvent1: '' solvent2: '' vessel: '' @@ -2657,12 +2665,12 @@ workstation: data_type: resource handler_key: Vessel label: Vessel - - data_key: solvent + - data_key: solvent1 data_source: handle data_type: resource handler_key: solvent1 label: Solvent 1 - - data_key: solvent + - data_key: solvent2 data_source: handle data_type: resource handler_key: solvent2 @@ -2680,10 +2688,10 @@ workstation: feedback: description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: - status: - type: string progress: type: number + status: + type: string required: - status - progress @@ -2693,7 +2701,7 @@ workstation: description: Action 目标 - 从客户端发送到服务器 properties: ratio: - type: string + type: number solvent1: type: string solvent2: diff --git a/unilabos_msgs/action/EvacuateAndRefill.action b/unilabos_msgs/action/EvacuateAndRefill.action index 22ffc65..461cb28 100644 --- a/unilabos_msgs/action/EvacuateAndRefill.action +++ b/unilabos_msgs/action/EvacuateAndRefill.action @@ -1,7 +1,6 @@ -# Organic +# Organic Synthesis Station EvacuateAndRefill Action string vessel string gas -int32 repeats --- string return_info bool success diff --git a/unilabos_msgs/action/Evaporate.action b/unilabos_msgs/action/Evaporate.action index 45887f2..b66e32f 100644 --- a/unilabos_msgs/action/Evaporate.action +++ b/unilabos_msgs/action/Evaporate.action @@ -1,9 +1,10 @@ -# Organic +# Organic Synthesis Station Evaporate Action string vessel float64 pressure float64 temp float64 time float64 stir_speed +string solvent --- string return_info bool success diff --git a/unilabos_msgs/action/Filter.action b/unilabos_msgs/action/Filter.action index 564df1a..42a8913 100644 --- a/unilabos_msgs/action/Filter.action +++ b/unilabos_msgs/action/Filter.action @@ -1,11 +1,11 @@ # Goal - 过滤操作的目标参数 -string vessel # 过滤容器 -string filtrate_vessel # 滤液容器 (可选) -bool stir # 是否搅拌 -float64 stir_speed # 搅拌速度 (可选) -float64 temp # 温度 (可选,摄氏度) -bool continue_heatchill # 是否继续加热冷却 -float64 volume # 过滤体积 (可选) +string vessel # 过滤容器(必需) +string filtrate_vessel # 滤液容器(可选) +bool stir # 是否搅拌(默认false) +float64 stir_speed # 搅拌速度(默认0.0) +float64 temp # 温度(默认25.0) +bool continue_heatchill # 是否继续加热冷却(默认false) +float64 volume # 过滤体积(默认0.0) --- # Result - 操作结果 bool success # 操作是否成功 diff --git a/unilabos_msgs/action/HeatChill.action b/unilabos_msgs/action/HeatChill.action index 87ebf52..a39510e 100644 --- a/unilabos_msgs/action/HeatChill.action +++ b/unilabos_msgs/action/HeatChill.action @@ -1,12 +1,19 @@ -# Organic -string vessel -float64 temp -float64 time -bool stir -float64 stir_speed -string purpose +# Goal - 加热冷却操作的目标参数 +string vessel # 加热容器名称(必需) +float64 temp # 目标温度(可选,默认25.0) +float64 time # 加热时间(可选,默认300.0) +string temp_spec # 温度规格(可选) +string time_spec # 时间规格(可选) +string pressure # 压力规格(可选,不做特殊处理) +string reflux_solvent # 回流溶剂名称(可选,不做特殊处理) +bool stir # 是否搅拌(可选,默认false) +float64 stir_speed # 搅拌速度(可选,默认300.0) +string purpose # 操作目的(可选) --- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 string return_info -bool success --- -string status \ No newline at end of file +# Feedback - 实时反馈 +string status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/PumpTransfer.action b/unilabos_msgs/action/PumpTransfer.action index 69d22b6..c8ca445 100644 --- a/unilabos_msgs/action/PumpTransfer.action +++ b/unilabos_msgs/action/PumpTransfer.action @@ -9,6 +9,11 @@ string rinsing_solvent float64 rinsing_volume int32 rinsing_repeats bool solid +float64 flowrate +float64 transfer_flowrate +string rate_spec +string event +string through --- string return_info bool success diff --git a/unilabos_msgs/action/Stir.action b/unilabos_msgs/action/Stir.action index 9542f9d..64e9f04 100644 --- a/unilabos_msgs/action/Stir.action +++ b/unilabos_msgs/action/Stir.action @@ -1,9 +1,16 @@ -# Organic -float64 stir_time -float64 stir_speed -float64 settling_time +# Goal - 搅拌操作的目标参数 +string vessel # 搅拌容器名称(必需) +string time # 搅拌时间(如 "0.5 h", "30 min") +string event # 事件标识(如 "A", "B") +string time_spec # 时间规格(如 "several minutes") +float64 stir_time # 解析后的搅拌时间(秒) +float64 stir_speed # 搅拌速度(默认200.0) +float64 settling_time # 沉降时间(默认60.0) --- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 string return_info -bool success --- -string status \ No newline at end of file +# Feedback - 实时反馈 +string status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/WashSolid.action b/unilabos_msgs/action/WashSolid.action index cb57e5c..7aa3c6e 100644 --- a/unilabos_msgs/action/WashSolid.action +++ b/unilabos_msgs/action/WashSolid.action @@ -1,16 +1,19 @@ -string vessel # 装有固体物质的容器名称 -string solvent # 用于清洗固体的溶剂名称 -float64 volume # 清洗溶剂的体积 -string filtrate_vessel # 滤液要收集到的容器名称,可选参数 -float64 temp # 清洗时的温度,可选参数 -bool stir # 是否在清洗过程中搅拌,默认为 False -float64 stir_speed # 搅拌速度,可选参数 -float64 time # 清洗的时间,可选参数 -int32 repeats # 清洗操作的重复次数,默认为 1 +# Goal - 固体清洗操作的目标参数 +string vessel # 装有固体的容器名称(必需) +string solvent # 清洗溶剂名称(必需) +float64 volume # 清洗溶剂体积(必需) +string filtrate_vessel # 滤液收集容器(可选,默认"") +float64 temp # 清洗温度(可选,默认25.0) +bool stir # 是否搅拌(可选,默认false) +float64 stir_speed # 搅拌速度(可选,默认0.0) +float64 time # 清洗时间(可选,默认0.0) +int32 repeats # 重复次数(可选,默认1) --- -bool success # 操作是否成功 -string message # 结果消息 +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 string return_info --- -string status # 当前状态描述 -float64 progress # 进度百分比 (0-100) \ No newline at end of file +# Feedback - 实时反馈 +string status # 当前状态描述 +float64 progress # 进度百分比 (0-100) \ No newline at end of file