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] =?UTF-8?q?protocol=E5=AE=8C=E6=95=B4=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=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 | 640 ++++++++---- unilabos/compile/dissolve_protocol.py | 983 ++++++++++++------ .../compile/evacuateandrefill_protocol.py | 282 ++--- unilabos/compile/evaporate_protocol.py | 307 +++--- unilabos/compile/filter_protocol.py | 254 +++-- unilabos/compile/pump_protocol.py | 468 ++++++--- unilabos/compile/run_column_protocol.py | 818 +++++++++++---- unilabos/compile/separate_protocol.py | 648 ++++++++---- unilabos/compile/stir_protocol.py | 304 ++++-- unilabos/compile/wash_solid_protocol.py | 601 ++++++++--- unilabos/devices/virtual/virtual_filter.py | 10 + unilabos/devices/virtual/virtual_rotavap.py | 56 +- .../devices/virtual/virtual_solenoid_valve.py | 56 +- 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, 3760 insertions(+), 1843 deletions(-) diff --git a/README.md b/README.md index 287efb5..5d14705 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 8bba3d7..5bd588b 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 4169c73..0445115 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 f9262e2..a576855 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 2d9aabd..80163ea 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ package_name = 'unilabos' 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 4762cd2..5dee23a 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 aaa4dfc..43e4cc6 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 6c51cc9..b5b017e 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}") - - # 1. 验证容器 - if vessel not in G.nodes(): - raise ValueError(f"容器 '{vessel}' 不存在") - - # 2. 判断固体 vs 液体 - is_solid = (mass > 0 or mol.strip() != "") + 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 = [] - 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})") + # === 参数验证 === + debug_print("步骤1: 参数验证...") + + if not vessel: + raise ValueError("vessel 参数不能为空") + if not reagent: + raise ValueError("reagent 参数不能为空") + + if vessel not in G.nodes(): + raise ValueError(f"容器 '{vessel}' 不存在于系统中") + + debug_print("✅ 基本参数验证通过") + + # === 🔧 关键修复:参数解析 === + debug_print("步骤2: 参数解析...") + + # 解析各种参数为数值 + final_volume = parse_volume_input(volume) + final_mass = parse_mass_input(mass) + final_time = parse_time_input(time) + + debug_print(f"解析结果:") + debug_print(f" - 体积: {final_volume}mL") + debug_print(f" - 质量: {final_mass}g") + debug_print(f" - 时间: {final_time}s") + debug_print(f" - 摩尔: '{mol}'") + debug_print(f" - 事件: '{event}'") + debug_print(f" - 速率: '{rate_spec}'") + + # === 判断添加类型 === + debug_print("步骤3: 判断添加类型...") + + # 🔧 修复:现在使用解析后的数值进行比较 + is_solid = (final_mass > 0 or (mol and mol.strip() != "")) + is_liquid = (final_volume > 0) + + if not is_solid and not is_liquid: + # 默认为液体,10mL + is_liquid = True + final_volume = 10.0 + debug_print("⚠️ 未指定体积或质量,默认为10mL液体") + + debug_print(f"添加类型: {'固体' if is_solid else '液体'}") + + # === 执行添加流程 === + debug_print("步骤4: 执行添加流程...") + + try: + if is_solid: + # === 固体添加路径 === + debug_print(f"使用固体添加路径") - 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 3da0d53..9008e6d 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 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: 体积(毫升) + """ + 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 = 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 + + 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: 质量输入(如 "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 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: + """ + 解析温度输入,支持带单位的字符串 + + 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: - """ - 查找溶剂容器 - """ - # 按照pump_protocol的命名规则查找溶剂瓶 - solvent_vessel_id = f"flask_{solvent}" + """增强版溶剂容器查找""" + debug_print(f"查找溶剂 '{solvent}' 的容器...") - if solvent_vessel_id in G.nodes(): - return solvent_vessel_id - - # 如果直接匹配失败,尝试模糊匹配 + # 🔧 方法1:直接搜索 data.reagent_name 和 config.reagent for node in G.nodes(): - if node.startswith('flask_') and solvent.lower() in node.lower(): - return node + 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 - # 如果还是找不到,列出所有可用的溶剂瓶 - available_flasks = [node for node in G.nodes() - if node.startswith('flask_') - and G.nodes[node].get('type') == 'container'] + # 🔧 方法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}" + ] - raise ValueError(f"找不到溶剂 '{solvent}' 对应的溶剂瓶。可用溶剂瓶: {available_flasks}") - + 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 = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_heatchill'] + """查找连接到指定容器的加热搅拌器""" + 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]]: 溶解操作的动作序列 - - Raises: - ValueError: 当找不到必要的设备或容器时 - - Examples: - dissolve_actions = generate_dissolve_protocol(G, "reaction_mixture", "DMF", 10.0, "NaCl 2g", 60.0, 600.0, 400.0) + 支持所有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" """ + + 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) + 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}") + heatchill_id = find_connected_heatchill(G, vessel) + stirrer_id = find_connected_stirrer(G, vessel) + + # 优先使用加热搅拌器,否则使用独立搅拌器 + 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 "溶解准备" + } + } + 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(stir_action) + + # 等待搅拌稳定 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 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: - print(f"DISSOLVE: 警告 - 需要加热/搅拌但未找到与容器 {vessel} 相连的加热搅拌器") - except Exception as e: - print(f"DISSOLVE: 加热搅拌器配置出错: {str(e)}") - - # === 第一步:启动加热搅拌(在添加溶剂前) === - if heatchill_id and (temp > 25.0 or time > 0): - print(f"DISSOLVE: 启动加热搅拌器,温度: {temp}°C") + debug_print("⚠️ 未找到固体加样器,跳过固体添加") - if time > 0: - # 如果指定了时间,使用定时加热搅拌 - heatchill_action = { + elif is_liquid_dissolve: + # === 液体溶解路径 === + debug_print(f"5.3: 使用液体溶解路径") + + # 查找溶剂容器 + try: + solvent_vessel = find_solvent_vessel(G, solvent) + except ValueError as e: + debug_print(f"⚠️ {str(e)},跳过溶剂添加") + solvent_vessel = None + + 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} + }) + + # 步骤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} + }) + + # 步骤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", + "action_name": "heat_chill_stop", "action_kwargs": { - "vessel": vessel, - "temp": temp, - "time": time, - "stir": True, - "stir_speed": stir_speed, - "purpose": f"溶解 {amount} 在 {solvent} 中" - } - } - else: - # 如果没有指定时间,使用持续加热搅拌 - heatchill_action = { - "device_id": heatchill_id, - "action_name": "heat_chill_start", - "action_kwargs": { - "vessel": vessel, - "temp": temp, - "purpose": f"溶解 {amount} 在 {solvent} 中" + "vessel": vessel } } + action_sequence.append(stop_action) - 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}") - - # 计算流速 - 溶解时通常用较慢的速度,避免飞溅 - transfer_flowrate = 1.0 # 较慢的转移速度 - flowrate = 0.5 # 较慢的注入速度 - - 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 # 转移速度 - ) - - action_sequence.extend(pump_actions) - - except Exception as e: - raise ValueError(f"生成泵协议时出错: {str(e)}") - - # 溶剂添加后等待 + except Exception as e: + debug_print(f"⚠️ 溶解流程执行失败: {str(e)}") + # 添加错误日志 action_sequence.append({ - "action_name": "wait", - "action_kwargs": {"time": 5} + "device_id": "system", + "action_name": "log_message", + "action_kwargs": { + "message": f"溶解失败: {str(e)}" + } }) - # === 第三步:如果没有使用定时加热搅拌,但需要等待溶解 === - if time > 0 and heatchill_id and temp <= 25.0: - # 只需要搅拌等待,不需要加热 - print(f"DISSOLVE: 室温搅拌 {time}s 等待溶解") - - 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}" - } - } - action_sequence.append(stir_action) - - # === 第四步:如果使用了持续加热,需要手动停止 === - if heatchill_id and time == 0 and temp > 25.0: - print(f"DISSOLVE: 停止加热搅拌器") - - stop_action = { - "device_id": heatchill_id, - "action_name": "heat_chill_stop", - "action_kwargs": { - "vessel": vessel - } - } - 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 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 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 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_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 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 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 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 generate_sequential_dissolve_protocol( - G: nx.DiGraph, - vessel: str, - dissolve_steps: List[Dict[str, Any]] -) -> List[Dict[str, Any]]: - """ - 生成连续溶解多种物质的协议 - - Args: - G: 网络图 - vessel: 目标容器 - dissolve_steps: 溶解步骤列表,每个元素包含溶解参数 - - 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 = [] - - 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 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 test_dissolve_protocol(): - """测试溶解协议的示例""" - print("=== DISSOLVE PROTOCOL 测试 ===") - print("测试完成") - + """测试溶解协议的各种参数解析""" + print("=== DISSOLVE PROTOCOL 修复版测试 ===") + + # 测试体积解析 + volumes = ["10 mL", "?", 10.0, "1 L", "500 μL"] + for vol in volumes: + result = parse_volume_input(vol) + print(f"体积解析: {vol} → {result}mL") + + # 测试质量解析 + masses = ["2.9 g", "?", 2.5, "500 mg"] + for mass in masses: + result = parse_mass_input(mass) + print(f"质量解析: {mass} → {result}g") + + # 测试温度解析 + 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 e5e8df5..c50b884 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 - - -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) - ] - - # 通过网络连接查找直接相连的电磁阀 - for solenoid in solenoid_valves: - if G.has_edge(device_id, solenoid) or G.has_edge(solenoid, device_id): - 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 + # 如果没有连接的搅拌器,返回第一个可用的 + if stirrer_nodes: + debug_print(f"未找到直接连接的搅拌器,使用第一个可用的: {stirrer_nodes[0]}") + return stirrer_nodes[0] + debug_print("未找到搅拌器") return None +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(solenoid, vacuum_pump) or G.has_edge(vacuum_pump, solenoid): + debug_print(f"✓ 找到连接真空泵的电磁阀: {solenoid}") + 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 b5170b5..e3ffb86 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}' 的液体体积...") +def find_rotavap_device(G: nx.DiGraph, vessel: str = None) -> Optional[str]: + """ + 在组态图中查找旋转蒸发仪设备 - if vessel not in G.nodes(): - debug_print(f"容器 '{vessel}' 不存在") - return 0.0 + Args: + G: 设备图 + vessel: 指定的设备名称(可选) - 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]: - """查找旋转蒸发仪设备""" + Returns: + str: 找到的旋转蒸发仪设备ID,如果没找到返回None + """ debug_print("查找旋转蒸发仪设备...") - # 查找各种可能的旋转蒸发仪设备 - possible_devices = [] - for node in G.nodes(): - node_data = G.nodes[node] + # 如果指定了vessel,先检查是否存在且是旋转蒸发仪 + if 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}") + + # 检查是否为旋转蒸发仪 + 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 any(keyword in node_class.lower() for keyword in ['rotavap', 'evaporator']): - possible_devices.append(node) - debug_print(f"找到旋转蒸发仪设备: {node}") + # 跳过非设备节点 + 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 possible_devices: - return possible_devices[0] + if rotavap_candidates: + selected = rotavap_candidates[0] # 选择第一个找到的 + debug_print(f"✓ 选择旋转蒸发仪: {selected}") + return selected - debug_print("未找到旋转蒸发仪设备") + debug_print("✗ 未找到旋转蒸发仪设备") return None -def find_rotavap_vessel(G: nx.DiGraph) -> Optional[str]: - """查找旋转蒸发仪样品容器""" - debug_print("查找旋转蒸发仪样品容器...") +def find_connected_vessel(G: nx.DiGraph, rotavap_device: str) -> Optional[str]: + """ + 查找与旋转蒸发仪连接的容器 - possible_vessels = [ - "rotavap", "rotavap_flask", "flask_rotavap", - "evaporation_flask", "evaporator", "rotary_evaporator" - ] + 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} 连接的容器...") - debug_print("未找到旋转蒸发仪样品容器") - return None - -def find_recovery_vessel(G: nx.DiGraph) -> Optional[str]: - """查找溶剂回收容器""" - debug_print("查找溶剂回收容器...") + # 查看旋转蒸发仪的子设备 + rotavap_data = G.nodes[rotavap_device] + children = rotavap_data.get('children', []) - possible_vessels = [ - "flask_distillate", "distillate", "solvent_recovery", - "rotavap_condenser", "condenser", "waste_workup", "waste" - ] + 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 vessel in possible_vessels: - if vessel in G.nodes(): - debug_print(f"找到回收容器: {vessel}") - return vessel + # 查看邻接的容器 + 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 = [] + # === 步骤1: 查找旋转蒸发仪设备 === + debug_print("步骤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 e673f22..6629684 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}'") +def validate_vessel(G: nx.DiGraph, vessel: str, vessel_type: str = "容器") -> None: + """验证容器是否存在""" + if not vessel: + raise ValueError(f"{vessel_type}不能为空") - # 如果指定了容器且存在,直接使用 - 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 vessel not in G.nodes(): + raise ValueError(f"{vessel_type} '{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" # 默认容器 + 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 参数不能为空") + validate_vessel(G, vessel, "过滤容器") - if vessel not in G.nodes(): - raise ValueError(f"容器 '{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: 体积检测...") + # === 转移到过滤器(如果需要)=== + debug_print("步骤3: 转移到过滤器...") - source_volume = get_vessel_liquid_volume(G, vessel) - - if source_volume > 0: - transfer_volume = source_volume - debug_print(f"检测到液体体积: {transfer_volume}mL") + 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 acdb52c..f214ff4 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: - min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone]) - except (KeyError, TypeError): + 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)) @@ -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]] + 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 + } } - }, - { - "device_id": pumps_from_node[pump_backbone[0]], - "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] + 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": 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 + } + }, + { + "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]] + 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 + } } - }, - { - "device_id": pumps_from_node[pump_backbone[-1]], - "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 + # # 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 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生成逻辑 - # 我先提供一个示例,您需要替换为实际的生成逻辑 + # # 根据物理属性调整冲洗参数 + # 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 f6b9214..c60e240 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: +def parse_rf_value(rf_str: str) -> float: + """ + 解析Rf值字符串 + + Args: + rf_str: Rf值字符串(如 "0.3", "0.45", "?") + + Returns: + float: Rf值(0-1) + """ + if not rf_str or not rf_str.strip(): + return 0.3 # 默认Rf值 + + rf_str = rf_str.strip().lower() + debug_print(f"解析Rf值: '{rf_str}'") + + # 处理未知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: """查找柱层析设备""" - # 首先检查是否有虚拟柱设备 - column_nodes = [node for node in G.nodes() - if (G.nodes[node].get('class') or '') == 'virtual_column'] + debug_print("查找柱层析设备...") - if column_nodes: - return column_nodes[0] + # 查找虚拟柱设备 + 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 - # 如果没有虚拟柱设备,抛出异常 - raise ValueError(f"系统中未找到柱层析设备。请确保配置了 virtual_column 设备") - + # 如果没有找到,尝试创建虚拟设备名称 + 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 参数作为容器名称 - if column in G.nodes(): - return column + debug_print(f"查找柱容器: '{column}'") - # 尝试常见的柱容器命名规则 + # 直接检查column参数是否是容器 + if column in G.nodes(): + 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}'") - if solvent_vessel_id in G.nodes(): - return solvent_vessel_id - - # 如果直接匹配失败,尝试模糊匹配 + # 🔧 方法1:直接搜索 data.reagent_name for node in G.nodes(): - if node.startswith('flask_') and eluting_solvent.lower() in node.lower(): - return node + 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 - # 如果还是找不到,列出所有可用的溶剂瓶 - available_flasks = [node for node in G.nodes() - if node.startswith('flask_') - and G.nodes[node].get('type') == 'container'] + # 🔧 方法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}" + ] - raise ValueError(f"找不到洗脱溶剂 '{eluting_solvent}' 对应的溶剂瓶。可用溶剂瓶: {available_flasks}") + 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(): + 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 + + # 🔧 方法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'] + } + + # 查找映射的同义词 + 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}") + # === 参数解析 === + debug_print("步骤2: 参数解析...") + + # 解析Rf值 + final_rf = parse_rf_value(rf) + debug_print(f"最终Rf值: {final_rf}") + + # 解析溶剂比例(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: - print(f"RUN_COLUMN: 警告 - 未找到柱层析设备") + final_pct1 = parse_percentage(pct1) if pct1 else 50.0 + final_pct2 = parse_percentage(pct2) if pct2 else 50.0 + + # 如果百分比和不是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 + + debug_print(f"使用百分比参数: {final_pct1:.1f}% : {final_pct2:.1f}%") + + # 设置默认溶剂(如果未指定) + 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) - print(f"RUN_COLUMN: 源容器 {from_vessel} 中有 {source_volume} mL 液体") + if source_volume <= 0: + source_volume = 100.0 # 默认体积 + debug_print(f"⚠️ 无法获取源容器体积,使用默认值: {source_volume}mL") + else: + debug_print(f"✅ 源容器体积: {source_volume}mL") - # === 第一步:样品转移到柱子(如果柱子是容器) === - if column in G.nodes() and G.nodes[column].get('type') == 'container': - print(f"RUN_COLUMN: 样品转移 - {source_volume} mL 从 {from_vessel} 到 {column}") - - 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)}") + # === 计算溶剂体积 === + debug_print("步骤5: 计算溶剂体积...") - # === 第二步:使用柱层析设备执行分离 === - if column_device_id: - print(f"RUN_COLUMN: 使用柱层析设备执行分离") + # 洗脱溶剂通常是样品体积的2-5倍 + total_elution_volume = source_volume * 3.0 + solvent1_volume, solvent2_volume = calculate_solvent_volumes( + total_elution_volume, final_pct1, final_pct2 + ) + + # === 执行柱层析流程 === + debug_print("步骤6: 执行柱层析流程...") + + 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)}") - column_separation_action = { - "device_id": column_device_id, - "action_name": "run_column", - "action_kwargs": { - "from_vessel": from_vessel, - "to_vessel": to_vessel, - "column": column + # 步骤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)}") + + # 步骤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)}") + + # 步骤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) - - # 等待柱层析设备完成分离 - action_sequence.append({ - "action_name": "wait", - "action_kwargs": {"time": 60} - }) - - # === 第三步:从柱子转移到目标容器(如果需要) === - 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 - ) - - -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 = [] - - # 每种溶剂单独执行一次柱层析 - for i, (solvent, volume) in enumerate(zip(gradient_solvents, gradient_volumes)): - print(f"RUN_COLUMN: 梯度洗脱第 {i+1}/{len(gradient_solvents)} 步: {volume} mL {solvent}") - - # 第一步使用源容器,后续步骤使用柱子作为源 - 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 - - 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) - - # 在梯度步骤之间加入等待时间 - if i < len(gradient_solvents) - 1: + 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_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, - 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="c18_column", + solvent1="methanol", + solvent2="water", + ratio="7:3", # 常见的MeOH:H2O比例 + **kwargs ) - -def generate_ion_exchange_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 -) -> 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 - ) - - -# 测试函数 -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 cbb028c..07d9ab4 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 debug_print(message): + """调试输出""" + print(f"[SEPARATE] {message}", flush=True) + logger.info(f"[SEPARATE] {message}") + +def parse_volume_input(volume_input: Union[str, float]) -> float: + """ + 解析体积输入,支持带单位的字符串 + + Args: + volume_input: 体积输入(如 "200 mL", "?", 50.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 = 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 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, - 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]: + 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]]: """ - 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. + 支持XDL参数格式: + - vessel: 分离容器(必需) + - purpose: "wash", "extract", "separate" + - product_phase: "top", "bottom" + - product_vessel: 产物收集容器 + - waste_vessel: 废液收集容器 + - solvent: 溶剂名称 + - volume: "200 mL", "?" 或数值 + - repeats: 重复次数 + + 分离流程: + 1. (可选)添加溶剂到分离容器 + 2. 搅拌混合 + 3. 静置分层 + 4. 收集指定相到目标容器 + 5. 重复指定次数 """ - # 生成泵操作的动作序列 - pump_action_sequence = [] - reactor_volume = 500.0 - waste_vessel = waste_phase_to_vessel + 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) - # TODO:通过物料管理系统找到溶剂的容器 - if "," in solvent: - solvents = solvent.split(",") - assert len(solvents) == repeats, "Number of solvents must match number of repeats." - else: - solvents = [solvent] * repeats + action_sequence = [] - # TODO: 通过设备连接图找到分离容器的控制器、底部出口 - separator_controller = f"{separation_vessel}_controller" - separation_vessel_bottom = f"flask_{separation_vessel}" + # === 参数验证和标准化 === + debug_print("步骤1: 参数验证和标准化...") - transfer_flowrate = flowrate = 2.5 + # 统一容器参数 + final_vessel = vessel or separation_vessel + if not final_vessel: + raise ValueError("必须指定分离容器 (vessel 或 separation_vessel)") - 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, + 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": { + "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 + } } + 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)}" } - ) - - # 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 i < repeats - 1: - pump_action_sequence.append( - { - "device_id": "", - "action_name": "PumpTransferProtocol", - "action_kwargs": { - "from_vessel": to_vessel, - "to_vessel": separation_vessel, - "volume": 250.0, - "time": 250.0 / flowrate, - # "transfer_flowrate": transfer_flowrate, - } - } - ) - return pump_action_sequence + # === 最终结果 === + 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 76345cc..a67155d 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 50bbed8..1a5c5e3 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}") + # 🔧 修复:分解为基础动作序列 - wash_action = { + # 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)}") + # 继续执行,可能有其他问题 + + # 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, + 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, + volume: float = 30.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, solvent, volume, **kwargs ) def generate_thorough_wash_protocol( G: nx.DiGraph, vessel: str, solvent: str, - volume: float, + volume: float = 100.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 - ) - -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 + 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 ca2e8b2..d70c854 100644 --- a/unilabos/devices/virtual/virtual_filter.py +++ b/unilabos/devices/virtual/virtual_filter.py @@ -67,6 +67,16 @@ class VirtualFilter: 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 fbbfe9f..9f576b5 100644 --- a/unilabos/devices/virtual/virtual_rotavap.py +++ b/unilabos/devices/virtual/virtual_rotavap.py @@ -3,6 +3,9 @@ import logging 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 @@ class VirtualRotavap: 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 @@ class VirtualRotavap: 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 @@ class VirtualRotavap: 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 @@ class VirtualRotavap: 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 @@ class VirtualRotavap: 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 @@ class VirtualRotavap: "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 @@ class VirtualRotavap: # 模拟蒸发体积 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 @@ class VirtualRotavap: # 蒸发完成 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 @@ class VirtualRotavap: "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 f25cc84..54a1e6d 100644 --- a/unilabos/devices/virtual/virtual_solenoid_valve.py +++ b/unilabos/devices/virtual/virtual_solenoid_valve.py @@ -43,10 +43,25 @@ class VirtualSolenoidValve: 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 @@ class VirtualSolenoidValve: 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 @@ class VirtualSolenoidValve: """关闭电磁阀 - 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 - else: - return {"success": False, "message": "Invalid command type"} + if string is None: + return {"success": False, "message": "Missing string parameter"} - return await self.set_valve_position(command=cmd_str) + # 将 string 参数转换为 command 参数 + if string.upper() in ["ON", "OPEN"]: + command = "OPEN" + elif string.upper() in ["OFF", "CLOSED"]: + command = "CLOSED" + else: + command = string + + return await self.set_valve_position(command=command) def toggle(self): """切换阀门状态""" @@ -129,19 +148,6 @@ class VirtualSolenoidValve: """检查阀门是否关闭""" 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 1bbc5f7..3a98e56 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 0c6ed2a..021199a 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 e2d2c43..f070a61 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 a487fea..fc185b6 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 7aa3c6e..8ef159d 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 # 操作是否成功