protocol完整修复版本& bump version to 0.9.10

This commit is contained in:
KCFeng425
2025-07-10 16:48:09 +08:00
parent d82ccd5cf1
commit 7b93332bf5
25 changed files with 3760 additions and 1843 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,6 +1,6 @@
package:
name: unilabos
version: "0.9.9"
version: "0.9.10"
source:
path: ../..

View File

@@ -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'],

View File

@@ -43,6 +43,15 @@
Hydrogenate <Hydrogenate temp="45 °C" time="?" vessel="main_reactor"/>
4. 参数对齐
class PumpTransferProtocol(BaseModel):
from_vessel: str
to_vessel: str
@@ -53,7 +62,7 @@ class PumpTransferProtocol(BaseModel):
rinsing_solvent: str = "air" <Transfer from_vessel="main_reactor" to_vessel="rotavap"/>
rinsing_volume: float = 5000 <Transfer event="A" from_vessel="reactor" rate_spec="dropwise" to_vessel="main_reactor"/>
rinsing_repeats: int = 2 <Transfer from_vessel="separator" through="cartridge" to_vessel="rotavap"/>
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 <Separate product_phase="bottom" purpose="wash" solvent="water" vessel="separator" volume="?"/>
through: str <Separate product_phase="top" purpose="separate" vessel="separator"/>
repeats: int <Separate product_phase="bottom" purpose="extract" repeats="3" solvent="CH2Cl2" vessel="separator" volume="?"/>
stir_time: float<Separate product_phase="top" product_vessel="flask" purpose="separate" vessel="separator" waste_vessel="separator"/>
repeats: int <Separate product_phase="bottom" purpose="extract" repeats="3" solvent="CH2Cl2" vessel="separator" volume="?"/>
stir_time: float <Separate product_phase="top" product_vessel="flask" purpose="separate" vessel="separator" waste_vessel="separator"/>
stir_speed: float
settling_time: float 写了action
settling_time: float 测完了能跑✅
class EvaporateProtocol(BaseModel):
vessel: str
pressure: float
temp: float <Evaporate solvent="ethanol" vessel="rotavap"/>
time: float 完了
time: float 完了能跑✅
stir_speed: float
class EvacuateAndRefillProtocol(BaseModel):
vessel: str
gas: str <EvacuateAndRefill gas="nitrogen" vessel="main_reactor"/>
repeats: int 处理完了
repeats: int 测完了能跑✅
class AddProtocol(BaseModel):
vessel: str
@@ -102,7 +111,7 @@ class AddProtocol(BaseModel):
vessel="main_reactor" volume="2.67 mL"/>
<Add ratio="?" reagent="tetrahydrofuran|tert-butanol" vessel="main_reactor" volume="?"/>
viscous: bool
purpose: str 写了action
purpose: str 测完了能跑✅
class CentrifugeProtocol(BaseModel):
vessel: str
@@ -115,7 +124,7 @@ class FilterProtocol(BaseModel):
filtrate_vessel: str
stir: bool <Filter vessel="filter"/>
stir_speed: float <Filter filtrate_vessel="rotavap" vessel="filter"/>
temp: float 处理了
temp: float 测完了能跑✅
continue_heatchill: bool
volume: float
@@ -127,7 +136,7 @@ class HeatChillProtocol(BaseModel):
<HeatChill temp="256 °C" time="?" vessel="main_reactor"/>
<HeatChill reflux_solvent="methanol" temp_spec="reflux" time="2 h" vessel="main_reactor"/>
<HeatChillToTemp temp_spec="room temperature" vessel="main_reactor"/>
stir: bool 处理了
stir: bool 测完了能跑✅
stir_speed: float
purpose: str
@@ -144,7 +153,7 @@ class StirProtocol(BaseModel):
stir_speed: float <Stir time="0.5 h" vessel="main_reactor"/>
<Stir event="A" time="30 min" vessel="main_reactor"/>
<Stir time_spec="several minutes" vessel="filter"/>
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 <Dissolve mass="2.9 g" mol="0.12 mol" reagent="magnesium" vessel="main_reactor"/>
amount: str = "" <Dissolve mass="12.9 g" reagent="4-tert-butylbenzyl bromide" vessel="main_reactor"/>
volume: float <Dissolve mass="2.9 g" mol="0.12 mol" reagent="magnesium" vessel="main_reactor"/>
amount: str = "" <Dissolve mass="12.9 g" reagent="4-tert-butylbenzyl bromide" vessel="main_reactor"/>
temp: float = 25.0 <Dissolve solvent="diisopropyl ether" vessel="rotavap" volume="?"/>
time: float = 0.0
stir_speed: float = 0.0 写了action
time: float = 0.0 <Dissolve event="A" mass="?" reagent="pyridinone" vessel="main_reactor"/>
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 <RunColumn Rf="?" column="column" from_vessel="rotavap" pct1="40 %" pct2="50 %" solvent1="ethyl acetate" solvent2="hexane" to_vessel="rotavap"/>
column: str 写了action
column: str 测完了能跑✅
class WashSolidProtocol(BaseModel):
vessel: str
solvent: str
volume: float
filtrate_vessel: str = ""
filtrate_vessel: str = "" <WashSolid repeats="4" solvent="water" vessel="main_reactor" volume="400 mL"/>
temp: float = 25.0 <WashSolid filtrate_vessel="rotavap" solvent="formic acid" vessel="main_reactor" volume="?"/>
stir: bool = False <WashSolid solvent="acetone" vessel="rotavap" volume="5 mL"/>
stir_speed: float = 0.0 处理完了
<WashSolid solvent="ethyl alcohol" vessel="main_reactor" volume_spec="small volume"/>
<WashSolid filtrate_vessel="rotavap" mass="10 g" solvent="toluene" vessel="separator"/>
<WashSolid repeats_spec="several" solvent="water" vessel="main_reactor" volume="?"/>
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="反应容器")
vessel: str = Field(..., description="反应容器")
还差
<dissolve>
<separate>
<CleanVessel vessel="centrifuge"/>

View File

@@ -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"
}
},
{

View File

@@ -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()

File diff suppressed because it is too large Load Diff

View File

@@ -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()

View File

@@ -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)

View File

@@ -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()

View File

@@ -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(

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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}'")
# 优先级1volume_spec
if volume_spec and volume_spec.strip():
result = parse_volume_spec(volume_spec)
debug_print(f"使用volume_spec: {result}mL")
return result
# 优先级2mass质量转体积
if mass and mass.strip():
result = parse_mass_to_volume(mass)
if result > 0:
debug_print(f"使用mass转换: {result}mL")
return result
# 优先级3volume
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()
repeats=4, temp=50.0, stir=True, stir_speed=200.0, time=300.0, **kwargs
)

View File

@@ -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}")

View File

@@ -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)}"

View File

@@ -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()

View File

@@ -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

View File

@@ -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',可选)

View File

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

View File

@@ -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)

View File

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