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 1/9] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E9=83=A8?= =?UTF-8?q?=E5=88=86=E7=9A=84protocol=E5=9B=A0=E4=B8=BAXDL=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=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 | 373 +++---- unilabos/compile/heatchill_protocol.py | 538 +++++----- unilabos/compile/pump_protocol.py | 922 +++++++++++++++--- unilabos/compile/stir_protocol.py | 297 +++++- unilabos/compile/wash_solid_protocol.py | 451 +++++---- unilabos/devices/virtual/virtual_rotavap.py | 21 +- unilabos/messages/__init__.py | 507 +++++++++- unilabos/registry/devices/work_station.yaml | 452 ++++----- 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, 3007 insertions(+), 1579 deletions(-) diff --git a/test/experiments/comprehensive_protocol/checklist.md b/test/experiments/comprehensive_protocol/checklist.md index 5bb14ae7d..9645aa4ce 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 aa44df69e..e5e8df5c4 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: 参数验证和修正...") + + # 验证必需参数 + if not vessel: + raise ValueError("vessel 参数不能为空") - print(f"EVACUATE_REFILL: 开始生成协议,目标容器: {vessel}, 气体: {gas}, 重复次数: {repeats}") + 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)}") - - # **新增**: 检查路径中的边数据 - 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}") + debug_print(f"充气路径: {' → '.join(gas_path)}") - 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 4cee78d9e..b5170b5a8 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', {}) - liquids = vessel_data.get('liquid', []) + debug_print(f"容器数据: {vessel_data}") - total_volume = 0.0 - for liquid in liquids: - if isinstance(liquid, dict) and 'liquid_volume' in liquid: - total_volume += liquid['liquid_volume'] + # 检查多种体积字段 + 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 - return total_volume - + # 检查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 + + 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_rotavap_vessel(G: nx.DiGraph) -> Optional[str]: + """查找旋转蒸发仪样品容器""" + debug_print("查找旋转蒸发仪样品容器...") + + possible_vessels = [ + "rotavap", "rotavap_flask", "flask_rotavap", + "evaporation_flask", "evaporator", "rotary_evaporator" + ] + + for vessel in possible_vessels: + if vessel in G.nodes(): + debug_print(f"找到旋转蒸发仪样品容器: {vessel}") + return vessel + + debug_print("未找到旋转蒸发仪样品容器") + return None -def find_solvent_recovery_vessel(G: nx.DiGraph) -> str: +def find_recovery_vessel(G: nx.DiGraph) -> Optional[str]: """查找溶剂回收容器""" - possible_names = [ - "flask_distillate", - "bottle_distillate", - "vessel_distillate", - "distillate", - "solvent_recovery", - "flask_solvent_recovery", - "collection_flask" - ] + debug_print("查找溶剂回收容器...") - for vessel_name in possible_names: - if vessel_name in G.nodes(): - return vessel_name + possible_vessels = [ + "flask_distillate", "distillate", "solvent_recovery", + "rotavap_condenser", "condenser", "waste_workup", "waste" + ] - # 如果找不到专门的回收容器,使用废液容器 - waste_names = ["waste_workup", "flask_waste", "bottle_waste", "waste"] - for vessel_name in waste_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 - raise ValueError(f"未找到溶剂回收容器。尝试了以下名称: {possible_names + waste_names}") - + 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]]: 蒸发操作的动作序列 + List[Dict[str, Any]]: 动作序列 + """ - Raises: - ValueError: 当找不到必要的设备时抛出异常 + 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) - Examples: - evaporate_actions = generate_evaporate_protocol(G, "reaction_mixture", 0.05, 80.0, 3600.0) - """ 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 - # 查找旋转蒸发仪样品容器 - 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 + 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 = 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") - - # === 第一步:将待蒸发溶液转移到旋转蒸发仪 === - 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)}") - - # 转移后等待 - wait_action = { + transfer_volume = 50.0 # 默认小体积,更安全 + debug_print(f"未检测到液体体积,使用默认转移体积 {transfer_volume}mL") + + # === 生成动作序列 === + debug_print("步骤4: 生成动作序列...") + + # 动作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 7e3ca6b41..e673f2291 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', {}) - liquids = vessel_data.get('liquid', []) - 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 - + # 检查多种体积字段 + 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 + + 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'] - - if filter_nodes: - return filter_nodes[0] + debug_print("查找过滤器设备...") - raise ValueError("系统中未找到过滤器设备") - - -def find_filter_vessel(G: nx.DiGraph) -> str: - """查找过滤器专用容器""" - possible_names = [ - "filter_vessel", # 标准过滤器容器 - "filtration_vessel", # 备选名称 - "vessel_filter", # 备选名称 - "filter_unit", # 备选名称 - "filter" # 简单名称 - ] + # 查找过滤器设备 + 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}") - for vessel_name in possible_names: - if vessel_name in G.nodes(): - return vessel_name + if filter_devices: + return filter_devices[0] - 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"✅ 参数验证通过") - # 查找过滤器设备 - try: - filter_id = find_filter_device(G) - print(f"FILTER: 找到过滤器: {filter_id}") - except ValueError as e: - raise ValueError(f"无法找到过滤器: {str(e)}") + # === 查找设备 === + debug_print("步骤2: 查找设备...") - # 查找过滤器容器 - 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)}") + + debug_print(f"设备配置:") + debug_print(f" - 过滤器设备: {filter_device}") + debug_print(f" - 滤液收集容器: {actual_filtrate_vessel}") + + except Exception as e: + debug_print(f"❌ 设备查找失败: {str(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)}") + # === 体积检测 === + debug_print("步骤3: 体积检测...") - # === 简化的体积计算策略 === - 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") + source_volume = get_vessel_liquid_volume(G, vessel) - # === 第一步:启动加热搅拌器(在转移前预热) === - 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) - - # 等待温度稳定 - 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} - }) + 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") - # === 第二步:将待过滤溶液转移到过滤器 === - 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("步骤4: 执行过滤操作...") - # 转移后等待 - action_sequence.append({ - "action_name": "wait", - "action_kwargs": {"time": 5} - }) + # 过滤动作(直接调用过滤器) + 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 5ce099284..260d9bc9e 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) - - print(f"HEATCHILL_STOP: 生成了 {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}' 不存在于系统中") + action_sequence.append(stop_action) - # 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)} 个动作") + debug_print(f"停止加热协议生成完成,动作数: {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 cddb86325..5200188ab 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 + +# 保持原有的同步版本兼容性 +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 + } + ] - 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 _parse_amount_to_volume(amount: str) -> float: + """解析 amount 字符串为体积""" + debug_print(f"🔍 解析 amount: '{amount}'") + + if not amount: + debug_print(" - amount 为空,返回 0.0") + return 0.0 - pump_action_sequence = generate_pump_protocol(G, from_vessel, to_vessel, float(volume), flowrate, transfer_flowrate) - if rinsing_solvent != "air" and rinsing_solvent != "": + 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) + for solvent in rinsing_solvents: + solvent_vessel = f"flask_{solvent.strip()}" + + # 检查溶剂容器是否存在 + 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 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) - return pump_action_sequence -# End Protocols + 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 6fc865cd8..76345cc3e 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, 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, time, speed, settling) \ No newline at end of file + """彻底搅拌:中速长时间""" + 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 a792b8f07..50bbed828 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: 当找不到必要的设备时抛出异常 + 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) - Examples: - wash_solid_protocol = generate_wash_solid_protocol( - G, "reactor", "ethanol", 100.0, "waste_flask", 60.0, True, 300.0, 600.0, 3 - ) - """ action_sequence = [] - # 验证容器是否存在 - if vessel not in G.nodes(): - raise ValueError(f"固体容器 {vessel} 不存在于图中") + # === 参数验证 === + debug_print("步骤1: 参数验证...") - if filtrate_vessel and filtrate_vessel not in G.nodes(): - raise ValueError(f"滤液容器 {filtrate_vessel} 不存在于图中") + # 验证必需参数 + if not vessel: + raise ValueError("vessel 参数不能为空") - # 查找转移泵设备(用于添加溶剂和转移滤液) - pump_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_transfer_pump'] + if not solvent: + raise ValueError("solvent 参数不能为空") - if not pump_nodes: - raise ValueError("没有找到可用的转移泵设备") + if volume <= 0: + raise ValueError("volume 必须大于0") - pump_id = pump_nodes[0] + if vessel not in G.nodes(): + raise ValueError(f"容器 '{vessel}' 不存在于系统中") - # 查找加热设备(如果需要加热) - heatchill_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_heatchill'] + # 修正参数范围 + if temp < 0 or temp > 200: + debug_print(f"温度 {temp}°C 超出范围,修正为 25°C") + temp = 25.0 - heatchill_id = heatchill_nodes[0] if heatchill_nodes else None + if stir_speed < 0 or stir_speed > 500: + debug_print(f"搅拌速度 {stir_speed} RPM 超出范围,修正为 0") + stir_speed = 0.0 - # 查找搅拌设备(如果需要搅拌) - stirrer_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_stirrer'] + if time < 0: + debug_print(f"时间 {time}s 无效,修正为 0") + time = 0.0 - stirrer_id = stirrer_nodes[0] if stirrer_nodes else None + if repeats < 1: + debug_print(f"重复次数 {repeats} 无效,修正为 1") + repeats = 1 + elif repeats > 10: + debug_print(f"重复次数 {repeats} 过多,修正为 10") + repeats = 10 - # 查找过滤设备(用于分离固体和滤液) - filter_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_filter'] + debug_print(f"✅ 参数验证通过") - filter_id = filter_nodes[0] if filter_nodes else None + # === 查找设备 === + debug_print("步骤2: 查找设备...") - # 查找溶剂容器 - 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次 } - }) - - # 步骤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 + } + action_sequence.append(wash_action) - # 步骤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 + # 等待清洗完成 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": max(10.0, time * 0.1)} + }) + + # === 总结 === + 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 ba01c7b7f..fbbfe9f07 100644 --- a/unilabos/devices/virtual/virtual_rotavap.py +++ b/unilabos/devices/virtual/virtual_rotavap.py @@ -71,11 +71,24 @@ async def evaporate( 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 47f21f1ca..dfdbab389 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 59416453d..3c4e3b671 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: + vessel: + type: string + description: 蒸发容器名称 pressure: type: number - stir_speed: - type: number + default: 0.1 + description: 真空度 (bar) temp: type: number + default: 60.0 + description: 加热温度 (°C) time: type: number - vessel: + 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 + 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 - vessel: - type: string + 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: + vessel: type: string - stir: - type: boolean - stir_speed: - type: number + description: 加热容器名称 temp: type: number + default: 25.0 + description: 目标温度 time: type: number - vessel: + 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: - type: string - viscous: - type: boolean - volume: + transfer_flowrate: type: number + rate_spec: + type: string + event: + type: string + through: + type: string 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: + 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 - stir_time: + 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: + vessel: type: string - repeats: - maximum: 2147483647 - minimum: -2147483648 - type: integer + 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 - temp: - type: number + default: 0.0 + description: 搅拌速度 time: type: number - vessel: - type: string - volume: - 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 22ffc659a..461cb2837 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 45887f27f..b66e32f0d 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 564df1a57..42a89135a 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 87ebf526a..a39510e71 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 69d22b6c4..c8ca445bc 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 9542f9dca..64e9f0436 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 cb57e5ccc..7aa3c6e13 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 From 5767266563b28e6a7e29844fdae83e65a4a13282 Mon Sep 17 00:00:00 2001 From: KCFeng425 <2100011801@stu.pku.edu.cn> Date: Mon, 7 Jul 2025 12:33:10 +0800 Subject: [PATCH 2/9] =?UTF-8?q?=E8=A1=A5=E5=85=85=E4=BA=86=E5=9B=9B?= =?UTF-8?q?=E4=B8=AAaction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- unilabos_msgs/action/Add.action | 25 +++++++++------ unilabos_msgs/action/Dissolve.action | 20 +++++++----- unilabos_msgs/action/RunColumn.action | 15 +++++++-- unilabos_msgs/action/Separate.action | 45 ++++++++++++++++----------- 4 files changed, 66 insertions(+), 39 deletions(-) diff --git a/unilabos_msgs/action/Add.action b/unilabos_msgs/action/Add.action index de06c6a0e..0c6ed2a85 100644 --- a/unilabos_msgs/action/Add.action +++ b/unilabos_msgs/action/Add.action @@ -1,14 +1,19 @@ # Goal - 添加试剂的目标参数 -string vessel # 目标容器 -string reagent # 试剂名称 -float64 volume # 体积 (可选) -float64 mass # 质量 (可选) -string amount # 数量描述 (可选) -float64 time # 添加时间 (可选) -bool stir # 是否搅拌 -float64 stir_speed # 搅拌速度 (可选) -bool viscous # 是否为粘性液体 -string purpose # 添加目的 (可选) +string vessel # 目标容器(必需) +string reagent # 试剂名称(必需) +float64 volume # 体积 (mL,可选) +float64 mass # 质量 (g,可选) +string amount # 数量描述 (可选) +float64 time # 添加时间 (s,可选) +bool stir # 是否搅拌(可选) +float64 stir_speed # 搅拌速度 (RPM,可选) +bool viscous # 是否为粘性液体(可选) +string purpose # 添加目的 (可选) +string event # 事件标识(如 'A', 'B',可选) +string mol # 摩尔数(如 '0.28 mol', '16.2 mmol',可选) +string rate_spec # 速率规格(如 'portionwise', 'dropwise',可选) +string equiv # 当量(如 '1.1',可选) +string ratio # 比例(如 '1:1',可选) --- # Result - 操作结果 bool success # 操作是否成功 diff --git a/unilabos_msgs/action/Dissolve.action b/unilabos_msgs/action/Dissolve.action index 6b860d079..e2d2c43c4 100644 --- a/unilabos_msgs/action/Dissolve.action +++ b/unilabos_msgs/action/Dissolve.action @@ -1,14 +1,20 @@ -string vessel # 装有要溶解物质的容器名称 -string solvent # 用于溶解物质的溶剂名称 -float64 volume # 溶剂的体积,可选参数 -string amount # 要溶解物质的量,可选参数 -float64 temp # 溶解时的温度,可选参数 -float64 time # 溶解的时间,可选参数 -float64 stir_speed # 搅拌速度,可选参数 +# Goal - 溶解操作的目标参数 +string vessel # 装有要溶解物质的容器名称(必需) +string solvent # 用于溶解物质的溶剂名称(可选) +float64 volume # 溶剂的体积(可选) +string amount # 要溶解物质的量(可选) +float64 temp # 溶解时的温度(可选) +float64 time # 溶解的时间(可选) +float64 stir_speed # 搅拌速度(可选) +string mass # 物质质量(如 "2.9 g",可选) +string mol # 物质摩尔数(如 "0.12 mol",可选) +string reagent # 试剂名称(可选) --- +# Result - 操作结果 bool success # 操作是否成功 string message # 结果消息 string return_info --- +# Feedback - 实时反馈 string status # 当前状态描述 float64 progress # 进度百分比 (0-100) \ No newline at end of file diff --git a/unilabos_msgs/action/RunColumn.action b/unilabos_msgs/action/RunColumn.action index 3fba948ad..889b4a5d8 100644 --- a/unilabos_msgs/action/RunColumn.action +++ b/unilabos_msgs/action/RunColumn.action @@ -1,10 +1,19 @@ -string from_vessel # 源容器的名称,即样品起始所在的容器 -string to_vessel # 目标容器的名称,分离后的样品要到达的容器 -string column # 所使用的柱子的名称 +# Goal - 柱层析操作的目标参数 +string from_vessel # 源容器的名称,即样品起始所在的容器(必需) +string to_vessel # 目标容器的名称,分离后的样品要到达的容器(必需) +string column # 所使用的柱子的名称(必需) +string Rf # Rf值(可选) +string pct1 # 第一种溶剂百分比(如 "40 %",可选) +string pct2 # 第二种溶剂百分比(如 "50 %",可选) +string solvent1 # 第一种溶剂名称(可选) +string solvent2 # 第二种溶剂名称(可选) +string ratio # 溶剂比例(如 "5:95",可选) --- +# Result - 操作结果 bool success # 操作是否成功 string message # 结果消息 string return_info --- +# Feedback - 实时反馈 string status # 当前状态描述 float64 progress # 进度百分比 (0-100) \ No newline at end of file diff --git a/unilabos_msgs/action/Separate.action b/unilabos_msgs/action/Separate.action index fe8976a75..a487fea72 100644 --- a/unilabos_msgs/action/Separate.action +++ b/unilabos_msgs/action/Separate.action @@ -1,22 +1,29 @@ -# Organic -string purpose # 'wash' or 'extract'. 'wash' means that product phase will not be the added solvent phase, 'extract' means product phase will be the added solvent phase. If no solvent is added just use 'extract'. -string product_phase # 'top' or 'bottom'. Phase that product will be in. -string from_vessel #Contents of from_vessel are transferred to separation_vessel and separation is performed. -string separation_vessel # Vessel in which separation of phases will be carried out. -string to_vessel # Vessel to send product phase to. -string waste_phase_to_vessel # Optional. Vessel to send waste phase to. -string solvent # Optional. Solvent to add to separation vessel after contents of from_vessel has been transferred to create two phases. -float64 solvent_volume # Optional. Volume of solvent to add. -string through # Optional. Solid chemical to send product phase through on way to to_vessel, e.g. 'celite'. -int32 repeats # Optional. Number of separations to perform. -float64 stir_time # Optional. Time stir for after adding solvent, before separation of phases. -float64 stir_speed # Optional. Speed to stir at after adding solvent, before separation of phases. -float64 settling_time # Optional. Time +# Goal - 分离操作的目标参数 +string purpose # 分离目的 ('wash', 'extract', 'separate',必需) +string product_phase # 产物相 ('top', 'bottom',必需) +string from_vessel # 源容器(可选) +string separation_vessel # 分离容器(可选) +string to_vessel # 目标容器(可选) +string waste_phase_to_vessel # 废相目标容器(可选) +string solvent # 溶剂名称(可选) +float64 solvent_volume # 溶剂体积(可选) +string through # 通过材料(如 'celite',可选) +int32 repeats # 重复次数(可选) +float64 stir_time # 搅拌时间(可选) +float64 stir_speed # 搅拌速度(可选) +float64 settling_time # 沉降时间(可选) +string vessel # 分离容器名称(XDL参数,可选) +string volume # 体积规格(XDL参数,可选) +string product_vessel # 产物收集容器(XDL参数,可选) +string waste_vessel # 废液收集容器(XDL参数,可选) --- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 string return_info -bool success --- -string status -string current_device -builtin_interfaces/Duration time_spent -builtin_interfaces/Duration time_remaining +# Feedback - 实时反馈 +string status # 当前状态描述 +string current_device # 当前设备 +builtin_interfaces/Duration time_spent # 已用时间 +builtin_interfaces/Duration time_remaining # 剩余时间 From ab2ab7fcc7b20d5566271dd4bdd4569820fc3eed Mon Sep 17 00:00:00 2001 From: KCFeng425 <2100011801@stu.pku.edu.cn> Date: Mon, 7 Jul 2025 18:35:35 +0800 Subject: [PATCH 3/9] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86=E5=9B=BA?= =?UTF-8?q?=E4=BD=93=E5=8A=A0=E6=A0=B7=E5=99=A8=EF=BC=8C=E4=B8=B0=E5=AF=8C?= =?UTF-8?q?=E4=BA=86json=EF=BC=8C=E4=BF=AE=E6=94=B9=E4=BA=86add=20protocol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comprehensive_protocol/checklist.md | 10 +- .../comprehensive_station.json | 138 +++- unilabos/compile/add_protocol.py | 747 ++++++------------ .../virtual/virtual_solid_dispenser.py | 335 ++++++++ unilabos/registry/devices/virtual_device.yaml | 353 ++++----- unilabos_msgs/action/AddSolid.action | 15 + 6 files changed, 889 insertions(+), 709 deletions(-) create mode 100644 unilabos/devices/virtual/virtual_solid_dispenser.py create mode 100644 unilabos_msgs/action/AddSolid.action diff --git a/test/experiments/comprehensive_protocol/checklist.md b/test/experiments/comprehensive_protocol/checklist.md index 9645aa4ce..4762cd22e 100644 --- a/test/experiments/comprehensive_protocol/checklist.md +++ b/test/experiments/comprehensive_protocol/checklist.md @@ -70,7 +70,7 @@ class SeparateProtocol(BaseModel): repeats: int stir_time: float stir_speed: float - settling_time: float + settling_time: float 写了action class EvaporateProtocol(BaseModel): @@ -102,7 +102,7 @@ class AddProtocol(BaseModel): vessel="main_reactor" volume="2.67 mL"/> viscous: bool - purpose: str + purpose: str 写了action class CentrifugeProtocol(BaseModel): vessel: str @@ -127,7 +127,7 @@ class HeatChillProtocol(BaseModel): - stir: bool + stir: bool 处理了 stir_speed: float purpose: str @@ -180,7 +180,7 @@ class DissolveProtocol(BaseModel): amount: str = "" temp: float = 25.0 time: float = 0.0 - stir_speed: float = 0.0 + stir_speed: float = 0.0 写了action class FilterThroughProtocol(BaseModel): from_vessel: str @@ -194,7 +194,7 @@ class FilterThroughProtocol(BaseModel): class RunColumnProtocol(BaseModel): from_vessel: str to_vessel: str - column: str + column: str 写了action class WashSolidProtocol(BaseModel): vessel: str diff --git a/test/experiments/comprehensive_protocol/comprehensive_station.json b/test/experiments/comprehensive_protocol/comprehensive_station.json index d0f5c6afc..aaa4dfc8e 100644 --- a/test/experiments/comprehensive_protocol/comprehensive_station.json +++ b/test/experiments/comprehensive_protocol/comprehensive_station.json @@ -32,7 +32,11 @@ "separator_1", "collection_bottle_1", "collection_bottle_2", - "collection_bottle_3" + "collection_bottle_3", + "solid_dispenser_1", + "solid_reagent_bottle_1", + "solid_reagent_bottle_2", + "solid_reagent_bottle_3" ], "parent": null, "type": "device", @@ -672,6 +676,98 @@ "data": { "current_volume": 0.0 } + }, + { + "id": "solid_dispenser_1", + "name": "固体粉末加样器", + "children": [], + "parent": "OrganicSynthesisStation", + "type": "device", + "class": "virtual_solid_dispenser", + "position": { + "x": 600, + "y": 300, + "z": 0 + }, + "config": { + "max_capacity": 100.0, + "precision": 0.001 + }, + "data": { + "status": "Ready", + "current_reagent": "", + "dispensed_amount": 0.0, + "total_operations": 0 + } + }, + { + "id": "solid_reagent_bottle_1", + "name": "固体试剂瓶1-氯化钠", + "children": [], + "parent": "OrganicSynthesisStation", + "type": "container", + "class": "container", + "position": { + "x": 550, + "y": 250, + "z": 0 + }, + "config": { + "volume": 500.0, + "reagent": "sodium_chloride", + "physical_state": "solid" + }, + "data": { + "current_mass": 500.0, + "reagent_name": "sodium_chloride", + "physical_state": "solid" + } + }, + { + "id": "solid_reagent_bottle_2", + "name": "固体试剂瓶2-碳酸钠", + "children": [], + "parent": "OrganicSynthesisStation", + "type": "container", + "class": "container", + "position": { + "x": 600, + "y": 250, + "z": 0 + }, + "config": { + "volume": 500.0, + "reagent": "sodium_carbonate", + "physical_state": "solid" + }, + "data": { + "current_mass": 500.0, + "reagent_name": "sodium_carbonate", + "physical_state": "solid" + } + }, + { + "id": "solid_reagent_bottle_3", + "name": "固体试剂瓶3-氯化镁", + "children": [], + "parent": "OrganicSynthesisStation", + "type": "container", + "class": "container", + "position": { + "x": 650, + "y": 250, + "z": 0 + }, + "config": { + "volume": 500.0, + "reagent": "magnesium_chloride", + "physical_state": "solid" + }, + "data": { + "current_mass": 500.0, + "reagent_name": "magnesium_chloride", + "physical_state": "solid" + } } ], "links": [ @@ -964,6 +1060,46 @@ "solenoid_valve_3": "out", "main_reactor": "top" } + }, + { + "id": "link_solid_dispenser_to_reactor", + "source": "solid_dispenser_1", + "target": "main_reactor", + "type": "resource", + "port": { + "solid_dispenser_1": "SolidOut", + "main_reactor": "top" + } + }, + { + "id": "link_solid_bottle1_to_dispenser", + "source": "solid_reagent_bottle_1", + "target": "solid_dispenser_1", + "type": "resource", + "port": { + "solid_reagent_bottle_1": "top", + "solid_dispenser_1": "SolidIn" + } + }, + { + "id": "link_solid_bottle2_to_dispenser", + "source": "solid_reagent_bottle_2", + "target": "solid_dispenser_1", + "type": "resource", + "port": { + "solid_reagent_bottle_2": "top", + "solid_dispenser_1": "SolidIn" + } + }, + { + "id": "link_solid_bottle3_to_dispenser", + "source": "solid_reagent_bottle_3", + "target": "solid_dispenser_1", + "type": "resource", + "port": { + "solid_reagent_bottle_3": "top", + "solid_dispenser_1": "SolidIn" + } } ] } \ No newline at end of file diff --git a/unilabos/compile/add_protocol.py b/unilabos/compile/add_protocol.py index 144ec96f9..6c51cc9fd 100644 --- a/unilabos/compile/add_protocol.py +++ b/unilabos/compile/add_protocol.py @@ -4,355 +4,227 @@ def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str: - """ - 根据试剂名称查找对应的试剂瓶,支持多种匹配模式: - 1. 容器名称匹配(如 flask_DMF, reagent_bottle_1-DMF) - 2. 容器内液体类型匹配(如 liquid_type: "DMF", name: "ethanol") - 3. 试剂名称匹配(如 reagent_name: "DMF", config.reagent: "ethyl_acetate") - - Args: - G: 网络图 - reagent: 试剂名称 - - Returns: - str: 试剂瓶的vessel ID + """增强版试剂容器查找,支持固体和液体""" + print(f"ADD_PROTOCOL: 查找试剂 '{reagent}' 的容器...") - Raises: - ValueError: 如果找不到对应的试剂瓶 - """ - print(f"ADD_PROTOCOL: 正在查找试剂 '{reagent}' 的容器...") - - # 第一步:通过容器名称匹配 + # 1. 直接名称匹配 possible_names = [ - f"flask_{reagent}", # flask_DMF, flask_ethanol - f"bottle_{reagent}", # bottle_DMF, bottle_ethanol - f"vessel_{reagent}", # vessel_DMF, vessel_ethanol - f"{reagent}_flask", # DMF_flask, ethanol_flask - f"{reagent}_bottle", # DMF_bottle, ethanol_bottle - f"{reagent}", # 直接用试剂名 - f"reagent_{reagent}", # reagent_DMF, reagent_ethanol - f"reagent_bottle_{reagent}", # reagent_bottle_DMF + reagent, + f"flask_{reagent}", + f"bottle_{reagent}", + f"vessel_{reagent}", + f"{reagent}_flask", + f"{reagent}_bottle", + f"reagent_{reagent}", + f"reagent_bottle_{reagent}", + f"solid_reagent_bottle_{reagent}", # 🔧 添加固体试剂瓶匹配 ] - # 尝试名称匹配 - for vessel_name in possible_names: - if vessel_name in G.nodes(): - print(f"ADD_PROTOCOL: 通过名称匹配找到容器: {vessel_name}") - return vessel_name + for name in possible_names: + if name in G.nodes(): + print(f"ADD_PROTOCOL: 找到容器: {name}") + return name - # 第二步:通过模糊名称匹配(名称中包含试剂名) + # 2. 模糊匹配 - 检查容器数据 for node_id in G.nodes(): - if G.nodes[node_id].get('type') == 'container': - # 检查节点ID或名称中是否包含试剂名 - node_name = G.nodes[node_id].get('name', '').lower() - if (reagent.lower() in node_id.lower() or - reagent.lower() in node_name): - print(f"ADD_PROTOCOL: 通过模糊名称匹配找到容器: {node_id} (名称: {node_name})") - return node_id - - # 第三步:通过液体类型匹配 - for node_id in G.nodes(): - if G.nodes[node_id].get('type') == 'container': - vessel_data = G.nodes[node_id].get('data', {}) - liquids = vessel_data.get('liquid', []) - - for liquid in liquids: - if isinstance(liquid, dict): - # 支持两种格式的液体类型字段 - liquid_type = liquid.get('liquid_type') or liquid.get('name', '') - reagent_name = vessel_data.get('reagent_name', '') - config_reagent = G.nodes[node_id].get('config', {}).get('reagent', '') - - # 检查多个可能的字段 - if (liquid_type.lower() == reagent.lower() or - reagent_name.lower() == reagent.lower() or - config_reagent.lower() == reagent.lower()): - print(f"ADD_PROTOCOL: 通过液体类型匹配找到容器: {node_id}") - print(f" - liquid_type: {liquid_type}") - print(f" - reagent_name: {reagent_name}") - print(f" - config.reagent: {config_reagent}") - return node_id - - # 第四步:列出所有可用的容器信息帮助调试 - available_containers = [] - for node_id in G.nodes(): - if G.nodes[node_id].get('type') == 'container': - vessel_data = G.nodes[node_id].get('data', {}) - config_data = G.nodes[node_id].get('config', {}) - liquids = vessel_data.get('liquid', []) - - container_info = { - 'id': node_id, - 'name': G.nodes[node_id].get('name', ''), - 'liquid_types': [], - 'reagent_name': vessel_data.get('reagent_name', ''), - 'config_reagent': config_data.get('reagent', '') - } - - for liquid in liquids: - if isinstance(liquid, dict): - liquid_type = liquid.get('liquid_type') or liquid.get('name', '') - if liquid_type: - container_info['liquid_types'].append(liquid_type) + node_data = G.nodes[node_id] + if node_data.get('type') == 'container': + # 检查配置中的试剂名称 + config_reagent = node_data.get('config', {}).get('reagent', '') + data_reagent = node_data.get('data', {}).get('reagent_name', '') - available_containers.append(container_info) - - print(f"ADD_PROTOCOL: 可用容器列表:") - for container in available_containers: - print(f" - {container['id']}: {container['name']}") - print(f" 液体类型: {container['liquid_types']}") - print(f" 试剂名称: {container['reagent_name']}") - print(f" 配置试剂: {container['config_reagent']}") - - raise ValueError(f"找不到试剂 '{reagent}' 对应的试剂瓶。尝试了名称匹配: {possible_names}") - - -def find_reagent_vessel_by_any_match(G: nx.DiGraph, reagent: str) -> str: - """ - 增强版试剂容器查找,支持各种匹配方式的别名函数 - """ - return find_reagent_vessel(G, reagent) - - -def get_vessel_reagent_volume(G: nx.DiGraph, vessel: str) -> float: - """获取容器中的试剂体积""" - if vessel not in G.nodes(): - return 0.0 - - vessel_data = G.nodes[vessel].get('data', {}) - liquids = vessel_data.get('liquid', []) - - total_volume = 0.0 - for liquid in liquids: - if isinstance(liquid, dict): - # 支持两种格式:新格式 (name, volume) 和旧格式 (liquid_type, liquid_volume) - volume = liquid.get('volume') or liquid.get('liquid_volume', 0.0) - total_volume += volume - - return total_volume - - -def get_vessel_reagent_types(G: nx.DiGraph, vessel: str) -> List[str]: - """获取容器中所有试剂的类型""" - if vessel not in G.nodes(): - return [] - - vessel_data = G.nodes[vessel].get('data', {}) - liquids = vessel_data.get('liquid', []) - - reagent_types = [] - for liquid in liquids: - if isinstance(liquid, dict): - # 支持两种格式的试剂类型字段 - reagent_type = liquid.get('liquid_type') or liquid.get('name', '') - if reagent_type: - reagent_types.append(reagent_type) - - # 同时检查配置中的试剂信息 - config_reagent = G.nodes[vessel].get('config', {}).get('reagent', '') - reagent_name = vessel_data.get('reagent_name', '') - - if config_reagent and config_reagent not in reagent_types: - reagent_types.append(config_reagent) - if reagent_name and reagent_name not in reagent_types: - reagent_types.append(reagent_name) - - return reagent_types - - -def find_vessels_by_reagent(G: nx.DiGraph, reagent: str) -> List[str]: - """ - 根据试剂类型查找所有匹配的容器 - 返回匹配容器的ID列表 - """ - matching_vessels = [] - - for node_id in G.nodes(): - if G.nodes[node_id].get('type') == 'container': - # 检查容器名称匹配 - node_name = G.nodes[node_id].get('name', '').lower() - if reagent.lower() in node_id.lower() or reagent.lower() in node_name: - matching_vessels.append(node_id) - continue + # 名称匹配 + if (config_reagent.lower() == reagent.lower() or + data_reagent.lower() == reagent.lower() or + reagent.lower() in node_id.lower()): + print(f"ADD_PROTOCOL: 模糊匹配到容器: {node_id}") + return node_id - # 检查试剂类型匹配 - vessel_data = G.nodes[node_id].get('data', {}) + # 液体类型匹配(保持原有逻辑) + vessel_data = node_data.get('data', {}) liquids = vessel_data.get('liquid', []) - config_data = G.nodes[node_id].get('config', {}) - - # 检查 reagent_name 和 config.reagent - reagent_name = vessel_data.get('reagent_name', '').lower() - config_reagent = config_data.get('reagent', '').lower() - - if (reagent.lower() == reagent_name or - reagent.lower() == config_reagent): - matching_vessels.append(node_id) - continue - - # 检查液体列表 for liquid in liquids: if isinstance(liquid, dict): liquid_type = liquid.get('liquid_type') or liquid.get('name', '') if liquid_type.lower() == reagent.lower(): - matching_vessels.append(node_id) - break + print(f"ADD_PROTOCOL: 液体类型匹配到容器: {node_id}") + return node_id - return matching_vessels + raise ValueError(f"找不到试剂 '{reagent}' 对应的容器") def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str: - """ - 查找与指定容器相连的搅拌器 - - Args: - G: 网络图 - vessel: 容器ID - - Returns: - str: 搅拌器ID,如果找不到则返回None - """ - # 查找所有搅拌器节点 - 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_class = G.nodes[node].get('class', '').lower() + if 'stirrer' in node_class: + stirrer_nodes.append(node) + + # 查找连接到容器的搅拌器 for stirrer in stirrer_nodes: if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): + print(f"ADD_PROTOCOL: 找到连接的搅拌器: {stirrer}") return stirrer - # 如果没有直接连接,返回第一个可用的搅拌器 - return stirrer_nodes[0] if stirrer_nodes else None + # 返回第一个搅拌器 + if stirrer_nodes: + print(f"ADD_PROTOCOL: 使用第一个搅拌器: {stirrer_nodes[0]}") + return stirrer_nodes[0] + + return None + + +def find_solid_dispenser(G: nx.DiGraph) -> str: + """查找固体加样器""" + for node in G.nodes(): + node_class = G.nodes[node].get('class', '').lower() + if 'solid_dispenser' in node_class: + print(f"ADD_PROTOCOL: 找到固体加样器: {node}") + return node + return None def generate_add_protocol( G: nx.DiGraph, vessel: str, reagent: str, - volume: float, + volume: float = 0.0, mass: float = 0.0, amount: str = "", time: float = 0.0, stir: bool = False, stir_speed: float = 300.0, viscous: bool = False, - purpose: str = "添加试剂" + purpose: str = "添加试剂", + # 新增XDL参数 + mol: str = "", + event: str = "", + rate_spec: str = "", + equiv: str = "", + ratio: str = "", + **kwargs ) -> List[Dict[str, Any]]: """ - 生成添加试剂的协议序列,支持智能试剂匹配 + 生成添加试剂协议 - 基于pump_protocol的成熟算法,实现试剂添加功能: - 1. 智能查找试剂瓶(支持名称匹配、液体类型匹配、试剂配置匹配) - 2. **先启动搅拌,再进行转移** - 确保试剂添加更均匀 - 3. 使用pump_protocol实现液体转移 - - Args: - G: 有向图,节点为容器和设备,边为连接关系 - vessel: 目标容器(要添加试剂的容器) - reagent: 试剂名称(用于查找对应的试剂瓶) - volume: 要添加的体积 (mL) - mass: 要添加的质量 (g) - 暂时未使用,预留接口 - amount: 其他数量描述 - time: 添加时间 (s),如果指定则计算流速 - stir: 是否启用搅拌 - stir_speed: 搅拌速度 (RPM) - viscous: 是否为粘稠液体 - purpose: 添加目的描述 - - Returns: - List[Dict[str, Any]]: 动作序列 - - Raises: - ValueError: 当找不到必要的设备或容器时 + 智能判断: + - 有 mass 或 mol → 固体加样器 + - 有 volume → 液体转移 + - 都没有 → 默认液体 1mL """ - action_sequence = [] - print(f"ADD_PROTOCOL: 开始生成添加试剂协议") - print(f" - 目标容器: {vessel}") - print(f" - 试剂: {reagent}") - print(f" - 体积: {volume} mL") - print(f" - 质量: {mass} g") - print(f" - 搅拌: {stir} (速度: {stir_speed} RPM)") - print(f" - 粘稠: {viscous}") - print(f" - 目的: {purpose}") + print(f"ADD_PROTOCOL: 添加 {reagent} 到 {vessel}") + print(f" - 体积: {volume} mL, 质量: {mass} g, 摩尔: {mol}") + print(f" - 时间: {time} s, 事件: {event}, 速率: {rate_spec}") - # 1. 验证目标容器存在 + # 1. 验证容器 if vessel not in G.nodes(): - raise ValueError(f"目标容器 '{vessel}' 不存在于系统中") - - # 2. 智能查找试剂瓶 - try: - reagent_vessel = find_reagent_vessel(G, reagent) - print(f"ADD_PROTOCOL: 找到试剂容器: {reagent_vessel}") - except ValueError as e: - raise ValueError(f"无法找到试剂 '{reagent}': {str(e)}") + raise ValueError(f"容器 '{vessel}' 不存在") - # 3. 验证试剂容器中的试剂体积 - available_volume = get_vessel_reagent_volume(G, reagent_vessel) - print(f"ADD_PROTOCOL: 试剂容器 {reagent_vessel} 中有 {available_volume} mL 试剂") + # 2. 判断固体 vs 液体 + is_solid = (mass > 0 or mol.strip() != "") - if available_volume < volume: - print(f"ADD_PROTOCOL: 警告 - 试剂容器中的试剂不足!需要 {volume} mL,可用 {available_volume} mL") - - # 4. 验证是否存在从试剂瓶到目标容器的路径 - try: - path = nx.shortest_path(G, source=reagent_vessel, target=vessel) - print(f"ADD_PROTOCOL: 找到路径 {reagent_vessel} -> {vessel}: {path}") - except nx.NetworkXNoPath: - raise ValueError(f"从试剂瓶 '{reagent_vessel}' 到目标容器 '{vessel}' 没有可用路径") + action_sequence = [] - # 5. **先启动搅拌** - 关键改进! - if stir: - try: + if is_solid: + # === 固体加样路径 === + print(f"ADD_PROTOCOL: 使用固体加样器") + + solid_dispenser = find_solid_dispenser(G) + if not solid_dispenser: + raise ValueError("未找到固体加样器") + + # 启动搅拌(如果需要) + if stir: stirrer_id = find_connected_stirrer(G, vessel) + if stirrer_id: + action_sequence.append({ + "device_id": stirrer_id, + "action_name": "start_stir", + "action_kwargs": { + "vessel": vessel, + "stir_speed": stir_speed, + "purpose": f"准备添加固体 {reagent}" + } + }) + # 等待搅拌稳定 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 3} + }) + + # 固体加样 + action_sequence.append({ + "device_id": solid_dispenser, + "action_name": "add_solid", + "action_kwargs": { + "vessel": vessel, + "reagent": reagent, + "mass": str(mass) if mass > 0 else "", + "mol": mol, + "purpose": purpose, + "event": event + } + }) + + else: + # === 液体转移路径 === + print(f"ADD_PROTOCOL: 使用液体转移") + + # 默认体积 + if volume <= 0: + volume = 1.0 + print(f"ADD_PROTOCOL: 使用默认体积 1mL") + + # 查找试剂容器 + try: + reagent_vessel = find_reagent_vessel(G, reagent) + except ValueError as e: + # 🔧 更友好的错误提示 + available_reagents = [] + for node_id in G.nodes(): + node_data = G.nodes[node_id] + if node_data.get('type') == 'container': + config_reagent = node_data.get('config', {}).get('reagent', '') + data_reagent = node_data.get('data', {}).get('reagent_name', '') + if config_reagent: + available_reagents.append(f"{node_id}({config_reagent})") + elif data_reagent: + available_reagents.append(f"{node_id}({data_reagent})") + error_msg = f"找不到试剂 '{reagent}'。可用试剂: {', '.join(available_reagents)}" + print(f"ADD_PROTOCOL: {error_msg}") + raise ValueError(error_msg) + + # 启动搅拌 + if stir: + stirrer_id = find_connected_stirrer(G, vessel) if stirrer_id: - print(f"ADD_PROTOCOL: 找到搅拌器 {stirrer_id},将在添加前启动搅拌") - - # 先启动搅拌 - stir_action = { + action_sequence.append({ "device_id": stirrer_id, "action_name": "start_stir", "action_kwargs": { "vessel": vessel, "stir_speed": stir_speed, - "purpose": f"{purpose}: 启动搅拌,准备添加 {reagent}" + "purpose": f"准备添加液体 {reagent}" } - } - - action_sequence.append(stir_action) - print(f"ADD_PROTOCOL: 已添加搅拌动作,速度 {stir_speed} RPM") - + }) # 等待搅拌稳定 action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 5} }) - else: - print(f"ADD_PROTOCOL: 警告 - 需要搅拌但未找到与容器 {vessel} 相连的搅拌器") - except Exception as e: - print(f"ADD_PROTOCOL: 搅拌器配置出错: {str(e)}") - - # 6. 如果指定了体积,执行液体转移 - if volume > 0: - # 6.1 计算流速参数 + # 计算流速 if time > 0: - # 根据时间计算流速 - transfer_flowrate = volume / time - flowrate = transfer_flowrate + flowrate = volume / time + transfer_flowrate = flowrate else: - # 使用默认流速 - if viscous: - transfer_flowrate = 0.3 # 粘稠液体用较慢速度 - flowrate = 1.0 - else: - transfer_flowrate = 0.5 # 普通液体默认速度 - flowrate = 2.5 + flowrate = 1.0 if viscous else 2.5 + transfer_flowrate = 0.3 if viscous else 0.5 - print(f"ADD_PROTOCOL: 准备转移 {volume} mL 从 {reagent_vessel} 到 {vessel}") - print(f"ADD_PROTOCOL: 转移流速={transfer_flowrate} mL/s, 注入流速={flowrate} mL/s") - - # 6.2 使用pump_protocol的核心算法实现液体转移 + # 🔧 调用 pump_protocol 时使用正确的参数 try: pump_actions = generate_pump_protocol_with_rinsing( G=G, @@ -360,267 +232,104 @@ def generate_add_protocol( to_vessel=vessel, volume=volume, amount=amount, - time=time, + duration=time, # 🔧 使用 duration 而不是 time viscous=viscous, - rinsing_solvent="", # 添加试剂通常不需要清洗 + rinsing_solvent="", rinsing_volume=0.0, rinsing_repeats=0, solid=False, flowrate=flowrate, - transfer_flowrate=transfer_flowrate + transfer_flowrate=transfer_flowrate, + rate_spec=rate_spec, + event=event, + through="", + equiv=equiv, + ratio=ratio, + **kwargs ) - - # 添加pump actions到序列中 action_sequence.extend(pump_actions) - except Exception as e: - raise ValueError(f"生成泵协议时出错: {str(e)}") - - print(f"ADD_PROTOCOL: 生成了 {len(action_sequence)} 个动作") - print(f"ADD_PROTOCOL: 添加试剂协议生成完成") + raise ValueError(f"液体转移失败: {str(e)}") + print(f"ADD_PROTOCOL: 生成 {len(action_sequence)} 个动作") return action_sequence -def generate_add_protocol_with_cleaning( - G: nx.DiGraph, - vessel: str, - reagent: str, - volume: float, - mass: float = 0.0, - amount: str = "", - time: float = 0.0, - stir: bool = False, - stir_speed: float = 300.0, - viscous: bool = False, - purpose: str = "添加试剂", - cleaning_solvent: str = "air", - cleaning_volume: float = 5.0, - cleaning_repeats: int = 1 -) -> List[Dict[str, Any]]: - """ - 生成带清洗的添加试剂协议,支持智能试剂匹配 - - 与普通添加协议的区别是会在添加后进行管道清洗 - - Args: - G: 有向图 - vessel: 目标容器 - reagent: 试剂名称 - volume: 添加体积 - mass: 添加质量(预留) - amount: 其他数量描述 - time: 添加时间 - stir: 是否搅拌 - stir_speed: 搅拌速度 - viscous: 是否粘稠 - purpose: 添加目的 - cleaning_solvent: 清洗溶剂("air"表示空气清洗) - cleaning_volume: 清洗体积 - cleaning_repeats: 清洗重复次数 - - Returns: - List[Dict[str, Any]]: 动作序列 - """ - action_sequence = [] - - # 1. 智能查找试剂瓶 - reagent_vessel = find_reagent_vessel(G, reagent) - - # 2. **先启动搅拌** - if stir: - stirrer_id = find_connected_stirrer(G, vessel) - if stirrer_id: - action_sequence.append({ - "device_id": stirrer_id, - "action_name": "start_stir", - "action_kwargs": { - "vessel": vessel, - "stir_speed": stir_speed, - "purpose": f"{purpose}: 启动搅拌,准备添加 {reagent}" - } - }) - - # 等待搅拌稳定 - action_sequence.append({ - "action_name": "wait", - "action_kwargs": {"time": 5} - }) - - # 3. 计算流速 - if time > 0: - transfer_flowrate = volume / time - flowrate = transfer_flowrate - else: - if viscous: - transfer_flowrate = 0.3 - flowrate = 1.0 - else: - transfer_flowrate = 0.5 - flowrate = 2.5 - - # 4. 使用带清洗的pump_protocol - pump_actions = generate_pump_protocol_with_rinsing( - G=G, - from_vessel=reagent_vessel, - to_vessel=vessel, - volume=volume, - amount=amount, - time=time, - viscous=viscous, - rinsing_solvent=cleaning_solvent, - rinsing_volume=cleaning_volume, - rinsing_repeats=cleaning_repeats, - solid=False, - flowrate=flowrate, - transfer_flowrate=transfer_flowrate +# 处理 wait 动作 +def process_wait_action(action_kwargs: Dict[str, Any]) -> Dict[str, Any]: + """处理等待动作""" + wait_time = action_kwargs.get('time', 1.0) + return { + "action_name": "wait", + "action_kwargs": {"time": wait_time}, + "description": f"等待 {wait_time} 秒" + } + + +# 便捷函数 +def add_liquid(G: nx.DiGraph, vessel: str, reagent: str, volume: float, + time: float = 0.0, rate_spec: str = "") -> List[Dict[str, Any]]: + """添加液体试剂""" + return generate_add_protocol( + G, vessel, reagent, + volume=volume, + time=time, + rate_spec=rate_spec ) - - action_sequence.extend(pump_actions) - - return action_sequence -def generate_sequential_add_protocol( - G: nx.DiGraph, - vessel: str, - reagents: List[Dict[str, Any]], - stir_between_additions: bool = True, - final_stir: bool = True, - final_stir_speed: float = 400.0, - final_stir_time: float = 300.0 -) -> List[Dict[str, Any]]: - """ - 生成连续添加多种试剂的协议,支持智能试剂匹配 - - Args: - G: 网络图 - vessel: 目标容器 - reagents: 试剂列表,每个元素包含试剂添加参数 - stir_between_additions: 是否在每次添加之间搅拌 - final_stir: 是否在所有添加完成后进行最终搅拌 - final_stir_speed: 最终搅拌速度 - final_stir_time: 最终搅拌时间 - - Returns: - List[Dict[str, Any]]: 完整的动作序列 - - Example: - reagents = [ - { - "reagent": "DMF", # 会匹配 reagent_bottle_1 (reagent_name: "DMF") - "volume": 10.0, - "viscous": False, - "stir_speed": 300.0 - }, - { - "reagent": "ethyl_acetate", # 会匹配 reagent_bottle_2 (reagent_name: "ethyl_acetate") - "volume": 5.0, - "viscous": False, - "stir_speed": 350.0 - } - ] - """ - action_sequence = [] - - print(f"ADD_PROTOCOL: 开始连续添加 {len(reagents)} 种试剂到容器 {vessel}") - - for i, reagent_params in enumerate(reagents): - reagent_name = reagent_params.get('reagent') - print(f"ADD_PROTOCOL: 处理第 {i+1}/{len(reagents)} 个试剂: {reagent_name}") - - # 生成单个试剂的添加协议 - add_actions = generate_add_protocol( - G=G, - vessel=vessel, - reagent=reagent_name, - volume=reagent_params.get('volume', 0.0), - mass=reagent_params.get('mass', 0.0), - amount=reagent_params.get('amount', ''), - time=reagent_params.get('time', 0.0), - stir=stir_between_additions, - stir_speed=reagent_params.get('stir_speed', 300.0), - viscous=reagent_params.get('viscous', False), - purpose=reagent_params.get('purpose', f'添加试剂 {reagent_name} ({i+1}/{len(reagents)})') - ) - - action_sequence.extend(add_actions) - - # 在添加之间加入等待时间 - if i < len(reagents) - 1: # 不是最后一个试剂 - action_sequence.append({ - "action_name": "wait", - "action_kwargs": {"time": 10} # 试剂混合时间 - }) - - # 最终搅拌 - if final_stir: - stirrer_id = find_connected_stirrer(G, vessel) - if stirrer_id: - print(f"ADD_PROTOCOL: 添加最终搅拌动作,速度 {final_stir_speed} RPM,时间 {final_stir_time} 秒") - action_sequence.extend([ - { - "device_id": stirrer_id, - "action_name": "stir", - "action_kwargs": { - "stir_time": final_stir_time, - "stir_speed": final_stir_speed, - "settling_time": 30.0 - } - } - ]) - - print(f"ADD_PROTOCOL: 连续添加协议生成完成,共 {len(action_sequence)} 个动作") - return action_sequence +def add_solid(G: nx.DiGraph, vessel: str, reagent: str, mass: float, + event: str = "") -> List[Dict[str, Any]]: + """添加固体试剂""" + return generate_add_protocol( + G, vessel, reagent, + mass=mass, + event=event + ) -# 便捷函数:常用添加方案 -def generate_organic_add_protocol( - G: nx.DiGraph, - vessel: str, - organic_reagent: str, - volume: float, - stir_speed: float = 400.0 -) -> List[Dict[str, Any]]: - """有机试剂添加:慢速、搅拌""" +def add_solid_mol(G: nx.DiGraph, vessel: str, reagent: str, mol: str, + event: str = "") -> List[Dict[str, Any]]: + """按摩尔数添加固体试剂""" return generate_add_protocol( - G, vessel, organic_reagent, volume, 0.0, "", 0.0, - True, stir_speed, False, f"添加有机试剂 {organic_reagent}" + G, vessel, reagent, + mol=mol, + event=event ) -def generate_viscous_add_protocol( - G: nx.DiGraph, - vessel: str, - viscous_reagent: str, - volume: float, - addition_time: float = 120.0 -) -> List[Dict[str, Any]]: - """粘稠试剂添加:慢速、长时间""" +def add_dropwise(G: nx.DiGraph, vessel: str, reagent: str, volume: float, + time: float = 0.0, event: str = "") -> List[Dict[str, Any]]: + """滴加液体试剂""" return generate_add_protocol( - G, vessel, viscous_reagent, volume, 0.0, "", addition_time, - True, 250.0, True, f"缓慢添加粘稠试剂 {viscous_reagent}" + G, vessel, reagent, + volume=volume, + time=time, + rate_spec="dropwise", + event=event ) -def generate_solvent_add_protocol( - G: nx.DiGraph, - vessel: str, - solvent: str, - volume: float -) -> List[Dict[str, Any]]: - """溶剂添加:快速、无需特殊处理""" +def add_portionwise(G: nx.DiGraph, vessel: str, reagent: str, mass: float, + time: float = 0.0, event: str = "") -> List[Dict[str, Any]]: + """分批添加固体试剂""" return generate_add_protocol( - G, vessel, solvent, volume, 0.0, "", 0.0, - False, 300.0, False, f"添加溶剂 {solvent}" + G, vessel, reagent, + mass=mass, + time=time, + rate_spec="portionwise", + event=event ) -# 使用示例和测试函数 +# 测试函数 def test_add_protocol(): - """测试添加协议的示例""" - print("=== ADD PROTOCOL 智能匹配测试 ===") - print("测试完成") + """测试添加协议""" + print("=== ADD PROTOCOL 修复版测试 ===") + print("✅ 已修复设备查找逻辑") + print("✅ 已添加固体试剂瓶支持") + print("✅ 已修复错误处理") + print("✅ 测试完成") if __name__ == "__main__": diff --git a/unilabos/devices/virtual/virtual_solid_dispenser.py b/unilabos/devices/virtual/virtual_solid_dispenser.py new file mode 100644 index 000000000..48ad9f560 --- /dev/null +++ b/unilabos/devices/virtual/virtual_solid_dispenser.py @@ -0,0 +1,335 @@ +import asyncio +import logging +import re +from typing import Dict, Any, Optional + +class VirtualSolidDispenser: + """ + 虚拟固体粉末加样器 - 用于处理 Add Protocol 中的固体试剂添加 + + 特点: + - 高兼容性:缺少参数不报错 + - 智能识别:自动查找固体试剂瓶 + - 简单反馈:成功/失败 + 消息 + """ + + def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): + self.device_id = device_id or "virtual_solid_dispenser" + self.config = config or {} + + # 设备参数 + self.max_capacity = float(self.config.get('max_capacity', 100.0)) # 最大加样量 (g) + self.precision = float(self.config.get('precision', 0.001)) # 精度 (g) + + # 状态变量 + self._status = "Idle" + self._current_reagent = "" + self._dispensed_amount = 0.0 + self._total_operations = 0 + + self.logger = logging.getLogger(f"VirtualSolidDispenser.{self.device_id}") + + print(f"=== VirtualSolidDispenser {self.device_id} 创建成功! ===") + print(f"=== 最大容量: {self.max_capacity}g, 精度: {self.precision}g ===") + + async def initialize(self) -> bool: + """初始化固体加样器""" + self.logger.info(f"初始化固体加样器 {self.device_id}") + self._status = "Ready" + self._current_reagent = "" + self._dispensed_amount = 0.0 + return True + + async def cleanup(self) -> bool: + """清理固体加样器""" + self.logger.info(f"清理固体加样器 {self.device_id}") + self._status = "Idle" + return True + + def parse_mass_string(self, mass_str: str) -> float: + """ + 解析质量字符串为数值 (g) + + 支持格式: "2.9 g", "19.3g", "4.5 mg", "1.2 kg" 等 + """ + if not mass_str or not isinstance(mass_str, str): + return 0.0 + + # 移除空格并转小写 + mass_clean = mass_str.strip().lower() + + # 正则匹配数字和单位 + pattern = r'(\d+(?:\.\d+)?)\s*([a-z]*)' + match = re.search(pattern, mass_clean) + + if not match: + return 0.0 + + try: + value = float(match.group(1)) + unit = match.group(2) or 'g' # 默认单位 g + + # 单位转换为 g + unit_multipliers = { + 'g': 1.0, + 'gram': 1.0, + 'grams': 1.0, + 'mg': 0.001, + 'milligram': 0.001, + 'milligrams': 0.001, + 'kg': 1000.0, + 'kilogram': 1000.0, + 'kilograms': 1000.0, + 'μg': 0.000001, + 'ug': 0.000001, + 'microgram': 0.000001, + 'micrograms': 0.000001, + } + + multiplier = unit_multipliers.get(unit, 1.0) + return value * multiplier + + except (ValueError, TypeError): + self.logger.warning(f"无法解析质量字符串: {mass_str}") + return 0.0 + + def parse_mol_string(self, mol_str: str) -> float: + """ + 解析摩尔数字符串为数值 (mol) + + 支持格式: "0.12 mol", "16.2 mmol", "25.2mmol" 等 + """ + if not mol_str or not isinstance(mol_str, str): + return 0.0 + + # 移除空格并转小写 + mol_clean = mol_str.strip().lower() + + # 正则匹配数字和单位 + pattern = r'(\d+(?:\.\d+)?)\s*(m?mol)' + match = re.search(pattern, mol_clean) + + if not match: + return 0.0 + + try: + value = float(match.group(1)) + unit = match.group(2) + + # 单位转换为 mol + if unit == 'mmol': + return value * 0.001 + else: # mol + return value + + except (ValueError, TypeError): + self.logger.warning(f"无法解析摩尔数字符串: {mol_str}") + return 0.0 + + def find_solid_reagent_bottle(self, reagent_name: str) -> str: + """ + 查找固体试剂瓶 + + 这是一个简化版本,实际使用时应该连接到系统的设备图 + """ + if not reagent_name: + return "unknown_solid_bottle" + + # 可能的固体试剂瓶命名模式 + possible_names = [ + f"solid_bottle_{reagent_name}", + f"reagent_solid_{reagent_name}", + f"powder_{reagent_name}", + f"{reagent_name}_solid", + f"{reagent_name}_powder", + f"solid_{reagent_name}", + ] + + # 这里简化处理,实际应该查询设备图 + return possible_names[0] + + async def add_solid( + self, + vessel: str, + reagent: str, + mass: str = "", + mol: str = "", + purpose: str = "", + **kwargs # 兼容额外参数 + ) -> Dict[str, Any]: + """ + 添加固体试剂的主要方法 + + Args: + vessel: 目标容器 + reagent: 试剂名称 + mass: 质量字符串 (如 "2.9 g") + mol: 摩尔数字符串 (如 "0.12 mol") + purpose: 添加目的 + **kwargs: 其他兼容参数 + + Returns: + Dict: 操作结果 + """ + try: + self.logger.info(f"=== 开始固体加样操作 ===") + self.logger.info(f"目标容器: {vessel}") + self.logger.info(f"试剂: {reagent}") + self.logger.info(f"质量: {mass}") + self.logger.info(f"摩尔数: {mol}") + self.logger.info(f"目的: {purpose}") + + # 参数验证 - 宽松处理 + if not vessel: + vessel = "main_reactor" # 默认容器 + self.logger.warning(f"未指定容器,使用默认容器: {vessel}") + + if not reagent: + return { + "success": False, + "message": "错误: 必须指定试剂名称", + "return_info": "missing_reagent" + } + + # 解析质量和摩尔数 + mass_value = self.parse_mass_string(mass) + mol_value = self.parse_mol_string(mol) + + self.logger.info(f"解析后 - 质量: {mass_value}g, 摩尔数: {mol_value}mol") + + # 确定实际加样量 + if mass_value > 0: + actual_amount = mass_value + amount_unit = "g" + self.logger.info(f"按质量加样: {actual_amount} {amount_unit}") + elif mol_value > 0: + # 简化处理:假设分子量为100 g/mol + assumed_mw = 100.0 + actual_amount = mol_value * assumed_mw + amount_unit = "g (from mol)" + self.logger.info(f"按摩尔数加样: {mol_value} mol → {actual_amount} g (假设分子量 {assumed_mw})") + else: + # 没有指定量,使用默认值 + actual_amount = 1.0 + amount_unit = "g (default)" + self.logger.warning(f"未指定质量或摩尔数,使用默认值: {actual_amount} {amount_unit}") + + # 检查容量限制 + if actual_amount > self.max_capacity: + return { + "success": False, + "message": f"错误: 请求量 {actual_amount}g 超过最大容量 {self.max_capacity}g", + "return_info": "exceeds_capacity" + } + + # 查找试剂瓶 + reagent_bottle = self.find_solid_reagent_bottle(reagent) + self.logger.info(f"使用试剂瓶: {reagent_bottle}") + + # 模拟加样过程 + self._status = "Dispensing" + self._current_reagent = reagent + + # 计算操作时间 (基于质量) + operation_time = max(0.5, actual_amount * 0.1) # 每克0.1秒,最少0.5秒 + + self.logger.info(f"开始加样,预计时间: {operation_time:.1f}秒") + await asyncio.sleep(operation_time) + + # 更新状态 + self._dispensed_amount = actual_amount + self._total_operations += 1 + self._status = "Ready" + + # 成功结果 + success_message = f"成功添加 {reagent} {actual_amount:.3f} {amount_unit} 到 {vessel}" + + self.logger.info(f"=== 固体加样完成 ===") + self.logger.info(success_message) + + return { + "success": True, + "message": success_message, + "return_info": f"dispensed_{actual_amount:.3f}g" + } + + except Exception as e: + error_message = f"固体加样失败: {str(e)}" + self.logger.error(error_message) + self._status = "Error" + + return { + "success": False, + "message": error_message, + "return_info": "operation_failed" + } + + # 状态属性 + @property + def status(self) -> str: + return self._status + + @property + def current_reagent(self) -> str: + return self._current_reagent + + @property + def dispensed_amount(self) -> float: + return self._dispensed_amount + + @property + def total_operations(self) -> int: + return self._total_operations + + def get_device_info(self) -> Dict[str, Any]: + """获取设备状态信息""" + return { + "device_id": self.device_id, + "status": self._status, + "current_reagent": self._current_reagent, + "last_dispensed_amount": self._dispensed_amount, + "total_operations": self._total_operations, + "max_capacity": self.max_capacity, + "precision": self.precision + } + + def __str__(self): + return f"VirtualSolidDispenser({self.device_id}: {self._status}, 最后加样 {self._dispensed_amount:.3f}g)" + + +# 测试函数 +async def test_solid_dispenser(): + """测试固体加样器""" + print("=== 固体加样器测试 ===") + + dispenser = VirtualSolidDispenser("test_dispenser") + await dispenser.initialize() + + # 测试1: 按质量加样 + result1 = await dispenser.add_solid( + vessel="main_reactor", + reagent="magnesium", + mass="2.9 g" + ) + print(f"测试1结果: {result1}") + + # 测试2: 按摩尔数加样 + result2 = await dispenser.add_solid( + vessel="main_reactor", + reagent="sodium_nitrite", + mol="0.28 mol" + ) + print(f"测试2结果: {result2}") + + # 测试3: 缺少参数 + result3 = await dispenser.add_solid( + reagent="test_compound" + ) + print(f"测试3结果: {result3}") + + print(f"设备信息: {dispenser.get_device_info()}") + print("=== 测试完成 ===") + + +if __name__ == "__main__": + asyncio.run(test_solid_dispenser()) \ No newline at end of file diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml index 889431459..387f98663 100644 --- a/unilabos/registry/devices/virtual_device.yaml +++ b/unilabos/registry/devices/virtual_device.yaml @@ -2956,264 +2956,126 @@ virtual_solenoid_valve: - goal title: open参数 type: object - type: UniLabJsonCommandAsync - auto-reset: - feedback: {} - goal: {} - goal_default: {} - handles: [] - result: {} - schema: - description: reset的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: reset参数 - type: object - type: UniLabJsonCommandAsync - auto-set_state: + type: UniLabJsonCommand + auto-set_status: feedback: {} goal: {} goal_default: - command: null + string: null handles: [] result: {} schema: - description: set_state的参数schema + description: set_status的参数schema properties: feedback: {} goal: properties: - command: + string: type: string required: - - command - type: object - result: {} - required: - - goal - title: set_state参数 - type: object - type: UniLabJsonCommandAsync - auto-set_valve_position: - feedback: {} - goal: {} - goal_default: - command: null - handles: [] - result: {} - schema: - description: set_valve_position的参数schema - properties: - feedback: {} - goal: - properties: - command: - type: string - required: [] + - string type: object result: {} required: - goal - title: set_valve_position参数 + title: set_status参数 type: object - type: UniLabJsonCommandAsync - auto-toggle: + type: UniLabJsonCommand + close: feedback: {} goal: {} goal_default: {} handles: [] result: {} schema: - description: toggle的参数schema - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: toggle参数 - type: object - type: UniLabJsonCommand - close: - feedback: {} - goal: - command: CLOSED - goal_default: - command: '' - handles: [] - result: - success: success - schema: - description: ROS Action SendCmd 的 JSON Schema + description: ROS Action EmptyIn 的 JSON Schema properties: feedback: description: Action 反馈 - 执行过程中从服务器发送到客户端 - properties: - status: - type: string - required: - - status - title: SendCmd_Feedback + properties: {} + required: [] + title: EmptyIn_Feedback type: object goal: description: Action 目标 - 从客户端发送到服务器 - properties: - command: - type: string - required: - - command - title: SendCmd_Goal + properties: {} + required: [] + title: EmptyIn_Goal type: object result: description: Action 结果 - 完成后从服务器发送到客户端 properties: return_info: type: string - success: - type: boolean required: - return_info - - success - title: SendCmd_Result + title: EmptyIn_Result type: object required: - goal - title: SendCmd + title: EmptyIn type: object - type: SendCmd + type: EmptyIn open: feedback: {} - goal: - command: OPEN - goal_default: - command: '' - handles: [] - result: - success: success - schema: - description: ROS Action SendCmd 的 JSON Schema - properties: - feedback: - description: Action 反馈 - 执行过程中从服务器发送到客户端 - properties: - status: - type: string - required: - - status - title: SendCmd_Feedback - type: object - goal: - description: Action 目标 - 从客户端发送到服务器 - properties: - command: - type: string - required: - - command - title: SendCmd_Goal - type: object - result: - description: Action 结果 - 完成后从服务器发送到客户端 - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: SendCmd_Result - type: object - required: - - goal - title: SendCmd - type: object - type: SendCmd - set_state: - feedback: {} - goal: - command: command - goal_default: - command: '' + goal: {} + goal_default: {} handles: [] - result: - success: success + result: {} schema: - description: ROS Action SendCmd 的 JSON Schema + description: ROS Action EmptyIn 的 JSON Schema properties: feedback: description: Action 反馈 - 执行过程中从服务器发送到客户端 - properties: - status: - type: string - required: - - status - title: SendCmd_Feedback + properties: {} + required: [] + title: EmptyIn_Feedback type: object goal: description: Action 目标 - 从客户端发送到服务器 - properties: - command: - type: string - required: - - command - title: SendCmd_Goal + properties: {} + required: [] + title: EmptyIn_Goal type: object result: description: Action 结果 - 完成后从服务器发送到客户端 properties: return_info: type: string - success: - type: boolean required: - return_info - - success - title: SendCmd_Result + title: EmptyIn_Result type: object required: - goal - title: SendCmd + title: EmptyIn type: object - type: SendCmd - set_valve_position: + type: EmptyIn + set_status: feedback: {} goal: - command: command + string: string goal_default: - command: '' + string: '' handles: [] - result: - success: success + result: {} schema: - description: ROS Action SendCmd 的 JSON Schema + description: ROS Action StrSingleInput 的 JSON Schema properties: feedback: description: Action 反馈 - 执行过程中从服务器发送到客户端 - properties: - status: - type: string - required: - - status - title: SendCmd_Feedback + properties: {} + required: [] + title: StrSingleInput_Feedback type: object goal: description: Action 目标 - 从客户端发送到服务器 properties: - command: + string: type: string required: - - command - title: SendCmd_Goal + - string + title: StrSingleInput_Goal type: object result: description: Action 结果 - 完成后从服务器发送到客户端 @@ -3225,13 +3087,13 @@ virtual_solenoid_valve: required: - return_info - success - title: SendCmd_Result + title: StrSingleInput_Result type: object required: - goal - title: SendCmd + title: StrSingleInput type: object - type: SendCmd + type: StrSingleInput module: unilabos.devices.virtual.virtual_solenoid_valve:VirtualSolenoidValve status_types: is_open: bool @@ -3321,6 +3183,7 @@ virtual_stirrer: properties: feedback: {} goal: + properties: {} required: [] type: object @@ -4504,3 +4367,125 @@ virtual_vacuum_pump: required: - status type: object +virtual_solid_dispenser: + class: + action_value_mappings: + auto-cleanup: + feedback: {} + goal: {} + goal_default: {} + handles: [] + result: {} + schema: + description: cleanup的参数schema + type: object + type: UniLabJsonCommandAsync + auto-initialize: + feedback: {} + goal: {} + goal_default: {} + handles: [] + result: {} + schema: + description: initialize的参数schema + type: object + type: UniLabJsonCommandAsync + auto-add_solid: + feedback: {} + goal: {} + goal_default: + vessel: main_reactor + reagent: '' + mass: '' + mol: '' + purpose: '' + handles: [] + result: {} + schema: + description: add_solid的参数schema + type: object + type: UniLabJsonCommandAsync + add_solid: + feedback: + current_status: status + progress: progress + goal: + vessel: vessel + reagent: reagent + mass: mass + mol: mol + purpose: purpose + goal_default: + vessel: 'main_reactor' + reagent: '' + mass: '' + mol: '' + purpose: '' + handles: [] + result: + message: message + success: success + return_info: return_info + schema: + description: ROS Action AddSolid 的 JSON Schema + type: object + type: AddSolid + module: unilabos.devices.virtual.virtual_solid_dispenser:VirtualSolidDispenser + status_types: + status: str + current_reagent: str + dispensed_amount: float + total_operations: int + type: python + description: Virtual Solid Dispenser for Add Protocol Solid Reagents + handles: + - data_key: SolidIn + data_source: handle + data_type: resource + description: 固体试剂进料口 + handler_key: SolidIn + io_type: target + label: SolidIn + side: WEST + - data_key: SolidOut + data_source: executor + data_type: resource + description: 固体试剂出料口 + handler_key: SolidOut + io_type: source + label: SolidOut + side: EAST + icon: '' + init_param_schema: + config: + properties: + config: + type: object + device_id: + type: string + max_capacity: + type: number + default: 100.0 + description: 最大加样容量 (g) + precision: + type: number + default: 0.001 + description: 加样精度 (g) + required: [] + type: object + data: + properties: + status: + type: string + current_reagent: + type: string + dispensed_amount: + type: number + total_operations: + type: integer + required: + - status + - current_reagent + - dispensed_amount + - total_operations + type: object diff --git a/unilabos_msgs/action/AddSolid.action b/unilabos_msgs/action/AddSolid.action new file mode 100644 index 000000000..881244189 --- /dev/null +++ b/unilabos_msgs/action/AddSolid.action @@ -0,0 +1,15 @@ +# Goal - 固体加样操作的目标参数 +string vessel # 目标容器(必需) +string reagent # 试剂名称(必需) +string mass # 质量字符串(如 "2.9 g",可选) +string mol # 摩尔数字符串(如 "0.12 mol",可选) +string purpose # 添加目的(可选) +--- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 +string return_info # 返回信息 +--- +# Feedback - 实时反馈 +string current_status # 当前状态描述 +float64 progress # 进度百分比 (0-100) \ No newline at end of file From acc9e5ce0db2279d9b4b6f9b879734b296421a35 Mon Sep 17 00:00:00 2001 From: KCFeng425 <2100011801@stu.pku.edu.cn> Date: Mon, 7 Jul 2025 18:44:01 +0800 Subject: [PATCH 4/9] bump version to 0.9.9 --- README.md | 2 +- README_zh.md | 2 +- recipes/ros-humble-unilabos-msgs/recipe.yaml | 2 +- recipes/unilabos/recipe.yaml | 2 +- setup.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0d02849c2..287efb50d 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n environment_name # Currently, you need to install the `unilabos_msgs` package # You can download the system-specific package from the Release page -conda install ros-humble-unilabos-msgs-0.9.8-xxxxx.tar.bz2 +conda install ros-humble-unilabos-msgs-0.9.9-xxxxx.tar.bz2 # Install PyLabRobot and other prerequisites git clone https://github.com/PyLabRobot/pylabrobot plr_repo diff --git a/README_zh.md b/README_zh.md index a80f06cf0..8bba3d77b 100644 --- a/README_zh.md +++ b/README_zh.md @@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n 环境名 # 现阶段,需要安装 `unilabos_msgs` 包 # 可以前往 Release 页面下载系统对应的包进行安装 -conda install ros-humble-unilabos-msgs-0.9.8-xxxxx.tar.bz2 +conda install ros-humble-unilabos-msgs-0.9.9-xxxxx.tar.bz2 # 安装PyLabRobot等前置 git clone https://github.com/PyLabRobot/pylabrobot plr_repo diff --git a/recipes/ros-humble-unilabos-msgs/recipe.yaml b/recipes/ros-humble-unilabos-msgs/recipe.yaml index dde6acc8f..4169c73d2 100644 --- a/recipes/ros-humble-unilabos-msgs/recipe.yaml +++ b/recipes/ros-humble-unilabos-msgs/recipe.yaml @@ -1,6 +1,6 @@ package: name: ros-humble-unilabos-msgs - version: 0.9.8 + version: 0.9.9 source: path: ../../unilabos_msgs folder: ros-humble-unilabos-msgs/src/work diff --git a/recipes/unilabos/recipe.yaml b/recipes/unilabos/recipe.yaml index c54e14122..f9262e2b7 100644 --- a/recipes/unilabos/recipe.yaml +++ b/recipes/unilabos/recipe.yaml @@ -1,6 +1,6 @@ package: name: unilabos - version: "0.9.8" + version: "0.9.9" source: path: ../.. diff --git a/setup.py b/setup.py index 4dde10701..2d9aabdb9 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name=package_name, - version='0.9.8', + version='0.9.9', packages=find_packages(), include_package_data=True, install_requires=['setuptools'], From d82ccd5cf1afc8c9203ced40ce94512e474481ec Mon Sep 17 00:00:00 2001 From: Junhan Chang Date: Tue, 8 Jul 2025 17:25:42 +0800 Subject: [PATCH 5/9] fix bugs from new actions --- unilabos/compile/heatchill_protocol.py | 136 ++ unilabos/compile/pump_protocol.py | 22 +- unilabos/messages/__init__.py | 27 +- unilabos/registry/devices/mock_devices.yaml | 83 +- .../devices/organic_miscellaneous.yaml | 21 +- unilabos/registry/devices/temperature.yaml | 21 +- unilabos/registry/devices/virtual_device.yaml | 408 ++++-- unilabos/registry/devices/work_station.yaml | 1304 +++++++++-------- unilabos_msgs/CMakeLists.txt | 1 + unilabos_msgs/action/RunColumn.action | 2 +- 10 files changed, 1297 insertions(+), 728 deletions(-) diff --git a/unilabos/compile/heatchill_protocol.py b/unilabos/compile/heatchill_protocol.py index 260d9bc9e..3e7f551f8 100644 --- a/unilabos/compile/heatchill_protocol.py +++ b/unilabos/compile/heatchill_protocol.py @@ -247,6 +247,142 @@ def generate_heat_chill_protocol( return action_sequence + +def generate_heat_chill_to_temp_protocol( + G: nx.DiGraph, + vessel: str, + 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 = "", + **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 = [] + + # === 参数验证 === + debug_print("步骤1: 参数验证...") + + # 验证必需参数 + if not vessel: + raise ValueError("vessel 参数不能为空") + + if vessel not in G.nodes(): + raise ValueError(f"容器 '{vessel}' 不存在于系统中") + + # 温度解析:优先使用 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) + debug_print(f"设备配置: 加热设备 = {heatchill_id}") + + except Exception as e: + debug_print(f"❌ 设备查找失败: {str(e)}") + raise ValueError(f"无法找到加热设备: {str(e)}") + + # === 执行加热操作 === + debug_print("步骤3: 执行加热操作...") + + heatchill_action = { + "device_id": heatchill_id, + "action_name": "heat_chill", + "action_kwargs": { + "vessel": vessel, + "temp": final_temp, + "time": final_time, + "stir": stir, + "stir_speed": stir_speed, + "purpose": purpose or f"加热到 {final_temp}°C" + } + } + + action_sequence.append(heatchill_action) + + # === 总结 === + 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, diff --git a/unilabos/compile/pump_protocol.py b/unilabos/compile/pump_protocol.py index 5200188ab..acdb52c6d 100644 --- a/unilabos/compile/pump_protocol.py +++ b/unilabos/compile/pump_protocol.py @@ -778,17 +778,17 @@ def generate_pump_protocol_with_rinsing( ) # 为每个动作添加唯一标识 - 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)} 个动作") + # 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 diff --git a/unilabos/messages/__init__.py b/unilabos/messages/__init__.py index dfdbab389..52fd97f84 100644 --- a/unilabos/messages/__init__.py +++ b/unilabos/messages/__init__.py @@ -372,6 +372,30 @@ def _parse_time_spec(self, time_spec: str) -> float: return 300.0 # 默认5分钟 + +class HeatChillStartProtocol(BaseModel): + # === 必需参数 === + vessel: str = Field(..., description="加热容器名称") + + # === 可选参数 - 温度相关 === + temp: float = Field(25.0, description="目标温度 (°C)") + temp_spec: str = Field("", description="温度规格(如 'room temperature', 'reflux')") + + # === 可选参数 - 其他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="操作目的") + + +class HeatChillStopProtocol(BaseModel): + # === 必需参数 === + vessel: str = Field(..., description="加热容器名称") + + class StirProtocol(BaseModel): # === 必需参数 === vessel: str = Field(..., description="搅拌容器名称") @@ -608,7 +632,8 @@ class HydrogenateProtocol(BaseModel): "Point3D", "PumpTransferProtocol", "CleanProtocol", "SeparateProtocol", "EvaporateProtocol", "EvacuateAndRefillProtocol", "AGVTransferProtocol", "CentrifugeProtocol", "AddProtocol", "FilterProtocol", - "HeatChillProtocol", "HeatChillStartProtocol", "HeatChillStopProtocol", + "HeatChillProtocol", + "HeatChillStartProtocol", "HeatChillStopProtocol", "StirProtocol", "StartStirProtocol", "StopStirProtocol", "TransferProtocol", "CleanVesselProtocol", "DissolveProtocol", "FilterThroughProtocol", "RunColumnProtocol", "WashSolidProtocol", diff --git a/unilabos/registry/devices/mock_devices.yaml b/unilabos/registry/devices/mock_devices.yaml index 4341434de..ea5c7e8a1 100644 --- a/unilabos/registry/devices/mock_devices.yaml +++ b/unilabos/registry/devices/mock_devices.yaml @@ -842,11 +842,15 @@ mock_heater: time: time vessel: vessel goal_default: + pressure: '' purpose: '' + reflux_solvent: '' stir: false stir_speed: 0.0 temp: 0.0 + temp_spec: '' time: 0.0 + time_spec: '' vessel: '' handles: [] result: @@ -866,22 +870,34 @@ mock_heater: goal: description: Action 目标 - 从客户端发送到服务器 properties: + pressure: + type: string purpose: type: string + reflux_solvent: + type: string stir: type: boolean stir_speed: type: number temp: type: number + temp_spec: + type: string time: type: number + time_spec: + type: string vessel: type: string required: - vessel - temp - time + - temp_spec + - time_spec + - pressure + - reflux_solvent - stir - stir_speed - purpose @@ -890,13 +906,16 @@ mock_heater: result: 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: @@ -1270,13 +1289,18 @@ mock_pump: volume: volume goal_default: amount: '' + event: '' + flowrate: 0.0 from_vessel: '' + rate_spec: '' rinsing_repeats: 0 rinsing_solvent: '' rinsing_volume: 0.0 solid: false + through: '' time: 0.0 to_vessel: '' + transfer_flowrate: 0.0 viscous: false volume: 0.0 handles: [] @@ -1334,8 +1358,14 @@ mock_pump: properties: amount: type: string + event: + type: string + flowrate: + type: number from_vessel: type: string + rate_spec: + type: string rinsing_repeats: maximum: 2147483647 minimum: -2147483648 @@ -1346,10 +1376,14 @@ mock_pump: type: number solid: type: boolean + through: + type: string time: type: number to_vessel: type: string + transfer_flowrate: + type: number viscous: type: boolean volume: @@ -1365,6 +1399,11 @@ mock_pump: - rinsing_volume - rinsing_repeats - solid + - flowrate + - transfer_flowrate + - rate_spec + - event + - through title: PumpTransfer_Goal type: object result: @@ -2281,6 +2320,7 @@ mock_separator: goal_default: from_vessel: '' product_phase: '' + product_vessel: '' purpose: '' repeats: 0 separation_vessel: '' @@ -2291,7 +2331,10 @@ mock_separator: stir_time: 0.0 through: '' to_vessel: '' + vessel: '' + volume: '' waste_phase_to_vessel: '' + waste_vessel: '' handles: [] result: success: success @@ -2349,6 +2392,8 @@ mock_separator: type: string product_phase: type: string + product_vessel: + type: string purpose: type: string repeats: @@ -2371,8 +2416,14 @@ mock_separator: type: string to_vessel: type: string + vessel: + type: string + volume: + type: string waste_phase_to_vessel: type: string + waste_vessel: + type: string required: - purpose - product_phase @@ -2387,18 +2438,25 @@ mock_separator: - stir_time - stir_speed - settling_time + - vessel + - volume + - product_vessel + - waste_vessel title: Separate_Goal type: object result: description: Action 结果 - 完成后从服务器发送到客户端 properties: + message: + type: string return_info: type: string success: type: boolean required: - - return_info - success + - message + - return_info title: Separate_Result type: object required: @@ -3472,9 +3530,13 @@ mock_stirrer_new: stir_speed: stir_speed stir_time: stir_time goal_default: + event: '' settling_time: 0.0 stir_speed: 0.0 stir_time: 0.0 + time: '' + time_spec: '' + vessel: '' handles: [] result: success: success @@ -3493,13 +3555,25 @@ mock_stirrer_new: goal: description: Action 目标 - 从客户端发送到服务器 properties: + event: + type: string settling_time: type: number stir_speed: type: number stir_time: type: number + time: + type: string + time_spec: + type: string + vessel: + type: string required: + - vessel + - time + - event + - time_spec - stir_time - stir_speed - settling_time @@ -3508,13 +3582,16 @@ mock_stirrer_new: result: 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: diff --git a/unilabos/registry/devices/organic_miscellaneous.yaml b/unilabos/registry/devices/organic_miscellaneous.yaml index 00e7550af..954970512 100644 --- a/unilabos/registry/devices/organic_miscellaneous.yaml +++ b/unilabos/registry/devices/organic_miscellaneous.yaml @@ -324,9 +324,13 @@ separator.homemade: stir_speed: stir_speed stir_time: stir_time, goal_default: + event: '' settling_time: 0.0 stir_speed: 0.0 stir_time: 0.0 + time: '' + time_spec: '' + vessel: '' handles: [] result: success: success @@ -345,13 +349,25 @@ separator.homemade: goal: description: Action 目标 - 从客户端发送到服务器 properties: + event: + type: string settling_time: type: number stir_speed: type: number stir_time: type: number + time: + type: string + time_spec: + type: string + vessel: + type: string required: + - vessel + - time + - event + - time_spec - stir_time - stir_speed - settling_time @@ -360,13 +376,16 @@ separator.homemade: result: 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: diff --git a/unilabos/registry/devices/temperature.yaml b/unilabos/registry/devices/temperature.yaml index 68a4f5ef1..6c606704f 100644 --- a/unilabos/registry/devices/temperature.yaml +++ b/unilabos/registry/devices/temperature.yaml @@ -377,11 +377,15 @@ heaterstirrer.dalong: time: time vessel: vessel goal_default: + pressure: '' purpose: '' + reflux_solvent: '' stir: false stir_speed: 0.0 temp: 0.0 + temp_spec: '' time: 0.0 + time_spec: '' vessel: '' handles: [] result: @@ -401,22 +405,34 @@ heaterstirrer.dalong: goal: description: Action 目标 - 从客户端发送到服务器 properties: + pressure: + type: string purpose: type: string + reflux_solvent: + type: string stir: type: boolean stir_speed: type: number temp: type: number + temp_spec: + type: string time: type: number + time_spec: + type: string vessel: type: string required: - vessel - temp - time + - temp_spec + - time_spec + - pressure + - reflux_solvent - stir - stir_speed - purpose @@ -425,13 +441,16 @@ heaterstirrer.dalong: result: 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: diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml index 387f98663..1bbc5f7f1 100644 --- a/unilabos/registry/devices/virtual_device.yaml +++ b/unilabos/registry/devices/virtual_device.yaml @@ -318,6 +318,12 @@ virtual_column: goal_default: column: '' from_vessel: '' + pct1: '' + pct2: '' + ratio: '' + rf: '' + solvent1: '' + solvent2: '' to_vessel: '' handles: [] result: @@ -346,12 +352,30 @@ virtual_column: type: string from_vessel: type: string + pct1: + type: string + pct2: + type: string + ratio: + type: string + rf: + type: string + solvent1: + type: string + solvent2: + type: string to_vessel: type: string required: - from_vessel - to_vessel - column + - rf + - pct1 + - pct2 + - solvent1 + - solvent2 + - ratio title: RunColumn_Goal type: object result: @@ -1155,11 +1179,15 @@ virtual_heatchill: time: time vessel: vessel goal_default: + pressure: '' purpose: '' + reflux_solvent: '' stir: false stir_speed: 0.0 temp: 0.0 + temp_spec: '' time: 0.0 + time_spec: '' vessel: '' handles: [] result: @@ -1179,22 +1207,34 @@ virtual_heatchill: goal: description: Action 目标 - 从客户端发送到服务器 properties: + pressure: + type: string purpose: type: string + reflux_solvent: + type: string stir: type: boolean stir_speed: type: number temp: type: number + temp_spec: + type: string time: type: number + time_spec: + type: string vessel: type: string required: - vessel - temp - time + - temp_spec + - time_spec + - pressure + - reflux_solvent - stir - stir_speed - purpose @@ -1203,13 +1243,16 @@ virtual_heatchill: result: 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: @@ -2048,13 +2091,18 @@ virtual_pump: volume: volume goal_default: amount: '' + event: '' + flowrate: 0.0 from_vessel: '' + rate_spec: '' rinsing_repeats: 0 rinsing_solvent: '' rinsing_volume: 0.0 solid: false + through: '' time: 0.0 to_vessel: '' + transfer_flowrate: 0.0 viscous: false volume: 0.0 handles: [] @@ -2112,8 +2160,14 @@ virtual_pump: properties: amount: type: string + event: + type: string + flowrate: + type: number from_vessel: type: string + rate_spec: + type: string rinsing_repeats: maximum: 2147483647 minimum: -2147483648 @@ -2124,10 +2178,14 @@ virtual_pump: type: number solid: type: boolean + through: + type: string time: type: number to_vessel: type: string + transfer_flowrate: + type: number viscous: type: boolean volume: @@ -2143,6 +2201,11 @@ virtual_pump: - rinsing_volume - rinsing_repeats - solid + - flowrate + - transfer_flowrate + - rate_spec + - event + - through title: PumpTransfer_Goal type: object result: @@ -2325,6 +2388,7 @@ virtual_rotavap: vessel: vessel goal_default: pressure: 0.0 + solvent: '' stir_speed: 0.0 temp: 0.0 time: 0.0 @@ -2385,6 +2449,8 @@ virtual_rotavap: properties: pressure: type: number + solvent: + type: string stir_speed: type: number temp: @@ -2399,6 +2465,7 @@ virtual_rotavap: - temp - time - stir_speed + - solvent title: Evaporate_Goal type: object result: @@ -2641,6 +2708,7 @@ virtual_separator: goal_default: from_vessel: '' product_phase: '' + product_vessel: '' purpose: '' repeats: 0 separation_vessel: '' @@ -2651,7 +2719,10 @@ virtual_separator: stir_time: 0.0 through: '' to_vessel: '' + vessel: '' + volume: '' waste_phase_to_vessel: '' + waste_vessel: '' handles: [] result: message: message @@ -2710,6 +2781,8 @@ virtual_separator: type: string product_phase: type: string + product_vessel: + type: string purpose: type: string repeats: @@ -2732,8 +2805,14 @@ virtual_separator: type: string to_vessel: type: string + vessel: + type: string + volume: + type: string waste_phase_to_vessel: type: string + waste_vessel: + type: string required: - purpose - product_phase @@ -2748,18 +2827,25 @@ virtual_separator: - stir_time - stir_speed - settling_time + - vessel + - volume + - product_vessel + - waste_vessel title: Separate_Goal type: object result: description: Action 结果 - 完成后从服务器发送到客户端 properties: + message: + type: string return_info: type: string success: type: boolean required: - - return_info - success + - message + - return_info title: Separate_Result type: object required: @@ -3149,6 +3235,180 @@ virtual_solenoid_valve: - valve_position - state type: object +virtual_solid_dispenser: + class: + action_value_mappings: + add_solid: + feedback: + current_status: status + progress: progress + goal: + mass: mass + mol: mol + purpose: purpose + reagent: reagent + vessel: vessel + goal_default: + mass: '' + mol: '' + purpose: '' + reagent: '' + vessel: '' + handles: [] + result: + message: message + return_info: return_info + success: success + schema: + description: ROS Action AddSolid 的 JSON Schema + properties: + feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 + properties: + current_status: + type: string + progress: + type: number + required: + - current_status + - progress + title: AddSolid_Feedback + type: object + goal: + description: Action 目标 - 从客户端发送到服务器 + properties: + mass: + type: string + mol: + type: string + purpose: + type: string + reagent: + type: string + vessel: + type: string + required: + - vessel + - reagent + - mass + - mol + - purpose + title: AddSolid_Goal + type: object + result: + description: Action 结果 - 完成后从服务器发送到客户端 + properties: + message: + type: string + return_info: + type: string + success: + type: boolean + required: + - success + - message + - return_info + title: AddSolid_Result + type: object + required: + - goal + title: AddSolid + type: object + type: AddSolid + auto-add_solid: + feedback: {} + goal: {} + goal_default: + mass: '' + mol: '' + purpose: '' + reagent: '' + vessel: main_reactor + handles: [] + result: {} + schema: + description: add_solid的参数schema + type: object + type: UniLabJsonCommandAsync + auto-cleanup: + feedback: {} + goal: {} + goal_default: {} + handles: [] + result: {} + schema: + description: cleanup的参数schema + type: object + type: UniLabJsonCommandAsync + auto-initialize: + feedback: {} + goal: {} + goal_default: {} + handles: [] + result: {} + schema: + description: initialize的参数schema + type: object + type: UniLabJsonCommandAsync + module: unilabos.devices.virtual.virtual_solid_dispenser:VirtualSolidDispenser + status_types: + current_reagent: str + dispensed_amount: float + status: str + total_operations: int + type: python + description: Virtual Solid Dispenser for Add Protocol Solid Reagents + handles: + - data_key: SolidIn + data_source: handle + data_type: resource + description: 固体试剂进料口 + handler_key: SolidIn + io_type: target + label: SolidIn + side: WEST + - data_key: SolidOut + data_source: executor + data_type: resource + description: 固体试剂出料口 + handler_key: SolidOut + io_type: source + label: SolidOut + side: EAST + icon: '' + init_param_schema: + config: + properties: + config: + type: object + device_id: + type: string + max_capacity: + default: 100.0 + description: 最大加样容量 (g) + type: number + precision: + default: 0.001 + description: 加样精度 (g) + type: number + required: [] + type: object + data: + properties: + current_reagent: + type: string + dispensed_amount: + type: number + status: + type: string + total_operations: + type: integer + required: + - status + - current_reagent + - dispensed_amount + - total_operations + type: object virtual_stirrer: class: action_value_mappings: @@ -3183,7 +3443,6 @@ virtual_stirrer: properties: feedback: {} goal: - properties: {} required: [] type: object @@ -3356,9 +3615,13 @@ virtual_stirrer: stir_speed: stir_speed stir_time: stir_time goal_default: + event: '' settling_time: 0.0 stir_speed: 0.0 stir_time: 0.0 + time: '' + time_spec: '' + vessel: '' handles: [] result: success: success @@ -3377,13 +3640,25 @@ virtual_stirrer: goal: description: Action 目标 - 从客户端发送到服务器 properties: + event: + type: string settling_time: type: number stir_speed: type: number stir_time: type: number + time: + type: string + time_spec: + type: string + vessel: + type: string required: + - vessel + - time + - event + - time_spec - stir_time - stir_speed - settling_time @@ -3392,13 +3667,16 @@ virtual_stirrer: result: 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: @@ -4367,125 +4645,3 @@ virtual_vacuum_pump: required: - status type: object -virtual_solid_dispenser: - class: - action_value_mappings: - auto-cleanup: - feedback: {} - goal: {} - goal_default: {} - handles: [] - result: {} - schema: - description: cleanup的参数schema - type: object - type: UniLabJsonCommandAsync - auto-initialize: - feedback: {} - goal: {} - goal_default: {} - handles: [] - result: {} - schema: - description: initialize的参数schema - type: object - type: UniLabJsonCommandAsync - auto-add_solid: - feedback: {} - goal: {} - goal_default: - vessel: main_reactor - reagent: '' - mass: '' - mol: '' - purpose: '' - handles: [] - result: {} - schema: - description: add_solid的参数schema - type: object - type: UniLabJsonCommandAsync - add_solid: - feedback: - current_status: status - progress: progress - goal: - vessel: vessel - reagent: reagent - mass: mass - mol: mol - purpose: purpose - goal_default: - vessel: 'main_reactor' - reagent: '' - mass: '' - mol: '' - purpose: '' - handles: [] - result: - message: message - success: success - return_info: return_info - schema: - description: ROS Action AddSolid 的 JSON Schema - type: object - type: AddSolid - module: unilabos.devices.virtual.virtual_solid_dispenser:VirtualSolidDispenser - status_types: - status: str - current_reagent: str - dispensed_amount: float - total_operations: int - type: python - description: Virtual Solid Dispenser for Add Protocol Solid Reagents - handles: - - data_key: SolidIn - data_source: handle - data_type: resource - description: 固体试剂进料口 - handler_key: SolidIn - io_type: target - label: SolidIn - side: WEST - - data_key: SolidOut - data_source: executor - data_type: resource - description: 固体试剂出料口 - handler_key: SolidOut - io_type: source - label: SolidOut - side: EAST - icon: '' - init_param_schema: - config: - properties: - config: - type: object - device_id: - type: string - max_capacity: - type: number - default: 100.0 - description: 最大加样容量 (g) - precision: - type: number - default: 0.001 - description: 加样精度 (g) - required: [] - type: object - data: - properties: - status: - type: string - current_reagent: - type: string - dispensed_amount: - type: number - total_operations: - type: integer - required: - - status - - current_reagent - - dispensed_amount - - total_operations - type: object diff --git a/unilabos/registry/devices/work_station.yaml b/unilabos/registry/devices/work_station.yaml index 3c4e3b671..91ea9bddf 100644 --- a/unilabos/registry/devices/work_station.yaml +++ b/unilabos/registry/devices/work_station.yaml @@ -257,8 +257,13 @@ workstation: volume: volume goal_default: amount: '' + equiv: '' + event: '' mass: 0.0 + mol: '' purpose: '' + rate_spec: '' + ratio: '' reagent: '' stir: false stir_speed: 0.0 @@ -305,10 +310,20 @@ workstation: properties: amount: type: string + equiv: + type: string + event: + type: string mass: type: number + mol: + type: string purpose: type: string + rate_spec: + type: string + ratio: + type: string reagent: type: string stir: @@ -334,6 +349,11 @@ workstation: - stir_speed - viscous - purpose + - event + - mol + - rate_spec + - equiv + - ratio title: Add_Goal type: object result: @@ -356,6 +376,85 @@ workstation: title: Add type: object type: Add + AdjustPHProtocol: + feedback: {} + goal: + ph_value: ph_value + reagent: reagent + vessel: vessel + goal_default: + ph_value: 0.0 + reagent: '' + vessel: '' + handles: + input: + - data_key: vessel + data_source: handle + data_type: resource + handler_key: Vessel + label: Vessel + - data_key: reagent + data_source: handle + data_type: resource + handler_key: reagent + label: Reagent + output: + - data_key: vessel + data_source: executor + data_type: resource + handler_key: VesselOut + label: Vessel + result: {} + schema: + description: ROS Action AdjustPH 的 JSON Schema + properties: + feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 + properties: + progress: + type: number + status: + type: string + required: + - status + - progress + title: AdjustPH_Feedback + type: object + goal: + description: Action 目标 - 从客户端发送到服务器 + properties: + ph_value: + type: number + reagent: + type: string + vessel: + type: string + required: + - vessel + - ph_value + - reagent + title: AdjustPH_Goal + type: object + result: + description: Action 结果 - 完成后从服务器发送到客户端 + properties: + message: + type: string + return_info: + type: string + success: + type: boolean + required: + - success + - message + - return_info + title: AdjustPH_Result + type: object + required: + - goal + title: AdjustPH + type: object + type: AdjustPH CentrifugeProtocol: feedback: {} goal: @@ -664,6 +763,9 @@ workstation: volume: volume goal_default: amount: '' + mass: '' + mol: '' + reagent: '' solvent: '' stir_speed: 0.0 temp: 0.0 @@ -709,6 +811,12 @@ workstation: properties: amount: type: string + mass: + type: string + mol: + type: string + reagent: + type: string solvent: type: string stir_speed: @@ -729,6 +837,9 @@ workstation: - temp - time - stir_speed + - mass + - mol + - reagent title: Dissolve_Goal type: object result: @@ -751,6 +862,75 @@ workstation: title: Dissolve type: object type: Dissolve + DryProtocol: + feedback: {} + goal: + compound: compound + vessel: vessel + goal_default: + compound: '' + vessel: '' + handles: + input: + - data_key: vessel + data_source: handle + data_type: resource + handler_key: Vessel + label: Vessel + output: + - data_key: vessel + data_source: executor + data_type: resource + handler_key: VesselOut + label: Vessel + result: {} + schema: + description: ROS Action Dry 的 JSON Schema + properties: + feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 + properties: + progress: + type: number + status: + type: string + required: + - status + - progress + title: Dry_Feedback + type: object + goal: + description: Action 目标 - 从客户端发送到服务器 + properties: + compound: + type: string + vessel: + type: string + required: + - compound + - vessel + title: Dry_Goal + type: object + result: + description: Action 结果 - 完成后从服务器发送到客户端 + properties: + message: + type: string + return_info: + type: string + success: + type: boolean + required: + - success + - message + - return_info + title: Dry_Result + type: object + required: + - goal + title: Dry + type: object + type: Dry EvacuateAndRefillProtocol: feedback: {} goal: @@ -852,19 +1032,19 @@ workstation: EvaporateProtocol: feedback: {} goal: - vessel: vessel pressure: pressure + solvent: solvent + stir_speed: stir_speed temp: temp time: time - stir_speed: stir_speed - solvent: solvent + vessel: vessel goal_default: - vessel: '' - pressure: 0.1 - temp: 60.0 - time: 1800.0 - stir_speed: 100.0 + pressure: 0.0 solvent: '' + stir_speed: 0.0 + temp: 0.0 + time: 0.0 + vessel: '' handles: input: - data_key: vessel @@ -882,36 +1062,86 @@ workstation: schema: description: ROS Action Evaporate 的 JSON Schema properties: - goal: - description: Action 目标 + feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: - vessel: + current_device: + type: string + status: type: string - description: 蒸发容器名称 + 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 目标 - 从客户端发送到服务器 + properties: pressure: type: number - default: 0.1 - description: 真空度 (bar) + solvent: + type: string + stir_speed: + type: number 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: + vessel: type: string - default: '' - description: 溶剂名称 required: - vessel + - pressure + - temp + - time + - stir_speed + - solvent 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 @@ -920,20 +1150,20 @@ workstation: FilterProtocol: feedback: {} goal: - vessel: vessel + continue_heatchill: continue_heatchill filtrate_vessel: filtrate_vessel stir: stir stir_speed: stir_speed temp: temp - continue_heatchill: continue_heatchill + vessel: vessel volume: volume goal_default: - vessel: '' + continue_heatchill: false filtrate_vessel: '' stir: false stir_speed: 0.0 - temp: 25.0 - continue_heatchill: false + temp: 0.0 + vessel: '' volume: 0.0 handles: input: @@ -983,35 +1213,28 @@ workstation: goal: description: Action 目标 - 从客户端发送到服务器 properties: - vessel: - type: string - description: 过滤容器名称 + continue_heatchill: + type: boolean 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: 是否继续加热冷却 + vessel: + type: string volume: type: number - default: 0.0 - description: 过滤体积 required: - vessel + - filtrate_vessel + - stir + - stir_speed + - temp + - continue_heatchill + - volume title: Filter_Goal type: object result: @@ -1148,27 +1371,27 @@ workstation: HeatChillProtocol: feedback: {} goal: - vessel: vessel - temp: temp - time: time - temp_spec: temp_spec - time_spec: time_spec pressure: pressure + purpose: purpose reflux_solvent: reflux_solvent stir: stir stir_speed: stir_speed - purpose: purpose + temp: temp + temp_spec: temp_spec + time: time + time_spec: time_spec + vessel: vessel goal_default: - vessel: '' - temp: 25.0 - time: 300.0 - temp_spec: '' - time_spec: '' pressure: '' + purpose: '' reflux_solvent: '' stir: false - stir_speed: 300.0 - purpose: '' + stir_speed: 0.0 + temp: 0.0 + temp_spec: '' + time: 0.0 + time_spec: '' + vessel: '' handles: input: - data_key: vessel @@ -1187,7 +1410,7 @@ workstation: description: ROS Action HeatChill 的 JSON Schema properties: feedback: - description: Action 反馈 + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: status: type: string @@ -1196,53 +1419,43 @@ workstation: title: HeatChill_Feedback type: object goal: - description: Action 目标 + description: Action 目标 - 从客户端发送到服务器 properties: - 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: 压力规格 + purpose: + type: string reflux_solvent: type: string - default: '' - description: 回流溶剂名称 stir: type: boolean - default: false - description: 是否搅拌 stir_speed: type: number - default: 300.0 - description: 搅拌速度 - purpose: + temp: + type: number + temp_spec: + type: string + time: + type: number + time_spec: + type: string + vessel: type: string - default: '' - description: 操作目的 required: - vessel + - temp + - time + - temp_spec + - time_spec + - pressure + - reflux_solvent + - stir + - stir_speed + - purpose title: HeatChill_Goal type: object result: - description: Action 结果 + description: Action 结果 - 完成后从服务器发送到客户端 properties: message: type: string @@ -1387,40 +1600,114 @@ workstation: title: HeatChillStop type: object type: HeatChillStop + HydrogenateProtocol: + feedback: {} + goal: + temp: temp + time: time + vessel: vessel + goal_default: + temp: '' + time: '' + vessel: '' + handles: + input: + - data_key: vessel + data_source: handle + data_type: resource + handler_key: Vessel + label: Vessel + output: + - data_key: vessel + data_source: executor + data_type: resource + handler_key: VesselOut + label: Vessel + result: {} + schema: + description: ROS Action Hydrogenate 的 JSON Schema + properties: + feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 + properties: + progress: + type: number + status: + type: string + required: + - status + - progress + title: Hydrogenate_Feedback + type: object + goal: + description: Action 目标 - 从客户端发送到服务器 + properties: + temp: + type: string + time: + type: string + vessel: + type: string + required: + - temp + - time + - vessel + title: Hydrogenate_Goal + type: object + result: + description: Action 结果 - 完成后从服务器发送到客户端 + properties: + message: + type: string + return_info: + type: string + success: + type: boolean + required: + - success + - message + - return_info + title: Hydrogenate_Result + type: object + required: + - goal + title: Hydrogenate + type: object + type: Hydrogenate PumpTransferProtocol: feedback: {} goal: amount: amount + event: event + flowrate: flowrate from_vessel: from_vessel + rate_spec: rate_spec rinsing_repeats: rinsing_repeats rinsing_solvent: rinsing_solvent rinsing_volume: rinsing_volume solid: solid + through: through time: time to_vessel: to_vessel + transfer_flowrate: transfer_flowrate viscous: viscous volume: volume - flowrate: flowrate - transfer_flowrate: transfer_flowrate - rate_spec: rate_spec - event: event - through: through goal_default: amount: '' + event: '' + flowrate: 0.0 from_vessel: '' + rate_spec: '' rinsing_repeats: 0 rinsing_solvent: '' rinsing_volume: 0.0 solid: false + through: '' time: 0.0 to_vessel: '' + transfer_flowrate: 0.0 viscous: false volume: 0.0 - flowrate: 0.0 - transfer_flowrate: 0.0 - rate_spec: '' - event: '' - through: '' handles: input: - data_key: vessel @@ -1451,7 +1738,7 @@ workstation: label: To Vessel result: {} schema: - description: ROS Action PumpTransfer 的 JSON Schema(兼容增强版) + description: ROS Action PumpTransfer 的 JSON Schema properties: feedback: description: Action 反馈 - 执行过程中从服务器发送到客户端 @@ -1461,8 +1748,34 @@ 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 @@ -1476,37 +1789,52 @@ workstation: properties: amount: type: string + event: + type: string + flowrate: + type: number from_vessel: type: string - to_vessel: + rate_spec: type: string - volume: - type: number - time: - type: number - viscous: - type: boolean + rinsing_repeats: + maximum: 2147483647 + minimum: -2147483648 + type: integer rinsing_solvent: type: string rinsing_volume: type: number - rinsing_repeats: - type: integer solid: type: boolean - flowrate: + through: + type: string + time: type: number + to_vessel: + type: string 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 + - flowrate + - transfer_flowrate + - rate_spec + - event + - through title: PumpTransfer_Goal type: object result: @@ -1526,30 +1854,189 @@ workstation: title: PumpTransfer type: object type: PumpTransfer - RunColumnProtocol: + RecrystallizeProtocol: feedback: {} goal: - column: column - from_vessel: from_vessel - to_vessel: to_vessel + ratio: ratio + solvent1: solvent1 + solvent2: solvent2 + vessel: vessel + volume: volume goal_default: - column: '' - from_vessel: '' - to_vessel: '' + ratio: '' + solvent1: '' + solvent2: '' + vessel: '' + volume: 0.0 handles: input: - data_key: vessel data_source: handle data_type: resource - handler_key: FromVessel - label: From Vessel - - data_key: vessel - data_source: executor - data_type: resource - handler_key: ToVessel - label: To Vessel - output: - - data_key: vessel + handler_key: Vessel + label: Vessel + - data_key: solvent1 + data_source: handle + data_type: resource + handler_key: solvent1 + label: Solvent 1 + - data_key: solvent2 + data_source: handle + data_type: resource + handler_key: solvent2 + label: Solvent 2 + output: + - data_key: vessel + data_source: executor + data_type: resource + handler_key: VesselOut + label: Vessel + result: {} + schema: + description: ROS Action Recrystallize 的 JSON Schema + properties: + feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 + properties: + progress: + type: number + status: + type: string + required: + - status + - progress + title: Recrystallize_Feedback + type: object + goal: + description: Action 目标 - 从客户端发送到服务器 + properties: + ratio: + type: string + solvent1: + type: string + solvent2: + type: string + vessel: + type: string + volume: + type: number + required: + - ratio + - solvent1 + - solvent2 + - vessel + - volume + title: Recrystallize_Goal + type: object + result: + description: Action 结果 - 完成后从服务器发送到客户端 + properties: + message: + type: string + return_info: + type: string + success: + type: boolean + required: + - success + - message + - return_info + title: Recrystallize_Result + type: object + required: + - goal + title: Recrystallize + type: object + type: Recrystallize + ResetHandlingProtocol: + feedback: {} + goal: + solvent: solvent + goal_default: + solvent: '' + handles: + input: + - data_key: solvent + data_source: handle + data_type: resource + handler_key: solvent + label: Solvent + output: [] + result: {} + schema: + description: ROS Action ResetHandling 的 JSON Schema + properties: + feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 + properties: + progress: + type: number + status: + type: string + required: + - status + - progress + title: ResetHandling_Feedback + type: object + goal: + description: Action 目标 - 从客户端发送到服务器 + properties: + solvent: + type: string + required: + - solvent + title: ResetHandling_Goal + type: object + result: + description: Action 结果 - 完成后从服务器发送到客户端 + properties: + message: + type: string + return_info: + type: string + success: + type: boolean + required: + - success + - message + - return_info + title: ResetHandling_Result + type: object + required: + - goal + title: ResetHandling + type: object + type: ResetHandling + RunColumnProtocol: + feedback: {} + goal: + column: column + from_vessel: from_vessel + to_vessel: to_vessel + goal_default: + column: '' + from_vessel: '' + pct1: '' + pct2: '' + ratio: '' + rf: '' + solvent1: '' + solvent2: '' + to_vessel: '' + handles: + input: + - data_key: vessel + data_source: handle + data_type: resource + handler_key: FromVessel + label: From Vessel + - data_key: vessel + data_source: executor + data_type: resource + handler_key: ToVessel + label: To Vessel + output: + - data_key: vessel data_source: handle data_type: resource handler_key: FromVesselOut @@ -1582,12 +2069,30 @@ workstation: type: string from_vessel: type: string + pct1: + type: string + pct2: + type: string + ratio: + type: string + rf: + type: string + solvent1: + type: string + solvent2: + type: string to_vessel: type: string required: - from_vessel - to_vessel - column + - rf + - pct1 + - pct2 + - solvent1 + - solvent2 + - ratio title: RunColumn_Goal type: object result: @@ -1629,6 +2134,7 @@ workstation: goal_default: from_vessel: '' product_phase: '' + product_vessel: '' purpose: '' repeats: 0 separation_vessel: '' @@ -1639,7 +2145,10 @@ workstation: stir_time: 0.0 through: '' to_vessel: '' + vessel: '' + volume: '' waste_phase_to_vessel: '' + waste_vessel: '' handles: input: - data_key: vessel @@ -1723,6 +2232,8 @@ workstation: type: string product_phase: type: string + product_vessel: + type: string purpose: type: string repeats: @@ -1745,8 +2256,14 @@ workstation: type: string to_vessel: type: string + vessel: + type: string + volume: + type: string waste_phase_to_vessel: type: string + waste_vessel: + type: string required: - purpose - product_phase @@ -1761,18 +2278,25 @@ workstation: - stir_time - stir_speed - settling_time + - vessel + - volume + - product_vessel + - waste_vessel title: Separate_Goal type: object result: description: Action 结果 - 完成后从服务器发送到客户端 properties: + message: + type: string return_info: type: string success: type: boolean required: - - return_info - success + - message + - return_info title: Separate_Result type: object required: @@ -1860,21 +2384,21 @@ workstation: StirProtocol: feedback: {} goal: - vessel: vessel - time: time event: event - time_spec: time_spec - stir_time: stir_time - stir_speed: stir_speed settling_time: settling_time + stir_speed: stir_speed + stir_time: stir_time + time: time + time_spec: time_spec + vessel: vessel goal_default: - vessel: '' - time: '5 min' event: '' + settling_time: 0.0 + stir_speed: 0.0 + stir_time: 0.0 + time: '' time_spec: '' - stir_time: 300.0 - stir_speed: 200.0 - settling_time: 60.0 + vessel: '' handles: input: - data_key: vessel @@ -1893,7 +2417,7 @@ workstation: description: ROS Action Stir 的 JSON Schema properties: feedback: - description: Action 反馈 + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: status: type: string @@ -1902,41 +2426,34 @@ workstation: title: Stir_Feedback type: object goal: - description: Action 目标 + description: Action 目标 - 从客户端发送到服务器 properties: - vessel: - type: string - description: 搅拌容器名称 - time: - type: string - default: '5 min' - description: 搅拌时间 event: type: string - default: '' - description: 事件标识 - time_spec: - type: string - default: '' - description: 时间规格 - stir_time: + settling_time: type: number - default: 300.0 - description: 搅拌时间(秒) stir_speed: type: number - default: 200.0 - description: 搅拌速度 - settling_time: + stir_time: type: number - default: 60.0 - description: 沉降时间 + time: + type: string + time_spec: + type: string + vessel: + type: string required: - vessel + - time + - event + - time_spec + - stir_time + - stir_speed + - settling_time title: Stir_Goal type: object result: - description: Action 结果 + description: Action 结果 - 完成后从服务器发送到客户端 properties: message: type: string @@ -2002,13 +2519,16 @@ workstation: result: description: Action 结果 - 完成后从服务器发送到客户端 properties: + message: + type: string return_info: type: string success: type: boolean required: - - return_info - success + - message + - return_info title: StopStir_Result type: object required: @@ -2148,25 +2668,25 @@ workstation: WashSolidProtocol: feedback: {} goal: - vessel: vessel - solvent: solvent - volume: volume filtrate_vessel: filtrate_vessel - temp: temp + repeats: repeats + solvent: solvent stir: stir stir_speed: stir_speed + temp: temp time: time - repeats: repeats + vessel: vessel + volume: volume goal_default: - vessel: '' - solvent: '' - volume: 0.0 filtrate_vessel: '' - temp: 25.0 + repeats: 0 + solvent: '' stir: false stir_speed: 0.0 + temp: 0.0 time: 0.0 - repeats: 1 + vessel: '' + volume: 0.0 handles: input: - data_key: vessel @@ -2214,45 +2734,36 @@ workstation: goal: description: Action 目标 - 从客户端发送到服务器 properties: - vessel: + filtrate_vessel: type: string - description: 装有固体的容器名称 + repeats: + maximum: 2147483647 + minimum: -2147483648 + type: integer solvent: type: string - description: 清洗溶剂名称 - volume: + stir: + type: boolean + stir_speed: 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: 重复次数 + vessel: + type: string + volume: + type: number required: - vessel - solvent - volume + - filtrate_vessel + - temp + - stir + - stir_speed + - time + - repeats title: WashSolid_Goal type: object result: @@ -2363,381 +2874,6 @@ workstation: title: initialize_device参数 type: object type: UniLabJsonCommand - AdjustPHProtocol: - feedback: {} - goal: - vessel: vessel - ph_value: ph_value - reagent: reagent - goal_default: - vessel: '' - ph_value: 7.0 - reagent: '' - handles: - input: - - data_key: vessel - data_source: handle - data_type: resource - handler_key: Vessel - label: Vessel - - data_key: reagent - data_source: handle - data_type: resource - handler_key: reagent - label: Reagent - output: - - data_key: vessel - data_source: executor - data_type: resource - handler_key: VesselOut - label: Vessel - result: {} - schema: - description: ROS Action AdjustPH 的 JSON Schema - properties: - feedback: - description: Action 反馈 - 执行过程中从服务器发送到客户端 - properties: - status: - type: string - progress: - type: number - required: - - status - - progress - title: AdjustPH_Feedback - type: object - goal: - description: Action 目标 - 从客户端发送到服务器 - properties: - vessel: - type: string - ph_value: - type: number - reagent: - type: string - required: - - vessel - - ph_value - - reagent - title: AdjustPH_Goal - type: object - result: - description: Action 结果 - 完成后从服务器发送到客户端 - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: AdjustPH_Result - type: object - required: - - goal - title: AdjustPH - type: object - type: AdjustPH - ResetHandlingProtocol: - feedback: {} - goal: - solvent: solvent - goal_default: - solvent: '' - handles: - input: - - data_key: solvent - data_source: handle - data_type: resource - handler_key: solvent - label: Solvent - output: [] - result: {} - schema: - description: ROS Action ResetHandling 的 JSON Schema - properties: - feedback: - description: Action 反馈 - 执行过程中从服务器发送到客户端 - properties: - status: - type: string - progress: - type: number - required: - - status - - progress - title: ResetHandling_Feedback - type: object - goal: - description: Action 目标 - 从客户端发送到服务器 - properties: - solvent: - type: string - required: - - solvent - title: ResetHandling_Goal - type: object - result: - description: Action 结果 - 完成后从服务器发送到客户端 - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: ResetHandling_Result - type: object - required: - - goal - title: ResetHandling - type: object - type: ResetHandling - DryProtocol: - feedback: {} - goal: - compound: compound - vessel: vessel - goal_default: - compound: '' - vessel: '' - handles: - input: - - data_key: vessel - data_source: handle - data_type: resource - handler_key: Vessel - label: Vessel - output: - - data_key: vessel - data_source: executor - data_type: resource - handler_key: VesselOut - label: Vessel - result: {} - schema: - description: ROS Action Dry 的 JSON Schema - properties: - feedback: - description: Action 反馈 - 执行过程中从服务器发送到客户端 - properties: - status: - type: string - progress: - type: number - required: - - status - - progress - title: Dry_Feedback - type: object - goal: - description: Action 目标 - 从客户端发送到服务器 - properties: - compound: - type: string - vessel: - type: string - required: - - compound - - vessel - title: Dry_Goal - type: object - result: - description: Action 结果 - 完成后从服务器发送到客户端 - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: Dry_Result - type: object - required: - - goal - title: Dry - type: object - type: Dry - HydrogenateProtocol: - feedback: {} - goal: - temp: temp - time: time - vessel: vessel - goal_default: - temp: '' - time: '' - vessel: '' - handles: - input: - - data_key: vessel - data_source: handle - data_type: resource - handler_key: Vessel - label: Vessel - output: - - data_key: vessel - data_source: executor - data_type: resource - handler_key: VesselOut - label: Vessel - result: {} - schema: - description: ROS Action Hydrogenate 的 JSON Schema - properties: - feedback: - description: Action 反馈 - 执行过程中从服务器发送到客户端 - properties: - status: - type: string - progress: - type: number - required: - - status - - progress - title: Hydrogenate_Feedback - type: object - goal: - description: Action 目标 - 从客户端发送到服务器 - properties: - temp: - type: string - time: - type: string - vessel: - type: string - required: - - temp - - time - - vessel - title: Hydrogenate_Goal - type: object - result: - description: Action 结果 - 完成后从服务器发送到客户端 - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: Hydrogenate_Result - type: object - required: - - goal - title: Hydrogenate - type: object - type: Hydrogenate - RecrystallizeProtocol: - feedback: {} - goal: - ratio: ratio - solvent1: solvent1 - solvent2: solvent2 - vessel: vessel - volume: volume - goal_default: - ratio: 1.0 - solvent1: '' - solvent2: '' - vessel: '' - volume: 0.0 - handles: - input: - - data_key: vessel - data_source: handle - data_type: resource - handler_key: Vessel - label: Vessel - - data_key: solvent1 - data_source: handle - data_type: resource - handler_key: solvent1 - label: Solvent 1 - - data_key: solvent2 - data_source: handle - data_type: resource - handler_key: solvent2 - label: Solvent 2 - output: - - data_key: vessel - data_source: executor - data_type: resource - handler_key: VesselOut - label: Vessel - result: {} - schema: - description: ROS Action Recrystallize 的 JSON Schema - properties: - feedback: - description: Action 反馈 - 执行过程中从服务器发送到客户端 - properties: - progress: - type: number - status: - type: string - required: - - status - - progress - title: Recrystallize_Feedback - type: object - goal: - description: Action 目标 - 从客户端发送到服务器 - properties: - ratio: - type: number - solvent1: - type: string - solvent2: - type: string - vessel: - type: string - volume: - type: number - required: - - ratio - - solvent1 - - solvent2 - - vessel - - volume - title: Recrystallize_Goal - type: object - result: - description: Action 结果 - 完成后从服务器发送到客户端 - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: Recrystallize_Result - type: object - required: - - goal - title: Recrystallize - type: object - type: Recrystallize module: unilabos.ros.nodes.presets.protocol_node:ROS2ProtocolNode status_types: {} type: ros2 diff --git a/unilabos_msgs/CMakeLists.txt b/unilabos_msgs/CMakeLists.txt index 7e8a1469b..b2c1bc71c 100644 --- a/unilabos_msgs/CMakeLists.txt +++ b/unilabos_msgs/CMakeLists.txt @@ -41,6 +41,7 @@ set(action_files "action/WashSolid.action" "action/Filter.action" "action/Add.action" + "action/AddSolid.action" "action/Centrifuge.action" "action/Crystallize.action" "action/Purge.action" diff --git a/unilabos_msgs/action/RunColumn.action b/unilabos_msgs/action/RunColumn.action index 889b4a5d8..1f8e9ba16 100644 --- a/unilabos_msgs/action/RunColumn.action +++ b/unilabos_msgs/action/RunColumn.action @@ -2,7 +2,7 @@ string from_vessel # 源容器的名称,即样品起始所在的容器(必需) string to_vessel # 目标容器的名称,分离后的样品要到达的容器(必需) string column # 所使用的柱子的名称(必需) -string Rf # Rf值(可选) +string rf # Rf值(可选) string pct1 # 第一种溶剂百分比(如 "40 %",可选) string pct2 # 第二种溶剂百分比(如 "50 %",可选) string solvent1 # 第一种溶剂名称(可选) From 7b93332bf5c6748a1b828ea1d3c9068793435238 Mon Sep 17 00:00:00 2001 From: KCFeng425 <2100011801@stu.pku.edu.cn> Date: Thu, 10 Jul 2025 16:48:09 +0800 Subject: [PATCH 6/9] =?UTF-8?q?protocol=E5=AE=8C=E6=95=B4=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E7=89=88=E6=9C=AC&=20bump=20version=20to=200.9.10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- README_zh.md | 2 +- recipes/ros-humble-unilabos-msgs/recipe.yaml | 2 +- recipes/unilabos/recipe.yaml | 2 +- setup.py | 2 +- .../comprehensive_protocol/checklist.md | 53 +- .../comprehensive_station.json | 4 +- unilabos/compile/add_protocol.py | 636 ++++++++---- unilabos/compile/dissolve_protocol.py | 965 ++++++++++++------ .../compile/evacuateandrefill_protocol.py | 272 ++--- unilabos/compile/evaporate_protocol.py | 321 +++--- unilabos/compile/filter_protocol.py | 258 +++-- unilabos/compile/pump_protocol.py | 470 ++++++--- unilabos/compile/run_column_protocol.py | 794 ++++++++++---- unilabos/compile/separate_protocol.py | 658 ++++++++---- unilabos/compile/stir_protocol.py | 304 ++++-- unilabos/compile/wash_solid_protocol.py | 589 ++++++++--- unilabos/devices/virtual/virtual_filter.py | 10 + unilabos/devices/virtual/virtual_rotavap.py | 56 +- .../devices/virtual/virtual_solenoid_valve.py | 54 +- unilabos/registry/devices/virtual_device.yaml | 52 +- unilabos_msgs/action/Add.action | 10 +- unilabos_msgs/action/Dissolve.action | 11 +- unilabos_msgs/action/Separate.action | 28 +- unilabos_msgs/action/WashSolid.action | 8 +- 25 files changed, 3740 insertions(+), 1823 deletions(-) diff --git a/README.md b/README.md index 287efb50d..5d14705a8 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n environment_name # Currently, you need to install the `unilabos_msgs` package # You can download the system-specific package from the Release page -conda install ros-humble-unilabos-msgs-0.9.9-xxxxx.tar.bz2 +conda install ros-humble-unilabos-msgs-10-xxxxx.tar.bz2 # Install PyLabRobot and other prerequisites git clone https://github.com/PyLabRobot/pylabrobot plr_repo diff --git a/README_zh.md b/README_zh.md index 8bba3d77b..5bd588bbd 100644 --- a/README_zh.md +++ b/README_zh.md @@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n 环境名 # 现阶段,需要安装 `unilabos_msgs` 包 # 可以前往 Release 页面下载系统对应的包进行安装 -conda install ros-humble-unilabos-msgs-0.9.9-xxxxx.tar.bz2 +conda install ros-humble-unilabos-msgs-0.9.10-xxxxx.tar.bz2 # 安装PyLabRobot等前置 git clone https://github.com/PyLabRobot/pylabrobot plr_repo diff --git a/recipes/ros-humble-unilabos-msgs/recipe.yaml b/recipes/ros-humble-unilabos-msgs/recipe.yaml index 4169c73d2..0445115d0 100644 --- a/recipes/ros-humble-unilabos-msgs/recipe.yaml +++ b/recipes/ros-humble-unilabos-msgs/recipe.yaml @@ -1,6 +1,6 @@ package: name: ros-humble-unilabos-msgs - version: 0.9.9 + version: 0.9.10 source: path: ../../unilabos_msgs folder: ros-humble-unilabos-msgs/src/work diff --git a/recipes/unilabos/recipe.yaml b/recipes/unilabos/recipe.yaml index f9262e2b7..a5768558b 100644 --- a/recipes/unilabos/recipe.yaml +++ b/recipes/unilabos/recipe.yaml @@ -1,6 +1,6 @@ package: name: unilabos - version: "0.9.9" + version: "0.9.10" source: path: ../.. diff --git a/setup.py b/setup.py index 2d9aabdb9..80163eab1 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name=package_name, - version='0.9.9', + version='0.9.10', packages=find_packages(), include_package_data=True, install_requires=['setuptools'], diff --git a/test/experiments/comprehensive_protocol/checklist.md b/test/experiments/comprehensive_protocol/checklist.md index 4762cd22e..5dee23a15 100644 --- a/test/experiments/comprehensive_protocol/checklist.md +++ b/test/experiments/comprehensive_protocol/checklist.md @@ -43,6 +43,15 @@ Hydrogenate 4. 参数对齐 + + + + + + + + + class PumpTransferProtocol(BaseModel): from_vessel: str to_vessel: str @@ -53,7 +62,7 @@ class PumpTransferProtocol(BaseModel): rinsing_solvent: str = "air" rinsing_volume: float = 5000 rinsing_repeats: int = 2 - solid: bool = False 添加了缺失的参数,但是体积为0以及打印日志的问题修不好 + solid: bool = False 测完了三个都能跑✅ flowrate: float = 500 transfer_flowrate: float = 2500 @@ -67,24 +76,24 @@ class SeparateProtocol(BaseModel): solvent: str solvent_volume: float through: str - repeats: int - stir_time: float + repeats: int + stir_time: float stir_speed: float - settling_time: float 写了action + settling_time: float 测完了能跑✅ 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 @@ -102,7 +111,7 @@ class AddProtocol(BaseModel): vessel="main_reactor" volume="2.67 mL"/> viscous: bool - purpose: str 写了action + purpose: str 测完了能跑✅ class CentrifugeProtocol(BaseModel): vessel: str @@ -115,7 +124,7 @@ class FilterProtocol(BaseModel): filtrate_vessel: str stir: bool stir_speed: float - temp: float 处理了 + temp: float 测完了能跑✅ continue_heatchill: bool volume: float @@ -127,7 +136,7 @@ class HeatChillProtocol(BaseModel): - stir: bool 处理了 + stir: bool 测完了能跑✅ stir_speed: float purpose: str @@ -144,7 +153,7 @@ class StirProtocol(BaseModel): stir_speed: float - settling_time: float 处理完了 + settling_time: float 测完了能跑✅ class StartStirProtocol(BaseModel): vessel: str @@ -176,11 +185,11 @@ class CleanVesselProtocol(BaseModel): class DissolveProtocol(BaseModel): vessel: str solvent: str - volume: float - amount: str = "" + volume: float + amount: str = "" temp: float = 25.0 - time: float = 0.0 - stir_speed: float = 0.0 写了action + time: float = 0.0 + stir_speed: float = 0.0 测完了能跑✅ class FilterThroughProtocol(BaseModel): from_vessel: str @@ -194,16 +203,19 @@ class FilterThroughProtocol(BaseModel): class RunColumnProtocol(BaseModel): from_vessel: str to_vessel: str - column: str 写了action + column: str 测完了能跑✅ class WashSolidProtocol(BaseModel): vessel: str solvent: str volume: float - filtrate_vessel: str = "" + filtrate_vessel: str = "" temp: float = 25.0 stir: bool = False - stir_speed: float = 0.0 处理完了 + + + + stir_speed: float = 0.0 测完了能跑✅ time: float = 0.0 repeats: int = 1 @@ -230,4 +242,9 @@ class RecrystallizeProtocol(BaseModel): class HydrogenateProtocol(BaseModel): temp: str = Field(..., description="反应温度(如 '45 °C')") time: str = Field(..., description="反应时间(如 '2 h')") <新写的,没问题> - vessel: str = Field(..., description="反应容器") \ No newline at end of file + vessel: str = Field(..., description="反应容器") + + 还差 + + + \ No newline at end of file diff --git a/test/experiments/comprehensive_protocol/comprehensive_station.json b/test/experiments/comprehensive_protocol/comprehensive_station.json index aaa4dfc8e..43e4cc67b 100644 --- a/test/experiments/comprehensive_protocol/comprehensive_station.json +++ b/test/experiments/comprehensive_protocol/comprehensive_station.json @@ -898,7 +898,7 @@ "type": "fluid", "port": { "multiway_valve_2": "3", - "solenoid_valve_2": "in" + "solenoid_valve_2": "out" } }, { @@ -908,7 +908,7 @@ "type": "fluid", "port": { "gas_source_1": "gassource", - "solenoid_valve_2": "out" + "solenoid_valve_2": "in" } }, { diff --git a/unilabos/compile/add_protocol.py b/unilabos/compile/add_protocol.py index 6c51cc9fd..b5b017eb0 100644 --- a/unilabos/compile/add_protocol.py +++ b/unilabos/compile/add_protocol.py @@ -1,58 +1,238 @@ import networkx as nx -from typing import List, Dict, Any +import re +import logging +from typing import List, Dict, Any, Union from .pump_protocol import generate_pump_protocol_with_rinsing +logger = logging.getLogger(__name__) + +def debug_print(message): + """调试输出""" + print(f"[ADD] {message}", flush=True) + logger.info(f"[ADD] {message}") + +def parse_volume_input(volume_input: Union[str, float]) -> float: + """ + 解析体积输入,支持带单位的字符串 + + Args: + volume_input: 体积输入(如 "2.7 mL", "2.67 mL", "?", 10.0) + + Returns: + float: 体积(毫升) + """ + if isinstance(volume_input, (int, float)): + return float(volume_input) + + if not volume_input or not str(volume_input).strip(): + return 0.0 + + volume_str = str(volume_input).lower().strip() + debug_print(f"解析体积输入: '{volume_str}'") + + # 处理未知体积 + if volume_str in ['?', 'unknown', 'tbd', 'to be determined']: + default_volume = 10.0 # 默认10mL + debug_print(f"检测到未知体积,使用默认值: {default_volume}mL") + return default_volume + + # 移除空格并提取数字和单位 + volume_clean = re.sub(r'\s+', '', volume_str) + + # 匹配数字和单位的正则表达式 + match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean) + + if not match: + debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值10mL") + return 10.0 + + value = float(match.group(1)) + unit = match.group(2) or 'ml' # 默认单位为毫升 + + # 转换为毫升 + if unit in ['l', 'liter']: + volume = value * 1000.0 # L -> mL + elif unit in ['μl', 'ul', 'microliter']: + volume = value / 1000.0 # μL -> mL + else: # ml, milliliter 或默认 + volume = value # 已经是mL + + debug_print(f"体积转换: {value}{unit} → {volume}mL") + return volume + +def parse_mass_input(mass_input: Union[str, float]) -> float: + """ + 解析质量输入,支持带单位的字符串 + + Args: + mass_input: 质量输入(如 "19.3 g", "4.5 g", 2.5) + + Returns: + float: 质量(克) + """ + if isinstance(mass_input, (int, float)): + return float(mass_input) + + if not mass_input or not str(mass_input).strip(): + return 0.0 + + mass_str = str(mass_input).lower().strip() + debug_print(f"解析质量输入: '{mass_str}'") + + # 移除空格并提取数字和单位 + mass_clean = re.sub(r'\s+', '', mass_str) + + # 匹配数字和单位的正则表达式 + match = re.match(r'([0-9]*\.?[0-9]+)\s*(g|mg|kg|gram|milligram|kilogram)?', mass_clean) + + if not match: + debug_print(f"⚠️ 无法解析质量: '{mass_str}',返回0.0g") + return 0.0 + + value = float(match.group(1)) + unit = match.group(2) or 'g' # 默认单位为克 + + # 转换为克 + if unit in ['mg', 'milligram']: + mass = value / 1000.0 # mg -> g + elif unit in ['kg', 'kilogram']: + mass = value * 1000.0 # kg -> g + else: # g, gram 或默认 + mass = value # 已经是g + + debug_print(f"质量转换: {value}{unit} → {mass}g") + return mass + +def parse_time_input(time_input: Union[str, float]) -> float: + """ + 解析时间输入,支持带单位的字符串 + + Args: + time_input: 时间输入(如 "1 h", "20 min", "30 s", 60.0) + + Returns: + float: 时间(秒) + """ + if isinstance(time_input, (int, float)): + return float(time_input) + + if not time_input or not str(time_input).strip(): + return 0.0 + + time_str = str(time_input).lower().strip() + debug_print(f"解析时间输入: '{time_str}'") + + # 处理未知时间 + if time_str in ['?', 'unknown', 'tbd']: + default_time = 60.0 # 默认1分钟 + debug_print(f"检测到未知时间,使用默认值: {default_time}s") + return default_time + + # 移除空格并提取数字和单位 + time_clean = re.sub(r'\s+', '', time_str) + + # 匹配数字和单位的正则表达式 + match = re.match(r'([0-9]*\.?[0-9]+)\s*(s|sec|second|min|minute|h|hr|hour|d|day)?', time_clean) + + if not match: + debug_print(f"⚠️ 无法解析时间: '{time_str}',返回0s") + return 0.0 + + value = float(match.group(1)) + unit = match.group(2) or 's' # 默认单位为秒 + + # 转换为秒 + if unit in ['min', 'minute']: + time_sec = value * 60.0 # min -> s + elif unit in ['h', 'hr', 'hour']: + time_sec = value * 3600.0 # h -> s + elif unit in ['d', 'day']: + time_sec = value * 86400.0 # d -> s + else: # s, sec, second 或默认 + time_sec = value # 已经是s + + debug_print(f"时间转换: {value}{unit} → {time_sec}s") + return time_sec def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str: """增强版试剂容器查找,支持固体和液体""" - print(f"ADD_PROTOCOL: 查找试剂 '{reagent}' 的容器...") + debug_print(f"查找试剂 '{reagent}' 的容器...") - # 1. 直接名称匹配 + # 🔧 方法1:直接搜索 data.reagent_name 和 config.reagent + for node in G.nodes(): + node_data = G.nodes[node].get('data', {}) + node_type = G.nodes[node].get('type', '') + config_data = G.nodes[node].get('config', {}) + + # 只搜索容器类型的节点 + if node_type == 'container': + reagent_name = node_data.get('reagent_name', '').lower() + config_reagent = config_data.get('reagent', '').lower() + + # 精确匹配 + if reagent_name == reagent.lower() or config_reagent == reagent.lower(): + debug_print(f"✅ 通过reagent字段找到容器: {node}") + return node + + # 模糊匹配 + if (reagent.lower() in reagent_name and reagent_name) or \ + (reagent.lower() in config_reagent and config_reagent): + debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node}") + return node + + # 🔧 方法2:常见的容器命名规则 + reagent_clean = reagent.lower().replace(' ', '_').replace('-', '_') possible_names = [ - reagent, - f"flask_{reagent}", - f"bottle_{reagent}", - f"vessel_{reagent}", - f"{reagent}_flask", - f"{reagent}_bottle", - f"reagent_{reagent}", - f"reagent_bottle_{reagent}", - f"solid_reagent_bottle_{reagent}", # 🔧 添加固体试剂瓶匹配 + reagent_clean, + f"flask_{reagent_clean}", + f"bottle_{reagent_clean}", + f"vessel_{reagent_clean}", + f"{reagent_clean}_flask", + f"{reagent_clean}_bottle", + f"reagent_{reagent_clean}", + f"reagent_bottle_{reagent_clean}", + f"solid_reagent_bottle_{reagent_clean}", + f"reagent_bottle_1", # 通用试剂瓶 + f"reagent_bottle_2", + f"reagent_bottle_3" ] for name in possible_names: if name in G.nodes(): - print(f"ADD_PROTOCOL: 找到容器: {name}") - return name + node_type = G.nodes[name].get('type', '') + if node_type == 'container': + debug_print(f"✅ 通过命名规则找到容器: {name}") + return name - # 2. 模糊匹配 - 检查容器数据 + # 🔧 方法3:节点名称模糊匹配 for node_id in G.nodes(): node_data = G.nodes[node_id] if node_data.get('type') == 'container': - # 检查配置中的试剂名称 - config_reagent = node_data.get('config', {}).get('reagent', '') - data_reagent = node_data.get('data', {}).get('reagent_name', '') - - # 名称匹配 - if (config_reagent.lower() == reagent.lower() or - data_reagent.lower() == reagent.lower() or - reagent.lower() in node_id.lower()): - print(f"ADD_PROTOCOL: 模糊匹配到容器: {node_id}") + # 检查节点名称是否包含试剂名称 + if reagent_clean in node_id.lower(): + debug_print(f"✅ 通过节点名称模糊匹配到容器: {node_id}") return node_id - # 液体类型匹配(保持原有逻辑) + # 检查液体类型匹配 vessel_data = node_data.get('data', {}) liquids = vessel_data.get('liquid', []) for liquid in liquids: if isinstance(liquid, dict): liquid_type = liquid.get('liquid_type') or liquid.get('name', '') if liquid_type.lower() == reagent.lower(): - print(f"ADD_PROTOCOL: 液体类型匹配到容器: {node_id}") + debug_print(f"✅ 通过液体类型匹配到容器: {node_id}") return node_id + # 🔧 方法4:使用第一个试剂瓶作为备选 + for node_id in G.nodes(): + node_data = G.nodes[node_id] + if (node_data.get('type') == 'container' and + ('reagent' in node_id.lower() or 'bottle' in node_id.lower())): + debug_print(f"⚠️ 未找到专用容器,使用备选试剂瓶: {node_id}") + return node_id + raise ValueError(f"找不到试剂 '{reagent}' 对应的容器") - def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str: """查找连接到指定容器的搅拌器""" stirrer_nodes = [] @@ -64,40 +244,41 @@ def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str: # 查找连接到容器的搅拌器 for stirrer in stirrer_nodes: if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): - print(f"ADD_PROTOCOL: 找到连接的搅拌器: {stirrer}") + debug_print(f"找到连接的搅拌器: {stirrer}") return stirrer # 返回第一个搅拌器 if stirrer_nodes: - print(f"ADD_PROTOCOL: 使用第一个搅拌器: {stirrer_nodes[0]}") + debug_print(f"使用第一个搅拌器: {stirrer_nodes[0]}") return stirrer_nodes[0] - return None - + return "" def find_solid_dispenser(G: nx.DiGraph) -> str: """查找固体加样器""" for node in G.nodes(): node_class = G.nodes[node].get('class', '').lower() - if 'solid_dispenser' in node_class: - print(f"ADD_PROTOCOL: 找到固体加样器: {node}") + if 'solid_dispenser' in node_class or 'dispenser' in node_class: + debug_print(f"找到固体加样器: {node}") return node - return None - + + debug_print("⚠️ 未找到固体加样器") + return "" def generate_add_protocol( G: nx.DiGraph, vessel: str, reagent: str, - volume: float = 0.0, - mass: float = 0.0, + # 🔧 修复:所有参数都用 Union 类型,支持字符串和数值 + volume: Union[str, float] = 0.0, + mass: Union[str, float] = 0.0, amount: str = "", - time: float = 0.0, + time: Union[str, float] = 0.0, stir: bool = False, stir_speed: float = 300.0, viscous: bool = False, purpose: str = "添加试剂", - # 新增XDL参数 + # XDL扩展参数 mol: str = "", event: str = "", rate_spec: str = "", @@ -106,133 +287,191 @@ def generate_add_protocol( **kwargs ) -> List[Dict[str, Any]]: """ - 生成添加试剂协议 + 生成添加试剂协议 - 修复版 - 智能判断: - - 有 mass 或 mol → 固体加样器 - - 有 volume → 液体转移 - - 都没有 → 默认液体 1mL + 支持所有XDL参数和单位: + - volume: "2.7 mL", "2.67 mL", "?" 或数值 + - mass: "19.3 g", "4.5 g" 或数值 + - time: "1 h", "20 min" 或数值(秒) + - mol: "0.28 mol", "16.2 mmol", "25.2 mmol" + - rate_spec: "portionwise", "dropwise" + - event: "A", "B" + - equiv: "1.1" + - ratio: "?", "1:1" """ - print(f"ADD_PROTOCOL: 添加 {reagent} 到 {vessel}") - print(f" - 体积: {volume} mL, 质量: {mass} g, 摩尔: {mol}") - print(f" - 时间: {time} s, 事件: {event}, 速率: {rate_spec}") + debug_print("=" * 60) + debug_print("开始生成添加试剂协议") + debug_print(f"原始参数:") + debug_print(f" - vessel: '{vessel}'") + debug_print(f" - reagent: '{reagent}'") + debug_print(f" - volume: {volume} (类型: {type(volume)})") + debug_print(f" - mass: {mass} (类型: {type(mass)})") + debug_print(f" - time: {time} (类型: {type(time)})") + debug_print(f" - mol: '{mol}'") + debug_print(f" - event: '{event}'") + debug_print(f" - rate_spec: '{rate_spec}'") + debug_print("=" * 60) + + action_sequence = [] + + # === 参数验证 === + debug_print("步骤1: 参数验证...") + + if not vessel: + raise ValueError("vessel 参数不能为空") + if not reagent: + raise ValueError("reagent 参数不能为空") - # 1. 验证容器 if vessel not in G.nodes(): - raise ValueError(f"容器 '{vessel}' 不存在") + raise ValueError(f"容器 '{vessel}' 不存在于系统中") - # 2. 判断固体 vs 液体 - is_solid = (mass > 0 or mol.strip() != "") + debug_print("✅ 基本参数验证通过") - action_sequence = [] + # === 🔧 关键修复:参数解析 === + debug_print("步骤2: 参数解析...") - if is_solid: - # === 固体加样路径 === - print(f"ADD_PROTOCOL: 使用固体加样器") - - solid_dispenser = find_solid_dispenser(G) - if not solid_dispenser: - raise ValueError("未找到固体加样器") - - # 启动搅拌(如果需要) - if stir: - stirrer_id = find_connected_stirrer(G, vessel) - if stirrer_id: - action_sequence.append({ - "device_id": stirrer_id, - "action_name": "start_stir", - "action_kwargs": { - "vessel": vessel, - "stir_speed": stir_speed, - "purpose": f"准备添加固体 {reagent}" - } - }) - # 等待搅拌稳定 - action_sequence.append({ - "action_name": "wait", - "action_kwargs": {"time": 3} - }) - - # 固体加样 - action_sequence.append({ - "device_id": solid_dispenser, - "action_name": "add_solid", - "action_kwargs": { - "vessel": vessel, - "reagent": reagent, - "mass": str(mass) if mass > 0 else "", - "mol": mol, - "purpose": purpose, - "event": event - } - }) - - else: - # === 液体转移路径 === - print(f"ADD_PROTOCOL: 使用液体转移") - - # 默认体积 - if volume <= 0: - volume = 1.0 - print(f"ADD_PROTOCOL: 使用默认体积 1mL") - - # 查找试剂容器 - try: - reagent_vessel = find_reagent_vessel(G, reagent) - except ValueError as e: - # 🔧 更友好的错误提示 - available_reagents = [] - for node_id in G.nodes(): - node_data = G.nodes[node_id] - if node_data.get('type') == 'container': - config_reagent = node_data.get('config', {}).get('reagent', '') - data_reagent = node_data.get('data', {}).get('reagent_name', '') - if config_reagent: - available_reagents.append(f"{node_id}({config_reagent})") - elif data_reagent: - available_reagents.append(f"{node_id}({data_reagent})") + # 解析各种参数为数值 + final_volume = parse_volume_input(volume) + final_mass = parse_mass_input(mass) + final_time = parse_time_input(time) + + debug_print(f"解析结果:") + debug_print(f" - 体积: {final_volume}mL") + debug_print(f" - 质量: {final_mass}g") + debug_print(f" - 时间: {final_time}s") + debug_print(f" - 摩尔: '{mol}'") + debug_print(f" - 事件: '{event}'") + debug_print(f" - 速率: '{rate_spec}'") + + # === 判断添加类型 === + debug_print("步骤3: 判断添加类型...") + + # 🔧 修复:现在使用解析后的数值进行比较 + is_solid = (final_mass > 0 or (mol and mol.strip() != "")) + is_liquid = (final_volume > 0) + + if not is_solid and not is_liquid: + # 默认为液体,10mL + is_liquid = True + final_volume = 10.0 + debug_print("⚠️ 未指定体积或质量,默认为10mL液体") + + debug_print(f"添加类型: {'固体' if is_solid else '液体'}") + + # === 执行添加流程 === + debug_print("步骤4: 执行添加流程...") + + try: + if is_solid: + # === 固体添加路径 === + debug_print(f"使用固体添加路径") - error_msg = f"找不到试剂 '{reagent}'。可用试剂: {', '.join(available_reagents)}" - print(f"ADD_PROTOCOL: {error_msg}") - raise ValueError(error_msg) - - # 启动搅拌 - if stir: - stirrer_id = find_connected_stirrer(G, vessel) - if stirrer_id: + solid_dispenser = find_solid_dispenser(G) + if solid_dispenser: + # 启动搅拌 + if stir: + stirrer_id = find_connected_stirrer(G, vessel) + if stirrer_id: + action_sequence.append({ + "device_id": stirrer_id, + "action_name": "start_stir", + "action_kwargs": { + "vessel": vessel, + "stir_speed": stir_speed, + "purpose": f"准备添加固体 {reagent}" + } + }) + # 等待搅拌稳定 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 3} + }) + + # 固体加样 + add_kwargs = { + "vessel": vessel, + "reagent": reagent, + "purpose": purpose, + "event": event, + "rate_spec": rate_spec + } + + if final_mass > 0: + add_kwargs["mass"] = str(final_mass) + if mol and mol.strip(): + add_kwargs["mol"] = mol + if equiv and equiv.strip(): + add_kwargs["equiv"] = equiv + action_sequence.append({ - "device_id": stirrer_id, - "action_name": "start_stir", - "action_kwargs": { - "vessel": vessel, - "stir_speed": stir_speed, - "purpose": f"准备添加液体 {reagent}" - } - }) - # 等待搅拌稳定 - action_sequence.append({ - "action_name": "wait", - "action_kwargs": {"time": 5} + "device_id": solid_dispenser, + "action_name": "add_solid", + "action_kwargs": add_kwargs }) + + # 添加后等待 + if final_time > 0: + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": final_time} + }) + + debug_print(f"✅ 固体添加完成") + else: + debug_print("⚠️ 未找到固体加样器,跳过固体添加") - # 计算流速 - if time > 0: - flowrate = volume / time - transfer_flowrate = flowrate else: - flowrate = 1.0 if viscous else 2.5 - transfer_flowrate = 0.3 if viscous else 0.5 - - # 🔧 调用 pump_protocol 时使用正确的参数 - try: + # === 液体添加路径 === + debug_print(f"使用液体添加路径") + + # 查找试剂容器 + reagent_vessel = find_reagent_vessel(G, reagent) + + # 启动搅拌 + if stir: + stirrer_id = find_connected_stirrer(G, vessel) + if stirrer_id: + action_sequence.append({ + "device_id": stirrer_id, + "action_name": "start_stir", + "action_kwargs": { + "vessel": vessel, + "stir_speed": stir_speed, + "purpose": f"准备添加液体 {reagent}" + } + }) + # 等待搅拌稳定 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 5} + }) + + # 计算流速 + if final_time > 0: + flowrate = final_volume / final_time * 60 # mL/min + transfer_flowrate = flowrate + else: + if rate_spec == "dropwise": + flowrate = 0.5 # 滴加,很慢 + transfer_flowrate = 0.2 + elif viscous: + flowrate = 1.0 # 粘性液体 + transfer_flowrate = 0.3 + else: + flowrate = 2.5 # 正常流速 + transfer_flowrate = 0.5 + + debug_print(f"流速设置: {flowrate} mL/min") + + # 调用pump protocol pump_actions = generate_pump_protocol_with_rinsing( G=G, from_vessel=reagent_vessel, to_vessel=vessel, - volume=volume, + volume=final_volume, amount=amount, - duration=time, # 🔧 使用 duration 而不是 time + time=final_time, viscous=viscous, rinsing_solvent="", rinsing_volume=0.0, @@ -243,33 +482,44 @@ def generate_add_protocol( rate_spec=rate_spec, event=event, through="", - equiv=equiv, - ratio=ratio, **kwargs ) action_sequence.extend(pump_actions) - except Exception as e: - raise ValueError(f"液体转移失败: {str(e)}") + debug_print(f"✅ 液体转移完成,添加了 {len(pump_actions)} 个动作") + + except Exception as e: + debug_print(f"⚠️ 试剂添加失败: {str(e)}") + # 添加错误日志 + action_sequence.append({ + "device_id": "system", + "action_name": "log_message", + "action_kwargs": { + "message": f"试剂 '{reagent}' 添加失败: {str(e)}" + } + }) + + # === 最终结果 === + debug_print("=" * 60) + debug_print(f"✅ 添加试剂协议生成完成") + debug_print(f"📊 总动作数: {len(action_sequence)}") + debug_print(f"📋 处理总结:") + debug_print(f" - 试剂: {reagent}") + debug_print(f" - 添加类型: {'固体' if is_solid else '液体'}") + debug_print(f" - 目标容器: {vessel}") + if is_liquid: + debug_print(f" - 体积: {final_volume}mL") + if is_solid: + debug_print(f" - 质量: {final_mass}g") + debug_print(f" - 摩尔: {mol}") + debug_print("=" * 60) - print(f"ADD_PROTOCOL: 生成 {len(action_sequence)} 个动作") return action_sequence +# === 便捷函数 === -# 处理 wait 动作 -def process_wait_action(action_kwargs: Dict[str, Any]) -> Dict[str, Any]: - """处理等待动作""" - wait_time = action_kwargs.get('time', 1.0) - return { - "action_name": "wait", - "action_kwargs": {"time": wait_time}, - "description": f"等待 {wait_time} 秒" - } - - -# 便捷函数 -def add_liquid(G: nx.DiGraph, vessel: str, reagent: str, volume: float, - time: float = 0.0, rate_spec: str = "") -> List[Dict[str, Any]]: - """添加液体试剂""" +def add_liquid_volume(G: nx.DiGraph, vessel: str, reagent: str, volume: Union[str, float], + time: Union[str, float] = 0.0, rate_spec: str = "") -> List[Dict[str, Any]]: + """添加指定体积的液体试剂""" return generate_add_protocol( G, vessel, reagent, volume=volume, @@ -277,19 +527,17 @@ def add_liquid(G: nx.DiGraph, vessel: str, reagent: str, volume: float, rate_spec=rate_spec ) - -def add_solid(G: nx.DiGraph, vessel: str, reagent: str, mass: float, - event: str = "") -> List[Dict[str, Any]]: - """添加固体试剂""" +def add_solid_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, float], + event: str = "") -> List[Dict[str, Any]]: + """添加指定质量的固体试剂""" return generate_add_protocol( G, vessel, reagent, mass=mass, event=event ) - -def add_solid_mol(G: nx.DiGraph, vessel: str, reagent: str, mol: str, - event: str = "") -> List[Dict[str, Any]]: +def add_solid_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str, + event: str = "") -> List[Dict[str, Any]]: """按摩尔数添加固体试剂""" return generate_add_protocol( G, vessel, reagent, @@ -297,9 +545,8 @@ def add_solid_mol(G: nx.DiGraph, vessel: str, reagent: str, mol: str, event=event ) - -def add_dropwise(G: nx.DiGraph, vessel: str, reagent: str, volume: float, - time: float = 0.0, event: str = "") -> List[Dict[str, Any]]: +def add_dropwise_liquid(G: nx.DiGraph, vessel: str, reagent: str, volume: Union[str, float], + time: Union[str, float] = "20 min", event: str = "") -> List[Dict[str, Any]]: """滴加液体试剂""" return generate_add_protocol( G, vessel, reagent, @@ -309,9 +556,8 @@ def add_dropwise(G: nx.DiGraph, vessel: str, reagent: str, volume: float, event=event ) - -def add_portionwise(G: nx.DiGraph, vessel: str, reagent: str, mass: float, - time: float = 0.0, event: str = "") -> List[Dict[str, Any]]: +def add_portionwise_solid(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, float], + time: Union[str, float] = "1 h", event: str = "") -> List[Dict[str, Any]]: """分批添加固体试剂""" return generate_add_protocol( G, vessel, reagent, @@ -321,16 +567,30 @@ def add_portionwise(G: nx.DiGraph, vessel: str, reagent: str, mass: float, event=event ) - # 测试函数 def test_add_protocol(): - """测试添加协议""" - print("=== ADD PROTOCOL 修复版测试 ===") - print("✅ 已修复设备查找逻辑") - print("✅ 已添加固体试剂瓶支持") - print("✅ 已修复错误处理") + """测试添加协议的各种参数解析""" + print("=== ADD PROTOCOL 增强版测试 ===") + + # 测试体积解析 + volumes = ["2.7 mL", "2.67 mL", "?", 10.0, "1 L", "500 μL"] + for vol in volumes: + result = parse_volume_input(vol) + print(f"体积解析: {vol} → {result}mL") + + # 测试质量解析 + masses = ["19.3 g", "4.5 g", 2.5, "500 mg", "1 kg"] + for mass in masses: + result = parse_mass_input(mass) + print(f"质量解析: {mass} → {result}g") + + # 测试时间解析 + times = ["1 h", "20 min", "30 s", 60.0, "?"] + for time in times: + result = parse_time_input(time) + print(f"时间解析: {time} → {result}s") + print("✅ 测试完成") - if __name__ == "__main__": test_add_protocol() \ No newline at end of file diff --git a/unilabos/compile/dissolve_protocol.py b/unilabos/compile/dissolve_protocol.py index 3da0d5378..9008e6d57 100644 --- a/unilabos/compile/dissolve_protocol.py +++ b/unilabos/compile/dissolve_protocol.py @@ -1,359 +1,742 @@ -from typing import List, Dict, Any import networkx as nx -from .pump_protocol import generate_pump_protocol +import re +import logging +from typing import List, Dict, Any, Union +from .pump_protocol import generate_pump_protocol_with_rinsing +logger = logging.getLogger(__name__) -def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: +def debug_print(message): + """调试输出""" + print(f"[DISSOLVE] {message}", flush=True) + logger.info(f"[DISSOLVE] {message}") + +def parse_volume_input(volume_input: Union[str, float]) -> float: """ - 查找溶剂容器 + 解析体积输入,支持带单位的字符串 + + Args: + volume_input: 体积输入(如 "10 mL", "?", 10.0) + + Returns: + float: 体积(毫升) """ - # 按照pump_protocol的命名规则查找溶剂瓶 - solvent_vessel_id = f"flask_{solvent}" + if isinstance(volume_input, (int, float)): + return float(volume_input) - if solvent_vessel_id in G.nodes(): - return solvent_vessel_id + if not volume_input or not str(volume_input).strip(): + return 0.0 - # 如果直接匹配失败,尝试模糊匹配 - for node in G.nodes(): - if node.startswith('flask_') and solvent.lower() in node.lower(): - return node + volume_str = str(volume_input).lower().strip() + debug_print(f"解析体积输入: '{volume_str}'") + + # 处理未知体积 + if volume_str in ['?', 'unknown', 'tbd', 'to be determined']: + default_volume = 50.0 # 默认50mL + debug_print(f"检测到未知体积,使用默认值: {default_volume}mL") + return default_volume + + # 移除空格并提取数字和单位 + volume_clean = re.sub(r'\s+', '', volume_str) + + # 匹配数字和单位的正则表达式 + match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean) + + if not match: + debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值50mL") + return 50.0 - # 如果还是找不到,列出所有可用的溶剂瓶 - available_flasks = [node for node in G.nodes() - if node.startswith('flask_') - and G.nodes[node].get('type') == 'container'] + value = float(match.group(1)) + unit = match.group(2) or 'ml' # 默认单位为毫升 - raise ValueError(f"找不到溶剂 '{solvent}' 对应的溶剂瓶。可用溶剂瓶: {available_flasks}") + # 转换为毫升 + if unit in ['l', 'liter']: + volume = value * 1000.0 # L -> mL + elif unit in ['μl', 'ul', 'microliter']: + volume = value / 1000.0 # μL -> mL + else: # ml, milliliter 或默认 + volume = value # 已经是mL + + debug_print(f"体积转换: {value}{unit} → {volume}mL") + return volume +def parse_mass_input(mass_input: Union[str, float]) -> float: + """ + 解析质量输入,支持带单位的字符串 + + Args: + mass_input: 质量输入(如 "2.9 g", "?", 2.5) + + Returns: + float: 质量(克) + """ + if isinstance(mass_input, (int, float)): + return float(mass_input) + + if not mass_input or not str(mass_input).strip(): + return 0.0 + + mass_str = str(mass_input).lower().strip() + debug_print(f"解析质量输入: '{mass_str}'") + + # 处理未知质量 + if mass_str in ['?', 'unknown', 'tbd', 'to be determined']: + default_mass = 1.0 # 默认1g + debug_print(f"检测到未知质量,使用默认值: {default_mass}g") + return default_mass + + # 移除空格并提取数字和单位 + mass_clean = re.sub(r'\s+', '', mass_str) + + # 匹配数字和单位的正则表达式 + match = re.match(r'([0-9]*\.?[0-9]+)\s*(g|mg|kg|gram|milligram|kilogram)?', mass_clean) + + if not match: + debug_print(f"⚠️ 无法解析质量: '{mass_str}',返回0.0g") + return 0.0 + + value = float(match.group(1)) + unit = match.group(2) or 'g' # 默认单位为克 + + # 转换为克 + if unit in ['mg', 'milligram']: + mass = value / 1000.0 # mg -> g + elif unit in ['kg', 'kilogram']: + mass = value * 1000.0 # kg -> g + else: # g, gram 或默认 + mass = value # 已经是g + + debug_print(f"质量转换: {value}{unit} → {mass}g") + return mass -def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str: +def parse_time_input(time_input: Union[str, float]) -> float: + """ + 解析时间输入,支持带单位的字符串 + + Args: + time_input: 时间输入(如 "30 min", "1 h", "?", 60.0) + + Returns: + float: 时间(秒) """ - 查找与指定容器相连的加热搅拌器 + if isinstance(time_input, (int, float)): + return float(time_input) + + if not time_input or not str(time_input).strip(): + return 0.0 + + time_str = str(time_input).lower().strip() + debug_print(f"解析时间输入: '{time_str}'") + + # 处理未知时间 + if time_str in ['?', 'unknown', 'tbd']: + default_time = 600.0 # 默认10分钟 + debug_print(f"检测到未知时间,使用默认值: {default_time}s") + return default_time + + # 移除空格并提取数字和单位 + time_clean = re.sub(r'\s+', '', time_str) + + # 匹配数字和单位的正则表达式 + match = re.match(r'([0-9]*\.?[0-9]+)\s*(s|sec|second|min|minute|h|hr|hour|d|day)?', time_clean) + + if not match: + debug_print(f"⚠️ 无法解析时间: '{time_str}',返回0s") + return 0.0 + + value = float(match.group(1)) + unit = match.group(2) or 's' # 默认单位为秒 + + # 转换为秒 + if unit in ['min', 'minute']: + time_sec = value * 60.0 # min -> s + elif unit in ['h', 'hr', 'hour']: + time_sec = value * 3600.0 # h -> s + elif unit in ['d', 'day']: + time_sec = value * 86400.0 # d -> s + else: # s, sec, second 或默认 + time_sec = value # 已经是s + + debug_print(f"时间转换: {value}{unit} → {time_sec}s") + return time_sec + +def parse_temperature_input(temp_input: Union[str, float]) -> float: """ - # 查找所有加热搅拌器节点 - heatchill_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_heatchill'] + 解析温度输入,支持带单位的字符串 - # 检查哪个加热器与目标容器相连 + Args: + temp_input: 温度输入(如 "60 °C", "room temperature", "?", 25.0) + + Returns: + float: 温度(摄氏度) + """ + if isinstance(temp_input, (int, float)): + return float(temp_input) + + if not temp_input or not str(temp_input).strip(): + return 25.0 # 默认室温 + + temp_str = str(temp_input).lower().strip() + debug_print(f"解析温度输入: '{temp_str}'") + + # 处理特殊温度描述 + temp_aliases = { + 'room temperature': 25.0, + 'rt': 25.0, + 'ambient': 25.0, + 'cold': 4.0, + 'ice': 0.0, + 'reflux': 80.0, # 默认回流温度 + '?': 25.0, + 'unknown': 25.0 + } + + if temp_str in temp_aliases: + result = temp_aliases[temp_str] + debug_print(f"温度别名解析: '{temp_str}' → {result}°C") + return result + + # 移除空格并提取数字和单位 + temp_clean = re.sub(r'\s+', '', temp_str) + + # 匹配数字和单位的正则表达式 + match = re.match(r'([0-9]*\.?[0-9]+)\s*(°c|c|celsius|°f|f|fahrenheit|k|kelvin)?', temp_clean) + + if not match: + debug_print(f"⚠️ 无法解析温度: '{temp_str}',使用默认值25°C") + return 25.0 + + value = float(match.group(1)) + unit = match.group(2) or 'c' # 默认单位为摄氏度 + + # 转换为摄氏度 + if unit in ['°f', 'f', 'fahrenheit']: + temp_c = (value - 32) * 5/9 # F -> C + elif unit in ['k', 'kelvin']: + temp_c = value - 273.15 # K -> C + else: # °c, c, celsius 或默认 + temp_c = value # 已经是C + + debug_print(f"温度转换: {value}{unit} → {temp_c}°C") + return temp_c + +def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: + """增强版溶剂容器查找""" + debug_print(f"查找溶剂 '{solvent}' 的容器...") + + # 🔧 方法1:直接搜索 data.reagent_name 和 config.reagent + for node in G.nodes(): + node_data = G.nodes[node].get('data', {}) + node_type = G.nodes[node].get('type', '') + config_data = G.nodes[node].get('config', {}) + + # 只搜索容器类型的节点 + if node_type == 'container': + reagent_name = node_data.get('reagent_name', '').lower() + config_reagent = config_data.get('reagent', '').lower() + + # 精确匹配 + if reagent_name == solvent.lower() or config_reagent == solvent.lower(): + debug_print(f"✅ 通过reagent字段找到容器: {node}") + return node + + # 模糊匹配 + if (solvent.lower() in reagent_name and reagent_name) or \ + (solvent.lower() in config_reagent and config_reagent): + debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node}") + return node + + # 🔧 方法2:常见的容器命名规则 + solvent_clean = solvent.lower().replace(' ', '_').replace('-', '_') + possible_names = [ + f"flask_{solvent_clean}", + f"bottle_{solvent_clean}", + f"vessel_{solvent_clean}", + f"{solvent_clean}_flask", + f"{solvent_clean}_bottle", + f"solvent_{solvent_clean}", + f"reagent_{solvent_clean}", + f"reagent_bottle_{solvent_clean}" + ] + + for name in possible_names: + if name in G.nodes(): + node_type = G.nodes[name].get('type', '') + if node_type == 'container': + debug_print(f"✅ 通过命名规则找到容器: {name}") + return name + + # 🔧 方法3:使用第一个试剂瓶作为备选 + for node_id in G.nodes(): + node_data = G.nodes[node_id] + if (node_data.get('type') == 'container' and + ('reagent' in node_id.lower() or 'bottle' in node_id.lower() or 'flask' in node_id.lower())): + debug_print(f"⚠️ 未找到专用容器,使用备选容器: {node_id}") + return node_id + + raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器") + +def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str: + """查找连接到指定容器的加热搅拌器""" + heatchill_nodes = [] + for node in G.nodes(): + node_class = G.nodes[node].get('class', '').lower() + if 'heatchill' in node_class: + heatchill_nodes.append(node) + + # 查找连接到容器的加热器 for heatchill in heatchill_nodes: if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill): + debug_print(f"找到连接的加热器: {heatchill}") return heatchill - # 如果没有直接连接,返回第一个可用的加热器 - return heatchill_nodes[0] if heatchill_nodes else None + # 返回第一个加热器 + if heatchill_nodes: + debug_print(f"使用第一个加热器: {heatchill_nodes[0]}") + return heatchill_nodes[0] + + return "" + +def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str: + """查找连接到指定容器的搅拌器""" + stirrer_nodes = [] + for node in G.nodes(): + node_class = G.nodes[node].get('class', '').lower() + if 'stirrer' in node_class: + stirrer_nodes.append(node) + + # 查找连接到容器的搅拌器 + for stirrer in stirrer_nodes: + if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): + debug_print(f"找到连接的搅拌器: {stirrer}") + return stirrer + + # 返回第一个搅拌器 + if stirrer_nodes: + debug_print(f"使用第一个搅拌器: {stirrer_nodes[0]}") + return stirrer_nodes[0] + + return "" +def find_solid_dispenser(G: nx.DiGraph) -> str: + """查找固体加样器""" + for node in G.nodes(): + node_class = G.nodes[node].get('class', '').lower() + if 'solid_dispenser' in node_class or 'dispenser' in node_class: + debug_print(f"找到固体加样器: {node}") + return node + + debug_print("⚠️ 未找到固体加样器") + return "" def generate_dissolve_protocol( G: nx.DiGraph, vessel: str, - solvent: str, - volume: float, + # 🔧 修复:按照checklist.md的DissolveProtocol参数 + solvent: str = "", + volume: Union[str, float] = 0.0, amount: str = "", - temp: float = 25.0, - time: float = 0.0, - stir_speed: float = 300.0 + temp: Union[str, float] = 25.0, + time: Union[str, float] = 0.0, + stir_speed: float = 300.0, + # 🔧 关键修复:添加缺失的参数,防止"unexpected keyword argument"错误 + mass: Union[str, float] = 0.0, # 这个参数在action文件中存在,必须包含 + mol: str = "", # 这个参数在action文件中存在,必须包含 + reagent: str = "", # 这个参数在action文件中存在,必须包含 + event: str = "", # 这个参数在action文件中存在,必须包含 + **kwargs # 🔧 关键:接受所有其他参数,防止unexpected keyword错误 ) -> List[Dict[str, Any]]: """ - 生成溶解操作的协议序列,复用 pump_protocol 的成熟算法 + 生成溶解操作的协议序列 - 修复版 - 溶解流程: - 1. 溶剂转移:将溶剂从溶剂瓶转移到目标容器 - 2. 启动加热搅拌:设置温度和搅拌 - 3. 等待溶解:监控溶解过程 - 4. 停止加热搅拌:完成溶解 + 🔧 修复要点: + 1. 添加action文件中的所有参数(mass, mol, reagent, event) + 2. 使用 **kwargs 接受所有额外参数,防止 unexpected keyword argument 错误 + 3. 支持固体溶解和液体溶解两种模式 - Args: - G: 有向图,节点为设备和容器,边为流体管道 - vessel: 目标容器(要进行溶解的容器) - solvent: 溶剂名称(用于查找对应的溶剂瓶) - volume: 溶剂体积 (mL) - amount: 要溶解的物质描述 - temp: 溶解温度 (°C),默认25°C(室温) - time: 溶解时间 (秒),默认0(立即完成) - stir_speed: 搅拌速度 (RPM),默认300 RPM + 支持两种溶解模式: + 1. 液体溶解:指定 solvent + volume,使用pump protocol转移溶剂 + 2. 固体溶解:指定 mass/mol + reagent,使用固体加样器添加固体试剂 - Returns: - List[Dict[str, Any]]: 溶解操作的动作序列 + 支持所有XDL参数和单位: + - volume: "10 mL", "?" 或数值 + - mass: "2.9 g", "?" 或数值 + - temp: "60 °C", "room temperature", "?" 或数值 + - time: "30 min", "1 h", "?" 或数值 + - mol: "0.12 mol", "16.2 mmol" + """ - Raises: - ValueError: 当找不到必要的设备或容器时 + debug_print("=" * 60) + debug_print("开始生成溶解协议 - 修复版") + debug_print(f"原始参数:") + debug_print(f" - vessel: '{vessel}'") + debug_print(f" - solvent: '{solvent}'") + debug_print(f" - volume: {volume} (类型: {type(volume)})") + debug_print(f" - mass: {mass} (类型: {type(mass)})") + debug_print(f" - temp: {temp} (类型: {type(temp)})") + debug_print(f" - time: {time} (类型: {type(time)})") + debug_print(f" - reagent: '{reagent}'") + debug_print(f" - mol: '{mol}'") + debug_print(f" - event: '{event}'") + debug_print(f" - kwargs: {kwargs}") # 显示额外参数 + debug_print("=" * 60) - Examples: - dissolve_actions = generate_dissolve_protocol(G, "reaction_mixture", "DMF", 10.0, "NaCl 2g", 60.0, 600.0, 400.0) - """ action_sequence = [] - print(f"DISSOLVE: 开始生成溶解协议") - print(f" - 目标容器: {vessel}") - print(f" - 溶剂: {solvent}") - print(f" - 溶剂体积: {volume} mL") - print(f" - 要溶解的物质: {amount}") - print(f" - 溶解温度: {temp}°C") - print(f" - 溶解时间: {time}s ({time/60:.1f}分钟)" if time > 0 else " - 溶解时间: 立即完成") - 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}' 不存在于系统中") - # 查找溶剂瓶 - try: - solvent_vessel = find_solvent_vessel(G, solvent) - print(f"DISSOLVE: 找到溶剂瓶: {solvent_vessel}") - except ValueError as e: - raise ValueError(f"无法找到溶剂 '{solvent}': {str(e)}") + debug_print("✅ 基本参数验证通过") - # 验证是否存在从溶剂瓶到目标容器的路径 - try: - path = nx.shortest_path(G, source=solvent_vessel, target=vessel) - print(f"DISSOLVE: 找到路径 {solvent_vessel} -> {vessel}: {path}") - except nx.NetworkXNoPath: - raise ValueError(f"从溶剂瓶 '{solvent_vessel}' 到目标容器 '{vessel}' 没有可用路径") + # === 🔧 关键修复:参数解析 === + debug_print("步骤2: 参数解析...") + + # 解析各种参数为数值 + final_volume = parse_volume_input(volume) + final_mass = parse_mass_input(mass) + final_temp = parse_temperature_input(temp) + final_time = parse_time_input(time) + + debug_print(f"解析结果:") + debug_print(f" - 体积: {final_volume}mL") + debug_print(f" - 质量: {final_mass}g") + debug_print(f" - 温度: {final_temp}°C") + debug_print(f" - 时间: {final_time}s") + debug_print(f" - 试剂: '{reagent}'") + debug_print(f" - 摩尔: '{mol}'") + debug_print(f" - 事件: '{event}'") + + # === 判断溶解类型 === + debug_print("步骤3: 判断溶解类型...") + + # 判断是固体溶解还是液体溶解 + is_solid_dissolve = (final_mass > 0 or (mol and mol.strip() != "") or (reagent and reagent.strip() != "")) + is_liquid_dissolve = (final_volume > 0 and solvent and solvent.strip() != "") + + if not is_solid_dissolve and not is_liquid_dissolve: + # 默认为液体溶解,50mL + is_liquid_dissolve = True + final_volume = 50.0 + if not solvent: + solvent = "water" # 默认溶剂 + debug_print("⚠️ 未明确指定溶解参数,默认为50mL水溶解") + + debug_print(f"溶解类型: {'固体溶解' if is_solid_dissolve else '液体溶解'}") + + # === 查找设备 === + debug_print("步骤4: 查找设备...") # 查找加热搅拌器 - heatchill_id = None - if temp > 25.0 or stir_speed > 0 or time > 0: - try: - heatchill_id = find_connected_heatchill(G, vessel) - if heatchill_id: - print(f"DISSOLVE: 找到加热搅拌器: {heatchill_id}") - else: - print(f"DISSOLVE: 警告 - 需要加热/搅拌但未找到与容器 {vessel} 相连的加热搅拌器") - except Exception as e: - print(f"DISSOLVE: 加热搅拌器配置出错: {str(e)}") + heatchill_id = find_connected_heatchill(G, vessel) + stirrer_id = find_connected_stirrer(G, vessel) - # === 第一步:启动加热搅拌(在添加溶剂前) === - if heatchill_id and (temp > 25.0 or time > 0): - print(f"DISSOLVE: 启动加热搅拌器,温度: {temp}°C") - - if time > 0: - # 如果指定了时间,使用定时加热搅拌 - heatchill_action = { - "device_id": heatchill_id, - "action_name": "heat_chill", - "action_kwargs": { - "vessel": vessel, - "temp": temp, - "time": time, - "stir": True, - "stir_speed": stir_speed, - "purpose": f"溶解 {amount} 在 {solvent} 中" + # 优先使用加热搅拌器,否则使用独立搅拌器 + stir_device_id = heatchill_id or stirrer_id + + debug_print(f"设备映射:") + debug_print(f" - 加热器: '{heatchill_id}'") + debug_print(f" - 搅拌器: '{stirrer_id}'") + debug_print(f" - 使用设备: '{stir_device_id}'") + + # === 执行溶解流程 === + debug_print("步骤5: 执行溶解流程...") + + try: + # 步骤5.1: 启动加热搅拌(如果需要) + if stir_device_id and (final_temp > 25.0 or final_time > 0 or stir_speed > 0): + debug_print(f"5.1: 启动加热搅拌,温度: {final_temp}°C") + + if heatchill_id and (final_temp > 25.0 or final_time > 0): + # 使用加热搅拌器 + heatchill_action = { + "device_id": heatchill_id, + "action_name": "heat_chill_start", + "action_kwargs": { + "vessel": vessel, + "temp": final_temp, + "purpose": f"溶解准备 - {event}" if event else "溶解准备" + } } - } - else: - # 如果没有指定时间,使用持续加热搅拌 - heatchill_action = { - "device_id": heatchill_id, - "action_name": "heat_chill_start", - "action_kwargs": { - "vessel": vessel, - "temp": temp, - "purpose": f"溶解 {amount} 在 {solvent} 中" + action_sequence.append(heatchill_action) + + # 等待温度稳定 + if final_temp > 25.0: + wait_time = min(60, abs(final_temp - 25.0) * 1.5) + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": wait_time} + }) + + elif stirrer_id: + # 使用独立搅拌器 + stir_action = { + "device_id": stirrer_id, + "action_name": "start_stir", + "action_kwargs": { + "vessel": vessel, + "stir_speed": stir_speed, + "purpose": f"溶解搅拌 - {event}" if event else "溶解搅拌" + } } - } - - action_sequence.append(heatchill_action) - - # 等待温度稳定 - if temp > 25.0: - wait_time = min(60, abs(temp - 25.0) * 1.5) # 根据温差估算预热时间 - action_sequence.append({ - "action_name": "wait", - "action_kwargs": {"time": wait_time} - }) - - # === 第二步:添加溶剂到目标容器 === - if volume > 0: - print(f"DISSOLVE: 将 {volume} mL {solvent} 从 {solvent_vessel} 转移到 {vessel}") + action_sequence.append(stir_action) + + # 等待搅拌稳定 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 5} + }) - # 计算流速 - 溶解时通常用较慢的速度,避免飞溅 - transfer_flowrate = 1.0 # 较慢的转移速度 - flowrate = 0.5 # 较慢的注入速度 + if is_solid_dissolve: + # === 固体溶解路径 === + debug_print(f"5.2: 使用固体溶解路径") + + solid_dispenser = find_solid_dispenser(G) + if solid_dispenser: + # 固体加样 + add_kwargs = { + "vessel": vessel, + "reagent": reagent or amount or "solid reagent", + "purpose": f"溶解固体试剂 - {event}" if event else "溶解固体试剂", + "event": event + } + + if final_mass > 0: + add_kwargs["mass"] = str(final_mass) + if mol and mol.strip(): + add_kwargs["mol"] = mol + + action_sequence.append({ + "device_id": solid_dispenser, + "action_name": "add_solid", + "action_kwargs": add_kwargs + }) + + debug_print(f"✅ 固体加样完成") + else: + debug_print("⚠️ 未找到固体加样器,跳过固体添加") - try: - # 使用成熟的 pump_protocol 算法进行液体转移 - pump_actions = generate_pump_protocol( - G=G, - from_vessel=solvent_vessel, - to_vessel=vessel, - volume=volume, - flowrate=flowrate, # 注入速度 - 较慢避免飞溅 - transfer_flowrate=transfer_flowrate # 转移速度 - ) + elif is_liquid_dissolve: + # === 液体溶解路径 === + debug_print(f"5.3: 使用液体溶解路径") - action_sequence.extend(pump_actions) + # 查找溶剂容器 + try: + solvent_vessel = find_solvent_vessel(G, solvent) + except ValueError as e: + debug_print(f"⚠️ {str(e)},跳过溶剂添加") + solvent_vessel = None - except Exception as e: - raise ValueError(f"生成泵协议时出错: {str(e)}") + if solvent_vessel: + # 计算流速 - 溶解时通常用较慢的速度,避免飞溅 + flowrate = 1.0 # 较慢的注入速度 + transfer_flowrate = 0.5 # 较慢的转移速度 + + # 调用pump protocol + pump_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=solvent_vessel, + to_vessel=vessel, + volume=final_volume, + amount=amount, + time=0.0, # 不在pump level控制时间 + viscous=False, + rinsing_solvent="", + rinsing_volume=0.0, + rinsing_repeats=0, + solid=False, + flowrate=flowrate, + transfer_flowrate=transfer_flowrate, + rate_spec="", + event=event, + through="", + **kwargs + ) + action_sequence.extend(pump_actions) + debug_print(f"✅ 溶剂转移完成,添加了 {len(pump_actions)} 个动作") + + # 溶剂添加后等待 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 5} + }) - # 溶剂添加后等待 - action_sequence.append({ - "action_name": "wait", - "action_kwargs": {"time": 5} - }) - - # === 第三步:如果没有使用定时加热搅拌,但需要等待溶解 === - if time > 0 and heatchill_id and temp <= 25.0: - # 只需要搅拌等待,不需要加热 - print(f"DISSOLVE: 室温搅拌 {time}s 等待溶解") + # 步骤5.4: 等待溶解完成 + if final_time > 0: + debug_print(f"5.4: 等待溶解完成 - {final_time}s") + + if heatchill_id: + # 使用定时加热搅拌 + dissolve_action = { + "device_id": heatchill_id, + "action_name": "heat_chill", + "action_kwargs": { + "vessel": vessel, + "temp": final_temp, + "time": final_time, + "stir": True, + "stir_speed": stir_speed, + "purpose": f"溶解等待 - {event}" if event else "溶解等待" + } + } + action_sequence.append(dissolve_action) + + elif stirrer_id: + # 使用定时搅拌 + stir_action = { + "device_id": stirrer_id, + "action_name": "stir", + "action_kwargs": { + "vessel": vessel, + "stir_time": final_time, + "stir_speed": stir_speed, + "settling_time": 0, + "purpose": f"溶解搅拌 - {event}" if event else "溶解搅拌" + } + } + action_sequence.append(stir_action) + + else: + # 简单等待 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": final_time} + }) - stir_action = { - "device_id": heatchill_id, - "action_name": "heat_chill", - "action_kwargs": { - "vessel": vessel, - "temp": 25.0, # 室温 - "time": time, - "stir": True, - "stir_speed": stir_speed, - "purpose": f"室温搅拌溶解 {amount}" + # 步骤5.5: 停止加热搅拌(如果需要) + if heatchill_id and final_time == 0 and final_temp > 25.0: + debug_print(f"5.5: 停止加热器") + + stop_action = { + "device_id": heatchill_id, + "action_name": "heat_chill_stop", + "action_kwargs": { + "vessel": vessel + } } - } - action_sequence.append(stir_action) - - # === 第四步:如果使用了持续加热,需要手动停止 === - if heatchill_id and time == 0 and temp > 25.0: - print(f"DISSOLVE: 停止加热搅拌器") + action_sequence.append(stop_action) - stop_action = { - "device_id": heatchill_id, - "action_name": "heat_chill_stop", + except Exception as e: + debug_print(f"⚠️ 溶解流程执行失败: {str(e)}") + # 添加错误日志 + action_sequence.append({ + "device_id": "system", + "action_name": "log_message", "action_kwargs": { - "vessel": vessel + "message": f"溶解失败: {str(e)}" } - } - action_sequence.append(stop_action) + }) - print(f"DISSOLVE: 生成了 {len(action_sequence)} 个动作") - print(f"DISSOLVE: 溶解协议生成完成") + # === 最终结果 === + debug_print("=" * 60) + debug_print(f"✅ 溶解协议生成完成") + debug_print(f"📊 总动作数: {len(action_sequence)}") + debug_print(f"📋 处理总结:") + debug_print(f" - 容器: {vessel}") + debug_print(f" - 溶解类型: {'固体溶解' if is_solid_dissolve else '液体溶解'}") + if is_liquid_dissolve: + debug_print(f" - 溶剂: {solvent} ({final_volume}mL)") + if is_solid_dissolve: + debug_print(f" - 试剂: {reagent}") + debug_print(f" - 质量: {final_mass}g") + debug_print(f" - 摩尔: {mol}") + debug_print(f" - 温度: {final_temp}°C") + debug_print(f" - 时间: {final_time}s") + debug_print("=" * 60) return action_sequence +# === 便捷函数 === -# 便捷函数:常用溶解方案 -def generate_room_temp_dissolve_protocol( - G: nx.DiGraph, - vessel: str, - solvent: str, - volume: float, - amount: str = "", - stir_time: float = 300.0 # 5分钟 -) -> List[Dict[str, Any]]: - """室温溶解:快速搅拌,短时间""" - return generate_dissolve_protocol(G, vessel, solvent, volume, amount, 25.0, stir_time, 400.0) - - -def generate_heated_dissolve_protocol( - G: nx.DiGraph, - vessel: str, - solvent: str, - volume: float, - amount: str = "", - temp: float = 60.0, - dissolve_time: float = 900.0 # 15分钟 -) -> List[Dict[str, Any]]: - """加热溶解:中等温度,较长时间""" - return generate_dissolve_protocol(G, vessel, solvent, volume, amount, temp, dissolve_time, 300.0) - +def dissolve_solid_by_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, float], + temp: Union[str, float] = 25.0, time: Union[str, float] = "10 min") -> List[Dict[str, Any]]: + """按质量溶解固体""" + return generate_dissolve_protocol( + G, vessel, + mass=mass, + reagent=reagent, + temp=temp, + time=time + ) -def generate_gentle_dissolve_protocol( - G: nx.DiGraph, - vessel: str, - solvent: str, - volume: float, - amount: str = "", - temp: float = 40.0, - dissolve_time: float = 1800.0 # 30分钟 -) -> List[Dict[str, Any]]: - """温和溶解:低温,长时间,慢搅拌""" - return generate_dissolve_protocol(G, vessel, solvent, volume, amount, temp, dissolve_time, 200.0) +def dissolve_solid_by_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str, + temp: Union[str, float] = 25.0, time: Union[str, float] = "10 min") -> List[Dict[str, Any]]: + """按摩尔数溶解固体""" + return generate_dissolve_protocol( + G, vessel, + mol=mol, + reagent=reagent, + temp=temp, + time=time + ) +def dissolve_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float], + temp: Union[str, float] = 25.0, time: Union[str, float] = "5 min") -> List[Dict[str, Any]]: + """用溶剂溶解""" + return generate_dissolve_protocol( + G, vessel, + solvent=solvent, + volume=volume, + temp=temp, + time=time + ) -def generate_hot_dissolve_protocol( - G: nx.DiGraph, - vessel: str, - solvent: str, - volume: float, - amount: str = "", - temp: float = 80.0, - dissolve_time: float = 600.0 # 10分钟 -) -> List[Dict[str, Any]]: - """高温溶解:高温,中等时间,快搅拌""" - return generate_dissolve_protocol(G, vessel, solvent, volume, amount, temp, dissolve_time, 500.0) +def dissolve_at_room_temp(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float]) -> List[Dict[str, Any]]: + """室温溶解""" + return generate_dissolve_protocol( + G, vessel, + solvent=solvent, + volume=volume, + temp="room temperature", + time="5 min" + ) +def dissolve_with_heating(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float], + temp: Union[str, float] = "60 °C", time: Union[str, float] = "15 min") -> List[Dict[str, Any]]: + """加热溶解""" + return generate_dissolve_protocol( + G, vessel, + solvent=solvent, + volume=volume, + temp=temp, + time=time + ) -def generate_sequential_dissolve_protocol( - G: nx.DiGraph, - vessel: str, - dissolve_steps: List[Dict[str, Any]] -) -> List[Dict[str, Any]]: - """ - 生成连续溶解多种物质的协议 +# 测试函数 +def test_dissolve_protocol(): + """测试溶解协议的各种参数解析""" + print("=== DISSOLVE PROTOCOL 修复版测试 ===") - Args: - G: 网络图 - vessel: 目标容器 - dissolve_steps: 溶解步骤列表,每个元素包含溶解参数 + # 测试体积解析 + volumes = ["10 mL", "?", 10.0, "1 L", "500 μL"] + for vol in volumes: + result = parse_volume_input(vol) + print(f"体积解析: {vol} → {result}mL") - Returns: - List[Dict[str, Any]]: 完整的动作序列 - - Example: - dissolve_steps = [ - { - "solvent": "water", - "volume": 5.0, - "amount": "NaCl 1g", - "temp": 25.0, - "time": 300.0, - "stir_speed": 300.0 - }, - { - "solvent": "ethanol", - "volume": 2.0, - "amount": "organic compound 0.5g", - "temp": 40.0, - "time": 600.0, - "stir_speed": 400.0 - } - ] - """ - action_sequence = [] + # 测试质量解析 + masses = ["2.9 g", "?", 2.5, "500 mg"] + for mass in masses: + result = parse_mass_input(mass) + print(f"质量解析: {mass} → {result}g") - for i, step in enumerate(dissolve_steps): - print(f"DISSOLVE: 处理第 {i+1}/{len(dissolve_steps)} 个溶解步骤") - - # 生成单个溶解步骤的协议 - dissolve_actions = generate_dissolve_protocol( - G=G, - vessel=vessel, - solvent=step.get('solvent'), - volume=step.get('volume', 0.0), - amount=step.get('amount', ''), - temp=step.get('temp', 25.0), - time=step.get('time', 0.0), - stir_speed=step.get('stir_speed', 300.0) - ) - - action_sequence.extend(dissolve_actions) - - # 在步骤之间加入等待时间 - if i < len(dissolve_steps) - 1: # 不是最后一个步骤 - action_sequence.append({ - "action_name": "wait", - "action_kwargs": {"time": 10} - }) - - print(f"DISSOLVE: 连续溶解协议生成完成,共 {len(action_sequence)} 个动作") - return action_sequence - - -# 测试函数 -def test_dissolve_protocol(): - """测试溶解协议的示例""" - print("=== DISSOLVE PROTOCOL 测试 ===") - print("测试完成") - + # 测试温度解析 + temps = ["60 °C", "room temperature", "?", 25.0, "reflux"] + for temp in temps: + result = parse_temperature_input(temp) + print(f"温度解析: {temp} → {result}°C") + + # 测试时间解析 + times = ["30 min", "1 h", "?", 60.0] + for time in times: + result = parse_time_input(time) + print(f"时间解析: {time} → {result}s") + + print("✅ 测试完成") if __name__ == "__main__": test_dissolve_protocol() \ No newline at end of file diff --git a/unilabos/compile/evacuateandrefill_protocol.py b/unilabos/compile/evacuateandrefill_protocol.py index e5e8df5c4..c50b88492 100644 --- a/unilabos/compile/evacuateandrefill_protocol.py +++ b/unilabos/compile/evacuateandrefill_protocol.py @@ -1,8 +1,16 @@ -import numpy as np import networkx as nx +import logging +import uuid # 🔧 移到顶部 from typing import List, Dict, Any, Optional from .pump_protocol import generate_pump_protocol_with_rinsing, generate_pump_protocol +# 设置日志 +logger = logging.getLogger(__name__) + +def debug_print(message): + """调试输出函数""" + print(f"[EVACUATE_REFILL] {message}", flush=True) + logger.info(f"[EVACUATE_REFILL] {message}") def find_gas_source(G: nx.DiGraph, gas: str) -> str: """ @@ -11,7 +19,7 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str: 2. 气体类型匹配(data.gas_type) 3. 默认气源 """ - print(f"EVACUATE_REFILL: 正在查找气体 '{gas}' 的气源...") + debug_print(f"正在查找气体 '{gas}' 的气源...") # 第一步:通过容器名称匹配 gas_source_patterns = [ @@ -26,7 +34,7 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str: for pattern in gas_source_patterns: if pattern in G.nodes(): - print(f"EVACUATE_REFILL: 通过名称匹配找到气源: {pattern}") + debug_print(f"通过名称匹配找到气源: {pattern}") return pattern # 第二步:通过气体类型匹配 (data.gas_type) @@ -44,7 +52,7 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str: gas_type = data.get('gas_type', '') if gas_type.lower() == gas.lower(): - print(f"EVACUATE_REFILL: 通过气体类型匹配找到气源: {node_id} (gas_type: {gas_type})") + debug_print(f"通过气体类型匹配找到气源: {node_id} (gas_type: {gas_type})") return node_id # 检查 config.gas_type @@ -52,7 +60,7 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str: config_gas_type = config.get('gas_type', '') if config_gas_type.lower() == gas.lower(): - print(f"EVACUATE_REFILL: 通过配置气体类型匹配找到气源: {node_id} (config.gas_type: {config_gas_type})") + debug_print(f"通过配置气体类型匹配找到气源: {node_id} (config.gas_type: {config_gas_type})") return node_id # 第三步:查找所有可用的气源设备 @@ -69,139 +77,138 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str: gas_type = data.get('gas_type', 'unknown') available_gas_sources.append(f"{node_id} (gas_type: {gas_type})") - print(f"EVACUATE_REFILL: 可用气源列表: {available_gas_sources}") + debug_print(f"可用气源列表: {available_gas_sources}") # 第四步:如果找不到特定气体,使用默认的第一个气源 default_gas_sources = [ node for node in G.nodes() - if ((G.nodes[node].get('class') or '').startswith('virtual_gas_source') + if ((G.nodes[node].get('class') or '').find('virtual_gas_source') != -1 or 'gas_source' in node) ] if default_gas_sources: default_source = default_gas_sources[0] - print(f"EVACUATE_REFILL: ⚠️ 未找到特定气体 '{gas}',使用默认气源: {default_source}") + debug_print(f"⚠️ 未找到特定气体 '{gas}',使用默认气源: {default_source}") return default_source raise ValueError(f"找不到气体 '{gas}' 对应的气源。可用气源: {available_gas_sources}") - -def find_gas_source_by_any_match(G: nx.DiGraph, gas: str) -> str: - """ - 增强版气源查找,支持各种匹配方式的别名函数 - """ - return find_gas_source(G, gas) - - -def get_gas_source_type(G: nx.DiGraph, gas_source: str) -> str: - """获取气源的气体类型""" - if gas_source not in G.nodes(): - return "unknown" - - node_data = G.nodes[gas_source] - data = node_data.get('data', {}) - config = node_data.get('config', {}) - - # 检查多个可能的字段 - gas_type = (data.get('gas_type') or - config.get('gas_type') or - data.get('gas') or - config.get('gas') or - "air") # 默认为空气 - - return gas_type - - -def find_vessels_by_gas_type(G: nx.DiGraph, gas: str) -> List[str]: - """ - 根据气体类型查找所有匹配的容器/气源 - """ - matching_vessels = [] - - for node_id in G.nodes(): - node_data = G.nodes[node_id] - - # 检查容器名称匹配 - if gas.lower() in node_id.lower(): - matching_vessels.append(f"{node_id} (名称匹配)") - continue - - # 检查气体类型匹配 - data = node_data.get('data', {}) - config = node_data.get('config', {}) - - gas_type = data.get('gas_type', '') or config.get('gas_type', '') - if gas_type.lower() == gas.lower(): - matching_vessels.append(f"{node_id} (gas_type: {gas_type})") - - return matching_vessels - - def find_vacuum_pump(G: nx.DiGraph) -> str: """查找真空泵设备""" - vacuum_pumps = [ - node for node in G.nodes() - if ((G.nodes[node].get('class') or '').startswith('virtual_vacuum_pump') - or 'vacuum_pump' in node - or 'vacuum' in (G.nodes[node].get('class') or '')) - ] + debug_print("查找真空泵设备...") + + vacuum_pumps = [] + for node in G.nodes(): + node_data = G.nodes[node] + node_class = node_data.get('class', '') or '' + + if ('virtual_vacuum_pump' in node_class or + 'vacuum_pump' in node.lower() or + 'vacuum' in node_class.lower()): + vacuum_pumps.append(node) if not vacuum_pumps: raise ValueError("系统中未找到真空泵设备") + debug_print(f"找到真空泵: {vacuum_pumps[0]}") return vacuum_pumps[0] - -def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str: +def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> Optional[str]: """查找与指定容器相连的搅拌器""" - stirrer_nodes = [node for node in G.nodes() - if (G.nodes[node].get('class') or '') == 'virtual_stirrer'] + debug_print(f"查找与容器 {vessel} 相连的搅拌器...") + + stirrer_nodes = [] + for node in G.nodes(): + node_data = G.nodes[node] + node_class = node_data.get('class', '') or '' + + if 'virtual_stirrer' in node_class or 'stirrer' in node.lower(): + stirrer_nodes.append(node) # 检查哪个搅拌器与目标容器相连 for stirrer in stirrer_nodes: if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): + debug_print(f"找到连接的搅拌器: {stirrer}") return stirrer - return stirrer_nodes[0] if stirrer_nodes else None - + # 如果没有连接的搅拌器,返回第一个可用的 + if stirrer_nodes: + debug_print(f"未找到直接连接的搅拌器,使用第一个可用的: {stirrer_nodes[0]}") + return stirrer_nodes[0] + + debug_print("未找到搅拌器") + return None -def find_associated_solenoid_valve(G: nx.DiGraph, device_id: str) -> Optional[str]: - """查找与指定设备相关联的电磁阀""" - solenoid_valves = [ - node for node in G.nodes() - if ('solenoid' in (G.nodes[node].get('class') or '').lower() - or 'solenoid_valve' in node) - ] +def find_vacuum_solenoid_valve(G: nx.DiGraph, vacuum_pump: str) -> Optional[str]: + """查找真空泵相关的电磁阀 - 根据实际连接逻辑""" + debug_print(f"查找真空泵 {vacuum_pump} 相关的电磁阀...") + + # 查找所有电磁阀 + solenoid_valves = [] + for node in G.nodes(): + node_data = G.nodes[node] + node_class = node_data.get('class', '') or '' + + if ('solenoid' in node_class.lower() or 'solenoid_valve' in node.lower()): + solenoid_valves.append(node) - # 通过网络连接查找直接相连的电磁阀 + debug_print(f"找到的电磁阀: {solenoid_valves}") + + # 🔧 修复:根据实际组态图连接逻辑查找 + # vacuum_pump_1 <- solenoid_valve_1 <- multiway_valve_2 for solenoid in solenoid_valves: - if G.has_edge(device_id, solenoid) or G.has_edge(solenoid, device_id): + # 检查电磁阀是否连接到真空泵 + if G.has_edge(solenoid, vacuum_pump) or G.has_edge(vacuum_pump, solenoid): + debug_print(f"✓ 找到连接真空泵的电磁阀: {solenoid}") return solenoid - # 通过命名规则查找关联的电磁阀 - device_type = "" - if 'vacuum' in device_id.lower(): - device_type = "vacuum" - elif 'gas' in device_id.lower(): - device_type = "gas" - - if device_type: - for solenoid in solenoid_valves: - if device_type in solenoid.lower(): - return solenoid + # 通过命名规则查找(备选方案) + for solenoid in solenoid_valves: + if 'vacuum' in solenoid.lower() or solenoid == 'solenoid_valve_1': + debug_print(f"✓ 通过命名规则找到真空电磁阀: {solenoid}") + return solenoid + debug_print("⚠️ 未找到真空电磁阀") return None +def find_gas_solenoid_valve(G: nx.DiGraph, gas_source: str) -> Optional[str]: + """查找气源相关的电磁阀 - 根据实际连接逻辑""" + debug_print(f"查找气源 {gas_source} 相关的电磁阀...") + + # 查找所有电磁阀 + solenoid_valves = [] + for node in G.nodes(): + node_data = G.nodes[node] + node_class = node_data.get('class', '') or '' + + if ('solenoid' in node_class.lower() or 'solenoid_valve' in node.lower()): + solenoid_valves.append(node) + + # 🔧 修复:根据实际组态图连接逻辑查找 + # gas_source_1 -> solenoid_valve_2 -> multiway_valve_2 + for solenoid in solenoid_valves: + # 检查气源是否连接到电磁阀 + if G.has_edge(gas_source, solenoid) or G.has_edge(solenoid, gas_source): + debug_print(f"✓ 找到连接气源的电磁阀: {solenoid}") + return solenoid + + # 通过命名规则查找(备选方案) + for solenoid in solenoid_valves: + if 'gas' in solenoid.lower() or solenoid == 'solenoid_valve_2': + debug_print(f"✓ 通过命名规则找到气源电磁阀: {solenoid}") + return solenoid + + debug_print("⚠️ 未找到气源电磁阀") + return None def generate_evacuateandrefill_protocol( G: nx.DiGraph, vessel: str, gas: str, - # 🔧 删除 repeats 参数,直接硬编码为 3 - **kwargs # 🔧 接受额外参数,增强兼容性 + **kwargs ) -> List[Dict[str, Any]]: """ - 生成抽真空和充气操作的动作序列 - 简化版本 + 生成抽真空和充气操作的动作序列 - 最终修复版本 Args: G: 设备图 @@ -213,9 +220,13 @@ def generate_evacuateandrefill_protocol( List[Dict[str, Any]]: 动作序列 """ - # 🔧 硬编码重复次数为 3 + # 硬编码重复次数为 3 repeats = 3 + # 🔧 修复:在函数开始就生成协议ID + protocol_id = str(uuid.uuid4()) + debug_print(f"生成协议ID: {protocol_id}") + debug_print("=" * 60) debug_print("开始生成抽真空充气协议") debug_print(f"输入参数:") @@ -264,8 +275,8 @@ def generate_evacuateandrefill_protocol( try: vacuum_pump = find_vacuum_pump(G) 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) + vacuum_solenoid = find_vacuum_solenoid_valve(G, vacuum_pump) + gas_solenoid = find_gas_solenoid_valve(G, gas_source) stirrer_id = find_connected_stirrer(G, vessel) debug_print(f"设备配置:") @@ -319,20 +330,22 @@ def generate_evacuateandrefill_protocol( debug_print("步骤4: 路径验证...") try: - # 验证抽真空路径 - vacuum_path = nx.shortest_path(G, source=vessel, target=vacuum_pump) - debug_print(f"抽真空路径: {' → '.join(vacuum_path)}") + # 验证抽真空路径: vessel -> vacuum_pump (通过八通阀和电磁阀) + if nx.has_path(G, vessel, vacuum_pump): + vacuum_path = nx.shortest_path(G, source=vessel, target=vacuum_pump) + debug_print(f"抽真空路径: {' → '.join(vacuum_path)}") + else: + debug_print(f"⚠️ 抽真空路径不存在,继续执行但可能有问题") - # 验证充气路径 - gas_path = nx.shortest_path(G, source=gas_source, target=vessel) - debug_print(f"充气路径: {' → '.join(gas_path)}") + # 验证充气路径: gas_source -> vessel (通过电磁阀和八通阀) + if nx.has_path(G, gas_source, vessel): + gas_path = nx.shortest_path(G, source=gas_source, target=vessel) + debug_print(f"充气路径: {' → '.join(gas_path)}") + else: + debug_print(f"⚠️ 充气路径不存在,继续执行但可能有问题") - 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)}") + debug_print(f"⚠️ 路径验证失败: {str(e)},继续执行") # === 启动搅拌器 === debug_print("步骤5: 启动搅拌器...") @@ -360,7 +373,7 @@ def generate_evacuateandrefill_protocol( # === 执行 3 次抽真空-充气循环 === debug_print("步骤6: 执行抽真空-充气循环...") - for cycle in range(repeats): # 这里 repeats = 3 + for cycle in range(repeats): debug_print(f"=== 第 {cycle+1}/{repeats} 次循环 ===") # ============ 抽真空阶段 ============ @@ -383,16 +396,17 @@ def generate_evacuateandrefill_protocol( "action_kwargs": {"command": "OPEN"} }) - # 抽真空操作 + # 抽真空操作 - 使用液体转移协议 debug_print(f"抽真空操作: {vessel} → {vacuum_pump}") try: + vacuum_transfer_actions = generate_pump_protocol_with_rinsing( G=G, from_vessel=vessel, to_vessel=vacuum_pump, volume=VACUUM_VOLUME, amount="", - duration=0.0, # 🔧 修复time参数名冲突 + time=0.0, viscous=False, rinsing_solvent="", rinsing_volume=0.0, @@ -423,7 +437,7 @@ def generate_evacuateandrefill_protocol( # 抽真空后等待 action_sequence.append({ "action_name": "wait", - "action_kwargs": {"time": 5.0} + "action_kwargs": {"time": VACUUM_TIME} }) # 关闭真空电磁阀 @@ -443,6 +457,12 @@ def generate_evacuateandrefill_protocol( "action_kwargs": {"string": "OFF"} }) + # 抽真空后等待 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 5.0} + }) + # ============ 充气阶段 ============ debug_print(f"充气阶段开始") @@ -463,16 +483,17 @@ def generate_evacuateandrefill_protocol( "action_kwargs": {"command": "OPEN"} }) - # 充气操作 + # 充气操作 - 使用液体转移协议 debug_print(f"充气操作: {gas_source} → {vessel}") try: + gas_transfer_actions = generate_pump_protocol_with_rinsing( G=G, from_vessel=gas_source, to_vessel=vessel, volume=REFILL_VOLUME, amount="", - duration=0.0, # 🔧 修复time参数名冲突 + time=0.0, viscous=False, rinsing_solvent="", rinsing_volume=0.0, @@ -503,7 +524,7 @@ def generate_evacuateandrefill_protocol( # 充气后等待 action_sequence.append({ "action_name": "wait", - "action_kwargs": {"time": 5.0} + "action_kwargs": {"time": REFILL_TIME} }) # 关闭气源电磁阀 @@ -559,12 +580,25 @@ def generate_evacuateandrefill_protocol( return action_sequence +# === 便捷函数 === + +def generate_nitrogen_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]: + """生成氮气置换协议""" + return generate_evacuateandrefill_protocol(G, vessel, "nitrogen", **kwargs) + +def generate_argon_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]: + """生成氩气置换协议""" + return generate_evacuateandrefill_protocol(G, vessel, "argon", **kwargs) + +def generate_air_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]: + """生成空气置换协议""" + return generate_evacuateandrefill_protocol(G, vessel, "air", **kwargs) + # 测试函数 def test_evacuateandrefill_protocol(): """测试抽真空充气协议""" - print("=== EVACUATE AND REFILL PROTOCOL 测试 ===") - print("测试完成") - + debug_print("=== EVACUATE AND REFILL PROTOCOL 测试 ===") + debug_print("测试完成") if __name__ == "__main__": test_evacuateandrefill_protocol() \ No newline at end of file diff --git a/unilabos/compile/evaporate_protocol.py b/unilabos/compile/evaporate_protocol.py index b5170b5a8..e3ffb8623 100644 --- a/unilabos/compile/evaporate_protocol.py +++ b/unilabos/compile/evaporate_protocol.py @@ -1,7 +1,6 @@ from typing import List, Dict, Any, Optional import networkx as nx import logging -from .pump_protocol import generate_pump_protocol logger = logging.getLogger(__name__) @@ -10,100 +9,101 @@ 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 - - debug_print(f"未检测到液体体积,返回 0.0") - return 0.0 - -def find_rotavap_device(G: nx.DiGraph) -> Optional[str]: - """查找旋转蒸发仪设备""" - debug_print("查找旋转蒸发仪设备...") - - # 查找各种可能的旋转蒸发仪设备 - 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}") - - if possible_devices: - return possible_devices[0] +def find_rotavap_device(G: nx.DiGraph, vessel: str = None) -> Optional[str]: + """ + 在组态图中查找旋转蒸发仪设备 - debug_print("未找到旋转蒸发仪设备") - return None - -def find_rotavap_vessel(G: nx.DiGraph) -> Optional[str]: - """查找旋转蒸发仪样品容器""" - debug_print("查找旋转蒸发仪样品容器...") + Args: + G: 设备图 + vessel: 指定的设备名称(可选) - possible_vessels = [ - "rotavap", "rotavap_flask", "flask_rotavap", - "evaporation_flask", "evaporator", "rotary_evaporator" - ] + Returns: + str: 找到的旋转蒸发仪设备ID,如果没找到返回None + """ + debug_print("查找旋转蒸发仪设备...") - for vessel in possible_vessels: + # 如果指定了vessel,先检查是否存在且是旋转蒸发仪 + if vessel: if vessel in G.nodes(): - debug_print(f"找到旋转蒸发仪样品容器: {vessel}") - return vessel - - debug_print("未找到旋转蒸发仪样品容器") + node_data = G.nodes[vessel] + node_class = node_data.get('class', '') + node_type = node_data.get('type', '') + + debug_print(f"检查指定设备 {vessel}: class={node_class}, type={node_type}") + + # 检查是否为旋转蒸发仪 + if any(keyword in str(node_class).lower() for keyword in ['rotavap', 'rotary', 'evaporat']): + debug_print(f"✓ 找到指定的旋转蒸发仪: {vessel}") + return vessel + elif node_type == 'device': + debug_print(f"✓ 指定设备存在,尝试直接使用: {vessel}") + return vessel + else: + debug_print(f"✗ 指定的设备 {vessel} 不存在") + + # 在所有设备中查找旋转蒸发仪 + rotavap_candidates = [] + + for node_id, node_data in G.nodes(data=True): + node_class = node_data.get('class', '') + node_type = node_data.get('type', '') + + # 跳过非设备节点 + if node_type != 'device': + continue + + # 检查设备类型 + if any(keyword in str(node_class).lower() for keyword in ['rotavap', 'rotary', 'evaporat']): + rotavap_candidates.append(node_id) + debug_print(f"✓ 找到旋转蒸发仪候选: {node_id} (class: {node_class})") + elif any(keyword in str(node_id).lower() for keyword in ['rotavap', 'rotary', 'evaporat']): + rotavap_candidates.append(node_id) + debug_print(f"✓ 找到旋转蒸发仪候选 (按名称): {node_id}") + + if rotavap_candidates: + selected = rotavap_candidates[0] # 选择第一个找到的 + debug_print(f"✓ 选择旋转蒸发仪: {selected}") + return selected + + debug_print("✗ 未找到旋转蒸发仪设备") return None -def find_recovery_vessel(G: nx.DiGraph) -> Optional[str]: - """查找溶剂回收容器""" - debug_print("查找溶剂回收容器...") +def find_connected_vessel(G: nx.DiGraph, rotavap_device: str) -> Optional[str]: + """ + 查找与旋转蒸发仪连接的容器 - possible_vessels = [ - "flask_distillate", "distillate", "solvent_recovery", - "rotavap_condenser", "condenser", "waste_workup", "waste" - ] + Args: + G: 设备图 + rotavap_device: 旋转蒸发仪设备ID - for vessel in possible_vessels: - if vessel in G.nodes(): - debug_print(f"找到回收容器: {vessel}") - return vessel + Returns: + str: 连接的容器ID,如果没找到返回None + """ + debug_print(f"查找与 {rotavap_device} 连接的容器...") + + # 查看旋转蒸发仪的子设备 + rotavap_data = G.nodes[rotavap_device] + children = rotavap_data.get('children', []) + + for child_id in children: + if child_id in G.nodes(): + child_data = G.nodes[child_id] + child_type = child_data.get('type', '') + + if child_type == 'container': + debug_print(f"✓ 找到连接的容器: {child_id}") + return child_id + + # 查看邻接的容器 + for neighbor in G.neighbors(rotavap_device): + neighbor_data = G.nodes[neighbor] + neighbor_type = neighbor_data.get('type', '') + + if neighbor_type == 'container': + debug_print(f"✓ 找到邻接的容器: {neighbor}") + return neighbor - debug_print("未找到回收容器") + debug_print("✗ 未找到连接的容器") return None def generate_evaporate_protocol( @@ -111,22 +111,22 @@ def generate_evaporate_protocol( vessel: str, pressure: float = 0.1, temp: float = 60.0, - time: float = 1800.0, + time: float = 180.0, stir_speed: float = 100.0, solvent: str = "", **kwargs # 接受任意额外参数,增强兼容性 ) -> List[Dict[str, Any]]: """ - 生成蒸发操作的协议序列 - 增强兼容性版本 + 生成蒸发操作的协议序列 Args: G: 设备图 - vessel: 蒸发容器名称(必需) + vessel: 容器名称或旋转蒸发仪名称 pressure: 真空度 (bar),默认0.1 temp: 加热温度 (°C),默认60 - time: 蒸发时间 (秒),默认1800 + time: 蒸发时间 (秒),默认180 stir_speed: 旋转速度 (RPM),默认100 - solvent: 溶剂名称(可选,用于参数优化) + solvent: 溶剂名称(用于参数优化) **kwargs: 其他参数(兼容性) Returns: @@ -142,20 +142,43 @@ def generate_evaporate_protocol( 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 = [] - - # === 参数验证和修正 === - debug_print("步骤1: 参数验证和修正...") + # === 步骤1: 查找旋转蒸发仪设备 === + debug_print("步骤1: 查找旋转蒸发仪设备...") - # 验证必需参数 + # 验证vessel参数 if not vessel: raise ValueError("vessel 参数不能为空") - if vessel not in G.nodes(): - raise ValueError(f"容器 '{vessel}' 不存在于系统中") + # 查找旋转蒸发仪设备 + rotavap_device = find_rotavap_device(G, vessel) + if not rotavap_device: + raise ValueError(f"未找到旋转蒸发仪设备。请检查组态图中是否包含 class 包含 'rotavap'、'rotary' 或 'evaporat' 的设备") + + # === 步骤2: 确定目标容器 === + debug_print("步骤2: 确定目标容器...") + + target_vessel = vessel + + # 如果vessel就是旋转蒸发仪设备,查找连接的容器 + if vessel == rotavap_device: + connected_vessel = find_connected_vessel(G, rotavap_device) + if connected_vessel: + target_vessel = connected_vessel + debug_print(f"使用连接的容器: {target_vessel}") + else: + debug_print(f"未找到连接的容器,使用设备本身: {rotavap_device}") + target_vessel = rotavap_device + elif vessel in G.nodes() and G.nodes[vessel].get('type') == 'container': + debug_print(f"使用指定的容器: {vessel}") + target_vessel = vessel + else: + debug_print(f"容器 '{vessel}' 不存在或类型不正确,使用旋转蒸发仪设备: {rotavap_device}") + target_vessel = rotavap_device + + # === 步骤3: 参数验证和修正 === + debug_print("步骤3: 参数验证和修正...") # 修正参数范围 if pressure <= 0 or pressure > 1.0: @@ -194,61 +217,10 @@ def generate_evaporate_protocol( 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 = find_rotavap_vessel(G) - if not rotavap_vessel: - debug_print("未找到旋转蒸发仪样品容器,使用默认容器") - rotavap_vessel = "rotavap" # 默认容器 - - # 查找回收容器 - recovery_vessel = find_recovery_vessel(G) - - 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 - debug_print(f"检测到液体体积 {source_volume}mL,转移 {transfer_volume}mL") - else: - transfer_volume = 50.0 # 默认小体积,更安全 - debug_print(f"未检测到液体体积,使用默认转移体积 {transfer_volume}mL") - - # === 生成动作序列 === + # === 步骤4: 生成动作序列 === debug_print("步骤4: 生成动作序列...") - # 动作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 = [] # 等待稳定 action_sequence.append({ @@ -256,13 +228,13 @@ def generate_evaporate_protocol( "action_kwargs": {"time": 10} }) - # 动作2: 执行蒸发 - debug_print(f"执行蒸发: {rotavap_device}") + # 执行蒸发 + debug_print(f"执行蒸发: 设备={rotavap_device}, 容器={target_vessel}") evaporate_action = { "device_id": rotavap_device, "action_name": "evaporate", "action_kwargs": { - "vessel": rotavap_vessel, + "vessel": target_vessel, "pressure": pressure, "temp": temp, "time": time, @@ -278,47 +250,12 @@ def generate_evaporate_protocol( "action_kwargs": {"time": 30} }) - # 动作3: 回收溶剂(如果有回收容器) - if recovery_vessel: - debug_print(f"回收溶剂到 {recovery_vessel}") - try: - recovery_volume = transfer_volume * 0.7 # 估算回收70% - recovery_actions = generate_pump_protocol( - G=G, - from_vessel="rotavap_condenser", # 假设的冷凝器 - to_vessel=recovery_vessel, - volume=recovery_volume, - flowrate=3.0, - transfer_flowrate=3.0 - ) - 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=vessel, - volume=concentrate_volume, - flowrate=1.0, # 浓缩物可能粘稠 - transfer_flowrate=1.0 - ) - action_sequence.extend(transfer_back_actions) - debug_print(f"添加了 {len(transfer_back_actions)} 个转移回收动作") - except Exception as e: - debug_print(f"浓缩物转移失败: {str(e)}") - # === 总结 === debug_print("=" * 50) debug_print(f"蒸发协议生成完成") debug_print(f"总动作数: {len(action_sequence)}") - debug_print(f"处理体积: {transfer_volume}mL") + debug_print(f"旋转蒸发仪: {rotavap_device}") + debug_print(f"目标容器: {target_vessel}") debug_print(f"蒸发参数: {pressure} bar, {temp}°C, {time}s, {stir_speed} RPM") debug_print("=" * 50) diff --git a/unilabos/compile/filter_protocol.py b/unilabos/compile/filter_protocol.py index e673f2291..662968480 100644 --- a/unilabos/compile/filter_protocol.py +++ b/unilabos/compile/filter_protocol.py @@ -1,8 +1,7 @@ -from typing import List, Dict, Any +from typing import List, Dict, Any, Optional import networkx as nx -from .pump_protocol import generate_pump_protocol import logging -import sys +from .pump_protocol import generate_pump_protocol_with_rinsing logger = logging.getLogger(__name__) @@ -11,124 +10,64 @@ 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 - - debug_print(f"未检测到液体体积,返回 0.0") - return 0.0 - 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) + if 'filter' in node_class.lower() or 'filter' in node.lower(): debug_print(f"找到过滤器设备: {node}") + return node - if filter_devices: - return filter_devices[0] + # 如果没找到,寻找可能的过滤器名称 + possible_names = ["filter", "filter_1", "virtual_filter", "filtration_unit"] + for name in possible_names: + if name in G.nodes(): + debug_print(f"找到过滤器设备: {name}") + return name - debug_print("未找到过滤器设备,使用默认设备") - return "filter_1" # 默认设备 + raise ValueError("未找到过滤器设备") -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 = [ - "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 - - debug_print("未找到滤液收集容器,使用默认容器") - return "filtrate_vessel" # 默认容器 +def validate_vessel(G: nx.DiGraph, vessel: str, vessel_type: str = "容器") -> None: + """验证容器是否存在""" + if not vessel: + raise ValueError(f"{vessel_type}不能为空") + + if vessel not in G.nodes(): + raise ValueError(f"{vessel_type} '{vessel}' 不存在于系统中") + + debug_print(f"✅ {vessel_type} '{vessel}' 验证通过") def generate_filter_protocol( G: nx.DiGraph, vessel: str, filtrate_vessel: str = "", - **kwargs # 🔧 接受额外参数,增强兼容性 + **kwargs ) -> List[Dict[str, Any]]: """ - 生成过滤操作的协议序列 - 简化版本 + 生成过滤操作的协议序列 Args: G: 设备图 - vessel: 过滤容器名称(必需) - filtrate_vessel: 滤液容器名称(可选,自动查找) + vessel: 过滤容器名称(必需)- 包含需要过滤的混合物 + filtrate_vessel: 滤液容器名称(可选)- 如果提供则收集滤液 **kwargs: 其他参数(兼容性) Returns: List[Dict[str, Any]]: 过滤操作的动作序列 """ - debug_print("=" * 50) + debug_print("=" * 60) debug_print("开始生成过滤协议") debug_print(f"输入参数:") debug_print(f" - vessel: {vessel}") debug_print(f" - filtrate_vessel: {filtrate_vessel}") debug_print(f" - 其他参数: {kwargs}") - debug_print("=" * 50) + debug_print("=" * 60) action_sequence = [] @@ -136,59 +75,83 @@ def generate_filter_protocol( debug_print("步骤1: 参数验证...") # 验证必需参数 - if not vessel: - raise ValueError("vessel 参数不能为空") - - if vessel not in G.nodes(): - raise ValueError(f"容器 '{vessel}' 不存在于系统中") + validate_vessel(G, vessel, "过滤容器") - debug_print(f"✅ 参数验证通过") + # 验证可选参数 + if filtrate_vessel: + validate_vessel(G, filtrate_vessel, "滤液容器") + debug_print("模式: 过滤并收集滤液") + else: + debug_print("模式: 过滤并收集固体") # === 查找设备 === debug_print("步骤2: 查找设备...") try: filter_device = find_filter_device(G) - actual_filtrate_vessel = find_filtrate_vessel(G, filtrate_vessel) - - debug_print(f"设备配置:") - debug_print(f" - 过滤器设备: {filter_device}") - debug_print(f" - 滤液收集容器: {actual_filtrate_vessel}") + debug_print(f"使用过滤器设备: {filter_device}") except Exception as e: debug_print(f"❌ 设备查找失败: {str(e)}") raise ValueError(f"设备查找失败: {str(e)}") - # === 体积检测 === - debug_print("步骤3: 体积检测...") - - source_volume = get_vessel_liquid_volume(G, vessel) + # === 转移到过滤器(如果需要)=== + debug_print("步骤3: 转移到过滤器...") - if source_volume > 0: - transfer_volume = source_volume - debug_print(f"检测到液体体积: {transfer_volume}mL") + if vessel != filter_device: + debug_print(f"需要转移: {vessel} → {filter_device}") + + try: + # 使用pump protocol转移液体到过滤器 + transfer_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=vessel, + to_vessel=filter_device, + volume=0.0, # 转移所有液体 + amount="", + time=0.0, + viscous=False, + rinsing_solvent="", + rinsing_volume=0.0, + rinsing_repeats=0, + solid=False, + flowrate=2.0, + transfer_flowrate=2.0 + ) + + if transfer_actions: + action_sequence.extend(transfer_actions) + debug_print(f"✅ 添加了 {len(transfer_actions)} 个转移动作") + else: + debug_print("⚠️ 转移协议返回空序列") + + except Exception as e: + debug_print(f"❌ 转移失败: {str(e)}") + # 继续执行,可能是直接连接的过滤器 else: - transfer_volume = 50.0 # 默认体积 - debug_print(f"未检测到液体体积,使用默认值: {transfer_volume}mL") + debug_print("过滤容器就是过滤器,无需转移") # === 执行过滤操作 === debug_print("步骤4: 执行过滤操作...") - # 过滤动作(直接调用过滤器) - debug_print(f"执行过滤: {vessel} -> {actual_filtrate_vessel}") + # 构建过滤动作参数 + filter_kwargs = { + "vessel": filter_device, # 过滤器设备 + "filtrate_vessel": filtrate_vessel, # 滤液容器(可能为空) + "stir": kwargs.get("stir", False), + "stir_speed": kwargs.get("stir_speed", 0.0), + "temp": kwargs.get("temp", 25.0), + "continue_heatchill": kwargs.get("continue_heatchill", False), + "volume": kwargs.get("volume", 0.0) # 0表示过滤所有 + } + + debug_print(f"过滤参数: {filter_kwargs}") + # 过滤动作 filter_action = { "device_id": filter_device, "action_name": "filter", - "action_kwargs": { - "vessel": vessel, - "filtrate_vessel": actual_filtrate_vessel, - "stir": False, # 🔧 使用默认值 - "stir_speed": 0.0, # 🔧 使用默认值 - "temp": 25.0, # 🔧 使用默认值 - "continue_heatchill": False, # 🔧 使用默认值 - "volume": transfer_volume # 🔧 使用检测到的体积 - } + "action_kwargs": filter_kwargs } action_sequence.append(filter_action) @@ -198,22 +161,55 @@ def generate_filter_protocol( "action_kwargs": {"time": 10.0} }) + # === 收集滤液(如果需要)=== + debug_print("步骤5: 收集滤液...") + + if filtrate_vessel: + debug_print(f"收集滤液: {filter_device} → {filtrate_vessel}") + + try: + # 使用pump protocol收集滤液 + collect_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=filter_device, + to_vessel=filtrate_vessel, + volume=0.0, # 收集所有滤液 + amount="", + time=0.0, + viscous=False, + rinsing_solvent="", + rinsing_volume=0.0, + rinsing_repeats=0, + solid=False, + flowrate=2.0, + transfer_flowrate=2.0 + ) + + if collect_actions: + action_sequence.extend(collect_actions) + debug_print(f"✅ 添加了 {len(collect_actions)} 个收集动作") + else: + debug_print("⚠️ 收集协议返回空序列") + + except Exception as e: + debug_print(f"❌ 收集滤液失败: {str(e)}") + # 继续执行,可能滤液直接流入指定容器 + else: + debug_print("未指定滤液容器,固体保留在过滤器中") + + # === 最终等待 === + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 5.0} + }) + # === 总结 === - debug_print("=" * 50) + debug_print("=" * 60) 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) + debug_print(f"过滤器设备: {filter_device}") + debug_print(f"滤液容器: {filtrate_vessel or '无(保留固体)'}") + debug_print("=" * 60) return action_sequence - -# 测试函数 -def test_filter_protocol(): - """测试过滤协议""" - debug_print("=== FILTER PROTOCOL 测试 ===") - debug_print("✅ 测试完成") - -if __name__ == "__main__": - test_filter_protocol() \ No newline at end of file diff --git a/unilabos/compile/pump_protocol.py b/unilabos/compile/pump_protocol.py index acdb52c6d..f214ff484 100644 --- a/unilabos/compile/pump_protocol.py +++ b/unilabos/compile/pump_protocol.py @@ -98,24 +98,112 @@ 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 "" - if "pump" in node_class: - return neighbor - raise ValueError(f"未找到与阀 {valve_node} 唯一相连的泵节点") + """ + 查找与阀门相连的泵节点 - 修复版本 + 🔧 修复:区分电磁阀和多通阀,电磁阀不参与泵查找 + """ + debug_print(f"🔍 查找与阀门 {valve_node} 相连的泵...") + + # 🔧 关键修复:检查节点类型,电磁阀不应该查找泵 + node_data = G.nodes.get(valve_node, {}) + node_class = node_data.get("class", "") or "" + + debug_print(f" - 阀门类型: {node_class}") + + # 如果是电磁阀,不应该查找泵(电磁阀只是开关) + if ("solenoid" in node_class.lower() or "solenoid_valve" in valve_node.lower()): + debug_print(f" ⚠️ {valve_node} 是电磁阀,不应该查找泵节点") + raise ValueError(f"电磁阀 {valve_node} 不应该参与泵查找逻辑") + + # 只有多通阀等复杂阀门才需要查找连接的泵 + if ("multiway" in node_class.lower() or "valve" in node_class.lower()): + debug_print(f" - {valve_node} 是多通阀,查找连接的泵...") + + # 方法1:直接相邻的泵 + for neighbor in G.neighbors(valve_node): + neighbor_class = G.nodes[neighbor].get("class", "") or "" + debug_print(f" - 检查邻居 {neighbor}, class: {neighbor_class}") + if "pump" in neighbor_class.lower(): + debug_print(f" ✅ 找到直接相连的泵: {neighbor}") + return neighbor + + # 方法2:通过路径查找泵(最多2跳) + debug_print(f" - 未找到直接相连的泵,尝试路径查找...") + + # 获取所有泵节点 + pump_nodes = [] + for node_id in G.nodes(): + node_class = G.nodes[node_id].get("class", "") or "" + if "pump" in node_class.lower(): + pump_nodes.append(node_id) + + debug_print(f" - 系统中的泵节点: {pump_nodes}") + + # 查找到泵的最短路径 + for pump_node in pump_nodes: + try: + if nx.has_path(G, valve_node, pump_node): + path = nx.shortest_path(G, valve_node, pump_node) + path_length = len(path) - 1 + debug_print(f" - 到泵 {pump_node} 的路径: {path}, 距离: {path_length}") + + if path_length <= 2: # 最多允许2跳 + debug_print(f" ✅ 通过路径找到泵: {pump_node}") + return pump_node + except nx.NetworkXNoPath: + continue + + # 方法3:降级方案 - 返回第一个可用的泵 + if pump_nodes: + debug_print(f" ⚠️ 未找到连接的泵,使用第一个可用的泵: {pump_nodes[0]}") + return pump_nodes[0] + + # 最终失败 + debug_print(f" ❌ 完全找不到泵节点") + raise ValueError(f"未找到与阀 {valve_node} 相连的泵节点") def build_pump_valve_maps(G, pump_backbone): + """ + 构建泵-阀门映射 - 修复版本 + 🔧 修复:过滤掉电磁阀,只处理需要泵的多通阀 + """ pumps_from_node = {} valve_from_node = {} + + debug_print(f"🔧 构建泵-阀门映射,原始骨架: {pump_backbone}") + + # 🔧 关键修复:过滤掉电磁阀 + filtered_backbone = [] for node in pump_backbone: + node_data = G.nodes.get(node, {}) + node_class = node_data.get("class", "") or "" + + # 跳过电磁阀 + if ("solenoid" in node_class.lower() or "solenoid_valve" in node.lower()): + debug_print(f" - 跳过电磁阀: {node}") + continue + + filtered_backbone.append(node) + + debug_print(f"🔧 过滤后的骨架: {filtered_backbone}") + + for node in filtered_backbone: if is_integrated_pump(node): pumps_from_node[node] = node valve_from_node[node] = node + debug_print(f" - 集成泵-阀: {node}") else: - pump_node = find_connected_pump(G, node) - pumps_from_node[node] = pump_node - valve_from_node[node] = node + try: + pump_node = find_connected_pump(G, node) + pumps_from_node[node] = pump_node + valve_from_node[node] = node + debug_print(f" - 阀门 {node} -> 泵 {pump_node}") + except ValueError as e: + debug_print(f" - 跳过节点 {node}: {str(e)}") + continue + + debug_print(f"🔧 最终映射: pumps={pumps_from_node}, valves={valve_from_node}") return pumps_from_node, valve_from_node @@ -128,7 +216,8 @@ def generate_pump_protocol( transfer_flowrate: float = 0.5, ) -> List[Dict[str, Any]]: """ - 生成泵操作的动作序列 + 生成泵操作的动作序列 - 修复版本 + 🔧 修复:正确处理包含电磁阀的路径 """ pump_action_sequence = [] nodes = G.nodes(data=True) @@ -162,25 +251,63 @@ def generate_pump_protocol( logger.error(f"无法找到从 '{from_vessel}' 到 '{to_vessel}' 的路径") return pump_action_sequence - pump_backbone = shortest_path - if not from_vessel.startswith("pump"): - pump_backbone = pump_backbone[1:] - if not to_vessel.startswith("pump"): - pump_backbone = pump_backbone[:-1] + # 🔧 关键修复:正确构建泵骨架,排除容器和电磁阀 + pump_backbone = [] + for node in shortest_path: + # 跳过起始和结束容器 + if node == from_vessel or node == to_vessel: + continue + + # 跳过电磁阀(电磁阀不参与泵操作) + node_data = G.nodes.get(node, {}) + node_class = node_data.get("class", "") or "" + if ("solenoid" in node_class.lower() or "solenoid_valve" in node.lower()): + debug_print(f"PUMP_TRANSFER: 跳过电磁阀 {node}") + continue + + # 只包含多通阀和泵 + if ("multiway" in node_class.lower() or "valve" in node_class.lower() or "pump" in node_class.lower()): + pump_backbone.append(node) + + debug_print(f"PUMP_TRANSFER: 过滤后的泵骨架: {pump_backbone}") if not pump_backbone: - debug_print("没有泵骨架节点,可能是直接容器连接") + debug_print("PUMP_TRANSFER: 没有泵骨架节点,可能是直接容器连接或只有电磁阀") + # 🔧 对于气体传输,这是正常的,直接返回空序列 return pump_action_sequence if transfer_flowrate == 0: transfer_flowrate = flowrate - pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone) + try: + pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone) + except Exception as e: + debug_print(f"PUMP_TRANSFER: 构建泵-阀门映射失败: {str(e)}") + return pump_action_sequence + + if not pumps_from_node: + debug_print("PUMP_TRANSFER: 没有可用的泵映射") + return pump_action_sequence - # 获取最小转移体积 + # 🔧 修复:安全地获取最小转移体积 try: - min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone]) - except (KeyError, TypeError): + min_transfer_volumes = [] + for node in pump_backbone: + if node in pumps_from_node: + pump_node = pumps_from_node[node] + if pump_node in nodes: + pump_config = nodes[pump_node].get("config", {}) + max_volume = pump_config.get("max_volume") + if max_volume is not None: + min_transfer_volumes.append(max_volume) + + if min_transfer_volumes: + min_transfer_volume = min(min_transfer_volumes) + else: + min_transfer_volume = 25.0 # 默认值 + debug_print(f"PUMP_TRANSFER: 无法获取泵的最大体积,使用默认值: {min_transfer_volume}mL") + except Exception as e: + debug_print(f"PUMP_TRANSFER: 获取最小转移体积失败: {str(e)}") min_transfer_volume = 25.0 # 默认值 repeats = int(np.ceil(volume / min_transfer_volume)) @@ -196,85 +323,108 @@ def generate_pump_protocol( for i in range(repeats): current_volume = min(volume_left, min_transfer_volume) + # 🔧 修复:安全地获取边数据 + def get_safe_edge_data(node_a, node_b, key): + try: + edge_data = G.get_edge_data(node_a, node_b) + if edge_data and "port" in edge_data: + port_data = edge_data["port"] + if isinstance(port_data, dict) and key in port_data: + return port_data[key] + return "default" + except Exception as e: + debug_print(f"PUMP_TRANSFER: 获取边数据失败 {node_a}->{node_b}: {str(e)}") + return "default" + # 从源容器吸液 - if not from_vessel.startswith("pump"): - pump_action_sequence.extend([ - { - "device_id": valve_from_node[pump_backbone[0]], - "action_name": "set_valve_position", - "action_kwargs": { - "command": G.get_edge_data(pump_backbone[0], from_vessel)["port"][pump_backbone[0]] - } - }, - { - "device_id": pumps_from_node[pump_backbone[0]], - "action_name": "set_position", - "action_kwargs": { - "position": float(current_volume), - "max_velocity": transfer_flowrate + if not from_vessel.startswith("pump") and pump_backbone: + first_pump_node = pump_backbone[0] + if first_pump_node in valve_from_node and first_pump_node in pumps_from_node: + port_command = get_safe_edge_data(first_pump_node, from_vessel, first_pump_node) + pump_action_sequence.extend([ + { + "device_id": valve_from_node[first_pump_node], + "action_name": "set_valve_position", + "action_kwargs": { + "command": port_command + } + }, + { + "device_id": pumps_from_node[first_pump_node], + "action_name": "set_position", + "action_kwargs": { + "position": float(current_volume), + "max_velocity": transfer_flowrate + } } - } - ]) - pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}}) + ]) + 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], - "action_name": "set_valve_position", - "action_kwargs": { - "command": G.get_edge_data(nodeA, nodeB)["port"][nodeA] - } - }, - { - "device_id": valve_from_node[nodeB], - "action_name": "set_valve_position", - "action_kwargs": { - "command": G.get_edge_data(nodeB, nodeA)["port"][nodeB], - } - } - ]) - pump_action_sequence.append([ - { - "device_id": pumps_from_node[nodeA], - "action_name": "set_position", - "action_kwargs": { - "position": 0.0, - "max_velocity": transfer_flowrate + if nodeA in valve_from_node and nodeB in valve_from_node and nodeA in pumps_from_node and nodeB in pumps_from_node: + port_a = get_safe_edge_data(nodeA, nodeB, nodeA) + port_b = get_safe_edge_data(nodeB, nodeA, nodeB) + + pump_action_sequence.append([ + { + "device_id": valve_from_node[nodeA], + "action_name": "set_valve_position", + "action_kwargs": { + "command": port_a + } + }, + { + "device_id": valve_from_node[nodeB], + "action_name": "set_valve_position", + "action_kwargs": { + "command": port_b + } } - }, - { - "device_id": pumps_from_node[nodeB], - "action_name": "set_position", - "action_kwargs": { - "position": float(current_volume), - "max_velocity": transfer_flowrate + ]) + pump_action_sequence.append([ + { + "device_id": pumps_from_node[nodeA], + "action_name": "set_position", + "action_kwargs": { + "position": 0.0, + "max_velocity": transfer_flowrate + } + }, + { + "device_id": pumps_from_node[nodeB], + "action_name": "set_position", + "action_kwargs": { + "position": float(current_volume), + "max_velocity": transfer_flowrate + } } - } - ]) - pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}}) + ]) + pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}}) # 排液到目标容器 - if not to_vessel.startswith("pump"): - pump_action_sequence.extend([ - { - "device_id": valve_from_node[pump_backbone[-1]], - "action_name": "set_valve_position", - "action_kwargs": { - "command": G.get_edge_data(pump_backbone[-1], to_vessel)["port"][pump_backbone[-1]] - } - }, - { - "device_id": pumps_from_node[pump_backbone[-1]], - "action_name": "set_position", - "action_kwargs": { - "position": 0.0, - "max_velocity": flowrate + if not to_vessel.startswith("pump") and pump_backbone: + last_pump_node = pump_backbone[-1] + if last_pump_node in valve_from_node and last_pump_node in pumps_from_node: + port_command = get_safe_edge_data(last_pump_node, to_vessel, last_pump_node) + pump_action_sequence.extend([ + { + "device_id": valve_from_node[last_pump_node], + "action_name": "set_valve_position", + "action_kwargs": { + "command": port_command + } + }, + { + "device_id": pumps_from_node[last_pump_node], + "action_name": "set_position", + "action_kwargs": { + "position": 0.0, + "max_velocity": flowrate + } } - } - ]) - pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}}) + ]) + pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}}) volume_left -= current_volume @@ -287,7 +437,7 @@ def generate_pump_protocol_with_rinsing( to_vessel: str, volume: float = 0.0, amount: str = "", - duration: float = 0.0, # 🔧 重命名参数,避免冲突 + time: float = 0.0, # 🔧 修复:统一使用 time viscous: bool = False, rinsing_solvent: str = "", rinsing_volume: float = 0.0, @@ -306,11 +456,11 @@ def generate_pump_protocol_with_rinsing( debug_print("=" * 60) debug_print(f"PUMP_TRANSFER: 🚀 开始生成协议") debug_print(f" 📍 路径: {from_vessel} -> {to_vessel}") - debug_print(f" 🕐 时间戳: {time_module.time()}") # 🔧 使用重命名的模块 + 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" - time: {time}") # 🔧 修复:统一使用 time debug_print(f" - flowrate: {flowrate}") debug_print(f" - transfer_flowrate: {transfer_flowrate}") debug_print(f" - rate_spec: '{rate_spec}'") @@ -382,9 +532,9 @@ def generate_pump_protocol_with_rinsing( 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 + if time > 0 and final_volume > 0: # 🔧 修复:统一使用 time debug_print(f"🔍 步骤4: 根据时间计算流速...") - calculated_flowrate = final_volume / duration + calculated_flowrate = final_volume / time debug_print(f" - 计算得到流速: {calculated_flowrate}mL/s") if flowrate <= 0 or flowrate == 2.5: @@ -412,31 +562,31 @@ def generate_pump_protocol_with_rinsing( 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 + # # 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 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") + # # 根据物理属性调整冲洗参数 + # 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(f" - 冲洗溶剂: '{final_rinsing_solvent}'") + # debug_print(f" - 冲洗体积: {final_rinsing_volume}mL") + # debug_print(f" - 冲洗次数: {final_rinsing_repeats}次") # ========== 执行基础转移 ========== @@ -503,36 +653,36 @@ def generate_pump_protocol_with_rinsing( # ========== 执行冲洗操作 ========== - debug_print("🔧 步骤8: 检查冲洗操作...") + # debug_print("🔧 步骤8: 检查冲洗操作...") - if final_rinsing_solvent and final_rinsing_solvent.strip() and final_rinsing_repeats > 0: - debug_print(f"🧽 开始冲洗操作,溶剂: '{final_rinsing_solvent}'") + # 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)}") + # 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)}") # ========== 最终结果 ========== @@ -742,34 +892,22 @@ def generate_pump_protocol_with_rinsing( 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生成逻辑 - # 我先提供一个示例,您需要替换为实际的生成逻辑 + # # 5. 处理冲洗参数 + # debug_print(f"🔍 步骤6: 处理冲洗参数...") + # final_rinsing_solvent = rinsing_solvent + # final_rinsing_volume = rinsing_volume if rinsing_volume > 0 else 5.0 + # final_rinsing_repeats = rinsing_repeats if rinsing_repeats > 0 else 2 + + # if rinsing_volume <= 0: + # logger.warning(f"⚠️ rinsing_volume <= 0,修正为: {final_rinsing_volume}mL") + # if rinsing_repeats <= 0: + # logger.warning(f"⚠️ rinsing_repeats <= 0,修正为: {final_rinsing_repeats}次") + + # # 根据物理属性调整冲洗参数 + # if viscous or solid: + # final_rinsing_repeats = max(final_rinsing_repeats, 3) + # final_rinsing_volume = max(final_rinsing_volume, 10.0) + # debug_print(f"🧪 粘稠/固体物质,调整冲洗参数:{final_rinsing_repeats}次,{final_rinsing_volume}mL") try: pump_action_sequence = generate_pump_protocol( diff --git a/unilabos/compile/run_column_protocol.py b/unilabos/compile/run_column_protocol.py index f6b9214fc..c60e24093 100644 --- a/unilabos/compile/run_column_protocol.py +++ b/unilabos/compile/run_column_protocol.py @@ -1,312 +1,668 @@ -from typing import List, Dict, Any +from typing import List, Dict, Any, Union import networkx as nx -from .pump_protocol import generate_pump_protocol +import logging +import re +from .pump_protocol import generate_pump_protocol_with_rinsing +logger = logging.getLogger(__name__) -def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float: - """获取容器中的液体体积""" - if vessel not in G.nodes(): +def debug_print(message): + """调试输出""" + print(f"[RUN_COLUMN] {message}", flush=True) + logger.info(f"[RUN_COLUMN] {message}") + +def parse_percentage(pct_str: str) -> float: + """ + 解析百分比字符串为数值 + + Args: + pct_str: 百分比字符串(如 "40 %", "40%", "40") + + Returns: + float: 百分比数值(0-100) + """ + if not pct_str or not pct_str.strip(): return 0.0 - vessel_data = G.nodes[vessel].get('data', {}) - liquids = vessel_data.get('liquid', []) + pct_str = pct_str.strip().lower() + debug_print(f"解析百分比: '{pct_str}'") - total_volume = 0.0 - for liquid in liquids: - if isinstance(liquid, dict): - # 支持两种格式:新格式 (name, volume) 和旧格式 (liquid_type, liquid_volume) - volume = liquid.get('volume') or liquid.get('liquid_volume', 0.0) - total_volume += volume + # 移除百分号和空格 + pct_clean = re.sub(r'[%\s]', '', pct_str) - return total_volume + # 提取数字 + match = re.search(r'([0-9]*\.?[0-9]+)', pct_clean) + if match: + value = float(match.group(1)) + debug_print(f"百分比解析结果: {value}%") + return value + + debug_print(f"⚠️ 无法解析百分比: '{pct_str}',返回0.0") + return 0.0 +def parse_ratio(ratio_str: str) -> tuple: + """ + 解析比例字符串为两个数值 + + Args: + ratio_str: 比例字符串(如 "5:95", "1:1", "40:60") + + Returns: + tuple: (ratio1, ratio2) 两个比例值 + """ + if not ratio_str or not ratio_str.strip(): + return (50.0, 50.0) # 默认1:1 + + ratio_str = ratio_str.strip() + debug_print(f"解析比例: '{ratio_str}'") + + # 支持多种分隔符:: / - + if ':' in ratio_str: + parts = ratio_str.split(':') + elif '/' in ratio_str: + parts = ratio_str.split('/') + elif '-' in ratio_str: + parts = ratio_str.split('-') + elif 'to' in ratio_str.lower(): + parts = ratio_str.lower().split('to') + else: + debug_print(f"⚠️ 无法解析比例格式: '{ratio_str}',使用默认1:1") + return (50.0, 50.0) + + if len(parts) >= 2: + try: + ratio1 = float(parts[0].strip()) + ratio2 = float(parts[1].strip()) + total = ratio1 + ratio2 + + # 转换为百分比 + pct1 = (ratio1 / total) * 100 + pct2 = (ratio2 / total) * 100 + + debug_print(f"比例解析结果: {ratio1}:{ratio2} -> {pct1:.1f}%:{pct2:.1f}%") + return (pct1, pct2) + except ValueError as e: + debug_print(f"⚠️ 比例数值转换失败: {str(e)}") + + debug_print(f"⚠️ 比例解析失败,使用默认1:1") + return (50.0, 50.0) -def find_column_device(G: nx.DiGraph, column: str) -> str: - """查找柱层析设备""" - # 首先检查是否有虚拟柱设备 - column_nodes = [node for node in G.nodes() - if (G.nodes[node].get('class') or '') == 'virtual_column'] +def parse_rf_value(rf_str: str) -> float: + """ + 解析Rf值字符串 - if column_nodes: - return column_nodes[0] + Args: + rf_str: Rf值字符串(如 "0.3", "0.45", "?") + + Returns: + float: Rf值(0-1) + """ + if not rf_str or not rf_str.strip(): + return 0.3 # 默认Rf值 + + rf_str = rf_str.strip().lower() + debug_print(f"解析Rf值: '{rf_str}'") - # 如果没有虚拟柱设备,抛出异常 - raise ValueError(f"系统中未找到柱层析设备。请确保配置了 virtual_column 设备") + # 处理未知Rf值 + if rf_str in ['?', 'unknown', 'tbd', 'to be determined']: + default_rf = 0.3 + debug_print(f"检测到未知Rf值,使用默认值: {default_rf}") + return default_rf + + # 提取数字 + match = re.search(r'([0-9]*\.?[0-9]+)', rf_str) + if match: + value = float(match.group(1)) + # 确保Rf值在0-1范围内 + if value > 1.0: + value = value / 100.0 # 可能是百分比形式 + value = max(0.0, min(1.0, value)) # 限制在0-1范围 + debug_print(f"Rf值解析结果: {value}") + return value + + debug_print(f"⚠️ 无法解析Rf值: '{rf_str}',使用默认值0.3") + return 0.3 +def find_column_device(G: nx.DiGraph) -> str: + """查找柱层析设备""" + debug_print("查找柱层析设备...") + + # 查找虚拟柱设备 + for node in G.nodes(): + node_data = G.nodes[node] + node_class = node_data.get('class', '') or '' + + if 'virtual_column' in node_class.lower() or 'column' in node_class.lower(): + debug_print(f"✅ 找到柱层析设备: {node}") + return node + + # 如果没有找到,尝试创建虚拟设备名称 + possible_names = ['column_1', 'virtual_column_1', 'chromatography_column_1'] + for name in possible_names: + if name in G.nodes(): + debug_print(f"✅ 找到柱设备: {name}") + return name + + debug_print("⚠️ 未找到柱层析设备,将使用pump protocol直接转移") + return "" def find_column_vessel(G: nx.DiGraph, column: str) -> str: """查找柱容器""" - # 直接使用 column 参数作为容器名称 + debug_print(f"查找柱容器: '{column}'") + + # 直接检查column参数是否是容器 if column in G.nodes(): - return column + node_type = G.nodes[column].get('type', '') + if node_type == 'container': + debug_print(f"✅ 找到柱容器: {column}") + return column - # 尝试常见的柱容器命名规则 + # 尝试常见的命名规则 possible_names = [ f"column_{column}", - f"{column}_column", + f"{column}_column", f"vessel_{column}", f"{column}_vessel", "column_vessel", "chromatography_column", "silica_column", - "preparative_column" + "preparative_column", + "column" ] for vessel_name in possible_names: if vessel_name in G.nodes(): - return vessel_name + node_type = G.nodes[vessel_name].get('type', '') + if node_type == 'container': + debug_print(f"✅ 找到柱容器: {vessel_name}") + return vessel_name - raise ValueError(f"未找到柱容器 '{column}'。尝试了以下名称: {[column] + possible_names}") - + debug_print(f"⚠️ 未找到柱容器,将直接在源容器中进行分离") + return "" -def find_eluting_solvent_vessel(G: nx.DiGraph, eluting_solvent: str) -> str: - """查找洗脱溶剂容器""" - if not eluting_solvent: +def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: + """查找溶剂容器 - 增强版""" + if not solvent or not solvent.strip(): return "" - # 按照命名规则查找溶剂瓶 - solvent_vessel_id = f"flask_{eluting_solvent}" + solvent = solvent.strip().replace(' ', '_').lower() + debug_print(f"查找溶剂容器: '{solvent}'") + + # 🔧 方法1:直接搜索 data.reagent_name + for node in G.nodes(): + node_data = G.nodes[node].get('data', {}) + node_type = G.nodes[node].get('type', '') + + # 只搜索容器类型的节点 + if node_type == 'container': + reagent_name = node_data.get('reagent_name', '').lower() + reagent_config = G.nodes[node].get('config', {}).get('reagent', '').lower() + + # 检查 data.reagent_name 和 config.reagent + if reagent_name == solvent or reagent_config == solvent: + debug_print(f"✅ 通过reagent_name找到溶剂容器: {node} (reagent: {reagent_name or reagent_config})") + return node + + # 模糊匹配 reagent_name + if solvent in reagent_name or reagent_name in solvent: + debug_print(f"✅ 通过reagent_name模糊匹配到溶剂容器: {node} (reagent: {reagent_name})") + return node + + if solvent in reagent_config or reagent_config in solvent: + debug_print(f"✅ 通过config.reagent模糊匹配到溶剂容器: {node} (reagent: {reagent_config})") + return node + + # 🔧 方法2:常见的溶剂容器命名规则 + possible_names = [ + f"flask_{solvent}", + f"bottle_{solvent}", + f"reagent_{solvent}", + f"{solvent}_bottle", + f"{solvent}_flask", + f"solvent_{solvent}", + f"reagent_bottle_{solvent}" + ] - if solvent_vessel_id in G.nodes(): - return solvent_vessel_id + for vessel_name in possible_names: + if vessel_name in G.nodes(): + node_type = G.nodes[vessel_name].get('type', '') + if node_type == 'container': + debug_print(f"✅ 通过命名规则找到溶剂容器: {vessel_name}") + return vessel_name - # 如果直接匹配失败,尝试模糊匹配 + # 🔧 方法3:节点名称模糊匹配 for node in G.nodes(): - if node.startswith('flask_') and eluting_solvent.lower() in node.lower(): - return node + node_type = G.nodes[node].get('type', '') + if node_type == 'container': + if ('flask_' in node or 'bottle_' in node or 'reagent_' in node) and solvent in node.lower(): + debug_print(f"✅ 通过节点名称模糊匹配到溶剂容器: {node}") + return node - # 如果还是找不到,列出所有可用的溶剂瓶 - available_flasks = [node for node in G.nodes() - if node.startswith('flask_') - and G.nodes[node].get('type') == 'container'] + # 🔧 方法4:特殊溶剂名称映射 + solvent_mapping = { + 'dmf': ['dmf', 'dimethylformamide', 'n,n-dimethylformamide'], + 'ethyl_acetate': ['ethyl_acetate', 'ethylacetate', 'etoac', 'ea'], + 'hexane': ['hexane', 'hexanes', 'n-hexane'], + 'methanol': ['methanol', 'meoh', 'ch3oh'], + 'water': ['water', 'h2o', 'distilled_water'], + 'acetone': ['acetone', 'ch3coch3', '2-propanone'], + 'dichloromethane': ['dichloromethane', 'dcm', 'ch2cl2', 'methylene_chloride'], + 'chloroform': ['chloroform', 'chcl3', 'trichloromethane'] + } - raise ValueError(f"找不到洗脱溶剂 '{eluting_solvent}' 对应的溶剂瓶。可用溶剂瓶: {available_flasks}") + # 查找映射的同义词 + for canonical_name, synonyms in solvent_mapping.items(): + if solvent in synonyms: + debug_print(f"检测到溶剂同义词: '{solvent}' -> '{canonical_name}'") + return find_solvent_vessel(G, canonical_name) # 递归搜索 + + debug_print(f"⚠️ 未找到溶剂 '{solvent}' 的容器") + return "" +def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float: + """获取容器中的液体体积 - 增强版""" + if vessel not in G.nodes(): + debug_print(f"⚠️ 节点 '{vessel}' 不存在") + return 0.0 + + node_type = G.nodes[vessel].get('type', '') + vessel_data = G.nodes[vessel].get('data', {}) + + debug_print(f"读取节点 '{vessel}' (类型: {node_type}) 体积数据: {vessel_data}") + + # 🔧 如果是设备类型,尝试查找关联的容器 + if node_type == 'device': + debug_print(f"'{vessel}' 是设备,尝试查找关联容器...") + + # 查找是否有内置容器数据 + config_data = G.nodes[vessel].get('config', {}) + if 'volume' in config_data: + default_volume = config_data.get('volume', 100.0) + debug_print(f"使用设备默认容量: {default_volume}mL") + return default_volume + + # 对于旋蒸等设备,使用默认值 + if 'rotavap' in vessel.lower(): + default_volume = 100.0 + debug_print(f"旋蒸设备使用默认容量: {default_volume}mL") + return default_volume + + debug_print(f"⚠️ 设备 '{vessel}' 无法确定容量,返回0") + return 0.0 + + # 🔧 如果是容器类型,正常读取体积 + total_volume = 0.0 + + # 方法1:检查液体列表 + liquids = vessel_data.get('liquid', []) + if isinstance(liquids, list): + for liquid in liquids: + if isinstance(liquid, dict): + volume = liquid.get('volume') or liquid.get('liquid_volume', 0.0) + total_volume += volume + + # 方法2:检查直接体积字段 + if total_volume == 0.0: + volume_keys = ['current_volume', 'total_volume', 'volume', 'liquid_volume'] + for key in volume_keys: + if key in vessel_data: + try: + total_volume = float(vessel_data[key]) + if total_volume > 0: + break + except (ValueError, TypeError): + continue + + # 方法3:检查配置中的初始体积 + if total_volume == 0.0: + config_data = G.nodes[vessel].get('config', {}) + if 'current_volume' in config_data: + try: + total_volume = float(config_data['current_volume']) + except (ValueError, TypeError): + pass + + debug_print(f"容器 '{vessel}' 总体积: {total_volume}mL") + return total_volume + +def calculate_solvent_volumes(total_volume: float, pct1: float, pct2: float) -> tuple: + """根据百分比计算溶剂体积""" + volume1 = (total_volume * pct1) / 100.0 + volume2 = (total_volume * pct2) / 100.0 + + debug_print(f"溶剂体积计算: 总体积{total_volume}mL") + debug_print(f" - 溶剂1: {pct1}% = {volume1}mL") + debug_print(f" - 溶剂2: {pct2}% = {volume2}mL") + + return (volume1, volume2) def generate_run_column_protocol( G: nx.DiGraph, from_vessel: str, to_vessel: str, - column: str + column: str, + rf: str = "", + pct1: str = "", + pct2: str = "", + solvent1: str = "", + solvent2: str = "", + ratio: str = "", + **kwargs ) -> List[Dict[str, Any]]: """ - 生成柱层析分离的协议序列 + 生成柱层析分离的协议序列 - 增强版 + + 支持新版XDL的所有参数,具有高兼容性和容错性 Args: G: 有向图,节点为设备和容器,边为流体管道 - from_vessel: 源容器的名称,即样品起始所在的容器 - to_vessel: 目标容器的名称,分离后的样品要到达的容器 - column: 所使用的柱子的名称 + from_vessel: 源容器的名称,即样品起始所在的容器(必需) + to_vessel: 目标容器的名称,分离后的样品要到达的容器(必需) + column: 所使用的柱子的名称(必需) + rf: Rf值(可选,支持 "?" 表示未知) + pct1: 第一种溶剂百分比(如 "40 %",可选) + pct2: 第二种溶剂百分比(如 "50 %",可选) + solvent1: 第一种溶剂名称(可选) + solvent2: 第二种溶剂名称(可选) + ratio: 溶剂比例(如 "5:95",可选,优先级高于pct1/pct2) + **kwargs: 其他可选参数 Returns: List[Dict[str, Any]]: 柱层析分离操作的动作序列 """ + + debug_print("=" * 60) + debug_print("开始生成柱层析协议") + debug_print(f"输入参数:") + debug_print(f" - from_vessel: '{from_vessel}'") + debug_print(f" - to_vessel: '{to_vessel}'") + debug_print(f" - column: '{column}'") + debug_print(f" - rf: '{rf}'") + debug_print(f" - pct1: '{pct1}'") + debug_print(f" - pct2: '{pct2}'") + debug_print(f" - solvent1: '{solvent1}'") + debug_print(f" - solvent2: '{solvent2}'") + debug_print(f" - ratio: '{ratio}'") + debug_print(f" - 其他参数: {kwargs}") + debug_print("=" * 60) + action_sequence = [] - print(f"RUN_COLUMN: 开始生成柱层析协议") - print(f" - 源容器: {from_vessel}") - print(f" - 目标容器: {to_vessel}") - print(f" - 柱子: {column}") + # === 参数验证 === + debug_print("步骤1: 参数验证...") + + if not from_vessel: + raise ValueError("from_vessel 参数不能为空") + if not to_vessel: + raise ValueError("to_vessel 参数不能为空") + if not column: + raise ValueError("column 参数不能为空") - # 验证源容器和目标容器存在 if from_vessel not in G.nodes(): raise ValueError(f"源容器 '{from_vessel}' 不存在于系统中") - if to_vessel not in G.nodes(): raise ValueError(f"目标容器 '{to_vessel}' 不存在于系统中") - # 查找柱层析设备 - column_device_id = None - column_nodes = [node for node in G.nodes() - if (G.nodes[node].get('class') or '') == 'virtual_column'] + debug_print("✅ 基本参数验证通过") - if column_nodes: - column_device_id = column_nodes[0] - print(f"RUN_COLUMN: 找到柱层析设备: {column_device_id}") - else: - print(f"RUN_COLUMN: 警告 - 未找到柱层析设备") + # === 参数解析 === + debug_print("步骤2: 参数解析...") - # 获取源容器中的液体体积 - source_volume = get_vessel_liquid_volume(G, from_vessel) - print(f"RUN_COLUMN: 源容器 {from_vessel} 中有 {source_volume} mL 液体") + # 解析Rf值 + final_rf = parse_rf_value(rf) + debug_print(f"最终Rf值: {final_rf}") - # === 第一步:样品转移到柱子(如果柱子是容器) === - if column in G.nodes() and G.nodes[column].get('type') == 'container': - print(f"RUN_COLUMN: 样品转移 - {source_volume} mL 从 {from_vessel} 到 {column}") + # 解析溶剂比例(ratio优先级高于pct1/pct2) + if ratio and ratio.strip(): + final_pct1, final_pct2 = parse_ratio(ratio) + debug_print(f"使用ratio参数: {final_pct1:.1f}% : {final_pct2:.1f}%") + else: + final_pct1 = parse_percentage(pct1) if pct1 else 50.0 + final_pct2 = parse_percentage(pct2) if pct2 else 50.0 - try: - sample_transfer_actions = generate_pump_protocol( - G=G, - from_vessel=from_vessel, - to_vessel=column, - volume=source_volume if source_volume > 0 else 100.0, - flowrate=2.0 - ) - action_sequence.extend(sample_transfer_actions) - except Exception as e: - print(f"RUN_COLUMN: 样品转移失败: {str(e)}") - - # === 第二步:使用柱层析设备执行分离 === - if column_device_id: - print(f"RUN_COLUMN: 使用柱层析设备执行分离") + # 如果百分比和不是100%,进行归一化 + total_pct = final_pct1 + final_pct2 + if total_pct == 0: + final_pct1, final_pct2 = 50.0, 50.0 + elif total_pct != 100.0: + final_pct1 = (final_pct1 / total_pct) * 100 + final_pct2 = (final_pct2 / total_pct) * 100 - column_separation_action = { - "device_id": column_device_id, - "action_name": "run_column", - "action_kwargs": { - "from_vessel": from_vessel, - "to_vessel": to_vessel, - "column": column - } - } - action_sequence.append(column_separation_action) - - # 等待柱层析设备完成分离 - action_sequence.append({ - "action_name": "wait", - "action_kwargs": {"time": 60} - }) + debug_print(f"使用百分比参数: {final_pct1:.1f}% : {final_pct2:.1f}%") - # === 第三步:从柱子转移到目标容器(如果需要) === - if column in G.nodes() and column != to_vessel: - print(f"RUN_COLUMN: 产物转移 - 从 {column} 到 {to_vessel}") - - try: - product_transfer_actions = generate_pump_protocol( - G=G, - from_vessel=column, - to_vessel=to_vessel, - volume=source_volume * 0.8 if source_volume > 0 else 80.0, # 假设有一些损失 - flowrate=1.5 - ) - action_sequence.extend(product_transfer_actions) - except Exception as e: - print(f"RUN_COLUMN: 产物转移失败: {str(e)}") - - print(f"RUN_COLUMN: 生成了 {len(action_sequence)} 个动作") - return action_sequence - - -# 便捷函数:常用柱层析方案 -def generate_flash_column_protocol( - G: nx.DiGraph, - from_vessel: str, - to_vessel: str, - column_material: str = "silica_gel", - mobile_phase: str = "ethyl_acetate", - mobile_phase_volume: float = 100.0 -) -> List[Dict[str, Any]]: - """快速柱层析:高流速分离""" - return generate_run_column_protocol( - G, from_vessel, to_vessel, column_material, - mobile_phase, mobile_phase_volume, 1, "", 0.0, 3.0 - ) - - -def generate_preparative_column_protocol( - G: nx.DiGraph, - from_vessel: str, - to_vessel: str, - column_material: str = "silica_gel", - equilibration_solvent: str = "hexane", - eluting_solvent: str = "ethyl_acetate", - eluting_volume: float = 50.0, - eluting_repeats: int = 3 -) -> List[Dict[str, Any]]: - """制备柱层析:带平衡和多次洗脱""" - return generate_run_column_protocol( - G, from_vessel, to_vessel, column_material, - eluting_solvent, eluting_volume, eluting_repeats, - equilibration_solvent, 30.0, 1.5 + # 设置默认溶剂(如果未指定) + final_solvent1 = solvent1.strip() if solvent1 else "ethyl_acetate" + final_solvent2 = solvent2.strip() if solvent2 else "hexane" + + debug_print(f"最终溶剂: {final_solvent1} : {final_solvent2}") + + # === 查找设备和容器 === + debug_print("步骤3: 查找设备和容器...") + + # 查找柱层析设备 + column_device_id = find_column_device(G) + + # 查找柱容器 + column_vessel = find_column_vessel(G, column) + + # 查找溶剂容器 + solvent1_vessel = find_solvent_vessel(G, final_solvent1) + solvent2_vessel = find_solvent_vessel(G, final_solvent2) + + debug_print(f"设备映射:") + debug_print(f" - 柱设备: '{column_device_id}'") + debug_print(f" - 柱容器: '{column_vessel}'") + debug_print(f" - 溶剂1容器: '{solvent1_vessel}'") + debug_print(f" - 溶剂2容器: '{solvent2_vessel}'") + + # === 获取源容器体积 === + debug_print("步骤4: 获取源容器体积...") + + source_volume = get_vessel_liquid_volume(G, from_vessel) + if source_volume <= 0: + source_volume = 100.0 # 默认体积 + debug_print(f"⚠️ 无法获取源容器体积,使用默认值: {source_volume}mL") + else: + debug_print(f"✅ 源容器体积: {source_volume}mL") + + # === 计算溶剂体积 === + debug_print("步骤5: 计算溶剂体积...") + + # 洗脱溶剂通常是样品体积的2-5倍 + total_elution_volume = source_volume * 3.0 + solvent1_volume, solvent2_volume = calculate_solvent_volumes( + total_elution_volume, final_pct1, final_pct2 ) - - -def generate_gradient_column_protocol( - G: nx.DiGraph, - from_vessel: str, - to_vessel: str, - column_material: str = "silica_gel", - gradient_solvents: List[str] = None, - gradient_volumes: List[float] = None -) -> List[Dict[str, Any]]: - """梯度洗脱柱层析:多种溶剂系统""" - if gradient_solvents is None: - gradient_solvents = ["hexane", "ethyl_acetate", "methanol"] - if gradient_volumes is None: - gradient_volumes = [50.0, 50.0, 30.0] - action_sequence = [] + # === 执行柱层析流程 === + debug_print("步骤6: 执行柱层析流程...") - # 每种溶剂单独执行一次柱层析 - for i, (solvent, volume) in enumerate(zip(gradient_solvents, gradient_volumes)): - print(f"RUN_COLUMN: 梯度洗脱第 {i+1}/{len(gradient_solvents)} 步: {volume} mL {solvent}") + try: + # 步骤6.1: 样品上柱(如果有独立的柱容器) + if column_vessel and column_vessel != from_vessel: + debug_print(f"6.1: 样品上柱 - {source_volume}mL 从 {from_vessel} 到 {column_vessel}") + + try: + sample_transfer_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=from_vessel, + to_vessel=column_vessel, + volume=source_volume, + flowrate=1.0, # 慢速上柱 + transfer_flowrate=0.5, + rinsing_solvent="", # 暂不冲洗 + rinsing_volume=0.0, + rinsing_repeats=0 + ) + action_sequence.extend(sample_transfer_actions) + debug_print(f"✅ 样品上柱完成,添加了 {len(sample_transfer_actions)} 个动作") + except Exception as e: + debug_print(f"⚠️ 样品上柱失败: {str(e)}") - # 第一步使用源容器,后续步骤使用柱子作为源 - step_from_vessel = from_vessel if i == 0 else column_material - # 最后一步使用目标容器,其他步骤使用柱子作为目标 - step_to_vessel = to_vessel if i == len(gradient_solvents) - 1 else column_material + # 步骤6.2: 添加洗脱溶剂1(如果有溶剂容器) + if solvent1_vessel and solvent1_volume > 0: + debug_print(f"6.2: 添加洗脱溶剂1 - {solvent1_volume:.1f}mL {final_solvent1}") + + try: + target_vessel = column_vessel if column_vessel else from_vessel + solvent1_transfer_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=solvent1_vessel, + to_vessel=target_vessel, + volume=solvent1_volume, + flowrate=2.0, + transfer_flowrate=1.0 + ) + action_sequence.extend(solvent1_transfer_actions) + debug_print(f"✅ 溶剂1添加完成,添加了 {len(solvent1_transfer_actions)} 个动作") + except Exception as e: + debug_print(f"⚠️ 溶剂1添加失败: {str(e)}") - step_actions = generate_run_column_protocol( - G, step_from_vessel, step_to_vessel, column_material, - solvent, volume, 1, "", 0.0, 1.0 - ) - action_sequence.extend(step_actions) + # 步骤6.3: 添加洗脱溶剂2(如果有溶剂容器) + if solvent2_vessel and solvent2_volume > 0: + debug_print(f"6.3: 添加洗脱溶剂2 - {solvent2_volume:.1f}mL {final_solvent2}") + + try: + target_vessel = column_vessel if column_vessel else from_vessel + solvent2_transfer_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=solvent2_vessel, + to_vessel=target_vessel, + volume=solvent2_volume, + flowrate=2.0, + transfer_flowrate=1.0 + ) + action_sequence.extend(solvent2_transfer_actions) + debug_print(f"✅ 溶剂2添加完成,添加了 {len(solvent2_transfer_actions)} 个动作") + except Exception as e: + debug_print(f"⚠️ 溶剂2添加失败: {str(e)}") - # 在梯度步骤之间加入等待时间 - if i < len(gradient_solvents) - 1: + # 步骤6.4: 使用柱层析设备执行分离(如果有设备) + if column_device_id: + debug_print(f"6.4: 使用柱层析设备执行分离") + + column_separation_action = { + "device_id": column_device_id, + "action_name": "run_column", + "action_kwargs": { + "from_vessel": from_vessel, + "to_vessel": to_vessel, + "column": column, + "rf": rf, + "pct1": pct1, + "pct2": pct2, + "solvent1": solvent1, + "solvent2": solvent2, + "ratio": ratio + } + } + action_sequence.append(column_separation_action) + debug_print(f"✅ 柱层析设备动作已添加") + + # 等待分离完成 + separation_time = max(30, int(total_elution_volume / 2)) # 基于体积估算时间 action_sequence.append({ "action_name": "wait", - "action_kwargs": {"time": 20} + "action_kwargs": {"time": separation_time} }) + debug_print(f"✅ 等待分离完成: {separation_time}秒") + + # 步骤6.5: 产物收集(从柱容器到目标容器) + if column_vessel and column_vessel != to_vessel: + debug_print(f"6.5: 产物收集 - 从 {column_vessel} 到 {to_vessel}") + + try: + # 估算产物体积(原始样品体积的70-90%) + product_volume = source_volume * 0.8 + + product_transfer_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=column_vessel, + to_vessel=to_vessel, + volume=product_volume, + flowrate=1.5, + transfer_flowrate=0.8 + ) + action_sequence.extend(product_transfer_actions) + debug_print(f"✅ 产物收集完成,添加了 {len(product_transfer_actions)} 个动作") + except Exception as e: + debug_print(f"⚠️ 产物收集失败: {str(e)}") + + # 步骤6.6: 如果没有独立的柱设备和容器,执行简化的直接转移 + if not column_device_id and not column_vessel: + debug_print(f"6.6: 简化模式 - 直接转移 {source_volume}mL 从 {from_vessel} 到 {to_vessel}") + + try: + direct_transfer_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=from_vessel, + to_vessel=to_vessel, + volume=source_volume, + flowrate=2.0, + transfer_flowrate=1.0 + ) + action_sequence.extend(direct_transfer_actions) + debug_print(f"✅ 直接转移完成,添加了 {len(direct_transfer_actions)} 个动作") + except Exception as e: + debug_print(f"⚠️ 直接转移失败: {str(e)}") + + except Exception as e: + debug_print(f"❌ 柱层析流程执行失败: {str(e)}") + # 添加错误日志动作 + action_sequence.append({ + "device_id": "system", + "action_name": "log_message", + "action_kwargs": { + "message": f"柱层析失败: {str(e)}" + } + }) + + # === 最终结果 === + debug_print("=" * 60) + debug_print(f"✅ 柱层析协议生成完成") + debug_print(f"📊 总动作数: {len(action_sequence)}") + debug_print(f"📋 参数总结:") + debug_print(f" - 源容器: {from_vessel} ({source_volume}mL)") + debug_print(f" - 目标容器: {to_vessel}") + debug_print(f" - 柱子: {column}") + debug_print(f" - Rf值: {final_rf}") + debug_print(f" - 溶剂比例: {final_solvent1} {final_pct1:.1f}% : {final_solvent2} {final_pct2:.1f}%") + debug_print(f" - 洗脱体积: {solvent1_volume:.1f}mL + {solvent2_volume:.1f}mL") + debug_print("=" * 60) return action_sequence +# === 便捷函数 === -def generate_reverse_phase_column_protocol( +def generate_silica_gel_column_protocol( G: nx.DiGraph, from_vessel: str, to_vessel: str, - column_material: str = "C18", - aqueous_phase: str = "water", - organic_phase: str = "methanol", - gradient_ratio: float = 0.5 + **kwargs ) -> List[Dict[str, Any]]: - """反相柱层析:C18柱,水-有机相梯度""" - # 先用水相平衡 - equilibration_volume = 20.0 - # 然后用有机相洗脱 - eluting_volume = 30.0 * gradient_ratio - + """硅胶柱层析协议便捷函数""" return generate_run_column_protocol( - G, from_vessel, to_vessel, column_material, - organic_phase, eluting_volume, 2, - aqueous_phase, equilibration_volume, 0.8 + G, from_vessel, to_vessel, + column="silica_column", + solvent1="ethyl_acetate", + solvent2="hexane", + ratio="1:9", # 常见的EA:Hex比例 + **kwargs ) - -def generate_ion_exchange_column_protocol( +def generate_reverse_phase_column_protocol( G: nx.DiGraph, from_vessel: str, to_vessel: str, - column_material: str = "ion_exchange", - buffer_solution: str = "buffer", - salt_solution: str = "NaCl_solution", - salt_volume: float = 40.0 + **kwargs ) -> List[Dict[str, Any]]: - """离子交换柱层析:缓冲液平衡,盐溶液洗脱""" + """反相柱层析协议便捷函数""" return generate_run_column_protocol( - G, from_vessel, to_vessel, column_material, - salt_solution, salt_volume, 1, - buffer_solution, 25.0, 0.5 + G, from_vessel, to_vessel, + column="c18_column", + solvent1="methanol", + solvent2="water", + ratio="7:3", # 常见的MeOH:H2O比例 + **kwargs ) - -# 测试函数 -def test_run_column_protocol(): - """测试柱层析协议的示例""" - print("=== RUN COLUMN PROTOCOL 测试 ===") - print("测试完成") - - -if __name__ == "__main__": - test_run_column_protocol() \ No newline at end of file diff --git a/unilabos/compile/separate_protocol.py b/unilabos/compile/separate_protocol.py index cbb028cb9..07d9ab4e8 100644 --- a/unilabos/compile/separate_protocol.py +++ b/unilabos/compile/separate_protocol.py @@ -1,230 +1,448 @@ -import numpy as np import networkx as nx +import re +import logging +from typing import List, Dict, Any, Union +from .pump_protocol import generate_pump_protocol_with_rinsing +logger = logging.getLogger(__name__) -def generate_separate_protocol( - G: nx.DiGraph, - purpose: str, # 'wash' or 'extract'. 'wash' means that product phase will not be the added solvent phase, 'extract' means product phase will be the added solvent phase. If no solvent is added just use 'extract'. - product_phase: str, # 'top' or 'bottom'. Phase that product will be in. - from_vessel: str, #Contents of from_vessel are transferred to separation_vessel and separation is performed. - separation_vessel: str, # Vessel in which separation of phases will be carried out. - to_vessel: str, # Vessel to send product phase to. - waste_phase_to_vessel: str, # Optional. Vessel to send waste phase to. - solvent: str, # Optional. Solvent to add to separation vessel after contents of from_vessel has been transferred to create two phases. - solvent_volume: float = 50, # Optional. Volume of solvent to add (mL). - through: str = "", # Optional. Solid chemical to send product phase through on way to to_vessel, e.g. 'celite'. - repeats: int = 1, # Optional. Number of separations to perform. - stir_time: float = 30, # Optional. Time stir for after adding solvent, before separation of phases. - stir_speed: float = 300, # Optional. Speed to stir at after adding solvent, before separation of phases. - settling_time: float = 300 # Optional. Time -) -> list[dict]: +def debug_print(message): + """调试输出""" + print(f"[SEPARATE] {message}", flush=True) + logger.info(f"[SEPARATE] {message}") + +def parse_volume_input(volume_input: Union[str, float]) -> float: """ - Generate a protocol to clean a vessel with a solvent. - - :param G: Directed graph. Nodes are containers and pumps, edges are fluidic connections. - :param vessel: Vessel to clean. - :param solvent: Solvent to clean vessel with. - :param volume: Volume of solvent to clean vessel with. - :param temp: Temperature to heat vessel to while cleaning. - :param repeats: Number of cleaning cycles to perform. - :return: List of actions to clean vessel. + 解析体积输入,支持带单位的字符串 + + Args: + volume_input: 体积输入(如 "200 mL", "?", 50.0) + + Returns: + float: 体积(毫升) """ + if isinstance(volume_input, (int, float)): + return float(volume_input) - # 生成泵操作的动作序列 - pump_action_sequence = [] - reactor_volume = 500.0 - waste_vessel = waste_phase_to_vessel - - # TODO:通过物料管理系统找到溶剂的容器 - if "," in solvent: - solvents = solvent.split(",") - assert len(solvents) == repeats, "Number of solvents must match number of repeats." - else: - solvents = [solvent] * repeats - - # TODO: 通过设备连接图找到分离容器的控制器、底部出口 - separator_controller = f"{separation_vessel}_controller" - separation_vessel_bottom = f"flask_{separation_vessel}" - - transfer_flowrate = flowrate = 2.5 - - if from_vessel != separation_vessel: - pump_action_sequence.append( - { - "device_id": "", - "action_name": "PumpTransferProtocol", - "action_kwargs": { - "from_vessel": from_vessel, - "to_vessel": separation_vessel, - "volume": reactor_volume, - "time": reactor_volume / flowrate, - # "transfer_flowrate": transfer_flowrate, - } - } - ) - - # for i in range(2): - # pump_action_sequence.append( - # { - # "device_id": "", - # "action_name": "CleanProtocol", - # "action_kwargs": { - # "vessel": from_vessel, - # "solvent": "H2O", # Solvent to clean vessel with. - # "volume": solvent_volume, # Optional. Volume of solvent to clean vessel with. - # "temp": 25.0, # Optional. Temperature to heat vessel to while cleaning. - # "repeats": 1 - # } - # } - # ) - # pump_action_sequence.append( - # { - # "device_id": "", - # "action_name": "CleanProtocol", - # "action_kwargs": { - # "vessel": from_vessel, - # "solvent": "CH2Cl2", # Solvent to clean vessel with. - # "volume": solvent_volume, # Optional. Volume of solvent to clean vessel with. - # "temp": 25.0, # Optional. Temperature to heat vessel to while cleaning. - # "repeats": 1 - # } - # } - # ) - - # 生成泵操作的动作序列 - for i in range(repeats): - # 找到当次萃取所用溶剂 - solvent_thistime = solvents[i] - solvent_vessel = f"flask_{solvent_thistime}" - - pump_action_sequence.append( - { - "device_id": "", - "action_name": "PumpTransferProtocol", - "action_kwargs": { - "from_vessel": solvent_vessel, - "to_vessel": separation_vessel, - "volume": solvent_volume, - "time": solvent_volume / flowrate, - # "transfer_flowrate": transfer_flowrate, - } - } - ) - pump_action_sequence.extend([ - # 搅拌、静置 - { - "device_id": separator_controller, - "action_name": "stir", - "action_kwargs": { - "stir_time": stir_time, - "stir_speed": stir_speed, - "settling_time": settling_time - } - }, - # 分液(判断电导突跃) - { - "device_id": separator_controller, - "action_name": "valve_open", - "action_kwargs": { - "command": "delta > 0.05" - } - } - ]) - - if product_phase == "bottom": - # 产物转移到目标瓶 - pump_action_sequence.append( - { - "device_id": "", - "action_name": "PumpTransferProtocol", - "action_kwargs": { - "from_vessel": separation_vessel_bottom, - "to_vessel": to_vessel, - "volume": 250.0, - "time": 250.0 / flowrate, - # "transfer_flowrate": transfer_flowrate, - } - } - ) - # 放出上面那一相,60秒后关阀门 - pump_action_sequence.append( - { - "device_id": separator_controller, - "action_name": "valve_open", - "action_kwargs": { - "command": "time > 60" - } - } - ) - # 弃去上面那一相进废液 - pump_action_sequence.append( - { - "device_id": "", - "action_name": "PumpTransferProtocol", - "action_kwargs": { - "from_vessel": separation_vessel_bottom, - "to_vessel": waste_vessel, - "volume": 250.0, - "time": 250.0 / flowrate, - # "transfer_flowrate": transfer_flowrate, - } - } - ) - elif product_phase == "top": - # 弃去下面那一相进废液 - pump_action_sequence.append( - { - "device_id": "", - "action_name": "PumpTransferProtocol", - "action_kwargs": { - "from_vessel": separation_vessel_bottom, - "to_vessel": waste_vessel, - "volume": 250.0, - "time": 250.0 / flowrate, - # "transfer_flowrate": transfer_flowrate, - } - } - ) - # 放出上面那一相 - pump_action_sequence.append( - { - "device_id": separator_controller, - "action_name": "valve_open", - "action_kwargs": { - "command": "time > 60" - } - } - ) - # 产物转移到目标瓶 - pump_action_sequence.append( - { - "device_id": "", - "action_name": "PumpTransferProtocol", - "action_kwargs": { - "from_vessel": separation_vessel_bottom, - "to_vessel": to_vessel, - "volume": 250.0, - "time": 250.0 / flowrate, - # "transfer_flowrate": transfer_flowrate, - } - } - ) - elif product_phase == "organic": - pass + if not volume_input or not str(volume_input).strip(): + return 0.0 + + volume_str = str(volume_input).lower().strip() + debug_print(f"解析体积输入: '{volume_str}'") + + # 处理未知体积 + if volume_str in ['?', 'unknown', 'tbd', 'to be determined']: + default_volume = 100.0 # 默认100mL + debug_print(f"检测到未知体积,使用默认值: {default_volume}mL") + return default_volume + + # 移除空格并提取数字和单位 + volume_clean = re.sub(r'\s+', '', volume_str) + + # 匹配数字和单位的正则表达式 + match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean) + + if not match: + debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值100mL") + return 100.0 + + value = float(match.group(1)) + unit = match.group(2) or 'ml' # 默认单位为毫升 + + # 转换为毫升 + if unit in ['l', 'liter']: + volume = value * 1000.0 # L -> mL + elif unit in ['μl', 'ul', 'microliter']: + volume = value / 1000.0 # μL -> mL + else: # ml, milliliter 或默认 + volume = value # 已经是mL + + debug_print(f"体积转换: {value}{unit} → {volume}mL") + return volume + +def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: + """查找溶剂容器""" + if not solvent or not solvent.strip(): + return "" + + debug_print(f"查找溶剂 '{solvent}' 的容器...") + + # 🔧 方法1:直接搜索 data.reagent_name 和 config.reagent + for node in G.nodes(): + node_data = G.nodes[node].get('data', {}) + node_type = G.nodes[node].get('type', '') + config_data = G.nodes[node].get('config', {}) - # 如果不是最后一次,从中转瓶转移回分液漏斗 - if i < repeats - 1: - pump_action_sequence.append( - { - "device_id": "", - "action_name": "PumpTransferProtocol", + # 只搜索容器类型的节点 + if node_type == 'container': + reagent_name = node_data.get('reagent_name', '').lower() + config_reagent = config_data.get('reagent', '').lower() + + # 精确匹配 + if reagent_name == solvent.lower() or config_reagent == solvent.lower(): + debug_print(f"✅ 通过reagent字段找到容器: {node}") + return node + + # 模糊匹配 + if (solvent.lower() in reagent_name and reagent_name) or \ + (solvent.lower() in config_reagent and config_reagent): + debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node}") + return node + + # 🔧 方法2:常见的容器命名规则 + solvent_clean = solvent.lower().replace(' ', '_').replace('-', '_') + possible_names = [ + f"flask_{solvent_clean}", + f"bottle_{solvent_clean}", + f"vessel_{solvent_clean}", + f"{solvent_clean}_flask", + f"{solvent_clean}_bottle", + f"solvent_{solvent_clean}", + f"reagent_{solvent_clean}", + f"reagent_bottle_{solvent_clean}" + ] + + for name in possible_names: + if name in G.nodes(): + node_type = G.nodes[name].get('type', '') + if node_type == 'container': + debug_print(f"✅ 通过命名规则找到容器: {name}") + return name + + # 🔧 方法3:使用第一个试剂瓶作为备选 + for node_id in G.nodes(): + node_data = G.nodes[node_id] + if (node_data.get('type') == 'container' and + ('reagent' in node_id.lower() or 'bottle' in node_id.lower())): + debug_print(f"⚠️ 未找到专用容器,使用备选容器: {node_id}") + return node_id + + debug_print(f"⚠️ 未找到溶剂 '{solvent}' 的容器") + return "" + +def find_separator_device(G: nx.DiGraph, vessel: str) -> str: + """查找分离器设备""" + debug_print(f"查找容器 '{vessel}' 对应的分离器设备...") + + # 方法1:查找连接到容器的分离器设备 + for node in G.nodes(): + node_class = G.nodes[node].get('class', '').lower() + if 'separator' in node_class: + # 检查是否连接到目标容器 + if G.has_edge(node, vessel) or G.has_edge(vessel, node): + debug_print(f"✅ 找到连接的分离器: {node}") + return node + + # 方法2:根据命名规则查找 + possible_names = [ + f"{vessel}_controller", + f"{vessel}_separator", + vessel, # 容器本身可能就是分离器 + "separator_1", + "virtual_separator" + ] + + for name in possible_names: + if name in G.nodes(): + node_class = G.nodes[name].get('class', '').lower() + if 'separator' in node_class: + debug_print(f"✅ 通过命名规则找到分离器: {name}") + return name + + # 方法3:查找第一个分离器设备 + for node in G.nodes(): + node_class = G.nodes[node].get('class', '').lower() + if 'separator' in node_class: + debug_print(f"⚠️ 使用第一个分离器设备: {node}") + return node + + debug_print(f"⚠️ 未找到分离器设备") + return "" + +def generate_separate_protocol( + G: nx.DiGraph, + # 🔧 基础参数,支持XDL的vessel参数 + vessel: str = "", # XDL: 分离容器 + purpose: str = "separate", # 分离目的 + product_phase: str = "top", # 产物相 + # 🔧 可选的详细参数 + from_vessel: str = "", # 源容器(通常在separate前已经transfer了) + separation_vessel: str = "", # 分离容器(与vessel同义) + to_vessel: str = "", # 目标容器(可选) + waste_phase_to_vessel: str = "", # 废相目标容器 + product_vessel: str = "", # XDL: 产物容器(与to_vessel同义) + waste_vessel: str = "", # XDL: 废液容器(与waste_phase_to_vessel同义) + # 🔧 溶剂相关参数 + solvent: str = "", # 溶剂名称 + solvent_volume: Union[str, float] = 0.0, # 溶剂体积 + volume: Union[str, float] = 0.0, # XDL: 体积(与solvent_volume同义) + # 🔧 操作参数 + through: str = "", # 通过材料 + repeats: int = 1, # 重复次数 + stir_time: float = 30.0, # 搅拌时间(秒) + stir_speed: float = 300.0, # 搅拌速度 + settling_time: float = 300.0, # 沉降时间(秒) + **kwargs +) -> List[Dict[str, Any]]: + """ + 生成分离操作的协议序列 - 修复版 + + 支持XDL参数格式: + - vessel: 分离容器(必需) + - purpose: "wash", "extract", "separate" + - product_phase: "top", "bottom" + - product_vessel: 产物收集容器 + - waste_vessel: 废液收集容器 + - solvent: 溶剂名称 + - volume: "200 mL", "?" 或数值 + - repeats: 重复次数 + + 分离流程: + 1. (可选)添加溶剂到分离容器 + 2. 搅拌混合 + 3. 静置分层 + 4. 收集指定相到目标容器 + 5. 重复指定次数 + """ + + debug_print("=" * 60) + debug_print("开始生成分离协议 - 修复版") + debug_print(f"原始参数:") + debug_print(f" - vessel: '{vessel}'") + debug_print(f" - purpose: '{purpose}'") + debug_print(f" - product_phase: '{product_phase}'") + debug_print(f" - solvent: '{solvent}'") + debug_print(f" - volume: {volume} (类型: {type(volume)})") + debug_print(f" - repeats: {repeats}") + debug_print(f" - product_vessel: '{product_vessel}'") + debug_print(f" - waste_vessel: '{waste_vessel}'") + debug_print("=" * 60) + + action_sequence = [] + + # === 参数验证和标准化 === + debug_print("步骤1: 参数验证和标准化...") + + # 统一容器参数 + final_vessel = vessel or separation_vessel + if not final_vessel: + raise ValueError("必须指定分离容器 (vessel 或 separation_vessel)") + + final_to_vessel = to_vessel or product_vessel + final_waste_vessel = waste_phase_to_vessel or waste_vessel + + # 统一体积参数 + final_volume = parse_volume_input(volume or solvent_volume) + + # 🔧 修复:确保repeats至少为1 + if repeats <= 0: + repeats = 1 + debug_print(f"⚠️ repeats参数 <= 0,自动设置为1") + + debug_print(f"标准化参数:") + debug_print(f" - 分离容器: '{final_vessel}'") + debug_print(f" - 产物容器: '{final_to_vessel}'") + debug_print(f" - 废液容器: '{final_waste_vessel}'") + debug_print(f" - 溶剂体积: {final_volume}mL") + debug_print(f" - 重复次数: {repeats}") + + # 验证必需参数 + if not purpose: + purpose = "separate" + if not product_phase: + product_phase = "top" + if purpose not in ["wash", "extract", "separate"]: + debug_print(f"⚠️ 未知的分离目的 '{purpose}',使用默认值 'separate'") + purpose = "separate" + if product_phase not in ["top", "bottom"]: + debug_print(f"⚠️ 未知的产物相 '{product_phase}',使用默认值 'top'") + product_phase = "top" + + debug_print("✅ 参数验证通过") + + # === 查找设备 === + debug_print("步骤2: 查找设备...") + + # 查找分离器设备 + separator_device = find_separator_device(G, final_vessel) + if not separator_device: + debug_print("⚠️ 未找到分离器设备,可能无法执行分离操作") + + # 查找溶剂容器(如果需要) + solvent_vessel = "" + if solvent and solvent.strip(): + solvent_vessel = find_solvent_vessel(G, solvent) + + debug_print(f"设备映射:") + debug_print(f" - 分离器设备: '{separator_device}'") + debug_print(f" - 溶剂容器: '{solvent_vessel}'") + + # === 执行分离流程 === + debug_print("步骤3: 执行分离流程...") + + try: + for repeat_idx in range(repeats): + debug_print(f"3.{repeat_idx+1}: 第 {repeat_idx+1}/{repeats} 次分离") + + # 步骤3.1: 添加溶剂(如果需要) + if solvent_vessel and final_volume > 0: + debug_print(f"3.{repeat_idx+1}.1: 添加溶剂 {solvent} ({final_volume}mL)") + + # 使用pump protocol添加溶剂 + pump_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=solvent_vessel, + to_vessel=final_vessel, + volume=final_volume, + amount="", + time=0.0, + viscous=False, + rinsing_solvent="", + rinsing_volume=0.0, + rinsing_repeats=0, + solid=False, + flowrate=2.5, + transfer_flowrate=0.5, + rate_spec="", + event="", + through="", + **kwargs + ) + action_sequence.extend(pump_actions) + debug_print(f"✅ 溶剂添加完成,添加了 {len(pump_actions)} 个动作") + + # 步骤3.2: 执行分离操作 + if separator_device: + debug_print(f"3.{repeat_idx+1}.2: 执行分离操作") + + # 调用分离器设备的separate方法 + separate_action = { + "device_id": separator_device, + "action_name": "separate", "action_kwargs": { - "from_vessel": to_vessel, - "to_vessel": separation_vessel, - "volume": 250.0, - "time": 250.0 / flowrate, - # "transfer_flowrate": transfer_flowrate, + "purpose": purpose, + "product_phase": product_phase, + "from_vessel": from_vessel or final_vessel, + "separation_vessel": final_vessel, + "to_vessel": final_to_vessel or final_vessel, + "waste_phase_to_vessel": final_waste_vessel or final_vessel, + "solvent": solvent, + "solvent_volume": final_volume, + "through": through, + "repeats": 1, # 每次调用只做一次分离 + "stir_time": stir_time, + "stir_speed": stir_speed, + "settling_time": settling_time } } - ) - return pump_action_sequence + action_sequence.append(separate_action) + debug_print(f"✅ 分离操作添加完成") + + else: + debug_print(f"3.{repeat_idx+1}.2: 无分离器设备,跳过分离操作") + # 添加等待时间模拟分离 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": stir_time + settling_time} + }) + + # 等待间隔(除了最后一次) + if repeat_idx < repeats - 1: + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 5} + }) + + except Exception as e: + debug_print(f"⚠️ 分离流程执行失败: {str(e)}") + # 添加错误日志 + action_sequence.append({ + "device_id": "system", + "action_name": "log_message", + "action_kwargs": { + "message": f"分离操作失败: {str(e)}" + } + }) + + # === 最终结果 === + debug_print("=" * 60) + debug_print(f"✅ 分离协议生成完成") + debug_print(f"📊 总动作数: {len(action_sequence)}") + debug_print(f"📋 处理总结:") + debug_print(f" - 分离容器: {final_vessel}") + debug_print(f" - 分离目的: {purpose}") + debug_print(f" - 产物相: {product_phase}") + debug_print(f" - 重复次数: {repeats}") + if solvent: + debug_print(f" - 溶剂: {solvent} ({final_volume}mL)") + if final_to_vessel: + debug_print(f" - 产物容器: {final_to_vessel}") + if final_waste_vessel: + debug_print(f" - 废液容器: {final_waste_vessel}") + debug_print("=" * 60) + + return action_sequence + +# === 便捷函数 === + +def separate_phases_only(G: nx.DiGraph, vessel: str, product_phase: str = "top", + product_vessel: str = "", waste_vessel: str = "") -> List[Dict[str, Any]]: + """仅进行相分离(不添加溶剂)""" + return generate_separate_protocol( + G, vessel=vessel, + purpose="separate", + product_phase=product_phase, + product_vessel=product_vessel, + waste_vessel=waste_vessel + ) + +def wash_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float], + product_phase: str = "top", repeats: int = 1) -> List[Dict[str, Any]]: + """用溶剂洗涤""" + return generate_separate_protocol( + G, vessel=vessel, + purpose="wash", + product_phase=product_phase, + solvent=solvent, + volume=volume, + repeats=repeats + ) + +def extract_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float], + product_phase: str = "bottom", repeats: int = 3) -> List[Dict[str, Any]]: + """用溶剂萃取""" + return generate_separate_protocol( + G, vessel=vessel, + purpose="extract", + product_phase=product_phase, + solvent=solvent, + volume=volume, + repeats=repeats + ) + +def separate_aqueous_organic(G: nx.DiGraph, vessel: str, organic_phase: str = "top", + product_vessel: str = "", waste_vessel: str = "") -> List[Dict[str, Any]]: + """水-有机相分离""" + return generate_separate_protocol( + G, vessel=vessel, + purpose="separate", + product_phase=organic_phase, + product_vessel=product_vessel, + waste_vessel=waste_vessel + ) + +# 测试函数 +def test_separate_protocol(): + """测试分离协议的各种参数解析""" + print("=== SEPARATE PROTOCOL 增强版测试 ===") + + # 测试体积解析 + volumes = ["200 mL", "?", 100.0, "1 L", "500 μL"] + for vol in volumes: + result = parse_volume_input(vol) + print(f"体积解析: {vol} → {result}mL") + + print("✅ 测试完成") + +if __name__ == "__main__": + test_separate_protocol() diff --git a/unilabos/compile/stir_protocol.py b/unilabos/compile/stir_protocol.py index 76345cc3e..a67155da6 100644 --- a/unilabos/compile/stir_protocol.py +++ b/unilabos/compile/stir_protocol.py @@ -1,6 +1,7 @@ -from typing import List, Dict, Any +from typing import List, Dict, Any, Union import networkx as nx import logging +import re logger = logging.getLogger(__name__) @@ -9,6 +10,173 @@ def debug_print(message): print(f"[STIR] {message}", flush=True) logger.info(f"[STIR] {message}") +def parse_time_spec(time_spec: str) -> float: + """ + 解析时间规格字符串为秒数 + + Args: + time_spec: 时间规格字符串(如 "several minutes", "overnight", "few hours") + + Returns: + float: 时间(秒) + """ + if not time_spec: + return 0.0 + + time_spec = time_spec.lower().strip() + + # 预定义的时间规格映射 + time_spec_map = { + # 几分钟 + "several minutes": 5.0 * 60, # 5分钟 + "few minutes": 3.0 * 60, # 3分钟 + "couple of minutes": 2.0 * 60, # 2分钟 + "a few minutes": 3.0 * 60, # 3分钟 + "some minutes": 5.0 * 60, # 5分钟 + + # 几小时 + "several hours": 3.0 * 3600, # 3小时 + "few hours": 2.0 * 3600, # 2小时 + "couple of hours": 2.0 * 3600, # 2小时 + "a few hours": 3.0 * 3600, # 3小时 + "some hours": 4.0 * 3600, # 4小时 + + # 特殊时间 + "overnight": 12.0 * 3600, # 12小时 + "over night": 12.0 * 3600, # 12小时 + "morning": 4.0 * 3600, # 4小时 + "afternoon": 6.0 * 3600, # 6小时 + "evening": 4.0 * 3600, # 4小时 + + # 短时间 + "briefly": 30.0, # 30秒 + "momentarily": 10.0, # 10秒 + "quickly": 60.0, # 1分钟 + "slowly": 10.0 * 60, # 10分钟 + + # 长时间 + "extended": 6.0 * 3600, # 6小时 + "prolonged": 8.0 * 3600, # 8小时 + "extensively": 12.0 * 3600, # 12小时 + } + + # 直接匹配 + if time_spec in time_spec_map: + result = time_spec_map[time_spec] + debug_print(f"时间规格解析: '{time_spec}' → {result/60:.1f}分钟") + return result + + # 模糊匹配 + for spec, value in time_spec_map.items(): + if spec in time_spec or time_spec in spec: + result = value + debug_print(f"时间规格模糊匹配: '{time_spec}' → '{spec}' → {result/60:.1f}分钟") + return result + + # 如果无法识别,返回默认值 + default_time = 5.0 * 60 # 5分钟 + debug_print(f"⚠️ 无法识别时间规格: '{time_spec}',使用默认值: {default_time/60:.1f}分钟") + return default_time + +def parse_time_string(time_str: str) -> float: + """ + 解析时间字符串为秒数,支持多种单位 + + Args: + time_str: 时间字符串(如 "0.5 h", "30 min", "120 s", "2.5") + + Returns: + float: 时间(秒) + """ + if not time_str: + return 0.0 + + # 如果是纯数字,默认单位为秒 + try: + return float(time_str) + except ValueError: + pass + + # 清理字符串 + time_str = time_str.lower().strip() + + # 使用正则表达式匹配数字和单位 + pattern = r'(\d+\.?\d*)\s*([a-z]*)' + match = re.match(pattern, time_str) + + if not match: + debug_print(f"⚠️ 无法解析时间字符串: '{time_str}',使用默认值: 60秒") + return 60.0 + + value = float(match.group(1)) + unit = match.group(2) + + # 单位转换映射 + unit_map = { + # 秒 + 's': 1.0, + 'sec': 1.0, + 'second': 1.0, + 'seconds': 1.0, + + # 分钟 + 'm': 60.0, + 'min': 60.0, + 'mins': 60.0, + 'minute': 60.0, + 'minutes': 60.0, + + # 小时 + 'h': 3600.0, + 'hr': 3600.0, + 'hrs': 3600.0, + 'hour': 3600.0, + 'hours': 3600.0, + + # 天 + 'd': 86400.0, + 'day': 86400.0, + 'days': 86400.0, + + # 如果没有单位,默认为秒 + '': 1.0, + } + + multiplier = unit_map.get(unit, 1.0) + result = value * multiplier + + debug_print(f"时间字符串解析: '{time_str}' → {value} {unit or 'seconds'} → {result}秒") + return result + +def parse_time_input(time_input: Union[str, float, int], time_spec: str = "") -> float: + """ + 统一的时间输入解析函数 + + Args: + time_input: 时间输入(可以是字符串、浮点数或整数) + time_spec: 时间规格字符串(优先级高于time_input) + + Returns: + float: 时间(秒) + """ + # 优先处理 time_spec + if time_spec: + return parse_time_spec(time_spec) + + # 处理 time_input + if isinstance(time_input, (int, float)): + # 数字默认单位为秒 + result = float(time_input) + debug_print(f"数字时间输入: {time_input} → {result}秒") + return result + + if isinstance(time_input, str): + return parse_time_string(time_input) + + # 默认值 + debug_print(f"⚠️ 无法处理时间输入: {time_input},使用默认值: 60秒") + return 60.0 + def find_connected_stirrer(G: nx.DiGraph, vessel: str = None) -> str: """ 查找与指定容器相连的搅拌设备,或查找可用的搅拌设备 @@ -43,18 +211,25 @@ def find_connected_stirrer(G: nx.DiGraph, vessel: str = None) -> str: def generate_stir_protocol( G: nx.DiGraph, vessel: str, - stir_time: float = 300.0, + time: Union[str, float, int] = 300.0, + stir_time: Union[str, float, int] = 0.0, + time_spec: str = "", + event: str = "", stir_speed: float = 200.0, settling_time: float = 60.0, - **kwargs # 🔧 接受额外参数,增强兼容性 + **kwargs ) -> List[Dict[str, Any]]: """ 生成搅拌操作的协议序列 - 定时搅拌 + 沉降 + 支持 time 和 stir_time 参数统一处理 Args: G: 设备图 vessel: 搅拌容器名称(必需) - stir_time: 搅拌时间 (秒),默认300s + time: 搅拌时间(支持多种格式) + stir_time: 搅拌时间(与time等效) + time_spec: 时间规格(优先级最高) + event: 事件标识 stir_speed: 搅拌速度 (RPM),默认200 RPM settling_time: 沉降时间 (秒),默认60s **kwargs: 其他参数(兼容性) @@ -67,9 +242,12 @@ def generate_stir_protocol( 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" - time: {time}") + debug_print(f" - stir_time: {stir_time}") + debug_print(f" - time_spec: {time_spec}") + debug_print(f" - event: {event}") + debug_print(f" - stir_speed: {stir_speed}") + debug_print(f" - settling_time: {settling_time}") debug_print(f" - 其他参数: {kwargs}") debug_print("=" * 50) @@ -85,13 +263,29 @@ def generate_stir_protocol( if vessel not in G.nodes(): raise ValueError(f"容器 '{vessel}' 不存在于系统中") + debug_print(f"✅ 参数验证通过") + + # === 时间处理(统一 time 和 stir_time)=== + debug_print("步骤2: 时间处理...") + + # 确定实际使用的时间值 + actual_time_input = stir_time if stir_time else time + + # 解析时间 + parsed_time = parse_time_input(actual_time_input, time_spec) + + debug_print(f"时间解析结果:") + debug_print(f" - 原始输入: time={time}, stir_time={stir_time}") + debug_print(f" - 时间规格: {time_spec}") + debug_print(f" - 最终时间: {parsed_time}秒 ({parsed_time/60:.1f}分钟)") + # 修正参数范围 - 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 parsed_time < 0: + debug_print(f"搅拌时间 {parsed_time}s 无效,修正为 300s") + parsed_time = 300.0 + elif parsed_time > 7200: + debug_print(f"搅拌时间 {parsed_time}s 过长,修正为 3600s") + parsed_time = 3600.0 if stir_speed < 10.0: debug_print(f"搅拌速度 {stir_speed} RPM 过低,修正为 100 RPM") @@ -107,10 +301,8 @@ def generate_stir_protocol( debug_print(f"沉降时间 {settling_time}s 过长,修正为 600s") settling_time = 600.0 - debug_print(f"✅ 参数验证通过") - # === 查找搅拌设备 === - debug_print("步骤2: 查找搅拌设备...") + debug_print("步骤3: 查找搅拌设备...") try: stirrer_id = find_connected_stirrer(G, vessel) @@ -121,16 +313,25 @@ def generate_stir_protocol( raise ValueError(f"无法找到搅拌设备: {str(e)}") # === 执行搅拌操作 === - debug_print("步骤3: 执行搅拌操作...") + debug_print("步骤4: 执行搅拌操作...") + + # 构建搅拌动作参数 + stir_kwargs = { + "vessel": vessel, + "time": str(time), # 保持原始字符串格式 + "event": event, + "time_spec": time_spec, + "stir_time": parsed_time, # 解析后的时间(秒) + "stir_speed": stir_speed, + "settling_time": settling_time + } + + debug_print(f"搅拌参数: {stir_kwargs}") stir_action = { "device_id": stirrer_id, "action_name": "stir", - "action_kwargs": { - "stir_time": stir_time, - "stir_speed": stir_speed, - "settling_time": settling_time - } + "action_kwargs": stir_kwargs } action_sequence.append(stir_action) @@ -140,7 +341,7 @@ def generate_stir_protocol( 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(f"搅拌参数: {stir_speed} RPM, {parsed_time}s, 沉降 {settling_time}s") debug_print("=" * 50) return action_sequence @@ -150,7 +351,7 @@ def generate_start_stir_protocol( vessel: str, stir_speed: float = 200.0, purpose: str = "", - **kwargs # 🔧 接受额外参数,增强兼容性 + **kwargs ) -> List[Dict[str, Any]]: """ 生成开始搅拌操作的协议序列 - 持续搅拌 @@ -237,7 +438,7 @@ def generate_start_stir_protocol( def generate_stop_stir_protocol( G: nx.DiGraph, vessel: str, - **kwargs # 🔧 接受额外参数,增强兼容性 + **kwargs ) -> List[Dict[str, Any]]: """ 生成停止搅拌操作的协议序列 @@ -304,56 +505,3 @@ def generate_stop_stir_protocol( debug_print("=" * 50) return action_sequence - -# === 便捷函数 === - -def generate_fast_stir_protocol( - G: nx.DiGraph, - vessel: str, - **kwargs -) -> List[Dict[str, Any]]: - """快速搅拌:高速短时间""" - 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, - vessel: str, - **kwargs -) -> List[Dict[str, Any]]: - """温和搅拌:低速长时间""" - 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 50bbed828..1a5c5e3c3 100644 --- a/unilabos/compile/wash_solid_protocol.py +++ b/unilabos/compile/wash_solid_protocol.py @@ -1,7 +1,7 @@ -from typing import List, Dict, Any +from typing import List, Dict, Any, Union import networkx as nx import logging -import sys +import re logger = logging.getLogger(__name__) @@ -10,6 +10,279 @@ def debug_print(message): print(f"[WASH_SOLID] {message}", flush=True) logger.info(f"[WASH_SOLID] {message}") +def parse_volume_spec(volume_spec: str) -> float: + """ + 解析体积规格字符串为毫升数 + + Args: + volume_spec: 体积规格字符串(如 "small volume", "large volume") + + Returns: + float: 体积(毫升) + """ + if not volume_spec: + return 0.0 + + volume_spec = volume_spec.lower().strip() + + # 预定义的体积规格映射 + volume_spec_map = { + # 小体积 + "small volume": 10.0, + "small amount": 10.0, + "minimal volume": 5.0, + "tiny volume": 5.0, + "little volume": 15.0, + + # 中等体积 + "medium volume": 50.0, + "moderate volume": 50.0, + "normal volume": 50.0, + "standard volume": 50.0, + + # 大体积 + "large volume": 100.0, + "big volume": 100.0, + "substantial volume": 150.0, + "generous volume": 200.0, + + # 极端体积 + "minimum": 5.0, + "maximum": 500.0, + "excess": 200.0, + "plenty": 100.0, + } + + # 直接匹配 + if volume_spec in volume_spec_map: + result = volume_spec_map[volume_spec] + debug_print(f"体积规格解析: '{volume_spec}' → {result}mL") + return result + + # 模糊匹配 + for spec, value in volume_spec_map.items(): + if spec in volume_spec or volume_spec in spec: + result = value + debug_print(f"体积规格模糊匹配: '{volume_spec}' → '{spec}' → {result}mL") + return result + + # 如果无法识别,返回默认值 + default_volume = 50.0 + debug_print(f"⚠️ 无法识别体积规格: '{volume_spec}',使用默认值: {default_volume}mL") + return default_volume + +def parse_repeats_spec(repeats_spec: str) -> int: + """ + 解析重复次数规格字符串为整数 + + Args: + repeats_spec: 重复次数规格字符串(如 "several", "many") + + Returns: + int: 重复次数 + """ + if not repeats_spec: + return 1 + + repeats_spec = repeats_spec.lower().strip() + + # 预定义的重复次数映射 + repeats_spec_map = { + # 少数次 + "once": 1, + "twice": 2, + "few": 3, + "couple": 2, + "several": 4, + "some": 3, + + # 多次 + "many": 5, + "multiple": 4, + "numerous": 6, + "repeated": 3, + "extensively": 5, + "thoroughly": 4, + + # 极端情况 + "minimal": 1, + "maximum": 10, + "excess": 8, + } + + # 直接匹配 + if repeats_spec in repeats_spec_map: + result = repeats_spec_map[repeats_spec] + debug_print(f"重复次数解析: '{repeats_spec}' → {result}次") + return result + + # 模糊匹配 + for spec, value in repeats_spec_map.items(): + if spec in repeats_spec or repeats_spec in spec: + result = value + debug_print(f"重复次数模糊匹配: '{repeats_spec}' → '{spec}' → {result}次") + return result + + # 如果无法识别,返回默认值 + default_repeats = 3 + debug_print(f"⚠️ 无法识别重复次数规格: '{repeats_spec}',使用默认值: {default_repeats}次") + return default_repeats + +def parse_mass_to_volume(mass: str) -> float: + """ + 将质量字符串转换为体积(简化假设:密度约为1 g/mL) + + Args: + mass: 质量字符串(如 "10 g", "2.5g", "100mg") + + Returns: + float: 体积(毫升) + """ + if not mass or not mass.strip(): + return 0.0 + + mass = mass.lower().strip() + debug_print(f"解析质量字符串: '{mass}'") + + # 移除空格并提取数字和单位 + mass_clean = re.sub(r'\s+', '', mass) + + # 匹配数字和单位的正则表达式 + match = re.match(r'([0-9]*\.?[0-9]+)\s*(g|mg|kg|gram|milligram|kilogram)?', mass_clean) + + if not match: + debug_print(f"⚠️ 无法解析质量字符串: '{mass}',返回0.0mL") + return 0.0 + + value = float(match.group(1)) + unit = match.group(2) or 'g' # 默认单位为克 + + # 转换为毫升(假设密度为1 g/mL) + if unit in ['mg', 'milligram']: + volume = value / 1000.0 # mg -> g -> mL + elif unit in ['kg', 'kilogram']: + volume = value * 1000.0 # kg -> g -> mL + else: # g, gram 或默认 + volume = value # g -> mL (密度=1) + + debug_print(f"质量转换: {value}{unit} → {volume}mL") + return volume + +def parse_volume_string(volume_str: str) -> float: + """ + 解析体积字符串,支持带单位的输入 + + Args: + volume_str: 体积字符串(如 "10", "10 mL", "2.5L", "500μL", "?") + + Returns: + float: 体积(毫升) + """ + if not volume_str or not volume_str.strip(): + return 0.0 + + volume_str = volume_str.lower().strip() + debug_print(f"解析体积字符串: '{volume_str}'") + + # 🔧 新增:处理未知体积符号 + if volume_str in ['?', 'unknown', 'tbd', 'to be determined', 'unspecified']: + default_unknown_volume = 50.0 # 未知体积时的默认值 + debug_print(f"检测到未知体积符号 '{volume_str}',使用默认值: {default_unknown_volume}mL") + return default_unknown_volume + + # 移除空格并提取数字和单位 + volume_clean = re.sub(r'\s+', '', volume_str) + + # 匹配数字和单位的正则表达式 + match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean) + + if not match: + debug_print(f"⚠️ 无法解析体积字符串: '{volume_str}',返回0.0mL") + return 0.0 + + value = float(match.group(1)) + unit = match.group(2) or 'ml' # 默认单位为毫升 + + # 转换为毫升 + if unit in ['l', 'liter']: + volume = value * 1000.0 # L -> mL + elif unit in ['μl', 'ul', 'microliter']: + volume = value / 1000.0 # μL -> mL + else: # ml, milliliter 或默认 + volume = value # 已经是mL + + debug_print(f"体积转换: {value}{unit} → {volume}mL") + return volume + +def parse_volume_input(volume: Union[float, str], volume_spec: str = "", mass: str = "") -> float: + """ + 统一的体积输入解析函数 - 增强版 + + Args: + volume: 体积数值或字符串 + volume_spec: 体积规格字符串(优先级最高) + mass: 质量字符串(优先级第二) + + Returns: + float: 体积(毫升) + """ + debug_print(f"解析体积输入: volume={volume}, volume_spec='{volume_spec}', mass='{mass}'") + + # 优先级1:volume_spec + if volume_spec and volume_spec.strip(): + result = parse_volume_spec(volume_spec) + debug_print(f"使用volume_spec: {result}mL") + return result + + # 优先级2:mass(质量转体积) + if mass and mass.strip(): + result = parse_mass_to_volume(mass) + if result > 0: + debug_print(f"使用mass转换: {result}mL") + return result + + # 优先级3:volume + if volume: + if isinstance(volume, str): + # 字符串形式的体积 + result = parse_volume_string(volume) + if result > 0: + debug_print(f"使用volume字符串: {result}mL") + return result + elif isinstance(volume, (int, float)) and volume > 0: + # 数值形式的体积 + result = float(volume) + debug_print(f"使用volume数值: {result}mL") + return result + + # 默认值 + default_volume = 50.0 + debug_print(f"⚠️ 所有体积输入无效,使用默认值: {default_volume}mL") + return default_volume + +def parse_repeats_input(repeats: int, repeats_spec: str = "") -> int: + """ + 统一的重复次数输入解析函数 + + Args: + repeats: 重复次数数值 + repeats_spec: 重复次数规格字符串(优先级高于repeats) + + Returns: + int: 重复次数 + """ + # 优先处理 repeats_spec + if repeats_spec: + return parse_repeats_spec(repeats_spec) + + # 处理 repeats + if repeats > 0: + return repeats + + # 默认值 + debug_print(f"⚠️ 无法处理重复次数输入: repeats={repeats}, repeats_spec='{repeats_spec}',使用默认值: 1次") + return 1 + def find_solvent_source(G: nx.DiGraph, solvent: str) -> str: """查找溶剂源容器""" debug_print(f"查找溶剂 '{solvent}' 的源容器...") @@ -20,7 +293,8 @@ def find_solvent_source(G: nx.DiGraph, solvent: str) -> str: f"reagent_bottle_{solvent}", f"bottle_{solvent}", f"container_{solvent}", - f"source_{solvent}" + f"source_{solvent}", + f"liquid_reagent_bottle_{solvent}" ] for name in possible_names: @@ -30,6 +304,8 @@ def find_solvent_source(G: nx.DiGraph, solvent: str) -> str: # 查找通用容器 generic_containers = [ + "liquid_reagent_bottle_1", + "liquid_reagent_bottle_2", "reagent_bottle_1", "reagent_bottle_2", "flask_1", @@ -77,91 +353,51 @@ def find_filtrate_vessel(G: nx.DiGraph, filtrate_vessel: str = "") -> str: 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, vessel: str, solvent: str, - volume: float, + volume: Union[float, str] = 0.0, # 🔧 修改:支持字符串输入 filtrate_vessel: str = "", temp: float = 25.0, stir: bool = False, stir_speed: float = 0.0, time: float = 0.0, repeats: int = 1, - **kwargs # 🔧 接受额外参数,增强兼容性 + # === 新增参数 === + volume_spec: str = "", # 体积规格 + repeats_spec: str = "", # 重复次数规格 + mass: str = "", # 🔧 新增:固体质量(用于转换体积) + event: str = "", # 事件标识符 + **kwargs ) -> List[Dict[str, Any]]: """ - 生成固体清洗操作的协议序列 - 简化版本 - - Args: - G: 设备图 - vessel: 装有固体的容器名称(必需) - solvent: 清洗溶剂名称(必需) - volume: 清洗溶剂体积(必需) - filtrate_vessel: 滤液收集容器(可选,自动查找) - temp: 清洗温度,默认25°C - stir: 是否搅拌,默认False - stir_speed: 搅拌速度,默认0 - time: 清洗时间,默认0 - repeats: 重复次数,默认1 - **kwargs: 其他参数(兼容性) + 生成固体清洗操作的协议序列 - 增强版 - Returns: - List[Dict[str, Any]]: 固体清洗操作的动作序列 + 支持多种体积输入方式: + 1. volume_spec: "small volume", "large volume" 等 + 2. mass: "10 g", "2.5 kg", "500 mg" 等(转换为体积) + 3. volume: 数值或字符串 "10", "10 mL", "2.5 L" 等 """ - debug_print("=" * 50) + debug_print("=" * 60) 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" - volume: {volume} (类型: {type(volume)})") + debug_print(f" - volume_spec: '{volume_spec}'") + debug_print(f" - mass: '{mass}'") # 🔧 新增日志 + 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" - repeats_spec: '{repeats_spec}'") + debug_print(f" - event: '{event}'") debug_print(f" - 其他参数: {kwargs}") - debug_print("=" * 50) + debug_print("=" * 60) action_sequence = [] @@ -175,143 +411,246 @@ def generate_wash_solid_protocol( if not solvent: raise ValueError("solvent 参数不能为空") - if volume <= 0: - raise ValueError("volume 必须大于0") - if vessel not in G.nodes(): raise ValueError(f"容器 '{vessel}' 不存在于系统中") + debug_print(f"✅ 必需参数验证通过") + + # === 参数处理 === + debug_print("步骤2: 参数处理...") + + # 🔧 修改:处理体积参数(支持mass转换和字符串解析) + final_volume = parse_volume_input(volume, volume_spec, mass) + debug_print(f"最终体积: {final_volume}mL") + + # 处理重复次数参数(repeats_spec优先) + final_repeats = parse_repeats_input(repeats, repeats_spec) + debug_print(f"最终重复次数: {final_repeats}次") + # 修正参数范围 if temp < 0 or temp > 200: debug_print(f"温度 {temp}°C 超出范围,修正为 25°C") temp = 25.0 if stir_speed < 0 or stir_speed > 500: - debug_print(f"搅拌速度 {stir_speed} RPM 超出范围,修正为 0") - stir_speed = 0.0 + debug_print(f"搅拌速度 {stir_speed} RPM 超出范围,修正为 200 RPM") + stir_speed = 200.0 if stir else 0.0 if time < 0: debug_print(f"时间 {time}s 无效,修正为 0") time = 0.0 - if repeats < 1: - debug_print(f"重复次数 {repeats} 无效,修正为 1") - repeats = 1 - elif repeats > 10: - debug_print(f"重复次数 {repeats} 过多,修正为 10") - repeats = 10 + if final_repeats < 1: + debug_print(f"重复次数 {final_repeats} 无效,修正为 1") + final_repeats = 1 + elif final_repeats > 10: + debug_print(f"重复次数 {final_repeats} 过多,修正为 10") + final_repeats = 10 - debug_print(f"✅ 参数验证通过") + debug_print(f"✅ 参数处理完成") # === 查找设备 === - debug_print("步骤2: 查找设备...") + debug_print("步骤3: 查找设备...") 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) + + # 查找过滤器(用于过滤操作) + filter_device = None + for node in G.nodes(): + node_data = G.nodes[node] + node_class = node_data.get('class', '') or '' + if 'filter' in node_class.lower(): + filter_device = node + break + + if not filter_device: + filter_device = "filter_1" # 默认过滤器 + + # 查找转移泵(用于转移溶剂) + transfer_pump = None + for node in G.nodes(): + node_data = G.nodes[node] + node_class = node_data.get('class', '') or '' + if 'transfer' in node_class.lower() and 'pump' in node_class.lower(): + transfer_pump = node + break + + if not transfer_pump: + transfer_pump = "transfer_pump_1" # 默认转移泵 + + # 查找搅拌器(如果需要搅拌) + stirrer_device = None + if stir: + for node in G.nodes(): + node_data = G.nodes[node] + node_class = node_data.get('class', '') or '' + if 'stirrer' in node_class.lower(): + stirrer_device = node + break + + if not stirrer_device: + stirrer_device = "stirrer_1" # 默认搅拌器 debug_print(f"设备配置:") debug_print(f" - 溶剂源: {solvent_source}") - debug_print(f" - 滤液容器: {actual_filtrate_vessel}") - debug_print(f" - 转移泵: {pump_device}") + debug_print(f" - 转移泵: {transfer_pump}") debug_print(f" - 过滤器: {filter_device}") + debug_print(f" - 搅拌器: {stirrer_device}") + debug_print(f" - 滤液容器: {actual_filtrate_vessel}") except Exception as e: debug_print(f"❌ 设备查找失败: {str(e)}") raise ValueError(f"设备查找失败: {str(e)}") # === 执行清洗循环 === - debug_print("步骤3: 执行清洗循环...") + debug_print("步骤4: 执行清洗循环...") - for cycle in range(repeats): - debug_print(f"=== 第 {cycle+1}/{repeats} 次清洗 ===") + for cycle in range(final_repeats): + debug_print(f"=== 第 {cycle+1}/{final_repeats} 次清洗 ===") + + # 🔧 修复:分解为基础动作序列 - # 添加清洗溶剂 - debug_print(f"添加清洗溶剂: {solvent_source} -> {vessel}") + # 1. 加入清洗溶剂 + debug_print(f" 步骤 {cycle+1}.1: 加入清洗溶剂") + # 🔧 修复:使用 pump protocol 而不是直接调用 transfer action + try: + from .pump_protocol import generate_pump_protocol_with_rinsing + + transfer_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=solvent_source, + to_vessel=vessel, + volume=final_volume, + amount="", + time=0.0, + viscous=False, + rinsing_solvent="", + rinsing_volume=0.0, + rinsing_repeats=0, + solid=False, + flowrate=2.5, + transfer_flowrate=0.5, + rate_spec="", + event=event, + through="" + ) + + if transfer_actions: + action_sequence.extend(transfer_actions) + debug_print(f"✅ 添加了 {len(transfer_actions)} 个转移动作") + else: + debug_print("⚠️ 转移协议返回空序列") + + except Exception as e: + debug_print(f"❌ 转移失败: {str(e)}") + # 继续执行,可能有其他问题 - wash_action = { + # 2. 搅拌混合(如果需要) + if stir and stirrer_device: + debug_print(f" 步骤 {cycle+1}.2: 搅拌混合") + stir_time = max(time, 30.0) if time > 0 else 60.0 # 默认搅拌1分钟 + + stir_action = { + "device_id": stirrer_device, + "action_name": "stir", + "action_kwargs": { + "vessel": vessel, + "time": str(int(stir_time)), # 转换为字符串格式 + "event": event, + "time_spec": "", + "stir_time": stir_time, + "stir_speed": stir_speed, + "settling_time": 30.0 + } + } + action_sequence.append(stir_action) + + # 3. 过滤分离 + debug_print(f" 步骤 {cycle+1}.3: 过滤分离") + filter_action = { "device_id": filter_device, - "action_name": "wash_solid", + "action_name": "filter", "action_kwargs": { "vessel": vessel, - "solvent": solvent, - "volume": volume, "filtrate_vessel": actual_filtrate_vessel, + "stir": False, # 过滤时不搅拌 + "stir_speed": 0.0, "temp": temp, - "stir": stir, - "stir_speed": stir_speed, - "time": time, - "repeats": 1 # 每次循环只做1次 + "continue_heatchill": False, + "volume": final_volume } } - action_sequence.append(wash_action) + action_sequence.append(filter_action) - # 等待清洗完成 + # 4. 等待完成 + wait_time = 10.0 action_sequence.append({ "action_name": "wait", - "action_kwargs": {"time": max(10.0, time * 0.1)} + "action_kwargs": {"time": wait_time} }) # === 总结 === - debug_print("=" * 50) + debug_print("=" * 60) 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) + debug_print(f"清洗体积: {final_volume}mL") + debug_print(f"重复次数: {final_repeats}") + debug_print(f"滤液收集: {actual_filtrate_vessel}") + debug_print(f"事件标识: {event}") + debug_print("=" * 60) return action_sequence +# 删除不需要的函数,简化代码 +def find_wash_solid_device(G: nx.DiGraph) -> str: + """ + 🗑️ 已弃用:WashSolid不再作为单一设备动作 + 现在分解为基础动作序列:transfer + stir + filter + """ + debug_print("⚠️ find_wash_solid_device 已弃用,使用基础动作序列") + return "OrganicSynthesisStation" # 兼容性返回 + # === 便捷函数 === -def generate_quick_wash_protocol( +def generate_water_wash_protocol( G: nx.DiGraph, vessel: str, - solvent: str, - volume: float, + volume: float = 50.0, **kwargs ) -> List[Dict[str, Any]]: - """快速清洗:1次,室温,不搅拌""" + """水洗协议:用水清洗固体""" return generate_wash_solid_protocol( - G, vessel, solvent, volume, - repeats=1, temp=25.0, stir=False, **kwargs + G, vessel, "water", volume, **kwargs ) -def generate_thorough_wash_protocol( +def generate_organic_wash_protocol( G: nx.DiGraph, vessel: str, solvent: str, - volume: float, + volume: float = 30.0, **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 + G, vessel, solvent, volume, **kwargs ) -def generate_gentle_wash_protocol( +def generate_thorough_wash_protocol( G: nx.DiGraph, vessel: str, solvent: str, - volume: float, + volume: float = 100.0, **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 + repeats=4, temp=50.0, stir=True, stir_speed=200.0, time=300.0, **kwargs + ) \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_filter.py b/unilabos/devices/virtual/virtual_filter.py index ca2e8b2cc..d70c854a8 100644 --- a/unilabos/devices/virtual/virtual_filter.py +++ b/unilabos/devices/virtual/virtual_filter.py @@ -67,6 +67,16 @@ async def filter( volume: float = 0.0 ) -> bool: """Execute filter action - 完全按照 Filter.action 参数""" + + # 🔧 新增:温度自动调整 + original_temp = temp + if temp == 0.0: + temp = 25.0 # 0度自动设置为室温 + self.logger.info(f"温度自动调整: {original_temp}°C → {temp}°C (室温)") + elif temp < 4.0: + temp = 4.0 # 小于4度自动设置为4度 + self.logger.info(f"温度自动调整: {original_temp}°C → {temp}°C (最低温度)") + self.logger.info(f"Filter: vessel={vessel}, filtrate_vessel={filtrate_vessel}") self.logger.info(f" stir={stir}, stir_speed={stir_speed}, temp={temp}") self.logger.info(f" continue_heatchill={continue_heatchill}, volume={volume}") diff --git a/unilabos/devices/virtual/virtual_rotavap.py b/unilabos/devices/virtual/virtual_rotavap.py index fbbfe9f07..9f576b5ec 100644 --- a/unilabos/devices/virtual/virtual_rotavap.py +++ b/unilabos/devices/virtual/virtual_rotavap.py @@ -3,6 +3,9 @@ import time as time_module from typing import Dict, Any, Optional +def debug_print(message): + """调试输出""" + print(f"[ROTAVAP] {message}", flush=True) class VirtualRotavap: """Virtual rotary evaporator device - 简化版,只保留核心功能""" @@ -70,12 +73,19 @@ async def evaporate( vessel: str, pressure: float = 0.1, temp: float = 60.0, - time: float = 1800.0, # 30分钟默认 + time: float = 1800.0, stir_speed: float = 100.0, - solvent: str = "", # 🔧 新增参数 - **kwargs # 🔧 接受额外参数 + solvent: str = "", + **kwargs ) -> bool: - """Execute evaporate action - 兼容性增强版""" + """Execute evaporate action - 简化版""" + + # 🔧 简化处理:如果vessel就是设备自己,直接操作 + if vessel == self.device_id: + debug_print(f"在设备 {self.device_id} 上直接执行蒸发操作") + actual_vessel = self.device_id + else: + actual_vessel = vessel # 参数预处理 if solvent: @@ -86,8 +96,12 @@ async def evaporate( temp = max(temp, 80.0) pressure = max(pressure, 0.2) self.logger.info("水系溶剂:调整参数") + elif any(s in solvent_lower for s in ['ethanol', 'methanol', 'acetone']): + temp = min(temp, 50.0) + pressure = min(pressure, 0.05) + 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}") + self.logger.info(f"Evaporate: vessel={actual_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: @@ -96,6 +110,9 @@ async def evaporate( self.data.update({ "status": f"Error: {error_msg}", "rotavap_state": "Error", + "current_temp": 25.0, + "progress": 0.0, + "evaporated_volume": 0.0, "message": error_msg }) return False @@ -106,6 +123,9 @@ async def evaporate( self.data.update({ "status": f"Error: {error_msg}", "rotavap_state": "Error", + "current_temp": 25.0, + "progress": 0.0, + "evaporated_volume": 0.0, "message": error_msg }) return False @@ -116,13 +136,16 @@ async def evaporate( self.data.update({ "status": f"Error: {error_msg}", "rotavap_state": "Error", + "current_temp": 25.0, + "progress": 0.0, + "evaporated_volume": 0.0, "message": error_msg }) return False # 开始蒸发 self.data.update({ - "status": f"蒸发中: {vessel}", + "status": f"蒸发中: {actual_vessel}", "rotavap_state": "Evaporating", "current_temp": temp, "target_temp": temp, @@ -131,7 +154,7 @@ async def evaporate( "remaining_time": time, "progress": 0.0, "evaporated_volume": 0.0, - "message": f"Evaporating {vessel} at {temp}°C, {pressure} bar, {stir_speed} RPM" + "message": f"Evaporating {actual_vessel} at {temp}°C, {pressure} bar, {stir_speed} RPM" }) try: @@ -148,12 +171,13 @@ async def evaporate( # 模拟蒸发体积 evaporated_vol = progress * 0.8 # 假设最多蒸发80mL - # 更新状态 + # 🔧 更新状态 - 确保包含所有必需字段 self.data.update({ "remaining_time": remaining, - "progress": progress, - "evaporated_volume": evaporated_vol, - "status": f"蒸发中: {vessel} | {temp}°C | {pressure} bar | {progress:.1f}% | 剩余: {remaining:.0f}s", + "progress": progress, # 确保这个字段存在 + "evaporated_volume": evaporated_vol, # 确保这个字段存在 + "current_temp": temp, # 确保这个字段存在 + "status": f"蒸发中: {actual_vessel} | {temp}°C | {pressure} bar | {progress:.1f}% | 剩余: {remaining:.0f}s", "message": f"Evaporating: {progress:.1f}% complete, {remaining:.0f}s remaining" }) @@ -167,18 +191,18 @@ async def evaporate( # 蒸发完成 final_evaporated = 80.0 self.data.update({ - "status": f"蒸发完成: {vessel} | 蒸发量: {final_evaporated:.1f}mL", + "status": f"蒸发完成: {actual_vessel} | 蒸发量: {final_evaporated:.1f}mL", "rotavap_state": "Completed", "evaporated_volume": final_evaporated, "progress": 100.0, + "current_temp": temp, # 保持温度信息 "remaining_time": 0.0, - "current_temp": 25.0, # 冷却下来 "rotation_speed": 0.0, # 停止旋转 "vacuum_pressure": 1.0, # 恢复大气压 - "message": f"Evaporation completed: {final_evaporated}mL evaporated from {vessel}" + "message": f"Evaporation completed: {final_evaporated}mL evaporated from {actual_vessel}" }) - self.logger.info(f"Evaporation completed: {final_evaporated}mL evaporated from {vessel}") + self.logger.info(f"Evaporation completed: {final_evaporated}mL evaporated from {actual_vessel}") return True except Exception as e: @@ -189,6 +213,8 @@ async def evaporate( "status": f"蒸发错误: {str(e)}", "rotavap_state": "Error", "current_temp": 25.0, + "progress": 0.0, + "evaporated_volume": 0.0, "rotation_speed": 0.0, "vacuum_pressure": 1.0, "message": f"Evaporation failed: {str(e)}" diff --git a/unilabos/devices/virtual/virtual_solenoid_valve.py b/unilabos/devices/virtual/virtual_solenoid_valve.py index f25cc843b..54a1e6d05 100644 --- a/unilabos/devices/virtual/virtual_solenoid_valve.py +++ b/unilabos/devices/virtual/virtual_solenoid_valve.py @@ -43,10 +43,25 @@ def valve_state(self) -> str: def is_open(self) -> bool: return self._is_open - def get_valve_position(self) -> str: + @property + def valve_position(self) -> str: """获取阀门位置状态""" return "OPEN" if self._is_open else "CLOSED" + @property + def state(self) -> dict: + """获取阀门完整状态""" + return { + "device_id": self.device_id, + "port": self.port, + "voltage": self.voltage, + "response_time": self.response_time, + "is_open": self._is_open, + "valve_state": self._valve_state, + "status": self._status, + "position": self.valve_position + } + async def set_valve_position(self, command: str = None, **kwargs): """ 设置阀门位置 - ROS动作接口 @@ -91,7 +106,7 @@ async def set_valve_position(self, command: str = None, **kwargs): return { "success": True, "message": result_msg, - "valve_position": self.get_valve_position() + "valve_position": self.valve_position } async def open(self, **kwargs): @@ -102,21 +117,25 @@ async def close(self, **kwargs): """关闭电磁阀 - ROS动作接口""" return await self.set_valve_position(command="CLOSED") - async def set_state(self, command: Union[bool, str], **kwargs): + async def set_status(self, string: str = None, **kwargs): """ - 设置阀门状态 - 兼容 SendCmd 类型 + 设置阀门状态 - 兼容 StrSingleInput 类型 Args: - command: True/False 或 "open"/"close" + string: "ON"/"OFF" 或 "OPEN"/"CLOSED" """ - if isinstance(command, bool): - cmd_str = "OPEN" if command else "CLOSED" - elif isinstance(command, str): - cmd_str = command + if string is None: + return {"success": False, "message": "Missing string parameter"} + + # 将 string 参数转换为 command 参数 + if string.upper() in ["ON", "OPEN"]: + command = "OPEN" + elif string.upper() in ["OFF", "CLOSED"]: + command = "CLOSED" else: - return {"success": False, "message": "Invalid command type"} + command = string - return await self.set_valve_position(command=cmd_str) + return await self.set_valve_position(command=command) def toggle(self): """切换阀门状态""" @@ -129,19 +148,6 @@ def is_closed(self) -> bool: """检查阀门是否关闭""" return not self._is_open - def get_state(self) -> dict: - """获取阀门完整状态""" - return { - "device_id": self.device_id, - "port": self.port, - "voltage": self.voltage, - "response_time": self.response_time, - "is_open": self._is_open, - "valve_state": self._valve_state, - "status": self._status, - "position": self.get_valve_position() - } - async def reset(self): """重置阀门到关闭状态""" return await self.close() \ No newline at end of file diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml index 1bbc5f7f1..3a98e566c 100644 --- a/unilabos/registry/devices/virtual_device.yaml +++ b/unilabos/registry/devices/virtual_device.yaml @@ -2376,10 +2376,8 @@ virtual_rotavap: type: UniLabJsonCommandAsync evaporate: feedback: - current_temp: current_temp - evaporated_volume: evaporated_volume - progress: progress status: status + current_device: current_device goal: pressure: pressure stir_speed: stir_speed @@ -3180,6 +3178,54 @@ virtual_solenoid_valve: title: StrSingleInput type: object type: StrSingleInput + set_valve_position: + feedback: {} + goal: + command: command + goal_default: + command: '' + handles: [] + result: + success: success + message: message + valve_position: valve_position + schema: + description: ROS Action SendCmd 的 JSON Schema + properties: + feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 + properties: {} + required: [] + title: SendCmd_Feedback + type: object + goal: + description: Action 目标 - 从客户端发送到服务器 + properties: + command: + type: string + required: + - command + title: SendCmd_Goal + type: object + result: + description: Action 结果 - 完成后从服务器发送到客户端 + properties: + success: + type: boolean + message: + type: string + valve_position: + type: string + required: + - success + - message + title: SendCmd_Result + type: object + required: + - goal + title: SendCmd + type: object + type: SendCmd module: unilabos.devices.virtual.virtual_solenoid_valve:VirtualSolenoidValve status_types: is_open: bool diff --git a/unilabos_msgs/action/Add.action b/unilabos_msgs/action/Add.action index 0c6ed2a85..021199ac0 100644 --- a/unilabos_msgs/action/Add.action +++ b/unilabos_msgs/action/Add.action @@ -1,14 +1,14 @@ # Goal - 添加试剂的目标参数 string vessel # 目标容器(必需) string reagent # 试剂名称(必需) -float64 volume # 体积 (mL,可选) -float64 mass # 质量 (g,可选) -string amount # 数量描述 (可选) -float64 time # 添加时间 (s,可选) +string volume # 体积(如 "2.7 mL",可选) +string mass # 质量(如 "19.3 g",可选) +string amount # 数量描述(可选) +string time # 添加时间(如 "1 h", "20 min",可选) bool stir # 是否搅拌(可选) float64 stir_speed # 搅拌速度 (RPM,可选) bool viscous # 是否为粘性液体(可选) -string purpose # 添加目的 (可选) +string purpose # 添加目的(可选) string event # 事件标识(如 'A', 'B',可选) string mol # 摩尔数(如 '0.28 mol', '16.2 mmol',可选) string rate_spec # 速率规格(如 'portionwise', 'dropwise',可选) diff --git a/unilabos_msgs/action/Dissolve.action b/unilabos_msgs/action/Dissolve.action index e2d2c43c4..f070a611c 100644 --- a/unilabos_msgs/action/Dissolve.action +++ b/unilabos_msgs/action/Dissolve.action @@ -1,14 +1,15 @@ # Goal - 溶解操作的目标参数 string vessel # 装有要溶解物质的容器名称(必需) string solvent # 用于溶解物质的溶剂名称(可选) -float64 volume # 溶剂的体积(可选) -string amount # 要溶解物质的量(可选) -float64 temp # 溶解时的温度(可选) -float64 time # 溶解的时间(可选) -float64 stir_speed # 搅拌速度(可选) +string volume # 溶剂的体积(如 "10 mL",可选) +string amount # 要溶解物质的量描述(可选) +string temp # 溶解时的温度(如 "60 °C", "room temperature",可选) +string time # 溶解的时间(如 "30 min", "1 h",可选) +float64 stir_speed # 搅拌速度(可选,默认300 RPM) string mass # 物质质量(如 "2.9 g",可选) string mol # 物质摩尔数(如 "0.12 mol",可选) string reagent # 试剂名称(可选) +string event # 事件标识(如 'A', 'B',可选) --- # Result - 操作结果 bool success # 操作是否成功 diff --git a/unilabos_msgs/action/Separate.action b/unilabos_msgs/action/Separate.action index a487fea72..fc185b6ed 100644 --- a/unilabos_msgs/action/Separate.action +++ b/unilabos_msgs/action/Separate.action @@ -1,21 +1,21 @@ # Goal - 分离操作的目标参数 -string purpose # 分离目的 ('wash', 'extract', 'separate',必需) -string product_phase # 产物相 ('top', 'bottom',必需) +string vessel # 分离容器名称(XDL参数,必需) +string purpose # 分离目的 ('wash', 'extract', 'separate',可选) +string product_phase # 产物相 ('top', 'bottom',可选) string from_vessel # 源容器(可选) -string separation_vessel # 分离容器(可选) +string separation_vessel # 分离容器(与vessel同义,可选) string to_vessel # 目标容器(可选) string waste_phase_to_vessel # 废相目标容器(可选) -string solvent # 溶剂名称(可选) -float64 solvent_volume # 溶剂体积(可选) -string through # 通过材料(如 'celite',可选) -int32 repeats # 重复次数(可选) -float64 stir_time # 搅拌时间(可选) -float64 stir_speed # 搅拌速度(可选) -float64 settling_time # 沉降时间(可选) -string vessel # 分离容器名称(XDL参数,可选) -string volume # 体积规格(XDL参数,可选) string product_vessel # 产物收集容器(XDL参数,可选) string waste_vessel # 废液收集容器(XDL参数,可选) +string solvent # 溶剂名称(可选) +string solvent_volume # 溶剂体积(如 "200 mL",可选) +string volume # 体积规格(XDL参数,如 "?",可选) +string through # 通过材料(如 'celite',可选) +int32 repeats # 重复次数(可选,默认1) +float64 stir_time # 搅拌时间(可选,默认30秒) +float64 stir_speed # 搅拌速度(可选,默认300 RPM) +float64 settling_time # 沉降时间(可选,默认300秒) --- # Result - 操作结果 bool success # 操作是否成功 @@ -24,6 +24,4 @@ string return_info --- # Feedback - 实时反馈 string status # 当前状态描述 -string current_device # 当前设备 -builtin_interfaces/Duration time_spent # 已用时间 -builtin_interfaces/Duration time_remaining # 剩余时间 +float64 progress # 进度百分比 (0-100) diff --git a/unilabos_msgs/action/WashSolid.action b/unilabos_msgs/action/WashSolid.action index 7aa3c6e13..8ef159d73 100644 --- a/unilabos_msgs/action/WashSolid.action +++ b/unilabos_msgs/action/WashSolid.action @@ -1,13 +1,17 @@ # Goal - 固体清洗操作的目标参数 string vessel # 装有固体的容器名称(必需) string solvent # 清洗溶剂名称(必需) -float64 volume # 清洗溶剂体积(必需) +string volume # 🔧 修改:体积(支持数字和带单位的字符串) string filtrate_vessel # 滤液收集容器(可选,默认"") float64 temp # 清洗温度(可选,默认25.0) bool stir # 是否搅拌(可选,默认false) float64 stir_speed # 搅拌速度(可选,默认0.0) float64 time # 清洗时间(可选,默认0.0) -int32 repeats # 重复次数(可选,默认1) +int32 repeats # 重复次数(与repeats_spec二选一) +string volume_spec # 体积规格(优先级高于volume) +string repeats_spec # 重复次数规格(优先级高于repeats) +string mass # 固体质量描述(可选) +string event # 事件标识符(可选) --- # Result - 操作结果 bool success # 操作是否成功 From 23eb1139a98ab8b1e621e296b8ab47ad40b87a98 Mon Sep 17 00:00:00 2001 From: KCFeng425 <2100011801@stu.pku.edu.cn> Date: Thu, 10 Jul 2025 18:25:13 +0800 Subject: [PATCH 7/9] =?UTF-8?q?=E4=BF=AE=E8=A1=A5=E4=BA=86=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E5=8D=95=E4=BD=8D=E5=A4=84=E7=90=86=EF=BC=8Cbump=20ve?= =?UTF-8?q?rsion=20to=200.9.11?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- README_zh.md | 2 +- recipes/ros-humble-unilabos-msgs/recipe.yaml | 2 +- recipes/unilabos/recipe.yaml | 2 +- setup.py | 2 +- .../comprehensive_protocol/checklist.md | 10 +- unilabos/compile/evaporate_protocol.py | 122 +++++++-- unilabos/compile/heatchill_protocol.py | 172 ++++++++---- unilabos/compile/recrystallize_protocol.py | 121 +++++++-- unilabos/compile/stir_protocol.py | 154 +++++++++-- unilabos/compile/utils/unit_parser.py | 206 +++++++++++++++ unilabos/compile/wash_solid_protocol.py | 247 +++++++++++++++--- unilabos/devices/virtual/virtual_heatchill.py | 67 ++++- unilabos/devices/virtual/virtual_rotavap.py | 2 +- unilabos/messages/__init__.py | 2 +- unilabos/registry/devices/virtual_device.yaml | 4 +- unilabos_msgs/action/Evaporate.action | 12 +- unilabos_msgs/action/HeatChill.action | 2 +- unilabos_msgs/action/Recrystallize.action | 10 +- unilabos_msgs/action/Stir.action | 4 +- unilabos_msgs/action/WashSolid.action | 4 +- 21 files changed, 962 insertions(+), 187 deletions(-) create mode 100644 unilabos/compile/utils/unit_parser.py diff --git a/README.md b/README.md index 5d14705a8..918e6faeb 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n environment_name # Currently, you need to install the `unilabos_msgs` package # You can download the system-specific package from the Release page -conda install ros-humble-unilabos-msgs-10-xxxxx.tar.bz2 +conda install ros-humble-unilabos-msgs-0.9.10-xxxxx.tar.bz2 # Install PyLabRobot and other prerequisites git clone https://github.com/PyLabRobot/pylabrobot plr_repo diff --git a/README_zh.md b/README_zh.md index 5bd588bbd..416385311 100644 --- a/README_zh.md +++ b/README_zh.md @@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n 环境名 # 现阶段,需要安装 `unilabos_msgs` 包 # 可以前往 Release 页面下载系统对应的包进行安装 -conda install ros-humble-unilabos-msgs-0.9.10-xxxxx.tar.bz2 +conda install ros-humble-unilabos-msgs-0.9.11-xxxxx.tar.bz2 # 安装PyLabRobot等前置 git clone https://github.com/PyLabRobot/pylabrobot plr_repo diff --git a/recipes/ros-humble-unilabos-msgs/recipe.yaml b/recipes/ros-humble-unilabos-msgs/recipe.yaml index 0445115d0..5807be8a1 100644 --- a/recipes/ros-humble-unilabos-msgs/recipe.yaml +++ b/recipes/ros-humble-unilabos-msgs/recipe.yaml @@ -1,6 +1,6 @@ package: name: ros-humble-unilabos-msgs - version: 0.9.10 + version: 0.9.11 source: path: ../../unilabos_msgs folder: ros-humble-unilabos-msgs/src/work diff --git a/recipes/unilabos/recipe.yaml b/recipes/unilabos/recipe.yaml index a5768558b..2f72c32a6 100644 --- a/recipes/unilabos/recipe.yaml +++ b/recipes/unilabos/recipe.yaml @@ -1,6 +1,6 @@ package: name: unilabos - version: "0.9.10" + version: "0.9.11" source: path: ../.. diff --git a/setup.py b/setup.py index 80163eab1..8a7eebd25 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name=package_name, - version='0.9.10', + version='0.9.11', packages=find_packages(), include_package_data=True, install_requires=['setuptools'], diff --git a/test/experiments/comprehensive_protocol/checklist.md b/test/experiments/comprehensive_protocol/checklist.md index 5dee23a15..67b4c3b65 100644 --- a/test/experiments/comprehensive_protocol/checklist.md +++ b/test/experiments/comprehensive_protocol/checklist.md @@ -247,4 +247,12 @@ class HydrogenateProtocol(BaseModel): 还差 - \ No newline at end of file + + + +单位修复: + evaporate + heatchill + recrysitallize + stir + wash solid \ No newline at end of file diff --git a/unilabos/compile/evaporate_protocol.py b/unilabos/compile/evaporate_protocol.py index e3ffb8623..4eedb3cdd 100644 --- a/unilabos/compile/evaporate_protocol.py +++ b/unilabos/compile/evaporate_protocol.py @@ -1,6 +1,7 @@ -from typing import List, Dict, Any, Optional +from typing import List, Dict, Any, Optional, Union import networkx as nx import logging +import re logger = logging.getLogger(__name__) @@ -9,6 +10,63 @@ def debug_print(message): print(f"[EVAPORATE] {message}", flush=True) logger.info(f"[EVAPORATE] {message}") +def parse_time_input(time_input: Union[str, float]) -> float: + """ + 解析时间输入,支持带单位的字符串 + + Args: + time_input: 时间输入(如 "3 min", "180", "0.5 h" 等) + + Returns: + float: 时间(秒) + """ + if isinstance(time_input, (int, float)): + return float(time_input) + + if not time_input or not str(time_input).strip(): + return 180.0 # 默认3分钟 + + time_str = str(time_input).lower().strip() + debug_print(f"解析时间输入: '{time_str}'") + + # 处理未知时间 + if time_str in ['?', 'unknown', 'tbd']: + default_time = 180.0 # 默认3分钟 + debug_print(f"检测到未知时间,使用默认值: {default_time}s") + return default_time + + # 移除空格并提取数字和单位 + time_clean = re.sub(r'\s+', '', time_str) + + # 匹配数字和单位的正则表达式 + match = re.match(r'([0-9]*\.?[0-9]+)\s*(s|sec|second|min|minute|h|hr|hour|d|day)?', time_clean) + + if not match: + # 如果无法解析,尝试直接转换为数字(默认秒) + try: + value = float(time_str) + debug_print(f"时间解析: {time_str} → {value}s(无单位,默认秒)") + return value + except ValueError: + debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值180s") + return 180.0 + + value = float(match.group(1)) + unit = match.group(2) or 's' # 默认单位为秒 + + # 转换为秒 + if unit in ['min', 'minute']: + time_sec = value * 60.0 # min -> s + elif unit in ['h', 'hr', 'hour']: + time_sec = value * 3600.0 # h -> s + elif unit in ['d', 'day']: + time_sec = value * 86400.0 # d -> s + else: # s, sec, second 或默认 + time_sec = value # 已经是s + + debug_print(f"时间转换: {value}{unit} → {time_sec}s") + return time_sec + def find_rotavap_device(G: nx.DiGraph, vessel: str = None) -> Optional[str]: """ 在组态图中查找旋转蒸发仪设备 @@ -111,20 +169,20 @@ def generate_evaporate_protocol( vessel: str, pressure: float = 0.1, temp: float = 60.0, - time: float = 180.0, + time: Union[str, float] = "180", # 🔧 修改:支持字符串时间 stir_speed: float = 100.0, solvent: str = "", - **kwargs # 接受任意额外参数,增强兼容性 + **kwargs ) -> List[Dict[str, Any]]: """ - 生成蒸发操作的协议序列 + 生成蒸发操作的协议序列 - 支持单位 Args: G: 设备图 vessel: 容器名称或旋转蒸发仪名称 pressure: 真空度 (bar),默认0.1 temp: 加热温度 (°C),默认60 - time: 蒸发时间 (秒),默认180 + time: 蒸发时间(支持 "3 min", "180", "0.5 h" 等) stir_speed: 旋转速度 (RPM),默认100 solvent: 溶剂名称(用于参数优化) **kwargs: 其他参数(兼容性) @@ -134,12 +192,12 @@ def generate_evaporate_protocol( """ debug_print("=" * 50) - debug_print("开始生成蒸发协议") + 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" - time: {time} (类型: {type(time)})") debug_print(f" - stir_speed: {stir_speed} RPM") debug_print(f" - solvent: '{solvent}'") debug_print("=" * 50) @@ -177,8 +235,15 @@ def generate_evaporate_protocol( debug_print(f"容器 '{vessel}' 不存在或类型不正确,使用旋转蒸发仪设备: {rotavap_device}") target_vessel = rotavap_device - # === 步骤3: 参数验证和修正 === - debug_print("步骤3: 参数验证和修正...") + # === 🔧 新增:步骤3:单位解析处理 === + debug_print("步骤3: 单位解析处理...") + + # 解析时间 + final_time = parse_time_input(time) + debug_print(f"时间解析: {time} → {final_time}s ({final_time/60:.1f}分钟)") + + # === 步骤4: 参数验证和修正 === + debug_print("步骤4: 参数验证和修正...") # 修正参数范围 if pressure <= 0 or pressure > 1.0: @@ -189,9 +254,9 @@ def generate_evaporate_protocol( debug_print(f"温度 {temp}°C 超出范围,修正为 60°C") temp = 60.0 - if time <= 0: - debug_print(f"时间 {time}s 无效,修正为 1800s") - time = 1800.0 + if final_time <= 0: + debug_print(f"时间 {final_time}s 无效,修正为 180s") + final_time = 180.0 if stir_speed < 10.0 or stir_speed > 300.0: debug_print(f"旋转速度 {stir_speed} RPM 超出范围,修正为 100 RPM") @@ -215,10 +280,10 @@ def generate_evaporate_protocol( pressure = min(pressure, 0.01) debug_print("高沸点溶剂:提高温度,降低真空度") - debug_print(f"最终参数: pressure={pressure}, temp={temp}, time={time}, stir_speed={stir_speed}") + debug_print(f"最终参数: pressure={pressure}, temp={temp}, time={final_time}, stir_speed={stir_speed}") - # === 步骤4: 生成动作序列 === - debug_print("步骤4: 生成动作序列...") + # === 步骤5: 生成动作序列 === + debug_print("步骤5: 生成动作序列...") action_sequence = [] @@ -237,7 +302,7 @@ def generate_evaporate_protocol( "vessel": target_vessel, "pressure": pressure, "temp": temp, - "time": time, + "time": final_time, "stir_speed": stir_speed, "solvent": solvent } @@ -256,7 +321,7 @@ def generate_evaporate_protocol( debug_print(f"总动作数: {len(action_sequence)}") debug_print(f"旋转蒸发仪: {rotavap_device}") debug_print(f"目标容器: {target_vessel}") - debug_print(f"蒸发参数: {pressure} bar, {temp}°C, {time}s, {stir_speed} RPM") + debug_print(f"蒸发参数: {pressure} bar, {temp}°C, {final_time}s, {stir_speed} RPM") debug_print("=" * 50) return action_sequence @@ -273,7 +338,7 @@ def generate_quick_evaporate_protocol( G, vessel, pressure=0.2, temp=40.0, - time=900.0, + time="15 min", # 🔧 使用带单位的时间 stir_speed=80.0, **kwargs ) @@ -288,7 +353,7 @@ def generate_gentle_evaporate_protocol( G, vessel, pressure=0.1, temp=50.0, - time=2700.0, + time="45 min", # 🔧 使用带单位的时间 stir_speed=60.0, **kwargs ) @@ -303,7 +368,7 @@ def generate_high_vacuum_evaporate_protocol( G, vessel, pressure=0.01, temp=35.0, - time=3600.0, + time="1 h", # 🔧 使用带单位的时间 stir_speed=120.0, **kwargs ) @@ -318,7 +383,22 @@ def generate_standard_evaporate_protocol( G, vessel, pressure=0.1, temp=60.0, - time=1800.0, + time="3 min", # 🔧 使用带单位的时间 stir_speed=100.0, **kwargs ) + +# 测试函数 +def test_time_parsing(): + """测试时间解析功能""" + print("=== EVAPORATE 时间解析测试 ===") + + test_times = ["3 min", "180", "0.5 h", "2 hours", "?", "unknown", "1.5", "30 s"] + for time_str in test_times: + result = parse_time_input(time_str) + print(f"时间解析: '{time_str}' → {result}s ({result/60:.1f}分钟)") + + print("✅ 测试完成") + +if __name__ == "__main__": + test_time_parsing() diff --git a/unilabos/compile/heatchill_protocol.py b/unilabos/compile/heatchill_protocol.py index 3e7f551f8..297015e34 100644 --- a/unilabos/compile/heatchill_protocol.py +++ b/unilabos/compile/heatchill_protocol.py @@ -1,4 +1,4 @@ -from typing import List, Dict, Any +from typing import List, Dict, Any, Union import networkx as nx import logging import re @@ -10,6 +10,97 @@ def debug_print(message): print(f"[HEATCHILL] {message}", flush=True) logger.info(f"[HEATCHILL] {message}") +def parse_time_with_units(time_input: Union[str, float, int], default_unit: str = "s") -> float: + """ + 解析带单位的时间输入 + + Args: + time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0) + default_unit: 默认单位(默认为秒) + + Returns: + float: 时间(秒) + """ + if not time_input: + return 0.0 + + # 处理数值输入 + if isinstance(time_input, (int, float)): + result = float(time_input) + debug_print(f"数值时间输入: {time_input} → {result}s(默认单位)") + return result + + # 处理字符串输入 + time_str = str(time_input).lower().strip() + debug_print(f"解析时间字符串: '{time_str}'") + + # 处理特殊值 + if time_str in ['?', 'unknown', 'tbd', 'to be determined']: + default_time = 300.0 # 5分钟默认值 + debug_print(f"检测到未知时间,使用默认值: {default_time}s") + return default_time + + # 如果是纯数字,使用默认单位 + try: + value = float(time_str) + if default_unit == "s": + result = value + elif default_unit in ["min", "minute"]: + result = value * 60.0 + elif default_unit in ["h", "hour"]: + result = value * 3600.0 + else: + result = value # 默认秒 + debug_print(f"纯数字输入: {time_str} → {result}s(单位: {default_unit})") + return result + except ValueError: + pass + + # 使用正则表达式匹配数字和单位 + pattern = r'(\d+\.?\d*)\s*([a-z]*)' + match = re.match(pattern, time_str) + + if not match: + debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 60s") + return 60.0 + + value = float(match.group(1)) + unit = match.group(2) or default_unit + + # 单位转换映射 + unit_multipliers = { + # 秒 + 's': 1.0, + 'sec': 1.0, + 'second': 1.0, + 'seconds': 1.0, + + # 分钟 + 'm': 60.0, + 'min': 60.0, + 'mins': 60.0, + 'minute': 60.0, + 'minutes': 60.0, + + # 小时 + 'h': 3600.0, + 'hr': 3600.0, + 'hrs': 3600.0, + 'hour': 3600.0, + 'hours': 3600.0, + + # 天 + 'd': 86400.0, + 'day': 86400.0, + 'days': 86400.0, + } + + multiplier = unit_multipliers.get(unit, 1.0) + result = value * multiplier + + debug_print(f"时间解析: '{time_str}' → {value} {unit} → {result}s") + return result + def parse_temp_spec(temp_spec: str) -> float: """解析温度规格为具体温度""" if not temp_spec: @@ -117,7 +208,7 @@ def generate_heat_chill_protocol( G: nx.DiGraph, vessel: str, temp: float = 25.0, - time: float = 300.0, + time: Union[str, float] = "300", # 🔧 修改:支持字符串时间 temp_spec: str = "", time_spec: str = "", pressure: str = "", @@ -125,35 +216,18 @@ def generate_heat_chill_protocol( stir: bool = False, stir_speed: float = 300.0, purpose: str = "", - **kwargs # 🔧 接受额外参数,增强兼容性 + **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("开始生成加热冷却协议(支持单位)") 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" - time: {time} (类型: {type(time)})") debug_print(f" - temp_spec: {temp_spec}") debug_print(f" - time_spec: {time_spec}") debug_print(f" - pressure: {pressure}") @@ -176,6 +250,9 @@ def generate_heat_chill_protocol( if vessel not in G.nodes(): raise ValueError(f"容器 '{vessel}' 不存在于系统中") + # === 🔧 新增:单位解析处理 === + debug_print("步骤2: 单位解析处理...") + # 温度解析:优先使用 temp_spec,然后是 temp final_temp = temp if temp_spec: @@ -183,10 +260,12 @@ def generate_heat_chill_protocol( 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}分钟)") + final_time = parse_time_spec(time_spec) # 使用现有的time_spec解析 + debug_print(f"时间解析: '{time_spec}' → {final_time}s") + else: + final_time = parse_time_with_units(time, "s") + debug_print(f"时间解析: {time} → {final_time}s ({final_time/60:.1f}分钟)") # 参数范围验证 if final_temp < -50.0 or final_temp > 300.0: @@ -201,10 +280,10 @@ def generate_heat_chill_protocol( debug_print(f"搅拌速度 {stir_speed} RPM 超出范围,修正为 300 RPM") stir_speed = 300.0 - debug_print(f"✅ 参数验证通过") + debug_print(f"✅ 单位解析和参数验证通过") # === 查找加热设备 === - debug_print("步骤2: 查找加热设备...") + debug_print("步骤3: 查找加热设备...") try: heatchill_id = find_connected_heatchill(G, vessel) @@ -215,18 +294,18 @@ def generate_heat_chill_protocol( raise ValueError(f"无法找到加热设备: {str(e)}") # === 执行加热操作 === - debug_print("步骤3: 执行加热操作...") + debug_print("步骤4: 执行加热操作...") heatchill_action = { "device_id": heatchill_id, "action_name": "heat_chill", "action_kwargs": { "vessel": vessel, - "temp": final_temp, - "time": final_time, - "stir": stir, - "stir_speed": stir_speed, - "purpose": purpose or f"加热到 {final_temp}°C" + "temp": float(final_temp), # 🔧 确保是浮点数 + "time": float(final_time), # 🔧 确保是浮点数 + "stir": bool(stir), # 🔧 确保是布尔值 + "stir_speed": float(stir_speed), # 🔧 确保是浮点数 + "purpose": str(purpose or f"加热到 {final_temp}°C") # 🔧 确保是字符串 } } @@ -234,7 +313,7 @@ def generate_heat_chill_protocol( # === 总结 === debug_print("=" * 50) - debug_print(f"加热冷却协议生成完成") + debug_print(f"加热冷却协议生成完成(支持单位)") debug_print(f"总动作数: {len(action_sequence)}") debug_print(f"加热容器: {vessel}") debug_print(f"目标温度: {final_temp}°C") @@ -247,12 +326,11 @@ def generate_heat_chill_protocol( return action_sequence - def generate_heat_chill_to_temp_protocol( G: nx.DiGraph, vessel: str, temp: float = 25.0, - time: float = 300.0, + time: Union[str, float] = 300.0, # 🔧 也支持字符串 temp_spec: str = "", time_spec: str = "", pressure: str = "", @@ -269,7 +347,7 @@ def generate_heat_chill_to_temp_protocol( G: 设备图 vessel: 加热容器名称(必需) temp: 目标温度 (°C) - time: 加热时间 (秒) + time: 加热时间(支持字符串和数字) temp_spec: 温度规格(如 'room temperature', 'reflux') time_spec: 时间规格(如 'overnight', '2 h') pressure: 压力规格(如 '1 mbar'),不做特殊处理 @@ -288,7 +366,7 @@ def generate_heat_chill_to_temp_protocol( 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" - time: {time} (类型: {type(time)})") debug_print(f" - temp_spec: {temp_spec}") debug_print(f" - time_spec: {time_spec}") debug_print(f" - pressure: {pressure}") @@ -317,11 +395,13 @@ def generate_heat_chill_to_temp_protocol( 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}分钟)") + else: + final_time = parse_time_with_units(time, "s") + debug_print(f"时间解析: {time} → {final_time}s ({final_time/60:.1f}分钟)") # 参数范围验证 if final_temp < -50.0 or final_temp > 300.0: @@ -357,11 +437,11 @@ def generate_heat_chill_to_temp_protocol( "action_name": "heat_chill", "action_kwargs": { "vessel": vessel, - "temp": final_temp, - "time": final_time, - "stir": stir, - "stir_speed": stir_speed, - "purpose": purpose or f"加热到 {final_temp}°C" + "temp": float(final_temp), # 🔧 确保是浮点数 + "time": float(final_time), # 🔧 确保是浮点数 + "stir": bool(stir), # 🔧 确保是布尔值 + "stir_speed": float(stir_speed), # 🔧 确保是浮点数 + "purpose": str(purpose or f"加热到 {final_temp}°C") # 🔧 确保是字符串 } } diff --git a/unilabos/compile/recrystallize_protocol.py b/unilabos/compile/recrystallize_protocol.py index b69d88b09..4e5f5923a 100644 --- a/unilabos/compile/recrystallize_protocol.py +++ b/unilabos/compile/recrystallize_protocol.py @@ -1,8 +1,80 @@ import networkx as nx -from typing import List, Dict, Any, Tuple +import re +from typing import List, Dict, Any, Tuple, Union from .pump_protocol import generate_pump_protocol_with_rinsing +def parse_volume_with_units(volume_input: Union[str, float, int], default_unit: str = "mL") -> float: + """ + 解析带单位的体积输入 + + Args: + volume_input: 体积输入(如 "100 mL", "2.5 L", "500", "?", 100.0) + default_unit: 默认单位(默认为毫升) + + Returns: + float: 体积(毫升) + """ + if not volume_input: + return 0.0 + + # 处理数值输入 + if isinstance(volume_input, (int, float)): + result = float(volume_input) + print(f"RECRYSTALLIZE: 数值体积输入: {volume_input} → {result}mL(默认单位)") + return result + + # 处理字符串输入 + volume_str = str(volume_input).lower().strip() + print(f"RECRYSTALLIZE: 解析体积字符串: '{volume_str}'") + + # 处理特殊值 + if volume_str in ['?', 'unknown', 'tbd', 'to be determined']: + default_volume = 50.0 # 50mL默认值 + print(f"RECRYSTALLIZE: 检测到未知体积,使用默认值: {default_volume}mL") + return default_volume + + # 如果是纯数字,使用默认单位 + try: + value = float(volume_str) + if default_unit.lower() in ["ml", "milliliter"]: + result = value + elif default_unit.lower() in ["l", "liter"]: + result = value * 1000.0 + elif default_unit.lower() in ["μl", "ul", "microliter"]: + result = value / 1000.0 + else: + result = value # 默认mL + print(f"RECRYSTALLIZE: 纯数字输入: {volume_str} → {result}mL(单位: {default_unit})") + return result + except ValueError: + pass + + # 移除空格并提取数字和单位 + volume_clean = re.sub(r'\s+', '', volume_str) + + # 匹配数字和单位的正则表达式 + match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean) + + if not match: + print(f"RECRYSTALLIZE: ⚠️ 无法解析体积: '{volume_str}',使用默认值: 50mL") + return 50.0 + + value = float(match.group(1)) + unit = match.group(2) or default_unit.lower() + + # 转换为毫升 + if unit in ['l', 'liter']: + volume = value * 1000.0 # L -> mL + elif unit in ['μl', 'ul', 'microliter']: + volume = value / 1000.0 # μL -> mL + else: # ml, milliliter 或默认 + volume = value # 已经是mL + + print(f"RECRYSTALLIZE: 体积解析: '{volume_str}' → {value} {unit} → {volume}mL") + return volume + + def parse_ratio(ratio_str: str) -> Tuple[float, float]: """ 解析比例字符串,支持多种格式 @@ -111,11 +183,11 @@ def generate_recrystallize_protocol( solvent1: str, solvent2: str, vessel: str, - volume: float, - **kwargs # 接收其他可能的参数但不使用 + volume: Union[str, float], # 🔧 修改:支持字符串和数值 + **kwargs ) -> List[Dict[str, Any]]: """ - 生成重结晶协议序列 + 生成重结晶协议序列 - 支持单位 Args: G: 有向图,节点为容器和设备 @@ -123,38 +195,42 @@ def generate_recrystallize_protocol( solvent1: 第一种溶剂名称 solvent2: 第二种溶剂名称 vessel: 目标容器 - volume: 总体积 (mL) - **kwargs: 其他可选参数,但不使用 + volume: 总体积(支持 "100 mL", "50", "2.5 L" 等) + **kwargs: 其他可选参数 Returns: List[Dict[str, Any]]: 动作序列 """ action_sequence = [] - print(f"RECRYSTALLIZE: 开始生成重结晶协议") + print(f"RECRYSTALLIZE: 开始生成重结晶协议(支持单位)") print(f" - 比例: {ratio}") print(f" - 溶剂1: {solvent1}") print(f" - 溶剂2: {solvent2}") print(f" - 容器: {vessel}") - print(f" - 总体积: {volume} mL") + print(f" - 总体积: {volume} (类型: {type(volume)})") # 1. 验证目标容器存在 if vessel not in G.nodes(): raise ValueError(f"目标容器 '{vessel}' 不存在于系统中") - # 2. 解析比例 + # 2. 🔧 新增:解析体积(支持单位) + final_volume = parse_volume_with_units(volume, "mL") + print(f"RECRYSTALLIZE: 解析体积: {volume} → {final_volume}mL") + + # 3. 解析比例 ratio1, ratio2 = parse_ratio(ratio) total_ratio = ratio1 + ratio2 - # 3. 计算各溶剂体积 - volume1 = volume * (ratio1 / total_ratio) - volume2 = volume * (ratio2 / total_ratio) + # 4. 计算各溶剂体积 + volume1 = final_volume * (ratio1 / total_ratio) + volume2 = final_volume * (ratio2 / total_ratio) print(f"RECRYSTALLIZE: 解析比例: {ratio1}:{ratio2}") print(f"RECRYSTALLIZE: {solvent1} 体积: {volume1:.2f} mL") print(f"RECRYSTALLIZE: {solvent2} 体积: {volume2:.2f} mL") - # 4. 查找溶剂容器 + # 5. 查找溶剂容器 try: solvent1_vessel = find_solvent_vessel(G, solvent1) print(f"RECRYSTALLIZE: 找到溶剂1容器: {solvent1_vessel}") @@ -167,7 +243,7 @@ def generate_recrystallize_protocol( except ValueError as e: raise ValueError(f"无法找到溶剂2 '{solvent2}': {str(e)}") - # 5. 验证路径存在 + # 6. 验证路径存在 try: path1 = nx.shortest_path(G, source=solvent1_vessel, target=vessel) print(f"RECRYSTALLIZE: 溶剂1路径: {' → '.join(path1)}") @@ -180,7 +256,7 @@ def generate_recrystallize_protocol( except nx.NetworkXNoPath: raise ValueError(f"从溶剂2容器 '{solvent2_vessel}' 到目标容器 '{vessel}' 没有可用路径") - # 6. 添加第一种溶剂 + # 7. 添加第一种溶剂 print(f"RECRYSTALLIZE: 开始添加溶剂1 {volume1:.2f} mL") try: @@ -188,7 +264,7 @@ def generate_recrystallize_protocol( G=G, from_vessel=solvent1_vessel, to_vessel=vessel, - volume=volume1, + volume=volume1, # 使用解析后的体积 amount="", time=0.0, viscous=False, @@ -205,7 +281,7 @@ def generate_recrystallize_protocol( except Exception as e: raise ValueError(f"生成溶剂1泵协议时出错: {str(e)}") - # 7. 等待溶剂1稳定 + # 8. 等待溶剂1稳定 action_sequence.append({ "action_name": "wait", "action_kwargs": { @@ -214,7 +290,7 @@ def generate_recrystallize_protocol( } }) - # 8. 添加第二种溶剂 + # 9. 添加第二种溶剂 print(f"RECRYSTALLIZE: 开始添加溶剂2 {volume2:.2f} mL") try: @@ -222,7 +298,7 @@ def generate_recrystallize_protocol( G=G, from_vessel=solvent2_vessel, to_vessel=vessel, - volume=volume2, + volume=volume2, # 使用解析后的体积 amount="", time=0.0, viscous=False, @@ -239,7 +315,7 @@ def generate_recrystallize_protocol( except Exception as e: raise ValueError(f"生成溶剂2泵协议时出错: {str(e)}") - # 9. 等待溶剂2稳定 + # 10. 等待溶剂2稳定 action_sequence.append({ "action_name": "wait", "action_kwargs": { @@ -248,17 +324,18 @@ def generate_recrystallize_protocol( } }) - # 10. 等待重结晶完成 + # 11. 等待重结晶完成 action_sequence.append({ "action_name": "wait", "action_kwargs": { "time": 600.0, # 等待10分钟进行重结晶 - "description": f"等待重结晶完成({solvent1}:{solvent2} = {ratio})" + "description": f"等待重结晶完成({solvent1}:{solvent2} = {ratio},总体积 {final_volume}mL)" } }) print(f"RECRYSTALLIZE: 协议生成完成,共 {len(action_sequence)} 个动作") print(f"RECRYSTALLIZE: 预计总时间: {620/60:.1f} 分钟") + print(f"RECRYSTALLIZE: 总体积: {final_volume}mL") return action_sequence diff --git a/unilabos/compile/stir_protocol.py b/unilabos/compile/stir_protocol.py index a67155da6..5343a2a47 100644 --- a/unilabos/compile/stir_protocol.py +++ b/unilabos/compile/stir_protocol.py @@ -10,6 +10,97 @@ def debug_print(message): print(f"[STIR] {message}", flush=True) logger.info(f"[STIR] {message}") +def parse_time_with_units(time_input: Union[str, float, int], default_unit: str = "s") -> float: + """ + 解析带单位的时间输入 + + Args: + time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0) + default_unit: 默认单位(默认为秒) + + Returns: + float: 时间(秒) + """ + if not time_input: + return 0.0 + + # 处理数值输入 + if isinstance(time_input, (int, float)): + result = float(time_input) + debug_print(f"数值时间输入: {time_input} → {result}s(默认单位)") + return result + + # 处理字符串输入 + time_str = str(time_input).lower().strip() + debug_print(f"解析时间字符串: '{time_str}'") + + # 处理特殊值 + if time_str in ['?', 'unknown', 'tbd', 'to be determined']: + default_time = 300.0 # 5分钟默认值 + debug_print(f"检测到未知时间,使用默认值: {default_time}s") + return default_time + + # 如果是纯数字,使用默认单位 + try: + value = float(time_str) + if default_unit == "s": + result = value + elif default_unit in ["min", "minute"]: + result = value * 60.0 + elif default_unit in ["h", "hour"]: + result = value * 3600.0 + else: + result = value # 默认秒 + debug_print(f"纯数字输入: {time_str} → {result}s(单位: {default_unit})") + return result + except ValueError: + pass + + # 使用正则表达式匹配数字和单位 + pattern = r'(\d+\.?\d*)\s*([a-z]*)' + match = re.match(pattern, time_str) + + if not match: + debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 60s") + return 60.0 + + value = float(match.group(1)) + unit = match.group(2) or default_unit + + # 单位转换映射 + unit_multipliers = { + # 秒 + 's': 1.0, + 'sec': 1.0, + 'second': 1.0, + 'seconds': 1.0, + + # 分钟 + 'm': 60.0, + 'min': 60.0, + 'mins': 60.0, + 'minute': 60.0, + 'minutes': 60.0, + + # 小时 + 'h': 3600.0, + 'hr': 3600.0, + 'hrs': 3600.0, + 'hour': 3600.0, + 'hours': 3600.0, + + # 天 + 'd': 86400.0, + 'day': 86400.0, + 'days': 86400.0, + } + + multiplier = unit_multipliers.get(unit, 1.0) + result = value * multiplier + + debug_print(f"时间解析: '{time_str}' → {value} {unit} → {result}s") + return result + def parse_time_spec(time_spec: str) -> float: """ 解析时间规格字符串为秒数 @@ -211,27 +302,26 @@ def find_connected_stirrer(G: nx.DiGraph, vessel: str = None) -> str: def generate_stir_protocol( G: nx.DiGraph, vessel: str, - time: Union[str, float, int] = 300.0, - stir_time: Union[str, float, int] = 0.0, + time: Union[str, float, int] = "300", # 🔧 修改:默认为字符串 + stir_time: Union[str, float, int] = "0", # 🔧 修改:支持字符串 time_spec: str = "", event: str = "", stir_speed: float = 200.0, - settling_time: float = 60.0, + settling_time: Union[str, float] = "60", # 🔧 修改:支持字符串 **kwargs ) -> List[Dict[str, Any]]: """ - 生成搅拌操作的协议序列 - 定时搅拌 + 沉降 - 支持 time 和 stir_time 参数统一处理 + 生成搅拌操作的协议序列 - 支持单位 Args: G: 设备图 vessel: 搅拌容器名称(必需) - time: 搅拌时间(支持多种格式) - stir_time: 搅拌时间(与time等效) + time: 搅拌时间(支持 "5 min", "300", "0.5 h" 等) + stir_time: 搅拌时间(与time等效,支持单位) time_spec: 时间规格(优先级最高) event: 事件标识 stir_speed: 搅拌速度 (RPM),默认200 RPM - settling_time: 沉降时间 (秒),默认60s + settling_time: 沉降时间(支持单位,默认60秒) **kwargs: 其他参数(兼容性) Returns: @@ -239,7 +329,7 @@ def generate_stir_protocol( """ debug_print("=" * 50) - debug_print("开始生成搅拌协议") + debug_print("开始生成搅拌协议(支持单位)") debug_print(f"输入参数:") debug_print(f" - vessel: {vessel}") debug_print(f" - time: {time}") @@ -265,19 +355,29 @@ def generate_stir_protocol( debug_print(f"✅ 参数验证通过") - # === 时间处理(统一 time 和 stir_time)=== - debug_print("步骤2: 时间处理...") + # === 🔧 新增:单位解析处理 === + debug_print("步骤2: 单位解析处理...") + + # 确定实际使用的时间值(stir_time优先) + actual_time_input = stir_time if stir_time not in ["0", 0, 0.0] else time - # 确定实际使用的时间值 - actual_time_input = stir_time if stir_time else time + # 解析时间(time_spec > actual_time_input) + if time_spec: + parsed_time = parse_time_spec(time_spec) # 使用现有的time_spec解析 + debug_print(f"使用time_spec: '{time_spec}' → {parsed_time}s") + else: + parsed_time = parse_time_with_units(actual_time_input, "s") + debug_print(f"解析时间: {actual_time_input} → {parsed_time}s") - # 解析时间 - parsed_time = parse_time_input(actual_time_input, time_spec) + # 解析沉降时间 + parsed_settling_time = parse_time_with_units(settling_time, "s") + debug_print(f"解析沉降时间: {settling_time} → {parsed_settling_time}s") debug_print(f"时间解析结果:") debug_print(f" - 原始输入: time={time}, stir_time={stir_time}") debug_print(f" - 时间规格: {time_spec}") - debug_print(f" - 最终时间: {parsed_time}秒 ({parsed_time/60:.1f}分钟)") + debug_print(f" - 最终搅拌时间: {parsed_time}s ({parsed_time/60:.1f}分钟)") + debug_print(f" - 最终沉降时间: {parsed_settling_time}s ({parsed_settling_time/60:.1f}分钟)") # 修正参数范围 if parsed_time < 0: @@ -294,12 +394,12 @@ def generate_stir_protocol( 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 + if parsed_settling_time < 0: + debug_print(f"沉降时间 {parsed_settling_time}s 无效,修正为 60s") + parsed_settling_time = 60.0 + elif parsed_settling_time > 1800: + debug_print(f"沉降时间 {parsed_settling_time}s 过长,修正为 600s") + parsed_settling_time = 600.0 # === 查找搅拌设备 === debug_print("步骤3: 查找搅拌设备...") @@ -318,12 +418,12 @@ def generate_stir_protocol( # 构建搅拌动作参数 stir_kwargs = { "vessel": vessel, - "time": str(time), # 保持原始字符串格式 + "time": str(time), # 保持原始字符串格式 "event": event, "time_spec": time_spec, - "stir_time": parsed_time, # 解析后的时间(秒) + "stir_time": parsed_time, # 解析后的时间(秒) "stir_speed": stir_speed, - "settling_time": settling_time + "settling_time": parsed_settling_time # 解析后的沉降时间(秒) } debug_print(f"搅拌参数: {stir_kwargs}") @@ -338,10 +438,10 @@ def generate_stir_protocol( # === 总结 === debug_print("=" * 50) - debug_print(f"搅拌协议生成完成") + debug_print(f"搅拌协议生成完成(支持单位)") debug_print(f"总动作数: {len(action_sequence)}") debug_print(f"搅拌容器: {vessel}") - debug_print(f"搅拌参数: {stir_speed} RPM, {parsed_time}s, 沉降 {settling_time}s") + debug_print(f"搅拌参数: {stir_speed} RPM, {parsed_time}s, 沉降 {parsed_settling_time}s") debug_print("=" * 50) return action_sequence diff --git a/unilabos/compile/utils/unit_parser.py b/unilabos/compile/utils/unit_parser.py new file mode 100644 index 000000000..d1d297cb5 --- /dev/null +++ b/unilabos/compile/utils/unit_parser.py @@ -0,0 +1,206 @@ +""" +统一的单位解析工具模块 +支持时间、体积、质量等各种单位的解析 +""" + +import re +import logging +from typing import Union + +logger = logging.getLogger(__name__) + +def debug_print(message, prefix="[UNIT_PARSER]"): + """调试输出""" + print(f"{prefix} {message}", flush=True) + logger.info(f"{prefix} {message}") + +def parse_time_with_units(time_input: Union[str, float, int], default_unit: str = "s") -> float: + """ + 解析带单位的时间输入 + + Args: + time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0) + default_unit: 默认单位(默认为秒) + + Returns: + float: 时间(秒) + """ + if not time_input: + return 0.0 + + # 处理数值输入 + if isinstance(time_input, (int, float)): + result = float(time_input) + debug_print(f"数值时间输入: {time_input} → {result}s(默认单位)") + return result + + # 处理字符串输入 + time_str = str(time_input).lower().strip() + debug_print(f"解析时间字符串: '{time_str}'") + + # 处理特殊值 + if time_str in ['?', 'unknown', 'tbd', 'to be determined']: + default_time = 300.0 # 5分钟默认值 + debug_print(f"检测到未知时间,使用默认值: {default_time}s") + return default_time + + # 如果是纯数字,使用默认单位 + try: + value = float(time_str) + if default_unit == "s": + result = value + elif default_unit in ["min", "minute"]: + result = value * 60.0 + elif default_unit in ["h", "hour"]: + result = value * 3600.0 + else: + result = value # 默认秒 + debug_print(f"纯数字输入: {time_str} → {result}s(单位: {default_unit})") + return result + except ValueError: + pass + + # 使用正则表达式匹配数字和单位 + pattern = r'(\d+\.?\d*)\s*([a-z]*)' + match = re.match(pattern, time_str) + + if not match: + debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 60s") + return 60.0 + + value = float(match.group(1)) + unit = match.group(2) or default_unit + + # 单位转换映射 + unit_multipliers = { + # 秒 + 's': 1.0, + 'sec': 1.0, + 'second': 1.0, + 'seconds': 1.0, + + # 分钟 + 'm': 60.0, + 'min': 60.0, + 'mins': 60.0, + 'minute': 60.0, + 'minutes': 60.0, + + # 小时 + 'h': 3600.0, + 'hr': 3600.0, + 'hrs': 3600.0, + 'hour': 3600.0, + 'hours': 3600.0, + + # 天 + 'd': 86400.0, + 'day': 86400.0, + 'days': 86400.0, + } + + multiplier = unit_multipliers.get(unit, 1.0) + result = value * multiplier + + debug_print(f"时间解析: '{time_str}' → {value} {unit} → {result}s") + return result + +def parse_volume_with_units(volume_input: Union[str, float, int], default_unit: str = "mL") -> float: + """ + 解析带单位的体积输入 + + Args: + volume_input: 体积输入(如 "100 mL", "2.5 L", "500", "?", 100.0) + default_unit: 默认单位(默认为毫升) + + Returns: + float: 体积(毫升) + """ + if not volume_input: + return 0.0 + + # 处理数值输入 + if isinstance(volume_input, (int, float)): + result = float(volume_input) + debug_print(f"数值体积输入: {volume_input} → {result}mL(默认单位)") + return result + + # 处理字符串输入 + volume_str = str(volume_input).lower().strip() + debug_print(f"解析体积字符串: '{volume_str}'") + + # 处理特殊值 + if volume_str in ['?', 'unknown', 'tbd', 'to be determined']: + default_volume = 50.0 # 50mL默认值 + debug_print(f"检测到未知体积,使用默认值: {default_volume}mL") + return default_volume + + # 如果是纯数字,使用默认单位 + try: + value = float(volume_str) + if default_unit.lower() in ["ml", "milliliter"]: + result = value + elif default_unit.lower() in ["l", "liter"]: + result = value * 1000.0 + elif default_unit.lower() in ["μl", "ul", "microliter"]: + result = value / 1000.0 + else: + result = value # 默认mL + debug_print(f"纯数字输入: {volume_str} → {result}mL(单位: {default_unit})") + return result + except ValueError: + pass + + # 移除空格并提取数字和单位 + volume_clean = re.sub(r'\s+', '', volume_str) + + # 匹配数字和单位的正则表达式 + match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean) + + if not match: + debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值: 50mL") + return 50.0 + + value = float(match.group(1)) + unit = match.group(2) or default_unit.lower() + + # 转换为毫升 + if unit in ['l', 'liter']: + volume = value * 1000.0 # L -> mL + elif unit in ['μl', 'ul', 'microliter']: + volume = value / 1000.0 # μL -> mL + else: # ml, milliliter 或默认 + volume = value # 已经是mL + + debug_print(f"体积解析: '{volume_str}' → {value} {unit} → {volume}mL") + return volume + +# 测试函数 +def test_unit_parser(): + """测试单位解析功能""" + print("=== 单位解析器测试 ===") + + # 测试时间解析 + time_tests = [ + "30 min", "1 h", "300", "5.5 h", "?", 60.0, "2 hours", "30 s" + ] + + print("\n时间解析测试:") + for time_input in time_tests: + result = parse_time_with_units(time_input) + print(f" {time_input} → {result}s ({result/60:.1f}min)") + + # 测试体积解析 + volume_tests = [ + "100 mL", "2.5 L", "500", "?", 100.0, "500 μL", "1 liter" + ] + + print("\n体积解析测试:") + for volume_input in volume_tests: + result = parse_volume_with_units(volume_input) + print(f" {volume_input} → {result}mL") + + print("\n✅ 测试完成") + +if __name__ == "__main__": + test_unit_parser() \ No newline at end of file diff --git a/unilabos/compile/wash_solid_protocol.py b/unilabos/compile/wash_solid_protocol.py index 1a5c5e3c3..8d431086c 100644 --- a/unilabos/compile/wash_solid_protocol.py +++ b/unilabos/compile/wash_solid_protocol.py @@ -10,6 +10,167 @@ def debug_print(message): print(f"[WASH_SOLID] {message}", flush=True) logger.info(f"[WASH_SOLID] {message}") +def parse_time_with_units(time_input: Union[str, float, int], default_unit: str = "s") -> float: + """ + 解析带单位的时间输入 + + Args: + time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0) + default_unit: 默认单位(默认为秒) + + Returns: + float: 时间(秒) + """ + if not time_input: + return 0.0 + + # 处理数值输入 + if isinstance(time_input, (int, float)): + result = float(time_input) + debug_print(f"数值时间输入: {time_input} → {result}s(默认单位)") + return result + + # 处理字符串输入 + time_str = str(time_input).lower().strip() + debug_print(f"解析时间字符串: '{time_str}'") + + # 处理特殊值 + if time_str in ['?', 'unknown', 'tbd', 'to be determined']: + default_time = 300.0 # 5分钟默认值 + debug_print(f"检测到未知时间,使用默认值: {default_time}s") + return default_time + + # 如果是纯数字,使用默认单位 + try: + value = float(time_str) + if default_unit == "s": + result = value + elif default_unit in ["min", "minute"]: + result = value * 60.0 + elif default_unit in ["h", "hour"]: + result = value * 3600.0 + else: + result = value # 默认秒 + debug_print(f"纯数字输入: {time_str} → {result}s(单位: {default_unit})") + return result + except ValueError: + pass + + # 使用正则表达式匹配数字和单位 + pattern = r'(\d+\.?\d*)\s*([a-z]*)' + match = re.match(pattern, time_str) + + if not match: + debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 60s") + return 60.0 + + value = float(match.group(1)) + unit = match.group(2) or default_unit + + # 单位转换映射 + unit_multipliers = { + # 秒 + 's': 1.0, + 'sec': 1.0, + 'second': 1.0, + 'seconds': 1.0, + + # 分钟 + 'm': 60.0, + 'min': 60.0, + 'mins': 60.0, + 'minute': 60.0, + 'minutes': 60.0, + + # 小时 + 'h': 3600.0, + 'hr': 3600.0, + 'hrs': 3600.0, + 'hour': 3600.0, + 'hours': 3600.0, + + # 天 + 'd': 86400.0, + 'day': 86400.0, + 'days': 86400.0, + } + + multiplier = unit_multipliers.get(unit, 1.0) + result = value * multiplier + + debug_print(f"时间解析: '{time_str}' → {value} {unit} → {result}s") + return result + +def parse_volume_with_units(volume_input: Union[str, float, int], default_unit: str = "mL") -> float: + """ + 解析带单位的体积输入 + + Args: + volume_input: 体积输入(如 "100 mL", "2.5 L", "500", "?", 100.0) + default_unit: 默认单位(默认为毫升) + + Returns: + float: 体积(毫升) + """ + if not volume_input: + return 0.0 + + # 处理数值输入 + if isinstance(volume_input, (int, float)): + result = float(volume_input) + debug_print(f"数值体积输入: {volume_input} → {result}mL(默认单位)") + return result + + # 处理字符串输入 + volume_str = str(volume_input).lower().strip() + debug_print(f"解析体积字符串: '{volume_str}'") + + # 处理特殊值 + if volume_str in ['?', 'unknown', 'tbd', 'to be determined']: + default_volume = 50.0 # 50mL默认值 + debug_print(f"检测到未知体积,使用默认值: {default_volume}mL") + return default_volume + + # 如果是纯数字,使用默认单位 + try: + value = float(volume_str) + if default_unit.lower() in ["ml", "milliliter"]: + result = value + elif default_unit.lower() in ["l", "liter"]: + result = value * 1000.0 + elif default_unit.lower() in ["μl", "ul", "microliter"]: + result = value / 1000.0 + else: + result = value # 默认mL + debug_print(f"纯数字输入: {volume_str} → {result}mL(单位: {default_unit})") + return result + except ValueError: + pass + + # 移除空格并提取数字和单位 + volume_clean = re.sub(r'\s+', '', volume_str) + + # 匹配数字和单位的正则表达式 + match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean) + + if not match: + debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值: 50mL") + return 50.0 + + value = float(match.group(1)) + unit = match.group(2) or default_unit.lower() + + # 转换为毫升 + if unit in ['l', 'liter']: + volume = value * 1000.0 # L -> mL + elif unit in ['μl', 'ul', 'microliter']: + volume = value / 1000.0 # μL -> mL + else: # ml, milliliter 或默认 + volume = value # 已经是mL + + debug_print(f"体积解析: '{volume_str}' → {value} {unit} → {volume}mL") + return volume + def parse_volume_spec(volume_spec: str) -> float: """ 解析体积规格字符串为毫升数 @@ -357,46 +518,46 @@ def generate_wash_solid_protocol( G: nx.DiGraph, vessel: str, solvent: str, - volume: Union[float, str] = 0.0, # 🔧 修改:支持字符串输入 + volume: Union[float, str] = "50", # 🔧 修改:默认为字符串 filtrate_vessel: str = "", temp: float = 25.0, stir: bool = False, stir_speed: float = 0.0, - time: float = 0.0, + time: Union[str, float] = "0", # 🔧 修改:支持字符串时间 repeats: int = 1, - # === 新增参数 === - volume_spec: str = "", # 体积规格 - repeats_spec: str = "", # 重复次数规格 - mass: str = "", # 🔧 新增:固体质量(用于转换体积) - event: str = "", # 事件标识符 + # === 现有参数保持不变 === + volume_spec: str = "", + repeats_spec: str = "", + mass: str = "", + event: str = "", **kwargs ) -> List[Dict[str, Any]]: """ - 生成固体清洗操作的协议序列 - 增强版 + 生成固体清洗操作的协议序列 - 增强版(支持单位) - 支持多种体积输入方式: - 1. volume_spec: "small volume", "large volume" 等 - 2. mass: "10 g", "2.5 kg", "500 mg" 等(转换为体积) - 3. volume: 数值或字符串 "10", "10 mL", "2.5 L" 等 + 支持多种输入方式: + 1. volume: "100 mL", "50", "2.5 L", "?" + 2. time: "5 min", "300", "0.5 h", "?" + 3. volume_spec: "small volume", "large volume" 等 + 4. mass: "10 g", "2.5 kg", "500 mg" 等(转换为体积) """ debug_print("=" * 60) - debug_print("开始生成固体清洗协议") + debug_print("开始生成固体清洗协议(支持单位)") debug_print(f"输入参数:") debug_print(f" - vessel: {vessel}") debug_print(f" - solvent: {solvent}") debug_print(f" - volume: {volume} (类型: {type(volume)})") + debug_print(f" - time: {time} (类型: {type(time)})") debug_print(f" - volume_spec: '{volume_spec}'") - debug_print(f" - mass: '{mass}'") # 🔧 新增日志 + debug_print(f" - mass: '{mass}'") 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" - repeats_spec: '{repeats_spec}'") debug_print(f" - event: '{event}'") - debug_print(f" - 其他参数: {kwargs}") debug_print("=" * 60) action_sequence = [] @@ -416,12 +577,27 @@ def generate_wash_solid_protocol( debug_print(f"✅ 必需参数验证通过") - # === 参数处理 === - debug_print("步骤2: 参数处理...") + # === 🔧 新增:单位解析处理 === + debug_print("步骤2: 单位解析处理...") - # 🔧 修改:处理体积参数(支持mass转换和字符串解析) - final_volume = parse_volume_input(volume, volume_spec, mass) - debug_print(f"最终体积: {final_volume}mL") + # 解析体积(优先级:volume_spec > mass > volume) + if volume_spec and volume_spec.strip(): + final_volume = parse_volume_spec(volume_spec) + debug_print(f"使用volume_spec: {final_volume}mL") + elif mass and mass.strip(): + final_volume = parse_mass_to_volume(mass) + if final_volume > 0: + debug_print(f"使用mass转换: {final_volume}mL") + else: + final_volume = parse_volume_with_units(volume, "mL") + debug_print(f"mass转换失败,使用volume: {final_volume}mL") + else: + final_volume = parse_volume_with_units(volume, "mL") + debug_print(f"使用volume: {final_volume}mL") + + # 解析时间 + final_time = parse_time_with_units(time, "s") + debug_print(f"解析时间: {time} → {final_time}s ({final_time/60:.1f}min)") # 处理重复次数参数(repeats_spec优先) final_repeats = parse_repeats_input(repeats, repeats_spec) @@ -436,9 +612,9 @@ def generate_wash_solid_protocol( debug_print(f"搅拌速度 {stir_speed} RPM 超出范围,修正为 200 RPM") stir_speed = 200.0 if stir else 0.0 - if time < 0: - debug_print(f"时间 {time}s 无效,修正为 0") - time = 0.0 + if final_time < 0: + debug_print(f"时间 {final_time}s 无效,修正为 0") + final_time = 0.0 if final_repeats < 1: debug_print(f"重复次数 {final_repeats} 无效,修正为 1") @@ -447,9 +623,9 @@ def generate_wash_solid_protocol( debug_print(f"重复次数 {final_repeats} 过多,修正为 10") final_repeats = 10 - debug_print(f"✅ 参数处理完成") + debug_print(f"✅ 单位解析和参数处理完成") - # === 查找设备 === + # === 查找设备(保持原有逻辑)=== debug_print("步骤3: 查找设备...") try: @@ -507,17 +683,14 @@ def generate_wash_solid_protocol( debug_print(f"❌ 设备查找失败: {str(e)}") raise ValueError(f"设备查找失败: {str(e)}") - # === 执行清洗循环 === + # === 执行清洗循环(保持原有逻辑,使用解析后的参数)=== debug_print("步骤4: 执行清洗循环...") for cycle in range(final_repeats): debug_print(f"=== 第 {cycle+1}/{final_repeats} 次清洗 ===") - # 🔧 修复:分解为基础动作序列 - # 1. 加入清洗溶剂 debug_print(f" 步骤 {cycle+1}.1: 加入清洗溶剂") - # 🔧 修复:使用 pump protocol 而不是直接调用 transfer action try: from .pump_protocol import generate_pump_protocol_with_rinsing @@ -525,7 +698,7 @@ def generate_wash_solid_protocol( G=G, from_vessel=solvent_source, to_vessel=vessel, - volume=final_volume, + volume=final_volume, # 使用解析后的体积 amount="", time=0.0, viscous=False, @@ -548,22 +721,21 @@ def generate_wash_solid_protocol( except Exception as e: debug_print(f"❌ 转移失败: {str(e)}") - # 继续执行,可能有其他问题 # 2. 搅拌混合(如果需要) if stir and stirrer_device: debug_print(f" 步骤 {cycle+1}.2: 搅拌混合") - stir_time = max(time, 30.0) if time > 0 else 60.0 # 默认搅拌1分钟 + stir_time = max(final_time, 30.0) if final_time > 0 else 60.0 # 使用解析后的时间 stir_action = { "device_id": stirrer_device, "action_name": "stir", "action_kwargs": { "vessel": vessel, - "time": str(int(stir_time)), # 转换为字符串格式 + "time": str(time), # 保持原始字符串格式 "event": event, "time_spec": "", - "stir_time": stir_time, + "stir_time": stir_time, # 解析后的时间(秒) "stir_speed": stir_speed, "settling_time": 30.0 } @@ -582,7 +754,7 @@ def generate_wash_solid_protocol( "stir_speed": 0.0, "temp": temp, "continue_heatchill": False, - "volume": final_volume + "volume": final_volume # 使用解析后的体积 } } action_sequence.append(filter_action) @@ -596,11 +768,12 @@ def generate_wash_solid_protocol( # === 总结 === debug_print("=" * 60) - debug_print(f"固体清洗协议生成完成") + debug_print(f"固体清洗协议生成完成(支持单位)") debug_print(f"总动作数: {len(action_sequence)}") debug_print(f"清洗容器: {vessel}") debug_print(f"使用溶剂: {solvent}") debug_print(f"清洗体积: {final_volume}mL") + debug_print(f"清洗时间: {final_time}s ({final_time/60:.1f}min)") debug_print(f"重复次数: {final_repeats}") debug_print(f"滤液收集: {actual_filtrate_vessel}") debug_print(f"事件标识: {event}") diff --git a/unilabos/devices/virtual/virtual_heatchill.py b/unilabos/devices/virtual/virtual_heatchill.py index 541434af1..20c9a196b 100644 --- a/unilabos/devices/virtual/virtual_heatchill.py +++ b/unilabos/devices/virtual/virtual_heatchill.py @@ -58,12 +58,30 @@ async def cleanup(self) -> bool: }) return True - async def heat_chill(self, vessel: str, temp: float, time: float, stir: bool, + async def heat_chill(self, vessel: str, temp: float, time, stir: bool, stir_speed: float, purpose: str) -> bool: - """Execute heat chill action - 按实际时间运行,实时更新剩余时间""" - self.logger.info(f"HeatChill: vessel={vessel}, temp={temp}°C, time={time}s, stir={stir}, stir_speed={stir_speed}") + """Execute heat chill action - 🔧 修复:确保参数类型正确""" - # 验证参数 + # 🔧 关键修复:确保所有参数类型正确 + try: + temp = float(temp) + time_value = float(time) # 强制转换为浮点数 + stir_speed = float(stir_speed) + stir = bool(stir) + vessel = str(vessel) + purpose = str(purpose) + except (ValueError, TypeError) as e: + error_msg = f"参数类型转换错误: temp={temp}({type(temp)}), time={time}({type(time)}), error={str(e)}" + self.logger.error(error_msg) + self.data.update({ + "status": f"Error: {error_msg}", + "operation_mode": "Error" + }) + return False + + self.logger.info(f"HeatChill: vessel={vessel}, temp={temp}°C, time={time_value}s, stir={stir}, stir_speed={stir_speed}") + + # 验证参数范围 if temp > self._max_temp or temp < self._min_temp: error_msg = f"温度 {temp}°C 超出范围 ({self._min_temp}°C - {self._max_temp}°C)" self.logger.error(error_msg) @@ -82,6 +100,15 @@ async def heat_chill(self, vessel: str, temp: float, time: float, stir: bool, }) return False + if time_value <= 0: + error_msg = f"时间 {time_value}s 必须大于0" + self.logger.error(error_msg) + self.data.update({ + "status": f"Error: {error_msg}", + "operation_mode": "Error" + }) + return False + # 确定操作模式 if temp > 25.0: operation_mode = "Heating" @@ -93,9 +120,9 @@ async def heat_chill(self, vessel: str, temp: float, time: float, stir: bool, operation_mode = "Maintaining" status_action = "保温" - # **修复**: 使用重命名的time模块 + # 🔧 修复:使用转换后的时间值 start_time = time_module.time() - total_time = time + total_time = time_value # 使用转换后的浮点数 # 开始操作 stir_info = f" | 搅拌: {stir_speed} RPM" if stir else "" @@ -107,9 +134,9 @@ async def heat_chill(self, vessel: str, temp: float, time: float, stir: bool, "remaining_time": total_time, }) - # **修复**: 在等待过程中每秒更新剩余时间 + # 在等待过程中每秒更新剩余时间 while True: - current_time = time_module.time() # 使用重命名的time模块 + current_time = time_module.time() elapsed = current_time - start_time remaining = max(0, total_time - elapsed) @@ -141,6 +168,21 @@ async def heat_chill(self, vessel: str, temp: float, time: float, stir: bool, async def heat_chill_start(self, vessel: str, temp: float, purpose: str) -> bool: """Start continuous heat chill""" + + # 🔧 添加类型转换 + try: + temp = float(temp) + vessel = str(vessel) + purpose = str(purpose) + except (ValueError, TypeError) as e: + error_msg = f"参数类型转换错误: {str(e)}" + self.logger.error(error_msg) + self.data.update({ + "status": f"Error: {error_msg}", + "operation_mode": "Error" + }) + return False + self.logger.info(f"HeatChillStart: vessel={vessel}, temp={temp}°C") # 验证参数 @@ -176,6 +218,15 @@ async def heat_chill_start(self, vessel: str, temp: float, purpose: str) -> bool async def heat_chill_stop(self, vessel: str) -> bool: """Stop heat chill""" + + # 🔧 添加类型转换 + try: + vessel = str(vessel) + except (ValueError, TypeError) as e: + error_msg = f"参数类型转换错误: {str(e)}" + self.logger.error(error_msg) + return False + self.logger.info(f"HeatChillStop: vessel={vessel}") self.data.update({ diff --git a/unilabos/devices/virtual/virtual_rotavap.py b/unilabos/devices/virtual/virtual_rotavap.py index 9f576b5ec..bfd64947e 100644 --- a/unilabos/devices/virtual/virtual_rotavap.py +++ b/unilabos/devices/virtual/virtual_rotavap.py @@ -73,7 +73,7 @@ async def evaporate( vessel: str, pressure: float = 0.1, temp: float = 60.0, - time: float = 1800.0, + time: float = 180.0, stir_speed: float = 100.0, solvent: str = "", **kwargs diff --git a/unilabos/messages/__init__.py b/unilabos/messages/__init__.py index 52fd97f84..f91f38288 100644 --- a/unilabos/messages/__init__.py +++ b/unilabos/messages/__init__.py @@ -125,7 +125,7 @@ class EvaporateProtocol(BaseModel): # === 所有其他参数都改为可选,添加默认值 === 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分钟)") + time: float = Field(180.0, description="蒸发时间 (秒),默认1800s (30分钟)") stir_speed: float = Field(100.0, description="旋转速度 (RPM),默认100 RPM") # === 新版XDL兼容参数(可选) === diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml index 3a98e566c..d16564968 100644 --- a/unilabos/registry/devices/virtual_device.yaml +++ b/unilabos/registry/devices/virtual_device.yaml @@ -2321,7 +2321,7 @@ virtual_rotavap: pressure: 0.1 stir_speed: 100.0 temp: 60.0 - time: 1800.0 + time: 180.0 vessel: null handles: [] result: {} @@ -2341,7 +2341,7 @@ virtual_rotavap: default: 60.0 type: number time: - default: 1800.0 + default: 180.0 type: number vessel: type: string diff --git a/unilabos_msgs/action/Evaporate.action b/unilabos_msgs/action/Evaporate.action index b66e32f0d..9cecb62cb 100644 --- a/unilabos_msgs/action/Evaporate.action +++ b/unilabos_msgs/action/Evaporate.action @@ -1,10 +1,10 @@ # Organic Synthesis Station Evaporate Action -string vessel -float64 pressure -float64 temp -float64 time -float64 stir_speed -string solvent +string vessel # 目标容器 +float64 pressure # 真空度 +float64 temp # 温度 +string time # 🔧 蒸发时间(支持带单位,如"3 min","180",默认秒) +float64 stir_speed # 旋转速度 +string solvent # 溶剂名称 --- string return_info bool success diff --git a/unilabos_msgs/action/HeatChill.action b/unilabos_msgs/action/HeatChill.action index a39510e71..1e7025e8b 100644 --- a/unilabos_msgs/action/HeatChill.action +++ b/unilabos_msgs/action/HeatChill.action @@ -1,7 +1,7 @@ # Goal - 加热冷却操作的目标参数 string vessel # 加热容器名称(必需) float64 temp # 目标温度(可选,默认25.0) -float64 time # 加热时间(可选,默认300.0) +string time # 🔧 加热时间(支持带单位,如"5 min","300",默认秒) string temp_spec # 温度规格(可选) string time_spec # 时间规格(可选) string pressure # 压力规格(可选,不做特殊处理) diff --git a/unilabos_msgs/action/Recrystallize.action b/unilabos_msgs/action/Recrystallize.action index fe727e8bd..2ae42bfe0 100644 --- a/unilabos_msgs/action/Recrystallize.action +++ b/unilabos_msgs/action/Recrystallize.action @@ -1,9 +1,9 @@ # Request -string ratio -string solvent1 -string solvent2 -string vessel -float64 volume +string ratio # 溶剂比例(如"1:1","3:7") +string solvent1 # 第一种溶剂 +string solvent2 # 第二种溶剂 +string vessel # 目标容器 +string volume # 🔧 总体积(支持带单位,如"100 mL","50",默认mL) --- # Result bool success diff --git a/unilabos_msgs/action/Stir.action b/unilabos_msgs/action/Stir.action index 64e9f0436..e3e5580bc 100644 --- a/unilabos_msgs/action/Stir.action +++ b/unilabos_msgs/action/Stir.action @@ -1,11 +1,11 @@ # Goal - 搅拌操作的目标参数 string vessel # 搅拌容器名称(必需) -string time # 搅拌时间(如 "0.5 h", "30 min") +string time # 🔧 搅拌时间(如 "0.5 h", "30 min", "300",默认秒) string event # 事件标识(如 "A", "B") string time_spec # 时间规格(如 "several minutes") float64 stir_time # 解析后的搅拌时间(秒) float64 stir_speed # 搅拌速度(默认200.0) -float64 settling_time # 沉降时间(默认60.0) +string settling_time # 🔧 沉降时间(支持带单位,默认秒) --- # Result - 操作结果 bool success # 操作是否成功 diff --git a/unilabos_msgs/action/WashSolid.action b/unilabos_msgs/action/WashSolid.action index 8ef159d73..281ca4cbc 100644 --- a/unilabos_msgs/action/WashSolid.action +++ b/unilabos_msgs/action/WashSolid.action @@ -1,12 +1,12 @@ # Goal - 固体清洗操作的目标参数 string vessel # 装有固体的容器名称(必需) string solvent # 清洗溶剂名称(必需) -string volume # 🔧 修改:体积(支持数字和带单位的字符串) +string volume # 🔧 体积(支持数字和带单位的字符串,如"100 mL","?") string filtrate_vessel # 滤液收集容器(可选,默认"") float64 temp # 清洗温度(可选,默认25.0) bool stir # 是否搅拌(可选,默认false) float64 stir_speed # 搅拌速度(可选,默认0.0) -float64 time # 清洗时间(可选,默认0.0) +string time # 🔧 清洗时间(支持带单位,如"5 min","300 s",默认秒) int32 repeats # 重复次数(与repeats_spec二选一) string volume_spec # 体积规格(优先级高于volume) string repeats_spec # 重复次数规格(优先级高于repeats) From ac294194e6da03ae4499847225e448f610b0903e Mon Sep 17 00:00:00 2001 From: KCFeng425 <2100011801@stu.pku.edu.cn> Date: Tue, 15 Jul 2025 10:31:19 +0800 Subject: [PATCH 8/9] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BA=86=E5=85=A8protoco?= =?UTF-8?q?l=E7=9A=84=E8=BF=90=E8=A1=8C=E6=97=B6=E9=97=B4=EF=BC=8C?= =?UTF-8?q?=E9=99=A4=E4=BA=86pumptransfer=E7=9B=B8=E5=85=B3=E7=9A=84?= =?UTF-8?q?=E8=BF=98=E6=B2=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- unilabos/compile/dry_protocol.py | 48 +- unilabos/compile/evaporate_protocol.py | 236 ++--- unilabos/compile/filter_protocol.py | 103 +- unilabos/compile/heatchill_protocol.py | 597 ++++-------- unilabos/compile/hydrogenate_protocol.py | 40 +- unilabos/compile/recrystallize_protocol.py | 155 ++- unilabos/compile/reset_handling_protocol.py | 121 ++- unilabos/compile/run_column_protocol.py | 82 +- unilabos/compile/stir_protocol.py | 671 ++++--------- unilabos/compile/wash_solid_protocol.py | 879 ++++-------------- unilabos/devices/virtual/virtual_column.py | 115 ++- unilabos/devices/virtual/virtual_filter.py | 109 ++- unilabos/devices/virtual/virtual_heatchill.py | 183 ++-- .../devices/virtual/virtual_multiway_valve.py | 169 +++- unilabos/devices/virtual/virtual_rotavap.py | 139 ++- .../virtual/virtual_solid_dispenser.py | 156 +++- unilabos/devices/virtual/virtual_stirrer.py | 178 +++- 17 files changed, 1808 insertions(+), 2173 deletions(-) diff --git a/unilabos/compile/dry_protocol.py b/unilabos/compile/dry_protocol.py index 34044eba4..1f06069aa 100644 --- a/unilabos/compile/dry_protocol.py +++ b/unilabos/compile/dry_protocol.py @@ -67,37 +67,47 @@ def generate_dry_protocol( # 默认参数 dry_temp = 60.0 # 默认干燥温度 60°C dry_time = 3600.0 # 默认干燥时间 1小时(3600秒) + simulation_time = 60.0 # 模拟时间 1分钟 - print(f"DRY: 开始生成干燥协议") - print(f" - 化合物: {compound}") - print(f" - 容器: {vessel}") - print(f" - 干燥温度: {dry_temp}°C") - print(f" - 干燥时间: {dry_time/60:.0f} 分钟") + print(f"🌡️ DRY: 开始生成干燥协议 ✨") + print(f" 🧪 化合物: {compound}") + print(f" 🥽 容器: {vessel}") + print(f" 🔥 干燥温度: {dry_temp}°C") + print(f" ⏰ 干燥时间: {dry_time/60:.0f} 分钟") # 1. 验证目标容器存在 + print(f"\n📋 步骤1: 验证目标容器 '{vessel}' 是否存在...") if vessel not in G.nodes(): - print(f"DRY: 警告 - 容器 '{vessel}' 不存在于系统中,跳过干燥") + print(f"⚠️ DRY: 警告 - 容器 '{vessel}' 不存在于系统中,跳过干燥 😢") return action_sequence + print(f"✅ 容器 '{vessel}' 验证通过!") # 2. 查找相连的加热器 + print(f"\n🔍 步骤2: 查找与容器相连的加热器...") heater_id = find_connected_heater(G, vessel) if heater_id is None: - print(f"DRY: 警告 - 未找到与容器 '{vessel}' 相连的加热器,跳过干燥") + print(f"😭 DRY: 警告 - 未找到与容器 '{vessel}' 相连的加热器,跳过干燥") + print(f"🎭 添加模拟干燥动作...") # 添加一个等待动作,表示干燥过程(模拟) action_sequence.append({ "action_name": "wait", "action_kwargs": { - "time": 60.0, # 等待1分钟 + "time": 10.0, # 模拟等待时间 "description": f"模拟干燥 {compound} (无加热器可用)" } }) + print(f"📄 DRY: 协议生成完成,共 {len(action_sequence)} 个动作 🎯") return action_sequence + print(f"🎉 找到加热器: {heater_id}!") + # 3. 启动加热器进行干燥 - print(f"DRY: 启动加热器 {heater_id} 进行干燥") + print(f"\n🚀 步骤3: 开始执行干燥流程...") + print(f"🔥 启动加热器 {heater_id} 进行干燥") # 3.1 启动加热 + print(f" ⚡ 动作1: 启动加热到 {dry_temp}°C...") action_sequence.append({ "device_id": heater_id, "action_name": "heat_chill_start", @@ -107,29 +117,35 @@ def generate_dry_protocol( "purpose": f"干燥 {compound}" } }) + print(f" ✅ 加热器启动命令已添加 🔥") # 3.2 等待温度稳定 + print(f" ⏳ 动作2: 等待温度稳定...") action_sequence.append({ "action_name": "wait", "action_kwargs": { - "time": 60.0, + "time": 10.0, "description": f"等待温度稳定到 {dry_temp}°C" } }) + print(f" ✅ 温度稳定等待命令已添加 🌡️") # 3.3 保持干燥温度 + print(f" 🔄 动作3: 保持干燥温度 {simulation_time/60:.0f} 分钟...") action_sequence.append({ "device_id": heater_id, "action_name": "heat_chill", "action_kwargs": { "vessel": vessel, "temp": dry_temp, - "time": dry_time, + "time": simulation_time, "purpose": f"干燥 {compound},保持温度 {dry_temp}°C" } }) + print(f" ✅ 温度保持命令已添加 🌡️⏰") # 3.4 停止加热 + print(f" ⏹️ 动作4: 停止加热...") action_sequence.append({ "device_id": heater_id, "action_name": "heat_chill_stop", @@ -138,18 +154,22 @@ def generate_dry_protocol( "purpose": f"干燥完成,停止加热" } }) + print(f" ✅ 停止加热命令已添加 🛑") # 3.5 等待冷却 + print(f" ❄️ 动作5: 等待冷却...") action_sequence.append({ "action_name": "wait", "action_kwargs": { - "time": 300.0, # 等待5分钟冷却 + "time": 10.0, # 等待10秒冷却 "description": f"等待 {compound} 冷却" } }) + print(f" ✅ 冷却等待命令已添加 🧊") - print(f"DRY: 协议生成完成,共 {len(action_sequence)} 个动作") - print(f"DRY: 预计总时间: {(dry_time + 360)/60:.0f} 分钟") + print(f"\n🎊 DRY: 协议生成完成,共 {len(action_sequence)} 个动作 🎯") + print(f"⏱️ DRY: 预计总时间: {(dry_time + 360)/60:.0f} 分钟 ⌛") + print(f"🏁 所有动作序列准备就绪! ✨") return action_sequence diff --git a/unilabos/compile/evaporate_protocol.py b/unilabos/compile/evaporate_protocol.py index 4eedb3cdd..6a2d6f69a 100644 --- a/unilabos/compile/evaporate_protocol.py +++ b/unilabos/compile/evaporate_protocol.py @@ -7,7 +7,7 @@ def debug_print(message): """调试输出""" - print(f"[EVAPORATE] {message}", flush=True) + print(f"🧪 [EVAPORATE] {message}", flush=True) logger.info(f"[EVAPORATE] {message}") def parse_time_input(time_input: Union[str, float]) -> float: @@ -21,18 +21,20 @@ def parse_time_input(time_input: Union[str, float]) -> float: float: 时间(秒) """ if isinstance(time_input, (int, float)): + debug_print(f"⏱️ 时间输入为数字: {time_input}s ✨") return float(time_input) if not time_input or not str(time_input).strip(): + debug_print(f"⚠️ 时间输入为空,使用默认值: 180s (3分钟) 🕐") return 180.0 # 默认3分钟 time_str = str(time_input).lower().strip() - debug_print(f"解析时间输入: '{time_str}'") + debug_print(f"🔍 解析时间输入: '{time_str}' 📝") # 处理未知时间 if time_str in ['?', 'unknown', 'tbd']: default_time = 180.0 # 默认3分钟 - debug_print(f"检测到未知时间,使用默认值: {default_time}s") + debug_print(f"❓ 检测到未知时间,使用默认值: {default_time}s (3分钟) 🤷‍♀️") return default_time # 移除空格并提取数字和单位 @@ -45,10 +47,10 @@ def parse_time_input(time_input: Union[str, float]) -> float: # 如果无法解析,尝试直接转换为数字(默认秒) try: value = float(time_str) - debug_print(f"时间解析: {time_str} → {value}s(无单位,默认秒)") + debug_print(f"✅ 时间解析成功: {time_str} → {value}s(无单位,默认秒)⏰") return value except ValueError: - debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值180s") + debug_print(f"❌ 无法解析时间: '{time_str}',使用默认值180s (3分钟) 😅") return 180.0 value = float(match.group(1)) @@ -57,14 +59,17 @@ def parse_time_input(time_input: Union[str, float]) -> float: # 转换为秒 if unit in ['min', 'minute']: time_sec = value * 60.0 # min -> s + debug_print(f"🕐 时间转换: {value} 分钟 → {time_sec}s ⏰") elif unit in ['h', 'hr', 'hour']: time_sec = value * 3600.0 # h -> s + debug_print(f"🕐 时间转换: {value} 小时 → {time_sec}s ({time_sec/60:.1f}分钟) ⏰") elif unit in ['d', 'day']: time_sec = value * 86400.0 # d -> s + debug_print(f"🕐 时间转换: {value} 天 → {time_sec}s ({time_sec/3600:.1f}小时) ⏰") else: # s, sec, second 或默认 time_sec = value # 已经是s + debug_print(f"🕐 时间转换: {value}s → {time_sec}s (已是秒) ⏰") - debug_print(f"时间转换: {value}{unit} → {time_sec}s") return time_sec def find_rotavap_device(G: nx.DiGraph, vessel: str = None) -> Optional[str]: @@ -78,28 +83,30 @@ def find_rotavap_device(G: nx.DiGraph, vessel: str = None) -> Optional[str]: Returns: str: 找到的旋转蒸发仪设备ID,如果没找到返回None """ - debug_print("查找旋转蒸发仪设备...") + debug_print("🔍 开始查找旋转蒸发仪设备... 🌪️") # 如果指定了vessel,先检查是否存在且是旋转蒸发仪 if vessel: + debug_print(f"🎯 检查指定设备: {vessel} 🔧") if vessel in G.nodes(): node_data = G.nodes[vessel] node_class = node_data.get('class', '') node_type = node_data.get('type', '') - debug_print(f"检查指定设备 {vessel}: class={node_class}, type={node_type}") + debug_print(f"📋 设备信息 {vessel}: class={node_class}, type={node_type}") # 检查是否为旋转蒸发仪 if any(keyword in str(node_class).lower() for keyword in ['rotavap', 'rotary', 'evaporat']): - debug_print(f"✓ 找到指定的旋转蒸发仪: {vessel}") + debug_print(f"🎉 找到指定的旋转蒸发仪: {vessel} ✨") return vessel elif node_type == 'device': - debug_print(f"✓ 指定设备存在,尝试直接使用: {vessel}") + debug_print(f"✅ 指定设备存在,尝试直接使用: {vessel} 🔧") return vessel else: - debug_print(f"✗ 指定的设备 {vessel} 不存在") + debug_print(f"❌ 指定的设备 {vessel} 不存在 😞") # 在所有设备中查找旋转蒸发仪 + debug_print("🔎 在所有设备中搜索旋转蒸发仪... 🕵️‍♀️") rotavap_candidates = [] for node_id, node_data in G.nodes(data=True): @@ -113,17 +120,17 @@ def find_rotavap_device(G: nx.DiGraph, vessel: str = None) -> Optional[str]: # 检查设备类型 if any(keyword in str(node_class).lower() for keyword in ['rotavap', 'rotary', 'evaporat']): rotavap_candidates.append(node_id) - debug_print(f"✓ 找到旋转蒸发仪候选: {node_id} (class: {node_class})") + debug_print(f"🌟 找到旋转蒸发仪候选: {node_id} (class: {node_class}) 🌪️") elif any(keyword in str(node_id).lower() for keyword in ['rotavap', 'rotary', 'evaporat']): rotavap_candidates.append(node_id) - debug_print(f"✓ 找到旋转蒸发仪候选 (按名称): {node_id}") + debug_print(f"🌟 找到旋转蒸发仪候选 (按名称): {node_id} 🌪️") if rotavap_candidates: selected = rotavap_candidates[0] # 选择第一个找到的 - debug_print(f"✓ 选择旋转蒸发仪: {selected}") + debug_print(f"🎯 选择旋转蒸发仪: {selected} 🏆") return selected - debug_print("✗ 未找到旋转蒸发仪设备") + debug_print("😭 未找到旋转蒸发仪设备 💔") return None def find_connected_vessel(G: nx.DiGraph, rotavap_device: str) -> Optional[str]: @@ -137,31 +144,33 @@ def find_connected_vessel(G: nx.DiGraph, rotavap_device: str) -> Optional[str]: Returns: str: 连接的容器ID,如果没找到返回None """ - debug_print(f"查找与 {rotavap_device} 连接的容器...") + debug_print(f"🔗 查找与 {rotavap_device} 连接的容器... 🥽") # 查看旋转蒸发仪的子设备 rotavap_data = G.nodes[rotavap_device] children = rotavap_data.get('children', []) + debug_print(f"👶 检查子设备: {children}") for child_id in children: if child_id in G.nodes(): child_data = G.nodes[child_id] child_type = child_data.get('type', '') if child_type == 'container': - debug_print(f"✓ 找到连接的容器: {child_id}") + debug_print(f"🎉 找到连接的容器: {child_id} 🥽✨") return child_id # 查看邻接的容器 + debug_print("🤝 检查邻接设备...") for neighbor in G.neighbors(rotavap_device): neighbor_data = G.nodes[neighbor] neighbor_type = neighbor_data.get('type', '') if neighbor_type == 'container': - debug_print(f"✓ 找到邻接的容器: {neighbor}") + debug_print(f"🎉 找到邻接的容器: {neighbor} 🥽✨") return neighbor - debug_print("✗ 未找到连接的容器") + debug_print("😞 未找到连接的容器 💔") return None def generate_evaporate_protocol( @@ -191,110 +200,136 @@ def generate_evaporate_protocol( 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} (类型: {type(time)})") - debug_print(f" - stir_speed: {stir_speed} RPM") - debug_print(f" - solvent: '{solvent}'") - debug_print("=" * 50) + debug_print("🌟" * 20) + 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} (类型: {type(time)})") + debug_print(f" 🌪️ stir_speed: {stir_speed} RPM") + debug_print(f" 🧪 solvent: '{solvent}'") + debug_print("🌟" * 20) # === 步骤1: 查找旋转蒸发仪设备 === - debug_print("步骤1: 查找旋转蒸发仪设备...") + debug_print("📍 步骤1: 查找旋转蒸发仪设备... 🔍") # 验证vessel参数 if not vessel: + debug_print("❌ vessel 参数不能为空! 😱") raise ValueError("vessel 参数不能为空") # 查找旋转蒸发仪设备 rotavap_device = find_rotavap_device(G, vessel) if not rotavap_device: + debug_print("💥 未找到旋转蒸发仪设备! 😭") raise ValueError(f"未找到旋转蒸发仪设备。请检查组态图中是否包含 class 包含 'rotavap'、'rotary' 或 'evaporat' 的设备") + debug_print(f"🎉 成功找到旋转蒸发仪: {rotavap_device} ✨") + # === 步骤2: 确定目标容器 === - debug_print("步骤2: 确定目标容器...") + debug_print("📍 步骤2: 确定目标容器... 🥽") target_vessel = vessel # 如果vessel就是旋转蒸发仪设备,查找连接的容器 if vessel == rotavap_device: + debug_print("🔄 vessel就是旋转蒸发仪,查找连接的容器...") connected_vessel = find_connected_vessel(G, rotavap_device) if connected_vessel: target_vessel = connected_vessel - debug_print(f"使用连接的容器: {target_vessel}") + debug_print(f"✅ 使用连接的容器: {target_vessel} 🥽✨") else: - debug_print(f"未找到连接的容器,使用设备本身: {rotavap_device}") + debug_print(f"⚠️ 未找到连接的容器,使用设备本身: {rotavap_device} 🔧") target_vessel = rotavap_device elif vessel in G.nodes() and G.nodes[vessel].get('type') == 'container': - debug_print(f"使用指定的容器: {vessel}") + debug_print(f"✅ 使用指定的容器: {vessel} 🥽✨") target_vessel = vessel else: - debug_print(f"容器 '{vessel}' 不存在或类型不正确,使用旋转蒸发仪设备: {rotavap_device}") + debug_print(f"⚠️ 容器 '{vessel}' 不存在或类型不正确,使用旋转蒸发仪设备: {rotavap_device} 🔧") target_vessel = rotavap_device # === 🔧 新增:步骤3:单位解析处理 === - debug_print("步骤3: 单位解析处理...") + debug_print("📍 步骤3: 单位解析处理... ⚡") # 解析时间 final_time = parse_time_input(time) - debug_print(f"时间解析: {time} → {final_time}s ({final_time/60:.1f}分钟)") + debug_print(f"🎯 时间解析完成: {time} → {final_time}s ({final_time/60:.1f}分钟) ⏰✨") # === 步骤4: 参数验证和修正 === - debug_print("步骤4: 参数验证和修正...") + debug_print("📍 步骤4: 参数验证和修正... 🔧") # 修正参数范围 if pressure <= 0 or pressure > 1.0: - debug_print(f"真空度 {pressure} bar 超出范围,修正为 0.1 bar") + debug_print(f"⚠️ 真空度 {pressure} bar 超出范围,修正为 0.1 bar 💨") pressure = 0.1 + else: + debug_print(f"✅ 真空度 {pressure} bar 在正常范围内 💨") if temp < 10.0 or temp > 200.0: - debug_print(f"温度 {temp}°C 超出范围,修正为 60°C") + debug_print(f"⚠️ 温度 {temp}°C 超出范围,修正为 60°C 🌡️") temp = 60.0 + else: + debug_print(f"✅ 温度 {temp}°C 在正常范围内 🌡️") if final_time <= 0: - debug_print(f"时间 {final_time}s 无效,修正为 180s") + debug_print(f"⚠️ 时间 {final_time}s 无效,修正为 180s (3分钟) ⏰") final_time = 180.0 + else: + debug_print(f"✅ 时间 {final_time}s ({final_time/60:.1f}分钟) 有效 ⏰") if stir_speed < 10.0 or stir_speed > 300.0: - debug_print(f"旋转速度 {stir_speed} RPM 超出范围,修正为 100 RPM") + debug_print(f"⚠️ 旋转速度 {stir_speed} RPM 超出范围,修正为 100 RPM 🌪️") stir_speed = 100.0 + else: + debug_print(f"✅ 旋转速度 {stir_speed} RPM 在正常范围内 🌪️") # 根据溶剂优化参数 if solvent: - debug_print(f"根据溶剂 '{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("水系溶剂:提高温度和真空度") + 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("易挥发溶剂:降低温度和真空度") + 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("🔥 高沸点溶剂:提高温度,降低真空度 🌡️💨") + else: + debug_print("🧪 通用溶剂,使用标准参数 ✨") + else: + debug_print("🤷‍♀️ 未指定溶剂,使用默认参数 ✨") - debug_print(f"最终参数: pressure={pressure}, temp={temp}, time={final_time}, stir_speed={stir_speed}") + debug_print(f"🎯 最终参数: pressure={pressure} bar 💨, temp={temp}°C 🌡️, time={final_time}s ⏰, stir_speed={stir_speed} RPM 🌪️") # === 步骤5: 生成动作序列 === - debug_print("步骤5: 生成动作序列...") + debug_print("📍 步骤5: 生成动作序列... 🎬") action_sequence = [] - # 等待稳定 + # 1. 等待稳定 + debug_print(" 🔄 动作1: 添加初始等待稳定... ⏳") action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 10} }) + debug_print(" ✅ 初始等待动作已添加 ⏳✨") + + # 2. 执行蒸发 + debug_print(f" 🌪️ 动作2: 执行蒸发操作...") + debug_print(f" 🔧 设备: {rotavap_device}") + debug_print(f" 🥽 容器: {target_vessel}") + debug_print(f" 💨 真空度: {pressure} bar") + debug_print(f" 🌡️ 温度: {temp}°C") + debug_print(f" ⏰ 时间: {final_time}s ({final_time/60:.1f}分钟)") + debug_print(f" 🌪️ 旋转速度: {stir_speed} RPM") - # 执行蒸发 - debug_print(f"执行蒸发: 设备={rotavap_device}, 容器={target_vessel}") evaporate_action = { "device_id": rotavap_device, "action_name": "evaporate", @@ -308,97 +343,24 @@ def generate_evaporate_protocol( } } action_sequence.append(evaporate_action) + debug_print(" ✅ 蒸发动作已添加 🌪️✨") - # 蒸发后等待 + # 3. 蒸发后等待 + debug_print(" 🔄 动作3: 添加蒸发后等待... ⏳") action_sequence.append({ "action_name": "wait", - "action_kwargs": {"time": 30} + "action_kwargs": {"time": 10} }) + debug_print(" ✅ 蒸发后等待动作已添加 ⏳✨") # === 总结 === - debug_print("=" * 50) - debug_print(f"蒸发协议生成完成") - debug_print(f"总动作数: {len(action_sequence)}") - debug_print(f"旋转蒸发仪: {rotavap_device}") - debug_print(f"目标容器: {target_vessel}") - debug_print(f"蒸发参数: {pressure} bar, {temp}°C, {final_time}s, {stir_speed} RPM") - debug_print("=" * 50) + debug_print("🎊" * 20) + debug_print(f"🎉 蒸发协议生成完成! ✨") + debug_print(f"📊 总动作数: {len(action_sequence)} 个 📝") + debug_print(f"🌪️ 旋转蒸发仪: {rotavap_device} 🔧") + debug_print(f"🥽 目标容器: {target_vessel} 🧪") + debug_print(f"⚙️ 蒸发参数: {pressure} bar 💨, {temp}°C 🌡️, {final_time}s ⏰, {stir_speed} RPM 🌪️") + debug_print(f"⏱️ 预计总时间: {(final_time + 20)/60:.1f} 分钟 ⌛") + debug_print("🎊" * 20) return action_sequence - -# === 便捷函数 === - -def generate_quick_evaporate_protocol( - G: nx.DiGraph, - vessel: str, - **kwargs -) -> List[Dict[str, Any]]: - """快速蒸发:低温短时间""" - return generate_evaporate_protocol( - G, vessel, - pressure=0.2, - temp=40.0, - time="15 min", # 🔧 使用带单位的时间 - stir_speed=80.0, - **kwargs - ) - -def generate_gentle_evaporate_protocol( - G: nx.DiGraph, - vessel: str, - **kwargs -) -> List[Dict[str, Any]]: - """温和蒸发:中等条件""" - return generate_evaporate_protocol( - G, vessel, - pressure=0.1, - temp=50.0, - time="45 min", # 🔧 使用带单位的时间 - stir_speed=60.0, - **kwargs - ) - -def generate_high_vacuum_evaporate_protocol( - G: nx.DiGraph, - vessel: str, - **kwargs -) -> List[Dict[str, Any]]: - """高真空蒸发:低温高真空""" - return generate_evaporate_protocol( - G, vessel, - pressure=0.01, - temp=35.0, - time="1 h", # 🔧 使用带单位的时间 - stir_speed=120.0, - **kwargs - ) - -def generate_standard_evaporate_protocol( - G: nx.DiGraph, - vessel: str, - **kwargs -) -> List[Dict[str, Any]]: - """标准蒸发:常用参数""" - return generate_evaporate_protocol( - G, vessel, - pressure=0.1, - temp=60.0, - time="3 min", # 🔧 使用带单位的时间 - stir_speed=100.0, - **kwargs - ) - -# 测试函数 -def test_time_parsing(): - """测试时间解析功能""" - print("=== EVAPORATE 时间解析测试 ===") - - test_times = ["3 min", "180", "0.5 h", "2 hours", "?", "unknown", "1.5", "30 s"] - for time_str in test_times: - result = parse_time_input(time_str) - print(f"时间解析: '{time_str}' → {result}s ({result/60:.1f}分钟)") - - print("✅ 测试完成") - -if __name__ == "__main__": - test_time_parsing() diff --git a/unilabos/compile/filter_protocol.py b/unilabos/compile/filter_protocol.py index 662968480..d974a416e 100644 --- a/unilabos/compile/filter_protocol.py +++ b/unilabos/compile/filter_protocol.py @@ -7,12 +7,12 @@ def debug_print(message): """调试输出""" - print(f"[FILTER] {message}", flush=True) + print(f"🧪 [FILTER] {message}", flush=True) logger.info(f"[FILTER] {message}") def find_filter_device(G: nx.DiGraph) -> str: """查找过滤器设备""" - debug_print("查找过滤器设备...") + debug_print("🔍 查找过滤器设备... 🌊") # 查找过滤器设备 for node in G.nodes(): @@ -20,27 +20,33 @@ def find_filter_device(G: nx.DiGraph) -> str: node_class = node_data.get('class', '') or '' if 'filter' in node_class.lower() or 'filter' in node.lower(): - debug_print(f"找到过滤器设备: {node}") + debug_print(f"🎉 找到过滤器设备: {node} ✨") return node # 如果没找到,寻找可能的过滤器名称 + debug_print("🔎 在预定义名称中搜索过滤器... 📋") possible_names = ["filter", "filter_1", "virtual_filter", "filtration_unit"] for name in possible_names: if name in G.nodes(): - debug_print(f"找到过滤器设备: {name}") + debug_print(f"🎉 找到过滤器设备: {name} ✨") return name + debug_print("😭 未找到过滤器设备 💔") raise ValueError("未找到过滤器设备") def validate_vessel(G: nx.DiGraph, vessel: str, vessel_type: str = "容器") -> None: """验证容器是否存在""" + debug_print(f"🔍 验证{vessel_type}: '{vessel}' 🧪") + if not vessel: + debug_print(f"❌ {vessel_type}不能为空! 😱") raise ValueError(f"{vessel_type}不能为空") if vessel not in G.nodes(): + debug_print(f"❌ {vessel_type} '{vessel}' 不存在于系统中! 😞") raise ValueError(f"{vessel_type} '{vessel}' 不存在于系统中") - debug_print(f"✅ {vessel_type} '{vessel}' 验证通过") + debug_print(f"✅ {vessel_type} '{vessel}' 验证通过 🎯") def generate_filter_protocol( G: nx.DiGraph, @@ -61,47 +67,53 @@ def generate_filter_protocol( List[Dict[str, Any]]: 过滤操作的动作序列 """ - debug_print("=" * 60) - debug_print("开始生成过滤协议") - debug_print(f"输入参数:") - debug_print(f" - vessel: {vessel}") - debug_print(f" - filtrate_vessel: {filtrate_vessel}") - debug_print(f" - 其他参数: {kwargs}") - debug_print("=" * 60) + debug_print("🌊" * 20) + debug_print("🚀 开始生成过滤协议 ✨") + debug_print(f"📝 输入参数:") + debug_print(f" 🥽 vessel: {vessel}") + debug_print(f" 🧪 filtrate_vessel: {filtrate_vessel}") + debug_print(f" ⚙️ 其他参数: {kwargs}") + debug_print("🌊" * 20) action_sequence = [] # === 参数验证 === - debug_print("步骤1: 参数验证...") + debug_print("📍 步骤1: 参数验证... 🔧") # 验证必需参数 + debug_print(" 🔍 验证必需参数...") validate_vessel(G, vessel, "过滤容器") + debug_print(" ✅ 必需参数验证完成 🎯") # 验证可选参数 + debug_print(" 🔍 验证可选参数...") if filtrate_vessel: validate_vessel(G, filtrate_vessel, "滤液容器") - debug_print("模式: 过滤并收集滤液") + debug_print(" 🌊 模式: 过滤并收集滤液 💧") else: - debug_print("模式: 过滤并收集固体") + debug_print(" 🧱 模式: 过滤并收集固体 🔬") + debug_print(" ✅ 可选参数验证完成 🎯") # === 查找设备 === - debug_print("步骤2: 查找设备...") + debug_print("📍 步骤2: 查找设备... 🔍") try: + debug_print(" 🔎 搜索过滤器设备...") filter_device = find_filter_device(G) - debug_print(f"使用过滤器设备: {filter_device}") + debug_print(f" 🎉 使用过滤器设备: {filter_device} 🌊✨") except Exception as e: - debug_print(f"❌ 设备查找失败: {str(e)}") + debug_print(f" ❌ 设备查找失败: {str(e)} 😭") raise ValueError(f"设备查找失败: {str(e)}") # === 转移到过滤器(如果需要)=== - debug_print("步骤3: 转移到过滤器...") + debug_print("📍 步骤3: 转移到过滤器... 🚚") if vessel != filter_device: - debug_print(f"需要转移: {vessel} → {filter_device}") + debug_print(f" 🚛 需要转移: {vessel} → {filter_device} 📦") try: + debug_print(" 🔄 开始执行转移操作...") # 使用pump protocol转移液体到过滤器 transfer_actions = generate_pump_protocol_with_rinsing( G=G, @@ -121,20 +133,21 @@ def generate_filter_protocol( if transfer_actions: action_sequence.extend(transfer_actions) - debug_print(f"✅ 添加了 {len(transfer_actions)} 个转移动作") + debug_print(f" ✅ 添加了 {len(transfer_actions)} 个转移动作 🚚✨") else: - debug_print("⚠️ 转移协议返回空序列") + debug_print(" ⚠️ 转移协议返回空序列 🤔") except Exception as e: - debug_print(f"❌ 转移失败: {str(e)}") - # 继续执行,可能是直接连接的过滤器 + debug_print(f" ❌ 转移失败: {str(e)} 😞") + debug_print(" 🔄 继续执行,可能是直接连接的过滤器 🤞") else: - debug_print("过滤容器就是过滤器,无需转移") + debug_print(" ✅ 过滤容器就是过滤器,无需转移 🎯") # === 执行过滤操作 === - debug_print("步骤4: 执行过滤操作...") + debug_print("📍 步骤4: 执行过滤操作... 🌊") # 构建过滤动作参数 + debug_print(" ⚙️ 构建过滤参数...") filter_kwargs = { "vessel": filter_device, # 过滤器设备 "filtrate_vessel": filtrate_vessel, # 滤液容器(可能为空) @@ -145,7 +158,8 @@ def generate_filter_protocol( "volume": kwargs.get("volume", 0.0) # 0表示过滤所有 } - debug_print(f"过滤参数: {filter_kwargs}") + debug_print(f" 📋 过滤参数: {filter_kwargs}") + debug_print(" 🌊 开始过滤操作...") # 过滤动作 filter_action = { @@ -154,20 +168,24 @@ def generate_filter_protocol( "action_kwargs": filter_kwargs } action_sequence.append(filter_action) + debug_print(" ✅ 过滤动作已添加 🌊✨") # 过滤后等待 + debug_print(" ⏳ 添加过滤后等待...") action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 10.0} }) + debug_print(" ✅ 过滤后等待动作已添加 ⏰✨") # === 收集滤液(如果需要)=== - debug_print("步骤5: 收集滤液...") + debug_print("📍 步骤5: 收集滤液... 💧") if filtrate_vessel: - debug_print(f"收集滤液: {filter_device} → {filtrate_vessel}") + debug_print(f" 🧪 收集滤液: {filter_device} → {filtrate_vessel} 💧") try: + debug_print(" 🔄 开始执行收集操作...") # 使用pump protocol收集滤液 collect_actions = generate_pump_protocol_with_rinsing( G=G, @@ -187,29 +205,32 @@ def generate_filter_protocol( if collect_actions: action_sequence.extend(collect_actions) - debug_print(f"✅ 添加了 {len(collect_actions)} 个收集动作") + debug_print(f" ✅ 添加了 {len(collect_actions)} 个收集动作 🧪✨") else: - debug_print("⚠️ 收集协议返回空序列") + debug_print(" ⚠️ 收集协议返回空序列 🤔") except Exception as e: - debug_print(f"❌ 收集滤液失败: {str(e)}") - # 继续执行,可能滤液直接流入指定容器 + debug_print(f" ❌ 收集滤液失败: {str(e)} 😞") + debug_print(" 🔄 继续执行,可能滤液直接流入指定容器 🤞") else: - debug_print("未指定滤液容器,固体保留在过滤器中") + debug_print(" 🧱 未指定滤液容器,固体保留在过滤器中 🔬") # === 最终等待 === + debug_print("📍 步骤6: 最终等待... ⏰") action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 5.0} }) + debug_print(" ✅ 最终等待动作已添加 ⏰✨") # === 总结 === - debug_print("=" * 60) - debug_print(f"过滤协议生成完成") - debug_print(f"总动作数: {len(action_sequence)}") - debug_print(f"过滤容器: {vessel}") - debug_print(f"过滤器设备: {filter_device}") - debug_print(f"滤液容器: {filtrate_vessel or '无(保留固体)'}") - debug_print("=" * 60) + debug_print("🎊" * 20) + debug_print(f"🎉 过滤协议生成完成! ✨") + debug_print(f"📊 总动作数: {len(action_sequence)} 个 📝") + debug_print(f"🥽 过滤容器: {vessel} 🧪") + debug_print(f"🌊 过滤器设备: {filter_device} 🔧") + debug_print(f"💧 滤液容器: {filtrate_vessel or '无(保留固体)'} 🧱") + debug_print(f"⏱️ 预计总时间: {(len(action_sequence) * 5):.0f} 秒 ⌛") + debug_print("🎊" * 20) return action_sequence diff --git a/unilabos/compile/heatchill_protocol.py b/unilabos/compile/heatchill_protocol.py index 297015e34..f8bcc110a 100644 --- a/unilabos/compile/heatchill_protocol.py +++ b/unilabos/compile/heatchill_protocol.py @@ -7,179 +7,129 @@ def debug_print(message): """调试输出""" - print(f"[HEATCHILL] {message}", flush=True) + print(f"🌡️ [HEATCHILL] {message}", flush=True) logger.info(f"[HEATCHILL] {message}") -def parse_time_with_units(time_input: Union[str, float, int], default_unit: str = "s") -> float: +def parse_time_input(time_input: Union[str, float, int]) -> float: """ - 解析带单位的时间输入 + 解析时间输入(统一函数) Args: time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0) - default_unit: 默认单位(默认为秒) Returns: float: 时间(秒) """ if not time_input: - return 0.0 + return 300.0 - # 处理数值输入 + # 🔢 处理数值输入 if isinstance(time_input, (int, float)): result = float(time_input) - debug_print(f"数值时间输入: {time_input} → {result}s(默认单位)") + debug_print(f"⏰ 数值时间: {time_input} → {result}s") return result - # 处理字符串输入 + # 📝 处理字符串输入 time_str = str(time_input).lower().strip() - debug_print(f"解析时间字符串: '{time_str}'") + debug_print(f"🔍 解析时间: '{time_str}'") - # 处理特殊值 - if time_str in ['?', 'unknown', 'tbd', 'to be determined']: - default_time = 300.0 # 5分钟默认值 - debug_print(f"检测到未知时间,使用默认值: {default_time}s") - return default_time + # ❓ 特殊值处理 + special_times = { + '?': 300.0, 'unknown': 300.0, 'tbd': 300.0, + 'overnight': 43200.0, 'several hours': 10800.0, + 'few hours': 7200.0, 'long time': 3600.0, 'short time': 300.0 + } + + if time_str in special_times: + result = special_times[time_str] + debug_print(f"🎯 特殊时间: '{time_str}' → {result}s ({result/60:.1f}分钟)") + return result - # 如果是纯数字,使用默认单位 + # 🔢 纯数字处理 try: - value = float(time_str) - if default_unit == "s": - result = value - elif default_unit in ["min", "minute"]: - result = value * 60.0 - elif default_unit in ["h", "hour"]: - result = value * 3600.0 - else: - result = value # 默认秒 - debug_print(f"纯数字输入: {time_str} → {result}s(单位: {default_unit})") + result = float(time_str) + debug_print(f"⏰ 纯数字: {time_str} → {result}s") return result except ValueError: pass - # 使用正则表达式匹配数字和单位 + # 📐 正则表达式解析 pattern = r'(\d+\.?\d*)\s*([a-z]*)' match = re.match(pattern, time_str) if not match: - debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 60s") - return 60.0 + debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 300s") + return 300.0 value = float(match.group(1)) - unit = match.group(2) or default_unit + unit = match.group(2) or 's' - # 单位转换映射 + # 📏 单位转换 unit_multipliers = { - # 秒 - 's': 1.0, - 'sec': 1.0, - 'second': 1.0, - 'seconds': 1.0, - - # 分钟 - 'm': 60.0, - 'min': 60.0, - 'mins': 60.0, - 'minute': 60.0, - 'minutes': 60.0, - - # 小时 - 'h': 3600.0, - 'hr': 3600.0, - 'hrs': 3600.0, - 'hour': 3600.0, - 'hours': 3600.0, - - # 天 - 'd': 86400.0, - 'day': 86400.0, - 'days': 86400.0, + 's': 1.0, 'sec': 1.0, 'second': 1.0, 'seconds': 1.0, + 'm': 60.0, 'min': 60.0, 'mins': 60.0, 'minute': 60.0, 'minutes': 60.0, + 'h': 3600.0, 'hr': 3600.0, 'hrs': 3600.0, 'hour': 3600.0, 'hours': 3600.0, + 'd': 86400.0, 'day': 86400.0, 'days': 86400.0 } multiplier = unit_multipliers.get(unit, 1.0) result = value * multiplier - debug_print(f"时间解析: '{time_str}' → {value} {unit} → {result}s") + debug_print(f"✅ 时间解析: '{time_str}' → {value} {unit} → {result}s ({result/60:.1f}分钟)") return result -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) +def parse_temp_input(temp_input: Union[str, float], default_temp: float = 25.0) -> float: + """ + 解析温度输入(统一函数) - if match: - return float(match.group(1)) + Args: + temp_input: 温度输入 + default_temp: 默认温度 + + Returns: + float: 温度(°C) + """ + if not temp_input: + return default_temp - return 25.0 - -def parse_time_spec(time_spec: str) -> float: - """解析时间规格为秒数""" - if not time_spec: - return 300.0 + # 🔢 数值输入 + if isinstance(temp_input, (int, float)): + result = float(temp_input) + debug_print(f"🌡️ 数值温度: {temp_input} → {result}°C") + return result - time_spec = time_spec.strip().lower() + # 📝 字符串输入 + temp_str = str(temp_input).lower().strip() + debug_print(f"🔍 解析温度: '{temp_str}'") - # 特殊时间规格 - 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分钟 + # 🎯 特殊温度 + 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 time_spec in special_times: - return special_times[time_spec] + if temp_str in special_temps: + result = special_temps[temp_str] + debug_print(f"🎯 特殊温度: '{temp_str}' → {result}°C") + return result - # 解析带单位的时间(如 "2 h") - time_pattern = r'(\d+(?:\.\d+)?)\s*([a-zA-Z]+)' - match = re.search(time_pattern, time_spec) + # 📐 正则解析(如 "256 °C") + temp_pattern = r'(\d+(?:\.\d+)?)\s*°?[cf]?' + match = re.search(temp_pattern, temp_str) 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 + result = float(match.group(1)) + debug_print(f"✅ 温度解析: '{temp_str}' → {result}°C") + return result - return 300.0 + debug_print(f"⚠️ 无法解析温度: '{temp_str}',使用默认值: {default_temp}°C") + return default_temp def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str: """查找与指定容器相连的加热/冷却设备""" - debug_print(f"查找加热设备,目标容器: {vessel}") + debug_print(f"🔍 查找加热设备,目标容器: {vessel}") - # 查找所有加热/冷却设备节点 + # 🔧 查找所有加热设备 heatchill_nodes = [] for node in G.nodes(): node_data = G.nodes[node] @@ -187,28 +137,55 @@ def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str: if 'heatchill' in node_class.lower() or 'virtual_heatchill' in node_class: heatchill_nodes.append(node) - debug_print(f"找到加热设备: {node}") + debug_print(f"🎉 找到加热设备: {node}") - if vessel: - # 检查哪个加热设备与目标容器相连 + # 🔗 检查连接 + if vessel and heatchill_nodes: for heatchill in heatchill_nodes: if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill): - debug_print(f"加热设备 '{heatchill}' 与容器 '{vessel}' 相连") + debug_print(f"✅ 加热设备 '{heatchill}' 与容器 '{vessel}' 相连") return heatchill - # 如果没有指定容器或没有直接连接,返回第一个可用的加热设备 + # 🎯 使用第一个可用设备 if heatchill_nodes: - debug_print(f"使用第一个加热设备: {heatchill_nodes[0]}") - return heatchill_nodes[0] + selected = heatchill_nodes[0] + debug_print(f"🔧 使用第一个加热设备: {selected}") + return selected - debug_print("未找到加热设备,使用默认设备") + # 🆘 默认设备 + debug_print("⚠️ 未找到加热设备,使用默认设备") return "heatchill_1" +def validate_and_fix_params(temp: float, time: float, stir_speed: float) -> tuple: + """验证和修正参数""" + # 🌡️ 温度范围验证 + if temp < -50.0 or temp > 300.0: + debug_print(f"⚠️ 温度 {temp}°C 超出范围,修正为 25°C") + temp = 25.0 + else: + debug_print(f"✅ 温度 {temp}°C 在正常范围内") + + # ⏰ 时间验证 + if time < 0: + debug_print(f"⚠️ 时间 {time}s 无效,修正为 300s") + time = 300.0 + else: + debug_print(f"✅ 时间 {time}s ({time/60:.1f}分钟) 有效") + + # 🌪️ 搅拌速度验证 + if stir_speed < 0 or stir_speed > 1500.0: + debug_print(f"⚠️ 搅拌速度 {stir_speed} RPM 超出范围,修正为 300 RPM") + stir_speed = 300.0 + else: + debug_print(f"✅ 搅拌速度 {stir_speed} RPM 在正常范围内") + + return temp, time, stir_speed + def generate_heat_chill_protocol( G: nx.DiGraph, vessel: str, temp: float = 25.0, - time: Union[str, float] = "300", # 🔧 修改:支持字符串时间 + time: Union[str, float] = "300", temp_spec: str = "", time_spec: str = "", pressure: str = "", @@ -219,249 +196,111 @@ def generate_heat_chill_protocol( **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" - time: {time} (类型: {type(time)})") - 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 = [] - - # === 参数验证 === - debug_print("步骤1: 参数验证...") - - # 验证必需参数 + debug_print("🌡️" * 20) + debug_print("🚀 开始生成加热冷却协议 ✨") + debug_print(f"📝 输入参数:") + debug_print(f" 🥽 vessel: {vessel}") + debug_print(f" 🌡️ temp: {temp}°C") + debug_print(f" ⏰ time: {time}") + debug_print(f" 🎯 temp_spec: {temp_spec}") + debug_print(f" ⏱️ time_spec: {time_spec}") + debug_print(f" 🌪️ stir: {stir} ({stir_speed} RPM)") + debug_print("🌡️" * 20) + + # 📋 参数验证 + debug_print("📍 步骤1: 参数验证... 🔧") if not vessel: + debug_print("❌ vessel 参数不能为空! 😱") raise ValueError("vessel 参数不能为空") if vessel not in G.nodes(): + debug_print(f"❌ 容器 '{vessel}' 不存在于系统中! 😞") raise ValueError(f"容器 '{vessel}' 不存在于系统中") - # === 🔧 新增:单位解析处理 === - debug_print("步骤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") + debug_print("✅ 基础参数验证通过 🎯") - # 时间解析:优先使用 time_spec,然后是 time - if time_spec: - final_time = parse_time_spec(time_spec) # 使用现有的time_spec解析 - debug_print(f"时间解析: '{time_spec}' → {final_time}s") - else: - final_time = parse_time_with_units(time, "s") - debug_print(f"时间解析: {time} → {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 + # 🔄 参数解析 + debug_print("📍 步骤2: 参数解析... ⚡") - if final_time < 0: - debug_print(f"时间 {final_time}s 无效,修正为 300s") - final_time = 300.0 + #温度解析:优先使用 temp_spec + final_temp = parse_temp_input(temp_spec, temp) if temp_spec else temp - if stir_speed < 0 or stir_speed > 1500.0: - debug_print(f"搅拌速度 {stir_speed} RPM 超出范围,修正为 300 RPM") - stir_speed = 300.0 + # 时间解析:优先使用 time_spec + final_time = parse_time_input(time_spec) if time_spec else parse_time_input(time) - debug_print(f"✅ 单位解析和参数验证通过") + # 参数修正 + final_temp, final_time, stir_speed = validate_and_fix_params(final_temp, final_time, stir_speed) - # === 查找加热设备 === - debug_print("步骤3: 查找加热设备...") + debug_print(f"🎯 最终参数: temp={final_temp}°C, time={final_time}s, stir_speed={stir_speed} RPM") + # 🔍 查找设备 + debug_print("📍 步骤3: 查找加热设备... 🔍") try: heatchill_id = find_connected_heatchill(G, vessel) - debug_print(f"设备配置: 加热设备 = {heatchill_id}") - + debug_print(f"🎉 使用加热设备: {heatchill_id} ✨") except Exception as e: - debug_print(f"❌ 设备查找失败: {str(e)}") + debug_print(f"❌ 设备查找失败: {str(e)} 😭") raise ValueError(f"无法找到加热设备: {str(e)}") - # === 执行加热操作 === - debug_print("步骤4: 执行加热操作...") + # 🚀 生成动作 + debug_print("📍 步骤4: 生成加热动作... 🔥") + + # 🕐 模拟运行时间优化 + debug_print(" ⏱️ 检查模拟运行时间限制...") + original_time = final_time + simulation_time_limit = 100.0 # 模拟运行时间限制:100秒 + if final_time > simulation_time_limit: + final_time = simulation_time_limit + debug_print(f" 🎮 模拟运行优化: {original_time}s → {final_time}s (限制为{simulation_time_limit}s) ⚡") + debug_print(f" 📊 时间缩短: {original_time/60:.1f}分钟 → {final_time/60:.1f}分钟 🚀") + else: + debug_print(f" ✅ 时间在限制内: {final_time}s ({final_time/60:.1f}分钟) 保持不变 🎯") + + action_sequence = [] heatchill_action = { "device_id": heatchill_id, "action_name": "heat_chill", "action_kwargs": { "vessel": vessel, - "temp": float(final_temp), # 🔧 确保是浮点数 - "time": float(final_time), # 🔧 确保是浮点数 - "stir": bool(stir), # 🔧 确保是布尔值 - "stir_speed": float(stir_speed), # 🔧 确保是浮点数 - "purpose": str(purpose or f"加热到 {final_temp}°C") # 🔧 确保是字符串 + "temp": float(final_temp), + "time": float(final_time), + "stir": bool(stir), + "stir_speed": float(stir_speed), + "purpose": str(purpose or f"加热到 {final_temp}°C") + (f" (模拟时间: {final_time}s)" if original_time != final_time else "") } } - action_sequence.append(heatchill_action) + debug_print("✅ 加热动作已添加 🔥✨") + + # 显示时间调整信息 + if original_time != final_time: + debug_print(f" 🎭 模拟优化说明: 原计划 {original_time/60:.1f}分钟,实际模拟 {final_time/60:.1f}分钟 ⚡") - # === 总结 === - 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) + # 🎊 总结 + debug_print("🎊" * 20) + 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}分钟)") + debug_print("🎊" * 20) return action_sequence def generate_heat_chill_to_temp_protocol( - G: nx.DiGraph, - vessel: str, - temp: float = 25.0, - time: Union[str, float] = 300.0, # 🔧 也支持字符串 - temp_spec: str = "", - time_spec: str = "", - pressure: str = "", - reflux_solvent: str = "", - stir: bool = False, - stir_speed: float = 300.0, - purpose: str = "", - **kwargs # 🔧 接受额外参数,增强兼容性 + G: nx.DiGraph, + vessel: str, + temp: float = 25.0, + time: Union[str, float] = 100.0, + **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} (类型: {type(time)})") - 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 = [] - - # === 参数验证 === - debug_print("步骤1: 参数验证...") - - # 验证必需参数 - if not vessel: - raise ValueError("vessel 参数不能为空") - - if vessel not in G.nodes(): - raise ValueError(f"容器 '{vessel}' 不存在于系统中") - - # 温度解析:优先使用 temp_spec,然后是 temp - final_temp = temp - if temp_spec: - final_temp = parse_temp_spec(temp_spec) - debug_print(f"温度解析: '{temp_spec}' → {final_temp}°C") - - # 🔧 修复:时间解析,支持字符串输入 - if time_spec: - final_time = parse_time_spec(time_spec) - debug_print(f"时间解析: '{time_spec}' → {final_time}s ({final_time / 60:.1f}分钟)") - else: - final_time = parse_time_with_units(time, "s") - debug_print(f"时间解析: {time} → {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) - debug_print(f"设备配置: 加热设备 = {heatchill_id}") - - except Exception as e: - debug_print(f"❌ 设备查找失败: {str(e)}") - raise ValueError(f"无法找到加热设备: {str(e)}") - - # === 执行加热操作 === - debug_print("步骤3: 执行加热操作...") - - heatchill_action = { - "device_id": heatchill_id, - "action_name": "heat_chill", - "action_kwargs": { - "vessel": vessel, - "temp": float(final_temp), # 🔧 确保是浮点数 - "time": float(final_time), # 🔧 确保是浮点数 - "stir": bool(stir), # 🔧 确保是布尔值 - "stir_speed": float(stir_speed), # 🔧 确保是浮点数 - "purpose": str(purpose or f"加热到 {final_temp}°C") # 🔧 确保是字符串 - } - } - - action_sequence.append(heatchill_action) - - # === 总结 === - 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 - + """生成加热到指定温度的协议(简化版)""" + debug_print(f"🌡️ 生成加热到温度协议: {vessel} → {temp}°C") + return generate_heat_chill_protocol(G, vessel, temp, time, **kwargs) def generate_heat_chill_start_protocol( G: nx.DiGraph, @@ -472,33 +311,19 @@ def generate_heat_chill_start_protocol( ) -> 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) + debug_print("🔥 开始生成启动加热协议 ✨") + debug_print(f"🥽 vessel: {vessel}, 🌡️ temp: {temp}°C") - action_sequence = [] + # 基础验证 + if not vessel or vessel not in G.nodes(): + debug_print("❌ 容器验证失败!") + raise ValueError("vessel 参数无效") - # 验证参数 - if not vessel: - raise ValueError("vessel 参数不能为空") - - if vessel not in G.nodes(): - raise ValueError(f"容器 '{vessel}' 不存在于系统中") - - # 查找加热设备 - try: - heatchill_id = find_connected_heatchill(G, vessel) - debug_print(f"设备配置: 加热设备 = {heatchill_id}") - except Exception as e: - debug_print(f"❌ 设备查找失败: {str(e)}") - raise ValueError(f"无法找到加热设备: {str(e)}") + # 查找设备 + heatchill_id = find_connected_heatchill(G, vessel) - # 执行开始加热操作 - start_action = { + # 生成动作 + action_sequence = [{ "device_id": heatchill_id, "action_name": "heat_chill_start", "action_kwargs": { @@ -506,11 +331,9 @@ def generate_heat_chill_start_protocol( "temp": temp, "purpose": purpose or f"开始加热到 {temp}°C" } - } + }] - action_sequence.append(start_action) - - debug_print(f"启动加热协议生成完成,动作数: {len(action_sequence)}") + debug_print(f"✅ 启动加热协议生成完成 🎯") return action_sequence def generate_heat_chill_stop_protocol( @@ -520,48 +343,34 @@ def generate_heat_chill_stop_protocol( ) -> List[Dict[str, Any]]: """生成停止加热操作的协议序列""" - debug_print("=" * 50) - debug_print("开始生成停止加热协议") - debug_print(f"输入参数:") - debug_print(f" - vessel: {vessel}") - debug_print("=" * 50) + debug_print("🛑 开始生成停止加热协议 ✨") + debug_print(f"🥽 vessel: {vessel}") - action_sequence = [] + # 基础验证 + if not vessel or vessel not in G.nodes(): + debug_print("❌ 容器验证失败!") + raise ValueError("vessel 参数无效") - # 验证参数 - if not vessel: - raise ValueError("vessel 参数不能为空") + # 查找设备 + heatchill_id = find_connected_heatchill(G, vessel) - if vessel not in G.nodes(): - raise ValueError(f"容器 '{vessel}' 不存在于系统中") - - # 查找加热设备 - try: - heatchill_id = find_connected_heatchill(G, vessel) - debug_print(f"设备配置: 加热设备 = {heatchill_id}") - except Exception as e: - debug_print(f"❌ 设备查找失败: {str(e)}") - raise ValueError(f"无法找到加热设备: {str(e)}") - - # 执行停止加热操作 - stop_action = { + # 生成动作 + action_sequence = [{ "device_id": heatchill_id, "action_name": "heat_chill_stop", "action_kwargs": { "vessel": vessel } - } - - action_sequence.append(stop_action) + }] - debug_print(f"停止加热协议生成完成,动作数: {len(action_sequence)}") + debug_print(f"✅ 停止加热协议生成完成 🎯") return action_sequence # 测试函数 def test_heatchill_protocol(): """测试加热协议""" - debug_print("=== HEATCHILL PROTOCOL 测试 ===") - debug_print("✅ 测试完成") + debug_print("🧪 === HEATCHILL PROTOCOL 测试 === ✨") + debug_print("✅ 测试完成 🎉") if __name__ == "__main__": test_heatchill_protocol() \ No newline at end of file diff --git a/unilabos/compile/hydrogenate_protocol.py b/unilabos/compile/hydrogenate_protocol.py index 80707057d..81cd926c5 100644 --- a/unilabos/compile/hydrogenate_protocol.py +++ b/unilabos/compile/hydrogenate_protocol.py @@ -255,11 +255,23 @@ def generate_hydrogenate_protocol( action_sequence.append({ "action_name": "wait", "action_kwargs": { - "time": 120.0, + "time": 20.0, "description": f"等待温度稳定到 {temperature}°C" } }) + # 🕐 模拟运行时间优化 + print("HYDROGENATE: 检查模拟运行时间限制...") + original_reaction_time = reaction_time + simulation_time_limit = 60.0 # 模拟运行时间限制:60秒 + + if reaction_time > simulation_time_limit: + reaction_time = simulation_time_limit + print(f"HYDROGENATE: 模拟运行优化: {original_reaction_time}s → {reaction_time}s (限制为{simulation_time_limit}s)") + print(f"HYDROGENATE: 时间缩短: {original_reaction_time/3600:.2f}小时 → {reaction_time/60:.1f}分钟") + else: + print(f"HYDROGENATE: 时间在限制内: {reaction_time}s ({reaction_time/60:.1f}分钟) 保持不变") + # 保持反应温度 action_sequence.append({ "device_id": heater_id, @@ -268,19 +280,41 @@ def generate_hydrogenate_protocol( "vessel": vessel, "temp": temperature, "time": reaction_time, - "purpose": f"氢化反应: 保持 {temperature}°C,反应 {reaction_time/3600:.1f} 小时" + "purpose": f"氢化反应: 保持 {temperature}°C,反应 {reaction_time/60:.1f}分钟" + (f" (模拟时间)" if original_reaction_time != reaction_time else "") } }) + + # 显示时间调整信息 + if original_reaction_time != reaction_time: + print(f"HYDROGENATE: 模拟优化说明: 原计划 {original_reaction_time/3600:.2f}小时,实际模拟 {reaction_time/60:.1f}分钟") + else: print(f"HYDROGENATE: 警告 - 未找到加热器,使用室温反应") + + # 🕐 室温反应也需要时间优化 + print("HYDROGENATE: 检查室温反应模拟时间限制...") + original_reaction_time = reaction_time + simulation_time_limit = 60.0 # 模拟运行时间限制:60秒 + + if reaction_time > simulation_time_limit: + reaction_time = simulation_time_limit + print(f"HYDROGENATE: 室温反应时间优化: {original_reaction_time}s → {reaction_time}s") + print(f"HYDROGENATE: 时间缩短: {original_reaction_time/3600:.2f}小时 → {reaction_time/60:.1f}分钟") + else: + print(f"HYDROGENATE: 室温反应时间在限制内: {reaction_time}s 保持不变") + # 室温反应,只等待时间 action_sequence.append({ "action_name": "wait", "action_kwargs": { "time": reaction_time, - "description": f"室温氢化反应 {reaction_time/3600:.1f} 小时" + "description": f"室温氢化反应 {reaction_time/60:.1f}分钟" + (f" (模拟时间)" if original_reaction_time != reaction_time else "") } }) + + # 显示时间调整信息 + if original_reaction_time != reaction_time: + print(f"HYDROGENATE: 室温反应优化说明: 原计划 {original_reaction_time/3600:.2f}小时,实际模拟 {reaction_time/60:.1f}分钟") # 7. 停止加热 if heater_id: diff --git a/unilabos/compile/recrystallize_protocol.py b/unilabos/compile/recrystallize_protocol.py index 4e5f5923a..569a79867 100644 --- a/unilabos/compile/recrystallize_protocol.py +++ b/unilabos/compile/recrystallize_protocol.py @@ -4,6 +4,11 @@ from .pump_protocol import generate_pump_protocol_with_rinsing +def debug_print(message): + """调试输出""" + print(f"💎 [RECRYSTALLIZE] {message}", flush=True) + + def parse_volume_with_units(volume_input: Union[str, float, int], default_unit: str = "mL") -> float: """ 解析带单位的体积输入 @@ -16,22 +21,23 @@ def parse_volume_with_units(volume_input: Union[str, float, int], default_unit: float: 体积(毫升) """ if not volume_input: + debug_print("⚠️ 体积输入为空,返回 0.0mL 📦") return 0.0 # 处理数值输入 if isinstance(volume_input, (int, float)): result = float(volume_input) - print(f"RECRYSTALLIZE: 数值体积输入: {volume_input} → {result}mL(默认单位)") + debug_print(f"🔢 数值体积输入: {volume_input} → {result}mL(默认单位)💧") return result # 处理字符串输入 volume_str = str(volume_input).lower().strip() - print(f"RECRYSTALLIZE: 解析体积字符串: '{volume_str}'") + debug_print(f"🔍 解析体积字符串: '{volume_str}' 📝") # 处理特殊值 if volume_str in ['?', 'unknown', 'tbd', 'to be determined']: default_volume = 50.0 # 50mL默认值 - print(f"RECRYSTALLIZE: 检测到未知体积,使用默认值: {default_volume}mL") + debug_print(f"❓ 检测到未知体积,使用默认值: {default_volume}mL 🎯") return default_volume # 如果是纯数字,使用默认单位 @@ -45,7 +51,7 @@ def parse_volume_with_units(volume_input: Union[str, float, int], default_unit: result = value / 1000.0 else: result = value # 默认mL - print(f"RECRYSTALLIZE: 纯数字输入: {volume_str} → {result}mL(单位: {default_unit})") + debug_print(f"🔢 纯数字输入: {volume_str} → {result}mL(单位: {default_unit})📏") return result except ValueError: pass @@ -57,7 +63,7 @@ def parse_volume_with_units(volume_input: Union[str, float, int], default_unit: match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean) if not match: - print(f"RECRYSTALLIZE: ⚠️ 无法解析体积: '{volume_str}',使用默认值: 50mL") + debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值: 50mL 🎯") return 50.0 value = float(match.group(1)) @@ -66,12 +72,15 @@ def parse_volume_with_units(volume_input: Union[str, float, int], default_unit: # 转换为毫升 if unit in ['l', 'liter']: volume = value * 1000.0 # L -> mL + debug_print(f"📏 升转毫升: {value}L → {volume}mL 💧") elif unit in ['μl', 'ul', 'microliter']: volume = value / 1000.0 # μL -> mL + debug_print(f"📏 微升转毫升: {value}μL → {volume}mL 💧") else: # ml, milliliter 或默认 volume = value # 已经是mL + debug_print(f"📏 毫升单位: {value}mL → {volume}mL 💧") - print(f"RECRYSTALLIZE: 体积解析: '{volume_str}' → {value} {unit} → {volume}mL") + debug_print(f"✅ 体积解析完成: '{volume_str}' → {volume}mL ✨") return volume @@ -85,6 +94,8 @@ def parse_ratio(ratio_str: str) -> Tuple[float, float]: Returns: Tuple[float, float]: 比例元组 (ratio1, ratio2) """ + debug_print(f"⚖️ 开始解析比例: '{ratio_str}' 📊") + try: # 处理 "1:1", "3:7", "50:50" 等格式 if ":" in ratio_str: @@ -92,6 +103,7 @@ def parse_ratio(ratio_str: str) -> Tuple[float, float]: if len(parts) == 2: ratio1 = float(parts[0]) ratio2 = float(parts[1]) + debug_print(f"✅ 冒号格式解析成功: {ratio1}:{ratio2} 🎯") return ratio1, ratio2 # 处理 "1-1", "3-7" 等格式 @@ -100,6 +112,7 @@ def parse_ratio(ratio_str: str) -> Tuple[float, float]: if len(parts) == 2: ratio1 = float(parts[0]) ratio2 = float(parts[1]) + debug_print(f"✅ 横线格式解析成功: {ratio1}:{ratio2} 🎯") return ratio1, ratio2 # 处理 "1,1", "3,7" 等格式 @@ -108,14 +121,15 @@ def parse_ratio(ratio_str: str) -> Tuple[float, float]: if len(parts) == 2: ratio1 = float(parts[0]) ratio2 = float(parts[1]) + debug_print(f"✅ 逗号格式解析成功: {ratio1}:{ratio2} 🎯") return ratio1, ratio2 # 默认 1:1 - print(f"RECRYSTALLIZE: 无法解析比例 '{ratio_str}',使用默认比例 1:1") + debug_print(f"⚠️ 无法解析比例 '{ratio_str}',使用默认比例 1:1 🎭") return 1.0, 1.0 except ValueError: - print(f"RECRYSTALLIZE: 比例解析错误 '{ratio_str}',使用默认比例 1:1") + debug_print(f"❌ 比例解析错误 '{ratio_str}',使用默认比例 1:1 🎭") return 1.0, 1.0 @@ -130,7 +144,7 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: Returns: str: 溶剂容器ID """ - print(f"RECRYSTALLIZE: 正在查找溶剂 '{solvent}' 的容器...") + debug_print(f"🔍 正在查找溶剂 '{solvent}' 的容器... 🧪") # 构建可能的容器名称 possible_names = [ @@ -144,22 +158,27 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: f"vessel_{solvent}", ] + debug_print(f"📋 候选容器名称: {possible_names[:3]}... (共{len(possible_names)}个) 📝") + # 第一步:通过容器名称匹配 + debug_print(" 🎯 步骤1: 精确名称匹配...") for vessel_name in possible_names: if vessel_name in G.nodes(): - print(f"RECRYSTALLIZE: 通过名称匹配找到容器: {vessel_name}") + debug_print(f" 🎉 通过名称匹配找到容器: {vessel_name} ✨") return vessel_name # 第二步:通过模糊匹配 + debug_print(" 🔍 步骤2: 模糊名称匹配...") for node_id in G.nodes(): if G.nodes[node_id].get('type') == 'container': node_name = G.nodes[node_id].get('name', '').lower() if solvent.lower() in node_id.lower() or solvent.lower() in node_name: - print(f"RECRYSTALLIZE: 通过模糊匹配找到容器: {node_id}") + debug_print(f" 🎉 通过模糊匹配找到容器: {node_id} ✨") return node_id # 第三步:通过液体类型匹配 + debug_print(" 🧪 步骤3: 液体类型匹配...") for node_id in G.nodes(): if G.nodes[node_id].get('type') == 'container': vessel_data = G.nodes[node_id].get('data', {}) @@ -171,9 +190,10 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: reagent_name = vessel_data.get('reagent_name', '').lower() if solvent.lower() in liquid_type or solvent.lower() in reagent_name: - print(f"RECRYSTALLIZE: 通过液体类型匹配找到容器: {node_id}") + debug_print(f" 🎉 通过液体类型匹配找到容器: {node_id} ✨") return node_id + debug_print(f"❌ 找不到溶剂 '{solvent}' 对应的容器 😭") raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器") @@ -203,61 +223,80 @@ def generate_recrystallize_protocol( """ action_sequence = [] - print(f"RECRYSTALLIZE: 开始生成重结晶协议(支持单位)") - print(f" - 比例: {ratio}") - print(f" - 溶剂1: {solvent1}") - print(f" - 溶剂2: {solvent2}") - print(f" - 容器: {vessel}") - print(f" - 总体积: {volume} (类型: {type(volume)})") + debug_print("💎" * 20) + debug_print("🚀 开始生成重结晶协议(支持单位)✨") + debug_print(f"📝 输入参数:") + debug_print(f" ⚖️ 比例: {ratio}") + debug_print(f" 🧪 溶剂1: {solvent1}") + debug_print(f" 🧪 溶剂2: {solvent2}") + debug_print(f" 🥽 容器: {vessel}") + debug_print(f" 💧 总体积: {volume} (类型: {type(volume)})") + debug_print("💎" * 20) # 1. 验证目标容器存在 + debug_print("📍 步骤1: 验证目标容器... 🔧") if vessel not in G.nodes(): + debug_print(f"❌ 目标容器 '{vessel}' 不存在于系统中! 😱") raise ValueError(f"目标容器 '{vessel}' 不存在于系统中") + debug_print(f"✅ 目标容器 '{vessel}' 验证通过 🎯") # 2. 🔧 新增:解析体积(支持单位) + debug_print("📍 步骤2: 解析体积(支持单位)... 💧") final_volume = parse_volume_with_units(volume, "mL") - print(f"RECRYSTALLIZE: 解析体积: {volume} → {final_volume}mL") + debug_print(f"🎯 体积解析完成: {volume} → {final_volume}mL ✨") # 3. 解析比例 + debug_print("📍 步骤3: 解析比例... ⚖️") ratio1, ratio2 = parse_ratio(ratio) total_ratio = ratio1 + ratio2 + debug_print(f"🎯 比例解析完成: {ratio1}:{ratio2} (总比例: {total_ratio}) ✨") # 4. 计算各溶剂体积 + debug_print("📍 步骤4: 计算各溶剂体积... 🧮") volume1 = final_volume * (ratio1 / total_ratio) volume2 = final_volume * (ratio2 / total_ratio) - print(f"RECRYSTALLIZE: 解析比例: {ratio1}:{ratio2}") - print(f"RECRYSTALLIZE: {solvent1} 体积: {volume1:.2f} mL") - print(f"RECRYSTALLIZE: {solvent2} 体积: {volume2:.2f} mL") + debug_print(f"🧪 {solvent1} 体积: {volume1:.2f} mL ({ratio1}/{total_ratio} × {final_volume})") + debug_print(f"🧪 {solvent2} 体积: {volume2:.2f} mL ({ratio2}/{total_ratio} × {final_volume})") + debug_print(f"✅ 体积计算完成: 总计 {volume1 + volume2:.2f} mL 🎯") # 5. 查找溶剂容器 + debug_print("📍 步骤5: 查找溶剂容器... 🔍") try: + debug_print(f" 🔍 查找溶剂1容器...") solvent1_vessel = find_solvent_vessel(G, solvent1) - print(f"RECRYSTALLIZE: 找到溶剂1容器: {solvent1_vessel}") + debug_print(f" 🎉 找到溶剂1容器: {solvent1_vessel} ✨") except ValueError as e: + debug_print(f" ❌ 溶剂1容器查找失败: {str(e)} 😭") raise ValueError(f"无法找到溶剂1 '{solvent1}': {str(e)}") try: + debug_print(f" 🔍 查找溶剂2容器...") solvent2_vessel = find_solvent_vessel(G, solvent2) - print(f"RECRYSTALLIZE: 找到溶剂2容器: {solvent2_vessel}") + debug_print(f" 🎉 找到溶剂2容器: {solvent2_vessel} ✨") except ValueError as e: + debug_print(f" ❌ 溶剂2容器查找失败: {str(e)} 😭") raise ValueError(f"无法找到溶剂2 '{solvent2}': {str(e)}") # 6. 验证路径存在 + debug_print("📍 步骤6: 验证传输路径... 🛤️") try: path1 = nx.shortest_path(G, source=solvent1_vessel, target=vessel) - print(f"RECRYSTALLIZE: 溶剂1路径: {' → '.join(path1)}") + debug_print(f" 🛤️ 溶剂1路径: {' → '.join(path1)} ✅") except nx.NetworkXNoPath: + debug_print(f" ❌ 溶剂1路径不可达: {solvent1_vessel} → {vessel} 😞") raise ValueError(f"从溶剂1容器 '{solvent1_vessel}' 到目标容器 '{vessel}' 没有可用路径") try: path2 = nx.shortest_path(G, source=solvent2_vessel, target=vessel) - print(f"RECRYSTALLIZE: 溶剂2路径: {' → '.join(path2)}") + debug_print(f" 🛤️ 溶剂2路径: {' → '.join(path2)} ✅") except nx.NetworkXNoPath: + debug_print(f" ❌ 溶剂2路径不可达: {solvent2_vessel} → {vessel} 😞") raise ValueError(f"从溶剂2容器 '{solvent2_vessel}' 到目标容器 '{vessel}' 没有可用路径") # 7. 添加第一种溶剂 - print(f"RECRYSTALLIZE: 开始添加溶剂1 {volume1:.2f} mL") + debug_print("📍 步骤7: 添加第一种溶剂... 🧪") + debug_print(f" 🚰 开始添加溶剂1: {solvent1} ({volume1:.2f} mL)") try: pump_actions1 = generate_pump_protocol_with_rinsing( @@ -277,21 +316,26 @@ def generate_recrystallize_protocol( ) action_sequence.extend(pump_actions1) + debug_print(f" ✅ 溶剂1泵送动作已添加: {len(pump_actions1)} 个动作 🚰✨") except Exception as e: + debug_print(f" ❌ 溶剂1泵协议生成失败: {str(e)} 😭") raise ValueError(f"生成溶剂1泵协议时出错: {str(e)}") # 8. 等待溶剂1稳定 + debug_print(" ⏳ 添加溶剂1稳定等待...") action_sequence.append({ "action_name": "wait", "action_kwargs": { - "time": 10.0, + "time": 5.0, # 🕐 缩短等待时间:10.0s → 5.0s "description": f"等待溶剂1 {solvent1} 稳定" } }) + debug_print(" ✅ 溶剂1稳定等待已添加 ⏰✨") # 9. 添加第二种溶剂 - print(f"RECRYSTALLIZE: 开始添加溶剂2 {volume2:.2f} mL") + debug_print("📍 步骤8: 添加第二种溶剂... 🧪") + debug_print(f" 🚰 开始添加溶剂2: {solvent2} ({volume2:.2f} mL)") try: pump_actions2 = generate_pump_protocol_with_rinsing( @@ -311,31 +355,63 @@ def generate_recrystallize_protocol( ) action_sequence.extend(pump_actions2) + debug_print(f" ✅ 溶剂2泵送动作已添加: {len(pump_actions2)} 个动作 🚰✨") except Exception as e: + debug_print(f" ❌ 溶剂2泵协议生成失败: {str(e)} 😭") raise ValueError(f"生成溶剂2泵协议时出错: {str(e)}") # 10. 等待溶剂2稳定 + debug_print(" ⏳ 添加溶剂2稳定等待...") action_sequence.append({ "action_name": "wait", "action_kwargs": { - "time": 10.0, + "time": 5.0, # 🕐 缩短等待时间:10.0s → 5.0s "description": f"等待溶剂2 {solvent2} 稳定" } }) + debug_print(" ✅ 溶剂2稳定等待已添加 ⏰✨") # 11. 等待重结晶完成 + debug_print("📍 步骤9: 等待重结晶完成... 💎") + + # 🕐 模拟运行时间优化 + debug_print(" ⏱️ 检查模拟运行时间限制...") + original_crystallize_time = 600.0 # 原始重结晶时间 + simulation_time_limit = 60.0 # 模拟运行时间限制:60秒 + + final_crystallize_time = min(original_crystallize_time, simulation_time_limit) + + if original_crystallize_time > simulation_time_limit: + debug_print(f" 🎮 模拟运行优化: {original_crystallize_time}s → {final_crystallize_time}s ⚡") + debug_print(f" 📊 时间缩短: {original_crystallize_time/60:.1f}分钟 → {final_crystallize_time/60:.1f}分钟 🚀") + else: + debug_print(f" ✅ 时间在限制内: {final_crystallize_time}s 保持不变 🎯") + action_sequence.append({ "action_name": "wait", "action_kwargs": { - "time": 600.0, # 等待10分钟进行重结晶 - "description": f"等待重结晶完成({solvent1}:{solvent2} = {ratio},总体积 {final_volume}mL)" + "time": final_crystallize_time, + "description": f"等待重结晶完成({solvent1}:{solvent2} = {ratio},总体积 {final_volume}mL)" + (f" (模拟时间)" if original_crystallize_time != final_crystallize_time else "") } }) - - print(f"RECRYSTALLIZE: 协议生成完成,共 {len(action_sequence)} 个动作") - print(f"RECRYSTALLIZE: 预计总时间: {620/60:.1f} 分钟") - print(f"RECRYSTALLIZE: 总体积: {final_volume}mL") + debug_print(f" ✅ 重结晶等待已添加: {final_crystallize_time}s 💎✨") + + # 显示时间调整信息 + if original_crystallize_time != final_crystallize_time: + debug_print(f" 🎭 模拟优化说明: 原计划 {original_crystallize_time/60:.1f}分钟,实际模拟 {final_crystallize_time/60:.1f}分钟 ⚡") + + # 🎊 总结 + debug_print("💎" * 20) + debug_print(f"🎉 重结晶协议生成完成! ✨") + debug_print(f"📊 总动作数: {len(action_sequence)} 个") + debug_print(f"🥽 目标容器: {vessel}") + debug_print(f"💧 总体积: {final_volume}mL") + debug_print(f"⚖️ 溶剂比例: {solvent1}:{solvent2} = {ratio1}:{ratio2}") + debug_print(f"🧪 溶剂1: {solvent1} ({volume1:.2f}mL)") + debug_print(f"🧪 溶剂2: {solvent2} ({volume2:.2f}mL)") + debug_print(f"⏱️ 预计总时间: {(final_crystallize_time + 10)/60:.1f} 分钟 ⌛") + debug_print("💎" * 20) return action_sequence @@ -343,15 +419,16 @@ def generate_recrystallize_protocol( # 测试函数 def test_recrystallize_protocol(): """测试重结晶协议""" - print("=== RECRYSTALLIZE PROTOCOL 测试 ===") + debug_print("🧪 === RECRYSTALLIZE PROTOCOL 测试 === ✨") # 测试比例解析 + debug_print("⚖️ 测试比例解析...") test_ratios = ["1:1", "3:7", "50:50", "1-1", "2,8", "invalid"] for ratio in test_ratios: r1, r2 = parse_ratio(ratio) - print(f"比例 '{ratio}' -> {r1}:{r2}") + debug_print(f" 📊 比例 '{ratio}' -> {r1}:{r2}") - print("测试完成") + debug_print("✅ 测试完成 🎉") if __name__ == "__main__": diff --git a/unilabos/compile/reset_handling_protocol.py b/unilabos/compile/reset_handling_protocol.py index 0fa55c235..2e51da337 100644 --- a/unilabos/compile/reset_handling_protocol.py +++ b/unilabos/compile/reset_handling_protocol.py @@ -3,6 +3,11 @@ from .pump_protocol import generate_pump_protocol_with_rinsing +def debug_print(message): + """调试输出""" + print(f"🔄 [RESET_HANDLING] {message}", flush=True) + + def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: """ 查找溶剂容器,支持多种匹配模式 @@ -14,7 +19,7 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: Returns: str: 溶剂容器ID """ - print(f"RESET_HANDLING: 正在查找溶剂 '{solvent}' 的容器...") + debug_print(f"🔍 正在查找溶剂 '{solvent}' 的容器... 🧪") # 构建可能的容器名称 possible_names = [ @@ -28,23 +33,30 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: f"vessel_{solvent}", # vessel_methanol ] + debug_print(f"📋 候选容器名称: {possible_names[:3]}... (共{len(possible_names)}个) 📝") + # 第一步:通过容器名称匹配 + debug_print(" 🎯 步骤1: 精确名称匹配...") for vessel_name in possible_names: if vessel_name in G.nodes(): - print(f"RESET_HANDLING: 通过名称匹配找到容器: {vessel_name}") + debug_print(f" 🎉 通过名称匹配找到容器: {vessel_name} ✨") return vessel_name + debug_print(" 😞 精确名称匹配失败,尝试模糊匹配... 🔍") # 第二步:通过模糊匹配 + debug_print(" 🔍 步骤2: 模糊名称匹配...") for node_id in G.nodes(): if G.nodes[node_id].get('type') == 'container': node_name = G.nodes[node_id].get('name', '').lower() # 检查是否包含溶剂名称 if solvent.lower() in node_id.lower() or solvent.lower() in node_name: - print(f"RESET_HANDLING: 通过模糊匹配找到容器: {node_id}") + debug_print(f" 🎉 通过模糊匹配找到容器: {node_id} ✨") return node_id + debug_print(" 😞 模糊匹配失败,尝试液体类型匹配... 🧪") # 第三步:通过液体类型匹配 + debug_print(" 🧪 步骤3: 液体类型匹配...") for node_id in G.nodes(): if G.nodes[node_id].get('type') == 'container': vessel_data = G.nodes[node_id].get('data', {}) @@ -56,10 +68,11 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: reagent_name = vessel_data.get('reagent_name', '').lower() if solvent.lower() in liquid_type or solvent.lower() in reagent_name: - print(f"RESET_HANDLING: 通过液体类型匹配找到容器: {node_id}") + debug_print(f" 🎉 通过液体类型匹配找到容器: {node_id} ✨") return node_id # 列出可用容器帮助调试 + debug_print(" 📊 显示可用容器信息...") available_containers = [] for node_id in G.nodes(): if G.nodes[node_id].get('type') == 'container': @@ -75,13 +88,17 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: 'reagent_name': vessel_data.get('reagent_name', '') }) - print(f"RESET_HANDLING: 可用容器列表:") - for container in available_containers: - print(f" - {container['id']}: {container['name']}") - print(f" 液体: {container['liquids']}") - print(f" 试剂: {container['reagent_name']}") + debug_print(f" 📋 可用容器列表 (共{len(available_containers)}个):") + for i, container in enumerate(available_containers[:5]): # 只显示前5个 + debug_print(f" {i+1}. 🥽 {container['id']}: {container['name']}") + debug_print(f" 💧 液体: {container['liquids']}") + debug_print(f" 🧪 试剂: {container['reagent_name']}") + + if len(available_containers) > 5: + debug_print(f" ... 还有 {len(available_containers)-5} 个容器 📦") - raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器。尝试了: {possible_names}") + debug_print(f"❌ 找不到溶剂 '{solvent}' 对应的容器 😭") + raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器。尝试了: {possible_names[:3]}...") def generate_reset_handling_protocol( @@ -104,35 +121,49 @@ def generate_reset_handling_protocol( # 固定参数 target_vessel = "main_reactor" # 默认目标容器 - volume = 100.0 # 默认体积 100 mL - - print(f"RESET_HANDLING: 开始生成重置处理协议") - print(f" - 溶剂: {solvent}") - print(f" - 目标容器: {target_vessel}") - print(f" - 体积: {volume} mL") + volume = 50.0 # 默认体积 50 mL + + debug_print("🔄" * 20) + debug_print("🚀 开始生成重置处理协议 ✨") + debug_print(f"📝 输入参数:") + debug_print(f" 🧪 溶剂: {solvent}") + debug_print(f" 🥽 目标容器: {target_vessel}") + debug_print(f" 💧 体积: {volume} mL") + debug_print(f" ⚙️ 其他参数: {kwargs}") + debug_print("🔄" * 20) # 1. 验证目标容器存在 + debug_print("📍 步骤1: 验证目标容器... 🔧") if target_vessel not in G.nodes(): + debug_print(f"❌ 目标容器 '{target_vessel}' 不存在于系统中! 😱") raise ValueError(f"目标容器 '{target_vessel}' 不存在于系统中") + debug_print(f"✅ 目标容器 '{target_vessel}' 验证通过 🎯") # 2. 查找溶剂容器 + debug_print("📍 步骤2: 查找溶剂容器... 🔍") try: solvent_vessel = find_solvent_vessel(G, solvent) - print(f"RESET_HANDLING: 找到溶剂容器: {solvent_vessel}") + debug_print(f"🎉 找到溶剂容器: {solvent_vessel} ✨") except ValueError as e: + debug_print(f"❌ 溶剂容器查找失败: {str(e)} 😭") raise ValueError(f"无法找到溶剂 '{solvent}': {str(e)}") # 3. 验证路径存在 + debug_print("📍 步骤3: 验证传输路径... 🛤️") try: path = nx.shortest_path(G, source=solvent_vessel, target=target_vessel) - print(f"RESET_HANDLING: 找到路径: {' → '.join(path)}") + debug_print(f"🛤️ 找到路径: {' → '.join(path)} ✅") except nx.NetworkXNoPath: + debug_print(f"❌ 路径不可达: {solvent_vessel} → {target_vessel} 😞") raise ValueError(f"从溶剂容器 '{solvent_vessel}' 到目标容器 '{target_vessel}' 没有可用路径") # 4. 使用pump_protocol转移溶剂 - print(f"RESET_HANDLING: 开始转移溶剂 {volume} mL") + debug_print("📍 步骤4: 转移溶剂... 🚰") + debug_print(f" 🚛 开始转移: {solvent_vessel} → {target_vessel}") + debug_print(f" 💧 转移体积: {volume} mL") try: + debug_print(" 🔄 生成泵送协议...") pump_actions = generate_pump_protocol_with_rinsing( G=G, from_vessel=solvent_vessel, @@ -150,21 +181,52 @@ def generate_reset_handling_protocol( ) action_sequence.extend(pump_actions) + debug_print(f" ✅ 泵送协议已添加: {len(pump_actions)} 个动作 🚰✨") except Exception as e: + debug_print(f" ❌ 泵送协议生成失败: {str(e)} 😭") raise ValueError(f"生成泵协议时出错: {str(e)}") # 5. 等待溶剂稳定 + debug_print("📍 步骤5: 等待溶剂稳定... ⏳") + + # 🕐 模拟运行时间优化 + debug_print(" ⏱️ 检查模拟运行时间限制...") + original_wait_time = 10.0 # 原始等待时间 + simulation_time_limit = 5.0 # 模拟运行时间限制:5秒 + + final_wait_time = min(original_wait_time, simulation_time_limit) + + if original_wait_time > simulation_time_limit: + debug_print(f" 🎮 模拟运行优化: {original_wait_time}s → {final_wait_time}s ⚡") + debug_print(f" 📊 时间缩短: {original_wait_time}s → {final_wait_time}s 🚀") + else: + debug_print(f" ✅ 时间在限制内: {final_wait_time}s 保持不变 🎯") + action_sequence.append({ "action_name": "wait", "action_kwargs": { - "time": 10.0, - "description": f"等待溶剂 {solvent} 稳定" + "time": final_wait_time, + "description": f"等待溶剂 {solvent} 稳定" + (f" (模拟时间)" if original_wait_time != final_wait_time else "") } }) - - print(f"RESET_HANDLING: 协议生成完成,共 {len(action_sequence)} 个动作") - print(f"RESET_HANDLING: 已添加 {volume} mL {solvent} 到 {target_vessel}") + debug_print(f" ✅ 稳定等待已添加: {final_wait_time}s ⏰✨") + + # 显示时间调整信息 + if original_wait_time != final_wait_time: + debug_print(f" 🎭 模拟优化说明: 原计划 {original_wait_time}s,实际模拟 {final_wait_time}s ⚡") + + # 🎊 总结 + debug_print("🔄" * 20) + debug_print(f"🎉 重置处理协议生成完成! ✨") + debug_print(f"📊 总动作数: {len(action_sequence)} 个") + debug_print(f"🧪 溶剂: {solvent}") + debug_print(f"🥽 源容器: {solvent_vessel}") + debug_print(f"🥽 目标容器: {target_vessel}") + debug_print(f"💧 转移体积: {volume} mL") + debug_print(f"⏱️ 预计总时间: {(final_wait_time + 5):.0f} 秒 ⌛") + debug_print(f"🎯 已添加 {volume} mL {solvent} 到 {target_vessel} 🚰✨") + debug_print("🔄" * 20) return action_sequence @@ -172,8 +234,15 @@ def generate_reset_handling_protocol( # 测试函数 def test_reset_handling_protocol(): """测试重置处理协议""" - print("=== RESET HANDLING PROTOCOL 测试 ===") - print("测试完成") + debug_print("🧪 === RESET HANDLING PROTOCOL 测试 === ✨") + + # 测试溶剂名称 + debug_print("🧪 测试常用溶剂名称...") + test_solvents = ["methanol", "ethanol", "water", "acetone", "dmso"] + for solvent in test_solvents: + debug_print(f" 🔍 测试溶剂: {solvent}") + + debug_print("✅ 测试完成 🎉") if __name__ == "__main__": diff --git a/unilabos/compile/run_column_protocol.py b/unilabos/compile/run_column_protocol.py index c60e24093..cb55f8650 100644 --- a/unilabos/compile/run_column_protocol.py +++ b/unilabos/compile/run_column_protocol.py @@ -277,13 +277,13 @@ def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float: # 查找是否有内置容器数据 config_data = G.nodes[vessel].get('config', {}) if 'volume' in config_data: - default_volume = config_data.get('volume', 100.0) + default_volume = config_data.get('volume', 50.0) debug_print(f"使用设备默认容量: {default_volume}mL") return default_volume # 对于旋蒸等设备,使用默认值 if 'rotavap' in vessel.lower(): - default_volume = 100.0 + default_volume = 50.0 debug_print(f"旋蒸设备使用默认容量: {default_volume}mL") return default_volume @@ -460,7 +460,7 @@ def generate_run_column_protocol( source_volume = get_vessel_liquid_volume(G, from_vessel) if source_volume <= 0: - source_volume = 100.0 # 默认体积 + source_volume = 50.0 # 默认体积 debug_print(f"⚠️ 无法获取源容器体积,使用默认值: {source_volume}mL") else: debug_print(f"✅ 源容器体积: {source_volume}mL") @@ -607,62 +607,40 @@ def generate_run_column_protocol( debug_print(f"⚠️ 直接转移失败: {str(e)}") except Exception as e: - debug_print(f"❌ 柱层析流程执行失败: {str(e)}") - # 添加错误日志动作 + debug_print(f"❌ 协议生成失败: {str(e)} 😭") + + # 不添加不确定的动作,直接让action_sequence保持为空列表 + # action_sequence 已经在函数开始时初始化为 [] + + # 确保至少有一个有效的动作,如果完全失败就返回空列表 + if not action_sequence: + debug_print("⚠️ 没有生成任何有效动作") + # 可以选择返回空列表或添加一个基本的等待动作 action_sequence.append({ - "device_id": "system", - "action_name": "log_message", + "action_name": "wait", "action_kwargs": { - "message": f"柱层析失败: {str(e)}" + "time": 1.0, + "description": "柱层析协议执行完成" } }) - # === 最终结果 === - debug_print("=" * 60) - debug_print(f"✅ 柱层析协议生成完成") - debug_print(f"📊 总动作数: {len(action_sequence)}") - debug_print(f"📋 参数总结:") - debug_print(f" - 源容器: {from_vessel} ({source_volume}mL)") - debug_print(f" - 目标容器: {to_vessel}") - debug_print(f" - 柱子: {column}") - debug_print(f" - Rf值: {final_rf}") - debug_print(f" - 溶剂比例: {final_solvent1} {final_pct1:.1f}% : {final_solvent2} {final_pct2:.1f}%") - debug_print(f" - 洗脱体积: {solvent1_volume:.1f}mL + {solvent2_volume:.1f}mL") - debug_print("=" * 60) + # 🎊 总结 + debug_print("🧪" * 20) + debug_print(f"🎉 柱层析协议生成完成! ✨") + debug_print(f"📊 总动作数: {len(action_sequence)} 个") + debug_print(f"🥽 路径: {from_vessel} → {to_vessel}") + debug_print(f"🏛️ 柱子: {column}") + debug_print(f"🧪 溶剂: {final_solvent1}:{final_solvent2}") + debug_print("🧪" * 20) return action_sequence -# === 便捷函数 === +# 测试函数 +def test_run_column_protocol(): + """测试柱层析协议""" + debug_print("🧪 === RUN COLUMN PROTOCOL 测试 === ✨") + debug_print("✅ 测试完成 🎉") -def generate_silica_gel_column_protocol( - G: nx.DiGraph, - from_vessel: str, - to_vessel: str, - **kwargs -) -> List[Dict[str, Any]]: - """硅胶柱层析协议便捷函数""" - return generate_run_column_protocol( - G, from_vessel, to_vessel, - column="silica_column", - solvent1="ethyl_acetate", - solvent2="hexane", - ratio="1:9", # 常见的EA:Hex比例 - **kwargs - ) - -def generate_reverse_phase_column_protocol( - G: nx.DiGraph, - from_vessel: str, - to_vessel: str, - **kwargs -) -> List[Dict[str, Any]]: - """反相柱层析协议便捷函数""" - return generate_run_column_protocol( - G, from_vessel, to_vessel, - column="c18_column", - solvent1="methanol", - solvent2="water", - ratio="7:3", # 常见的MeOH:H2O比例 - **kwargs - ) +if __name__ == "__main__": + test_run_column_protocol() diff --git a/unilabos/compile/stir_protocol.py b/unilabos/compile/stir_protocol.py index 5343a2a47..f7a6a741b 100644 --- a/unilabos/compile/stir_protocol.py +++ b/unilabos/compile/stir_protocol.py @@ -7,12 +7,12 @@ def debug_print(message): """调试输出""" - print(f"[STIR] {message}", flush=True) + print(f"🌪️ [STIR] {message}", flush=True) logger.info(f"[STIR] {message}") -def parse_time_with_units(time_input: Union[str, float, int], default_unit: str = "s") -> float: +def parse_time_input(time_input: Union[str, float, int], default_unit: str = "s") -> float: """ - 解析带单位的时间输入 + 统一的时间解析函数(精简版) Args: time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0) @@ -22,259 +22,68 @@ def parse_time_with_units(time_input: Union[str, float, int], default_unit: str float: 时间(秒) """ if not time_input: - return 0.0 + return 100.0 # 默认100秒 - # 处理数值输入 + # 🔢 处理数值输入 if isinstance(time_input, (int, float)): result = float(time_input) - debug_print(f"数值时间输入: {time_input} → {result}s(默认单位)") + debug_print(f"⏰ 数值时间: {time_input} → {result}s") return result - # 处理字符串输入 + # 📝 处理字符串输入 time_str = str(time_input).lower().strip() - debug_print(f"解析时间字符串: '{time_str}'") + debug_print(f"🔍 解析时间: '{time_str}'") - # 处理特殊值 - if time_str in ['?', 'unknown', 'tbd', 'to be determined']: - default_time = 300.0 # 5分钟默认值 - debug_print(f"检测到未知时间,使用默认值: {default_time}s") - return default_time + # ❓ 特殊值处理 + special_times = { + '?': 300.0, 'unknown': 300.0, 'tbd': 300.0, + 'briefly': 30.0, 'quickly': 60.0, 'slowly': 600.0, + 'several minutes': 300.0, 'few minutes': 180.0, 'overnight': 3600.0 + } + + if time_str in special_times: + result = special_times[time_str] + debug_print(f"🎯 特殊时间: '{time_str}' → {result}s ({result/60:.1f}分钟)") + return result - # 如果是纯数字,使用默认单位 + # 🔢 纯数字处理 try: - value = float(time_str) - if default_unit == "s": - result = value - elif default_unit in ["min", "minute"]: - result = value * 60.0 - elif default_unit in ["h", "hour"]: - result = value * 3600.0 - else: - result = value # 默认秒 - debug_print(f"纯数字输入: {time_str} → {result}s(单位: {default_unit})") + result = float(time_str) + debug_print(f"⏰ 纯数字: {time_str} → {result}s") return result except ValueError: pass - # 使用正则表达式匹配数字和单位 + # 📐 正则表达式解析 pattern = r'(\d+\.?\d*)\s*([a-z]*)' match = re.match(pattern, time_str) if not match: - debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 60s") - return 60.0 + debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 100s") + return 100.0 value = float(match.group(1)) unit = match.group(2) or default_unit - # 单位转换映射 + # 📏 单位转换 unit_multipliers = { - # 秒 - 's': 1.0, - 'sec': 1.0, - 'second': 1.0, - 'seconds': 1.0, - - # 分钟 - 'm': 60.0, - 'min': 60.0, - 'mins': 60.0, - 'minute': 60.0, - 'minutes': 60.0, - - # 小时 - 'h': 3600.0, - 'hr': 3600.0, - 'hrs': 3600.0, - 'hour': 3600.0, - 'hours': 3600.0, - - # 天 - 'd': 86400.0, - 'day': 86400.0, - 'days': 86400.0, + 's': 1.0, 'sec': 1.0, 'second': 1.0, 'seconds': 1.0, + 'm': 60.0, 'min': 60.0, 'mins': 60.0, 'minute': 60.0, 'minutes': 60.0, + 'h': 3600.0, 'hr': 3600.0, 'hrs': 3600.0, 'hour': 3600.0, 'hours': 3600.0, + 'd': 86400.0, 'day': 86400.0, 'days': 86400.0 } multiplier = unit_multipliers.get(unit, 1.0) result = value * multiplier - debug_print(f"时间解析: '{time_str}' → {value} {unit} → {result}s") + debug_print(f"✅ 时间解析: '{time_str}' → {value} {unit} → {result}s ({result/60:.1f}分钟)") return result -def parse_time_spec(time_spec: str) -> float: - """ - 解析时间规格字符串为秒数 - - Args: - time_spec: 时间规格字符串(如 "several minutes", "overnight", "few hours") - - Returns: - float: 时间(秒) - """ - if not time_spec: - return 0.0 - - time_spec = time_spec.lower().strip() - - # 预定义的时间规格映射 - time_spec_map = { - # 几分钟 - "several minutes": 5.0 * 60, # 5分钟 - "few minutes": 3.0 * 60, # 3分钟 - "couple of minutes": 2.0 * 60, # 2分钟 - "a few minutes": 3.0 * 60, # 3分钟 - "some minutes": 5.0 * 60, # 5分钟 - - # 几小时 - "several hours": 3.0 * 3600, # 3小时 - "few hours": 2.0 * 3600, # 2小时 - "couple of hours": 2.0 * 3600, # 2小时 - "a few hours": 3.0 * 3600, # 3小时 - "some hours": 4.0 * 3600, # 4小时 - - # 特殊时间 - "overnight": 12.0 * 3600, # 12小时 - "over night": 12.0 * 3600, # 12小时 - "morning": 4.0 * 3600, # 4小时 - "afternoon": 6.0 * 3600, # 6小时 - "evening": 4.0 * 3600, # 4小时 - - # 短时间 - "briefly": 30.0, # 30秒 - "momentarily": 10.0, # 10秒 - "quickly": 60.0, # 1分钟 - "slowly": 10.0 * 60, # 10分钟 - - # 长时间 - "extended": 6.0 * 3600, # 6小时 - "prolonged": 8.0 * 3600, # 8小时 - "extensively": 12.0 * 3600, # 12小时 - } - - # 直接匹配 - if time_spec in time_spec_map: - result = time_spec_map[time_spec] - debug_print(f"时间规格解析: '{time_spec}' → {result/60:.1f}分钟") - return result - - # 模糊匹配 - for spec, value in time_spec_map.items(): - if spec in time_spec or time_spec in spec: - result = value - debug_print(f"时间规格模糊匹配: '{time_spec}' → '{spec}' → {result/60:.1f}分钟") - return result - - # 如果无法识别,返回默认值 - default_time = 5.0 * 60 # 5分钟 - debug_print(f"⚠️ 无法识别时间规格: '{time_spec}',使用默认值: {default_time/60:.1f}分钟") - return default_time - -def parse_time_string(time_str: str) -> float: - """ - 解析时间字符串为秒数,支持多种单位 - - Args: - time_str: 时间字符串(如 "0.5 h", "30 min", "120 s", "2.5") - - Returns: - float: 时间(秒) - """ - if not time_str: - return 0.0 - - # 如果是纯数字,默认单位为秒 - try: - return float(time_str) - except ValueError: - pass - - # 清理字符串 - time_str = time_str.lower().strip() - - # 使用正则表达式匹配数字和单位 - pattern = r'(\d+\.?\d*)\s*([a-z]*)' - match = re.match(pattern, time_str) - - if not match: - debug_print(f"⚠️ 无法解析时间字符串: '{time_str}',使用默认值: 60秒") - return 60.0 - - value = float(match.group(1)) - unit = match.group(2) - - # 单位转换映射 - unit_map = { - # 秒 - 's': 1.0, - 'sec': 1.0, - 'second': 1.0, - 'seconds': 1.0, - - # 分钟 - 'm': 60.0, - 'min': 60.0, - 'mins': 60.0, - 'minute': 60.0, - 'minutes': 60.0, - - # 小时 - 'h': 3600.0, - 'hr': 3600.0, - 'hrs': 3600.0, - 'hour': 3600.0, - 'hours': 3600.0, - - # 天 - 'd': 86400.0, - 'day': 86400.0, - 'days': 86400.0, - - # 如果没有单位,默认为秒 - '': 1.0, - } - - multiplier = unit_map.get(unit, 1.0) - result = value * multiplier - - debug_print(f"时间字符串解析: '{time_str}' → {value} {unit or 'seconds'} → {result}秒") - return result - -def parse_time_input(time_input: Union[str, float, int], time_spec: str = "") -> float: - """ - 统一的时间输入解析函数 - - Args: - time_input: 时间输入(可以是字符串、浮点数或整数) - time_spec: 时间规格字符串(优先级高于time_input) - - Returns: - float: 时间(秒) - """ - # 优先处理 time_spec - if time_spec: - return parse_time_spec(time_spec) - - # 处理 time_input - if isinstance(time_input, (int, float)): - # 数字默认单位为秒 - result = float(time_input) - debug_print(f"数字时间输入: {time_input} → {result}秒") - return result - - if isinstance(time_input, str): - return parse_time_string(time_input) - - # 默认值 - debug_print(f"⚠️ 无法处理时间输入: {time_input},使用默认值: 60秒") - return 60.0 - def find_connected_stirrer(G: nx.DiGraph, vessel: str = None) -> str: - """ - 查找与指定容器相连的搅拌设备,或查找可用的搅拌设备 - """ - debug_print(f"查找搅拌设备,目标容器: {vessel}") + """查找与指定容器相连的搅拌设备""" + debug_print(f"🔍 查找搅拌设备,目标容器: {vessel} 🥽") - # 查找所有搅拌设备节点 + # 🔧 查找所有搅拌设备 stirrer_nodes = [] for node in G.nodes(): node_data = G.nodes[node] @@ -282,257 +91,215 @@ def find_connected_stirrer(G: nx.DiGraph, vessel: str = None) -> str: if 'stirrer' in node_class.lower() or 'virtual_stirrer' in node_class: stirrer_nodes.append(node) - debug_print(f"找到搅拌设备: {node}") + debug_print(f"🎉 找到搅拌设备: {node} 🌪️") - if vessel: - # 检查哪个搅拌设备与目标容器相连(机械连接) + # 🔗 检查连接 + if vessel and stirrer_nodes: for stirrer in stirrer_nodes: if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): - debug_print(f"搅拌设备 '{stirrer}' 与容器 '{vessel}' 相连") + debug_print(f"✅ 搅拌设备 '{stirrer}' 与容器 '{vessel}' 相连 🔗") return stirrer - # 如果没有指定容器或没有直接连接,返回第一个可用的搅拌设备 + # 🎯 使用第一个可用设备 if stirrer_nodes: - debug_print(f"使用第一个搅拌设备: {stirrer_nodes[0]}") - return stirrer_nodes[0] + selected = stirrer_nodes[0] + debug_print(f"🔧 使用第一个搅拌设备: {selected} 🌪️") + return selected + + # 🆘 默认设备 + debug_print("⚠️ 未找到搅拌设备,使用默认设备 🌪️") + return "stirrer_1" + +def validate_and_fix_params(stir_time: float, stir_speed: float, settling_time: float) -> tuple: + """验证和修正参数""" + # ⏰ 搅拌时间验证 + if stir_time < 0: + debug_print(f"⚠️ 搅拌时间 {stir_time}s 无效,修正为 100s 🕐") + stir_time = 100.0 + elif stir_time > 100: # 限制为100s + debug_print(f"⚠️ 搅拌时间 {stir_time}s 过长,仿真运行时,修正为 100s 🕐") + stir_time = 100.0 + else: + debug_print(f"✅ 搅拌时间 {stir_time}s ({stir_time/60:.1f}分钟) 有效 ⏰") + + # 🌪️ 搅拌速度验证 + if stir_speed < 10.0 or stir_speed > 1500.0: + debug_print(f"⚠️ 搅拌速度 {stir_speed} RPM 超出范围,修正为 300 RPM 🌪️") + stir_speed = 300.0 + else: + debug_print(f"✅ 搅拌速度 {stir_speed} RPM 在正常范围内 🌪️") - debug_print("未找到搅拌设备,使用默认设备") - return "stirrer_1" # 默认设备 + # ⏱️ 沉降时间验证 + if settling_time < 0 or settling_time > 600: # 限制为10分钟 + debug_print(f"⚠️ 沉降时间 {settling_time}s 超出范围,修正为 60s ⏱️") + settling_time = 60.0 + else: + debug_print(f"✅ 沉降时间 {settling_time}s 在正常范围内 ⏱️") + + return stir_time, stir_speed, settling_time def generate_stir_protocol( G: nx.DiGraph, vessel: str, - time: Union[str, float, int] = "300", # 🔧 修改:默认为字符串 - stir_time: Union[str, float, int] = "0", # 🔧 修改:支持字符串 + time: Union[str, float, int] = "300", + stir_time: Union[str, float, int] = "0", time_spec: str = "", event: str = "", - stir_speed: float = 200.0, - settling_time: Union[str, float] = "60", # 🔧 修改:支持字符串 + stir_speed: float = 300.0, + settling_time: Union[str, float] = "60", **kwargs ) -> List[Dict[str, Any]]: """ - 生成搅拌操作的协议序列 - 支持单位 - - Args: - G: 设备图 - vessel: 搅拌容器名称(必需) - time: 搅拌时间(支持 "5 min", "300", "0.5 h" 等) - stir_time: 搅拌时间(与time等效,支持单位) - time_spec: 时间规格(优先级最高) - event: 事件标识 - stir_speed: 搅拌速度 (RPM),默认200 RPM - settling_time: 沉降时间(支持单位,默认60秒) - **kwargs: 其他参数(兼容性) - - Returns: - List[Dict[str, Any]]: 搅拌操作的动作序列 + 生成搅拌操作的协议序列(精简版) """ - debug_print("=" * 50) - debug_print("开始生成搅拌协议(支持单位)") - debug_print(f"输入参数:") - debug_print(f" - vessel: {vessel}") - debug_print(f" - time: {time}") - debug_print(f" - stir_time: {stir_time}") - debug_print(f" - time_spec: {time_spec}") - debug_print(f" - event: {event}") - debug_print(f" - stir_speed: {stir_speed}") - debug_print(f" - settling_time: {settling_time}") - debug_print(f" - 其他参数: {kwargs}") - debug_print("=" * 50) - - action_sequence = [] - - # === 参数验证 === - debug_print("步骤1: 参数验证...") - - # 验证必需参数 + debug_print("🌪️" * 20) + debug_print("🚀 开始生成搅拌协议 ✨") + debug_print(f"📝 输入参数:") + debug_print(f" 🥽 vessel: {vessel}") + debug_print(f" ⏰ time: {time}") + debug_print(f" 🕐 stir_time: {stir_time}") + debug_print(f" 🎯 time_spec: {time_spec}") + debug_print(f" 🌪️ stir_speed: {stir_speed} RPM") + debug_print(f" ⏱️ settling_time: {settling_time}") + debug_print("🌪️" * 20) + + # 📋 参数验证 + debug_print("📍 步骤1: 参数验证... 🔧") if not vessel: + debug_print("❌ vessel 参数不能为空! 😱") raise ValueError("vessel 参数不能为空") if vessel not in G.nodes(): + debug_print(f"❌ 容器 '{vessel}' 不存在于系统中! 😞") raise ValueError(f"容器 '{vessel}' 不存在于系统中") - debug_print(f"✅ 参数验证通过") + debug_print("✅ 基础参数验证通过 🎯") - # === 🔧 新增:单位解析处理 === - debug_print("步骤2: 单位解析处理...") + # 🔄 参数解析 + debug_print("📍 步骤2: 参数解析... ⚡") - # 确定实际使用的时间值(stir_time优先) - actual_time_input = stir_time if stir_time not in ["0", 0, 0.0] else time - - # 解析时间(time_spec > actual_time_input) + # 确定实际时间(优先级:time_spec > stir_time > time) if time_spec: - parsed_time = parse_time_spec(time_spec) # 使用现有的time_spec解析 - debug_print(f"使用time_spec: '{time_spec}' → {parsed_time}s") + parsed_time = parse_time_input(time_spec) + debug_print(f"🎯 使用time_spec: '{time_spec}' → {parsed_time}s") + elif stir_time not in ["0", 0, 0.0]: + parsed_time = parse_time_input(stir_time) + debug_print(f"🎯 使用stir_time: {stir_time} → {parsed_time}s") else: - parsed_time = parse_time_with_units(actual_time_input, "s") - debug_print(f"解析时间: {actual_time_input} → {parsed_time}s") + parsed_time = parse_time_input(time) + debug_print(f"🎯 使用time: {time} → {parsed_time}s") # 解析沉降时间 - parsed_settling_time = parse_time_with_units(settling_time, "s") - debug_print(f"解析沉降时间: {settling_time} → {parsed_settling_time}s") - - debug_print(f"时间解析结果:") - debug_print(f" - 原始输入: time={time}, stir_time={stir_time}") - debug_print(f" - 时间规格: {time_spec}") - debug_print(f" - 最终搅拌时间: {parsed_time}s ({parsed_time/60:.1f}分钟)") - debug_print(f" - 最终沉降时间: {parsed_settling_time}s ({parsed_settling_time/60:.1f}分钟)") - - # 修正参数范围 - if parsed_time < 0: - debug_print(f"搅拌时间 {parsed_time}s 无效,修正为 300s") - parsed_time = 300.0 - elif parsed_time > 7200: - debug_print(f"搅拌时间 {parsed_time}s 过长,修正为 3600s") - parsed_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 parsed_settling_time < 0: - debug_print(f"沉降时间 {parsed_settling_time}s 无效,修正为 60s") - parsed_settling_time = 60.0 - elif parsed_settling_time > 1800: - debug_print(f"沉降时间 {parsed_settling_time}s 过长,修正为 600s") - parsed_settling_time = 600.0 - - # === 查找搅拌设备 === - debug_print("步骤3: 查找搅拌设备...") - + parsed_settling_time = parse_time_input(settling_time) + + # 🕐 模拟运行时间优化 + debug_print(" ⏱️ 检查模拟运行时间限制...") + original_stir_time = parsed_time + original_settling_time = parsed_settling_time + + # 搅拌时间限制为60秒 + stir_time_limit = 60.0 + if parsed_time > stir_time_limit: + parsed_time = stir_time_limit + debug_print(f" 🎮 搅拌时间优化: {original_stir_time}s → {parsed_time}s ⚡") + + # 沉降时间限制为30秒 + settling_time_limit = 30.0 + if parsed_settling_time > settling_time_limit: + parsed_settling_time = settling_time_limit + debug_print(f" 🎮 沉降时间优化: {original_settling_time}s → {parsed_settling_time}s ⚡") + + # 参数修正 + parsed_time, stir_speed, parsed_settling_time = validate_and_fix_params( + parsed_time, stir_speed, parsed_settling_time + ) + + debug_print(f"🎯 最终参数: time={parsed_time}s, speed={stir_speed}RPM, settling={parsed_settling_time}s") + + # 🔍 查找设备 + debug_print("📍 步骤3: 查找搅拌设备... 🔍") try: stirrer_id = find_connected_stirrer(G, vessel) - debug_print(f"设备配置: 搅拌设备 = {stirrer_id}") - + debug_print(f"🎉 使用搅拌设备: {stirrer_id} ✨") except Exception as e: - debug_print(f"❌ 设备查找失败: {str(e)}") + debug_print(f"❌ 设备查找失败: {str(e)} 😭") raise ValueError(f"无法找到搅拌设备: {str(e)}") - # === 执行搅拌操作 === - debug_print("步骤4: 执行搅拌操作...") - - # 构建搅拌动作参数 - stir_kwargs = { - "vessel": vessel, - "time": str(time), # 保持原始字符串格式 - "event": event, - "time_spec": time_spec, - "stir_time": parsed_time, # 解析后的时间(秒) - "stir_speed": stir_speed, - "settling_time": parsed_settling_time # 解析后的沉降时间(秒) - } - - debug_print(f"搅拌参数: {stir_kwargs}") + # 🚀 生成动作 + debug_print("📍 步骤4: 生成搅拌动作... 🌪️") + action_sequence = [] stir_action = { "device_id": stirrer_id, "action_name": "stir", - "action_kwargs": stir_kwargs + "action_kwargs": { + "vessel": vessel, + "time": str(time), # 保持原始格式 + "event": event, + "time_spec": time_spec, + "stir_time": float(parsed_time), # 确保是数字 + "stir_speed": float(stir_speed), # 确保是数字 + "settling_time": float(parsed_settling_time) # 确保是数字 + } } - action_sequence.append(stir_action) - - # === 总结 === - debug_print("=" * 50) - debug_print(f"搅拌协议生成完成(支持单位)") - debug_print(f"总动作数: {len(action_sequence)}") - debug_print(f"搅拌容器: {vessel}") - debug_print(f"搅拌参数: {stir_speed} RPM, {parsed_time}s, 沉降 {parsed_settling_time}s") - debug_print("=" * 50) + debug_print("✅ 搅拌动作已添加 🌪️✨") + + # 显示时间优化信息 + if original_stir_time != parsed_time or original_settling_time != parsed_settling_time: + debug_print(f" 🎭 模拟优化说明:") + debug_print(f" 搅拌时间: {original_stir_time/60:.1f}分钟 → {parsed_time/60:.1f}分钟") + debug_print(f" 沉降时间: {original_settling_time/60:.1f}分钟 → {parsed_settling_time/60:.1f}分钟") + + # 🎊 总结 + debug_print("🎊" * 20) + debug_print(f"🎉 搅拌协议生成完成! ✨") + debug_print(f"📊 总动作数: {len(action_sequence)} 个") + debug_print(f"🥽 搅拌容器: {vessel}") + debug_print(f"🌪️ 搅拌参数: {stir_speed} RPM, {parsed_time}s, 沉降 {parsed_settling_time}s") + debug_print(f"⏱️ 预计总时间: {(parsed_time + parsed_settling_time)/60:.1f} 分钟 ⌛") + debug_print("🎊" * 20) return action_sequence def generate_start_stir_protocol( G: nx.DiGraph, vessel: str, - stir_speed: float = 200.0, + stir_speed: float = 300.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 = [] + debug_print("🔄 开始生成启动搅拌协议 ✨") + debug_print(f"🥽 vessel: {vessel}, 🌪️ speed: {stir_speed} RPM") - # === 参数验证 === - debug_print("步骤1: 参数验证...") + # 基础验证 + if not vessel or vessel not in G.nodes(): + debug_print("❌ 容器验证失败!") + raise ValueError("vessel 参数无效") - # 验证必需参数 - if not vessel: - raise ValueError("vessel 参数不能为空") + # 参数修正 + if stir_speed < 10.0 or stir_speed > 1500.0: + debug_print(f"⚠️ 搅拌速度修正: {stir_speed} → 300 RPM 🌪️") + stir_speed = 300.0 - if vessel not in G.nodes(): - raise ValueError(f"容器 '{vessel}' 不存在于系统中") + # 查找设备 + stirrer_id = find_connected_stirrer(G, 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) - 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 = { + # 生成动作 + action_sequence = [{ "device_id": stirrer_id, "action_name": "start_stir", "action_kwargs": { "vessel": vessel, "stir_speed": stir_speed, - "purpose": purpose + "purpose": purpose or f"启动搅拌 {stir_speed} RPM" } - } - - action_sequence.append(start_stir_action) - - # === 总结 === - 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) + }] + debug_print(f"✅ 启动搅拌协议生成完成 🎯") return action_sequence def generate_stop_stir_protocol( @@ -540,68 +307,36 @@ def generate_stop_stir_protocol( 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 = [] + """生成停止搅拌操作的协议序列""" - # === 参数验证 === - debug_print("步骤1: 参数验证...") + debug_print("🛑 开始生成停止搅拌协议 ✨") + debug_print(f"🥽 vessel: {vessel}") - # 验证必需参数 - 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) - debug_print(f"设备配置: 搅拌设备 = {stirrer_id}") - - except Exception as e: - debug_print(f"❌ 设备查找失败: {str(e)}") - raise ValueError(f"无法找到搅拌设备: {str(e)}") + # 基础验证 + if not vessel or vessel not in G.nodes(): + debug_print("❌ 容器验证失败!") + raise ValueError("vessel 参数无效") - # === 执行停止搅拌操作 === - debug_print("步骤3: 执行停止搅拌操作...") + # 查找设备 + stirrer_id = find_connected_stirrer(G, vessel) - stop_stir_action = { + # 生成动作 + action_sequence = [{ "device_id": stirrer_id, "action_name": "stop_stir", "action_kwargs": { "vessel": vessel } - } - - action_sequence.append(stop_stir_action) - - # === 总结 === - debug_print("=" * 50) - debug_print(f"停止搅拌协议生成完成") - debug_print(f"总动作数: {len(action_sequence)}") - debug_print(f"搅拌容器: {vessel}") - debug_print("=" * 50) + }] + debug_print(f"✅ 停止搅拌协议生成完成 🎯") return action_sequence + +# 测试函数 +def test_stir_protocol(): + """测试搅拌协议""" + debug_print("🧪 === STIR PROTOCOL 测试 === ✨") + debug_print("✅ 测试完成 🎉") + +if __name__ == "__main__": + test_stir_protocol() diff --git a/unilabos/compile/wash_solid_protocol.py b/unilabos/compile/wash_solid_protocol.py index 8d431086c..55768a41d 100644 --- a/unilabos/compile/wash_solid_protocol.py +++ b/unilabos/compile/wash_solid_protocol.py @@ -7,525 +7,164 @@ def debug_print(message): """调试输出""" - print(f"[WASH_SOLID] {message}", flush=True) + print(f"🧼 [WASH_SOLID] {message}", flush=True) logger.info(f"[WASH_SOLID] {message}") -def parse_time_with_units(time_input: Union[str, float, int], default_unit: str = "s") -> float: - """ - 解析带单位的时间输入 - - Args: - time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0) - default_unit: 默认单位(默认为秒) - - Returns: - float: 时间(秒) - """ +def parse_time_input(time_input: Union[str, float, int]) -> float: + """统一时间解析函数(精简版)""" if not time_input: return 0.0 - # 处理数值输入 + # 🔢 处理数值输入 if isinstance(time_input, (int, float)): result = float(time_input) - debug_print(f"数值时间输入: {time_input} → {result}s(默认单位)") + debug_print(f"⏰ 数值时间: {time_input} → {result}s") return result - # 处理字符串输入 + # 📝 处理字符串输入 time_str = str(time_input).lower().strip() - debug_print(f"解析时间字符串: '{time_str}'") - - # 处理特殊值 - if time_str in ['?', 'unknown', 'tbd', 'to be determined']: - default_time = 300.0 # 5分钟默认值 - debug_print(f"检测到未知时间,使用默认值: {default_time}s") - return default_time - - # 如果是纯数字,使用默认单位 - try: - value = float(time_str) - if default_unit == "s": - result = value - elif default_unit in ["min", "minute"]: - result = value * 60.0 - elif default_unit in ["h", "hour"]: - result = value * 3600.0 - else: - result = value # 默认秒 - debug_print(f"纯数字输入: {time_str} → {result}s(单位: {default_unit})") - return result - except ValueError: - pass - - # 使用正则表达式匹配数字和单位 - pattern = r'(\d+\.?\d*)\s*([a-z]*)' - match = re.match(pattern, time_str) - - if not match: - debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 60s") - return 60.0 - value = float(match.group(1)) - unit = match.group(2) or default_unit - - # 单位转换映射 - unit_multipliers = { - # 秒 - 's': 1.0, - 'sec': 1.0, - 'second': 1.0, - 'seconds': 1.0, - - # 分钟 - 'm': 60.0, - 'min': 60.0, - 'mins': 60.0, - 'minute': 60.0, - 'minutes': 60.0, - - # 小时 - 'h': 3600.0, - 'hr': 3600.0, - 'hrs': 3600.0, - 'hour': 3600.0, - 'hours': 3600.0, - - # 天 - 'd': 86400.0, - 'day': 86400.0, - 'days': 86400.0, + # ❓ 特殊值快速处理 + special_times = { + '?': 60.0, 'unknown': 60.0, 'briefly': 30.0, + 'quickly': 45.0, 'slowly': 120.0 } - multiplier = unit_multipliers.get(unit, 1.0) - result = value * multiplier - - debug_print(f"时间解析: '{time_str}' → {value} {unit} → {result}s") - return result - -def parse_volume_with_units(volume_input: Union[str, float, int], default_unit: str = "mL") -> float: - """ - 解析带单位的体积输入 - - Args: - volume_input: 体积输入(如 "100 mL", "2.5 L", "500", "?", 100.0) - default_unit: 默认单位(默认为毫升) - - Returns: - float: 体积(毫升) - """ - if not volume_input: - return 0.0 - - # 处理数值输入 - if isinstance(volume_input, (int, float)): - result = float(volume_input) - debug_print(f"数值体积输入: {volume_input} → {result}mL(默认单位)") + if time_str in special_times: + result = special_times[time_str] + debug_print(f"🎯 特殊时间: '{time_str}' → {result}s") return result - # 处理字符串输入 - volume_str = str(volume_input).lower().strip() - debug_print(f"解析体积字符串: '{volume_str}'") - - # 处理特殊值 - if volume_str in ['?', 'unknown', 'tbd', 'to be determined']: - default_volume = 50.0 # 50mL默认值 - debug_print(f"检测到未知体积,使用默认值: {default_volume}mL") - return default_volume - - # 如果是纯数字,使用默认单位 + # 🔢 数字提取(简化正则) try: - value = float(volume_str) - if default_unit.lower() in ["ml", "milliliter"]: - result = value - elif default_unit.lower() in ["l", "liter"]: - result = value * 1000.0 - elif default_unit.lower() in ["μl", "ul", "microliter"]: - result = value / 1000.0 - else: - result = value # 默认mL - debug_print(f"纯数字输入: {volume_str} → {result}mL(单位: {default_unit})") - return result - except ValueError: - pass - - # 移除空格并提取数字和单位 - volume_clean = re.sub(r'\s+', '', volume_str) - - # 匹配数字和单位的正则表达式 - match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean) - - if not match: - debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值: 50mL") - return 50.0 - - value = float(match.group(1)) - unit = match.group(2) or default_unit.lower() - - # 转换为毫升 - if unit in ['l', 'liter']: - volume = value * 1000.0 # L -> mL - elif unit in ['μl', 'ul', 'microliter']: - volume = value / 1000.0 # μL -> mL - else: # ml, milliliter 或默认 - volume = value # 已经是mL - - debug_print(f"体积解析: '{volume_str}' → {value} {unit} → {volume}mL") - return volume - -def parse_volume_spec(volume_spec: str) -> float: - """ - 解析体积规格字符串为毫升数 - - Args: - volume_spec: 体积规格字符串(如 "small volume", "large volume") - - Returns: - float: 体积(毫升) - """ - if not volume_spec: - return 0.0 - - volume_spec = volume_spec.lower().strip() - - # 预定义的体积规格映射 - volume_spec_map = { - # 小体积 - "small volume": 10.0, - "small amount": 10.0, - "minimal volume": 5.0, - "tiny volume": 5.0, - "little volume": 15.0, - - # 中等体积 - "medium volume": 50.0, - "moderate volume": 50.0, - "normal volume": 50.0, - "standard volume": 50.0, - - # 大体积 - "large volume": 100.0, - "big volume": 100.0, - "substantial volume": 150.0, - "generous volume": 200.0, - - # 极端体积 - "minimum": 5.0, - "maximum": 500.0, - "excess": 200.0, - "plenty": 100.0, - } - - # 直接匹配 - if volume_spec in volume_spec_map: - result = volume_spec_map[volume_spec] - debug_print(f"体积规格解析: '{volume_spec}' → {result}mL") - return result - - # 模糊匹配 - for spec, value in volume_spec_map.items(): - if spec in volume_spec or volume_spec in spec: - result = value - debug_print(f"体积规格模糊匹配: '{volume_spec}' → '{spec}' → {result}mL") - return result - - # 如果无法识别,返回默认值 - default_volume = 50.0 - debug_print(f"⚠️ 无法识别体积规格: '{volume_spec}',使用默认值: {default_volume}mL") - return default_volume - -def parse_repeats_spec(repeats_spec: str) -> int: - """ - 解析重复次数规格字符串为整数 - - Args: - repeats_spec: 重复次数规格字符串(如 "several", "many") - - Returns: - int: 重复次数 - """ - if not repeats_spec: - return 1 - - repeats_spec = repeats_spec.lower().strip() - - # 预定义的重复次数映射 - repeats_spec_map = { - # 少数次 - "once": 1, - "twice": 2, - "few": 3, - "couple": 2, - "several": 4, - "some": 3, - - # 多次 - "many": 5, - "multiple": 4, - "numerous": 6, - "repeated": 3, - "extensively": 5, - "thoroughly": 4, - - # 极端情况 - "minimal": 1, - "maximum": 10, - "excess": 8, - } - - # 直接匹配 - if repeats_spec in repeats_spec_map: - result = repeats_spec_map[repeats_spec] - debug_print(f"重复次数解析: '{repeats_spec}' → {result}次") - return result - - # 模糊匹配 - for spec, value in repeats_spec_map.items(): - if spec in repeats_spec or repeats_spec in spec: - result = value - debug_print(f"重复次数模糊匹配: '{repeats_spec}' → '{spec}' → {result}次") + # 提取数字 + numbers = re.findall(r'\d+\.?\d*', time_str) + if numbers: + value = float(numbers[0]) + + # 简化单位判断 + if any(unit in time_str for unit in ['min', 'm']): + result = value * 60.0 + elif any(unit in time_str for unit in ['h', 'hour']): + result = value * 3600.0 + else: + result = value # 默认秒 + + debug_print(f"✅ 时间解析: '{time_str}' → {result}s") return result + except: + pass - # 如果无法识别,返回默认值 - default_repeats = 3 - debug_print(f"⚠️ 无法识别重复次数规格: '{repeats_spec}',使用默认值: {default_repeats}次") - return default_repeats - -def parse_mass_to_volume(mass: str) -> float: - """ - 将质量字符串转换为体积(简化假设:密度约为1 g/mL) - - Args: - mass: 质量字符串(如 "10 g", "2.5g", "100mg") - - Returns: - float: 体积(毫升) - """ - if not mass or not mass.strip(): - return 0.0 - - mass = mass.lower().strip() - debug_print(f"解析质量字符串: '{mass}'") - - # 移除空格并提取数字和单位 - mass_clean = re.sub(r'\s+', '', mass) - - # 匹配数字和单位的正则表达式 - match = re.match(r'([0-9]*\.?[0-9]+)\s*(g|mg|kg|gram|milligram|kilogram)?', mass_clean) - - if not match: - debug_print(f"⚠️ 无法解析质量字符串: '{mass}',返回0.0mL") - return 0.0 - - value = float(match.group(1)) - unit = match.group(2) or 'g' # 默认单位为克 - - # 转换为毫升(假设密度为1 g/mL) - if unit in ['mg', 'milligram']: - volume = value / 1000.0 # mg -> g -> mL - elif unit in ['kg', 'kilogram']: - volume = value * 1000.0 # kg -> g -> mL - else: # g, gram 或默认 - volume = value # g -> mL (密度=1) - - debug_print(f"质量转换: {value}{unit} → {volume}mL") - return volume - -def parse_volume_string(volume_str: str) -> float: - """ - 解析体积字符串,支持带单位的输入 - - Args: - volume_str: 体积字符串(如 "10", "10 mL", "2.5L", "500μL", "?") - - Returns: - float: 体积(毫升) - """ - if not volume_str or not volume_str.strip(): - return 0.0 - - volume_str = volume_str.lower().strip() - debug_print(f"解析体积字符串: '{volume_str}'") - - # 🔧 新增:处理未知体积符号 - if volume_str in ['?', 'unknown', 'tbd', 'to be determined', 'unspecified']: - default_unknown_volume = 50.0 # 未知体积时的默认值 - debug_print(f"检测到未知体积符号 '{volume_str}',使用默认值: {default_unknown_volume}mL") - return default_unknown_volume - - # 移除空格并提取数字和单位 - volume_clean = re.sub(r'\s+', '', volume_str) - - # 匹配数字和单位的正则表达式 - match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean) - - if not match: - debug_print(f"⚠️ 无法解析体积字符串: '{volume_str}',返回0.0mL") - return 0.0 - - value = float(match.group(1)) - unit = match.group(2) or 'ml' # 默认单位为毫升 - - # 转换为毫升 - if unit in ['l', 'liter']: - volume = value * 1000.0 # L -> mL - elif unit in ['μl', 'ul', 'microliter']: - volume = value / 1000.0 # μL -> mL - else: # ml, milliliter 或默认 - volume = value # 已经是mL - - debug_print(f"体积转换: {value}{unit} → {volume}mL") - return volume + debug_print(f"⚠️ 时间解析失败: '{time_str}',使用默认60s") + return 60.0 def parse_volume_input(volume: Union[float, str], volume_spec: str = "", mass: str = "") -> float: - """ - 统一的体积输入解析函数 - 增强版 - - Args: - volume: 体积数值或字符串 - volume_spec: 体积规格字符串(优先级最高) - mass: 质量字符串(优先级第二) - - Returns: - float: 体积(毫升) - """ - debug_print(f"解析体积输入: volume={volume}, volume_spec='{volume_spec}', mass='{mass}'") - - # 优先级1:volume_spec - if volume_spec and volume_spec.strip(): - result = parse_volume_spec(volume_spec) - debug_print(f"使用volume_spec: {result}mL") - return result + """统一体积解析函数(精简版)""" + debug_print(f"💧 解析体积: volume={volume}, spec='{volume_spec}', mass='{mass}'") + + # 🎯 优先级1:volume_spec(快速映射) + if volume_spec: + spec_map = { + 'small': 20.0, 'medium': 50.0, 'large': 100.0, + 'minimal': 10.0, 'normal': 50.0, 'generous': 150.0 + } + for key, val in spec_map.items(): + if key in volume_spec.lower(): + debug_print(f"🎯 规格匹配: '{volume_spec}' → {val}mL") + return val - # 优先级2:mass(质量转体积) - if mass and mass.strip(): - result = parse_mass_to_volume(mass) - if result > 0: - debug_print(f"使用mass转换: {result}mL") - return result + # 🧮 优先级2:mass转体积(简化:1g=1mL) + if mass: + try: + numbers = re.findall(r'\d+\.?\d*', mass) + if numbers: + value = float(numbers[0]) + if 'mg' in mass.lower(): + result = value / 1000.0 + elif 'kg' in mass.lower(): + result = value * 1000.0 + else: + result = value # 默认g + debug_print(f"⚖️ 质量转换: {mass} → {result}mL") + return result + except: + pass - # 优先级3:volume + # 📦 优先级3:volume if volume: - if isinstance(volume, str): - # 字符串形式的体积 - result = parse_volume_string(volume) - if result > 0: - debug_print(f"使用volume字符串: {result}mL") - return result - elif isinstance(volume, (int, float)) and volume > 0: - # 数值形式的体积 + if isinstance(volume, (int, float)): result = float(volume) - debug_print(f"使用volume数值: {result}mL") + debug_print(f"💧 数值体积: {volume} → {result}mL") return result + elif isinstance(volume, str): + try: + # 提取数字 + numbers = re.findall(r'\d+\.?\d*', volume) + if numbers: + value = float(numbers[0]) + # 简化单位判断 + if 'l' in volume.lower() and 'ml' not in volume.lower(): + result = value * 1000.0 # L转mL + else: + result = value # 默认mL + debug_print(f"💧 字符串体积: '{volume}' → {result}mL") + return result + except: + pass # 默认值 - default_volume = 50.0 - debug_print(f"⚠️ 所有体积输入无效,使用默认值: {default_volume}mL") - return default_volume - -def parse_repeats_input(repeats: int, repeats_spec: str = "") -> int: - """ - 统一的重复次数输入解析函数 - - Args: - repeats: 重复次数数值 - repeats_spec: 重复次数规格字符串(优先级高于repeats) - - Returns: - int: 重复次数 - """ - # 优先处理 repeats_spec - if repeats_spec: - return parse_repeats_spec(repeats_spec) - - # 处理 repeats - if repeats > 0: - return repeats - - # 默认值 - debug_print(f"⚠️ 无法处理重复次数输入: repeats={repeats}, repeats_spec='{repeats_spec}',使用默认值: 1次") - return 1 + debug_print(f"⚠️ 体积解析失败,使用默认50mL") + return 50.0 def find_solvent_source(G: nx.DiGraph, solvent: str) -> str: - """查找溶剂源容器""" - debug_print(f"查找溶剂 '{solvent}' 的源容器...") + """查找溶剂源(精简版)""" + debug_print(f"🔍 查找溶剂源: {solvent}") - # 可能的溶剂容器名称 - possible_names = [ - f"flask_{solvent}", - f"reagent_bottle_{solvent}", - f"bottle_{solvent}", - f"container_{solvent}", - f"source_{solvent}", - f"liquid_reagent_bottle_{solvent}" + # 简化搜索列表 + search_patterns = [ + f"flask_{solvent}", f"bottle_{solvent}", f"reagent_{solvent}", + "liquid_reagent_bottle_1", "flask_1", "solvent_bottle" ] - for name in possible_names: - if name in G.nodes(): - debug_print(f"找到溶剂容器: {name}") - return name + for pattern in search_patterns: + if pattern in G.nodes(): + debug_print(f"🎉 找到溶剂源: {pattern}") + return pattern - # 查找通用容器 - generic_containers = [ - "liquid_reagent_bottle_1", - "liquid_reagent_bottle_2", - "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("未找到溶剂容器,使用默认容器") + debug_print(f"⚠️ 使用默认溶剂源: flask_{solvent}") return f"flask_{solvent}" def find_filtrate_vessel(G: nx.DiGraph, filtrate_vessel: str = "") -> str: - """查找滤液收集容器""" - debug_print(f"查找滤液收集容器,指定容器: '{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}' 不存在,查找默认容器") + # 如果指定了且存在,直接使用 + if filtrate_vessel and filtrate_vessel in G.nodes(): + debug_print(f"✅ 使用指定容器: {filtrate_vessel}") + return filtrate_vessel - # 自动查找滤液容器 - possible_names = [ - "waste_workup", # 废液收集 - "filtrate_vessel", # 标准滤液容器 - "collection_bottle_1", # 收集瓶 - "collection_bottle_2", # 收集瓶 - "rotavap", # 旋蒸仪 - "waste_flask", # 废液瓶 - "flask_1", # 通用烧瓶 - "flask_2" # 通用烧瓶 - ] + # 简化搜索列表 + default_vessels = ["waste_workup", "filtrate_vessel", "flask_1", "collection_bottle_1"] - for vessel_name in possible_names: - if vessel_name in G.nodes(): - debug_print(f"找到滤液收集容器: {vessel_name}") - return vessel_name + for vessel in default_vessels: + if vessel in G.nodes(): + debug_print(f"🎉 找到滤液容器: {vessel}") + return vessel - debug_print("未找到滤液收集容器,使用默认容器") + debug_print(f"⚠️ 使用默认滤液容器: waste_workup") return "waste_workup" def generate_wash_solid_protocol( G: nx.DiGraph, vessel: str, solvent: str, - volume: Union[float, str] = "50", # 🔧 修改:默认为字符串 + volume: Union[float, str] = "50", filtrate_vessel: str = "", temp: float = 25.0, stir: bool = False, stir_speed: float = 0.0, - time: Union[str, float] = "0", # 🔧 修改:支持字符串时间 + time: Union[str, float] = "0", repeats: int = 1, - # === 现有参数保持不变 === volume_spec: str = "", repeats_spec: str = "", mass: str = "", @@ -533,164 +172,73 @@ def generate_wash_solid_protocol( **kwargs ) -> List[Dict[str, Any]]: """ - 生成固体清洗操作的协议序列 - 增强版(支持单位) - - 支持多种输入方式: - 1. volume: "100 mL", "50", "2.5 L", "?" - 2. time: "5 min", "300", "0.5 h", "?" - 3. volume_spec: "small volume", "large volume" 等 - 4. mass: "10 g", "2.5 kg", "500 mg" 等(转换为体积) + 生成固体清洗协议(精简版) """ - debug_print("=" * 60) - debug_print("开始生成固体清洗协议(支持单位)") - debug_print(f"输入参数:") - debug_print(f" - vessel: {vessel}") - debug_print(f" - solvent: {solvent}") - debug_print(f" - volume: {volume} (类型: {type(volume)})") - debug_print(f" - time: {time} (类型: {type(time)})") - debug_print(f" - volume_spec: '{volume_spec}'") - debug_print(f" - mass: '{mass}'") - 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" - repeats: {repeats}") - debug_print(f" - repeats_spec: '{repeats_spec}'") - debug_print(f" - event: '{event}'") - debug_print("=" * 60) - - action_sequence = [] - - # === 参数验证 === - debug_print("步骤1: 参数验证...") - - # 验证必需参数 - if not vessel: - raise ValueError("vessel 参数不能为空") + debug_print("🧼" * 20) + debug_print("🚀 开始生成固体清洗协议 ✨") + debug_print(f"📝 输入参数:") + debug_print(f" 🥽 vessel: {vessel}") + debug_print(f" 🧪 solvent: {solvent}") + debug_print(f" 💧 volume: {volume}") + debug_print(f" ⏰ time: {time}") + debug_print(f" 🔄 repeats: {repeats}") + debug_print("🧼" * 20) + + # 📋 快速验证 + if not vessel or vessel not in G.nodes(): + debug_print("❌ 容器验证失败! 😱") + raise ValueError("vessel 参数无效") if not solvent: + debug_print("❌ 溶剂不能为空! 😱") raise ValueError("solvent 参数不能为空") - if vessel not in G.nodes(): - raise ValueError(f"容器 '{vessel}' 不存在于系统中") - - debug_print(f"✅ 必需参数验证通过") + debug_print("✅ 基础验证通过 🎯") - # === 🔧 新增:单位解析处理 === - debug_print("步骤2: 单位解析处理...") + # 🔄 参数解析 + debug_print("📍 步骤1: 参数解析... ⚡") + final_volume = parse_volume_input(volume, volume_spec, mass) + final_time = parse_time_input(time) - # 解析体积(优先级:volume_spec > mass > volume) - if volume_spec and volume_spec.strip(): - final_volume = parse_volume_spec(volume_spec) - debug_print(f"使用volume_spec: {final_volume}mL") - elif mass and mass.strip(): - final_volume = parse_mass_to_volume(mass) - if final_volume > 0: - debug_print(f"使用mass转换: {final_volume}mL") - else: - final_volume = parse_volume_with_units(volume, "mL") - debug_print(f"mass转换失败,使用volume: {final_volume}mL") + # 重复次数处理(简化) + if repeats_spec: + spec_map = {'few': 2, 'several': 3, 'many': 4, 'thorough': 5} + final_repeats = next((v for k, v in spec_map.items() if k in repeats_spec.lower()), repeats) else: - final_volume = parse_volume_with_units(volume, "mL") - debug_print(f"使用volume: {final_volume}mL") - - # 解析时间 - final_time = parse_time_with_units(time, "s") - debug_print(f"解析时间: {time} → {final_time}s ({final_time/60:.1f}min)") - - # 处理重复次数参数(repeats_spec优先) - final_repeats = parse_repeats_input(repeats, repeats_spec) - debug_print(f"最终重复次数: {final_repeats}次") - - # 修正参数范围 - if temp < 0 or temp > 200: - debug_print(f"温度 {temp}°C 超出范围,修正为 25°C") - temp = 25.0 - - if stir_speed < 0 or stir_speed > 500: - debug_print(f"搅拌速度 {stir_speed} RPM 超出范围,修正为 200 RPM") - stir_speed = 200.0 if stir else 0.0 - - if final_time < 0: - debug_print(f"时间 {final_time}s 无效,修正为 0") - final_time = 0.0 + final_repeats = max(1, min(repeats, 5)) # 限制1-5次 - if final_repeats < 1: - debug_print(f"重复次数 {final_repeats} 无效,修正为 1") - final_repeats = 1 - elif final_repeats > 10: - debug_print(f"重复次数 {final_repeats} 过多,修正为 10") - final_repeats = 10 + # 🕐 模拟时间优化 + debug_print(" ⏱️ 模拟时间优化...") + original_time = final_time + if final_time > 60.0: + final_time = 60.0 # 限制最长60秒 + debug_print(f" 🎮 时间优化: {original_time}s → {final_time}s ⚡") - debug_print(f"✅ 单位解析和参数处理完成") + # 参数修正 + temp = max(25.0, min(temp, 80.0)) # 温度范围25-80°C + stir_speed = max(0.0, min(stir_speed, 300.0)) if stir else 0.0 # 速度范围0-300 - # === 查找设备(保持原有逻辑)=== - debug_print("步骤3: 查找设备...") + debug_print(f"🎯 最终参数: 体积={final_volume}mL, 时间={final_time}s, 重复={final_repeats}次") + # 🔍 查找设备 + debug_print("📍 步骤2: 查找设备... 🔍") try: - # 查找溶剂源 solvent_source = find_solvent_source(G, solvent) - - # 查找滤液收集容器 actual_filtrate_vessel = find_filtrate_vessel(G, filtrate_vessel) - - # 查找过滤器(用于过滤操作) - filter_device = None - for node in G.nodes(): - node_data = G.nodes[node] - node_class = node_data.get('class', '') or '' - if 'filter' in node_class.lower(): - filter_device = node - break - - if not filter_device: - filter_device = "filter_1" # 默认过滤器 - - # 查找转移泵(用于转移溶剂) - transfer_pump = None - for node in G.nodes(): - node_data = G.nodes[node] - node_class = node_data.get('class', '') or '' - if 'transfer' in node_class.lower() and 'pump' in node_class.lower(): - transfer_pump = node - break - - if not transfer_pump: - transfer_pump = "transfer_pump_1" # 默认转移泵 - - # 查找搅拌器(如果需要搅拌) - stirrer_device = None - if stir: - for node in G.nodes(): - node_data = G.nodes[node] - node_class = node_data.get('class', '') or '' - if 'stirrer' in node_class.lower(): - stirrer_device = node - break - - if not stirrer_device: - stirrer_device = "stirrer_1" # 默认搅拌器 - - debug_print(f"设备配置:") - debug_print(f" - 溶剂源: {solvent_source}") - debug_print(f" - 转移泵: {transfer_pump}") - debug_print(f" - 过滤器: {filter_device}") - debug_print(f" - 搅拌器: {stirrer_device}") - debug_print(f" - 滤液容器: {actual_filtrate_vessel}") - + debug_print(f"🎉 设备配置完成 ✨") except Exception as e: - debug_print(f"❌ 设备查找失败: {str(e)}") + debug_print(f"❌ 设备查找失败: {str(e)} 😭") raise ValueError(f"设备查找失败: {str(e)}") - # === 执行清洗循环(保持原有逻辑,使用解析后的参数)=== - debug_print("步骤4: 执行清洗循环...") + # 🚀 生成动作序列 + debug_print("📍 步骤3: 生成清洗动作... 🧼") + action_sequence = [] for cycle in range(final_repeats): - debug_print(f"=== 第 {cycle+1}/{final_repeats} 次清洗 ===") + debug_print(f" 🔄 第{cycle+1}/{final_repeats}次清洗...") - # 1. 加入清洗溶剂 - debug_print(f" 步骤 {cycle+1}.1: 加入清洗溶剂") + # 1. 转移溶剂 try: from .pump_protocol import generate_pump_protocol_with_rinsing @@ -698,7 +246,7 @@ def generate_wash_solid_protocol( G=G, from_vessel=solvent_source, to_vessel=vessel, - volume=final_volume, # 使用解析后的体积 + volume=final_volume, amount="", time=0.0, viscous=False, @@ -707,123 +255,62 @@ def generate_wash_solid_protocol( rinsing_repeats=0, solid=False, flowrate=2.5, - transfer_flowrate=0.5, - rate_spec="", - event=event, - through="" + transfer_flowrate=0.5 ) if transfer_actions: action_sequence.extend(transfer_actions) - debug_print(f"✅ 添加了 {len(transfer_actions)} 个转移动作") - else: - debug_print("⚠️ 转移协议返回空序列") - + debug_print(f" ✅ 转移动作: {len(transfer_actions)}个 🚚") + except Exception as e: - debug_print(f"❌ 转移失败: {str(e)}") + debug_print(f" ❌ 转移失败: {str(e)} 😞") - # 2. 搅拌混合(如果需要) - if stir and stirrer_device: - debug_print(f" 步骤 {cycle+1}.2: 搅拌混合") - stir_time = max(final_time, 30.0) if final_time > 0 else 60.0 # 使用解析后的时间 - + # 2. 搅拌(如果需要) + if stir and final_time > 0: stir_action = { - "device_id": stirrer_device, + "device_id": "stirrer_1", "action_name": "stir", "action_kwargs": { "vessel": vessel, - "time": str(time), # 保持原始字符串格式 - "event": event, - "time_spec": "", - "stir_time": stir_time, # 解析后的时间(秒) + "time": str(time), + "stir_time": final_time, "stir_speed": stir_speed, - "settling_time": 30.0 + "settling_time": 10.0 # 🕐 缩短沉降时间 } } action_sequence.append(stir_action) + debug_print(f" ✅ 搅拌动作: {final_time}s, {stir_speed}RPM 🌪️") - # 3. 过滤分离 - debug_print(f" 步骤 {cycle+1}.3: 过滤分离") + # 3. 过滤 filter_action = { - "device_id": filter_device, + "device_id": "filter_1", "action_name": "filter", "action_kwargs": { "vessel": vessel, "filtrate_vessel": actual_filtrate_vessel, - "stir": False, # 过滤时不搅拌 - "stir_speed": 0.0, "temp": temp, - "continue_heatchill": False, - "volume": final_volume # 使用解析后的体积 + "volume": final_volume } } action_sequence.append(filter_action) + debug_print(f" ✅ 过滤动作: → {actual_filtrate_vessel} 🌊") - # 4. 等待完成 - wait_time = 10.0 + # 4. 等待(缩短时间) + wait_time = 5.0 # 🕐 缩短等待时间:10s → 5s action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": wait_time} }) - - # === 总结 === - debug_print("=" * 60) - debug_print(f"固体清洗协议生成完成(支持单位)") - debug_print(f"总动作数: {len(action_sequence)}") - debug_print(f"清洗容器: {vessel}") - debug_print(f"使用溶剂: {solvent}") - debug_print(f"清洗体积: {final_volume}mL") - debug_print(f"清洗时间: {final_time}s ({final_time/60:.1f}min)") - debug_print(f"重复次数: {final_repeats}") - debug_print(f"滤液收集: {actual_filtrate_vessel}") - debug_print(f"事件标识: {event}") - debug_print("=" * 60) - - return action_sequence - -# 删除不需要的函数,简化代码 -def find_wash_solid_device(G: nx.DiGraph) -> str: - """ - 🗑️ 已弃用:WashSolid不再作为单一设备动作 - 现在分解为基础动作序列:transfer + stir + filter - """ - debug_print("⚠️ find_wash_solid_device 已弃用,使用基础动作序列") - return "OrganicSynthesisStation" # 兼容性返回 - -# === 便捷函数 === - -def generate_water_wash_protocol( - G: nx.DiGraph, - vessel: str, - volume: float = 50.0, - **kwargs -) -> List[Dict[str, Any]]: - """水洗协议:用水清洗固体""" - return generate_wash_solid_protocol( - G, vessel, "water", volume, **kwargs - ) - -def generate_organic_wash_protocol( - G: nx.DiGraph, - vessel: str, - solvent: str, - volume: float = 30.0, - **kwargs -) -> List[Dict[str, Any]]: - """有机溶剂清洗协议:用有机溶剂清洗固体""" - return generate_wash_solid_protocol( - G, vessel, solvent, volume, **kwargs - ) - -def generate_thorough_wash_protocol( - G: nx.DiGraph, - vessel: str, - solvent: str, - volume: float = 100.0, - **kwargs -) -> List[Dict[str, Any]]: - """彻底清洗协议:多次清洗,搅拌,加热""" - return generate_wash_solid_protocol( - G, vessel, solvent, volume, - repeats=4, temp=50.0, stir=True, stir_speed=200.0, time=300.0, **kwargs - ) \ No newline at end of file + debug_print(f" ✅ 等待: {wait_time}s ⏰") + + # 🎊 总结 + debug_print("🧼" * 20) + debug_print(f"🎉 固体清洗协议生成完成! ✨") + debug_print(f"📊 总动作数: {len(action_sequence)} 个") + debug_print(f"🥽 清洗容器: {vessel}") + debug_print(f"🧪 使用溶剂: {solvent}") + debug_print(f"💧 清洗体积: {final_volume}mL × {final_repeats}次") + debug_print(f"⏱️ 预计总时间: {(final_time + 5) * final_repeats / 60:.1f} 分钟") + debug_print("🧼" * 20) + + return action_sequence \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_column.py b/unilabos/devices/virtual/virtual_column.py index c83da1c29..892a320f7 100644 --- a/unilabos/devices/virtual/virtual_column.py +++ b/unilabos/devices/virtual/virtual_column.py @@ -3,7 +3,7 @@ from typing import Dict, Any, Optional class VirtualColumn: - """Virtual column device for RunColumn protocol""" + """Virtual column device for RunColumn protocol 🏛️""" def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): # 处理可能的不同调用方式 @@ -25,45 +25,77 @@ def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwarg self._column_length = self.config.get('column_length') or kwargs.get('column_length', 25.0) self._column_diameter = self.config.get('column_diameter') or kwargs.get('column_diameter', 2.0) - print(f"=== VirtualColumn {self.device_id} created with max_flow_rate={self._max_flow_rate}, length={self._column_length}cm ===") + print(f"🏛️ === 虚拟色谱柱 {self.device_id} 已创建 === ✨") + print(f"📏 柱参数: 流速={self._max_flow_rate}mL/min | 长度={self._column_length}cm | 直径={self._column_diameter}cm 🔬") async def initialize(self) -> bool: - """Initialize virtual column""" - self.logger.info(f"Initializing virtual column {self.device_id}") + """Initialize virtual column 🚀""" + self.logger.info(f"🔧 初始化虚拟色谱柱 {self.device_id} ✨") + self.data.update({ "status": "Idle", - "column_state": "Ready", + "column_state": "Ready", "current_flow_rate": 0.0, "max_flow_rate": self._max_flow_rate, "column_length": self._column_length, "column_diameter": self._column_diameter, "processed_volume": 0.0, "progress": 0.0, - "current_status": "Ready" + "current_status": "Ready for separation" }) + + self.logger.info(f"✅ 色谱柱 {self.device_id} 初始化完成 🏛️") + self.logger.info(f"📊 设备规格: 最大流速 {self._max_flow_rate}mL/min | 柱长 {self._column_length}cm 📏") return True async def cleanup(self) -> bool: - """Cleanup virtual column""" - self.logger.info(f"Cleaning up virtual column {self.device_id}") + """Cleanup virtual column 🧹""" + self.logger.info(f"🧹 清理虚拟色谱柱 {self.device_id} 🔚") + + self.data.update({ + "status": "Offline", + "column_state": "Offline", + "current_status": "System offline" + }) + + self.logger.info(f"✅ 色谱柱 {self.device_id} 清理完成 💤") return True - async def run_column(self, from_vessel: str, to_vessel: str, column: str) -> bool: - """Execute column chromatography run - matches RunColumn action""" - self.logger.info(f"Running column separation: {from_vessel} -> {to_vessel} using {column}") + async def run_column(self, from_vessel: str, to_vessel: str, column: str, **kwargs) -> bool: + """Execute column chromatography run - matches RunColumn action 🏛️""" + + # 提取额外参数 + rf = kwargs.get('rf', '0.3') + solvent1 = kwargs.get('solvent1', 'ethyl_acetate') + solvent2 = kwargs.get('solvent2', 'hexane') + ratio = kwargs.get('ratio', '30:70') + + self.logger.info(f"🏛️ 开始柱层析分离: {from_vessel} → {to_vessel} 🚰") + self.logger.info(f" 🧪 使用色谱柱: {column}") + self.logger.info(f" 🎯 Rf值: {rf}") + self.logger.info(f" 🧪 洗脱溶剂: {solvent1}:{solvent2} ({ratio}) 💧") # 更新设备状态 self.data.update({ "status": "Running", "column_state": "Separating", - "current_status": "Column separation in progress", + "current_status": "🏛️ Column separation in progress", "progress": 0.0, - "processed_volume": 0.0 + "processed_volume": 0.0, + "current_from_vessel": from_vessel, + "current_to_vessel": to_vessel, + "current_column": column, + "current_rf": rf, + "current_solvents": f"{solvent1}:{solvent2} ({ratio})" }) # 模拟柱层析分离过程 # 假设处理时间基于流速和柱子长度 - separation_time = (self._column_length * 2) / self._max_flow_rate # 简化计算 + base_time = (self._column_length * 2) / self._max_flow_rate # 简化计算 + separation_time = max(base_time, 20.0) # 最少20秒 + + self.logger.info(f"⏱️ 预计分离时间: {separation_time:.1f}秒 ⌛") + self.logger.info(f"📏 柱参数: 长度 {self._column_length}cm | 流速 {self._max_flow_rate}mL/min 🌊") steps = 20 # 分20个步骤模拟分离过程 step_time = separation_time / steps @@ -74,34 +106,65 @@ async def run_column(self, from_vessel: str, to_vessel: str, column: str) -> boo progress = (i + 1) / steps * 100 volume_processed = (i + 1) * 5.0 # 假设每步处理5mL + # 不同阶段的状态描述 + if progress <= 25: + phase = "🌊 样品上柱阶段" + phase_emoji = "📥" + elif progress <= 50: + phase = "🧪 洗脱开始" + phase_emoji = "💧" + elif progress <= 75: + phase = "⚗️ 成分分离中" + phase_emoji = "🔄" + else: + phase = "🎯 收集产物" + phase_emoji = "📤" + # 更新状态 + status_msg = f"{phase_emoji} {phase}: {progress:.1f}% | 💧 已处理: {volume_processed:.1f}mL" + self.data.update({ "progress": progress, "processed_volume": volume_processed, - "current_status": f"Column separation: {progress:.1f}% - Processing {volume_processed:.1f}mL" + "current_status": status_msg, + "current_phase": phase }) - self.logger.info(f"Column separation progress: {progress:.1f}%") + # 进度日志(每25%打印一次) + if progress >= 25 and (i + 1) % 5 == 0: # 每5步(25%)打印一次 + self.logger.info(f"📊 分离进度: {progress:.0f}% | {phase} | 💧 {volume_processed:.1f}mL 完成 ✨") # 分离完成 + final_status = f"✅ 柱层析分离完成: {from_vessel} → {to_vessel} | 💧 共处理 {volume_processed:.1f}mL" + self.data.update({ "status": "Idle", "column_state": "Ready", - "current_status": "Column separation completed", - "progress": 100.0 + "current_status": final_status, + "progress": 100.0, + "final_volume": volume_processed }) - self.logger.info(f"Column separation completed: {from_vessel} -> {to_vessel}") + self.logger.info(f"🎉 柱层析分离完成! ✨") + self.logger.info(f"📊 分离结果:") + self.logger.info(f" 🥽 源容器: {from_vessel}") + self.logger.info(f" 🥽 目标容器: {to_vessel}") + self.logger.info(f" 🏛️ 使用色谱柱: {column}") + self.logger.info(f" 💧 处理体积: {volume_processed:.1f}mL") + self.logger.info(f" 🧪 洗脱条件: {solvent1}:{solvent2} ({ratio})") + self.logger.info(f" 🎯 Rf值: {rf}") + self.logger.info(f" ⏱️ 总耗时: {separation_time:.1f}秒 🏁") + return True # 状态属性 @property def status(self) -> str: - return self.data.get("status", "Unknown") + return self.data.get("status", "❓ Unknown") @property def column_state(self) -> str: - return self.data.get("column_state", "Unknown") + return self.data.get("column_state", "❓ Unknown") @property def current_flow_rate(self) -> float: @@ -129,4 +192,12 @@ def progress(self) -> float: @property def current_status(self) -> str: - return self.data.get("current_status", "Ready") \ No newline at end of file + return self.data.get("current_status", "📋 Ready") + + @property + def current_phase(self) -> str: + return self.data.get("current_phase", "🏠 待机中") + + @property + def final_volume(self) -> float: + return self.data.get("final_volume", 0.0) \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_filter.py b/unilabos/devices/virtual/virtual_filter.py index d70c854a8..655934b64 100644 --- a/unilabos/devices/virtual/virtual_filter.py +++ b/unilabos/devices/virtual/virtual_filter.py @@ -5,7 +5,7 @@ class VirtualFilter: - """Virtual filter device - 完全按照 Filter.action 规范""" + """Virtual filter device - 完全按照 Filter.action 规范 🌊""" def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs): if device_id is None and 'id' in kwargs: @@ -31,8 +31,8 @@ def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, A setattr(self, key, value) async def initialize(self) -> bool: - """Initialize virtual filter""" - self.logger.info(f"Initializing virtual filter {self.device_id}") + """Initialize virtual filter 🚀""" + self.logger.info(f"🔧 初始化虚拟过滤器 {self.device_id} ✨") # 按照 Filter.action 的 feedback 字段初始化 self.data.update({ @@ -43,17 +43,21 @@ async def initialize(self) -> bool: "current_status": "Ready for filtration", # Filter.action feedback "message": "Ready for filtration" }) + + self.logger.info(f"✅ 过滤器 {self.device_id} 初始化完成 🌊") return True async def cleanup(self) -> bool: - """Cleanup virtual filter""" - self.logger.info(f"Cleaning up virtual filter {self.device_id}") + """Cleanup virtual filter 🧹""" + self.logger.info(f"🧹 清理虚拟过滤器 {self.device_id} 🔚") self.data.update({ "status": "Offline", "current_status": "System offline", "message": "System offline" }) + + self.logger.info(f"✅ 过滤器 {self.device_id} 清理完成 💤") return True async def filter( @@ -66,74 +70,82 @@ async def filter( continue_heatchill: bool = False, volume: float = 0.0 ) -> bool: - """Execute filter action - 完全按照 Filter.action 参数""" + """Execute filter action - 完全按照 Filter.action 参数 🌊""" # 🔧 新增:温度自动调整 original_temp = temp if temp == 0.0: temp = 25.0 # 0度自动设置为室温 - self.logger.info(f"温度自动调整: {original_temp}°C → {temp}°C (室温)") + self.logger.info(f"🌡️ 温度自动调整: {original_temp}°C → {temp}°C (室温) 🏠") elif temp < 4.0: temp = 4.0 # 小于4度自动设置为4度 - self.logger.info(f"温度自动调整: {original_temp}°C → {temp}°C (最低温度)") + self.logger.info(f"🌡️ 温度自动调整: {original_temp}°C → {temp}°C (最低温度) ❄️") - self.logger.info(f"Filter: vessel={vessel}, filtrate_vessel={filtrate_vessel}") - self.logger.info(f" stir={stir}, stir_speed={stir_speed}, temp={temp}") - self.logger.info(f" continue_heatchill={continue_heatchill}, volume={volume}") + self.logger.info(f"🌊 开始过滤操作: {vessel} → {filtrate_vessel} 🚰") + self.logger.info(f" 🌪️ 搅拌: {stir} ({stir_speed} RPM)") + self.logger.info(f" 🌡️ 温度: {temp}°C") + self.logger.info(f" 💧 体积: {volume}mL") + self.logger.info(f" 🔥 保持加热: {continue_heatchill}") # 验证参数 if temp > self._max_temp or temp < 4.0: - error_msg = f"温度 {temp}°C 超出范围 (4-{self._max_temp}°C)" - self.logger.error(error_msg) + error_msg = f"🌡️ 温度 {temp}°C 超出范围 (4-{self._max_temp}°C) ⚠️" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"Error: {error_msg}", - "current_status": f"Error: {error_msg}", + "status": f"Error: 温度超出范围 ⚠️", + "current_status": f"Error: 温度超出范围 ⚠️", "message": error_msg }) return False if stir and stir_speed > self._max_stir_speed: - error_msg = f"搅拌速度 {stir_speed} RPM 超出范围 (0-{self._max_stir_speed} RPM)" - self.logger.error(error_msg) + error_msg = f"🌪️ 搅拌速度 {stir_speed} RPM 超出范围 (0-{self._max_stir_speed} RPM) ⚠️" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"Error: {error_msg}", - "current_status": f"Error: {error_msg}", + "status": f"Error: 搅拌速度超出范围 ⚠️", + "current_status": f"Error: 搅拌速度超出范围 ⚠️", "message": error_msg }) return False if volume > self._max_volume: - error_msg = f"过滤体积 {volume} mL 超出范围 (0-{self._max_volume} mL)" - self.logger.error(error_msg) + error_msg = f"💧 过滤体积 {volume} mL 超出范围 (0-{self._max_volume} mL) ⚠️" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"Error: {error_msg}", - "current_status": f"Error: {error_msg}", + "status": f"Error: 体积超出范围 ⚠️", + "current_status": f"Error: 体积超出范围 ⚠️", "message": error_msg }) return False # 开始过滤 filter_volume = volume if volume > 0 else 50.0 + self.logger.info(f"🚀 开始过滤 {filter_volume}mL 液体 💧") self.data.update({ - "status": f"过滤中: {vessel}", + "status": f"🌊 过滤中: {vessel}", "current_temp": temp, "filtered_volume": 0.0, "progress": 0.0, - "current_status": f"Filtering {vessel} → {filtrate_vessel}", - "message": f"Starting filtration: {vessel} → {filtrate_vessel}" + "current_status": f"🌊 Filtering {vessel} → {filtrate_vessel}", + "message": f"🚀 Starting filtration: {vessel} → {filtrate_vessel}" }) try: # 过滤过程 - 实时更新进度 start_time = time_module.time() + # 根据体积和搅拌估算过滤时间 base_time = filter_volume / 5.0 # 5mL/s 基础速度 if stir: base_time *= 0.8 # 搅拌加速过滤 + self.logger.info(f"🌪️ 搅拌加速过滤,预计时间减少20% ⚡") if temp > 50.0: base_time *= 0.7 # 高温加速过滤 + self.logger.info(f"🔥 高温加速过滤,预计时间减少30% ⚡") + filter_time = max(base_time, 10.0) # 最少10秒 + self.logger.info(f"⏱️ 预计过滤时间: {filter_time:.1f}秒 ⌛") while True: current_time = time_module.time() @@ -143,20 +155,24 @@ async def filter( current_filtered = (progress / 100.0) * filter_volume # 更新状态 - 按照 Filter.action feedback 字段 - status_msg = f"过滤中: {vessel}" + status_msg = f"🌊 过滤中: {vessel}" if stir: - status_msg += f" | 搅拌: {stir_speed} RPM" - status_msg += f" | {temp}°C | {progress:.1f}% | 已过滤: {current_filtered:.1f}mL" + status_msg += f" | 🌪️ 搅拌: {stir_speed} RPM" + status_msg += f" | 🌡️ {temp}°C | 📊 {progress:.1f}% | 💧 已过滤: {current_filtered:.1f}mL" self.data.update({ "progress": progress, # Filter.action feedback "current_temp": temp, # Filter.action feedback "filtered_volume": current_filtered, # Filter.action feedback - "current_status": f"Filtering: {progress:.1f}% complete", # Filter.action feedback + "current_status": f"🌊 Filtering: {progress:.1f}% complete", # Filter.action feedback "status": status_msg, - "message": f"Filtering: {progress:.1f}% complete, {current_filtered:.1f}mL filtered" + "message": f"🌊 Filtering: {progress:.1f}% complete, {current_filtered:.1f}mL filtered" }) + # 进度日志(每25%打印一次) + if progress >= 25 and progress % 25 < 1: + self.logger.info(f"📊 过滤进度: {progress:.0f}% | 💧 {current_filtered:.1f}mL 完成 ✨") + if remaining <= 0: break @@ -164,54 +180,57 @@ async def filter( # 过滤完成 final_temp = temp if continue_heatchill else 25.0 - final_status = f"过滤完成: {vessel} | {filter_volume}mL → {filtrate_vessel}" + final_status = f"✅ 过滤完成: {vessel} | 💧 {filter_volume}mL → {filtrate_vessel}" if continue_heatchill: - final_status += " | 继续加热搅拌" + final_status += " | 🔥 继续加热搅拌" + self.logger.info(f"🔥 继续保持加热搅拌状态 🌪️") self.data.update({ "status": final_status, "progress": 100.0, # Filter.action feedback "current_temp": final_temp, # Filter.action feedback "filtered_volume": filter_volume, # Filter.action feedback - "current_status": f"Filtration completed: {filter_volume}mL", # Filter.action feedback - "message": f"Filtration completed: {filter_volume}mL filtered from {vessel}" + "current_status": f"✅ Filtration completed: {filter_volume}mL", # Filter.action feedback + "message": f"✅ Filtration completed: {filter_volume}mL filtered from {vessel}" }) - self.logger.info(f"Filtration completed: {filter_volume}mL from {vessel} to {filtrate_vessel}") + self.logger.info(f"🎉 过滤完成! 💧 {filter_volume}mL 从 {vessel} 过滤到 {filtrate_vessel} ✨") + self.logger.info(f"📊 最终状态: 温度 {final_temp}°C | 进度 100% | 体积 {filter_volume}mL 🏁") return True except Exception as e: - self.logger.error(f"Error during filtration: {str(e)}") + error_msg = f"过滤过程中发生错误: {str(e)} 💥" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"过滤错误: {str(e)}", - "current_status": f"Filtration failed: {str(e)}", - "message": f"Filtration failed: {str(e)}" + "status": f"❌ 过滤错误: {str(e)}", + "current_status": f"❌ Filtration failed: {str(e)}", + "message": f"❌ Filtration failed: {str(e)}" }) return False # === 核心状态属性 - 按照 Filter.action feedback 字段 === @property def status(self) -> str: - return self.data.get("status", "Unknown") + return self.data.get("status", "❓ Unknown") @property def progress(self) -> float: - """Filter.action feedback 字段""" + """Filter.action feedback 字段 📊""" return self.data.get("progress", 0.0) @property def current_temp(self) -> float: - """Filter.action feedback 字段""" + """Filter.action feedback 字段 🌡️""" return self.data.get("current_temp", 25.0) @property def filtered_volume(self) -> float: - """Filter.action feedback 字段""" + """Filter.action feedback 字段 💧""" return self.data.get("filtered_volume", 0.0) @property def current_status(self) -> str: - """Filter.action feedback 字段""" + """Filter.action feedback 字段 📋""" return self.data.get("current_status", "") @property diff --git a/unilabos/devices/virtual/virtual_heatchill.py b/unilabos/devices/virtual/virtual_heatchill.py index 20c9a196b..94ab5720d 100644 --- a/unilabos/devices/virtual/virtual_heatchill.py +++ b/unilabos/devices/virtual/virtual_heatchill.py @@ -4,7 +4,7 @@ from typing import Dict, Any class VirtualHeatChill: - """Virtual heat chill device for HeatChillProtocol testing""" + """Virtual heat chill device for HeatChillProtocol testing 🌡️""" def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): # 处理可能的不同调用方式 @@ -31,31 +31,40 @@ def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwarg for key, value in kwargs.items(): if key not in skip_keys and not hasattr(self, key): setattr(self, key, value) + + print(f"🌡️ === 虚拟温控设备 {self.device_id} 已创建 === ✨") + print(f"🔥 温度范围: {self._min_temp}°C ~ {self._max_temp}°C | 🌪️ 最大搅拌: {self._max_stir_speed} RPM") async def initialize(self) -> bool: - """Initialize virtual heat chill""" - self.logger.info(f"Initializing virtual heat chill {self.device_id}") + """Initialize virtual heat chill 🚀""" + self.logger.info(f"🔧 初始化虚拟温控设备 {self.device_id} ✨") # 初始化状态信息 self.data.update({ - "status": "Idle", + "status": "🏠 待机中", "operation_mode": "Idle", "is_stirring": False, "stir_speed": 0.0, "remaining_time": 0.0, }) + + self.logger.info(f"✅ 温控设备 {self.device_id} 初始化完成 🌡️") + self.logger.info(f"📊 设备规格: 温度范围 {self._min_temp}°C ~ {self._max_temp}°C | 搅拌范围 0 ~ {self._max_stir_speed} RPM") return True async def cleanup(self) -> bool: - """Cleanup virtual heat chill""" - self.logger.info(f"Cleaning up virtual heat chill {self.device_id}") + """Cleanup virtual heat chill 🧹""" + self.logger.info(f"🧹 清理虚拟温控设备 {self.device_id} 🔚") + self.data.update({ - "status": "Offline", + "status": "💤 离线", "operation_mode": "Offline", "is_stirring": False, "stir_speed": 0.0, "remaining_time": 0.0 }) + + self.logger.info(f"✅ 温控设备 {self.device_id} 清理完成 💤") return True async def heat_chill(self, vessel: str, temp: float, time, stir: bool, @@ -72,62 +81,73 @@ async def heat_chill(self, vessel: str, temp: float, time, stir: bool, purpose = str(purpose) except (ValueError, TypeError) as e: error_msg = f"参数类型转换错误: temp={temp}({type(temp)}), time={time}({type(time)}), error={str(e)}" - self.logger.error(error_msg) + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"Error: {error_msg}", + "status": f"❌ 错误: {error_msg}", "operation_mode": "Error" }) return False - self.logger.info(f"HeatChill: vessel={vessel}, temp={temp}°C, time={time_value}s, stir={stir}, stir_speed={stir_speed}") + # 确定温度操作emoji + if temp > 25.0: + temp_emoji = "🔥" + operation_mode = "Heating" + status_action = "加热" + elif temp < 25.0: + temp_emoji = "❄️" + operation_mode = "Cooling" + status_action = "冷却" + else: + temp_emoji = "🌡️" + operation_mode = "Maintaining" + status_action = "保温" + + self.logger.info(f"🌡️ 开始温控操作: {vessel} → {temp}°C {temp_emoji}") + self.logger.info(f" 🥽 容器: {vessel}") + self.logger.info(f" 🎯 目标温度: {temp}°C {temp_emoji}") + self.logger.info(f" ⏰ 持续时间: {time_value}s") + self.logger.info(f" 🌪️ 搅拌: {stir} ({stir_speed} RPM)") + self.logger.info(f" 📝 目的: {purpose}") # 验证参数范围 if temp > self._max_temp or temp < self._min_temp: - error_msg = f"温度 {temp}°C 超出范围 ({self._min_temp}°C - {self._max_temp}°C)" - self.logger.error(error_msg) + error_msg = f"🌡️ 温度 {temp}°C 超出范围 ({self._min_temp}°C - {self._max_temp}°C) ⚠️" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"Error: {error_msg}", + "status": f"❌ 错误: 温度超出范围 ⚠️", "operation_mode": "Error" }) return False if stir and stir_speed > self._max_stir_speed: - error_msg = f"搅拌速度 {stir_speed} RPM 超出最大值 {self._max_stir_speed} RPM" - self.logger.error(error_msg) + error_msg = f"🌪️ 搅拌速度 {stir_speed} RPM 超出最大值 {self._max_stir_speed} RPM ⚠️" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"Error: {error_msg}", + "status": f"❌ 错误: 搅拌速度超出范围 ⚠️", "operation_mode": "Error" }) return False if time_value <= 0: - error_msg = f"时间 {time_value}s 必须大于0" - self.logger.error(error_msg) + error_msg = f"⏰ 时间 {time_value}s 必须大于0 ⚠️" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"Error: {error_msg}", + "status": f"❌ 错误: 时间参数无效 ⚠️", "operation_mode": "Error" }) return False - # 确定操作模式 - if temp > 25.0: - operation_mode = "Heating" - status_action = "加热" - elif temp < 25.0: - operation_mode = "Cooling" - status_action = "冷却" - else: - operation_mode = "Maintaining" - status_action = "保温" - # 🔧 修复:使用转换后的时间值 start_time = time_module.time() total_time = time_value # 使用转换后的浮点数 + self.logger.info(f"🚀 开始{status_action}程序! 预计用时 {total_time:.1f}秒 ⏱️") + # 开始操作 - stir_info = f" | 搅拌: {stir_speed} RPM" if stir else "" + stir_info = f" | 🌪️ 搅拌: {stir_speed} RPM" if stir else "" + self.data.update({ - "status": f"运行中: {status_action} {vessel} 至 {temp}°C | 剩余: {total_time:.0f}s{stir_info}", + "status": f"{temp_emoji} 运行中: {status_action} {vessel} 至 {temp}°C | ⏰ 剩余: {total_time:.0f}s{stir_info}", "operation_mode": operation_mode, "is_stirring": stir, "stir_speed": stir_speed if stir else 0.0, @@ -135,17 +155,25 @@ async def heat_chill(self, vessel: str, temp: float, time, stir: bool, }) # 在等待过程中每秒更新剩余时间 + last_logged_time = 0 while True: current_time = time_module.time() elapsed = current_time - start_time remaining = max(0, total_time - elapsed) + progress = (elapsed / total_time) * 100 if total_time > 0 else 100 # 更新剩余时间和状态 self.data.update({ "remaining_time": remaining, - "status": f"运行中: {status_action} {vessel} 至 {temp}°C | 剩余: {remaining:.0f}s{stir_info}" + "status": f"{temp_emoji} 运行中: {status_action} {vessel} 至 {temp}°C | ⏰ 剩余: {remaining:.0f}s{stir_info}", + "progress": progress }) + # 进度日志(每25%打印一次) + if progress >= 25 and int(progress) % 25 == 0 and int(progress) != last_logged_time: + self.logger.info(f"📊 {status_action}进度: {progress:.0f}% | ⏰ 剩余: {remaining:.0f}s | {temp_emoji} 目标: {temp}°C ✨") + last_logged_time = int(progress) + # 如果时间到了,退出循环 if remaining <= 0: break @@ -154,20 +182,30 @@ async def heat_chill(self, vessel: str, temp: float, time, stir: bool, await asyncio.sleep(1.0) # 操作完成 - final_stir_info = f" | 搅拌: {stir_speed} RPM" if stir else "" + final_stir_info = f" | 🌪️ 搅拌: {stir_speed} RPM" if stir else "" + self.data.update({ - "status": f"完成: {vessel} 已达到 {temp}°C | 用时: {total_time:.0f}s{final_stir_info}", + "status": f"✅ 完成: {vessel} 已达到 {temp}°C {temp_emoji} | ⏱️ 用时: {total_time:.0f}s{final_stir_info}", "operation_mode": "Completed", "remaining_time": 0.0, "is_stirring": False, - "stir_speed": 0.0 + "stir_speed": 0.0, + "progress": 100.0 }) - self.logger.info(f"HeatChill completed for vessel {vessel} at {temp}°C after {total_time}s") + self.logger.info(f"🎉 温控操作完成! ✨") + self.logger.info(f"📊 操作结果:") + self.logger.info(f" 🥽 容器: {vessel}") + self.logger.info(f" 🌡️ 达到温度: {temp}°C {temp_emoji}") + self.logger.info(f" ⏱️ 总用时: {total_time:.0f}s") + if stir: + self.logger.info(f" 🌪️ 搅拌速度: {stir_speed} RPM") + self.logger.info(f" 📝 操作目的: {purpose} 🏁") + return True async def heat_chill_start(self, vessel: str, temp: float, purpose: str) -> bool: - """Start continuous heat chill""" + """Start continuous heat chill 🔄""" # 🔧 添加类型转换 try: @@ -176,73 +214,82 @@ async def heat_chill_start(self, vessel: str, temp: float, purpose: str) -> bool purpose = str(purpose) except (ValueError, TypeError) as e: error_msg = f"参数类型转换错误: {str(e)}" - self.logger.error(error_msg) - self.data.update({ - "status": f"Error: {error_msg}", - "operation_mode": "Error" - }) - return False - - self.logger.info(f"HeatChillStart: vessel={vessel}, temp={temp}°C") - - # 验证参数 - if temp > self._max_temp or temp < self._min_temp: - error_msg = f"温度 {temp}°C 超出范围 ({self._min_temp}°C - {self._max_temp}°C)" - self.logger.error(error_msg) + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"Error: {error_msg}", + "status": f"❌ 错误: {error_msg}", "operation_mode": "Error" }) return False - # 确定操作模式 + # 确定温度操作emoji if temp > 25.0: + temp_emoji = "🔥" operation_mode = "Heating" status_action = "持续加热" elif temp < 25.0: + temp_emoji = "❄️" operation_mode = "Cooling" status_action = "持续冷却" else: + temp_emoji = "🌡️" operation_mode = "Maintaining" status_action = "恒温保持" + self.logger.info(f"🔄 启动持续温控: {vessel} → {temp}°C {temp_emoji}") + self.logger.info(f" 🥽 容器: {vessel}") + self.logger.info(f" 🎯 目标温度: {temp}°C {temp_emoji}") + self.logger.info(f" 🔄 模式: {status_action}") + self.logger.info(f" 📝 目的: {purpose}") + + # 验证参数 + if temp > self._max_temp or temp < self._min_temp: + error_msg = f"🌡️ 温度 {temp}°C 超出范围 ({self._min_temp}°C - {self._max_temp}°C) ⚠️" + self.logger.error(f"❌ {error_msg}") + self.data.update({ + "status": f"❌ 错误: 温度超出范围 ⚠️", + "operation_mode": "Error" + }) + return False + self.data.update({ - "status": f"启动: {status_action} {vessel} 至 {temp}°C | 持续运行", + "status": f"🔄 启动: {status_action} {vessel} 至 {temp}°C {temp_emoji} | ♾️ 持续运行", "operation_mode": operation_mode, "is_stirring": False, "stir_speed": 0.0, "remaining_time": -1.0, # -1 表示持续运行 }) + self.logger.info(f"✅ 持续温控已启动! {temp_emoji} {status_action}模式 🚀") return True async def heat_chill_stop(self, vessel: str) -> bool: - """Stop heat chill""" + """Stop heat chill 🛑""" # 🔧 添加类型转换 try: vessel = str(vessel) except (ValueError, TypeError) as e: error_msg = f"参数类型转换错误: {str(e)}" - self.logger.error(error_msg) + self.logger.error(f"❌ {error_msg}") return False - self.logger.info(f"HeatChillStop: vessel={vessel}") + self.logger.info(f"🛑 停止温控: {vessel}") self.data.update({ - "status": f"已停止: {vessel} 温控停止", + "status": f"🛑 已停止: {vessel} 温控停止", "operation_mode": "Stopped", "is_stirring": False, "stir_speed": 0.0, "remaining_time": 0.0, }) + self.logger.info(f"✅ 温控设备已停止 {vessel} 的温度控制 🏁") return True # 状态属性 @property def status(self) -> str: - return self.data.get("status", "Idle") + return self.data.get("status", "🏠 待机中") @property def operation_mode(self) -> str: @@ -258,4 +305,20 @@ def stir_speed(self) -> float: @property def remaining_time(self) -> float: - return self.data.get("remaining_time", 0.0) \ No newline at end of file + return self.data.get("remaining_time", 0.0) + + @property + def progress(self) -> float: + return self.data.get("progress", 0.0) + + @property + def max_temp(self) -> float: + return self._max_temp + + @property + def min_temp(self) -> float: + return self._min_temp + + @property + def max_stir_speed(self) -> float: + return self._max_stir_speed \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_multiway_valve.py b/unilabos/devices/virtual/virtual_multiway_valve.py index c24b7b1c5..c12aa7fe6 100644 --- a/unilabos/devices/virtual/virtual_multiway_valve.py +++ b/unilabos/devices/virtual/virtual_multiway_valve.py @@ -1,16 +1,20 @@ import time +import logging from typing import Union, Dict, Optional class VirtualMultiwayValve: """ - 虚拟九通阀门 - 0号位连接transfer pump,1-8号位连接其他设备 + 虚拟九通阀门 - 0号位连接transfer pump,1-8号位连接其他设备 🔄 """ def __init__(self, port: str = "VIRTUAL", positions: int = 8): self.port = port self.max_positions = positions # 1-8号位 self.total_positions = positions + 1 # 0-8号位,共9个位置 + # 添加日志记录器 + self.logger = logging.getLogger(f"VirtualMultiwayValve.{port}") + # 状态属性 self._status = "Idle" self._valve_state = "Ready" @@ -29,6 +33,10 @@ def __init__(self, port: str = "VIRTUAL", positions: int = 8): 7: "port_7", # 7号位 8: "port_8" # 8号位 } + + print(f"🔄 === 虚拟多通阀门已创建 === ✨") + print(f"🎯 端口: {port} | 📊 位置范围: 0-{self.max_positions} | 🏠 初始位置: 0 (transfer_pump)") + self.logger.info(f"🔧 多通阀门初始化: 端口={port}, 最大位置={self.max_positions}") @property def status(self) -> str: @@ -47,16 +55,16 @@ def target_position(self) -> int: return self._target_position def get_current_position(self) -> int: - """获取当前阀门位置""" + """获取当前阀门位置 📍""" return self._current_position def get_current_port(self) -> str: - """获取当前连接的端口名称""" + """获取当前连接的端口名称 🔌""" return self.position_map.get(self._current_position, "unknown") def set_position(self, command: Union[int, str]): """ - 设置阀门位置 - 支持0-8位置 + 设置阀门位置 - 支持0-8位置 🎯 Args: command: 目标位置 (0-8) 或位置字符串 @@ -71,112 +79,162 @@ def set_position(self, command: Union[int, str]): pos = int(command) if pos < 0 or pos > self.max_positions: - raise ValueError(f"Position must be between 0 and {self.max_positions}") + error_msg = f"位置必须在 0-{self.max_positions} 范围内" + self.logger.error(f"❌ {error_msg}: 请求位置={pos}") + raise ValueError(error_msg) + + # 获取位置描述emoji + if pos == 0: + pos_emoji = "🚰" + pos_desc = "泵位置" + else: + pos_emoji = "🔌" + pos_desc = f"端口{pos}" + + old_position = self._current_position + old_port = self.get_current_port() + + self.logger.info(f"🔄 阀门切换: {old_position}({old_port}) → {pos}({self.position_map.get(pos, 'unknown')}) {pos_emoji}") self._status = "Busy" self._valve_state = "Moving" self._target_position = pos # 模拟阀门切换时间 - switch_time = abs(self._current_position - pos) * 0.5 # 每个位置0.5秒 - time.sleep(switch_time) + switch_time = abs(self._current_position - pos) * 0.5 # 每个位置0.1秒 + + if switch_time > 0: + self.logger.info(f"⏱️ 阀门移动中... 预计用时: {switch_time:.1f}秒 🔄") + time.sleep(switch_time) self._current_position = pos self._status = "Idle" self._valve_state = "Ready" current_port = self.get_current_port() - return f"Position set to {pos} ({current_port})" + success_msg = f"✅ 阀门已切换到位置 {pos} ({current_port}) {pos_emoji}" + + self.logger.info(success_msg) + return success_msg except ValueError as e: + error_msg = f"❌ 阀门切换失败: {str(e)}" self._status = "Error" self._valve_state = "Error" - return f"Error: {str(e)}" + self.logger.error(error_msg) + return error_msg def set_to_pump_position(self): - """切换到transfer pump位置(0号位)""" + """切换到transfer pump位置(0号位)🚰""" + self.logger.info(f"🚰 切换到泵位置...") return self.set_position(0) def set_to_port(self, port_number: int): """ - 切换到指定端口位置 + 切换到指定端口位置 🔌 Args: port_number: 端口号 (1-8) """ if port_number < 1 or port_number > self.max_positions: - raise ValueError(f"Port number must be between 1 and {self.max_positions}") + error_msg = f"端口号必须在 1-{self.max_positions} 范围内" + self.logger.error(f"❌ {error_msg}: 请求端口={port_number}") + raise ValueError(error_msg) + + self.logger.info(f"🔌 切换到端口 {port_number}...") return self.set_position(port_number) def open(self): - """打开阀门 - 设置到transfer pump位置(0号位)""" + """打开阀门 - 设置到transfer pump位置(0号位)🔓""" + self.logger.info(f"🔓 打开阀门,设置到泵位置...") return self.set_to_pump_position() def close(self): - """关闭阀门 - 对于多通阀门,设置到一个"关闭"状态""" + """关闭阀门 - 对于多通阀门,设置到一个"关闭"状态 🔒""" + self.logger.info(f"🔒 关闭阀门...") + self._status = "Busy" self._valve_state = "Closing" time.sleep(0.5) - + # 可以选择保持当前位置或设置特殊关闭状态 self._status = "Idle" self._valve_state = "Closed" - return f"Valve closed at position {self._current_position}" + close_msg = f"🔒 阀门已关闭,保持在位置 {self._current_position} ({self.get_current_port()})" + self.logger.info(close_msg) + return close_msg def get_valve_position(self) -> int: - """获取阀门位置 - 兼容性方法""" + """获取阀门位置 - 兼容性方法 📍""" return self._current_position def is_at_position(self, position: int) -> bool: - """检查是否在指定位置""" - return self._current_position == position + """检查是否在指定位置 🎯""" + result = self._current_position == position + self.logger.debug(f"🎯 位置检查: 当前={self._current_position}, 目标={position}, 匹配={result}") + return result def is_at_pump_position(self) -> bool: - """检查是否在transfer pump位置""" - return self._current_position == 0 + """检查是否在transfer pump位置 🚰""" + result = self._current_position == 0 + pump_status = "是" if result else "否" + self.logger.debug(f"🚰 泵位置检查: {pump_status} (当前位置: {self._current_position})") + return result def is_at_port(self, port_number: int) -> bool: - """检查是否在指定端口位置""" - return self._current_position == port_number + """检查是否在指定端口位置 🔌""" + result = self._current_position == port_number + port_status = "是" if result else "否" + self.logger.debug(f"🔌 端口{port_number}检查: {port_status} (当前位置: {self._current_position})") + return result def get_available_positions(self) -> list: - """获取可用位置列表""" - return list(range(0, self.max_positions + 1)) + """获取可用位置列表 📋""" + positions = list(range(0, self.max_positions + 1)) + self.logger.debug(f"📋 可用位置: {positions}") + return positions def get_available_ports(self) -> Dict[int, str]: - """获取可用端口映射""" + """获取可用端口映射 🗺️""" + self.logger.debug(f"🗺️ 端口映射: {self.position_map}") return self.position_map.copy() def reset(self): - """重置阀门到transfer pump位置(0号位)""" + """重置阀门到transfer pump位置(0号位)🔄""" + self.logger.info(f"🔄 重置阀门到泵位置...") return self.set_position(0) def switch_between_pump_and_port(self, port_number: int): """ - 在transfer pump位置和指定端口之间切换 + 在transfer pump位置和指定端口之间切换 🔄 Args: port_number: 目标端口号 (1-8) """ if self._current_position == 0: # 当前在pump位置,切换到指定端口 + self.logger.info(f"🔄 从泵位置切换到端口 {port_number}...") return self.set_to_port(port_number) else: # 当前在某个端口,切换到pump位置 + self.logger.info(f"🔄 从端口 {self._current_position} 切换到泵位置...") return self.set_to_pump_position() def get_flow_path(self) -> str: - """获取当前流路路径描述""" + """获取当前流路路径描述 🌊""" current_port = self.get_current_port() if self._current_position == 0: - return f"Transfer pump connected (position {self._current_position})" + flow_path = f"🚰 转移泵已连接 (位置 {self._current_position})" else: - return f"Port {self._current_position} connected ({current_port})" + flow_path = f"🔌 端口 {self._current_position} 已连接 ({current_port})" + + self.logger.debug(f"🌊 当前流路: {flow_path}") + return flow_path def get_info(self) -> dict: - """获取阀门详细信息""" - return { + """获取阀门详细信息 📊""" + info = { "port": self.port, "max_positions": self.max_positions, "total_positions": self.total_positions, @@ -188,18 +246,25 @@ def get_info(self) -> dict: "flow_path": self.get_flow_path(), "position_map": self.position_map } + + self.logger.debug(f"📊 阀门信息: 位置={self._current_position}, 状态={self._status}, 端口={self.get_current_port()}") + return info def __str__(self): - return f"VirtualMultiwayValve(Position: {self._current_position}/{self.max_positions}, Port: {self.get_current_port()}, Status: {self._status})" + current_port = self.get_current_port() + status_emoji = "✅" if self._status == "Idle" else "🔄" if self._status == "Busy" else "❌" + + return f"🔄 VirtualMultiwayValve({status_emoji} 位置: {self._current_position}/{self.max_positions}, 端口: {current_port}, 状态: {self._status})" def set_valve_position(self, command: Union[int, str]): """ - 设置阀门位置 - 兼容pump_protocol调用 + 设置阀门位置 - 兼容pump_protocol调用 🎯 这是set_position的别名方法,用于兼容pump_protocol.py Args: command: 目标位置 (0-8) 或位置字符串 """ + self.logger.debug(f"🎯 兼容性调用: set_valve_position({command})") return self.set_position(command) @@ -207,25 +272,35 @@ def set_valve_position(self, command: Union[int, str]): if __name__ == "__main__": valve = VirtualMultiwayValve() - print("=== 虚拟九通阀门测试 ===") - print(f"初始状态: {valve}") - print(f"当前流路: {valve.get_flow_path()}") + print("🔄 === 虚拟九通阀门测试 === ✨") + print(f"🏠 初始状态: {valve}") + print(f"🌊 当前流路: {valve.get_flow_path()}") # 切换到试剂瓶1(1号位) - print(f"\n切换到1号位: {valve.set_position(1)}") - print(f"当前状态: {valve}") + print(f"\n🔌 切换到1号位: {valve.set_position(1)}") + print(f"📍 当前状态: {valve}") # 切换到transfer pump位置(0号位) - print(f"\n切换到pump位置: {valve.set_to_pump_position()}") - print(f"当前状态: {valve}") + print(f"\n🚰 切换到pump位置: {valve.set_to_pump_position()}") + print(f"📍 当前状态: {valve}") # 切换到试剂瓶2(2号位) - print(f"\n切换到2号位: {valve.set_to_port(2)}") - print(f"当前状态: {valve}") + print(f"\n🔌 切换到2号位: {valve.set_to_port(2)}") + print(f"📍 当前状态: {valve}") # 显示所有可用位置 - print(f"\n可用位置: {valve.get_available_positions()}") - print(f"端口映射: {valve.get_available_ports()}") + print(f"\n📋 可用位置: {valve.get_available_positions()}") + print(f"🗺️ 端口映射: {valve.get_available_ports()}") # 获取详细信息 - print(f"\n详细信息: {valve.get_info()}") \ No newline at end of file + print(f"\n📊 详细信息: {valve.get_info()}") + + # 测试切换功能 + print(f"\n🔄 智能切换测试:") + print(f"当前位置: {valve._current_position}") + print(f"切换结果: {valve.switch_between_pump_and_port(3)}") + print(f"新位置: {valve._current_position}") + + # 重置测试 + print(f"\n🔄 重置测试: {valve.reset()}") + print(f"📍 重置后状态: {valve}") \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_rotavap.py b/unilabos/devices/virtual/virtual_rotavap.py index bfd64947e..dd1cca43d 100644 --- a/unilabos/devices/virtual/virtual_rotavap.py +++ b/unilabos/devices/virtual/virtual_rotavap.py @@ -4,11 +4,11 @@ from typing import Dict, Any, Optional def debug_print(message): - """调试输出""" - print(f"[ROTAVAP] {message}", flush=True) + """调试输出 🔍""" + print(f"🌪️ [ROTAVAP] {message}", flush=True) class VirtualRotavap: - """Virtual rotary evaporator device - 简化版,只保留核心功能""" + """Virtual rotary evaporator device - 简化版,只保留核心功能 🌪️""" def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs): # 处理可能的不同调用方式 @@ -35,13 +35,16 @@ def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, A if key not in skip_keys and not hasattr(self, key): setattr(self, key, value) + print(f"🌪️ === 虚拟旋转蒸发仪 {self.device_id} 已创建 === ✨") + print(f"🔥 温度范围: 10°C ~ {self._max_temp}°C | 🌀 转速范围: 10 ~ {self._max_rotation_speed} RPM") + async def initialize(self) -> bool: - """Initialize virtual rotary evaporator""" - self.logger.info(f"Initializing virtual rotary evaporator {self.device_id}") + """Initialize virtual rotary evaporator 🚀""" + self.logger.info(f"🔧 初始化虚拟旋转蒸发仪 {self.device_id} ✨") # 只保留核心状态 self.data.update({ - "status": "Idle", + "status": "🏠 待机中", "rotavap_state": "Ready", # Ready, Evaporating, Completed, Error "current_temp": 25.0, "target_temp": 25.0, @@ -50,22 +53,27 @@ async def initialize(self) -> bool: "evaporated_volume": 0.0, "progress": 0.0, "remaining_time": 0.0, - "message": "Ready for evaporation" + "message": "🌪️ Ready for evaporation" }) + + self.logger.info(f"✅ 旋转蒸发仪 {self.device_id} 初始化完成 🌪️") + self.logger.info(f"📊 设备规格: 温度范围 10°C ~ {self._max_temp}°C | 转速范围 10 ~ {self._max_rotation_speed} RPM") return True async def cleanup(self) -> bool: - """Cleanup virtual rotary evaporator""" - self.logger.info(f"Cleaning up virtual rotary evaporator {self.device_id}") + """Cleanup virtual rotary evaporator 🧹""" + self.logger.info(f"🧹 清理虚拟旋转蒸发仪 {self.device_id} 🔚") self.data.update({ - "status": "Offline", + "status": "💤 离线", "rotavap_state": "Offline", "current_temp": 25.0, "rotation_speed": 0.0, "vacuum_pressure": 1.0, - "message": "System offline" + "message": "💤 System offline" }) + + self.logger.info(f"✅ 旋转蒸发仪 {self.device_id} 清理完成 💤") return True async def evaporate( @@ -78,37 +86,44 @@ async def evaporate( solvent: str = "", **kwargs ) -> bool: - """Execute evaporate action - 简化版""" + """Execute evaporate action - 简化版 🌪️""" # 🔧 简化处理:如果vessel就是设备自己,直接操作 if vessel == self.device_id: - debug_print(f"在设备 {self.device_id} 上直接执行蒸发操作") + debug_print(f"🎯 在设备 {self.device_id} 上直接执行蒸发操作") actual_vessel = self.device_id else: actual_vessel = vessel # 参数预处理 if solvent: - self.logger.info(f"识别到溶剂: {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"💧 水系溶剂:调整参数 → 温度 {temp}°C, 压力 {pressure} bar") elif any(s in solvent_lower for s in ['ethanol', 'methanol', 'acetone']): temp = min(temp, 50.0) pressure = min(pressure, 0.05) - self.logger.info("易挥发溶剂:调整参数") + self.logger.info(f"⚡ 易挥发溶剂:调整参数 → 温度 {temp}°C, 压力 {pressure} bar") - self.logger.info(f"Evaporate: vessel={actual_vessel}, pressure={pressure} bar, temp={temp}°C, time={time}s, rotation={stir_speed} RPM, solvent={solvent}") + self.logger.info(f"🌪️ 开始蒸发操作: {actual_vessel}") + self.logger.info(f" 🥽 容器: {actual_vessel}") + self.logger.info(f" 🌡️ 温度: {temp}°C") + self.logger.info(f" 💨 真空度: {pressure} bar") + self.logger.info(f" ⏰ 时间: {time}s") + self.logger.info(f" 🌀 转速: {stir_speed} RPM") + if solvent: + self.logger.info(f" 🧪 溶剂: {solvent}") # 验证参数 if temp > self._max_temp or temp < 10.0: - error_msg = f"温度 {temp}°C 超出范围 (10-{self._max_temp}°C)" - self.logger.error(error_msg) + error_msg = f"🌡️ 温度 {temp}°C 超出范围 (10-{self._max_temp}°C) ⚠️" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"Error: {error_msg}", + "status": f"❌ 错误: 温度超出范围", "rotavap_state": "Error", "current_temp": 25.0, "progress": 0.0, @@ -118,10 +133,10 @@ async def evaporate( return False if stir_speed > self._max_rotation_speed or stir_speed < 10.0: - error_msg = f"旋转速度 {stir_speed} RPM 超出范围 (10-{self._max_rotation_speed} RPM)" - self.logger.error(error_msg) + error_msg = f"🌀 旋转速度 {stir_speed} RPM 超出范围 (10-{self._max_rotation_speed} RPM) ⚠️" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"Error: {error_msg}", + "status": f"❌ 错误: 转速超出范围", "rotavap_state": "Error", "current_temp": 25.0, "progress": 0.0, @@ -131,10 +146,10 @@ async def evaporate( return False if pressure < 0.01 or pressure > 1.0: - error_msg = f"真空度 {pressure} bar 超出范围 (0.01-1.0 bar)" - self.logger.error(error_msg) + error_msg = f"💨 真空度 {pressure} bar 超出范围 (0.01-1.0 bar) ⚠️" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"Error: {error_msg}", + "status": f"❌ 错误: 压力超出范围", "rotavap_state": "Error", "current_temp": 25.0, "progress": 0.0, @@ -144,8 +159,10 @@ async def evaporate( return False # 开始蒸发 + self.logger.info(f"🚀 启动蒸发程序! 预计用时 {time/60:.1f}分钟 ⏱️") + self.data.update({ - "status": f"蒸发中: {actual_vessel}", + "status": f"🌪️ 蒸发中: {actual_vessel}", "rotavap_state": "Evaporating", "current_temp": temp, "target_temp": temp, @@ -154,13 +171,14 @@ async def evaporate( "remaining_time": time, "progress": 0.0, "evaporated_volume": 0.0, - "message": f"Evaporating {actual_vessel} at {temp}°C, {pressure} bar, {stir_speed} RPM" + "message": f"🌪️ Evaporating {actual_vessel} at {temp}°C, {pressure} bar, {stir_speed} RPM" }) try: # 蒸发过程 - 实时更新进度 start_time = time_module.time() total_time = time + last_logged_progress = 0 while True: current_time = time_module.time() @@ -168,19 +186,31 @@ async def evaporate( remaining = max(0, total_time - elapsed) progress = min(100.0, (elapsed / total_time) * 100) - # 模拟蒸发体积 - evaporated_vol = progress * 0.8 # 假设最多蒸发80mL + # 模拟蒸发体积 - 根据溶剂类型调整 + if solvent and any(s in solvent.lower() for s in ['water', 'aqueous']): + evaporated_vol = progress * 0.6 # 水系溶剂蒸发慢 + elif solvent and any(s in solvent.lower() for s in ['ethanol', 'methanol', 'acetone']): + evaporated_vol = progress * 1.0 # 易挥发溶剂蒸发快 + else: + evaporated_vol = progress * 0.8 # 默认蒸发量 # 🔧 更新状态 - 确保包含所有必需字段 + status_msg = f"🌪️ 蒸发中: {actual_vessel} | 🌡️ {temp}°C | 💨 {pressure} bar | 🌀 {stir_speed} RPM | 📊 {progress:.1f}% | ⏰ 剩余: {remaining:.0f}s" + self.data.update({ "remaining_time": remaining, - "progress": progress, # 确保这个字段存在 - "evaporated_volume": evaporated_vol, # 确保这个字段存在 - "current_temp": temp, # 确保这个字段存在 - "status": f"蒸发中: {actual_vessel} | {temp}°C | {pressure} bar | {progress:.1f}% | 剩余: {remaining:.0f}s", - "message": f"Evaporating: {progress:.1f}% complete, {remaining:.0f}s remaining" + "progress": progress, + "evaporated_volume": evaporated_vol, + "current_temp": temp, + "status": status_msg, + "message": f"🌪️ Evaporating: {progress:.1f}% complete, 💧 {evaporated_vol:.1f}mL evaporated, ⏰ {remaining:.0f}s remaining" }) + # 进度日志(每25%打印一次) + if progress >= 25 and int(progress) % 25 == 0 and int(progress) != last_logged_progress: + self.logger.info(f"📊 蒸发进度: {progress:.0f}% | 💧 已蒸发: {evaporated_vol:.1f}mL | ⏰ 剩余: {remaining:.0f}s ✨") + last_logged_progress = int(progress) + # 时间到了,退出循环 if remaining <= 0: break @@ -189,42 +219,59 @@ async def evaporate( await asyncio.sleep(1.0) # 蒸发完成 - final_evaporated = 80.0 + if solvent and any(s in solvent.lower() for s in ['water', 'aqueous']): + final_evaporated = 60.0 # 水系溶剂 + elif solvent and any(s in solvent.lower() for s in ['ethanol', 'methanol', 'acetone']): + final_evaporated = 100.0 # 易挥发溶剂 + else: + final_evaporated = 80.0 # 默认 + self.data.update({ - "status": f"蒸发完成: {actual_vessel} | 蒸发量: {final_evaporated:.1f}mL", + "status": f"✅ 蒸发完成: {actual_vessel} | 💧 蒸发量: {final_evaporated:.1f}mL", "rotavap_state": "Completed", "evaporated_volume": final_evaporated, "progress": 100.0, - "current_temp": temp, # 保持温度信息 + "current_temp": temp, "remaining_time": 0.0, - "rotation_speed": 0.0, # 停止旋转 - "vacuum_pressure": 1.0, # 恢复大气压 - "message": f"Evaporation completed: {final_evaporated}mL evaporated from {actual_vessel}" + "rotation_speed": 0.0, + "vacuum_pressure": 1.0, + "message": f"✅ Evaporation completed: {final_evaporated}mL evaporated from {actual_vessel}" }) - self.logger.info(f"Evaporation completed: {final_evaporated}mL evaporated from {actual_vessel}") + self.logger.info(f"🎉 蒸发操作完成! ✨") + self.logger.info(f"📊 蒸发结果:") + self.logger.info(f" 🥽 容器: {actual_vessel}") + self.logger.info(f" 💧 蒸发量: {final_evaporated:.1f}mL") + self.logger.info(f" 🌡️ 蒸发温度: {temp}°C") + self.logger.info(f" 💨 真空度: {pressure} bar") + self.logger.info(f" 🌀 旋转速度: {stir_speed} RPM") + self.logger.info(f" ⏱️ 总用时: {total_time:.0f}s") + if solvent: + self.logger.info(f" 🧪 处理溶剂: {solvent} 🏁") + return True except Exception as e: # 出错处理 - self.logger.error(f"Error during evaporation: {str(e)}") + error_msg = f"蒸发过程中发生错误: {str(e)} 💥" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"蒸发错误: {str(e)}", + "status": f"❌ 蒸发错误: {str(e)}", "rotavap_state": "Error", "current_temp": 25.0, "progress": 0.0, "evaporated_volume": 0.0, "rotation_speed": 0.0, "vacuum_pressure": 1.0, - "message": f"Evaporation failed: {str(e)}" + "message": f"❌ Evaporation failed: {str(e)}" }) return False # === 核心状态属性 === @property def status(self) -> str: - return self.data.get("status", "Unknown") + return self.data.get("status", "❓ Unknown") @property def rotavap_state(self) -> str: diff --git a/unilabos/devices/virtual/virtual_solid_dispenser.py b/unilabos/devices/virtual/virtual_solid_dispenser.py index 48ad9f560..439c34837 100644 --- a/unilabos/devices/virtual/virtual_solid_dispenser.py +++ b/unilabos/devices/virtual/virtual_solid_dispenser.py @@ -5,12 +5,12 @@ class VirtualSolidDispenser: """ - 虚拟固体粉末加样器 - 用于处理 Add Protocol 中的固体试剂添加 + 虚拟固体粉末加样器 - 用于处理 Add Protocol 中的固体试剂添加 ⚗️ 特点: - - 高兼容性:缺少参数不报错 - - 智能识别:自动查找固体试剂瓶 - - 简单反馈:成功/失败 + 消息 + - 高兼容性:缺少参数不报错 ✅ + - 智能识别:自动查找固体试剂瓶 🔍 + - 简单反馈:成功/失败 + 消息 📊 """ def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): @@ -29,26 +29,30 @@ def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwarg self.logger = logging.getLogger(f"VirtualSolidDispenser.{self.device_id}") - print(f"=== VirtualSolidDispenser {self.device_id} 创建成功! ===") - print(f"=== 最大容量: {self.max_capacity}g, 精度: {self.precision}g ===") + print(f"⚗️ === 虚拟固体分配器 {self.device_id} 创建成功! === ✨") + print(f"📊 设备规格: 最大容量 {self.max_capacity}g | 精度 {self.precision}g 🎯") async def initialize(self) -> bool: - """初始化固体加样器""" - self.logger.info(f"初始化固体加样器 {self.device_id}") + """初始化固体加样器 🚀""" + self.logger.info(f"🔧 初始化固体分配器 {self.device_id} ✨") self._status = "Ready" self._current_reagent = "" self._dispensed_amount = 0.0 + + self.logger.info(f"✅ 固体分配器 {self.device_id} 初始化完成 ⚗️") return True async def cleanup(self) -> bool: - """清理固体加样器""" - self.logger.info(f"清理固体加样器 {self.device_id}") + """清理固体加样器 🧹""" + self.logger.info(f"🧹 清理固体分配器 {self.device_id} 🔚") self._status = "Idle" + + self.logger.info(f"✅ 固体分配器 {self.device_id} 清理完成 💤") return True def parse_mass_string(self, mass_str: str) -> float: """ - 解析质量字符串为数值 (g) + 解析质量字符串为数值 (g) ⚖️ 支持格式: "2.9 g", "19.3g", "4.5 mg", "1.2 kg" 等 """ @@ -63,6 +67,7 @@ def parse_mass_string(self, mass_str: str) -> float: match = re.search(pattern, mass_clean) if not match: + self.logger.debug(f"🔍 无法解析质量字符串: {mass_str}") return 0.0 try: @@ -87,15 +92,18 @@ def parse_mass_string(self, mass_str: str) -> float: } multiplier = unit_multipliers.get(unit, 1.0) - return value * multiplier + result = value * multiplier + + self.logger.debug(f"⚖️ 质量解析: {mass_str} → {result:.6f}g (原值: {value} {unit})") + return result except (ValueError, TypeError): - self.logger.warning(f"无法解析质量字符串: {mass_str}") + self.logger.warning(f"⚠️ 无法解析质量字符串: {mass_str}") return 0.0 def parse_mol_string(self, mol_str: str) -> float: """ - 解析摩尔数字符串为数值 (mol) + 解析摩尔数字符串为数值 (mol) 🧮 支持格式: "0.12 mol", "16.2 mmol", "25.2mmol" 等 """ @@ -110,6 +118,7 @@ def parse_mol_string(self, mol_str: str) -> float: match = re.search(pattern, mol_clean) if not match: + self.logger.debug(f"🔍 无法解析摩尔数字符串: {mol_str}") return 0.0 try: @@ -118,21 +127,25 @@ def parse_mol_string(self, mol_str: str) -> float: # 单位转换为 mol if unit == 'mmol': - return value * 0.001 + result = value * 0.001 else: # mol - return value + result = value + + self.logger.debug(f"🧮 摩尔数解析: {mol_str} → {result:.6f}mol (原值: {value} {unit})") + return result except (ValueError, TypeError): - self.logger.warning(f"无法解析摩尔数字符串: {mol_str}") + self.logger.warning(f"⚠️ 无法解析摩尔数字符串: {mol_str}") return 0.0 def find_solid_reagent_bottle(self, reagent_name: str) -> str: """ - 查找固体试剂瓶 + 查找固体试剂瓶 🔍 这是一个简化版本,实际使用时应该连接到系统的设备图 """ if not reagent_name: + self.logger.debug(f"🔍 未指定试剂名称,使用默认瓶") return "unknown_solid_bottle" # 可能的固体试剂瓶命名模式 @@ -146,7 +159,9 @@ def find_solid_reagent_bottle(self, reagent_name: str) -> str: ] # 这里简化处理,实际应该查询设备图 - return possible_names[0] + selected_bottle = possible_names[0] + self.logger.debug(f"🔍 为试剂 {reagent_name} 选择试剂瓶: {selected_bottle}") + return selected_bottle async def add_solid( self, @@ -158,7 +173,7 @@ async def add_solid( **kwargs # 兼容额外参数 ) -> Dict[str, Any]: """ - 添加固体试剂的主要方法 + 添加固体试剂的主要方法 ⚗️ Args: vessel: 目标容器 @@ -172,22 +187,24 @@ async def add_solid( Dict: 操作结果 """ try: - self.logger.info(f"=== 开始固体加样操作 ===") - self.logger.info(f"目标容器: {vessel}") - self.logger.info(f"试剂: {reagent}") - self.logger.info(f"质量: {mass}") - self.logger.info(f"摩尔数: {mol}") - self.logger.info(f"目的: {purpose}") + self.logger.info(f"⚗️ === 开始固体加样操作 === ✨") + self.logger.info(f" 🥽 目标容器: {vessel}") + self.logger.info(f" 🧪 试剂: {reagent}") + self.logger.info(f" ⚖️ 质量: {mass}") + self.logger.info(f" 🧮 摩尔数: {mol}") + self.logger.info(f" 📝 目的: {purpose}") # 参数验证 - 宽松处理 if not vessel: vessel = "main_reactor" # 默认容器 - self.logger.warning(f"未指定容器,使用默认容器: {vessel}") + self.logger.warning(f"⚠️ 未指定容器,使用默认容器: {vessel} 🏠") if not reagent: + error_msg = "❌ 错误: 必须指定试剂名称" + self.logger.error(error_msg) return { "success": False, - "message": "错误: 必须指定试剂名称", + "message": error_msg, "return_info": "missing_reagent" } @@ -195,36 +212,41 @@ async def add_solid( mass_value = self.parse_mass_string(mass) mol_value = self.parse_mol_string(mol) - self.logger.info(f"解析后 - 质量: {mass_value}g, 摩尔数: {mol_value}mol") + self.logger.info(f"📊 解析结果 - 质量: {mass_value:.6f}g | 摩尔数: {mol_value:.6f}mol") # 确定实际加样量 if mass_value > 0: actual_amount = mass_value amount_unit = "g" - self.logger.info(f"按质量加样: {actual_amount} {amount_unit}") + amount_emoji = "⚖️" + self.logger.info(f"⚖️ 按质量加样: {actual_amount:.6f} {amount_unit}") elif mol_value > 0: # 简化处理:假设分子量为100 g/mol assumed_mw = 100.0 actual_amount = mol_value * assumed_mw amount_unit = "g (from mol)" - self.logger.info(f"按摩尔数加样: {mol_value} mol → {actual_amount} g (假设分子量 {assumed_mw})") + amount_emoji = "🧮" + self.logger.info(f"🧮 按摩尔数加样: {mol_value:.6f} mol → {actual_amount:.6f} g (假设分子量 {assumed_mw})") else: # 没有指定量,使用默认值 actual_amount = 1.0 amount_unit = "g (default)" - self.logger.warning(f"未指定质量或摩尔数,使用默认值: {actual_amount} {amount_unit}") + amount_emoji = "🎯" + self.logger.warning(f"⚠️ 未指定质量或摩尔数,使用默认值: {actual_amount} {amount_unit} 🎯") # 检查容量限制 if actual_amount > self.max_capacity: + error_msg = f"❌ 错误: 请求量 {actual_amount:.3f}g 超过最大容量 {self.max_capacity}g" + self.logger.error(error_msg) return { "success": False, - "message": f"错误: 请求量 {actual_amount}g 超过最大容量 {self.max_capacity}g", + "message": error_msg, "return_info": "exceeds_capacity" } # 查找试剂瓶 reagent_bottle = self.find_solid_reagent_bottle(reagent) - self.logger.info(f"使用试剂瓶: {reagent_bottle}") + self.logger.info(f"🔍 使用试剂瓶: {reagent_bottle}") # 模拟加样过程 self._status = "Dispensing" @@ -233,8 +255,17 @@ async def add_solid( # 计算操作时间 (基于质量) operation_time = max(0.5, actual_amount * 0.1) # 每克0.1秒,最少0.5秒 - self.logger.info(f"开始加样,预计时间: {operation_time:.1f}秒") - await asyncio.sleep(operation_time) + self.logger.info(f"🚀 开始加样,预计时间: {operation_time:.1f}秒 ⏱️") + + # 显示进度的模拟 + steps = max(3, int(operation_time)) + step_time = operation_time / steps + + for i in range(steps): + progress = (i + 1) / steps * 100 + await asyncio.sleep(step_time) + if i % 2 == 0: # 每隔一步显示进度 + self.logger.debug(f"📊 加样进度: {progress:.0f}% | {amount_emoji} 正在分配 {reagent}...") # 更新状态 self._dispensed_amount = actual_amount @@ -242,19 +273,26 @@ async def add_solid( self._status = "Ready" # 成功结果 - success_message = f"成功添加 {reagent} {actual_amount:.3f} {amount_unit} 到 {vessel}" + success_message = f"✅ 成功添加 {reagent} {actual_amount:.6f} {amount_unit} 到 {vessel}" - self.logger.info(f"=== 固体加样完成 ===") - self.logger.info(success_message) + self.logger.info(f"🎉 === 固体加样完成 === ✨") + self.logger.info(f"📊 操作结果:") + self.logger.info(f" ✅ {success_message}") + self.logger.info(f" 🧪 试剂瓶: {reagent_bottle}") + self.logger.info(f" ⏱️ 用时: {operation_time:.1f}秒") + self.logger.info(f" 🎯 总操作次数: {self._total_operations} 🏁") return { "success": True, "message": success_message, - "return_info": f"dispensed_{actual_amount:.3f}g" + "return_info": f"dispensed_{actual_amount:.6f}g", + "dispensed_amount": actual_amount, + "reagent": reagent, + "vessel": vessel } except Exception as e: - error_message = f"固体加样失败: {str(e)}" + error_message = f"❌ 固体加样失败: {str(e)} 💥" self.logger.error(error_message) self._status = "Error" @@ -282,8 +320,8 @@ def total_operations(self) -> int: return self._total_operations def get_device_info(self) -> Dict[str, Any]: - """获取设备状态信息""" - return { + """获取设备状态信息 📊""" + info = { "device_id": self.device_id, "status": self._status, "current_reagent": self._current_reagent, @@ -292,43 +330,59 @@ def get_device_info(self) -> Dict[str, Any]: "max_capacity": self.max_capacity, "precision": self.precision } + + self.logger.debug(f"📊 设备信息: 状态={self._status}, 试剂={self._current_reagent}, 加样量={self._dispensed_amount:.6f}g") + return info def __str__(self): - return f"VirtualSolidDispenser({self.device_id}: {self._status}, 最后加样 {self._dispensed_amount:.3f}g)" + status_emoji = "✅" if self._status == "Ready" else "🔄" if self._status == "Dispensing" else "❌" if self._status == "Error" else "🏠" + return f"⚗️ VirtualSolidDispenser({status_emoji} {self.device_id}: {self._status}, 最后加样 {self._dispensed_amount:.3f}g)" # 测试函数 async def test_solid_dispenser(): - """测试固体加样器""" - print("=== 固体加样器测试 ===") + """测试固体加样器 🧪""" + print("⚗️ === 固体加样器测试开始 === 🧪") dispenser = VirtualSolidDispenser("test_dispenser") await dispenser.initialize() # 测试1: 按质量加样 + print(f"\n🧪 测试1: 按质量加样...") result1 = await dispenser.add_solid( vessel="main_reactor", reagent="magnesium", mass="2.9 g" ) - print(f"测试1结果: {result1}") + print(f"📊 测试1结果: {result1}") # 测试2: 按摩尔数加样 + print(f"\n🧮 测试2: 按摩尔数加样...") result2 = await dispenser.add_solid( vessel="main_reactor", reagent="sodium_nitrite", mol="0.28 mol" ) - print(f"测试2结果: {result2}") + print(f"📊 测试2结果: {result2}") # 测试3: 缺少参数 + print(f"\n⚠️ 测试3: 缺少参数测试...") result3 = await dispenser.add_solid( reagent="test_compound" ) - print(f"测试3结果: {result3}") + print(f"📊 测试3结果: {result3}") + + # 测试4: 超容量测试 + print(f"\n❌ 测试4: 超容量测试...") + result4 = await dispenser.add_solid( + vessel="main_reactor", + reagent="heavy_compound", + mass="150 g" # 超过100g限制 + ) + print(f"📊 测试4结果: {result4}") - print(f"设备信息: {dispenser.get_device_info()}") - print("=== 测试完成 ===") + print(f"\n📊 最终设备信息: {dispenser.get_device_info()}") + print(f"✅ === 测试完成 === 🎉") if __name__ == "__main__": diff --git a/unilabos/devices/virtual/virtual_stirrer.py b/unilabos/devices/virtual/virtual_stirrer.py index 874f997a6..2b9058be5 100644 --- a/unilabos/devices/virtual/virtual_stirrer.py +++ b/unilabos/devices/virtual/virtual_stirrer.py @@ -4,7 +4,7 @@ from typing import Dict, Any class VirtualStirrer: - """Virtual stirrer device for StirProtocol testing - 功能完整版""" + """Virtual stirrer device for StirProtocol testing - 功能完整版 🌪️""" def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): # 处理可能的不同调用方式 @@ -30,45 +30,69 @@ def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwarg for key, value in kwargs.items(): if key not in skip_keys and not hasattr(self, key): setattr(self, key, value) + + print(f"🌪️ === 虚拟搅拌器 {self.device_id} 已创建 === ✨") + print(f"🔧 速度范围: {self._min_speed} ~ {self._max_speed} RPM | 📱 端口: {self.port}") async def initialize(self) -> bool: - """Initialize virtual stirrer""" - self.logger.info(f"Initializing virtual stirrer {self.device_id}") + """Initialize virtual stirrer 🚀""" + self.logger.info(f"🔧 初始化虚拟搅拌器 {self.device_id} ✨") # 初始化状态信息 self.data.update({ - "status": "Idle", + "status": "🏠 待机中", "operation_mode": "Idle", # 操作模式: Idle, Stirring, Settling, Completed, Error "current_vessel": "", # 当前搅拌的容器 "current_speed": 0.0, # 当前搅拌速度 "is_stirring": False, # 是否正在搅拌 "remaining_time": 0.0, # 剩余时间 }) + + self.logger.info(f"✅ 搅拌器 {self.device_id} 初始化完成 🌪️") + self.logger.info(f"📊 设备规格: 速度范围 {self._min_speed} ~ {self._max_speed} RPM") return True async def cleanup(self) -> bool: - """Cleanup virtual stirrer""" - self.logger.info(f"Cleaning up virtual stirrer {self.device_id}") + """Cleanup virtual stirrer 🧹""" + self.logger.info(f"🧹 清理虚拟搅拌器 {self.device_id} 🔚") + self.data.update({ - "status": "Offline", + "status": "💤 离线", "operation_mode": "Offline", "current_vessel": "", "current_speed": 0.0, "is_stirring": False, "remaining_time": 0.0, }) + + self.logger.info(f"✅ 搅拌器 {self.device_id} 清理完成 💤") return True - async def stir(self, stir_time: float, stir_speed: float, settling_time: float) -> bool: - """Execute stir action - 定时搅拌 + 沉降""" - self.logger.info(f"Stir: speed={stir_speed} RPM, time={stir_time}s, settling={settling_time}s") + async def stir(self, stir_time: float, stir_speed: float, settling_time: float, **kwargs) -> bool: + """Execute stir action - 定时搅拌 + 沉降 🌪️""" + + # 🔧 类型转换 - 确保所有参数都是数字类型 + try: + stir_time = float(stir_time) + stir_speed = float(stir_speed) + settling_time = float(settling_time) + except (ValueError, TypeError) as e: + error_msg = f"参数类型转换失败: stir_time={stir_time}, stir_speed={stir_speed}, settling_time={settling_time}, error={e}" + self.logger.error(f"❌ {error_msg}") + self.data.update({ + "status": f"❌ 错误: {error_msg}", + "operation_mode": "Error" + }) + return False + + self.logger.info(f"🌪️ 开始搅拌操作: 速度 {stir_speed} RPM | 时间 {stir_time}s | 沉降 {settling_time}s") # 验证参数 if stir_speed > self._max_speed or stir_speed < self._min_speed: - error_msg = f"搅拌速度 {stir_speed} RPM 超出范围 ({self._min_speed} - {self._max_speed} RPM)" - self.logger.error(error_msg) + error_msg = f"🌪️ 搅拌速度 {stir_speed} RPM 超出范围 ({self._min_speed} - {self._max_speed} RPM) ⚠️" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"Error: {error_msg}", + "status": f"❌ 错误: 速度超出范围", "operation_mode": "Error" }) return False @@ -77,8 +101,10 @@ async def stir(self, stir_time: float, stir_speed: float, settling_time: float) start_time = time_module.time() total_stir_time = stir_time + self.logger.info(f"🚀 开始搅拌阶段: {stir_speed} RPM × {total_stir_time}s ⏱️") + self.data.update({ - "status": f"搅拌中: {stir_speed} RPM | 剩余: {total_stir_time:.0f}s", + "status": f"🌪️ 搅拌中: {stir_speed} RPM | ⏰ 剩余: {total_stir_time:.0f}s", "operation_mode": "Stirring", "current_speed": stir_speed, "is_stirring": True, @@ -86,30 +112,41 @@ async def stir(self, stir_time: float, stir_speed: float, settling_time: float) }) # 搅拌过程 - 实时更新剩余时间 + last_logged_time = 0 while True: current_time = time_module.time() elapsed = current_time - start_time remaining = max(0, total_stir_time - elapsed) + progress = (elapsed / total_stir_time) * 100 if total_stir_time > 0 else 100 # 更新状态 self.data.update({ "remaining_time": remaining, - "status": f"搅拌中: {stir_speed} RPM | 剩余: {remaining:.0f}s" + "status": f"🌪️ 搅拌中: {stir_speed} RPM | ⏰ 剩余: {remaining:.0f}s" }) + # 进度日志(每25%打印一次) + if progress >= 25 and int(progress) % 25 == 0 and int(progress) != last_logged_time: + self.logger.info(f"📊 搅拌进度: {progress:.0f}% | 🌪️ {stir_speed} RPM | ⏰ 剩余: {remaining:.0f}s ✨") + last_logged_time = int(progress) + # 搅拌时间到了 if remaining <= 0: break await asyncio.sleep(1.0) + self.logger.info(f"✅ 搅拌阶段完成! 🌪️ {stir_speed} RPM × {stir_time}s") + # === 第二阶段:沉降(如果需要)=== if settling_time > 0: start_settling_time = time_module.time() total_settling_time = settling_time + self.logger.info(f"🛑 开始沉降阶段: 停止搅拌 × {total_settling_time}s ⏱️") + self.data.update({ - "status": f"沉降中: 停止搅拌 | 剩余: {total_settling_time:.0f}s", + "status": f"🛑 沉降中: 停止搅拌 | ⏰ 剩余: {total_settling_time:.0f}s", "operation_mode": "Settling", "current_speed": 0.0, "is_stirring": False, @@ -117,52 +154,87 @@ async def stir(self, stir_time: float, stir_speed: float, settling_time: float) }) # 沉降过程 - 实时更新剩余时间 + last_logged_settling = 0 while True: current_time = time_module.time() elapsed = current_time - start_settling_time remaining = max(0, total_settling_time - elapsed) + progress = (elapsed / total_settling_time) * 100 if total_settling_time > 0 else 100 # 更新状态 self.data.update({ "remaining_time": remaining, - "status": f"沉降中: 停止搅拌 | 剩余: {remaining:.0f}s" + "status": f"🛑 沉降中: 停止搅拌 | ⏰ 剩余: {remaining:.0f}s" }) + # 进度日志(每25%打印一次) + if progress >= 25 and int(progress) % 25 == 0 and int(progress) != last_logged_settling: + self.logger.info(f"📊 沉降进度: {progress:.0f}% | 🛑 静置中 | ⏰ 剩余: {remaining:.0f}s ✨") + last_logged_settling = int(progress) + # 沉降时间到了 if remaining <= 0: break await asyncio.sleep(1.0) + + self.logger.info(f"✅ 沉降阶段完成! 🛑 静置 {settling_time}s") # === 操作完成 === - settling_info = f" | 沉降: {settling_time:.0f}s" if settling_time > 0 else "" + settling_info = f" | 🛑 沉降: {settling_time:.0f}s" if settling_time > 0 else "" + self.data.update({ - "status": f"完成: 搅拌 {stir_speed} RPM, {stir_time:.0f}s{settling_info}", + "status": f"✅ 完成: 🌪️ 搅拌 {stir_speed} RPM × {stir_time:.0f}s{settling_info}", "operation_mode": "Completed", "current_speed": 0.0, "is_stirring": False, "remaining_time": 0.0, }) - self.logger.info(f"Stir completed: {stir_speed} RPM for {stir_time}s + settling {settling_time}s") + self.logger.info(f"🎉 搅拌操作完成! ✨") + self.logger.info(f"📊 操作总结:") + self.logger.info(f" 🌪️ 搅拌: {stir_speed} RPM × {stir_time}s") + if settling_time > 0: + self.logger.info(f" 🛑 沉降: {settling_time}s") + self.logger.info(f" ⏱️ 总用时: {(stir_time + settling_time):.0f}s 🏁") + return True - async def start_stir(self, vessel: str, stir_speed: float, purpose: str) -> bool: - """Start stir action - 开始持续搅拌""" - self.logger.info(f"StartStir: vessel={vessel}, speed={stir_speed} RPM, purpose={purpose}") + async def start_stir(self, vessel: str, stir_speed: float, purpose: str = "") -> bool: + """Start stir action - 开始持续搅拌 🔄""" + + # 🔧 类型转换 + try: + stir_speed = float(stir_speed) + vessel = str(vessel) + purpose = str(purpose) + except (ValueError, TypeError) as e: + error_msg = f"参数类型转换错误: {str(e)}" + self.logger.error(f"❌ {error_msg}") + self.data.update({ + "status": f"❌ 错误: {error_msg}", + "operation_mode": "Error" + }) + return False + + self.logger.info(f"🔄 启动持续搅拌: {vessel} | 🌪️ {stir_speed} RPM") + if purpose: + self.logger.info(f"📝 搅拌目的: {purpose}") # 验证参数 if stir_speed > self._max_speed or stir_speed < self._min_speed: - error_msg = f"搅拌速度 {stir_speed} RPM 超出范围 ({self._min_speed} - {self._max_speed} RPM)" - self.logger.error(error_msg) + error_msg = f"🌪️ 搅拌速度 {stir_speed} RPM 超出范围 ({self._min_speed} - {self._max_speed} RPM) ⚠️" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"Error: {error_msg}", + "status": f"❌ 错误: 速度超出范围", "operation_mode": "Error" }) return False + purpose_info = f" | 📝 {purpose}" if purpose else "" + self.data.update({ - "status": f"启动: 持续搅拌 {vessel} at {stir_speed} RPM | {purpose}", + "status": f"🔄 启动: 持续搅拌 {vessel} | 🌪️ {stir_speed} RPM{purpose_info}", "operation_mode": "Stirring", "current_vessel": vessel, "current_speed": stir_speed, @@ -170,16 +242,28 @@ async def start_stir(self, vessel: str, stir_speed: float, purpose: str) -> bool "remaining_time": -1.0, # -1 表示持续运行 }) + self.logger.info(f"✅ 持续搅拌已启动! 🌪️ {stir_speed} RPM × ♾️ 🚀") return True async def stop_stir(self, vessel: str) -> bool: - """Stop stir action - 停止搅拌""" - self.logger.info(f"StopStir: vessel={vessel}") + """Stop stir action - 停止搅拌 🛑""" + + # 🔧 类型转换 + try: + vessel = str(vessel) + except (ValueError, TypeError) as e: + error_msg = f"参数类型转换错误: {str(e)}" + self.logger.error(f"❌ {error_msg}") + return False current_speed = self.data.get("current_speed", 0.0) + self.logger.info(f"🛑 停止搅拌: {vessel}") + if current_speed > 0: + self.logger.info(f"🌪️ 之前搅拌速度: {current_speed} RPM") + self.data.update({ - "status": f"已停止: {vessel} 搅拌停止 | 之前速度: {current_speed} RPM", + "status": f"🛑 已停止: {vessel} 搅拌停止 | 之前速度: {current_speed} RPM", "operation_mode": "Stopped", "current_vessel": "", "current_speed": 0.0, @@ -187,12 +271,13 @@ async def stop_stir(self, vessel: str) -> bool: "remaining_time": 0.0, }) + self.logger.info(f"✅ 搅拌器已停止 {vessel} 的搅拌操作 🏁") return True # 状态属性 @property def status(self) -> str: - return self.data.get("status", "Idle") + return self.data.get("status", "🏠 待机中") @property def operation_mode(self) -> str: @@ -212,4 +297,33 @@ def is_stirring(self) -> bool: @property def remaining_time(self) -> float: - return self.data.get("remaining_time", 0.0) \ No newline at end of file + return self.data.get("remaining_time", 0.0) + + @property + def max_speed(self) -> float: + return self._max_speed + + @property + def min_speed(self) -> float: + return self._min_speed + + def get_device_info(self) -> Dict[str, Any]: + """获取设备状态信息 📊""" + info = { + "device_id": self.device_id, + "status": self.status, + "operation_mode": self.operation_mode, + "current_vessel": self.current_vessel, + "current_speed": self.current_speed, + "is_stirring": self.is_stirring, + "remaining_time": self.remaining_time, + "max_speed": self._max_speed, + "min_speed": self._min_speed + } + + self.logger.debug(f"📊 设备信息: 模式={self.operation_mode}, 速度={self.current_speed} RPM, 搅拌={self.is_stirring}") + return info + + def __str__(self): + status_emoji = "✅" if self.operation_mode == "Idle" else "🌪️" if self.operation_mode == "Stirring" else "🛑" if self.operation_mode == "Settling" else "❌" + return f"🌪️ VirtualStirrer({status_emoji} {self.device_id}: {self.operation_mode}, {self.current_speed} RPM)" \ No newline at end of file From ed3b22a7380ae19d7f3d4dceec5fd718ee33cab7 Mon Sep 17 00:00:00 2001 From: KCFeng425 <2100011801@stu.pku.edu.cn> Date: Wed, 16 Jul 2025 10:38:12 +0800 Subject: [PATCH 9/9] =?UTF-8?q?=E8=A1=A5=E5=85=85=E4=BA=86=E5=89=A9?= =?UTF-8?q?=E4=B8=8B=E7=9A=84=E5=87=A0=E4=B8=AAprotocol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- unilabos/app/mq.py | 2 +- unilabos/compile/add_protocol.py | 224 +++- unilabos/compile/adjustph_protocol.py | 313 +++-- unilabos/compile/dissolve_protocol.py | 315 +++-- .../compile/evacuateandrefill_protocol.py | 398 ++++-- unilabos/compile/pump_protocol.py | 1070 ++++++++++++++--- unilabos/compile/separate_protocol.py | 424 +++++-- .../devices/virtual/virtual_multiway_valve.py | 22 +- .../devices/virtual/virtual_transferpump.py | 205 +++- unilabos/ros/nodes/presets/protocol_node.py | 2 +- 10 files changed, 2334 insertions(+), 641 deletions(-) diff --git a/unilabos/app/mq.py b/unilabos/app/mq.py index 061add291..12ec2c0d8 100644 --- a/unilabos/app/mq.py +++ b/unilabos/app/mq.py @@ -166,7 +166,7 @@ def publish_device_status(self, device_status: dict, device_id, property_name): status = {"data": device_status.get(device_id, {}), "device_id": device_id} address = f"labs/{MQConfig.lab_id}/devices/" self.client.publish(address, json.dumps(status), qos=2) - logger.debug(f"Device status published: address: {address}, {status}") + # logger.debug(f"Device status published: address: {address}, {status}") def publish_job_status(self, feedback_data: dict, job_id: str, status: str, return_info: Optional[str] = None): if self.mqtt_disable: diff --git a/unilabos/compile/add_protocol.py b/unilabos/compile/add_protocol.py index b5b017eb0..c46befeff 100644 --- a/unilabos/compile/add_protocol.py +++ b/unilabos/compile/add_protocol.py @@ -22,18 +22,20 @@ def parse_volume_input(volume_input: Union[str, float]) -> float: float: 体积(毫升) """ if isinstance(volume_input, (int, float)): + debug_print(f"📏 体积输入为数值: {volume_input}") return float(volume_input) if not volume_input or not str(volume_input).strip(): + debug_print(f"⚠️ 体积输入为空,返回0.0mL") return 0.0 volume_str = str(volume_input).lower().strip() - debug_print(f"解析体积输入: '{volume_str}'") + debug_print(f"🔍 解析体积输入: '{volume_str}'") # 处理未知体积 if volume_str in ['?', 'unknown', 'tbd', 'to be determined']: default_volume = 10.0 # 默认10mL - debug_print(f"检测到未知体积,使用默认值: {default_volume}mL") + debug_print(f"❓ 检测到未知体积,使用默认值: {default_volume}mL 🎯") return default_volume # 移除空格并提取数字和单位 @@ -43,7 +45,7 @@ def parse_volume_input(volume_input: Union[str, float]) -> float: match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean) if not match: - debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值10mL") + debug_print(f"❌ 无法解析体积: '{volume_str}',使用默认值10mL") return 10.0 value = float(match.group(1)) @@ -52,12 +54,14 @@ def parse_volume_input(volume_input: Union[str, float]) -> float: # 转换为毫升 if unit in ['l', 'liter']: volume = value * 1000.0 # L -> mL + debug_print(f"🔄 体积转换: {value}L → {volume}mL") elif unit in ['μl', 'ul', 'microliter']: volume = value / 1000.0 # μL -> mL + debug_print(f"🔄 体积转换: {value}μL → {volume}mL") else: # ml, milliliter 或默认 volume = value # 已经是mL + debug_print(f"✅ 体积已为mL: {volume}mL") - debug_print(f"体积转换: {value}{unit} → {volume}mL") return volume def parse_mass_input(mass_input: Union[str, float]) -> float: @@ -71,13 +75,15 @@ def parse_mass_input(mass_input: Union[str, float]) -> float: float: 质量(克) """ if isinstance(mass_input, (int, float)): + debug_print(f"⚖️ 质量输入为数值: {mass_input}g") return float(mass_input) if not mass_input or not str(mass_input).strip(): + debug_print(f"⚠️ 质量输入为空,返回0.0g") return 0.0 mass_str = str(mass_input).lower().strip() - debug_print(f"解析质量输入: '{mass_str}'") + debug_print(f"🔍 解析质量输入: '{mass_str}'") # 移除空格并提取数字和单位 mass_clean = re.sub(r'\s+', '', mass_str) @@ -86,7 +92,7 @@ def parse_mass_input(mass_input: Union[str, float]) -> float: match = re.match(r'([0-9]*\.?[0-9]+)\s*(g|mg|kg|gram|milligram|kilogram)?', mass_clean) if not match: - debug_print(f"⚠️ 无法解析质量: '{mass_str}',返回0.0g") + debug_print(f"❌ 无法解析质量: '{mass_str}',返回0.0g") return 0.0 value = float(match.group(1)) @@ -95,12 +101,14 @@ def parse_mass_input(mass_input: Union[str, float]) -> float: # 转换为克 if unit in ['mg', 'milligram']: mass = value / 1000.0 # mg -> g + debug_print(f"🔄 质量转换: {value}mg → {mass}g") elif unit in ['kg', 'kilogram']: mass = value * 1000.0 # kg -> g + debug_print(f"🔄 质量转换: {value}kg → {mass}g") else: # g, gram 或默认 mass = value # 已经是g + debug_print(f"✅ 质量已为g: {mass}g") - debug_print(f"质量转换: {value}{unit} → {mass}g") return mass def parse_time_input(time_input: Union[str, float]) -> float: @@ -114,18 +122,20 @@ def parse_time_input(time_input: Union[str, float]) -> float: float: 时间(秒) """ if isinstance(time_input, (int, float)): + debug_print(f"⏱️ 时间输入为数值: {time_input}秒") return float(time_input) if not time_input or not str(time_input).strip(): + debug_print(f"⚠️ 时间输入为空,返回0秒") return 0.0 time_str = str(time_input).lower().strip() - debug_print(f"解析时间输入: '{time_str}'") + debug_print(f"🔍 解析时间输入: '{time_str}'") # 处理未知时间 if time_str in ['?', 'unknown', 'tbd']: default_time = 60.0 # 默认1分钟 - debug_print(f"检测到未知时间,使用默认值: {default_time}s") + debug_print(f"❓ 检测到未知时间,使用默认值: {default_time}s (1分钟) ⏰") return default_time # 移除空格并提取数字和单位 @@ -135,7 +145,7 @@ def parse_time_input(time_input: Union[str, float]) -> float: match = re.match(r'([0-9]*\.?[0-9]+)\s*(s|sec|second|min|minute|h|hr|hour|d|day)?', time_clean) if not match: - debug_print(f"⚠️ 无法解析时间: '{time_str}',返回0s") + debug_print(f"❌ 无法解析时间: '{time_str}',返回0s") return 0.0 value = float(match.group(1)) @@ -144,21 +154,25 @@ def parse_time_input(time_input: Union[str, float]) -> float: # 转换为秒 if unit in ['min', 'minute']: time_sec = value * 60.0 # min -> s + debug_print(f"🔄 时间转换: {value}分钟 → {time_sec}秒") elif unit in ['h', 'hr', 'hour']: time_sec = value * 3600.0 # h -> s + debug_print(f"🔄 时间转换: {value}小时 → {time_sec}秒") elif unit in ['d', 'day']: time_sec = value * 86400.0 # d -> s + debug_print(f"🔄 时间转换: {value}天 → {time_sec}秒") else: # s, sec, second 或默认 time_sec = value # 已经是s + debug_print(f"✅ 时间已为秒: {time_sec}秒") - debug_print(f"时间转换: {value}{unit} → {time_sec}s") return time_sec def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str: """增强版试剂容器查找,支持固体和液体""" - debug_print(f"查找试剂 '{reagent}' 的容器...") + debug_print(f"🔍 开始查找试剂 '{reagent}' 的容器...") # 🔧 方法1:直接搜索 data.reagent_name 和 config.reagent + debug_print(f"📋 方法1: 搜索reagent字段...") for node in G.nodes(): node_data = G.nodes[node].get('data', {}) node_type = G.nodes[node].get('type', '') @@ -171,16 +185,17 @@ def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str: # 精确匹配 if reagent_name == reagent.lower() or config_reagent == reagent.lower(): - debug_print(f"✅ 通过reagent字段找到容器: {node}") + debug_print(f"✅ 通过reagent字段精确匹配到容器: {node} 🎯") return node # 模糊匹配 if (reagent.lower() in reagent_name and reagent_name) or \ (reagent.lower() in config_reagent and config_reagent): - debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node}") + debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node} 🔍") return node # 🔧 方法2:常见的容器命名规则 + debug_print(f"📋 方法2: 使用命名规则查找...") reagent_clean = reagent.lower().replace(' ', '_').replace('-', '_') possible_names = [ reagent_clean, @@ -197,20 +212,23 @@ def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str: f"reagent_bottle_3" ] + debug_print(f"🔍 尝试的容器名称: {possible_names[:5]}... (共{len(possible_names)}个)") + for name in possible_names: if name in G.nodes(): node_type = G.nodes[name].get('type', '') if node_type == 'container': - debug_print(f"✅ 通过命名规则找到容器: {name}") + debug_print(f"✅ 通过命名规则找到容器: {name} 📝") return name # 🔧 方法3:节点名称模糊匹配 + debug_print(f"📋 方法3: 节点名称模糊匹配...") for node_id in G.nodes(): node_data = G.nodes[node_id] if node_data.get('type') == 'container': # 检查节点名称是否包含试剂名称 if reagent_clean in node_id.lower(): - debug_print(f"✅ 通过节点名称模糊匹配到容器: {node_id}") + debug_print(f"✅ 通过节点名称模糊匹配到容器: {node_id} 🔍") return node_id # 检查液体类型匹配 @@ -220,51 +238,77 @@ def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str: if isinstance(liquid, dict): liquid_type = liquid.get('liquid_type') or liquid.get('name', '') if liquid_type.lower() == reagent.lower(): - debug_print(f"✅ 通过液体类型匹配到容器: {node_id}") + debug_print(f"✅ 通过液体类型匹配到容器: {node_id} 💧") return node_id # 🔧 方法4:使用第一个试剂瓶作为备选 + debug_print(f"📋 方法4: 查找备选试剂瓶...") for node_id in G.nodes(): node_data = G.nodes[node_id] if (node_data.get('type') == 'container' and ('reagent' in node_id.lower() or 'bottle' in node_id.lower())): - debug_print(f"⚠️ 未找到专用容器,使用备选试剂瓶: {node_id}") + debug_print(f"⚠️ 未找到专用容器,使用备选试剂瓶: {node_id} 🔄") return node_id + debug_print(f"❌ 所有方法都失败了,无法找到容器!") raise ValueError(f"找不到试剂 '{reagent}' 对应的容器") def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str: """查找连接到指定容器的搅拌器""" + debug_print(f"🔍 查找连接到容器 '{vessel}' 的搅拌器...") + stirrer_nodes = [] for node in G.nodes(): node_class = G.nodes[node].get('class', '').lower() if 'stirrer' in node_class: stirrer_nodes.append(node) + debug_print(f"📋 发现搅拌器: {node}") + + debug_print(f"📊 共找到 {len(stirrer_nodes)} 个搅拌器") # 查找连接到容器的搅拌器 for stirrer in stirrer_nodes: if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): - debug_print(f"找到连接的搅拌器: {stirrer}") + debug_print(f"✅ 找到连接的搅拌器: {stirrer} 🔗") return stirrer # 返回第一个搅拌器 if stirrer_nodes: - debug_print(f"使用第一个搅拌器: {stirrer_nodes[0]}") + debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个: {stirrer_nodes[0]} 🔄") return stirrer_nodes[0] + debug_print(f"❌ 未找到任何搅拌器") return "" def find_solid_dispenser(G: nx.DiGraph) -> str: """查找固体加样器""" + debug_print(f"🔍 查找固体加样器...") + for node in G.nodes(): node_class = G.nodes[node].get('class', '').lower() if 'solid_dispenser' in node_class or 'dispenser' in node_class: - debug_print(f"找到固体加样器: {node}") + debug_print(f"✅ 找到固体加样器: {node} 🥄") return node - debug_print("⚠️ 未找到固体加样器") + debug_print(f"❌ 未找到固体加样器") return "" +# 🆕 创建进度日志动作 +def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]: + """创建一个动作日志""" + full_message = f"{emoji} {message}" + debug_print(full_message) + logger.info(full_message) + print(f"[ACTION] {full_message}", flush=True) + + return { + "action_name": "wait", + "action_kwargs": { + "time": 0.1, + "log_message": full_message + } + } + def generate_add_protocol( G: nx.DiGraph, vessel: str, @@ -301,51 +345,58 @@ def generate_add_protocol( """ debug_print("=" * 60) - debug_print("开始生成添加试剂协议") - debug_print(f"原始参数:") - debug_print(f" - vessel: '{vessel}'") - debug_print(f" - reagent: '{reagent}'") - debug_print(f" - volume: {volume} (类型: {type(volume)})") - debug_print(f" - mass: {mass} (类型: {type(mass)})") - debug_print(f" - time: {time} (类型: {type(time)})") - debug_print(f" - mol: '{mol}'") - debug_print(f" - event: '{event}'") - debug_print(f" - rate_spec: '{rate_spec}'") + debug_print("🚀 开始生成添加试剂协议") + debug_print(f"📋 原始参数:") + debug_print(f" 🥼 vessel: '{vessel}'") + debug_print(f" 🧪 reagent: '{reagent}'") + debug_print(f" 📏 volume: {volume} (类型: {type(volume)})") + debug_print(f" ⚖️ mass: {mass} (类型: {type(mass)})") + debug_print(f" ⏱️ time: {time} (类型: {type(time)})") + debug_print(f" 🧬 mol: '{mol}'") + debug_print(f" 🎯 event: '{event}'") + debug_print(f" ⚡ rate_spec: '{rate_spec}'") + debug_print(f" 🌪️ stir: {stir}") + debug_print(f" 🔄 stir_speed: {stir_speed} rpm") debug_print("=" * 60) action_sequence = [] # === 参数验证 === - debug_print("步骤1: 参数验证...") + debug_print("🔍 步骤1: 参数验证...") + action_sequence.append(create_action_log(f"开始添加试剂 '{reagent}' 到容器 '{vessel}'", "🎬")) if not vessel: + debug_print("❌ vessel 参数不能为空") raise ValueError("vessel 参数不能为空") if not reagent: + debug_print("❌ reagent 参数不能为空") raise ValueError("reagent 参数不能为空") if vessel not in G.nodes(): + debug_print(f"❌ 容器 '{vessel}' 不存在于系统中") raise ValueError(f"容器 '{vessel}' 不存在于系统中") debug_print("✅ 基本参数验证通过") # === 🔧 关键修复:参数解析 === - debug_print("步骤2: 参数解析...") + debug_print("🔍 步骤2: 参数解析...") + action_sequence.append(create_action_log("正在解析添加参数...", "🔍")) # 解析各种参数为数值 final_volume = parse_volume_input(volume) final_mass = parse_mass_input(mass) final_time = parse_time_input(time) - debug_print(f"解析结果:") - debug_print(f" - 体积: {final_volume}mL") - debug_print(f" - 质量: {final_mass}g") - debug_print(f" - 时间: {final_time}s") - debug_print(f" - 摩尔: '{mol}'") - debug_print(f" - 事件: '{event}'") - debug_print(f" - 速率: '{rate_spec}'") + debug_print(f"📊 解析结果:") + debug_print(f" 📏 体积: {final_volume}mL") + debug_print(f" ⚖️ 质量: {final_mass}g") + debug_print(f" ⏱️ 时间: {final_time}s") + debug_print(f" 🧬 摩尔: '{mol}'") + debug_print(f" 🎯 事件: '{event}'") + debug_print(f" ⚡ 速率: '{rate_spec}'") # === 判断添加类型 === - debug_print("步骤3: 判断添加类型...") + debug_print("🔍 步骤3: 判断添加类型...") # 🔧 修复:现在使用解析后的数值进行比较 is_solid = (final_mass > 0 or (mol and mol.strip() != "")) @@ -357,22 +408,34 @@ def generate_add_protocol( final_volume = 10.0 debug_print("⚠️ 未指定体积或质量,默认为10mL液体") - debug_print(f"添加类型: {'固体' if is_solid else '液体'}") + add_type = "固体" if is_solid else "液体" + add_emoji = "🧂" if is_solid else "💧" + debug_print(f"📋 添加类型: {add_type} {add_emoji}") + + action_sequence.append(create_action_log(f"确定添加类型: {add_type} {add_emoji}", "📋")) # === 执行添加流程 === - debug_print("步骤4: 执行添加流程...") + debug_print("🔍 步骤4: 执行添加流程...") try: if is_solid: # === 固体添加路径 === - debug_print(f"使用固体添加路径") + debug_print(f"🧂 使用固体添加路径") + action_sequence.append(create_action_log("开始固体试剂添加流程", "🧂")) solid_dispenser = find_solid_dispenser(G) if solid_dispenser: + action_sequence.append(create_action_log(f"找到固体加样器: {solid_dispenser}", "🥄")) + # 启动搅拌 if stir: + debug_print("🌪️ 准备启动搅拌...") + action_sequence.append(create_action_log("准备启动搅拌器", "🌪️")) + stirrer_id = find_connected_stirrer(G, vessel) if stirrer_id: + action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {stir_speed} rpm)", "🔄")) + action_sequence.append({ "device_id": stirrer_id, "action_name": "start_stir", @@ -383,6 +446,7 @@ def generate_add_protocol( } }) # 等待搅拌稳定 + action_sequence.append(create_action_log("等待搅拌稳定...", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 3} @@ -399,19 +463,27 @@ def generate_add_protocol( if final_mass > 0: add_kwargs["mass"] = str(final_mass) + action_sequence.append(create_action_log(f"准备添加固体: {final_mass}g", "⚖️")) if mol and mol.strip(): add_kwargs["mol"] = mol + action_sequence.append(create_action_log(f"按摩尔数添加: {mol}", "🧬")) if equiv and equiv.strip(): add_kwargs["equiv"] = equiv + action_sequence.append(create_action_log(f"当量: {equiv}", "🔢")) + action_sequence.append(create_action_log("开始固体加样操作", "🥄")) action_sequence.append({ "device_id": solid_dispenser, "action_name": "add_solid", "action_kwargs": add_kwargs }) + action_sequence.append(create_action_log("固体加样完成", "✅")) + # 添加后等待 if final_time > 0: + wait_minutes = final_time / 60 + action_sequence.append(create_action_log(f"等待反应进行 ({wait_minutes:.1f}分钟)", "⏰")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": final_time} @@ -419,19 +491,28 @@ def generate_add_protocol( debug_print(f"✅ 固体添加完成") else: - debug_print("⚠️ 未找到固体加样器,跳过固体添加") + debug_print("❌ 未找到固体加样器,跳过固体添加") + action_sequence.append(create_action_log("未找到固体加样器,无法添加固体", "❌")) else: # === 液体添加路径 === - debug_print(f"使用液体添加路径") + debug_print(f"💧 使用液体添加路径") + action_sequence.append(create_action_log("开始液体试剂添加流程", "💧")) # 查找试剂容器 + action_sequence.append(create_action_log("正在查找试剂容器...", "🔍")) reagent_vessel = find_reagent_vessel(G, reagent) + action_sequence.append(create_action_log(f"找到试剂容器: {reagent_vessel}", "🧪")) # 启动搅拌 if stir: + debug_print("🌪️ 准备启动搅拌...") + action_sequence.append(create_action_log("准备启动搅拌器", "🌪️")) + stirrer_id = find_connected_stirrer(G, vessel) if stirrer_id: + action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {stir_speed} rpm)", "🔄")) + action_sequence.append({ "device_id": stirrer_id, "action_name": "start_stir", @@ -442,6 +523,7 @@ def generate_add_protocol( } }) # 等待搅拌稳定 + action_sequence.append(create_action_log("等待搅拌稳定...", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 5} @@ -451,18 +533,23 @@ def generate_add_protocol( if final_time > 0: flowrate = final_volume / final_time * 60 # mL/min transfer_flowrate = flowrate + debug_print(f"⚡ 根据时间计算流速: {flowrate:.2f} mL/min") else: if rate_spec == "dropwise": flowrate = 0.5 # 滴加,很慢 transfer_flowrate = 0.2 + debug_print(f"💧 滴加模式,流速: {flowrate} mL/min") elif viscous: flowrate = 1.0 # 粘性液体 transfer_flowrate = 0.3 + debug_print(f"🍯 粘性液体,流速: {flowrate} mL/min") else: flowrate = 2.5 # 正常流速 transfer_flowrate = 0.5 + debug_print(f"⚡ 正常流速: {flowrate} mL/min") - debug_print(f"流速设置: {flowrate} mL/min") + action_sequence.append(create_action_log(f"设置流速: {flowrate:.2f} mL/min", "⚡")) + action_sequence.append(create_action_log(f"开始转移 {final_volume}mL 液体", "🚰")) # 调用pump protocol pump_actions = generate_pump_protocol_with_rinsing( @@ -486,9 +573,11 @@ def generate_add_protocol( ) action_sequence.extend(pump_actions) debug_print(f"✅ 液体转移完成,添加了 {len(pump_actions)} 个动作") + action_sequence.append(create_action_log(f"液体转移完成 ({len(pump_actions)} 个操作)", "✅")) except Exception as e: - debug_print(f"⚠️ 试剂添加失败: {str(e)}") + debug_print(f"❌ 试剂添加失败: {str(e)}") + action_sequence.append(create_action_log(f"试剂添加失败: {str(e)}", "❌")) # 添加错误日志 action_sequence.append({ "device_id": "system", @@ -500,19 +589,28 @@ def generate_add_protocol( # === 最终结果 === debug_print("=" * 60) - debug_print(f"✅ 添加试剂协议生成完成") + debug_print(f"🎉 添加试剂协议生成完成") debug_print(f"📊 总动作数: {len(action_sequence)}") debug_print(f"📋 处理总结:") - debug_print(f" - 试剂: {reagent}") - debug_print(f" - 添加类型: {'固体' if is_solid else '液体'}") - debug_print(f" - 目标容器: {vessel}") + debug_print(f" 🧪 试剂: {reagent}") + debug_print(f" {add_emoji} 添加类型: {add_type}") + debug_print(f" 🥼 目标容器: {vessel}") if is_liquid: - debug_print(f" - 体积: {final_volume}mL") + debug_print(f" 📏 体积: {final_volume}mL") if is_solid: - debug_print(f" - 质量: {final_mass}g") - debug_print(f" - 摩尔: {mol}") + debug_print(f" ⚖️ 质量: {final_mass}g") + debug_print(f" 🧬 摩尔: {mol}") debug_print("=" * 60) + # 添加完成日志 + summary_msg = f"试剂添加协议完成: {reagent} → {vessel}" + if is_liquid: + summary_msg += f" ({final_volume}mL)" + if is_solid: + summary_msg += f" ({final_mass}g)" + + action_sequence.append(create_action_log(summary_msg, "🎉")) + return action_sequence # === 便捷函数 === @@ -520,6 +618,7 @@ def generate_add_protocol( def add_liquid_volume(G: nx.DiGraph, vessel: str, reagent: str, volume: Union[str, float], time: Union[str, float] = 0.0, rate_spec: str = "") -> List[Dict[str, Any]]: """添加指定体积的液体试剂""" + debug_print(f"💧 快速添加液体: {reagent} ({volume}) → {vessel}") return generate_add_protocol( G, vessel, reagent, volume=volume, @@ -530,6 +629,7 @@ def add_liquid_volume(G: nx.DiGraph, vessel: str, reagent: str, volume: Union[st def add_solid_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, float], event: str = "") -> List[Dict[str, Any]]: """添加指定质量的固体试剂""" + debug_print(f"🧂 快速添加固体: {reagent} ({mass}) → {vessel}") return generate_add_protocol( G, vessel, reagent, mass=mass, @@ -539,6 +639,7 @@ def add_solid_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, fl def add_solid_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str, event: str = "") -> List[Dict[str, Any]]: """按摩尔数添加固体试剂""" + debug_print(f"🧬 按摩尔数添加固体: {reagent} ({mol}) → {vessel}") return generate_add_protocol( G, vessel, reagent, mol=mol, @@ -548,6 +649,7 @@ def add_solid_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str, def add_dropwise_liquid(G: nx.DiGraph, vessel: str, reagent: str, volume: Union[str, float], time: Union[str, float] = "20 min", event: str = "") -> List[Dict[str, Any]]: """滴加液体试剂""" + debug_print(f"💧 滴加液体: {reagent} ({volume}) → {vessel} (用时: {time})") return generate_add_protocol( G, vessel, reagent, volume=volume, @@ -559,6 +661,7 @@ def add_dropwise_liquid(G: nx.DiGraph, vessel: str, reagent: str, volume: Union[ def add_portionwise_solid(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, float], time: Union[str, float] = "1 h", event: str = "") -> List[Dict[str, Any]]: """分批添加固体试剂""" + debug_print(f"🧂 分批添加固体: {reagent} ({mass}) → {vessel} (用时: {time})") return generate_add_protocol( G, vessel, reagent, mass=mass, @@ -573,22 +676,25 @@ def test_add_protocol(): print("=== ADD PROTOCOL 增强版测试 ===") # 测试体积解析 + debug_print("🧪 测试体积解析...") volumes = ["2.7 mL", "2.67 mL", "?", 10.0, "1 L", "500 μL"] for vol in volumes: result = parse_volume_input(vol) - print(f"体积解析: {vol} → {result}mL") + print(f"📏 体积解析: {vol} → {result}mL") # 测试质量解析 + debug_print("⚖️ 测试质量解析...") masses = ["19.3 g", "4.5 g", 2.5, "500 mg", "1 kg"] for mass in masses: result = parse_mass_input(mass) - print(f"质量解析: {mass} → {result}g") + print(f"⚖️ 质量解析: {mass} → {result}g") # 测试时间解析 + debug_print("⏱️ 测试时间解析...") times = ["1 h", "20 min", "30 s", 60.0, "?"] for time in times: result = parse_time_input(time) - print(f"时间解析: {time} → {result}s") + print(f"⏱️ 时间解析: {time} → {result}s") print("✅ 测试完成") diff --git a/unilabos/compile/adjustph_protocol.py b/unilabos/compile/adjustph_protocol.py index ce7c1c3f3..d8f1b1b64 100644 --- a/unilabos/compile/adjustph_protocol.py +++ b/unilabos/compile/adjustph_protocol.py @@ -1,7 +1,30 @@ import networkx as nx +import logging from typing import List, Dict, Any from .pump_protocol import generate_pump_protocol_with_rinsing +logger = logging.getLogger(__name__) + +def debug_print(message): + """调试输出""" + print(f"[ADJUST_PH] {message}", flush=True) + logger.info(f"[ADJUST_PH] {message}") + +# 🆕 创建进度日志动作 +def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]: + """创建一个动作日志""" + full_message = f"{emoji} {message}" + debug_print(full_message) + logger.info(full_message) + print(f"[ACTION] {full_message}", flush=True) + + return { + "action_name": "wait", + "action_kwargs": { + "time": 0.1, + "log_message": full_message + } + } def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str: """ @@ -14,7 +37,7 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str: Returns: str: 试剂容器ID """ - print(f"ADJUST_PH: 正在查找试剂 '{reagent}' 的容器...") + debug_print(f"🔍 正在查找试剂 '{reagent}' 的容器...") # 常见酸碱试剂的别名映射 reagent_aliases = { @@ -29,11 +52,16 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str: # 构建搜索名称列表 search_names = [reagent.lower()] + debug_print(f"📋 基础搜索名称: {reagent.lower()}") # 添加别名 for base_name, aliases in reagent_aliases.items(): if reagent.lower() in base_name.lower() or base_name.lower() in reagent.lower(): search_names.extend([alias.lower() for alias in aliases]) + debug_print(f"🔗 添加别名: {aliases}") + break + + debug_print(f"📝 完整搜索列表: {search_names}") # 构建可能的容器名称 possible_names = [] @@ -49,13 +77,17 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str: name_clean ]) + debug_print(f"🎯 可能的容器名称 (前5个): {possible_names[:5]}... (共{len(possible_names)}个)") + # 第一步:通过容器名称匹配 + debug_print(f"📋 方法1: 精确名称匹配...") for vessel_name in possible_names: if vessel_name in G.nodes(): - print(f"ADJUST_PH: 通过名称匹配找到容器: {vessel_name}") + debug_print(f"✅ 通过名称匹配找到容器: {vessel_name} 🎯") return vessel_name # 第二步:通过模糊匹配 + debug_print(f"📋 方法2: 模糊名称匹配...") for node_id in G.nodes(): if G.nodes[node_id].get('type') == 'container': node_name = G.nodes[node_id].get('name', '').lower() @@ -63,10 +95,11 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str: # 检查是否包含任何搜索名称 for search_name in search_names: if search_name in node_id.lower() or search_name in node_name: - print(f"ADJUST_PH: 通过模糊匹配找到容器: {node_id}") + debug_print(f"✅ 通过模糊匹配找到容器: {node_id} 🔍") return node_id # 第三步:通过液体类型匹配 + debug_print(f"📋 方法3: 液体类型匹配...") for node_id in G.nodes(): if G.nodes[node_id].get('type') == 'container': vessel_data = G.nodes[node_id].get('data', {}) @@ -79,10 +112,11 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str: for search_name in search_names: if search_name in liquid_type or search_name in reagent_name: - print(f"ADJUST_PH: 通过液体类型匹配找到容器: {node_id}") + debug_print(f"✅ 通过液体类型匹配找到容器: {node_id} 💧") return node_id # 列出可用容器帮助调试 + debug_print(f"📊 列出可用容器帮助调试...") available_containers = [] for node_id in G.nodes(): if G.nodes[node_id].get('type') == 'container': @@ -98,67 +132,92 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str: 'reagent_name': vessel_data.get('reagent_name', '') }) - print(f"ADJUST_PH: 可用容器列表:") + debug_print(f"📋 可用容器列表:") for container in available_containers: - print(f" - {container['id']}: {container['name']}") - print(f" 液体: {container['liquids']}") - print(f" 试剂: {container['reagent_name']}") + debug_print(f" - 🧪 {container['id']}: {container['name']}") + debug_print(f" 💧 液体: {container['liquids']}") + debug_print(f" 🏷️ 试剂: {container['reagent_name']}") - raise ValueError(f"找不到试剂 '{reagent}' 对应的容器。尝试了: {possible_names}") - + debug_print(f"❌ 所有匹配方法都失败了") + raise ValueError(f"找不到试剂 '{reagent}' 对应的容器。尝试了: {possible_names[:10]}...") def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str: """查找与容器相连的搅拌器""" + debug_print(f"🔍 查找连接到容器 '{vessel}' 的搅拌器...") + stirrer_nodes = [node for node in G.nodes() if (G.nodes[node].get('class') or '') == 'virtual_stirrer'] + debug_print(f"📊 发现 {len(stirrer_nodes)} 个搅拌器: {stirrer_nodes}") + for stirrer in stirrer_nodes: if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): + debug_print(f"✅ 找到连接的搅拌器: {stirrer} 🔗") return stirrer - return stirrer_nodes[0] if stirrer_nodes else None - + if stirrer_nodes: + debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个: {stirrer_nodes[0]} 🔄") + return stirrer_nodes[0] + + debug_print(f"❌ 未找到任何搅拌器") + return None -def calculate_reagent_volume(target_ph_value: float, reagent: str, vessel_volume: float = 100.0) -> float: # 改为 target_ph_value +def calculate_reagent_volume(target_ph_value: float, reagent: str, vessel_volume: float = 100.0) -> float: """ 估算需要的试剂体积来调节pH Args: - target_ph_value: 目标pH值 # 改为 target_ph_value + target_ph_value: 目标pH值 reagent: 试剂名称 vessel_volume: 容器体积 (mL) Returns: float: 估算的试剂体积 (mL) """ + debug_print(f"🧮 计算试剂体积...") + debug_print(f" 📍 目标pH: {target_ph_value}") + debug_print(f" 🧪 试剂: {reagent}") + debug_print(f" 📏 容器体积: {vessel_volume}mL") + # 简化的pH调节体积估算(实际应用中需要更精确的计算) if "acid" in reagent.lower() or "hcl" in reagent.lower(): + debug_print(f"🍋 检测到酸性试剂") # 酸性试剂:pH越低需要的体积越大 - if target_ph_value < 3: # 改为 target_ph_value - return vessel_volume * 0.05 # 5% - elif target_ph_value < 5: # 改为 target_ph_value - return vessel_volume * 0.02 # 2% + if target_ph_value < 3: + volume = vessel_volume * 0.05 # 5% + debug_print(f" 💪 强酸性 (pH<3): 使用 5% 体积") + elif target_ph_value < 5: + volume = vessel_volume * 0.02 # 2% + debug_print(f" 🔸 中酸性 (pH<5): 使用 2% 体积") else: - return vessel_volume * 0.01 # 1% + volume = vessel_volume * 0.01 # 1% + debug_print(f" 🔹 弱酸性 (pH≥5): 使用 1% 体积") elif "hydroxide" in reagent.lower() or "naoh" in reagent.lower(): + debug_print(f"🧂 检测到碱性试剂") # 碱性试剂:pH越高需要的体积越大 - if target_ph_value > 11: # 改为 target_ph_value - return vessel_volume * 0.05 # 5% - elif target_ph_value > 9: # 改为 target_ph_value - return vessel_volume * 0.02 # 2% + if target_ph_value > 11: + volume = vessel_volume * 0.05 # 5% + debug_print(f" 💪 强碱性 (pH>11): 使用 5% 体积") + elif target_ph_value > 9: + volume = vessel_volume * 0.02 # 2% + debug_print(f" 🔸 中碱性 (pH>9): 使用 2% 体积") else: - return vessel_volume * 0.01 # 1% + volume = vessel_volume * 0.01 # 1% + debug_print(f" 🔹 弱碱性 (pH≤9): 使用 1% 体积") else: # 未知试剂,使用默认值 - return vessel_volume * 0.01 - + volume = vessel_volume * 0.01 + debug_print(f"❓ 未知试剂类型,使用默认 1% 体积") + + debug_print(f"📊 计算结果: {volume:.2f}mL") + return volume def generate_adjust_ph_protocol( G: nx.DiGraph, vessel: str, - ph_value: float, # 改为 ph_value + ph_value: float, reagent: str, **kwargs ) -> List[Dict[str, Any]]: @@ -168,13 +227,23 @@ def generate_adjust_ph_protocol( Args: G: 有向图,节点为容器和设备 vessel: 目标容器(需要调节pH的容器) - ph_value: 目标pH值(从XDL传入) # 改为 ph_value + ph_value: 目标pH值(从XDL传入) reagent: 酸碱试剂名称(从XDL传入) **kwargs: 其他可选参数,使用默认值 Returns: List[Dict[str, Any]]: 动作序列 """ + + debug_print("=" * 60) + debug_print("🧪 开始生成pH调节协议") + debug_print(f"📋 原始参数:") + debug_print(f" 🥼 vessel: '{vessel}'") + debug_print(f" 📊 ph_value: {ph_value}") + debug_print(f" 🧪 reagent: '{reagent}'") + debug_print(f" 📦 kwargs: {kwargs}") + debug_print("=" * 60) + action_sequence = [] # 从kwargs中获取可选参数,如果没有则使用默认值 @@ -184,48 +253,84 @@ def generate_adjust_ph_protocol( stir_time = kwargs.get('stir_time', 60.0) # 默认搅拌时间 settling_time = kwargs.get('settling_time', 30.0) # 默认平衡时间 - print(f"ADJUST_PH: 开始生成pH调节协议") - print(f" - 目标容器: {vessel}") - print(f" - 目标pH: {ph_value}") # 改为 ph_value - print(f" - 试剂: {reagent}") - print(f" - 使用默认参数: 体积=自动估算, 搅拌=True, 搅拌速度=300RPM") + debug_print(f"🔧 处理后的参数:") + debug_print(f" 📏 volume: {volume}mL (0.0表示自动估算)") + debug_print(f" 🌪️ stir: {stir}") + debug_print(f" 🔄 stir_speed: {stir_speed}rpm") + debug_print(f" ⏱️ stir_time: {stir_time}s") + debug_print(f" ⏳ settling_time: {settling_time}s") + + # 开始处理 + action_sequence.append(create_action_log(f"开始调节pH至 {ph_value}", "🧪")) + action_sequence.append(create_action_log(f"目标容器: {vessel}", "🥼")) + action_sequence.append(create_action_log(f"使用试剂: {reagent}", "⚗️")) # 1. 验证目标容器存在 + debug_print(f"🔍 步骤1: 验证目标容器...") if vessel not in G.nodes(): + debug_print(f"❌ 目标容器 '{vessel}' 不存在于系统中") raise ValueError(f"目标容器 '{vessel}' 不存在于系统中") + debug_print(f"✅ 目标容器验证通过") + action_sequence.append(create_action_log("目标容器验证通过", "✅")) + # 2. 查找酸碱试剂容器 + debug_print(f"🔍 步骤2: 查找试剂容器...") + action_sequence.append(create_action_log("正在查找试剂容器...", "🔍")) + try: reagent_vessel = find_acid_base_vessel(G, reagent) - print(f"ADJUST_PH: 找到试剂容器: {reagent_vessel}") + debug_print(f"✅ 找到试剂容器: {reagent_vessel}") + action_sequence.append(create_action_log(f"找到试剂容器: {reagent_vessel}", "🧪")) except ValueError as e: + debug_print(f"❌ 无法找到试剂容器: {str(e)}") + action_sequence.append(create_action_log(f"试剂容器查找失败: {str(e)}", "❌")) raise ValueError(f"无法找到试剂 '{reagent}': {str(e)}") - # 3. 如果未指定体积,自动估算 + # 3. 体积估算 + debug_print(f"🔍 步骤3: 体积处理...") if volume <= 0: + action_sequence.append(create_action_log("开始自动估算试剂体积", "🧮")) + # 获取目标容器的体积信息 vessel_data = G.nodes[vessel].get('data', {}) vessel_volume = vessel_data.get('max_volume', 100.0) # 默认100mL + debug_print(f"📏 容器最大体积: {vessel_volume}mL") - estimated_volume = calculate_reagent_volume(ph_value, reagent, vessel_volume) # 改为 ph_value + estimated_volume = calculate_reagent_volume(ph_value, reagent, vessel_volume) volume = estimated_volume - print(f"ADJUST_PH: 自动估算试剂体积: {volume:.2f} mL") + debug_print(f"✅ 自动估算试剂体积: {volume:.2f} mL") + action_sequence.append(create_action_log(f"估算试剂体积: {volume:.2f}mL", "📊")) + else: + debug_print(f"📏 使用指定体积: {volume}mL") + action_sequence.append(create_action_log(f"使用指定体积: {volume}mL", "📏")) # 4. 验证路径存在 + debug_print(f"🔍 步骤4: 路径验证...") + action_sequence.append(create_action_log("验证转移路径...", "🛤️")) + try: path = nx.shortest_path(G, source=reagent_vessel, target=vessel) - print(f"ADJUST_PH: 找到路径: {' → '.join(path)}") + debug_print(f"✅ 找到路径: {' → '.join(path)}") + action_sequence.append(create_action_log(f"找到转移路径: {' → '.join(path)}", "🛤️")) except nx.NetworkXNoPath: + debug_print(f"❌ 无法找到转移路径") + action_sequence.append(create_action_log("转移路径不存在", "❌")) raise ValueError(f"从试剂容器 '{reagent_vessel}' 到目标容器 '{vessel}' 没有可用路径") - # 5. 先启动搅拌(如果需要) + # 5. 搅拌器设置 + debug_print(f"🔍 步骤5: 搅拌器设置...") stirrer_id = None if stir: + action_sequence.append(create_action_log("准备启动搅拌器", "🌪️")) + try: stirrer_id = find_connected_stirrer(G, vessel) if stirrer_id: - print(f"ADJUST_PH: 找到搅拌器 {stirrer_id},启动搅拌") + debug_print(f"✅ 找到搅拌器 {stirrer_id},启动搅拌") + action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {stir_speed}rpm)", "🔄")) + action_sequence.append({ "device_id": stirrer_id, "action_name": "start_stir", @@ -237,23 +342,34 @@ def generate_adjust_ph_protocol( }) # 等待搅拌稳定 + action_sequence.append(create_action_log("等待搅拌稳定...", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 5} }) else: - print(f"ADJUST_PH: 警告 - 未找到搅拌器,继续执行") + debug_print(f"⚠️ 未找到搅拌器,继续执行") + action_sequence.append(create_action_log("未找到搅拌器,跳过搅拌", "⚠️")) except Exception as e: - print(f"ADJUST_PH: 搅拌器配置出错: {str(e)}") + debug_print(f"❌ 搅拌器配置出错: {str(e)}") + action_sequence.append(create_action_log(f"搅拌器配置失败: {str(e)}", "❌")) + else: + debug_print(f"📋 跳过搅拌设置") + action_sequence.append(create_action_log("跳过搅拌设置", "⏭️")) - # 6. 缓慢添加试剂 - 使用pump_protocol - print(f"ADJUST_PH: 开始添加试剂 {volume:.2f} mL") + # 6. 试剂添加 + debug_print(f"🔍 步骤6: 试剂添加...") + action_sequence.append(create_action_log(f"开始添加试剂 {volume:.2f}mL", "🚰")) # 计算添加时间(pH调节需要缓慢添加) addition_time = max(30.0, volume * 2.0) # 至少30秒,每mL需要2秒 + debug_print(f"⏱️ 计算添加时间: {addition_time}s (缓慢注入)") + action_sequence.append(create_action_log(f"设置添加时间: {addition_time:.0f}s (缓慢注入)", "⏱️")) try: + action_sequence.append(create_action_log("调用泵协议进行试剂转移", "🔄")) + pump_actions = generate_pump_protocol_with_rinsing( G=G, from_vessel=reagent_vessel, @@ -266,17 +382,24 @@ def generate_adjust_ph_protocol( rinsing_volume=0.0, rinsing_repeats=0, solid=False, - flowrate=0.5 # 缓慢注入 + flowrate=0.5, # 缓慢注入 + transfer_flowrate=0.3 ) action_sequence.extend(pump_actions) + debug_print(f"✅ 泵协议生成完成,添加了 {len(pump_actions)} 个动作") + action_sequence.append(create_action_log(f"试剂转移完成 ({len(pump_actions)} 个操作)", "✅")) except Exception as e: + debug_print(f"❌ 生成泵协议时出错: {str(e)}") + action_sequence.append(create_action_log(f"泵协议生成失败: {str(e)}", "❌")) raise ValueError(f"生成泵协议时出错: {str(e)}") - # 7. 持续搅拌以混合和平衡 + # 7. 混合搅拌 if stir and stirrer_id: - print(f"ADJUST_PH: 持续搅拌 {stir_time} 秒以混合试剂") + debug_print(f"🔍 步骤7: 混合搅拌...") + action_sequence.append(create_action_log(f"开始混合搅拌 {stir_time:.0f}s", "🌀")) + action_sequence.append({ "device_id": stirrer_id, "action_name": "stir", @@ -284,25 +407,47 @@ def generate_adjust_ph_protocol( "stir_time": stir_time, "stir_speed": stir_speed, "settling_time": settling_time, - "purpose": f"pH调节: 混合试剂,目标pH={ph_value}" # 改为 ph_value + "purpose": f"pH调节: 混合试剂,目标pH={ph_value}" } }) + + debug_print(f"✅ 混合搅拌设置完成") + else: + debug_print(f"⏭️ 跳过混合搅拌") + action_sequence.append(create_action_log("跳过混合搅拌", "⏭️")) + + # 8. 等待平衡 + debug_print(f"🔍 步骤8: 反应平衡...") + action_sequence.append(create_action_log(f"等待pH平衡 {settling_time:.0f}s", "⚖️")) - # 8. 等待反应平衡 action_sequence.append({ "action_name": "wait", "action_kwargs": { "time": settling_time, - "description": f"等待pH平衡到目标值 {ph_value}" # 改为 ph_value + "description": f"等待pH平衡到目标值 {ph_value}" } }) - print(f"ADJUST_PH: 协议生成完成,共 {len(action_sequence)} 个动作") - print(f"ADJUST_PH: 预计总时间: {addition_time + stir_time + settling_time:.0f} 秒") + # 9. 完成总结 + total_time = addition_time + stir_time + settling_time + + debug_print("=" * 60) + debug_print(f"🎉 pH调节协议生成完成") + debug_print(f"📊 协议统计:") + debug_print(f" 📋 总动作数: {len(action_sequence)}") + debug_print(f" ⏱️ 预计总时间: {total_time:.0f}s ({total_time/60:.1f}分钟)") + debug_print(f" 🧪 试剂: {reagent}") + debug_print(f" 📏 体积: {volume:.2f}mL") + debug_print(f" 📊 目标pH: {ph_value}") + debug_print(f" 🥼 目标容器: {vessel}") + debug_print("=" * 60) + + # 添加完成日志 + summary_msg = f"pH调节协议完成: {vessel} → pH {ph_value} (使用 {volume:.2f}mL {reagent})" + action_sequence.append(create_action_log(summary_msg, "🎉")) return action_sequence - def generate_adjust_ph_protocol_stepwise( G: nx.DiGraph, vessel: str, @@ -317,7 +462,7 @@ def generate_adjust_ph_protocol_stepwise( Args: G: 网络图 vessel: 目标容器 - pH: 目标pH值 + ph_value: 目标pH值 reagent: 酸碱试剂 max_volume: 最大试剂体积 steps: 分步数量 @@ -325,15 +470,28 @@ def generate_adjust_ph_protocol_stepwise( Returns: List[Dict[str, Any]]: 动作序列 """ - action_sequence = [] + debug_print("=" * 60) + debug_print(f"🔄 开始分步pH调节") + debug_print(f"📋 分步参数:") + debug_print(f" 🥼 vessel: {vessel}") + debug_print(f" 📊 ph_value: {ph_value}") + debug_print(f" 🧪 reagent: {reagent}") + debug_print(f" 📏 max_volume: {max_volume}mL") + debug_print(f" 🔢 steps: {steps}") + debug_print("=" * 60) - print(f"ADJUST_PH: 开始分步pH调节({steps}步)") + action_sequence = [] # 每步添加的体积 step_volume = max_volume / steps + debug_print(f"📊 每步体积: {step_volume:.2f}mL") + + action_sequence.append(create_action_log(f"开始分步pH调节 ({steps}步)", "🔄")) + action_sequence.append(create_action_log(f"每步添加: {step_volume:.2f}mL", "📏")) for i in range(steps): - print(f"ADJUST_PH: 第 {i+1}/{steps} 步,添加 {step_volume} mL") + debug_print(f"🔄 执行第 {i+1}/{steps} 步,添加 {step_volume:.2f}mL") + action_sequence.append(create_action_log(f"第 {i+1}/{steps} 步开始", "🚀")) # 生成单步协议 step_actions = generate_adjust_ph_protocol( @@ -349,9 +507,13 @@ def generate_adjust_ph_protocol_stepwise( ) action_sequence.extend(step_actions) + debug_print(f"✅ 第 {i+1}/{steps} 步完成,添加了 {len(step_actions)} 个动作") + action_sequence.append(create_action_log(f"第 {i+1}/{steps} 步完成", "✅")) # 步骤间等待 if i < steps - 1: + debug_print(f"⏳ 步骤间等待30s") + action_sequence.append(create_action_log("步骤间等待...", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": { @@ -360,10 +522,11 @@ def generate_adjust_ph_protocol_stepwise( } }) - print(f"ADJUST_PH: 分步pH调节完成") + debug_print(f"🎉 分步pH调节完成,共 {len(action_sequence)} 个动作") + action_sequence.append(create_action_log("分步pH调节全部完成", "🎉")) + return action_sequence - # 便捷函数:常用pH调节 def generate_acidify_protocol( G: nx.DiGraph, @@ -372,11 +535,11 @@ def generate_acidify_protocol( acid: str = "hydrochloric acid" ) -> List[Dict[str, Any]]: """酸化协议""" + debug_print(f"🍋 生成酸化协议: {vessel} → pH {target_ph} (使用 {acid})") return generate_adjust_ph_protocol( - G, vessel, target_ph, acid, 0.0, True, 300.0, 120.0, 60.0 + G, vessel, target_ph, acid ) - def generate_basify_protocol( G: nx.DiGraph, vessel: str, @@ -384,28 +547,42 @@ def generate_basify_protocol( base: str = "sodium hydroxide" ) -> List[Dict[str, Any]]: """碱化协议""" + debug_print(f"🧂 生成碱化协议: {vessel} → pH {target_ph} (使用 {base})") return generate_adjust_ph_protocol( - G, vessel, target_ph, base, 0.0, True, 300.0, 120.0, 60.0 + G, vessel, target_ph, base ) - def generate_neutralize_protocol( G: nx.DiGraph, vessel: str, reagent: str = "sodium hydroxide" ) -> List[Dict[str, Any]]: """中和协议(pH=7)""" + debug_print(f"⚖️ 生成中和协议: {vessel} → pH 7.0 (使用 {reagent})") return generate_adjust_ph_protocol( - G, vessel, 7.0, reagent, 0.0, True, 350.0, 180.0, 90.0 + G, vessel, 7.0, reagent ) - # 测试函数 def test_adjust_ph_protocol(): """测试pH调节协议""" - print("=== ADJUST PH PROTOCOL 测试 ===") - print("测试完成") - + debug_print("=== ADJUST PH PROTOCOL 增强版测试 ===") + + # 测试体积计算 + debug_print("🧮 测试体积计算...") + test_cases = [ + (2.0, "hydrochloric acid", 100.0), + (4.0, "hydrochloric acid", 100.0), + (12.0, "sodium hydroxide", 100.0), + (10.0, "sodium hydroxide", 100.0), + (7.0, "unknown reagent", 100.0) + ] + + for ph, reagent, volume in test_cases: + result = calculate_reagent_volume(ph, reagent, volume) + debug_print(f"📊 {reagent} → pH {ph}: {result:.2f}mL") + + debug_print("✅ 测试完成") if __name__ == "__main__": test_adjust_ph_protocol() \ No newline at end of file diff --git a/unilabos/compile/dissolve_protocol.py b/unilabos/compile/dissolve_protocol.py index 9008e6d57..065196ef4 100644 --- a/unilabos/compile/dissolve_protocol.py +++ b/unilabos/compile/dissolve_protocol.py @@ -11,6 +11,22 @@ def debug_print(message): print(f"[DISSOLVE] {message}", flush=True) logger.info(f"[DISSOLVE] {message}") +# 🆕 创建进度日志动作 +def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]: + """创建一个动作日志""" + full_message = f"{emoji} {message}" + debug_print(full_message) + logger.info(full_message) + print(f"[ACTION] {full_message}", flush=True) + + return { + "action_name": "wait", + "action_kwargs": { + "time": 0.1, + "log_message": full_message + } + } + def parse_volume_input(volume_input: Union[str, float]) -> float: """ 解析体积输入,支持带单位的字符串 @@ -22,18 +38,20 @@ def parse_volume_input(volume_input: Union[str, float]) -> float: float: 体积(毫升) """ if isinstance(volume_input, (int, float)): + debug_print(f"📏 体积输入为数值: {volume_input}") return float(volume_input) if not volume_input or not str(volume_input).strip(): + debug_print(f"⚠️ 体积输入为空,返回0.0mL") return 0.0 volume_str = str(volume_input).lower().strip() - debug_print(f"解析体积输入: '{volume_str}'") + debug_print(f"🔍 解析体积输入: '{volume_str}'") # 处理未知体积 if volume_str in ['?', 'unknown', 'tbd', 'to be determined']: default_volume = 50.0 # 默认50mL - debug_print(f"检测到未知体积,使用默认值: {default_volume}mL") + debug_print(f"❓ 检测到未知体积,使用默认值: {default_volume}mL 🎯") return default_volume # 移除空格并提取数字和单位 @@ -43,7 +61,7 @@ def parse_volume_input(volume_input: Union[str, float]) -> float: match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean) if not match: - debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值50mL") + debug_print(f"❌ 无法解析体积: '{volume_str}',使用默认值50mL") return 50.0 value = float(match.group(1)) @@ -52,12 +70,14 @@ def parse_volume_input(volume_input: Union[str, float]) -> float: # 转换为毫升 if unit in ['l', 'liter']: volume = value * 1000.0 # L -> mL + debug_print(f"🔄 体积转换: {value}L → {volume}mL") elif unit in ['μl', 'ul', 'microliter']: volume = value / 1000.0 # μL -> mL + debug_print(f"🔄 体积转换: {value}μL → {volume}mL") else: # ml, milliliter 或默认 volume = value # 已经是mL + debug_print(f"✅ 体积已为mL: {volume}mL") - debug_print(f"体积转换: {value}{unit} → {volume}mL") return volume def parse_mass_input(mass_input: Union[str, float]) -> float: @@ -71,18 +91,20 @@ def parse_mass_input(mass_input: Union[str, float]) -> float: float: 质量(克) """ if isinstance(mass_input, (int, float)): + debug_print(f"⚖️ 质量输入为数值: {mass_input}g") return float(mass_input) if not mass_input or not str(mass_input).strip(): + debug_print(f"⚠️ 质量输入为空,返回0.0g") return 0.0 mass_str = str(mass_input).lower().strip() - debug_print(f"解析质量输入: '{mass_str}'") + debug_print(f"🔍 解析质量输入: '{mass_str}'") # 处理未知质量 if mass_str in ['?', 'unknown', 'tbd', 'to be determined']: default_mass = 1.0 # 默认1g - debug_print(f"检测到未知质量,使用默认值: {default_mass}g") + debug_print(f"❓ 检测到未知质量,使用默认值: {default_mass}g 🎯") return default_mass # 移除空格并提取数字和单位 @@ -92,7 +114,7 @@ def parse_mass_input(mass_input: Union[str, float]) -> float: match = re.match(r'([0-9]*\.?[0-9]+)\s*(g|mg|kg|gram|milligram|kilogram)?', mass_clean) if not match: - debug_print(f"⚠️ 无法解析质量: '{mass_str}',返回0.0g") + debug_print(f"❌ 无法解析质量: '{mass_str}',返回0.0g") return 0.0 value = float(match.group(1)) @@ -101,12 +123,14 @@ def parse_mass_input(mass_input: Union[str, float]) -> float: # 转换为克 if unit in ['mg', 'milligram']: mass = value / 1000.0 # mg -> g + debug_print(f"🔄 质量转换: {value}mg → {mass}g") elif unit in ['kg', 'kilogram']: mass = value * 1000.0 # kg -> g + debug_print(f"🔄 质量转换: {value}kg → {mass}g") else: # g, gram 或默认 mass = value # 已经是g + debug_print(f"✅ 质量已为g: {mass}g") - debug_print(f"质量转换: {value}{unit} → {mass}g") return mass def parse_time_input(time_input: Union[str, float]) -> float: @@ -120,18 +144,20 @@ def parse_time_input(time_input: Union[str, float]) -> float: float: 时间(秒) """ if isinstance(time_input, (int, float)): + debug_print(f"⏱️ 时间输入为数值: {time_input}秒") return float(time_input) if not time_input or not str(time_input).strip(): + debug_print(f"⚠️ 时间输入为空,返回0秒") return 0.0 time_str = str(time_input).lower().strip() - debug_print(f"解析时间输入: '{time_str}'") + debug_print(f"🔍 解析时间输入: '{time_str}'") # 处理未知时间 if time_str in ['?', 'unknown', 'tbd']: default_time = 600.0 # 默认10分钟 - debug_print(f"检测到未知时间,使用默认值: {default_time}s") + debug_print(f"❓ 检测到未知时间,使用默认值: {default_time}s (10分钟) ⏰") return default_time # 移除空格并提取数字和单位 @@ -141,7 +167,7 @@ def parse_time_input(time_input: Union[str, float]) -> float: match = re.match(r'([0-9]*\.?[0-9]+)\s*(s|sec|second|min|minute|h|hr|hour|d|day)?', time_clean) if not match: - debug_print(f"⚠️ 无法解析时间: '{time_str}',返回0s") + debug_print(f"❌ 无法解析时间: '{time_str}',返回0s") return 0.0 value = float(match.group(1)) @@ -150,14 +176,17 @@ def parse_time_input(time_input: Union[str, float]) -> float: # 转换为秒 if unit in ['min', 'minute']: time_sec = value * 60.0 # min -> s + debug_print(f"🔄 时间转换: {value}分钟 → {time_sec}秒") elif unit in ['h', 'hr', 'hour']: time_sec = value * 3600.0 # h -> s + debug_print(f"🔄 时间转换: {value}小时 → {time_sec}秒") elif unit in ['d', 'day']: time_sec = value * 86400.0 # d -> s + debug_print(f"🔄 时间转换: {value}天 → {time_sec}秒") else: # s, sec, second 或默认 time_sec = value # 已经是s + debug_print(f"✅ 时间已为秒: {time_sec}秒") - debug_print(f"时间转换: {value}{unit} → {time_sec}s") return time_sec def parse_temperature_input(temp_input: Union[str, float]) -> float: @@ -171,13 +200,15 @@ def parse_temperature_input(temp_input: Union[str, float]) -> float: float: 温度(摄氏度) """ if isinstance(temp_input, (int, float)): + debug_print(f"🌡️ 温度输入为数值: {temp_input}°C") return float(temp_input) if not temp_input or not str(temp_input).strip(): + debug_print(f"⚠️ 温度输入为空,使用默认室温25°C") return 25.0 # 默认室温 temp_str = str(temp_input).lower().strip() - debug_print(f"解析温度输入: '{temp_str}'") + debug_print(f"🔍 解析温度输入: '{temp_str}'") # 处理特殊温度描述 temp_aliases = { @@ -193,7 +224,7 @@ def parse_temperature_input(temp_input: Union[str, float]) -> float: if temp_str in temp_aliases: result = temp_aliases[temp_str] - debug_print(f"温度别名解析: '{temp_str}' → {result}°C") + debug_print(f"🏷️ 温度别名解析: '{temp_str}' → {result}°C") return result # 移除空格并提取数字和单位 @@ -203,7 +234,7 @@ def parse_temperature_input(temp_input: Union[str, float]) -> float: match = re.match(r'([0-9]*\.?[0-9]+)\s*(°c|c|celsius|°f|f|fahrenheit|k|kelvin)?', temp_clean) if not match: - debug_print(f"⚠️ 无法解析温度: '{temp_str}',使用默认值25°C") + debug_print(f"❌ 无法解析温度: '{temp_str}',使用默认值25°C") return 25.0 value = float(match.group(1)) @@ -212,19 +243,22 @@ def parse_temperature_input(temp_input: Union[str, float]) -> float: # 转换为摄氏度 if unit in ['°f', 'f', 'fahrenheit']: temp_c = (value - 32) * 5/9 # F -> C + debug_print(f"🔄 温度转换: {value}°F → {temp_c:.1f}°C") elif unit in ['k', 'kelvin']: temp_c = value - 273.15 # K -> C + debug_print(f"🔄 温度转换: {value}K → {temp_c:.1f}°C") else: # °c, c, celsius 或默认 temp_c = value # 已经是C + debug_print(f"✅ 温度已为°C: {temp_c}°C") - debug_print(f"温度转换: {value}{unit} → {temp_c}°C") return temp_c def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: - """增强版溶剂容器查找""" - debug_print(f"查找溶剂 '{solvent}' 的容器...") + """增强版溶剂容器查找,支持多种匹配模式""" + debug_print(f"🔍 开始查找溶剂 '{solvent}' 的容器...") # 🔧 方法1:直接搜索 data.reagent_name 和 config.reagent + debug_print(f"📋 方法1: 搜索reagent字段...") for node in G.nodes(): node_data = G.nodes[node].get('data', {}) node_type = G.nodes[node].get('type', '') @@ -237,18 +271,20 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: # 精确匹配 if reagent_name == solvent.lower() or config_reagent == solvent.lower(): - debug_print(f"✅ 通过reagent字段找到容器: {node}") + debug_print(f"✅ 通过reagent字段精确匹配到容器: {node} 🎯") return node # 模糊匹配 if (solvent.lower() in reagent_name and reagent_name) or \ (solvent.lower() in config_reagent and config_reagent): - debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node}") + debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node} 🔍") return node # 🔧 方法2:常见的容器命名规则 + debug_print(f"📋 方法2: 使用命名规则查找...") solvent_clean = solvent.lower().replace(' ', '_').replace('-', '_') possible_names = [ + solvent_clean, f"flask_{solvent_clean}", f"bottle_{solvent_clean}", f"vessel_{solvent_clean}", @@ -256,77 +292,118 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: f"{solvent_clean}_bottle", f"solvent_{solvent_clean}", f"reagent_{solvent_clean}", - f"reagent_bottle_{solvent_clean}" + f"reagent_bottle_{solvent_clean}", + f"reagent_bottle_1", # 通用试剂瓶 + f"reagent_bottle_2", + f"reagent_bottle_3" ] + debug_print(f"🔍 尝试的容器名称: {possible_names[:5]}... (共{len(possible_names)}个)") + for name in possible_names: if name in G.nodes(): node_type = G.nodes[name].get('type', '') if node_type == 'container': - debug_print(f"✅ 通过命名规则找到容器: {name}") + debug_print(f"✅ 通过命名规则找到容器: {name} 📝") return name - # 🔧 方法3:使用第一个试剂瓶作为备选 + # 🔧 方法3:节点名称模糊匹配 + debug_print(f"📋 方法3: 节点名称模糊匹配...") + for node_id in G.nodes(): + node_data = G.nodes[node_id] + if node_data.get('type') == 'container': + # 检查节点名称是否包含溶剂名称 + if solvent_clean in node_id.lower(): + debug_print(f"✅ 通过节点名称模糊匹配到容器: {node_id} 🔍") + return node_id + + # 检查液体类型匹配 + vessel_data = node_data.get('data', {}) + liquids = vessel_data.get('liquid', []) + for liquid in liquids: + if isinstance(liquid, dict): + liquid_type = liquid.get('liquid_type') or liquid.get('name', '') + if liquid_type.lower() == solvent.lower(): + debug_print(f"✅ 通过液体类型匹配到容器: {node_id} 💧") + return node_id + + # 🔧 方法4:使用第一个试剂瓶作为备选 + debug_print(f"📋 方法4: 查找备选试剂瓶...") for node_id in G.nodes(): node_data = G.nodes[node_id] if (node_data.get('type') == 'container' and ('reagent' in node_id.lower() or 'bottle' in node_id.lower() or 'flask' in node_id.lower())): - debug_print(f"⚠️ 未找到专用容器,使用备选容器: {node_id}") + debug_print(f"⚠️ 未找到专用容器,使用备选试剂瓶: {node_id} 🔄") return node_id + debug_print(f"❌ 所有方法都失败了,无法找到容器!") raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器") def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str: """查找连接到指定容器的加热搅拌器""" + debug_print(f"🔍 查找连接到容器 '{vessel}' 的加热搅拌器...") + heatchill_nodes = [] for node in G.nodes(): node_class = G.nodes[node].get('class', '').lower() if 'heatchill' in node_class: heatchill_nodes.append(node) + debug_print(f"📋 发现加热搅拌器: {node}") + + debug_print(f"📊 共找到 {len(heatchill_nodes)} 个加热搅拌器") # 查找连接到容器的加热器 for heatchill in heatchill_nodes: if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill): - debug_print(f"找到连接的加热器: {heatchill}") + debug_print(f"✅ 找到连接的加热搅拌器: {heatchill} 🔗") return heatchill # 返回第一个加热器 if heatchill_nodes: - debug_print(f"使用第一个加热器: {heatchill_nodes[0]}") + debug_print(f"⚠️ 未找到直接连接的加热搅拌器,使用第一个: {heatchill_nodes[0]} 🔄") return heatchill_nodes[0] + debug_print(f"❌ 未找到任何加热搅拌器") return "" def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str: """查找连接到指定容器的搅拌器""" + debug_print(f"🔍 查找连接到容器 '{vessel}' 的搅拌器...") + stirrer_nodes = [] for node in G.nodes(): node_class = G.nodes[node].get('class', '').lower() if 'stirrer' in node_class: stirrer_nodes.append(node) + debug_print(f"📋 发现搅拌器: {node}") + + debug_print(f"📊 共找到 {len(stirrer_nodes)} 个搅拌器") # 查找连接到容器的搅拌器 for stirrer in stirrer_nodes: if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): - debug_print(f"找到连接的搅拌器: {stirrer}") + debug_print(f"✅ 找到连接的搅拌器: {stirrer} 🔗") return stirrer # 返回第一个搅拌器 if stirrer_nodes: - debug_print(f"使用第一个搅拌器: {stirrer_nodes[0]}") + debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个: {stirrer_nodes[0]} 🔄") return stirrer_nodes[0] + debug_print(f"❌ 未找到任何搅拌器") return "" def find_solid_dispenser(G: nx.DiGraph) -> str: """查找固体加样器""" + debug_print(f"🔍 查找固体加样器...") + for node in G.nodes(): node_class = G.nodes[node].get('class', '').lower() if 'solid_dispenser' in node_class or 'dispenser' in node_class: - debug_print(f"找到固体加样器: {node}") + debug_print(f"✅ 找到固体加样器: {node} 🥄") return node - debug_print("⚠️ 未找到固体加样器") + debug_print(f"❌ 未找到固体加样器") return "" def generate_dissolve_protocol( @@ -347,12 +424,13 @@ def generate_dissolve_protocol( **kwargs # 🔧 关键:接受所有其他参数,防止unexpected keyword错误 ) -> List[Dict[str, Any]]: """ - 生成溶解操作的协议序列 - 修复版 + 生成溶解操作的协议序列 - 增强版 🔧 修复要点: 1. 添加action文件中的所有参数(mass, mol, reagent, event) 2. 使用 **kwargs 接受所有额外参数,防止 unexpected keyword argument 错误 3. 支持固体溶解和液体溶解两种模式 + 4. 添加详细的emoji日志系统 支持两种溶解模式: 1. 液体溶解:指定 solvent + volume,使用pump protocol转移溶剂 @@ -367,35 +445,40 @@ def generate_dissolve_protocol( """ debug_print("=" * 60) - debug_print("开始生成溶解协议 - 修复版") - debug_print(f"原始参数:") - debug_print(f" - vessel: '{vessel}'") - debug_print(f" - solvent: '{solvent}'") - debug_print(f" - volume: {volume} (类型: {type(volume)})") - debug_print(f" - mass: {mass} (类型: {type(mass)})") - debug_print(f" - temp: {temp} (类型: {type(temp)})") - debug_print(f" - time: {time} (类型: {type(time)})") - debug_print(f" - reagent: '{reagent}'") - debug_print(f" - mol: '{mol}'") - debug_print(f" - event: '{event}'") - debug_print(f" - kwargs: {kwargs}") # 显示额外参数 + debug_print("🧪 开始生成溶解协议") + debug_print(f"📋 原始参数:") + debug_print(f" 🥼 vessel: '{vessel}'") + debug_print(f" 💧 solvent: '{solvent}'") + debug_print(f" 📏 volume: {volume} (类型: {type(volume)})") + debug_print(f" ⚖️ mass: {mass} (类型: {type(mass)})") + debug_print(f" 🌡️ temp: {temp} (类型: {type(temp)})") + debug_print(f" ⏱️ time: {time} (类型: {type(time)})") + debug_print(f" 🧪 reagent: '{reagent}'") + debug_print(f" 🧬 mol: '{mol}'") + debug_print(f" 🎯 event: '{event}'") + debug_print(f" 📦 kwargs: {kwargs}") # 显示额外参数 debug_print("=" * 60) action_sequence = [] # === 参数验证 === - debug_print("步骤1: 参数验证...") + debug_print("🔍 步骤1: 参数验证...") + action_sequence.append(create_action_log(f"开始溶解操作 - 容器: {vessel}", "🎬")) if not vessel: + debug_print("❌ vessel 参数不能为空") raise ValueError("vessel 参数不能为空") if vessel not in G.nodes(): + debug_print(f"❌ 容器 '{vessel}' 不存在于系统中") raise ValueError(f"容器 '{vessel}' 不存在于系统中") debug_print("✅ 基本参数验证通过") + action_sequence.append(create_action_log("参数验证通过", "✅")) # === 🔧 关键修复:参数解析 === - debug_print("步骤2: 参数解析...") + debug_print("🔍 步骤2: 参数解析...") + action_sequence.append(create_action_log("正在解析溶解参数...", "🔍")) # 解析各种参数为数值 final_volume = parse_volume_input(volume) @@ -403,17 +486,18 @@ def generate_dissolve_protocol( final_temp = parse_temperature_input(temp) final_time = parse_time_input(time) - debug_print(f"解析结果:") - debug_print(f" - 体积: {final_volume}mL") - debug_print(f" - 质量: {final_mass}g") - debug_print(f" - 温度: {final_temp}°C") - debug_print(f" - 时间: {final_time}s") - debug_print(f" - 试剂: '{reagent}'") - debug_print(f" - 摩尔: '{mol}'") - debug_print(f" - 事件: '{event}'") + debug_print(f"📊 解析结果:") + debug_print(f" 📏 体积: {final_volume}mL") + debug_print(f" ⚖️ 质量: {final_mass}g") + debug_print(f" 🌡️ 温度: {final_temp}°C") + debug_print(f" ⏱️ 时间: {final_time}s") + debug_print(f" 🧪 试剂: '{reagent}'") + debug_print(f" 🧬 摩尔: '{mol}'") + debug_print(f" 🎯 事件: '{event}'") # === 判断溶解类型 === - debug_print("步骤3: 判断溶解类型...") + debug_print("🔍 步骤3: 判断溶解类型...") + action_sequence.append(create_action_log("正在判断溶解类型...", "🔍")) # 判断是固体溶解还是液体溶解 is_solid_dissolve = (final_mass > 0 or (mol and mol.strip() != "") or (reagent and reagent.strip() != "")) @@ -427,10 +511,15 @@ def generate_dissolve_protocol( solvent = "water" # 默认溶剂 debug_print("⚠️ 未明确指定溶解参数,默认为50mL水溶解") - debug_print(f"溶解类型: {'固体溶解' if is_solid_dissolve else '液体溶解'}") + dissolve_type = "固体溶解" if is_solid_dissolve else "液体溶解" + dissolve_emoji = "🧂" if is_solid_dissolve else "💧" + debug_print(f"📋 溶解类型: {dissolve_type} {dissolve_emoji}") + + action_sequence.append(create_action_log(f"确定溶解类型: {dissolve_type} {dissolve_emoji}", "📋")) # === 查找设备 === - debug_print("步骤4: 查找设备...") + debug_print("🔍 步骤4: 查找设备...") + action_sequence.append(create_action_log("正在查找相关设备...", "🔍")) # 查找加热搅拌器 heatchill_id = find_connected_heatchill(G, vessel) @@ -439,21 +528,31 @@ def generate_dissolve_protocol( # 优先使用加热搅拌器,否则使用独立搅拌器 stir_device_id = heatchill_id or stirrer_id - debug_print(f"设备映射:") - debug_print(f" - 加热器: '{heatchill_id}'") - debug_print(f" - 搅拌器: '{stirrer_id}'") - debug_print(f" - 使用设备: '{stir_device_id}'") + debug_print(f"📊 设备映射:") + debug_print(f" 🔥 加热器: '{heatchill_id}'") + debug_print(f" 🌪️ 搅拌器: '{stirrer_id}'") + debug_print(f" 🎯 使用设备: '{stir_device_id}'") + + if heatchill_id: + action_sequence.append(create_action_log(f"找到加热搅拌器: {heatchill_id}", "🔥")) + elif stirrer_id: + action_sequence.append(create_action_log(f"找到搅拌器: {stirrer_id}", "🌪️")) + else: + action_sequence.append(create_action_log("未找到搅拌设备,将跳过搅拌", "⚠️")) # === 执行溶解流程 === - debug_print("步骤5: 执行溶解流程...") + debug_print("🔍 步骤5: 执行溶解流程...") try: # 步骤5.1: 启动加热搅拌(如果需要) if stir_device_id and (final_temp > 25.0 or final_time > 0 or stir_speed > 0): - debug_print(f"5.1: 启动加热搅拌,温度: {final_temp}°C") + debug_print(f"🔍 5.1: 启动加热搅拌,温度: {final_temp}°C") + action_sequence.append(create_action_log(f"准备加热搅拌 (目标温度: {final_temp}°C)", "🔥")) if heatchill_id and (final_temp > 25.0 or final_time > 0): # 使用加热搅拌器 + action_sequence.append(create_action_log(f"启动加热搅拌器 {heatchill_id}", "🔥")) + heatchill_action = { "device_id": heatchill_id, "action_name": "heat_chill_start", @@ -468,6 +567,7 @@ def generate_dissolve_protocol( # 等待温度稳定 if final_temp > 25.0: wait_time = min(60, abs(final_temp - 25.0) * 1.5) + action_sequence.append(create_action_log(f"等待温度稳定 ({wait_time:.0f}秒)", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": wait_time} @@ -475,6 +575,8 @@ def generate_dissolve_protocol( elif stirrer_id: # 使用独立搅拌器 + action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {stir_speed}rpm)", "🌪️")) + stir_action = { "device_id": stirrer_id, "action_name": "start_stir", @@ -487,6 +589,7 @@ def generate_dissolve_protocol( action_sequence.append(stir_action) # 等待搅拌稳定 + action_sequence.append(create_action_log("等待搅拌稳定...", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 5} @@ -494,10 +597,13 @@ def generate_dissolve_protocol( if is_solid_dissolve: # === 固体溶解路径 === - debug_print(f"5.2: 使用固体溶解路径") + debug_print(f"🔍 5.2: 使用固体溶解路径") + action_sequence.append(create_action_log("开始固体溶解流程", "🧂")) solid_dispenser = find_solid_dispenser(G) if solid_dispenser: + action_sequence.append(create_action_log(f"找到固体加样器: {solid_dispenser}", "🥄")) + # 固体加样 add_kwargs = { "vessel": vessel, @@ -508,9 +614,12 @@ def generate_dissolve_protocol( if final_mass > 0: add_kwargs["mass"] = str(final_mass) + action_sequence.append(create_action_log(f"准备添加固体: {final_mass}g", "⚖️")) if mol and mol.strip(): add_kwargs["mol"] = mol + action_sequence.append(create_action_log(f"按摩尔数添加: {mol}", "🧬")) + action_sequence.append(create_action_log("开始固体加样操作", "🥄")) action_sequence.append({ "device_id": solid_dispenser, "action_name": "add_solid", @@ -518,18 +627,24 @@ def generate_dissolve_protocol( }) debug_print(f"✅ 固体加样完成") + action_sequence.append(create_action_log("固体加样完成", "✅")) else: debug_print("⚠️ 未找到固体加样器,跳过固体添加") + action_sequence.append(create_action_log("未找到固体加样器,无法添加固体", "❌")) elif is_liquid_dissolve: # === 液体溶解路径 === - debug_print(f"5.3: 使用液体溶解路径") + debug_print(f"🔍 5.3: 使用液体溶解路径") + action_sequence.append(create_action_log("开始液体溶解流程", "💧")) # 查找溶剂容器 + action_sequence.append(create_action_log("正在查找溶剂容器...", "🔍")) try: solvent_vessel = find_solvent_vessel(G, solvent) + action_sequence.append(create_action_log(f"找到溶剂容器: {solvent_vessel}", "🧪")) except ValueError as e: debug_print(f"⚠️ {str(e)},跳过溶剂添加") + action_sequence.append(create_action_log(f"溶剂容器查找失败: {str(e)}", "❌")) solvent_vessel = None if solvent_vessel: @@ -537,6 +652,9 @@ def generate_dissolve_protocol( flowrate = 1.0 # 较慢的注入速度 transfer_flowrate = 0.5 # 较慢的转移速度 + action_sequence.append(create_action_log(f"设置流速: {flowrate}mL/min (缓慢注入)", "⚡")) + action_sequence.append(create_action_log(f"开始转移 {final_volume}mL {solvent}", "🚰")) + # 调用pump protocol pump_actions = generate_pump_protocol_with_rinsing( G=G, @@ -559,8 +677,10 @@ def generate_dissolve_protocol( ) action_sequence.extend(pump_actions) debug_print(f"✅ 溶剂转移完成,添加了 {len(pump_actions)} 个动作") + action_sequence.append(create_action_log(f"溶剂转移完成 ({len(pump_actions)} 个操作)", "✅")) # 溶剂添加后等待 + action_sequence.append(create_action_log("溶剂添加后短暂等待...", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 5} @@ -568,10 +688,14 @@ def generate_dissolve_protocol( # 步骤5.4: 等待溶解完成 if final_time > 0: - debug_print(f"5.4: 等待溶解完成 - {final_time}s") + debug_print(f"🔍 5.4: 等待溶解完成 - {final_time}s") + wait_minutes = final_time / 60 + action_sequence.append(create_action_log(f"开始溶解等待 ({wait_minutes:.1f}分钟)", "⏰")) if heatchill_id: # 使用定时加热搅拌 + action_sequence.append(create_action_log(f"使用加热搅拌器进行定时溶解", "🔥")) + dissolve_action = { "device_id": heatchill_id, "action_name": "heat_chill", @@ -588,6 +712,8 @@ def generate_dissolve_protocol( elif stirrer_id: # 使用定时搅拌 + action_sequence.append(create_action_log(f"使用搅拌器进行定时溶解", "🌪️")) + stir_action = { "device_id": stirrer_id, "action_name": "stir", @@ -603,6 +729,7 @@ def generate_dissolve_protocol( else: # 简单等待 + action_sequence.append(create_action_log(f"简单等待溶解完成", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": final_time} @@ -610,7 +737,8 @@ def generate_dissolve_protocol( # 步骤5.5: 停止加热搅拌(如果需要) if heatchill_id and final_time == 0 and final_temp > 25.0: - debug_print(f"5.5: 停止加热器") + debug_print(f"🔍 5.5: 停止加热器") + action_sequence.append(create_action_log("停止加热搅拌器", "🛑")) stop_action = { "device_id": heatchill_id, @@ -622,7 +750,8 @@ def generate_dissolve_protocol( action_sequence.append(stop_action) except Exception as e: - debug_print(f"⚠️ 溶解流程执行失败: {str(e)}") + debug_print(f"❌ 溶解流程执行失败: {str(e)}") + action_sequence.append(create_action_log(f"溶解流程失败: {str(e)}", "❌")) # 添加错误日志 action_sequence.append({ "device_id": "system", @@ -634,21 +763,30 @@ def generate_dissolve_protocol( # === 最终结果 === debug_print("=" * 60) - debug_print(f"✅ 溶解协议生成完成") - debug_print(f"📊 总动作数: {len(action_sequence)}") - debug_print(f"📋 处理总结:") - debug_print(f" - 容器: {vessel}") - debug_print(f" - 溶解类型: {'固体溶解' if is_solid_dissolve else '液体溶解'}") + debug_print(f"🎉 溶解协议生成完成") + debug_print(f"📊 协议统计:") + debug_print(f" 📋 总动作数: {len(action_sequence)}") + debug_print(f" 🥼 容器: {vessel}") + debug_print(f" {dissolve_emoji} 溶解类型: {dissolve_type}") if is_liquid_dissolve: - debug_print(f" - 溶剂: {solvent} ({final_volume}mL)") + debug_print(f" 💧 溶剂: {solvent} ({final_volume}mL)") if is_solid_dissolve: - debug_print(f" - 试剂: {reagent}") - debug_print(f" - 质量: {final_mass}g") - debug_print(f" - 摩尔: {mol}") - debug_print(f" - 温度: {final_temp}°C") - debug_print(f" - 时间: {final_time}s") + debug_print(f" 🧪 试剂: {reagent}") + debug_print(f" ⚖️ 质量: {final_mass}g") + debug_print(f" 🧬 摩尔: {mol}") + debug_print(f" 🌡️ 温度: {final_temp}°C") + debug_print(f" ⏱️ 时间: {final_time}s") debug_print("=" * 60) + # 添加完成日志 + summary_msg = f"溶解协议完成: {vessel}" + if is_liquid_dissolve: + summary_msg += f" (使用 {final_volume}mL {solvent})" + if is_solid_dissolve: + summary_msg += f" (溶解 {final_mass}g {reagent})" + + action_sequence.append(create_action_log(summary_msg, "🎉")) + return action_sequence # === 便捷函数 === @@ -656,6 +794,7 @@ def generate_dissolve_protocol( def dissolve_solid_by_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, float], temp: Union[str, float] = 25.0, time: Union[str, float] = "10 min") -> List[Dict[str, Any]]: """按质量溶解固体""" + debug_print(f"🧂 快速固体溶解: {reagent} ({mass}) → {vessel}") return generate_dissolve_protocol( G, vessel, mass=mass, @@ -667,6 +806,7 @@ def dissolve_solid_by_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union def dissolve_solid_by_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str, temp: Union[str, float] = 25.0, time: Union[str, float] = "10 min") -> List[Dict[str, Any]]: """按摩尔数溶解固体""" + debug_print(f"🧬 按摩尔数溶解固体: {reagent} ({mol}) → {vessel}") return generate_dissolve_protocol( G, vessel, mol=mol, @@ -678,6 +818,7 @@ def dissolve_solid_by_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str, def dissolve_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float], temp: Union[str, float] = 25.0, time: Union[str, float] = "5 min") -> List[Dict[str, Any]]: """用溶剂溶解""" + debug_print(f"💧 溶剂溶解: {solvent} ({volume}) → {vessel}") return generate_dissolve_protocol( G, vessel, solvent=solvent, @@ -688,6 +829,7 @@ def dissolve_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Unio def dissolve_at_room_temp(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float]) -> List[Dict[str, Any]]: """室温溶解""" + debug_print(f"🌡️ 室温溶解: {solvent} ({volume}) → {vessel}") return generate_dissolve_protocol( G, vessel, solvent=solvent, @@ -699,6 +841,7 @@ def dissolve_at_room_temp(G: nx.DiGraph, vessel: str, solvent: str, volume: Unio def dissolve_with_heating(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float], temp: Union[str, float] = "60 °C", time: Union[str, float] = "15 min") -> List[Dict[str, Any]]: """加热溶解""" + debug_print(f"🔥 加热溶解: {solvent} ({volume}) → {vessel} @ {temp}") return generate_dissolve_protocol( G, vessel, solvent=solvent, @@ -710,33 +853,37 @@ def dissolve_with_heating(G: nx.DiGraph, vessel: str, solvent: str, volume: Unio # 测试函数 def test_dissolve_protocol(): """测试溶解协议的各种参数解析""" - print("=== DISSOLVE PROTOCOL 修复版测试 ===") + debug_print("=== DISSOLVE PROTOCOL 增强版测试 ===") # 测试体积解析 + debug_print("💧 测试体积解析...") volumes = ["10 mL", "?", 10.0, "1 L", "500 μL"] for vol in volumes: result = parse_volume_input(vol) - print(f"体积解析: {vol} → {result}mL") + debug_print(f"📏 体积解析: {vol} → {result}mL") # 测试质量解析 + debug_print("⚖️ 测试质量解析...") masses = ["2.9 g", "?", 2.5, "500 mg"] for mass in masses: result = parse_mass_input(mass) - print(f"质量解析: {mass} → {result}g") + debug_print(f"⚖️ 质量解析: {mass} → {result}g") # 测试温度解析 + debug_print("🌡️ 测试温度解析...") temps = ["60 °C", "room temperature", "?", 25.0, "reflux"] for temp in temps: result = parse_temperature_input(temp) - print(f"温度解析: {temp} → {result}°C") + debug_print(f"🌡️ 温度解析: {temp} → {result}°C") # 测试时间解析 + debug_print("⏱️ 测试时间解析...") times = ["30 min", "1 h", "?", 60.0] for time in times: result = parse_time_input(time) - print(f"时间解析: {time} → {result}s") + debug_print(f"⏱️ 时间解析: {time} → {result}s") - print("✅ 测试完成") + debug_print("✅ 测试完成") if __name__ == "__main__": test_dissolve_protocol() \ No newline at end of file diff --git a/unilabos/compile/evacuateandrefill_protocol.py b/unilabos/compile/evacuateandrefill_protocol.py index c50b88492..cbcf19bb9 100644 --- a/unilabos/compile/evacuateandrefill_protocol.py +++ b/unilabos/compile/evacuateandrefill_protocol.py @@ -1,16 +1,68 @@ import networkx as nx import logging -import uuid # 🔧 移到顶部 +import uuid +import sys from typing import List, Dict, Any, Optional from .pump_protocol import generate_pump_protocol_with_rinsing, generate_pump_protocol # 设置日志 logger = logging.getLogger(__name__) +# 确保输出编码为UTF-8 +if hasattr(sys.stdout, 'reconfigure'): + try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') + except: + pass + def debug_print(message): - """调试输出函数""" - print(f"[EVACUATE_REFILL] {message}", flush=True) - logger.info(f"[EVACUATE_REFILL] {message}") + """调试输出函数 - 支持中文""" + try: + # 确保消息是字符串格式 + safe_message = str(message) + print(f"[抽真空充气] {safe_message}", flush=True) + logger.info(f"[抽真空充气] {safe_message}") + except UnicodeEncodeError: + # 如果编码失败,尝试替换不支持的字符 + safe_message = str(message).encode('utf-8', errors='replace').decode('utf-8') + print(f"[抽真空充气] {safe_message}", flush=True) + logger.info(f"[抽真空充气] {safe_message}") + except Exception as e: + # 最后的安全措施 + fallback_message = f"日志输出错误: {repr(message)}" + print(f"[抽真空充气] {fallback_message}", flush=True) + logger.info(f"[抽真空充气] {fallback_message}") + +def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]: + """创建一个动作日志 - 支持中文和emoji""" + try: + full_message = f"{emoji} {message}" + debug_print(full_message) + logger.info(full_message) + + return { + "action_name": "wait", + "action_kwargs": { + "time": 0.1, + "log_message": full_message, + "progress_message": full_message + } + } + except Exception as e: + # 如果emoji有问题,使用纯文本 + safe_message = f"[日志] {message}" + debug_print(safe_message) + logger.info(safe_message) + + return { + "action_name": "wait", + "action_kwargs": { + "time": 0.1, + "log_message": safe_message, + "progress_message": safe_message + } + } def find_gas_source(G: nx.DiGraph, gas: str) -> str: """ @@ -19,9 +71,10 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str: 2. 气体类型匹配(data.gas_type) 3. 默认气源 """ - debug_print(f"正在查找气体 '{gas}' 的气源...") + debug_print(f"🔍 正在查找气体 '{gas}' 的气源...") # 第一步:通过容器名称匹配 + debug_print(f"📋 方法1: 容器名称匹配...") gas_source_patterns = [ f"gas_source_{gas}", f"gas_{gas}", @@ -32,12 +85,15 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str: f"bottle_{gas}" ] + debug_print(f"🎯 尝试的容器名称: {gas_source_patterns}") + for pattern in gas_source_patterns: if pattern in G.nodes(): - debug_print(f"通过名称匹配找到气源: {pattern}") + debug_print(f"✅ 通过名称找到气源: {pattern}") return pattern # 第二步:通过气体类型匹配 (data.gas_type) + debug_print(f"📋 方法2: 气体类型匹配...") for node_id in G.nodes(): node_data = G.nodes[node_id] node_class = node_data.get('class', '') or '' @@ -52,7 +108,7 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str: gas_type = data.get('gas_type', '') if gas_type.lower() == gas.lower(): - debug_print(f"通过气体类型匹配找到气源: {node_id} (gas_type: {gas_type})") + debug_print(f"✅ 通过气体类型找到气源: {node_id} (气体类型: {gas_type})") return node_id # 检查 config.gas_type @@ -60,10 +116,11 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str: config_gas_type = config.get('gas_type', '') if config_gas_type.lower() == gas.lower(): - debug_print(f"通过配置气体类型匹配找到气源: {node_id} (config.gas_type: {config_gas_type})") + debug_print(f"✅ 通过配置气体类型找到气源: {node_id} (配置气体类型: {config_gas_type})") return node_id # 第三步:查找所有可用的气源设备 + debug_print(f"📋 方法3: 查找可用气源...") available_gas_sources = [] for node_id in G.nodes(): node_data = G.nodes[node_id] @@ -74,12 +131,13 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str: (node_id.startswith('flask_') and any(g in node_id.lower() for g in ['air', 'nitrogen', 'argon']))): data = node_data.get('data', {}) - gas_type = data.get('gas_type', 'unknown') - available_gas_sources.append(f"{node_id} (gas_type: {gas_type})") + gas_type = data.get('gas_type', '未知') + available_gas_sources.append(f"{node_id} (气体类型: {gas_type})") - debug_print(f"可用气源列表: {available_gas_sources}") + debug_print(f"📊 可用气源: {available_gas_sources}") # 第四步:如果找不到特定气体,使用默认的第一个气源 + debug_print(f"📋 方法4: 查找默认气源...") default_gas_sources = [ node for node in G.nodes() if ((G.nodes[node].get('class') or '').find('virtual_gas_source') != -1 @@ -91,11 +149,12 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str: debug_print(f"⚠️ 未找到特定气体 '{gas}',使用默认气源: {default_source}") return default_source - raise ValueError(f"找不到气体 '{gas}' 对应的气源。可用气源: {available_gas_sources}") + debug_print(f"❌ 所有方法都失败了!") + raise ValueError(f"无法找到气体 '{gas}' 的气源。可用气源: {available_gas_sources}") def find_vacuum_pump(G: nx.DiGraph) -> str: """查找真空泵设备""" - debug_print("查找真空泵设备...") + debug_print("🔍 正在查找真空泵...") vacuum_pumps = [] for node in G.nodes(): @@ -106,16 +165,18 @@ def find_vacuum_pump(G: nx.DiGraph) -> str: 'vacuum_pump' in node.lower() or 'vacuum' in node_class.lower()): vacuum_pumps.append(node) + debug_print(f"📋 发现真空泵: {node}") if not vacuum_pumps: - raise ValueError("系统中未找到真空泵设备") + debug_print(f"❌ 系统中未找到真空泵") + raise ValueError("系统中未找到真空泵") - debug_print(f"找到真空泵: {vacuum_pumps[0]}") + debug_print(f"✅ 使用真空泵: {vacuum_pumps[0]}") return vacuum_pumps[0] def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> Optional[str]: """查找与指定容器相连的搅拌器""" - debug_print(f"查找与容器 {vessel} 相连的搅拌器...") + debug_print(f"🔍 正在查找与容器 {vessel} 连接的搅拌器...") stirrer_nodes = [] for node in G.nodes(): @@ -124,24 +185,27 @@ def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> Optional[str]: if 'virtual_stirrer' in node_class or 'stirrer' in node.lower(): stirrer_nodes.append(node) + debug_print(f"📋 发现搅拌器: {node}") + + debug_print(f"📊 找到的搅拌器总数: {len(stirrer_nodes)}") # 检查哪个搅拌器与目标容器相连 for stirrer in stirrer_nodes: if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): - debug_print(f"找到连接的搅拌器: {stirrer}") + debug_print(f"✅ 找到连接的搅拌器: {stirrer}") return stirrer # 如果没有连接的搅拌器,返回第一个可用的 if stirrer_nodes: - debug_print(f"未找到直接连接的搅拌器,使用第一个可用的: {stirrer_nodes[0]}") + debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个可用的: {stirrer_nodes[0]}") return stirrer_nodes[0] - debug_print("未找到搅拌器") + debug_print("❌ 未找到搅拌器") return None def find_vacuum_solenoid_valve(G: nx.DiGraph, vacuum_pump: str) -> Optional[str]: - """查找真空泵相关的电磁阀 - 根据实际连接逻辑""" - debug_print(f"查找真空泵 {vacuum_pump} 相关的电磁阀...") + """查找真空泵相关的电磁阀""" + debug_print(f"🔍 正在查找真空泵 {vacuum_pump} 的电磁阀...") # 查找所有电磁阀 solenoid_valves = [] @@ -151,29 +215,30 @@ def find_vacuum_solenoid_valve(G: nx.DiGraph, vacuum_pump: str) -> Optional[str] if ('solenoid' in node_class.lower() or 'solenoid_valve' in node.lower()): solenoid_valves.append(node) + debug_print(f"📋 发现电磁阀: {node}") - debug_print(f"找到的电磁阀: {solenoid_valves}") + debug_print(f"📊 找到的电磁阀: {solenoid_valves}") - # 🔧 修复:根据实际组态图连接逻辑查找 - # vacuum_pump_1 <- solenoid_valve_1 <- multiway_valve_2 + # 检查连接关系 + debug_print(f"📋 方法1: 检查连接关系...") for solenoid in solenoid_valves: - # 检查电磁阀是否连接到真空泵 if G.has_edge(solenoid, vacuum_pump) or G.has_edge(vacuum_pump, solenoid): - debug_print(f"✓ 找到连接真空泵的电磁阀: {solenoid}") + debug_print(f"✅ 找到连接的真空电磁阀: {solenoid}") return solenoid - # 通过命名规则查找(备选方案) + # 通过命名规则查找 + debug_print(f"📋 方法2: 检查命名规则...") for solenoid in solenoid_valves: if 'vacuum' in solenoid.lower() or solenoid == 'solenoid_valve_1': - debug_print(f"✓ 通过命名规则找到真空电磁阀: {solenoid}") + debug_print(f"✅ 通过命名找到真空电磁阀: {solenoid}") return solenoid debug_print("⚠️ 未找到真空电磁阀") return None def find_gas_solenoid_valve(G: nx.DiGraph, gas_source: str) -> Optional[str]: - """查找气源相关的电磁阀 - 根据实际连接逻辑""" - debug_print(f"查找气源 {gas_source} 相关的电磁阀...") + """查找气源相关的电磁阀""" + debug_print(f"🔍 正在查找气源 {gas_source} 的电磁阀...") # 查找所有电磁阀 solenoid_valves = [] @@ -184,18 +249,20 @@ def find_gas_solenoid_valve(G: nx.DiGraph, gas_source: str) -> Optional[str]: if ('solenoid' in node_class.lower() or 'solenoid_valve' in node.lower()): solenoid_valves.append(node) - # 🔧 修复:根据实际组态图连接逻辑查找 - # gas_source_1 -> solenoid_valve_2 -> multiway_valve_2 + debug_print(f"📊 找到的电磁阀: {solenoid_valves}") + + # 检查连接关系 + debug_print(f"📋 方法1: 检查连接关系...") for solenoid in solenoid_valves: - # 检查气源是否连接到电磁阀 if G.has_edge(gas_source, solenoid) or G.has_edge(solenoid, gas_source): - debug_print(f"✓ 找到连接气源的电磁阀: {solenoid}") + debug_print(f"✅ 找到连接的气源电磁阀: {solenoid}") return solenoid - # 通过命名规则查找(备选方案) + # 通过命名规则查找 + debug_print(f"📋 方法2: 检查命名规则...") for solenoid in solenoid_valves: if 'gas' in solenoid.lower() or solenoid == 'solenoid_valve_2': - debug_print(f"✓ 通过命名规则找到气源电磁阀: {solenoid}") + debug_print(f"✅ 通过命名找到气源电磁阀: {solenoid}") return solenoid debug_print("⚠️ 未找到气源电磁阀") @@ -208,7 +275,7 @@ def generate_evacuateandrefill_protocol( **kwargs ) -> List[Dict[str, Any]]: """ - 生成抽真空和充气操作的动作序列 - 最终修复版本 + 生成抽真空和充气操作的动作序列 - 中文版 Args: G: 设备图 @@ -223,75 +290,113 @@ def generate_evacuateandrefill_protocol( # 硬编码重复次数为 3 repeats = 3 - # 🔧 修复:在函数开始就生成协议ID + # 生成协议ID protocol_id = str(uuid.uuid4()) - debug_print(f"生成协议ID: {protocol_id}") + debug_print(f"🆔 生成协议ID: {protocol_id}") debug_print("=" * 60) - debug_print("开始生成抽真空充气协议") - debug_print(f"输入参数:") - debug_print(f" - vessel: {vessel}") - debug_print(f" - gas: {gas}") - debug_print(f" - repeats: {repeats} (硬编码)") - debug_print(f" - 其他参数: {kwargs}") + debug_print("🧪 开始生成抽真空充气协议") + debug_print(f"📋 原始参数:") + debug_print(f" 🥼 容器: '{vessel}'") + debug_print(f" 💨 气体: '{gas}'") + debug_print(f" 🔄 循环次数: {repeats} (硬编码)") + debug_print(f" 📦 其他参数: {kwargs}") debug_print("=" * 60) action_sequence = [] # === 参数验证和修正 === - debug_print("步骤1: 参数验证和修正...") + debug_print("🔍 步骤1: 参数验证和修正...") + action_sequence.append(create_action_log(f"开始抽真空充气操作 - 容器: {vessel}", "🎬")) + action_sequence.append(create_action_log(f"目标气体: {gas}", "💨")) + action_sequence.append(create_action_log(f"循环次数: {repeats}", "🔄")) # 验证必需参数 if not vessel: - raise ValueError("vessel 参数不能为空") + debug_print("❌ 容器参数不能为空") + raise ValueError("容器参数不能为空") if not gas: - raise ValueError("gas 参数不能为空") + debug_print("❌ 气体参数不能为空") + raise ValueError("气体参数不能为空") if vessel not in G.nodes(): - raise ValueError(f"容器 '{vessel}' 不存在于系统中") + debug_print(f"❌ 容器 '{vessel}' 在系统中不存在") + raise ValueError(f"容器 '{vessel}' 在系统中不存在") + + debug_print("✅ 基本参数验证通过") + action_sequence.append(create_action_log("参数验证通过", "✅")) # 标准化气体名称 + debug_print("🔧 标准化气体名称...") gas_aliases = { 'n2': 'nitrogen', 'ar': 'argon', 'air': 'air', 'o2': 'oxygen', 'co2': 'carbon_dioxide', - 'h2': 'hydrogen' + 'h2': 'hydrogen', + '氮气': 'nitrogen', + '氩气': 'argon', + '空气': 'air', + '氧气': 'oxygen', + '二氧化碳': 'carbon_dioxide', + '氢气': 'hydrogen' } original_gas = gas gas_lower = gas.lower().strip() if gas_lower in gas_aliases: gas = gas_aliases[gas_lower] - debug_print(f"标准化气体名称: {original_gas} -> {gas}") + debug_print(f"🔄 标准化气体名称: {original_gas} -> {gas}") + action_sequence.append(create_action_log(f"气体名称标准化: {original_gas} -> {gas}", "🔄")) - debug_print(f"最终参数: vessel={vessel}, gas={gas}, repeats={repeats}") + debug_print(f"📋 最终参数: 容器={vessel}, 气体={gas}, 重复={repeats}") # === 查找设备 === - debug_print("步骤2: 查找设备...") + debug_print("🔍 步骤2: 查找设备...") + action_sequence.append(create_action_log("正在查找相关设备...", "🔍")) try: vacuum_pump = find_vacuum_pump(G) + action_sequence.append(create_action_log(f"找到真空泵: {vacuum_pump}", "🌪️")) + gas_source = find_gas_source(G, gas) + action_sequence.append(create_action_log(f"找到气源: {gas_source}", "💨")) + vacuum_solenoid = find_vacuum_solenoid_valve(G, vacuum_pump) + if vacuum_solenoid: + action_sequence.append(create_action_log(f"找到真空电磁阀: {vacuum_solenoid}", "🚪")) + else: + action_sequence.append(create_action_log("未找到真空电磁阀", "⚠️")) + gas_solenoid = find_gas_solenoid_valve(G, gas_source) + if gas_solenoid: + action_sequence.append(create_action_log(f"找到气源电磁阀: {gas_solenoid}", "🚪")) + else: + action_sequence.append(create_action_log("未找到气源电磁阀", "⚠️")) + stirrer_id = find_connected_stirrer(G, vessel) + if stirrer_id: + action_sequence.append(create_action_log(f"找到搅拌器: {stirrer_id}", "🌪️")) + else: + action_sequence.append(create_action_log("未找到搅拌器", "⚠️")) - debug_print(f"设备配置:") - debug_print(f" - 真空泵: {vacuum_pump}") - debug_print(f" - 气源: {gas_source}") - debug_print(f" - 真空电磁阀: {vacuum_solenoid}") - debug_print(f" - 气源电磁阀: {gas_solenoid}") - debug_print(f" - 搅拌器: {stirrer_id}") + debug_print(f"📊 设备配置:") + debug_print(f" 🌪️ 真空泵: {vacuum_pump}") + debug_print(f" 💨 气源: {gas_source}") + debug_print(f" 🚪 真空电磁阀: {vacuum_solenoid}") + debug_print(f" 🚪 气源电磁阀: {gas_solenoid}") + debug_print(f" 🌪️ 搅拌器: {stirrer_id}") except Exception as e: debug_print(f"❌ 设备查找失败: {str(e)}") + action_sequence.append(create_action_log(f"设备查找失败: {str(e)}", "❌")) raise ValueError(f"设备查找失败: {str(e)}") # === 参数设置 === - debug_print("步骤3: 参数设置...") + debug_print("🔍 步骤3: 参数设置...") + action_sequence.append(create_action_log("设置操作参数...", "⚙️")) # 根据气体类型调整参数 if gas.lower() in ['nitrogen', 'argon']: @@ -300,87 +405,108 @@ def generate_evacuateandrefill_protocol( PUMP_FLOW_RATE = 2.0 VACUUM_TIME = 30.0 REFILL_TIME = 20.0 - debug_print("惰性气体:使用标准参数") + debug_print("💨 惰性气体: 使用标准参数") + action_sequence.append(create_action_log("检测到惰性气体,使用标准参数", "💨")) elif gas.lower() in ['air', 'oxygen']: VACUUM_VOLUME = 20.0 REFILL_VOLUME = 20.0 PUMP_FLOW_RATE = 1.5 VACUUM_TIME = 45.0 REFILL_TIME = 25.0 - debug_print("活性气体:使用保守参数") + debug_print("🔥 活性气体: 使用保守参数") + action_sequence.append(create_action_log("检测到活性气体,使用保守参数", "🔥")) else: VACUUM_VOLUME = 15.0 REFILL_VOLUME = 15.0 PUMP_FLOW_RATE = 1.0 VACUUM_TIME = 60.0 REFILL_TIME = 30.0 - debug_print("未知气体:使用安全参数") + debug_print("❓ 未知气体: 使用安全参数") + action_sequence.append(create_action_log("未知气体类型,使用安全参数", "❓")) STIR_SPEED = 200.0 - debug_print(f"操作参数:") - debug_print(f" - 抽真空体积: {VACUUM_VOLUME}mL") - debug_print(f" - 充气体积: {REFILL_VOLUME}mL") - debug_print(f" - 泵流速: {PUMP_FLOW_RATE}mL/s") - debug_print(f" - 抽真空时间: {VACUUM_TIME}s") - debug_print(f" - 充气时间: {REFILL_TIME}s") - debug_print(f" - 搅拌速度: {STIR_SPEED}RPM") + debug_print(f"⚙️ 操作参数:") + debug_print(f" 📏 真空体积: {VACUUM_VOLUME}mL") + debug_print(f" 📏 充气体积: {REFILL_VOLUME}mL") + debug_print(f" ⚡ 泵流速: {PUMP_FLOW_RATE}mL/s") + debug_print(f" ⏱️ 真空时间: {VACUUM_TIME}s") + debug_print(f" ⏱️ 充气时间: {REFILL_TIME}s") + debug_print(f" 🌪️ 搅拌速度: {STIR_SPEED}RPM") + + action_sequence.append(create_action_log(f"真空体积: {VACUUM_VOLUME}mL", "📏")) + action_sequence.append(create_action_log(f"充气体积: {REFILL_VOLUME}mL", "📏")) + action_sequence.append(create_action_log(f"泵流速: {PUMP_FLOW_RATE}mL/s", "⚡")) # === 路径验证 === - debug_print("步骤4: 路径验证...") + debug_print("🔍 步骤4: 路径验证...") + action_sequence.append(create_action_log("验证传输路径...", "🛤️")) try: - # 验证抽真空路径: vessel -> vacuum_pump (通过八通阀和电磁阀) + # 验证抽真空路径 if nx.has_path(G, vessel, vacuum_pump): vacuum_path = nx.shortest_path(G, source=vessel, target=vacuum_pump) - debug_print(f"抽真空路径: {' → '.join(vacuum_path)}") + debug_print(f"✅ 真空路径: {' -> '.join(vacuum_path)}") + action_sequence.append(create_action_log(f"真空路径: {' -> '.join(vacuum_path)}", "🛤️")) else: - debug_print(f"⚠️ 抽真空路径不存在,继续执行但可能有问题") + debug_print(f"⚠️ 真空路径不存在,继续执行但可能有问题") + action_sequence.append(create_action_log("真空路径检查: 路径不存在", "⚠️")) - # 验证充气路径: gas_source -> vessel (通过电磁阀和八通阀) + # 验证充气路径 if nx.has_path(G, gas_source, vessel): gas_path = nx.shortest_path(G, source=gas_source, target=vessel) - debug_print(f"充气路径: {' → '.join(gas_path)}") + debug_print(f"✅ 气体路径: {' -> '.join(gas_path)}") + action_sequence.append(create_action_log(f"气体路径: {' -> '.join(gas_path)}", "🛤️")) else: - debug_print(f"⚠️ 充气路径不存在,继续执行但可能有问题") + debug_print(f"⚠️ 气体路径不存在,继续执行但可能有问题") + action_sequence.append(create_action_log("气体路径检查: 路径不存在", "⚠️")) except Exception as e: debug_print(f"⚠️ 路径验证失败: {str(e)},继续执行") + action_sequence.append(create_action_log(f"路径验证失败: {str(e)}", "⚠️")) # === 启动搅拌器 === - debug_print("步骤5: 启动搅拌器...") + debug_print("🔍 步骤5: 启动搅拌器...") if stirrer_id: - debug_print(f"启动搅拌器: {stirrer_id}") + debug_print(f"🌪️ 启动搅拌器: {stirrer_id}") + action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {STIR_SPEED}rpm)", "🌪️")) + action_sequence.append({ "device_id": stirrer_id, "action_name": "start_stir", "action_kwargs": { "vessel": vessel, "stir_speed": STIR_SPEED, - "purpose": "抽真空充气操作前启动搅拌" + "purpose": "抽真空充气前预搅拌" } }) # 等待搅拌稳定 + action_sequence.append(create_action_log("等待搅拌稳定...", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 5.0} }) else: - debug_print("未找到搅拌器,跳过搅拌启动") + debug_print("⚠️ 未找到搅拌器,跳过搅拌器启动") + action_sequence.append(create_action_log("跳过搅拌器启动", "⏭️")) - # === 执行 3 次抽真空-充气循环 === - debug_print("步骤6: 执行抽真空-充气循环...") + # === 执行循环 === + debug_print("🔍 步骤6: 执行抽真空-充气循环...") + action_sequence.append(create_action_log(f"开始 {repeats} 次抽真空-充气循环", "🔄")) for cycle in range(repeats): - debug_print(f"=== 第 {cycle+1}/{repeats} 次循环 ===") + debug_print(f"=== 第 {cycle+1}/{repeats} 轮循环 ===") + action_sequence.append(create_action_log(f"第 {cycle+1}/{repeats} 轮循环开始", "🚀")) # ============ 抽真空阶段 ============ - debug_print(f"抽真空阶段开始") + debug_print(f"🌪️ 抽真空阶段开始") + action_sequence.append(create_action_log("开始抽真空阶段", "🌪️")) # 启动真空泵 - debug_print(f"启动真空泵: {vacuum_pump}") + debug_print(f"🔛 启动真空泵: {vacuum_pump}") + action_sequence.append(create_action_log(f"启动真空泵: {vacuum_pump}", "🔛")) action_sequence.append({ "device_id": vacuum_pump, "action_name": "set_status", @@ -389,17 +515,19 @@ def generate_evacuateandrefill_protocol( # 开启真空电磁阀 if vacuum_solenoid: - debug_print(f"开启真空电磁阀: {vacuum_solenoid}") + debug_print(f"🚪 打开真空电磁阀: {vacuum_solenoid}") + action_sequence.append(create_action_log(f"打开真空电磁阀: {vacuum_solenoid}", "🚪")) action_sequence.append({ "device_id": vacuum_solenoid, "action_name": "set_valve_position", "action_kwargs": {"command": "OPEN"} }) - # 抽真空操作 - 使用液体转移协议 - debug_print(f"抽真空操作: {vessel} → {vacuum_pump}") + # 抽真空操作 + debug_print(f"🌪️ 抽真空操作: {vessel} -> {vacuum_pump}") + action_sequence.append(create_action_log(f"开始抽真空: {vessel} -> {vacuum_pump}", "🌪️")) + try: - vacuum_transfer_actions = generate_pump_protocol_with_rinsing( G=G, from_vessel=vessel, @@ -419,8 +547,10 @@ def generate_evacuateandrefill_protocol( if vacuum_transfer_actions: action_sequence.extend(vacuum_transfer_actions) debug_print(f"✅ 添加了 {len(vacuum_transfer_actions)} 个抽真空动作") + action_sequence.append(create_action_log(f"抽真空协议完成 ({len(vacuum_transfer_actions)} 个操作)", "✅")) else: debug_print("⚠️ 抽真空协议返回空序列,添加手动动作") + action_sequence.append(create_action_log("抽真空协议为空,使用手动等待", "⚠️")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": VACUUM_TIME} @@ -428,13 +558,15 @@ def generate_evacuateandrefill_protocol( except Exception as e: debug_print(f"❌ 抽真空失败: {str(e)}") - # 添加等待时间作为备选 + action_sequence.append(create_action_log(f"抽真空失败: {str(e)}", "❌")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": VACUUM_TIME} }) # 抽真空后等待 + wait_minutes = VACUUM_TIME / 60 + action_sequence.append(create_action_log(f"抽真空后等待 ({wait_minutes:.1f} 分钟)", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": VACUUM_TIME} @@ -442,7 +574,8 @@ def generate_evacuateandrefill_protocol( # 关闭真空电磁阀 if vacuum_solenoid: - debug_print(f"关闭真空电磁阀: {vacuum_solenoid}") + debug_print(f"🚪 关闭真空电磁阀: {vacuum_solenoid}") + action_sequence.append(create_action_log(f"关闭真空电磁阀: {vacuum_solenoid}", "🚪")) action_sequence.append({ "device_id": vacuum_solenoid, "action_name": "set_valve_position", @@ -450,24 +583,28 @@ def generate_evacuateandrefill_protocol( }) # 关闭真空泵 - debug_print(f"关闭真空泵: {vacuum_pump}") + debug_print(f"🔴 停止真空泵: {vacuum_pump}") + action_sequence.append(create_action_log(f"停止真空泵: {vacuum_pump}", "🔴")) action_sequence.append({ "device_id": vacuum_pump, "action_name": "set_status", "action_kwargs": {"string": "OFF"} }) - # 抽真空后等待 + # 阶段间等待 + action_sequence.append(create_action_log("抽真空阶段完成,短暂等待", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 5.0} }) # ============ 充气阶段 ============ - debug_print(f"充气阶段开始") + debug_print(f"💨 充气阶段开始") + action_sequence.append(create_action_log("开始气体充气阶段", "💨")) # 启动气源 - debug_print(f"启动气源: {gas_source}") + debug_print(f"🔛 启动气源: {gas_source}") + action_sequence.append(create_action_log(f"启动气源: {gas_source}", "🔛")) action_sequence.append({ "device_id": gas_source, "action_name": "set_status", @@ -476,17 +613,19 @@ def generate_evacuateandrefill_protocol( # 开启气源电磁阀 if gas_solenoid: - debug_print(f"开启气源电磁阀: {gas_solenoid}") + debug_print(f"🚪 打开气源电磁阀: {gas_solenoid}") + action_sequence.append(create_action_log(f"打开气源电磁阀: {gas_solenoid}", "🚪")) action_sequence.append({ "device_id": gas_solenoid, "action_name": "set_valve_position", "action_kwargs": {"command": "OPEN"} }) - # 充气操作 - 使用液体转移协议 - debug_print(f"充气操作: {gas_source} → {vessel}") + # 充气操作 + debug_print(f"💨 充气操作: {gas_source} -> {vessel}") + action_sequence.append(create_action_log(f"开始气体充气: {gas_source} -> {vessel}", "💨")) + try: - gas_transfer_actions = generate_pump_protocol_with_rinsing( G=G, from_vessel=gas_source, @@ -506,22 +645,26 @@ def generate_evacuateandrefill_protocol( if gas_transfer_actions: action_sequence.extend(gas_transfer_actions) debug_print(f"✅ 添加了 {len(gas_transfer_actions)} 个充气动作") + action_sequence.append(create_action_log(f"气体充气协议完成 ({len(gas_transfer_actions)} 个操作)", "✅")) else: debug_print("⚠️ 充气协议返回空序列,添加手动动作") + action_sequence.append(create_action_log("充气协议为空,使用手动等待", "⚠️")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": REFILL_TIME} }) except Exception as e: - debug_print(f"❌ 充气失败: {str(e)}") - # 添加等待时间作为备选 + debug_print(f"❌ 气体充气失败: {str(e)}") + action_sequence.append(create_action_log(f"气体充气失败: {str(e)}", "❌")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": REFILL_TIME} }) # 充气后等待 + refill_wait_minutes = REFILL_TIME / 60 + action_sequence.append(create_action_log(f"充气后等待 ({refill_wait_minutes:.1f} 分钟)", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": REFILL_TIME} @@ -529,7 +672,8 @@ def generate_evacuateandrefill_protocol( # 关闭气源电磁阀 if gas_solenoid: - debug_print(f"关闭气源电磁阀: {gas_solenoid}") + debug_print(f"🚪 关闭气源电磁阀: {gas_solenoid}") + action_sequence.append(create_action_log(f"关闭气源电磁阀: {gas_solenoid}", "🚪")) action_sequence.append({ "device_id": gas_solenoid, "action_name": "set_valve_position", @@ -537,68 +681,92 @@ def generate_evacuateandrefill_protocol( }) # 关闭气源 - debug_print(f"关闭气源: {gas_source}") + debug_print(f"🔴 停止气源: {gas_source}") + action_sequence.append(create_action_log(f"停止气源: {gas_source}", "🔴")) action_sequence.append({ "device_id": gas_source, "action_name": "set_status", "action_kwargs": {"string": "OFF"} }) - # 等待下一次循环 + # 循环间等待 if cycle < repeats - 1: - debug_print(f"等待下一次循环...") + debug_print(f"⏳ 等待下一个循环...") + action_sequence.append(create_action_log("等待下一个循环...", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 10.0} }) + else: + action_sequence.append(create_action_log(f"第 {cycle+1}/{repeats} 轮循环完成", "✅")) # === 停止搅拌器 === - debug_print("步骤7: 停止搅拌器...") + debug_print("🔍 步骤7: 停止搅拌器...") if stirrer_id: - debug_print(f"停止搅拌器: {stirrer_id}") + debug_print(f"🛑 停止搅拌器: {stirrer_id}") + action_sequence.append(create_action_log(f"停止搅拌器: {stirrer_id}", "🛑")) action_sequence.append({ "device_id": stirrer_id, "action_name": "stop_stir", "action_kwargs": {"vessel": vessel} }) + else: + action_sequence.append(create_action_log("跳过搅拌器停止", "⏭️")) # === 最终等待 === + action_sequence.append(create_action_log("最终稳定等待...", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 10.0} }) # === 总结 === + total_time = (VACUUM_TIME + REFILL_TIME + 25) * repeats + 20 + debug_print("=" * 60) - debug_print(f"抽真空充气协议生成完成") - debug_print(f"总动作数: {len(action_sequence)}") - debug_print(f"处理容器: {vessel}") - debug_print(f"使用气体: {gas}") - debug_print(f"重复次数: {repeats} (硬编码)") + debug_print(f"🎉 抽真空充气协议生成完成") + debug_print(f"📊 协议统计:") + debug_print(f" 📋 总动作数: {len(action_sequence)}") + debug_print(f" ⏱️ 预计总时间: {total_time:.0f}s ({total_time/60:.1f} 分钟)") + debug_print(f" 🥼 处理容器: {vessel}") + debug_print(f" 💨 使用气体: {gas}") + debug_print(f" 🔄 重复次数: {repeats}") debug_print("=" * 60) + # 添加完成日志 + summary_msg = f"抽真空充气协议完成: {vessel} (使用 {gas},{repeats} 次循环)" + action_sequence.append(create_action_log(summary_msg, "🎉")) + return action_sequence # === 便捷函数 === def generate_nitrogen_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]: """生成氮气置换协议""" + debug_print(f"💨 生成氮气置换协议: {vessel}") return generate_evacuateandrefill_protocol(G, vessel, "nitrogen", **kwargs) def generate_argon_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]: """生成氩气置换协议""" + debug_print(f"💨 生成氩气置换协议: {vessel}") return generate_evacuateandrefill_protocol(G, vessel, "argon", **kwargs) def generate_air_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]: """生成空气置换协议""" + debug_print(f"💨 生成空气置换协议: {vessel}") return generate_evacuateandrefill_protocol(G, vessel, "air", **kwargs) +def generate_inert_atmosphere_protocol(G: nx.DiGraph, vessel: str, gas: str = "nitrogen", **kwargs) -> List[Dict[str, Any]]: + """生成惰性气氛协议""" + debug_print(f"🛡️ 生成惰性气氛协议: {vessel} (使用 {gas})") + return generate_evacuateandrefill_protocol(G, vessel, gas, **kwargs) + # 测试函数 def test_evacuateandrefill_protocol(): """测试抽真空充气协议""" - debug_print("=== EVACUATE AND REFILL PROTOCOL 测试 ===") - debug_print("测试完成") + debug_print("=== 抽真空充气协议增强中文版测试 ===") + debug_print("✅ 测试完成") if __name__ == "__main__": test_evacuateandrefill_protocol() \ No newline at end of file diff --git a/unilabos/compile/pump_protocol.py b/unilabos/compile/pump_protocol.py index f214ff484..a54218e30 100644 --- a/unilabos/compile/pump_protocol.py +++ b/unilabos/compile/pump_protocol.py @@ -273,7 +273,6 @@ def generate_pump_protocol( if not pump_backbone: debug_print("PUMP_TRANSFER: 没有泵骨架节点,可能是直接容器连接或只有电磁阀") - # 🔧 对于气体传输,这是正常的,直接返回空序列 return pump_action_sequence if transfer_flowrate == 0: @@ -319,10 +318,31 @@ def generate_pump_protocol( volume_left = volume debug_print(f"PUMP_TRANSFER: 需要 {repeats} 次转移,单次最大体积 {min_transfer_volume} mL") + # 🆕 只在开头打印总体概览 + if repeats > 1: + debug_print(f"🔄 分批转移概览: 总体积 {volume:.2f}mL,需要 {repeats} 次转移") + logger.info(f"🔄 分批转移概览: 总体积 {volume:.2f}mL,需要 {repeats} 次转移") + + # 🔧 创建一个自定义的wait动作,用于在执行时打印日志 + def create_progress_log_action(message: str) -> Dict[str, Any]: + """创建一个特殊的等待动作,在执行时打印进度日志""" + return { + "action_name": "wait", + "action_kwargs": { + "time": 0.1, # 很短的等待时间 + "progress_message": message # 自定义字段,用于进度日志 + } + } + # 生成泵操作序列 for i in range(repeats): current_volume = min(volume_left, min_transfer_volume) + # 🆕 在每次循环开始时添加进度日志 + if repeats > 1: + start_message = f"🚀 准备开始第 {i+1}/{repeats} 次转移: {current_volume:.2f}mL ({from_vessel} → {to_vessel}) 🚰" + pump_action_sequence.append(create_progress_log_action(start_message)) + # 🔧 修复:安全地获取边数据 def get_safe_edge_data(node_a, node_b, key): try: @@ -426,6 +446,16 @@ def get_safe_edge_data(node_a, node_b, key): ]) pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}}) + # 🆕 在每次循环结束时添加完成日志 + if repeats > 1: + remaining_volume = volume_left - current_volume + if remaining_volume > 0: + end_message = f"✅ 第 {i+1}/{repeats} 次转移完成! 剩余 {remaining_volume:.2f}mL 待转移 ⏳" + else: + end_message = f"🎉 第 {i+1}/{repeats} 次转移完成! 全部 {volume:.2f}mL 转移完毕 ✨" + + pump_action_sequence.append(create_progress_log_action(end_message)) + volume_left -= current_volume return pump_action_sequence @@ -437,7 +467,745 @@ def generate_pump_protocol_with_rinsing( to_vessel: str, volume: float = 0.0, amount: str = "", - time: float = 0.0, # 🔧 修复:统一使用 time + time: float = 0.0, # 🔧 修复:统一使用 time + viscous: bool = False, + rinsing_solvent: str = "", + rinsing_volume: float = 0.0, + rinsing_repeats: int = 0, + solid: bool = False, + flowrate: float = 2.5, + transfer_flowrate: float = 0.5, + rate_spec: str = "", + event: str = "", + through: str = "", + **kwargs +) -> List[Dict[str, Any]]: + """ + 原有的同步版本,添加防冲突机制 + """ + + # 添加执行锁,防止并发调用 + import threading + if not hasattr(generate_pump_protocol_with_rinsing, '_lock'): + generate_pump_protocol_with_rinsing._lock = threading.Lock() + + with generate_pump_protocol_with_rinsing._lock: + debug_print("=" * 60) + debug_print(f"PUMP_TRANSFER: 🚀 开始生成协议 (同步版本)") + debug_print(f" 📍 路径: {from_vessel} -> {to_vessel}") + debug_print(f" 🕐 时间戳: {time_module.time()}") + debug_print(f" 🔒 获得执行锁") + debug_print("=" * 60) + + # 短暂延迟,避免快速重复调用 + time_module.sleep(0.01) + + debug_print("🔍 步骤1: 开始体积处理...") + + # 1. 处理体积参数 + final_volume = volume + debug_print(f"📋 初始设置: final_volume = {final_volume}") + + # 🔧 修复:如果volume为0(ROS2传入的空值),从容器读取实际体积 + if volume == 0.0: + debug_print("🎯 检测到 volume=0.0,开始自动体积检测...") + + # 直接从源容器读取实际体积 + actual_volume = get_vessel_liquid_volume(G, from_vessel) + debug_print(f"📖 从容器 '{from_vessel}' 读取到体积: {actual_volume}mL") + + if actual_volume > 0: + final_volume = actual_volume + debug_print(f"✅ 成功设置体积为: {final_volume}mL") + else: + final_volume = 10.0 # 如果读取失败,使用默认值 + logger.warning(f"⚠️ 无法从容器读取体积,使用默认值: {final_volume}mL") + else: + debug_print(f"📌 体积非零,直接使用: {final_volume}mL") + + # 处理 amount 参数 + if amount and amount.strip(): + debug_print(f"🔍 检测到 amount 参数: '{amount}',开始解析...") + parsed_volume = _parse_amount_to_volume(amount) + debug_print(f"📖 从 amount 解析得到体积: {parsed_volume}mL") + + if parsed_volume > 0: + final_volume = parsed_volume + debug_print(f"✅ 使用从 amount 解析的体积: {final_volume}mL") + elif parsed_volume == 0.0 and amount.lower().strip() == "all": + debug_print("🎯 检测到 amount='all',从容器读取全部体积...") + actual_volume = get_vessel_liquid_volume(G, from_vessel) + if actual_volume > 0: + final_volume = actual_volume + debug_print(f"✅ amount='all',设置体积为: {final_volume}mL") + + # 最终体积验证 + debug_print(f"🔍 步骤2: 最终体积验证...") + if final_volume <= 0: + logger.error(f"❌ 体积无效: {final_volume}mL") + final_volume = 10.0 + logger.warning(f"⚠️ 强制设置为默认值: {final_volume}mL") + + debug_print(f"✅ 最终确定体积: {final_volume}mL") + + # 2. 处理流速参数 + debug_print(f"🔍 步骤3: 处理流速参数...") + debug_print(f" - 原始 flowrate: {flowrate}") + debug_print(f" - 原始 transfer_flowrate: {transfer_flowrate}") + + final_flowrate = flowrate if flowrate > 0 else 2.5 + final_transfer_flowrate = transfer_flowrate if transfer_flowrate > 0 else 0.5 + + if flowrate <= 0: + logger.warning(f"⚠️ flowrate <= 0,修正为: {final_flowrate}mL/s") + if transfer_flowrate <= 0: + logger.warning(f"⚠️ transfer_flowrate <= 0,修正为: {final_transfer_flowrate}mL/s") + + debug_print(f"✅ 修正后流速: flowrate={final_flowrate}mL/s, transfer_flowrate={final_transfer_flowrate}mL/s") + + # 3. 根据时间计算流速 + if time > 0 and final_volume > 0: + debug_print(f"🔍 步骤4: 根据时间计算流速...") + calculated_flowrate = final_volume / time + debug_print(f" - 计算得到流速: {calculated_flowrate}mL/s") + + if flowrate <= 0 or flowrate == 2.5: + final_flowrate = min(calculated_flowrate, 10.0) + debug_print(f" - 调整 flowrate 为: {final_flowrate}mL/s") + if transfer_flowrate <= 0 or transfer_flowrate == 0.5: + final_transfer_flowrate = min(calculated_flowrate, 5.0) + debug_print(f" - 调整 transfer_flowrate 为: {final_transfer_flowrate}mL/s") + + # 4. 根据速度规格调整 + if rate_spec: + debug_print(f"🔍 步骤5: 根据速度规格调整...") + debug_print(f" - 速度规格: '{rate_spec}'") + + if rate_spec == "dropwise": + final_flowrate = min(final_flowrate, 0.1) + final_transfer_flowrate = min(final_transfer_flowrate, 0.1) + debug_print(f" - dropwise模式,流速调整为: {final_flowrate}mL/s") + elif rate_spec == "slowly": + final_flowrate = min(final_flowrate, 0.5) + final_transfer_flowrate = min(final_transfer_flowrate, 0.3) + debug_print(f" - slowly模式,流速调整为: {final_flowrate}mL/s") + elif rate_spec == "quickly": + final_flowrate = max(final_flowrate, 5.0) + final_transfer_flowrate = max(final_transfer_flowrate, 2.0) + debug_print(f" - quickly模式,流速调整为: {final_flowrate}mL/s") + + try: + # 🆕 修复:在这里调用带有循环日志的generate_pump_protocol_with_loop_logging函数 + pump_action_sequence = generate_pump_protocol_with_loop_logging( + G, from_vessel, to_vessel, final_volume, + final_flowrate, final_transfer_flowrate + ) + + debug_print(f"🔓 释放执行锁") + return pump_action_sequence + + except Exception as e: + logger.error(f"❌ 协议生成失败: {str(e)}") + return [ + { + "device_id": "system", + "action_name": "log_message", + "action_kwargs": { + "message": f"❌ 协议生成失败: {str(e)}" + } + } + ] + + +def generate_pump_protocol_with_loop_logging( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + volume: float, + flowrate: float = 2.5, + transfer_flowrate: float = 0.5, +) -> List[Dict[str, Any]]: + """ + 生成泵操作的动作序列 - 带循环日志版本 + 🔧 修复:正确处理包含电磁阀的路径,并在合适时机打印循环日志 + """ + pump_action_sequence = [] + nodes = G.nodes(data=True) + + # 验证输入参数 + if volume <= 0: + logger.error(f"无效的体积参数: {volume}mL") + return pump_action_sequence + + if flowrate <= 0: + flowrate = 2.5 + logger.warning(f"flowrate <= 0,使用默认值 {flowrate}mL/s") + + if transfer_flowrate <= 0: + transfer_flowrate = 0.5 + logger.warning(f"transfer_flowrate <= 0,使用默认值 {transfer_flowrate}mL/s") + + # 验证容器存在 + if from_vessel not in G.nodes(): + logger.error(f"源容器 '{from_vessel}' 不存在") + return pump_action_sequence + + if to_vessel not in G.nodes(): + logger.error(f"目标容器 '{to_vessel}' 不存在") + return pump_action_sequence + + try: + shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel) + debug_print(f"PUMP_TRANSFER: 路径 {from_vessel} -> {to_vessel}: {shortest_path}") + except nx.NetworkXNoPath: + logger.error(f"无法找到从 '{from_vessel}' 到 '{to_vessel}' 的路径") + return pump_action_sequence + + # 🔧 关键修复:正确构建泵骨架,排除容器和电磁阀 + pump_backbone = [] + for node in shortest_path: + # 跳过起始和结束容器 + if node == from_vessel or node == to_vessel: + continue + + # 跳过电磁阀(电磁阀不参与泵操作) + node_data = G.nodes.get(node, {}) + node_class = node_data.get("class", "") or "" + if ("solenoid" in node_class.lower() or "solenoid_valve" in node.lower()): + debug_print(f"PUMP_TRANSFER: 跳过电磁阀 {node}") + continue + + # 只包含多通阀和泵 + if ("multiway" in node_class.lower() or "valve" in node_class.lower() or "pump" in node_class.lower()): + pump_backbone.append(node) + + debug_print(f"PUMP_TRANSFER: 过滤后的泵骨架: {pump_backbone}") + + if not pump_backbone: + debug_print("PUMP_TRANSFER: 没有泵骨架节点,可能是直接容器连接或只有电磁阀") + return pump_action_sequence + + if transfer_flowrate == 0: + transfer_flowrate = flowrate + + try: + pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone) + except Exception as e: + debug_print(f"PUMP_TRANSFER: 构建泵-阀门映射失败: {str(e)}") + return pump_action_sequence + + if not pumps_from_node: + debug_print("PUMP_TRANSFER: 没有可用的泵映射") + return pump_action_sequence + + # 🔧 修复:安全地获取最小转移体积 + try: + min_transfer_volumes = [] + for node in pump_backbone: + if node in pumps_from_node: + pump_node = pumps_from_node[node] + if pump_node in nodes: + pump_config = nodes[pump_node].get("config", {}) + max_volume = pump_config.get("max_volume") + if max_volume is not None: + min_transfer_volumes.append(max_volume) + + if min_transfer_volumes: + min_transfer_volume = min(min_transfer_volumes) + else: + min_transfer_volume = 25.0 # 默认值 + debug_print(f"PUMP_TRANSFER: 无法获取泵的最大体积,使用默认值: {min_transfer_volume}mL") + except Exception as e: + debug_print(f"PUMP_TRANSFER: 获取最小转移体积失败: {str(e)}") + min_transfer_volume = 25.0 # 默认值 + + repeats = int(np.ceil(volume / min_transfer_volume)) + + if repeats > 1 and (from_vessel.startswith("pump") or to_vessel.startswith("pump")): + logger.error("Cannot transfer volume larger than min_transfer_volume between two pumps.") + return pump_action_sequence + + volume_left = volume + debug_print(f"PUMP_TRANSFER: 需要 {repeats} 次转移,单次最大体积 {min_transfer_volume} mL") + + # 🆕 只在开头打印总体概览 + if repeats > 1: + debug_print(f"🔄 分批转移概览: 总体积 {volume:.2f}mL,需要 {repeats} 次转移") + logger.info(f"🔄 分批转移概览: 总体积 {volume:.2f}mL,需要 {repeats} 次转移") + + # 🔧 创建一个自定义的wait动作,用于在执行时打印日志 + def create_progress_log_action(message: str) -> Dict[str, Any]: + """创建一个特殊的等待动作,在执行时打印进度日志""" + return { + "action_name": "wait", + "action_kwargs": { + "time": 0.1, # 很短的等待时间 + "progress_message": message # 自定义字段,用于进度日志 + } + } + + # 生成泵操作序列 + for i in range(repeats): + current_volume = min(volume_left, min_transfer_volume) + + # 🆕 在每次循环开始时添加进度日志 + if repeats > 1: + start_message = f"🚀 准备开始第 {i+1}/{repeats} 次转移: {current_volume:.2f}mL ({from_vessel} → {to_vessel}) 🚰" + pump_action_sequence.append(create_progress_log_action(start_message)) + + # 🔧 修复:安全地获取边数据 + def get_safe_edge_data(node_a, node_b, key): + try: + edge_data = G.get_edge_data(node_a, node_b) + if edge_data and "port" in edge_data: + port_data = edge_data["port"] + if isinstance(port_data, dict) and key in port_data: + return port_data[key] + return "default" + except Exception as e: + debug_print(f"PUMP_TRANSFER: 获取边数据失败 {node_a}->{node_b}: {str(e)}") + return "default" + + # 从源容器吸液 + if not from_vessel.startswith("pump") and pump_backbone: + first_pump_node = pump_backbone[0] + if first_pump_node in valve_from_node and first_pump_node in pumps_from_node: + port_command = get_safe_edge_data(first_pump_node, from_vessel, first_pump_node) + pump_action_sequence.extend([ + { + "device_id": valve_from_node[first_pump_node], + "action_name": "set_valve_position", + "action_kwargs": { + "command": port_command + } + }, + { + "device_id": pumps_from_node[first_pump_node], + "action_name": "set_position", + "action_kwargs": { + "position": float(current_volume), + "max_velocity": transfer_flowrate + } + } + ]) + pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}}) + + # 泵间转移 + for nodeA, nodeB in zip(pump_backbone[:-1], pump_backbone[1:]): + if nodeA in valve_from_node and nodeB in valve_from_node and nodeA in pumps_from_node and nodeB in pumps_from_node: + port_a = get_safe_edge_data(nodeA, nodeB, nodeA) + port_b = get_safe_edge_data(nodeB, nodeA, nodeB) + + pump_action_sequence.append([ + { + "device_id": valve_from_node[nodeA], + "action_name": "set_valve_position", + "action_kwargs": { + "command": port_a + } + }, + { + "device_id": valve_from_node[nodeB], + "action_name": "set_valve_position", + "action_kwargs": { + "command": port_b + } + } + ]) + pump_action_sequence.append([ + { + "device_id": pumps_from_node[nodeA], + "action_name": "set_position", + "action_kwargs": { + "position": 0.0, + "max_velocity": transfer_flowrate + } + }, + { + "device_id": pumps_from_node[nodeB], + "action_name": "set_position", + "action_kwargs": { + "position": float(current_volume), + "max_velocity": transfer_flowrate + } + } + ]) + pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}}) + + # 排液到目标容器 + if not to_vessel.startswith("pump") and pump_backbone: + last_pump_node = pump_backbone[-1] + if last_pump_node in valve_from_node and last_pump_node in pumps_from_node: + port_command = get_safe_edge_data(last_pump_node, to_vessel, last_pump_node) + pump_action_sequence.extend([ + { + "device_id": valve_from_node[last_pump_node], + "action_name": "set_valve_position", + "action_kwargs": { + "command": port_command + } + }, + { + "device_id": pumps_from_node[last_pump_node], + "action_name": "set_position", + "action_kwargs": { + "position": 0.0, + "max_velocity": flowrate + } + } + ]) + pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}}) + + # 🆕 在每次循环结束时添加完成日志 + if repeats > 1: + remaining_volume = volume_left - current_volume + if remaining_volume > 0: + end_message = f"✅ 第 {i+1}/{repeats} 次转移完成! 剩余 {remaining_volume:.2f}mL 待转移 ⏳" + else: + end_message = f"🎉 第 {i+1}/{repeats} 次转移完成! 全部 {volume:.2f}mL 转移完毕 ✨" + + pump_action_sequence.append(create_progress_log_action(end_message)) + + volume_left -= current_volume + + return pump_action_sequence + + +def generate_pump_protocol_with_rinsing( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + volume: float = 0.0, + amount: str = "", + time: float = 0.0, # 🔧 修复:统一使用 time + viscous: bool = False, + rinsing_solvent: str = "", + rinsing_volume: float = 0.0, + rinsing_repeats: int = 0, + solid: bool = False, + flowrate: float = 2.5, + transfer_flowrate: float = 0.5, + rate_spec: str = "", + event: str = "", + through: str = "", + **kwargs +) -> List[Dict[str, Any]]: + """ + 增强兼容性的泵转移协议生成器,支持自动体积检测 + """ + 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" - time: {time}") # 🔧 修复:统一使用 time + 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 time > 0 and final_volume > 0: # 🔧 修复:统一使用 time + 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: + # 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 + + +async def generate_pump_protocol_with_rinsing_async( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + volume: float = 0.0, + amount: str = "", + time: float = 0.0, + viscous: bool = False, + rinsing_solvent: str = "", + rinsing_volume: float = 0.0, + rinsing_repeats: int = 0, + solid: bool = False, + flowrate: float = 2.5, + transfer_flowrate: float = 0.5, + rate_spec: str = "", + event: str = "", + through: str = "", + **kwargs +) -> List[Dict[str, Any]]: + """ + 异步版本的泵转移协议生成器,避免并发问题 + """ + debug_print("=" * 60) + debug_print(f"PUMP_TRANSFER: 🚀 开始生成协议 (异步版本)") + debug_print(f" 📍 路径: {from_vessel} -> {to_vessel}") + debug_print(f" 🕐 时间戳: {time_module.time()}") + debug_print("=" * 60) + + # 添加唯一标识符 + protocol_id = f"pump_transfer_{int(time_module.time() * 1000000)}" + debug_print(f"📋 协议ID: {protocol_id}") + + # 调用原有的同步版本 + result = generate_pump_protocol_with_rinsing( + G, from_vessel, to_vessel, volume, amount, time, viscous, + rinsing_solvent, rinsing_volume, rinsing_repeats, solid, + flowrate, transfer_flowrate, rate_spec, event, through, **kwargs + ) + + # 为每个动作添加唯一标识 + for i, action in enumerate(result): + if isinstance(action, dict): + action['_protocol_id'] = protocol_id + action['_action_sequence'] = i + action['_timestamp'] = time_module.time() + + debug_print(f"📊 协议 {protocol_id} 生成完成,共 {len(result)} 个动作") + return result + +# 保持原有的同步版本兼容性 +def generate_pump_protocol_with_rinsing( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + volume: float = 0.0, + amount: str = "", + time: float = 0.0, viscous: bool = False, rinsing_solvent: str = "", rinsing_volume: float = 0.0, @@ -451,116 +1219,118 @@ def generate_pump_protocol_with_rinsing( **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(f" 📊 原始参数:") - debug_print(f" - volume: {volume} (类型: {type(volume)})") - debug_print(f" - amount: '{amount}'") - debug_print(f" - time: {time}") # 🔧 修复:统一使用 time - 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}") + # 添加执行锁,防止并发调用 + import threading + if not hasattr(generate_pump_protocol_with_rinsing, '_lock'): + generate_pump_protocol_with_rinsing._lock = threading.Lock() - # 🔧 修复:如果volume为0(ROS2传入的空值),从容器读取实际体积 - if volume == 0.0: - debug_print("🎯 检测到 volume=0.0,开始自动体积检测...") + 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) - # 直接从源容器读取实际体积 - actual_volume = get_vessel_liquid_volume(G, from_vessel) - debug_print(f"📖 从容器 '{from_vessel}' 读取到体积: {actual_volume}mL") + # 短暂延迟,避免快速重复调用 + time_module.sleep(0.01) - 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") + debug_print("🔍 步骤1: 开始体积处理...") - 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',从容器读取全部体积...") + # 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"✅ 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 time > 0 and final_volume > 0: # 🔧 修复:统一使用 time - debug_print(f"🔍 步骤4: 根据时间计算流速...") - calculated_flowrate = final_volume / time - debug_print(f" - 计算得到流速: {calculated_flowrate}mL/s") + debug_print(f"✅ 成功设置体积为: {final_volume}mL") + else: + final_volume = 10.0 # 如果读取失败,使用默认值 + logger.warning(f"⚠️ 无法从容器读取体积,使用默认值: {final_volume}mL") + else: + debug_print(f"📌 体积非零,直接使用: {final_volume}mL") - 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}'") + # 处理 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") - 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") + # 最终体积验证 + 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: 处理冲洗参数...") @@ -569,9 +1339,9 @@ def generate_pump_protocol_with_rinsing( # 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") + # logger.warning(f"⚠️ rinsing_volume <= 0,修正为: {final_rinsing_volume}mL") # if rinsing_repeats <= 0: - # debug_print(f"⚠️ rinsing_repeats <= 0,修正为: {final_rinsing_repeats}次") + # logger.warning(f"⚠️ rinsing_repeats <= 0,修正为: {final_rinsing_repeats}次") # # 根据物理属性调整冲洗参数 # if viscous or solid: @@ -726,7 +1496,7 @@ async def generate_pump_protocol_with_rinsing_async( rate_spec: str = "", event: str = "", through: str = "", - **kwargs + **kwargs ) -> List[Dict[str, Any]]: """ 异步版本的泵转移协议生成器,避免并发问题 @@ -891,58 +1661,56 @@ def generate_pump_protocol_with_rinsing( final_flowrate = max(final_flowrate, 5.0) final_transfer_flowrate = max(final_transfer_flowrate, 2.0) debug_print(f" - quickly模式,流速调整为: {final_flowrate}mL/s") + + # # 5. 处理冲洗参数 + # debug_print(f"🔍 步骤6: 处理冲洗参数...") + # final_rinsing_solvent = rinsing_solvent + # final_rinsing_volume = rinsing_volume if rinsing_volume > 0 else 5.0 + # final_rinsing_repeats = rinsing_repeats if rinsing_repeats > 0 else 2 + + # if rinsing_volume <= 0: + # logger.warning(f"⚠️ rinsing_volume <= 0,修正为: {final_rinsing_volume}mL") + # if rinsing_repeats <= 0: + # logger.warning(f"⚠️ rinsing_repeats <= 0,修正为: {final_rinsing_repeats}次") + + # # 根据物理属性调整冲洗参数 + # if viscous or solid: + # final_rinsing_repeats = max(final_rinsing_repeats, 3) + # final_rinsing_volume = max(final_rinsing_volume, 10.0) + # debug_print(f"🧪 粘稠/固体物质,调整冲洗参数:{final_rinsing_repeats}次,{final_rinsing_volume}mL") + + try: + pump_action_sequence = generate_pump_protocol( + G, from_vessel, to_vessel, final_volume, + flowrate, transfer_flowrate + ) - # # 5. 处理冲洗参数 - # debug_print(f"🔍 步骤6: 处理冲洗参数...") - # final_rinsing_solvent = rinsing_solvent - # final_rinsing_volume = rinsing_volume if rinsing_volume > 0 else 5.0 - # final_rinsing_repeats = rinsing_repeats if rinsing_repeats > 0 else 2 - - # if rinsing_volume <= 0: - # logger.warning(f"⚠️ rinsing_volume <= 0,修正为: {final_rinsing_volume}mL") - # if rinsing_repeats <= 0: - # logger.warning(f"⚠️ rinsing_repeats <= 0,修正为: {final_rinsing_repeats}次") - - # # 根据物理属性调整冲洗参数 - # if viscous or solid: - # final_rinsing_repeats = max(final_rinsing_repeats, 3) - # final_rinsing_volume = max(final_rinsing_volume, 10.0) - # debug_print(f"🧪 粘稠/固体物质,调整冲洗参数:{final_rinsing_repeats}次,{final_rinsing_volume}mL") - - try: - pump_action_sequence = generate_pump_protocol( - G, from_vessel, to_vessel, final_volume, - flowrate, transfer_flowrate - ) - - # 为每个动作添加唯一标识 - # for i, action in enumerate(pump_action_sequence): - # if isinstance(action, dict): - # action['_protocol_id'] = protocol_id - # action['_action_sequence'] = i - # elif isinstance(action, list): - # for j, sub_action in enumerate(action): - # if isinstance(sub_action, dict): - # sub_action['_protocol_id'] = protocol_id - # sub_action['_action_sequence'] = f"{i}_{j}" + # 为每个动作添加唯一标识 + # 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 + 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 + except Exception as e: + logger.error(f"❌ 协议生成失败: {str(e)}") + return [ + { + "device_id": "system", + "action_name": "log_message", + "action_kwargs": { + "message": f"❌ 协议生成失败: {str(e)}" } - ] + } + ] def _parse_amount_to_volume(amount: str) -> float: """解析 amount 字符串为体积""" diff --git a/unilabos/compile/separate_protocol.py b/unilabos/compile/separate_protocol.py index 07d9ab4e8..258c37d28 100644 --- a/unilabos/compile/separate_protocol.py +++ b/unilabos/compile/separate_protocol.py @@ -1,15 +1,67 @@ import networkx as nx import re import logging +import sys from typing import List, Dict, Any, Union from .pump_protocol import generate_pump_protocol_with_rinsing logger = logging.getLogger(__name__) +# 确保输出编码为UTF-8 +if hasattr(sys.stdout, 'reconfigure'): + try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') + except: + pass + def debug_print(message): - """调试输出""" - print(f"[SEPARATE] {message}", flush=True) - logger.info(f"[SEPARATE] {message}") + """调试输出函数 - 支持中文""" + try: + # 确保消息是字符串格式 + safe_message = str(message) + print(f"[分离协议] {safe_message}", flush=True) + logger.info(f"[分离协议] {safe_message}") + except UnicodeEncodeError: + # 如果编码失败,尝试替换不支持的字符 + safe_message = str(message).encode('utf-8', errors='replace').decode('utf-8') + print(f"[分离协议] {safe_message}", flush=True) + logger.info(f"[分离协议] {safe_message}") + except Exception as e: + # 最后的安全措施 + fallback_message = f"日志输出错误: {repr(message)}" + print(f"[分离协议] {fallback_message}", flush=True) + logger.info(f"[分离协议] {fallback_message}") + +def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]: + """创建一个动作日志 - 支持中文和emoji""" + try: + full_message = f"{emoji} {message}" + debug_print(full_message) + logger.info(full_message) + + return { + "action_name": "wait", + "action_kwargs": { + "time": 0.1, + "log_message": full_message, + "progress_message": full_message + } + } + except Exception as e: + # 如果emoji有问题,使用纯文本 + safe_message = f"[日志] {message}" + debug_print(safe_message) + logger.info(safe_message) + + return { + "action_name": "wait", + "action_kwargs": { + "time": 0.1, + "log_message": safe_message, + "progress_message": safe_message + } + } def parse_volume_input(volume_input: Union[str, float]) -> float: """ @@ -22,52 +74,58 @@ def parse_volume_input(volume_input: Union[str, float]) -> float: float: 体积(毫升) """ if isinstance(volume_input, (int, float)): + debug_print(f"📏 体积输入为数值: {volume_input}") return float(volume_input) if not volume_input or not str(volume_input).strip(): + debug_print(f"⚠️ 体积输入为空,返回 0.0mL") return 0.0 volume_str = str(volume_input).lower().strip() - debug_print(f"解析体积输入: '{volume_str}'") + debug_print(f"🔍 解析体积输入: '{volume_str}'") # 处理未知体积 - if volume_str in ['?', 'unknown', 'tbd', 'to be determined']: + if volume_str in ['?', 'unknown', 'tbd', 'to be determined', '未知', '待定']: default_volume = 100.0 # 默认100mL - debug_print(f"检测到未知体积,使用默认值: {default_volume}mL") + debug_print(f"❓ 检测到未知体积,使用默认值: {default_volume}mL") return default_volume # 移除空格并提取数字和单位 volume_clean = re.sub(r'\s+', '', volume_str) # 匹配数字和单位的正则表达式 - match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean) + match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter|毫升|升|微升)?', volume_clean) if not match: - debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值100mL") + debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值 100mL") return 100.0 value = float(match.group(1)) unit = match.group(2) or 'ml' # 默认单位为毫升 # 转换为毫升 - if unit in ['l', 'liter']: + if unit in ['l', 'liter', '升']: volume = value * 1000.0 # L -> mL - elif unit in ['μl', 'ul', 'microliter']: + debug_print(f"🔄 体积转换: {value}L -> {volume}mL") + elif unit in ['μl', 'ul', 'microliter', '微升']: volume = value / 1000.0 # μL -> mL - else: # ml, milliliter 或默认 + debug_print(f"🔄 体积转换: {value}μL -> {volume}mL") + else: # ml, milliliter, 毫升 或默认 volume = value # 已经是mL + debug_print(f"✅ 体积已为毫升单位: {volume}mL") - debug_print(f"体积转换: {value}{unit} → {volume}mL") return volume def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: - """查找溶剂容器""" + """查找溶剂容器,支持多种匹配模式""" if not solvent or not solvent.strip(): + debug_print("⏭️ 未指定溶剂,跳过溶剂容器查找") return "" - debug_print(f"查找溶剂 '{solvent}' 的容器...") + debug_print(f"🔍 正在查找溶剂 '{solvent}' 的容器...") # 🔧 方法1:直接搜索 data.reagent_name 和 config.reagent + debug_print(f"📋 方法1: 搜索试剂字段...") for node in G.nodes(): node_data = G.nodes[node].get('data', {}) node_type = G.nodes[node].get('type', '') @@ -80,16 +138,17 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: # 精确匹配 if reagent_name == solvent.lower() or config_reagent == solvent.lower(): - debug_print(f"✅ 通过reagent字段找到容器: {node}") + debug_print(f"✅ 通过试剂字段精确匹配找到容器: {node}") return node # 模糊匹配 if (solvent.lower() in reagent_name and reagent_name) or \ (solvent.lower() in config_reagent and config_reagent): - debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node}") + debug_print(f"✅ 通过试剂字段模糊匹配找到容器: {node}") return node # 🔧 方法2:常见的容器命名规则 + debug_print(f"📋 方法2: 使用命名规则...") solvent_clean = solvent.lower().replace(' ', '_').replace('-', '_') possible_names = [ f"flask_{solvent_clean}", @@ -99,9 +158,14 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: f"{solvent_clean}_bottle", f"solvent_{solvent_clean}", f"reagent_{solvent_clean}", - f"reagent_bottle_{solvent_clean}" + f"reagent_bottle_{solvent_clean}", + f"reagent_bottle_1", # 通用试剂瓶 + f"reagent_bottle_2", + f"reagent_bottle_3" ] + debug_print(f"🎯 尝试的容器名称: {possible_names[:5]}... (共 {len(possible_names)} 个)") + for name in possible_names: if name in G.nodes(): node_type = G.nodes[name].get('type', '') @@ -110,53 +174,94 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: return name # 🔧 方法3:使用第一个试剂瓶作为备选 + debug_print(f"📋 方法3: 查找备用试剂瓶...") for node_id in G.nodes(): node_data = G.nodes[node_id] if (node_data.get('type') == 'container' and ('reagent' in node_id.lower() or 'bottle' in node_id.lower())): - debug_print(f"⚠️ 未找到专用容器,使用备选容器: {node_id}") + debug_print(f"⚠️ 未找到专用容器,使用备用容器: {node_id}") return node_id - debug_print(f"⚠️ 未找到溶剂 '{solvent}' 的容器") + debug_print(f"❌ 无法找到溶剂 '{solvent}' 的容器") return "" def find_separator_device(G: nx.DiGraph, vessel: str) -> str: - """查找分离器设备""" - debug_print(f"查找容器 '{vessel}' 对应的分离器设备...") + """查找分离器设备,支持多种查找方式""" + debug_print(f"🔍 正在查找容器 '{vessel}' 的分离器设备...") # 方法1:查找连接到容器的分离器设备 + debug_print(f"📋 方法1: 检查连接的分离器...") + separator_nodes = [] for node in G.nodes(): node_class = G.nodes[node].get('class', '').lower() if 'separator' in node_class: + separator_nodes.append(node) + debug_print(f"📋 发现分离器设备: {node}") + # 检查是否连接到目标容器 if G.has_edge(node, vessel) or G.has_edge(vessel, node): debug_print(f"✅ 找到连接的分离器: {node}") return node + debug_print(f"📊 找到的分离器总数: {len(separator_nodes)}") + # 方法2:根据命名规则查找 + debug_print(f"📋 方法2: 使用命名规则...") possible_names = [ f"{vessel}_controller", f"{vessel}_separator", vessel, # 容器本身可能就是分离器 "separator_1", - "virtual_separator" + "virtual_separator", + "liquid_handler_1", # 液体处理器也可能用于分离 + "controller_1" ] + debug_print(f"🎯 尝试的分离器名称: {possible_names}") + for name in possible_names: if name in G.nodes(): node_class = G.nodes[name].get('class', '').lower() - if 'separator' in node_class: + if 'separator' in node_class or 'controller' in node_class: debug_print(f"✅ 通过命名规则找到分离器: {name}") return name # 方法3:查找第一个分离器设备 + debug_print(f"📋 方法3: 使用第一个可用分离器...") + if separator_nodes: + debug_print(f"⚠️ 使用第一个分离器设备: {separator_nodes[0]}") + return separator_nodes[0] + + debug_print(f"❌ 未找到分离器设备") + return "" + +def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str: + """查找连接到指定容器的搅拌器""" + debug_print(f"🔍 正在查找与容器 {vessel} 连接的搅拌器...") + + stirrer_nodes = [] for node in G.nodes(): - node_class = G.nodes[node].get('class', '').lower() - if 'separator' in node_class: - debug_print(f"⚠️ 使用第一个分离器设备: {node}") - return node + node_data = G.nodes[node] + node_class = node_data.get('class', '') or '' + + if 'stirrer' in node_class.lower(): + stirrer_nodes.append(node) + debug_print(f"📋 发现搅拌器: {node}") + + debug_print(f"📊 找到的搅拌器总数: {len(stirrer_nodes)}") - debug_print(f"⚠️ 未找到分离器设备") + # 检查哪个搅拌器与目标容器相连 + for stirrer in stirrer_nodes: + if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): + debug_print(f"✅ 找到连接的搅拌器: {stirrer}") + return stirrer + + # 如果没有连接的搅拌器,返回第一个可用的 + if stirrer_nodes: + debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个可用的: {stirrer_nodes[0]}") + return stirrer_nodes[0] + + debug_print("❌ 未找到搅拌器") return "" def generate_separate_protocol( @@ -185,7 +290,7 @@ def generate_separate_protocol( **kwargs ) -> List[Dict[str, Any]]: """ - 生成分离操作的协议序列 - 修复版 + 生成分离操作的协议序列 - 增强中文版 支持XDL参数格式: - vessel: 分离容器(必需) @@ -206,26 +311,31 @@ def generate_separate_protocol( """ debug_print("=" * 60) - debug_print("开始生成分离协议 - 修复版") - debug_print(f"原始参数:") - debug_print(f" - vessel: '{vessel}'") - debug_print(f" - purpose: '{purpose}'") - debug_print(f" - product_phase: '{product_phase}'") - debug_print(f" - solvent: '{solvent}'") - debug_print(f" - volume: {volume} (类型: {type(volume)})") - debug_print(f" - repeats: {repeats}") - debug_print(f" - product_vessel: '{product_vessel}'") - debug_print(f" - waste_vessel: '{waste_vessel}'") + debug_print("🧪 开始生成分离协议 - 增强中文版") + debug_print(f"📋 原始参数:") + debug_print(f" 🥼 容器: '{vessel}'") + debug_print(f" 🎯 分离目的: '{purpose}'") + debug_print(f" 📊 产物相: '{product_phase}'") + debug_print(f" 💧 溶剂: '{solvent}'") + debug_print(f" 📏 体积: {volume} (类型: {type(volume)})") + debug_print(f" 🔄 重复次数: {repeats}") + debug_print(f" 🎯 产物容器: '{product_vessel}'") + debug_print(f" 🗑️ 废液容器: '{waste_vessel}'") + debug_print(f" 📦 其他参数: {kwargs}") debug_print("=" * 60) action_sequence = [] # === 参数验证和标准化 === - debug_print("步骤1: 参数验证和标准化...") + debug_print("🔍 步骤1: 参数验证和标准化...") + action_sequence.append(create_action_log(f"开始分离操作 - 容器: {vessel}", "🎬")) + action_sequence.append(create_action_log(f"分离目的: {purpose}", "🧪")) + action_sequence.append(create_action_log(f"产物相: {product_phase}", "📊")) # 统一容器参数 final_vessel = vessel or separation_vessel if not final_vessel: + debug_print("❌ 必须指定分离容器") raise ValueError("必须指定分离容器 (vessel 或 separation_vessel)") final_to_vessel = to_vessel or product_vessel @@ -237,14 +347,18 @@ def generate_separate_protocol( # 🔧 修复:确保repeats至少为1 if repeats <= 0: repeats = 1 - debug_print(f"⚠️ repeats参数 <= 0,自动设置为1") + debug_print(f"⚠️ 重复次数参数 <= 0,自动设置为 1") + + debug_print(f"🔧 标准化后的参数:") + debug_print(f" 🥼 分离容器: '{final_vessel}'") + debug_print(f" 🎯 产物容器: '{final_to_vessel}'") + debug_print(f" 🗑️ 废液容器: '{final_waste_vessel}'") + debug_print(f" 📏 溶剂体积: {final_volume}mL") + debug_print(f" 🔄 重复次数: {repeats}") - debug_print(f"标准化参数:") - debug_print(f" - 分离容器: '{final_vessel}'") - debug_print(f" - 产物容器: '{final_to_vessel}'") - debug_print(f" - 废液容器: '{final_waste_vessel}'") - debug_print(f" - 溶剂体积: {final_volume}mL") - debug_print(f" - 重复次数: {repeats}") + action_sequence.append(create_action_log(f"分离容器: {final_vessel}", "🧪")) + action_sequence.append(create_action_log(f"溶剂体积: {final_volume}mL", "📏")) + action_sequence.append(create_action_log(f"重复次数: {repeats}", "🔄")) # 验证必需参数 if not purpose: @@ -254,66 +368,147 @@ def generate_separate_protocol( if purpose not in ["wash", "extract", "separate"]: debug_print(f"⚠️ 未知的分离目的 '{purpose}',使用默认值 'separate'") purpose = "separate" + action_sequence.append(create_action_log(f"未知目的,使用: {purpose}", "⚠️")) if product_phase not in ["top", "bottom"]: debug_print(f"⚠️ 未知的产物相 '{product_phase}',使用默认值 'top'") product_phase = "top" + action_sequence.append(create_action_log(f"未知相别,使用: {product_phase}", "⚠️")) debug_print("✅ 参数验证通过") + action_sequence.append(create_action_log("参数验证通过", "✅")) # === 查找设备 === - debug_print("步骤2: 查找设备...") + debug_print("🔍 步骤2: 查找设备...") + action_sequence.append(create_action_log("正在查找相关设备...", "🔍")) # 查找分离器设备 separator_device = find_separator_device(G, final_vessel) - if not separator_device: - debug_print("⚠️ 未找到分离器设备,可能无法执行分离操作") + if separator_device: + action_sequence.append(create_action_log(f"找到分离器设备: {separator_device}", "🧪")) + else: + debug_print("⚠️ 未找到分离器设备,可能无法执行分离") + action_sequence.append(create_action_log("未找到分离器设备", "⚠️")) + + # 查找搅拌器 + stirrer_device = find_connected_stirrer(G, final_vessel) + if stirrer_device: + action_sequence.append(create_action_log(f"找到搅拌器: {stirrer_device}", "🌪️")) + else: + action_sequence.append(create_action_log("未找到搅拌器", "⚠️")) # 查找溶剂容器(如果需要) solvent_vessel = "" if solvent and solvent.strip(): solvent_vessel = find_solvent_vessel(G, solvent) + if solvent_vessel: + action_sequence.append(create_action_log(f"找到溶剂容器: {solvent_vessel}", "💧")) + else: + action_sequence.append(create_action_log(f"未找到溶剂容器: {solvent}", "⚠️")) - debug_print(f"设备映射:") - debug_print(f" - 分离器设备: '{separator_device}'") - debug_print(f" - 溶剂容器: '{solvent_vessel}'") + debug_print(f"📊 设备配置:") + debug_print(f" 🧪 分离器设备: '{separator_device}'") + debug_print(f" 🌪️ 搅拌器设备: '{stirrer_device}'") + debug_print(f" 💧 溶剂容器: '{solvent_vessel}'") # === 执行分离流程 === - debug_print("步骤3: 执行分离流程...") + debug_print("🔍 步骤3: 执行分离流程...") + action_sequence.append(create_action_log("开始分离工作流程", "🎯")) try: for repeat_idx in range(repeats): - debug_print(f"3.{repeat_idx+1}: 第 {repeat_idx+1}/{repeats} 次分离") + cycle_num = repeat_idx + 1 + debug_print(f"🔄 第{cycle_num}轮: 开始分离循环 {cycle_num}/{repeats}") + action_sequence.append(create_action_log(f"分离循环 {cycle_num}/{repeats} 开始", "🔄")) # 步骤3.1: 添加溶剂(如果需要) if solvent_vessel and final_volume > 0: - debug_print(f"3.{repeat_idx+1}.1: 添加溶剂 {solvent} ({final_volume}mL)") + debug_print(f"🔄 第{cycle_num}轮 步骤1: 添加溶剂 {solvent} ({final_volume}mL)") + action_sequence.append(create_action_log(f"向分离容器添加 {final_volume}mL {solvent}", "💧")) - # 使用pump protocol添加溶剂 - pump_actions = generate_pump_protocol_with_rinsing( - G=G, - from_vessel=solvent_vessel, - to_vessel=final_vessel, - volume=final_volume, - amount="", - time=0.0, - viscous=False, - rinsing_solvent="", - rinsing_volume=0.0, - rinsing_repeats=0, - solid=False, - flowrate=2.5, - transfer_flowrate=0.5, - rate_spec="", - event="", - through="", - **kwargs - ) - action_sequence.extend(pump_actions) - debug_print(f"✅ 溶剂添加完成,添加了 {len(pump_actions)} 个动作") + try: + # 使用pump protocol添加溶剂 + pump_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=solvent_vessel, + to_vessel=final_vessel, + volume=final_volume, + amount="", + time=0.0, + viscous=False, + rinsing_solvent="", + rinsing_volume=0.0, + rinsing_repeats=0, + solid=False, + flowrate=2.5, + transfer_flowrate=0.5, + rate_spec="", + event="", + through="", + **kwargs + ) + action_sequence.extend(pump_actions) + debug_print(f"✅ 溶剂添加完成,添加了 {len(pump_actions)} 个动作") + action_sequence.append(create_action_log(f"溶剂转移完成 ({len(pump_actions)} 个操作)", "✅")) + + except Exception as e: + debug_print(f"❌ 溶剂添加失败: {str(e)}") + action_sequence.append(create_action_log(f"溶剂添加失败: {str(e)}", "❌")) + else: + debug_print(f"🔄 第{cycle_num}轮 步骤1: 无需添加溶剂") + action_sequence.append(create_action_log("无需添加溶剂", "⏭️")) - # 步骤3.2: 执行分离操作 + # 步骤3.2: 启动搅拌(如果有搅拌器) + if stirrer_device and stir_time > 0: + debug_print(f"🔄 第{cycle_num}轮 步骤2: 开始搅拌 ({stir_speed}rpm,持续 {stir_time}s)") + action_sequence.append(create_action_log(f"开始搅拌: {stir_speed}rpm,持续 {stir_time}s", "🌪️")) + + action_sequence.append({ + "device_id": stirrer_device, + "action_name": "start_stir", + "action_kwargs": { + "vessel": final_vessel, + "stir_speed": stir_speed, + "purpose": f"分离混合 - {purpose}" + } + }) + + # 搅拌等待 + stir_minutes = stir_time / 60 + action_sequence.append(create_action_log(f"搅拌中,持续 {stir_minutes:.1f} 分钟", "⏱️")) + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": stir_time} + }) + + # 停止搅拌 + action_sequence.append(create_action_log("停止搅拌器", "🛑")) + action_sequence.append({ + "device_id": stirrer_device, + "action_name": "stop_stir", + "action_kwargs": {"vessel": final_vessel} + }) + + else: + debug_print(f"🔄 第{cycle_num}轮 步骤2: 无需搅拌") + action_sequence.append(create_action_log("无需搅拌", "⏭️")) + + # 步骤3.3: 静置分层 + if settling_time > 0: + debug_print(f"🔄 第{cycle_num}轮 步骤3: 静置分层 ({settling_time}s)") + settling_minutes = settling_time / 60 + action_sequence.append(create_action_log(f"静置分层 ({settling_minutes:.1f} 分钟)", "⚖️")) + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": settling_time} + }) + else: + debug_print(f"🔄 第{cycle_num}轮 步骤3: 未指定静置时间") + action_sequence.append(create_action_log("未指定静置时间", "⏭️")) + + # 步骤3.4: 执行分离操作 if separator_device: - debug_print(f"3.{repeat_idx+1}.2: 执行分离操作") + debug_print(f"🔄 第{cycle_num}轮 步骤4: 执行分离操作") + action_sequence.append(create_action_log(f"执行分离: 收集{product_phase}相", "🧪")) # 调用分离器设备的separate方法 separate_action = { @@ -330,31 +525,44 @@ def generate_separate_protocol( "solvent_volume": final_volume, "through": through, "repeats": 1, # 每次调用只做一次分离 - "stir_time": stir_time, + "stir_time": 0, # 已经在上面完成 "stir_speed": stir_speed, - "settling_time": settling_time + "settling_time": 0 # 已经在上面完成 } } action_sequence.append(separate_action) - debug_print(f"✅ 分离操作添加完成") + debug_print(f"✅ 分离操作已添加") + action_sequence.append(create_action_log("分离操作完成", "✅")) + + # 收集结果 + if final_to_vessel: + action_sequence.append(create_action_log(f"产物 ({product_phase}相) 收集到: {final_to_vessel}", "📦")) + if final_waste_vessel: + action_sequence.append(create_action_log(f"废相收集到: {final_waste_vessel}", "🗑️")) else: - debug_print(f"3.{repeat_idx+1}.2: 无分离器设备,跳过分离操作") + debug_print(f"🔄 第{cycle_num}轮 步骤4: 无分离器设备,跳过分离") + action_sequence.append(create_action_log("无分离器设备可用", "❌")) # 添加等待时间模拟分离 action_sequence.append({ "action_name": "wait", - "action_kwargs": {"time": stir_time + settling_time} + "action_kwargs": {"time": 10.0} }) - # 等待间隔(除了最后一次) + # 循环间等待(除了最后一次) if repeat_idx < repeats - 1: + debug_print(f"🔄 第{cycle_num}轮: 等待下一次循环...") + action_sequence.append(create_action_log("等待下一次循环...", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 5} }) + else: + action_sequence.append(create_action_log(f"分离循环 {cycle_num}/{repeats} 完成", "🌟")) except Exception as e: - debug_print(f"⚠️ 分离流程执行失败: {str(e)}") + debug_print(f"❌ 分离工作流程执行失败: {str(e)}") + action_sequence.append(create_action_log(f"分离工作流程失败: {str(e)}", "❌")) # 添加错误日志 action_sequence.append({ "device_id": "system", @@ -365,22 +573,31 @@ def generate_separate_protocol( }) # === 最终结果 === + total_time = (stir_time + settling_time + 15) * repeats # 估算总时间 + debug_print("=" * 60) - debug_print(f"✅ 分离协议生成完成") - debug_print(f"📊 总动作数: {len(action_sequence)}") - debug_print(f"📋 处理总结:") - debug_print(f" - 分离容器: {final_vessel}") - debug_print(f" - 分离目的: {purpose}") - debug_print(f" - 产物相: {product_phase}") - debug_print(f" - 重复次数: {repeats}") + debug_print(f"🎉 分离协议生成完成") + debug_print(f"📊 协议统计:") + debug_print(f" 📋 总动作数: {len(action_sequence)}") + debug_print(f" ⏱️ 预计总时间: {total_time:.0f}s ({total_time/60:.1f} 分钟)") + debug_print(f" 🥼 分离容器: {final_vessel}") + debug_print(f" 🎯 分离目的: {purpose}") + debug_print(f" 📊 产物相: {product_phase}") + debug_print(f" 🔄 重复次数: {repeats}") if solvent: - debug_print(f" - 溶剂: {solvent} ({final_volume}mL)") + debug_print(f" 💧 溶剂: {solvent} ({final_volume}mL)") if final_to_vessel: - debug_print(f" - 产物容器: {final_to_vessel}") + debug_print(f" 🎯 产物容器: {final_to_vessel}") if final_waste_vessel: - debug_print(f" - 废液容器: {final_waste_vessel}") + debug_print(f" 🗑️ 废液容器: {final_waste_vessel}") debug_print("=" * 60) + # 添加完成日志 + summary_msg = f"分离协议完成: {final_vessel} ({purpose},{repeats} 次循环)" + if solvent: + summary_msg += f",使用 {final_volume}mL {solvent}" + action_sequence.append(create_action_log(summary_msg, "🎉")) + return action_sequence # === 便捷函数 === @@ -388,6 +605,7 @@ def generate_separate_protocol( def separate_phases_only(G: nx.DiGraph, vessel: str, product_phase: str = "top", product_vessel: str = "", waste_vessel: str = "") -> List[Dict[str, Any]]: """仅进行相分离(不添加溶剂)""" + debug_print(f"⚡ 快速相分离: {vessel} ({product_phase}相)") return generate_separate_protocol( G, vessel=vessel, purpose="separate", @@ -399,6 +617,7 @@ def separate_phases_only(G: nx.DiGraph, vessel: str, product_phase: str = "top", def wash_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float], product_phase: str = "top", repeats: int = 1) -> List[Dict[str, Any]]: """用溶剂洗涤""" + debug_print(f"🧽 用{solvent}洗涤: {vessel} ({repeats} 次)") return generate_separate_protocol( G, vessel=vessel, purpose="wash", @@ -411,6 +630,7 @@ def wash_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[st def extract_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float], product_phase: str = "bottom", repeats: int = 3) -> List[Dict[str, Any]]: """用溶剂萃取""" + debug_print(f"🧪 用{solvent}萃取: {vessel} ({repeats} 次)") return generate_separate_protocol( G, vessel=vessel, purpose="extract", @@ -423,6 +643,7 @@ def extract_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union def separate_aqueous_organic(G: nx.DiGraph, vessel: str, organic_phase: str = "top", product_vessel: str = "", waste_vessel: str = "") -> List[Dict[str, Any]]: """水-有机相分离""" + debug_print(f"💧 水-有机相分离: {vessel} (有机相: {organic_phase})") return generate_separate_protocol( G, vessel=vessel, purpose="separate", @@ -434,15 +655,16 @@ def separate_aqueous_organic(G: nx.DiGraph, vessel: str, organic_phase: str = "t # 测试函数 def test_separate_protocol(): """测试分离协议的各种参数解析""" - print("=== SEPARATE PROTOCOL 增强版测试 ===") + debug_print("=== 分离协议增强中文版测试 ===") # 测试体积解析 - volumes = ["200 mL", "?", 100.0, "1 L", "500 μL"] + debug_print("🧪 测试体积解析...") + volumes = ["200 mL", "?", 100.0, "1 L", "500 μL", "未知", "50毫升"] for vol in volumes: result = parse_volume_input(vol) - print(f"体积解析: {vol} → {result}mL") + debug_print(f"📊 体积解析结果: {vol} -> {result}mL") - print("✅ 测试完成") + debug_print("✅ 测试完成") if __name__ == "__main__": test_separate_protocol() diff --git a/unilabos/devices/virtual/virtual_multiway_valve.py b/unilabos/devices/virtual/virtual_multiway_valve.py index c12aa7fe6..468175cee 100644 --- a/unilabos/devices/virtual/virtual_multiway_valve.py +++ b/unilabos/devices/virtual/virtual_multiway_valve.py @@ -101,7 +101,7 @@ def set_position(self, command: Union[int, str]): self._target_position = pos # 模拟阀门切换时间 - switch_time = abs(self._current_position - pos) * 0.5 # 每个位置0.1秒 + switch_time = abs(self._current_position - pos) * 0.5 # 每个位置0.5秒 if switch_time > 0: self.logger.info(f"⏱️ 阀门移动中... 预计用时: {switch_time:.1f}秒 🔄") @@ -172,32 +172,32 @@ def get_valve_position(self) -> int: def is_at_position(self, position: int) -> bool: """检查是否在指定位置 🎯""" result = self._current_position == position - self.logger.debug(f"🎯 位置检查: 当前={self._current_position}, 目标={position}, 匹配={result}") + # 删除debug日志:self.logger.debug(f"🎯 位置检查: 当前={self._current_position}, 目标={position}, 匹配={result}") return result def is_at_pump_position(self) -> bool: """检查是否在transfer pump位置 🚰""" result = self._current_position == 0 - pump_status = "是" if result else "否" - self.logger.debug(f"🚰 泵位置检查: {pump_status} (当前位置: {self._current_position})") + # 删除debug日志:pump_status = "是" if result else "否" + # 删除debug日志:self.logger.debug(f"🚰 泵位置检查: {pump_status} (当前位置: {self._current_position})") return result def is_at_port(self, port_number: int) -> bool: """检查是否在指定端口位置 🔌""" result = self._current_position == port_number - port_status = "是" if result else "否" - self.logger.debug(f"🔌 端口{port_number}检查: {port_status} (当前位置: {self._current_position})") + # 删除debug日志:port_status = "是" if result else "否" + # 删除debug日志:self.logger.debug(f"🔌 端口{port_number}检查: {port_status} (当前位置: {self._current_position})") return result def get_available_positions(self) -> list: """获取可用位置列表 📋""" positions = list(range(0, self.max_positions + 1)) - self.logger.debug(f"📋 可用位置: {positions}") + # 删除debug日志:self.logger.debug(f"📋 可用位置: {positions}") return positions def get_available_ports(self) -> Dict[int, str]: """获取可用端口映射 🗺️""" - self.logger.debug(f"🗺️ 端口映射: {self.position_map}") + # 删除debug日志:self.logger.debug(f"🗺️ 端口映射: {self.position_map}") return self.position_map.copy() def reset(self): @@ -229,7 +229,7 @@ def get_flow_path(self) -> str: else: flow_path = f"🔌 端口 {self._current_position} 已连接 ({current_port})" - self.logger.debug(f"🌊 当前流路: {flow_path}") + # 删除debug日志:self.logger.debug(f"🌊 当前流路: {flow_path}") return flow_path def get_info(self) -> dict: @@ -247,7 +247,7 @@ def get_info(self) -> dict: "position_map": self.position_map } - self.logger.debug(f"📊 阀门信息: 位置={self._current_position}, 状态={self._status}, 端口={self.get_current_port()}") + # 删除debug日志:self.logger.debug(f"📊 阀门信息: 位置={self._current_position}, 状态={self._status}, 端口={self.get_current_port()}") return info def __str__(self): @@ -264,7 +264,7 @@ def set_valve_position(self, command: Union[int, str]): Args: command: 目标位置 (0-8) 或位置字符串 """ - self.logger.debug(f"🎯 兼容性调用: set_valve_position({command})") + # 删除debug日志:self.logger.debug(f"🎯 兼容性调用: set_valve_position({command})") return self.set_position(command) diff --git a/unilabos/devices/virtual/virtual_transferpump.py b/unilabos/devices/virtual/virtual_transferpump.py index a2cba9c4b..7d80744c3 100644 --- a/unilabos/devices/virtual/virtual_transferpump.py +++ b/unilabos/devices/virtual/virtual_transferpump.py @@ -12,7 +12,7 @@ class VirtualPumpMode(Enum): class VirtualTransferPump: - """虚拟转移泵类 - 模拟泵的基本功能,无需实际硬件""" + """虚拟转移泵类 - 模拟泵的基本功能,无需实际硬件 🚰""" def __init__(self, device_id: str = None, config: dict = None, **kwargs): """ @@ -42,20 +42,31 @@ def __init__(self, device_id: str = None, config: dict = None, **kwargs): self._max_velocity = 5.0 # float self._current_volume = 0.0 # float + # 🚀 新增:快速模式设置 - 大幅缩短执行时间 + self._fast_mode = True # 是否启用快速模式 + self._fast_move_time = 1.0 # 快速移动时间(秒) + self._fast_dispense_time = 1.0 # 快速喷射时间(秒) + self.logger = logging.getLogger(f"VirtualTransferPump.{self.device_id}") + + print(f"🚰 === 虚拟转移泵 {self.device_id} 已创建 === ✨") + print(f"💨 快速模式: {'启用' if self._fast_mode else '禁用'} | 移动时间: {self._fast_move_time}s | 喷射时间: {self._fast_dispense_time}s") + print(f"📊 最大容量: {self.max_volume}mL | 端口: {self.port}") async def initialize(self) -> bool: - """初始化虚拟泵""" - self.logger.info(f"Initializing virtual pump {self.device_id}") + """初始化虚拟泵 🚀""" + self.logger.info(f"🔧 初始化虚拟转移泵 {self.device_id} ✨") self._status = "Idle" self._position = 0.0 self._current_volume = 0.0 + self.logger.info(f"✅ 转移泵 {self.device_id} 初始化完成 🚰") return True async def cleanup(self) -> bool: - """清理虚拟泵""" - self.logger.info(f"Cleaning up virtual pump {self.device_id}") + """清理虚拟泵 🧹""" + self.logger.info(f"🧹 清理虚拟转移泵 {self.device_id} 🔚") self._status = "Idle" + self.logger.info(f"✅ 转移泵 {self.device_id} 清理完成 💤") return True # 基本属性 @@ -65,12 +76,12 @@ def status(self) -> str: @property def position(self) -> float: - """当前柱塞位置 (ml)""" + """当前柱塞位置 (ml) 📍""" return self._position @property def current_volume(self) -> float: - """当前注射器中的体积 (ml)""" + """当前注射器中的体积 (ml) 💧""" return self._current_volume @property @@ -82,22 +93,50 @@ def transfer_rate(self) -> float: return self._transfer_rate def set_max_velocity(self, velocity: float): - """设置最大速度 (ml/s)""" + """设置最大速度 (ml/s) 🌊""" self._max_velocity = max(0.1, min(50.0, velocity)) # 限制在合理范围内 - self.logger.info(f"Set max velocity to {self._max_velocity} ml/s") + self.logger.info(f"🌊 设置最大速度为 {self._max_velocity} mL/s") def get_status(self) -> str: - """获取泵状态""" + """获取泵状态 📋""" return self._status async def _simulate_operation(self, duration: float): - """模拟操作延时""" + """模拟操作延时 ⏱️""" self._status = "Busy" await asyncio.sleep(duration) self._status = "Idle" def _calculate_duration(self, volume: float, velocity: float = None) -> float: - """计算操作持续时间""" + """ + 计算操作持续时间 ⏰ + 🚀 快速模式:保留计算逻辑用于日志显示,但实际使用固定的快速时间 + """ + if velocity is None: + velocity = self._max_velocity + + # 📊 计算理论时间(用于日志显示) + theoretical_duration = abs(volume) / velocity + + # 🚀 如果启用快速模式,使用固定的快速时间 + if self._fast_mode: + # 根据操作类型选择快速时间 + if abs(volume) > 0.1: # 大于0.1mL的操作 + actual_duration = self._fast_move_time + else: # 很小的操作 + actual_duration = 0.5 + + self.logger.debug(f"⚡ 快速模式: 理论时间 {theoretical_duration:.2f}s → 实际时间 {actual_duration:.2f}s") + return actual_duration + else: + # 正常模式使用理论时间 + return theoretical_duration + + def _calculate_display_duration(self, volume: float, velocity: float = None) -> float: + """ + 计算显示用的持续时间(用于日志) 📊 + 这个函数返回理论计算时间,用于日志显示 + """ if velocity is None: velocity = self._max_velocity return abs(volume) / velocity @@ -105,7 +144,7 @@ def _calculate_duration(self, volume: float, velocity: float = None) -> float: # 新的set_position方法 - 专门用于SetPumpPosition动作 async def set_position(self, position: float, max_velocity: float = None): """ - 移动到绝对位置 - 专门用于SetPumpPosition动作 + 移动到绝对位置 - 专门用于SetPumpPosition动作 🎯 Args: position (float): 目标位置 (ml) @@ -122,56 +161,107 @@ async def set_position(self, position: float, max_velocity: float = None): # 限制位置在有效范围内 target_position = max(0.0, min(float(self.max_volume), target_position)) - # 计算移动距离和时间 + # 计算移动距离 volume_to_move = abs(target_position - self._position) - duration = self._calculate_duration(volume_to_move, velocity) - self.logger.info(f"SET_POSITION: Moving to {target_position} ml (current: {self._position} ml), velocity: {velocity} ml/s") + # 📊 计算显示用的时间(用于日志) + display_duration = self._calculate_display_duration(volume_to_move, velocity) + + # ⚡ 计算实际执行时间(快速模式) + actual_duration = self._calculate_duration(volume_to_move, velocity) + + # 🎯 确定操作类型和emoji + if target_position > self._position: + operation_type = "吸液" + operation_emoji = "📥" + elif target_position < self._position: + operation_type = "排液" + operation_emoji = "📤" + else: + operation_type = "保持" + operation_emoji = "📍" - # 模拟移动过程 - start_position = self._position - steps = 10 if duration > 0.1 else 1 # 如果移动距离很小,只用1步 - step_duration = duration / steps if steps > 1 else duration + self.logger.info(f"🎯 SET_POSITION: {operation_type} {operation_emoji}") + self.logger.info(f" 📍 位置: {self._position:.2f}mL → {target_position:.2f}mL (移动 {volume_to_move:.2f}mL)") + self.logger.info(f" 🌊 速度: {velocity:.2f} mL/s") + self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s") - for i in range(steps + 1): - # 计算当前位置和进度 - progress = (i / steps) * 100 if steps > 0 else 100 - current_pos = start_position + (target_position - start_position) * (i / steps) if steps > 0 else target_position + if self._fast_mode: + self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s") + + # 🚀 模拟移动过程 + if volume_to_move > 0.01: # 只有当移动距离足够大时才显示进度 + start_position = self._position + steps = 5 if actual_duration > 0.5 else 2 # 根据实际时间调整步数 + step_duration = actual_duration / steps - # 更新状态 - self._status = "Moving" if i < steps else "Idle" - self._position = current_pos - self._current_volume = current_pos + self.logger.info(f"🚀 开始{operation_type}... {operation_emoji}") - # 等待一小步时间 - if i < steps and step_duration > 0: - await asyncio.sleep(step_duration) + for i in range(steps + 1): + # 计算当前位置和进度 + progress = (i / steps) * 100 if steps > 0 else 100 + current_pos = start_position + (target_position - start_position) * (i / steps) if steps > 0 else target_position + + # 更新状态 + if i < steps: + self._status = f"{operation_type}中" + status_emoji = "🔄" + else: + self._status = "Idle" + status_emoji = "✅" + + self._position = current_pos + self._current_volume = current_pos + + # 显示进度(每25%或最后一步) + if i == 0: + self.logger.debug(f" 🔄 {operation_type}开始: {progress:.0f}%") + elif progress >= 50 and i == steps // 2: + self.logger.debug(f" 🔄 {operation_type}进度: {progress:.0f}%") + elif i == steps: + self.logger.info(f" ✅ {operation_type}完成: {progress:.0f}% | 当前位置: {current_pos:.2f}mL") + + # 等待一小步时间 + if i < steps and step_duration > 0: + await asyncio.sleep(step_duration) + else: + # 移动距离很小,直接完成 + self._position = target_position + self._current_volume = target_position + self.logger.info(f" 📍 微调完成: {target_position:.2f}mL") # 确保最终位置准确 self._position = target_position self._current_volume = target_position self._status = "Idle" - self.logger.info(f"SET_POSITION: Reached position {self._position} ml, current volume: {self._current_volume} ml") + # 📊 最终状态日志 + if volume_to_move > 0.01: + self.logger.info(f"🎉 SET_POSITION 完成! 📍 最终位置: {self._position:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL") # 返回符合action定义的结果 return { "success": True, - "message": f"Successfully moved to position {self._position} ml" + "message": f"✅ 成功移动到位置 {self._position:.2f}mL ({operation_type})", + "final_position": self._position, + "final_volume": self._current_volume, + "operation_type": operation_type } except Exception as e: - error_msg = f"Failed to set position: {str(e)}" + error_msg = f"❌ 设置位置失败: {str(e)}" self.logger.error(error_msg) return { "success": False, - "message": error_msg + "message": error_msg, + "final_position": self._position, + "final_volume": self._current_volume } # 其他泵操作方法 async def pull_plunger(self, volume: float, velocity: float = None): """ - 拉取柱塞(吸液) + 拉取柱塞(吸液) 📥 Args: volume (float): 要拉取的体积 (ml) @@ -181,23 +271,29 @@ async def pull_plunger(self, volume: float, velocity: float = None): actual_volume = new_position - self._position if actual_volume <= 0: - self.logger.warning("Cannot pull - already at maximum volume") + self.logger.warning("⚠️ 无法吸液 - 已达到最大容量") return - duration = self._calculate_duration(actual_volume, velocity) + display_duration = self._calculate_display_duration(actual_volume, velocity) + actual_duration = self._calculate_duration(actual_volume, velocity) + + self.logger.info(f"📥 开始吸液: {actual_volume:.2f}mL") + self.logger.info(f" 📍 位置: {self._position:.2f}mL → {new_position:.2f}mL") + self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s") - self.logger.info(f"Pulling {actual_volume} ml (from {self._position} to {new_position})") + if self._fast_mode: + self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s") - await self._simulate_operation(duration) + await self._simulate_operation(actual_duration) self._position = new_position self._current_volume = new_position - self.logger.info(f"Pulled {actual_volume} ml, current volume: {self._current_volume} ml") + self.logger.info(f"✅ 吸液完成: {actual_volume:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL") async def push_plunger(self, volume: float, velocity: float = None): """ - 推出柱塞(排液) + 推出柱塞(排液) 📤 Args: volume (float): 要推出的体积 (ml) @@ -207,35 +303,44 @@ async def push_plunger(self, volume: float, velocity: float = None): actual_volume = self._position - new_position if actual_volume <= 0: - self.logger.warning("Cannot push - already at minimum volume") + self.logger.warning("⚠️ 无法排液 - 已达到最小容量") return - duration = self._calculate_duration(actual_volume, velocity) + display_duration = self._calculate_display_duration(actual_volume, velocity) + actual_duration = self._calculate_duration(actual_volume, velocity) - self.logger.info(f"Pushing {actual_volume} ml (from {self._position} to {new_position})") + self.logger.info(f"📤 开始排液: {actual_volume:.2f}mL") + self.logger.info(f" 📍 位置: {self._position:.2f}mL → {new_position:.2f}mL") + self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s") - await self._simulate_operation(duration) + if self._fast_mode: + self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s") + + await self._simulate_operation(actual_duration) self._position = new_position self._current_volume = new_position - self.logger.info(f"Pushed {actual_volume} ml, current volume: {self._current_volume} ml") + self.logger.info(f"✅ 排液完成: {actual_volume:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL") # 便捷操作方法 async def aspirate(self, volume: float, velocity: float = None): - """吸液操作""" + """吸液操作 📥""" await self.pull_plunger(volume, velocity) async def dispense(self, volume: float, velocity: float = None): - """排液操作""" + """排液操作 📤""" await self.push_plunger(volume, velocity) async def transfer(self, volume: float, aspirate_velocity: float = None, dispense_velocity: float = None): - """转移操作(先吸后排)""" + """转移操作(先吸后排) 🔄""" + self.logger.info(f"🔄 开始转移操作: {volume:.2f}mL") + # 吸液 await self.aspirate(volume, aspirate_velocity) # 短暂停顿 + self.logger.debug("⏸️ 短暂停顿...") await asyncio.sleep(0.1) # 排液 diff --git a/unilabos/ros/nodes/presets/protocol_node.py b/unilabos/ros/nodes/presets/protocol_node.py index f8921dadf..033cbd3b4 100644 --- a/unilabos/ros/nodes/presets/protocol_node.py +++ b/unilabos/ros/nodes/presets/protocol_node.py @@ -211,7 +211,7 @@ async def execute_protocol(goal_handle: ServerGoalHandle): # 逐步执行工作流 step_results = [] for i, action in enumerate(protocol_steps): - self.get_logger().info(f"Running step {i + 1}: {action}") + # self.get_logger().info(f"Running step {i + 1}: {action}") if isinstance(action, dict): # 如果是单个动作,直接执行 if action["action_name"] == "wait":