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 # 操作是否成功