From 4c6e437eb18fde835e0bcf2d8a2f2aa0779e22c6 Mon Sep 17 00:00:00 2001
From: KCFeng425 <2100011801@stu.pku.edu.cn>
Date: Sun, 6 Jul 2025 19:21:53 +0800
Subject: [PATCH 1/9] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E9=83=A8?=
=?UTF-8?q?=E5=88=86=E7=9A=84protocol=E5=9B=A0=E4=B8=BAXDL=E6=9B=B4?=
=?UTF-8?q?=E6=96=B0=E5=AF=BC=E8=87=B4=E7=9A=84=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
但是pumptransfer,add,dissolve,separate还没修,后续还需要写virtual固体加料器
---
.../comprehensive_protocol/checklist.md | 47 +-
.../compile/evacuateandrefill_protocol.py | 365 +++----
unilabos/compile/evaporate_protocol.py | 515 +++++-----
unilabos/compile/filter_protocol.py | 373 +++----
unilabos/compile/heatchill_protocol.py | 538 +++++-----
unilabos/compile/pump_protocol.py | 922 +++++++++++++++---
unilabos/compile/stir_protocol.py | 297 +++++-
unilabos/compile/wash_solid_protocol.py | 451 +++++----
unilabos/devices/virtual/virtual_rotavap.py | 21 +-
unilabos/messages/__init__.py | 507 +++++++++-
unilabos/registry/devices/work_station.yaml | 452 ++++-----
unilabos_msgs/action/EvacuateAndRefill.action | 3 +-
unilabos_msgs/action/Evaporate.action | 3 +-
unilabos_msgs/action/Filter.action | 14 +-
unilabos_msgs/action/HeatChill.action | 25 +-
unilabos_msgs/action/PumpTransfer.action | 5 +
unilabos_msgs/action/Stir.action | 19 +-
unilabos_msgs/action/WashSolid.action | 29 +-
18 files changed, 3007 insertions(+), 1579 deletions(-)
diff --git a/test/experiments/comprehensive_protocol/checklist.md b/test/experiments/comprehensive_protocol/checklist.md
index 5bb14ae7d..9645aa4ce 100644
--- a/test/experiments/comprehensive_protocol/checklist.md
+++ b/test/experiments/comprehensive_protocol/checklist.md
@@ -51,9 +51,9 @@ class PumpTransferProtocol(BaseModel):
time: float = 0
viscous: bool = False
rinsing_solvent: str = "air"
- rinsing_volume: float = 5000
- rinsing_repeats: int = 2
- solid: bool = False
+ rinsing_volume: float = 5000
+ rinsing_repeats: int = 2
+ solid: bool = False 添加了缺失的参数,但是体积为0以及打印日志的问题修不好
flowrate: float = 500
transfer_flowrate: float = 2500
@@ -66,9 +66,9 @@ class SeparateProtocol(BaseModel):
waste_phase_to_vessel: str
solvent: str
solvent_volume: float
- through: str
- repeats: int
- stir_time: float
+ through: str
+ repeats: int
+ stir_time: float
stir_speed: float
settling_time: float
@@ -77,14 +77,14 @@ class EvaporateProtocol(BaseModel):
vessel: str
pressure: float
temp: float
- time: float
+ time: float 加完了
stir_speed: float
class EvacuateAndRefillProtocol(BaseModel):
vessel: str
gas: str
- repeats: int
+ repeats: int 处理完了
class AddProtocol(BaseModel):
vessel: str
@@ -95,21 +95,27 @@ class AddProtocol(BaseModel):
time: float
stir: bool
stir_speed: float
+
+
+
+
+
viscous: bool
purpose: str
class CentrifugeProtocol(BaseModel):
vessel: str
speed: float
- time: float 自创的
+ time: float 没毛病
temp: float
class FilterProtocol(BaseModel):
vessel: str
filtrate_vessel: str
stir: bool
- stir_speed: float
- temp: float
+ stir_speed: float
+ temp: float 处理了
continue_heatchill: bool
volume: float
@@ -118,6 +124,9 @@ class HeatChillProtocol(BaseModel):
temp: float
time: float
+
+
+
stir: bool
stir_speed: float
purpose: str
@@ -133,7 +142,9 @@ class HeatChillStopProtocol(BaseModel):
class StirProtocol(BaseModel):
stir_time: float
stir_speed: float
- settling_time: float
+
+
+ settling_time: float 处理完了
class StartStirProtocol(BaseModel):
vessel: str
@@ -149,11 +160,11 @@ class TransferProtocol(BaseModel):
volume: float
amount: str = ""
time: float = 0
- viscous: bool = False
+ viscous: bool = False
rinsing_solvent: str = ""
rinsing_volume: float = 0.0
rinsing_repeats: int = 0
- solid: bool = False
+ solid: bool = False 这个protocol早该删掉了
class CleanVesselProtocol(BaseModel):
vessel: str
@@ -166,8 +177,8 @@ class DissolveProtocol(BaseModel):
vessel: str
solvent: str
volume: float
- amount: str = ""
- temp: float = 25.0
+ amount: str = ""
+ temp: float = 25.0
time: float = 0.0
stir_speed: float = 0.0
@@ -191,8 +202,8 @@ class WashSolidProtocol(BaseModel):
volume: float
filtrate_vessel: str = ""
temp: float = 25.0
- stir: bool = False
- stir_speed: float = 0.0
+ stir: bool = False
+ stir_speed: float = 0.0 处理完了
time: float = 0.0
repeats: int = 1
diff --git a/unilabos/compile/evacuateandrefill_protocol.py b/unilabos/compile/evacuateandrefill_protocol.py
index aa44df69e..e5e8df5c4 100644
--- a/unilabos/compile/evacuateandrefill_protocol.py
+++ b/unilabos/compile/evacuateandrefill_protocol.py
@@ -197,77 +197,148 @@ def generate_evacuateandrefill_protocol(
G: nx.DiGraph,
vessel: str,
gas: str,
- repeats: int = 1
+ # 🔧 删除 repeats 参数,直接硬编码为 3
+ **kwargs # 🔧 接受额外参数,增强兼容性
) -> List[Dict[str, Any]]:
"""
- 生成抽真空和充气操作的动作序列
+ 生成抽真空和充气操作的动作序列 - 简化版本
- **修复版本**: 正确调用 pump_protocol 并处理异常
+ Args:
+ G: 设备图
+ vessel: 目标容器名称(必需)
+ gas: 气体名称(必需)
+ **kwargs: 其他参数(兼容性)
+
+ Returns:
+ List[Dict[str, Any]]: 动作序列
"""
+
+ # 🔧 硬编码重复次数为 3
+ repeats = 3
+
+ debug_print("=" * 60)
+ debug_print("开始生成抽真空充气协议")
+ debug_print(f"输入参数:")
+ debug_print(f" - vessel: {vessel}")
+ debug_print(f" - gas: {gas}")
+ debug_print(f" - repeats: {repeats} (硬编码)")
+ debug_print(f" - 其他参数: {kwargs}")
+ debug_print("=" * 60)
+
action_sequence = []
- # 参数设置 - 关键修复:减小体积避免超出泵容量
- VACUUM_VOLUME = 20.0 # 减小抽真空体积
- REFILL_VOLUME = 20.0 # 减小充气体积
- PUMP_FLOW_RATE = 2.5 # 降低流速
- STIR_SPEED = 300.0
+ # === 参数验证和修正 ===
+ debug_print("步骤1: 参数验证和修正...")
+
+ # 验证必需参数
+ if not vessel:
+ raise ValueError("vessel 参数不能为空")
- print(f"EVACUATE_REFILL: 开始生成协议,目标容器: {vessel}, 气体: {gas}, 重复次数: {repeats}")
+ if not gas:
+ raise ValueError("gas 参数不能为空")
- # 1. 验证设备存在
if vessel not in G.nodes():
- raise ValueError(f"目标容器 '{vessel}' 不存在于系统中")
+ raise ValueError(f"容器 '{vessel}' 不存在于系统中")
+
+ # 标准化气体名称
+ gas_aliases = {
+ 'n2': 'nitrogen',
+ 'ar': 'argon',
+ 'air': 'air',
+ 'o2': 'oxygen',
+ 'co2': 'carbon_dioxide',
+ 'h2': 'hydrogen'
+ }
+
+ original_gas = gas
+ gas_lower = gas.lower().strip()
+ if gas_lower in gas_aliases:
+ gas = gas_aliases[gas_lower]
+ debug_print(f"标准化气体名称: {original_gas} -> {gas}")
+
+ debug_print(f"最终参数: vessel={vessel}, gas={gas}, repeats={repeats}")
+
+ # === 查找设备 ===
+ debug_print("步骤2: 查找设备...")
- # 2. 查找设备
try:
vacuum_pump = find_vacuum_pump(G)
- vacuum_solenoid = find_associated_solenoid_valve(G, vacuum_pump)
gas_source = find_gas_source(G, gas)
+ vacuum_solenoid = find_associated_solenoid_valve(G, vacuum_pump)
gas_solenoid = find_associated_solenoid_valve(G, gas_source)
stirrer_id = find_connected_stirrer(G, vessel)
- print(f"EVACUATE_REFILL: 找到设备")
- print(f" - 真空泵: {vacuum_pump}")
- print(f" - 气源: {gas_source}")
- print(f" - 真空电磁阀: {vacuum_solenoid}")
- print(f" - 气源电磁阀: {gas_solenoid}")
- print(f" - 搅拌器: {stirrer_id}")
+ debug_print(f"设备配置:")
+ debug_print(f" - 真空泵: {vacuum_pump}")
+ debug_print(f" - 气源: {gas_source}")
+ debug_print(f" - 真空电磁阀: {vacuum_solenoid}")
+ debug_print(f" - 气源电磁阀: {gas_solenoid}")
+ debug_print(f" - 搅拌器: {stirrer_id}")
- except ValueError as e:
+ except Exception as e:
+ debug_print(f"❌ 设备查找失败: {str(e)}")
raise ValueError(f"设备查找失败: {str(e)}")
- # 3. **关键修复**: 验证路径存在性
+ # === 参数设置 ===
+ debug_print("步骤3: 参数设置...")
+
+ # 根据气体类型调整参数
+ if gas.lower() in ['nitrogen', 'argon']:
+ VACUUM_VOLUME = 25.0
+ REFILL_VOLUME = 25.0
+ PUMP_FLOW_RATE = 2.0
+ VACUUM_TIME = 30.0
+ REFILL_TIME = 20.0
+ debug_print("惰性气体:使用标准参数")
+ elif gas.lower() in ['air', 'oxygen']:
+ VACUUM_VOLUME = 20.0
+ REFILL_VOLUME = 20.0
+ PUMP_FLOW_RATE = 1.5
+ VACUUM_TIME = 45.0
+ REFILL_TIME = 25.0
+ debug_print("活性气体:使用保守参数")
+ else:
+ VACUUM_VOLUME = 15.0
+ REFILL_VOLUME = 15.0
+ PUMP_FLOW_RATE = 1.0
+ VACUUM_TIME = 60.0
+ REFILL_TIME = 30.0
+ debug_print("未知气体:使用安全参数")
+
+ STIR_SPEED = 200.0
+
+ debug_print(f"操作参数:")
+ debug_print(f" - 抽真空体积: {VACUUM_VOLUME}mL")
+ debug_print(f" - 充气体积: {REFILL_VOLUME}mL")
+ debug_print(f" - 泵流速: {PUMP_FLOW_RATE}mL/s")
+ debug_print(f" - 抽真空时间: {VACUUM_TIME}s")
+ debug_print(f" - 充气时间: {REFILL_TIME}s")
+ debug_print(f" - 搅拌速度: {STIR_SPEED}RPM")
+
+ # === 路径验证 ===
+ debug_print("步骤4: 路径验证...")
+
try:
# 验证抽真空路径
vacuum_path = nx.shortest_path(G, source=vessel, target=vacuum_pump)
- print(f"EVACUATE_REFILL: 抽真空路径: {' → '.join(vacuum_path)}")
+ debug_print(f"抽真空路径: {' → '.join(vacuum_path)}")
# 验证充气路径
gas_path = nx.shortest_path(G, source=gas_source, target=vessel)
- print(f"EVACUATE_REFILL: 充气路径: {' → '.join(gas_path)}")
-
- # **新增**: 检查路径中的边数据
- for i in range(len(vacuum_path) - 1):
- nodeA, nodeB = vacuum_path[i], vacuum_path[i + 1]
- edge_data = G.get_edge_data(nodeA, nodeB)
- if not edge_data or 'port' not in edge_data:
- raise ValueError(f"路径 {nodeA} → {nodeB} 缺少端口信息")
- print(f" 抽真空路径边 {nodeA} → {nodeB}: {edge_data}")
+ debug_print(f"充气路径: {' → '.join(gas_path)}")
- for i in range(len(gas_path) - 1):
- nodeA, nodeB = gas_path[i], gas_path[i + 1]
- edge_data = G.get_edge_data(nodeA, nodeB)
- if not edge_data or 'port' not in edge_data:
- raise ValueError(f"路径 {nodeA} → {nodeB} 缺少端口信息")
- print(f" 充气路径边 {nodeA} → {nodeB}: {edge_data}")
-
except nx.NetworkXNoPath as e:
+ debug_print(f"❌ 路径不存在: {str(e)}")
raise ValueError(f"路径不存在: {str(e)}")
except Exception as e:
+ debug_print(f"❌ 路径验证失败: {str(e)}")
raise ValueError(f"路径验证失败: {str(e)}")
- # 4. 启动搅拌器
+ # === 启动搅拌器 ===
+ debug_print("步骤5: 启动搅拌器...")
+
if stirrer_id:
+ debug_print(f"启动搅拌器: {stirrer_id}")
action_sequence.append({
"device_id": stirrer_id,
"action_name": "start_stir",
@@ -277,15 +348,26 @@ def generate_evacuateandrefill_protocol(
"purpose": "抽真空充气操作前启动搅拌"
}
})
+
+ # 等待搅拌稳定
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": 5.0}
+ })
+ else:
+ debug_print("未找到搅拌器,跳过搅拌启动")
- # 5. 执行多次抽真空-充气循环
- for cycle in range(repeats):
- print(f"EVACUATE_REFILL: === 第 {cycle+1}/{repeats} 次循环 ===")
+ # === 执行 3 次抽真空-充气循环 ===
+ debug_print("步骤6: 执行抽真空-充气循环...")
+
+ for cycle in range(repeats): # 这里 repeats = 3
+ debug_print(f"=== 第 {cycle+1}/{repeats} 次循环 ===")
# ============ 抽真空阶段 ============
- print(f"EVACUATE_REFILL: 抽真空阶段开始")
+ debug_print(f"抽真空阶段开始")
# 启动真空泵
+ debug_print(f"启动真空泵: {vacuum_pump}")
action_sequence.append({
"device_id": vacuum_pump,
"action_name": "set_status",
@@ -294,17 +376,15 @@ def generate_evacuateandrefill_protocol(
# 开启真空电磁阀
if vacuum_solenoid:
+ debug_print(f"开启真空电磁阀: {vacuum_solenoid}")
action_sequence.append({
"device_id": vacuum_solenoid,
"action_name": "set_valve_position",
"action_kwargs": {"command": "OPEN"}
})
- # **关键修复**: 改进 pump_protocol 调用和错误处理
- print(f"EVACUATE_REFILL: 调用抽真空 pump_protocol: {vessel} → {vacuum_pump}")
- print(f" - 体积: {VACUUM_VOLUME} mL")
- print(f" - 流速: {PUMP_FLOW_RATE} mL/s")
-
+ # 抽真空操作
+ debug_print(f"抽真空操作: {vessel} → {vacuum_pump}")
try:
vacuum_transfer_actions = generate_pump_protocol_with_rinsing(
G=G,
@@ -312,9 +392,9 @@ def generate_evacuateandrefill_protocol(
to_vessel=vacuum_pump,
volume=VACUUM_VOLUME,
amount="",
- time=0.0,
+ duration=0.0, # 🔧 修复time参数名冲突
viscous=False,
- rinsing_solvent="", # **修复**: 明确不使用清洗
+ rinsing_solvent="",
rinsing_volume=0.0,
rinsing_repeats=0,
solid=False,
@@ -324,52 +404,31 @@ def generate_evacuateandrefill_protocol(
if vacuum_transfer_actions:
action_sequence.extend(vacuum_transfer_actions)
- print(f"EVACUATE_REFILL: ✅ 成功添加 {len(vacuum_transfer_actions)} 个抽真空动作")
+ debug_print(f"✅ 添加了 {len(vacuum_transfer_actions)} 个抽真空动作")
else:
- print(f"EVACUATE_REFILL: ⚠️ 抽真空 pump_protocol 返回空序列")
- # **修复**: 添加手动泵动作作为备选
- action_sequence.extend([
- {
- "device_id": "multiway_valve_1",
- "action_name": "set_valve_position",
- "action_kwargs": {"command": "5"} # 连接到反应器
- },
- {
- "device_id": "transfer_pump_1",
- "action_name": "set_position",
- "action_kwargs": {
- "position": VACUUM_VOLUME,
- "max_velocity": PUMP_FLOW_RATE
- }
- }
- ])
- print(f"EVACUATE_REFILL: 使用备选手动泵动作")
+ debug_print("⚠️ 抽真空协议返回空序列,添加手动动作")
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": VACUUM_TIME}
+ })
except Exception as e:
- print(f"EVACUATE_REFILL: ❌ 抽真空 pump_protocol 失败: {str(e)}")
- import traceback
- print(f"EVACUATE_REFILL: 详细错误:\n{traceback.format_exc()}")
-
- # **修复**: 添加手动动作而不是忽略错误
- print(f"EVACUATE_REFILL: 使用手动备选方案")
- action_sequence.extend([
- {
- "device_id": "multiway_valve_1",
- "action_name": "set_valve_position",
- "action_kwargs": {"command": "5"} # 反应器端口
- },
- {
- "device_id": "transfer_pump_1",
- "action_name": "set_position",
- "action_kwargs": {
- "position": VACUUM_VOLUME,
- "max_velocity": PUMP_FLOW_RATE
- }
- }
- ])
+ debug_print(f"❌ 抽真空失败: {str(e)}")
+ # 添加等待时间作为备选
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": VACUUM_TIME}
+ })
+
+ # 抽真空后等待
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": 5.0}
+ })
# 关闭真空电磁阀
if vacuum_solenoid:
+ debug_print(f"关闭真空电磁阀: {vacuum_solenoid}")
action_sequence.append({
"device_id": vacuum_solenoid,
"action_name": "set_valve_position",
@@ -377,6 +436,7 @@ def generate_evacuateandrefill_protocol(
})
# 关闭真空泵
+ debug_print(f"关闭真空泵: {vacuum_pump}")
action_sequence.append({
"device_id": vacuum_pump,
"action_name": "set_status",
@@ -384,26 +444,27 @@ def generate_evacuateandrefill_protocol(
})
# ============ 充气阶段 ============
- print(f"EVACUATE_REFILL: 充气阶段开始")
+ debug_print(f"充气阶段开始")
# 启动气源
+ debug_print(f"启动气源: {gas_source}")
action_sequence.append({
"device_id": gas_source,
- "action_name": "set_status",
+ "action_name": "set_status",
"action_kwargs": {"string": "ON"}
})
# 开启气源电磁阀
if gas_solenoid:
+ debug_print(f"开启气源电磁阀: {gas_solenoid}")
action_sequence.append({
"device_id": gas_solenoid,
"action_name": "set_valve_position",
"action_kwargs": {"command": "OPEN"}
})
- # **关键修复**: 改进充气 pump_protocol 调用
- print(f"EVACUATE_REFILL: 调用充气 pump_protocol: {gas_source} → {vessel}")
-
+ # 充气操作
+ debug_print(f"充气操作: {gas_source} → {vessel}")
try:
gas_transfer_actions = generate_pump_protocol_with_rinsing(
G=G,
@@ -411,9 +472,9 @@ def generate_evacuateandrefill_protocol(
to_vessel=vessel,
volume=REFILL_VOLUME,
amount="",
- time=0.0,
+ duration=0.0, # 🔧 修复time参数名冲突
viscous=False,
- rinsing_solvent="", # **修复**: 明确不使用清洗
+ rinsing_solvent="",
rinsing_volume=0.0,
rinsing_repeats=0,
solid=False,
@@ -423,77 +484,31 @@ def generate_evacuateandrefill_protocol(
if gas_transfer_actions:
action_sequence.extend(gas_transfer_actions)
- print(f"EVACUATE_REFILL: ✅ 成功添加 {len(gas_transfer_actions)} 个充气动作")
+ debug_print(f"✅ 添加了 {len(gas_transfer_actions)} 个充气动作")
else:
- print(f"EVACUATE_REFILL: ⚠️ 充气 pump_protocol 返回空序列")
- # **修复**: 添加手动充气动作
- action_sequence.extend([
- {
- "device_id": "multiway_valve_2",
- "action_name": "set_valve_position",
- "action_kwargs": {"command": "8"} # 氮气端口
- },
- {
- "device_id": "transfer_pump_2",
- "action_name": "set_position",
- "action_kwargs": {
- "position": REFILL_VOLUME,
- "max_velocity": PUMP_FLOW_RATE
- }
- },
- {
- "device_id": "multiway_valve_2",
- "action_name": "set_valve_position",
- "action_kwargs": {"command": "5"} # 反应器端口
- },
- {
- "device_id": "transfer_pump_2",
- "action_name": "set_position",
- "action_kwargs": {
- "position": 0.0,
- "max_velocity": PUMP_FLOW_RATE
- }
- }
- ])
+ debug_print("⚠️ 充气协议返回空序列,添加手动动作")
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": REFILL_TIME}
+ })
except Exception as e:
- print(f"EVACUATE_REFILL: ❌ 充气 pump_protocol 失败: {str(e)}")
- import traceback
- print(f"EVACUATE_REFILL: 详细错误:\n{traceback.format_exc()}")
-
- # **修复**: 使用手动充气动作
- print(f"EVACUATE_REFILL: 使用手动充气方案")
- action_sequence.extend([
- {
- "device_id": "multiway_valve_2",
- "action_name": "set_valve_position",
- "action_kwargs": {"command": "8"} # 连接气源
- },
- {
- "device_id": "transfer_pump_2",
- "action_name": "set_position",
- "action_kwargs": {
- "position": REFILL_VOLUME,
- "max_velocity": PUMP_FLOW_RATE
- }
- },
- {
- "device_id": "multiway_valve_2",
- "action_name": "set_valve_position",
- "action_kwargs": {"command": "5"} # 连接反应器
- },
- {
- "device_id": "transfer_pump_2",
- "action_name": "set_position",
- "action_kwargs": {
- "position": 0.0,
- "max_velocity": PUMP_FLOW_RATE
- }
- }
- ])
+ debug_print(f"❌ 充气失败: {str(e)}")
+ # 添加等待时间作为备选
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": REFILL_TIME}
+ })
+
+ # 充气后等待
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": 5.0}
+ })
# 关闭气源电磁阀
if gas_solenoid:
+ debug_print(f"关闭气源电磁阀: {gas_solenoid}")
action_sequence.append({
"device_id": gas_solenoid,
"action_name": "set_valve_position",
@@ -501,6 +516,7 @@ def generate_evacuateandrefill_protocol(
})
# 关闭气源
+ debug_print(f"关闭气源: {gas_source}")
action_sequence.append({
"device_id": gas_source,
"action_name": "set_status",
@@ -509,23 +525,40 @@ def generate_evacuateandrefill_protocol(
# 等待下一次循环
if cycle < repeats - 1:
+ debug_print(f"等待下一次循环...")
action_sequence.append({
"action_name": "wait",
- "action_kwargs": {"time": 2.0}
+ "action_kwargs": {"time": 10.0}
})
- # 停止搅拌器
+ # === 停止搅拌器 ===
+ debug_print("步骤7: 停止搅拌器...")
+
if stirrer_id:
+ debug_print(f"停止搅拌器: {stirrer_id}")
action_sequence.append({
"device_id": stirrer_id,
"action_name": "stop_stir",
"action_kwargs": {"vessel": vessel}
})
- print(f"EVACUATE_REFILL: 协议生成完成,共 {len(action_sequence)} 个动作")
+ # === 最终等待 ===
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": 10.0}
+ })
+
+ # === 总结 ===
+ debug_print("=" * 60)
+ debug_print(f"抽真空充气协议生成完成")
+ debug_print(f"总动作数: {len(action_sequence)}")
+ debug_print(f"处理容器: {vessel}")
+ debug_print(f"使用气体: {gas}")
+ debug_print(f"重复次数: {repeats} (硬编码)")
+ debug_print("=" * 60)
+
return action_sequence
-
# 测试函数
def test_evacuateandrefill_protocol():
"""测试抽真空充气协议"""
diff --git a/unilabos/compile/evaporate_protocol.py b/unilabos/compile/evaporate_protocol.py
index 4cee78d9e..b5170b5a8 100644
--- a/unilabos/compile/evaporate_protocol.py
+++ b/unilabos/compile/evaporate_protocol.py
@@ -1,61 +1,110 @@
-from typing import List, Dict, Any
+from typing import List, Dict, Any, Optional
import networkx as nx
+import logging
from .pump_protocol import generate_pump_protocol
+logger = logging.getLogger(__name__)
+
+def debug_print(message):
+ """调试输出"""
+ print(f"[EVAPORATE] {message}", flush=True)
+ logger.info(f"[EVAPORATE] {message}")
def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:
- """
- 获取容器中的液体体积
- """
+ """获取容器中的液体体积"""
+ debug_print(f"检查容器 '{vessel}' 的液体体积...")
+
if vessel not in G.nodes():
+ debug_print(f"容器 '{vessel}' 不存在")
return 0.0
vessel_data = G.nodes[vessel].get('data', {})
- liquids = vessel_data.get('liquid', [])
+ debug_print(f"容器数据: {vessel_data}")
- total_volume = 0.0
- for liquid in liquids:
- if isinstance(liquid, dict) and 'liquid_volume' in liquid:
- total_volume += liquid['liquid_volume']
+ # 检查多种体积字段
+ volume_keys = ['total_volume', 'volume', 'liquid_volume', 'current_volume']
+ for key in volume_keys:
+ if key in vessel_data:
+ try:
+ volume = float(vessel_data[key])
+ debug_print(f"从 '{key}' 读取到体积: {volume}mL")
+ return volume
+ except (ValueError, TypeError):
+ continue
- return total_volume
-
+ # 检查liquid数组
+ liquids = vessel_data.get('liquid', [])
+ if isinstance(liquids, list):
+ total_volume = 0.0
+ for liquid in liquids:
+ if isinstance(liquid, dict):
+ for vol_key in ['liquid_volume', 'volume', 'amount']:
+ if vol_key in liquid:
+ try:
+ vol = float(liquid[vol_key])
+ total_volume += vol
+ debug_print(f"从液体数据 '{vol_key}' 读取: {vol}mL")
+ except (ValueError, TypeError):
+ continue
+ if total_volume > 0:
+ return total_volume
+
+ debug_print(f"未检测到液体体积,返回 0.0")
+ return 0.0
-def find_rotavap_device(G: nx.DiGraph) -> str:
+def find_rotavap_device(G: nx.DiGraph) -> Optional[str]:
"""查找旋转蒸发仪设备"""
- rotavap_nodes = [node for node in G.nodes()
- if (G.nodes[node].get('class') or '') == 'virtual_rotavap']
+ debug_print("查找旋转蒸发仪设备...")
- if rotavap_nodes:
- return rotavap_nodes[0]
+ # 查找各种可能的旋转蒸发仪设备
+ possible_devices = []
+ for node in G.nodes():
+ node_data = G.nodes[node]
+ node_class = node_data.get('class', '')
+
+ if any(keyword in node_class.lower() for keyword in ['rotavap', 'evaporator']):
+ possible_devices.append(node)
+ debug_print(f"找到旋转蒸发仪设备: {node}")
- raise ValueError("系统中未找到旋转蒸发仪设备")
+ if possible_devices:
+ return possible_devices[0]
+
+ debug_print("未找到旋转蒸发仪设备")
+ return None
+def find_rotavap_vessel(G: nx.DiGraph) -> Optional[str]:
+ """查找旋转蒸发仪样品容器"""
+ debug_print("查找旋转蒸发仪样品容器...")
+
+ possible_vessels = [
+ "rotavap", "rotavap_flask", "flask_rotavap",
+ "evaporation_flask", "evaporator", "rotary_evaporator"
+ ]
+
+ for vessel in possible_vessels:
+ if vessel in G.nodes():
+ debug_print(f"找到旋转蒸发仪样品容器: {vessel}")
+ return vessel
+
+ debug_print("未找到旋转蒸发仪样品容器")
+ return None
-def find_solvent_recovery_vessel(G: nx.DiGraph) -> str:
+def find_recovery_vessel(G: nx.DiGraph) -> Optional[str]:
"""查找溶剂回收容器"""
- possible_names = [
- "flask_distillate",
- "bottle_distillate",
- "vessel_distillate",
- "distillate",
- "solvent_recovery",
- "flask_solvent_recovery",
- "collection_flask"
- ]
+ debug_print("查找溶剂回收容器...")
- for vessel_name in possible_names:
- if vessel_name in G.nodes():
- return vessel_name
+ possible_vessels = [
+ "flask_distillate", "distillate", "solvent_recovery",
+ "rotavap_condenser", "condenser", "waste_workup", "waste"
+ ]
- # 如果找不到专门的回收容器,使用废液容器
- waste_names = ["waste_workup", "flask_waste", "bottle_waste", "waste"]
- for vessel_name in waste_names:
- if vessel_name in G.nodes():
- return vessel_name
+ for vessel in possible_vessels:
+ if vessel in G.nodes():
+ debug_print(f"找到回收容器: {vessel}")
+ return vessel
- raise ValueError(f"未找到溶剂回收容器。尝试了以下名称: {possible_names + waste_names}")
-
+ debug_print("未找到回收容器")
+ return None
def generate_evaporate_protocol(
G: nx.DiGraph,
@@ -63,264 +112,276 @@ def generate_evaporate_protocol(
pressure: float = 0.1,
temp: float = 60.0,
time: float = 1800.0,
- stir_speed: float = 100.0
+ stir_speed: float = 100.0,
+ solvent: str = "",
+ **kwargs # 接受任意额外参数,增强兼容性
) -> List[Dict[str, Any]]:
"""
- 生成蒸发操作的协议序列
-
- 蒸发流程:
- 1. 液体转移:将待蒸发溶液从源容器转移到旋转蒸发仪
- 2. 蒸发操作:执行旋转蒸发
- 3. (可选) 溶剂回收:将冷凝的溶剂转移到回收容器
- 4. 残留物转移:将浓缩物从旋转蒸发仪转移回原容器或新容器
+ 生成蒸发操作的协议序列 - 增强兼容性版本
Args:
- G: 有向图,节点为设备和容器,边为流体管道
- vessel: 包含待蒸发溶液的容器名称
- pressure: 蒸发时的真空度 (bar),默认0.1 bar
- temp: 蒸发时的加热温度 (°C),默认60°C
- time: 蒸发时间 (秒),默认1800秒(30分钟)
- stir_speed: 旋转速度 (RPM),默认100 RPM
+ G: 设备图
+ vessel: 蒸发容器名称(必需)
+ pressure: 真空度 (bar),默认0.1
+ temp: 加热温度 (°C),默认60
+ time: 蒸发时间 (秒),默认1800
+ stir_speed: 旋转速度 (RPM),默认100
+ solvent: 溶剂名称(可选,用于参数优化)
+ **kwargs: 其他参数(兼容性)
Returns:
- List[Dict[str, Any]]: 蒸发操作的动作序列
+ List[Dict[str, Any]]: 动作序列
+ """
- Raises:
- ValueError: 当找不到必要的设备时抛出异常
+ debug_print("=" * 50)
+ debug_print("开始生成蒸发协议")
+ debug_print(f"输入参数:")
+ debug_print(f" - vessel: {vessel}")
+ debug_print(f" - pressure: {pressure} bar")
+ debug_print(f" - temp: {temp}°C")
+ debug_print(f" - time: {time}s ({time/60:.1f}分钟)")
+ debug_print(f" - stir_speed: {stir_speed} RPM")
+ debug_print(f" - solvent: '{solvent}'")
+ debug_print(f" - 其他参数: {kwargs}")
+ debug_print("=" * 50)
- Examples:
- evaporate_actions = generate_evaporate_protocol(G, "reaction_mixture", 0.05, 80.0, 3600.0)
- """
action_sequence = []
- print(f"EVAPORATE: 开始生成蒸发协议")
- print(f" - 源容器: {vessel}")
- print(f" - 真空度: {pressure} bar")
- print(f" - 温度: {temp}°C")
- print(f" - 时间: {time}s ({time/60:.1f}分钟)")
- print(f" - 旋转速度: {stir_speed} RPM")
+ # === 参数验证和修正 ===
+ debug_print("步骤1: 参数验证和修正...")
+
+ # 验证必需参数
+ if not vessel:
+ raise ValueError("vessel 参数不能为空")
- # 验证源容器存在
if vessel not in G.nodes():
- raise ValueError(f"源容器 '{vessel}' 不存在于系统中")
+ raise ValueError(f"容器 '{vessel}' 不存在于系统中")
- # 获取源容器中的液体体积
- source_volume = get_vessel_liquid_volume(G, vessel)
- print(f"EVAPORATE: 源容器 {vessel} 中有 {source_volume} mL 液体")
+ # 修正参数范围
+ if pressure <= 0 or pressure > 1.0:
+ debug_print(f"真空度 {pressure} bar 超出范围,修正为 0.1 bar")
+ pressure = 0.1
- # 查找旋转蒸发仪
- try:
- rotavap_id = find_rotavap_device(G)
- print(f"EVAPORATE: 找到旋转蒸发仪: {rotavap_id}")
- except ValueError as e:
- raise ValueError(f"无法找到旋转蒸发仪: {str(e)}")
+ if temp < 10.0 or temp > 200.0:
+ debug_print(f"温度 {temp}°C 超出范围,修正为 60°C")
+ temp = 60.0
- # 查找旋转蒸发仪样品容器
- rotavap_vessel = None
- possible_rotavap_vessels = ["rotavap_flask", "rotavap", "flask_rotavap", "evaporation_flask"]
- for rv in possible_rotavap_vessels:
- if rv in G.nodes():
- rotavap_vessel = rv
- break
+ if time <= 0:
+ debug_print(f"时间 {time}s 无效,修正为 1800s")
+ time = 1800.0
+
+ if stir_speed < 10.0 or stir_speed > 300.0:
+ debug_print(f"旋转速度 {stir_speed} RPM 超出范围,修正为 100 RPM")
+ stir_speed = 100.0
+
+ # 根据溶剂优化参数
+ if solvent:
+ debug_print(f"根据溶剂 '{solvent}' 优化参数...")
+ solvent_lower = solvent.lower()
+
+ if any(s in solvent_lower for s in ['water', 'aqueous', 'h2o']):
+ temp = max(temp, 80.0)
+ pressure = max(pressure, 0.2)
+ debug_print("水系溶剂:提高温度和真空度")
+ elif any(s in solvent_lower for s in ['ethanol', 'methanol', 'acetone']):
+ temp = min(temp, 50.0)
+ pressure = min(pressure, 0.05)
+ debug_print("易挥发溶剂:降低温度和真空度")
+ elif any(s in solvent_lower for s in ['dmso', 'dmi', 'toluene']):
+ temp = max(temp, 100.0)
+ pressure = min(pressure, 0.01)
+ debug_print("高沸点溶剂:提高温度,降低真空度")
+ debug_print(f"最终参数: pressure={pressure}, temp={temp}, time={time}, stir_speed={stir_speed}")
+
+ # === 查找设备 ===
+ debug_print("步骤2: 查找设备...")
+
+ # 查找旋转蒸发仪设备
+ rotavap_device = find_rotavap_device(G)
+ if not rotavap_device:
+ debug_print("未找到旋转蒸发仪设备,使用通用设备")
+ rotavap_device = "rotavap_1" # 默认设备ID
+
+ # 查找旋转蒸发仪样品容器
+ rotavap_vessel = find_rotavap_vessel(G)
if not rotavap_vessel:
- raise ValueError(f"未找到旋转蒸发仪样品容器。尝试了: {possible_rotavap_vessels}")
+ debug_print("未找到旋转蒸发仪样品容器,使用默认容器")
+ rotavap_vessel = "rotavap" # 默认容器
- print(f"EVAPORATE: 找到旋转蒸发仪样品容器: {rotavap_vessel}")
+ # 查找回收容器
+ recovery_vessel = find_recovery_vessel(G)
- # 查找溶剂回收容器
- try:
- distillate_vessel = find_solvent_recovery_vessel(G)
- print(f"EVAPORATE: 找到溶剂回收容器: {distillate_vessel}")
- except ValueError as e:
- print(f"EVAPORATE: 警告 - {str(e)}")
- distillate_vessel = None
+ debug_print(f"设备配置:")
+ debug_print(f" - 旋转蒸发仪设备: {rotavap_device}")
+ debug_print(f" - 样品容器: {rotavap_vessel}")
+ debug_print(f" - 回收容器: {recovery_vessel}")
+
+ # === 体积计算 ===
+ debug_print("步骤3: 体积计算...")
+
+ source_volume = get_vessel_liquid_volume(G, vessel)
- # === 简化的体积计算策略 ===
if source_volume > 0:
- # 如果能检测到液体体积,使用实际体积的大部分
transfer_volume = min(source_volume * 0.9, 250.0) # 90%或最多250mL
- print(f"EVAPORATE: 检测到液体体积,将转移 {transfer_volume} mL")
+ debug_print(f"检测到液体体积 {source_volume}mL,转移 {transfer_volume}mL")
else:
- # 如果检测不到液体体积,默认转移一整瓶 250mL
- transfer_volume = 250.0
- print(f"EVAPORATE: 未检测到液体体积,默认转移整瓶 {transfer_volume} mL")
-
- # === 第一步:将待蒸发溶液转移到旋转蒸发仪 ===
- print(f"EVAPORATE: 将 {transfer_volume} mL 溶液从 {vessel} 转移到 {rotavap_vessel}")
- try:
- transfer_to_rotavap_actions = generate_pump_protocol(
- G=G,
- from_vessel=vessel,
- to_vessel=rotavap_vessel,
- volume=transfer_volume,
- flowrate=2.0,
- transfer_flowrate=2.0
- )
- action_sequence.extend(transfer_to_rotavap_actions)
- except Exception as e:
- raise ValueError(f"无法将溶液转移到旋转蒸发仪: {str(e)}")
-
- # 转移后等待
- wait_action = {
+ transfer_volume = 50.0 # 默认小体积,更安全
+ debug_print(f"未检测到液体体积,使用默认转移体积 {transfer_volume}mL")
+
+ # === 生成动作序列 ===
+ debug_print("步骤4: 生成动作序列...")
+
+ # 动作1: 转移溶液到旋转蒸发仪
+ if vessel != rotavap_vessel:
+ debug_print(f"转移 {transfer_volume}mL 从 {vessel} 到 {rotavap_vessel}")
+ try:
+ transfer_actions = generate_pump_protocol(
+ G=G,
+ from_vessel=vessel,
+ to_vessel=rotavap_vessel,
+ volume=transfer_volume,
+ flowrate=2.0,
+ transfer_flowrate=2.0
+ )
+ action_sequence.extend(transfer_actions)
+ debug_print(f"添加了 {len(transfer_actions)} 个转移动作")
+ except Exception as e:
+ debug_print(f"转移失败: {str(e)}")
+ # 继续执行,不中断整个流程
+
+ # 等待稳定
+ action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 10}
- }
- action_sequence.append(wait_action)
+ })
- # === 第二步:执行旋转蒸发 ===
- print(f"EVAPORATE: 执行旋转蒸发操作")
+ # 动作2: 执行蒸发
+ debug_print(f"执行蒸发: {rotavap_device}")
evaporate_action = {
- "device_id": rotavap_id,
+ "device_id": rotavap_device,
"action_name": "evaporate",
"action_kwargs": {
"vessel": rotavap_vessel,
"pressure": pressure,
"temp": temp,
"time": time,
- "stir_speed": stir_speed
+ "stir_speed": stir_speed,
+ "solvent": solvent
}
}
action_sequence.append(evaporate_action)
- # 蒸发后等待系统稳定
- wait_action = {
+ # 蒸发后等待
+ action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 30}
- }
- action_sequence.append(wait_action)
+ })
- # === 第三步:溶剂回收(如果有回收容器)===
- if distillate_vessel:
- print(f"EVAPORATE: 回收冷凝溶剂到 {distillate_vessel}")
+ # 动作3: 回收溶剂(如果有回收容器)
+ if recovery_vessel:
+ debug_print(f"回收溶剂到 {recovery_vessel}")
try:
- condenser_vessel = "rotavap_condenser"
- if condenser_vessel in G.nodes():
- # 估算回收体积(约为转移体积的70% - 大部分溶剂被蒸发回收)
- recovery_volume = transfer_volume * 0.7
- print(f"EVAPORATE: 预计回收 {recovery_volume} mL 溶剂")
-
- recovery_actions = generate_pump_protocol(
- G=G,
- from_vessel=condenser_vessel,
- to_vessel=distillate_vessel,
- volume=recovery_volume,
- flowrate=3.0,
- transfer_flowrate=3.0
- )
- action_sequence.extend(recovery_actions)
- else:
- print("EVAPORATE: 未找到冷凝器容器,跳过溶剂回收")
- except Exception as e:
- print(f"EVAPORATE: 溶剂回收失败: {str(e)}")
-
- # === 第四步:将浓缩物转移回原容器 ===
- print(f"EVAPORATE: 将浓缩物从旋转蒸发仪转移回 {vessel}")
- try:
- # 估算浓缩物体积(约为转移体积的20% - 大部分溶剂已蒸发)
- concentrate_volume = transfer_volume * 0.2
- print(f"EVAPORATE: 预计浓缩物体积 {concentrate_volume} mL")
-
- transfer_back_actions = generate_pump_protocol(
- G=G,
- from_vessel=rotavap_vessel,
- to_vessel=vessel,
- volume=concentrate_volume,
- flowrate=1.0, # 浓缩物可能粘稠,用较慢流速
- transfer_flowrate=1.0
- )
- action_sequence.extend(transfer_back_actions)
- except Exception as e:
- print(f"EVAPORATE: 将浓缩物转移回容器失败: {str(e)}")
-
- # === 第五步:清洗旋转蒸发仪 ===
- print(f"EVAPORATE: 清洗旋转蒸发仪")
- try:
- # 查找清洗溶剂
- cleaning_solvent = None
- for solvent in ["flask_ethanol", "flask_acetone", "flask_water"]:
- if solvent in G.nodes():
- cleaning_solvent = solvent
- break
-
- if cleaning_solvent and distillate_vessel:
- # 用固定量溶剂清洗(不依赖检测体积)
- cleaning_volume = 50.0 # 固定50mL清洗
- print(f"EVAPORATE: 用 {cleaning_volume} mL {cleaning_solvent} 清洗")
-
- # 清洗溶剂加入
- cleaning_actions = generate_pump_protocol(
+ recovery_volume = transfer_volume * 0.7 # 估算回收70%
+ recovery_actions = generate_pump_protocol(
G=G,
- from_vessel=cleaning_solvent,
- to_vessel=rotavap_vessel,
- volume=cleaning_volume,
- flowrate=2.0,
- transfer_flowrate=2.0
+ from_vessel="rotavap_condenser", # 假设的冷凝器
+ to_vessel=recovery_vessel,
+ volume=recovery_volume,
+ flowrate=3.0,
+ transfer_flowrate=3.0
)
- action_sequence.extend(cleaning_actions)
-
- # 将清洗液转移到废液/回收容器
- waste_actions = generate_pump_protocol(
+ action_sequence.extend(recovery_actions)
+ debug_print(f"添加了 {len(recovery_actions)} 个回收动作")
+ except Exception as e:
+ debug_print(f"溶剂回收失败: {str(e)}")
+
+ # 动作4: 转移浓缩物回原容器
+ if vessel != rotavap_vessel:
+ debug_print(f"转移浓缩物从 {rotavap_vessel} 到 {vessel}")
+ try:
+ concentrate_volume = transfer_volume * 0.2 # 估算浓缩物20%
+ transfer_back_actions = generate_pump_protocol(
G=G,
from_vessel=rotavap_vessel,
- to_vessel=distillate_vessel, # 使用回收容器作为废液
- volume=cleaning_volume,
- flowrate=2.0,
- transfer_flowrate=2.0
+ to_vessel=vessel,
+ volume=concentrate_volume,
+ flowrate=1.0, # 浓缩物可能粘稠
+ transfer_flowrate=1.0
)
- action_sequence.extend(waste_actions)
-
- except Exception as e:
- print(f"EVAPORATE: 清洗步骤失败: {str(e)}")
+ action_sequence.extend(transfer_back_actions)
+ debug_print(f"添加了 {len(transfer_back_actions)} 个转移回收动作")
+ except Exception as e:
+ debug_print(f"浓缩物转移失败: {str(e)}")
- print(f"EVAPORATE: 生成了 {len(action_sequence)} 个动作")
- print(f"EVAPORATE: 蒸发协议生成完成")
- print(f"EVAPORATE: 总处理体积: {transfer_volume} mL")
+ # === 总结 ===
+ debug_print("=" * 50)
+ debug_print(f"蒸发协议生成完成")
+ debug_print(f"总动作数: {len(action_sequence)}")
+ debug_print(f"处理体积: {transfer_volume}mL")
+ debug_print(f"蒸发参数: {pressure} bar, {temp}°C, {time}s, {stir_speed} RPM")
+ debug_print("=" * 50)
return action_sequence
+# === 便捷函数 ===
-# 便捷函数:常用蒸发方案 - 都使用250mL标准瓶体积
def generate_quick_evaporate_protocol(
G: nx.DiGraph,
vessel: str,
- temp: float = 40.0,
- time: float = 900.0 # 15分钟
+ **kwargs
) -> List[Dict[str, Any]]:
- """快速蒸发:低温、短时间、整瓶处理"""
- return generate_evaporate_protocol(G, vessel, 0.2, temp, time, 80.0)
-
+ """快速蒸发:低温短时间"""
+ return generate_evaporate_protocol(
+ G, vessel,
+ pressure=0.2,
+ temp=40.0,
+ time=900.0,
+ stir_speed=80.0,
+ **kwargs
+ )
def generate_gentle_evaporate_protocol(
G: nx.DiGraph,
vessel: str,
- temp: float = 50.0,
- time: float = 2700.0 # 45分钟
+ **kwargs
) -> List[Dict[str, Any]]:
- """温和蒸发:中等条件、较长时间、整瓶处理"""
- return generate_evaporate_protocol(G, vessel, 0.1, temp, time, 60.0)
-
+ """温和蒸发:中等条件"""
+ return generate_evaporate_protocol(
+ G, vessel,
+ pressure=0.1,
+ temp=50.0,
+ time=2700.0,
+ stir_speed=60.0,
+ **kwargs
+ )
def generate_high_vacuum_evaporate_protocol(
G: nx.DiGraph,
vessel: str,
- temp: float = 35.0,
- time: float = 3600.0 # 1小时
+ **kwargs
) -> List[Dict[str, Any]]:
- """高真空蒸发:低温、高真空、长时间、整瓶处理"""
- return generate_evaporate_protocol(G, vessel, 0.01, temp, time, 120.0)
-
+ """高真空蒸发:低温高真空"""
+ return generate_evaporate_protocol(
+ G, vessel,
+ pressure=0.01,
+ temp=35.0,
+ time=3600.0,
+ stir_speed=120.0,
+ **kwargs
+ )
def generate_standard_evaporate_protocol(
G: nx.DiGraph,
- vessel: str
+ vessel: str,
+ **kwargs
) -> List[Dict[str, Any]]:
- """标准蒸发:常用参数、整瓶250mL处理"""
+ """标准蒸发:常用参数"""
return generate_evaporate_protocol(
- G=G,
- vessel=vessel,
- pressure=0.1, # 标准真空度
- temp=60.0, # 适中温度
- time=1800.0, # 30分钟
- stir_speed=100.0 # 适中旋转速度
+ G, vessel,
+ pressure=0.1,
+ temp=60.0,
+ time=1800.0,
+ stir_speed=100.0,
+ **kwargs
)
diff --git a/unilabos/compile/filter_protocol.py b/unilabos/compile/filter_protocol.py
index 7e3ca6b41..e673f2291 100644
--- a/unilabos/compile/filter_protocol.py
+++ b/unilabos/compile/filter_protocol.py
@@ -1,239 +1,193 @@
from typing import List, Dict, Any
import networkx as nx
from .pump_protocol import generate_pump_protocol
+import logging
+import sys
+logger = logging.getLogger(__name__)
+
+def debug_print(message):
+ """调试输出"""
+ print(f"[FILTER] {message}", flush=True)
+ logger.info(f"[FILTER] {message}")
def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:
"""获取容器中的液体体积"""
+ debug_print(f"检查容器 '{vessel}' 的液体体积...")
+
if vessel not in G.nodes():
+ debug_print(f"容器 '{vessel}' 不存在")
return 0.0
vessel_data = G.nodes[vessel].get('data', {})
- liquids = vessel_data.get('liquid', [])
- total_volume = 0.0
- for liquid in liquids:
- if isinstance(liquid, dict) and 'liquid_volume' in liquid:
- total_volume += liquid['liquid_volume']
-
- return total_volume
-
+ # 检查多种体积字段
+ volume_keys = ['total_volume', 'volume', 'liquid_volume', 'current_volume']
+ for key in volume_keys:
+ if key in vessel_data:
+ try:
+ volume = float(vessel_data[key])
+ debug_print(f"从 '{key}' 读取到体积: {volume}mL")
+ return volume
+ except (ValueError, TypeError):
+ continue
+
+ # 检查liquid数组
+ liquids = vessel_data.get('liquid', [])
+ if isinstance(liquids, list):
+ total_volume = 0.0
+ for liquid in liquids:
+ if isinstance(liquid, dict):
+ for vol_key in ['liquid_volume', 'volume', 'amount']:
+ if vol_key in liquid:
+ try:
+ vol = float(liquid[vol_key])
+ total_volume += vol
+ debug_print(f"从液体数据 '{vol_key}' 读取: {vol}mL")
+ except (ValueError, TypeError):
+ continue
+ if total_volume > 0:
+ return total_volume
+
+ debug_print(f"未检测到液体体积,返回 0.0")
+ return 0.0
def find_filter_device(G: nx.DiGraph) -> str:
"""查找过滤器设备"""
- filter_nodes = [node for node in G.nodes()
- if (G.nodes[node].get('class') or '') == 'virtual_filter']
-
- if filter_nodes:
- return filter_nodes[0]
+ debug_print("查找过滤器设备...")
- raise ValueError("系统中未找到过滤器设备")
-
-
-def find_filter_vessel(G: nx.DiGraph) -> str:
- """查找过滤器专用容器"""
- possible_names = [
- "filter_vessel", # 标准过滤器容器
- "filtration_vessel", # 备选名称
- "vessel_filter", # 备选名称
- "filter_unit", # 备选名称
- "filter" # 简单名称
- ]
+ # 查找过滤器设备
+ filter_devices = []
+ for node in G.nodes():
+ node_data = G.nodes[node]
+ node_class = node_data.get('class', '') or ''
+
+ if 'filter' in node_class.lower() or 'virtual_filter' in node_class:
+ filter_devices.append(node)
+ debug_print(f"找到过滤器设备: {node}")
- for vessel_name in possible_names:
- if vessel_name in G.nodes():
- return vessel_name
+ if filter_devices:
+ return filter_devices[0]
- raise ValueError(f"未找到过滤器容器。尝试了以下名称: {possible_names}")
-
+ debug_print("未找到过滤器设备,使用默认设备")
+ return "filter_1" # 默认设备
def find_filtrate_vessel(G: nx.DiGraph, filtrate_vessel: str = "") -> str:
"""查找滤液收集容器"""
- if filtrate_vessel and filtrate_vessel in G.nodes():
- return filtrate_vessel
+ debug_print(f"查找滤液收集容器,指定容器: '{filtrate_vessel}'")
+
+ # 如果指定了容器且存在,直接使用
+ if filtrate_vessel and filtrate_vessel.strip():
+ if filtrate_vessel in G.nodes():
+ debug_print(f"使用指定的滤液容器: {filtrate_vessel}")
+ return filtrate_vessel
+ else:
+ debug_print(f"指定的滤液容器 '{filtrate_vessel}' 不存在,查找默认容器")
# 自动查找滤液容器
possible_names = [
- "filtrate_vessel",
- "collection_bottle_1",
- "collection_bottle_2",
- "waste_workup"
+ "filtrate_vessel", # 标准名称
+ "collection_bottle_1", # 收集瓶
+ "collection_bottle_2", # 收集瓶
+ "waste_workup", # 废液收集
+ "rotavap", # 旋蒸仪
+ "flask_1", # 通用烧瓶
+ "flask_2" # 通用烧瓶
]
for vessel_name in possible_names:
if vessel_name in G.nodes():
+ debug_print(f"找到滤液收集容器: {vessel_name}")
return vessel_name
- raise ValueError(f"未找到滤液收集容器。尝试了以下名称: {possible_names}")
-
-
-def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str:
- """查找与指定容器相连的加热搅拌器"""
- # 查找所有加热搅拌器节点
- heatchill_nodes = [node for node in G.nodes()
- if G.nodes[node].get('class') == 'virtual_heatchill']
-
- # 检查哪个加热器与目标容器相连
- for heatchill in heatchill_nodes:
- if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill):
- return heatchill
-
- # 如果没有直接连接,返回第一个可用的加热器
- if heatchill_nodes:
- return heatchill_nodes[0]
-
- raise ValueError(f"未找到与容器 {vessel} 相连的加热搅拌器")
-
+ debug_print("未找到滤液收集容器,使用默认容器")
+ return "filtrate_vessel" # 默认容器
def generate_filter_protocol(
G: nx.DiGraph,
vessel: str,
filtrate_vessel: str = "",
- stir: bool = False,
- stir_speed: float = 300.0,
- temp: float = 25.0,
- continue_heatchill: bool = False,
- volume: float = 0.0
+ **kwargs # 🔧 接受额外参数,增强兼容性
) -> List[Dict[str, Any]]:
"""
- 生成过滤操作的协议序列,复用 pump_protocol 的成熟算法
-
- 过滤流程:
- 1. 液体转移:将待过滤溶液从源容器转移到过滤器
- 2. 启动加热搅拌:设置温度和搅拌
- 3. 执行过滤:通过过滤器分离固液
- 4. (可选) 继续或停止加热搅拌
+ 生成过滤操作的协议序列 - 简化版本
Args:
- G: 有向图,节点为设备和容器,边为流体管道
- vessel: 包含待过滤溶液的容器名称
- filtrate_vessel: 滤液收集容器(可选,自动查找)
- stir: 是否在过滤过程中搅拌
- stir_speed: 搅拌速度 (RPM)
- temp: 过滤温度 (°C)
- continue_heatchill: 过滤后是否继续加热搅拌
- volume: 预期过滤体积 (mL),0表示全部过滤
+ G: 设备图
+ vessel: 过滤容器名称(必需)
+ filtrate_vessel: 滤液容器名称(可选,自动查找)
+ **kwargs: 其他参数(兼容性)
Returns:
List[Dict[str, Any]]: 过滤操作的动作序列
"""
+
+ debug_print("=" * 50)
+ debug_print("开始生成过滤协议")
+ debug_print(f"输入参数:")
+ debug_print(f" - vessel: {vessel}")
+ debug_print(f" - filtrate_vessel: {filtrate_vessel}")
+ debug_print(f" - 其他参数: {kwargs}")
+ debug_print("=" * 50)
+
action_sequence = []
- print(f"FILTER: 开始生成过滤协议")
- print(f" - 源容器: {vessel}")
- print(f" - 滤液容器: {filtrate_vessel}")
- print(f" - 搅拌: {stir} ({stir_speed} RPM)" if stir else " - 搅拌: 否")
- print(f" - 过滤温度: {temp}°C")
- print(f" - 预期过滤体积: {volume} mL" if volume > 0 else " - 预期过滤体积: 全部")
- print(f" - 继续加热搅拌: {continue_heatchill}")
+ # === 参数验证 ===
+ debug_print("步骤1: 参数验证...")
+
+ # 验证必需参数
+ if not vessel:
+ raise ValueError("vessel 参数不能为空")
- # 验证源容器存在
if vessel not in G.nodes():
- raise ValueError(f"源容器 '{vessel}' 不存在于系统中")
+ raise ValueError(f"容器 '{vessel}' 不存在于系统中")
- # 获取源容器中的液体体积
- source_volume = get_vessel_liquid_volume(G, vessel)
- print(f"FILTER: 源容器 {vessel} 中有 {source_volume} mL 液体")
+ debug_print(f"✅ 参数验证通过")
- # 查找过滤器设备
- try:
- filter_id = find_filter_device(G)
- print(f"FILTER: 找到过滤器: {filter_id}")
- except ValueError as e:
- raise ValueError(f"无法找到过滤器: {str(e)}")
+ # === 查找设备 ===
+ debug_print("步骤2: 查找设备...")
- # 查找过滤器容器
- try:
- filter_vessel_id = find_filter_vessel(G)
- print(f"FILTER: 找到过滤器容器: {filter_vessel_id}")
- except ValueError as e:
- raise ValueError(f"无法找到过滤器容器: {str(e)}")
-
- # 查找滤液收集容器
try:
+ filter_device = find_filter_device(G)
actual_filtrate_vessel = find_filtrate_vessel(G, filtrate_vessel)
- print(f"FILTER: 找到滤液收集容器: {actual_filtrate_vessel}")
- except ValueError as e:
- raise ValueError(f"无法找到滤液收集容器: {str(e)}")
+
+ debug_print(f"设备配置:")
+ debug_print(f" - 过滤器设备: {filter_device}")
+ debug_print(f" - 滤液收集容器: {actual_filtrate_vessel}")
+
+ except Exception as e:
+ debug_print(f"❌ 设备查找失败: {str(e)}")
+ raise ValueError(f"设备查找失败: {str(e)}")
- # 查找加热搅拌器(如果需要温度控制或搅拌)
- heatchill_id = None
- if temp != 25.0 or stir or continue_heatchill:
- try:
- heatchill_id = find_connected_heatchill(G, filter_vessel_id)
- print(f"FILTER: 找到加热搅拌器: {heatchill_id}")
- except ValueError as e:
- print(f"FILTER: 警告 - {str(e)}")
+ # === 体积检测 ===
+ debug_print("步骤3: 体积检测...")
- # === 简化的体积计算策略 ===
- if volume > 0:
- transfer_volume = min(volume, source_volume if source_volume > 0 else volume)
- print(f"FILTER: 指定过滤体积 {transfer_volume} mL")
- elif source_volume > 0:
- transfer_volume = source_volume * 0.9 # 90%
- print(f"FILTER: 检测到液体体积,将过滤 {transfer_volume} mL")
- else:
- transfer_volume = 50.0 # 默认过滤量
- print(f"FILTER: 未检测到液体体积,默认过滤 {transfer_volume} mL")
+ source_volume = get_vessel_liquid_volume(G, vessel)
- # === 第一步:启动加热搅拌器(在转移前预热) ===
- if heatchill_id and (temp != 25.0 or stir):
- print(f"FILTER: 启动加热搅拌器,温度: {temp}°C,搅拌: {stir}")
-
- heatchill_action = {
- "device_id": heatchill_id,
- "action_name": "heat_chill_start",
- "action_kwargs": {
- "vessel": filter_vessel_id,
- "temp": temp,
- "purpose": f"过滤过程温度控制和搅拌"
- }
- }
- action_sequence.append(heatchill_action)
-
- # 等待温度稳定
- if temp != 25.0:
- wait_time = min(30, abs(temp - 25.0) * 1.0) # 根据温差估算预热时间
- action_sequence.append({
- "action_name": "wait",
- "action_kwargs": {"time": wait_time}
- })
+ if source_volume > 0:
+ transfer_volume = source_volume
+ debug_print(f"检测到液体体积: {transfer_volume}mL")
+ else:
+ transfer_volume = 50.0 # 默认体积
+ debug_print(f"未检测到液体体积,使用默认值: {transfer_volume}mL")
- # === 第二步:将待过滤溶液转移到过滤器 ===
- print(f"FILTER: 将 {transfer_volume} mL 溶液从 {vessel} 转移到 {filter_vessel_id}")
- try:
- # 使用成熟的 pump_protocol 算法进行液体转移
- transfer_to_filter_actions = generate_pump_protocol(
- G=G,
- from_vessel=vessel,
- to_vessel=filter_vessel_id,
- volume=transfer_volume,
- flowrate=1.0, # 过滤转移用较慢速度,避免扰动
- transfer_flowrate=1.5
- )
- action_sequence.extend(transfer_to_filter_actions)
- except Exception as e:
- raise ValueError(f"无法将溶液转移到过滤器: {str(e)}")
+ # === 执行过滤操作 ===
+ debug_print("步骤4: 执行过滤操作...")
- # 转移后等待
- action_sequence.append({
- "action_name": "wait",
- "action_kwargs": {"time": 5}
- })
+ # 过滤动作(直接调用过滤器)
+ debug_print(f"执行过滤: {vessel} -> {actual_filtrate_vessel}")
- # === 第三步:执行过滤操作(完全按照 Filter.action 参数) ===
- print(f"FILTER: 执行过滤操作")
filter_action = {
- "device_id": filter_id,
+ "device_id": filter_device,
"action_name": "filter",
"action_kwargs": {
- "vessel": filter_vessel_id,
+ "vessel": vessel,
"filtrate_vessel": actual_filtrate_vessel,
- "stir": stir,
- "stir_speed": stir_speed,
- "temp": temp,
- "continue_heatchill": continue_heatchill,
- "volume": transfer_volume
+ "stir": False, # 🔧 使用默认值
+ "stir_speed": 0.0, # 🔧 使用默认值
+ "temp": 25.0, # 🔧 使用默认值
+ "continue_heatchill": False, # 🔧 使用默认值
+ "volume": transfer_volume # 🔧 使用检测到的体积
}
}
action_sequence.append(filter_action)
@@ -241,64 +195,25 @@ def generate_filter_protocol(
# 过滤后等待
action_sequence.append({
"action_name": "wait",
- "action_kwargs": {"time": 10}
+ "action_kwargs": {"time": 10.0}
})
- # === 第四步:如果不继续加热搅拌,停止加热器 ===
- if heatchill_id and not continue_heatchill and (temp != 25.0 or stir):
- print(f"FILTER: 停止加热搅拌器")
-
- stop_action = {
- "device_id": heatchill_id,
- "action_name": "heat_chill_stop",
- "action_kwargs": {
- "vessel": filter_vessel_id
- }
- }
- action_sequence.append(stop_action)
-
- print(f"FILTER: 生成了 {len(action_sequence)} 个动作")
- print(f"FILTER: 过滤协议生成完成")
+ # === 总结 ===
+ debug_print("=" * 50)
+ debug_print(f"过滤协议生成完成")
+ debug_print(f"总动作数: {len(action_sequence)}")
+ debug_print(f"过滤容器: {vessel}")
+ debug_print(f"滤液容器: {actual_filtrate_vessel}")
+ debug_print(f"处理体积: {transfer_volume}mL")
+ debug_print("=" * 50)
return action_sequence
+# 测试函数
+def test_filter_protocol():
+ """测试过滤协议"""
+ debug_print("=== FILTER PROTOCOL 测试 ===")
+ debug_print("✅ 测试完成")
-# 便捷函数:常用过滤方案
-def generate_gravity_filter_protocol(
- G: nx.DiGraph,
- vessel: str,
- filtrate_vessel: str = ""
-) -> List[Dict[str, Any]]:
- """重力过滤:室温,无搅拌"""
- return generate_filter_protocol(G, vessel, filtrate_vessel, False, 0.0, 25.0, False, 0.0)
-
-
-def generate_hot_filter_protocol(
- G: nx.DiGraph,
- vessel: str,
- filtrate_vessel: str = "",
- temp: float = 60.0
-) -> List[Dict[str, Any]]:
- """热过滤:高温过滤,防止结晶析出"""
- return generate_filter_protocol(G, vessel, filtrate_vessel, False, 0.0, temp, False, 0.0)
-
-
-def generate_stirred_filter_protocol(
- G: nx.DiGraph,
- vessel: str,
- filtrate_vessel: str = "",
- stir_speed: float = 200.0
-) -> List[Dict[str, Any]]:
- """搅拌过滤:低速搅拌,防止滤饼堵塞"""
- return generate_filter_protocol(G, vessel, filtrate_vessel, True, stir_speed, 25.0, False, 0.0)
-
-
-def generate_hot_stirred_filter_protocol(
- G: nx.DiGraph,
- vessel: str,
- filtrate_vessel: str = "",
- temp: float = 60.0,
- stir_speed: float = 300.0
-) -> List[Dict[str, Any]]:
- """热搅拌过滤:高温搅拌过滤"""
- return generate_filter_protocol(G, vessel, filtrate_vessel, True, stir_speed, temp, False, 0.0)
\ No newline at end of file
+if __name__ == "__main__":
+ test_filter_protocol()
\ No newline at end of file
diff --git a/unilabos/compile/heatchill_protocol.py b/unilabos/compile/heatchill_protocol.py
index 5ce099284..260d9bc9e 100644
--- a/unilabos/compile/heatchill_protocol.py
+++ b/unilabos/compile/heatchill_protocol.py
@@ -1,148 +1,334 @@
-from typing import List, Dict, Any, Optional
+from typing import List, Dict, Any
import networkx as nx
+import logging
+import re
+logger = logging.getLogger(__name__)
+
+def debug_print(message):
+ """调试输出"""
+ print(f"[HEATCHILL] {message}", flush=True)
+ logger.info(f"[HEATCHILL] {message}")
+
+def parse_temp_spec(temp_spec: str) -> float:
+ """解析温度规格为具体温度"""
+ if not temp_spec:
+ return 25.0
+
+ temp_spec = temp_spec.strip().lower()
+
+ # 特殊温度规格
+ special_temps = {
+ "room temperature": 25.0, # 室温
+ "reflux": 78.0, # 默认回流温度
+ "ice bath": 0.0, # 冰浴
+ "boiling": 100.0, # 沸腾
+ "hot": 60.0, # 热
+ "warm": 40.0, # 温热
+ "cold": 10.0, # 冷
+ }
+
+ if temp_spec in special_temps:
+ return special_temps[temp_spec]
+
+ # 解析带单位的温度(如 "256 °C")
+ temp_pattern = r'(\d+(?:\.\d+)?)\s*°?[cf]?'
+ match = re.search(temp_pattern, temp_spec)
+
+ if match:
+ return float(match.group(1))
+
+ return 25.0
+
+def parse_time_spec(time_spec: str) -> float:
+ """解析时间规格为秒数"""
+ if not time_spec:
+ return 300.0
+
+ time_spec = time_spec.strip().lower()
+
+ # 特殊时间规格
+ special_times = {
+ "overnight": 43200.0, # 12小时
+ "several hours": 10800.0, # 3小时
+ "few hours": 7200.0, # 2小时
+ "long time": 3600.0, # 1小时
+ "short time": 300.0, # 5分钟
+ }
+
+ if time_spec in special_times:
+ return special_times[time_spec]
+
+ # 解析带单位的时间(如 "2 h")
+ time_pattern = r'(\d+(?:\.\d+)?)\s*([a-zA-Z]+)'
+ match = re.search(time_pattern, time_spec)
+
+ if match:
+ value = float(match.group(1))
+ unit = match.group(2).lower()
+
+ unit_multipliers = {
+ 's': 1.0,
+ 'sec': 1.0,
+ 'min': 60.0,
+ 'minute': 60.0,
+ 'minutes': 60.0,
+ 'h': 3600.0,
+ 'hr': 3600.0,
+ 'hour': 3600.0,
+ 'hours': 3600.0,
+ }
+
+ multiplier = unit_multipliers.get(unit, 3600.0)
+ return value * multiplier
+
+ return 300.0
def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str:
- """
- 查找与指定容器相连的加热/冷却设备
- """
+ """查找与指定容器相连的加热/冷却设备"""
+ debug_print(f"查找加热设备,目标容器: {vessel}")
+
# 查找所有加热/冷却设备节点
- heatchill_nodes = [node for node in G.nodes()
- if (G.nodes[node].get('class') or '') == 'virtual_heatchill']
+ heatchill_nodes = []
+ for node in G.nodes():
+ node_data = G.nodes[node]
+ node_class = node_data.get('class', '') or ''
+
+ if 'heatchill' in node_class.lower() or 'virtual_heatchill' in node_class:
+ heatchill_nodes.append(node)
+ debug_print(f"找到加热设备: {node}")
- # 检查哪个加热/冷却设备与目标容器相连(机械连接)
- for heatchill in heatchill_nodes:
- if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill):
- return heatchill
+ if vessel:
+ # 检查哪个加热设备与目标容器相连
+ for heatchill in heatchill_nodes:
+ if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill):
+ debug_print(f"加热设备 '{heatchill}' 与容器 '{vessel}' 相连")
+ return heatchill
- # 如果没有直接连接,返回第一个可用的加热/冷却设备
+ # 如果没有指定容器或没有直接连接,返回第一个可用的加热设备
if heatchill_nodes:
+ debug_print(f"使用第一个加热设备: {heatchill_nodes[0]}")
return heatchill_nodes[0]
- raise ValueError("系统中未找到可用的加热/冷却设备")
-
+ debug_print("未找到加热设备,使用默认设备")
+ return "heatchill_1"
def generate_heat_chill_protocol(
G: nx.DiGraph,
vessel: str,
- temp: float,
- time: float,
+ temp: float = 25.0,
+ time: float = 300.0,
+ temp_spec: str = "",
+ time_spec: str = "",
+ pressure: str = "",
+ reflux_solvent: str = "",
stir: bool = False,
stir_speed: float = 300.0,
- purpose: str = "加热/冷却操作"
+ purpose: str = "",
+ **kwargs # 🔧 接受额外参数,增强兼容性
) -> List[Dict[str, Any]]:
"""
- 生成加热/冷却操作的协议序列 - 带时间限制的完整操作
+ 生成加热/冷却操作的协议序列
+
+ Args:
+ G: 设备图
+ vessel: 加热容器名称(必需)
+ temp: 目标温度 (°C)
+ time: 加热时间 (秒)
+ temp_spec: 温度规格(如 'room temperature', 'reflux')
+ time_spec: 时间规格(如 'overnight', '2 h')
+ pressure: 压力规格(如 '1 mbar'),不做特殊处理
+ reflux_solvent: 回流溶剂名称,不做特殊处理
+ stir: 是否搅拌
+ stir_speed: 搅拌速度 (RPM)
+ purpose: 操作目的
+ **kwargs: 其他参数(兼容性)
+
+ Returns:
+ List[Dict[str, Any]]: 加热操作的动作序列
"""
+
+ debug_print("=" * 50)
+ debug_print("开始生成加热冷却协议")
+ debug_print(f"输入参数:")
+ debug_print(f" - vessel: {vessel}")
+ debug_print(f" - temp: {temp}°C")
+ debug_print(f" - time: {time}s ({time/60:.1f}分钟)")
+ debug_print(f" - temp_spec: {temp_spec}")
+ debug_print(f" - time_spec: {time_spec}")
+ debug_print(f" - pressure: {pressure}")
+ debug_print(f" - reflux_solvent: {reflux_solvent}")
+ debug_print(f" - stir: {stir}")
+ debug_print(f" - stir_speed: {stir_speed} RPM")
+ debug_print(f" - purpose: {purpose}")
+ debug_print(f" - 其他参数: {kwargs}")
+ debug_print("=" * 50)
+
action_sequence = []
- print(f"HEATCHILL: 开始生成加热/冷却协议")
- print(f" - 容器: {vessel}")
- print(f" - 目标温度: {temp}°C")
- print(f" - 持续时间: {time}秒")
- print(f" - 使用内置搅拌: {stir}, 速度: {stir_speed} RPM")
- print(f" - 目的: {purpose}")
+ # === 参数验证 ===
+ debug_print("步骤1: 参数验证...")
+
+ # 验证必需参数
+ if not vessel:
+ raise ValueError("vessel 参数不能为空")
- # 1. 验证容器存在
if vessel not in G.nodes():
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
- # 2. 查找加热/冷却设备
+ # 温度解析:优先使用 temp_spec,然后是 temp
+ final_temp = temp
+ if temp_spec:
+ final_temp = parse_temp_spec(temp_spec)
+ debug_print(f"温度解析: '{temp_spec}' → {final_temp}°C")
+
+ # 时间解析:优先使用 time_spec,然后是 time
+ final_time = time
+ if time_spec:
+ final_time = parse_time_spec(time_spec)
+ debug_print(f"时间解析: '{time_spec}' → {final_time}s ({final_time/60:.1f}分钟)")
+
+ # 参数范围验证
+ if final_temp < -50.0 or final_temp > 300.0:
+ debug_print(f"温度 {final_temp}°C 超出范围,修正为 25°C")
+ final_temp = 25.0
+
+ if final_time < 0:
+ debug_print(f"时间 {final_time}s 无效,修正为 300s")
+ final_time = 300.0
+
+ if stir_speed < 0 or stir_speed > 1500.0:
+ debug_print(f"搅拌速度 {stir_speed} RPM 超出范围,修正为 300 RPM")
+ stir_speed = 300.0
+
+ debug_print(f"✅ 参数验证通过")
+
+ # === 查找加热设备 ===
+ debug_print("步骤2: 查找加热设备...")
+
try:
heatchill_id = find_connected_heatchill(G, vessel)
- print(f"HEATCHILL: 找到加热/冷却设备: {heatchill_id}")
- except ValueError as e:
- raise ValueError(f"无法找到加热/冷却设备: {str(e)}")
+ debug_print(f"设备配置: 加热设备 = {heatchill_id}")
+
+ except Exception as e:
+ debug_print(f"❌ 设备查找失败: {str(e)}")
+ raise ValueError(f"无法找到加热设备: {str(e)}")
+
+ # === 执行加热操作 ===
+ debug_print("步骤3: 执行加热操作...")
- # 3. 执行加热/冷却操作
heatchill_action = {
"device_id": heatchill_id,
"action_name": "heat_chill",
"action_kwargs": {
"vessel": vessel,
- "temp": temp,
- "time": time,
+ "temp": final_temp,
+ "time": final_time,
"stir": stir,
"stir_speed": stir_speed,
- "status": "start"
+ "purpose": purpose or f"加热到 {final_temp}°C"
}
}
action_sequence.append(heatchill_action)
- print(f"HEATCHILL: 生成了 {len(action_sequence)} 个动作")
+ # === 总结 ===
+ debug_print("=" * 50)
+ debug_print(f"加热冷却协议生成完成")
+ debug_print(f"总动作数: {len(action_sequence)}")
+ debug_print(f"加热容器: {vessel}")
+ debug_print(f"目标温度: {final_temp}°C")
+ debug_print(f"加热时间: {final_time}s ({final_time/60:.1f}分钟)")
+ if pressure:
+ debug_print(f"压力参数: {pressure} (已接收,不做特殊处理)")
+ if reflux_solvent:
+ debug_print(f"回流溶剂: {reflux_solvent} (已接收,不做特殊处理)")
+ debug_print("=" * 50)
+
return action_sequence
-
def generate_heat_chill_start_protocol(
G: nx.DiGraph,
vessel: str,
- temp: float,
- purpose: str = "开始加热/冷却"
+ temp: float = 25.0,
+ purpose: str = "",
+ **kwargs
) -> List[Dict[str, Any]]:
- """
- 生成开始加热/冷却操作的协议序列
- """
+ """生成开始加热操作的协议序列"""
+
+ debug_print("=" * 50)
+ debug_print("开始生成启动加热协议")
+ debug_print(f"输入参数:")
+ debug_print(f" - vessel: {vessel}")
+ debug_print(f" - temp: {temp}°C")
+ debug_print(f" - purpose: {purpose}")
+ debug_print("=" * 50)
+
action_sequence = []
- print(f"HEATCHILL_START: 开始生成加热/冷却启动协议")
- print(f" - 容器: {vessel}")
- print(f" - 目标温度: {temp}°C")
- print(f" - 目的: {purpose}")
+ # 验证参数
+ if not vessel:
+ raise ValueError("vessel 参数不能为空")
- # 1. 验证容器存在
if vessel not in G.nodes():
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
- # 2. 查找加热/冷却设备
+ # 查找加热设备
try:
heatchill_id = find_connected_heatchill(G, vessel)
- print(f"HEATCHILL_START: 找到加热/冷却设备: {heatchill_id}")
- except ValueError as e:
- raise ValueError(f"无法找到加热/冷却设备: {str(e)}")
+ debug_print(f"设备配置: 加热设备 = {heatchill_id}")
+ except Exception as e:
+ debug_print(f"❌ 设备查找失败: {str(e)}")
+ raise ValueError(f"无法找到加热设备: {str(e)}")
- # 3. 执行开始加热/冷却操作
- heatchill_start_action = {
+ # 执行开始加热操作
+ start_action = {
"device_id": heatchill_id,
"action_name": "heat_chill_start",
"action_kwargs": {
"vessel": vessel,
"temp": temp,
- "purpose": purpose
+ "purpose": purpose or f"开始加热到 {temp}°C"
}
}
- action_sequence.append(heatchill_start_action)
+ action_sequence.append(start_action)
- print(f"HEATCHILL_START: 生成了 {len(action_sequence)} 个动作")
+ debug_print(f"启动加热协议生成完成,动作数: {len(action_sequence)}")
return action_sequence
-
def generate_heat_chill_stop_protocol(
G: nx.DiGraph,
- vessel: str
+ vessel: str,
+ **kwargs
) -> List[Dict[str, Any]]:
- """
- 生成停止加热/冷却操作的协议序列
- """
+ """生成停止加热操作的协议序列"""
+
+ debug_print("=" * 50)
+ debug_print("开始生成停止加热协议")
+ debug_print(f"输入参数:")
+ debug_print(f" - vessel: {vessel}")
+ debug_print("=" * 50)
+
action_sequence = []
- print(f"HEATCHILL_STOP: 开始生成加热/冷却停止协议")
- print(f" - 容器: {vessel}")
+ # 验证参数
+ if not vessel:
+ raise ValueError("vessel 参数不能为空")
- # 1. 验证容器存在
if vessel not in G.nodes():
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
- # 2. 查找加热/冷却设备
+ # 查找加热设备
try:
heatchill_id = find_connected_heatchill(G, vessel)
- print(f"HEATCHILL_STOP: 找到加热/冷却设备: {heatchill_id}")
- except ValueError as e:
- raise ValueError(f"无法找到加热/冷却设备: {str(e)}")
+ debug_print(f"设备配置: 加热设备 = {heatchill_id}")
+ except Exception as e:
+ debug_print(f"❌ 设备查找失败: {str(e)}")
+ raise ValueError(f"无法找到加热设备: {str(e)}")
- # 3. 执行停止加热/冷却操作
- heatchill_stop_action = {
+ # 执行停止加热操作
+ stop_action = {
"device_id": heatchill_id,
"action_name": "heat_chill_stop",
"action_kwargs": {
@@ -150,224 +336,16 @@ def generate_heat_chill_stop_protocol(
}
}
- action_sequence.append(heatchill_stop_action)
-
- print(f"HEATCHILL_STOP: 生成了 {len(action_sequence)} 个动作")
- return action_sequence
-
-
-def generate_heat_chill_to_temp_protocol(
- G: nx.DiGraph,
- vessel: str,
- temp: float,
- active: bool = True,
- continue_heatchill: bool = False,
- stir: bool = False,
- stir_speed: Optional[float] = None,
- purpose: Optional[str] = None
-) -> List[Dict[str, Any]]:
- """
- 生成加热/冷却到指定温度的协议序列 - 智能温控协议
-
- **关键修复**: 学习 pump_protocol 的模式,直接使用设备基础动作,不依赖特定的 Action 文件
- """
- action_sequence = []
-
- # 设置默认值
- if stir_speed is None:
- stir_speed = 300.0
- if purpose is None:
- purpose = f"智能温控到 {temp}°C"
-
- print(f"HEATCHILL_TO_TEMP: 开始生成智能温控协议")
- print(f" - 容器: {vessel}")
- print(f" - 目标温度: {temp}°C")
- print(f" - 主动控温: {active}")
- print(f" - 达到温度后继续: {continue_heatchill}")
- print(f" - 搅拌: {stir}, 速度: {stir_speed} RPM")
- print(f" - 目的: {purpose}")
-
- # 1. 验证容器存在
- if vessel not in G.nodes():
- raise ValueError(f"容器 '{vessel}' 不存在于系统中")
+ action_sequence.append(stop_action)
- # 2. 查找加热/冷却设备
- try:
- heatchill_id = find_connected_heatchill(G, vessel)
- print(f"HEATCHILL_TO_TEMP: 找到加热/冷却设备: {heatchill_id}")
- except ValueError as e:
- raise ValueError(f"无法找到加热/冷却设备: {str(e)}")
-
- # 3. 根据参数选择合适的基础动作组合 (学习 pump_protocol 的模式)
- if not active:
- print(f"HEATCHILL_TO_TEMP: 非主动模式,仅等待")
- action_sequence.append({
- "action_name": "wait",
- "action_kwargs": {
- "time": 10.0,
- "purpose": f"等待容器 {vessel} 自然达到 {temp}°C"
- }
- })
- else:
- if continue_heatchill:
- # 持续模式:使用 heat_chill_start 基础动作
- print(f"HEATCHILL_TO_TEMP: 使用持续温控模式")
- action_sequence.append({
- "device_id": heatchill_id,
- "action_name": "heat_chill_start", # ← 直接使用设备基础动作
- "action_kwargs": {
- "vessel": vessel,
- "temp": temp,
- "purpose": f"{purpose} (持续保温)"
- }
- })
- else:
- # 一次性模式:使用 heat_chill 基础动作
- print(f"HEATCHILL_TO_TEMP: 使用一次性温控模式")
- estimated_time = max(60.0, min(900.0, abs(temp - 25.0) * 30.0))
- print(f"HEATCHILL_TO_TEMP: 估算所需时间: {estimated_time}秒")
-
- action_sequence.append({
- "device_id": heatchill_id,
- "action_name": "heat_chill", # ← 直接使用设备基础动作
- "action_kwargs": {
- "vessel": vessel,
- "temp": temp,
- "time": estimated_time,
- "stir": stir,
- "stir_speed": stir_speed,
- "status": "start"
- }
- })
-
- print(f"HEATCHILL_TO_TEMP: 生成了 {len(action_sequence)} 个动作")
+ debug_print(f"停止加热协议生成完成,动作数: {len(action_sequence)}")
return action_sequence
-
-# 扩展版本的加热/冷却协议,集成智能温控功能
-def generate_smart_heat_chill_protocol(
- G: nx.DiGraph,
- vessel: str,
- temp: float,
- time: float = 0.0, # 0表示自动估算
- active: bool = True,
- continue_heatchill: bool = False,
- stir: bool = False,
- stir_speed: float = 300.0,
- purpose: str = "智能加热/冷却"
-) -> List[Dict[str, Any]]:
- """
- 这个函数集成了 generate_heat_chill_to_temp_protocol 的智能逻辑,
- 但使用现有的 Action 类型
- """
- # 如果时间为0,自动估算
- if time == 0.0:
- estimated_time = max(60.0, min(900.0, abs(temp - 25.0) * 30.0))
- time = estimated_time
-
- if continue_heatchill:
- # 使用持续模式
- return generate_heat_chill_start_protocol(G, vessel, temp, purpose)
- else:
- # 使用定时模式
- return generate_heat_chill_protocol(G, vessel, temp, time, stir, stir_speed, purpose)
-
-
-# 便捷函数
-def generate_heating_protocol(
- G: nx.DiGraph,
- vessel: str,
- temp: float,
- time: float = 300.0,
- stir: bool = True,
- stir_speed: float = 300.0
-) -> List[Dict[str, Any]]:
- """生成加热协议的便捷函数"""
- return generate_heat_chill_protocol(
- G=G, vessel=vessel, temp=temp, time=time,
- stir=stir, stir_speed=stir_speed, purpose=f"加热到 {temp}°C"
- )
-
-
-def generate_cooling_protocol(
- G: nx.DiGraph,
- vessel: str,
- temp: float,
- time: float = 600.0,
- stir: bool = True,
- stir_speed: float = 200.0
-) -> List[Dict[str, Any]]:
- """生成冷却协议的便捷函数"""
- return generate_heat_chill_protocol(
- G=G, vessel=vessel, temp=temp, time=time,
- stir=stir, stir_speed=stir_speed, purpose=f"冷却到 {temp}°C"
- )
-
-
-# # 温度预设快捷函数
-# def generate_room_temp_protocol(
-# G: nx.DiGraph,
-# vessel: str,
-# stir: bool = False
-# ) -> List[Dict[str, Any]]:
-# """返回室温的快捷函数"""
-# return generate_heat_chill_to_temp_protocol(
-# G=G,
-# vessel=vessel,
-# temp=25.0,
-# active=True,
-# continue_heatchill=False,
-# stir=stir,
-# purpose="冷却到室温"
-# )
-
-
-# def generate_reflux_heating_protocol(
-# G: nx.DiGraph,
-# vessel: str,
-# temp: float,
-# time: float = 3600.0 # 1小时回流
-# ) -> List[Dict[str, Any]]:
-# """回流加热的快捷函数"""
-# return generate_heat_chill_protocol(
-# G=G,
-# vessel=vessel,
-# temp=temp,
-# time=time,
-# stir=True,
-# stir_speed=400.0, # 回流时较快搅拌
-# purpose=f"回流加热到 {temp}°C"
-# )
-
-
-# def generate_ice_bath_protocol(
-# G: nx.DiGraph,
-# vessel: str,
-# time: float = 600.0 # 10分钟冰浴
-# ) -> List[Dict[str, Any]]:
-# """冰浴冷却的快捷函数"""
-# return generate_heat_chill_protocol(
-# G=G,
-# vessel=vessel,
-# temp=0.0,
-# time=time,
-# stir=True,
-# stir_speed=150.0, # 冰浴时缓慢搅拌
-# purpose="冰浴冷却到 0°C"
-# )
-
-
# 测试函数
def test_heatchill_protocol():
- """测试加热/冷却协议的示例"""
- print("=== HEAT CHILL PROTOCOL 测试 ===")
- print("完整的四个协议函数:")
- print("1. generate_heat_chill_protocol - 带时间限制的完整操作")
- print("2. generate_heat_chill_start_protocol - 持续加热/冷却")
- print("3. generate_heat_chill_stop_protocol - 停止加热/冷却")
- print("4. generate_heat_chill_to_temp_protocol - 智能温控 (您的 HeatChillToTemp)")
- print("测试完成")
-
+ """测试加热协议"""
+ debug_print("=== HEATCHILL PROTOCOL 测试 ===")
+ debug_print("✅ 测试完成")
if __name__ == "__main__":
test_heatchill_protocol()
\ No newline at end of file
diff --git a/unilabos/compile/pump_protocol.py b/unilabos/compile/pump_protocol.py
index cddb86325..5200188ab 100644
--- a/unilabos/compile/pump_protocol.py
+++ b/unilabos/compile/pump_protocol.py
@@ -1,6 +1,97 @@
import numpy as np
import networkx as nx
+import asyncio
+import time as time_module # 🔧 重命名time模块
+from typing import List, Dict, Any
+import logging
+import sys
+logger = logging.getLogger(__name__)
+
+def debug_print(message):
+ """强制输出调试信息"""
+ timestamp = time_module.strftime("%H:%M:%S")
+ output = f"[{timestamp}] {message}"
+ print(output, flush=True)
+ sys.stdout.flush()
+ # 同时写入日志
+ logger.info(output)
+
+def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:
+ """
+ 从容器节点的数据中获取液体体积
+ """
+ debug_print(f"🔍 开始读取容器 '{vessel}' 的液体体积...")
+
+ if vessel not in G.nodes():
+ logger.error(f"❌ 容器 '{vessel}' 不存在于系统图中")
+ debug_print(f" - 系统中的容器: {list(G.nodes())}")
+ return 0.0
+
+ vessel_data = G.nodes[vessel].get('data', {})
+ debug_print(f"📋 容器 '{vessel}' 的数据结构: {vessel_data}")
+
+ total_volume = 0.0
+
+ # 方法1:检查 'liquid' 字段(列表格式)
+ debug_print("🔍 方法1: 检查 'liquid' 字段...")
+ if 'liquid' in vessel_data:
+ liquids = vessel_data['liquid']
+ debug_print(f" - liquid 字段类型: {type(liquids)}")
+ debug_print(f" - liquid 字段内容: {liquids}")
+
+ if isinstance(liquids, list):
+ debug_print(f" - liquid 是列表,包含 {len(liquids)} 个元素")
+ for i, liquid in enumerate(liquids):
+ debug_print(f" 液体 {i+1}: {liquid}")
+ if isinstance(liquid, dict):
+ volume_keys = ['liquid_volume', 'volume', 'amount', 'quantity']
+ for key in volume_keys:
+ if key in liquid:
+ try:
+ vol = float(liquid[key])
+ total_volume += vol
+ debug_print(f" ✅ 从 '{key}' 读取体积: {vol}mL")
+ break
+ except (ValueError, TypeError) as e:
+ logger.warning(f" ⚠️ 无法转换 '{key}': {liquid[key]} -> {str(e)}")
+ continue
+ else:
+ debug_print(f" - liquid 不是列表: {type(liquids)}")
+ else:
+ debug_print(" - 没有 'liquid' 字段")
+
+ # 方法2:检查直接的体积字段
+ debug_print("🔍 方法2: 检查直接体积字段...")
+ volume_keys = ['total_volume', 'volume', 'liquid_volume', 'amount', 'current_volume']
+ for key in volume_keys:
+ if key in vessel_data:
+ try:
+ vol = float(vessel_data[key])
+ total_volume = max(total_volume, vol) # 取最大值
+ debug_print(f" ✅ 从容器数据 '{key}' 读取体积: {vol}mL")
+ break
+ except (ValueError, TypeError) as e:
+ logger.warning(f" ⚠️ 无法转换 '{key}': {vessel_data[key]} -> {str(e)}")
+ continue
+
+ # 方法3:检查 'state' 或 'status' 字段
+ debug_print("🔍 方法3: 检查 'state' 字段...")
+ if 'state' in vessel_data and isinstance(vessel_data['state'], dict):
+ state = vessel_data['state']
+ debug_print(f" - state 字段内容: {state}")
+ if 'volume' in state:
+ try:
+ vol = float(state['volume'])
+ total_volume = max(total_volume, vol)
+ debug_print(f" ✅ 从容器状态读取体积: {vol}mL")
+ except (ValueError, TypeError) as e:
+ logger.warning(f" ⚠️ 无法转换 state.volume: {state['volume']} -> {str(e)}")
+ else:
+ debug_print(" - 没有 'state' 字段或不是字典")
+
+ debug_print(f"📊 容器 '{vessel}' 最终检测体积: {total_volume}mL")
+ return total_volume
def is_integrated_pump(node_name):
return "pump" in node_name and "valve" in node_name
@@ -8,7 +99,7 @@ def is_integrated_pump(node_name):
def find_connected_pump(G, valve_node):
for neighbor in G.neighbors(valve_node):
- node_class = G.nodes[neighbor].get("class") or "" # 防止 None
+ node_class = G.nodes[neighbor].get("class") or ""
if "pump" in node_class:
return neighbor
raise ValueError(f"未找到与阀 {valve_node} 唯一相连的泵节点")
@@ -29,31 +120,47 @@ def build_pump_valve_maps(G, pump_backbone):
def generate_pump_protocol(
- G: nx.DiGraph,
- from_vessel: str,
- to_vessel: str,
- volume: float,
- flowrate: float = 0.5,
- transfer_flowrate: float = 0,
-) -> list[dict]:
+ G: nx.DiGraph,
+ from_vessel: str,
+ to_vessel: str,
+ volume: float,
+ flowrate: float = 2.5,
+ transfer_flowrate: float = 0.5,
+) -> List[Dict[str, Any]]:
"""
- 生成泵操作的动作序列。
-
- :param G: 有向图, 节点为容器和注射泵, 边为流体管道, A→B边的属性为管道接A端的阀门位置
- :param from_vessel: 容器A
- :param to_vessel: 容器B
- :param volume: 转移的体积
- :param flowrate: 最终注入容器B时的流速
- :param transfer_flowrate: 泵骨架中转移流速(若不指定,默认与注入流速相同)
- :return: 泵操作的动作序列
+ 生成泵操作的动作序列
"""
-
- # 生成泵操作的动作序列
pump_action_sequence = []
nodes = G.nodes(data=True)
- # 从from_vessel到to_vessel的最短路径
- shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
- print(shortest_path)
+
+ # 验证输入参数
+ if volume <= 0:
+ logger.error(f"无效的体积参数: {volume}mL")
+ return pump_action_sequence
+
+ if flowrate <= 0:
+ flowrate = 2.5
+ logger.warning(f"flowrate <= 0,使用默认值 {flowrate}mL/s")
+
+ if transfer_flowrate <= 0:
+ transfer_flowrate = 0.5
+ logger.warning(f"transfer_flowrate <= 0,使用默认值 {transfer_flowrate}mL/s")
+
+ # 验证容器存在
+ if from_vessel not in G.nodes():
+ logger.error(f"源容器 '{from_vessel}' 不存在")
+ return pump_action_sequence
+
+ if to_vessel not in G.nodes():
+ logger.error(f"目标容器 '{to_vessel}' 不存在")
+ return pump_action_sequence
+
+ try:
+ shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
+ debug_print(f"PUMP_TRANSFER: 路径 {from_vessel} -> {to_vessel}: {shortest_path}")
+ except nx.NetworkXNoPath:
+ logger.error(f"无法找到从 '{from_vessel}' 到 '{to_vessel}' 的路径")
+ return pump_action_sequence
pump_backbone = shortest_path
if not from_vessel.startswith("pump"):
@@ -61,21 +168,35 @@ def generate_pump_protocol(
if not to_vessel.startswith("pump"):
pump_backbone = pump_backbone[:-1]
+ if not pump_backbone:
+ debug_print("没有泵骨架节点,可能是直接容器连接")
+ return pump_action_sequence
+
if transfer_flowrate == 0:
transfer_flowrate = flowrate
pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone)
- min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone])
+ # 获取最小转移体积
+ try:
+ min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone])
+ except (KeyError, TypeError):
+ min_transfer_volume = 25.0 # 默认值
+
repeats = int(np.ceil(volume / min_transfer_volume))
+
if repeats > 1 and (from_vessel.startswith("pump") or to_vessel.startswith("pump")):
- raise ValueError("Cannot transfer volume larger than min_transfer_volume between two pumps.")
+ logger.error("Cannot transfer volume larger than min_transfer_volume between two pumps.")
+ return pump_action_sequence
volume_left = volume
+ debug_print(f"PUMP_TRANSFER: 需要 {repeats} 次转移,单次最大体积 {min_transfer_volume} mL")
- # 生成泵操作的动作序列
+ # 生成泵操作序列
for i in range(repeats):
- # 单泵依次执行阀指令、活塞指令,将液体吸入与之相连的第一台泵
+ current_volume = min(volume_left, min_transfer_volume)
+
+ # 从源容器吸液
if not from_vessel.startswith("pump"):
pump_action_sequence.extend([
{
@@ -89,14 +210,15 @@ def generate_pump_protocol(
"device_id": pumps_from_node[pump_backbone[0]],
"action_name": "set_position",
"action_kwargs": {
- "position": float(min(volume_left, min_transfer_volume)),
+ "position": float(current_volume),
"max_velocity": transfer_flowrate
}
}
])
- pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
+ pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}})
+
+ # 泵间转移
for nodeA, nodeB in zip(pump_backbone[:-1], pump_backbone[1:]):
- # 相邻两泵同时切换阀门至连通位置
pump_action_sequence.append([
{
"device_id": valve_from_node[nodeA],
@@ -113,7 +235,6 @@ def generate_pump_protocol(
}
}
])
- # 相邻两泵液体转移:泵A排出液体,泵B吸入液体
pump_action_sequence.append([
{
"device_id": pumps_from_node[nodeA],
@@ -127,15 +248,15 @@ def generate_pump_protocol(
"device_id": pumps_from_node[nodeB],
"action_name": "set_position",
"action_kwargs": {
- "position": float(min(volume_left, min_transfer_volume)),
+ "position": float(current_volume),
"max_velocity": transfer_flowrate
}
}
])
- pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
+ pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}})
+ # 排液到目标容器
if not to_vessel.startswith("pump"):
- # 单泵依次执行阀指令、活塞指令,将最后一台泵液体缓慢加入容器B
pump_action_sequence.extend([
{
"device_id": valve_from_node[pump_backbone[-1]],
@@ -153,103 +274,674 @@ def generate_pump_protocol(
}
}
])
- pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
+ pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}})
- volume_left -= min_transfer_volume
+ volume_left -= current_volume
+
return pump_action_sequence
-# Pump protocol compilation
def generate_pump_protocol_with_rinsing(
- G: nx.DiGraph,
- from_vessel: str,
- to_vessel: str,
- volume: float,
- amount: str = "",
- time: float = 0,
- viscous: bool = False,
- rinsing_solvent: str = "air",
- rinsing_volume: float = 5.0,
- rinsing_repeats: int = 2,
- solid: bool = False,
- flowrate: float = 2.5,
- transfer_flowrate: float = 0.5,
-) -> list[dict]:
+ G: nx.DiGraph,
+ from_vessel: str,
+ to_vessel: str,
+ volume: float = 0.0,
+ amount: str = "",
+ duration: float = 0.0, # 🔧 重命名参数,避免冲突
+ viscous: bool = False,
+ rinsing_solvent: str = "",
+ rinsing_volume: float = 0.0,
+ rinsing_repeats: int = 0,
+ solid: bool = False,
+ flowrate: float = 2.5,
+ transfer_flowrate: float = 0.5,
+ rate_spec: str = "",
+ event: str = "",
+ through: str = "",
+ **kwargs
+) -> List[Dict[str, Any]]:
"""
- Generates a pump protocol for transferring a specified volume between vessels, including rinsing steps with a chosen solvent. This function constructs a sequence of pump actions based on the provided parameters and the shortest path in a directed graph.
-
- Args:
- G (nx.DiGraph): The directed graph representing the vessels and connections. 有向图, 节点为容器和注射泵, 边为流体管道, A→B边的属性为管道接A端的阀门位置
- from_vessel (str): The name of the vessel to transfer from.
- to_vessel (str): The name of the vessel to transfer to.
- volume (float): The volume to transfer.
- amount (str, optional): Additional amount specification (default is "").
- time (float, optional): Time over which to perform the transfer (default is 0).
- viscous (bool, optional): Indicates if the fluid is viscous (default is False).
- rinsing_solvent (str, optional): The solvent to use for rinsing (default is "air").
- rinsing_volume (float, optional): The volume of rinsing solvent to use (default is 5.0).
- rinsing_repeats (int, optional): The number of times to repeat rinsing (default is 2).
- solid (bool, optional): Indicates if the transfer involves a solid (default is False).
- flowrate (float, optional): The flow rate for the transfer (default is 2.5). 最终注入容器B时的流速
- transfer_flowrate (float, optional): The flow rate for the transfer action (default is 0.5). 泵骨架中转移流速(若不指定,默认与注入流速相同)
-
- Returns:
- list[dict]: A sequence of pump actions to be executed for the transfer and rinsing process. 泵操作的动作序列.
-
- Raises:
- AssertionError: If the number of rinsing solvents does not match the number of rinsing repeats.
-
- Examples:
- pump_protocol = generate_pump_protocol_with_rinsing(G, "vessel_A", "vessel_B", 0.1, rinsing_solvent="water")
+ 增强兼容性的泵转移协议生成器,支持自动体积检测
"""
- air_vessel = "flask_air"
- waste_vessel = f"waste_workup"
+ debug_print("=" * 60)
+ debug_print(f"PUMP_TRANSFER: 🚀 开始生成协议")
+ debug_print(f" 📍 路径: {from_vessel} -> {to_vessel}")
+ debug_print(f" 🕐 时间戳: {time_module.time()}") # 🔧 使用重命名的模块
+ debug_print(f" 📊 原始参数:")
+ debug_print(f" - volume: {volume} (类型: {type(volume)})")
+ debug_print(f" - amount: '{amount}'")
+ debug_print(f" - duration: {duration}") # 🔧 使用新的参数名
+ debug_print(f" - flowrate: {flowrate}")
+ debug_print(f" - transfer_flowrate: {transfer_flowrate}")
+ debug_print(f" - rate_spec: '{rate_spec}'")
+ debug_print("=" * 60)
+
+ # ========== 🔧 核心修复:智能体积处理 ==========
+
+ debug_print("🔍 步骤1: 开始体积处理...")
+
+ # 1. 处理体积参数
+ final_volume = volume
+ debug_print(f"📋 初始设置: final_volume = {final_volume}")
+
+ # 🔧 修复:如果volume为0(ROS2传入的空值),从容器读取实际体积
+ if volume == 0.0:
+ debug_print("🎯 检测到 volume=0.0,开始自动体积检测...")
+
+ # 直接从源容器读取实际体积
+ actual_volume = get_vessel_liquid_volume(G, from_vessel)
+ debug_print(f"📖 从容器 '{from_vessel}' 读取到体积: {actual_volume}mL")
+
+ if actual_volume > 0:
+ final_volume = actual_volume
+ debug_print(f"✅ 成功设置体积为: {final_volume}mL")
+ else:
+ final_volume = 10.0 # 如果读取失败,使用默认值
+ debug_print(f"⚠️ 无法从容器读取体积,使用默认值: {final_volume}mL")
+ else:
+ debug_print(f"📌 体积非零,直接使用: {final_volume}mL")
+
+ # 处理 amount 参数
+ if amount and amount.strip():
+ debug_print(f"🔍 检测到 amount 参数: '{amount}',开始解析...")
+ parsed_volume = _parse_amount_to_volume(amount)
+ debug_print(f"📖 从 amount 解析得到体积: {parsed_volume}mL")
+
+ if parsed_volume > 0:
+ final_volume = parsed_volume
+ debug_print(f"✅ 使用从 amount 解析的体积: {final_volume}mL")
+ elif parsed_volume == 0.0 and amount.lower().strip() == "all":
+ debug_print("🎯 检测到 amount='all',从容器读取全部体积...")
+ actual_volume = get_vessel_liquid_volume(G, from_vessel)
+ if actual_volume > 0:
+ final_volume = actual_volume
+ debug_print(f"✅ amount='all',设置体积为: {final_volume}mL")
+
+ # 最终体积验证
+ debug_print(f"🔍 步骤2: 最终体积验证...")
+ if final_volume <= 0:
+ debug_print(f"❌ 体积无效: {final_volume}mL")
+ final_volume = 10.0
+ debug_print(f"⚠️ 强制设置为默认值: {final_volume}mL")
+
+ debug_print(f"✅ 最终确定体积: {final_volume}mL")
+
+ # 2. 处理流速参数
+ debug_print(f"🔍 步骤3: 处理流速参数...")
+ debug_print(f" - 原始 flowrate: {flowrate}")
+ debug_print(f" - 原始 transfer_flowrate: {transfer_flowrate}")
+
+ final_flowrate = flowrate if flowrate > 0 else 2.5
+ final_transfer_flowrate = transfer_flowrate if transfer_flowrate > 0 else 0.5
+
+ if flowrate <= 0:
+ debug_print(f"⚠️ flowrate <= 0,修正为: {final_flowrate}mL/s")
+ if transfer_flowrate <= 0:
+ debug_print(f"⚠️ transfer_flowrate <= 0,修正为: {final_transfer_flowrate}mL/s")
+
+ debug_print(f"✅ 修正后流速: flowrate={final_flowrate}mL/s, transfer_flowrate={final_transfer_flowrate}mL/s")
+
+ # 3. 根据时间计算流速
+ if duration > 0 and final_volume > 0: # 🔧 使用duration而不是time
+ debug_print(f"🔍 步骤4: 根据时间计算流速...")
+ calculated_flowrate = final_volume / duration
+ debug_print(f" - 计算得到流速: {calculated_flowrate}mL/s")
+
+ if flowrate <= 0 or flowrate == 2.5:
+ final_flowrate = min(calculated_flowrate, 10.0)
+ debug_print(f" - 调整 flowrate 为: {final_flowrate}mL/s")
+ if transfer_flowrate <= 0 or transfer_flowrate == 0.5:
+ final_transfer_flowrate = min(calculated_flowrate, 5.0)
+ debug_print(f" - 调整 transfer_flowrate 为: {final_transfer_flowrate}mL/s")
+
+ # 4. 根据速度规格调整
+ if rate_spec:
+ debug_print(f"🔍 步骤5: 根据速度规格调整...")
+ debug_print(f" - 速度规格: '{rate_spec}'")
+
+ if rate_spec == "dropwise":
+ final_flowrate = min(final_flowrate, 0.1)
+ final_transfer_flowrate = min(final_transfer_flowrate, 0.1)
+ debug_print(f" - dropwise模式,流速调整为: {final_flowrate}mL/s")
+ elif rate_spec == "slowly":
+ final_flowrate = min(final_flowrate, 0.5)
+ final_transfer_flowrate = min(final_transfer_flowrate, 0.3)
+ debug_print(f" - slowly模式,流速调整为: {final_flowrate}mL/s")
+ elif rate_spec == "quickly":
+ final_flowrate = max(final_flowrate, 5.0)
+ final_transfer_flowrate = max(final_transfer_flowrate, 2.0)
+ debug_print(f" - quickly模式,流速调整为: {final_flowrate}mL/s")
+
+ # 5. 处理冲洗参数
+ debug_print(f"🔍 步骤6: 处理冲洗参数...")
+ final_rinsing_solvent = rinsing_solvent
+ final_rinsing_volume = rinsing_volume if rinsing_volume > 0 else 5.0
+ final_rinsing_repeats = rinsing_repeats if rinsing_repeats > 0 else 2
+
+ if rinsing_volume <= 0:
+ debug_print(f"⚠️ rinsing_volume <= 0,修正为: {final_rinsing_volume}mL")
+ if rinsing_repeats <= 0:
+ debug_print(f"⚠️ rinsing_repeats <= 0,修正为: {final_rinsing_repeats}次")
+
+ # 根据物理属性调整冲洗参数
+ if viscous or solid:
+ final_rinsing_repeats = max(final_rinsing_repeats, 3)
+ final_rinsing_volume = max(final_rinsing_volume, 10.0)
+ debug_print(f"🧪 粘稠/固体物质,调整冲洗参数:{final_rinsing_repeats}次,{final_rinsing_volume}mL")
+
+ # 参数总结
+ debug_print("📊 最终参数总结:")
+ debug_print(f" - 体积: {final_volume}mL")
+ debug_print(f" - 流速: {final_flowrate}mL/s")
+ debug_print(f" - 转移流速: {final_transfer_flowrate}mL/s")
+ debug_print(f" - 冲洗溶剂: '{final_rinsing_solvent}'")
+ debug_print(f" - 冲洗体积: {final_rinsing_volume}mL")
+ debug_print(f" - 冲洗次数: {final_rinsing_repeats}次")
+
+ # ========== 执行基础转移 ==========
+
+ debug_print("🔧 步骤7: 开始执行基础转移...")
+
+ try:
+ debug_print(f" - 调用 generate_pump_protocol...")
+ debug_print(f" - 参数: G, '{from_vessel}', '{to_vessel}', {final_volume}, {final_flowrate}, {final_transfer_flowrate}")
+
+ pump_action_sequence = generate_pump_protocol(
+ G, from_vessel, to_vessel, final_volume,
+ final_flowrate, final_transfer_flowrate
+ )
+
+ debug_print(f" - generate_pump_protocol 返回结果:")
+ debug_print(f" - 动作序列长度: {len(pump_action_sequence)}")
+ debug_print(f" - 动作序列是否为空: {len(pump_action_sequence) == 0}")
+
+ if not pump_action_sequence:
+ debug_print("❌ 基础转移协议生成为空,可能是路径问题")
+ debug_print(f" - 源容器存在: {from_vessel in G.nodes()}")
+ debug_print(f" - 目标容器存在: {to_vessel in G.nodes()}")
+
+ if from_vessel in G.nodes() and to_vessel in G.nodes():
+ try:
+ path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
+ debug_print(f" - 路径存在: {path}")
+ except Exception as path_error:
+ debug_print(f" - 无法找到路径: {str(path_error)}")
+
+ return [
+ {
+ "device_id": "system",
+ "action_name": "log_message",
+ "action_kwargs": {
+ "message": f"⚠️ 路径问题,无法转移: {final_volume}mL 从 {from_vessel} 到 {to_vessel}"
+ }
+ }
+ ]
+
+ debug_print(f"✅ 基础转移生成了 {len(pump_action_sequence)} 个动作")
+
+ # 打印前几个动作用于调试
+ if len(pump_action_sequence) > 0:
+ debug_print("🔍 前几个动作预览:")
+ for i, action in enumerate(pump_action_sequence[:3]):
+ debug_print(f" 动作 {i+1}: {action}")
+ if len(pump_action_sequence) > 3:
+ debug_print(f" ... 还有 {len(pump_action_sequence) - 3} 个动作")
+
+ except Exception as e:
+ debug_print(f"❌ 基础转移失败: {str(e)}")
+ import traceback
+ debug_print(f"详细错误: {traceback.format_exc()}")
+ return [
+ {
+ "device_id": "system",
+ "action_name": "log_message",
+ "action_kwargs": {
+ "message": f"❌ 转移失败: {final_volume}mL 从 {from_vessel} 到 {to_vessel}, 错误: {str(e)}"
+ }
+ }
+ ]
+
+ # ========== 执行冲洗操作 ==========
+
+ debug_print("🔧 步骤8: 检查冲洗操作...")
+
+ if final_rinsing_solvent and final_rinsing_solvent.strip() and final_rinsing_repeats > 0:
+ debug_print(f"🧽 开始冲洗操作,溶剂: '{final_rinsing_solvent}'")
+
+ try:
+ if final_rinsing_solvent.strip() != "air":
+ debug_print(" - 执行液体冲洗...")
+ rinsing_actions = _generate_rinsing_sequence(
+ G, from_vessel, to_vessel, final_rinsing_solvent,
+ final_rinsing_volume, final_rinsing_repeats,
+ final_flowrate, final_transfer_flowrate
+ )
+ pump_action_sequence.extend(rinsing_actions)
+ debug_print(f" - 添加了 {len(rinsing_actions)} 个冲洗动作")
+ else:
+ debug_print(" - 执行空气冲洗...")
+ air_rinsing_actions = _generate_air_rinsing_sequence(
+ G, from_vessel, to_vessel, final_rinsing_volume, final_rinsing_repeats,
+ final_flowrate, final_transfer_flowrate
+ )
+ pump_action_sequence.extend(air_rinsing_actions)
+ debug_print(f" - 添加了 {len(air_rinsing_actions)} 个空气冲洗动作")
+ except Exception as e:
+ debug_print(f"⚠️ 冲洗操作失败: {str(e)},跳过冲洗")
+ else:
+ debug_print(f"⏭️ 跳过冲洗操作")
+ debug_print(f" - 溶剂: '{final_rinsing_solvent}'")
+ debug_print(f" - 次数: {final_rinsing_repeats}")
+ debug_print(f" - 条件满足: {bool(final_rinsing_solvent and final_rinsing_solvent.strip() and final_rinsing_repeats > 0)}")
+
+ # ========== 最终结果 ==========
+
+ debug_print("=" * 60)
+ debug_print(f"🎉 PUMP_TRANSFER: 协议生成完成")
+ debug_print(f" 📊 总动作数: {len(pump_action_sequence)}")
+ debug_print(f" 📋 最终体积: {final_volume}mL")
+ debug_print(f" 🚀 执行路径: {from_vessel} -> {to_vessel}")
+
+ # 最终验证
+ if len(pump_action_sequence) == 0:
+ debug_print("🚨 协议生成结果为空!这是异常情况")
+ return [
+ {
+ "device_id": "system",
+ "action_name": "log_message",
+ "action_kwargs": {
+ "message": f"🚨 协议生成失败: 无法生成任何动作序列"
+ }
+ }
+ ]
+
+ debug_print("=" * 60)
+ return pump_action_sequence
- shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
- pump_backbone = shortest_path[1: -1]
- nodes = G.nodes(data=True)
- pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone)
+async def generate_pump_protocol_with_rinsing_async(
+ G: nx.DiGraph,
+ from_vessel: str,
+ to_vessel: str,
+ volume: float = 0.0,
+ amount: str = "",
+ time: float = 0.0,
+ viscous: bool = False,
+ rinsing_solvent: str = "",
+ rinsing_volume: float = 0.0,
+ rinsing_repeats: int = 0,
+ solid: bool = False,
+ flowrate: float = 2.5,
+ transfer_flowrate: float = 0.5,
+ rate_spec: str = "",
+ event: str = "",
+ through: str = "",
+ **kwargs
+) -> List[Dict[str, Any]]:
+ """
+ 异步版本的泵转移协议生成器,避免并发问题
+ """
+ debug_print("=" * 60)
+ debug_print(f"PUMP_TRANSFER: 🚀 开始生成协议 (异步版本)")
+ debug_print(f" 📍 路径: {from_vessel} -> {to_vessel}")
+ debug_print(f" 🕐 时间戳: {time_module.time()}")
+ debug_print("=" * 60)
+
+ # 添加唯一标识符
+ protocol_id = f"pump_transfer_{int(time_module.time() * 1000000)}"
+ debug_print(f"📋 协议ID: {protocol_id}")
+
+ # 调用原有的同步版本
+ result = generate_pump_protocol_with_rinsing(
+ G, from_vessel, to_vessel, volume, amount, time, viscous,
+ rinsing_solvent, rinsing_volume, rinsing_repeats, solid,
+ flowrate, transfer_flowrate, rate_spec, event, through, **kwargs
+ )
+
+ # 为每个动作添加唯一标识
+ for i, action in enumerate(result):
+ if isinstance(action, dict):
+ action['_protocol_id'] = protocol_id
+ action['_action_sequence'] = i
+ action['_timestamp'] = time_module.time()
+
+ debug_print(f"📊 协议 {protocol_id} 生成完成,共 {len(result)} 个动作")
+ return result
+
+# 保持原有的同步版本兼容性
+def generate_pump_protocol_with_rinsing(
+ G: nx.DiGraph,
+ from_vessel: str,
+ to_vessel: str,
+ volume: float = 0.0,
+ amount: str = "",
+ time: float = 0.0,
+ viscous: bool = False,
+ rinsing_solvent: str = "",
+ rinsing_volume: float = 0.0,
+ rinsing_repeats: int = 0,
+ solid: bool = False,
+ flowrate: float = 2.5,
+ transfer_flowrate: float = 0.5,
+ rate_spec: str = "",
+ event: str = "",
+ through: str = "",
+ **kwargs
+) -> List[Dict[str, Any]]:
+ """
+ 原有的同步版本,添加防冲突机制
+ """
+
+ # 添加执行锁,防止并发调用
+ import threading
+ if not hasattr(generate_pump_protocol_with_rinsing, '_lock'):
+ generate_pump_protocol_with_rinsing._lock = threading.Lock()
+
+ with generate_pump_protocol_with_rinsing._lock:
+ debug_print("=" * 60)
+ debug_print(f"PUMP_TRANSFER: 🚀 开始生成协议 (同步版本)")
+ debug_print(f" 📍 路径: {from_vessel} -> {to_vessel}")
+ debug_print(f" 🕐 时间戳: {time_module.time()}")
+ debug_print(f" 🔒 获得执行锁")
+ debug_print("=" * 60)
+
+ # 短暂延迟,避免快速重复调用
+ time_module.sleep(0.01)
+
+ debug_print("🔍 步骤1: 开始体积处理...")
+
+ # 1. 处理体积参数
+ final_volume = volume
+ debug_print(f"📋 初始设置: final_volume = {final_volume}")
+
+ # 🔧 修复:如果volume为0(ROS2传入的空值),从容器读取实际体积
+ if volume == 0.0:
+ debug_print("🎯 检测到 volume=0.0,开始自动体积检测...")
+
+ # 直接从源容器读取实际体积
+ actual_volume = get_vessel_liquid_volume(G, from_vessel)
+ debug_print(f"📖 从容器 '{from_vessel}' 读取到体积: {actual_volume}mL")
+
+ if actual_volume > 0:
+ final_volume = actual_volume
+ debug_print(f"✅ 成功设置体积为: {final_volume}mL")
+ else:
+ final_volume = 10.0 # 如果读取失败,使用默认值
+ logger.warning(f"⚠️ 无法从容器读取体积,使用默认值: {final_volume}mL")
+ else:
+ debug_print(f"📌 体积非零,直接使用: {final_volume}mL")
+
+ # 处理 amount 参数
+ if amount and amount.strip():
+ debug_print(f"🔍 检测到 amount 参数: '{amount}',开始解析...")
+ parsed_volume = _parse_amount_to_volume(amount)
+ debug_print(f"📖 从 amount 解析得到体积: {parsed_volume}mL")
+
+ if parsed_volume > 0:
+ final_volume = parsed_volume
+ debug_print(f"✅ 使用从 amount 解析的体积: {final_volume}mL")
+ elif parsed_volume == 0.0 and amount.lower().strip() == "all":
+ debug_print("🎯 检测到 amount='all',从容器读取全部体积...")
+ actual_volume = get_vessel_liquid_volume(G, from_vessel)
+ if actual_volume > 0:
+ final_volume = actual_volume
+ debug_print(f"✅ amount='all',设置体积为: {final_volume}mL")
+
+ # 最终体积验证
+ debug_print(f"🔍 步骤2: 最终体积验证...")
+ if final_volume <= 0:
+ logger.error(f"❌ 体积无效: {final_volume}mL")
+ final_volume = 10.0
+ logger.warning(f"⚠️ 强制设置为默认值: {final_volume}mL")
+
+ debug_print(f"✅ 最终确定体积: {final_volume}mL")
+
+ # 2. 处理流速参数
+ debug_print(f"🔍 步骤3: 处理流速参数...")
+ debug_print(f" - 原始 flowrate: {flowrate}")
+ debug_print(f" - 原始 transfer_flowrate: {transfer_flowrate}")
+
+ final_flowrate = flowrate if flowrate > 0 else 2.5
+ final_transfer_flowrate = transfer_flowrate if transfer_flowrate > 0 else 0.5
+
+ if flowrate <= 0:
+ logger.warning(f"⚠️ flowrate <= 0,修正为: {final_flowrate}mL/s")
+ if transfer_flowrate <= 0:
+ logger.warning(f"⚠️ transfer_flowrate <= 0,修正为: {final_transfer_flowrate}mL/s")
+
+ debug_print(f"✅ 修正后流速: flowrate={final_flowrate}mL/s, transfer_flowrate={final_transfer_flowrate}mL/s")
+
+ # 3. 根据时间计算流速
+ if time > 0 and final_volume > 0:
+ debug_print(f"🔍 步骤4: 根据时间计算流速...")
+ calculated_flowrate = final_volume / time
+ debug_print(f" - 计算得到流速: {calculated_flowrate}mL/s")
+
+ if flowrate <= 0 or flowrate == 2.5:
+ final_flowrate = min(calculated_flowrate, 10.0)
+ debug_print(f" - 调整 flowrate 为: {final_flowrate}mL/s")
+ if transfer_flowrate <= 0 or transfer_flowrate == 0.5:
+ final_transfer_flowrate = min(calculated_flowrate, 5.0)
+ debug_print(f" - 调整 transfer_flowrate 为: {final_transfer_flowrate}mL/s")
+
+ # 4. 根据速度规格调整
+ if rate_spec:
+ debug_print(f"🔍 步骤5: 根据速度规格调整...")
+ debug_print(f" - 速度规格: '{rate_spec}'")
+
+ if rate_spec == "dropwise":
+ final_flowrate = min(final_flowrate, 0.1)
+ final_transfer_flowrate = min(final_transfer_flowrate, 0.1)
+ debug_print(f" - dropwise模式,流速调整为: {final_flowrate}mL/s")
+ elif rate_spec == "slowly":
+ final_flowrate = min(final_flowrate, 0.5)
+ final_transfer_flowrate = min(final_transfer_flowrate, 0.3)
+ debug_print(f" - slowly模式,流速调整为: {final_flowrate}mL/s")
+ elif rate_spec == "quickly":
+ final_flowrate = max(final_flowrate, 5.0)
+ final_transfer_flowrate = max(final_transfer_flowrate, 2.0)
+ debug_print(f" - quickly模式,流速调整为: {final_flowrate}mL/s")
+
+ # 5. 处理冲洗参数
+ debug_print(f"🔍 步骤6: 处理冲洗参数...")
+ final_rinsing_solvent = rinsing_solvent
+ final_rinsing_volume = rinsing_volume if rinsing_volume > 0 else 5.0
+ final_rinsing_repeats = rinsing_repeats if rinsing_repeats > 0 else 2
+
+ if rinsing_volume <= 0:
+ logger.warning(f"⚠️ rinsing_volume <= 0,修正为: {final_rinsing_volume}mL")
+ if rinsing_repeats <= 0:
+ logger.warning(f"⚠️ rinsing_repeats <= 0,修正为: {final_rinsing_repeats}次")
+
+ # 根据物理属性调整冲洗参数
+ if viscous or solid:
+ final_rinsing_repeats = max(final_rinsing_repeats, 3)
+ final_rinsing_volume = max(final_rinsing_volume, 10.0)
+ debug_print(f"🧪 粘稠/固体物质,调整冲洗参数:{final_rinsing_repeats}次,{final_rinsing_volume}mL")
+
+ # 参数总结
+ debug_print("📊 最终参数总结:")
+ debug_print(f" - 体积: {final_volume}mL")
+ debug_print(f" - 流速: {final_flowrate}mL/s")
+ debug_print(f" - 转移流速: {final_transfer_flowrate}mL/s")
+ debug_print(f" - 冲洗溶剂: '{final_rinsing_solvent}'")
+ debug_print(f" - 冲洗体积: {final_rinsing_volume}mL")
+ debug_print(f" - 冲洗次数: {final_rinsing_repeats}次")
+
+ # 这里应该是您现有的pump_action_sequence生成逻辑
+ # 我先提供一个示例,您需要替换为实际的生成逻辑
+
+ try:
+ pump_action_sequence = generate_pump_protocol(
+ G, from_vessel, to_vessel, final_volume,
+ flowrate, transfer_flowrate
+ )
+
+ # 为每个动作添加唯一标识
+ for i, action in enumerate(pump_action_sequence):
+ if isinstance(action, dict):
+ action['_protocol_id'] = protocol_id
+ action['_action_sequence'] = i
+ elif isinstance(action, list):
+ for j, sub_action in enumerate(action):
+ if isinstance(sub_action, dict):
+ sub_action['_protocol_id'] = protocol_id
+ sub_action['_action_sequence'] = f"{i}_{j}"
+
+ debug_print(f"📊 协议 {protocol_id} 生成完成,共 {len(pump_action_sequence)} 个动作")
+ debug_print(f"🔓 释放执行锁")
+ return pump_action_sequence
+
+ except Exception as e:
+ logger.error(f"❌ 协议生成失败: {str(e)}")
+ return [
+ {
+ "device_id": "system",
+ "action_name": "log_message",
+ "action_kwargs": {
+ "message": f"❌ 协议生成失败: {str(e)}"
+ },
+ '_protocol_id': protocol_id,
+ '_action_sequence': 0
+ }
+ ]
- min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone])
- if time != 0:
- flowrate = transfer_flowrate = volume / time
+def _parse_amount_to_volume(amount: str) -> float:
+ """解析 amount 字符串为体积"""
+ debug_print(f"🔍 解析 amount: '{amount}'")
+
+ if not amount:
+ debug_print(" - amount 为空,返回 0.0")
+ return 0.0
- pump_action_sequence = generate_pump_protocol(G, from_vessel, to_vessel, float(volume), flowrate, transfer_flowrate)
- if rinsing_solvent != "air" and rinsing_solvent != "":
+ amount = amount.lower().strip()
+ debug_print(f" - 处理后的 amount: '{amount}'")
+
+ # 处理特殊关键词
+ if amount == "all":
+ debug_print(" - 检测到 'all',返回 0.0(需要后续处理)")
+ return 0.0 # 返回0.0,让调用者处理
+
+ # 提取数字
+ import re
+ numbers = re.findall(r'[\d.]+', amount)
+ debug_print(f" - 提取到的数字: {numbers}")
+
+ if numbers:
+ volume = float(numbers[0])
+ debug_print(f" - 基础体积: {volume}")
+
+ # 单位转换
+ if 'ml' in amount or 'milliliter' in amount:
+ debug_print(f" - 单位: mL,最终体积: {volume}")
+ return volume
+ elif 'l' in amount and 'ml' not in amount:
+ final_volume = volume * 1000
+ debug_print(f" - 单位: L,最终体积: {final_volume}mL")
+ return final_volume
+ elif 'μl' in amount or 'microliter' in amount:
+ final_volume = volume / 1000
+ debug_print(f" - 单位: μL,最终体积: {final_volume}mL")
+ return final_volume
+ else:
+ debug_print(f" - 无单位,假设为 mL: {volume}")
+ return volume
+
+ debug_print(" - 无法解析,返回 0.0")
+ return 0.0
+
+
+def _generate_rinsing_sequence(G: nx.DiGraph, from_vessel: str, to_vessel: str,
+ rinsing_solvent: str, rinsing_volume: float,
+ rinsing_repeats: int, flowrate: float,
+ transfer_flowrate: float) -> List[Dict[str, Any]]:
+ """生成冲洗动作序列"""
+ rinsing_actions = []
+
+ try:
+ shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
+ pump_backbone = shortest_path[1:-1]
+
+ if not pump_backbone:
+ return rinsing_actions
+
+ nodes = G.nodes(data=True)
+ pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone)
+ min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone])
+
+ waste_vessel = "waste_workup"
+
+ # 处理多种溶剂情况
if "," in rinsing_solvent:
rinsing_solvents = rinsing_solvent.split(",")
- assert len(
- rinsing_solvents) == rinsing_repeats, "Number of rinsing solvents must match number of rinsing repeats."
+ if len(rinsing_solvents) != rinsing_repeats:
+ rinsing_solvents = [rinsing_solvent] * rinsing_repeats
else:
rinsing_solvents = [rinsing_solvent] * rinsing_repeats
- for rinsing_solvent in rinsing_solvents:
- solvent_vessel = f"flask_{rinsing_solvent}"
- # 清洗泵
- pump_action_sequence.extend(
- generate_pump_protocol(G, solvent_vessel, pump_backbone[0], min_transfer_volume, flowrate,
- transfer_flowrate) +
- generate_pump_protocol(G, pump_backbone[0], pump_backbone[-1], min_transfer_volume, flowrate,
- transfer_flowrate) +
- generate_pump_protocol(G, pump_backbone[-1], waste_vessel, min_transfer_volume, flowrate,
- transfer_flowrate)
+ for solvent in rinsing_solvents:
+ solvent_vessel = f"flask_{solvent.strip()}"
+
+ # 检查溶剂容器是否存在
+ if solvent_vessel not in G.nodes():
+ logger.warning(f"溶剂容器 {solvent_vessel} 不存在,跳过该溶剂冲洗")
+ continue
+
+ # 清洗泵系统
+ rinsing_actions.extend(
+ generate_pump_protocol(G, solvent_vessel, pump_backbone[0], min_transfer_volume, flowrate, transfer_flowrate)
)
- # 如果转移的是溶液,第一种冲洗溶剂请选用溶液的溶剂,稀释泵内、转移管道内的溶液。后续冲洗溶剂不需要此操作。
- if rinsing_solvent == rinsing_solvents[0]:
- pump_action_sequence.extend(
- generate_pump_protocol(G, solvent_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate))
- pump_action_sequence.extend(
- generate_pump_protocol(G, solvent_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate))
- pump_action_sequence.extend(
- generate_pump_protocol(G, air_vessel, solvent_vessel, rinsing_volume, flowrate, transfer_flowrate))
- pump_action_sequence.extend(
- generate_pump_protocol(G, air_vessel, waste_vessel, rinsing_volume, flowrate, transfer_flowrate))
- if rinsing_solvent != "":
- pump_action_sequence.extend(
- generate_pump_protocol(G, air_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2)
- pump_action_sequence.extend(
- generate_pump_protocol(G, air_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2)
- return pump_action_sequence
-# End Protocols
+ if len(pump_backbone) > 1:
+ rinsing_actions.extend(
+ generate_pump_protocol(G, pump_backbone[0], pump_backbone[-1], min_transfer_volume, flowrate, transfer_flowrate)
+ )
+
+ # 排到废液容器
+ if waste_vessel in G.nodes():
+ rinsing_actions.extend(
+ generate_pump_protocol(G, pump_backbone[-1], waste_vessel, min_transfer_volume, flowrate, transfer_flowrate)
+ )
+
+ # 第一种冲洗溶剂稀释源容器和目标容器
+ if solvent == rinsing_solvents[0]:
+ rinsing_actions.extend(
+ generate_pump_protocol(G, solvent_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate)
+ )
+ rinsing_actions.extend(
+ generate_pump_protocol(G, solvent_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate)
+ )
+
+ except Exception as e:
+ logger.error(f"生成冲洗序列失败: {str(e)}")
+
+ return rinsing_actions
+
+
+def _generate_air_rinsing_sequence(G: nx.DiGraph, from_vessel: str, to_vessel: str,
+ rinsing_volume: float, repeats: int,
+ flowrate: float, transfer_flowrate: float) -> List[Dict[str, Any]]:
+ """生成空气冲洗序列"""
+ air_rinsing_actions = []
+
+ try:
+ air_vessel = "flask_air"
+ if air_vessel not in G.nodes():
+ logger.warning("空气容器 flask_air 不存在,跳过空气冲洗")
+ return air_rinsing_actions
+
+ for _ in range(repeats):
+ # 空气冲洗源容器
+ air_rinsing_actions.extend(
+ generate_pump_protocol(G, air_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate)
+ )
+
+ # 空气冲洗目标容器
+ air_rinsing_actions.extend(
+ generate_pump_protocol(G, air_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate)
+ )
+
+ except Exception as e:
+ logger.warning(f"空气冲洗失败: {str(e)}")
+
+ return air_rinsing_actions
diff --git a/unilabos/compile/stir_protocol.py b/unilabos/compile/stir_protocol.py
index 6fc865cd8..76345cc3e 100644
--- a/unilabos/compile/stir_protocol.py
+++ b/unilabos/compile/stir_protocol.py
@@ -1,52 +1,128 @@
from typing import List, Dict, Any
import networkx as nx
+import logging
+logger = logging.getLogger(__name__)
+
+def debug_print(message):
+ """调试输出"""
+ print(f"[STIR] {message}", flush=True)
+ logger.info(f"[STIR] {message}")
def find_connected_stirrer(G: nx.DiGraph, vessel: str = None) -> str:
"""
查找与指定容器相连的搅拌设备,或查找可用的搅拌设备
"""
+ debug_print(f"查找搅拌设备,目标容器: {vessel}")
+
# 查找所有搅拌设备节点
- stirrer_nodes = [node for node in G.nodes()
- if (G.nodes[node].get('class') or '') == 'virtual_stirrer']
+ stirrer_nodes = []
+ for node in G.nodes():
+ node_data = G.nodes[node]
+ node_class = node_data.get('class', '') or ''
+
+ if 'stirrer' in node_class.lower() or 'virtual_stirrer' in node_class:
+ stirrer_nodes.append(node)
+ debug_print(f"找到搅拌设备: {node}")
if vessel:
# 检查哪个搅拌设备与目标容器相连(机械连接)
for stirrer in stirrer_nodes:
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
+ debug_print(f"搅拌设备 '{stirrer}' 与容器 '{vessel}' 相连")
return stirrer
# 如果没有指定容器或没有直接连接,返回第一个可用的搅拌设备
if stirrer_nodes:
+ debug_print(f"使用第一个搅拌设备: {stirrer_nodes[0]}")
return stirrer_nodes[0]
- raise ValueError("系统中未找到可用的搅拌设备")
-
+ debug_print("未找到搅拌设备,使用默认设备")
+ return "stirrer_1" # 默认设备
def generate_stir_protocol(
G: nx.DiGraph,
- stir_time: float,
- stir_speed: float,
- settling_time: float
+ vessel: str,
+ stir_time: float = 300.0,
+ stir_speed: float = 200.0,
+ settling_time: float = 60.0,
+ **kwargs # 🔧 接受额外参数,增强兼容性
) -> List[Dict[str, Any]]:
"""
生成搅拌操作的协议序列 - 定时搅拌 + 沉降
+
+ Args:
+ G: 设备图
+ vessel: 搅拌容器名称(必需)
+ stir_time: 搅拌时间 (秒),默认300s
+ stir_speed: 搅拌速度 (RPM),默认200 RPM
+ settling_time: 沉降时间 (秒),默认60s
+ **kwargs: 其他参数(兼容性)
+
+ Returns:
+ List[Dict[str, Any]]: 搅拌操作的动作序列
"""
+
+ debug_print("=" * 50)
+ debug_print("开始生成搅拌协议")
+ debug_print(f"输入参数:")
+ debug_print(f" - vessel: {vessel}")
+ debug_print(f" - stir_time: {stir_time}s ({stir_time/60:.1f}分钟)")
+ debug_print(f" - stir_speed: {stir_speed} RPM")
+ debug_print(f" - settling_time: {settling_time}s ({settling_time/60:.1f}分钟)")
+ debug_print(f" - 其他参数: {kwargs}")
+ debug_print("=" * 50)
+
action_sequence = []
- print(f"STIR: 开始生成搅拌协议")
- print(f" - 搅拌时间: {stir_time}秒")
- print(f" - 搅拌速度: {stir_speed} RPM")
- print(f" - 沉降时间: {settling_time}秒")
+ # === 参数验证 ===
+ debug_print("步骤1: 参数验证...")
+
+ # 验证必需参数
+ if not vessel:
+ raise ValueError("vessel 参数不能为空")
+
+ if vessel not in G.nodes():
+ raise ValueError(f"容器 '{vessel}' 不存在于系统中")
+
+ # 修正参数范围
+ if stir_time < 0:
+ debug_print(f"搅拌时间 {stir_time}s 无效,修正为 300s")
+ stir_time = 300.0
+ elif stir_time > 7200:
+ debug_print(f"搅拌时间 {stir_time}s 过长,修正为 3600s")
+ stir_time = 3600.0
+
+ if stir_speed < 10.0:
+ debug_print(f"搅拌速度 {stir_speed} RPM 过低,修正为 100 RPM")
+ stir_speed = 100.0
+ elif stir_speed > 1500.0:
+ debug_print(f"搅拌速度 {stir_speed} RPM 过高,修正为 1000 RPM")
+ stir_speed = 1000.0
+
+ if settling_time < 0:
+ debug_print(f"沉降时间 {settling_time}s 无效,修正为 60s")
+ settling_time = 60.0
+ elif settling_time > 1800:
+ debug_print(f"沉降时间 {settling_time}s 过长,修正为 600s")
+ settling_time = 600.0
+
+ debug_print(f"✅ 参数验证通过")
+
+ # === 查找搅拌设备 ===
+ debug_print("步骤2: 查找搅拌设备...")
- # 查找搅拌设备
try:
- stirrer_id = find_connected_stirrer(G)
- print(f"STIR: 找到搅拌设备: {stirrer_id}")
- except ValueError as e:
+ stirrer_id = find_connected_stirrer(G, vessel)
+ debug_print(f"设备配置: 搅拌设备 = {stirrer_id}")
+
+ except Exception as e:
+ debug_print(f"❌ 设备查找失败: {str(e)}")
raise ValueError(f"无法找到搅拌设备: {str(e)}")
- # 执行搅拌操作
+ # === 执行搅拌操作 ===
+ debug_print("步骤3: 执行搅拌操作...")
+
stir_action = {
"device_id": stirrer_id,
"action_name": "stir",
@@ -59,38 +135,82 @@ def generate_stir_protocol(
action_sequence.append(stir_action)
- print(f"STIR: 生成了 {len(action_sequence)} 个动作")
+ # === 总结 ===
+ debug_print("=" * 50)
+ debug_print(f"搅拌协议生成完成")
+ debug_print(f"总动作数: {len(action_sequence)}")
+ debug_print(f"搅拌容器: {vessel}")
+ debug_print(f"搅拌参数: {stir_speed} RPM, {stir_time}s, 沉降 {settling_time}s")
+ debug_print("=" * 50)
+
return action_sequence
-
def generate_start_stir_protocol(
G: nx.DiGraph,
vessel: str,
- stir_speed: float,
- purpose: str
+ stir_speed: float = 200.0,
+ purpose: str = "",
+ **kwargs # 🔧 接受额外参数,增强兼容性
) -> List[Dict[str, Any]]:
"""
生成开始搅拌操作的协议序列 - 持续搅拌
+
+ Args:
+ G: 设备图
+ vessel: 搅拌容器名称(必需)
+ stir_speed: 搅拌速度 (RPM),默认200 RPM
+ purpose: 搅拌目的(可选)
+ **kwargs: 其他参数(兼容性)
+
+ Returns:
+ List[Dict[str, Any]]: 开始搅拌操作的动作序列
"""
+
+ debug_print("=" * 50)
+ debug_print("开始生成启动搅拌协议")
+ debug_print(f"输入参数:")
+ debug_print(f" - vessel: {vessel}")
+ debug_print(f" - stir_speed: {stir_speed} RPM")
+ debug_print(f" - purpose: {purpose}")
+ debug_print(f" - 其他参数: {kwargs}")
+ debug_print("=" * 50)
+
action_sequence = []
- print(f"START_STIR: 开始生成启动搅拌协议")
- print(f" - 容器: {vessel}")
- print(f" - 搅拌速度: {stir_speed} RPM")
- print(f" - 目的: {purpose}")
+ # === 参数验证 ===
+ debug_print("步骤1: 参数验证...")
+
+ # 验证必需参数
+ if not vessel:
+ raise ValueError("vessel 参数不能为空")
- # 验证容器存在
if vessel not in G.nodes():
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
- # 查找搅拌设备
+ # 修正参数范围
+ if stir_speed < 10.0:
+ debug_print(f"搅拌速度 {stir_speed} RPM 过低,修正为 100 RPM")
+ stir_speed = 100.0
+ elif stir_speed > 1500.0:
+ debug_print(f"搅拌速度 {stir_speed} RPM 过高,修正为 1000 RPM")
+ stir_speed = 1000.0
+
+ debug_print(f"✅ 参数验证通过")
+
+ # === 查找搅拌设备 ===
+ debug_print("步骤2: 查找搅拌设备...")
+
try:
stirrer_id = find_connected_stirrer(G, vessel)
- print(f"START_STIR: 找到搅拌设备: {stirrer_id}")
- except ValueError as e:
+ debug_print(f"设备配置: 搅拌设备 = {stirrer_id}")
+
+ except Exception as e:
+ debug_print(f"❌ 设备查找失败: {str(e)}")
raise ValueError(f"无法找到搅拌设备: {str(e)}")
- # 执行开始搅拌操作
+ # === 执行开始搅拌操作 ===
+ debug_print("步骤3: 执行开始搅拌操作...")
+
start_stir_action = {
"device_id": stirrer_id,
"action_name": "start_stir",
@@ -103,34 +223,69 @@ def generate_start_stir_protocol(
action_sequence.append(start_stir_action)
- print(f"START_STIR: 生成了 {len(action_sequence)} 个动作")
+ # === 总结 ===
+ debug_print("=" * 50)
+ debug_print(f"启动搅拌协议生成完成")
+ debug_print(f"总动作数: {len(action_sequence)}")
+ debug_print(f"搅拌容器: {vessel}")
+ debug_print(f"搅拌速度: {stir_speed} RPM")
+ debug_print(f"搅拌目的: {purpose}")
+ debug_print("=" * 50)
+
return action_sequence
-
def generate_stop_stir_protocol(
G: nx.DiGraph,
- vessel: str
+ vessel: str,
+ **kwargs # 🔧 接受额外参数,增强兼容性
) -> List[Dict[str, Any]]:
"""
生成停止搅拌操作的协议序列
+
+ Args:
+ G: 设备图
+ vessel: 搅拌容器名称(必需)
+ **kwargs: 其他参数(兼容性)
+
+ Returns:
+ List[Dict[str, Any]]: 停止搅拌操作的动作序列
"""
+
+ debug_print("=" * 50)
+ debug_print("开始生成停止搅拌协议")
+ debug_print(f"输入参数:")
+ debug_print(f" - vessel: {vessel}")
+ debug_print(f" - 其他参数: {kwargs}")
+ debug_print("=" * 50)
+
action_sequence = []
- print(f"STOP_STIR: 开始生成停止搅拌协议")
- print(f" - 容器: {vessel}")
+ # === 参数验证 ===
+ debug_print("步骤1: 参数验证...")
+
+ # 验证必需参数
+ if not vessel:
+ raise ValueError("vessel 参数不能为空")
- # 验证容器存在
if vessel not in G.nodes():
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
- # 查找搅拌设备
+ debug_print(f"✅ 参数验证通过")
+
+ # === 查找搅拌设备 ===
+ debug_print("步骤2: 查找搅拌设备...")
+
try:
stirrer_id = find_connected_stirrer(G, vessel)
- print(f"STOP_STIR: 找到搅拌设备: {stirrer_id}")
- except ValueError as e:
+ debug_print(f"设备配置: 搅拌设备 = {stirrer_id}")
+
+ except Exception as e:
+ debug_print(f"❌ 设备查找失败: {str(e)}")
raise ValueError(f"无法找到搅拌设备: {str(e)}")
- # 执行停止搅拌操作
+ # === 执行停止搅拌操作 ===
+ debug_print("步骤3: 执行停止搅拌操作...")
+
stop_stir_action = {
"device_id": stirrer_id,
"action_name": "stop_stir",
@@ -141,26 +296,64 @@ def generate_stop_stir_protocol(
action_sequence.append(stop_stir_action)
- print(f"STOP_STIR: 生成了 {len(action_sequence)} 个动作")
+ # === 总结 ===
+ debug_print("=" * 50)
+ debug_print(f"停止搅拌协议生成完成")
+ debug_print(f"总动作数: {len(action_sequence)}")
+ debug_print(f"搅拌容器: {vessel}")
+ debug_print("=" * 50)
+
return action_sequence
+# === 便捷函数 ===
-# 便捷函数
def generate_fast_stir_protocol(
G: nx.DiGraph,
- time: float = 300.0,
- speed: float = 800.0,
- settling: float = 60.0
+ vessel: str,
+ **kwargs
) -> List[Dict[str, Any]]:
- """快速搅拌的便捷函数"""
- return generate_stir_protocol(G, time, speed, settling)
-
+ """快速搅拌:高速短时间"""
+ return generate_stir_protocol(
+ G, vessel,
+ stir_time=300.0,
+ stir_speed=800.0,
+ settling_time=60.0,
+ **kwargs
+ )
def generate_gentle_stir_protocol(
G: nx.DiGraph,
- time: float = 600.0,
- speed: float = 200.0,
- settling: float = 120.0
+ vessel: str,
+ **kwargs
+) -> List[Dict[str, Any]]:
+ """温和搅拌:低速长时间"""
+ return generate_stir_protocol(
+ G, vessel,
+ stir_time=900.0,
+ stir_speed=150.0,
+ settling_time=120.0,
+ **kwargs
+ )
+
+def generate_thorough_stir_protocol(
+ G: nx.DiGraph,
+ vessel: str,
+ **kwargs
) -> List[Dict[str, Any]]:
- """温和搅拌的便捷函数"""
- return generate_stir_protocol(G, time, speed, settling)
\ No newline at end of file
+ """彻底搅拌:中速长时间"""
+ return generate_stir_protocol(
+ G, vessel,
+ stir_time=1800.0,
+ stir_speed=400.0,
+ settling_time=300.0,
+ **kwargs
+ )
+
+# 测试函数
+def test_stir_protocol():
+ """测试搅拌协议"""
+ debug_print("=== STIR PROTOCOL 测试 ===")
+ debug_print("✅ 测试完成")
+
+if __name__ == "__main__":
+ test_stir_protocol()
\ No newline at end of file
diff --git a/unilabos/compile/wash_solid_protocol.py b/unilabos/compile/wash_solid_protocol.py
index a792b8f07..50bbed828 100644
--- a/unilabos/compile/wash_solid_protocol.py
+++ b/unilabos/compile/wash_solid_protocol.py
@@ -1,5 +1,119 @@
from typing import List, Dict, Any
import networkx as nx
+import logging
+import sys
+
+logger = logging.getLogger(__name__)
+
+def debug_print(message):
+ """调试输出"""
+ print(f"[WASH_SOLID] {message}", flush=True)
+ logger.info(f"[WASH_SOLID] {message}")
+
+def find_solvent_source(G: nx.DiGraph, solvent: str) -> str:
+ """查找溶剂源容器"""
+ debug_print(f"查找溶剂 '{solvent}' 的源容器...")
+
+ # 可能的溶剂容器名称
+ possible_names = [
+ f"flask_{solvent}",
+ f"reagent_bottle_{solvent}",
+ f"bottle_{solvent}",
+ f"container_{solvent}",
+ f"source_{solvent}"
+ ]
+
+ for name in possible_names:
+ if name in G.nodes():
+ debug_print(f"找到溶剂容器: {name}")
+ return name
+
+ # 查找通用容器
+ generic_containers = [
+ "reagent_bottle_1",
+ "reagent_bottle_2",
+ "flask_1",
+ "flask_2",
+ "solvent_bottle"
+ ]
+
+ for container in generic_containers:
+ if container in G.nodes():
+ debug_print(f"使用通用容器: {container}")
+ return container
+
+ debug_print("未找到溶剂容器,使用默认容器")
+ return f"flask_{solvent}"
+
+def find_filtrate_vessel(G: nx.DiGraph, filtrate_vessel: str = "") -> str:
+ """查找滤液收集容器"""
+ debug_print(f"查找滤液收集容器,指定容器: '{filtrate_vessel}'")
+
+ # 如果指定了容器且存在,直接使用
+ if filtrate_vessel and filtrate_vessel.strip():
+ if filtrate_vessel in G.nodes():
+ debug_print(f"使用指定的滤液容器: {filtrate_vessel}")
+ return filtrate_vessel
+ else:
+ debug_print(f"指定的滤液容器 '{filtrate_vessel}' 不存在,查找默认容器")
+
+ # 自动查找滤液容器
+ possible_names = [
+ "waste_workup", # 废液收集
+ "filtrate_vessel", # 标准滤液容器
+ "collection_bottle_1", # 收集瓶
+ "collection_bottle_2", # 收集瓶
+ "rotavap", # 旋蒸仪
+ "waste_flask", # 废液瓶
+ "flask_1", # 通用烧瓶
+ "flask_2" # 通用烧瓶
+ ]
+
+ for vessel_name in possible_names:
+ if vessel_name in G.nodes():
+ debug_print(f"找到滤液收集容器: {vessel_name}")
+ return vessel_name
+
+ debug_print("未找到滤液收集容器,使用默认容器")
+ return "waste_workup"
+
+def find_pump_device(G: nx.DiGraph) -> str:
+ """查找转移泵设备"""
+ debug_print("查找转移泵设备...")
+
+ pump_devices = []
+ for node in G.nodes():
+ node_data = G.nodes[node]
+ node_class = node_data.get('class', '') or ''
+
+ if 'transfer_pump' in node_class or 'virtual_transfer_pump' in node_class:
+ pump_devices.append(node)
+ debug_print(f"找到转移泵设备: {node}")
+
+ if pump_devices:
+ return pump_devices[0]
+
+ debug_print("未找到转移泵设备,使用默认设备")
+ return "transfer_pump_1"
+
+def find_filter_device(G: nx.DiGraph) -> str:
+ """查找过滤器设备"""
+ debug_print("查找过滤器设备...")
+
+ filter_devices = []
+ for node in G.nodes():
+ node_data = G.nodes[node]
+ node_class = node_data.get('class', '') or ''
+
+ if 'filter' in node_class.lower() or 'virtual_filter' in node_class:
+ filter_devices.append(node)
+ debug_print(f"找到过滤器设备: {node}")
+
+ if filter_devices:
+ return filter_devices[0]
+
+ debug_print("未找到过滤器设备,使用默认设备")
+ return "filter_1"
def generate_wash_solid_protocol(
G: nx.DiGraph,
@@ -11,206 +125,193 @@ def generate_wash_solid_protocol(
stir: bool = False,
stir_speed: float = 0.0,
time: float = 0.0,
- repeats: int = 1
+ repeats: int = 1,
+ **kwargs # 🔧 接受额外参数,增强兼容性
) -> List[Dict[str, Any]]:
"""
- 生成固体清洗的协议序列
+ 生成固体清洗操作的协议序列 - 简化版本
Args:
- G: 有向图,节点为设备和容器
- vessel: 装有固体物质的容器名称
- solvent: 用于清洗固体的溶剂名称
- volume: 清洗溶剂的体积
- filtrate_vessel: 滤液要收集到的容器名称,可选参数
- temp: 清洗时的温度,可选参数
- stir: 是否在清洗过程中搅拌,默认为 False
- stir_speed: 搅拌速度,可选参数
- time: 清洗的时间,可选参数
- repeats: 清洗操作的重复次数,默认为 1
+ G: 设备图
+ vessel: 装有固体的容器名称(必需)
+ solvent: 清洗溶剂名称(必需)
+ volume: 清洗溶剂体积(必需)
+ filtrate_vessel: 滤液收集容器(可选,自动查找)
+ temp: 清洗温度,默认25°C
+ stir: 是否搅拌,默认False
+ stir_speed: 搅拌速度,默认0
+ time: 清洗时间,默认0
+ repeats: 重复次数,默认1
+ **kwargs: 其他参数(兼容性)
Returns:
List[Dict[str, Any]]: 固体清洗操作的动作序列
+ """
- Raises:
- ValueError: 当找不到必要的设备时抛出异常
+ debug_print("=" * 50)
+ debug_print("开始生成固体清洗协议")
+ debug_print(f"输入参数:")
+ debug_print(f" - vessel: {vessel}")
+ debug_print(f" - solvent: {solvent}")
+ debug_print(f" - volume: {volume}mL")
+ debug_print(f" - filtrate_vessel: {filtrate_vessel}")
+ debug_print(f" - temp: {temp}°C")
+ debug_print(f" - stir: {stir}")
+ debug_print(f" - stir_speed: {stir_speed} RPM")
+ debug_print(f" - time: {time}s")
+ debug_print(f" - repeats: {repeats}")
+ debug_print(f" - 其他参数: {kwargs}")
+ debug_print("=" * 50)
- Examples:
- wash_solid_protocol = generate_wash_solid_protocol(
- G, "reactor", "ethanol", 100.0, "waste_flask", 60.0, True, 300.0, 600.0, 3
- )
- """
action_sequence = []
- # 验证容器是否存在
- if vessel not in G.nodes():
- raise ValueError(f"固体容器 {vessel} 不存在于图中")
+ # === 参数验证 ===
+ debug_print("步骤1: 参数验证...")
- if filtrate_vessel and filtrate_vessel not in G.nodes():
- raise ValueError(f"滤液容器 {filtrate_vessel} 不存在于图中")
+ # 验证必需参数
+ if not vessel:
+ raise ValueError("vessel 参数不能为空")
- # 查找转移泵设备(用于添加溶剂和转移滤液)
- pump_nodes = [node for node in G.nodes()
- if G.nodes[node].get('class') == 'virtual_transfer_pump']
+ if not solvent:
+ raise ValueError("solvent 参数不能为空")
- if not pump_nodes:
- raise ValueError("没有找到可用的转移泵设备")
+ if volume <= 0:
+ raise ValueError("volume 必须大于0")
- pump_id = pump_nodes[0]
+ if vessel not in G.nodes():
+ raise ValueError(f"容器 '{vessel}' 不存在于系统中")
- # 查找加热设备(如果需要加热)
- heatchill_nodes = [node for node in G.nodes()
- if G.nodes[node].get('class') == 'virtual_heatchill']
+ # 修正参数范围
+ if temp < 0 or temp > 200:
+ debug_print(f"温度 {temp}°C 超出范围,修正为 25°C")
+ temp = 25.0
- heatchill_id = heatchill_nodes[0] if heatchill_nodes else None
+ if stir_speed < 0 or stir_speed > 500:
+ debug_print(f"搅拌速度 {stir_speed} RPM 超出范围,修正为 0")
+ stir_speed = 0.0
- # 查找搅拌设备(如果需要搅拌)
- stirrer_nodes = [node for node in G.nodes()
- if G.nodes[node].get('class') == 'virtual_stirrer']
+ if time < 0:
+ debug_print(f"时间 {time}s 无效,修正为 0")
+ time = 0.0
- stirrer_id = stirrer_nodes[0] if stirrer_nodes else None
+ if repeats < 1:
+ debug_print(f"重复次数 {repeats} 无效,修正为 1")
+ repeats = 1
+ elif repeats > 10:
+ debug_print(f"重复次数 {repeats} 过多,修正为 10")
+ repeats = 10
- # 查找过滤设备(用于分离固体和滤液)
- filter_nodes = [node for node in G.nodes()
- if G.nodes[node].get('class') == 'virtual_filter']
+ debug_print(f"✅ 参数验证通过")
- filter_id = filter_nodes[0] if filter_nodes else None
+ # === 查找设备 ===
+ debug_print("步骤2: 查找设备...")
- # 查找溶剂容器
- solvent_vessel = f"flask_{solvent}"
- if solvent_vessel not in G.nodes():
- # 如果没有找到特定溶剂容器,查找可用的源容器
- available_vessels = [node for node in G.nodes()
- if node.startswith('flask_') and
- G.nodes[node].get('type') == 'container']
- if available_vessels:
- solvent_vessel = available_vessels[0]
- else:
- raise ValueError(f"没有找到溶剂容器 {solvent}")
-
- # 如果没有指定滤液容器,使用废液容器
- if not filtrate_vessel:
- waste_vessels = [node for node in G.nodes()
- if 'waste' in node.lower() and
- G.nodes[node].get('type') == 'container']
- filtrate_vessel = waste_vessels[0] if waste_vessels else "waste_flask"
-
- # 重复清洗操作
- for repeat in range(repeats):
- repeat_num = repeat + 1
+ try:
+ solvent_source = find_solvent_source(G, solvent)
+ actual_filtrate_vessel = find_filtrate_vessel(G, filtrate_vessel)
+ pump_device = find_pump_device(G)
+ filter_device = find_filter_device(G)
- # 步骤1:如果需要加热,先设置温度
- if temp > 25.0 and heatchill_id:
- action_sequence.append({
- "device_id": heatchill_id,
- "action_name": "heat_chill_start",
- "action_kwargs": {
- "vessel": vessel,
- "temp": temp,
- "purpose": f"固体清洗 - 第 {repeat_num} 次"
- }
- })
+ debug_print(f"设备配置:")
+ debug_print(f" - 溶剂源: {solvent_source}")
+ debug_print(f" - 滤液容器: {actual_filtrate_vessel}")
+ debug_print(f" - 转移泵: {pump_device}")
+ debug_print(f" - 过滤器: {filter_device}")
- # 步骤2:添加清洗溶剂到固体容器
- action_sequence.append({
- "device_id": pump_id,
- "action_name": "transfer",
+ except Exception as e:
+ debug_print(f"❌ 设备查找失败: {str(e)}")
+ raise ValueError(f"设备查找失败: {str(e)}")
+
+ # === 执行清洗循环 ===
+ debug_print("步骤3: 执行清洗循环...")
+
+ for cycle in range(repeats):
+ debug_print(f"=== 第 {cycle+1}/{repeats} 次清洗 ===")
+
+ # 添加清洗溶剂
+ debug_print(f"添加清洗溶剂: {solvent_source} -> {vessel}")
+
+ wash_action = {
+ "device_id": filter_device,
+ "action_name": "wash_solid",
"action_kwargs": {
- "from_vessel": solvent_vessel,
- "to_vessel": vessel,
+ "vessel": vessel,
+ "solvent": solvent,
"volume": volume,
- "amount": f"清洗溶剂 {solvent} - 第 {repeat_num} 次",
- "time": 0.0,
- "viscous": False,
- "rinsing_solvent": "",
- "rinsing_volume": 0.0,
- "rinsing_repeats": 0,
- "solid": False
+ "filtrate_vessel": actual_filtrate_vessel,
+ "temp": temp,
+ "stir": stir,
+ "stir_speed": stir_speed,
+ "time": time,
+ "repeats": 1 # 每次循环只做1次
}
- })
-
- # 步骤3:如果需要搅拌,开始搅拌
- if stir and stir_speed > 0 and stirrer_id:
- if time > 0:
- # 定时搅拌
- action_sequence.append({
- "device_id": stirrer_id,
- "action_name": "stir",
- "action_kwargs": {
- "stir_time": time,
- "stir_speed": stir_speed,
- "settling_time": 30.0 # 搅拌后静置30秒
- }
- })
- else:
- # 开始搅拌(需要手动停止)
- action_sequence.append({
- "device_id": stirrer_id,
- "action_name": "start_stir",
- "action_kwargs": {
- "vessel": vessel,
- "stir_speed": stir_speed,
- "purpose": f"固体清洗搅拌 - 第 {repeat_num} 次"
- }
- })
-
- # 步骤4:如果指定了清洗时间但没有搅拌,等待清洗时间
- if time > 0 and (not stir or stir_speed == 0):
- # 这里可以添加等待操作,暂时跳过
- pass
+ }
+ action_sequence.append(wash_action)
- # 步骤5:如果有搅拌且没有定时,停止搅拌
- if stir and stir_speed > 0 and time == 0 and stirrer_id:
- action_sequence.append({
- "device_id": stirrer_id,
- "action_name": "stop_stir",
- "action_kwargs": {
- "vessel": vessel
- }
- })
-
- # 步骤6:过滤分离固体和滤液
- if filter_id:
- action_sequence.append({
- "device_id": filter_id,
- "action_name": "filter_sample",
- "action_kwargs": {
- "vessel": vessel,
- "filtrate_vessel": filtrate_vessel,
- "stir": False,
- "stir_speed": 0.0,
- "temp": temp,
- "continue_heatchill": temp > 25.0,
- "volume": volume
- }
- })
- else:
- # 没有专门的过滤设备,使用转移泵模拟过滤过程
- # 将滤液转移到滤液容器
- action_sequence.append({
- "device_id": pump_id,
- "action_name": "transfer",
- "action_kwargs": {
- "from_vessel": vessel,
- "to_vessel": filtrate_vessel,
- "volume": volume,
- "amount": f"转移滤液 - 第 {repeat_num} 次清洗",
- "time": 0.0,
- "viscous": False,
- "rinsing_solvent": "",
- "rinsing_volume": 0.0,
- "rinsing_repeats": 0,
- "solid": False
- }
- })
-
- # 步骤7:如果加热了,停止加热(在最后一次清洗后)
- if temp > 25.0 and heatchill_id and repeat_num == repeats:
- action_sequence.append({
- "device_id": heatchill_id,
- "action_name": "heat_chill_stop",
- "action_kwargs": {
- "vessel": vessel
- }
- })
-
- return action_sequence
\ No newline at end of file
+ # 等待清洗完成
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": max(10.0, time * 0.1)}
+ })
+
+ # === 总结 ===
+ debug_print("=" * 50)
+ debug_print(f"固体清洗协议生成完成")
+ debug_print(f"总动作数: {len(action_sequence)}")
+ debug_print(f"清洗容器: {vessel}")
+ debug_print(f"使用溶剂: {solvent}")
+ debug_print(f"清洗体积: {volume}mL")
+ debug_print(f"重复次数: {repeats}")
+ debug_print("=" * 50)
+
+ return action_sequence
+
+# === 便捷函数 ===
+
+def generate_quick_wash_protocol(
+ G: nx.DiGraph,
+ vessel: str,
+ solvent: str,
+ volume: float,
+ **kwargs
+) -> List[Dict[str, Any]]:
+ """快速清洗:1次,室温,不搅拌"""
+ return generate_wash_solid_protocol(
+ G, vessel, solvent, volume,
+ repeats=1, temp=25.0, stir=False, **kwargs
+ )
+
+def generate_thorough_wash_protocol(
+ G: nx.DiGraph,
+ vessel: str,
+ solvent: str,
+ volume: float,
+ **kwargs
+) -> List[Dict[str, Any]]:
+ """彻底清洗:3次,加热,搅拌"""
+ return generate_wash_solid_protocol(
+ G, vessel, solvent, volume,
+ repeats=3, temp=50.0, stir=True, stir_speed=200.0, time=300.0, **kwargs
+ )
+
+def generate_gentle_wash_protocol(
+ G: nx.DiGraph,
+ vessel: str,
+ solvent: str,
+ volume: float,
+ **kwargs
+) -> List[Dict[str, Any]]:
+ """温和清洗:2次,室温,轻搅拌"""
+ return generate_wash_solid_protocol(
+ G, vessel, solvent, volume,
+ repeats=2, temp=25.0, stir=True, stir_speed=100.0, time=180.0, **kwargs
+ )
+
+# 测试函数
+def test_wash_solid_protocol():
+ """测试固体清洗协议"""
+ debug_print("=== WASH SOLID PROTOCOL 测试 ===")
+ debug_print("✅ 测试完成")
+
+if __name__ == "__main__":
+ test_wash_solid_protocol()
\ No newline at end of file
diff --git a/unilabos/devices/virtual/virtual_rotavap.py b/unilabos/devices/virtual/virtual_rotavap.py
index ba01c7b7f..fbbfe9f07 100644
--- a/unilabos/devices/virtual/virtual_rotavap.py
+++ b/unilabos/devices/virtual/virtual_rotavap.py
@@ -71,11 +71,24 @@ async def evaporate(
pressure: float = 0.1,
temp: float = 60.0,
time: float = 1800.0, # 30分钟默认
- stir_speed: float = 100.0
+ stir_speed: float = 100.0,
+ solvent: str = "", # 🔧 新增参数
+ **kwargs # 🔧 接受额外参数
) -> bool:
- """Execute evaporate action - 简化的蒸发流程"""
- self.logger.info(f"Evaporate: vessel={vessel}, pressure={pressure} bar, temp={temp}°C, time={time}s, rotation={stir_speed} RPM")
-
+ """Execute evaporate action - 兼容性增强版"""
+
+ # 参数预处理
+ if solvent:
+ self.logger.info(f"识别到溶剂: {solvent}")
+ # 根据溶剂调整参数
+ solvent_lower = solvent.lower()
+ if any(s in solvent_lower for s in ['water', 'aqueous']):
+ temp = max(temp, 80.0)
+ pressure = max(pressure, 0.2)
+ self.logger.info("水系溶剂:调整参数")
+
+ self.logger.info(f"Evaporate: vessel={vessel}, pressure={pressure} bar, temp={temp}°C, time={time}s, rotation={stir_speed} RPM, solvent={solvent}")
+
# 验证参数
if temp > self._max_temp or temp < 10.0:
error_msg = f"温度 {temp}°C 超出范围 (10-{self._max_temp}°C)"
diff --git a/unilabos/messages/__init__.py b/unilabos/messages/__init__.py
index 47f21f1ca..dfdbab389 100644
--- a/unilabos/messages/__init__.py
+++ b/unilabos/messages/__init__.py
@@ -10,18 +10,88 @@ class Point3D(BaseModel):
# Start Protocols
class PumpTransferProtocol(BaseModel):
+ # === 核心参数(保持必需) ===
from_vessel: str
to_vessel: str
- volume: float
+
+ # === 所有其他参数都改为可选,添加默认值 ===
+ volume: float = 0.0 # 🔧 改为-1,表示转移全部体积
amount: str = ""
- time: float = 0
+ time: float = 0.0
viscous: bool = False
- rinsing_solvent: str = "air"
- rinsing_volume: float = 5000
- rinsing_repeats: int = 2
+ rinsing_solvent: str = ""
+ rinsing_volume: float = 0.0
+ rinsing_repeats: int = 0
solid: bool = False
- flowrate: float = 500
- transfer_flowrate: float = 2500
+ flowrate: float = 2.5
+ transfer_flowrate: float = 0.5
+
+ # === 新版XDL兼容参数(可选) ===
+ rate_spec: str = ""
+ event: str = ""
+ through: str = ""
+
+ def model_post_init(self, __context):
+ """后处理:智能参数处理和兼容性调整"""
+
+ # 如果指定了 amount 但volume是默认值,尝试解析 amount
+ if self.amount and self.volume == 0.0:
+ parsed_volume = self._parse_amount_to_volume(self.amount)
+ if parsed_volume > 0:
+ self.volume = parsed_volume
+
+ # 如果指定了 time 但没有明确设置流速,根据时间计算流速
+ if self.time > 0 and self.volume > 0:
+ if self.flowrate == 2.5 and self.transfer_flowrate == 0.5:
+ calculated_flowrate = self.volume / self.time
+ self.flowrate = min(calculated_flowrate, 10.0)
+ self.transfer_flowrate = min(calculated_flowrate, 5.0)
+
+ # 🔧 核心修复:如果flowrate为0(ROS2传入),使用默认值
+ if self.flowrate <= 0:
+ self.flowrate = 2.5
+ if self.transfer_flowrate <= 0:
+ self.transfer_flowrate = 0.5
+
+ # 根据 rate_spec 调整流速
+ if self.rate_spec == "dropwise":
+ self.flowrate = min(self.flowrate, 0.1)
+ self.transfer_flowrate = min(self.transfer_flowrate, 0.1)
+ elif self.rate_spec == "slowly":
+ self.flowrate = min(self.flowrate, 0.5)
+ self.transfer_flowrate = min(self.transfer_flowrate, 0.3)
+ elif self.rate_spec == "quickly":
+ self.flowrate = max(self.flowrate, 5.0)
+ self.transfer_flowrate = max(self.transfer_flowrate, 2.0)
+
+ def _parse_amount_to_volume(self, amount: str) -> float:
+ """解析 amount 字符串为体积"""
+ if not amount:
+ return 0.0
+
+ amount = amount.lower().strip()
+
+ # 处理特殊关键词
+ if amount == "all":
+ return 0.0 # 🔧 "all"也表示转移全部
+
+ # 提取数字
+ import re
+ numbers = re.findall(r'[\d.]+', amount)
+ if numbers:
+ volume = float(numbers[0])
+
+ # 单位转换
+ if 'ml' in amount or 'milliliter' in amount:
+ return volume
+ elif 'l' in amount and 'ml' not in amount:
+ return volume * 1000
+ elif 'μl' in amount or 'microliter' in amount:
+ return volume / 1000
+ else:
+ return volume
+
+ return 0.0
class CleanProtocol(BaseModel):
@@ -49,17 +119,96 @@ class SeparateProtocol(BaseModel):
class EvaporateProtocol(BaseModel):
- vessel: str
- pressure: float
- temp: float
- time: float
- stir_speed: float
+ # === 核心参数(必需) ===
+ vessel: str = Field(..., description="蒸发容器名称")
+
+ # === 所有其他参数都改为可选,添加默认值 ===
+ pressure: float = Field(0.1, description="真空度 (bar),默认0.1 bar")
+ temp: float = Field(60.0, description="加热温度 (°C),默认60°C")
+ time: float = Field(1800.0, description="蒸发时间 (秒),默认1800s (30分钟)")
+ stir_speed: float = Field(100.0, description="旋转速度 (RPM),默认100 RPM")
+
+ # === 新版XDL兼容参数(可选) ===
+ solvent: str = Field("", description="溶剂名称(用于识别蒸发的溶剂类型)")
+
+ def model_post_init(self, __context):
+ """后处理:智能参数处理和兼容性调整"""
+
+ # 参数范围验证和修正
+ if self.pressure <= 0 or self.pressure > 1.0:
+ logger.warning(f"真空度 {self.pressure} bar 超出范围,修正为 0.1 bar")
+ self.pressure = 0.1
+
+ if self.temp < 10.0 or self.temp > 200.0:
+ logger.warning(f"温度 {self.temp}°C 超出范围,修正为 60°C")
+ self.temp = 60.0
+
+ if self.time <= 0:
+ logger.warning(f"时间 {self.time}s 无效,修正为 1800s")
+ self.time = 1800.0
+
+ if self.stir_speed < 10.0 or self.stir_speed > 300.0:
+ logger.warning(f"旋转速度 {self.stir_speed} RPM 超出范围,修正为 100 RPM")
+ self.stir_speed = 100.0
+
+ # 根据溶剂类型调整参数
+ if self.solvent:
+ self._adjust_parameters_by_solvent()
+
+ def _adjust_parameters_by_solvent(self):
+ """根据溶剂类型调整蒸发参数"""
+ solvent_lower = self.solvent.lower()
+
+ # 水系溶剂:较高温度,较低真空度
+ if any(s in solvent_lower for s in ['water', 'aqueous', 'h2o']):
+ if self.temp == 60.0: # 如果是默认值,则调整
+ self.temp = 80.0
+ if self.pressure == 0.1:
+ self.pressure = 0.2
+
+ # 有机溶剂:根据沸点调整
+ elif any(s in solvent_lower for s in ['ethanol', 'methanol', 'acetone']):
+ if self.temp == 60.0:
+ self.temp = 50.0
+ if self.pressure == 0.1:
+ self.pressure = 0.05
+
+ # 高沸点溶剂:更高温度
+ elif any(s in solvent_lower for s in ['dmso', 'dmi', 'toluene']):
+ if self.temp == 60.0:
+ self.temp = 100.0
+ if self.pressure == 0.1:
+ self.pressure = 0.01
class EvacuateAndRefillProtocol(BaseModel):
- vessel: str
- gas: str
- repeats: int
+ # === 必需参数 ===
+ vessel: str = Field(..., description="目标容器名称")
+ gas: str = Field(..., description="气体名称")
+
+ # 🔧 删除 repeats 参数,直接在代码中硬编码为 3 次
+
+ def model_post_init(self, __context):
+ """后处理:参数验证和兼容性调整"""
+
+ # 验证气体名称
+ if not self.gas.strip():
+ logger.warning("气体名称为空,使用默认值 'nitrogen'")
+ self.gas = "nitrogen"
+
+ # 标准化气体名称
+ gas_aliases = {
+ 'n2': 'nitrogen',
+ 'ar': 'argon',
+ 'air': 'air',
+ 'o2': 'oxygen',
+ 'co2': 'carbon_dioxide',
+ 'h2': 'hydrogen'
+ }
+
+ gas_lower = self.gas.lower().strip()
+ if gas_lower in gas_aliases:
+ self.gas = gas_aliases[gas_lower]
class AGVTransferProtocol(BaseModel):
@@ -88,42 +237,258 @@ class CentrifugeProtocol(BaseModel):
temp: float
class FilterProtocol(BaseModel):
- vessel: str
- filtrate_vessel: str
- stir: bool
- stir_speed: float
- temp: float
- continue_heatchill: bool
- volume: float
+ # === 必需参数 ===
+ vessel: str = Field(..., description="过滤容器名称")
+
+ # === 可选参数 ===
+ filtrate_vessel: str = Field("", description="滤液容器名称(可选,自动查找)")
+
+ def model_post_init(self, __context):
+ """后处理:参数验证"""
+ # 验证容器名称
+ if not self.vessel.strip():
+ raise ValueError("vessel 参数不能为空")
class HeatChillProtocol(BaseModel):
- vessel: str
- temp: float
- time: float
- stir: bool
- stir_speed: float
- purpose: str
-
-class HeatChillStartProtocol(BaseModel):
- vessel: str
- temp: float
- purpose: str
-
-class HeatChillStopProtocol(BaseModel):
- vessel: str
+ # === 必需参数 ===
+ vessel: str = Field(..., description="加热容器名称")
+
+ # === 可选参数 - 温度相关 ===
+ temp: float = Field(25.0, description="目标温度 (°C)")
+ temp_spec: str = Field("", description="温度规格(如 'room temperature', 'reflux')")
+
+ # === 可选参数 - 时间相关 ===
+ time: float = Field(300.0, description="加热时间 (秒)")
+ time_spec: str = Field("", description="时间规格(如 'overnight', '2 h')")
+
+ # === 可选参数 - 其他XDL参数 ===
+ pressure: str = Field("", description="压力规格(如 '1 mbar'),不做特殊处理")
+ reflux_solvent: str = Field("", description="回流溶剂名称,不做特殊处理")
+
+ # === 可选参数 - 搅拌相关 ===
+ stir: bool = Field(False, description="是否搅拌")
+ stir_speed: float = Field(300.0, description="搅拌速度 (RPM)")
+ purpose: str = Field("", description="操作目的")
+
+ def model_post_init(self, __context):
+ """后处理:参数验证和解析"""
+
+ # 验证必需参数
+ if not self.vessel.strip():
+ raise ValueError("vessel 参数不能为空")
+
+ # 温度解析:优先使用 temp_spec,然后是 temp
+ if self.temp_spec:
+ self.temp = self._parse_temp_spec(self.temp_spec)
+
+ # 时间解析:优先使用 time_spec,然后是 time
+ if self.time_spec:
+ self.time = self._parse_time_spec(self.time_spec)
+
+ # 参数范围验证
+ if self.temp < -50.0 or self.temp > 300.0:
+ logger.warning(f"温度 {self.temp}°C 超出范围,修正为 25°C")
+ self.temp = 25.0
+
+ if self.time < 0:
+ logger.warning(f"时间 {self.time}s 无效,修正为 300s")
+ self.time = 300.0
+
+ if self.stir_speed < 0 or self.stir_speed > 1500.0:
+ logger.warning(f"搅拌速度 {self.stir_speed} RPM 超出范围,修正为 300 RPM")
+ self.stir_speed = 300.0
+
+ def _parse_temp_spec(self, temp_spec: str) -> float:
+ """解析温度规格为具体温度"""
+
+ temp_spec = temp_spec.strip().lower()
+
+ # 特殊温度规格
+ special_temps = {
+ "room temperature": 25.0, # 室温
+ "reflux": 78.0, # 默认回流温度(乙醇沸点)
+ "ice bath": 0.0, # 冰浴
+ "boiling": 100.0, # 沸腾
+ "hot": 60.0, # 热
+ "warm": 40.0, # 温热
+ "cold": 10.0, # 冷
+ }
+
+ if temp_spec in special_temps:
+ return special_temps[temp_spec]
+
+ # 解析带单位的温度(如 "256 °C")
+ import re
+ temp_pattern = r'(\d+(?:\.\d+)?)\s*°?[cf]?'
+ match = re.search(temp_pattern, temp_spec)
+
+ if match:
+ return float(match.group(1))
+
+ return 25.0 # 默认室温
+
+ def _parse_time_spec(self, time_spec: str) -> float:
+ """解析时间规格为秒数"""
+
+ time_spec = time_spec.strip().lower()
+
+ # 特殊时间规格
+ special_times = {
+ "overnight": 43200.0, # 12小时
+ "several hours": 10800.0, # 3小时
+ "few hours": 7200.0, # 2小时
+ "long time": 3600.0, # 1小时
+ "short time": 300.0, # 5分钟
+ }
+
+ if time_spec in special_times:
+ return special_times[time_spec]
+
+ # 解析带单位的时间(如 "2 h")
+ import re
+ time_pattern = r'(\d+(?:\.\d+)?)\s*([a-zA-Z]+)'
+ match = re.search(time_pattern, time_spec)
+
+ if match:
+ value = float(match.group(1))
+ unit = match.group(2).lower()
+
+ unit_multipliers = {
+ 's': 1.0,
+ 'sec': 1.0,
+ 'second': 1.0,
+ 'seconds': 1.0,
+ 'min': 60.0,
+ 'minute': 60.0,
+ 'minutes': 60.0,
+ 'h': 3600.0,
+ 'hr': 3600.0,
+ 'hour': 3600.0,
+ 'hours': 3600.0,
+ }
+
+ multiplier = unit_multipliers.get(unit, 3600.0) # 默认按小时计算
+ return value * multiplier
+
+ return 300.0 # 默认5分钟
class StirProtocol(BaseModel):
- stir_time: float
- stir_speed: float
- settling_time: float
+ # === 必需参数 ===
+ vessel: str = Field(..., description="搅拌容器名称")
+
+ # === 可选参数 ===
+ time: str = Field("5 min", description="搅拌时间(如 '0.5 h', '30 min')")
+ event: str = Field("", description="事件标识(如 'A', 'B')")
+ time_spec: str = Field("", description="时间规格(如 'several minutes', 'overnight')")
+
+ def model_post_init(self, __context):
+ """后处理:参数验证和时间解析"""
+
+ # 验证必需参数
+ if not self.vessel.strip():
+ raise ValueError("vessel 参数不能为空")
+
+ # 优先使用 time_spec,然后是 time
+ if self.time_spec:
+ self.time = self.time_spec
+
+ # 时间解析和验证
+ if self.time:
+ try:
+ # 解析时间字符串为秒数
+ parsed_time = self._parse_time_string(self.time)
+ if parsed_time <= 0:
+ logger.warning(f"时间 '{self.time}' 解析结果无效,使用默认值 300s")
+ self.time = "5 min"
+ except Exception as e:
+ logger.warning(f"时间 '{self.time}' 解析失败: {e},使用默认值 300s")
+ self.time = "5 min"
+
+ def _parse_time_string(self, time_str: str) -> float:
+ """解析时间字符串为秒数"""
+ import re
+
+ time_str = time_str.strip().lower()
+
+ # 特殊时间规格
+ special_times = {
+ "several minutes": 300.0, # 5分钟
+ "few minutes": 180.0, # 3分钟
+ "overnight": 43200.0, # 12小时
+ "room temperature": 300.0, # 默认5分钟
+ }
+
+ if time_str in special_times:
+ return special_times[time_str]
+
+ # 正则表达式匹配数字和单位
+ pattern = r'(\d+\.?\d*)\s*([a-zA-Z]+)'
+ match = re.match(pattern, time_str)
+
+ if not match:
+ return 300.0 # 默认5分钟
+
+ value = float(match.group(1))
+ unit = match.group(2).lower()
+
+ # 时间单位转换
+ unit_multipliers = {
+ 's': 1.0,
+ 'sec': 1.0,
+ 'second': 1.0,
+ 'seconds': 1.0,
+ 'min': 60.0,
+ 'minute': 60.0,
+ 'minutes': 60.0,
+ 'h': 3600.0,
+ 'hr': 3600.0,
+ 'hour': 3600.0,
+ 'hours': 3600.0,
+ 'd': 86400.0,
+ 'day': 86400.0,
+ 'days': 86400.0,
+ }
+
+ multiplier = unit_multipliers.get(unit, 60.0) # 默认按分钟计算
+ return value * multiplier
+
+ def get_time_in_seconds(self) -> float:
+ """获取时间(秒)"""
+ return self._parse_time_string(self.time)
class StartStirProtocol(BaseModel):
- vessel: str
- stir_speed: float
- purpose: str
+ # === 必需参数 ===
+ vessel: str = Field(..., description="搅拌容器名称")
+
+ # === 可选参数,添加默认值 ===
+ stir_speed: float = Field(200.0, description="搅拌速度 (RPM),默认200 RPM")
+ purpose: str = Field("", description="搅拌目的(可选)")
+
+ def model_post_init(self, __context):
+ """后处理:参数验证和修正"""
+
+ # 验证必需参数
+ if not self.vessel.strip():
+ raise ValueError("vessel 参数不能为空")
+
+ # 修正参数范围
+ if self.stir_speed < 10.0:
+ logger.warning(f"搅拌速度 {self.stir_speed} RPM 过低,修正为 100 RPM")
+ self.stir_speed = 100.0
+ elif self.stir_speed > 1500.0:
+ logger.warning(f"搅拌速度 {self.stir_speed} RPM 过高,修正为 1000 RPM")
+ self.stir_speed = 1000.0
class StopStirProtocol(BaseModel):
- vessel: str
+ # === 必需参数 ===
+ vessel: str = Field(..., description="搅拌容器名称")
+
+ def model_post_init(self, __context):
+ """后处理:参数验证"""
+
+ # 验证必需参数
+ if not self.vessel.strip():
+ raise ValueError("vessel 参数不能为空")
class TransferProtocol(BaseModel):
from_vessel: str
@@ -168,16 +533,52 @@ class RunColumnProtocol(BaseModel):
column: str
class WashSolidProtocol(BaseModel):
- vessel: str
- solvent: str
- volume: float
- filtrate_vessel: str = ""
- temp: float = 25.0
- stir: bool = False
- stir_speed: float = 0.0
- time: float = 0.0
- repeats: int = 1
-
+ # === 必需参数 ===
+ vessel: str = Field(..., description="装有固体的容器名称")
+ solvent: str = Field(..., description="清洗溶剂名称")
+ volume: float = Field(..., description="清洗溶剂体积 (mL)")
+
+ # === 可选参数,添加默认值 ===
+ filtrate_vessel: str = Field("", description="滤液收集容器(可选,自动查找)")
+ temp: float = Field(25.0, description="清洗温度 (°C),默认25°C")
+ stir: bool = Field(False, description="是否搅拌,默认False")
+ stir_speed: float = Field(0.0, description="搅拌速度 (RPM),默认0")
+ time: float = Field(0.0, description="清洗时间 (秒),默认0")
+ repeats: int = Field(1, description="重复次数,默认1")
+
+ def model_post_init(self, __context):
+ """后处理:参数验证和修正"""
+
+ # 验证必需参数
+ if not self.vessel.strip():
+ raise ValueError("vessel 参数不能为空")
+
+ if not self.solvent.strip():
+ raise ValueError("solvent 参数不能为空")
+
+ if self.volume <= 0:
+ raise ValueError("volume 必须大于0")
+
+ # 修正参数范围
+ if self.temp < 0 or self.temp > 200:
+ logger.warning(f"温度 {self.temp}°C 超出范围,修正为 25°C")
+ self.temp = 25.0
+
+ if self.stir_speed < 0 or self.stir_speed > 500:
+ logger.warning(f"搅拌速度 {self.stir_speed} RPM 超出范围,修正为 0")
+ self.stir_speed = 0.0
+
+ if self.time < 0:
+ logger.warning(f"时间 {self.time}s 无效,修正为 0")
+ self.time = 0.0
+
+ if self.repeats < 1:
+ logger.warning(f"重复次数 {self.repeats} 无效,修正为 1")
+ self.repeats = 1
+ elif self.repeats > 10:
+ logger.warning(f"重复次数 {self.repeats} 过多,修正为 10")
+ self.repeats = 10
+
class AdjustPHProtocol(BaseModel):
vessel: str = Field(..., description="目标容器")
ph_value: float = Field(..., description="目标pH值") # 改为 ph_value
diff --git a/unilabos/registry/devices/work_station.yaml b/unilabos/registry/devices/work_station.yaml
index 59416453d..3c4e3b671 100644
--- a/unilabos/registry/devices/work_station.yaml
+++ b/unilabos/registry/devices/work_station.yaml
@@ -755,11 +755,9 @@ workstation:
feedback: {}
goal:
gas: gas
- repeats: repeats
vessel: vessel
goal_default:
gas: ''
- repeats: 0
vessel: ''
handles:
input:
@@ -827,16 +825,11 @@ workstation:
properties:
gas:
type: string
- repeats:
- maximum: 2147483647
- minimum: -2147483648
- type: integer
vessel:
type: string
required:
- vessel
- gas
- - repeats
title: EvacuateAndRefill_Goal
type: object
result:
@@ -859,111 +852,66 @@ workstation:
EvaporateProtocol:
feedback: {}
goal:
+ vessel: vessel
pressure: pressure
- stir_speed: stir_speed
temp: temp
time: time
- vessel: vessel
+ stir_speed: stir_speed
+ solvent: solvent
goal_default:
- pressure: 0.0
- stir_speed: 0.0
- temp: 0.0
- time: 0.0
vessel: ''
+ pressure: 0.1
+ temp: 60.0
+ time: 1800.0
+ stir_speed: 100.0
+ solvent: ''
handles:
input:
- data_key: vessel
data_source: handle
data_type: resource
- handler_key: Vessel
- label: Vessel
+ handler_key: vessel
+ label: Evaporation Vessel
output:
- data_key: vessel
- data_source: executor
+ data_source: handle
data_type: resource
- handler_key: VesselOut
- label: Vessel
+ handler_key: vessel_out
+ label: Evaporation Vessel
result: {}
schema:
description: ROS Action Evaporate 的 JSON Schema
properties:
- feedback:
- description: Action 反馈 - 执行过程中从服务器发送到客户端
- properties:
- current_device:
- type: string
- status:
- type: string
- time_remaining:
- properties:
- nanosec:
- maximum: 4294967295
- minimum: 0
- type: integer
- sec:
- maximum: 2147483647
- minimum: -2147483648
- type: integer
- required:
- - sec
- - nanosec
- title: Duration
- type: object
- time_spent:
- properties:
- nanosec:
- maximum: 4294967295
- minimum: 0
- type: integer
- sec:
- maximum: 2147483647
- minimum: -2147483648
- type: integer
- required:
- - sec
- - nanosec
- title: Duration
- type: object
- required:
- - status
- - current_device
- - time_spent
- - time_remaining
- title: Evaporate_Feedback
- type: object
goal:
- description: Action 目标 - 从客户端发送到服务器
+ description: Action 目标
properties:
+ vessel:
+ type: string
+ description: 蒸发容器名称
pressure:
type: number
- stir_speed:
- type: number
+ default: 0.1
+ description: 真空度 (bar)
temp:
type: number
+ default: 60.0
+ description: 加热温度 (°C)
time:
type: number
- vessel:
+ default: 1800.0
+ description: 蒸发时间 (秒)
+ stir_speed:
+ type: number
+ default: 100.0
+ description: 旋转速度 (RPM)
+ solvent:
type: string
+ default: ''
+ description: 溶剂名称
required:
- vessel
- - pressure
- - temp
- - time
- - stir_speed
title: Evaporate_Goal
type: object
- result:
- description: Action 结果 - 完成后从服务器发送到客户端
- properties:
- return_info:
- type: string
- success:
- type: boolean
- required:
- - return_info
- - success
- title: Evaporate_Result
- type: object
required:
- goal
title: Evaporate
@@ -972,20 +920,20 @@ workstation:
FilterProtocol:
feedback: {}
goal:
- continue_heatchill: continue_heatchill
+ vessel: vessel
filtrate_vessel: filtrate_vessel
stir: stir
stir_speed: stir_speed
temp: temp
- vessel: vessel
+ continue_heatchill: continue_heatchill
volume: volume
goal_default:
- continue_heatchill: false
+ vessel: ''
filtrate_vessel: ''
stir: false
stir_speed: 0.0
- temp: 0.0
- vessel: ''
+ temp: 25.0
+ continue_heatchill: false
volume: 0.0
handles:
input:
@@ -994,7 +942,7 @@ workstation:
data_type: resource
handler_key: Vessel
label: Vessel
- - data_key: vessel
+ - data_key: filtrate_vessel
data_source: handle
data_type: resource
handler_key: filtrate_vessel
@@ -1005,7 +953,7 @@ workstation:
data_type: resource
handler_key: VesselOut
label: Vessel
- - data_key: vessel
+ - data_key: filtrate_vessel
data_source: executor
data_type: resource
handler_key: filtrate_out
@@ -1035,28 +983,35 @@ workstation:
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
- continue_heatchill:
- type: boolean
+ vessel:
+ type: string
+ description: 过滤容器名称
filtrate_vessel:
type: string
+ default: ''
+ description: 滤液容器名称(可选)
stir:
type: boolean
+ default: false
+ description: 是否搅拌
stir_speed:
type: number
+ default: 0.0
+ description: 搅拌速度
temp:
type: number
- vessel:
- type: string
+ default: 25.0
+ description: 温度
+ continue_heatchill:
+ type: boolean
+ default: false
+ description: 是否继续加热冷却
volume:
type: number
+ default: 0.0
+ description: 过滤体积
required:
- vessel
- - filtrate_vessel
- - stir
- - stir_speed
- - temp
- - continue_heatchill
- - volume
title: Filter_Goal
type: object
result:
@@ -1193,19 +1148,27 @@ workstation:
HeatChillProtocol:
feedback: {}
goal:
- purpose: purpose
- stir: stir
- stir_speed: stir_speed
+ vessel: vessel
temp: temp
time: time
- vessel: vessel
+ temp_spec: temp_spec
+ time_spec: time_spec
+ pressure: pressure
+ reflux_solvent: reflux_solvent
+ stir: stir
+ stir_speed: stir_speed
+ purpose: purpose
goal_default:
- purpose: ''
- stir: false
- stir_speed: 0.0
- temp: 0.0
- time: 0.0
vessel: ''
+ temp: 25.0
+ time: 300.0
+ temp_spec: ''
+ time_spec: ''
+ pressure: ''
+ reflux_solvent: ''
+ stir: false
+ stir_speed: 300.0
+ purpose: ''
handles:
input:
- data_key: vessel
@@ -1224,7 +1187,7 @@ workstation:
description: ROS Action HeatChill 的 JSON Schema
properties:
feedback:
- description: Action 反馈 - 执行过程中从服务器发送到客户端
+ description: Action 反馈
properties:
status:
type: string
@@ -1233,39 +1196,64 @@ workstation:
title: HeatChill_Feedback
type: object
goal:
- description: Action 目标 - 从客户端发送到服务器
+ description: Action 目标
properties:
- purpose:
+ vessel:
type: string
- stir:
- type: boolean
- stir_speed:
- type: number
+ description: 加热容器名称
temp:
type: number
+ default: 25.0
+ description: 目标温度
time:
type: number
- vessel:
+ default: 300.0
+ description: 加热时间
+ temp_spec:
type: string
+ default: ''
+ description: 温度规格
+ time_spec:
+ type: string
+ default: ''
+ description: 时间规格
+ pressure:
+ type: string
+ default: ''
+ description: 压力规格
+ reflux_solvent:
+ type: string
+ default: ''
+ description: 回流溶剂名称
+ stir:
+ type: boolean
+ default: false
+ description: 是否搅拌
+ stir_speed:
+ type: number
+ default: 300.0
+ description: 搅拌速度
+ purpose:
+ type: string
+ default: ''
+ description: 操作目的
required:
- vessel
- - temp
- - time
- - stir
- - stir_speed
- - purpose
title: HeatChill_Goal
type: object
result:
- description: Action 结果 - 完成后从服务器发送到客户端
+ description: Action 结果
properties:
+ message:
+ type: string
return_info:
type: string
success:
type: boolean
required:
- - return_info
- success
+ - message
+ - return_info
title: HeatChill_Result
type: object
required:
@@ -1412,6 +1400,11 @@ workstation:
to_vessel: to_vessel
viscous: viscous
volume: volume
+ flowrate: flowrate
+ transfer_flowrate: transfer_flowrate
+ rate_spec: rate_spec
+ event: event
+ through: through
goal_default:
amount: ''
from_vessel: ''
@@ -1423,6 +1416,11 @@ workstation:
to_vessel: ''
viscous: false
volume: 0.0
+ flowrate: 0.0
+ transfer_flowrate: 0.0
+ rate_spec: ''
+ event: ''
+ through: ''
handles:
input:
- data_key: vessel
@@ -1453,7 +1451,7 @@ workstation:
label: To Vessel
result: {}
schema:
- description: ROS Action PumpTransfer 的 JSON Schema
+ description: ROS Action PumpTransfer 的 JSON Schema(兼容增强版)
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
@@ -1463,34 +1461,8 @@ workstation:
status:
type: string
time_remaining:
- properties:
- nanosec:
- maximum: 4294967295
- minimum: 0
- type: integer
- sec:
- maximum: 2147483647
- minimum: -2147483648
- type: integer
- required:
- - sec
- - nanosec
- title: Duration
type: object
time_spent:
- properties:
- nanosec:
- maximum: 4294967295
- minimum: 0
- type: integer
- sec:
- maximum: 2147483647
- minimum: -2147483648
- type: integer
- required:
- - sec
- - nanosec
- title: Duration
type: object
required:
- status
@@ -1506,35 +1478,35 @@ workstation:
type: string
from_vessel:
type: string
- rinsing_repeats:
- maximum: 2147483647
- minimum: -2147483648
- type: integer
+ to_vessel:
+ type: string
+ volume:
+ type: number
+ time:
+ type: number
+ viscous:
+ type: boolean
rinsing_solvent:
type: string
rinsing_volume:
type: number
+ rinsing_repeats:
+ type: integer
solid:
type: boolean
- time:
+ flowrate:
type: number
- to_vessel:
- type: string
- viscous:
- type: boolean
- volume:
+ transfer_flowrate:
type: number
+ rate_spec:
+ type: string
+ event:
+ type: string
+ through:
+ type: string
required:
- from_vessel
- to_vessel
- - volume
- - amount
- - time
- - viscous
- - rinsing_solvent
- - rinsing_volume
- - rinsing_repeats
- - solid
title: PumpTransfer_Goal
type: object
result:
@@ -1888,13 +1860,21 @@ workstation:
StirProtocol:
feedback: {}
goal:
- settling_time: settling_time
- stir_speed: stir_speed
+ vessel: vessel
+ time: time
+ event: event
+ time_spec: time_spec
stir_time: stir_time
+ stir_speed: stir_speed
+ settling_time: settling_time
goal_default:
- settling_time: 0.0
- stir_speed: 0.0
- stir_time: 0.0
+ vessel: ''
+ time: '5 min'
+ event: ''
+ time_spec: ''
+ stir_time: 300.0
+ stir_speed: 200.0
+ settling_time: 60.0
handles:
input:
- data_key: vessel
@@ -1913,7 +1893,7 @@ workstation:
description: ROS Action Stir 的 JSON Schema
properties:
feedback:
- description: Action 反馈 - 执行过程中从服务器发送到客户端
+ description: Action 反馈
properties:
status:
type: string
@@ -1922,30 +1902,52 @@ workstation:
title: Stir_Feedback
type: object
goal:
- description: Action 目标 - 从客户端发送到服务器
+ description: Action 目标
properties:
- settling_time:
+ vessel:
+ type: string
+ description: 搅拌容器名称
+ time:
+ type: string
+ default: '5 min'
+ description: 搅拌时间
+ event:
+ type: string
+ default: ''
+ description: 事件标识
+ time_spec:
+ type: string
+ default: ''
+ description: 时间规格
+ stir_time:
type: number
+ default: 300.0
+ description: 搅拌时间(秒)
stir_speed:
type: number
- stir_time:
+ default: 200.0
+ description: 搅拌速度
+ settling_time:
type: number
+ default: 60.0
+ description: 沉降时间
required:
- - stir_time
- - stir_speed
- - settling_time
+ - vessel
title: Stir_Goal
type: object
result:
- description: Action 结果 - 完成后从服务器发送到客户端
+ description: Action 结果
properties:
+ message:
+ type: string
return_info:
type: string
success:
type: boolean
required:
- - return_info
- success
+ - message
+ - return_info
title: Stir_Result
type: object
required:
@@ -2000,16 +2002,13 @@ workstation:
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
- message:
- type: string
return_info:
type: string
success:
type: boolean
required:
- - success
- - message
- return_info
+ - success
title: StopStir_Result
type: object
required:
@@ -2149,25 +2148,25 @@ workstation:
WashSolidProtocol:
feedback: {}
goal:
- filtrate_vessel: filtrate_vessel
- repeats: repeats
+ vessel: vessel
solvent: solvent
+ volume: volume
+ filtrate_vessel: filtrate_vessel
+ temp: temp
stir: stir
stir_speed: stir_speed
- temp: temp
time: time
- vessel: vessel
- volume: volume
+ repeats: repeats
goal_default:
- filtrate_vessel: ''
- repeats: 0
+ vessel: ''
solvent: ''
+ volume: 0.0
+ filtrate_vessel: ''
+ temp: 25.0
stir: false
stir_speed: 0.0
- temp: 0.0
time: 0.0
- vessel: ''
- volume: 0.0
+ repeats: 1
handles:
input:
- data_key: vessel
@@ -2180,8 +2179,8 @@ workstation:
data_type: resource
handler_key: solvent
label: Solvent
- - data_key: vessel
- data_source: executor
+ - data_key: filtrate_vessel
+ data_source: handle
data_type: resource
handler_key: filtrate_vessel
label: Filtrate Vessel
@@ -2191,7 +2190,7 @@ workstation:
data_type: resource
handler_key: VesselOut
label: Vessel Out
- - data_key: vessel
+ - data_key: filtrate_vessel
data_source: executor
data_type: resource
handler_key: filtrate_vessel_out
@@ -2215,36 +2214,45 @@ workstation:
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
- filtrate_vessel:
+ vessel:
type: string
- repeats:
- maximum: 2147483647
- minimum: -2147483648
- type: integer
+ description: 装有固体的容器名称
solvent:
type: string
+ description: 清洗溶剂名称
+ volume:
+ type: number
+ description: 清洗溶剂体积
+ filtrate_vessel:
+ type: string
+ default: ''
+ description: 滤液收集容器(可选)
+ temp:
+ type: number
+ default: 25.0
+ description: 清洗温度
stir:
type: boolean
+ default: false
+ description: 是否搅拌
stir_speed:
type: number
- temp:
- type: number
+ default: 0.0
+ description: 搅拌速度
time:
type: number
- vessel:
- type: string
- volume:
- type: number
+ default: 0.0
+ description: 清洗时间
+ repeats:
+ type: integer
+ default: 1
+ minimum: 1
+ maximum: 10
+ description: 重复次数
required:
- vessel
- solvent
- volume
- - filtrate_vessel
- - temp
- - stir
- - stir_speed
- - time
- - repeats
title: WashSolid_Goal
type: object
result:
@@ -2645,7 +2653,7 @@ workstation:
vessel: vessel
volume: volume
goal_default:
- ratio: ''
+ ratio: 1.0
solvent1: ''
solvent2: ''
vessel: ''
@@ -2657,12 +2665,12 @@ workstation:
data_type: resource
handler_key: Vessel
label: Vessel
- - data_key: solvent
+ - data_key: solvent1
data_source: handle
data_type: resource
handler_key: solvent1
label: Solvent 1
- - data_key: solvent
+ - data_key: solvent2
data_source: handle
data_type: resource
handler_key: solvent2
@@ -2680,10 +2688,10 @@ workstation:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
- status:
- type: string
progress:
type: number
+ status:
+ type: string
required:
- status
- progress
@@ -2693,7 +2701,7 @@ workstation:
description: Action 目标 - 从客户端发送到服务器
properties:
ratio:
- type: string
+ type: number
solvent1:
type: string
solvent2:
diff --git a/unilabos_msgs/action/EvacuateAndRefill.action b/unilabos_msgs/action/EvacuateAndRefill.action
index 22ffc659a..461cb2837 100644
--- a/unilabos_msgs/action/EvacuateAndRefill.action
+++ b/unilabos_msgs/action/EvacuateAndRefill.action
@@ -1,7 +1,6 @@
-# Organic
+# Organic Synthesis Station EvacuateAndRefill Action
string vessel
string gas
-int32 repeats
---
string return_info
bool success
diff --git a/unilabos_msgs/action/Evaporate.action b/unilabos_msgs/action/Evaporate.action
index 45887f27f..b66e32f0d 100644
--- a/unilabos_msgs/action/Evaporate.action
+++ b/unilabos_msgs/action/Evaporate.action
@@ -1,9 +1,10 @@
-# Organic
+# Organic Synthesis Station Evaporate Action
string vessel
float64 pressure
float64 temp
float64 time
float64 stir_speed
+string solvent
---
string return_info
bool success
diff --git a/unilabos_msgs/action/Filter.action b/unilabos_msgs/action/Filter.action
index 564df1a57..42a89135a 100644
--- a/unilabos_msgs/action/Filter.action
+++ b/unilabos_msgs/action/Filter.action
@@ -1,11 +1,11 @@
# Goal - 过滤操作的目标参数
-string vessel # 过滤容器
-string filtrate_vessel # 滤液容器 (可选)
-bool stir # 是否搅拌
-float64 stir_speed # 搅拌速度 (可选)
-float64 temp # 温度 (可选,摄氏度)
-bool continue_heatchill # 是否继续加热冷却
-float64 volume # 过滤体积 (可选)
+string vessel # 过滤容器(必需)
+string filtrate_vessel # 滤液容器(可选)
+bool stir # 是否搅拌(默认false)
+float64 stir_speed # 搅拌速度(默认0.0)
+float64 temp # 温度(默认25.0)
+bool continue_heatchill # 是否继续加热冷却(默认false)
+float64 volume # 过滤体积(默认0.0)
---
# Result - 操作结果
bool success # 操作是否成功
diff --git a/unilabos_msgs/action/HeatChill.action b/unilabos_msgs/action/HeatChill.action
index 87ebf526a..a39510e71 100644
--- a/unilabos_msgs/action/HeatChill.action
+++ b/unilabos_msgs/action/HeatChill.action
@@ -1,12 +1,19 @@
-# Organic
-string vessel
-float64 temp
-float64 time
-bool stir
-float64 stir_speed
-string purpose
+# Goal - 加热冷却操作的目标参数
+string vessel # 加热容器名称(必需)
+float64 temp # 目标温度(可选,默认25.0)
+float64 time # 加热时间(可选,默认300.0)
+string temp_spec # 温度规格(可选)
+string time_spec # 时间规格(可选)
+string pressure # 压力规格(可选,不做特殊处理)
+string reflux_solvent # 回流溶剂名称(可选,不做特殊处理)
+bool stir # 是否搅拌(可选,默认false)
+float64 stir_speed # 搅拌速度(可选,默认300.0)
+string purpose # 操作目的(可选)
---
+# Result - 操作结果
+bool success # 操作是否成功
+string message # 结果消息
string return_info
-bool success
---
-string status
\ No newline at end of file
+# Feedback - 实时反馈
+string status # 当前状态描述
\ No newline at end of file
diff --git a/unilabos_msgs/action/PumpTransfer.action b/unilabos_msgs/action/PumpTransfer.action
index 69d22b6c4..c8ca445bc 100644
--- a/unilabos_msgs/action/PumpTransfer.action
+++ b/unilabos_msgs/action/PumpTransfer.action
@@ -9,6 +9,11 @@ string rinsing_solvent
float64 rinsing_volume
int32 rinsing_repeats
bool solid
+float64 flowrate
+float64 transfer_flowrate
+string rate_spec
+string event
+string through
---
string return_info
bool success
diff --git a/unilabos_msgs/action/Stir.action b/unilabos_msgs/action/Stir.action
index 9542f9dca..64e9f0436 100644
--- a/unilabos_msgs/action/Stir.action
+++ b/unilabos_msgs/action/Stir.action
@@ -1,9 +1,16 @@
-# Organic
-float64 stir_time
-float64 stir_speed
-float64 settling_time
+# Goal - 搅拌操作的目标参数
+string vessel # 搅拌容器名称(必需)
+string time # 搅拌时间(如 "0.5 h", "30 min")
+string event # 事件标识(如 "A", "B")
+string time_spec # 时间规格(如 "several minutes")
+float64 stir_time # 解析后的搅拌时间(秒)
+float64 stir_speed # 搅拌速度(默认200.0)
+float64 settling_time # 沉降时间(默认60.0)
---
+# Result - 操作结果
+bool success # 操作是否成功
+string message # 结果消息
string return_info
-bool success
---
-string status
\ No newline at end of file
+# Feedback - 实时反馈
+string status # 当前状态描述
\ No newline at end of file
diff --git a/unilabos_msgs/action/WashSolid.action b/unilabos_msgs/action/WashSolid.action
index cb57e5ccc..7aa3c6e13 100644
--- a/unilabos_msgs/action/WashSolid.action
+++ b/unilabos_msgs/action/WashSolid.action
@@ -1,16 +1,19 @@
-string vessel # 装有固体物质的容器名称
-string solvent # 用于清洗固体的溶剂名称
-float64 volume # 清洗溶剂的体积
-string filtrate_vessel # 滤液要收集到的容器名称,可选参数
-float64 temp # 清洗时的温度,可选参数
-bool stir # 是否在清洗过程中搅拌,默认为 False
-float64 stir_speed # 搅拌速度,可选参数
-float64 time # 清洗的时间,可选参数
-int32 repeats # 清洗操作的重复次数,默认为 1
+# Goal - 固体清洗操作的目标参数
+string vessel # 装有固体的容器名称(必需)
+string solvent # 清洗溶剂名称(必需)
+float64 volume # 清洗溶剂体积(必需)
+string filtrate_vessel # 滤液收集容器(可选,默认"")
+float64 temp # 清洗温度(可选,默认25.0)
+bool stir # 是否搅拌(可选,默认false)
+float64 stir_speed # 搅拌速度(可选,默认0.0)
+float64 time # 清洗时间(可选,默认0.0)
+int32 repeats # 重复次数(可选,默认1)
---
-bool success # 操作是否成功
-string message # 结果消息
+# Result - 操作结果
+bool success # 操作是否成功
+string message # 结果消息
string return_info
---
-string status # 当前状态描述
-float64 progress # 进度百分比 (0-100)
\ No newline at end of file
+# Feedback - 实时反馈
+string status # 当前状态描述
+float64 progress # 进度百分比 (0-100)
\ No newline at end of file
From 5767266563b28e6a7e29844fdae83e65a4a13282 Mon Sep 17 00:00:00 2001
From: KCFeng425 <2100011801@stu.pku.edu.cn>
Date: Mon, 7 Jul 2025 12:33:10 +0800
Subject: [PATCH 2/9] =?UTF-8?q?=E8=A1=A5=E5=85=85=E4=BA=86=E5=9B=9B?=
=?UTF-8?q?=E4=B8=AAaction?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
unilabos_msgs/action/Add.action | 25 +++++++++------
unilabos_msgs/action/Dissolve.action | 20 +++++++-----
unilabos_msgs/action/RunColumn.action | 15 +++++++--
unilabos_msgs/action/Separate.action | 45 ++++++++++++++++-----------
4 files changed, 66 insertions(+), 39 deletions(-)
diff --git a/unilabos_msgs/action/Add.action b/unilabos_msgs/action/Add.action
index de06c6a0e..0c6ed2a85 100644
--- a/unilabos_msgs/action/Add.action
+++ b/unilabos_msgs/action/Add.action
@@ -1,14 +1,19 @@
# Goal - 添加试剂的目标参数
-string vessel # 目标容器
-string reagent # 试剂名称
-float64 volume # 体积 (可选)
-float64 mass # 质量 (可选)
-string amount # 数量描述 (可选)
-float64 time # 添加时间 (可选)
-bool stir # 是否搅拌
-float64 stir_speed # 搅拌速度 (可选)
-bool viscous # 是否为粘性液体
-string purpose # 添加目的 (可选)
+string vessel # 目标容器(必需)
+string reagent # 试剂名称(必需)
+float64 volume # 体积 (mL,可选)
+float64 mass # 质量 (g,可选)
+string amount # 数量描述 (可选)
+float64 time # 添加时间 (s,可选)
+bool stir # 是否搅拌(可选)
+float64 stir_speed # 搅拌速度 (RPM,可选)
+bool viscous # 是否为粘性液体(可选)
+string purpose # 添加目的 (可选)
+string event # 事件标识(如 'A', 'B',可选)
+string mol # 摩尔数(如 '0.28 mol', '16.2 mmol',可选)
+string rate_spec # 速率规格(如 'portionwise', 'dropwise',可选)
+string equiv # 当量(如 '1.1',可选)
+string ratio # 比例(如 '1:1',可选)
---
# Result - 操作结果
bool success # 操作是否成功
diff --git a/unilabos_msgs/action/Dissolve.action b/unilabos_msgs/action/Dissolve.action
index 6b860d079..e2d2c43c4 100644
--- a/unilabos_msgs/action/Dissolve.action
+++ b/unilabos_msgs/action/Dissolve.action
@@ -1,14 +1,20 @@
-string vessel # 装有要溶解物质的容器名称
-string solvent # 用于溶解物质的溶剂名称
-float64 volume # 溶剂的体积,可选参数
-string amount # 要溶解物质的量,可选参数
-float64 temp # 溶解时的温度,可选参数
-float64 time # 溶解的时间,可选参数
-float64 stir_speed # 搅拌速度,可选参数
+# Goal - 溶解操作的目标参数
+string vessel # 装有要溶解物质的容器名称(必需)
+string solvent # 用于溶解物质的溶剂名称(可选)
+float64 volume # 溶剂的体积(可选)
+string amount # 要溶解物质的量(可选)
+float64 temp # 溶解时的温度(可选)
+float64 time # 溶解的时间(可选)
+float64 stir_speed # 搅拌速度(可选)
+string mass # 物质质量(如 "2.9 g",可选)
+string mol # 物质摩尔数(如 "0.12 mol",可选)
+string reagent # 试剂名称(可选)
---
+# Result - 操作结果
bool success # 操作是否成功
string message # 结果消息
string return_info
---
+# Feedback - 实时反馈
string status # 当前状态描述
float64 progress # 进度百分比 (0-100)
\ No newline at end of file
diff --git a/unilabos_msgs/action/RunColumn.action b/unilabos_msgs/action/RunColumn.action
index 3fba948ad..889b4a5d8 100644
--- a/unilabos_msgs/action/RunColumn.action
+++ b/unilabos_msgs/action/RunColumn.action
@@ -1,10 +1,19 @@
-string from_vessel # 源容器的名称,即样品起始所在的容器
-string to_vessel # 目标容器的名称,分离后的样品要到达的容器
-string column # 所使用的柱子的名称
+# Goal - 柱层析操作的目标参数
+string from_vessel # 源容器的名称,即样品起始所在的容器(必需)
+string to_vessel # 目标容器的名称,分离后的样品要到达的容器(必需)
+string column # 所使用的柱子的名称(必需)
+string Rf # Rf值(可选)
+string pct1 # 第一种溶剂百分比(如 "40 %",可选)
+string pct2 # 第二种溶剂百分比(如 "50 %",可选)
+string solvent1 # 第一种溶剂名称(可选)
+string solvent2 # 第二种溶剂名称(可选)
+string ratio # 溶剂比例(如 "5:95",可选)
---
+# Result - 操作结果
bool success # 操作是否成功
string message # 结果消息
string return_info
---
+# Feedback - 实时反馈
string status # 当前状态描述
float64 progress # 进度百分比 (0-100)
\ No newline at end of file
diff --git a/unilabos_msgs/action/Separate.action b/unilabos_msgs/action/Separate.action
index fe8976a75..a487fea72 100644
--- a/unilabos_msgs/action/Separate.action
+++ b/unilabos_msgs/action/Separate.action
@@ -1,22 +1,29 @@
-# Organic
-string purpose # 'wash' or 'extract'. 'wash' means that product phase will not be the added solvent phase, 'extract' means product phase will be the added solvent phase. If no solvent is added just use 'extract'.
-string product_phase # 'top' or 'bottom'. Phase that product will be in.
-string from_vessel #Contents of from_vessel are transferred to separation_vessel and separation is performed.
-string separation_vessel # Vessel in which separation of phases will be carried out.
-string to_vessel # Vessel to send product phase to.
-string waste_phase_to_vessel # Optional. Vessel to send waste phase to.
-string solvent # Optional. Solvent to add to separation vessel after contents of from_vessel has been transferred to create two phases.
-float64 solvent_volume # Optional. Volume of solvent to add.
-string through # Optional. Solid chemical to send product phase through on way to to_vessel, e.g. 'celite'.
-int32 repeats # Optional. Number of separations to perform.
-float64 stir_time # Optional. Time stir for after adding solvent, before separation of phases.
-float64 stir_speed # Optional. Speed to stir at after adding solvent, before separation of phases.
-float64 settling_time # Optional. Time
+# Goal - 分离操作的目标参数
+string purpose # 分离目的 ('wash', 'extract', 'separate',必需)
+string product_phase # 产物相 ('top', 'bottom',必需)
+string from_vessel # 源容器(可选)
+string separation_vessel # 分离容器(可选)
+string to_vessel # 目标容器(可选)
+string waste_phase_to_vessel # 废相目标容器(可选)
+string solvent # 溶剂名称(可选)
+float64 solvent_volume # 溶剂体积(可选)
+string through # 通过材料(如 'celite',可选)
+int32 repeats # 重复次数(可选)
+float64 stir_time # 搅拌时间(可选)
+float64 stir_speed # 搅拌速度(可选)
+float64 settling_time # 沉降时间(可选)
+string vessel # 分离容器名称(XDL参数,可选)
+string volume # 体积规格(XDL参数,可选)
+string product_vessel # 产物收集容器(XDL参数,可选)
+string waste_vessel # 废液收集容器(XDL参数,可选)
---
+# Result - 操作结果
+bool success # 操作是否成功
+string message # 结果消息
string return_info
-bool success
---
-string status
-string current_device
-builtin_interfaces/Duration time_spent
-builtin_interfaces/Duration time_remaining
+# Feedback - 实时反馈
+string status # 当前状态描述
+string current_device # 当前设备
+builtin_interfaces/Duration time_spent # 已用时间
+builtin_interfaces/Duration time_remaining # 剩余时间
From ab2ab7fcc7b20d5566271dd4bdd4569820fc3eed Mon Sep 17 00:00:00 2001
From: KCFeng425 <2100011801@stu.pku.edu.cn>
Date: Mon, 7 Jul 2025 18:35:35 +0800
Subject: [PATCH 3/9] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86=E5=9B=BA?=
=?UTF-8?q?=E4=BD=93=E5=8A=A0=E6=A0=B7=E5=99=A8=EF=BC=8C=E4=B8=B0=E5=AF=8C?=
=?UTF-8?q?=E4=BA=86json=EF=BC=8C=E4=BF=AE=E6=94=B9=E4=BA=86add=20protocol?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../comprehensive_protocol/checklist.md | 10 +-
.../comprehensive_station.json | 138 +++-
unilabos/compile/add_protocol.py | 747 ++++++------------
.../virtual/virtual_solid_dispenser.py | 335 ++++++++
unilabos/registry/devices/virtual_device.yaml | 353 ++++-----
unilabos_msgs/action/AddSolid.action | 15 +
6 files changed, 889 insertions(+), 709 deletions(-)
create mode 100644 unilabos/devices/virtual/virtual_solid_dispenser.py
create mode 100644 unilabos_msgs/action/AddSolid.action
diff --git a/test/experiments/comprehensive_protocol/checklist.md b/test/experiments/comprehensive_protocol/checklist.md
index 9645aa4ce..4762cd22e 100644
--- a/test/experiments/comprehensive_protocol/checklist.md
+++ b/test/experiments/comprehensive_protocol/checklist.md
@@ -70,7 +70,7 @@ class SeparateProtocol(BaseModel):
repeats: int
stir_time: float
stir_speed: float
- settling_time: float
+ settling_time: float 写了action
class EvaporateProtocol(BaseModel):
@@ -102,7 +102,7 @@ class AddProtocol(BaseModel):
vessel="main_reactor" volume="2.67 mL"/>
viscous: bool
- purpose: str
+ purpose: str 写了action
class CentrifugeProtocol(BaseModel):
vessel: str
@@ -127,7 +127,7 @@ class HeatChillProtocol(BaseModel):
- stir: bool
+ stir: bool 处理了
stir_speed: float
purpose: str
@@ -180,7 +180,7 @@ class DissolveProtocol(BaseModel):
amount: str = ""
temp: float = 25.0
time: float = 0.0
- stir_speed: float = 0.0
+ stir_speed: float = 0.0 写了action
class FilterThroughProtocol(BaseModel):
from_vessel: str
@@ -194,7 +194,7 @@ class FilterThroughProtocol(BaseModel):
class RunColumnProtocol(BaseModel):
from_vessel: str
to_vessel: str
- column: str
+ column: str 写了action
class WashSolidProtocol(BaseModel):
vessel: str
diff --git a/test/experiments/comprehensive_protocol/comprehensive_station.json b/test/experiments/comprehensive_protocol/comprehensive_station.json
index d0f5c6afc..aaa4dfc8e 100644
--- a/test/experiments/comprehensive_protocol/comprehensive_station.json
+++ b/test/experiments/comprehensive_protocol/comprehensive_station.json
@@ -32,7 +32,11 @@
"separator_1",
"collection_bottle_1",
"collection_bottle_2",
- "collection_bottle_3"
+ "collection_bottle_3",
+ "solid_dispenser_1",
+ "solid_reagent_bottle_1",
+ "solid_reagent_bottle_2",
+ "solid_reagent_bottle_3"
],
"parent": null,
"type": "device",
@@ -672,6 +676,98 @@
"data": {
"current_volume": 0.0
}
+ },
+ {
+ "id": "solid_dispenser_1",
+ "name": "固体粉末加样器",
+ "children": [],
+ "parent": "OrganicSynthesisStation",
+ "type": "device",
+ "class": "virtual_solid_dispenser",
+ "position": {
+ "x": 600,
+ "y": 300,
+ "z": 0
+ },
+ "config": {
+ "max_capacity": 100.0,
+ "precision": 0.001
+ },
+ "data": {
+ "status": "Ready",
+ "current_reagent": "",
+ "dispensed_amount": 0.0,
+ "total_operations": 0
+ }
+ },
+ {
+ "id": "solid_reagent_bottle_1",
+ "name": "固体试剂瓶1-氯化钠",
+ "children": [],
+ "parent": "OrganicSynthesisStation",
+ "type": "container",
+ "class": "container",
+ "position": {
+ "x": 550,
+ "y": 250,
+ "z": 0
+ },
+ "config": {
+ "volume": 500.0,
+ "reagent": "sodium_chloride",
+ "physical_state": "solid"
+ },
+ "data": {
+ "current_mass": 500.0,
+ "reagent_name": "sodium_chloride",
+ "physical_state": "solid"
+ }
+ },
+ {
+ "id": "solid_reagent_bottle_2",
+ "name": "固体试剂瓶2-碳酸钠",
+ "children": [],
+ "parent": "OrganicSynthesisStation",
+ "type": "container",
+ "class": "container",
+ "position": {
+ "x": 600,
+ "y": 250,
+ "z": 0
+ },
+ "config": {
+ "volume": 500.0,
+ "reagent": "sodium_carbonate",
+ "physical_state": "solid"
+ },
+ "data": {
+ "current_mass": 500.0,
+ "reagent_name": "sodium_carbonate",
+ "physical_state": "solid"
+ }
+ },
+ {
+ "id": "solid_reagent_bottle_3",
+ "name": "固体试剂瓶3-氯化镁",
+ "children": [],
+ "parent": "OrganicSynthesisStation",
+ "type": "container",
+ "class": "container",
+ "position": {
+ "x": 650,
+ "y": 250,
+ "z": 0
+ },
+ "config": {
+ "volume": 500.0,
+ "reagent": "magnesium_chloride",
+ "physical_state": "solid"
+ },
+ "data": {
+ "current_mass": 500.0,
+ "reagent_name": "magnesium_chloride",
+ "physical_state": "solid"
+ }
}
],
"links": [
@@ -964,6 +1060,46 @@
"solenoid_valve_3": "out",
"main_reactor": "top"
}
+ },
+ {
+ "id": "link_solid_dispenser_to_reactor",
+ "source": "solid_dispenser_1",
+ "target": "main_reactor",
+ "type": "resource",
+ "port": {
+ "solid_dispenser_1": "SolidOut",
+ "main_reactor": "top"
+ }
+ },
+ {
+ "id": "link_solid_bottle1_to_dispenser",
+ "source": "solid_reagent_bottle_1",
+ "target": "solid_dispenser_1",
+ "type": "resource",
+ "port": {
+ "solid_reagent_bottle_1": "top",
+ "solid_dispenser_1": "SolidIn"
+ }
+ },
+ {
+ "id": "link_solid_bottle2_to_dispenser",
+ "source": "solid_reagent_bottle_2",
+ "target": "solid_dispenser_1",
+ "type": "resource",
+ "port": {
+ "solid_reagent_bottle_2": "top",
+ "solid_dispenser_1": "SolidIn"
+ }
+ },
+ {
+ "id": "link_solid_bottle3_to_dispenser",
+ "source": "solid_reagent_bottle_3",
+ "target": "solid_dispenser_1",
+ "type": "resource",
+ "port": {
+ "solid_reagent_bottle_3": "top",
+ "solid_dispenser_1": "SolidIn"
+ }
}
]
}
\ No newline at end of file
diff --git a/unilabos/compile/add_protocol.py b/unilabos/compile/add_protocol.py
index 144ec96f9..6c51cc9fd 100644
--- a/unilabos/compile/add_protocol.py
+++ b/unilabos/compile/add_protocol.py
@@ -4,355 +4,227 @@
def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str:
- """
- 根据试剂名称查找对应的试剂瓶,支持多种匹配模式:
- 1. 容器名称匹配(如 flask_DMF, reagent_bottle_1-DMF)
- 2. 容器内液体类型匹配(如 liquid_type: "DMF", name: "ethanol")
- 3. 试剂名称匹配(如 reagent_name: "DMF", config.reagent: "ethyl_acetate")
-
- Args:
- G: 网络图
- reagent: 试剂名称
-
- Returns:
- str: 试剂瓶的vessel ID
+ """增强版试剂容器查找,支持固体和液体"""
+ print(f"ADD_PROTOCOL: 查找试剂 '{reagent}' 的容器...")
- Raises:
- ValueError: 如果找不到对应的试剂瓶
- """
- print(f"ADD_PROTOCOL: 正在查找试剂 '{reagent}' 的容器...")
-
- # 第一步:通过容器名称匹配
+ # 1. 直接名称匹配
possible_names = [
- f"flask_{reagent}", # flask_DMF, flask_ethanol
- f"bottle_{reagent}", # bottle_DMF, bottle_ethanol
- f"vessel_{reagent}", # vessel_DMF, vessel_ethanol
- f"{reagent}_flask", # DMF_flask, ethanol_flask
- f"{reagent}_bottle", # DMF_bottle, ethanol_bottle
- f"{reagent}", # 直接用试剂名
- f"reagent_{reagent}", # reagent_DMF, reagent_ethanol
- f"reagent_bottle_{reagent}", # reagent_bottle_DMF
+ reagent,
+ f"flask_{reagent}",
+ f"bottle_{reagent}",
+ f"vessel_{reagent}",
+ f"{reagent}_flask",
+ f"{reagent}_bottle",
+ f"reagent_{reagent}",
+ f"reagent_bottle_{reagent}",
+ f"solid_reagent_bottle_{reagent}", # 🔧 添加固体试剂瓶匹配
]
- # 尝试名称匹配
- for vessel_name in possible_names:
- if vessel_name in G.nodes():
- print(f"ADD_PROTOCOL: 通过名称匹配找到容器: {vessel_name}")
- return vessel_name
+ for name in possible_names:
+ if name in G.nodes():
+ print(f"ADD_PROTOCOL: 找到容器: {name}")
+ return name
- # 第二步:通过模糊名称匹配(名称中包含试剂名)
+ # 2. 模糊匹配 - 检查容器数据
for node_id in G.nodes():
- if G.nodes[node_id].get('type') == 'container':
- # 检查节点ID或名称中是否包含试剂名
- node_name = G.nodes[node_id].get('name', '').lower()
- if (reagent.lower() in node_id.lower() or
- reagent.lower() in node_name):
- print(f"ADD_PROTOCOL: 通过模糊名称匹配找到容器: {node_id} (名称: {node_name})")
- return node_id
-
- # 第三步:通过液体类型匹配
- for node_id in G.nodes():
- if G.nodes[node_id].get('type') == 'container':
- vessel_data = G.nodes[node_id].get('data', {})
- liquids = vessel_data.get('liquid', [])
-
- for liquid in liquids:
- if isinstance(liquid, dict):
- # 支持两种格式的液体类型字段
- liquid_type = liquid.get('liquid_type') or liquid.get('name', '')
- reagent_name = vessel_data.get('reagent_name', '')
- config_reagent = G.nodes[node_id].get('config', {}).get('reagent', '')
-
- # 检查多个可能的字段
- if (liquid_type.lower() == reagent.lower() or
- reagent_name.lower() == reagent.lower() or
- config_reagent.lower() == reagent.lower()):
- print(f"ADD_PROTOCOL: 通过液体类型匹配找到容器: {node_id}")
- print(f" - liquid_type: {liquid_type}")
- print(f" - reagent_name: {reagent_name}")
- print(f" - config.reagent: {config_reagent}")
- return node_id
-
- # 第四步:列出所有可用的容器信息帮助调试
- available_containers = []
- for node_id in G.nodes():
- if G.nodes[node_id].get('type') == 'container':
- vessel_data = G.nodes[node_id].get('data', {})
- config_data = G.nodes[node_id].get('config', {})
- liquids = vessel_data.get('liquid', [])
-
- container_info = {
- 'id': node_id,
- 'name': G.nodes[node_id].get('name', ''),
- 'liquid_types': [],
- 'reagent_name': vessel_data.get('reagent_name', ''),
- 'config_reagent': config_data.get('reagent', '')
- }
-
- for liquid in liquids:
- if isinstance(liquid, dict):
- liquid_type = liquid.get('liquid_type') or liquid.get('name', '')
- if liquid_type:
- container_info['liquid_types'].append(liquid_type)
+ node_data = G.nodes[node_id]
+ if node_data.get('type') == 'container':
+ # 检查配置中的试剂名称
+ config_reagent = node_data.get('config', {}).get('reagent', '')
+ data_reagent = node_data.get('data', {}).get('reagent_name', '')
- available_containers.append(container_info)
-
- print(f"ADD_PROTOCOL: 可用容器列表:")
- for container in available_containers:
- print(f" - {container['id']}: {container['name']}")
- print(f" 液体类型: {container['liquid_types']}")
- print(f" 试剂名称: {container['reagent_name']}")
- print(f" 配置试剂: {container['config_reagent']}")
-
- raise ValueError(f"找不到试剂 '{reagent}' 对应的试剂瓶。尝试了名称匹配: {possible_names}")
-
-
-def find_reagent_vessel_by_any_match(G: nx.DiGraph, reagent: str) -> str:
- """
- 增强版试剂容器查找,支持各种匹配方式的别名函数
- """
- return find_reagent_vessel(G, reagent)
-
-
-def get_vessel_reagent_volume(G: nx.DiGraph, vessel: str) -> float:
- """获取容器中的试剂体积"""
- if vessel not in G.nodes():
- return 0.0
-
- vessel_data = G.nodes[vessel].get('data', {})
- liquids = vessel_data.get('liquid', [])
-
- total_volume = 0.0
- for liquid in liquids:
- if isinstance(liquid, dict):
- # 支持两种格式:新格式 (name, volume) 和旧格式 (liquid_type, liquid_volume)
- volume = liquid.get('volume') or liquid.get('liquid_volume', 0.0)
- total_volume += volume
-
- return total_volume
-
-
-def get_vessel_reagent_types(G: nx.DiGraph, vessel: str) -> List[str]:
- """获取容器中所有试剂的类型"""
- if vessel not in G.nodes():
- return []
-
- vessel_data = G.nodes[vessel].get('data', {})
- liquids = vessel_data.get('liquid', [])
-
- reagent_types = []
- for liquid in liquids:
- if isinstance(liquid, dict):
- # 支持两种格式的试剂类型字段
- reagent_type = liquid.get('liquid_type') or liquid.get('name', '')
- if reagent_type:
- reagent_types.append(reagent_type)
-
- # 同时检查配置中的试剂信息
- config_reagent = G.nodes[vessel].get('config', {}).get('reagent', '')
- reagent_name = vessel_data.get('reagent_name', '')
-
- if config_reagent and config_reagent not in reagent_types:
- reagent_types.append(config_reagent)
- if reagent_name and reagent_name not in reagent_types:
- reagent_types.append(reagent_name)
-
- return reagent_types
-
-
-def find_vessels_by_reagent(G: nx.DiGraph, reagent: str) -> List[str]:
- """
- 根据试剂类型查找所有匹配的容器
- 返回匹配容器的ID列表
- """
- matching_vessels = []
-
- for node_id in G.nodes():
- if G.nodes[node_id].get('type') == 'container':
- # 检查容器名称匹配
- node_name = G.nodes[node_id].get('name', '').lower()
- if reagent.lower() in node_id.lower() or reagent.lower() in node_name:
- matching_vessels.append(node_id)
- continue
+ # 名称匹配
+ if (config_reagent.lower() == reagent.lower() or
+ data_reagent.lower() == reagent.lower() or
+ reagent.lower() in node_id.lower()):
+ print(f"ADD_PROTOCOL: 模糊匹配到容器: {node_id}")
+ return node_id
- # 检查试剂类型匹配
- vessel_data = G.nodes[node_id].get('data', {})
+ # 液体类型匹配(保持原有逻辑)
+ vessel_data = node_data.get('data', {})
liquids = vessel_data.get('liquid', [])
- config_data = G.nodes[node_id].get('config', {})
-
- # 检查 reagent_name 和 config.reagent
- reagent_name = vessel_data.get('reagent_name', '').lower()
- config_reagent = config_data.get('reagent', '').lower()
-
- if (reagent.lower() == reagent_name or
- reagent.lower() == config_reagent):
- matching_vessels.append(node_id)
- continue
-
- # 检查液体列表
for liquid in liquids:
if isinstance(liquid, dict):
liquid_type = liquid.get('liquid_type') or liquid.get('name', '')
if liquid_type.lower() == reagent.lower():
- matching_vessels.append(node_id)
- break
+ print(f"ADD_PROTOCOL: 液体类型匹配到容器: {node_id}")
+ return node_id
- return matching_vessels
+ raise ValueError(f"找不到试剂 '{reagent}' 对应的容器")
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
- """
- 查找与指定容器相连的搅拌器
-
- Args:
- G: 网络图
- vessel: 容器ID
-
- Returns:
- str: 搅拌器ID,如果找不到则返回None
- """
- # 查找所有搅拌器节点
- stirrer_nodes = [node for node in G.nodes()
- if (G.nodes[node].get('class') or '') == 'virtual_stirrer']
-
- # 检查哪个搅拌器与目标容器相连
+ """查找连接到指定容器的搅拌器"""
+ stirrer_nodes = []
+ for node in G.nodes():
+ node_class = G.nodes[node].get('class', '').lower()
+ if 'stirrer' in node_class:
+ stirrer_nodes.append(node)
+
+ # 查找连接到容器的搅拌器
for stirrer in stirrer_nodes:
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
+ print(f"ADD_PROTOCOL: 找到连接的搅拌器: {stirrer}")
return stirrer
- # 如果没有直接连接,返回第一个可用的搅拌器
- return stirrer_nodes[0] if stirrer_nodes else None
+ # 返回第一个搅拌器
+ if stirrer_nodes:
+ print(f"ADD_PROTOCOL: 使用第一个搅拌器: {stirrer_nodes[0]}")
+ return stirrer_nodes[0]
+
+ return None
+
+
+def find_solid_dispenser(G: nx.DiGraph) -> str:
+ """查找固体加样器"""
+ for node in G.nodes():
+ node_class = G.nodes[node].get('class', '').lower()
+ if 'solid_dispenser' in node_class:
+ print(f"ADD_PROTOCOL: 找到固体加样器: {node}")
+ return node
+ return None
def generate_add_protocol(
G: nx.DiGraph,
vessel: str,
reagent: str,
- volume: float,
+ volume: float = 0.0,
mass: float = 0.0,
amount: str = "",
time: float = 0.0,
stir: bool = False,
stir_speed: float = 300.0,
viscous: bool = False,
- purpose: str = "添加试剂"
+ purpose: str = "添加试剂",
+ # 新增XDL参数
+ mol: str = "",
+ event: str = "",
+ rate_spec: str = "",
+ equiv: str = "",
+ ratio: str = "",
+ **kwargs
) -> List[Dict[str, Any]]:
"""
- 生成添加试剂的协议序列,支持智能试剂匹配
+ 生成添加试剂协议
- 基于pump_protocol的成熟算法,实现试剂添加功能:
- 1. 智能查找试剂瓶(支持名称匹配、液体类型匹配、试剂配置匹配)
- 2. **先启动搅拌,再进行转移** - 确保试剂添加更均匀
- 3. 使用pump_protocol实现液体转移
-
- Args:
- G: 有向图,节点为容器和设备,边为连接关系
- vessel: 目标容器(要添加试剂的容器)
- reagent: 试剂名称(用于查找对应的试剂瓶)
- volume: 要添加的体积 (mL)
- mass: 要添加的质量 (g) - 暂时未使用,预留接口
- amount: 其他数量描述
- time: 添加时间 (s),如果指定则计算流速
- stir: 是否启用搅拌
- stir_speed: 搅拌速度 (RPM)
- viscous: 是否为粘稠液体
- purpose: 添加目的描述
-
- Returns:
- List[Dict[str, Any]]: 动作序列
-
- Raises:
- ValueError: 当找不到必要的设备或容器时
+ 智能判断:
+ - 有 mass 或 mol → 固体加样器
+ - 有 volume → 液体转移
+ - 都没有 → 默认液体 1mL
"""
- action_sequence = []
- print(f"ADD_PROTOCOL: 开始生成添加试剂协议")
- print(f" - 目标容器: {vessel}")
- print(f" - 试剂: {reagent}")
- print(f" - 体积: {volume} mL")
- print(f" - 质量: {mass} g")
- print(f" - 搅拌: {stir} (速度: {stir_speed} RPM)")
- print(f" - 粘稠: {viscous}")
- print(f" - 目的: {purpose}")
+ print(f"ADD_PROTOCOL: 添加 {reagent} 到 {vessel}")
+ print(f" - 体积: {volume} mL, 质量: {mass} g, 摩尔: {mol}")
+ print(f" - 时间: {time} s, 事件: {event}, 速率: {rate_spec}")
- # 1. 验证目标容器存在
+ # 1. 验证容器
if vessel not in G.nodes():
- raise ValueError(f"目标容器 '{vessel}' 不存在于系统中")
-
- # 2. 智能查找试剂瓶
- try:
- reagent_vessel = find_reagent_vessel(G, reagent)
- print(f"ADD_PROTOCOL: 找到试剂容器: {reagent_vessel}")
- except ValueError as e:
- raise ValueError(f"无法找到试剂 '{reagent}': {str(e)}")
+ raise ValueError(f"容器 '{vessel}' 不存在")
- # 3. 验证试剂容器中的试剂体积
- available_volume = get_vessel_reagent_volume(G, reagent_vessel)
- print(f"ADD_PROTOCOL: 试剂容器 {reagent_vessel} 中有 {available_volume} mL 试剂")
+ # 2. 判断固体 vs 液体
+ is_solid = (mass > 0 or mol.strip() != "")
- if available_volume < volume:
- print(f"ADD_PROTOCOL: 警告 - 试剂容器中的试剂不足!需要 {volume} mL,可用 {available_volume} mL")
-
- # 4. 验证是否存在从试剂瓶到目标容器的路径
- try:
- path = nx.shortest_path(G, source=reagent_vessel, target=vessel)
- print(f"ADD_PROTOCOL: 找到路径 {reagent_vessel} -> {vessel}: {path}")
- except nx.NetworkXNoPath:
- raise ValueError(f"从试剂瓶 '{reagent_vessel}' 到目标容器 '{vessel}' 没有可用路径")
+ action_sequence = []
- # 5. **先启动搅拌** - 关键改进!
- if stir:
- try:
+ if is_solid:
+ # === 固体加样路径 ===
+ print(f"ADD_PROTOCOL: 使用固体加样器")
+
+ solid_dispenser = find_solid_dispenser(G)
+ if not solid_dispenser:
+ raise ValueError("未找到固体加样器")
+
+ # 启动搅拌(如果需要)
+ if stir:
stirrer_id = find_connected_stirrer(G, vessel)
+ if stirrer_id:
+ action_sequence.append({
+ "device_id": stirrer_id,
+ "action_name": "start_stir",
+ "action_kwargs": {
+ "vessel": vessel,
+ "stir_speed": stir_speed,
+ "purpose": f"准备添加固体 {reagent}"
+ }
+ })
+ # 等待搅拌稳定
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": 3}
+ })
+
+ # 固体加样
+ action_sequence.append({
+ "device_id": solid_dispenser,
+ "action_name": "add_solid",
+ "action_kwargs": {
+ "vessel": vessel,
+ "reagent": reagent,
+ "mass": str(mass) if mass > 0 else "",
+ "mol": mol,
+ "purpose": purpose,
+ "event": event
+ }
+ })
+
+ else:
+ # === 液体转移路径 ===
+ print(f"ADD_PROTOCOL: 使用液体转移")
+
+ # 默认体积
+ if volume <= 0:
+ volume = 1.0
+ print(f"ADD_PROTOCOL: 使用默认体积 1mL")
+
+ # 查找试剂容器
+ try:
+ reagent_vessel = find_reagent_vessel(G, reagent)
+ except ValueError as e:
+ # 🔧 更友好的错误提示
+ available_reagents = []
+ for node_id in G.nodes():
+ node_data = G.nodes[node_id]
+ if node_data.get('type') == 'container':
+ config_reagent = node_data.get('config', {}).get('reagent', '')
+ data_reagent = node_data.get('data', {}).get('reagent_name', '')
+ if config_reagent:
+ available_reagents.append(f"{node_id}({config_reagent})")
+ elif data_reagent:
+ available_reagents.append(f"{node_id}({data_reagent})")
+ error_msg = f"找不到试剂 '{reagent}'。可用试剂: {', '.join(available_reagents)}"
+ print(f"ADD_PROTOCOL: {error_msg}")
+ raise ValueError(error_msg)
+
+ # 启动搅拌
+ if stir:
+ stirrer_id = find_connected_stirrer(G, vessel)
if stirrer_id:
- print(f"ADD_PROTOCOL: 找到搅拌器 {stirrer_id},将在添加前启动搅拌")
-
- # 先启动搅拌
- stir_action = {
+ action_sequence.append({
"device_id": stirrer_id,
"action_name": "start_stir",
"action_kwargs": {
"vessel": vessel,
"stir_speed": stir_speed,
- "purpose": f"{purpose}: 启动搅拌,准备添加 {reagent}"
+ "purpose": f"准备添加液体 {reagent}"
}
- }
-
- action_sequence.append(stir_action)
- print(f"ADD_PROTOCOL: 已添加搅拌动作,速度 {stir_speed} RPM")
-
+ })
# 等待搅拌稳定
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5}
})
- else:
- print(f"ADD_PROTOCOL: 警告 - 需要搅拌但未找到与容器 {vessel} 相连的搅拌器")
- except Exception as e:
- print(f"ADD_PROTOCOL: 搅拌器配置出错: {str(e)}")
-
- # 6. 如果指定了体积,执行液体转移
- if volume > 0:
- # 6.1 计算流速参数
+ # 计算流速
if time > 0:
- # 根据时间计算流速
- transfer_flowrate = volume / time
- flowrate = transfer_flowrate
+ flowrate = volume / time
+ transfer_flowrate = flowrate
else:
- # 使用默认流速
- if viscous:
- transfer_flowrate = 0.3 # 粘稠液体用较慢速度
- flowrate = 1.0
- else:
- transfer_flowrate = 0.5 # 普通液体默认速度
- flowrate = 2.5
+ flowrate = 1.0 if viscous else 2.5
+ transfer_flowrate = 0.3 if viscous else 0.5
- print(f"ADD_PROTOCOL: 准备转移 {volume} mL 从 {reagent_vessel} 到 {vessel}")
- print(f"ADD_PROTOCOL: 转移流速={transfer_flowrate} mL/s, 注入流速={flowrate} mL/s")
-
- # 6.2 使用pump_protocol的核心算法实现液体转移
+ # 🔧 调用 pump_protocol 时使用正确的参数
try:
pump_actions = generate_pump_protocol_with_rinsing(
G=G,
@@ -360,267 +232,104 @@ def generate_add_protocol(
to_vessel=vessel,
volume=volume,
amount=amount,
- time=time,
+ duration=time, # 🔧 使用 duration 而不是 time
viscous=viscous,
- rinsing_solvent="", # 添加试剂通常不需要清洗
+ rinsing_solvent="",
rinsing_volume=0.0,
rinsing_repeats=0,
solid=False,
flowrate=flowrate,
- transfer_flowrate=transfer_flowrate
+ transfer_flowrate=transfer_flowrate,
+ rate_spec=rate_spec,
+ event=event,
+ through="",
+ equiv=equiv,
+ ratio=ratio,
+ **kwargs
)
-
- # 添加pump actions到序列中
action_sequence.extend(pump_actions)
-
except Exception as e:
- raise ValueError(f"生成泵协议时出错: {str(e)}")
-
- print(f"ADD_PROTOCOL: 生成了 {len(action_sequence)} 个动作")
- print(f"ADD_PROTOCOL: 添加试剂协议生成完成")
+ raise ValueError(f"液体转移失败: {str(e)}")
+ print(f"ADD_PROTOCOL: 生成 {len(action_sequence)} 个动作")
return action_sequence
-def generate_add_protocol_with_cleaning(
- G: nx.DiGraph,
- vessel: str,
- reagent: str,
- volume: float,
- mass: float = 0.0,
- amount: str = "",
- time: float = 0.0,
- stir: bool = False,
- stir_speed: float = 300.0,
- viscous: bool = False,
- purpose: str = "添加试剂",
- cleaning_solvent: str = "air",
- cleaning_volume: float = 5.0,
- cleaning_repeats: int = 1
-) -> List[Dict[str, Any]]:
- """
- 生成带清洗的添加试剂协议,支持智能试剂匹配
-
- 与普通添加协议的区别是会在添加后进行管道清洗
-
- Args:
- G: 有向图
- vessel: 目标容器
- reagent: 试剂名称
- volume: 添加体积
- mass: 添加质量(预留)
- amount: 其他数量描述
- time: 添加时间
- stir: 是否搅拌
- stir_speed: 搅拌速度
- viscous: 是否粘稠
- purpose: 添加目的
- cleaning_solvent: 清洗溶剂("air"表示空气清洗)
- cleaning_volume: 清洗体积
- cleaning_repeats: 清洗重复次数
-
- Returns:
- List[Dict[str, Any]]: 动作序列
- """
- action_sequence = []
-
- # 1. 智能查找试剂瓶
- reagent_vessel = find_reagent_vessel(G, reagent)
-
- # 2. **先启动搅拌**
- if stir:
- stirrer_id = find_connected_stirrer(G, vessel)
- if stirrer_id:
- action_sequence.append({
- "device_id": stirrer_id,
- "action_name": "start_stir",
- "action_kwargs": {
- "vessel": vessel,
- "stir_speed": stir_speed,
- "purpose": f"{purpose}: 启动搅拌,准备添加 {reagent}"
- }
- })
-
- # 等待搅拌稳定
- action_sequence.append({
- "action_name": "wait",
- "action_kwargs": {"time": 5}
- })
-
- # 3. 计算流速
- if time > 0:
- transfer_flowrate = volume / time
- flowrate = transfer_flowrate
- else:
- if viscous:
- transfer_flowrate = 0.3
- flowrate = 1.0
- else:
- transfer_flowrate = 0.5
- flowrate = 2.5
-
- # 4. 使用带清洗的pump_protocol
- pump_actions = generate_pump_protocol_with_rinsing(
- G=G,
- from_vessel=reagent_vessel,
- to_vessel=vessel,
- volume=volume,
- amount=amount,
- time=time,
- viscous=viscous,
- rinsing_solvent=cleaning_solvent,
- rinsing_volume=cleaning_volume,
- rinsing_repeats=cleaning_repeats,
- solid=False,
- flowrate=flowrate,
- transfer_flowrate=transfer_flowrate
+# 处理 wait 动作
+def process_wait_action(action_kwargs: Dict[str, Any]) -> Dict[str, Any]:
+ """处理等待动作"""
+ wait_time = action_kwargs.get('time', 1.0)
+ return {
+ "action_name": "wait",
+ "action_kwargs": {"time": wait_time},
+ "description": f"等待 {wait_time} 秒"
+ }
+
+
+# 便捷函数
+def add_liquid(G: nx.DiGraph, vessel: str, reagent: str, volume: float,
+ time: float = 0.0, rate_spec: str = "") -> List[Dict[str, Any]]:
+ """添加液体试剂"""
+ return generate_add_protocol(
+ G, vessel, reagent,
+ volume=volume,
+ time=time,
+ rate_spec=rate_spec
)
-
- action_sequence.extend(pump_actions)
-
- return action_sequence
-def generate_sequential_add_protocol(
- G: nx.DiGraph,
- vessel: str,
- reagents: List[Dict[str, Any]],
- stir_between_additions: bool = True,
- final_stir: bool = True,
- final_stir_speed: float = 400.0,
- final_stir_time: float = 300.0
-) -> List[Dict[str, Any]]:
- """
- 生成连续添加多种试剂的协议,支持智能试剂匹配
-
- Args:
- G: 网络图
- vessel: 目标容器
- reagents: 试剂列表,每个元素包含试剂添加参数
- stir_between_additions: 是否在每次添加之间搅拌
- final_stir: 是否在所有添加完成后进行最终搅拌
- final_stir_speed: 最终搅拌速度
- final_stir_time: 最终搅拌时间
-
- Returns:
- List[Dict[str, Any]]: 完整的动作序列
-
- Example:
- reagents = [
- {
- "reagent": "DMF", # 会匹配 reagent_bottle_1 (reagent_name: "DMF")
- "volume": 10.0,
- "viscous": False,
- "stir_speed": 300.0
- },
- {
- "reagent": "ethyl_acetate", # 会匹配 reagent_bottle_2 (reagent_name: "ethyl_acetate")
- "volume": 5.0,
- "viscous": False,
- "stir_speed": 350.0
- }
- ]
- """
- action_sequence = []
-
- print(f"ADD_PROTOCOL: 开始连续添加 {len(reagents)} 种试剂到容器 {vessel}")
-
- for i, reagent_params in enumerate(reagents):
- reagent_name = reagent_params.get('reagent')
- print(f"ADD_PROTOCOL: 处理第 {i+1}/{len(reagents)} 个试剂: {reagent_name}")
-
- # 生成单个试剂的添加协议
- add_actions = generate_add_protocol(
- G=G,
- vessel=vessel,
- reagent=reagent_name,
- volume=reagent_params.get('volume', 0.0),
- mass=reagent_params.get('mass', 0.0),
- amount=reagent_params.get('amount', ''),
- time=reagent_params.get('time', 0.0),
- stir=stir_between_additions,
- stir_speed=reagent_params.get('stir_speed', 300.0),
- viscous=reagent_params.get('viscous', False),
- purpose=reagent_params.get('purpose', f'添加试剂 {reagent_name} ({i+1}/{len(reagents)})')
- )
-
- action_sequence.extend(add_actions)
-
- # 在添加之间加入等待时间
- if i < len(reagents) - 1: # 不是最后一个试剂
- action_sequence.append({
- "action_name": "wait",
- "action_kwargs": {"time": 10} # 试剂混合时间
- })
-
- # 最终搅拌
- if final_stir:
- stirrer_id = find_connected_stirrer(G, vessel)
- if stirrer_id:
- print(f"ADD_PROTOCOL: 添加最终搅拌动作,速度 {final_stir_speed} RPM,时间 {final_stir_time} 秒")
- action_sequence.extend([
- {
- "device_id": stirrer_id,
- "action_name": "stir",
- "action_kwargs": {
- "stir_time": final_stir_time,
- "stir_speed": final_stir_speed,
- "settling_time": 30.0
- }
- }
- ])
-
- print(f"ADD_PROTOCOL: 连续添加协议生成完成,共 {len(action_sequence)} 个动作")
- return action_sequence
+def add_solid(G: nx.DiGraph, vessel: str, reagent: str, mass: float,
+ event: str = "") -> List[Dict[str, Any]]:
+ """添加固体试剂"""
+ return generate_add_protocol(
+ G, vessel, reagent,
+ mass=mass,
+ event=event
+ )
-# 便捷函数:常用添加方案
-def generate_organic_add_protocol(
- G: nx.DiGraph,
- vessel: str,
- organic_reagent: str,
- volume: float,
- stir_speed: float = 400.0
-) -> List[Dict[str, Any]]:
- """有机试剂添加:慢速、搅拌"""
+def add_solid_mol(G: nx.DiGraph, vessel: str, reagent: str, mol: str,
+ event: str = "") -> List[Dict[str, Any]]:
+ """按摩尔数添加固体试剂"""
return generate_add_protocol(
- G, vessel, organic_reagent, volume, 0.0, "", 0.0,
- True, stir_speed, False, f"添加有机试剂 {organic_reagent}"
+ G, vessel, reagent,
+ mol=mol,
+ event=event
)
-def generate_viscous_add_protocol(
- G: nx.DiGraph,
- vessel: str,
- viscous_reagent: str,
- volume: float,
- addition_time: float = 120.0
-) -> List[Dict[str, Any]]:
- """粘稠试剂添加:慢速、长时间"""
+def add_dropwise(G: nx.DiGraph, vessel: str, reagent: str, volume: float,
+ time: float = 0.0, event: str = "") -> List[Dict[str, Any]]:
+ """滴加液体试剂"""
return generate_add_protocol(
- G, vessel, viscous_reagent, volume, 0.0, "", addition_time,
- True, 250.0, True, f"缓慢添加粘稠试剂 {viscous_reagent}"
+ G, vessel, reagent,
+ volume=volume,
+ time=time,
+ rate_spec="dropwise",
+ event=event
)
-def generate_solvent_add_protocol(
- G: nx.DiGraph,
- vessel: str,
- solvent: str,
- volume: float
-) -> List[Dict[str, Any]]:
- """溶剂添加:快速、无需特殊处理"""
+def add_portionwise(G: nx.DiGraph, vessel: str, reagent: str, mass: float,
+ time: float = 0.0, event: str = "") -> List[Dict[str, Any]]:
+ """分批添加固体试剂"""
return generate_add_protocol(
- G, vessel, solvent, volume, 0.0, "", 0.0,
- False, 300.0, False, f"添加溶剂 {solvent}"
+ G, vessel, reagent,
+ mass=mass,
+ time=time,
+ rate_spec="portionwise",
+ event=event
)
-# 使用示例和测试函数
+# 测试函数
def test_add_protocol():
- """测试添加协议的示例"""
- print("=== ADD PROTOCOL 智能匹配测试 ===")
- print("测试完成")
+ """测试添加协议"""
+ print("=== ADD PROTOCOL 修复版测试 ===")
+ print("✅ 已修复设备查找逻辑")
+ print("✅ 已添加固体试剂瓶支持")
+ print("✅ 已修复错误处理")
+ print("✅ 测试完成")
if __name__ == "__main__":
diff --git a/unilabos/devices/virtual/virtual_solid_dispenser.py b/unilabos/devices/virtual/virtual_solid_dispenser.py
new file mode 100644
index 000000000..48ad9f560
--- /dev/null
+++ b/unilabos/devices/virtual/virtual_solid_dispenser.py
@@ -0,0 +1,335 @@
+import asyncio
+import logging
+import re
+from typing import Dict, Any, Optional
+
+class VirtualSolidDispenser:
+ """
+ 虚拟固体粉末加样器 - 用于处理 Add Protocol 中的固体试剂添加
+
+ 特点:
+ - 高兼容性:缺少参数不报错
+ - 智能识别:自动查找固体试剂瓶
+ - 简单反馈:成功/失败 + 消息
+ """
+
+ def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
+ self.device_id = device_id or "virtual_solid_dispenser"
+ self.config = config or {}
+
+ # 设备参数
+ self.max_capacity = float(self.config.get('max_capacity', 100.0)) # 最大加样量 (g)
+ self.precision = float(self.config.get('precision', 0.001)) # 精度 (g)
+
+ # 状态变量
+ self._status = "Idle"
+ self._current_reagent = ""
+ self._dispensed_amount = 0.0
+ self._total_operations = 0
+
+ self.logger = logging.getLogger(f"VirtualSolidDispenser.{self.device_id}")
+
+ print(f"=== VirtualSolidDispenser {self.device_id} 创建成功! ===")
+ print(f"=== 最大容量: {self.max_capacity}g, 精度: {self.precision}g ===")
+
+ async def initialize(self) -> bool:
+ """初始化固体加样器"""
+ self.logger.info(f"初始化固体加样器 {self.device_id}")
+ self._status = "Ready"
+ self._current_reagent = ""
+ self._dispensed_amount = 0.0
+ return True
+
+ async def cleanup(self) -> bool:
+ """清理固体加样器"""
+ self.logger.info(f"清理固体加样器 {self.device_id}")
+ self._status = "Idle"
+ return True
+
+ def parse_mass_string(self, mass_str: str) -> float:
+ """
+ 解析质量字符串为数值 (g)
+
+ 支持格式: "2.9 g", "19.3g", "4.5 mg", "1.2 kg" 等
+ """
+ if not mass_str or not isinstance(mass_str, str):
+ return 0.0
+
+ # 移除空格并转小写
+ mass_clean = mass_str.strip().lower()
+
+ # 正则匹配数字和单位
+ pattern = r'(\d+(?:\.\d+)?)\s*([a-z]*)'
+ match = re.search(pattern, mass_clean)
+
+ if not match:
+ return 0.0
+
+ try:
+ value = float(match.group(1))
+ unit = match.group(2) or 'g' # 默认单位 g
+
+ # 单位转换为 g
+ unit_multipliers = {
+ 'g': 1.0,
+ 'gram': 1.0,
+ 'grams': 1.0,
+ 'mg': 0.001,
+ 'milligram': 0.001,
+ 'milligrams': 0.001,
+ 'kg': 1000.0,
+ 'kilogram': 1000.0,
+ 'kilograms': 1000.0,
+ 'μg': 0.000001,
+ 'ug': 0.000001,
+ 'microgram': 0.000001,
+ 'micrograms': 0.000001,
+ }
+
+ multiplier = unit_multipliers.get(unit, 1.0)
+ return value * multiplier
+
+ except (ValueError, TypeError):
+ self.logger.warning(f"无法解析质量字符串: {mass_str}")
+ return 0.0
+
+ def parse_mol_string(self, mol_str: str) -> float:
+ """
+ 解析摩尔数字符串为数值 (mol)
+
+ 支持格式: "0.12 mol", "16.2 mmol", "25.2mmol" 等
+ """
+ if not mol_str or not isinstance(mol_str, str):
+ return 0.0
+
+ # 移除空格并转小写
+ mol_clean = mol_str.strip().lower()
+
+ # 正则匹配数字和单位
+ pattern = r'(\d+(?:\.\d+)?)\s*(m?mol)'
+ match = re.search(pattern, mol_clean)
+
+ if not match:
+ return 0.0
+
+ try:
+ value = float(match.group(1))
+ unit = match.group(2)
+
+ # 单位转换为 mol
+ if unit == 'mmol':
+ return value * 0.001
+ else: # mol
+ return value
+
+ except (ValueError, TypeError):
+ self.logger.warning(f"无法解析摩尔数字符串: {mol_str}")
+ return 0.0
+
+ def find_solid_reagent_bottle(self, reagent_name: str) -> str:
+ """
+ 查找固体试剂瓶
+
+ 这是一个简化版本,实际使用时应该连接到系统的设备图
+ """
+ if not reagent_name:
+ return "unknown_solid_bottle"
+
+ # 可能的固体试剂瓶命名模式
+ possible_names = [
+ f"solid_bottle_{reagent_name}",
+ f"reagent_solid_{reagent_name}",
+ f"powder_{reagent_name}",
+ f"{reagent_name}_solid",
+ f"{reagent_name}_powder",
+ f"solid_{reagent_name}",
+ ]
+
+ # 这里简化处理,实际应该查询设备图
+ return possible_names[0]
+
+ async def add_solid(
+ self,
+ vessel: str,
+ reagent: str,
+ mass: str = "",
+ mol: str = "",
+ purpose: str = "",
+ **kwargs # 兼容额外参数
+ ) -> Dict[str, Any]:
+ """
+ 添加固体试剂的主要方法
+
+ Args:
+ vessel: 目标容器
+ reagent: 试剂名称
+ mass: 质量字符串 (如 "2.9 g")
+ mol: 摩尔数字符串 (如 "0.12 mol")
+ purpose: 添加目的
+ **kwargs: 其他兼容参数
+
+ Returns:
+ Dict: 操作结果
+ """
+ try:
+ self.logger.info(f"=== 开始固体加样操作 ===")
+ self.logger.info(f"目标容器: {vessel}")
+ self.logger.info(f"试剂: {reagent}")
+ self.logger.info(f"质量: {mass}")
+ self.logger.info(f"摩尔数: {mol}")
+ self.logger.info(f"目的: {purpose}")
+
+ # 参数验证 - 宽松处理
+ if not vessel:
+ vessel = "main_reactor" # 默认容器
+ self.logger.warning(f"未指定容器,使用默认容器: {vessel}")
+
+ if not reagent:
+ return {
+ "success": False,
+ "message": "错误: 必须指定试剂名称",
+ "return_info": "missing_reagent"
+ }
+
+ # 解析质量和摩尔数
+ mass_value = self.parse_mass_string(mass)
+ mol_value = self.parse_mol_string(mol)
+
+ self.logger.info(f"解析后 - 质量: {mass_value}g, 摩尔数: {mol_value}mol")
+
+ # 确定实际加样量
+ if mass_value > 0:
+ actual_amount = mass_value
+ amount_unit = "g"
+ self.logger.info(f"按质量加样: {actual_amount} {amount_unit}")
+ elif mol_value > 0:
+ # 简化处理:假设分子量为100 g/mol
+ assumed_mw = 100.0
+ actual_amount = mol_value * assumed_mw
+ amount_unit = "g (from mol)"
+ self.logger.info(f"按摩尔数加样: {mol_value} mol → {actual_amount} g (假设分子量 {assumed_mw})")
+ else:
+ # 没有指定量,使用默认值
+ actual_amount = 1.0
+ amount_unit = "g (default)"
+ self.logger.warning(f"未指定质量或摩尔数,使用默认值: {actual_amount} {amount_unit}")
+
+ # 检查容量限制
+ if actual_amount > self.max_capacity:
+ return {
+ "success": False,
+ "message": f"错误: 请求量 {actual_amount}g 超过最大容量 {self.max_capacity}g",
+ "return_info": "exceeds_capacity"
+ }
+
+ # 查找试剂瓶
+ reagent_bottle = self.find_solid_reagent_bottle(reagent)
+ self.logger.info(f"使用试剂瓶: {reagent_bottle}")
+
+ # 模拟加样过程
+ self._status = "Dispensing"
+ self._current_reagent = reagent
+
+ # 计算操作时间 (基于质量)
+ operation_time = max(0.5, actual_amount * 0.1) # 每克0.1秒,最少0.5秒
+
+ self.logger.info(f"开始加样,预计时间: {operation_time:.1f}秒")
+ await asyncio.sleep(operation_time)
+
+ # 更新状态
+ self._dispensed_amount = actual_amount
+ self._total_operations += 1
+ self._status = "Ready"
+
+ # 成功结果
+ success_message = f"成功添加 {reagent} {actual_amount:.3f} {amount_unit} 到 {vessel}"
+
+ self.logger.info(f"=== 固体加样完成 ===")
+ self.logger.info(success_message)
+
+ return {
+ "success": True,
+ "message": success_message,
+ "return_info": f"dispensed_{actual_amount:.3f}g"
+ }
+
+ except Exception as e:
+ error_message = f"固体加样失败: {str(e)}"
+ self.logger.error(error_message)
+ self._status = "Error"
+
+ return {
+ "success": False,
+ "message": error_message,
+ "return_info": "operation_failed"
+ }
+
+ # 状态属性
+ @property
+ def status(self) -> str:
+ return self._status
+
+ @property
+ def current_reagent(self) -> str:
+ return self._current_reagent
+
+ @property
+ def dispensed_amount(self) -> float:
+ return self._dispensed_amount
+
+ @property
+ def total_operations(self) -> int:
+ return self._total_operations
+
+ def get_device_info(self) -> Dict[str, Any]:
+ """获取设备状态信息"""
+ return {
+ "device_id": self.device_id,
+ "status": self._status,
+ "current_reagent": self._current_reagent,
+ "last_dispensed_amount": self._dispensed_amount,
+ "total_operations": self._total_operations,
+ "max_capacity": self.max_capacity,
+ "precision": self.precision
+ }
+
+ def __str__(self):
+ return f"VirtualSolidDispenser({self.device_id}: {self._status}, 最后加样 {self._dispensed_amount:.3f}g)"
+
+
+# 测试函数
+async def test_solid_dispenser():
+ """测试固体加样器"""
+ print("=== 固体加样器测试 ===")
+
+ dispenser = VirtualSolidDispenser("test_dispenser")
+ await dispenser.initialize()
+
+ # 测试1: 按质量加样
+ result1 = await dispenser.add_solid(
+ vessel="main_reactor",
+ reagent="magnesium",
+ mass="2.9 g"
+ )
+ print(f"测试1结果: {result1}")
+
+ # 测试2: 按摩尔数加样
+ result2 = await dispenser.add_solid(
+ vessel="main_reactor",
+ reagent="sodium_nitrite",
+ mol="0.28 mol"
+ )
+ print(f"测试2结果: {result2}")
+
+ # 测试3: 缺少参数
+ result3 = await dispenser.add_solid(
+ reagent="test_compound"
+ )
+ print(f"测试3结果: {result3}")
+
+ print(f"设备信息: {dispenser.get_device_info()}")
+ print("=== 测试完成 ===")
+
+
+if __name__ == "__main__":
+ asyncio.run(test_solid_dispenser())
\ No newline at end of file
diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml
index 889431459..387f98663 100644
--- a/unilabos/registry/devices/virtual_device.yaml
+++ b/unilabos/registry/devices/virtual_device.yaml
@@ -2956,264 +2956,126 @@ virtual_solenoid_valve:
- goal
title: open参数
type: object
- type: UniLabJsonCommandAsync
- auto-reset:
- feedback: {}
- goal: {}
- goal_default: {}
- handles: []
- result: {}
- schema:
- description: reset的参数schema
- properties:
- feedback: {}
- goal:
- properties: {}
- required: []
- type: object
- result: {}
- required:
- - goal
- title: reset参数
- type: object
- type: UniLabJsonCommandAsync
- auto-set_state:
+ type: UniLabJsonCommand
+ auto-set_status:
feedback: {}
goal: {}
goal_default:
- command: null
+ string: null
handles: []
result: {}
schema:
- description: set_state的参数schema
+ description: set_status的参数schema
properties:
feedback: {}
goal:
properties:
- command:
+ string:
type: string
required:
- - command
- type: object
- result: {}
- required:
- - goal
- title: set_state参数
- type: object
- type: UniLabJsonCommandAsync
- auto-set_valve_position:
- feedback: {}
- goal: {}
- goal_default:
- command: null
- handles: []
- result: {}
- schema:
- description: set_valve_position的参数schema
- properties:
- feedback: {}
- goal:
- properties:
- command:
- type: string
- required: []
+ - string
type: object
result: {}
required:
- goal
- title: set_valve_position参数
+ title: set_status参数
type: object
- type: UniLabJsonCommandAsync
- auto-toggle:
+ type: UniLabJsonCommand
+ close:
feedback: {}
goal: {}
goal_default: {}
handles: []
result: {}
schema:
- description: toggle的参数schema
- properties:
- feedback: {}
- goal:
- properties: {}
- required: []
- type: object
- result: {}
- required:
- - goal
- title: toggle参数
- type: object
- type: UniLabJsonCommand
- close:
- feedback: {}
- goal:
- command: CLOSED
- goal_default:
- command: ''
- handles: []
- result:
- success: success
- schema:
- description: ROS Action SendCmd 的 JSON Schema
+ description: ROS Action EmptyIn 的 JSON Schema
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
- properties:
- status:
- type: string
- required:
- - status
- title: SendCmd_Feedback
+ properties: {}
+ required: []
+ title: EmptyIn_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
- properties:
- command:
- type: string
- required:
- - command
- title: SendCmd_Goal
+ properties: {}
+ required: []
+ title: EmptyIn_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
- success:
- type: boolean
required:
- return_info
- - success
- title: SendCmd_Result
+ title: EmptyIn_Result
type: object
required:
- goal
- title: SendCmd
+ title: EmptyIn
type: object
- type: SendCmd
+ type: EmptyIn
open:
feedback: {}
- goal:
- command: OPEN
- goal_default:
- command: ''
- handles: []
- result:
- success: success
- schema:
- description: ROS Action SendCmd 的 JSON Schema
- properties:
- feedback:
- description: Action 反馈 - 执行过程中从服务器发送到客户端
- properties:
- status:
- type: string
- required:
- - status
- title: SendCmd_Feedback
- type: object
- goal:
- description: Action 目标 - 从客户端发送到服务器
- properties:
- command:
- type: string
- required:
- - command
- title: SendCmd_Goal
- type: object
- result:
- description: Action 结果 - 完成后从服务器发送到客户端
- properties:
- return_info:
- type: string
- success:
- type: boolean
- required:
- - return_info
- - success
- title: SendCmd_Result
- type: object
- required:
- - goal
- title: SendCmd
- type: object
- type: SendCmd
- set_state:
- feedback: {}
- goal:
- command: command
- goal_default:
- command: ''
+ goal: {}
+ goal_default: {}
handles: []
- result:
- success: success
+ result: {}
schema:
- description: ROS Action SendCmd 的 JSON Schema
+ description: ROS Action EmptyIn 的 JSON Schema
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
- properties:
- status:
- type: string
- required:
- - status
- title: SendCmd_Feedback
+ properties: {}
+ required: []
+ title: EmptyIn_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
- properties:
- command:
- type: string
- required:
- - command
- title: SendCmd_Goal
+ properties: {}
+ required: []
+ title: EmptyIn_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
- success:
- type: boolean
required:
- return_info
- - success
- title: SendCmd_Result
+ title: EmptyIn_Result
type: object
required:
- goal
- title: SendCmd
+ title: EmptyIn
type: object
- type: SendCmd
- set_valve_position:
+ type: EmptyIn
+ set_status:
feedback: {}
goal:
- command: command
+ string: string
goal_default:
- command: ''
+ string: ''
handles: []
- result:
- success: success
+ result: {}
schema:
- description: ROS Action SendCmd 的 JSON Schema
+ description: ROS Action StrSingleInput 的 JSON Schema
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
- properties:
- status:
- type: string
- required:
- - status
- title: SendCmd_Feedback
+ properties: {}
+ required: []
+ title: StrSingleInput_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
- command:
+ string:
type: string
required:
- - command
- title: SendCmd_Goal
+ - string
+ title: StrSingleInput_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
@@ -3225,13 +3087,13 @@ virtual_solenoid_valve:
required:
- return_info
- success
- title: SendCmd_Result
+ title: StrSingleInput_Result
type: object
required:
- goal
- title: SendCmd
+ title: StrSingleInput
type: object
- type: SendCmd
+ type: StrSingleInput
module: unilabos.devices.virtual.virtual_solenoid_valve:VirtualSolenoidValve
status_types:
is_open: bool
@@ -3321,6 +3183,7 @@ virtual_stirrer:
properties:
feedback: {}
goal:
+
properties: {}
required: []
type: object
@@ -4504,3 +4367,125 @@ virtual_vacuum_pump:
required:
- status
type: object
+virtual_solid_dispenser:
+ class:
+ action_value_mappings:
+ auto-cleanup:
+ feedback: {}
+ goal: {}
+ goal_default: {}
+ handles: []
+ result: {}
+ schema:
+ description: cleanup的参数schema
+ type: object
+ type: UniLabJsonCommandAsync
+ auto-initialize:
+ feedback: {}
+ goal: {}
+ goal_default: {}
+ handles: []
+ result: {}
+ schema:
+ description: initialize的参数schema
+ type: object
+ type: UniLabJsonCommandAsync
+ auto-add_solid:
+ feedback: {}
+ goal: {}
+ goal_default:
+ vessel: main_reactor
+ reagent: ''
+ mass: ''
+ mol: ''
+ purpose: ''
+ handles: []
+ result: {}
+ schema:
+ description: add_solid的参数schema
+ type: object
+ type: UniLabJsonCommandAsync
+ add_solid:
+ feedback:
+ current_status: status
+ progress: progress
+ goal:
+ vessel: vessel
+ reagent: reagent
+ mass: mass
+ mol: mol
+ purpose: purpose
+ goal_default:
+ vessel: 'main_reactor'
+ reagent: ''
+ mass: ''
+ mol: ''
+ purpose: ''
+ handles: []
+ result:
+ message: message
+ success: success
+ return_info: return_info
+ schema:
+ description: ROS Action AddSolid 的 JSON Schema
+ type: object
+ type: AddSolid
+ module: unilabos.devices.virtual.virtual_solid_dispenser:VirtualSolidDispenser
+ status_types:
+ status: str
+ current_reagent: str
+ dispensed_amount: float
+ total_operations: int
+ type: python
+ description: Virtual Solid Dispenser for Add Protocol Solid Reagents
+ handles:
+ - data_key: SolidIn
+ data_source: handle
+ data_type: resource
+ description: 固体试剂进料口
+ handler_key: SolidIn
+ io_type: target
+ label: SolidIn
+ side: WEST
+ - data_key: SolidOut
+ data_source: executor
+ data_type: resource
+ description: 固体试剂出料口
+ handler_key: SolidOut
+ io_type: source
+ label: SolidOut
+ side: EAST
+ icon: ''
+ init_param_schema:
+ config:
+ properties:
+ config:
+ type: object
+ device_id:
+ type: string
+ max_capacity:
+ type: number
+ default: 100.0
+ description: 最大加样容量 (g)
+ precision:
+ type: number
+ default: 0.001
+ description: 加样精度 (g)
+ required: []
+ type: object
+ data:
+ properties:
+ status:
+ type: string
+ current_reagent:
+ type: string
+ dispensed_amount:
+ type: number
+ total_operations:
+ type: integer
+ required:
+ - status
+ - current_reagent
+ - dispensed_amount
+ - total_operations
+ type: object
diff --git a/unilabos_msgs/action/AddSolid.action b/unilabos_msgs/action/AddSolid.action
new file mode 100644
index 000000000..881244189
--- /dev/null
+++ b/unilabos_msgs/action/AddSolid.action
@@ -0,0 +1,15 @@
+# Goal - 固体加样操作的目标参数
+string vessel # 目标容器(必需)
+string reagent # 试剂名称(必需)
+string mass # 质量字符串(如 "2.9 g",可选)
+string mol # 摩尔数字符串(如 "0.12 mol",可选)
+string purpose # 添加目的(可选)
+---
+# Result - 操作结果
+bool success # 操作是否成功
+string message # 结果消息
+string return_info # 返回信息
+---
+# Feedback - 实时反馈
+string current_status # 当前状态描述
+float64 progress # 进度百分比 (0-100)
\ No newline at end of file
From acc9e5ce0db2279d9b4b6f9b879734b296421a35 Mon Sep 17 00:00:00 2001
From: KCFeng425 <2100011801@stu.pku.edu.cn>
Date: Mon, 7 Jul 2025 18:44:01 +0800
Subject: [PATCH 4/9] bump version to 0.9.9
---
README.md | 2 +-
README_zh.md | 2 +-
recipes/ros-humble-unilabos-msgs/recipe.yaml | 2 +-
recipes/unilabos/recipe.yaml | 2 +-
setup.py | 2 +-
5 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index 0d02849c2..287efb50d 100644
--- a/README.md
+++ b/README.md
@@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n environment_name
# Currently, you need to install the `unilabos_msgs` package
# You can download the system-specific package from the Release page
-conda install ros-humble-unilabos-msgs-0.9.8-xxxxx.tar.bz2
+conda install ros-humble-unilabos-msgs-0.9.9-xxxxx.tar.bz2
# Install PyLabRobot and other prerequisites
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
diff --git a/README_zh.md b/README_zh.md
index a80f06cf0..8bba3d77b 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n 环境名
# 现阶段,需要安装 `unilabos_msgs` 包
# 可以前往 Release 页面下载系统对应的包进行安装
-conda install ros-humble-unilabos-msgs-0.9.8-xxxxx.tar.bz2
+conda install ros-humble-unilabos-msgs-0.9.9-xxxxx.tar.bz2
# 安装PyLabRobot等前置
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
diff --git a/recipes/ros-humble-unilabos-msgs/recipe.yaml b/recipes/ros-humble-unilabos-msgs/recipe.yaml
index dde6acc8f..4169c73d2 100644
--- a/recipes/ros-humble-unilabos-msgs/recipe.yaml
+++ b/recipes/ros-humble-unilabos-msgs/recipe.yaml
@@ -1,6 +1,6 @@
package:
name: ros-humble-unilabos-msgs
- version: 0.9.8
+ version: 0.9.9
source:
path: ../../unilabos_msgs
folder: ros-humble-unilabos-msgs/src/work
diff --git a/recipes/unilabos/recipe.yaml b/recipes/unilabos/recipe.yaml
index c54e14122..f9262e2b7 100644
--- a/recipes/unilabos/recipe.yaml
+++ b/recipes/unilabos/recipe.yaml
@@ -1,6 +1,6 @@
package:
name: unilabos
- version: "0.9.8"
+ version: "0.9.9"
source:
path: ../..
diff --git a/setup.py b/setup.py
index 4dde10701..2d9aabdb9 100644
--- a/setup.py
+++ b/setup.py
@@ -4,7 +4,7 @@
setup(
name=package_name,
- version='0.9.8',
+ version='0.9.9',
packages=find_packages(),
include_package_data=True,
install_requires=['setuptools'],
From d82ccd5cf1afc8c9203ced40ce94512e474481ec Mon Sep 17 00:00:00 2001
From: Junhan Chang
Date: Tue, 8 Jul 2025 17:25:42 +0800
Subject: [PATCH 5/9] fix bugs from new actions
---
unilabos/compile/heatchill_protocol.py | 136 ++
unilabos/compile/pump_protocol.py | 22 +-
unilabos/messages/__init__.py | 27 +-
unilabos/registry/devices/mock_devices.yaml | 83 +-
.../devices/organic_miscellaneous.yaml | 21 +-
unilabos/registry/devices/temperature.yaml | 21 +-
unilabos/registry/devices/virtual_device.yaml | 408 ++++--
unilabos/registry/devices/work_station.yaml | 1304 +++++++++--------
unilabos_msgs/CMakeLists.txt | 1 +
unilabos_msgs/action/RunColumn.action | 2 +-
10 files changed, 1297 insertions(+), 728 deletions(-)
diff --git a/unilabos/compile/heatchill_protocol.py b/unilabos/compile/heatchill_protocol.py
index 260d9bc9e..3e7f551f8 100644
--- a/unilabos/compile/heatchill_protocol.py
+++ b/unilabos/compile/heatchill_protocol.py
@@ -247,6 +247,142 @@ def generate_heat_chill_protocol(
return action_sequence
+
+def generate_heat_chill_to_temp_protocol(
+ G: nx.DiGraph,
+ vessel: str,
+ temp: float = 25.0,
+ time: float = 300.0,
+ temp_spec: str = "",
+ time_spec: str = "",
+ pressure: str = "",
+ reflux_solvent: str = "",
+ stir: bool = False,
+ stir_speed: float = 300.0,
+ purpose: str = "",
+ **kwargs # 🔧 接受额外参数,增强兼容性
+) -> List[Dict[str, Any]]:
+ """
+ 生成加热/冷却操作的协议序列
+
+ Args:
+ G: 设备图
+ vessel: 加热容器名称(必需)
+ temp: 目标温度 (°C)
+ time: 加热时间 (秒)
+ temp_spec: 温度规格(如 'room temperature', 'reflux')
+ time_spec: 时间规格(如 'overnight', '2 h')
+ pressure: 压力规格(如 '1 mbar'),不做特殊处理
+ reflux_solvent: 回流溶剂名称,不做特殊处理
+ stir: 是否搅拌
+ stir_speed: 搅拌速度 (RPM)
+ purpose: 操作目的
+ **kwargs: 其他参数(兼容性)
+
+ Returns:
+ List[Dict[str, Any]]: 加热操作的动作序列
+ """
+
+ debug_print("=" * 50)
+ debug_print("开始生成加热冷却协议")
+ debug_print(f"输入参数:")
+ debug_print(f" - vessel: {vessel}")
+ debug_print(f" - temp: {temp}°C")
+ debug_print(f" - time: {time}s ({time / 60:.1f}分钟)")
+ debug_print(f" - temp_spec: {temp_spec}")
+ debug_print(f" - time_spec: {time_spec}")
+ debug_print(f" - pressure: {pressure}")
+ debug_print(f" - reflux_solvent: {reflux_solvent}")
+ debug_print(f" - stir: {stir}")
+ debug_print(f" - stir_speed: {stir_speed} RPM")
+ debug_print(f" - purpose: {purpose}")
+ debug_print(f" - 其他参数: {kwargs}")
+ debug_print("=" * 50)
+
+ action_sequence = []
+
+ # === 参数验证 ===
+ debug_print("步骤1: 参数验证...")
+
+ # 验证必需参数
+ if not vessel:
+ raise ValueError("vessel 参数不能为空")
+
+ if vessel not in G.nodes():
+ raise ValueError(f"容器 '{vessel}' 不存在于系统中")
+
+ # 温度解析:优先使用 temp_spec,然后是 temp
+ final_temp = temp
+ if temp_spec:
+ final_temp = parse_temp_spec(temp_spec)
+ debug_print(f"温度解析: '{temp_spec}' → {final_temp}°C")
+
+ # 时间解析:优先使用 time_spec,然后是 time
+ final_time = time
+ if time_spec:
+ final_time = parse_time_spec(time_spec)
+ debug_print(f"时间解析: '{time_spec}' → {final_time}s ({final_time / 60:.1f}分钟)")
+
+ # 参数范围验证
+ if final_temp < -50.0 or final_temp > 300.0:
+ debug_print(f"温度 {final_temp}°C 超出范围,修正为 25°C")
+ final_temp = 25.0
+
+ if final_time < 0:
+ debug_print(f"时间 {final_time}s 无效,修正为 300s")
+ final_time = 300.0
+
+ if stir_speed < 0 or stir_speed > 1500.0:
+ debug_print(f"搅拌速度 {stir_speed} RPM 超出范围,修正为 300 RPM")
+ stir_speed = 300.0
+
+ debug_print(f"✅ 参数验证通过")
+
+ # === 查找加热设备 ===
+ debug_print("步骤2: 查找加热设备...")
+
+ try:
+ heatchill_id = find_connected_heatchill(G, vessel)
+ debug_print(f"设备配置: 加热设备 = {heatchill_id}")
+
+ except Exception as e:
+ debug_print(f"❌ 设备查找失败: {str(e)}")
+ raise ValueError(f"无法找到加热设备: {str(e)}")
+
+ # === 执行加热操作 ===
+ debug_print("步骤3: 执行加热操作...")
+
+ heatchill_action = {
+ "device_id": heatchill_id,
+ "action_name": "heat_chill",
+ "action_kwargs": {
+ "vessel": vessel,
+ "temp": final_temp,
+ "time": final_time,
+ "stir": stir,
+ "stir_speed": stir_speed,
+ "purpose": purpose or f"加热到 {final_temp}°C"
+ }
+ }
+
+ action_sequence.append(heatchill_action)
+
+ # === 总结 ===
+ debug_print("=" * 50)
+ debug_print(f"加热冷却协议生成完成")
+ debug_print(f"总动作数: {len(action_sequence)}")
+ debug_print(f"加热容器: {vessel}")
+ debug_print(f"目标温度: {final_temp}°C")
+ debug_print(f"加热时间: {final_time}s ({final_time / 60:.1f}分钟)")
+ if pressure:
+ debug_print(f"压力参数: {pressure} (已接收,不做特殊处理)")
+ if reflux_solvent:
+ debug_print(f"回流溶剂: {reflux_solvent} (已接收,不做特殊处理)")
+ debug_print("=" * 50)
+
+ return action_sequence
+
+
def generate_heat_chill_start_protocol(
G: nx.DiGraph,
vessel: str,
diff --git a/unilabos/compile/pump_protocol.py b/unilabos/compile/pump_protocol.py
index 5200188ab..acdb52c6d 100644
--- a/unilabos/compile/pump_protocol.py
+++ b/unilabos/compile/pump_protocol.py
@@ -778,17 +778,17 @@ def generate_pump_protocol_with_rinsing(
)
# 为每个动作添加唯一标识
- for i, action in enumerate(pump_action_sequence):
- if isinstance(action, dict):
- action['_protocol_id'] = protocol_id
- action['_action_sequence'] = i
- elif isinstance(action, list):
- for j, sub_action in enumerate(action):
- if isinstance(sub_action, dict):
- sub_action['_protocol_id'] = protocol_id
- sub_action['_action_sequence'] = f"{i}_{j}"
-
- debug_print(f"📊 协议 {protocol_id} 生成完成,共 {len(pump_action_sequence)} 个动作")
+ # for i, action in enumerate(pump_action_sequence):
+ # if isinstance(action, dict):
+ # action['_protocol_id'] = protocol_id
+ # action['_action_sequence'] = i
+ # elif isinstance(action, list):
+ # for j, sub_action in enumerate(action):
+ # if isinstance(sub_action, dict):
+ # sub_action['_protocol_id'] = protocol_id
+ # sub_action['_action_sequence'] = f"{i}_{j}"
+ #
+ # debug_print(f"📊 协议 {protocol_id} 生成完成,共 {len(pump_action_sequence)} 个动作")
debug_print(f"🔓 释放执行锁")
return pump_action_sequence
diff --git a/unilabos/messages/__init__.py b/unilabos/messages/__init__.py
index dfdbab389..52fd97f84 100644
--- a/unilabos/messages/__init__.py
+++ b/unilabos/messages/__init__.py
@@ -372,6 +372,30 @@ def _parse_time_spec(self, time_spec: str) -> float:
return 300.0 # 默认5分钟
+
+class HeatChillStartProtocol(BaseModel):
+ # === 必需参数 ===
+ vessel: str = Field(..., description="加热容器名称")
+
+ # === 可选参数 - 温度相关 ===
+ temp: float = Field(25.0, description="目标温度 (°C)")
+ temp_spec: str = Field("", description="温度规格(如 'room temperature', 'reflux')")
+
+ # === 可选参数 - 其他XDL参数 ===
+ pressure: str = Field("", description="压力规格(如 '1 mbar'),不做特殊处理")
+ reflux_solvent: str = Field("", description="回流溶剂名称,不做特殊处理")
+
+ # === 可选参数 - 搅拌相关 ===
+ stir: bool = Field(False, description="是否搅拌")
+ stir_speed: float = Field(300.0, description="搅拌速度 (RPM)")
+ purpose: str = Field("", description="操作目的")
+
+
+class HeatChillStopProtocol(BaseModel):
+ # === 必需参数 ===
+ vessel: str = Field(..., description="加热容器名称")
+
+
class StirProtocol(BaseModel):
# === 必需参数 ===
vessel: str = Field(..., description="搅拌容器名称")
@@ -608,7 +632,8 @@ class HydrogenateProtocol(BaseModel):
"Point3D", "PumpTransferProtocol", "CleanProtocol", "SeparateProtocol",
"EvaporateProtocol", "EvacuateAndRefillProtocol", "AGVTransferProtocol",
"CentrifugeProtocol", "AddProtocol", "FilterProtocol",
- "HeatChillProtocol", "HeatChillStartProtocol", "HeatChillStopProtocol",
+ "HeatChillProtocol",
+ "HeatChillStartProtocol", "HeatChillStopProtocol",
"StirProtocol", "StartStirProtocol", "StopStirProtocol",
"TransferProtocol", "CleanVesselProtocol", "DissolveProtocol",
"FilterThroughProtocol", "RunColumnProtocol", "WashSolidProtocol",
diff --git a/unilabos/registry/devices/mock_devices.yaml b/unilabos/registry/devices/mock_devices.yaml
index 4341434de..ea5c7e8a1 100644
--- a/unilabos/registry/devices/mock_devices.yaml
+++ b/unilabos/registry/devices/mock_devices.yaml
@@ -842,11 +842,15 @@ mock_heater:
time: time
vessel: vessel
goal_default:
+ pressure: ''
purpose: ''
+ reflux_solvent: ''
stir: false
stir_speed: 0.0
temp: 0.0
+ temp_spec: ''
time: 0.0
+ time_spec: ''
vessel: ''
handles: []
result:
@@ -866,22 +870,34 @@ mock_heater:
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
+ pressure:
+ type: string
purpose:
type: string
+ reflux_solvent:
+ type: string
stir:
type: boolean
stir_speed:
type: number
temp:
type: number
+ temp_spec:
+ type: string
time:
type: number
+ time_spec:
+ type: string
vessel:
type: string
required:
- vessel
- temp
- time
+ - temp_spec
+ - time_spec
+ - pressure
+ - reflux_solvent
- stir
- stir_speed
- purpose
@@ -890,13 +906,16 @@ mock_heater:
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
+ message:
+ type: string
return_info:
type: string
success:
type: boolean
required:
- - return_info
- success
+ - message
+ - return_info
title: HeatChill_Result
type: object
required:
@@ -1270,13 +1289,18 @@ mock_pump:
volume: volume
goal_default:
amount: ''
+ event: ''
+ flowrate: 0.0
from_vessel: ''
+ rate_spec: ''
rinsing_repeats: 0
rinsing_solvent: ''
rinsing_volume: 0.0
solid: false
+ through: ''
time: 0.0
to_vessel: ''
+ transfer_flowrate: 0.0
viscous: false
volume: 0.0
handles: []
@@ -1334,8 +1358,14 @@ mock_pump:
properties:
amount:
type: string
+ event:
+ type: string
+ flowrate:
+ type: number
from_vessel:
type: string
+ rate_spec:
+ type: string
rinsing_repeats:
maximum: 2147483647
minimum: -2147483648
@@ -1346,10 +1376,14 @@ mock_pump:
type: number
solid:
type: boolean
+ through:
+ type: string
time:
type: number
to_vessel:
type: string
+ transfer_flowrate:
+ type: number
viscous:
type: boolean
volume:
@@ -1365,6 +1399,11 @@ mock_pump:
- rinsing_volume
- rinsing_repeats
- solid
+ - flowrate
+ - transfer_flowrate
+ - rate_spec
+ - event
+ - through
title: PumpTransfer_Goal
type: object
result:
@@ -2281,6 +2320,7 @@ mock_separator:
goal_default:
from_vessel: ''
product_phase: ''
+ product_vessel: ''
purpose: ''
repeats: 0
separation_vessel: ''
@@ -2291,7 +2331,10 @@ mock_separator:
stir_time: 0.0
through: ''
to_vessel: ''
+ vessel: ''
+ volume: ''
waste_phase_to_vessel: ''
+ waste_vessel: ''
handles: []
result:
success: success
@@ -2349,6 +2392,8 @@ mock_separator:
type: string
product_phase:
type: string
+ product_vessel:
+ type: string
purpose:
type: string
repeats:
@@ -2371,8 +2416,14 @@ mock_separator:
type: string
to_vessel:
type: string
+ vessel:
+ type: string
+ volume:
+ type: string
waste_phase_to_vessel:
type: string
+ waste_vessel:
+ type: string
required:
- purpose
- product_phase
@@ -2387,18 +2438,25 @@ mock_separator:
- stir_time
- stir_speed
- settling_time
+ - vessel
+ - volume
+ - product_vessel
+ - waste_vessel
title: Separate_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
+ message:
+ type: string
return_info:
type: string
success:
type: boolean
required:
- - return_info
- success
+ - message
+ - return_info
title: Separate_Result
type: object
required:
@@ -3472,9 +3530,13 @@ mock_stirrer_new:
stir_speed: stir_speed
stir_time: stir_time
goal_default:
+ event: ''
settling_time: 0.0
stir_speed: 0.0
stir_time: 0.0
+ time: ''
+ time_spec: ''
+ vessel: ''
handles: []
result:
success: success
@@ -3493,13 +3555,25 @@ mock_stirrer_new:
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
+ event:
+ type: string
settling_time:
type: number
stir_speed:
type: number
stir_time:
type: number
+ time:
+ type: string
+ time_spec:
+ type: string
+ vessel:
+ type: string
required:
+ - vessel
+ - time
+ - event
+ - time_spec
- stir_time
- stir_speed
- settling_time
@@ -3508,13 +3582,16 @@ mock_stirrer_new:
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
+ message:
+ type: string
return_info:
type: string
success:
type: boolean
required:
- - return_info
- success
+ - message
+ - return_info
title: Stir_Result
type: object
required:
diff --git a/unilabos/registry/devices/organic_miscellaneous.yaml b/unilabos/registry/devices/organic_miscellaneous.yaml
index 00e7550af..954970512 100644
--- a/unilabos/registry/devices/organic_miscellaneous.yaml
+++ b/unilabos/registry/devices/organic_miscellaneous.yaml
@@ -324,9 +324,13 @@ separator.homemade:
stir_speed: stir_speed
stir_time: stir_time,
goal_default:
+ event: ''
settling_time: 0.0
stir_speed: 0.0
stir_time: 0.0
+ time: ''
+ time_spec: ''
+ vessel: ''
handles: []
result:
success: success
@@ -345,13 +349,25 @@ separator.homemade:
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
+ event:
+ type: string
settling_time:
type: number
stir_speed:
type: number
stir_time:
type: number
+ time:
+ type: string
+ time_spec:
+ type: string
+ vessel:
+ type: string
required:
+ - vessel
+ - time
+ - event
+ - time_spec
- stir_time
- stir_speed
- settling_time
@@ -360,13 +376,16 @@ separator.homemade:
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
+ message:
+ type: string
return_info:
type: string
success:
type: boolean
required:
- - return_info
- success
+ - message
+ - return_info
title: Stir_Result
type: object
required:
diff --git a/unilabos/registry/devices/temperature.yaml b/unilabos/registry/devices/temperature.yaml
index 68a4f5ef1..6c606704f 100644
--- a/unilabos/registry/devices/temperature.yaml
+++ b/unilabos/registry/devices/temperature.yaml
@@ -377,11 +377,15 @@ heaterstirrer.dalong:
time: time
vessel: vessel
goal_default:
+ pressure: ''
purpose: ''
+ reflux_solvent: ''
stir: false
stir_speed: 0.0
temp: 0.0
+ temp_spec: ''
time: 0.0
+ time_spec: ''
vessel: ''
handles: []
result:
@@ -401,22 +405,34 @@ heaterstirrer.dalong:
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
+ pressure:
+ type: string
purpose:
type: string
+ reflux_solvent:
+ type: string
stir:
type: boolean
stir_speed:
type: number
temp:
type: number
+ temp_spec:
+ type: string
time:
type: number
+ time_spec:
+ type: string
vessel:
type: string
required:
- vessel
- temp
- time
+ - temp_spec
+ - time_spec
+ - pressure
+ - reflux_solvent
- stir
- stir_speed
- purpose
@@ -425,13 +441,16 @@ heaterstirrer.dalong:
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
+ message:
+ type: string
return_info:
type: string
success:
type: boolean
required:
- - return_info
- success
+ - message
+ - return_info
title: HeatChill_Result
type: object
required:
diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml
index 387f98663..1bbc5f7f1 100644
--- a/unilabos/registry/devices/virtual_device.yaml
+++ b/unilabos/registry/devices/virtual_device.yaml
@@ -318,6 +318,12 @@ virtual_column:
goal_default:
column: ''
from_vessel: ''
+ pct1: ''
+ pct2: ''
+ ratio: ''
+ rf: ''
+ solvent1: ''
+ solvent2: ''
to_vessel: ''
handles: []
result:
@@ -346,12 +352,30 @@ virtual_column:
type: string
from_vessel:
type: string
+ pct1:
+ type: string
+ pct2:
+ type: string
+ ratio:
+ type: string
+ rf:
+ type: string
+ solvent1:
+ type: string
+ solvent2:
+ type: string
to_vessel:
type: string
required:
- from_vessel
- to_vessel
- column
+ - rf
+ - pct1
+ - pct2
+ - solvent1
+ - solvent2
+ - ratio
title: RunColumn_Goal
type: object
result:
@@ -1155,11 +1179,15 @@ virtual_heatchill:
time: time
vessel: vessel
goal_default:
+ pressure: ''
purpose: ''
+ reflux_solvent: ''
stir: false
stir_speed: 0.0
temp: 0.0
+ temp_spec: ''
time: 0.0
+ time_spec: ''
vessel: ''
handles: []
result:
@@ -1179,22 +1207,34 @@ virtual_heatchill:
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
+ pressure:
+ type: string
purpose:
type: string
+ reflux_solvent:
+ type: string
stir:
type: boolean
stir_speed:
type: number
temp:
type: number
+ temp_spec:
+ type: string
time:
type: number
+ time_spec:
+ type: string
vessel:
type: string
required:
- vessel
- temp
- time
+ - temp_spec
+ - time_spec
+ - pressure
+ - reflux_solvent
- stir
- stir_speed
- purpose
@@ -1203,13 +1243,16 @@ virtual_heatchill:
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
+ message:
+ type: string
return_info:
type: string
success:
type: boolean
required:
- - return_info
- success
+ - message
+ - return_info
title: HeatChill_Result
type: object
required:
@@ -2048,13 +2091,18 @@ virtual_pump:
volume: volume
goal_default:
amount: ''
+ event: ''
+ flowrate: 0.0
from_vessel: ''
+ rate_spec: ''
rinsing_repeats: 0
rinsing_solvent: ''
rinsing_volume: 0.0
solid: false
+ through: ''
time: 0.0
to_vessel: ''
+ transfer_flowrate: 0.0
viscous: false
volume: 0.0
handles: []
@@ -2112,8 +2160,14 @@ virtual_pump:
properties:
amount:
type: string
+ event:
+ type: string
+ flowrate:
+ type: number
from_vessel:
type: string
+ rate_spec:
+ type: string
rinsing_repeats:
maximum: 2147483647
minimum: -2147483648
@@ -2124,10 +2178,14 @@ virtual_pump:
type: number
solid:
type: boolean
+ through:
+ type: string
time:
type: number
to_vessel:
type: string
+ transfer_flowrate:
+ type: number
viscous:
type: boolean
volume:
@@ -2143,6 +2201,11 @@ virtual_pump:
- rinsing_volume
- rinsing_repeats
- solid
+ - flowrate
+ - transfer_flowrate
+ - rate_spec
+ - event
+ - through
title: PumpTransfer_Goal
type: object
result:
@@ -2325,6 +2388,7 @@ virtual_rotavap:
vessel: vessel
goal_default:
pressure: 0.0
+ solvent: ''
stir_speed: 0.0
temp: 0.0
time: 0.0
@@ -2385,6 +2449,8 @@ virtual_rotavap:
properties:
pressure:
type: number
+ solvent:
+ type: string
stir_speed:
type: number
temp:
@@ -2399,6 +2465,7 @@ virtual_rotavap:
- temp
- time
- stir_speed
+ - solvent
title: Evaporate_Goal
type: object
result:
@@ -2641,6 +2708,7 @@ virtual_separator:
goal_default:
from_vessel: ''
product_phase: ''
+ product_vessel: ''
purpose: ''
repeats: 0
separation_vessel: ''
@@ -2651,7 +2719,10 @@ virtual_separator:
stir_time: 0.0
through: ''
to_vessel: ''
+ vessel: ''
+ volume: ''
waste_phase_to_vessel: ''
+ waste_vessel: ''
handles: []
result:
message: message
@@ -2710,6 +2781,8 @@ virtual_separator:
type: string
product_phase:
type: string
+ product_vessel:
+ type: string
purpose:
type: string
repeats:
@@ -2732,8 +2805,14 @@ virtual_separator:
type: string
to_vessel:
type: string
+ vessel:
+ type: string
+ volume:
+ type: string
waste_phase_to_vessel:
type: string
+ waste_vessel:
+ type: string
required:
- purpose
- product_phase
@@ -2748,18 +2827,25 @@ virtual_separator:
- stir_time
- stir_speed
- settling_time
+ - vessel
+ - volume
+ - product_vessel
+ - waste_vessel
title: Separate_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
+ message:
+ type: string
return_info:
type: string
success:
type: boolean
required:
- - return_info
- success
+ - message
+ - return_info
title: Separate_Result
type: object
required:
@@ -3149,6 +3235,180 @@ virtual_solenoid_valve:
- valve_position
- state
type: object
+virtual_solid_dispenser:
+ class:
+ action_value_mappings:
+ add_solid:
+ feedback:
+ current_status: status
+ progress: progress
+ goal:
+ mass: mass
+ mol: mol
+ purpose: purpose
+ reagent: reagent
+ vessel: vessel
+ goal_default:
+ mass: ''
+ mol: ''
+ purpose: ''
+ reagent: ''
+ vessel: ''
+ handles: []
+ result:
+ message: message
+ return_info: return_info
+ success: success
+ schema:
+ description: ROS Action AddSolid 的 JSON Schema
+ properties:
+ feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
+ properties:
+ current_status:
+ type: string
+ progress:
+ type: number
+ required:
+ - current_status
+ - progress
+ title: AddSolid_Feedback
+ type: object
+ goal:
+ description: Action 目标 - 从客户端发送到服务器
+ properties:
+ mass:
+ type: string
+ mol:
+ type: string
+ purpose:
+ type: string
+ reagent:
+ type: string
+ vessel:
+ type: string
+ required:
+ - vessel
+ - reagent
+ - mass
+ - mol
+ - purpose
+ title: AddSolid_Goal
+ type: object
+ result:
+ description: Action 结果 - 完成后从服务器发送到客户端
+ properties:
+ message:
+ type: string
+ return_info:
+ type: string
+ success:
+ type: boolean
+ required:
+ - success
+ - message
+ - return_info
+ title: AddSolid_Result
+ type: object
+ required:
+ - goal
+ title: AddSolid
+ type: object
+ type: AddSolid
+ auto-add_solid:
+ feedback: {}
+ goal: {}
+ goal_default:
+ mass: ''
+ mol: ''
+ purpose: ''
+ reagent: ''
+ vessel: main_reactor
+ handles: []
+ result: {}
+ schema:
+ description: add_solid的参数schema
+ type: object
+ type: UniLabJsonCommandAsync
+ auto-cleanup:
+ feedback: {}
+ goal: {}
+ goal_default: {}
+ handles: []
+ result: {}
+ schema:
+ description: cleanup的参数schema
+ type: object
+ type: UniLabJsonCommandAsync
+ auto-initialize:
+ feedback: {}
+ goal: {}
+ goal_default: {}
+ handles: []
+ result: {}
+ schema:
+ description: initialize的参数schema
+ type: object
+ type: UniLabJsonCommandAsync
+ module: unilabos.devices.virtual.virtual_solid_dispenser:VirtualSolidDispenser
+ status_types:
+ current_reagent: str
+ dispensed_amount: float
+ status: str
+ total_operations: int
+ type: python
+ description: Virtual Solid Dispenser for Add Protocol Solid Reagents
+ handles:
+ - data_key: SolidIn
+ data_source: handle
+ data_type: resource
+ description: 固体试剂进料口
+ handler_key: SolidIn
+ io_type: target
+ label: SolidIn
+ side: WEST
+ - data_key: SolidOut
+ data_source: executor
+ data_type: resource
+ description: 固体试剂出料口
+ handler_key: SolidOut
+ io_type: source
+ label: SolidOut
+ side: EAST
+ icon: ''
+ init_param_schema:
+ config:
+ properties:
+ config:
+ type: object
+ device_id:
+ type: string
+ max_capacity:
+ default: 100.0
+ description: 最大加样容量 (g)
+ type: number
+ precision:
+ default: 0.001
+ description: 加样精度 (g)
+ type: number
+ required: []
+ type: object
+ data:
+ properties:
+ current_reagent:
+ type: string
+ dispensed_amount:
+ type: number
+ status:
+ type: string
+ total_operations:
+ type: integer
+ required:
+ - status
+ - current_reagent
+ - dispensed_amount
+ - total_operations
+ type: object
virtual_stirrer:
class:
action_value_mappings:
@@ -3183,7 +3443,6 @@ virtual_stirrer:
properties:
feedback: {}
goal:
-
properties: {}
required: []
type: object
@@ -3356,9 +3615,13 @@ virtual_stirrer:
stir_speed: stir_speed
stir_time: stir_time
goal_default:
+ event: ''
settling_time: 0.0
stir_speed: 0.0
stir_time: 0.0
+ time: ''
+ time_spec: ''
+ vessel: ''
handles: []
result:
success: success
@@ -3377,13 +3640,25 @@ virtual_stirrer:
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
+ event:
+ type: string
settling_time:
type: number
stir_speed:
type: number
stir_time:
type: number
+ time:
+ type: string
+ time_spec:
+ type: string
+ vessel:
+ type: string
required:
+ - vessel
+ - time
+ - event
+ - time_spec
- stir_time
- stir_speed
- settling_time
@@ -3392,13 +3667,16 @@ virtual_stirrer:
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
+ message:
+ type: string
return_info:
type: string
success:
type: boolean
required:
- - return_info
- success
+ - message
+ - return_info
title: Stir_Result
type: object
required:
@@ -4367,125 +4645,3 @@ virtual_vacuum_pump:
required:
- status
type: object
-virtual_solid_dispenser:
- class:
- action_value_mappings:
- auto-cleanup:
- feedback: {}
- goal: {}
- goal_default: {}
- handles: []
- result: {}
- schema:
- description: cleanup的参数schema
- type: object
- type: UniLabJsonCommandAsync
- auto-initialize:
- feedback: {}
- goal: {}
- goal_default: {}
- handles: []
- result: {}
- schema:
- description: initialize的参数schema
- type: object
- type: UniLabJsonCommandAsync
- auto-add_solid:
- feedback: {}
- goal: {}
- goal_default:
- vessel: main_reactor
- reagent: ''
- mass: ''
- mol: ''
- purpose: ''
- handles: []
- result: {}
- schema:
- description: add_solid的参数schema
- type: object
- type: UniLabJsonCommandAsync
- add_solid:
- feedback:
- current_status: status
- progress: progress
- goal:
- vessel: vessel
- reagent: reagent
- mass: mass
- mol: mol
- purpose: purpose
- goal_default:
- vessel: 'main_reactor'
- reagent: ''
- mass: ''
- mol: ''
- purpose: ''
- handles: []
- result:
- message: message
- success: success
- return_info: return_info
- schema:
- description: ROS Action AddSolid 的 JSON Schema
- type: object
- type: AddSolid
- module: unilabos.devices.virtual.virtual_solid_dispenser:VirtualSolidDispenser
- status_types:
- status: str
- current_reagent: str
- dispensed_amount: float
- total_operations: int
- type: python
- description: Virtual Solid Dispenser for Add Protocol Solid Reagents
- handles:
- - data_key: SolidIn
- data_source: handle
- data_type: resource
- description: 固体试剂进料口
- handler_key: SolidIn
- io_type: target
- label: SolidIn
- side: WEST
- - data_key: SolidOut
- data_source: executor
- data_type: resource
- description: 固体试剂出料口
- handler_key: SolidOut
- io_type: source
- label: SolidOut
- side: EAST
- icon: ''
- init_param_schema:
- config:
- properties:
- config:
- type: object
- device_id:
- type: string
- max_capacity:
- type: number
- default: 100.0
- description: 最大加样容量 (g)
- precision:
- type: number
- default: 0.001
- description: 加样精度 (g)
- required: []
- type: object
- data:
- properties:
- status:
- type: string
- current_reagent:
- type: string
- dispensed_amount:
- type: number
- total_operations:
- type: integer
- required:
- - status
- - current_reagent
- - dispensed_amount
- - total_operations
- type: object
diff --git a/unilabos/registry/devices/work_station.yaml b/unilabos/registry/devices/work_station.yaml
index 3c4e3b671..91ea9bddf 100644
--- a/unilabos/registry/devices/work_station.yaml
+++ b/unilabos/registry/devices/work_station.yaml
@@ -257,8 +257,13 @@ workstation:
volume: volume
goal_default:
amount: ''
+ equiv: ''
+ event: ''
mass: 0.0
+ mol: ''
purpose: ''
+ rate_spec: ''
+ ratio: ''
reagent: ''
stir: false
stir_speed: 0.0
@@ -305,10 +310,20 @@ workstation:
properties:
amount:
type: string
+ equiv:
+ type: string
+ event:
+ type: string
mass:
type: number
+ mol:
+ type: string
purpose:
type: string
+ rate_spec:
+ type: string
+ ratio:
+ type: string
reagent:
type: string
stir:
@@ -334,6 +349,11 @@ workstation:
- stir_speed
- viscous
- purpose
+ - event
+ - mol
+ - rate_spec
+ - equiv
+ - ratio
title: Add_Goal
type: object
result:
@@ -356,6 +376,85 @@ workstation:
title: Add
type: object
type: Add
+ AdjustPHProtocol:
+ feedback: {}
+ goal:
+ ph_value: ph_value
+ reagent: reagent
+ vessel: vessel
+ goal_default:
+ ph_value: 0.0
+ reagent: ''
+ vessel: ''
+ handles:
+ input:
+ - data_key: vessel
+ data_source: handle
+ data_type: resource
+ handler_key: Vessel
+ label: Vessel
+ - data_key: reagent
+ data_source: handle
+ data_type: resource
+ handler_key: reagent
+ label: Reagent
+ output:
+ - data_key: vessel
+ data_source: executor
+ data_type: resource
+ handler_key: VesselOut
+ label: Vessel
+ result: {}
+ schema:
+ description: ROS Action AdjustPH 的 JSON Schema
+ properties:
+ feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
+ properties:
+ progress:
+ type: number
+ status:
+ type: string
+ required:
+ - status
+ - progress
+ title: AdjustPH_Feedback
+ type: object
+ goal:
+ description: Action 目标 - 从客户端发送到服务器
+ properties:
+ ph_value:
+ type: number
+ reagent:
+ type: string
+ vessel:
+ type: string
+ required:
+ - vessel
+ - ph_value
+ - reagent
+ title: AdjustPH_Goal
+ type: object
+ result:
+ description: Action 结果 - 完成后从服务器发送到客户端
+ properties:
+ message:
+ type: string
+ return_info:
+ type: string
+ success:
+ type: boolean
+ required:
+ - success
+ - message
+ - return_info
+ title: AdjustPH_Result
+ type: object
+ required:
+ - goal
+ title: AdjustPH
+ type: object
+ type: AdjustPH
CentrifugeProtocol:
feedback: {}
goal:
@@ -664,6 +763,9 @@ workstation:
volume: volume
goal_default:
amount: ''
+ mass: ''
+ mol: ''
+ reagent: ''
solvent: ''
stir_speed: 0.0
temp: 0.0
@@ -709,6 +811,12 @@ workstation:
properties:
amount:
type: string
+ mass:
+ type: string
+ mol:
+ type: string
+ reagent:
+ type: string
solvent:
type: string
stir_speed:
@@ -729,6 +837,9 @@ workstation:
- temp
- time
- stir_speed
+ - mass
+ - mol
+ - reagent
title: Dissolve_Goal
type: object
result:
@@ -751,6 +862,75 @@ workstation:
title: Dissolve
type: object
type: Dissolve
+ DryProtocol:
+ feedback: {}
+ goal:
+ compound: compound
+ vessel: vessel
+ goal_default:
+ compound: ''
+ vessel: ''
+ handles:
+ input:
+ - data_key: vessel
+ data_source: handle
+ data_type: resource
+ handler_key: Vessel
+ label: Vessel
+ output:
+ - data_key: vessel
+ data_source: executor
+ data_type: resource
+ handler_key: VesselOut
+ label: Vessel
+ result: {}
+ schema:
+ description: ROS Action Dry 的 JSON Schema
+ properties:
+ feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
+ properties:
+ progress:
+ type: number
+ status:
+ type: string
+ required:
+ - status
+ - progress
+ title: Dry_Feedback
+ type: object
+ goal:
+ description: Action 目标 - 从客户端发送到服务器
+ properties:
+ compound:
+ type: string
+ vessel:
+ type: string
+ required:
+ - compound
+ - vessel
+ title: Dry_Goal
+ type: object
+ result:
+ description: Action 结果 - 完成后从服务器发送到客户端
+ properties:
+ message:
+ type: string
+ return_info:
+ type: string
+ success:
+ type: boolean
+ required:
+ - success
+ - message
+ - return_info
+ title: Dry_Result
+ type: object
+ required:
+ - goal
+ title: Dry
+ type: object
+ type: Dry
EvacuateAndRefillProtocol:
feedback: {}
goal:
@@ -852,19 +1032,19 @@ workstation:
EvaporateProtocol:
feedback: {}
goal:
- vessel: vessel
pressure: pressure
+ solvent: solvent
+ stir_speed: stir_speed
temp: temp
time: time
- stir_speed: stir_speed
- solvent: solvent
+ vessel: vessel
goal_default:
- vessel: ''
- pressure: 0.1
- temp: 60.0
- time: 1800.0
- stir_speed: 100.0
+ pressure: 0.0
solvent: ''
+ stir_speed: 0.0
+ temp: 0.0
+ time: 0.0
+ vessel: ''
handles:
input:
- data_key: vessel
@@ -882,36 +1062,86 @@ workstation:
schema:
description: ROS Action Evaporate 的 JSON Schema
properties:
- goal:
- description: Action 目标
+ feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
- vessel:
+ current_device:
+ type: string
+ status:
type: string
- description: 蒸发容器名称
+ time_remaining:
+ properties:
+ nanosec:
+ maximum: 4294967295
+ minimum: 0
+ type: integer
+ sec:
+ maximum: 2147483647
+ minimum: -2147483648
+ type: integer
+ required:
+ - sec
+ - nanosec
+ title: Duration
+ type: object
+ time_spent:
+ properties:
+ nanosec:
+ maximum: 4294967295
+ minimum: 0
+ type: integer
+ sec:
+ maximum: 2147483647
+ minimum: -2147483648
+ type: integer
+ required:
+ - sec
+ - nanosec
+ title: Duration
+ type: object
+ required:
+ - status
+ - current_device
+ - time_spent
+ - time_remaining
+ title: Evaporate_Feedback
+ type: object
+ goal:
+ description: Action 目标 - 从客户端发送到服务器
+ properties:
pressure:
type: number
- default: 0.1
- description: 真空度 (bar)
+ solvent:
+ type: string
+ stir_speed:
+ type: number
temp:
type: number
- default: 60.0
- description: 加热温度 (°C)
time:
type: number
- default: 1800.0
- description: 蒸发时间 (秒)
- stir_speed:
- type: number
- default: 100.0
- description: 旋转速度 (RPM)
- solvent:
+ vessel:
type: string
- default: ''
- description: 溶剂名称
required:
- vessel
+ - pressure
+ - temp
+ - time
+ - stir_speed
+ - solvent
title: Evaporate_Goal
type: object
+ result:
+ description: Action 结果 - 完成后从服务器发送到客户端
+ properties:
+ return_info:
+ type: string
+ success:
+ type: boolean
+ required:
+ - return_info
+ - success
+ title: Evaporate_Result
+ type: object
required:
- goal
title: Evaporate
@@ -920,20 +1150,20 @@ workstation:
FilterProtocol:
feedback: {}
goal:
- vessel: vessel
+ continue_heatchill: continue_heatchill
filtrate_vessel: filtrate_vessel
stir: stir
stir_speed: stir_speed
temp: temp
- continue_heatchill: continue_heatchill
+ vessel: vessel
volume: volume
goal_default:
- vessel: ''
+ continue_heatchill: false
filtrate_vessel: ''
stir: false
stir_speed: 0.0
- temp: 25.0
- continue_heatchill: false
+ temp: 0.0
+ vessel: ''
volume: 0.0
handles:
input:
@@ -983,35 +1213,28 @@ workstation:
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
- vessel:
- type: string
- description: 过滤容器名称
+ continue_heatchill:
+ type: boolean
filtrate_vessel:
type: string
- default: ''
- description: 滤液容器名称(可选)
stir:
type: boolean
- default: false
- description: 是否搅拌
stir_speed:
type: number
- default: 0.0
- description: 搅拌速度
temp:
type: number
- default: 25.0
- description: 温度
- continue_heatchill:
- type: boolean
- default: false
- description: 是否继续加热冷却
+ vessel:
+ type: string
volume:
type: number
- default: 0.0
- description: 过滤体积
required:
- vessel
+ - filtrate_vessel
+ - stir
+ - stir_speed
+ - temp
+ - continue_heatchill
+ - volume
title: Filter_Goal
type: object
result:
@@ -1148,27 +1371,27 @@ workstation:
HeatChillProtocol:
feedback: {}
goal:
- vessel: vessel
- temp: temp
- time: time
- temp_spec: temp_spec
- time_spec: time_spec
pressure: pressure
+ purpose: purpose
reflux_solvent: reflux_solvent
stir: stir
stir_speed: stir_speed
- purpose: purpose
+ temp: temp
+ temp_spec: temp_spec
+ time: time
+ time_spec: time_spec
+ vessel: vessel
goal_default:
- vessel: ''
- temp: 25.0
- time: 300.0
- temp_spec: ''
- time_spec: ''
pressure: ''
+ purpose: ''
reflux_solvent: ''
stir: false
- stir_speed: 300.0
- purpose: ''
+ stir_speed: 0.0
+ temp: 0.0
+ temp_spec: ''
+ time: 0.0
+ time_spec: ''
+ vessel: ''
handles:
input:
- data_key: vessel
@@ -1187,7 +1410,7 @@ workstation:
description: ROS Action HeatChill 的 JSON Schema
properties:
feedback:
- description: Action 反馈
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -1196,53 +1419,43 @@ workstation:
title: HeatChill_Feedback
type: object
goal:
- description: Action 目标
+ description: Action 目标 - 从客户端发送到服务器
properties:
- vessel:
- type: string
- description: 加热容器名称
- temp:
- type: number
- default: 25.0
- description: 目标温度
- time:
- type: number
- default: 300.0
- description: 加热时间
- temp_spec:
- type: string
- default: ''
- description: 温度规格
- time_spec:
- type: string
- default: ''
- description: 时间规格
pressure:
type: string
- default: ''
- description: 压力规格
+ purpose:
+ type: string
reflux_solvent:
type: string
- default: ''
- description: 回流溶剂名称
stir:
type: boolean
- default: false
- description: 是否搅拌
stir_speed:
type: number
- default: 300.0
- description: 搅拌速度
- purpose:
+ temp:
+ type: number
+ temp_spec:
+ type: string
+ time:
+ type: number
+ time_spec:
+ type: string
+ vessel:
type: string
- default: ''
- description: 操作目的
required:
- vessel
+ - temp
+ - time
+ - temp_spec
+ - time_spec
+ - pressure
+ - reflux_solvent
+ - stir
+ - stir_speed
+ - purpose
title: HeatChill_Goal
type: object
result:
- description: Action 结果
+ description: Action 结果 - 完成后从服务器发送到客户端
properties:
message:
type: string
@@ -1387,40 +1600,114 @@ workstation:
title: HeatChillStop
type: object
type: HeatChillStop
+ HydrogenateProtocol:
+ feedback: {}
+ goal:
+ temp: temp
+ time: time
+ vessel: vessel
+ goal_default:
+ temp: ''
+ time: ''
+ vessel: ''
+ handles:
+ input:
+ - data_key: vessel
+ data_source: handle
+ data_type: resource
+ handler_key: Vessel
+ label: Vessel
+ output:
+ - data_key: vessel
+ data_source: executor
+ data_type: resource
+ handler_key: VesselOut
+ label: Vessel
+ result: {}
+ schema:
+ description: ROS Action Hydrogenate 的 JSON Schema
+ properties:
+ feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
+ properties:
+ progress:
+ type: number
+ status:
+ type: string
+ required:
+ - status
+ - progress
+ title: Hydrogenate_Feedback
+ type: object
+ goal:
+ description: Action 目标 - 从客户端发送到服务器
+ properties:
+ temp:
+ type: string
+ time:
+ type: string
+ vessel:
+ type: string
+ required:
+ - temp
+ - time
+ - vessel
+ title: Hydrogenate_Goal
+ type: object
+ result:
+ description: Action 结果 - 完成后从服务器发送到客户端
+ properties:
+ message:
+ type: string
+ return_info:
+ type: string
+ success:
+ type: boolean
+ required:
+ - success
+ - message
+ - return_info
+ title: Hydrogenate_Result
+ type: object
+ required:
+ - goal
+ title: Hydrogenate
+ type: object
+ type: Hydrogenate
PumpTransferProtocol:
feedback: {}
goal:
amount: amount
+ event: event
+ flowrate: flowrate
from_vessel: from_vessel
+ rate_spec: rate_spec
rinsing_repeats: rinsing_repeats
rinsing_solvent: rinsing_solvent
rinsing_volume: rinsing_volume
solid: solid
+ through: through
time: time
to_vessel: to_vessel
+ transfer_flowrate: transfer_flowrate
viscous: viscous
volume: volume
- flowrate: flowrate
- transfer_flowrate: transfer_flowrate
- rate_spec: rate_spec
- event: event
- through: through
goal_default:
amount: ''
+ event: ''
+ flowrate: 0.0
from_vessel: ''
+ rate_spec: ''
rinsing_repeats: 0
rinsing_solvent: ''
rinsing_volume: 0.0
solid: false
+ through: ''
time: 0.0
to_vessel: ''
+ transfer_flowrate: 0.0
viscous: false
volume: 0.0
- flowrate: 0.0
- transfer_flowrate: 0.0
- rate_spec: ''
- event: ''
- through: ''
handles:
input:
- data_key: vessel
@@ -1451,7 +1738,7 @@ workstation:
label: To Vessel
result: {}
schema:
- description: ROS Action PumpTransfer 的 JSON Schema(兼容增强版)
+ description: ROS Action PumpTransfer 的 JSON Schema
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
@@ -1461,8 +1748,34 @@ workstation:
status:
type: string
time_remaining:
+ properties:
+ nanosec:
+ maximum: 4294967295
+ minimum: 0
+ type: integer
+ sec:
+ maximum: 2147483647
+ minimum: -2147483648
+ type: integer
+ required:
+ - sec
+ - nanosec
+ title: Duration
type: object
time_spent:
+ properties:
+ nanosec:
+ maximum: 4294967295
+ minimum: 0
+ type: integer
+ sec:
+ maximum: 2147483647
+ minimum: -2147483648
+ type: integer
+ required:
+ - sec
+ - nanosec
+ title: Duration
type: object
required:
- status
@@ -1476,37 +1789,52 @@ workstation:
properties:
amount:
type: string
+ event:
+ type: string
+ flowrate:
+ type: number
from_vessel:
type: string
- to_vessel:
+ rate_spec:
type: string
- volume:
- type: number
- time:
- type: number
- viscous:
- type: boolean
+ rinsing_repeats:
+ maximum: 2147483647
+ minimum: -2147483648
+ type: integer
rinsing_solvent:
type: string
rinsing_volume:
type: number
- rinsing_repeats:
- type: integer
solid:
type: boolean
- flowrate:
+ through:
+ type: string
+ time:
type: number
+ to_vessel:
+ type: string
transfer_flowrate:
type: number
- rate_spec:
- type: string
- event:
- type: string
- through:
- type: string
+ viscous:
+ type: boolean
+ volume:
+ type: number
required:
- from_vessel
- to_vessel
+ - volume
+ - amount
+ - time
+ - viscous
+ - rinsing_solvent
+ - rinsing_volume
+ - rinsing_repeats
+ - solid
+ - flowrate
+ - transfer_flowrate
+ - rate_spec
+ - event
+ - through
title: PumpTransfer_Goal
type: object
result:
@@ -1526,30 +1854,189 @@ workstation:
title: PumpTransfer
type: object
type: PumpTransfer
- RunColumnProtocol:
+ RecrystallizeProtocol:
feedback: {}
goal:
- column: column
- from_vessel: from_vessel
- to_vessel: to_vessel
+ ratio: ratio
+ solvent1: solvent1
+ solvent2: solvent2
+ vessel: vessel
+ volume: volume
goal_default:
- column: ''
- from_vessel: ''
- to_vessel: ''
+ ratio: ''
+ solvent1: ''
+ solvent2: ''
+ vessel: ''
+ volume: 0.0
handles:
input:
- data_key: vessel
data_source: handle
data_type: resource
- handler_key: FromVessel
- label: From Vessel
- - data_key: vessel
- data_source: executor
- data_type: resource
- handler_key: ToVessel
- label: To Vessel
- output:
- - data_key: vessel
+ handler_key: Vessel
+ label: Vessel
+ - data_key: solvent1
+ data_source: handle
+ data_type: resource
+ handler_key: solvent1
+ label: Solvent 1
+ - data_key: solvent2
+ data_source: handle
+ data_type: resource
+ handler_key: solvent2
+ label: Solvent 2
+ output:
+ - data_key: vessel
+ data_source: executor
+ data_type: resource
+ handler_key: VesselOut
+ label: Vessel
+ result: {}
+ schema:
+ description: ROS Action Recrystallize 的 JSON Schema
+ properties:
+ feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
+ properties:
+ progress:
+ type: number
+ status:
+ type: string
+ required:
+ - status
+ - progress
+ title: Recrystallize_Feedback
+ type: object
+ goal:
+ description: Action 目标 - 从客户端发送到服务器
+ properties:
+ ratio:
+ type: string
+ solvent1:
+ type: string
+ solvent2:
+ type: string
+ vessel:
+ type: string
+ volume:
+ type: number
+ required:
+ - ratio
+ - solvent1
+ - solvent2
+ - vessel
+ - volume
+ title: Recrystallize_Goal
+ type: object
+ result:
+ description: Action 结果 - 完成后从服务器发送到客户端
+ properties:
+ message:
+ type: string
+ return_info:
+ type: string
+ success:
+ type: boolean
+ required:
+ - success
+ - message
+ - return_info
+ title: Recrystallize_Result
+ type: object
+ required:
+ - goal
+ title: Recrystallize
+ type: object
+ type: Recrystallize
+ ResetHandlingProtocol:
+ feedback: {}
+ goal:
+ solvent: solvent
+ goal_default:
+ solvent: ''
+ handles:
+ input:
+ - data_key: solvent
+ data_source: handle
+ data_type: resource
+ handler_key: solvent
+ label: Solvent
+ output: []
+ result: {}
+ schema:
+ description: ROS Action ResetHandling 的 JSON Schema
+ properties:
+ feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
+ properties:
+ progress:
+ type: number
+ status:
+ type: string
+ required:
+ - status
+ - progress
+ title: ResetHandling_Feedback
+ type: object
+ goal:
+ description: Action 目标 - 从客户端发送到服务器
+ properties:
+ solvent:
+ type: string
+ required:
+ - solvent
+ title: ResetHandling_Goal
+ type: object
+ result:
+ description: Action 结果 - 完成后从服务器发送到客户端
+ properties:
+ message:
+ type: string
+ return_info:
+ type: string
+ success:
+ type: boolean
+ required:
+ - success
+ - message
+ - return_info
+ title: ResetHandling_Result
+ type: object
+ required:
+ - goal
+ title: ResetHandling
+ type: object
+ type: ResetHandling
+ RunColumnProtocol:
+ feedback: {}
+ goal:
+ column: column
+ from_vessel: from_vessel
+ to_vessel: to_vessel
+ goal_default:
+ column: ''
+ from_vessel: ''
+ pct1: ''
+ pct2: ''
+ ratio: ''
+ rf: ''
+ solvent1: ''
+ solvent2: ''
+ to_vessel: ''
+ handles:
+ input:
+ - data_key: vessel
+ data_source: handle
+ data_type: resource
+ handler_key: FromVessel
+ label: From Vessel
+ - data_key: vessel
+ data_source: executor
+ data_type: resource
+ handler_key: ToVessel
+ label: To Vessel
+ output:
+ - data_key: vessel
data_source: handle
data_type: resource
handler_key: FromVesselOut
@@ -1582,12 +2069,30 @@ workstation:
type: string
from_vessel:
type: string
+ pct1:
+ type: string
+ pct2:
+ type: string
+ ratio:
+ type: string
+ rf:
+ type: string
+ solvent1:
+ type: string
+ solvent2:
+ type: string
to_vessel:
type: string
required:
- from_vessel
- to_vessel
- column
+ - rf
+ - pct1
+ - pct2
+ - solvent1
+ - solvent2
+ - ratio
title: RunColumn_Goal
type: object
result:
@@ -1629,6 +2134,7 @@ workstation:
goal_default:
from_vessel: ''
product_phase: ''
+ product_vessel: ''
purpose: ''
repeats: 0
separation_vessel: ''
@@ -1639,7 +2145,10 @@ workstation:
stir_time: 0.0
through: ''
to_vessel: ''
+ vessel: ''
+ volume: ''
waste_phase_to_vessel: ''
+ waste_vessel: ''
handles:
input:
- data_key: vessel
@@ -1723,6 +2232,8 @@ workstation:
type: string
product_phase:
type: string
+ product_vessel:
+ type: string
purpose:
type: string
repeats:
@@ -1745,8 +2256,14 @@ workstation:
type: string
to_vessel:
type: string
+ vessel:
+ type: string
+ volume:
+ type: string
waste_phase_to_vessel:
type: string
+ waste_vessel:
+ type: string
required:
- purpose
- product_phase
@@ -1761,18 +2278,25 @@ workstation:
- stir_time
- stir_speed
- settling_time
+ - vessel
+ - volume
+ - product_vessel
+ - waste_vessel
title: Separate_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
+ message:
+ type: string
return_info:
type: string
success:
type: boolean
required:
- - return_info
- success
+ - message
+ - return_info
title: Separate_Result
type: object
required:
@@ -1860,21 +2384,21 @@ workstation:
StirProtocol:
feedback: {}
goal:
- vessel: vessel
- time: time
event: event
- time_spec: time_spec
- stir_time: stir_time
- stir_speed: stir_speed
settling_time: settling_time
+ stir_speed: stir_speed
+ stir_time: stir_time
+ time: time
+ time_spec: time_spec
+ vessel: vessel
goal_default:
- vessel: ''
- time: '5 min'
event: ''
+ settling_time: 0.0
+ stir_speed: 0.0
+ stir_time: 0.0
+ time: ''
time_spec: ''
- stir_time: 300.0
- stir_speed: 200.0
- settling_time: 60.0
+ vessel: ''
handles:
input:
- data_key: vessel
@@ -1893,7 +2417,7 @@ workstation:
description: ROS Action Stir 的 JSON Schema
properties:
feedback:
- description: Action 反馈
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -1902,41 +2426,34 @@ workstation:
title: Stir_Feedback
type: object
goal:
- description: Action 目标
+ description: Action 目标 - 从客户端发送到服务器
properties:
- vessel:
- type: string
- description: 搅拌容器名称
- time:
- type: string
- default: '5 min'
- description: 搅拌时间
event:
type: string
- default: ''
- description: 事件标识
- time_spec:
- type: string
- default: ''
- description: 时间规格
- stir_time:
+ settling_time:
type: number
- default: 300.0
- description: 搅拌时间(秒)
stir_speed:
type: number
- default: 200.0
- description: 搅拌速度
- settling_time:
+ stir_time:
type: number
- default: 60.0
- description: 沉降时间
+ time:
+ type: string
+ time_spec:
+ type: string
+ vessel:
+ type: string
required:
- vessel
+ - time
+ - event
+ - time_spec
+ - stir_time
+ - stir_speed
+ - settling_time
title: Stir_Goal
type: object
result:
- description: Action 结果
+ description: Action 结果 - 完成后从服务器发送到客户端
properties:
message:
type: string
@@ -2002,13 +2519,16 @@ workstation:
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
+ message:
+ type: string
return_info:
type: string
success:
type: boolean
required:
- - return_info
- success
+ - message
+ - return_info
title: StopStir_Result
type: object
required:
@@ -2148,25 +2668,25 @@ workstation:
WashSolidProtocol:
feedback: {}
goal:
- vessel: vessel
- solvent: solvent
- volume: volume
filtrate_vessel: filtrate_vessel
- temp: temp
+ repeats: repeats
+ solvent: solvent
stir: stir
stir_speed: stir_speed
+ temp: temp
time: time
- repeats: repeats
+ vessel: vessel
+ volume: volume
goal_default:
- vessel: ''
- solvent: ''
- volume: 0.0
filtrate_vessel: ''
- temp: 25.0
+ repeats: 0
+ solvent: ''
stir: false
stir_speed: 0.0
+ temp: 0.0
time: 0.0
- repeats: 1
+ vessel: ''
+ volume: 0.0
handles:
input:
- data_key: vessel
@@ -2214,45 +2734,36 @@ workstation:
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
- vessel:
+ filtrate_vessel:
type: string
- description: 装有固体的容器名称
+ repeats:
+ maximum: 2147483647
+ minimum: -2147483648
+ type: integer
solvent:
type: string
- description: 清洗溶剂名称
- volume:
+ stir:
+ type: boolean
+ stir_speed:
type: number
- description: 清洗溶剂体积
- filtrate_vessel:
- type: string
- default: ''
- description: 滤液收集容器(可选)
temp:
type: number
- default: 25.0
- description: 清洗温度
- stir:
- type: boolean
- default: false
- description: 是否搅拌
- stir_speed:
- type: number
- default: 0.0
- description: 搅拌速度
time:
type: number
- default: 0.0
- description: 清洗时间
- repeats:
- type: integer
- default: 1
- minimum: 1
- maximum: 10
- description: 重复次数
+ vessel:
+ type: string
+ volume:
+ type: number
required:
- vessel
- solvent
- volume
+ - filtrate_vessel
+ - temp
+ - stir
+ - stir_speed
+ - time
+ - repeats
title: WashSolid_Goal
type: object
result:
@@ -2363,381 +2874,6 @@ workstation:
title: initialize_device参数
type: object
type: UniLabJsonCommand
- AdjustPHProtocol:
- feedback: {}
- goal:
- vessel: vessel
- ph_value: ph_value
- reagent: reagent
- goal_default:
- vessel: ''
- ph_value: 7.0
- reagent: ''
- handles:
- input:
- - data_key: vessel
- data_source: handle
- data_type: resource
- handler_key: Vessel
- label: Vessel
- - data_key: reagent
- data_source: handle
- data_type: resource
- handler_key: reagent
- label: Reagent
- output:
- - data_key: vessel
- data_source: executor
- data_type: resource
- handler_key: VesselOut
- label: Vessel
- result: {}
- schema:
- description: ROS Action AdjustPH 的 JSON Schema
- properties:
- feedback:
- description: Action 反馈 - 执行过程中从服务器发送到客户端
- properties:
- status:
- type: string
- progress:
- type: number
- required:
- - status
- - progress
- title: AdjustPH_Feedback
- type: object
- goal:
- description: Action 目标 - 从客户端发送到服务器
- properties:
- vessel:
- type: string
- ph_value:
- type: number
- reagent:
- type: string
- required:
- - vessel
- - ph_value
- - reagent
- title: AdjustPH_Goal
- type: object
- result:
- description: Action 结果 - 完成后从服务器发送到客户端
- properties:
- message:
- type: string
- return_info:
- type: string
- success:
- type: boolean
- required:
- - success
- - message
- - return_info
- title: AdjustPH_Result
- type: object
- required:
- - goal
- title: AdjustPH
- type: object
- type: AdjustPH
- ResetHandlingProtocol:
- feedback: {}
- goal:
- solvent: solvent
- goal_default:
- solvent: ''
- handles:
- input:
- - data_key: solvent
- data_source: handle
- data_type: resource
- handler_key: solvent
- label: Solvent
- output: []
- result: {}
- schema:
- description: ROS Action ResetHandling 的 JSON Schema
- properties:
- feedback:
- description: Action 反馈 - 执行过程中从服务器发送到客户端
- properties:
- status:
- type: string
- progress:
- type: number
- required:
- - status
- - progress
- title: ResetHandling_Feedback
- type: object
- goal:
- description: Action 目标 - 从客户端发送到服务器
- properties:
- solvent:
- type: string
- required:
- - solvent
- title: ResetHandling_Goal
- type: object
- result:
- description: Action 结果 - 完成后从服务器发送到客户端
- properties:
- message:
- type: string
- return_info:
- type: string
- success:
- type: boolean
- required:
- - success
- - message
- - return_info
- title: ResetHandling_Result
- type: object
- required:
- - goal
- title: ResetHandling
- type: object
- type: ResetHandling
- DryProtocol:
- feedback: {}
- goal:
- compound: compound
- vessel: vessel
- goal_default:
- compound: ''
- vessel: ''
- handles:
- input:
- - data_key: vessel
- data_source: handle
- data_type: resource
- handler_key: Vessel
- label: Vessel
- output:
- - data_key: vessel
- data_source: executor
- data_type: resource
- handler_key: VesselOut
- label: Vessel
- result: {}
- schema:
- description: ROS Action Dry 的 JSON Schema
- properties:
- feedback:
- description: Action 反馈 - 执行过程中从服务器发送到客户端
- properties:
- status:
- type: string
- progress:
- type: number
- required:
- - status
- - progress
- title: Dry_Feedback
- type: object
- goal:
- description: Action 目标 - 从客户端发送到服务器
- properties:
- compound:
- type: string
- vessel:
- type: string
- required:
- - compound
- - vessel
- title: Dry_Goal
- type: object
- result:
- description: Action 结果 - 完成后从服务器发送到客户端
- properties:
- message:
- type: string
- return_info:
- type: string
- success:
- type: boolean
- required:
- - success
- - message
- - return_info
- title: Dry_Result
- type: object
- required:
- - goal
- title: Dry
- type: object
- type: Dry
- HydrogenateProtocol:
- feedback: {}
- goal:
- temp: temp
- time: time
- vessel: vessel
- goal_default:
- temp: ''
- time: ''
- vessel: ''
- handles:
- input:
- - data_key: vessel
- data_source: handle
- data_type: resource
- handler_key: Vessel
- label: Vessel
- output:
- - data_key: vessel
- data_source: executor
- data_type: resource
- handler_key: VesselOut
- label: Vessel
- result: {}
- schema:
- description: ROS Action Hydrogenate 的 JSON Schema
- properties:
- feedback:
- description: Action 反馈 - 执行过程中从服务器发送到客户端
- properties:
- status:
- type: string
- progress:
- type: number
- required:
- - status
- - progress
- title: Hydrogenate_Feedback
- type: object
- goal:
- description: Action 目标 - 从客户端发送到服务器
- properties:
- temp:
- type: string
- time:
- type: string
- vessel:
- type: string
- required:
- - temp
- - time
- - vessel
- title: Hydrogenate_Goal
- type: object
- result:
- description: Action 结果 - 完成后从服务器发送到客户端
- properties:
- message:
- type: string
- return_info:
- type: string
- success:
- type: boolean
- required:
- - success
- - message
- - return_info
- title: Hydrogenate_Result
- type: object
- required:
- - goal
- title: Hydrogenate
- type: object
- type: Hydrogenate
- RecrystallizeProtocol:
- feedback: {}
- goal:
- ratio: ratio
- solvent1: solvent1
- solvent2: solvent2
- vessel: vessel
- volume: volume
- goal_default:
- ratio: 1.0
- solvent1: ''
- solvent2: ''
- vessel: ''
- volume: 0.0
- handles:
- input:
- - data_key: vessel
- data_source: handle
- data_type: resource
- handler_key: Vessel
- label: Vessel
- - data_key: solvent1
- data_source: handle
- data_type: resource
- handler_key: solvent1
- label: Solvent 1
- - data_key: solvent2
- data_source: handle
- data_type: resource
- handler_key: solvent2
- label: Solvent 2
- output:
- - data_key: vessel
- data_source: executor
- data_type: resource
- handler_key: VesselOut
- label: Vessel
- result: {}
- schema:
- description: ROS Action Recrystallize 的 JSON Schema
- properties:
- feedback:
- description: Action 反馈 - 执行过程中从服务器发送到客户端
- properties:
- progress:
- type: number
- status:
- type: string
- required:
- - status
- - progress
- title: Recrystallize_Feedback
- type: object
- goal:
- description: Action 目标 - 从客户端发送到服务器
- properties:
- ratio:
- type: number
- solvent1:
- type: string
- solvent2:
- type: string
- vessel:
- type: string
- volume:
- type: number
- required:
- - ratio
- - solvent1
- - solvent2
- - vessel
- - volume
- title: Recrystallize_Goal
- type: object
- result:
- description: Action 结果 - 完成后从服务器发送到客户端
- properties:
- message:
- type: string
- return_info:
- type: string
- success:
- type: boolean
- required:
- - success
- - message
- - return_info
- title: Recrystallize_Result
- type: object
- required:
- - goal
- title: Recrystallize
- type: object
- type: Recrystallize
module: unilabos.ros.nodes.presets.protocol_node:ROS2ProtocolNode
status_types: {}
type: ros2
diff --git a/unilabos_msgs/CMakeLists.txt b/unilabos_msgs/CMakeLists.txt
index 7e8a1469b..b2c1bc71c 100644
--- a/unilabos_msgs/CMakeLists.txt
+++ b/unilabos_msgs/CMakeLists.txt
@@ -41,6 +41,7 @@ set(action_files
"action/WashSolid.action"
"action/Filter.action"
"action/Add.action"
+ "action/AddSolid.action"
"action/Centrifuge.action"
"action/Crystallize.action"
"action/Purge.action"
diff --git a/unilabos_msgs/action/RunColumn.action b/unilabos_msgs/action/RunColumn.action
index 889b4a5d8..1f8e9ba16 100644
--- a/unilabos_msgs/action/RunColumn.action
+++ b/unilabos_msgs/action/RunColumn.action
@@ -2,7 +2,7 @@
string from_vessel # 源容器的名称,即样品起始所在的容器(必需)
string to_vessel # 目标容器的名称,分离后的样品要到达的容器(必需)
string column # 所使用的柱子的名称(必需)
-string Rf # Rf值(可选)
+string rf # Rf值(可选)
string pct1 # 第一种溶剂百分比(如 "40 %",可选)
string pct2 # 第二种溶剂百分比(如 "50 %",可选)
string solvent1 # 第一种溶剂名称(可选)
From 7b93332bf5c6748a1b828ea1d3c9068793435238 Mon Sep 17 00:00:00 2001
From: KCFeng425 <2100011801@stu.pku.edu.cn>
Date: Thu, 10 Jul 2025 16:48:09 +0800
Subject: [PATCH 6/9] =?UTF-8?q?protocol=E5=AE=8C=E6=95=B4=E4=BF=AE?=
=?UTF-8?q?=E5=A4=8D=E7=89=88=E6=9C=AC&=20bump=20version=20to=200.9.10?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 2 +-
README_zh.md | 2 +-
recipes/ros-humble-unilabos-msgs/recipe.yaml | 2 +-
recipes/unilabos/recipe.yaml | 2 +-
setup.py | 2 +-
.../comprehensive_protocol/checklist.md | 53 +-
.../comprehensive_station.json | 4 +-
unilabos/compile/add_protocol.py | 636 ++++++++----
unilabos/compile/dissolve_protocol.py | 965 ++++++++++++------
.../compile/evacuateandrefill_protocol.py | 272 ++---
unilabos/compile/evaporate_protocol.py | 321 +++---
unilabos/compile/filter_protocol.py | 258 +++--
unilabos/compile/pump_protocol.py | 470 ++++++---
unilabos/compile/run_column_protocol.py | 794 ++++++++++----
unilabos/compile/separate_protocol.py | 658 ++++++++----
unilabos/compile/stir_protocol.py | 304 ++++--
unilabos/compile/wash_solid_protocol.py | 589 ++++++++---
unilabos/devices/virtual/virtual_filter.py | 10 +
unilabos/devices/virtual/virtual_rotavap.py | 56 +-
.../devices/virtual/virtual_solenoid_valve.py | 54 +-
unilabos/registry/devices/virtual_device.yaml | 52 +-
unilabos_msgs/action/Add.action | 10 +-
unilabos_msgs/action/Dissolve.action | 11 +-
unilabos_msgs/action/Separate.action | 28 +-
unilabos_msgs/action/WashSolid.action | 8 +-
25 files changed, 3740 insertions(+), 1823 deletions(-)
diff --git a/README.md b/README.md
index 287efb50d..5d14705a8 100644
--- a/README.md
+++ b/README.md
@@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n environment_name
# Currently, you need to install the `unilabos_msgs` package
# You can download the system-specific package from the Release page
-conda install ros-humble-unilabos-msgs-0.9.9-xxxxx.tar.bz2
+conda install ros-humble-unilabos-msgs-10-xxxxx.tar.bz2
# Install PyLabRobot and other prerequisites
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
diff --git a/README_zh.md b/README_zh.md
index 8bba3d77b..5bd588bbd 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n 环境名
# 现阶段,需要安装 `unilabos_msgs` 包
# 可以前往 Release 页面下载系统对应的包进行安装
-conda install ros-humble-unilabos-msgs-0.9.9-xxxxx.tar.bz2
+conda install ros-humble-unilabos-msgs-0.9.10-xxxxx.tar.bz2
# 安装PyLabRobot等前置
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
diff --git a/recipes/ros-humble-unilabos-msgs/recipe.yaml b/recipes/ros-humble-unilabos-msgs/recipe.yaml
index 4169c73d2..0445115d0 100644
--- a/recipes/ros-humble-unilabos-msgs/recipe.yaml
+++ b/recipes/ros-humble-unilabos-msgs/recipe.yaml
@@ -1,6 +1,6 @@
package:
name: ros-humble-unilabos-msgs
- version: 0.9.9
+ version: 0.9.10
source:
path: ../../unilabos_msgs
folder: ros-humble-unilabos-msgs/src/work
diff --git a/recipes/unilabos/recipe.yaml b/recipes/unilabos/recipe.yaml
index f9262e2b7..a5768558b 100644
--- a/recipes/unilabos/recipe.yaml
+++ b/recipes/unilabos/recipe.yaml
@@ -1,6 +1,6 @@
package:
name: unilabos
- version: "0.9.9"
+ version: "0.9.10"
source:
path: ../..
diff --git a/setup.py b/setup.py
index 2d9aabdb9..80163eab1 100644
--- a/setup.py
+++ b/setup.py
@@ -4,7 +4,7 @@
setup(
name=package_name,
- version='0.9.9',
+ version='0.9.10',
packages=find_packages(),
include_package_data=True,
install_requires=['setuptools'],
diff --git a/test/experiments/comprehensive_protocol/checklist.md b/test/experiments/comprehensive_protocol/checklist.md
index 4762cd22e..5dee23a15 100644
--- a/test/experiments/comprehensive_protocol/checklist.md
+++ b/test/experiments/comprehensive_protocol/checklist.md
@@ -43,6 +43,15 @@
Hydrogenate
4. 参数对齐
+
+
+
+
+
+
+
+
+
class PumpTransferProtocol(BaseModel):
from_vessel: str
to_vessel: str
@@ -53,7 +62,7 @@ class PumpTransferProtocol(BaseModel):
rinsing_solvent: str = "air"
rinsing_volume: float = 5000
rinsing_repeats: int = 2
- solid: bool = False 添加了缺失的参数,但是体积为0以及打印日志的问题修不好
+ solid: bool = False 测完了三个都能跑✅
flowrate: float = 500
transfer_flowrate: float = 2500
@@ -67,24 +76,24 @@ class SeparateProtocol(BaseModel):
solvent: str
solvent_volume: float
through: str
- repeats: int
- stir_time: float
+ repeats: int
+ stir_time: float
stir_speed: float
- settling_time: float 写了action
+ settling_time: float 测完了能跑✅
class EvaporateProtocol(BaseModel):
vessel: str
pressure: float
temp: float
- time: float 加完了
+ time: float 测完了能跑✅
stir_speed: float
class EvacuateAndRefillProtocol(BaseModel):
vessel: str
gas: str
- repeats: int 处理完了
+ repeats: int 测完了能跑✅
class AddProtocol(BaseModel):
vessel: str
@@ -102,7 +111,7 @@ class AddProtocol(BaseModel):
vessel="main_reactor" volume="2.67 mL"/>
viscous: bool
- purpose: str 写了action
+ purpose: str 测完了能跑✅
class CentrifugeProtocol(BaseModel):
vessel: str
@@ -115,7 +124,7 @@ class FilterProtocol(BaseModel):
filtrate_vessel: str
stir: bool
stir_speed: float
- temp: float 处理了
+ temp: float 测完了能跑✅
continue_heatchill: bool
volume: float
@@ -127,7 +136,7 @@ class HeatChillProtocol(BaseModel):
- stir: bool 处理了
+ stir: bool 测完了能跑✅
stir_speed: float
purpose: str
@@ -144,7 +153,7 @@ class StirProtocol(BaseModel):
stir_speed: float
- settling_time: float 处理完了
+ settling_time: float 测完了能跑✅
class StartStirProtocol(BaseModel):
vessel: str
@@ -176,11 +185,11 @@ class CleanVesselProtocol(BaseModel):
class DissolveProtocol(BaseModel):
vessel: str
solvent: str
- volume: float
- amount: str = ""
+ volume: float
+ amount: str = ""
temp: float = 25.0
- time: float = 0.0
- stir_speed: float = 0.0 写了action
+ time: float = 0.0
+ stir_speed: float = 0.0 测完了能跑✅
class FilterThroughProtocol(BaseModel):
from_vessel: str
@@ -194,16 +203,19 @@ class FilterThroughProtocol(BaseModel):
class RunColumnProtocol(BaseModel):
from_vessel: str
to_vessel: str
- column: str 写了action
+ column: str 测完了能跑✅
class WashSolidProtocol(BaseModel):
vessel: str
solvent: str
volume: float
- filtrate_vessel: str = ""
+ filtrate_vessel: str = ""
temp: float = 25.0
stir: bool = False
- stir_speed: float = 0.0 处理完了
+
+
+
+ stir_speed: float = 0.0 测完了能跑✅
time: float = 0.0
repeats: int = 1
@@ -230,4 +242,9 @@ class RecrystallizeProtocol(BaseModel):
class HydrogenateProtocol(BaseModel):
temp: str = Field(..., description="反应温度(如 '45 °C')")
time: str = Field(..., description="反应时间(如 '2 h')") <新写的,没问题>
- vessel: str = Field(..., description="反应容器")
\ No newline at end of file
+ vessel: str = Field(..., description="反应容器")
+
+ 还差
+
+
+
\ No newline at end of file
diff --git a/test/experiments/comprehensive_protocol/comprehensive_station.json b/test/experiments/comprehensive_protocol/comprehensive_station.json
index aaa4dfc8e..43e4cc67b 100644
--- a/test/experiments/comprehensive_protocol/comprehensive_station.json
+++ b/test/experiments/comprehensive_protocol/comprehensive_station.json
@@ -898,7 +898,7 @@
"type": "fluid",
"port": {
"multiway_valve_2": "3",
- "solenoid_valve_2": "in"
+ "solenoid_valve_2": "out"
}
},
{
@@ -908,7 +908,7 @@
"type": "fluid",
"port": {
"gas_source_1": "gassource",
- "solenoid_valve_2": "out"
+ "solenoid_valve_2": "in"
}
},
{
diff --git a/unilabos/compile/add_protocol.py b/unilabos/compile/add_protocol.py
index 6c51cc9fd..b5b017eb0 100644
--- a/unilabos/compile/add_protocol.py
+++ b/unilabos/compile/add_protocol.py
@@ -1,58 +1,238 @@
import networkx as nx
-from typing import List, Dict, Any
+import re
+import logging
+from typing import List, Dict, Any, Union
from .pump_protocol import generate_pump_protocol_with_rinsing
+logger = logging.getLogger(__name__)
+
+def debug_print(message):
+ """调试输出"""
+ print(f"[ADD] {message}", flush=True)
+ logger.info(f"[ADD] {message}")
+
+def parse_volume_input(volume_input: Union[str, float]) -> float:
+ """
+ 解析体积输入,支持带单位的字符串
+
+ Args:
+ volume_input: 体积输入(如 "2.7 mL", "2.67 mL", "?", 10.0)
+
+ Returns:
+ float: 体积(毫升)
+ """
+ if isinstance(volume_input, (int, float)):
+ return float(volume_input)
+
+ if not volume_input or not str(volume_input).strip():
+ return 0.0
+
+ volume_str = str(volume_input).lower().strip()
+ debug_print(f"解析体积输入: '{volume_str}'")
+
+ # 处理未知体积
+ if volume_str in ['?', 'unknown', 'tbd', 'to be determined']:
+ default_volume = 10.0 # 默认10mL
+ debug_print(f"检测到未知体积,使用默认值: {default_volume}mL")
+ return default_volume
+
+ # 移除空格并提取数字和单位
+ volume_clean = re.sub(r'\s+', '', volume_str)
+
+ # 匹配数字和单位的正则表达式
+ match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
+
+ if not match:
+ debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值10mL")
+ return 10.0
+
+ value = float(match.group(1))
+ unit = match.group(2) or 'ml' # 默认单位为毫升
+
+ # 转换为毫升
+ if unit in ['l', 'liter']:
+ volume = value * 1000.0 # L -> mL
+ elif unit in ['μl', 'ul', 'microliter']:
+ volume = value / 1000.0 # μL -> mL
+ else: # ml, milliliter 或默认
+ volume = value # 已经是mL
+
+ debug_print(f"体积转换: {value}{unit} → {volume}mL")
+ return volume
+
+def parse_mass_input(mass_input: Union[str, float]) -> float:
+ """
+ 解析质量输入,支持带单位的字符串
+
+ Args:
+ mass_input: 质量输入(如 "19.3 g", "4.5 g", 2.5)
+
+ Returns:
+ float: 质量(克)
+ """
+ if isinstance(mass_input, (int, float)):
+ return float(mass_input)
+
+ if not mass_input or not str(mass_input).strip():
+ return 0.0
+
+ mass_str = str(mass_input).lower().strip()
+ debug_print(f"解析质量输入: '{mass_str}'")
+
+ # 移除空格并提取数字和单位
+ mass_clean = re.sub(r'\s+', '', mass_str)
+
+ # 匹配数字和单位的正则表达式
+ match = re.match(r'([0-9]*\.?[0-9]+)\s*(g|mg|kg|gram|milligram|kilogram)?', mass_clean)
+
+ if not match:
+ debug_print(f"⚠️ 无法解析质量: '{mass_str}',返回0.0g")
+ return 0.0
+
+ value = float(match.group(1))
+ unit = match.group(2) or 'g' # 默认单位为克
+
+ # 转换为克
+ if unit in ['mg', 'milligram']:
+ mass = value / 1000.0 # mg -> g
+ elif unit in ['kg', 'kilogram']:
+ mass = value * 1000.0 # kg -> g
+ else: # g, gram 或默认
+ mass = value # 已经是g
+
+ debug_print(f"质量转换: {value}{unit} → {mass}g")
+ return mass
+
+def parse_time_input(time_input: Union[str, float]) -> float:
+ """
+ 解析时间输入,支持带单位的字符串
+
+ Args:
+ time_input: 时间输入(如 "1 h", "20 min", "30 s", 60.0)
+
+ Returns:
+ float: 时间(秒)
+ """
+ if isinstance(time_input, (int, float)):
+ return float(time_input)
+
+ if not time_input or not str(time_input).strip():
+ return 0.0
+
+ time_str = str(time_input).lower().strip()
+ debug_print(f"解析时间输入: '{time_str}'")
+
+ # 处理未知时间
+ if time_str in ['?', 'unknown', 'tbd']:
+ default_time = 60.0 # 默认1分钟
+ debug_print(f"检测到未知时间,使用默认值: {default_time}s")
+ return default_time
+
+ # 移除空格并提取数字和单位
+ time_clean = re.sub(r'\s+', '', time_str)
+
+ # 匹配数字和单位的正则表达式
+ match = re.match(r'([0-9]*\.?[0-9]+)\s*(s|sec|second|min|minute|h|hr|hour|d|day)?', time_clean)
+
+ if not match:
+ debug_print(f"⚠️ 无法解析时间: '{time_str}',返回0s")
+ return 0.0
+
+ value = float(match.group(1))
+ unit = match.group(2) or 's' # 默认单位为秒
+
+ # 转换为秒
+ if unit in ['min', 'minute']:
+ time_sec = value * 60.0 # min -> s
+ elif unit in ['h', 'hr', 'hour']:
+ time_sec = value * 3600.0 # h -> s
+ elif unit in ['d', 'day']:
+ time_sec = value * 86400.0 # d -> s
+ else: # s, sec, second 或默认
+ time_sec = value # 已经是s
+
+ debug_print(f"时间转换: {value}{unit} → {time_sec}s")
+ return time_sec
def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str:
"""增强版试剂容器查找,支持固体和液体"""
- print(f"ADD_PROTOCOL: 查找试剂 '{reagent}' 的容器...")
+ debug_print(f"查找试剂 '{reagent}' 的容器...")
- # 1. 直接名称匹配
+ # 🔧 方法1:直接搜索 data.reagent_name 和 config.reagent
+ for node in G.nodes():
+ node_data = G.nodes[node].get('data', {})
+ node_type = G.nodes[node].get('type', '')
+ config_data = G.nodes[node].get('config', {})
+
+ # 只搜索容器类型的节点
+ if node_type == 'container':
+ reagent_name = node_data.get('reagent_name', '').lower()
+ config_reagent = config_data.get('reagent', '').lower()
+
+ # 精确匹配
+ if reagent_name == reagent.lower() or config_reagent == reagent.lower():
+ debug_print(f"✅ 通过reagent字段找到容器: {node}")
+ return node
+
+ # 模糊匹配
+ if (reagent.lower() in reagent_name and reagent_name) or \
+ (reagent.lower() in config_reagent and config_reagent):
+ debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node}")
+ return node
+
+ # 🔧 方法2:常见的容器命名规则
+ reagent_clean = reagent.lower().replace(' ', '_').replace('-', '_')
possible_names = [
- reagent,
- f"flask_{reagent}",
- f"bottle_{reagent}",
- f"vessel_{reagent}",
- f"{reagent}_flask",
- f"{reagent}_bottle",
- f"reagent_{reagent}",
- f"reagent_bottle_{reagent}",
- f"solid_reagent_bottle_{reagent}", # 🔧 添加固体试剂瓶匹配
+ reagent_clean,
+ f"flask_{reagent_clean}",
+ f"bottle_{reagent_clean}",
+ f"vessel_{reagent_clean}",
+ f"{reagent_clean}_flask",
+ f"{reagent_clean}_bottle",
+ f"reagent_{reagent_clean}",
+ f"reagent_bottle_{reagent_clean}",
+ f"solid_reagent_bottle_{reagent_clean}",
+ f"reagent_bottle_1", # 通用试剂瓶
+ f"reagent_bottle_2",
+ f"reagent_bottle_3"
]
for name in possible_names:
if name in G.nodes():
- print(f"ADD_PROTOCOL: 找到容器: {name}")
- return name
+ node_type = G.nodes[name].get('type', '')
+ if node_type == 'container':
+ debug_print(f"✅ 通过命名规则找到容器: {name}")
+ return name
- # 2. 模糊匹配 - 检查容器数据
+ # 🔧 方法3:节点名称模糊匹配
for node_id in G.nodes():
node_data = G.nodes[node_id]
if node_data.get('type') == 'container':
- # 检查配置中的试剂名称
- config_reagent = node_data.get('config', {}).get('reagent', '')
- data_reagent = node_data.get('data', {}).get('reagent_name', '')
-
- # 名称匹配
- if (config_reagent.lower() == reagent.lower() or
- data_reagent.lower() == reagent.lower() or
- reagent.lower() in node_id.lower()):
- print(f"ADD_PROTOCOL: 模糊匹配到容器: {node_id}")
+ # 检查节点名称是否包含试剂名称
+ if reagent_clean in node_id.lower():
+ debug_print(f"✅ 通过节点名称模糊匹配到容器: {node_id}")
return node_id
- # 液体类型匹配(保持原有逻辑)
+ # 检查液体类型匹配
vessel_data = node_data.get('data', {})
liquids = vessel_data.get('liquid', [])
for liquid in liquids:
if isinstance(liquid, dict):
liquid_type = liquid.get('liquid_type') or liquid.get('name', '')
if liquid_type.lower() == reagent.lower():
- print(f"ADD_PROTOCOL: 液体类型匹配到容器: {node_id}")
+ debug_print(f"✅ 通过液体类型匹配到容器: {node_id}")
return node_id
+ # 🔧 方法4:使用第一个试剂瓶作为备选
+ for node_id in G.nodes():
+ node_data = G.nodes[node_id]
+ if (node_data.get('type') == 'container' and
+ ('reagent' in node_id.lower() or 'bottle' in node_id.lower())):
+ debug_print(f"⚠️ 未找到专用容器,使用备选试剂瓶: {node_id}")
+ return node_id
+
raise ValueError(f"找不到试剂 '{reagent}' 对应的容器")
-
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
"""查找连接到指定容器的搅拌器"""
stirrer_nodes = []
@@ -64,40 +244,41 @@ def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
# 查找连接到容器的搅拌器
for stirrer in stirrer_nodes:
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
- print(f"ADD_PROTOCOL: 找到连接的搅拌器: {stirrer}")
+ debug_print(f"找到连接的搅拌器: {stirrer}")
return stirrer
# 返回第一个搅拌器
if stirrer_nodes:
- print(f"ADD_PROTOCOL: 使用第一个搅拌器: {stirrer_nodes[0]}")
+ debug_print(f"使用第一个搅拌器: {stirrer_nodes[0]}")
return stirrer_nodes[0]
- return None
-
+ return ""
def find_solid_dispenser(G: nx.DiGraph) -> str:
"""查找固体加样器"""
for node in G.nodes():
node_class = G.nodes[node].get('class', '').lower()
- if 'solid_dispenser' in node_class:
- print(f"ADD_PROTOCOL: 找到固体加样器: {node}")
+ if 'solid_dispenser' in node_class or 'dispenser' in node_class:
+ debug_print(f"找到固体加样器: {node}")
return node
- return None
-
+
+ debug_print("⚠️ 未找到固体加样器")
+ return ""
def generate_add_protocol(
G: nx.DiGraph,
vessel: str,
reagent: str,
- volume: float = 0.0,
- mass: float = 0.0,
+ # 🔧 修复:所有参数都用 Union 类型,支持字符串和数值
+ volume: Union[str, float] = 0.0,
+ mass: Union[str, float] = 0.0,
amount: str = "",
- time: float = 0.0,
+ time: Union[str, float] = 0.0,
stir: bool = False,
stir_speed: float = 300.0,
viscous: bool = False,
purpose: str = "添加试剂",
- # 新增XDL参数
+ # XDL扩展参数
mol: str = "",
event: str = "",
rate_spec: str = "",
@@ -106,133 +287,191 @@ def generate_add_protocol(
**kwargs
) -> List[Dict[str, Any]]:
"""
- 生成添加试剂协议
+ 生成添加试剂协议 - 修复版
- 智能判断:
- - 有 mass 或 mol → 固体加样器
- - 有 volume → 液体转移
- - 都没有 → 默认液体 1mL
+ 支持所有XDL参数和单位:
+ - volume: "2.7 mL", "2.67 mL", "?" 或数值
+ - mass: "19.3 g", "4.5 g" 或数值
+ - time: "1 h", "20 min" 或数值(秒)
+ - mol: "0.28 mol", "16.2 mmol", "25.2 mmol"
+ - rate_spec: "portionwise", "dropwise"
+ - event: "A", "B"
+ - equiv: "1.1"
+ - ratio: "?", "1:1"
"""
- print(f"ADD_PROTOCOL: 添加 {reagent} 到 {vessel}")
- print(f" - 体积: {volume} mL, 质量: {mass} g, 摩尔: {mol}")
- print(f" - 时间: {time} s, 事件: {event}, 速率: {rate_spec}")
+ debug_print("=" * 60)
+ debug_print("开始生成添加试剂协议")
+ debug_print(f"原始参数:")
+ debug_print(f" - vessel: '{vessel}'")
+ debug_print(f" - reagent: '{reagent}'")
+ debug_print(f" - volume: {volume} (类型: {type(volume)})")
+ debug_print(f" - mass: {mass} (类型: {type(mass)})")
+ debug_print(f" - time: {time} (类型: {type(time)})")
+ debug_print(f" - mol: '{mol}'")
+ debug_print(f" - event: '{event}'")
+ debug_print(f" - rate_spec: '{rate_spec}'")
+ debug_print("=" * 60)
+
+ action_sequence = []
+
+ # === 参数验证 ===
+ debug_print("步骤1: 参数验证...")
+
+ if not vessel:
+ raise ValueError("vessel 参数不能为空")
+ if not reagent:
+ raise ValueError("reagent 参数不能为空")
- # 1. 验证容器
if vessel not in G.nodes():
- raise ValueError(f"容器 '{vessel}' 不存在")
+ raise ValueError(f"容器 '{vessel}' 不存在于系统中")
- # 2. 判断固体 vs 液体
- is_solid = (mass > 0 or mol.strip() != "")
+ debug_print("✅ 基本参数验证通过")
- action_sequence = []
+ # === 🔧 关键修复:参数解析 ===
+ debug_print("步骤2: 参数解析...")
- if is_solid:
- # === 固体加样路径 ===
- print(f"ADD_PROTOCOL: 使用固体加样器")
-
- solid_dispenser = find_solid_dispenser(G)
- if not solid_dispenser:
- raise ValueError("未找到固体加样器")
-
- # 启动搅拌(如果需要)
- if stir:
- stirrer_id = find_connected_stirrer(G, vessel)
- if stirrer_id:
- action_sequence.append({
- "device_id": stirrer_id,
- "action_name": "start_stir",
- "action_kwargs": {
- "vessel": vessel,
- "stir_speed": stir_speed,
- "purpose": f"准备添加固体 {reagent}"
- }
- })
- # 等待搅拌稳定
- action_sequence.append({
- "action_name": "wait",
- "action_kwargs": {"time": 3}
- })
-
- # 固体加样
- action_sequence.append({
- "device_id": solid_dispenser,
- "action_name": "add_solid",
- "action_kwargs": {
- "vessel": vessel,
- "reagent": reagent,
- "mass": str(mass) if mass > 0 else "",
- "mol": mol,
- "purpose": purpose,
- "event": event
- }
- })
-
- else:
- # === 液体转移路径 ===
- print(f"ADD_PROTOCOL: 使用液体转移")
-
- # 默认体积
- if volume <= 0:
- volume = 1.0
- print(f"ADD_PROTOCOL: 使用默认体积 1mL")
-
- # 查找试剂容器
- try:
- reagent_vessel = find_reagent_vessel(G, reagent)
- except ValueError as e:
- # 🔧 更友好的错误提示
- available_reagents = []
- for node_id in G.nodes():
- node_data = G.nodes[node_id]
- if node_data.get('type') == 'container':
- config_reagent = node_data.get('config', {}).get('reagent', '')
- data_reagent = node_data.get('data', {}).get('reagent_name', '')
- if config_reagent:
- available_reagents.append(f"{node_id}({config_reagent})")
- elif data_reagent:
- available_reagents.append(f"{node_id}({data_reagent})")
+ # 解析各种参数为数值
+ final_volume = parse_volume_input(volume)
+ final_mass = parse_mass_input(mass)
+ final_time = parse_time_input(time)
+
+ debug_print(f"解析结果:")
+ debug_print(f" - 体积: {final_volume}mL")
+ debug_print(f" - 质量: {final_mass}g")
+ debug_print(f" - 时间: {final_time}s")
+ debug_print(f" - 摩尔: '{mol}'")
+ debug_print(f" - 事件: '{event}'")
+ debug_print(f" - 速率: '{rate_spec}'")
+
+ # === 判断添加类型 ===
+ debug_print("步骤3: 判断添加类型...")
+
+ # 🔧 修复:现在使用解析后的数值进行比较
+ is_solid = (final_mass > 0 or (mol and mol.strip() != ""))
+ is_liquid = (final_volume > 0)
+
+ if not is_solid and not is_liquid:
+ # 默认为液体,10mL
+ is_liquid = True
+ final_volume = 10.0
+ debug_print("⚠️ 未指定体积或质量,默认为10mL液体")
+
+ debug_print(f"添加类型: {'固体' if is_solid else '液体'}")
+
+ # === 执行添加流程 ===
+ debug_print("步骤4: 执行添加流程...")
+
+ try:
+ if is_solid:
+ # === 固体添加路径 ===
+ debug_print(f"使用固体添加路径")
- error_msg = f"找不到试剂 '{reagent}'。可用试剂: {', '.join(available_reagents)}"
- print(f"ADD_PROTOCOL: {error_msg}")
- raise ValueError(error_msg)
-
- # 启动搅拌
- if stir:
- stirrer_id = find_connected_stirrer(G, vessel)
- if stirrer_id:
+ solid_dispenser = find_solid_dispenser(G)
+ if solid_dispenser:
+ # 启动搅拌
+ if stir:
+ stirrer_id = find_connected_stirrer(G, vessel)
+ if stirrer_id:
+ action_sequence.append({
+ "device_id": stirrer_id,
+ "action_name": "start_stir",
+ "action_kwargs": {
+ "vessel": vessel,
+ "stir_speed": stir_speed,
+ "purpose": f"准备添加固体 {reagent}"
+ }
+ })
+ # 等待搅拌稳定
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": 3}
+ })
+
+ # 固体加样
+ add_kwargs = {
+ "vessel": vessel,
+ "reagent": reagent,
+ "purpose": purpose,
+ "event": event,
+ "rate_spec": rate_spec
+ }
+
+ if final_mass > 0:
+ add_kwargs["mass"] = str(final_mass)
+ if mol and mol.strip():
+ add_kwargs["mol"] = mol
+ if equiv and equiv.strip():
+ add_kwargs["equiv"] = equiv
+
action_sequence.append({
- "device_id": stirrer_id,
- "action_name": "start_stir",
- "action_kwargs": {
- "vessel": vessel,
- "stir_speed": stir_speed,
- "purpose": f"准备添加液体 {reagent}"
- }
- })
- # 等待搅拌稳定
- action_sequence.append({
- "action_name": "wait",
- "action_kwargs": {"time": 5}
+ "device_id": solid_dispenser,
+ "action_name": "add_solid",
+ "action_kwargs": add_kwargs
})
+
+ # 添加后等待
+ if final_time > 0:
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": final_time}
+ })
+
+ debug_print(f"✅ 固体添加完成")
+ else:
+ debug_print("⚠️ 未找到固体加样器,跳过固体添加")
- # 计算流速
- if time > 0:
- flowrate = volume / time
- transfer_flowrate = flowrate
else:
- flowrate = 1.0 if viscous else 2.5
- transfer_flowrate = 0.3 if viscous else 0.5
-
- # 🔧 调用 pump_protocol 时使用正确的参数
- try:
+ # === 液体添加路径 ===
+ debug_print(f"使用液体添加路径")
+
+ # 查找试剂容器
+ reagent_vessel = find_reagent_vessel(G, reagent)
+
+ # 启动搅拌
+ if stir:
+ stirrer_id = find_connected_stirrer(G, vessel)
+ if stirrer_id:
+ action_sequence.append({
+ "device_id": stirrer_id,
+ "action_name": "start_stir",
+ "action_kwargs": {
+ "vessel": vessel,
+ "stir_speed": stir_speed,
+ "purpose": f"准备添加液体 {reagent}"
+ }
+ })
+ # 等待搅拌稳定
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": 5}
+ })
+
+ # 计算流速
+ if final_time > 0:
+ flowrate = final_volume / final_time * 60 # mL/min
+ transfer_flowrate = flowrate
+ else:
+ if rate_spec == "dropwise":
+ flowrate = 0.5 # 滴加,很慢
+ transfer_flowrate = 0.2
+ elif viscous:
+ flowrate = 1.0 # 粘性液体
+ transfer_flowrate = 0.3
+ else:
+ flowrate = 2.5 # 正常流速
+ transfer_flowrate = 0.5
+
+ debug_print(f"流速设置: {flowrate} mL/min")
+
+ # 调用pump protocol
pump_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=reagent_vessel,
to_vessel=vessel,
- volume=volume,
+ volume=final_volume,
amount=amount,
- duration=time, # 🔧 使用 duration 而不是 time
+ time=final_time,
viscous=viscous,
rinsing_solvent="",
rinsing_volume=0.0,
@@ -243,33 +482,44 @@ def generate_add_protocol(
rate_spec=rate_spec,
event=event,
through="",
- equiv=equiv,
- ratio=ratio,
**kwargs
)
action_sequence.extend(pump_actions)
- except Exception as e:
- raise ValueError(f"液体转移失败: {str(e)}")
+ debug_print(f"✅ 液体转移完成,添加了 {len(pump_actions)} 个动作")
+
+ except Exception as e:
+ debug_print(f"⚠️ 试剂添加失败: {str(e)}")
+ # 添加错误日志
+ action_sequence.append({
+ "device_id": "system",
+ "action_name": "log_message",
+ "action_kwargs": {
+ "message": f"试剂 '{reagent}' 添加失败: {str(e)}"
+ }
+ })
+
+ # === 最终结果 ===
+ debug_print("=" * 60)
+ debug_print(f"✅ 添加试剂协议生成完成")
+ debug_print(f"📊 总动作数: {len(action_sequence)}")
+ debug_print(f"📋 处理总结:")
+ debug_print(f" - 试剂: {reagent}")
+ debug_print(f" - 添加类型: {'固体' if is_solid else '液体'}")
+ debug_print(f" - 目标容器: {vessel}")
+ if is_liquid:
+ debug_print(f" - 体积: {final_volume}mL")
+ if is_solid:
+ debug_print(f" - 质量: {final_mass}g")
+ debug_print(f" - 摩尔: {mol}")
+ debug_print("=" * 60)
- print(f"ADD_PROTOCOL: 生成 {len(action_sequence)} 个动作")
return action_sequence
+# === 便捷函数 ===
-# 处理 wait 动作
-def process_wait_action(action_kwargs: Dict[str, Any]) -> Dict[str, Any]:
- """处理等待动作"""
- wait_time = action_kwargs.get('time', 1.0)
- return {
- "action_name": "wait",
- "action_kwargs": {"time": wait_time},
- "description": f"等待 {wait_time} 秒"
- }
-
-
-# 便捷函数
-def add_liquid(G: nx.DiGraph, vessel: str, reagent: str, volume: float,
- time: float = 0.0, rate_spec: str = "") -> List[Dict[str, Any]]:
- """添加液体试剂"""
+def add_liquid_volume(G: nx.DiGraph, vessel: str, reagent: str, volume: Union[str, float],
+ time: Union[str, float] = 0.0, rate_spec: str = "") -> List[Dict[str, Any]]:
+ """添加指定体积的液体试剂"""
return generate_add_protocol(
G, vessel, reagent,
volume=volume,
@@ -277,19 +527,17 @@ def add_liquid(G: nx.DiGraph, vessel: str, reagent: str, volume: float,
rate_spec=rate_spec
)
-
-def add_solid(G: nx.DiGraph, vessel: str, reagent: str, mass: float,
- event: str = "") -> List[Dict[str, Any]]:
- """添加固体试剂"""
+def add_solid_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, float],
+ event: str = "") -> List[Dict[str, Any]]:
+ """添加指定质量的固体试剂"""
return generate_add_protocol(
G, vessel, reagent,
mass=mass,
event=event
)
-
-def add_solid_mol(G: nx.DiGraph, vessel: str, reagent: str, mol: str,
- event: str = "") -> List[Dict[str, Any]]:
+def add_solid_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str,
+ event: str = "") -> List[Dict[str, Any]]:
"""按摩尔数添加固体试剂"""
return generate_add_protocol(
G, vessel, reagent,
@@ -297,9 +545,8 @@ def add_solid_mol(G: nx.DiGraph, vessel: str, reagent: str, mol: str,
event=event
)
-
-def add_dropwise(G: nx.DiGraph, vessel: str, reagent: str, volume: float,
- time: float = 0.0, event: str = "") -> List[Dict[str, Any]]:
+def add_dropwise_liquid(G: nx.DiGraph, vessel: str, reagent: str, volume: Union[str, float],
+ time: Union[str, float] = "20 min", event: str = "") -> List[Dict[str, Any]]:
"""滴加液体试剂"""
return generate_add_protocol(
G, vessel, reagent,
@@ -309,9 +556,8 @@ def add_dropwise(G: nx.DiGraph, vessel: str, reagent: str, volume: float,
event=event
)
-
-def add_portionwise(G: nx.DiGraph, vessel: str, reagent: str, mass: float,
- time: float = 0.0, event: str = "") -> List[Dict[str, Any]]:
+def add_portionwise_solid(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, float],
+ time: Union[str, float] = "1 h", event: str = "") -> List[Dict[str, Any]]:
"""分批添加固体试剂"""
return generate_add_protocol(
G, vessel, reagent,
@@ -321,16 +567,30 @@ def add_portionwise(G: nx.DiGraph, vessel: str, reagent: str, mass: float,
event=event
)
-
# 测试函数
def test_add_protocol():
- """测试添加协议"""
- print("=== ADD PROTOCOL 修复版测试 ===")
- print("✅ 已修复设备查找逻辑")
- print("✅ 已添加固体试剂瓶支持")
- print("✅ 已修复错误处理")
+ """测试添加协议的各种参数解析"""
+ print("=== ADD PROTOCOL 增强版测试 ===")
+
+ # 测试体积解析
+ volumes = ["2.7 mL", "2.67 mL", "?", 10.0, "1 L", "500 μL"]
+ for vol in volumes:
+ result = parse_volume_input(vol)
+ print(f"体积解析: {vol} → {result}mL")
+
+ # 测试质量解析
+ masses = ["19.3 g", "4.5 g", 2.5, "500 mg", "1 kg"]
+ for mass in masses:
+ result = parse_mass_input(mass)
+ print(f"质量解析: {mass} → {result}g")
+
+ # 测试时间解析
+ times = ["1 h", "20 min", "30 s", 60.0, "?"]
+ for time in times:
+ result = parse_time_input(time)
+ print(f"时间解析: {time} → {result}s")
+
print("✅ 测试完成")
-
if __name__ == "__main__":
test_add_protocol()
\ No newline at end of file
diff --git a/unilabos/compile/dissolve_protocol.py b/unilabos/compile/dissolve_protocol.py
index 3da0d5378..9008e6d57 100644
--- a/unilabos/compile/dissolve_protocol.py
+++ b/unilabos/compile/dissolve_protocol.py
@@ -1,359 +1,742 @@
-from typing import List, Dict, Any
import networkx as nx
-from .pump_protocol import generate_pump_protocol
+import re
+import logging
+from typing import List, Dict, Any, Union
+from .pump_protocol import generate_pump_protocol_with_rinsing
+logger = logging.getLogger(__name__)
-def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
+def debug_print(message):
+ """调试输出"""
+ print(f"[DISSOLVE] {message}", flush=True)
+ logger.info(f"[DISSOLVE] {message}")
+
+def parse_volume_input(volume_input: Union[str, float]) -> float:
"""
- 查找溶剂容器
+ 解析体积输入,支持带单位的字符串
+
+ Args:
+ volume_input: 体积输入(如 "10 mL", "?", 10.0)
+
+ Returns:
+ float: 体积(毫升)
"""
- # 按照pump_protocol的命名规则查找溶剂瓶
- solvent_vessel_id = f"flask_{solvent}"
+ if isinstance(volume_input, (int, float)):
+ return float(volume_input)
- if solvent_vessel_id in G.nodes():
- return solvent_vessel_id
+ if not volume_input or not str(volume_input).strip():
+ return 0.0
- # 如果直接匹配失败,尝试模糊匹配
- for node in G.nodes():
- if node.startswith('flask_') and solvent.lower() in node.lower():
- return node
+ volume_str = str(volume_input).lower().strip()
+ debug_print(f"解析体积输入: '{volume_str}'")
+
+ # 处理未知体积
+ if volume_str in ['?', 'unknown', 'tbd', 'to be determined']:
+ default_volume = 50.0 # 默认50mL
+ debug_print(f"检测到未知体积,使用默认值: {default_volume}mL")
+ return default_volume
+
+ # 移除空格并提取数字和单位
+ volume_clean = re.sub(r'\s+', '', volume_str)
+
+ # 匹配数字和单位的正则表达式
+ match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
+
+ if not match:
+ debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值50mL")
+ return 50.0
- # 如果还是找不到,列出所有可用的溶剂瓶
- available_flasks = [node for node in G.nodes()
- if node.startswith('flask_')
- and G.nodes[node].get('type') == 'container']
+ value = float(match.group(1))
+ unit = match.group(2) or 'ml' # 默认单位为毫升
- raise ValueError(f"找不到溶剂 '{solvent}' 对应的溶剂瓶。可用溶剂瓶: {available_flasks}")
+ # 转换为毫升
+ if unit in ['l', 'liter']:
+ volume = value * 1000.0 # L -> mL
+ elif unit in ['μl', 'ul', 'microliter']:
+ volume = value / 1000.0 # μL -> mL
+ else: # ml, milliliter 或默认
+ volume = value # 已经是mL
+
+ debug_print(f"体积转换: {value}{unit} → {volume}mL")
+ return volume
+def parse_mass_input(mass_input: Union[str, float]) -> float:
+ """
+ 解析质量输入,支持带单位的字符串
+
+ Args:
+ mass_input: 质量输入(如 "2.9 g", "?", 2.5)
+
+ Returns:
+ float: 质量(克)
+ """
+ if isinstance(mass_input, (int, float)):
+ return float(mass_input)
+
+ if not mass_input or not str(mass_input).strip():
+ return 0.0
+
+ mass_str = str(mass_input).lower().strip()
+ debug_print(f"解析质量输入: '{mass_str}'")
+
+ # 处理未知质量
+ if mass_str in ['?', 'unknown', 'tbd', 'to be determined']:
+ default_mass = 1.0 # 默认1g
+ debug_print(f"检测到未知质量,使用默认值: {default_mass}g")
+ return default_mass
+
+ # 移除空格并提取数字和单位
+ mass_clean = re.sub(r'\s+', '', mass_str)
+
+ # 匹配数字和单位的正则表达式
+ match = re.match(r'([0-9]*\.?[0-9]+)\s*(g|mg|kg|gram|milligram|kilogram)?', mass_clean)
+
+ if not match:
+ debug_print(f"⚠️ 无法解析质量: '{mass_str}',返回0.0g")
+ return 0.0
+
+ value = float(match.group(1))
+ unit = match.group(2) or 'g' # 默认单位为克
+
+ # 转换为克
+ if unit in ['mg', 'milligram']:
+ mass = value / 1000.0 # mg -> g
+ elif unit in ['kg', 'kilogram']:
+ mass = value * 1000.0 # kg -> g
+ else: # g, gram 或默认
+ mass = value # 已经是g
+
+ debug_print(f"质量转换: {value}{unit} → {mass}g")
+ return mass
-def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str:
+def parse_time_input(time_input: Union[str, float]) -> float:
+ """
+ 解析时间输入,支持带单位的字符串
+
+ Args:
+ time_input: 时间输入(如 "30 min", "1 h", "?", 60.0)
+
+ Returns:
+ float: 时间(秒)
"""
- 查找与指定容器相连的加热搅拌器
+ if isinstance(time_input, (int, float)):
+ return float(time_input)
+
+ if not time_input or not str(time_input).strip():
+ return 0.0
+
+ time_str = str(time_input).lower().strip()
+ debug_print(f"解析时间输入: '{time_str}'")
+
+ # 处理未知时间
+ if time_str in ['?', 'unknown', 'tbd']:
+ default_time = 600.0 # 默认10分钟
+ debug_print(f"检测到未知时间,使用默认值: {default_time}s")
+ return default_time
+
+ # 移除空格并提取数字和单位
+ time_clean = re.sub(r'\s+', '', time_str)
+
+ # 匹配数字和单位的正则表达式
+ match = re.match(r'([0-9]*\.?[0-9]+)\s*(s|sec|second|min|minute|h|hr|hour|d|day)?', time_clean)
+
+ if not match:
+ debug_print(f"⚠️ 无法解析时间: '{time_str}',返回0s")
+ return 0.0
+
+ value = float(match.group(1))
+ unit = match.group(2) or 's' # 默认单位为秒
+
+ # 转换为秒
+ if unit in ['min', 'minute']:
+ time_sec = value * 60.0 # min -> s
+ elif unit in ['h', 'hr', 'hour']:
+ time_sec = value * 3600.0 # h -> s
+ elif unit in ['d', 'day']:
+ time_sec = value * 86400.0 # d -> s
+ else: # s, sec, second 或默认
+ time_sec = value # 已经是s
+
+ debug_print(f"时间转换: {value}{unit} → {time_sec}s")
+ return time_sec
+
+def parse_temperature_input(temp_input: Union[str, float]) -> float:
"""
- # 查找所有加热搅拌器节点
- heatchill_nodes = [node for node in G.nodes()
- if G.nodes[node].get('class') == 'virtual_heatchill']
+ 解析温度输入,支持带单位的字符串
- # 检查哪个加热器与目标容器相连
+ Args:
+ temp_input: 温度输入(如 "60 °C", "room temperature", "?", 25.0)
+
+ Returns:
+ float: 温度(摄氏度)
+ """
+ if isinstance(temp_input, (int, float)):
+ return float(temp_input)
+
+ if not temp_input or not str(temp_input).strip():
+ return 25.0 # 默认室温
+
+ temp_str = str(temp_input).lower().strip()
+ debug_print(f"解析温度输入: '{temp_str}'")
+
+ # 处理特殊温度描述
+ temp_aliases = {
+ 'room temperature': 25.0,
+ 'rt': 25.0,
+ 'ambient': 25.0,
+ 'cold': 4.0,
+ 'ice': 0.0,
+ 'reflux': 80.0, # 默认回流温度
+ '?': 25.0,
+ 'unknown': 25.0
+ }
+
+ if temp_str in temp_aliases:
+ result = temp_aliases[temp_str]
+ debug_print(f"温度别名解析: '{temp_str}' → {result}°C")
+ return result
+
+ # 移除空格并提取数字和单位
+ temp_clean = re.sub(r'\s+', '', temp_str)
+
+ # 匹配数字和单位的正则表达式
+ match = re.match(r'([0-9]*\.?[0-9]+)\s*(°c|c|celsius|°f|f|fahrenheit|k|kelvin)?', temp_clean)
+
+ if not match:
+ debug_print(f"⚠️ 无法解析温度: '{temp_str}',使用默认值25°C")
+ return 25.0
+
+ value = float(match.group(1))
+ unit = match.group(2) or 'c' # 默认单位为摄氏度
+
+ # 转换为摄氏度
+ if unit in ['°f', 'f', 'fahrenheit']:
+ temp_c = (value - 32) * 5/9 # F -> C
+ elif unit in ['k', 'kelvin']:
+ temp_c = value - 273.15 # K -> C
+ else: # °c, c, celsius 或默认
+ temp_c = value # 已经是C
+
+ debug_print(f"温度转换: {value}{unit} → {temp_c}°C")
+ return temp_c
+
+def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
+ """增强版溶剂容器查找"""
+ debug_print(f"查找溶剂 '{solvent}' 的容器...")
+
+ # 🔧 方法1:直接搜索 data.reagent_name 和 config.reagent
+ for node in G.nodes():
+ node_data = G.nodes[node].get('data', {})
+ node_type = G.nodes[node].get('type', '')
+ config_data = G.nodes[node].get('config', {})
+
+ # 只搜索容器类型的节点
+ if node_type == 'container':
+ reagent_name = node_data.get('reagent_name', '').lower()
+ config_reagent = config_data.get('reagent', '').lower()
+
+ # 精确匹配
+ if reagent_name == solvent.lower() or config_reagent == solvent.lower():
+ debug_print(f"✅ 通过reagent字段找到容器: {node}")
+ return node
+
+ # 模糊匹配
+ if (solvent.lower() in reagent_name and reagent_name) or \
+ (solvent.lower() in config_reagent and config_reagent):
+ debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node}")
+ return node
+
+ # 🔧 方法2:常见的容器命名规则
+ solvent_clean = solvent.lower().replace(' ', '_').replace('-', '_')
+ possible_names = [
+ f"flask_{solvent_clean}",
+ f"bottle_{solvent_clean}",
+ f"vessel_{solvent_clean}",
+ f"{solvent_clean}_flask",
+ f"{solvent_clean}_bottle",
+ f"solvent_{solvent_clean}",
+ f"reagent_{solvent_clean}",
+ f"reagent_bottle_{solvent_clean}"
+ ]
+
+ for name in possible_names:
+ if name in G.nodes():
+ node_type = G.nodes[name].get('type', '')
+ if node_type == 'container':
+ debug_print(f"✅ 通过命名规则找到容器: {name}")
+ return name
+
+ # 🔧 方法3:使用第一个试剂瓶作为备选
+ for node_id in G.nodes():
+ node_data = G.nodes[node_id]
+ if (node_data.get('type') == 'container' and
+ ('reagent' in node_id.lower() or 'bottle' in node_id.lower() or 'flask' in node_id.lower())):
+ debug_print(f"⚠️ 未找到专用容器,使用备选容器: {node_id}")
+ return node_id
+
+ raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器")
+
+def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str:
+ """查找连接到指定容器的加热搅拌器"""
+ heatchill_nodes = []
+ for node in G.nodes():
+ node_class = G.nodes[node].get('class', '').lower()
+ if 'heatchill' in node_class:
+ heatchill_nodes.append(node)
+
+ # 查找连接到容器的加热器
for heatchill in heatchill_nodes:
if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill):
+ debug_print(f"找到连接的加热器: {heatchill}")
return heatchill
- # 如果没有直接连接,返回第一个可用的加热器
- return heatchill_nodes[0] if heatchill_nodes else None
+ # 返回第一个加热器
+ if heatchill_nodes:
+ debug_print(f"使用第一个加热器: {heatchill_nodes[0]}")
+ return heatchill_nodes[0]
+
+ return ""
+
+def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
+ """查找连接到指定容器的搅拌器"""
+ stirrer_nodes = []
+ for node in G.nodes():
+ node_class = G.nodes[node].get('class', '').lower()
+ if 'stirrer' in node_class:
+ stirrer_nodes.append(node)
+
+ # 查找连接到容器的搅拌器
+ for stirrer in stirrer_nodes:
+ if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
+ debug_print(f"找到连接的搅拌器: {stirrer}")
+ return stirrer
+
+ # 返回第一个搅拌器
+ if stirrer_nodes:
+ debug_print(f"使用第一个搅拌器: {stirrer_nodes[0]}")
+ return stirrer_nodes[0]
+
+ return ""
+def find_solid_dispenser(G: nx.DiGraph) -> str:
+ """查找固体加样器"""
+ for node in G.nodes():
+ node_class = G.nodes[node].get('class', '').lower()
+ if 'solid_dispenser' in node_class or 'dispenser' in node_class:
+ debug_print(f"找到固体加样器: {node}")
+ return node
+
+ debug_print("⚠️ 未找到固体加样器")
+ return ""
def generate_dissolve_protocol(
G: nx.DiGraph,
vessel: str,
- solvent: str,
- volume: float,
+ # 🔧 修复:按照checklist.md的DissolveProtocol参数
+ solvent: str = "",
+ volume: Union[str, float] = 0.0,
amount: str = "",
- temp: float = 25.0,
- time: float = 0.0,
- stir_speed: float = 300.0
+ temp: Union[str, float] = 25.0,
+ time: Union[str, float] = 0.0,
+ stir_speed: float = 300.0,
+ # 🔧 关键修复:添加缺失的参数,防止"unexpected keyword argument"错误
+ mass: Union[str, float] = 0.0, # 这个参数在action文件中存在,必须包含
+ mol: str = "", # 这个参数在action文件中存在,必须包含
+ reagent: str = "", # 这个参数在action文件中存在,必须包含
+ event: str = "", # 这个参数在action文件中存在,必须包含
+ **kwargs # 🔧 关键:接受所有其他参数,防止unexpected keyword错误
) -> List[Dict[str, Any]]:
"""
- 生成溶解操作的协议序列,复用 pump_protocol 的成熟算法
+ 生成溶解操作的协议序列 - 修复版
- 溶解流程:
- 1. 溶剂转移:将溶剂从溶剂瓶转移到目标容器
- 2. 启动加热搅拌:设置温度和搅拌
- 3. 等待溶解:监控溶解过程
- 4. 停止加热搅拌:完成溶解
+ 🔧 修复要点:
+ 1. 添加action文件中的所有参数(mass, mol, reagent, event)
+ 2. 使用 **kwargs 接受所有额外参数,防止 unexpected keyword argument 错误
+ 3. 支持固体溶解和液体溶解两种模式
- Args:
- G: 有向图,节点为设备和容器,边为流体管道
- vessel: 目标容器(要进行溶解的容器)
- solvent: 溶剂名称(用于查找对应的溶剂瓶)
- volume: 溶剂体积 (mL)
- amount: 要溶解的物质描述
- temp: 溶解温度 (°C),默认25°C(室温)
- time: 溶解时间 (秒),默认0(立即完成)
- stir_speed: 搅拌速度 (RPM),默认300 RPM
+ 支持两种溶解模式:
+ 1. 液体溶解:指定 solvent + volume,使用pump protocol转移溶剂
+ 2. 固体溶解:指定 mass/mol + reagent,使用固体加样器添加固体试剂
- Returns:
- List[Dict[str, Any]]: 溶解操作的动作序列
+ 支持所有XDL参数和单位:
+ - volume: "10 mL", "?" 或数值
+ - mass: "2.9 g", "?" 或数值
+ - temp: "60 °C", "room temperature", "?" 或数值
+ - time: "30 min", "1 h", "?" 或数值
+ - mol: "0.12 mol", "16.2 mmol"
+ """
- Raises:
- ValueError: 当找不到必要的设备或容器时
+ debug_print("=" * 60)
+ debug_print("开始生成溶解协议 - 修复版")
+ debug_print(f"原始参数:")
+ debug_print(f" - vessel: '{vessel}'")
+ debug_print(f" - solvent: '{solvent}'")
+ debug_print(f" - volume: {volume} (类型: {type(volume)})")
+ debug_print(f" - mass: {mass} (类型: {type(mass)})")
+ debug_print(f" - temp: {temp} (类型: {type(temp)})")
+ debug_print(f" - time: {time} (类型: {type(time)})")
+ debug_print(f" - reagent: '{reagent}'")
+ debug_print(f" - mol: '{mol}'")
+ debug_print(f" - event: '{event}'")
+ debug_print(f" - kwargs: {kwargs}") # 显示额外参数
+ debug_print("=" * 60)
- Examples:
- dissolve_actions = generate_dissolve_protocol(G, "reaction_mixture", "DMF", 10.0, "NaCl 2g", 60.0, 600.0, 400.0)
- """
action_sequence = []
- print(f"DISSOLVE: 开始生成溶解协议")
- print(f" - 目标容器: {vessel}")
- print(f" - 溶剂: {solvent}")
- print(f" - 溶剂体积: {volume} mL")
- print(f" - 要溶解的物质: {amount}")
- print(f" - 溶解温度: {temp}°C")
- print(f" - 溶解时间: {time}s ({time/60:.1f}分钟)" if time > 0 else " - 溶解时间: 立即完成")
- print(f" - 搅拌速度: {stir_speed} RPM")
+ # === 参数验证 ===
+ debug_print("步骤1: 参数验证...")
+
+ if not vessel:
+ raise ValueError("vessel 参数不能为空")
- # 验证目标容器存在
if vessel not in G.nodes():
- raise ValueError(f"目标容器 '{vessel}' 不存在于系统中")
+ raise ValueError(f"容器 '{vessel}' 不存在于系统中")
- # 查找溶剂瓶
- try:
- solvent_vessel = find_solvent_vessel(G, solvent)
- print(f"DISSOLVE: 找到溶剂瓶: {solvent_vessel}")
- except ValueError as e:
- raise ValueError(f"无法找到溶剂 '{solvent}': {str(e)}")
+ debug_print("✅ 基本参数验证通过")
- # 验证是否存在从溶剂瓶到目标容器的路径
- try:
- path = nx.shortest_path(G, source=solvent_vessel, target=vessel)
- print(f"DISSOLVE: 找到路径 {solvent_vessel} -> {vessel}: {path}")
- except nx.NetworkXNoPath:
- raise ValueError(f"从溶剂瓶 '{solvent_vessel}' 到目标容器 '{vessel}' 没有可用路径")
+ # === 🔧 关键修复:参数解析 ===
+ debug_print("步骤2: 参数解析...")
+
+ # 解析各种参数为数值
+ final_volume = parse_volume_input(volume)
+ final_mass = parse_mass_input(mass)
+ final_temp = parse_temperature_input(temp)
+ final_time = parse_time_input(time)
+
+ debug_print(f"解析结果:")
+ debug_print(f" - 体积: {final_volume}mL")
+ debug_print(f" - 质量: {final_mass}g")
+ debug_print(f" - 温度: {final_temp}°C")
+ debug_print(f" - 时间: {final_time}s")
+ debug_print(f" - 试剂: '{reagent}'")
+ debug_print(f" - 摩尔: '{mol}'")
+ debug_print(f" - 事件: '{event}'")
+
+ # === 判断溶解类型 ===
+ debug_print("步骤3: 判断溶解类型...")
+
+ # 判断是固体溶解还是液体溶解
+ is_solid_dissolve = (final_mass > 0 or (mol and mol.strip() != "") or (reagent and reagent.strip() != ""))
+ is_liquid_dissolve = (final_volume > 0 and solvent and solvent.strip() != "")
+
+ if not is_solid_dissolve and not is_liquid_dissolve:
+ # 默认为液体溶解,50mL
+ is_liquid_dissolve = True
+ final_volume = 50.0
+ if not solvent:
+ solvent = "water" # 默认溶剂
+ debug_print("⚠️ 未明确指定溶解参数,默认为50mL水溶解")
+
+ debug_print(f"溶解类型: {'固体溶解' if is_solid_dissolve else '液体溶解'}")
+
+ # === 查找设备 ===
+ debug_print("步骤4: 查找设备...")
# 查找加热搅拌器
- heatchill_id = None
- if temp > 25.0 or stir_speed > 0 or time > 0:
- try:
- heatchill_id = find_connected_heatchill(G, vessel)
- if heatchill_id:
- print(f"DISSOLVE: 找到加热搅拌器: {heatchill_id}")
- else:
- print(f"DISSOLVE: 警告 - 需要加热/搅拌但未找到与容器 {vessel} 相连的加热搅拌器")
- except Exception as e:
- print(f"DISSOLVE: 加热搅拌器配置出错: {str(e)}")
+ heatchill_id = find_connected_heatchill(G, vessel)
+ stirrer_id = find_connected_stirrer(G, vessel)
- # === 第一步:启动加热搅拌(在添加溶剂前) ===
- if heatchill_id and (temp > 25.0 or time > 0):
- print(f"DISSOLVE: 启动加热搅拌器,温度: {temp}°C")
-
- if time > 0:
- # 如果指定了时间,使用定时加热搅拌
- heatchill_action = {
- "device_id": heatchill_id,
- "action_name": "heat_chill",
- "action_kwargs": {
- "vessel": vessel,
- "temp": temp,
- "time": time,
- "stir": True,
- "stir_speed": stir_speed,
- "purpose": f"溶解 {amount} 在 {solvent} 中"
+ # 优先使用加热搅拌器,否则使用独立搅拌器
+ stir_device_id = heatchill_id or stirrer_id
+
+ debug_print(f"设备映射:")
+ debug_print(f" - 加热器: '{heatchill_id}'")
+ debug_print(f" - 搅拌器: '{stirrer_id}'")
+ debug_print(f" - 使用设备: '{stir_device_id}'")
+
+ # === 执行溶解流程 ===
+ debug_print("步骤5: 执行溶解流程...")
+
+ try:
+ # 步骤5.1: 启动加热搅拌(如果需要)
+ if stir_device_id and (final_temp > 25.0 or final_time > 0 or stir_speed > 0):
+ debug_print(f"5.1: 启动加热搅拌,温度: {final_temp}°C")
+
+ if heatchill_id and (final_temp > 25.0 or final_time > 0):
+ # 使用加热搅拌器
+ heatchill_action = {
+ "device_id": heatchill_id,
+ "action_name": "heat_chill_start",
+ "action_kwargs": {
+ "vessel": vessel,
+ "temp": final_temp,
+ "purpose": f"溶解准备 - {event}" if event else "溶解准备"
+ }
}
- }
- else:
- # 如果没有指定时间,使用持续加热搅拌
- heatchill_action = {
- "device_id": heatchill_id,
- "action_name": "heat_chill_start",
- "action_kwargs": {
- "vessel": vessel,
- "temp": temp,
- "purpose": f"溶解 {amount} 在 {solvent} 中"
+ action_sequence.append(heatchill_action)
+
+ # 等待温度稳定
+ if final_temp > 25.0:
+ wait_time = min(60, abs(final_temp - 25.0) * 1.5)
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": wait_time}
+ })
+
+ elif stirrer_id:
+ # 使用独立搅拌器
+ stir_action = {
+ "device_id": stirrer_id,
+ "action_name": "start_stir",
+ "action_kwargs": {
+ "vessel": vessel,
+ "stir_speed": stir_speed,
+ "purpose": f"溶解搅拌 - {event}" if event else "溶解搅拌"
+ }
}
- }
-
- action_sequence.append(heatchill_action)
-
- # 等待温度稳定
- if temp > 25.0:
- wait_time = min(60, abs(temp - 25.0) * 1.5) # 根据温差估算预热时间
- action_sequence.append({
- "action_name": "wait",
- "action_kwargs": {"time": wait_time}
- })
-
- # === 第二步:添加溶剂到目标容器 ===
- if volume > 0:
- print(f"DISSOLVE: 将 {volume} mL {solvent} 从 {solvent_vessel} 转移到 {vessel}")
+ action_sequence.append(stir_action)
+
+ # 等待搅拌稳定
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": 5}
+ })
- # 计算流速 - 溶解时通常用较慢的速度,避免飞溅
- transfer_flowrate = 1.0 # 较慢的转移速度
- flowrate = 0.5 # 较慢的注入速度
+ if is_solid_dissolve:
+ # === 固体溶解路径 ===
+ debug_print(f"5.2: 使用固体溶解路径")
+
+ solid_dispenser = find_solid_dispenser(G)
+ if solid_dispenser:
+ # 固体加样
+ add_kwargs = {
+ "vessel": vessel,
+ "reagent": reagent or amount or "solid reagent",
+ "purpose": f"溶解固体试剂 - {event}" if event else "溶解固体试剂",
+ "event": event
+ }
+
+ if final_mass > 0:
+ add_kwargs["mass"] = str(final_mass)
+ if mol and mol.strip():
+ add_kwargs["mol"] = mol
+
+ action_sequence.append({
+ "device_id": solid_dispenser,
+ "action_name": "add_solid",
+ "action_kwargs": add_kwargs
+ })
+
+ debug_print(f"✅ 固体加样完成")
+ else:
+ debug_print("⚠️ 未找到固体加样器,跳过固体添加")
- try:
- # 使用成熟的 pump_protocol 算法进行液体转移
- pump_actions = generate_pump_protocol(
- G=G,
- from_vessel=solvent_vessel,
- to_vessel=vessel,
- volume=volume,
- flowrate=flowrate, # 注入速度 - 较慢避免飞溅
- transfer_flowrate=transfer_flowrate # 转移速度
- )
+ elif is_liquid_dissolve:
+ # === 液体溶解路径 ===
+ debug_print(f"5.3: 使用液体溶解路径")
- action_sequence.extend(pump_actions)
+ # 查找溶剂容器
+ try:
+ solvent_vessel = find_solvent_vessel(G, solvent)
+ except ValueError as e:
+ debug_print(f"⚠️ {str(e)},跳过溶剂添加")
+ solvent_vessel = None
- except Exception as e:
- raise ValueError(f"生成泵协议时出错: {str(e)}")
+ if solvent_vessel:
+ # 计算流速 - 溶解时通常用较慢的速度,避免飞溅
+ flowrate = 1.0 # 较慢的注入速度
+ transfer_flowrate = 0.5 # 较慢的转移速度
+
+ # 调用pump protocol
+ pump_actions = generate_pump_protocol_with_rinsing(
+ G=G,
+ from_vessel=solvent_vessel,
+ to_vessel=vessel,
+ volume=final_volume,
+ amount=amount,
+ time=0.0, # 不在pump level控制时间
+ viscous=False,
+ rinsing_solvent="",
+ rinsing_volume=0.0,
+ rinsing_repeats=0,
+ solid=False,
+ flowrate=flowrate,
+ transfer_flowrate=transfer_flowrate,
+ rate_spec="",
+ event=event,
+ through="",
+ **kwargs
+ )
+ action_sequence.extend(pump_actions)
+ debug_print(f"✅ 溶剂转移完成,添加了 {len(pump_actions)} 个动作")
+
+ # 溶剂添加后等待
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": 5}
+ })
- # 溶剂添加后等待
- action_sequence.append({
- "action_name": "wait",
- "action_kwargs": {"time": 5}
- })
-
- # === 第三步:如果没有使用定时加热搅拌,但需要等待溶解 ===
- if time > 0 and heatchill_id and temp <= 25.0:
- # 只需要搅拌等待,不需要加热
- print(f"DISSOLVE: 室温搅拌 {time}s 等待溶解")
+ # 步骤5.4: 等待溶解完成
+ if final_time > 0:
+ debug_print(f"5.4: 等待溶解完成 - {final_time}s")
+
+ if heatchill_id:
+ # 使用定时加热搅拌
+ dissolve_action = {
+ "device_id": heatchill_id,
+ "action_name": "heat_chill",
+ "action_kwargs": {
+ "vessel": vessel,
+ "temp": final_temp,
+ "time": final_time,
+ "stir": True,
+ "stir_speed": stir_speed,
+ "purpose": f"溶解等待 - {event}" if event else "溶解等待"
+ }
+ }
+ action_sequence.append(dissolve_action)
+
+ elif stirrer_id:
+ # 使用定时搅拌
+ stir_action = {
+ "device_id": stirrer_id,
+ "action_name": "stir",
+ "action_kwargs": {
+ "vessel": vessel,
+ "stir_time": final_time,
+ "stir_speed": stir_speed,
+ "settling_time": 0,
+ "purpose": f"溶解搅拌 - {event}" if event else "溶解搅拌"
+ }
+ }
+ action_sequence.append(stir_action)
+
+ else:
+ # 简单等待
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": final_time}
+ })
- stir_action = {
- "device_id": heatchill_id,
- "action_name": "heat_chill",
- "action_kwargs": {
- "vessel": vessel,
- "temp": 25.0, # 室温
- "time": time,
- "stir": True,
- "stir_speed": stir_speed,
- "purpose": f"室温搅拌溶解 {amount}"
+ # 步骤5.5: 停止加热搅拌(如果需要)
+ if heatchill_id and final_time == 0 and final_temp > 25.0:
+ debug_print(f"5.5: 停止加热器")
+
+ stop_action = {
+ "device_id": heatchill_id,
+ "action_name": "heat_chill_stop",
+ "action_kwargs": {
+ "vessel": vessel
+ }
}
- }
- action_sequence.append(stir_action)
-
- # === 第四步:如果使用了持续加热,需要手动停止 ===
- if heatchill_id and time == 0 and temp > 25.0:
- print(f"DISSOLVE: 停止加热搅拌器")
+ action_sequence.append(stop_action)
- stop_action = {
- "device_id": heatchill_id,
- "action_name": "heat_chill_stop",
+ except Exception as e:
+ debug_print(f"⚠️ 溶解流程执行失败: {str(e)}")
+ # 添加错误日志
+ action_sequence.append({
+ "device_id": "system",
+ "action_name": "log_message",
"action_kwargs": {
- "vessel": vessel
+ "message": f"溶解失败: {str(e)}"
}
- }
- action_sequence.append(stop_action)
+ })
- print(f"DISSOLVE: 生成了 {len(action_sequence)} 个动作")
- print(f"DISSOLVE: 溶解协议生成完成")
+ # === 最终结果 ===
+ debug_print("=" * 60)
+ debug_print(f"✅ 溶解协议生成完成")
+ debug_print(f"📊 总动作数: {len(action_sequence)}")
+ debug_print(f"📋 处理总结:")
+ debug_print(f" - 容器: {vessel}")
+ debug_print(f" - 溶解类型: {'固体溶解' if is_solid_dissolve else '液体溶解'}")
+ if is_liquid_dissolve:
+ debug_print(f" - 溶剂: {solvent} ({final_volume}mL)")
+ if is_solid_dissolve:
+ debug_print(f" - 试剂: {reagent}")
+ debug_print(f" - 质量: {final_mass}g")
+ debug_print(f" - 摩尔: {mol}")
+ debug_print(f" - 温度: {final_temp}°C")
+ debug_print(f" - 时间: {final_time}s")
+ debug_print("=" * 60)
return action_sequence
+# === 便捷函数 ===
-# 便捷函数:常用溶解方案
-def generate_room_temp_dissolve_protocol(
- G: nx.DiGraph,
- vessel: str,
- solvent: str,
- volume: float,
- amount: str = "",
- stir_time: float = 300.0 # 5分钟
-) -> List[Dict[str, Any]]:
- """室温溶解:快速搅拌,短时间"""
- return generate_dissolve_protocol(G, vessel, solvent, volume, amount, 25.0, stir_time, 400.0)
-
-
-def generate_heated_dissolve_protocol(
- G: nx.DiGraph,
- vessel: str,
- solvent: str,
- volume: float,
- amount: str = "",
- temp: float = 60.0,
- dissolve_time: float = 900.0 # 15分钟
-) -> List[Dict[str, Any]]:
- """加热溶解:中等温度,较长时间"""
- return generate_dissolve_protocol(G, vessel, solvent, volume, amount, temp, dissolve_time, 300.0)
-
+def dissolve_solid_by_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, float],
+ temp: Union[str, float] = 25.0, time: Union[str, float] = "10 min") -> List[Dict[str, Any]]:
+ """按质量溶解固体"""
+ return generate_dissolve_protocol(
+ G, vessel,
+ mass=mass,
+ reagent=reagent,
+ temp=temp,
+ time=time
+ )
-def generate_gentle_dissolve_protocol(
- G: nx.DiGraph,
- vessel: str,
- solvent: str,
- volume: float,
- amount: str = "",
- temp: float = 40.0,
- dissolve_time: float = 1800.0 # 30分钟
-) -> List[Dict[str, Any]]:
- """温和溶解:低温,长时间,慢搅拌"""
- return generate_dissolve_protocol(G, vessel, solvent, volume, amount, temp, dissolve_time, 200.0)
+def dissolve_solid_by_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str,
+ temp: Union[str, float] = 25.0, time: Union[str, float] = "10 min") -> List[Dict[str, Any]]:
+ """按摩尔数溶解固体"""
+ return generate_dissolve_protocol(
+ G, vessel,
+ mol=mol,
+ reagent=reagent,
+ temp=temp,
+ time=time
+ )
+def dissolve_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float],
+ temp: Union[str, float] = 25.0, time: Union[str, float] = "5 min") -> List[Dict[str, Any]]:
+ """用溶剂溶解"""
+ return generate_dissolve_protocol(
+ G, vessel,
+ solvent=solvent,
+ volume=volume,
+ temp=temp,
+ time=time
+ )
-def generate_hot_dissolve_protocol(
- G: nx.DiGraph,
- vessel: str,
- solvent: str,
- volume: float,
- amount: str = "",
- temp: float = 80.0,
- dissolve_time: float = 600.0 # 10分钟
-) -> List[Dict[str, Any]]:
- """高温溶解:高温,中等时间,快搅拌"""
- return generate_dissolve_protocol(G, vessel, solvent, volume, amount, temp, dissolve_time, 500.0)
+def dissolve_at_room_temp(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float]) -> List[Dict[str, Any]]:
+ """室温溶解"""
+ return generate_dissolve_protocol(
+ G, vessel,
+ solvent=solvent,
+ volume=volume,
+ temp="room temperature",
+ time="5 min"
+ )
+def dissolve_with_heating(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float],
+ temp: Union[str, float] = "60 °C", time: Union[str, float] = "15 min") -> List[Dict[str, Any]]:
+ """加热溶解"""
+ return generate_dissolve_protocol(
+ G, vessel,
+ solvent=solvent,
+ volume=volume,
+ temp=temp,
+ time=time
+ )
-def generate_sequential_dissolve_protocol(
- G: nx.DiGraph,
- vessel: str,
- dissolve_steps: List[Dict[str, Any]]
-) -> List[Dict[str, Any]]:
- """
- 生成连续溶解多种物质的协议
+# 测试函数
+def test_dissolve_protocol():
+ """测试溶解协议的各种参数解析"""
+ print("=== DISSOLVE PROTOCOL 修复版测试 ===")
- Args:
- G: 网络图
- vessel: 目标容器
- dissolve_steps: 溶解步骤列表,每个元素包含溶解参数
+ # 测试体积解析
+ volumes = ["10 mL", "?", 10.0, "1 L", "500 μL"]
+ for vol in volumes:
+ result = parse_volume_input(vol)
+ print(f"体积解析: {vol} → {result}mL")
- Returns:
- List[Dict[str, Any]]: 完整的动作序列
-
- Example:
- dissolve_steps = [
- {
- "solvent": "water",
- "volume": 5.0,
- "amount": "NaCl 1g",
- "temp": 25.0,
- "time": 300.0,
- "stir_speed": 300.0
- },
- {
- "solvent": "ethanol",
- "volume": 2.0,
- "amount": "organic compound 0.5g",
- "temp": 40.0,
- "time": 600.0,
- "stir_speed": 400.0
- }
- ]
- """
- action_sequence = []
+ # 测试质量解析
+ masses = ["2.9 g", "?", 2.5, "500 mg"]
+ for mass in masses:
+ result = parse_mass_input(mass)
+ print(f"质量解析: {mass} → {result}g")
- for i, step in enumerate(dissolve_steps):
- print(f"DISSOLVE: 处理第 {i+1}/{len(dissolve_steps)} 个溶解步骤")
-
- # 生成单个溶解步骤的协议
- dissolve_actions = generate_dissolve_protocol(
- G=G,
- vessel=vessel,
- solvent=step.get('solvent'),
- volume=step.get('volume', 0.0),
- amount=step.get('amount', ''),
- temp=step.get('temp', 25.0),
- time=step.get('time', 0.0),
- stir_speed=step.get('stir_speed', 300.0)
- )
-
- action_sequence.extend(dissolve_actions)
-
- # 在步骤之间加入等待时间
- if i < len(dissolve_steps) - 1: # 不是最后一个步骤
- action_sequence.append({
- "action_name": "wait",
- "action_kwargs": {"time": 10}
- })
-
- print(f"DISSOLVE: 连续溶解协议生成完成,共 {len(action_sequence)} 个动作")
- return action_sequence
-
-
-# 测试函数
-def test_dissolve_protocol():
- """测试溶解协议的示例"""
- print("=== DISSOLVE PROTOCOL 测试 ===")
- print("测试完成")
-
+ # 测试温度解析
+ temps = ["60 °C", "room temperature", "?", 25.0, "reflux"]
+ for temp in temps:
+ result = parse_temperature_input(temp)
+ print(f"温度解析: {temp} → {result}°C")
+
+ # 测试时间解析
+ times = ["30 min", "1 h", "?", 60.0]
+ for time in times:
+ result = parse_time_input(time)
+ print(f"时间解析: {time} → {result}s")
+
+ print("✅ 测试完成")
if __name__ == "__main__":
test_dissolve_protocol()
\ No newline at end of file
diff --git a/unilabos/compile/evacuateandrefill_protocol.py b/unilabos/compile/evacuateandrefill_protocol.py
index e5e8df5c4..c50b88492 100644
--- a/unilabos/compile/evacuateandrefill_protocol.py
+++ b/unilabos/compile/evacuateandrefill_protocol.py
@@ -1,8 +1,16 @@
-import numpy as np
import networkx as nx
+import logging
+import uuid # 🔧 移到顶部
from typing import List, Dict, Any, Optional
from .pump_protocol import generate_pump_protocol_with_rinsing, generate_pump_protocol
+# 设置日志
+logger = logging.getLogger(__name__)
+
+def debug_print(message):
+ """调试输出函数"""
+ print(f"[EVACUATE_REFILL] {message}", flush=True)
+ logger.info(f"[EVACUATE_REFILL] {message}")
def find_gas_source(G: nx.DiGraph, gas: str) -> str:
"""
@@ -11,7 +19,7 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str:
2. 气体类型匹配(data.gas_type)
3. 默认气源
"""
- print(f"EVACUATE_REFILL: 正在查找气体 '{gas}' 的气源...")
+ debug_print(f"正在查找气体 '{gas}' 的气源...")
# 第一步:通过容器名称匹配
gas_source_patterns = [
@@ -26,7 +34,7 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str:
for pattern in gas_source_patterns:
if pattern in G.nodes():
- print(f"EVACUATE_REFILL: 通过名称匹配找到气源: {pattern}")
+ debug_print(f"通过名称匹配找到气源: {pattern}")
return pattern
# 第二步:通过气体类型匹配 (data.gas_type)
@@ -44,7 +52,7 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str:
gas_type = data.get('gas_type', '')
if gas_type.lower() == gas.lower():
- print(f"EVACUATE_REFILL: 通过气体类型匹配找到气源: {node_id} (gas_type: {gas_type})")
+ debug_print(f"通过气体类型匹配找到气源: {node_id} (gas_type: {gas_type})")
return node_id
# 检查 config.gas_type
@@ -52,7 +60,7 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str:
config_gas_type = config.get('gas_type', '')
if config_gas_type.lower() == gas.lower():
- print(f"EVACUATE_REFILL: 通过配置气体类型匹配找到气源: {node_id} (config.gas_type: {config_gas_type})")
+ debug_print(f"通过配置气体类型匹配找到气源: {node_id} (config.gas_type: {config_gas_type})")
return node_id
# 第三步:查找所有可用的气源设备
@@ -69,139 +77,138 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str:
gas_type = data.get('gas_type', 'unknown')
available_gas_sources.append(f"{node_id} (gas_type: {gas_type})")
- print(f"EVACUATE_REFILL: 可用气源列表: {available_gas_sources}")
+ debug_print(f"可用气源列表: {available_gas_sources}")
# 第四步:如果找不到特定气体,使用默认的第一个气源
default_gas_sources = [
node for node in G.nodes()
- if ((G.nodes[node].get('class') or '').startswith('virtual_gas_source')
+ if ((G.nodes[node].get('class') or '').find('virtual_gas_source') != -1
or 'gas_source' in node)
]
if default_gas_sources:
default_source = default_gas_sources[0]
- print(f"EVACUATE_REFILL: ⚠️ 未找到特定气体 '{gas}',使用默认气源: {default_source}")
+ debug_print(f"⚠️ 未找到特定气体 '{gas}',使用默认气源: {default_source}")
return default_source
raise ValueError(f"找不到气体 '{gas}' 对应的气源。可用气源: {available_gas_sources}")
-
-def find_gas_source_by_any_match(G: nx.DiGraph, gas: str) -> str:
- """
- 增强版气源查找,支持各种匹配方式的别名函数
- """
- return find_gas_source(G, gas)
-
-
-def get_gas_source_type(G: nx.DiGraph, gas_source: str) -> str:
- """获取气源的气体类型"""
- if gas_source not in G.nodes():
- return "unknown"
-
- node_data = G.nodes[gas_source]
- data = node_data.get('data', {})
- config = node_data.get('config', {})
-
- # 检查多个可能的字段
- gas_type = (data.get('gas_type') or
- config.get('gas_type') or
- data.get('gas') or
- config.get('gas') or
- "air") # 默认为空气
-
- return gas_type
-
-
-def find_vessels_by_gas_type(G: nx.DiGraph, gas: str) -> List[str]:
- """
- 根据气体类型查找所有匹配的容器/气源
- """
- matching_vessels = []
-
- for node_id in G.nodes():
- node_data = G.nodes[node_id]
-
- # 检查容器名称匹配
- if gas.lower() in node_id.lower():
- matching_vessels.append(f"{node_id} (名称匹配)")
- continue
-
- # 检查气体类型匹配
- data = node_data.get('data', {})
- config = node_data.get('config', {})
-
- gas_type = data.get('gas_type', '') or config.get('gas_type', '')
- if gas_type.lower() == gas.lower():
- matching_vessels.append(f"{node_id} (gas_type: {gas_type})")
-
- return matching_vessels
-
-
def find_vacuum_pump(G: nx.DiGraph) -> str:
"""查找真空泵设备"""
- vacuum_pumps = [
- node for node in G.nodes()
- if ((G.nodes[node].get('class') or '').startswith('virtual_vacuum_pump')
- or 'vacuum_pump' in node
- or 'vacuum' in (G.nodes[node].get('class') or ''))
- ]
+ debug_print("查找真空泵设备...")
+
+ vacuum_pumps = []
+ for node in G.nodes():
+ node_data = G.nodes[node]
+ node_class = node_data.get('class', '') or ''
+
+ if ('virtual_vacuum_pump' in node_class or
+ 'vacuum_pump' in node.lower() or
+ 'vacuum' in node_class.lower()):
+ vacuum_pumps.append(node)
if not vacuum_pumps:
raise ValueError("系统中未找到真空泵设备")
+ debug_print(f"找到真空泵: {vacuum_pumps[0]}")
return vacuum_pumps[0]
-
-def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
+def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> Optional[str]:
"""查找与指定容器相连的搅拌器"""
- stirrer_nodes = [node for node in G.nodes()
- if (G.nodes[node].get('class') or '') == 'virtual_stirrer']
+ debug_print(f"查找与容器 {vessel} 相连的搅拌器...")
+
+ stirrer_nodes = []
+ for node in G.nodes():
+ node_data = G.nodes[node]
+ node_class = node_data.get('class', '') or ''
+
+ if 'virtual_stirrer' in node_class or 'stirrer' in node.lower():
+ stirrer_nodes.append(node)
# 检查哪个搅拌器与目标容器相连
for stirrer in stirrer_nodes:
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
+ debug_print(f"找到连接的搅拌器: {stirrer}")
return stirrer
- return stirrer_nodes[0] if stirrer_nodes else None
-
+ # 如果没有连接的搅拌器,返回第一个可用的
+ if stirrer_nodes:
+ debug_print(f"未找到直接连接的搅拌器,使用第一个可用的: {stirrer_nodes[0]}")
+ return stirrer_nodes[0]
+
+ debug_print("未找到搅拌器")
+ return None
-def find_associated_solenoid_valve(G: nx.DiGraph, device_id: str) -> Optional[str]:
- """查找与指定设备相关联的电磁阀"""
- solenoid_valves = [
- node for node in G.nodes()
- if ('solenoid' in (G.nodes[node].get('class') or '').lower()
- or 'solenoid_valve' in node)
- ]
+def find_vacuum_solenoid_valve(G: nx.DiGraph, vacuum_pump: str) -> Optional[str]:
+ """查找真空泵相关的电磁阀 - 根据实际连接逻辑"""
+ debug_print(f"查找真空泵 {vacuum_pump} 相关的电磁阀...")
+
+ # 查找所有电磁阀
+ solenoid_valves = []
+ for node in G.nodes():
+ node_data = G.nodes[node]
+ node_class = node_data.get('class', '') or ''
+
+ if ('solenoid' in node_class.lower() or 'solenoid_valve' in node.lower()):
+ solenoid_valves.append(node)
- # 通过网络连接查找直接相连的电磁阀
+ debug_print(f"找到的电磁阀: {solenoid_valves}")
+
+ # 🔧 修复:根据实际组态图连接逻辑查找
+ # vacuum_pump_1 <- solenoid_valve_1 <- multiway_valve_2
for solenoid in solenoid_valves:
- if G.has_edge(device_id, solenoid) or G.has_edge(solenoid, device_id):
+ # 检查电磁阀是否连接到真空泵
+ if G.has_edge(solenoid, vacuum_pump) or G.has_edge(vacuum_pump, solenoid):
+ debug_print(f"✓ 找到连接真空泵的电磁阀: {solenoid}")
return solenoid
- # 通过命名规则查找关联的电磁阀
- device_type = ""
- if 'vacuum' in device_id.lower():
- device_type = "vacuum"
- elif 'gas' in device_id.lower():
- device_type = "gas"
-
- if device_type:
- for solenoid in solenoid_valves:
- if device_type in solenoid.lower():
- return solenoid
+ # 通过命名规则查找(备选方案)
+ for solenoid in solenoid_valves:
+ if 'vacuum' in solenoid.lower() or solenoid == 'solenoid_valve_1':
+ debug_print(f"✓ 通过命名规则找到真空电磁阀: {solenoid}")
+ return solenoid
+ debug_print("⚠️ 未找到真空电磁阀")
return None
+def find_gas_solenoid_valve(G: nx.DiGraph, gas_source: str) -> Optional[str]:
+ """查找气源相关的电磁阀 - 根据实际连接逻辑"""
+ debug_print(f"查找气源 {gas_source} 相关的电磁阀...")
+
+ # 查找所有电磁阀
+ solenoid_valves = []
+ for node in G.nodes():
+ node_data = G.nodes[node]
+ node_class = node_data.get('class', '') or ''
+
+ if ('solenoid' in node_class.lower() or 'solenoid_valve' in node.lower()):
+ solenoid_valves.append(node)
+
+ # 🔧 修复:根据实际组态图连接逻辑查找
+ # gas_source_1 -> solenoid_valve_2 -> multiway_valve_2
+ for solenoid in solenoid_valves:
+ # 检查气源是否连接到电磁阀
+ if G.has_edge(gas_source, solenoid) or G.has_edge(solenoid, gas_source):
+ debug_print(f"✓ 找到连接气源的电磁阀: {solenoid}")
+ return solenoid
+
+ # 通过命名规则查找(备选方案)
+ for solenoid in solenoid_valves:
+ if 'gas' in solenoid.lower() or solenoid == 'solenoid_valve_2':
+ debug_print(f"✓ 通过命名规则找到气源电磁阀: {solenoid}")
+ return solenoid
+
+ debug_print("⚠️ 未找到气源电磁阀")
+ return None
def generate_evacuateandrefill_protocol(
G: nx.DiGraph,
vessel: str,
gas: str,
- # 🔧 删除 repeats 参数,直接硬编码为 3
- **kwargs # 🔧 接受额外参数,增强兼容性
+ **kwargs
) -> List[Dict[str, Any]]:
"""
- 生成抽真空和充气操作的动作序列 - 简化版本
+ 生成抽真空和充气操作的动作序列 - 最终修复版本
Args:
G: 设备图
@@ -213,9 +220,13 @@ def generate_evacuateandrefill_protocol(
List[Dict[str, Any]]: 动作序列
"""
- # 🔧 硬编码重复次数为 3
+ # 硬编码重复次数为 3
repeats = 3
+ # 🔧 修复:在函数开始就生成协议ID
+ protocol_id = str(uuid.uuid4())
+ debug_print(f"生成协议ID: {protocol_id}")
+
debug_print("=" * 60)
debug_print("开始生成抽真空充气协议")
debug_print(f"输入参数:")
@@ -264,8 +275,8 @@ def generate_evacuateandrefill_protocol(
try:
vacuum_pump = find_vacuum_pump(G)
gas_source = find_gas_source(G, gas)
- vacuum_solenoid = find_associated_solenoid_valve(G, vacuum_pump)
- gas_solenoid = find_associated_solenoid_valve(G, gas_source)
+ vacuum_solenoid = find_vacuum_solenoid_valve(G, vacuum_pump)
+ gas_solenoid = find_gas_solenoid_valve(G, gas_source)
stirrer_id = find_connected_stirrer(G, vessel)
debug_print(f"设备配置:")
@@ -319,20 +330,22 @@ def generate_evacuateandrefill_protocol(
debug_print("步骤4: 路径验证...")
try:
- # 验证抽真空路径
- vacuum_path = nx.shortest_path(G, source=vessel, target=vacuum_pump)
- debug_print(f"抽真空路径: {' → '.join(vacuum_path)}")
+ # 验证抽真空路径: vessel -> vacuum_pump (通过八通阀和电磁阀)
+ if nx.has_path(G, vessel, vacuum_pump):
+ vacuum_path = nx.shortest_path(G, source=vessel, target=vacuum_pump)
+ debug_print(f"抽真空路径: {' → '.join(vacuum_path)}")
+ else:
+ debug_print(f"⚠️ 抽真空路径不存在,继续执行但可能有问题")
- # 验证充气路径
- gas_path = nx.shortest_path(G, source=gas_source, target=vessel)
- debug_print(f"充气路径: {' → '.join(gas_path)}")
+ # 验证充气路径: gas_source -> vessel (通过电磁阀和八通阀)
+ if nx.has_path(G, gas_source, vessel):
+ gas_path = nx.shortest_path(G, source=gas_source, target=vessel)
+ debug_print(f"充气路径: {' → '.join(gas_path)}")
+ else:
+ debug_print(f"⚠️ 充气路径不存在,继续执行但可能有问题")
- except nx.NetworkXNoPath as e:
- debug_print(f"❌ 路径不存在: {str(e)}")
- raise ValueError(f"路径不存在: {str(e)}")
except Exception as e:
- debug_print(f"❌ 路径验证失败: {str(e)}")
- raise ValueError(f"路径验证失败: {str(e)}")
+ debug_print(f"⚠️ 路径验证失败: {str(e)},继续执行")
# === 启动搅拌器 ===
debug_print("步骤5: 启动搅拌器...")
@@ -360,7 +373,7 @@ def generate_evacuateandrefill_protocol(
# === 执行 3 次抽真空-充气循环 ===
debug_print("步骤6: 执行抽真空-充气循环...")
- for cycle in range(repeats): # 这里 repeats = 3
+ for cycle in range(repeats):
debug_print(f"=== 第 {cycle+1}/{repeats} 次循环 ===")
# ============ 抽真空阶段 ============
@@ -383,16 +396,17 @@ def generate_evacuateandrefill_protocol(
"action_kwargs": {"command": "OPEN"}
})
- # 抽真空操作
+ # 抽真空操作 - 使用液体转移协议
debug_print(f"抽真空操作: {vessel} → {vacuum_pump}")
try:
+
vacuum_transfer_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=vessel,
to_vessel=vacuum_pump,
volume=VACUUM_VOLUME,
amount="",
- duration=0.0, # 🔧 修复time参数名冲突
+ time=0.0,
viscous=False,
rinsing_solvent="",
rinsing_volume=0.0,
@@ -423,7 +437,7 @@ def generate_evacuateandrefill_protocol(
# 抽真空后等待
action_sequence.append({
"action_name": "wait",
- "action_kwargs": {"time": 5.0}
+ "action_kwargs": {"time": VACUUM_TIME}
})
# 关闭真空电磁阀
@@ -443,6 +457,12 @@ def generate_evacuateandrefill_protocol(
"action_kwargs": {"string": "OFF"}
})
+ # 抽真空后等待
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": 5.0}
+ })
+
# ============ 充气阶段 ============
debug_print(f"充气阶段开始")
@@ -463,16 +483,17 @@ def generate_evacuateandrefill_protocol(
"action_kwargs": {"command": "OPEN"}
})
- # 充气操作
+ # 充气操作 - 使用液体转移协议
debug_print(f"充气操作: {gas_source} → {vessel}")
try:
+
gas_transfer_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=gas_source,
to_vessel=vessel,
volume=REFILL_VOLUME,
amount="",
- duration=0.0, # 🔧 修复time参数名冲突
+ time=0.0,
viscous=False,
rinsing_solvent="",
rinsing_volume=0.0,
@@ -503,7 +524,7 @@ def generate_evacuateandrefill_protocol(
# 充气后等待
action_sequence.append({
"action_name": "wait",
- "action_kwargs": {"time": 5.0}
+ "action_kwargs": {"time": REFILL_TIME}
})
# 关闭气源电磁阀
@@ -559,12 +580,25 @@ def generate_evacuateandrefill_protocol(
return action_sequence
+# === 便捷函数 ===
+
+def generate_nitrogen_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]:
+ """生成氮气置换协议"""
+ return generate_evacuateandrefill_protocol(G, vessel, "nitrogen", **kwargs)
+
+def generate_argon_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]:
+ """生成氩气置换协议"""
+ return generate_evacuateandrefill_protocol(G, vessel, "argon", **kwargs)
+
+def generate_air_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]:
+ """生成空气置换协议"""
+ return generate_evacuateandrefill_protocol(G, vessel, "air", **kwargs)
+
# 测试函数
def test_evacuateandrefill_protocol():
"""测试抽真空充气协议"""
- print("=== EVACUATE AND REFILL PROTOCOL 测试 ===")
- print("测试完成")
-
+ debug_print("=== EVACUATE AND REFILL PROTOCOL 测试 ===")
+ debug_print("测试完成")
if __name__ == "__main__":
test_evacuateandrefill_protocol()
\ No newline at end of file
diff --git a/unilabos/compile/evaporate_protocol.py b/unilabos/compile/evaporate_protocol.py
index b5170b5a8..e3ffb8623 100644
--- a/unilabos/compile/evaporate_protocol.py
+++ b/unilabos/compile/evaporate_protocol.py
@@ -1,7 +1,6 @@
from typing import List, Dict, Any, Optional
import networkx as nx
import logging
-from .pump_protocol import generate_pump_protocol
logger = logging.getLogger(__name__)
@@ -10,100 +9,101 @@ def debug_print(message):
print(f"[EVAPORATE] {message}", flush=True)
logger.info(f"[EVAPORATE] {message}")
-def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:
- """获取容器中的液体体积"""
- debug_print(f"检查容器 '{vessel}' 的液体体积...")
-
- if vessel not in G.nodes():
- debug_print(f"容器 '{vessel}' 不存在")
- return 0.0
-
- vessel_data = G.nodes[vessel].get('data', {})
- debug_print(f"容器数据: {vessel_data}")
-
- # 检查多种体积字段
- volume_keys = ['total_volume', 'volume', 'liquid_volume', 'current_volume']
- for key in volume_keys:
- if key in vessel_data:
- try:
- volume = float(vessel_data[key])
- debug_print(f"从 '{key}' 读取到体积: {volume}mL")
- return volume
- except (ValueError, TypeError):
- continue
-
- # 检查liquid数组
- liquids = vessel_data.get('liquid', [])
- if isinstance(liquids, list):
- total_volume = 0.0
- for liquid in liquids:
- if isinstance(liquid, dict):
- for vol_key in ['liquid_volume', 'volume', 'amount']:
- if vol_key in liquid:
- try:
- vol = float(liquid[vol_key])
- total_volume += vol
- debug_print(f"从液体数据 '{vol_key}' 读取: {vol}mL")
- except (ValueError, TypeError):
- continue
- if total_volume > 0:
- return total_volume
-
- debug_print(f"未检测到液体体积,返回 0.0")
- return 0.0
-
-def find_rotavap_device(G: nx.DiGraph) -> Optional[str]:
- """查找旋转蒸发仪设备"""
- debug_print("查找旋转蒸发仪设备...")
-
- # 查找各种可能的旋转蒸发仪设备
- possible_devices = []
- for node in G.nodes():
- node_data = G.nodes[node]
- node_class = node_data.get('class', '')
-
- if any(keyword in node_class.lower() for keyword in ['rotavap', 'evaporator']):
- possible_devices.append(node)
- debug_print(f"找到旋转蒸发仪设备: {node}")
-
- if possible_devices:
- return possible_devices[0]
+def find_rotavap_device(G: nx.DiGraph, vessel: str = None) -> Optional[str]:
+ """
+ 在组态图中查找旋转蒸发仪设备
- debug_print("未找到旋转蒸发仪设备")
- return None
-
-def find_rotavap_vessel(G: nx.DiGraph) -> Optional[str]:
- """查找旋转蒸发仪样品容器"""
- debug_print("查找旋转蒸发仪样品容器...")
+ Args:
+ G: 设备图
+ vessel: 指定的设备名称(可选)
- possible_vessels = [
- "rotavap", "rotavap_flask", "flask_rotavap",
- "evaporation_flask", "evaporator", "rotary_evaporator"
- ]
+ Returns:
+ str: 找到的旋转蒸发仪设备ID,如果没找到返回None
+ """
+ debug_print("查找旋转蒸发仪设备...")
- for vessel in possible_vessels:
+ # 如果指定了vessel,先检查是否存在且是旋转蒸发仪
+ if vessel:
if vessel in G.nodes():
- debug_print(f"找到旋转蒸发仪样品容器: {vessel}")
- return vessel
-
- debug_print("未找到旋转蒸发仪样品容器")
+ node_data = G.nodes[vessel]
+ node_class = node_data.get('class', '')
+ node_type = node_data.get('type', '')
+
+ debug_print(f"检查指定设备 {vessel}: class={node_class}, type={node_type}")
+
+ # 检查是否为旋转蒸发仪
+ if any(keyword in str(node_class).lower() for keyword in ['rotavap', 'rotary', 'evaporat']):
+ debug_print(f"✓ 找到指定的旋转蒸发仪: {vessel}")
+ return vessel
+ elif node_type == 'device':
+ debug_print(f"✓ 指定设备存在,尝试直接使用: {vessel}")
+ return vessel
+ else:
+ debug_print(f"✗ 指定的设备 {vessel} 不存在")
+
+ # 在所有设备中查找旋转蒸发仪
+ rotavap_candidates = []
+
+ for node_id, node_data in G.nodes(data=True):
+ node_class = node_data.get('class', '')
+ node_type = node_data.get('type', '')
+
+ # 跳过非设备节点
+ if node_type != 'device':
+ continue
+
+ # 检查设备类型
+ if any(keyword in str(node_class).lower() for keyword in ['rotavap', 'rotary', 'evaporat']):
+ rotavap_candidates.append(node_id)
+ debug_print(f"✓ 找到旋转蒸发仪候选: {node_id} (class: {node_class})")
+ elif any(keyword in str(node_id).lower() for keyword in ['rotavap', 'rotary', 'evaporat']):
+ rotavap_candidates.append(node_id)
+ debug_print(f"✓ 找到旋转蒸发仪候选 (按名称): {node_id}")
+
+ if rotavap_candidates:
+ selected = rotavap_candidates[0] # 选择第一个找到的
+ debug_print(f"✓ 选择旋转蒸发仪: {selected}")
+ return selected
+
+ debug_print("✗ 未找到旋转蒸发仪设备")
return None
-def find_recovery_vessel(G: nx.DiGraph) -> Optional[str]:
- """查找溶剂回收容器"""
- debug_print("查找溶剂回收容器...")
+def find_connected_vessel(G: nx.DiGraph, rotavap_device: str) -> Optional[str]:
+ """
+ 查找与旋转蒸发仪连接的容器
- possible_vessels = [
- "flask_distillate", "distillate", "solvent_recovery",
- "rotavap_condenser", "condenser", "waste_workup", "waste"
- ]
+ Args:
+ G: 设备图
+ rotavap_device: 旋转蒸发仪设备ID
- for vessel in possible_vessels:
- if vessel in G.nodes():
- debug_print(f"找到回收容器: {vessel}")
- return vessel
+ Returns:
+ str: 连接的容器ID,如果没找到返回None
+ """
+ debug_print(f"查找与 {rotavap_device} 连接的容器...")
+
+ # 查看旋转蒸发仪的子设备
+ rotavap_data = G.nodes[rotavap_device]
+ children = rotavap_data.get('children', [])
+
+ for child_id in children:
+ if child_id in G.nodes():
+ child_data = G.nodes[child_id]
+ child_type = child_data.get('type', '')
+
+ if child_type == 'container':
+ debug_print(f"✓ 找到连接的容器: {child_id}")
+ return child_id
+
+ # 查看邻接的容器
+ for neighbor in G.neighbors(rotavap_device):
+ neighbor_data = G.nodes[neighbor]
+ neighbor_type = neighbor_data.get('type', '')
+
+ if neighbor_type == 'container':
+ debug_print(f"✓ 找到邻接的容器: {neighbor}")
+ return neighbor
- debug_print("未找到回收容器")
+ debug_print("✗ 未找到连接的容器")
return None
def generate_evaporate_protocol(
@@ -111,22 +111,22 @@ def generate_evaporate_protocol(
vessel: str,
pressure: float = 0.1,
temp: float = 60.0,
- time: float = 1800.0,
+ time: float = 180.0,
stir_speed: float = 100.0,
solvent: str = "",
**kwargs # 接受任意额外参数,增强兼容性
) -> List[Dict[str, Any]]:
"""
- 生成蒸发操作的协议序列 - 增强兼容性版本
+ 生成蒸发操作的协议序列
Args:
G: 设备图
- vessel: 蒸发容器名称(必需)
+ vessel: 容器名称或旋转蒸发仪名称
pressure: 真空度 (bar),默认0.1
temp: 加热温度 (°C),默认60
- time: 蒸发时间 (秒),默认1800
+ time: 蒸发时间 (秒),默认180
stir_speed: 旋转速度 (RPM),默认100
- solvent: 溶剂名称(可选,用于参数优化)
+ solvent: 溶剂名称(用于参数优化)
**kwargs: 其他参数(兼容性)
Returns:
@@ -142,20 +142,43 @@ def generate_evaporate_protocol(
debug_print(f" - time: {time}s ({time/60:.1f}分钟)")
debug_print(f" - stir_speed: {stir_speed} RPM")
debug_print(f" - solvent: '{solvent}'")
- debug_print(f" - 其他参数: {kwargs}")
debug_print("=" * 50)
- action_sequence = []
-
- # === 参数验证和修正 ===
- debug_print("步骤1: 参数验证和修正...")
+ # === 步骤1: 查找旋转蒸发仪设备 ===
+ debug_print("步骤1: 查找旋转蒸发仪设备...")
- # 验证必需参数
+ # 验证vessel参数
if not vessel:
raise ValueError("vessel 参数不能为空")
- if vessel not in G.nodes():
- raise ValueError(f"容器 '{vessel}' 不存在于系统中")
+ # 查找旋转蒸发仪设备
+ rotavap_device = find_rotavap_device(G, vessel)
+ if not rotavap_device:
+ raise ValueError(f"未找到旋转蒸发仪设备。请检查组态图中是否包含 class 包含 'rotavap'、'rotary' 或 'evaporat' 的设备")
+
+ # === 步骤2: 确定目标容器 ===
+ debug_print("步骤2: 确定目标容器...")
+
+ target_vessel = vessel
+
+ # 如果vessel就是旋转蒸发仪设备,查找连接的容器
+ if vessel == rotavap_device:
+ connected_vessel = find_connected_vessel(G, rotavap_device)
+ if connected_vessel:
+ target_vessel = connected_vessel
+ debug_print(f"使用连接的容器: {target_vessel}")
+ else:
+ debug_print(f"未找到连接的容器,使用设备本身: {rotavap_device}")
+ target_vessel = rotavap_device
+ elif vessel in G.nodes() and G.nodes[vessel].get('type') == 'container':
+ debug_print(f"使用指定的容器: {vessel}")
+ target_vessel = vessel
+ else:
+ debug_print(f"容器 '{vessel}' 不存在或类型不正确,使用旋转蒸发仪设备: {rotavap_device}")
+ target_vessel = rotavap_device
+
+ # === 步骤3: 参数验证和修正 ===
+ debug_print("步骤3: 参数验证和修正...")
# 修正参数范围
if pressure <= 0 or pressure > 1.0:
@@ -194,61 +217,10 @@ def generate_evaporate_protocol(
debug_print(f"最终参数: pressure={pressure}, temp={temp}, time={time}, stir_speed={stir_speed}")
- # === 查找设备 ===
- debug_print("步骤2: 查找设备...")
-
- # 查找旋转蒸发仪设备
- rotavap_device = find_rotavap_device(G)
- if not rotavap_device:
- debug_print("未找到旋转蒸发仪设备,使用通用设备")
- rotavap_device = "rotavap_1" # 默认设备ID
-
- # 查找旋转蒸发仪样品容器
- rotavap_vessel = find_rotavap_vessel(G)
- if not rotavap_vessel:
- debug_print("未找到旋转蒸发仪样品容器,使用默认容器")
- rotavap_vessel = "rotavap" # 默认容器
-
- # 查找回收容器
- recovery_vessel = find_recovery_vessel(G)
-
- debug_print(f"设备配置:")
- debug_print(f" - 旋转蒸发仪设备: {rotavap_device}")
- debug_print(f" - 样品容器: {rotavap_vessel}")
- debug_print(f" - 回收容器: {recovery_vessel}")
-
- # === 体积计算 ===
- debug_print("步骤3: 体积计算...")
-
- source_volume = get_vessel_liquid_volume(G, vessel)
-
- if source_volume > 0:
- transfer_volume = min(source_volume * 0.9, 250.0) # 90%或最多250mL
- debug_print(f"检测到液体体积 {source_volume}mL,转移 {transfer_volume}mL")
- else:
- transfer_volume = 50.0 # 默认小体积,更安全
- debug_print(f"未检测到液体体积,使用默认转移体积 {transfer_volume}mL")
-
- # === 生成动作序列 ===
+ # === 步骤4: 生成动作序列 ===
debug_print("步骤4: 生成动作序列...")
- # 动作1: 转移溶液到旋转蒸发仪
- if vessel != rotavap_vessel:
- debug_print(f"转移 {transfer_volume}mL 从 {vessel} 到 {rotavap_vessel}")
- try:
- transfer_actions = generate_pump_protocol(
- G=G,
- from_vessel=vessel,
- to_vessel=rotavap_vessel,
- volume=transfer_volume,
- flowrate=2.0,
- transfer_flowrate=2.0
- )
- action_sequence.extend(transfer_actions)
- debug_print(f"添加了 {len(transfer_actions)} 个转移动作")
- except Exception as e:
- debug_print(f"转移失败: {str(e)}")
- # 继续执行,不中断整个流程
+ action_sequence = []
# 等待稳定
action_sequence.append({
@@ -256,13 +228,13 @@ def generate_evaporate_protocol(
"action_kwargs": {"time": 10}
})
- # 动作2: 执行蒸发
- debug_print(f"执行蒸发: {rotavap_device}")
+ # 执行蒸发
+ debug_print(f"执行蒸发: 设备={rotavap_device}, 容器={target_vessel}")
evaporate_action = {
"device_id": rotavap_device,
"action_name": "evaporate",
"action_kwargs": {
- "vessel": rotavap_vessel,
+ "vessel": target_vessel,
"pressure": pressure,
"temp": temp,
"time": time,
@@ -278,47 +250,12 @@ def generate_evaporate_protocol(
"action_kwargs": {"time": 30}
})
- # 动作3: 回收溶剂(如果有回收容器)
- if recovery_vessel:
- debug_print(f"回收溶剂到 {recovery_vessel}")
- try:
- recovery_volume = transfer_volume * 0.7 # 估算回收70%
- recovery_actions = generate_pump_protocol(
- G=G,
- from_vessel="rotavap_condenser", # 假设的冷凝器
- to_vessel=recovery_vessel,
- volume=recovery_volume,
- flowrate=3.0,
- transfer_flowrate=3.0
- )
- action_sequence.extend(recovery_actions)
- debug_print(f"添加了 {len(recovery_actions)} 个回收动作")
- except Exception as e:
- debug_print(f"溶剂回收失败: {str(e)}")
-
- # 动作4: 转移浓缩物回原容器
- if vessel != rotavap_vessel:
- debug_print(f"转移浓缩物从 {rotavap_vessel} 到 {vessel}")
- try:
- concentrate_volume = transfer_volume * 0.2 # 估算浓缩物20%
- transfer_back_actions = generate_pump_protocol(
- G=G,
- from_vessel=rotavap_vessel,
- to_vessel=vessel,
- volume=concentrate_volume,
- flowrate=1.0, # 浓缩物可能粘稠
- transfer_flowrate=1.0
- )
- action_sequence.extend(transfer_back_actions)
- debug_print(f"添加了 {len(transfer_back_actions)} 个转移回收动作")
- except Exception as e:
- debug_print(f"浓缩物转移失败: {str(e)}")
-
# === 总结 ===
debug_print("=" * 50)
debug_print(f"蒸发协议生成完成")
debug_print(f"总动作数: {len(action_sequence)}")
- debug_print(f"处理体积: {transfer_volume}mL")
+ debug_print(f"旋转蒸发仪: {rotavap_device}")
+ debug_print(f"目标容器: {target_vessel}")
debug_print(f"蒸发参数: {pressure} bar, {temp}°C, {time}s, {stir_speed} RPM")
debug_print("=" * 50)
diff --git a/unilabos/compile/filter_protocol.py b/unilabos/compile/filter_protocol.py
index e673f2291..662968480 100644
--- a/unilabos/compile/filter_protocol.py
+++ b/unilabos/compile/filter_protocol.py
@@ -1,8 +1,7 @@
-from typing import List, Dict, Any
+from typing import List, Dict, Any, Optional
import networkx as nx
-from .pump_protocol import generate_pump_protocol
import logging
-import sys
+from .pump_protocol import generate_pump_protocol_with_rinsing
logger = logging.getLogger(__name__)
@@ -11,124 +10,64 @@ def debug_print(message):
print(f"[FILTER] {message}", flush=True)
logger.info(f"[FILTER] {message}")
-def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:
- """获取容器中的液体体积"""
- debug_print(f"检查容器 '{vessel}' 的液体体积...")
-
- if vessel not in G.nodes():
- debug_print(f"容器 '{vessel}' 不存在")
- return 0.0
-
- vessel_data = G.nodes[vessel].get('data', {})
-
- # 检查多种体积字段
- volume_keys = ['total_volume', 'volume', 'liquid_volume', 'current_volume']
- for key in volume_keys:
- if key in vessel_data:
- try:
- volume = float(vessel_data[key])
- debug_print(f"从 '{key}' 读取到体积: {volume}mL")
- return volume
- except (ValueError, TypeError):
- continue
-
- # 检查liquid数组
- liquids = vessel_data.get('liquid', [])
- if isinstance(liquids, list):
- total_volume = 0.0
- for liquid in liquids:
- if isinstance(liquid, dict):
- for vol_key in ['liquid_volume', 'volume', 'amount']:
- if vol_key in liquid:
- try:
- vol = float(liquid[vol_key])
- total_volume += vol
- debug_print(f"从液体数据 '{vol_key}' 读取: {vol}mL")
- except (ValueError, TypeError):
- continue
- if total_volume > 0:
- return total_volume
-
- debug_print(f"未检测到液体体积,返回 0.0")
- return 0.0
-
def find_filter_device(G: nx.DiGraph) -> str:
"""查找过滤器设备"""
debug_print("查找过滤器设备...")
# 查找过滤器设备
- filter_devices = []
for node in G.nodes():
node_data = G.nodes[node]
node_class = node_data.get('class', '') or ''
- if 'filter' in node_class.lower() or 'virtual_filter' in node_class:
- filter_devices.append(node)
+ if 'filter' in node_class.lower() or 'filter' in node.lower():
debug_print(f"找到过滤器设备: {node}")
+ return node
- if filter_devices:
- return filter_devices[0]
+ # 如果没找到,寻找可能的过滤器名称
+ possible_names = ["filter", "filter_1", "virtual_filter", "filtration_unit"]
+ for name in possible_names:
+ if name in G.nodes():
+ debug_print(f"找到过滤器设备: {name}")
+ return name
- debug_print("未找到过滤器设备,使用默认设备")
- return "filter_1" # 默认设备
+ raise ValueError("未找到过滤器设备")
-def find_filtrate_vessel(G: nx.DiGraph, filtrate_vessel: str = "") -> str:
- """查找滤液收集容器"""
- debug_print(f"查找滤液收集容器,指定容器: '{filtrate_vessel}'")
-
- # 如果指定了容器且存在,直接使用
- if filtrate_vessel and filtrate_vessel.strip():
- if filtrate_vessel in G.nodes():
- debug_print(f"使用指定的滤液容器: {filtrate_vessel}")
- return filtrate_vessel
- else:
- debug_print(f"指定的滤液容器 '{filtrate_vessel}' 不存在,查找默认容器")
-
- # 自动查找滤液容器
- possible_names = [
- "filtrate_vessel", # 标准名称
- "collection_bottle_1", # 收集瓶
- "collection_bottle_2", # 收集瓶
- "waste_workup", # 废液收集
- "rotavap", # 旋蒸仪
- "flask_1", # 通用烧瓶
- "flask_2" # 通用烧瓶
- ]
-
- for vessel_name in possible_names:
- if vessel_name in G.nodes():
- debug_print(f"找到滤液收集容器: {vessel_name}")
- return vessel_name
-
- debug_print("未找到滤液收集容器,使用默认容器")
- return "filtrate_vessel" # 默认容器
+def validate_vessel(G: nx.DiGraph, vessel: str, vessel_type: str = "容器") -> None:
+ """验证容器是否存在"""
+ if not vessel:
+ raise ValueError(f"{vessel_type}不能为空")
+
+ if vessel not in G.nodes():
+ raise ValueError(f"{vessel_type} '{vessel}' 不存在于系统中")
+
+ debug_print(f"✅ {vessel_type} '{vessel}' 验证通过")
def generate_filter_protocol(
G: nx.DiGraph,
vessel: str,
filtrate_vessel: str = "",
- **kwargs # 🔧 接受额外参数,增强兼容性
+ **kwargs
) -> List[Dict[str, Any]]:
"""
- 生成过滤操作的协议序列 - 简化版本
+ 生成过滤操作的协议序列
Args:
G: 设备图
- vessel: 过滤容器名称(必需)
- filtrate_vessel: 滤液容器名称(可选,自动查找)
+ vessel: 过滤容器名称(必需)- 包含需要过滤的混合物
+ filtrate_vessel: 滤液容器名称(可选)- 如果提供则收集滤液
**kwargs: 其他参数(兼容性)
Returns:
List[Dict[str, Any]]: 过滤操作的动作序列
"""
- debug_print("=" * 50)
+ debug_print("=" * 60)
debug_print("开始生成过滤协议")
debug_print(f"输入参数:")
debug_print(f" - vessel: {vessel}")
debug_print(f" - filtrate_vessel: {filtrate_vessel}")
debug_print(f" - 其他参数: {kwargs}")
- debug_print("=" * 50)
+ debug_print("=" * 60)
action_sequence = []
@@ -136,59 +75,83 @@ def generate_filter_protocol(
debug_print("步骤1: 参数验证...")
# 验证必需参数
- if not vessel:
- raise ValueError("vessel 参数不能为空")
-
- if vessel not in G.nodes():
- raise ValueError(f"容器 '{vessel}' 不存在于系统中")
+ validate_vessel(G, vessel, "过滤容器")
- debug_print(f"✅ 参数验证通过")
+ # 验证可选参数
+ if filtrate_vessel:
+ validate_vessel(G, filtrate_vessel, "滤液容器")
+ debug_print("模式: 过滤并收集滤液")
+ else:
+ debug_print("模式: 过滤并收集固体")
# === 查找设备 ===
debug_print("步骤2: 查找设备...")
try:
filter_device = find_filter_device(G)
- actual_filtrate_vessel = find_filtrate_vessel(G, filtrate_vessel)
-
- debug_print(f"设备配置:")
- debug_print(f" - 过滤器设备: {filter_device}")
- debug_print(f" - 滤液收集容器: {actual_filtrate_vessel}")
+ debug_print(f"使用过滤器设备: {filter_device}")
except Exception as e:
debug_print(f"❌ 设备查找失败: {str(e)}")
raise ValueError(f"设备查找失败: {str(e)}")
- # === 体积检测 ===
- debug_print("步骤3: 体积检测...")
-
- source_volume = get_vessel_liquid_volume(G, vessel)
+ # === 转移到过滤器(如果需要)===
+ debug_print("步骤3: 转移到过滤器...")
- if source_volume > 0:
- transfer_volume = source_volume
- debug_print(f"检测到液体体积: {transfer_volume}mL")
+ if vessel != filter_device:
+ debug_print(f"需要转移: {vessel} → {filter_device}")
+
+ try:
+ # 使用pump protocol转移液体到过滤器
+ transfer_actions = generate_pump_protocol_with_rinsing(
+ G=G,
+ from_vessel=vessel,
+ to_vessel=filter_device,
+ volume=0.0, # 转移所有液体
+ amount="",
+ time=0.0,
+ viscous=False,
+ rinsing_solvent="",
+ rinsing_volume=0.0,
+ rinsing_repeats=0,
+ solid=False,
+ flowrate=2.0,
+ transfer_flowrate=2.0
+ )
+
+ if transfer_actions:
+ action_sequence.extend(transfer_actions)
+ debug_print(f"✅ 添加了 {len(transfer_actions)} 个转移动作")
+ else:
+ debug_print("⚠️ 转移协议返回空序列")
+
+ except Exception as e:
+ debug_print(f"❌ 转移失败: {str(e)}")
+ # 继续执行,可能是直接连接的过滤器
else:
- transfer_volume = 50.0 # 默认体积
- debug_print(f"未检测到液体体积,使用默认值: {transfer_volume}mL")
+ debug_print("过滤容器就是过滤器,无需转移")
# === 执行过滤操作 ===
debug_print("步骤4: 执行过滤操作...")
- # 过滤动作(直接调用过滤器)
- debug_print(f"执行过滤: {vessel} -> {actual_filtrate_vessel}")
+ # 构建过滤动作参数
+ filter_kwargs = {
+ "vessel": filter_device, # 过滤器设备
+ "filtrate_vessel": filtrate_vessel, # 滤液容器(可能为空)
+ "stir": kwargs.get("stir", False),
+ "stir_speed": kwargs.get("stir_speed", 0.0),
+ "temp": kwargs.get("temp", 25.0),
+ "continue_heatchill": kwargs.get("continue_heatchill", False),
+ "volume": kwargs.get("volume", 0.0) # 0表示过滤所有
+ }
+
+ debug_print(f"过滤参数: {filter_kwargs}")
+ # 过滤动作
filter_action = {
"device_id": filter_device,
"action_name": "filter",
- "action_kwargs": {
- "vessel": vessel,
- "filtrate_vessel": actual_filtrate_vessel,
- "stir": False, # 🔧 使用默认值
- "stir_speed": 0.0, # 🔧 使用默认值
- "temp": 25.0, # 🔧 使用默认值
- "continue_heatchill": False, # 🔧 使用默认值
- "volume": transfer_volume # 🔧 使用检测到的体积
- }
+ "action_kwargs": filter_kwargs
}
action_sequence.append(filter_action)
@@ -198,22 +161,55 @@ def generate_filter_protocol(
"action_kwargs": {"time": 10.0}
})
+ # === 收集滤液(如果需要)===
+ debug_print("步骤5: 收集滤液...")
+
+ if filtrate_vessel:
+ debug_print(f"收集滤液: {filter_device} → {filtrate_vessel}")
+
+ try:
+ # 使用pump protocol收集滤液
+ collect_actions = generate_pump_protocol_with_rinsing(
+ G=G,
+ from_vessel=filter_device,
+ to_vessel=filtrate_vessel,
+ volume=0.0, # 收集所有滤液
+ amount="",
+ time=0.0,
+ viscous=False,
+ rinsing_solvent="",
+ rinsing_volume=0.0,
+ rinsing_repeats=0,
+ solid=False,
+ flowrate=2.0,
+ transfer_flowrate=2.0
+ )
+
+ if collect_actions:
+ action_sequence.extend(collect_actions)
+ debug_print(f"✅ 添加了 {len(collect_actions)} 个收集动作")
+ else:
+ debug_print("⚠️ 收集协议返回空序列")
+
+ except Exception as e:
+ debug_print(f"❌ 收集滤液失败: {str(e)}")
+ # 继续执行,可能滤液直接流入指定容器
+ else:
+ debug_print("未指定滤液容器,固体保留在过滤器中")
+
+ # === 最终等待 ===
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": 5.0}
+ })
+
# === 总结 ===
- debug_print("=" * 50)
+ debug_print("=" * 60)
debug_print(f"过滤协议生成完成")
debug_print(f"总动作数: {len(action_sequence)}")
debug_print(f"过滤容器: {vessel}")
- debug_print(f"滤液容器: {actual_filtrate_vessel}")
- debug_print(f"处理体积: {transfer_volume}mL")
- debug_print("=" * 50)
+ debug_print(f"过滤器设备: {filter_device}")
+ debug_print(f"滤液容器: {filtrate_vessel or '无(保留固体)'}")
+ debug_print("=" * 60)
return action_sequence
-
-# 测试函数
-def test_filter_protocol():
- """测试过滤协议"""
- debug_print("=== FILTER PROTOCOL 测试 ===")
- debug_print("✅ 测试完成")
-
-if __name__ == "__main__":
- test_filter_protocol()
\ No newline at end of file
diff --git a/unilabos/compile/pump_protocol.py b/unilabos/compile/pump_protocol.py
index acdb52c6d..f214ff484 100644
--- a/unilabos/compile/pump_protocol.py
+++ b/unilabos/compile/pump_protocol.py
@@ -98,24 +98,112 @@ def is_integrated_pump(node_name):
def find_connected_pump(G, valve_node):
- for neighbor in G.neighbors(valve_node):
- node_class = G.nodes[neighbor].get("class") or ""
- if "pump" in node_class:
- return neighbor
- raise ValueError(f"未找到与阀 {valve_node} 唯一相连的泵节点")
+ """
+ 查找与阀门相连的泵节点 - 修复版本
+ 🔧 修复:区分电磁阀和多通阀,电磁阀不参与泵查找
+ """
+ debug_print(f"🔍 查找与阀门 {valve_node} 相连的泵...")
+
+ # 🔧 关键修复:检查节点类型,电磁阀不应该查找泵
+ node_data = G.nodes.get(valve_node, {})
+ node_class = node_data.get("class", "") or ""
+
+ debug_print(f" - 阀门类型: {node_class}")
+
+ # 如果是电磁阀,不应该查找泵(电磁阀只是开关)
+ if ("solenoid" in node_class.lower() or "solenoid_valve" in valve_node.lower()):
+ debug_print(f" ⚠️ {valve_node} 是电磁阀,不应该查找泵节点")
+ raise ValueError(f"电磁阀 {valve_node} 不应该参与泵查找逻辑")
+
+ # 只有多通阀等复杂阀门才需要查找连接的泵
+ if ("multiway" in node_class.lower() or "valve" in node_class.lower()):
+ debug_print(f" - {valve_node} 是多通阀,查找连接的泵...")
+
+ # 方法1:直接相邻的泵
+ for neighbor in G.neighbors(valve_node):
+ neighbor_class = G.nodes[neighbor].get("class", "") or ""
+ debug_print(f" - 检查邻居 {neighbor}, class: {neighbor_class}")
+ if "pump" in neighbor_class.lower():
+ debug_print(f" ✅ 找到直接相连的泵: {neighbor}")
+ return neighbor
+
+ # 方法2:通过路径查找泵(最多2跳)
+ debug_print(f" - 未找到直接相连的泵,尝试路径查找...")
+
+ # 获取所有泵节点
+ pump_nodes = []
+ for node_id in G.nodes():
+ node_class = G.nodes[node_id].get("class", "") or ""
+ if "pump" in node_class.lower():
+ pump_nodes.append(node_id)
+
+ debug_print(f" - 系统中的泵节点: {pump_nodes}")
+
+ # 查找到泵的最短路径
+ for pump_node in pump_nodes:
+ try:
+ if nx.has_path(G, valve_node, pump_node):
+ path = nx.shortest_path(G, valve_node, pump_node)
+ path_length = len(path) - 1
+ debug_print(f" - 到泵 {pump_node} 的路径: {path}, 距离: {path_length}")
+
+ if path_length <= 2: # 最多允许2跳
+ debug_print(f" ✅ 通过路径找到泵: {pump_node}")
+ return pump_node
+ except nx.NetworkXNoPath:
+ continue
+
+ # 方法3:降级方案 - 返回第一个可用的泵
+ if pump_nodes:
+ debug_print(f" ⚠️ 未找到连接的泵,使用第一个可用的泵: {pump_nodes[0]}")
+ return pump_nodes[0]
+
+ # 最终失败
+ debug_print(f" ❌ 完全找不到泵节点")
+ raise ValueError(f"未找到与阀 {valve_node} 相连的泵节点")
def build_pump_valve_maps(G, pump_backbone):
+ """
+ 构建泵-阀门映射 - 修复版本
+ 🔧 修复:过滤掉电磁阀,只处理需要泵的多通阀
+ """
pumps_from_node = {}
valve_from_node = {}
+
+ debug_print(f"🔧 构建泵-阀门映射,原始骨架: {pump_backbone}")
+
+ # 🔧 关键修复:过滤掉电磁阀
+ filtered_backbone = []
for node in pump_backbone:
+ node_data = G.nodes.get(node, {})
+ node_class = node_data.get("class", "") or ""
+
+ # 跳过电磁阀
+ if ("solenoid" in node_class.lower() or "solenoid_valve" in node.lower()):
+ debug_print(f" - 跳过电磁阀: {node}")
+ continue
+
+ filtered_backbone.append(node)
+
+ debug_print(f"🔧 过滤后的骨架: {filtered_backbone}")
+
+ for node in filtered_backbone:
if is_integrated_pump(node):
pumps_from_node[node] = node
valve_from_node[node] = node
+ debug_print(f" - 集成泵-阀: {node}")
else:
- pump_node = find_connected_pump(G, node)
- pumps_from_node[node] = pump_node
- valve_from_node[node] = node
+ try:
+ pump_node = find_connected_pump(G, node)
+ pumps_from_node[node] = pump_node
+ valve_from_node[node] = node
+ debug_print(f" - 阀门 {node} -> 泵 {pump_node}")
+ except ValueError as e:
+ debug_print(f" - 跳过节点 {node}: {str(e)}")
+ continue
+
+ debug_print(f"🔧 最终映射: pumps={pumps_from_node}, valves={valve_from_node}")
return pumps_from_node, valve_from_node
@@ -128,7 +216,8 @@ def generate_pump_protocol(
transfer_flowrate: float = 0.5,
) -> List[Dict[str, Any]]:
"""
- 生成泵操作的动作序列
+ 生成泵操作的动作序列 - 修复版本
+ 🔧 修复:正确处理包含电磁阀的路径
"""
pump_action_sequence = []
nodes = G.nodes(data=True)
@@ -162,25 +251,63 @@ def generate_pump_protocol(
logger.error(f"无法找到从 '{from_vessel}' 到 '{to_vessel}' 的路径")
return pump_action_sequence
- pump_backbone = shortest_path
- if not from_vessel.startswith("pump"):
- pump_backbone = pump_backbone[1:]
- if not to_vessel.startswith("pump"):
- pump_backbone = pump_backbone[:-1]
+ # 🔧 关键修复:正确构建泵骨架,排除容器和电磁阀
+ pump_backbone = []
+ for node in shortest_path:
+ # 跳过起始和结束容器
+ if node == from_vessel or node == to_vessel:
+ continue
+
+ # 跳过电磁阀(电磁阀不参与泵操作)
+ node_data = G.nodes.get(node, {})
+ node_class = node_data.get("class", "") or ""
+ if ("solenoid" in node_class.lower() or "solenoid_valve" in node.lower()):
+ debug_print(f"PUMP_TRANSFER: 跳过电磁阀 {node}")
+ continue
+
+ # 只包含多通阀和泵
+ if ("multiway" in node_class.lower() or "valve" in node_class.lower() or "pump" in node_class.lower()):
+ pump_backbone.append(node)
+
+ debug_print(f"PUMP_TRANSFER: 过滤后的泵骨架: {pump_backbone}")
if not pump_backbone:
- debug_print("没有泵骨架节点,可能是直接容器连接")
+ debug_print("PUMP_TRANSFER: 没有泵骨架节点,可能是直接容器连接或只有电磁阀")
+ # 🔧 对于气体传输,这是正常的,直接返回空序列
return pump_action_sequence
if transfer_flowrate == 0:
transfer_flowrate = flowrate
- pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone)
+ try:
+ pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone)
+ except Exception as e:
+ debug_print(f"PUMP_TRANSFER: 构建泵-阀门映射失败: {str(e)}")
+ return pump_action_sequence
+
+ if not pumps_from_node:
+ debug_print("PUMP_TRANSFER: 没有可用的泵映射")
+ return pump_action_sequence
- # 获取最小转移体积
+ # 🔧 修复:安全地获取最小转移体积
try:
- min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone])
- except (KeyError, TypeError):
+ min_transfer_volumes = []
+ for node in pump_backbone:
+ if node in pumps_from_node:
+ pump_node = pumps_from_node[node]
+ if pump_node in nodes:
+ pump_config = nodes[pump_node].get("config", {})
+ max_volume = pump_config.get("max_volume")
+ if max_volume is not None:
+ min_transfer_volumes.append(max_volume)
+
+ if min_transfer_volumes:
+ min_transfer_volume = min(min_transfer_volumes)
+ else:
+ min_transfer_volume = 25.0 # 默认值
+ debug_print(f"PUMP_TRANSFER: 无法获取泵的最大体积,使用默认值: {min_transfer_volume}mL")
+ except Exception as e:
+ debug_print(f"PUMP_TRANSFER: 获取最小转移体积失败: {str(e)}")
min_transfer_volume = 25.0 # 默认值
repeats = int(np.ceil(volume / min_transfer_volume))
@@ -196,85 +323,108 @@ def generate_pump_protocol(
for i in range(repeats):
current_volume = min(volume_left, min_transfer_volume)
+ # 🔧 修复:安全地获取边数据
+ def get_safe_edge_data(node_a, node_b, key):
+ try:
+ edge_data = G.get_edge_data(node_a, node_b)
+ if edge_data and "port" in edge_data:
+ port_data = edge_data["port"]
+ if isinstance(port_data, dict) and key in port_data:
+ return port_data[key]
+ return "default"
+ except Exception as e:
+ debug_print(f"PUMP_TRANSFER: 获取边数据失败 {node_a}->{node_b}: {str(e)}")
+ return "default"
+
# 从源容器吸液
- if not from_vessel.startswith("pump"):
- pump_action_sequence.extend([
- {
- "device_id": valve_from_node[pump_backbone[0]],
- "action_name": "set_valve_position",
- "action_kwargs": {
- "command": G.get_edge_data(pump_backbone[0], from_vessel)["port"][pump_backbone[0]]
- }
- },
- {
- "device_id": pumps_from_node[pump_backbone[0]],
- "action_name": "set_position",
- "action_kwargs": {
- "position": float(current_volume),
- "max_velocity": transfer_flowrate
+ if not from_vessel.startswith("pump") and pump_backbone:
+ first_pump_node = pump_backbone[0]
+ if first_pump_node in valve_from_node and first_pump_node in pumps_from_node:
+ port_command = get_safe_edge_data(first_pump_node, from_vessel, first_pump_node)
+ pump_action_sequence.extend([
+ {
+ "device_id": valve_from_node[first_pump_node],
+ "action_name": "set_valve_position",
+ "action_kwargs": {
+ "command": port_command
+ }
+ },
+ {
+ "device_id": pumps_from_node[first_pump_node],
+ "action_name": "set_position",
+ "action_kwargs": {
+ "position": float(current_volume),
+ "max_velocity": transfer_flowrate
+ }
}
- }
- ])
- pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}})
+ ])
+ pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}})
# 泵间转移
for nodeA, nodeB in zip(pump_backbone[:-1], pump_backbone[1:]):
- pump_action_sequence.append([
- {
- "device_id": valve_from_node[nodeA],
- "action_name": "set_valve_position",
- "action_kwargs": {
- "command": G.get_edge_data(nodeA, nodeB)["port"][nodeA]
- }
- },
- {
- "device_id": valve_from_node[nodeB],
- "action_name": "set_valve_position",
- "action_kwargs": {
- "command": G.get_edge_data(nodeB, nodeA)["port"][nodeB],
- }
- }
- ])
- pump_action_sequence.append([
- {
- "device_id": pumps_from_node[nodeA],
- "action_name": "set_position",
- "action_kwargs": {
- "position": 0.0,
- "max_velocity": transfer_flowrate
+ if nodeA in valve_from_node and nodeB in valve_from_node and nodeA in pumps_from_node and nodeB in pumps_from_node:
+ port_a = get_safe_edge_data(nodeA, nodeB, nodeA)
+ port_b = get_safe_edge_data(nodeB, nodeA, nodeB)
+
+ pump_action_sequence.append([
+ {
+ "device_id": valve_from_node[nodeA],
+ "action_name": "set_valve_position",
+ "action_kwargs": {
+ "command": port_a
+ }
+ },
+ {
+ "device_id": valve_from_node[nodeB],
+ "action_name": "set_valve_position",
+ "action_kwargs": {
+ "command": port_b
+ }
}
- },
- {
- "device_id": pumps_from_node[nodeB],
- "action_name": "set_position",
- "action_kwargs": {
- "position": float(current_volume),
- "max_velocity": transfer_flowrate
+ ])
+ pump_action_sequence.append([
+ {
+ "device_id": pumps_from_node[nodeA],
+ "action_name": "set_position",
+ "action_kwargs": {
+ "position": 0.0,
+ "max_velocity": transfer_flowrate
+ }
+ },
+ {
+ "device_id": pumps_from_node[nodeB],
+ "action_name": "set_position",
+ "action_kwargs": {
+ "position": float(current_volume),
+ "max_velocity": transfer_flowrate
+ }
}
- }
- ])
- pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}})
+ ])
+ pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}})
# 排液到目标容器
- if not to_vessel.startswith("pump"):
- pump_action_sequence.extend([
- {
- "device_id": valve_from_node[pump_backbone[-1]],
- "action_name": "set_valve_position",
- "action_kwargs": {
- "command": G.get_edge_data(pump_backbone[-1], to_vessel)["port"][pump_backbone[-1]]
- }
- },
- {
- "device_id": pumps_from_node[pump_backbone[-1]],
- "action_name": "set_position",
- "action_kwargs": {
- "position": 0.0,
- "max_velocity": flowrate
+ if not to_vessel.startswith("pump") and pump_backbone:
+ last_pump_node = pump_backbone[-1]
+ if last_pump_node in valve_from_node and last_pump_node in pumps_from_node:
+ port_command = get_safe_edge_data(last_pump_node, to_vessel, last_pump_node)
+ pump_action_sequence.extend([
+ {
+ "device_id": valve_from_node[last_pump_node],
+ "action_name": "set_valve_position",
+ "action_kwargs": {
+ "command": port_command
+ }
+ },
+ {
+ "device_id": pumps_from_node[last_pump_node],
+ "action_name": "set_position",
+ "action_kwargs": {
+ "position": 0.0,
+ "max_velocity": flowrate
+ }
}
- }
- ])
- pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}})
+ ])
+ pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}})
volume_left -= current_volume
@@ -287,7 +437,7 @@ def generate_pump_protocol_with_rinsing(
to_vessel: str,
volume: float = 0.0,
amount: str = "",
- duration: float = 0.0, # 🔧 重命名参数,避免冲突
+ time: float = 0.0, # 🔧 修复:统一使用 time
viscous: bool = False,
rinsing_solvent: str = "",
rinsing_volume: float = 0.0,
@@ -306,11 +456,11 @@ def generate_pump_protocol_with_rinsing(
debug_print("=" * 60)
debug_print(f"PUMP_TRANSFER: 🚀 开始生成协议")
debug_print(f" 📍 路径: {from_vessel} -> {to_vessel}")
- debug_print(f" 🕐 时间戳: {time_module.time()}") # 🔧 使用重命名的模块
+ debug_print(f" 🕐 时间戳: {time_module.time()}")
debug_print(f" 📊 原始参数:")
debug_print(f" - volume: {volume} (类型: {type(volume)})")
debug_print(f" - amount: '{amount}'")
- debug_print(f" - duration: {duration}") # 🔧 使用新的参数名
+ debug_print(f" - time: {time}") # 🔧 修复:统一使用 time
debug_print(f" - flowrate: {flowrate}")
debug_print(f" - transfer_flowrate: {transfer_flowrate}")
debug_print(f" - rate_spec: '{rate_spec}'")
@@ -382,9 +532,9 @@ def generate_pump_protocol_with_rinsing(
debug_print(f"✅ 修正后流速: flowrate={final_flowrate}mL/s, transfer_flowrate={final_transfer_flowrate}mL/s")
# 3. 根据时间计算流速
- if duration > 0 and final_volume > 0: # 🔧 使用duration而不是time
+ if time > 0 and final_volume > 0: # 🔧 修复:统一使用 time
debug_print(f"🔍 步骤4: 根据时间计算流速...")
- calculated_flowrate = final_volume / duration
+ calculated_flowrate = final_volume / time
debug_print(f" - 计算得到流速: {calculated_flowrate}mL/s")
if flowrate <= 0 or flowrate == 2.5:
@@ -412,31 +562,31 @@ def generate_pump_protocol_with_rinsing(
final_transfer_flowrate = max(final_transfer_flowrate, 2.0)
debug_print(f" - quickly模式,流速调整为: {final_flowrate}mL/s")
- # 5. 处理冲洗参数
- debug_print(f"🔍 步骤6: 处理冲洗参数...")
- final_rinsing_solvent = rinsing_solvent
- final_rinsing_volume = rinsing_volume if rinsing_volume > 0 else 5.0
- final_rinsing_repeats = rinsing_repeats if rinsing_repeats > 0 else 2
+ # # 5. 处理冲洗参数
+ # debug_print(f"🔍 步骤6: 处理冲洗参数...")
+ # final_rinsing_solvent = rinsing_solvent
+ # final_rinsing_volume = rinsing_volume if rinsing_volume > 0 else 5.0
+ # final_rinsing_repeats = rinsing_repeats if rinsing_repeats > 0 else 2
- if rinsing_volume <= 0:
- debug_print(f"⚠️ rinsing_volume <= 0,修正为: {final_rinsing_volume}mL")
- if rinsing_repeats <= 0:
- debug_print(f"⚠️ rinsing_repeats <= 0,修正为: {final_rinsing_repeats}次")
+ # if rinsing_volume <= 0:
+ # debug_print(f"⚠️ rinsing_volume <= 0,修正为: {final_rinsing_volume}mL")
+ # if rinsing_repeats <= 0:
+ # debug_print(f"⚠️ rinsing_repeats <= 0,修正为: {final_rinsing_repeats}次")
- # 根据物理属性调整冲洗参数
- if viscous or solid:
- final_rinsing_repeats = max(final_rinsing_repeats, 3)
- final_rinsing_volume = max(final_rinsing_volume, 10.0)
- debug_print(f"🧪 粘稠/固体物质,调整冲洗参数:{final_rinsing_repeats}次,{final_rinsing_volume}mL")
+ # # 根据物理属性调整冲洗参数
+ # if viscous or solid:
+ # final_rinsing_repeats = max(final_rinsing_repeats, 3)
+ # final_rinsing_volume = max(final_rinsing_volume, 10.0)
+ # debug_print(f"🧪 粘稠/固体物质,调整冲洗参数:{final_rinsing_repeats}次,{final_rinsing_volume}mL")
# 参数总结
debug_print("📊 最终参数总结:")
debug_print(f" - 体积: {final_volume}mL")
debug_print(f" - 流速: {final_flowrate}mL/s")
debug_print(f" - 转移流速: {final_transfer_flowrate}mL/s")
- debug_print(f" - 冲洗溶剂: '{final_rinsing_solvent}'")
- debug_print(f" - 冲洗体积: {final_rinsing_volume}mL")
- debug_print(f" - 冲洗次数: {final_rinsing_repeats}次")
+ # debug_print(f" - 冲洗溶剂: '{final_rinsing_solvent}'")
+ # debug_print(f" - 冲洗体积: {final_rinsing_volume}mL")
+ # debug_print(f" - 冲洗次数: {final_rinsing_repeats}次")
# ========== 执行基础转移 ==========
@@ -503,36 +653,36 @@ def generate_pump_protocol_with_rinsing(
# ========== 执行冲洗操作 ==========
- debug_print("🔧 步骤8: 检查冲洗操作...")
+ # debug_print("🔧 步骤8: 检查冲洗操作...")
- if final_rinsing_solvent and final_rinsing_solvent.strip() and final_rinsing_repeats > 0:
- debug_print(f"🧽 开始冲洗操作,溶剂: '{final_rinsing_solvent}'")
+ # if final_rinsing_solvent and final_rinsing_solvent.strip() and final_rinsing_repeats > 0:
+ # debug_print(f"🧽 开始冲洗操作,溶剂: '{final_rinsing_solvent}'")
- try:
- if final_rinsing_solvent.strip() != "air":
- debug_print(" - 执行液体冲洗...")
- rinsing_actions = _generate_rinsing_sequence(
- G, from_vessel, to_vessel, final_rinsing_solvent,
- final_rinsing_volume, final_rinsing_repeats,
- final_flowrate, final_transfer_flowrate
- )
- pump_action_sequence.extend(rinsing_actions)
- debug_print(f" - 添加了 {len(rinsing_actions)} 个冲洗动作")
- else:
- debug_print(" - 执行空气冲洗...")
- air_rinsing_actions = _generate_air_rinsing_sequence(
- G, from_vessel, to_vessel, final_rinsing_volume, final_rinsing_repeats,
- final_flowrate, final_transfer_flowrate
- )
- pump_action_sequence.extend(air_rinsing_actions)
- debug_print(f" - 添加了 {len(air_rinsing_actions)} 个空气冲洗动作")
- except Exception as e:
- debug_print(f"⚠️ 冲洗操作失败: {str(e)},跳过冲洗")
- else:
- debug_print(f"⏭️ 跳过冲洗操作")
- debug_print(f" - 溶剂: '{final_rinsing_solvent}'")
- debug_print(f" - 次数: {final_rinsing_repeats}")
- debug_print(f" - 条件满足: {bool(final_rinsing_solvent and final_rinsing_solvent.strip() and final_rinsing_repeats > 0)}")
+ # try:
+ # if final_rinsing_solvent.strip() != "air":
+ # debug_print(" - 执行液体冲洗...")
+ # rinsing_actions = _generate_rinsing_sequence(
+ # G, from_vessel, to_vessel, final_rinsing_solvent,
+ # final_rinsing_volume, final_rinsing_repeats,
+ # final_flowrate, final_transfer_flowrate
+ # )
+ # pump_action_sequence.extend(rinsing_actions)
+ # debug_print(f" - 添加了 {len(rinsing_actions)} 个冲洗动作")
+ # else:
+ # debug_print(" - 执行空气冲洗...")
+ # air_rinsing_actions = _generate_air_rinsing_sequence(
+ # G, from_vessel, to_vessel, final_rinsing_volume, final_rinsing_repeats,
+ # final_flowrate, final_transfer_flowrate
+ # )
+ # pump_action_sequence.extend(air_rinsing_actions)
+ # debug_print(f" - 添加了 {len(air_rinsing_actions)} 个空气冲洗动作")
+ # except Exception as e:
+ # debug_print(f"⚠️ 冲洗操作失败: {str(e)},跳过冲洗")
+ # else:
+ # debug_print(f"⏭️ 跳过冲洗操作")
+ # debug_print(f" - 溶剂: '{final_rinsing_solvent}'")
+ # debug_print(f" - 次数: {final_rinsing_repeats}")
+ # debug_print(f" - 条件满足: {bool(final_rinsing_solvent and final_rinsing_solvent.strip() and final_rinsing_repeats > 0)}")
# ========== 最终结果 ==========
@@ -742,34 +892,22 @@ def generate_pump_protocol_with_rinsing(
final_transfer_flowrate = max(final_transfer_flowrate, 2.0)
debug_print(f" - quickly模式,流速调整为: {final_flowrate}mL/s")
- # 5. 处理冲洗参数
- debug_print(f"🔍 步骤6: 处理冲洗参数...")
- final_rinsing_solvent = rinsing_solvent
- final_rinsing_volume = rinsing_volume if rinsing_volume > 0 else 5.0
- final_rinsing_repeats = rinsing_repeats if rinsing_repeats > 0 else 2
-
- if rinsing_volume <= 0:
- logger.warning(f"⚠️ rinsing_volume <= 0,修正为: {final_rinsing_volume}mL")
- if rinsing_repeats <= 0:
- logger.warning(f"⚠️ rinsing_repeats <= 0,修正为: {final_rinsing_repeats}次")
-
- # 根据物理属性调整冲洗参数
- if viscous or solid:
- final_rinsing_repeats = max(final_rinsing_repeats, 3)
- final_rinsing_volume = max(final_rinsing_volume, 10.0)
- debug_print(f"🧪 粘稠/固体物质,调整冲洗参数:{final_rinsing_repeats}次,{final_rinsing_volume}mL")
-
- # 参数总结
- debug_print("📊 最终参数总结:")
- debug_print(f" - 体积: {final_volume}mL")
- debug_print(f" - 流速: {final_flowrate}mL/s")
- debug_print(f" - 转移流速: {final_transfer_flowrate}mL/s")
- debug_print(f" - 冲洗溶剂: '{final_rinsing_solvent}'")
- debug_print(f" - 冲洗体积: {final_rinsing_volume}mL")
- debug_print(f" - 冲洗次数: {final_rinsing_repeats}次")
-
- # 这里应该是您现有的pump_action_sequence生成逻辑
- # 我先提供一个示例,您需要替换为实际的生成逻辑
+ # # 5. 处理冲洗参数
+ # debug_print(f"🔍 步骤6: 处理冲洗参数...")
+ # final_rinsing_solvent = rinsing_solvent
+ # final_rinsing_volume = rinsing_volume if rinsing_volume > 0 else 5.0
+ # final_rinsing_repeats = rinsing_repeats if rinsing_repeats > 0 else 2
+
+ # if rinsing_volume <= 0:
+ # logger.warning(f"⚠️ rinsing_volume <= 0,修正为: {final_rinsing_volume}mL")
+ # if rinsing_repeats <= 0:
+ # logger.warning(f"⚠️ rinsing_repeats <= 0,修正为: {final_rinsing_repeats}次")
+
+ # # 根据物理属性调整冲洗参数
+ # if viscous or solid:
+ # final_rinsing_repeats = max(final_rinsing_repeats, 3)
+ # final_rinsing_volume = max(final_rinsing_volume, 10.0)
+ # debug_print(f"🧪 粘稠/固体物质,调整冲洗参数:{final_rinsing_repeats}次,{final_rinsing_volume}mL")
try:
pump_action_sequence = generate_pump_protocol(
diff --git a/unilabos/compile/run_column_protocol.py b/unilabos/compile/run_column_protocol.py
index f6b9214fc..c60e24093 100644
--- a/unilabos/compile/run_column_protocol.py
+++ b/unilabos/compile/run_column_protocol.py
@@ -1,312 +1,668 @@
-from typing import List, Dict, Any
+from typing import List, Dict, Any, Union
import networkx as nx
-from .pump_protocol import generate_pump_protocol
+import logging
+import re
+from .pump_protocol import generate_pump_protocol_with_rinsing
+logger = logging.getLogger(__name__)
-def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:
- """获取容器中的液体体积"""
- if vessel not in G.nodes():
+def debug_print(message):
+ """调试输出"""
+ print(f"[RUN_COLUMN] {message}", flush=True)
+ logger.info(f"[RUN_COLUMN] {message}")
+
+def parse_percentage(pct_str: str) -> float:
+ """
+ 解析百分比字符串为数值
+
+ Args:
+ pct_str: 百分比字符串(如 "40 %", "40%", "40")
+
+ Returns:
+ float: 百分比数值(0-100)
+ """
+ if not pct_str or not pct_str.strip():
return 0.0
- vessel_data = G.nodes[vessel].get('data', {})
- liquids = vessel_data.get('liquid', [])
+ pct_str = pct_str.strip().lower()
+ debug_print(f"解析百分比: '{pct_str}'")
- total_volume = 0.0
- for liquid in liquids:
- if isinstance(liquid, dict):
- # 支持两种格式:新格式 (name, volume) 和旧格式 (liquid_type, liquid_volume)
- volume = liquid.get('volume') or liquid.get('liquid_volume', 0.0)
- total_volume += volume
+ # 移除百分号和空格
+ pct_clean = re.sub(r'[%\s]', '', pct_str)
- return total_volume
+ # 提取数字
+ match = re.search(r'([0-9]*\.?[0-9]+)', pct_clean)
+ if match:
+ value = float(match.group(1))
+ debug_print(f"百分比解析结果: {value}%")
+ return value
+
+ debug_print(f"⚠️ 无法解析百分比: '{pct_str}',返回0.0")
+ return 0.0
+def parse_ratio(ratio_str: str) -> tuple:
+ """
+ 解析比例字符串为两个数值
+
+ Args:
+ ratio_str: 比例字符串(如 "5:95", "1:1", "40:60")
+
+ Returns:
+ tuple: (ratio1, ratio2) 两个比例值
+ """
+ if not ratio_str or not ratio_str.strip():
+ return (50.0, 50.0) # 默认1:1
+
+ ratio_str = ratio_str.strip()
+ debug_print(f"解析比例: '{ratio_str}'")
+
+ # 支持多种分隔符:: / -
+ if ':' in ratio_str:
+ parts = ratio_str.split(':')
+ elif '/' in ratio_str:
+ parts = ratio_str.split('/')
+ elif '-' in ratio_str:
+ parts = ratio_str.split('-')
+ elif 'to' in ratio_str.lower():
+ parts = ratio_str.lower().split('to')
+ else:
+ debug_print(f"⚠️ 无法解析比例格式: '{ratio_str}',使用默认1:1")
+ return (50.0, 50.0)
+
+ if len(parts) >= 2:
+ try:
+ ratio1 = float(parts[0].strip())
+ ratio2 = float(parts[1].strip())
+ total = ratio1 + ratio2
+
+ # 转换为百分比
+ pct1 = (ratio1 / total) * 100
+ pct2 = (ratio2 / total) * 100
+
+ debug_print(f"比例解析结果: {ratio1}:{ratio2} -> {pct1:.1f}%:{pct2:.1f}%")
+ return (pct1, pct2)
+ except ValueError as e:
+ debug_print(f"⚠️ 比例数值转换失败: {str(e)}")
+
+ debug_print(f"⚠️ 比例解析失败,使用默认1:1")
+ return (50.0, 50.0)
-def find_column_device(G: nx.DiGraph, column: str) -> str:
- """查找柱层析设备"""
- # 首先检查是否有虚拟柱设备
- column_nodes = [node for node in G.nodes()
- if (G.nodes[node].get('class') or '') == 'virtual_column']
+def parse_rf_value(rf_str: str) -> float:
+ """
+ 解析Rf值字符串
- if column_nodes:
- return column_nodes[0]
+ Args:
+ rf_str: Rf值字符串(如 "0.3", "0.45", "?")
+
+ Returns:
+ float: Rf值(0-1)
+ """
+ if not rf_str or not rf_str.strip():
+ return 0.3 # 默认Rf值
+
+ rf_str = rf_str.strip().lower()
+ debug_print(f"解析Rf值: '{rf_str}'")
- # 如果没有虚拟柱设备,抛出异常
- raise ValueError(f"系统中未找到柱层析设备。请确保配置了 virtual_column 设备")
+ # 处理未知Rf值
+ if rf_str in ['?', 'unknown', 'tbd', 'to be determined']:
+ default_rf = 0.3
+ debug_print(f"检测到未知Rf值,使用默认值: {default_rf}")
+ return default_rf
+
+ # 提取数字
+ match = re.search(r'([0-9]*\.?[0-9]+)', rf_str)
+ if match:
+ value = float(match.group(1))
+ # 确保Rf值在0-1范围内
+ if value > 1.0:
+ value = value / 100.0 # 可能是百分比形式
+ value = max(0.0, min(1.0, value)) # 限制在0-1范围
+ debug_print(f"Rf值解析结果: {value}")
+ return value
+
+ debug_print(f"⚠️ 无法解析Rf值: '{rf_str}',使用默认值0.3")
+ return 0.3
+def find_column_device(G: nx.DiGraph) -> str:
+ """查找柱层析设备"""
+ debug_print("查找柱层析设备...")
+
+ # 查找虚拟柱设备
+ for node in G.nodes():
+ node_data = G.nodes[node]
+ node_class = node_data.get('class', '') or ''
+
+ if 'virtual_column' in node_class.lower() or 'column' in node_class.lower():
+ debug_print(f"✅ 找到柱层析设备: {node}")
+ return node
+
+ # 如果没有找到,尝试创建虚拟设备名称
+ possible_names = ['column_1', 'virtual_column_1', 'chromatography_column_1']
+ for name in possible_names:
+ if name in G.nodes():
+ debug_print(f"✅ 找到柱设备: {name}")
+ return name
+
+ debug_print("⚠️ 未找到柱层析设备,将使用pump protocol直接转移")
+ return ""
def find_column_vessel(G: nx.DiGraph, column: str) -> str:
"""查找柱容器"""
- # 直接使用 column 参数作为容器名称
+ debug_print(f"查找柱容器: '{column}'")
+
+ # 直接检查column参数是否是容器
if column in G.nodes():
- return column
+ node_type = G.nodes[column].get('type', '')
+ if node_type == 'container':
+ debug_print(f"✅ 找到柱容器: {column}")
+ return column
- # 尝试常见的柱容器命名规则
+ # 尝试常见的命名规则
possible_names = [
f"column_{column}",
- f"{column}_column",
+ f"{column}_column",
f"vessel_{column}",
f"{column}_vessel",
"column_vessel",
"chromatography_column",
"silica_column",
- "preparative_column"
+ "preparative_column",
+ "column"
]
for vessel_name in possible_names:
if vessel_name in G.nodes():
- return vessel_name
+ node_type = G.nodes[vessel_name].get('type', '')
+ if node_type == 'container':
+ debug_print(f"✅ 找到柱容器: {vessel_name}")
+ return vessel_name
- raise ValueError(f"未找到柱容器 '{column}'。尝试了以下名称: {[column] + possible_names}")
-
+ debug_print(f"⚠️ 未找到柱容器,将直接在源容器中进行分离")
+ return ""
-def find_eluting_solvent_vessel(G: nx.DiGraph, eluting_solvent: str) -> str:
- """查找洗脱溶剂容器"""
- if not eluting_solvent:
+def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
+ """查找溶剂容器 - 增强版"""
+ if not solvent or not solvent.strip():
return ""
- # 按照命名规则查找溶剂瓶
- solvent_vessel_id = f"flask_{eluting_solvent}"
+ solvent = solvent.strip().replace(' ', '_').lower()
+ debug_print(f"查找溶剂容器: '{solvent}'")
+
+ # 🔧 方法1:直接搜索 data.reagent_name
+ for node in G.nodes():
+ node_data = G.nodes[node].get('data', {})
+ node_type = G.nodes[node].get('type', '')
+
+ # 只搜索容器类型的节点
+ if node_type == 'container':
+ reagent_name = node_data.get('reagent_name', '').lower()
+ reagent_config = G.nodes[node].get('config', {}).get('reagent', '').lower()
+
+ # 检查 data.reagent_name 和 config.reagent
+ if reagent_name == solvent or reagent_config == solvent:
+ debug_print(f"✅ 通过reagent_name找到溶剂容器: {node} (reagent: {reagent_name or reagent_config})")
+ return node
+
+ # 模糊匹配 reagent_name
+ if solvent in reagent_name or reagent_name in solvent:
+ debug_print(f"✅ 通过reagent_name模糊匹配到溶剂容器: {node} (reagent: {reagent_name})")
+ return node
+
+ if solvent in reagent_config or reagent_config in solvent:
+ debug_print(f"✅ 通过config.reagent模糊匹配到溶剂容器: {node} (reagent: {reagent_config})")
+ return node
+
+ # 🔧 方法2:常见的溶剂容器命名规则
+ possible_names = [
+ f"flask_{solvent}",
+ f"bottle_{solvent}",
+ f"reagent_{solvent}",
+ f"{solvent}_bottle",
+ f"{solvent}_flask",
+ f"solvent_{solvent}",
+ f"reagent_bottle_{solvent}"
+ ]
- if solvent_vessel_id in G.nodes():
- return solvent_vessel_id
+ for vessel_name in possible_names:
+ if vessel_name in G.nodes():
+ node_type = G.nodes[vessel_name].get('type', '')
+ if node_type == 'container':
+ debug_print(f"✅ 通过命名规则找到溶剂容器: {vessel_name}")
+ return vessel_name
- # 如果直接匹配失败,尝试模糊匹配
+ # 🔧 方法3:节点名称模糊匹配
for node in G.nodes():
- if node.startswith('flask_') and eluting_solvent.lower() in node.lower():
- return node
+ node_type = G.nodes[node].get('type', '')
+ if node_type == 'container':
+ if ('flask_' in node or 'bottle_' in node or 'reagent_' in node) and solvent in node.lower():
+ debug_print(f"✅ 通过节点名称模糊匹配到溶剂容器: {node}")
+ return node
- # 如果还是找不到,列出所有可用的溶剂瓶
- available_flasks = [node for node in G.nodes()
- if node.startswith('flask_')
- and G.nodes[node].get('type') == 'container']
+ # 🔧 方法4:特殊溶剂名称映射
+ solvent_mapping = {
+ 'dmf': ['dmf', 'dimethylformamide', 'n,n-dimethylformamide'],
+ 'ethyl_acetate': ['ethyl_acetate', 'ethylacetate', 'etoac', 'ea'],
+ 'hexane': ['hexane', 'hexanes', 'n-hexane'],
+ 'methanol': ['methanol', 'meoh', 'ch3oh'],
+ 'water': ['water', 'h2o', 'distilled_water'],
+ 'acetone': ['acetone', 'ch3coch3', '2-propanone'],
+ 'dichloromethane': ['dichloromethane', 'dcm', 'ch2cl2', 'methylene_chloride'],
+ 'chloroform': ['chloroform', 'chcl3', 'trichloromethane']
+ }
- raise ValueError(f"找不到洗脱溶剂 '{eluting_solvent}' 对应的溶剂瓶。可用溶剂瓶: {available_flasks}")
+ # 查找映射的同义词
+ for canonical_name, synonyms in solvent_mapping.items():
+ if solvent in synonyms:
+ debug_print(f"检测到溶剂同义词: '{solvent}' -> '{canonical_name}'")
+ return find_solvent_vessel(G, canonical_name) # 递归搜索
+
+ debug_print(f"⚠️ 未找到溶剂 '{solvent}' 的容器")
+ return ""
+def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:
+ """获取容器中的液体体积 - 增强版"""
+ if vessel not in G.nodes():
+ debug_print(f"⚠️ 节点 '{vessel}' 不存在")
+ return 0.0
+
+ node_type = G.nodes[vessel].get('type', '')
+ vessel_data = G.nodes[vessel].get('data', {})
+
+ debug_print(f"读取节点 '{vessel}' (类型: {node_type}) 体积数据: {vessel_data}")
+
+ # 🔧 如果是设备类型,尝试查找关联的容器
+ if node_type == 'device':
+ debug_print(f"'{vessel}' 是设备,尝试查找关联容器...")
+
+ # 查找是否有内置容器数据
+ config_data = G.nodes[vessel].get('config', {})
+ if 'volume' in config_data:
+ default_volume = config_data.get('volume', 100.0)
+ debug_print(f"使用设备默认容量: {default_volume}mL")
+ return default_volume
+
+ # 对于旋蒸等设备,使用默认值
+ if 'rotavap' in vessel.lower():
+ default_volume = 100.0
+ debug_print(f"旋蒸设备使用默认容量: {default_volume}mL")
+ return default_volume
+
+ debug_print(f"⚠️ 设备 '{vessel}' 无法确定容量,返回0")
+ return 0.0
+
+ # 🔧 如果是容器类型,正常读取体积
+ total_volume = 0.0
+
+ # 方法1:检查液体列表
+ liquids = vessel_data.get('liquid', [])
+ if isinstance(liquids, list):
+ for liquid in liquids:
+ if isinstance(liquid, dict):
+ volume = liquid.get('volume') or liquid.get('liquid_volume', 0.0)
+ total_volume += volume
+
+ # 方法2:检查直接体积字段
+ if total_volume == 0.0:
+ volume_keys = ['current_volume', 'total_volume', 'volume', 'liquid_volume']
+ for key in volume_keys:
+ if key in vessel_data:
+ try:
+ total_volume = float(vessel_data[key])
+ if total_volume > 0:
+ break
+ except (ValueError, TypeError):
+ continue
+
+ # 方法3:检查配置中的初始体积
+ if total_volume == 0.0:
+ config_data = G.nodes[vessel].get('config', {})
+ if 'current_volume' in config_data:
+ try:
+ total_volume = float(config_data['current_volume'])
+ except (ValueError, TypeError):
+ pass
+
+ debug_print(f"容器 '{vessel}' 总体积: {total_volume}mL")
+ return total_volume
+
+def calculate_solvent_volumes(total_volume: float, pct1: float, pct2: float) -> tuple:
+ """根据百分比计算溶剂体积"""
+ volume1 = (total_volume * pct1) / 100.0
+ volume2 = (total_volume * pct2) / 100.0
+
+ debug_print(f"溶剂体积计算: 总体积{total_volume}mL")
+ debug_print(f" - 溶剂1: {pct1}% = {volume1}mL")
+ debug_print(f" - 溶剂2: {pct2}% = {volume2}mL")
+
+ return (volume1, volume2)
def generate_run_column_protocol(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
- column: str
+ column: str,
+ rf: str = "",
+ pct1: str = "",
+ pct2: str = "",
+ solvent1: str = "",
+ solvent2: str = "",
+ ratio: str = "",
+ **kwargs
) -> List[Dict[str, Any]]:
"""
- 生成柱层析分离的协议序列
+ 生成柱层析分离的协议序列 - 增强版
+
+ 支持新版XDL的所有参数,具有高兼容性和容错性
Args:
G: 有向图,节点为设备和容器,边为流体管道
- from_vessel: 源容器的名称,即样品起始所在的容器
- to_vessel: 目标容器的名称,分离后的样品要到达的容器
- column: 所使用的柱子的名称
+ from_vessel: 源容器的名称,即样品起始所在的容器(必需)
+ to_vessel: 目标容器的名称,分离后的样品要到达的容器(必需)
+ column: 所使用的柱子的名称(必需)
+ rf: Rf值(可选,支持 "?" 表示未知)
+ pct1: 第一种溶剂百分比(如 "40 %",可选)
+ pct2: 第二种溶剂百分比(如 "50 %",可选)
+ solvent1: 第一种溶剂名称(可选)
+ solvent2: 第二种溶剂名称(可选)
+ ratio: 溶剂比例(如 "5:95",可选,优先级高于pct1/pct2)
+ **kwargs: 其他可选参数
Returns:
List[Dict[str, Any]]: 柱层析分离操作的动作序列
"""
+
+ debug_print("=" * 60)
+ debug_print("开始生成柱层析协议")
+ debug_print(f"输入参数:")
+ debug_print(f" - from_vessel: '{from_vessel}'")
+ debug_print(f" - to_vessel: '{to_vessel}'")
+ debug_print(f" - column: '{column}'")
+ debug_print(f" - rf: '{rf}'")
+ debug_print(f" - pct1: '{pct1}'")
+ debug_print(f" - pct2: '{pct2}'")
+ debug_print(f" - solvent1: '{solvent1}'")
+ debug_print(f" - solvent2: '{solvent2}'")
+ debug_print(f" - ratio: '{ratio}'")
+ debug_print(f" - 其他参数: {kwargs}")
+ debug_print("=" * 60)
+
action_sequence = []
- print(f"RUN_COLUMN: 开始生成柱层析协议")
- print(f" - 源容器: {from_vessel}")
- print(f" - 目标容器: {to_vessel}")
- print(f" - 柱子: {column}")
+ # === 参数验证 ===
+ debug_print("步骤1: 参数验证...")
+
+ if not from_vessel:
+ raise ValueError("from_vessel 参数不能为空")
+ if not to_vessel:
+ raise ValueError("to_vessel 参数不能为空")
+ if not column:
+ raise ValueError("column 参数不能为空")
- # 验证源容器和目标容器存在
if from_vessel not in G.nodes():
raise ValueError(f"源容器 '{from_vessel}' 不存在于系统中")
-
if to_vessel not in G.nodes():
raise ValueError(f"目标容器 '{to_vessel}' 不存在于系统中")
- # 查找柱层析设备
- column_device_id = None
- column_nodes = [node for node in G.nodes()
- if (G.nodes[node].get('class') or '') == 'virtual_column']
+ debug_print("✅ 基本参数验证通过")
- if column_nodes:
- column_device_id = column_nodes[0]
- print(f"RUN_COLUMN: 找到柱层析设备: {column_device_id}")
- else:
- print(f"RUN_COLUMN: 警告 - 未找到柱层析设备")
+ # === 参数解析 ===
+ debug_print("步骤2: 参数解析...")
- # 获取源容器中的液体体积
- source_volume = get_vessel_liquid_volume(G, from_vessel)
- print(f"RUN_COLUMN: 源容器 {from_vessel} 中有 {source_volume} mL 液体")
+ # 解析Rf值
+ final_rf = parse_rf_value(rf)
+ debug_print(f"最终Rf值: {final_rf}")
- # === 第一步:样品转移到柱子(如果柱子是容器) ===
- if column in G.nodes() and G.nodes[column].get('type') == 'container':
- print(f"RUN_COLUMN: 样品转移 - {source_volume} mL 从 {from_vessel} 到 {column}")
+ # 解析溶剂比例(ratio优先级高于pct1/pct2)
+ if ratio and ratio.strip():
+ final_pct1, final_pct2 = parse_ratio(ratio)
+ debug_print(f"使用ratio参数: {final_pct1:.1f}% : {final_pct2:.1f}%")
+ else:
+ final_pct1 = parse_percentage(pct1) if pct1 else 50.0
+ final_pct2 = parse_percentage(pct2) if pct2 else 50.0
- try:
- sample_transfer_actions = generate_pump_protocol(
- G=G,
- from_vessel=from_vessel,
- to_vessel=column,
- volume=source_volume if source_volume > 0 else 100.0,
- flowrate=2.0
- )
- action_sequence.extend(sample_transfer_actions)
- except Exception as e:
- print(f"RUN_COLUMN: 样品转移失败: {str(e)}")
-
- # === 第二步:使用柱层析设备执行分离 ===
- if column_device_id:
- print(f"RUN_COLUMN: 使用柱层析设备执行分离")
+ # 如果百分比和不是100%,进行归一化
+ total_pct = final_pct1 + final_pct2
+ if total_pct == 0:
+ final_pct1, final_pct2 = 50.0, 50.0
+ elif total_pct != 100.0:
+ final_pct1 = (final_pct1 / total_pct) * 100
+ final_pct2 = (final_pct2 / total_pct) * 100
- column_separation_action = {
- "device_id": column_device_id,
- "action_name": "run_column",
- "action_kwargs": {
- "from_vessel": from_vessel,
- "to_vessel": to_vessel,
- "column": column
- }
- }
- action_sequence.append(column_separation_action)
-
- # 等待柱层析设备完成分离
- action_sequence.append({
- "action_name": "wait",
- "action_kwargs": {"time": 60}
- })
+ debug_print(f"使用百分比参数: {final_pct1:.1f}% : {final_pct2:.1f}%")
- # === 第三步:从柱子转移到目标容器(如果需要) ===
- if column in G.nodes() and column != to_vessel:
- print(f"RUN_COLUMN: 产物转移 - 从 {column} 到 {to_vessel}")
-
- try:
- product_transfer_actions = generate_pump_protocol(
- G=G,
- from_vessel=column,
- to_vessel=to_vessel,
- volume=source_volume * 0.8 if source_volume > 0 else 80.0, # 假设有一些损失
- flowrate=1.5
- )
- action_sequence.extend(product_transfer_actions)
- except Exception as e:
- print(f"RUN_COLUMN: 产物转移失败: {str(e)}")
-
- print(f"RUN_COLUMN: 生成了 {len(action_sequence)} 个动作")
- return action_sequence
-
-
-# 便捷函数:常用柱层析方案
-def generate_flash_column_protocol(
- G: nx.DiGraph,
- from_vessel: str,
- to_vessel: str,
- column_material: str = "silica_gel",
- mobile_phase: str = "ethyl_acetate",
- mobile_phase_volume: float = 100.0
-) -> List[Dict[str, Any]]:
- """快速柱层析:高流速分离"""
- return generate_run_column_protocol(
- G, from_vessel, to_vessel, column_material,
- mobile_phase, mobile_phase_volume, 1, "", 0.0, 3.0
- )
-
-
-def generate_preparative_column_protocol(
- G: nx.DiGraph,
- from_vessel: str,
- to_vessel: str,
- column_material: str = "silica_gel",
- equilibration_solvent: str = "hexane",
- eluting_solvent: str = "ethyl_acetate",
- eluting_volume: float = 50.0,
- eluting_repeats: int = 3
-) -> List[Dict[str, Any]]:
- """制备柱层析:带平衡和多次洗脱"""
- return generate_run_column_protocol(
- G, from_vessel, to_vessel, column_material,
- eluting_solvent, eluting_volume, eluting_repeats,
- equilibration_solvent, 30.0, 1.5
+ # 设置默认溶剂(如果未指定)
+ final_solvent1 = solvent1.strip() if solvent1 else "ethyl_acetate"
+ final_solvent2 = solvent2.strip() if solvent2 else "hexane"
+
+ debug_print(f"最终溶剂: {final_solvent1} : {final_solvent2}")
+
+ # === 查找设备和容器 ===
+ debug_print("步骤3: 查找设备和容器...")
+
+ # 查找柱层析设备
+ column_device_id = find_column_device(G)
+
+ # 查找柱容器
+ column_vessel = find_column_vessel(G, column)
+
+ # 查找溶剂容器
+ solvent1_vessel = find_solvent_vessel(G, final_solvent1)
+ solvent2_vessel = find_solvent_vessel(G, final_solvent2)
+
+ debug_print(f"设备映射:")
+ debug_print(f" - 柱设备: '{column_device_id}'")
+ debug_print(f" - 柱容器: '{column_vessel}'")
+ debug_print(f" - 溶剂1容器: '{solvent1_vessel}'")
+ debug_print(f" - 溶剂2容器: '{solvent2_vessel}'")
+
+ # === 获取源容器体积 ===
+ debug_print("步骤4: 获取源容器体积...")
+
+ source_volume = get_vessel_liquid_volume(G, from_vessel)
+ if source_volume <= 0:
+ source_volume = 100.0 # 默认体积
+ debug_print(f"⚠️ 无法获取源容器体积,使用默认值: {source_volume}mL")
+ else:
+ debug_print(f"✅ 源容器体积: {source_volume}mL")
+
+ # === 计算溶剂体积 ===
+ debug_print("步骤5: 计算溶剂体积...")
+
+ # 洗脱溶剂通常是样品体积的2-5倍
+ total_elution_volume = source_volume * 3.0
+ solvent1_volume, solvent2_volume = calculate_solvent_volumes(
+ total_elution_volume, final_pct1, final_pct2
)
-
-
-def generate_gradient_column_protocol(
- G: nx.DiGraph,
- from_vessel: str,
- to_vessel: str,
- column_material: str = "silica_gel",
- gradient_solvents: List[str] = None,
- gradient_volumes: List[float] = None
-) -> List[Dict[str, Any]]:
- """梯度洗脱柱层析:多种溶剂系统"""
- if gradient_solvents is None:
- gradient_solvents = ["hexane", "ethyl_acetate", "methanol"]
- if gradient_volumes is None:
- gradient_volumes = [50.0, 50.0, 30.0]
- action_sequence = []
+ # === 执行柱层析流程 ===
+ debug_print("步骤6: 执行柱层析流程...")
- # 每种溶剂单独执行一次柱层析
- for i, (solvent, volume) in enumerate(zip(gradient_solvents, gradient_volumes)):
- print(f"RUN_COLUMN: 梯度洗脱第 {i+1}/{len(gradient_solvents)} 步: {volume} mL {solvent}")
+ try:
+ # 步骤6.1: 样品上柱(如果有独立的柱容器)
+ if column_vessel and column_vessel != from_vessel:
+ debug_print(f"6.1: 样品上柱 - {source_volume}mL 从 {from_vessel} 到 {column_vessel}")
+
+ try:
+ sample_transfer_actions = generate_pump_protocol_with_rinsing(
+ G=G,
+ from_vessel=from_vessel,
+ to_vessel=column_vessel,
+ volume=source_volume,
+ flowrate=1.0, # 慢速上柱
+ transfer_flowrate=0.5,
+ rinsing_solvent="", # 暂不冲洗
+ rinsing_volume=0.0,
+ rinsing_repeats=0
+ )
+ action_sequence.extend(sample_transfer_actions)
+ debug_print(f"✅ 样品上柱完成,添加了 {len(sample_transfer_actions)} 个动作")
+ except Exception as e:
+ debug_print(f"⚠️ 样品上柱失败: {str(e)}")
- # 第一步使用源容器,后续步骤使用柱子作为源
- step_from_vessel = from_vessel if i == 0 else column_material
- # 最后一步使用目标容器,其他步骤使用柱子作为目标
- step_to_vessel = to_vessel if i == len(gradient_solvents) - 1 else column_material
+ # 步骤6.2: 添加洗脱溶剂1(如果有溶剂容器)
+ if solvent1_vessel and solvent1_volume > 0:
+ debug_print(f"6.2: 添加洗脱溶剂1 - {solvent1_volume:.1f}mL {final_solvent1}")
+
+ try:
+ target_vessel = column_vessel if column_vessel else from_vessel
+ solvent1_transfer_actions = generate_pump_protocol_with_rinsing(
+ G=G,
+ from_vessel=solvent1_vessel,
+ to_vessel=target_vessel,
+ volume=solvent1_volume,
+ flowrate=2.0,
+ transfer_flowrate=1.0
+ )
+ action_sequence.extend(solvent1_transfer_actions)
+ debug_print(f"✅ 溶剂1添加完成,添加了 {len(solvent1_transfer_actions)} 个动作")
+ except Exception as e:
+ debug_print(f"⚠️ 溶剂1添加失败: {str(e)}")
- step_actions = generate_run_column_protocol(
- G, step_from_vessel, step_to_vessel, column_material,
- solvent, volume, 1, "", 0.0, 1.0
- )
- action_sequence.extend(step_actions)
+ # 步骤6.3: 添加洗脱溶剂2(如果有溶剂容器)
+ if solvent2_vessel and solvent2_volume > 0:
+ debug_print(f"6.3: 添加洗脱溶剂2 - {solvent2_volume:.1f}mL {final_solvent2}")
+
+ try:
+ target_vessel = column_vessel if column_vessel else from_vessel
+ solvent2_transfer_actions = generate_pump_protocol_with_rinsing(
+ G=G,
+ from_vessel=solvent2_vessel,
+ to_vessel=target_vessel,
+ volume=solvent2_volume,
+ flowrate=2.0,
+ transfer_flowrate=1.0
+ )
+ action_sequence.extend(solvent2_transfer_actions)
+ debug_print(f"✅ 溶剂2添加完成,添加了 {len(solvent2_transfer_actions)} 个动作")
+ except Exception as e:
+ debug_print(f"⚠️ 溶剂2添加失败: {str(e)}")
- # 在梯度步骤之间加入等待时间
- if i < len(gradient_solvents) - 1:
+ # 步骤6.4: 使用柱层析设备执行分离(如果有设备)
+ if column_device_id:
+ debug_print(f"6.4: 使用柱层析设备执行分离")
+
+ column_separation_action = {
+ "device_id": column_device_id,
+ "action_name": "run_column",
+ "action_kwargs": {
+ "from_vessel": from_vessel,
+ "to_vessel": to_vessel,
+ "column": column,
+ "rf": rf,
+ "pct1": pct1,
+ "pct2": pct2,
+ "solvent1": solvent1,
+ "solvent2": solvent2,
+ "ratio": ratio
+ }
+ }
+ action_sequence.append(column_separation_action)
+ debug_print(f"✅ 柱层析设备动作已添加")
+
+ # 等待分离完成
+ separation_time = max(30, int(total_elution_volume / 2)) # 基于体积估算时间
action_sequence.append({
"action_name": "wait",
- "action_kwargs": {"time": 20}
+ "action_kwargs": {"time": separation_time}
})
+ debug_print(f"✅ 等待分离完成: {separation_time}秒")
+
+ # 步骤6.5: 产物收集(从柱容器到目标容器)
+ if column_vessel and column_vessel != to_vessel:
+ debug_print(f"6.5: 产物收集 - 从 {column_vessel} 到 {to_vessel}")
+
+ try:
+ # 估算产物体积(原始样品体积的70-90%)
+ product_volume = source_volume * 0.8
+
+ product_transfer_actions = generate_pump_protocol_with_rinsing(
+ G=G,
+ from_vessel=column_vessel,
+ to_vessel=to_vessel,
+ volume=product_volume,
+ flowrate=1.5,
+ transfer_flowrate=0.8
+ )
+ action_sequence.extend(product_transfer_actions)
+ debug_print(f"✅ 产物收集完成,添加了 {len(product_transfer_actions)} 个动作")
+ except Exception as e:
+ debug_print(f"⚠️ 产物收集失败: {str(e)}")
+
+ # 步骤6.6: 如果没有独立的柱设备和容器,执行简化的直接转移
+ if not column_device_id and not column_vessel:
+ debug_print(f"6.6: 简化模式 - 直接转移 {source_volume}mL 从 {from_vessel} 到 {to_vessel}")
+
+ try:
+ direct_transfer_actions = generate_pump_protocol_with_rinsing(
+ G=G,
+ from_vessel=from_vessel,
+ to_vessel=to_vessel,
+ volume=source_volume,
+ flowrate=2.0,
+ transfer_flowrate=1.0
+ )
+ action_sequence.extend(direct_transfer_actions)
+ debug_print(f"✅ 直接转移完成,添加了 {len(direct_transfer_actions)} 个动作")
+ except Exception as e:
+ debug_print(f"⚠️ 直接转移失败: {str(e)}")
+
+ except Exception as e:
+ debug_print(f"❌ 柱层析流程执行失败: {str(e)}")
+ # 添加错误日志动作
+ action_sequence.append({
+ "device_id": "system",
+ "action_name": "log_message",
+ "action_kwargs": {
+ "message": f"柱层析失败: {str(e)}"
+ }
+ })
+
+ # === 最终结果 ===
+ debug_print("=" * 60)
+ debug_print(f"✅ 柱层析协议生成完成")
+ debug_print(f"📊 总动作数: {len(action_sequence)}")
+ debug_print(f"📋 参数总结:")
+ debug_print(f" - 源容器: {from_vessel} ({source_volume}mL)")
+ debug_print(f" - 目标容器: {to_vessel}")
+ debug_print(f" - 柱子: {column}")
+ debug_print(f" - Rf值: {final_rf}")
+ debug_print(f" - 溶剂比例: {final_solvent1} {final_pct1:.1f}% : {final_solvent2} {final_pct2:.1f}%")
+ debug_print(f" - 洗脱体积: {solvent1_volume:.1f}mL + {solvent2_volume:.1f}mL")
+ debug_print("=" * 60)
return action_sequence
+# === 便捷函数 ===
-def generate_reverse_phase_column_protocol(
+def generate_silica_gel_column_protocol(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
- column_material: str = "C18",
- aqueous_phase: str = "water",
- organic_phase: str = "methanol",
- gradient_ratio: float = 0.5
+ **kwargs
) -> List[Dict[str, Any]]:
- """反相柱层析:C18柱,水-有机相梯度"""
- # 先用水相平衡
- equilibration_volume = 20.0
- # 然后用有机相洗脱
- eluting_volume = 30.0 * gradient_ratio
-
+ """硅胶柱层析协议便捷函数"""
return generate_run_column_protocol(
- G, from_vessel, to_vessel, column_material,
- organic_phase, eluting_volume, 2,
- aqueous_phase, equilibration_volume, 0.8
+ G, from_vessel, to_vessel,
+ column="silica_column",
+ solvent1="ethyl_acetate",
+ solvent2="hexane",
+ ratio="1:9", # 常见的EA:Hex比例
+ **kwargs
)
-
-def generate_ion_exchange_column_protocol(
+def generate_reverse_phase_column_protocol(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
- column_material: str = "ion_exchange",
- buffer_solution: str = "buffer",
- salt_solution: str = "NaCl_solution",
- salt_volume: float = 40.0
+ **kwargs
) -> List[Dict[str, Any]]:
- """离子交换柱层析:缓冲液平衡,盐溶液洗脱"""
+ """反相柱层析协议便捷函数"""
return generate_run_column_protocol(
- G, from_vessel, to_vessel, column_material,
- salt_solution, salt_volume, 1,
- buffer_solution, 25.0, 0.5
+ G, from_vessel, to_vessel,
+ column="c18_column",
+ solvent1="methanol",
+ solvent2="water",
+ ratio="7:3", # 常见的MeOH:H2O比例
+ **kwargs
)
-
-# 测试函数
-def test_run_column_protocol():
- """测试柱层析协议的示例"""
- print("=== RUN COLUMN PROTOCOL 测试 ===")
- print("测试完成")
-
-
-if __name__ == "__main__":
- test_run_column_protocol()
\ No newline at end of file
diff --git a/unilabos/compile/separate_protocol.py b/unilabos/compile/separate_protocol.py
index cbb028cb9..07d9ab4e8 100644
--- a/unilabos/compile/separate_protocol.py
+++ b/unilabos/compile/separate_protocol.py
@@ -1,230 +1,448 @@
-import numpy as np
import networkx as nx
+import re
+import logging
+from typing import List, Dict, Any, Union
+from .pump_protocol import generate_pump_protocol_with_rinsing
+logger = logging.getLogger(__name__)
-def generate_separate_protocol(
- G: nx.DiGraph,
- purpose: str, # 'wash' or 'extract'. 'wash' means that product phase will not be the added solvent phase, 'extract' means product phase will be the added solvent phase. If no solvent is added just use 'extract'.
- product_phase: str, # 'top' or 'bottom'. Phase that product will be in.
- from_vessel: str, #Contents of from_vessel are transferred to separation_vessel and separation is performed.
- separation_vessel: str, # Vessel in which separation of phases will be carried out.
- to_vessel: str, # Vessel to send product phase to.
- waste_phase_to_vessel: str, # Optional. Vessel to send waste phase to.
- solvent: str, # Optional. Solvent to add to separation vessel after contents of from_vessel has been transferred to create two phases.
- solvent_volume: float = 50, # Optional. Volume of solvent to add (mL).
- through: str = "", # Optional. Solid chemical to send product phase through on way to to_vessel, e.g. 'celite'.
- repeats: int = 1, # Optional. Number of separations to perform.
- stir_time: float = 30, # Optional. Time stir for after adding solvent, before separation of phases.
- stir_speed: float = 300, # Optional. Speed to stir at after adding solvent, before separation of phases.
- settling_time: float = 300 # Optional. Time
-) -> list[dict]:
+def debug_print(message):
+ """调试输出"""
+ print(f"[SEPARATE] {message}", flush=True)
+ logger.info(f"[SEPARATE] {message}")
+
+def parse_volume_input(volume_input: Union[str, float]) -> float:
"""
- Generate a protocol to clean a vessel with a solvent.
-
- :param G: Directed graph. Nodes are containers and pumps, edges are fluidic connections.
- :param vessel: Vessel to clean.
- :param solvent: Solvent to clean vessel with.
- :param volume: Volume of solvent to clean vessel with.
- :param temp: Temperature to heat vessel to while cleaning.
- :param repeats: Number of cleaning cycles to perform.
- :return: List of actions to clean vessel.
+ 解析体积输入,支持带单位的字符串
+
+ Args:
+ volume_input: 体积输入(如 "200 mL", "?", 50.0)
+
+ Returns:
+ float: 体积(毫升)
"""
+ if isinstance(volume_input, (int, float)):
+ return float(volume_input)
- # 生成泵操作的动作序列
- pump_action_sequence = []
- reactor_volume = 500.0
- waste_vessel = waste_phase_to_vessel
-
- # TODO:通过物料管理系统找到溶剂的容器
- if "," in solvent:
- solvents = solvent.split(",")
- assert len(solvents) == repeats, "Number of solvents must match number of repeats."
- else:
- solvents = [solvent] * repeats
-
- # TODO: 通过设备连接图找到分离容器的控制器、底部出口
- separator_controller = f"{separation_vessel}_controller"
- separation_vessel_bottom = f"flask_{separation_vessel}"
-
- transfer_flowrate = flowrate = 2.5
-
- if from_vessel != separation_vessel:
- pump_action_sequence.append(
- {
- "device_id": "",
- "action_name": "PumpTransferProtocol",
- "action_kwargs": {
- "from_vessel": from_vessel,
- "to_vessel": separation_vessel,
- "volume": reactor_volume,
- "time": reactor_volume / flowrate,
- # "transfer_flowrate": transfer_flowrate,
- }
- }
- )
-
- # for i in range(2):
- # pump_action_sequence.append(
- # {
- # "device_id": "",
- # "action_name": "CleanProtocol",
- # "action_kwargs": {
- # "vessel": from_vessel,
- # "solvent": "H2O", # Solvent to clean vessel with.
- # "volume": solvent_volume, # Optional. Volume of solvent to clean vessel with.
- # "temp": 25.0, # Optional. Temperature to heat vessel to while cleaning.
- # "repeats": 1
- # }
- # }
- # )
- # pump_action_sequence.append(
- # {
- # "device_id": "",
- # "action_name": "CleanProtocol",
- # "action_kwargs": {
- # "vessel": from_vessel,
- # "solvent": "CH2Cl2", # Solvent to clean vessel with.
- # "volume": solvent_volume, # Optional. Volume of solvent to clean vessel with.
- # "temp": 25.0, # Optional. Temperature to heat vessel to while cleaning.
- # "repeats": 1
- # }
- # }
- # )
-
- # 生成泵操作的动作序列
- for i in range(repeats):
- # 找到当次萃取所用溶剂
- solvent_thistime = solvents[i]
- solvent_vessel = f"flask_{solvent_thistime}"
-
- pump_action_sequence.append(
- {
- "device_id": "",
- "action_name": "PumpTransferProtocol",
- "action_kwargs": {
- "from_vessel": solvent_vessel,
- "to_vessel": separation_vessel,
- "volume": solvent_volume,
- "time": solvent_volume / flowrate,
- # "transfer_flowrate": transfer_flowrate,
- }
- }
- )
- pump_action_sequence.extend([
- # 搅拌、静置
- {
- "device_id": separator_controller,
- "action_name": "stir",
- "action_kwargs": {
- "stir_time": stir_time,
- "stir_speed": stir_speed,
- "settling_time": settling_time
- }
- },
- # 分液(判断电导突跃)
- {
- "device_id": separator_controller,
- "action_name": "valve_open",
- "action_kwargs": {
- "command": "delta > 0.05"
- }
- }
- ])
-
- if product_phase == "bottom":
- # 产物转移到目标瓶
- pump_action_sequence.append(
- {
- "device_id": "",
- "action_name": "PumpTransferProtocol",
- "action_kwargs": {
- "from_vessel": separation_vessel_bottom,
- "to_vessel": to_vessel,
- "volume": 250.0,
- "time": 250.0 / flowrate,
- # "transfer_flowrate": transfer_flowrate,
- }
- }
- )
- # 放出上面那一相,60秒后关阀门
- pump_action_sequence.append(
- {
- "device_id": separator_controller,
- "action_name": "valve_open",
- "action_kwargs": {
- "command": "time > 60"
- }
- }
- )
- # 弃去上面那一相进废液
- pump_action_sequence.append(
- {
- "device_id": "",
- "action_name": "PumpTransferProtocol",
- "action_kwargs": {
- "from_vessel": separation_vessel_bottom,
- "to_vessel": waste_vessel,
- "volume": 250.0,
- "time": 250.0 / flowrate,
- # "transfer_flowrate": transfer_flowrate,
- }
- }
- )
- elif product_phase == "top":
- # 弃去下面那一相进废液
- pump_action_sequence.append(
- {
- "device_id": "",
- "action_name": "PumpTransferProtocol",
- "action_kwargs": {
- "from_vessel": separation_vessel_bottom,
- "to_vessel": waste_vessel,
- "volume": 250.0,
- "time": 250.0 / flowrate,
- # "transfer_flowrate": transfer_flowrate,
- }
- }
- )
- # 放出上面那一相
- pump_action_sequence.append(
- {
- "device_id": separator_controller,
- "action_name": "valve_open",
- "action_kwargs": {
- "command": "time > 60"
- }
- }
- )
- # 产物转移到目标瓶
- pump_action_sequence.append(
- {
- "device_id": "",
- "action_name": "PumpTransferProtocol",
- "action_kwargs": {
- "from_vessel": separation_vessel_bottom,
- "to_vessel": to_vessel,
- "volume": 250.0,
- "time": 250.0 / flowrate,
- # "transfer_flowrate": transfer_flowrate,
- }
- }
- )
- elif product_phase == "organic":
- pass
+ if not volume_input or not str(volume_input).strip():
+ return 0.0
+
+ volume_str = str(volume_input).lower().strip()
+ debug_print(f"解析体积输入: '{volume_str}'")
+
+ # 处理未知体积
+ if volume_str in ['?', 'unknown', 'tbd', 'to be determined']:
+ default_volume = 100.0 # 默认100mL
+ debug_print(f"检测到未知体积,使用默认值: {default_volume}mL")
+ return default_volume
+
+ # 移除空格并提取数字和单位
+ volume_clean = re.sub(r'\s+', '', volume_str)
+
+ # 匹配数字和单位的正则表达式
+ match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
+
+ if not match:
+ debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值100mL")
+ return 100.0
+
+ value = float(match.group(1))
+ unit = match.group(2) or 'ml' # 默认单位为毫升
+
+ # 转换为毫升
+ if unit in ['l', 'liter']:
+ volume = value * 1000.0 # L -> mL
+ elif unit in ['μl', 'ul', 'microliter']:
+ volume = value / 1000.0 # μL -> mL
+ else: # ml, milliliter 或默认
+ volume = value # 已经是mL
+
+ debug_print(f"体积转换: {value}{unit} → {volume}mL")
+ return volume
+
+def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
+ """查找溶剂容器"""
+ if not solvent or not solvent.strip():
+ return ""
+
+ debug_print(f"查找溶剂 '{solvent}' 的容器...")
+
+ # 🔧 方法1:直接搜索 data.reagent_name 和 config.reagent
+ for node in G.nodes():
+ node_data = G.nodes[node].get('data', {})
+ node_type = G.nodes[node].get('type', '')
+ config_data = G.nodes[node].get('config', {})
- # 如果不是最后一次,从中转瓶转移回分液漏斗
- if i < repeats - 1:
- pump_action_sequence.append(
- {
- "device_id": "",
- "action_name": "PumpTransferProtocol",
+ # 只搜索容器类型的节点
+ if node_type == 'container':
+ reagent_name = node_data.get('reagent_name', '').lower()
+ config_reagent = config_data.get('reagent', '').lower()
+
+ # 精确匹配
+ if reagent_name == solvent.lower() or config_reagent == solvent.lower():
+ debug_print(f"✅ 通过reagent字段找到容器: {node}")
+ return node
+
+ # 模糊匹配
+ if (solvent.lower() in reagent_name and reagent_name) or \
+ (solvent.lower() in config_reagent and config_reagent):
+ debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node}")
+ return node
+
+ # 🔧 方法2:常见的容器命名规则
+ solvent_clean = solvent.lower().replace(' ', '_').replace('-', '_')
+ possible_names = [
+ f"flask_{solvent_clean}",
+ f"bottle_{solvent_clean}",
+ f"vessel_{solvent_clean}",
+ f"{solvent_clean}_flask",
+ f"{solvent_clean}_bottle",
+ f"solvent_{solvent_clean}",
+ f"reagent_{solvent_clean}",
+ f"reagent_bottle_{solvent_clean}"
+ ]
+
+ for name in possible_names:
+ if name in G.nodes():
+ node_type = G.nodes[name].get('type', '')
+ if node_type == 'container':
+ debug_print(f"✅ 通过命名规则找到容器: {name}")
+ return name
+
+ # 🔧 方法3:使用第一个试剂瓶作为备选
+ for node_id in G.nodes():
+ node_data = G.nodes[node_id]
+ if (node_data.get('type') == 'container' and
+ ('reagent' in node_id.lower() or 'bottle' in node_id.lower())):
+ debug_print(f"⚠️ 未找到专用容器,使用备选容器: {node_id}")
+ return node_id
+
+ debug_print(f"⚠️ 未找到溶剂 '{solvent}' 的容器")
+ return ""
+
+def find_separator_device(G: nx.DiGraph, vessel: str) -> str:
+ """查找分离器设备"""
+ debug_print(f"查找容器 '{vessel}' 对应的分离器设备...")
+
+ # 方法1:查找连接到容器的分离器设备
+ for node in G.nodes():
+ node_class = G.nodes[node].get('class', '').lower()
+ if 'separator' in node_class:
+ # 检查是否连接到目标容器
+ if G.has_edge(node, vessel) or G.has_edge(vessel, node):
+ debug_print(f"✅ 找到连接的分离器: {node}")
+ return node
+
+ # 方法2:根据命名规则查找
+ possible_names = [
+ f"{vessel}_controller",
+ f"{vessel}_separator",
+ vessel, # 容器本身可能就是分离器
+ "separator_1",
+ "virtual_separator"
+ ]
+
+ for name in possible_names:
+ if name in G.nodes():
+ node_class = G.nodes[name].get('class', '').lower()
+ if 'separator' in node_class:
+ debug_print(f"✅ 通过命名规则找到分离器: {name}")
+ return name
+
+ # 方法3:查找第一个分离器设备
+ for node in G.nodes():
+ node_class = G.nodes[node].get('class', '').lower()
+ if 'separator' in node_class:
+ debug_print(f"⚠️ 使用第一个分离器设备: {node}")
+ return node
+
+ debug_print(f"⚠️ 未找到分离器设备")
+ return ""
+
+def generate_separate_protocol(
+ G: nx.DiGraph,
+ # 🔧 基础参数,支持XDL的vessel参数
+ vessel: str = "", # XDL: 分离容器
+ purpose: str = "separate", # 分离目的
+ product_phase: str = "top", # 产物相
+ # 🔧 可选的详细参数
+ from_vessel: str = "", # 源容器(通常在separate前已经transfer了)
+ separation_vessel: str = "", # 分离容器(与vessel同义)
+ to_vessel: str = "", # 目标容器(可选)
+ waste_phase_to_vessel: str = "", # 废相目标容器
+ product_vessel: str = "", # XDL: 产物容器(与to_vessel同义)
+ waste_vessel: str = "", # XDL: 废液容器(与waste_phase_to_vessel同义)
+ # 🔧 溶剂相关参数
+ solvent: str = "", # 溶剂名称
+ solvent_volume: Union[str, float] = 0.0, # 溶剂体积
+ volume: Union[str, float] = 0.0, # XDL: 体积(与solvent_volume同义)
+ # 🔧 操作参数
+ through: str = "", # 通过材料
+ repeats: int = 1, # 重复次数
+ stir_time: float = 30.0, # 搅拌时间(秒)
+ stir_speed: float = 300.0, # 搅拌速度
+ settling_time: float = 300.0, # 沉降时间(秒)
+ **kwargs
+) -> List[Dict[str, Any]]:
+ """
+ 生成分离操作的协议序列 - 修复版
+
+ 支持XDL参数格式:
+ - vessel: 分离容器(必需)
+ - purpose: "wash", "extract", "separate"
+ - product_phase: "top", "bottom"
+ - product_vessel: 产物收集容器
+ - waste_vessel: 废液收集容器
+ - solvent: 溶剂名称
+ - volume: "200 mL", "?" 或数值
+ - repeats: 重复次数
+
+ 分离流程:
+ 1. (可选)添加溶剂到分离容器
+ 2. 搅拌混合
+ 3. 静置分层
+ 4. 收集指定相到目标容器
+ 5. 重复指定次数
+ """
+
+ debug_print("=" * 60)
+ debug_print("开始生成分离协议 - 修复版")
+ debug_print(f"原始参数:")
+ debug_print(f" - vessel: '{vessel}'")
+ debug_print(f" - purpose: '{purpose}'")
+ debug_print(f" - product_phase: '{product_phase}'")
+ debug_print(f" - solvent: '{solvent}'")
+ debug_print(f" - volume: {volume} (类型: {type(volume)})")
+ debug_print(f" - repeats: {repeats}")
+ debug_print(f" - product_vessel: '{product_vessel}'")
+ debug_print(f" - waste_vessel: '{waste_vessel}'")
+ debug_print("=" * 60)
+
+ action_sequence = []
+
+ # === 参数验证和标准化 ===
+ debug_print("步骤1: 参数验证和标准化...")
+
+ # 统一容器参数
+ final_vessel = vessel or separation_vessel
+ if not final_vessel:
+ raise ValueError("必须指定分离容器 (vessel 或 separation_vessel)")
+
+ final_to_vessel = to_vessel or product_vessel
+ final_waste_vessel = waste_phase_to_vessel or waste_vessel
+
+ # 统一体积参数
+ final_volume = parse_volume_input(volume or solvent_volume)
+
+ # 🔧 修复:确保repeats至少为1
+ if repeats <= 0:
+ repeats = 1
+ debug_print(f"⚠️ repeats参数 <= 0,自动设置为1")
+
+ debug_print(f"标准化参数:")
+ debug_print(f" - 分离容器: '{final_vessel}'")
+ debug_print(f" - 产物容器: '{final_to_vessel}'")
+ debug_print(f" - 废液容器: '{final_waste_vessel}'")
+ debug_print(f" - 溶剂体积: {final_volume}mL")
+ debug_print(f" - 重复次数: {repeats}")
+
+ # 验证必需参数
+ if not purpose:
+ purpose = "separate"
+ if not product_phase:
+ product_phase = "top"
+ if purpose not in ["wash", "extract", "separate"]:
+ debug_print(f"⚠️ 未知的分离目的 '{purpose}',使用默认值 'separate'")
+ purpose = "separate"
+ if product_phase not in ["top", "bottom"]:
+ debug_print(f"⚠️ 未知的产物相 '{product_phase}',使用默认值 'top'")
+ product_phase = "top"
+
+ debug_print("✅ 参数验证通过")
+
+ # === 查找设备 ===
+ debug_print("步骤2: 查找设备...")
+
+ # 查找分离器设备
+ separator_device = find_separator_device(G, final_vessel)
+ if not separator_device:
+ debug_print("⚠️ 未找到分离器设备,可能无法执行分离操作")
+
+ # 查找溶剂容器(如果需要)
+ solvent_vessel = ""
+ if solvent and solvent.strip():
+ solvent_vessel = find_solvent_vessel(G, solvent)
+
+ debug_print(f"设备映射:")
+ debug_print(f" - 分离器设备: '{separator_device}'")
+ debug_print(f" - 溶剂容器: '{solvent_vessel}'")
+
+ # === 执行分离流程 ===
+ debug_print("步骤3: 执行分离流程...")
+
+ try:
+ for repeat_idx in range(repeats):
+ debug_print(f"3.{repeat_idx+1}: 第 {repeat_idx+1}/{repeats} 次分离")
+
+ # 步骤3.1: 添加溶剂(如果需要)
+ if solvent_vessel and final_volume > 0:
+ debug_print(f"3.{repeat_idx+1}.1: 添加溶剂 {solvent} ({final_volume}mL)")
+
+ # 使用pump protocol添加溶剂
+ pump_actions = generate_pump_protocol_with_rinsing(
+ G=G,
+ from_vessel=solvent_vessel,
+ to_vessel=final_vessel,
+ volume=final_volume,
+ amount="",
+ time=0.0,
+ viscous=False,
+ rinsing_solvent="",
+ rinsing_volume=0.0,
+ rinsing_repeats=0,
+ solid=False,
+ flowrate=2.5,
+ transfer_flowrate=0.5,
+ rate_spec="",
+ event="",
+ through="",
+ **kwargs
+ )
+ action_sequence.extend(pump_actions)
+ debug_print(f"✅ 溶剂添加完成,添加了 {len(pump_actions)} 个动作")
+
+ # 步骤3.2: 执行分离操作
+ if separator_device:
+ debug_print(f"3.{repeat_idx+1}.2: 执行分离操作")
+
+ # 调用分离器设备的separate方法
+ separate_action = {
+ "device_id": separator_device,
+ "action_name": "separate",
"action_kwargs": {
- "from_vessel": to_vessel,
- "to_vessel": separation_vessel,
- "volume": 250.0,
- "time": 250.0 / flowrate,
- # "transfer_flowrate": transfer_flowrate,
+ "purpose": purpose,
+ "product_phase": product_phase,
+ "from_vessel": from_vessel or final_vessel,
+ "separation_vessel": final_vessel,
+ "to_vessel": final_to_vessel or final_vessel,
+ "waste_phase_to_vessel": final_waste_vessel or final_vessel,
+ "solvent": solvent,
+ "solvent_volume": final_volume,
+ "through": through,
+ "repeats": 1, # 每次调用只做一次分离
+ "stir_time": stir_time,
+ "stir_speed": stir_speed,
+ "settling_time": settling_time
}
}
- )
- return pump_action_sequence
+ action_sequence.append(separate_action)
+ debug_print(f"✅ 分离操作添加完成")
+
+ else:
+ debug_print(f"3.{repeat_idx+1}.2: 无分离器设备,跳过分离操作")
+ # 添加等待时间模拟分离
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": stir_time + settling_time}
+ })
+
+ # 等待间隔(除了最后一次)
+ if repeat_idx < repeats - 1:
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": 5}
+ })
+
+ except Exception as e:
+ debug_print(f"⚠️ 分离流程执行失败: {str(e)}")
+ # 添加错误日志
+ action_sequence.append({
+ "device_id": "system",
+ "action_name": "log_message",
+ "action_kwargs": {
+ "message": f"分离操作失败: {str(e)}"
+ }
+ })
+
+ # === 最终结果 ===
+ debug_print("=" * 60)
+ debug_print(f"✅ 分离协议生成完成")
+ debug_print(f"📊 总动作数: {len(action_sequence)}")
+ debug_print(f"📋 处理总结:")
+ debug_print(f" - 分离容器: {final_vessel}")
+ debug_print(f" - 分离目的: {purpose}")
+ debug_print(f" - 产物相: {product_phase}")
+ debug_print(f" - 重复次数: {repeats}")
+ if solvent:
+ debug_print(f" - 溶剂: {solvent} ({final_volume}mL)")
+ if final_to_vessel:
+ debug_print(f" - 产物容器: {final_to_vessel}")
+ if final_waste_vessel:
+ debug_print(f" - 废液容器: {final_waste_vessel}")
+ debug_print("=" * 60)
+
+ return action_sequence
+
+# === 便捷函数 ===
+
+def separate_phases_only(G: nx.DiGraph, vessel: str, product_phase: str = "top",
+ product_vessel: str = "", waste_vessel: str = "") -> List[Dict[str, Any]]:
+ """仅进行相分离(不添加溶剂)"""
+ return generate_separate_protocol(
+ G, vessel=vessel,
+ purpose="separate",
+ product_phase=product_phase,
+ product_vessel=product_vessel,
+ waste_vessel=waste_vessel
+ )
+
+def wash_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float],
+ product_phase: str = "top", repeats: int = 1) -> List[Dict[str, Any]]:
+ """用溶剂洗涤"""
+ return generate_separate_protocol(
+ G, vessel=vessel,
+ purpose="wash",
+ product_phase=product_phase,
+ solvent=solvent,
+ volume=volume,
+ repeats=repeats
+ )
+
+def extract_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float],
+ product_phase: str = "bottom", repeats: int = 3) -> List[Dict[str, Any]]:
+ """用溶剂萃取"""
+ return generate_separate_protocol(
+ G, vessel=vessel,
+ purpose="extract",
+ product_phase=product_phase,
+ solvent=solvent,
+ volume=volume,
+ repeats=repeats
+ )
+
+def separate_aqueous_organic(G: nx.DiGraph, vessel: str, organic_phase: str = "top",
+ product_vessel: str = "", waste_vessel: str = "") -> List[Dict[str, Any]]:
+ """水-有机相分离"""
+ return generate_separate_protocol(
+ G, vessel=vessel,
+ purpose="separate",
+ product_phase=organic_phase,
+ product_vessel=product_vessel,
+ waste_vessel=waste_vessel
+ )
+
+# 测试函数
+def test_separate_protocol():
+ """测试分离协议的各种参数解析"""
+ print("=== SEPARATE PROTOCOL 增强版测试 ===")
+
+ # 测试体积解析
+ volumes = ["200 mL", "?", 100.0, "1 L", "500 μL"]
+ for vol in volumes:
+ result = parse_volume_input(vol)
+ print(f"体积解析: {vol} → {result}mL")
+
+ print("✅ 测试完成")
+
+if __name__ == "__main__":
+ test_separate_protocol()
diff --git a/unilabos/compile/stir_protocol.py b/unilabos/compile/stir_protocol.py
index 76345cc3e..a67155da6 100644
--- a/unilabos/compile/stir_protocol.py
+++ b/unilabos/compile/stir_protocol.py
@@ -1,6 +1,7 @@
-from typing import List, Dict, Any
+from typing import List, Dict, Any, Union
import networkx as nx
import logging
+import re
logger = logging.getLogger(__name__)
@@ -9,6 +10,173 @@ def debug_print(message):
print(f"[STIR] {message}", flush=True)
logger.info(f"[STIR] {message}")
+def parse_time_spec(time_spec: str) -> float:
+ """
+ 解析时间规格字符串为秒数
+
+ Args:
+ time_spec: 时间规格字符串(如 "several minutes", "overnight", "few hours")
+
+ Returns:
+ float: 时间(秒)
+ """
+ if not time_spec:
+ return 0.0
+
+ time_spec = time_spec.lower().strip()
+
+ # 预定义的时间规格映射
+ time_spec_map = {
+ # 几分钟
+ "several minutes": 5.0 * 60, # 5分钟
+ "few minutes": 3.0 * 60, # 3分钟
+ "couple of minutes": 2.0 * 60, # 2分钟
+ "a few minutes": 3.0 * 60, # 3分钟
+ "some minutes": 5.0 * 60, # 5分钟
+
+ # 几小时
+ "several hours": 3.0 * 3600, # 3小时
+ "few hours": 2.0 * 3600, # 2小时
+ "couple of hours": 2.0 * 3600, # 2小时
+ "a few hours": 3.0 * 3600, # 3小时
+ "some hours": 4.0 * 3600, # 4小时
+
+ # 特殊时间
+ "overnight": 12.0 * 3600, # 12小时
+ "over night": 12.0 * 3600, # 12小时
+ "morning": 4.0 * 3600, # 4小时
+ "afternoon": 6.0 * 3600, # 6小时
+ "evening": 4.0 * 3600, # 4小时
+
+ # 短时间
+ "briefly": 30.0, # 30秒
+ "momentarily": 10.0, # 10秒
+ "quickly": 60.0, # 1分钟
+ "slowly": 10.0 * 60, # 10分钟
+
+ # 长时间
+ "extended": 6.0 * 3600, # 6小时
+ "prolonged": 8.0 * 3600, # 8小时
+ "extensively": 12.0 * 3600, # 12小时
+ }
+
+ # 直接匹配
+ if time_spec in time_spec_map:
+ result = time_spec_map[time_spec]
+ debug_print(f"时间规格解析: '{time_spec}' → {result/60:.1f}分钟")
+ return result
+
+ # 模糊匹配
+ for spec, value in time_spec_map.items():
+ if spec in time_spec or time_spec in spec:
+ result = value
+ debug_print(f"时间规格模糊匹配: '{time_spec}' → '{spec}' → {result/60:.1f}分钟")
+ return result
+
+ # 如果无法识别,返回默认值
+ default_time = 5.0 * 60 # 5分钟
+ debug_print(f"⚠️ 无法识别时间规格: '{time_spec}',使用默认值: {default_time/60:.1f}分钟")
+ return default_time
+
+def parse_time_string(time_str: str) -> float:
+ """
+ 解析时间字符串为秒数,支持多种单位
+
+ Args:
+ time_str: 时间字符串(如 "0.5 h", "30 min", "120 s", "2.5")
+
+ Returns:
+ float: 时间(秒)
+ """
+ if not time_str:
+ return 0.0
+
+ # 如果是纯数字,默认单位为秒
+ try:
+ return float(time_str)
+ except ValueError:
+ pass
+
+ # 清理字符串
+ time_str = time_str.lower().strip()
+
+ # 使用正则表达式匹配数字和单位
+ pattern = r'(\d+\.?\d*)\s*([a-z]*)'
+ match = re.match(pattern, time_str)
+
+ if not match:
+ debug_print(f"⚠️ 无法解析时间字符串: '{time_str}',使用默认值: 60秒")
+ return 60.0
+
+ value = float(match.group(1))
+ unit = match.group(2)
+
+ # 单位转换映射
+ unit_map = {
+ # 秒
+ 's': 1.0,
+ 'sec': 1.0,
+ 'second': 1.0,
+ 'seconds': 1.0,
+
+ # 分钟
+ 'm': 60.0,
+ 'min': 60.0,
+ 'mins': 60.0,
+ 'minute': 60.0,
+ 'minutes': 60.0,
+
+ # 小时
+ 'h': 3600.0,
+ 'hr': 3600.0,
+ 'hrs': 3600.0,
+ 'hour': 3600.0,
+ 'hours': 3600.0,
+
+ # 天
+ 'd': 86400.0,
+ 'day': 86400.0,
+ 'days': 86400.0,
+
+ # 如果没有单位,默认为秒
+ '': 1.0,
+ }
+
+ multiplier = unit_map.get(unit, 1.0)
+ result = value * multiplier
+
+ debug_print(f"时间字符串解析: '{time_str}' → {value} {unit or 'seconds'} → {result}秒")
+ return result
+
+def parse_time_input(time_input: Union[str, float, int], time_spec: str = "") -> float:
+ """
+ 统一的时间输入解析函数
+
+ Args:
+ time_input: 时间输入(可以是字符串、浮点数或整数)
+ time_spec: 时间规格字符串(优先级高于time_input)
+
+ Returns:
+ float: 时间(秒)
+ """
+ # 优先处理 time_spec
+ if time_spec:
+ return parse_time_spec(time_spec)
+
+ # 处理 time_input
+ if isinstance(time_input, (int, float)):
+ # 数字默认单位为秒
+ result = float(time_input)
+ debug_print(f"数字时间输入: {time_input} → {result}秒")
+ return result
+
+ if isinstance(time_input, str):
+ return parse_time_string(time_input)
+
+ # 默认值
+ debug_print(f"⚠️ 无法处理时间输入: {time_input},使用默认值: 60秒")
+ return 60.0
+
def find_connected_stirrer(G: nx.DiGraph, vessel: str = None) -> str:
"""
查找与指定容器相连的搅拌设备,或查找可用的搅拌设备
@@ -43,18 +211,25 @@ def find_connected_stirrer(G: nx.DiGraph, vessel: str = None) -> str:
def generate_stir_protocol(
G: nx.DiGraph,
vessel: str,
- stir_time: float = 300.0,
+ time: Union[str, float, int] = 300.0,
+ stir_time: Union[str, float, int] = 0.0,
+ time_spec: str = "",
+ event: str = "",
stir_speed: float = 200.0,
settling_time: float = 60.0,
- **kwargs # 🔧 接受额外参数,增强兼容性
+ **kwargs
) -> List[Dict[str, Any]]:
"""
生成搅拌操作的协议序列 - 定时搅拌 + 沉降
+ 支持 time 和 stir_time 参数统一处理
Args:
G: 设备图
vessel: 搅拌容器名称(必需)
- stir_time: 搅拌时间 (秒),默认300s
+ time: 搅拌时间(支持多种格式)
+ stir_time: 搅拌时间(与time等效)
+ time_spec: 时间规格(优先级最高)
+ event: 事件标识
stir_speed: 搅拌速度 (RPM),默认200 RPM
settling_time: 沉降时间 (秒),默认60s
**kwargs: 其他参数(兼容性)
@@ -67,9 +242,12 @@ def generate_stir_protocol(
debug_print("开始生成搅拌协议")
debug_print(f"输入参数:")
debug_print(f" - vessel: {vessel}")
- debug_print(f" - stir_time: {stir_time}s ({stir_time/60:.1f}分钟)")
- debug_print(f" - stir_speed: {stir_speed} RPM")
- debug_print(f" - settling_time: {settling_time}s ({settling_time/60:.1f}分钟)")
+ debug_print(f" - time: {time}")
+ debug_print(f" - stir_time: {stir_time}")
+ debug_print(f" - time_spec: {time_spec}")
+ debug_print(f" - event: {event}")
+ debug_print(f" - stir_speed: {stir_speed}")
+ debug_print(f" - settling_time: {settling_time}")
debug_print(f" - 其他参数: {kwargs}")
debug_print("=" * 50)
@@ -85,13 +263,29 @@ def generate_stir_protocol(
if vessel not in G.nodes():
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
+ debug_print(f"✅ 参数验证通过")
+
+ # === 时间处理(统一 time 和 stir_time)===
+ debug_print("步骤2: 时间处理...")
+
+ # 确定实际使用的时间值
+ actual_time_input = stir_time if stir_time else time
+
+ # 解析时间
+ parsed_time = parse_time_input(actual_time_input, time_spec)
+
+ debug_print(f"时间解析结果:")
+ debug_print(f" - 原始输入: time={time}, stir_time={stir_time}")
+ debug_print(f" - 时间规格: {time_spec}")
+ debug_print(f" - 最终时间: {parsed_time}秒 ({parsed_time/60:.1f}分钟)")
+
# 修正参数范围
- if stir_time < 0:
- debug_print(f"搅拌时间 {stir_time}s 无效,修正为 300s")
- stir_time = 300.0
- elif stir_time > 7200:
- debug_print(f"搅拌时间 {stir_time}s 过长,修正为 3600s")
- stir_time = 3600.0
+ if parsed_time < 0:
+ debug_print(f"搅拌时间 {parsed_time}s 无效,修正为 300s")
+ parsed_time = 300.0
+ elif parsed_time > 7200:
+ debug_print(f"搅拌时间 {parsed_time}s 过长,修正为 3600s")
+ parsed_time = 3600.0
if stir_speed < 10.0:
debug_print(f"搅拌速度 {stir_speed} RPM 过低,修正为 100 RPM")
@@ -107,10 +301,8 @@ def generate_stir_protocol(
debug_print(f"沉降时间 {settling_time}s 过长,修正为 600s")
settling_time = 600.0
- debug_print(f"✅ 参数验证通过")
-
# === 查找搅拌设备 ===
- debug_print("步骤2: 查找搅拌设备...")
+ debug_print("步骤3: 查找搅拌设备...")
try:
stirrer_id = find_connected_stirrer(G, vessel)
@@ -121,16 +313,25 @@ def generate_stir_protocol(
raise ValueError(f"无法找到搅拌设备: {str(e)}")
# === 执行搅拌操作 ===
- debug_print("步骤3: 执行搅拌操作...")
+ debug_print("步骤4: 执行搅拌操作...")
+
+ # 构建搅拌动作参数
+ stir_kwargs = {
+ "vessel": vessel,
+ "time": str(time), # 保持原始字符串格式
+ "event": event,
+ "time_spec": time_spec,
+ "stir_time": parsed_time, # 解析后的时间(秒)
+ "stir_speed": stir_speed,
+ "settling_time": settling_time
+ }
+
+ debug_print(f"搅拌参数: {stir_kwargs}")
stir_action = {
"device_id": stirrer_id,
"action_name": "stir",
- "action_kwargs": {
- "stir_time": stir_time,
- "stir_speed": stir_speed,
- "settling_time": settling_time
- }
+ "action_kwargs": stir_kwargs
}
action_sequence.append(stir_action)
@@ -140,7 +341,7 @@ def generate_stir_protocol(
debug_print(f"搅拌协议生成完成")
debug_print(f"总动作数: {len(action_sequence)}")
debug_print(f"搅拌容器: {vessel}")
- debug_print(f"搅拌参数: {stir_speed} RPM, {stir_time}s, 沉降 {settling_time}s")
+ debug_print(f"搅拌参数: {stir_speed} RPM, {parsed_time}s, 沉降 {settling_time}s")
debug_print("=" * 50)
return action_sequence
@@ -150,7 +351,7 @@ def generate_start_stir_protocol(
vessel: str,
stir_speed: float = 200.0,
purpose: str = "",
- **kwargs # 🔧 接受额外参数,增强兼容性
+ **kwargs
) -> List[Dict[str, Any]]:
"""
生成开始搅拌操作的协议序列 - 持续搅拌
@@ -237,7 +438,7 @@ def generate_start_stir_protocol(
def generate_stop_stir_protocol(
G: nx.DiGraph,
vessel: str,
- **kwargs # 🔧 接受额外参数,增强兼容性
+ **kwargs
) -> List[Dict[str, Any]]:
"""
生成停止搅拌操作的协议序列
@@ -304,56 +505,3 @@ def generate_stop_stir_protocol(
debug_print("=" * 50)
return action_sequence
-
-# === 便捷函数 ===
-
-def generate_fast_stir_protocol(
- G: nx.DiGraph,
- vessel: str,
- **kwargs
-) -> List[Dict[str, Any]]:
- """快速搅拌:高速短时间"""
- return generate_stir_protocol(
- G, vessel,
- stir_time=300.0,
- stir_speed=800.0,
- settling_time=60.0,
- **kwargs
- )
-
-def generate_gentle_stir_protocol(
- G: nx.DiGraph,
- vessel: str,
- **kwargs
-) -> List[Dict[str, Any]]:
- """温和搅拌:低速长时间"""
- return generate_stir_protocol(
- G, vessel,
- stir_time=900.0,
- stir_speed=150.0,
- settling_time=120.0,
- **kwargs
- )
-
-def generate_thorough_stir_protocol(
- G: nx.DiGraph,
- vessel: str,
- **kwargs
-) -> List[Dict[str, Any]]:
- """彻底搅拌:中速长时间"""
- return generate_stir_protocol(
- G, vessel,
- stir_time=1800.0,
- stir_speed=400.0,
- settling_time=300.0,
- **kwargs
- )
-
-# 测试函数
-def test_stir_protocol():
- """测试搅拌协议"""
- debug_print("=== STIR PROTOCOL 测试 ===")
- debug_print("✅ 测试完成")
-
-if __name__ == "__main__":
- test_stir_protocol()
\ No newline at end of file
diff --git a/unilabos/compile/wash_solid_protocol.py b/unilabos/compile/wash_solid_protocol.py
index 50bbed828..1a5c5e3c3 100644
--- a/unilabos/compile/wash_solid_protocol.py
+++ b/unilabos/compile/wash_solid_protocol.py
@@ -1,7 +1,7 @@
-from typing import List, Dict, Any
+from typing import List, Dict, Any, Union
import networkx as nx
import logging
-import sys
+import re
logger = logging.getLogger(__name__)
@@ -10,6 +10,279 @@ def debug_print(message):
print(f"[WASH_SOLID] {message}", flush=True)
logger.info(f"[WASH_SOLID] {message}")
+def parse_volume_spec(volume_spec: str) -> float:
+ """
+ 解析体积规格字符串为毫升数
+
+ Args:
+ volume_spec: 体积规格字符串(如 "small volume", "large volume")
+
+ Returns:
+ float: 体积(毫升)
+ """
+ if not volume_spec:
+ return 0.0
+
+ volume_spec = volume_spec.lower().strip()
+
+ # 预定义的体积规格映射
+ volume_spec_map = {
+ # 小体积
+ "small volume": 10.0,
+ "small amount": 10.0,
+ "minimal volume": 5.0,
+ "tiny volume": 5.0,
+ "little volume": 15.0,
+
+ # 中等体积
+ "medium volume": 50.0,
+ "moderate volume": 50.0,
+ "normal volume": 50.0,
+ "standard volume": 50.0,
+
+ # 大体积
+ "large volume": 100.0,
+ "big volume": 100.0,
+ "substantial volume": 150.0,
+ "generous volume": 200.0,
+
+ # 极端体积
+ "minimum": 5.0,
+ "maximum": 500.0,
+ "excess": 200.0,
+ "plenty": 100.0,
+ }
+
+ # 直接匹配
+ if volume_spec in volume_spec_map:
+ result = volume_spec_map[volume_spec]
+ debug_print(f"体积规格解析: '{volume_spec}' → {result}mL")
+ return result
+
+ # 模糊匹配
+ for spec, value in volume_spec_map.items():
+ if spec in volume_spec or volume_spec in spec:
+ result = value
+ debug_print(f"体积规格模糊匹配: '{volume_spec}' → '{spec}' → {result}mL")
+ return result
+
+ # 如果无法识别,返回默认值
+ default_volume = 50.0
+ debug_print(f"⚠️ 无法识别体积规格: '{volume_spec}',使用默认值: {default_volume}mL")
+ return default_volume
+
+def parse_repeats_spec(repeats_spec: str) -> int:
+ """
+ 解析重复次数规格字符串为整数
+
+ Args:
+ repeats_spec: 重复次数规格字符串(如 "several", "many")
+
+ Returns:
+ int: 重复次数
+ """
+ if not repeats_spec:
+ return 1
+
+ repeats_spec = repeats_spec.lower().strip()
+
+ # 预定义的重复次数映射
+ repeats_spec_map = {
+ # 少数次
+ "once": 1,
+ "twice": 2,
+ "few": 3,
+ "couple": 2,
+ "several": 4,
+ "some": 3,
+
+ # 多次
+ "many": 5,
+ "multiple": 4,
+ "numerous": 6,
+ "repeated": 3,
+ "extensively": 5,
+ "thoroughly": 4,
+
+ # 极端情况
+ "minimal": 1,
+ "maximum": 10,
+ "excess": 8,
+ }
+
+ # 直接匹配
+ if repeats_spec in repeats_spec_map:
+ result = repeats_spec_map[repeats_spec]
+ debug_print(f"重复次数解析: '{repeats_spec}' → {result}次")
+ return result
+
+ # 模糊匹配
+ for spec, value in repeats_spec_map.items():
+ if spec in repeats_spec or repeats_spec in spec:
+ result = value
+ debug_print(f"重复次数模糊匹配: '{repeats_spec}' → '{spec}' → {result}次")
+ return result
+
+ # 如果无法识别,返回默认值
+ default_repeats = 3
+ debug_print(f"⚠️ 无法识别重复次数规格: '{repeats_spec}',使用默认值: {default_repeats}次")
+ return default_repeats
+
+def parse_mass_to_volume(mass: str) -> float:
+ """
+ 将质量字符串转换为体积(简化假设:密度约为1 g/mL)
+
+ Args:
+ mass: 质量字符串(如 "10 g", "2.5g", "100mg")
+
+ Returns:
+ float: 体积(毫升)
+ """
+ if not mass or not mass.strip():
+ return 0.0
+
+ mass = mass.lower().strip()
+ debug_print(f"解析质量字符串: '{mass}'")
+
+ # 移除空格并提取数字和单位
+ mass_clean = re.sub(r'\s+', '', mass)
+
+ # 匹配数字和单位的正则表达式
+ match = re.match(r'([0-9]*\.?[0-9]+)\s*(g|mg|kg|gram|milligram|kilogram)?', mass_clean)
+
+ if not match:
+ debug_print(f"⚠️ 无法解析质量字符串: '{mass}',返回0.0mL")
+ return 0.0
+
+ value = float(match.group(1))
+ unit = match.group(2) or 'g' # 默认单位为克
+
+ # 转换为毫升(假设密度为1 g/mL)
+ if unit in ['mg', 'milligram']:
+ volume = value / 1000.0 # mg -> g -> mL
+ elif unit in ['kg', 'kilogram']:
+ volume = value * 1000.0 # kg -> g -> mL
+ else: # g, gram 或默认
+ volume = value # g -> mL (密度=1)
+
+ debug_print(f"质量转换: {value}{unit} → {volume}mL")
+ return volume
+
+def parse_volume_string(volume_str: str) -> float:
+ """
+ 解析体积字符串,支持带单位的输入
+
+ Args:
+ volume_str: 体积字符串(如 "10", "10 mL", "2.5L", "500μL", "?")
+
+ Returns:
+ float: 体积(毫升)
+ """
+ if not volume_str or not volume_str.strip():
+ return 0.0
+
+ volume_str = volume_str.lower().strip()
+ debug_print(f"解析体积字符串: '{volume_str}'")
+
+ # 🔧 新增:处理未知体积符号
+ if volume_str in ['?', 'unknown', 'tbd', 'to be determined', 'unspecified']:
+ default_unknown_volume = 50.0 # 未知体积时的默认值
+ debug_print(f"检测到未知体积符号 '{volume_str}',使用默认值: {default_unknown_volume}mL")
+ return default_unknown_volume
+
+ # 移除空格并提取数字和单位
+ volume_clean = re.sub(r'\s+', '', volume_str)
+
+ # 匹配数字和单位的正则表达式
+ match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
+
+ if not match:
+ debug_print(f"⚠️ 无法解析体积字符串: '{volume_str}',返回0.0mL")
+ return 0.0
+
+ value = float(match.group(1))
+ unit = match.group(2) or 'ml' # 默认单位为毫升
+
+ # 转换为毫升
+ if unit in ['l', 'liter']:
+ volume = value * 1000.0 # L -> mL
+ elif unit in ['μl', 'ul', 'microliter']:
+ volume = value / 1000.0 # μL -> mL
+ else: # ml, milliliter 或默认
+ volume = value # 已经是mL
+
+ debug_print(f"体积转换: {value}{unit} → {volume}mL")
+ return volume
+
+def parse_volume_input(volume: Union[float, str], volume_spec: str = "", mass: str = "") -> float:
+ """
+ 统一的体积输入解析函数 - 增强版
+
+ Args:
+ volume: 体积数值或字符串
+ volume_spec: 体积规格字符串(优先级最高)
+ mass: 质量字符串(优先级第二)
+
+ Returns:
+ float: 体积(毫升)
+ """
+ debug_print(f"解析体积输入: volume={volume}, volume_spec='{volume_spec}', mass='{mass}'")
+
+ # 优先级1:volume_spec
+ if volume_spec and volume_spec.strip():
+ result = parse_volume_spec(volume_spec)
+ debug_print(f"使用volume_spec: {result}mL")
+ return result
+
+ # 优先级2:mass(质量转体积)
+ if mass and mass.strip():
+ result = parse_mass_to_volume(mass)
+ if result > 0:
+ debug_print(f"使用mass转换: {result}mL")
+ return result
+
+ # 优先级3:volume
+ if volume:
+ if isinstance(volume, str):
+ # 字符串形式的体积
+ result = parse_volume_string(volume)
+ if result > 0:
+ debug_print(f"使用volume字符串: {result}mL")
+ return result
+ elif isinstance(volume, (int, float)) and volume > 0:
+ # 数值形式的体积
+ result = float(volume)
+ debug_print(f"使用volume数值: {result}mL")
+ return result
+
+ # 默认值
+ default_volume = 50.0
+ debug_print(f"⚠️ 所有体积输入无效,使用默认值: {default_volume}mL")
+ return default_volume
+
+def parse_repeats_input(repeats: int, repeats_spec: str = "") -> int:
+ """
+ 统一的重复次数输入解析函数
+
+ Args:
+ repeats: 重复次数数值
+ repeats_spec: 重复次数规格字符串(优先级高于repeats)
+
+ Returns:
+ int: 重复次数
+ """
+ # 优先处理 repeats_spec
+ if repeats_spec:
+ return parse_repeats_spec(repeats_spec)
+
+ # 处理 repeats
+ if repeats > 0:
+ return repeats
+
+ # 默认值
+ debug_print(f"⚠️ 无法处理重复次数输入: repeats={repeats}, repeats_spec='{repeats_spec}',使用默认值: 1次")
+ return 1
+
def find_solvent_source(G: nx.DiGraph, solvent: str) -> str:
"""查找溶剂源容器"""
debug_print(f"查找溶剂 '{solvent}' 的源容器...")
@@ -20,7 +293,8 @@ def find_solvent_source(G: nx.DiGraph, solvent: str) -> str:
f"reagent_bottle_{solvent}",
f"bottle_{solvent}",
f"container_{solvent}",
- f"source_{solvent}"
+ f"source_{solvent}",
+ f"liquid_reagent_bottle_{solvent}"
]
for name in possible_names:
@@ -30,6 +304,8 @@ def find_solvent_source(G: nx.DiGraph, solvent: str) -> str:
# 查找通用容器
generic_containers = [
+ "liquid_reagent_bottle_1",
+ "liquid_reagent_bottle_2",
"reagent_bottle_1",
"reagent_bottle_2",
"flask_1",
@@ -77,91 +353,51 @@ def find_filtrate_vessel(G: nx.DiGraph, filtrate_vessel: str = "") -> str:
debug_print("未找到滤液收集容器,使用默认容器")
return "waste_workup"
-def find_pump_device(G: nx.DiGraph) -> str:
- """查找转移泵设备"""
- debug_print("查找转移泵设备...")
-
- pump_devices = []
- for node in G.nodes():
- node_data = G.nodes[node]
- node_class = node_data.get('class', '') or ''
-
- if 'transfer_pump' in node_class or 'virtual_transfer_pump' in node_class:
- pump_devices.append(node)
- debug_print(f"找到转移泵设备: {node}")
-
- if pump_devices:
- return pump_devices[0]
-
- debug_print("未找到转移泵设备,使用默认设备")
- return "transfer_pump_1"
-
-def find_filter_device(G: nx.DiGraph) -> str:
- """查找过滤器设备"""
- debug_print("查找过滤器设备...")
-
- filter_devices = []
- for node in G.nodes():
- node_data = G.nodes[node]
- node_class = node_data.get('class', '') or ''
-
- if 'filter' in node_class.lower() or 'virtual_filter' in node_class:
- filter_devices.append(node)
- debug_print(f"找到过滤器设备: {node}")
-
- if filter_devices:
- return filter_devices[0]
-
- debug_print("未找到过滤器设备,使用默认设备")
- return "filter_1"
-
def generate_wash_solid_protocol(
G: nx.DiGraph,
vessel: str,
solvent: str,
- volume: float,
+ volume: Union[float, str] = 0.0, # 🔧 修改:支持字符串输入
filtrate_vessel: str = "",
temp: float = 25.0,
stir: bool = False,
stir_speed: float = 0.0,
time: float = 0.0,
repeats: int = 1,
- **kwargs # 🔧 接受额外参数,增强兼容性
+ # === 新增参数 ===
+ volume_spec: str = "", # 体积规格
+ repeats_spec: str = "", # 重复次数规格
+ mass: str = "", # 🔧 新增:固体质量(用于转换体积)
+ event: str = "", # 事件标识符
+ **kwargs
) -> List[Dict[str, Any]]:
"""
- 生成固体清洗操作的协议序列 - 简化版本
-
- Args:
- G: 设备图
- vessel: 装有固体的容器名称(必需)
- solvent: 清洗溶剂名称(必需)
- volume: 清洗溶剂体积(必需)
- filtrate_vessel: 滤液收集容器(可选,自动查找)
- temp: 清洗温度,默认25°C
- stir: 是否搅拌,默认False
- stir_speed: 搅拌速度,默认0
- time: 清洗时间,默认0
- repeats: 重复次数,默认1
- **kwargs: 其他参数(兼容性)
+ 生成固体清洗操作的协议序列 - 增强版
- Returns:
- List[Dict[str, Any]]: 固体清洗操作的动作序列
+ 支持多种体积输入方式:
+ 1. volume_spec: "small volume", "large volume" 等
+ 2. mass: "10 g", "2.5 kg", "500 mg" 等(转换为体积)
+ 3. volume: 数值或字符串 "10", "10 mL", "2.5 L" 等
"""
- debug_print("=" * 50)
+ debug_print("=" * 60)
debug_print("开始生成固体清洗协议")
debug_print(f"输入参数:")
debug_print(f" - vessel: {vessel}")
debug_print(f" - solvent: {solvent}")
- debug_print(f" - volume: {volume}mL")
- debug_print(f" - filtrate_vessel: {filtrate_vessel}")
+ debug_print(f" - volume: {volume} (类型: {type(volume)})")
+ debug_print(f" - volume_spec: '{volume_spec}'")
+ debug_print(f" - mass: '{mass}'") # 🔧 新增日志
+ debug_print(f" - filtrate_vessel: '{filtrate_vessel}'")
debug_print(f" - temp: {temp}°C")
debug_print(f" - stir: {stir}")
debug_print(f" - stir_speed: {stir_speed} RPM")
debug_print(f" - time: {time}s")
debug_print(f" - repeats: {repeats}")
+ debug_print(f" - repeats_spec: '{repeats_spec}'")
+ debug_print(f" - event: '{event}'")
debug_print(f" - 其他参数: {kwargs}")
- debug_print("=" * 50)
+ debug_print("=" * 60)
action_sequence = []
@@ -175,143 +411,246 @@ def generate_wash_solid_protocol(
if not solvent:
raise ValueError("solvent 参数不能为空")
- if volume <= 0:
- raise ValueError("volume 必须大于0")
-
if vessel not in G.nodes():
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
+ debug_print(f"✅ 必需参数验证通过")
+
+ # === 参数处理 ===
+ debug_print("步骤2: 参数处理...")
+
+ # 🔧 修改:处理体积参数(支持mass转换和字符串解析)
+ final_volume = parse_volume_input(volume, volume_spec, mass)
+ debug_print(f"最终体积: {final_volume}mL")
+
+ # 处理重复次数参数(repeats_spec优先)
+ final_repeats = parse_repeats_input(repeats, repeats_spec)
+ debug_print(f"最终重复次数: {final_repeats}次")
+
# 修正参数范围
if temp < 0 or temp > 200:
debug_print(f"温度 {temp}°C 超出范围,修正为 25°C")
temp = 25.0
if stir_speed < 0 or stir_speed > 500:
- debug_print(f"搅拌速度 {stir_speed} RPM 超出范围,修正为 0")
- stir_speed = 0.0
+ debug_print(f"搅拌速度 {stir_speed} RPM 超出范围,修正为 200 RPM")
+ stir_speed = 200.0 if stir else 0.0
if time < 0:
debug_print(f"时间 {time}s 无效,修正为 0")
time = 0.0
- if repeats < 1:
- debug_print(f"重复次数 {repeats} 无效,修正为 1")
- repeats = 1
- elif repeats > 10:
- debug_print(f"重复次数 {repeats} 过多,修正为 10")
- repeats = 10
+ if final_repeats < 1:
+ debug_print(f"重复次数 {final_repeats} 无效,修正为 1")
+ final_repeats = 1
+ elif final_repeats > 10:
+ debug_print(f"重复次数 {final_repeats} 过多,修正为 10")
+ final_repeats = 10
- debug_print(f"✅ 参数验证通过")
+ debug_print(f"✅ 参数处理完成")
# === 查找设备 ===
- debug_print("步骤2: 查找设备...")
+ debug_print("步骤3: 查找设备...")
try:
+ # 查找溶剂源
solvent_source = find_solvent_source(G, solvent)
+
+ # 查找滤液收集容器
actual_filtrate_vessel = find_filtrate_vessel(G, filtrate_vessel)
- pump_device = find_pump_device(G)
- filter_device = find_filter_device(G)
+
+ # 查找过滤器(用于过滤操作)
+ filter_device = None
+ for node in G.nodes():
+ node_data = G.nodes[node]
+ node_class = node_data.get('class', '') or ''
+ if 'filter' in node_class.lower():
+ filter_device = node
+ break
+
+ if not filter_device:
+ filter_device = "filter_1" # 默认过滤器
+
+ # 查找转移泵(用于转移溶剂)
+ transfer_pump = None
+ for node in G.nodes():
+ node_data = G.nodes[node]
+ node_class = node_data.get('class', '') or ''
+ if 'transfer' in node_class.lower() and 'pump' in node_class.lower():
+ transfer_pump = node
+ break
+
+ if not transfer_pump:
+ transfer_pump = "transfer_pump_1" # 默认转移泵
+
+ # 查找搅拌器(如果需要搅拌)
+ stirrer_device = None
+ if stir:
+ for node in G.nodes():
+ node_data = G.nodes[node]
+ node_class = node_data.get('class', '') or ''
+ if 'stirrer' in node_class.lower():
+ stirrer_device = node
+ break
+
+ if not stirrer_device:
+ stirrer_device = "stirrer_1" # 默认搅拌器
debug_print(f"设备配置:")
debug_print(f" - 溶剂源: {solvent_source}")
- debug_print(f" - 滤液容器: {actual_filtrate_vessel}")
- debug_print(f" - 转移泵: {pump_device}")
+ debug_print(f" - 转移泵: {transfer_pump}")
debug_print(f" - 过滤器: {filter_device}")
+ debug_print(f" - 搅拌器: {stirrer_device}")
+ debug_print(f" - 滤液容器: {actual_filtrate_vessel}")
except Exception as e:
debug_print(f"❌ 设备查找失败: {str(e)}")
raise ValueError(f"设备查找失败: {str(e)}")
# === 执行清洗循环 ===
- debug_print("步骤3: 执行清洗循环...")
+ debug_print("步骤4: 执行清洗循环...")
- for cycle in range(repeats):
- debug_print(f"=== 第 {cycle+1}/{repeats} 次清洗 ===")
+ for cycle in range(final_repeats):
+ debug_print(f"=== 第 {cycle+1}/{final_repeats} 次清洗 ===")
+
+ # 🔧 修复:分解为基础动作序列
- # 添加清洗溶剂
- debug_print(f"添加清洗溶剂: {solvent_source} -> {vessel}")
+ # 1. 加入清洗溶剂
+ debug_print(f" 步骤 {cycle+1}.1: 加入清洗溶剂")
+ # 🔧 修复:使用 pump protocol 而不是直接调用 transfer action
+ try:
+ from .pump_protocol import generate_pump_protocol_with_rinsing
+
+ transfer_actions = generate_pump_protocol_with_rinsing(
+ G=G,
+ from_vessel=solvent_source,
+ to_vessel=vessel,
+ volume=final_volume,
+ amount="",
+ time=0.0,
+ viscous=False,
+ rinsing_solvent="",
+ rinsing_volume=0.0,
+ rinsing_repeats=0,
+ solid=False,
+ flowrate=2.5,
+ transfer_flowrate=0.5,
+ rate_spec="",
+ event=event,
+ through=""
+ )
+
+ if transfer_actions:
+ action_sequence.extend(transfer_actions)
+ debug_print(f"✅ 添加了 {len(transfer_actions)} 个转移动作")
+ else:
+ debug_print("⚠️ 转移协议返回空序列")
+
+ except Exception as e:
+ debug_print(f"❌ 转移失败: {str(e)}")
+ # 继续执行,可能有其他问题
- wash_action = {
+ # 2. 搅拌混合(如果需要)
+ if stir and stirrer_device:
+ debug_print(f" 步骤 {cycle+1}.2: 搅拌混合")
+ stir_time = max(time, 30.0) if time > 0 else 60.0 # 默认搅拌1分钟
+
+ stir_action = {
+ "device_id": stirrer_device,
+ "action_name": "stir",
+ "action_kwargs": {
+ "vessel": vessel,
+ "time": str(int(stir_time)), # 转换为字符串格式
+ "event": event,
+ "time_spec": "",
+ "stir_time": stir_time,
+ "stir_speed": stir_speed,
+ "settling_time": 30.0
+ }
+ }
+ action_sequence.append(stir_action)
+
+ # 3. 过滤分离
+ debug_print(f" 步骤 {cycle+1}.3: 过滤分离")
+ filter_action = {
"device_id": filter_device,
- "action_name": "wash_solid",
+ "action_name": "filter",
"action_kwargs": {
"vessel": vessel,
- "solvent": solvent,
- "volume": volume,
"filtrate_vessel": actual_filtrate_vessel,
+ "stir": False, # 过滤时不搅拌
+ "stir_speed": 0.0,
"temp": temp,
- "stir": stir,
- "stir_speed": stir_speed,
- "time": time,
- "repeats": 1 # 每次循环只做1次
+ "continue_heatchill": False,
+ "volume": final_volume
}
}
- action_sequence.append(wash_action)
+ action_sequence.append(filter_action)
- # 等待清洗完成
+ # 4. 等待完成
+ wait_time = 10.0
action_sequence.append({
"action_name": "wait",
- "action_kwargs": {"time": max(10.0, time * 0.1)}
+ "action_kwargs": {"time": wait_time}
})
# === 总结 ===
- debug_print("=" * 50)
+ debug_print("=" * 60)
debug_print(f"固体清洗协议生成完成")
debug_print(f"总动作数: {len(action_sequence)}")
debug_print(f"清洗容器: {vessel}")
debug_print(f"使用溶剂: {solvent}")
- debug_print(f"清洗体积: {volume}mL")
- debug_print(f"重复次数: {repeats}")
- debug_print("=" * 50)
+ debug_print(f"清洗体积: {final_volume}mL")
+ debug_print(f"重复次数: {final_repeats}")
+ debug_print(f"滤液收集: {actual_filtrate_vessel}")
+ debug_print(f"事件标识: {event}")
+ debug_print("=" * 60)
return action_sequence
+# 删除不需要的函数,简化代码
+def find_wash_solid_device(G: nx.DiGraph) -> str:
+ """
+ 🗑️ 已弃用:WashSolid不再作为单一设备动作
+ 现在分解为基础动作序列:transfer + stir + filter
+ """
+ debug_print("⚠️ find_wash_solid_device 已弃用,使用基础动作序列")
+ return "OrganicSynthesisStation" # 兼容性返回
+
# === 便捷函数 ===
-def generate_quick_wash_protocol(
+def generate_water_wash_protocol(
G: nx.DiGraph,
vessel: str,
- solvent: str,
- volume: float,
+ volume: float = 50.0,
**kwargs
) -> List[Dict[str, Any]]:
- """快速清洗:1次,室温,不搅拌"""
+ """水洗协议:用水清洗固体"""
return generate_wash_solid_protocol(
- G, vessel, solvent, volume,
- repeats=1, temp=25.0, stir=False, **kwargs
+ G, vessel, "water", volume, **kwargs
)
-def generate_thorough_wash_protocol(
+def generate_organic_wash_protocol(
G: nx.DiGraph,
vessel: str,
solvent: str,
- volume: float,
+ volume: float = 30.0,
**kwargs
) -> List[Dict[str, Any]]:
- """彻底清洗:3次,加热,搅拌"""
+ """有机溶剂清洗协议:用有机溶剂清洗固体"""
return generate_wash_solid_protocol(
- G, vessel, solvent, volume,
- repeats=3, temp=50.0, stir=True, stir_speed=200.0, time=300.0, **kwargs
+ G, vessel, solvent, volume, **kwargs
)
-def generate_gentle_wash_protocol(
+def generate_thorough_wash_protocol(
G: nx.DiGraph,
vessel: str,
solvent: str,
- volume: float,
+ volume: float = 100.0,
**kwargs
) -> List[Dict[str, Any]]:
- """温和清洗:2次,室温,轻搅拌"""
+ """彻底清洗协议:多次清洗,搅拌,加热"""
return generate_wash_solid_protocol(
G, vessel, solvent, volume,
- repeats=2, temp=25.0, stir=True, stir_speed=100.0, time=180.0, **kwargs
- )
-
-# 测试函数
-def test_wash_solid_protocol():
- """测试固体清洗协议"""
- debug_print("=== WASH SOLID PROTOCOL 测试 ===")
- debug_print("✅ 测试完成")
-
-if __name__ == "__main__":
- test_wash_solid_protocol()
\ No newline at end of file
+ repeats=4, temp=50.0, stir=True, stir_speed=200.0, time=300.0, **kwargs
+ )
\ No newline at end of file
diff --git a/unilabos/devices/virtual/virtual_filter.py b/unilabos/devices/virtual/virtual_filter.py
index ca2e8b2cc..d70c854a8 100644
--- a/unilabos/devices/virtual/virtual_filter.py
+++ b/unilabos/devices/virtual/virtual_filter.py
@@ -67,6 +67,16 @@ async def filter(
volume: float = 0.0
) -> bool:
"""Execute filter action - 完全按照 Filter.action 参数"""
+
+ # 🔧 新增:温度自动调整
+ original_temp = temp
+ if temp == 0.0:
+ temp = 25.0 # 0度自动设置为室温
+ self.logger.info(f"温度自动调整: {original_temp}°C → {temp}°C (室温)")
+ elif temp < 4.0:
+ temp = 4.0 # 小于4度自动设置为4度
+ self.logger.info(f"温度自动调整: {original_temp}°C → {temp}°C (最低温度)")
+
self.logger.info(f"Filter: vessel={vessel}, filtrate_vessel={filtrate_vessel}")
self.logger.info(f" stir={stir}, stir_speed={stir_speed}, temp={temp}")
self.logger.info(f" continue_heatchill={continue_heatchill}, volume={volume}")
diff --git a/unilabos/devices/virtual/virtual_rotavap.py b/unilabos/devices/virtual/virtual_rotavap.py
index fbbfe9f07..9f576b5ec 100644
--- a/unilabos/devices/virtual/virtual_rotavap.py
+++ b/unilabos/devices/virtual/virtual_rotavap.py
@@ -3,6 +3,9 @@
import time as time_module
from typing import Dict, Any, Optional
+def debug_print(message):
+ """调试输出"""
+ print(f"[ROTAVAP] {message}", flush=True)
class VirtualRotavap:
"""Virtual rotary evaporator device - 简化版,只保留核心功能"""
@@ -70,12 +73,19 @@ async def evaporate(
vessel: str,
pressure: float = 0.1,
temp: float = 60.0,
- time: float = 1800.0, # 30分钟默认
+ time: float = 1800.0,
stir_speed: float = 100.0,
- solvent: str = "", # 🔧 新增参数
- **kwargs # 🔧 接受额外参数
+ solvent: str = "",
+ **kwargs
) -> bool:
- """Execute evaporate action - 兼容性增强版"""
+ """Execute evaporate action - 简化版"""
+
+ # 🔧 简化处理:如果vessel就是设备自己,直接操作
+ if vessel == self.device_id:
+ debug_print(f"在设备 {self.device_id} 上直接执行蒸发操作")
+ actual_vessel = self.device_id
+ else:
+ actual_vessel = vessel
# 参数预处理
if solvent:
@@ -86,8 +96,12 @@ async def evaporate(
temp = max(temp, 80.0)
pressure = max(pressure, 0.2)
self.logger.info("水系溶剂:调整参数")
+ elif any(s in solvent_lower for s in ['ethanol', 'methanol', 'acetone']):
+ temp = min(temp, 50.0)
+ pressure = min(pressure, 0.05)
+ self.logger.info("易挥发溶剂:调整参数")
- self.logger.info(f"Evaporate: vessel={vessel}, pressure={pressure} bar, temp={temp}°C, time={time}s, rotation={stir_speed} RPM, solvent={solvent}")
+ self.logger.info(f"Evaporate: vessel={actual_vessel}, pressure={pressure} bar, temp={temp}°C, time={time}s, rotation={stir_speed} RPM, solvent={solvent}")
# 验证参数
if temp > self._max_temp or temp < 10.0:
@@ -96,6 +110,9 @@ async def evaporate(
self.data.update({
"status": f"Error: {error_msg}",
"rotavap_state": "Error",
+ "current_temp": 25.0,
+ "progress": 0.0,
+ "evaporated_volume": 0.0,
"message": error_msg
})
return False
@@ -106,6 +123,9 @@ async def evaporate(
self.data.update({
"status": f"Error: {error_msg}",
"rotavap_state": "Error",
+ "current_temp": 25.0,
+ "progress": 0.0,
+ "evaporated_volume": 0.0,
"message": error_msg
})
return False
@@ -116,13 +136,16 @@ async def evaporate(
self.data.update({
"status": f"Error: {error_msg}",
"rotavap_state": "Error",
+ "current_temp": 25.0,
+ "progress": 0.0,
+ "evaporated_volume": 0.0,
"message": error_msg
})
return False
# 开始蒸发
self.data.update({
- "status": f"蒸发中: {vessel}",
+ "status": f"蒸发中: {actual_vessel}",
"rotavap_state": "Evaporating",
"current_temp": temp,
"target_temp": temp,
@@ -131,7 +154,7 @@ async def evaporate(
"remaining_time": time,
"progress": 0.0,
"evaporated_volume": 0.0,
- "message": f"Evaporating {vessel} at {temp}°C, {pressure} bar, {stir_speed} RPM"
+ "message": f"Evaporating {actual_vessel} at {temp}°C, {pressure} bar, {stir_speed} RPM"
})
try:
@@ -148,12 +171,13 @@ async def evaporate(
# 模拟蒸发体积
evaporated_vol = progress * 0.8 # 假设最多蒸发80mL
- # 更新状态
+ # 🔧 更新状态 - 确保包含所有必需字段
self.data.update({
"remaining_time": remaining,
- "progress": progress,
- "evaporated_volume": evaporated_vol,
- "status": f"蒸发中: {vessel} | {temp}°C | {pressure} bar | {progress:.1f}% | 剩余: {remaining:.0f}s",
+ "progress": progress, # 确保这个字段存在
+ "evaporated_volume": evaporated_vol, # 确保这个字段存在
+ "current_temp": temp, # 确保这个字段存在
+ "status": f"蒸发中: {actual_vessel} | {temp}°C | {pressure} bar | {progress:.1f}% | 剩余: {remaining:.0f}s",
"message": f"Evaporating: {progress:.1f}% complete, {remaining:.0f}s remaining"
})
@@ -167,18 +191,18 @@ async def evaporate(
# 蒸发完成
final_evaporated = 80.0
self.data.update({
- "status": f"蒸发完成: {vessel} | 蒸发量: {final_evaporated:.1f}mL",
+ "status": f"蒸发完成: {actual_vessel} | 蒸发量: {final_evaporated:.1f}mL",
"rotavap_state": "Completed",
"evaporated_volume": final_evaporated,
"progress": 100.0,
+ "current_temp": temp, # 保持温度信息
"remaining_time": 0.0,
- "current_temp": 25.0, # 冷却下来
"rotation_speed": 0.0, # 停止旋转
"vacuum_pressure": 1.0, # 恢复大气压
- "message": f"Evaporation completed: {final_evaporated}mL evaporated from {vessel}"
+ "message": f"Evaporation completed: {final_evaporated}mL evaporated from {actual_vessel}"
})
- self.logger.info(f"Evaporation completed: {final_evaporated}mL evaporated from {vessel}")
+ self.logger.info(f"Evaporation completed: {final_evaporated}mL evaporated from {actual_vessel}")
return True
except Exception as e:
@@ -189,6 +213,8 @@ async def evaporate(
"status": f"蒸发错误: {str(e)}",
"rotavap_state": "Error",
"current_temp": 25.0,
+ "progress": 0.0,
+ "evaporated_volume": 0.0,
"rotation_speed": 0.0,
"vacuum_pressure": 1.0,
"message": f"Evaporation failed: {str(e)}"
diff --git a/unilabos/devices/virtual/virtual_solenoid_valve.py b/unilabos/devices/virtual/virtual_solenoid_valve.py
index f25cc843b..54a1e6d05 100644
--- a/unilabos/devices/virtual/virtual_solenoid_valve.py
+++ b/unilabos/devices/virtual/virtual_solenoid_valve.py
@@ -43,10 +43,25 @@ def valve_state(self) -> str:
def is_open(self) -> bool:
return self._is_open
- def get_valve_position(self) -> str:
+ @property
+ def valve_position(self) -> str:
"""获取阀门位置状态"""
return "OPEN" if self._is_open else "CLOSED"
+ @property
+ def state(self) -> dict:
+ """获取阀门完整状态"""
+ return {
+ "device_id": self.device_id,
+ "port": self.port,
+ "voltage": self.voltage,
+ "response_time": self.response_time,
+ "is_open": self._is_open,
+ "valve_state": self._valve_state,
+ "status": self._status,
+ "position": self.valve_position
+ }
+
async def set_valve_position(self, command: str = None, **kwargs):
"""
设置阀门位置 - ROS动作接口
@@ -91,7 +106,7 @@ async def set_valve_position(self, command: str = None, **kwargs):
return {
"success": True,
"message": result_msg,
- "valve_position": self.get_valve_position()
+ "valve_position": self.valve_position
}
async def open(self, **kwargs):
@@ -102,21 +117,25 @@ async def close(self, **kwargs):
"""关闭电磁阀 - ROS动作接口"""
return await self.set_valve_position(command="CLOSED")
- async def set_state(self, command: Union[bool, str], **kwargs):
+ async def set_status(self, string: str = None, **kwargs):
"""
- 设置阀门状态 - 兼容 SendCmd 类型
+ 设置阀门状态 - 兼容 StrSingleInput 类型
Args:
- command: True/False 或 "open"/"close"
+ string: "ON"/"OFF" 或 "OPEN"/"CLOSED"
"""
- if isinstance(command, bool):
- cmd_str = "OPEN" if command else "CLOSED"
- elif isinstance(command, str):
- cmd_str = command
+ if string is None:
+ return {"success": False, "message": "Missing string parameter"}
+
+ # 将 string 参数转换为 command 参数
+ if string.upper() in ["ON", "OPEN"]:
+ command = "OPEN"
+ elif string.upper() in ["OFF", "CLOSED"]:
+ command = "CLOSED"
else:
- return {"success": False, "message": "Invalid command type"}
+ command = string
- return await self.set_valve_position(command=cmd_str)
+ return await self.set_valve_position(command=command)
def toggle(self):
"""切换阀门状态"""
@@ -129,19 +148,6 @@ def is_closed(self) -> bool:
"""检查阀门是否关闭"""
return not self._is_open
- def get_state(self) -> dict:
- """获取阀门完整状态"""
- return {
- "device_id": self.device_id,
- "port": self.port,
- "voltage": self.voltage,
- "response_time": self.response_time,
- "is_open": self._is_open,
- "valve_state": self._valve_state,
- "status": self._status,
- "position": self.get_valve_position()
- }
-
async def reset(self):
"""重置阀门到关闭状态"""
return await self.close()
\ No newline at end of file
diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml
index 1bbc5f7f1..3a98e566c 100644
--- a/unilabos/registry/devices/virtual_device.yaml
+++ b/unilabos/registry/devices/virtual_device.yaml
@@ -2376,10 +2376,8 @@ virtual_rotavap:
type: UniLabJsonCommandAsync
evaporate:
feedback:
- current_temp: current_temp
- evaporated_volume: evaporated_volume
- progress: progress
status: status
+ current_device: current_device
goal:
pressure: pressure
stir_speed: stir_speed
@@ -3180,6 +3178,54 @@ virtual_solenoid_valve:
title: StrSingleInput
type: object
type: StrSingleInput
+ set_valve_position:
+ feedback: {}
+ goal:
+ command: command
+ goal_default:
+ command: ''
+ handles: []
+ result:
+ success: success
+ message: message
+ valve_position: valve_position
+ schema:
+ description: ROS Action SendCmd 的 JSON Schema
+ properties:
+ feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
+ properties: {}
+ required: []
+ title: SendCmd_Feedback
+ type: object
+ goal:
+ description: Action 目标 - 从客户端发送到服务器
+ properties:
+ command:
+ type: string
+ required:
+ - command
+ title: SendCmd_Goal
+ type: object
+ result:
+ description: Action 结果 - 完成后从服务器发送到客户端
+ properties:
+ success:
+ type: boolean
+ message:
+ type: string
+ valve_position:
+ type: string
+ required:
+ - success
+ - message
+ title: SendCmd_Result
+ type: object
+ required:
+ - goal
+ title: SendCmd
+ type: object
+ type: SendCmd
module: unilabos.devices.virtual.virtual_solenoid_valve:VirtualSolenoidValve
status_types:
is_open: bool
diff --git a/unilabos_msgs/action/Add.action b/unilabos_msgs/action/Add.action
index 0c6ed2a85..021199ac0 100644
--- a/unilabos_msgs/action/Add.action
+++ b/unilabos_msgs/action/Add.action
@@ -1,14 +1,14 @@
# Goal - 添加试剂的目标参数
string vessel # 目标容器(必需)
string reagent # 试剂名称(必需)
-float64 volume # 体积 (mL,可选)
-float64 mass # 质量 (g,可选)
-string amount # 数量描述 (可选)
-float64 time # 添加时间 (s,可选)
+string volume # 体积(如 "2.7 mL",可选)
+string mass # 质量(如 "19.3 g",可选)
+string amount # 数量描述(可选)
+string time # 添加时间(如 "1 h", "20 min",可选)
bool stir # 是否搅拌(可选)
float64 stir_speed # 搅拌速度 (RPM,可选)
bool viscous # 是否为粘性液体(可选)
-string purpose # 添加目的 (可选)
+string purpose # 添加目的(可选)
string event # 事件标识(如 'A', 'B',可选)
string mol # 摩尔数(如 '0.28 mol', '16.2 mmol',可选)
string rate_spec # 速率规格(如 'portionwise', 'dropwise',可选)
diff --git a/unilabos_msgs/action/Dissolve.action b/unilabos_msgs/action/Dissolve.action
index e2d2c43c4..f070a611c 100644
--- a/unilabos_msgs/action/Dissolve.action
+++ b/unilabos_msgs/action/Dissolve.action
@@ -1,14 +1,15 @@
# Goal - 溶解操作的目标参数
string vessel # 装有要溶解物质的容器名称(必需)
string solvent # 用于溶解物质的溶剂名称(可选)
-float64 volume # 溶剂的体积(可选)
-string amount # 要溶解物质的量(可选)
-float64 temp # 溶解时的温度(可选)
-float64 time # 溶解的时间(可选)
-float64 stir_speed # 搅拌速度(可选)
+string volume # 溶剂的体积(如 "10 mL",可选)
+string amount # 要溶解物质的量描述(可选)
+string temp # 溶解时的温度(如 "60 °C", "room temperature",可选)
+string time # 溶解的时间(如 "30 min", "1 h",可选)
+float64 stir_speed # 搅拌速度(可选,默认300 RPM)
string mass # 物质质量(如 "2.9 g",可选)
string mol # 物质摩尔数(如 "0.12 mol",可选)
string reagent # 试剂名称(可选)
+string event # 事件标识(如 'A', 'B',可选)
---
# Result - 操作结果
bool success # 操作是否成功
diff --git a/unilabos_msgs/action/Separate.action b/unilabos_msgs/action/Separate.action
index a487fea72..fc185b6ed 100644
--- a/unilabos_msgs/action/Separate.action
+++ b/unilabos_msgs/action/Separate.action
@@ -1,21 +1,21 @@
# Goal - 分离操作的目标参数
-string purpose # 分离目的 ('wash', 'extract', 'separate',必需)
-string product_phase # 产物相 ('top', 'bottom',必需)
+string vessel # 分离容器名称(XDL参数,必需)
+string purpose # 分离目的 ('wash', 'extract', 'separate',可选)
+string product_phase # 产物相 ('top', 'bottom',可选)
string from_vessel # 源容器(可选)
-string separation_vessel # 分离容器(可选)
+string separation_vessel # 分离容器(与vessel同义,可选)
string to_vessel # 目标容器(可选)
string waste_phase_to_vessel # 废相目标容器(可选)
-string solvent # 溶剂名称(可选)
-float64 solvent_volume # 溶剂体积(可选)
-string through # 通过材料(如 'celite',可选)
-int32 repeats # 重复次数(可选)
-float64 stir_time # 搅拌时间(可选)
-float64 stir_speed # 搅拌速度(可选)
-float64 settling_time # 沉降时间(可选)
-string vessel # 分离容器名称(XDL参数,可选)
-string volume # 体积规格(XDL参数,可选)
string product_vessel # 产物收集容器(XDL参数,可选)
string waste_vessel # 废液收集容器(XDL参数,可选)
+string solvent # 溶剂名称(可选)
+string solvent_volume # 溶剂体积(如 "200 mL",可选)
+string volume # 体积规格(XDL参数,如 "?",可选)
+string through # 通过材料(如 'celite',可选)
+int32 repeats # 重复次数(可选,默认1)
+float64 stir_time # 搅拌时间(可选,默认30秒)
+float64 stir_speed # 搅拌速度(可选,默认300 RPM)
+float64 settling_time # 沉降时间(可选,默认300秒)
---
# Result - 操作结果
bool success # 操作是否成功
@@ -24,6 +24,4 @@ string return_info
---
# Feedback - 实时反馈
string status # 当前状态描述
-string current_device # 当前设备
-builtin_interfaces/Duration time_spent # 已用时间
-builtin_interfaces/Duration time_remaining # 剩余时间
+float64 progress # 进度百分比 (0-100)
diff --git a/unilabos_msgs/action/WashSolid.action b/unilabos_msgs/action/WashSolid.action
index 7aa3c6e13..8ef159d73 100644
--- a/unilabos_msgs/action/WashSolid.action
+++ b/unilabos_msgs/action/WashSolid.action
@@ -1,13 +1,17 @@
# Goal - 固体清洗操作的目标参数
string vessel # 装有固体的容器名称(必需)
string solvent # 清洗溶剂名称(必需)
-float64 volume # 清洗溶剂体积(必需)
+string volume # 🔧 修改:体积(支持数字和带单位的字符串)
string filtrate_vessel # 滤液收集容器(可选,默认"")
float64 temp # 清洗温度(可选,默认25.0)
bool stir # 是否搅拌(可选,默认false)
float64 stir_speed # 搅拌速度(可选,默认0.0)
float64 time # 清洗时间(可选,默认0.0)
-int32 repeats # 重复次数(可选,默认1)
+int32 repeats # 重复次数(与repeats_spec二选一)
+string volume_spec # 体积规格(优先级高于volume)
+string repeats_spec # 重复次数规格(优先级高于repeats)
+string mass # 固体质量描述(可选)
+string event # 事件标识符(可选)
---
# Result - 操作结果
bool success # 操作是否成功
From 23eb1139a98ab8b1e621e296b8ab47ad40b87a98 Mon Sep 17 00:00:00 2001
From: KCFeng425 <2100011801@stu.pku.edu.cn>
Date: Thu, 10 Jul 2025 18:25:13 +0800
Subject: [PATCH 7/9] =?UTF-8?q?=E4=BF=AE=E8=A1=A5=E4=BA=86=E4=B8=80?=
=?UTF-8?q?=E4=BA=9B=E5=8D=95=E4=BD=8D=E5=A4=84=E7=90=86=EF=BC=8Cbump=20ve?=
=?UTF-8?q?rsion=20to=200.9.11?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 2 +-
README_zh.md | 2 +-
recipes/ros-humble-unilabos-msgs/recipe.yaml | 2 +-
recipes/unilabos/recipe.yaml | 2 +-
setup.py | 2 +-
.../comprehensive_protocol/checklist.md | 10 +-
unilabos/compile/evaporate_protocol.py | 122 +++++++--
unilabos/compile/heatchill_protocol.py | 172 ++++++++----
unilabos/compile/recrystallize_protocol.py | 121 +++++++--
unilabos/compile/stir_protocol.py | 154 +++++++++--
unilabos/compile/utils/unit_parser.py | 206 +++++++++++++++
unilabos/compile/wash_solid_protocol.py | 247 +++++++++++++++---
unilabos/devices/virtual/virtual_heatchill.py | 67 ++++-
unilabos/devices/virtual/virtual_rotavap.py | 2 +-
unilabos/messages/__init__.py | 2 +-
unilabos/registry/devices/virtual_device.yaml | 4 +-
unilabos_msgs/action/Evaporate.action | 12 +-
unilabos_msgs/action/HeatChill.action | 2 +-
unilabos_msgs/action/Recrystallize.action | 10 +-
unilabos_msgs/action/Stir.action | 4 +-
unilabos_msgs/action/WashSolid.action | 4 +-
21 files changed, 962 insertions(+), 187 deletions(-)
create mode 100644 unilabos/compile/utils/unit_parser.py
diff --git a/README.md b/README.md
index 5d14705a8..918e6faeb 100644
--- a/README.md
+++ b/README.md
@@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n environment_name
# Currently, you need to install the `unilabos_msgs` package
# You can download the system-specific package from the Release page
-conda install ros-humble-unilabos-msgs-10-xxxxx.tar.bz2
+conda install ros-humble-unilabos-msgs-0.9.10-xxxxx.tar.bz2
# Install PyLabRobot and other prerequisites
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
diff --git a/README_zh.md b/README_zh.md
index 5bd588bbd..416385311 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n 环境名
# 现阶段,需要安装 `unilabos_msgs` 包
# 可以前往 Release 页面下载系统对应的包进行安装
-conda install ros-humble-unilabos-msgs-0.9.10-xxxxx.tar.bz2
+conda install ros-humble-unilabos-msgs-0.9.11-xxxxx.tar.bz2
# 安装PyLabRobot等前置
git clone https://github.com/PyLabRobot/pylabrobot plr_repo
diff --git a/recipes/ros-humble-unilabos-msgs/recipe.yaml b/recipes/ros-humble-unilabos-msgs/recipe.yaml
index 0445115d0..5807be8a1 100644
--- a/recipes/ros-humble-unilabos-msgs/recipe.yaml
+++ b/recipes/ros-humble-unilabos-msgs/recipe.yaml
@@ -1,6 +1,6 @@
package:
name: ros-humble-unilabos-msgs
- version: 0.9.10
+ version: 0.9.11
source:
path: ../../unilabos_msgs
folder: ros-humble-unilabos-msgs/src/work
diff --git a/recipes/unilabos/recipe.yaml b/recipes/unilabos/recipe.yaml
index a5768558b..2f72c32a6 100644
--- a/recipes/unilabos/recipe.yaml
+++ b/recipes/unilabos/recipe.yaml
@@ -1,6 +1,6 @@
package:
name: unilabos
- version: "0.9.10"
+ version: "0.9.11"
source:
path: ../..
diff --git a/setup.py b/setup.py
index 80163eab1..8a7eebd25 100644
--- a/setup.py
+++ b/setup.py
@@ -4,7 +4,7 @@
setup(
name=package_name,
- version='0.9.10',
+ version='0.9.11',
packages=find_packages(),
include_package_data=True,
install_requires=['setuptools'],
diff --git a/test/experiments/comprehensive_protocol/checklist.md b/test/experiments/comprehensive_protocol/checklist.md
index 5dee23a15..67b4c3b65 100644
--- a/test/experiments/comprehensive_protocol/checklist.md
+++ b/test/experiments/comprehensive_protocol/checklist.md
@@ -247,4 +247,12 @@ class HydrogenateProtocol(BaseModel):
还差
-
\ No newline at end of file
+
+
+
+单位修复:
+ evaporate
+ heatchill
+ recrysitallize
+ stir
+ wash solid
\ No newline at end of file
diff --git a/unilabos/compile/evaporate_protocol.py b/unilabos/compile/evaporate_protocol.py
index e3ffb8623..4eedb3cdd 100644
--- a/unilabos/compile/evaporate_protocol.py
+++ b/unilabos/compile/evaporate_protocol.py
@@ -1,6 +1,7 @@
-from typing import List, Dict, Any, Optional
+from typing import List, Dict, Any, Optional, Union
import networkx as nx
import logging
+import re
logger = logging.getLogger(__name__)
@@ -9,6 +10,63 @@ def debug_print(message):
print(f"[EVAPORATE] {message}", flush=True)
logger.info(f"[EVAPORATE] {message}")
+def parse_time_input(time_input: Union[str, float]) -> float:
+ """
+ 解析时间输入,支持带单位的字符串
+
+ Args:
+ time_input: 时间输入(如 "3 min", "180", "0.5 h" 等)
+
+ Returns:
+ float: 时间(秒)
+ """
+ if isinstance(time_input, (int, float)):
+ return float(time_input)
+
+ if not time_input or not str(time_input).strip():
+ return 180.0 # 默认3分钟
+
+ time_str = str(time_input).lower().strip()
+ debug_print(f"解析时间输入: '{time_str}'")
+
+ # 处理未知时间
+ if time_str in ['?', 'unknown', 'tbd']:
+ default_time = 180.0 # 默认3分钟
+ debug_print(f"检测到未知时间,使用默认值: {default_time}s")
+ return default_time
+
+ # 移除空格并提取数字和单位
+ time_clean = re.sub(r'\s+', '', time_str)
+
+ # 匹配数字和单位的正则表达式
+ match = re.match(r'([0-9]*\.?[0-9]+)\s*(s|sec|second|min|minute|h|hr|hour|d|day)?', time_clean)
+
+ if not match:
+ # 如果无法解析,尝试直接转换为数字(默认秒)
+ try:
+ value = float(time_str)
+ debug_print(f"时间解析: {time_str} → {value}s(无单位,默认秒)")
+ return value
+ except ValueError:
+ debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值180s")
+ return 180.0
+
+ value = float(match.group(1))
+ unit = match.group(2) or 's' # 默认单位为秒
+
+ # 转换为秒
+ if unit in ['min', 'minute']:
+ time_sec = value * 60.0 # min -> s
+ elif unit in ['h', 'hr', 'hour']:
+ time_sec = value * 3600.0 # h -> s
+ elif unit in ['d', 'day']:
+ time_sec = value * 86400.0 # d -> s
+ else: # s, sec, second 或默认
+ time_sec = value # 已经是s
+
+ debug_print(f"时间转换: {value}{unit} → {time_sec}s")
+ return time_sec
+
def find_rotavap_device(G: nx.DiGraph, vessel: str = None) -> Optional[str]:
"""
在组态图中查找旋转蒸发仪设备
@@ -111,20 +169,20 @@ def generate_evaporate_protocol(
vessel: str,
pressure: float = 0.1,
temp: float = 60.0,
- time: float = 180.0,
+ time: Union[str, float] = "180", # 🔧 修改:支持字符串时间
stir_speed: float = 100.0,
solvent: str = "",
- **kwargs # 接受任意额外参数,增强兼容性
+ **kwargs
) -> List[Dict[str, Any]]:
"""
- 生成蒸发操作的协议序列
+ 生成蒸发操作的协议序列 - 支持单位
Args:
G: 设备图
vessel: 容器名称或旋转蒸发仪名称
pressure: 真空度 (bar),默认0.1
temp: 加热温度 (°C),默认60
- time: 蒸发时间 (秒),默认180
+ time: 蒸发时间(支持 "3 min", "180", "0.5 h" 等)
stir_speed: 旋转速度 (RPM),默认100
solvent: 溶剂名称(用于参数优化)
**kwargs: 其他参数(兼容性)
@@ -134,12 +192,12 @@ def generate_evaporate_protocol(
"""
debug_print("=" * 50)
- debug_print("开始生成蒸发协议")
+ debug_print("开始生成蒸发协议(支持单位)")
debug_print(f"输入参数:")
debug_print(f" - vessel: {vessel}")
debug_print(f" - pressure: {pressure} bar")
debug_print(f" - temp: {temp}°C")
- debug_print(f" - time: {time}s ({time/60:.1f}分钟)")
+ debug_print(f" - time: {time} (类型: {type(time)})")
debug_print(f" - stir_speed: {stir_speed} RPM")
debug_print(f" - solvent: '{solvent}'")
debug_print("=" * 50)
@@ -177,8 +235,15 @@ def generate_evaporate_protocol(
debug_print(f"容器 '{vessel}' 不存在或类型不正确,使用旋转蒸发仪设备: {rotavap_device}")
target_vessel = rotavap_device
- # === 步骤3: 参数验证和修正 ===
- debug_print("步骤3: 参数验证和修正...")
+ # === 🔧 新增:步骤3:单位解析处理 ===
+ debug_print("步骤3: 单位解析处理...")
+
+ # 解析时间
+ final_time = parse_time_input(time)
+ debug_print(f"时间解析: {time} → {final_time}s ({final_time/60:.1f}分钟)")
+
+ # === 步骤4: 参数验证和修正 ===
+ debug_print("步骤4: 参数验证和修正...")
# 修正参数范围
if pressure <= 0 or pressure > 1.0:
@@ -189,9 +254,9 @@ def generate_evaporate_protocol(
debug_print(f"温度 {temp}°C 超出范围,修正为 60°C")
temp = 60.0
- if time <= 0:
- debug_print(f"时间 {time}s 无效,修正为 1800s")
- time = 1800.0
+ if final_time <= 0:
+ debug_print(f"时间 {final_time}s 无效,修正为 180s")
+ final_time = 180.0
if stir_speed < 10.0 or stir_speed > 300.0:
debug_print(f"旋转速度 {stir_speed} RPM 超出范围,修正为 100 RPM")
@@ -215,10 +280,10 @@ def generate_evaporate_protocol(
pressure = min(pressure, 0.01)
debug_print("高沸点溶剂:提高温度,降低真空度")
- debug_print(f"最终参数: pressure={pressure}, temp={temp}, time={time}, stir_speed={stir_speed}")
+ debug_print(f"最终参数: pressure={pressure}, temp={temp}, time={final_time}, stir_speed={stir_speed}")
- # === 步骤4: 生成动作序列 ===
- debug_print("步骤4: 生成动作序列...")
+ # === 步骤5: 生成动作序列 ===
+ debug_print("步骤5: 生成动作序列...")
action_sequence = []
@@ -237,7 +302,7 @@ def generate_evaporate_protocol(
"vessel": target_vessel,
"pressure": pressure,
"temp": temp,
- "time": time,
+ "time": final_time,
"stir_speed": stir_speed,
"solvent": solvent
}
@@ -256,7 +321,7 @@ def generate_evaporate_protocol(
debug_print(f"总动作数: {len(action_sequence)}")
debug_print(f"旋转蒸发仪: {rotavap_device}")
debug_print(f"目标容器: {target_vessel}")
- debug_print(f"蒸发参数: {pressure} bar, {temp}°C, {time}s, {stir_speed} RPM")
+ debug_print(f"蒸发参数: {pressure} bar, {temp}°C, {final_time}s, {stir_speed} RPM")
debug_print("=" * 50)
return action_sequence
@@ -273,7 +338,7 @@ def generate_quick_evaporate_protocol(
G, vessel,
pressure=0.2,
temp=40.0,
- time=900.0,
+ time="15 min", # 🔧 使用带单位的时间
stir_speed=80.0,
**kwargs
)
@@ -288,7 +353,7 @@ def generate_gentle_evaporate_protocol(
G, vessel,
pressure=0.1,
temp=50.0,
- time=2700.0,
+ time="45 min", # 🔧 使用带单位的时间
stir_speed=60.0,
**kwargs
)
@@ -303,7 +368,7 @@ def generate_high_vacuum_evaporate_protocol(
G, vessel,
pressure=0.01,
temp=35.0,
- time=3600.0,
+ time="1 h", # 🔧 使用带单位的时间
stir_speed=120.0,
**kwargs
)
@@ -318,7 +383,22 @@ def generate_standard_evaporate_protocol(
G, vessel,
pressure=0.1,
temp=60.0,
- time=1800.0,
+ time="3 min", # 🔧 使用带单位的时间
stir_speed=100.0,
**kwargs
)
+
+# 测试函数
+def test_time_parsing():
+ """测试时间解析功能"""
+ print("=== EVAPORATE 时间解析测试 ===")
+
+ test_times = ["3 min", "180", "0.5 h", "2 hours", "?", "unknown", "1.5", "30 s"]
+ for time_str in test_times:
+ result = parse_time_input(time_str)
+ print(f"时间解析: '{time_str}' → {result}s ({result/60:.1f}分钟)")
+
+ print("✅ 测试完成")
+
+if __name__ == "__main__":
+ test_time_parsing()
diff --git a/unilabos/compile/heatchill_protocol.py b/unilabos/compile/heatchill_protocol.py
index 3e7f551f8..297015e34 100644
--- a/unilabos/compile/heatchill_protocol.py
+++ b/unilabos/compile/heatchill_protocol.py
@@ -1,4 +1,4 @@
-from typing import List, Dict, Any
+from typing import List, Dict, Any, Union
import networkx as nx
import logging
import re
@@ -10,6 +10,97 @@ def debug_print(message):
print(f"[HEATCHILL] {message}", flush=True)
logger.info(f"[HEATCHILL] {message}")
+def parse_time_with_units(time_input: Union[str, float, int], default_unit: str = "s") -> float:
+ """
+ 解析带单位的时间输入
+
+ Args:
+ time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0)
+ default_unit: 默认单位(默认为秒)
+
+ Returns:
+ float: 时间(秒)
+ """
+ if not time_input:
+ return 0.0
+
+ # 处理数值输入
+ if isinstance(time_input, (int, float)):
+ result = float(time_input)
+ debug_print(f"数值时间输入: {time_input} → {result}s(默认单位)")
+ return result
+
+ # 处理字符串输入
+ time_str = str(time_input).lower().strip()
+ debug_print(f"解析时间字符串: '{time_str}'")
+
+ # 处理特殊值
+ if time_str in ['?', 'unknown', 'tbd', 'to be determined']:
+ default_time = 300.0 # 5分钟默认值
+ debug_print(f"检测到未知时间,使用默认值: {default_time}s")
+ return default_time
+
+ # 如果是纯数字,使用默认单位
+ try:
+ value = float(time_str)
+ if default_unit == "s":
+ result = value
+ elif default_unit in ["min", "minute"]:
+ result = value * 60.0
+ elif default_unit in ["h", "hour"]:
+ result = value * 3600.0
+ else:
+ result = value # 默认秒
+ debug_print(f"纯数字输入: {time_str} → {result}s(单位: {default_unit})")
+ return result
+ except ValueError:
+ pass
+
+ # 使用正则表达式匹配数字和单位
+ pattern = r'(\d+\.?\d*)\s*([a-z]*)'
+ match = re.match(pattern, time_str)
+
+ if not match:
+ debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 60s")
+ return 60.0
+
+ value = float(match.group(1))
+ unit = match.group(2) or default_unit
+
+ # 单位转换映射
+ unit_multipliers = {
+ # 秒
+ 's': 1.0,
+ 'sec': 1.0,
+ 'second': 1.0,
+ 'seconds': 1.0,
+
+ # 分钟
+ 'm': 60.0,
+ 'min': 60.0,
+ 'mins': 60.0,
+ 'minute': 60.0,
+ 'minutes': 60.0,
+
+ # 小时
+ 'h': 3600.0,
+ 'hr': 3600.0,
+ 'hrs': 3600.0,
+ 'hour': 3600.0,
+ 'hours': 3600.0,
+
+ # 天
+ 'd': 86400.0,
+ 'day': 86400.0,
+ 'days': 86400.0,
+ }
+
+ multiplier = unit_multipliers.get(unit, 1.0)
+ result = value * multiplier
+
+ debug_print(f"时间解析: '{time_str}' → {value} {unit} → {result}s")
+ return result
+
def parse_temp_spec(temp_spec: str) -> float:
"""解析温度规格为具体温度"""
if not temp_spec:
@@ -117,7 +208,7 @@ def generate_heat_chill_protocol(
G: nx.DiGraph,
vessel: str,
temp: float = 25.0,
- time: float = 300.0,
+ time: Union[str, float] = "300", # 🔧 修改:支持字符串时间
temp_spec: str = "",
time_spec: str = "",
pressure: str = "",
@@ -125,35 +216,18 @@ def generate_heat_chill_protocol(
stir: bool = False,
stir_speed: float = 300.0,
purpose: str = "",
- **kwargs # 🔧 接受额外参数,增强兼容性
+ **kwargs
) -> List[Dict[str, Any]]:
"""
- 生成加热/冷却操作的协议序列
-
- Args:
- G: 设备图
- vessel: 加热容器名称(必需)
- temp: 目标温度 (°C)
- time: 加热时间 (秒)
- temp_spec: 温度规格(如 'room temperature', 'reflux')
- time_spec: 时间规格(如 'overnight', '2 h')
- pressure: 压力规格(如 '1 mbar'),不做特殊处理
- reflux_solvent: 回流溶剂名称,不做特殊处理
- stir: 是否搅拌
- stir_speed: 搅拌速度 (RPM)
- purpose: 操作目的
- **kwargs: 其他参数(兼容性)
-
- Returns:
- List[Dict[str, Any]]: 加热操作的动作序列
+ 生成加热/冷却操作的协议序列 - 支持单位
"""
debug_print("=" * 50)
- debug_print("开始生成加热冷却协议")
+ debug_print("开始生成加热冷却协议(支持单位)")
debug_print(f"输入参数:")
debug_print(f" - vessel: {vessel}")
debug_print(f" - temp: {temp}°C")
- debug_print(f" - time: {time}s ({time/60:.1f}分钟)")
+ debug_print(f" - time: {time} (类型: {type(time)})")
debug_print(f" - temp_spec: {temp_spec}")
debug_print(f" - time_spec: {time_spec}")
debug_print(f" - pressure: {pressure}")
@@ -176,6 +250,9 @@ def generate_heat_chill_protocol(
if vessel not in G.nodes():
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
+ # === 🔧 新增:单位解析处理 ===
+ debug_print("步骤2: 单位解析处理...")
+
# 温度解析:优先使用 temp_spec,然后是 temp
final_temp = temp
if temp_spec:
@@ -183,10 +260,12 @@ def generate_heat_chill_protocol(
debug_print(f"温度解析: '{temp_spec}' → {final_temp}°C")
# 时间解析:优先使用 time_spec,然后是 time
- final_time = time
if time_spec:
- final_time = parse_time_spec(time_spec)
- debug_print(f"时间解析: '{time_spec}' → {final_time}s ({final_time/60:.1f}分钟)")
+ final_time = parse_time_spec(time_spec) # 使用现有的time_spec解析
+ debug_print(f"时间解析: '{time_spec}' → {final_time}s")
+ else:
+ final_time = parse_time_with_units(time, "s")
+ debug_print(f"时间解析: {time} → {final_time}s ({final_time/60:.1f}分钟)")
# 参数范围验证
if final_temp < -50.0 or final_temp > 300.0:
@@ -201,10 +280,10 @@ def generate_heat_chill_protocol(
debug_print(f"搅拌速度 {stir_speed} RPM 超出范围,修正为 300 RPM")
stir_speed = 300.0
- debug_print(f"✅ 参数验证通过")
+ debug_print(f"✅ 单位解析和参数验证通过")
# === 查找加热设备 ===
- debug_print("步骤2: 查找加热设备...")
+ debug_print("步骤3: 查找加热设备...")
try:
heatchill_id = find_connected_heatchill(G, vessel)
@@ -215,18 +294,18 @@ def generate_heat_chill_protocol(
raise ValueError(f"无法找到加热设备: {str(e)}")
# === 执行加热操作 ===
- debug_print("步骤3: 执行加热操作...")
+ debug_print("步骤4: 执行加热操作...")
heatchill_action = {
"device_id": heatchill_id,
"action_name": "heat_chill",
"action_kwargs": {
"vessel": vessel,
- "temp": final_temp,
- "time": final_time,
- "stir": stir,
- "stir_speed": stir_speed,
- "purpose": purpose or f"加热到 {final_temp}°C"
+ "temp": float(final_temp), # 🔧 确保是浮点数
+ "time": float(final_time), # 🔧 确保是浮点数
+ "stir": bool(stir), # 🔧 确保是布尔值
+ "stir_speed": float(stir_speed), # 🔧 确保是浮点数
+ "purpose": str(purpose or f"加热到 {final_temp}°C") # 🔧 确保是字符串
}
}
@@ -234,7 +313,7 @@ def generate_heat_chill_protocol(
# === 总结 ===
debug_print("=" * 50)
- debug_print(f"加热冷却协议生成完成")
+ debug_print(f"加热冷却协议生成完成(支持单位)")
debug_print(f"总动作数: {len(action_sequence)}")
debug_print(f"加热容器: {vessel}")
debug_print(f"目标温度: {final_temp}°C")
@@ -247,12 +326,11 @@ def generate_heat_chill_protocol(
return action_sequence
-
def generate_heat_chill_to_temp_protocol(
G: nx.DiGraph,
vessel: str,
temp: float = 25.0,
- time: float = 300.0,
+ time: Union[str, float] = 300.0, # 🔧 也支持字符串
temp_spec: str = "",
time_spec: str = "",
pressure: str = "",
@@ -269,7 +347,7 @@ def generate_heat_chill_to_temp_protocol(
G: 设备图
vessel: 加热容器名称(必需)
temp: 目标温度 (°C)
- time: 加热时间 (秒)
+ time: 加热时间(支持字符串和数字)
temp_spec: 温度规格(如 'room temperature', 'reflux')
time_spec: 时间规格(如 'overnight', '2 h')
pressure: 压力规格(如 '1 mbar'),不做特殊处理
@@ -288,7 +366,7 @@ def generate_heat_chill_to_temp_protocol(
debug_print(f"输入参数:")
debug_print(f" - vessel: {vessel}")
debug_print(f" - temp: {temp}°C")
- debug_print(f" - time: {time}s ({time / 60:.1f}分钟)")
+ debug_print(f" - time: {time} (类型: {type(time)})")
debug_print(f" - temp_spec: {temp_spec}")
debug_print(f" - time_spec: {time_spec}")
debug_print(f" - pressure: {pressure}")
@@ -317,11 +395,13 @@ def generate_heat_chill_to_temp_protocol(
final_temp = parse_temp_spec(temp_spec)
debug_print(f"温度解析: '{temp_spec}' → {final_temp}°C")
- # 时间解析:优先使用 time_spec,然后是 time
- final_time = time
+ # 🔧 修复:时间解析,支持字符串输入
if time_spec:
final_time = parse_time_spec(time_spec)
debug_print(f"时间解析: '{time_spec}' → {final_time}s ({final_time / 60:.1f}分钟)")
+ else:
+ final_time = parse_time_with_units(time, "s")
+ debug_print(f"时间解析: {time} → {final_time}s ({final_time/60:.1f}分钟)")
# 参数范围验证
if final_temp < -50.0 or final_temp > 300.0:
@@ -357,11 +437,11 @@ def generate_heat_chill_to_temp_protocol(
"action_name": "heat_chill",
"action_kwargs": {
"vessel": vessel,
- "temp": final_temp,
- "time": final_time,
- "stir": stir,
- "stir_speed": stir_speed,
- "purpose": purpose or f"加热到 {final_temp}°C"
+ "temp": float(final_temp), # 🔧 确保是浮点数
+ "time": float(final_time), # 🔧 确保是浮点数
+ "stir": bool(stir), # 🔧 确保是布尔值
+ "stir_speed": float(stir_speed), # 🔧 确保是浮点数
+ "purpose": str(purpose or f"加热到 {final_temp}°C") # 🔧 确保是字符串
}
}
diff --git a/unilabos/compile/recrystallize_protocol.py b/unilabos/compile/recrystallize_protocol.py
index b69d88b09..4e5f5923a 100644
--- a/unilabos/compile/recrystallize_protocol.py
+++ b/unilabos/compile/recrystallize_protocol.py
@@ -1,8 +1,80 @@
import networkx as nx
-from typing import List, Dict, Any, Tuple
+import re
+from typing import List, Dict, Any, Tuple, Union
from .pump_protocol import generate_pump_protocol_with_rinsing
+def parse_volume_with_units(volume_input: Union[str, float, int], default_unit: str = "mL") -> float:
+ """
+ 解析带单位的体积输入
+
+ Args:
+ volume_input: 体积输入(如 "100 mL", "2.5 L", "500", "?", 100.0)
+ default_unit: 默认单位(默认为毫升)
+
+ Returns:
+ float: 体积(毫升)
+ """
+ if not volume_input:
+ return 0.0
+
+ # 处理数值输入
+ if isinstance(volume_input, (int, float)):
+ result = float(volume_input)
+ print(f"RECRYSTALLIZE: 数值体积输入: {volume_input} → {result}mL(默认单位)")
+ return result
+
+ # 处理字符串输入
+ volume_str = str(volume_input).lower().strip()
+ print(f"RECRYSTALLIZE: 解析体积字符串: '{volume_str}'")
+
+ # 处理特殊值
+ if volume_str in ['?', 'unknown', 'tbd', 'to be determined']:
+ default_volume = 50.0 # 50mL默认值
+ print(f"RECRYSTALLIZE: 检测到未知体积,使用默认值: {default_volume}mL")
+ return default_volume
+
+ # 如果是纯数字,使用默认单位
+ try:
+ value = float(volume_str)
+ if default_unit.lower() in ["ml", "milliliter"]:
+ result = value
+ elif default_unit.lower() in ["l", "liter"]:
+ result = value * 1000.0
+ elif default_unit.lower() in ["μl", "ul", "microliter"]:
+ result = value / 1000.0
+ else:
+ result = value # 默认mL
+ print(f"RECRYSTALLIZE: 纯数字输入: {volume_str} → {result}mL(单位: {default_unit})")
+ return result
+ except ValueError:
+ pass
+
+ # 移除空格并提取数字和单位
+ volume_clean = re.sub(r'\s+', '', volume_str)
+
+ # 匹配数字和单位的正则表达式
+ match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
+
+ if not match:
+ print(f"RECRYSTALLIZE: ⚠️ 无法解析体积: '{volume_str}',使用默认值: 50mL")
+ return 50.0
+
+ value = float(match.group(1))
+ unit = match.group(2) or default_unit.lower()
+
+ # 转换为毫升
+ if unit in ['l', 'liter']:
+ volume = value * 1000.0 # L -> mL
+ elif unit in ['μl', 'ul', 'microliter']:
+ volume = value / 1000.0 # μL -> mL
+ else: # ml, milliliter 或默认
+ volume = value # 已经是mL
+
+ print(f"RECRYSTALLIZE: 体积解析: '{volume_str}' → {value} {unit} → {volume}mL")
+ return volume
+
+
def parse_ratio(ratio_str: str) -> Tuple[float, float]:
"""
解析比例字符串,支持多种格式
@@ -111,11 +183,11 @@ def generate_recrystallize_protocol(
solvent1: str,
solvent2: str,
vessel: str,
- volume: float,
- **kwargs # 接收其他可能的参数但不使用
+ volume: Union[str, float], # 🔧 修改:支持字符串和数值
+ **kwargs
) -> List[Dict[str, Any]]:
"""
- 生成重结晶协议序列
+ 生成重结晶协议序列 - 支持单位
Args:
G: 有向图,节点为容器和设备
@@ -123,38 +195,42 @@ def generate_recrystallize_protocol(
solvent1: 第一种溶剂名称
solvent2: 第二种溶剂名称
vessel: 目标容器
- volume: 总体积 (mL)
- **kwargs: 其他可选参数,但不使用
+ volume: 总体积(支持 "100 mL", "50", "2.5 L" 等)
+ **kwargs: 其他可选参数
Returns:
List[Dict[str, Any]]: 动作序列
"""
action_sequence = []
- print(f"RECRYSTALLIZE: 开始生成重结晶协议")
+ print(f"RECRYSTALLIZE: 开始生成重结晶协议(支持单位)")
print(f" - 比例: {ratio}")
print(f" - 溶剂1: {solvent1}")
print(f" - 溶剂2: {solvent2}")
print(f" - 容器: {vessel}")
- print(f" - 总体积: {volume} mL")
+ print(f" - 总体积: {volume} (类型: {type(volume)})")
# 1. 验证目标容器存在
if vessel not in G.nodes():
raise ValueError(f"目标容器 '{vessel}' 不存在于系统中")
- # 2. 解析比例
+ # 2. 🔧 新增:解析体积(支持单位)
+ final_volume = parse_volume_with_units(volume, "mL")
+ print(f"RECRYSTALLIZE: 解析体积: {volume} → {final_volume}mL")
+
+ # 3. 解析比例
ratio1, ratio2 = parse_ratio(ratio)
total_ratio = ratio1 + ratio2
- # 3. 计算各溶剂体积
- volume1 = volume * (ratio1 / total_ratio)
- volume2 = volume * (ratio2 / total_ratio)
+ # 4. 计算各溶剂体积
+ volume1 = final_volume * (ratio1 / total_ratio)
+ volume2 = final_volume * (ratio2 / total_ratio)
print(f"RECRYSTALLIZE: 解析比例: {ratio1}:{ratio2}")
print(f"RECRYSTALLIZE: {solvent1} 体积: {volume1:.2f} mL")
print(f"RECRYSTALLIZE: {solvent2} 体积: {volume2:.2f} mL")
- # 4. 查找溶剂容器
+ # 5. 查找溶剂容器
try:
solvent1_vessel = find_solvent_vessel(G, solvent1)
print(f"RECRYSTALLIZE: 找到溶剂1容器: {solvent1_vessel}")
@@ -167,7 +243,7 @@ def generate_recrystallize_protocol(
except ValueError as e:
raise ValueError(f"无法找到溶剂2 '{solvent2}': {str(e)}")
- # 5. 验证路径存在
+ # 6. 验证路径存在
try:
path1 = nx.shortest_path(G, source=solvent1_vessel, target=vessel)
print(f"RECRYSTALLIZE: 溶剂1路径: {' → '.join(path1)}")
@@ -180,7 +256,7 @@ def generate_recrystallize_protocol(
except nx.NetworkXNoPath:
raise ValueError(f"从溶剂2容器 '{solvent2_vessel}' 到目标容器 '{vessel}' 没有可用路径")
- # 6. 添加第一种溶剂
+ # 7. 添加第一种溶剂
print(f"RECRYSTALLIZE: 开始添加溶剂1 {volume1:.2f} mL")
try:
@@ -188,7 +264,7 @@ def generate_recrystallize_protocol(
G=G,
from_vessel=solvent1_vessel,
to_vessel=vessel,
- volume=volume1,
+ volume=volume1, # 使用解析后的体积
amount="",
time=0.0,
viscous=False,
@@ -205,7 +281,7 @@ def generate_recrystallize_protocol(
except Exception as e:
raise ValueError(f"生成溶剂1泵协议时出错: {str(e)}")
- # 7. 等待溶剂1稳定
+ # 8. 等待溶剂1稳定
action_sequence.append({
"action_name": "wait",
"action_kwargs": {
@@ -214,7 +290,7 @@ def generate_recrystallize_protocol(
}
})
- # 8. 添加第二种溶剂
+ # 9. 添加第二种溶剂
print(f"RECRYSTALLIZE: 开始添加溶剂2 {volume2:.2f} mL")
try:
@@ -222,7 +298,7 @@ def generate_recrystallize_protocol(
G=G,
from_vessel=solvent2_vessel,
to_vessel=vessel,
- volume=volume2,
+ volume=volume2, # 使用解析后的体积
amount="",
time=0.0,
viscous=False,
@@ -239,7 +315,7 @@ def generate_recrystallize_protocol(
except Exception as e:
raise ValueError(f"生成溶剂2泵协议时出错: {str(e)}")
- # 9. 等待溶剂2稳定
+ # 10. 等待溶剂2稳定
action_sequence.append({
"action_name": "wait",
"action_kwargs": {
@@ -248,17 +324,18 @@ def generate_recrystallize_protocol(
}
})
- # 10. 等待重结晶完成
+ # 11. 等待重结晶完成
action_sequence.append({
"action_name": "wait",
"action_kwargs": {
"time": 600.0, # 等待10分钟进行重结晶
- "description": f"等待重结晶完成({solvent1}:{solvent2} = {ratio})"
+ "description": f"等待重结晶完成({solvent1}:{solvent2} = {ratio},总体积 {final_volume}mL)"
}
})
print(f"RECRYSTALLIZE: 协议生成完成,共 {len(action_sequence)} 个动作")
print(f"RECRYSTALLIZE: 预计总时间: {620/60:.1f} 分钟")
+ print(f"RECRYSTALLIZE: 总体积: {final_volume}mL")
return action_sequence
diff --git a/unilabos/compile/stir_protocol.py b/unilabos/compile/stir_protocol.py
index a67155da6..5343a2a47 100644
--- a/unilabos/compile/stir_protocol.py
+++ b/unilabos/compile/stir_protocol.py
@@ -10,6 +10,97 @@ def debug_print(message):
print(f"[STIR] {message}", flush=True)
logger.info(f"[STIR] {message}")
+def parse_time_with_units(time_input: Union[str, float, int], default_unit: str = "s") -> float:
+ """
+ 解析带单位的时间输入
+
+ Args:
+ time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0)
+ default_unit: 默认单位(默认为秒)
+
+ Returns:
+ float: 时间(秒)
+ """
+ if not time_input:
+ return 0.0
+
+ # 处理数值输入
+ if isinstance(time_input, (int, float)):
+ result = float(time_input)
+ debug_print(f"数值时间输入: {time_input} → {result}s(默认单位)")
+ return result
+
+ # 处理字符串输入
+ time_str = str(time_input).lower().strip()
+ debug_print(f"解析时间字符串: '{time_str}'")
+
+ # 处理特殊值
+ if time_str in ['?', 'unknown', 'tbd', 'to be determined']:
+ default_time = 300.0 # 5分钟默认值
+ debug_print(f"检测到未知时间,使用默认值: {default_time}s")
+ return default_time
+
+ # 如果是纯数字,使用默认单位
+ try:
+ value = float(time_str)
+ if default_unit == "s":
+ result = value
+ elif default_unit in ["min", "minute"]:
+ result = value * 60.0
+ elif default_unit in ["h", "hour"]:
+ result = value * 3600.0
+ else:
+ result = value # 默认秒
+ debug_print(f"纯数字输入: {time_str} → {result}s(单位: {default_unit})")
+ return result
+ except ValueError:
+ pass
+
+ # 使用正则表达式匹配数字和单位
+ pattern = r'(\d+\.?\d*)\s*([a-z]*)'
+ match = re.match(pattern, time_str)
+
+ if not match:
+ debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 60s")
+ return 60.0
+
+ value = float(match.group(1))
+ unit = match.group(2) or default_unit
+
+ # 单位转换映射
+ unit_multipliers = {
+ # 秒
+ 's': 1.0,
+ 'sec': 1.0,
+ 'second': 1.0,
+ 'seconds': 1.0,
+
+ # 分钟
+ 'm': 60.0,
+ 'min': 60.0,
+ 'mins': 60.0,
+ 'minute': 60.0,
+ 'minutes': 60.0,
+
+ # 小时
+ 'h': 3600.0,
+ 'hr': 3600.0,
+ 'hrs': 3600.0,
+ 'hour': 3600.0,
+ 'hours': 3600.0,
+
+ # 天
+ 'd': 86400.0,
+ 'day': 86400.0,
+ 'days': 86400.0,
+ }
+
+ multiplier = unit_multipliers.get(unit, 1.0)
+ result = value * multiplier
+
+ debug_print(f"时间解析: '{time_str}' → {value} {unit} → {result}s")
+ return result
+
def parse_time_spec(time_spec: str) -> float:
"""
解析时间规格字符串为秒数
@@ -211,27 +302,26 @@ def find_connected_stirrer(G: nx.DiGraph, vessel: str = None) -> str:
def generate_stir_protocol(
G: nx.DiGraph,
vessel: str,
- time: Union[str, float, int] = 300.0,
- stir_time: Union[str, float, int] = 0.0,
+ time: Union[str, float, int] = "300", # 🔧 修改:默认为字符串
+ stir_time: Union[str, float, int] = "0", # 🔧 修改:支持字符串
time_spec: str = "",
event: str = "",
stir_speed: float = 200.0,
- settling_time: float = 60.0,
+ settling_time: Union[str, float] = "60", # 🔧 修改:支持字符串
**kwargs
) -> List[Dict[str, Any]]:
"""
- 生成搅拌操作的协议序列 - 定时搅拌 + 沉降
- 支持 time 和 stir_time 参数统一处理
+ 生成搅拌操作的协议序列 - 支持单位
Args:
G: 设备图
vessel: 搅拌容器名称(必需)
- time: 搅拌时间(支持多种格式)
- stir_time: 搅拌时间(与time等效)
+ time: 搅拌时间(支持 "5 min", "300", "0.5 h" 等)
+ stir_time: 搅拌时间(与time等效,支持单位)
time_spec: 时间规格(优先级最高)
event: 事件标识
stir_speed: 搅拌速度 (RPM),默认200 RPM
- settling_time: 沉降时间 (秒),默认60s
+ settling_time: 沉降时间(支持单位,默认60秒)
**kwargs: 其他参数(兼容性)
Returns:
@@ -239,7 +329,7 @@ def generate_stir_protocol(
"""
debug_print("=" * 50)
- debug_print("开始生成搅拌协议")
+ debug_print("开始生成搅拌协议(支持单位)")
debug_print(f"输入参数:")
debug_print(f" - vessel: {vessel}")
debug_print(f" - time: {time}")
@@ -265,19 +355,29 @@ def generate_stir_protocol(
debug_print(f"✅ 参数验证通过")
- # === 时间处理(统一 time 和 stir_time)===
- debug_print("步骤2: 时间处理...")
+ # === 🔧 新增:单位解析处理 ===
+ debug_print("步骤2: 单位解析处理...")
+
+ # 确定实际使用的时间值(stir_time优先)
+ actual_time_input = stir_time if stir_time not in ["0", 0, 0.0] else time
- # 确定实际使用的时间值
- actual_time_input = stir_time if stir_time else time
+ # 解析时间(time_spec > actual_time_input)
+ if time_spec:
+ parsed_time = parse_time_spec(time_spec) # 使用现有的time_spec解析
+ debug_print(f"使用time_spec: '{time_spec}' → {parsed_time}s")
+ else:
+ parsed_time = parse_time_with_units(actual_time_input, "s")
+ debug_print(f"解析时间: {actual_time_input} → {parsed_time}s")
- # 解析时间
- parsed_time = parse_time_input(actual_time_input, time_spec)
+ # 解析沉降时间
+ parsed_settling_time = parse_time_with_units(settling_time, "s")
+ debug_print(f"解析沉降时间: {settling_time} → {parsed_settling_time}s")
debug_print(f"时间解析结果:")
debug_print(f" - 原始输入: time={time}, stir_time={stir_time}")
debug_print(f" - 时间规格: {time_spec}")
- debug_print(f" - 最终时间: {parsed_time}秒 ({parsed_time/60:.1f}分钟)")
+ debug_print(f" - 最终搅拌时间: {parsed_time}s ({parsed_time/60:.1f}分钟)")
+ debug_print(f" - 最终沉降时间: {parsed_settling_time}s ({parsed_settling_time/60:.1f}分钟)")
# 修正参数范围
if parsed_time < 0:
@@ -294,12 +394,12 @@ def generate_stir_protocol(
debug_print(f"搅拌速度 {stir_speed} RPM 过高,修正为 1000 RPM")
stir_speed = 1000.0
- if settling_time < 0:
- debug_print(f"沉降时间 {settling_time}s 无效,修正为 60s")
- settling_time = 60.0
- elif settling_time > 1800:
- debug_print(f"沉降时间 {settling_time}s 过长,修正为 600s")
- settling_time = 600.0
+ if parsed_settling_time < 0:
+ debug_print(f"沉降时间 {parsed_settling_time}s 无效,修正为 60s")
+ parsed_settling_time = 60.0
+ elif parsed_settling_time > 1800:
+ debug_print(f"沉降时间 {parsed_settling_time}s 过长,修正为 600s")
+ parsed_settling_time = 600.0
# === 查找搅拌设备 ===
debug_print("步骤3: 查找搅拌设备...")
@@ -318,12 +418,12 @@ def generate_stir_protocol(
# 构建搅拌动作参数
stir_kwargs = {
"vessel": vessel,
- "time": str(time), # 保持原始字符串格式
+ "time": str(time), # 保持原始字符串格式
"event": event,
"time_spec": time_spec,
- "stir_time": parsed_time, # 解析后的时间(秒)
+ "stir_time": parsed_time, # 解析后的时间(秒)
"stir_speed": stir_speed,
- "settling_time": settling_time
+ "settling_time": parsed_settling_time # 解析后的沉降时间(秒)
}
debug_print(f"搅拌参数: {stir_kwargs}")
@@ -338,10 +438,10 @@ def generate_stir_protocol(
# === 总结 ===
debug_print("=" * 50)
- debug_print(f"搅拌协议生成完成")
+ debug_print(f"搅拌协议生成完成(支持单位)")
debug_print(f"总动作数: {len(action_sequence)}")
debug_print(f"搅拌容器: {vessel}")
- debug_print(f"搅拌参数: {stir_speed} RPM, {parsed_time}s, 沉降 {settling_time}s")
+ debug_print(f"搅拌参数: {stir_speed} RPM, {parsed_time}s, 沉降 {parsed_settling_time}s")
debug_print("=" * 50)
return action_sequence
diff --git a/unilabos/compile/utils/unit_parser.py b/unilabos/compile/utils/unit_parser.py
new file mode 100644
index 000000000..d1d297cb5
--- /dev/null
+++ b/unilabos/compile/utils/unit_parser.py
@@ -0,0 +1,206 @@
+"""
+统一的单位解析工具模块
+支持时间、体积、质量等各种单位的解析
+"""
+
+import re
+import logging
+from typing import Union
+
+logger = logging.getLogger(__name__)
+
+def debug_print(message, prefix="[UNIT_PARSER]"):
+ """调试输出"""
+ print(f"{prefix} {message}", flush=True)
+ logger.info(f"{prefix} {message}")
+
+def parse_time_with_units(time_input: Union[str, float, int], default_unit: str = "s") -> float:
+ """
+ 解析带单位的时间输入
+
+ Args:
+ time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0)
+ default_unit: 默认单位(默认为秒)
+
+ Returns:
+ float: 时间(秒)
+ """
+ if not time_input:
+ return 0.0
+
+ # 处理数值输入
+ if isinstance(time_input, (int, float)):
+ result = float(time_input)
+ debug_print(f"数值时间输入: {time_input} → {result}s(默认单位)")
+ return result
+
+ # 处理字符串输入
+ time_str = str(time_input).lower().strip()
+ debug_print(f"解析时间字符串: '{time_str}'")
+
+ # 处理特殊值
+ if time_str in ['?', 'unknown', 'tbd', 'to be determined']:
+ default_time = 300.0 # 5分钟默认值
+ debug_print(f"检测到未知时间,使用默认值: {default_time}s")
+ return default_time
+
+ # 如果是纯数字,使用默认单位
+ try:
+ value = float(time_str)
+ if default_unit == "s":
+ result = value
+ elif default_unit in ["min", "minute"]:
+ result = value * 60.0
+ elif default_unit in ["h", "hour"]:
+ result = value * 3600.0
+ else:
+ result = value # 默认秒
+ debug_print(f"纯数字输入: {time_str} → {result}s(单位: {default_unit})")
+ return result
+ except ValueError:
+ pass
+
+ # 使用正则表达式匹配数字和单位
+ pattern = r'(\d+\.?\d*)\s*([a-z]*)'
+ match = re.match(pattern, time_str)
+
+ if not match:
+ debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 60s")
+ return 60.0
+
+ value = float(match.group(1))
+ unit = match.group(2) or default_unit
+
+ # 单位转换映射
+ unit_multipliers = {
+ # 秒
+ 's': 1.0,
+ 'sec': 1.0,
+ 'second': 1.0,
+ 'seconds': 1.0,
+
+ # 分钟
+ 'm': 60.0,
+ 'min': 60.0,
+ 'mins': 60.0,
+ 'minute': 60.0,
+ 'minutes': 60.0,
+
+ # 小时
+ 'h': 3600.0,
+ 'hr': 3600.0,
+ 'hrs': 3600.0,
+ 'hour': 3600.0,
+ 'hours': 3600.0,
+
+ # 天
+ 'd': 86400.0,
+ 'day': 86400.0,
+ 'days': 86400.0,
+ }
+
+ multiplier = unit_multipliers.get(unit, 1.0)
+ result = value * multiplier
+
+ debug_print(f"时间解析: '{time_str}' → {value} {unit} → {result}s")
+ return result
+
+def parse_volume_with_units(volume_input: Union[str, float, int], default_unit: str = "mL") -> float:
+ """
+ 解析带单位的体积输入
+
+ Args:
+ volume_input: 体积输入(如 "100 mL", "2.5 L", "500", "?", 100.0)
+ default_unit: 默认单位(默认为毫升)
+
+ Returns:
+ float: 体积(毫升)
+ """
+ if not volume_input:
+ return 0.0
+
+ # 处理数值输入
+ if isinstance(volume_input, (int, float)):
+ result = float(volume_input)
+ debug_print(f"数值体积输入: {volume_input} → {result}mL(默认单位)")
+ return result
+
+ # 处理字符串输入
+ volume_str = str(volume_input).lower().strip()
+ debug_print(f"解析体积字符串: '{volume_str}'")
+
+ # 处理特殊值
+ if volume_str in ['?', 'unknown', 'tbd', 'to be determined']:
+ default_volume = 50.0 # 50mL默认值
+ debug_print(f"检测到未知体积,使用默认值: {default_volume}mL")
+ return default_volume
+
+ # 如果是纯数字,使用默认单位
+ try:
+ value = float(volume_str)
+ if default_unit.lower() in ["ml", "milliliter"]:
+ result = value
+ elif default_unit.lower() in ["l", "liter"]:
+ result = value * 1000.0
+ elif default_unit.lower() in ["μl", "ul", "microliter"]:
+ result = value / 1000.0
+ else:
+ result = value # 默认mL
+ debug_print(f"纯数字输入: {volume_str} → {result}mL(单位: {default_unit})")
+ return result
+ except ValueError:
+ pass
+
+ # 移除空格并提取数字和单位
+ volume_clean = re.sub(r'\s+', '', volume_str)
+
+ # 匹配数字和单位的正则表达式
+ match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
+
+ if not match:
+ debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值: 50mL")
+ return 50.0
+
+ value = float(match.group(1))
+ unit = match.group(2) or default_unit.lower()
+
+ # 转换为毫升
+ if unit in ['l', 'liter']:
+ volume = value * 1000.0 # L -> mL
+ elif unit in ['μl', 'ul', 'microliter']:
+ volume = value / 1000.0 # μL -> mL
+ else: # ml, milliliter 或默认
+ volume = value # 已经是mL
+
+ debug_print(f"体积解析: '{volume_str}' → {value} {unit} → {volume}mL")
+ return volume
+
+# 测试函数
+def test_unit_parser():
+ """测试单位解析功能"""
+ print("=== 单位解析器测试 ===")
+
+ # 测试时间解析
+ time_tests = [
+ "30 min", "1 h", "300", "5.5 h", "?", 60.0, "2 hours", "30 s"
+ ]
+
+ print("\n时间解析测试:")
+ for time_input in time_tests:
+ result = parse_time_with_units(time_input)
+ print(f" {time_input} → {result}s ({result/60:.1f}min)")
+
+ # 测试体积解析
+ volume_tests = [
+ "100 mL", "2.5 L", "500", "?", 100.0, "500 μL", "1 liter"
+ ]
+
+ print("\n体积解析测试:")
+ for volume_input in volume_tests:
+ result = parse_volume_with_units(volume_input)
+ print(f" {volume_input} → {result}mL")
+
+ print("\n✅ 测试完成")
+
+if __name__ == "__main__":
+ test_unit_parser()
\ No newline at end of file
diff --git a/unilabos/compile/wash_solid_protocol.py b/unilabos/compile/wash_solid_protocol.py
index 1a5c5e3c3..8d431086c 100644
--- a/unilabos/compile/wash_solid_protocol.py
+++ b/unilabos/compile/wash_solid_protocol.py
@@ -10,6 +10,167 @@ def debug_print(message):
print(f"[WASH_SOLID] {message}", flush=True)
logger.info(f"[WASH_SOLID] {message}")
+def parse_time_with_units(time_input: Union[str, float, int], default_unit: str = "s") -> float:
+ """
+ 解析带单位的时间输入
+
+ Args:
+ time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0)
+ default_unit: 默认单位(默认为秒)
+
+ Returns:
+ float: 时间(秒)
+ """
+ if not time_input:
+ return 0.0
+
+ # 处理数值输入
+ if isinstance(time_input, (int, float)):
+ result = float(time_input)
+ debug_print(f"数值时间输入: {time_input} → {result}s(默认单位)")
+ return result
+
+ # 处理字符串输入
+ time_str = str(time_input).lower().strip()
+ debug_print(f"解析时间字符串: '{time_str}'")
+
+ # 处理特殊值
+ if time_str in ['?', 'unknown', 'tbd', 'to be determined']:
+ default_time = 300.0 # 5分钟默认值
+ debug_print(f"检测到未知时间,使用默认值: {default_time}s")
+ return default_time
+
+ # 如果是纯数字,使用默认单位
+ try:
+ value = float(time_str)
+ if default_unit == "s":
+ result = value
+ elif default_unit in ["min", "minute"]:
+ result = value * 60.0
+ elif default_unit in ["h", "hour"]:
+ result = value * 3600.0
+ else:
+ result = value # 默认秒
+ debug_print(f"纯数字输入: {time_str} → {result}s(单位: {default_unit})")
+ return result
+ except ValueError:
+ pass
+
+ # 使用正则表达式匹配数字和单位
+ pattern = r'(\d+\.?\d*)\s*([a-z]*)'
+ match = re.match(pattern, time_str)
+
+ if not match:
+ debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 60s")
+ return 60.0
+
+ value = float(match.group(1))
+ unit = match.group(2) or default_unit
+
+ # 单位转换映射
+ unit_multipliers = {
+ # 秒
+ 's': 1.0,
+ 'sec': 1.0,
+ 'second': 1.0,
+ 'seconds': 1.0,
+
+ # 分钟
+ 'm': 60.0,
+ 'min': 60.0,
+ 'mins': 60.0,
+ 'minute': 60.0,
+ 'minutes': 60.0,
+
+ # 小时
+ 'h': 3600.0,
+ 'hr': 3600.0,
+ 'hrs': 3600.0,
+ 'hour': 3600.0,
+ 'hours': 3600.0,
+
+ # 天
+ 'd': 86400.0,
+ 'day': 86400.0,
+ 'days': 86400.0,
+ }
+
+ multiplier = unit_multipliers.get(unit, 1.0)
+ result = value * multiplier
+
+ debug_print(f"时间解析: '{time_str}' → {value} {unit} → {result}s")
+ return result
+
+def parse_volume_with_units(volume_input: Union[str, float, int], default_unit: str = "mL") -> float:
+ """
+ 解析带单位的体积输入
+
+ Args:
+ volume_input: 体积输入(如 "100 mL", "2.5 L", "500", "?", 100.0)
+ default_unit: 默认单位(默认为毫升)
+
+ Returns:
+ float: 体积(毫升)
+ """
+ if not volume_input:
+ return 0.0
+
+ # 处理数值输入
+ if isinstance(volume_input, (int, float)):
+ result = float(volume_input)
+ debug_print(f"数值体积输入: {volume_input} → {result}mL(默认单位)")
+ return result
+
+ # 处理字符串输入
+ volume_str = str(volume_input).lower().strip()
+ debug_print(f"解析体积字符串: '{volume_str}'")
+
+ # 处理特殊值
+ if volume_str in ['?', 'unknown', 'tbd', 'to be determined']:
+ default_volume = 50.0 # 50mL默认值
+ debug_print(f"检测到未知体积,使用默认值: {default_volume}mL")
+ return default_volume
+
+ # 如果是纯数字,使用默认单位
+ try:
+ value = float(volume_str)
+ if default_unit.lower() in ["ml", "milliliter"]:
+ result = value
+ elif default_unit.lower() in ["l", "liter"]:
+ result = value * 1000.0
+ elif default_unit.lower() in ["μl", "ul", "microliter"]:
+ result = value / 1000.0
+ else:
+ result = value # 默认mL
+ debug_print(f"纯数字输入: {volume_str} → {result}mL(单位: {default_unit})")
+ return result
+ except ValueError:
+ pass
+
+ # 移除空格并提取数字和单位
+ volume_clean = re.sub(r'\s+', '', volume_str)
+
+ # 匹配数字和单位的正则表达式
+ match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
+
+ if not match:
+ debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值: 50mL")
+ return 50.0
+
+ value = float(match.group(1))
+ unit = match.group(2) or default_unit.lower()
+
+ # 转换为毫升
+ if unit in ['l', 'liter']:
+ volume = value * 1000.0 # L -> mL
+ elif unit in ['μl', 'ul', 'microliter']:
+ volume = value / 1000.0 # μL -> mL
+ else: # ml, milliliter 或默认
+ volume = value # 已经是mL
+
+ debug_print(f"体积解析: '{volume_str}' → {value} {unit} → {volume}mL")
+ return volume
+
def parse_volume_spec(volume_spec: str) -> float:
"""
解析体积规格字符串为毫升数
@@ -357,46 +518,46 @@ def generate_wash_solid_protocol(
G: nx.DiGraph,
vessel: str,
solvent: str,
- volume: Union[float, str] = 0.0, # 🔧 修改:支持字符串输入
+ volume: Union[float, str] = "50", # 🔧 修改:默认为字符串
filtrate_vessel: str = "",
temp: float = 25.0,
stir: bool = False,
stir_speed: float = 0.0,
- time: float = 0.0,
+ time: Union[str, float] = "0", # 🔧 修改:支持字符串时间
repeats: int = 1,
- # === 新增参数 ===
- volume_spec: str = "", # 体积规格
- repeats_spec: str = "", # 重复次数规格
- mass: str = "", # 🔧 新增:固体质量(用于转换体积)
- event: str = "", # 事件标识符
+ # === 现有参数保持不变 ===
+ volume_spec: str = "",
+ repeats_spec: str = "",
+ mass: str = "",
+ event: str = "",
**kwargs
) -> List[Dict[str, Any]]:
"""
- 生成固体清洗操作的协议序列 - 增强版
+ 生成固体清洗操作的协议序列 - 增强版(支持单位)
- 支持多种体积输入方式:
- 1. volume_spec: "small volume", "large volume" 等
- 2. mass: "10 g", "2.5 kg", "500 mg" 等(转换为体积)
- 3. volume: 数值或字符串 "10", "10 mL", "2.5 L" 等
+ 支持多种输入方式:
+ 1. volume: "100 mL", "50", "2.5 L", "?"
+ 2. time: "5 min", "300", "0.5 h", "?"
+ 3. volume_spec: "small volume", "large volume" 等
+ 4. mass: "10 g", "2.5 kg", "500 mg" 等(转换为体积)
"""
debug_print("=" * 60)
- debug_print("开始生成固体清洗协议")
+ debug_print("开始生成固体清洗协议(支持单位)")
debug_print(f"输入参数:")
debug_print(f" - vessel: {vessel}")
debug_print(f" - solvent: {solvent}")
debug_print(f" - volume: {volume} (类型: {type(volume)})")
+ debug_print(f" - time: {time} (类型: {type(time)})")
debug_print(f" - volume_spec: '{volume_spec}'")
- debug_print(f" - mass: '{mass}'") # 🔧 新增日志
+ debug_print(f" - mass: '{mass}'")
debug_print(f" - filtrate_vessel: '{filtrate_vessel}'")
debug_print(f" - temp: {temp}°C")
debug_print(f" - stir: {stir}")
debug_print(f" - stir_speed: {stir_speed} RPM")
- debug_print(f" - time: {time}s")
debug_print(f" - repeats: {repeats}")
debug_print(f" - repeats_spec: '{repeats_spec}'")
debug_print(f" - event: '{event}'")
- debug_print(f" - 其他参数: {kwargs}")
debug_print("=" * 60)
action_sequence = []
@@ -416,12 +577,27 @@ def generate_wash_solid_protocol(
debug_print(f"✅ 必需参数验证通过")
- # === 参数处理 ===
- debug_print("步骤2: 参数处理...")
+ # === 🔧 新增:单位解析处理 ===
+ debug_print("步骤2: 单位解析处理...")
- # 🔧 修改:处理体积参数(支持mass转换和字符串解析)
- final_volume = parse_volume_input(volume, volume_spec, mass)
- debug_print(f"最终体积: {final_volume}mL")
+ # 解析体积(优先级:volume_spec > mass > volume)
+ if volume_spec and volume_spec.strip():
+ final_volume = parse_volume_spec(volume_spec)
+ debug_print(f"使用volume_spec: {final_volume}mL")
+ elif mass and mass.strip():
+ final_volume = parse_mass_to_volume(mass)
+ if final_volume > 0:
+ debug_print(f"使用mass转换: {final_volume}mL")
+ else:
+ final_volume = parse_volume_with_units(volume, "mL")
+ debug_print(f"mass转换失败,使用volume: {final_volume}mL")
+ else:
+ final_volume = parse_volume_with_units(volume, "mL")
+ debug_print(f"使用volume: {final_volume}mL")
+
+ # 解析时间
+ final_time = parse_time_with_units(time, "s")
+ debug_print(f"解析时间: {time} → {final_time}s ({final_time/60:.1f}min)")
# 处理重复次数参数(repeats_spec优先)
final_repeats = parse_repeats_input(repeats, repeats_spec)
@@ -436,9 +612,9 @@ def generate_wash_solid_protocol(
debug_print(f"搅拌速度 {stir_speed} RPM 超出范围,修正为 200 RPM")
stir_speed = 200.0 if stir else 0.0
- if time < 0:
- debug_print(f"时间 {time}s 无效,修正为 0")
- time = 0.0
+ if final_time < 0:
+ debug_print(f"时间 {final_time}s 无效,修正为 0")
+ final_time = 0.0
if final_repeats < 1:
debug_print(f"重复次数 {final_repeats} 无效,修正为 1")
@@ -447,9 +623,9 @@ def generate_wash_solid_protocol(
debug_print(f"重复次数 {final_repeats} 过多,修正为 10")
final_repeats = 10
- debug_print(f"✅ 参数处理完成")
+ debug_print(f"✅ 单位解析和参数处理完成")
- # === 查找设备 ===
+ # === 查找设备(保持原有逻辑)===
debug_print("步骤3: 查找设备...")
try:
@@ -507,17 +683,14 @@ def generate_wash_solid_protocol(
debug_print(f"❌ 设备查找失败: {str(e)}")
raise ValueError(f"设备查找失败: {str(e)}")
- # === 执行清洗循环 ===
+ # === 执行清洗循环(保持原有逻辑,使用解析后的参数)===
debug_print("步骤4: 执行清洗循环...")
for cycle in range(final_repeats):
debug_print(f"=== 第 {cycle+1}/{final_repeats} 次清洗 ===")
- # 🔧 修复:分解为基础动作序列
-
# 1. 加入清洗溶剂
debug_print(f" 步骤 {cycle+1}.1: 加入清洗溶剂")
- # 🔧 修复:使用 pump protocol 而不是直接调用 transfer action
try:
from .pump_protocol import generate_pump_protocol_with_rinsing
@@ -525,7 +698,7 @@ def generate_wash_solid_protocol(
G=G,
from_vessel=solvent_source,
to_vessel=vessel,
- volume=final_volume,
+ volume=final_volume, # 使用解析后的体积
amount="",
time=0.0,
viscous=False,
@@ -548,22 +721,21 @@ def generate_wash_solid_protocol(
except Exception as e:
debug_print(f"❌ 转移失败: {str(e)}")
- # 继续执行,可能有其他问题
# 2. 搅拌混合(如果需要)
if stir and stirrer_device:
debug_print(f" 步骤 {cycle+1}.2: 搅拌混合")
- stir_time = max(time, 30.0) if time > 0 else 60.0 # 默认搅拌1分钟
+ stir_time = max(final_time, 30.0) if final_time > 0 else 60.0 # 使用解析后的时间
stir_action = {
"device_id": stirrer_device,
"action_name": "stir",
"action_kwargs": {
"vessel": vessel,
- "time": str(int(stir_time)), # 转换为字符串格式
+ "time": str(time), # 保持原始字符串格式
"event": event,
"time_spec": "",
- "stir_time": stir_time,
+ "stir_time": stir_time, # 解析后的时间(秒)
"stir_speed": stir_speed,
"settling_time": 30.0
}
@@ -582,7 +754,7 @@ def generate_wash_solid_protocol(
"stir_speed": 0.0,
"temp": temp,
"continue_heatchill": False,
- "volume": final_volume
+ "volume": final_volume # 使用解析后的体积
}
}
action_sequence.append(filter_action)
@@ -596,11 +768,12 @@ def generate_wash_solid_protocol(
# === 总结 ===
debug_print("=" * 60)
- debug_print(f"固体清洗协议生成完成")
+ debug_print(f"固体清洗协议生成完成(支持单位)")
debug_print(f"总动作数: {len(action_sequence)}")
debug_print(f"清洗容器: {vessel}")
debug_print(f"使用溶剂: {solvent}")
debug_print(f"清洗体积: {final_volume}mL")
+ debug_print(f"清洗时间: {final_time}s ({final_time/60:.1f}min)")
debug_print(f"重复次数: {final_repeats}")
debug_print(f"滤液收集: {actual_filtrate_vessel}")
debug_print(f"事件标识: {event}")
diff --git a/unilabos/devices/virtual/virtual_heatchill.py b/unilabos/devices/virtual/virtual_heatchill.py
index 541434af1..20c9a196b 100644
--- a/unilabos/devices/virtual/virtual_heatchill.py
+++ b/unilabos/devices/virtual/virtual_heatchill.py
@@ -58,12 +58,30 @@ async def cleanup(self) -> bool:
})
return True
- async def heat_chill(self, vessel: str, temp: float, time: float, stir: bool,
+ async def heat_chill(self, vessel: str, temp: float, time, stir: bool,
stir_speed: float, purpose: str) -> bool:
- """Execute heat chill action - 按实际时间运行,实时更新剩余时间"""
- self.logger.info(f"HeatChill: vessel={vessel}, temp={temp}°C, time={time}s, stir={stir}, stir_speed={stir_speed}")
+ """Execute heat chill action - 🔧 修复:确保参数类型正确"""
- # 验证参数
+ # 🔧 关键修复:确保所有参数类型正确
+ try:
+ temp = float(temp)
+ time_value = float(time) # 强制转换为浮点数
+ stir_speed = float(stir_speed)
+ stir = bool(stir)
+ vessel = str(vessel)
+ purpose = str(purpose)
+ except (ValueError, TypeError) as e:
+ error_msg = f"参数类型转换错误: temp={temp}({type(temp)}), time={time}({type(time)}), error={str(e)}"
+ self.logger.error(error_msg)
+ self.data.update({
+ "status": f"Error: {error_msg}",
+ "operation_mode": "Error"
+ })
+ return False
+
+ self.logger.info(f"HeatChill: vessel={vessel}, temp={temp}°C, time={time_value}s, stir={stir}, stir_speed={stir_speed}")
+
+ # 验证参数范围
if temp > self._max_temp or temp < self._min_temp:
error_msg = f"温度 {temp}°C 超出范围 ({self._min_temp}°C - {self._max_temp}°C)"
self.logger.error(error_msg)
@@ -82,6 +100,15 @@ async def heat_chill(self, vessel: str, temp: float, time: float, stir: bool,
})
return False
+ if time_value <= 0:
+ error_msg = f"时间 {time_value}s 必须大于0"
+ self.logger.error(error_msg)
+ self.data.update({
+ "status": f"Error: {error_msg}",
+ "operation_mode": "Error"
+ })
+ return False
+
# 确定操作模式
if temp > 25.0:
operation_mode = "Heating"
@@ -93,9 +120,9 @@ async def heat_chill(self, vessel: str, temp: float, time: float, stir: bool,
operation_mode = "Maintaining"
status_action = "保温"
- # **修复**: 使用重命名的time模块
+ # 🔧 修复:使用转换后的时间值
start_time = time_module.time()
- total_time = time
+ total_time = time_value # 使用转换后的浮点数
# 开始操作
stir_info = f" | 搅拌: {stir_speed} RPM" if stir else ""
@@ -107,9 +134,9 @@ async def heat_chill(self, vessel: str, temp: float, time: float, stir: bool,
"remaining_time": total_time,
})
- # **修复**: 在等待过程中每秒更新剩余时间
+ # 在等待过程中每秒更新剩余时间
while True:
- current_time = time_module.time() # 使用重命名的time模块
+ current_time = time_module.time()
elapsed = current_time - start_time
remaining = max(0, total_time - elapsed)
@@ -141,6 +168,21 @@ async def heat_chill(self, vessel: str, temp: float, time: float, stir: bool,
async def heat_chill_start(self, vessel: str, temp: float, purpose: str) -> bool:
"""Start continuous heat chill"""
+
+ # 🔧 添加类型转换
+ try:
+ temp = float(temp)
+ vessel = str(vessel)
+ purpose = str(purpose)
+ except (ValueError, TypeError) as e:
+ error_msg = f"参数类型转换错误: {str(e)}"
+ self.logger.error(error_msg)
+ self.data.update({
+ "status": f"Error: {error_msg}",
+ "operation_mode": "Error"
+ })
+ return False
+
self.logger.info(f"HeatChillStart: vessel={vessel}, temp={temp}°C")
# 验证参数
@@ -176,6 +218,15 @@ async def heat_chill_start(self, vessel: str, temp: float, purpose: str) -> bool
async def heat_chill_stop(self, vessel: str) -> bool:
"""Stop heat chill"""
+
+ # 🔧 添加类型转换
+ try:
+ vessel = str(vessel)
+ except (ValueError, TypeError) as e:
+ error_msg = f"参数类型转换错误: {str(e)}"
+ self.logger.error(error_msg)
+ return False
+
self.logger.info(f"HeatChillStop: vessel={vessel}")
self.data.update({
diff --git a/unilabos/devices/virtual/virtual_rotavap.py b/unilabos/devices/virtual/virtual_rotavap.py
index 9f576b5ec..bfd64947e 100644
--- a/unilabos/devices/virtual/virtual_rotavap.py
+++ b/unilabos/devices/virtual/virtual_rotavap.py
@@ -73,7 +73,7 @@ async def evaporate(
vessel: str,
pressure: float = 0.1,
temp: float = 60.0,
- time: float = 1800.0,
+ time: float = 180.0,
stir_speed: float = 100.0,
solvent: str = "",
**kwargs
diff --git a/unilabos/messages/__init__.py b/unilabos/messages/__init__.py
index 52fd97f84..f91f38288 100644
--- a/unilabos/messages/__init__.py
+++ b/unilabos/messages/__init__.py
@@ -125,7 +125,7 @@ class EvaporateProtocol(BaseModel):
# === 所有其他参数都改为可选,添加默认值 ===
pressure: float = Field(0.1, description="真空度 (bar),默认0.1 bar")
temp: float = Field(60.0, description="加热温度 (°C),默认60°C")
- time: float = Field(1800.0, description="蒸发时间 (秒),默认1800s (30分钟)")
+ time: float = Field(180.0, description="蒸发时间 (秒),默认1800s (30分钟)")
stir_speed: float = Field(100.0, description="旋转速度 (RPM),默认100 RPM")
# === 新版XDL兼容参数(可选) ===
diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml
index 3a98e566c..d16564968 100644
--- a/unilabos/registry/devices/virtual_device.yaml
+++ b/unilabos/registry/devices/virtual_device.yaml
@@ -2321,7 +2321,7 @@ virtual_rotavap:
pressure: 0.1
stir_speed: 100.0
temp: 60.0
- time: 1800.0
+ time: 180.0
vessel: null
handles: []
result: {}
@@ -2341,7 +2341,7 @@ virtual_rotavap:
default: 60.0
type: number
time:
- default: 1800.0
+ default: 180.0
type: number
vessel:
type: string
diff --git a/unilabos_msgs/action/Evaporate.action b/unilabos_msgs/action/Evaporate.action
index b66e32f0d..9cecb62cb 100644
--- a/unilabos_msgs/action/Evaporate.action
+++ b/unilabos_msgs/action/Evaporate.action
@@ -1,10 +1,10 @@
# Organic Synthesis Station Evaporate Action
-string vessel
-float64 pressure
-float64 temp
-float64 time
-float64 stir_speed
-string solvent
+string vessel # 目标容器
+float64 pressure # 真空度
+float64 temp # 温度
+string time # 🔧 蒸发时间(支持带单位,如"3 min","180",默认秒)
+float64 stir_speed # 旋转速度
+string solvent # 溶剂名称
---
string return_info
bool success
diff --git a/unilabos_msgs/action/HeatChill.action b/unilabos_msgs/action/HeatChill.action
index a39510e71..1e7025e8b 100644
--- a/unilabos_msgs/action/HeatChill.action
+++ b/unilabos_msgs/action/HeatChill.action
@@ -1,7 +1,7 @@
# Goal - 加热冷却操作的目标参数
string vessel # 加热容器名称(必需)
float64 temp # 目标温度(可选,默认25.0)
-float64 time # 加热时间(可选,默认300.0)
+string time # 🔧 加热时间(支持带单位,如"5 min","300",默认秒)
string temp_spec # 温度规格(可选)
string time_spec # 时间规格(可选)
string pressure # 压力规格(可选,不做特殊处理)
diff --git a/unilabos_msgs/action/Recrystallize.action b/unilabos_msgs/action/Recrystallize.action
index fe727e8bd..2ae42bfe0 100644
--- a/unilabos_msgs/action/Recrystallize.action
+++ b/unilabos_msgs/action/Recrystallize.action
@@ -1,9 +1,9 @@
# Request
-string ratio
-string solvent1
-string solvent2
-string vessel
-float64 volume
+string ratio # 溶剂比例(如"1:1","3:7")
+string solvent1 # 第一种溶剂
+string solvent2 # 第二种溶剂
+string vessel # 目标容器
+string volume # 🔧 总体积(支持带单位,如"100 mL","50",默认mL)
---
# Result
bool success
diff --git a/unilabos_msgs/action/Stir.action b/unilabos_msgs/action/Stir.action
index 64e9f0436..e3e5580bc 100644
--- a/unilabos_msgs/action/Stir.action
+++ b/unilabos_msgs/action/Stir.action
@@ -1,11 +1,11 @@
# Goal - 搅拌操作的目标参数
string vessel # 搅拌容器名称(必需)
-string time # 搅拌时间(如 "0.5 h", "30 min")
+string time # 🔧 搅拌时间(如 "0.5 h", "30 min", "300",默认秒)
string event # 事件标识(如 "A", "B")
string time_spec # 时间规格(如 "several minutes")
float64 stir_time # 解析后的搅拌时间(秒)
float64 stir_speed # 搅拌速度(默认200.0)
-float64 settling_time # 沉降时间(默认60.0)
+string settling_time # 🔧 沉降时间(支持带单位,默认秒)
---
# Result - 操作结果
bool success # 操作是否成功
diff --git a/unilabos_msgs/action/WashSolid.action b/unilabos_msgs/action/WashSolid.action
index 8ef159d73..281ca4cbc 100644
--- a/unilabos_msgs/action/WashSolid.action
+++ b/unilabos_msgs/action/WashSolid.action
@@ -1,12 +1,12 @@
# Goal - 固体清洗操作的目标参数
string vessel # 装有固体的容器名称(必需)
string solvent # 清洗溶剂名称(必需)
-string volume # 🔧 修改:体积(支持数字和带单位的字符串)
+string volume # 🔧 体积(支持数字和带单位的字符串,如"100 mL","?")
string filtrate_vessel # 滤液收集容器(可选,默认"")
float64 temp # 清洗温度(可选,默认25.0)
bool stir # 是否搅拌(可选,默认false)
float64 stir_speed # 搅拌速度(可选,默认0.0)
-float64 time # 清洗时间(可选,默认0.0)
+string time # 🔧 清洗时间(支持带单位,如"5 min","300 s",默认秒)
int32 repeats # 重复次数(与repeats_spec二选一)
string volume_spec # 体积规格(优先级高于volume)
string repeats_spec # 重复次数规格(优先级高于repeats)
From ac294194e6da03ae4499847225e448f610b0903e Mon Sep 17 00:00:00 2001
From: KCFeng425 <2100011801@stu.pku.edu.cn>
Date: Tue, 15 Jul 2025 10:31:19 +0800
Subject: [PATCH 8/9] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BA=86=E5=85=A8protoco?=
=?UTF-8?q?l=E7=9A=84=E8=BF=90=E8=A1=8C=E6=97=B6=E9=97=B4=EF=BC=8C?=
=?UTF-8?q?=E9=99=A4=E4=BA=86pumptransfer=E7=9B=B8=E5=85=B3=E7=9A=84?=
=?UTF-8?q?=E8=BF=98=E6=B2=A1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
unilabos/compile/dry_protocol.py | 48 +-
unilabos/compile/evaporate_protocol.py | 236 ++---
unilabos/compile/filter_protocol.py | 103 +-
unilabos/compile/heatchill_protocol.py | 597 ++++--------
unilabos/compile/hydrogenate_protocol.py | 40 +-
unilabos/compile/recrystallize_protocol.py | 155 ++-
unilabos/compile/reset_handling_protocol.py | 121 ++-
unilabos/compile/run_column_protocol.py | 82 +-
unilabos/compile/stir_protocol.py | 671 ++++---------
unilabos/compile/wash_solid_protocol.py | 879 ++++--------------
unilabos/devices/virtual/virtual_column.py | 115 ++-
unilabos/devices/virtual/virtual_filter.py | 109 ++-
unilabos/devices/virtual/virtual_heatchill.py | 183 ++--
.../devices/virtual/virtual_multiway_valve.py | 169 +++-
unilabos/devices/virtual/virtual_rotavap.py | 139 ++-
.../virtual/virtual_solid_dispenser.py | 156 +++-
unilabos/devices/virtual/virtual_stirrer.py | 178 +++-
17 files changed, 1808 insertions(+), 2173 deletions(-)
diff --git a/unilabos/compile/dry_protocol.py b/unilabos/compile/dry_protocol.py
index 34044eba4..1f06069aa 100644
--- a/unilabos/compile/dry_protocol.py
+++ b/unilabos/compile/dry_protocol.py
@@ -67,37 +67,47 @@ def generate_dry_protocol(
# 默认参数
dry_temp = 60.0 # 默认干燥温度 60°C
dry_time = 3600.0 # 默认干燥时间 1小时(3600秒)
+ simulation_time = 60.0 # 模拟时间 1分钟
- print(f"DRY: 开始生成干燥协议")
- print(f" - 化合物: {compound}")
- print(f" - 容器: {vessel}")
- print(f" - 干燥温度: {dry_temp}°C")
- print(f" - 干燥时间: {dry_time/60:.0f} 分钟")
+ print(f"🌡️ DRY: 开始生成干燥协议 ✨")
+ print(f" 🧪 化合物: {compound}")
+ print(f" 🥽 容器: {vessel}")
+ print(f" 🔥 干燥温度: {dry_temp}°C")
+ print(f" ⏰ 干燥时间: {dry_time/60:.0f} 分钟")
# 1. 验证目标容器存在
+ print(f"\n📋 步骤1: 验证目标容器 '{vessel}' 是否存在...")
if vessel not in G.nodes():
- print(f"DRY: 警告 - 容器 '{vessel}' 不存在于系统中,跳过干燥")
+ print(f"⚠️ DRY: 警告 - 容器 '{vessel}' 不存在于系统中,跳过干燥 😢")
return action_sequence
+ print(f"✅ 容器 '{vessel}' 验证通过!")
# 2. 查找相连的加热器
+ print(f"\n🔍 步骤2: 查找与容器相连的加热器...")
heater_id = find_connected_heater(G, vessel)
if heater_id is None:
- print(f"DRY: 警告 - 未找到与容器 '{vessel}' 相连的加热器,跳过干燥")
+ print(f"😭 DRY: 警告 - 未找到与容器 '{vessel}' 相连的加热器,跳过干燥")
+ print(f"🎭 添加模拟干燥动作...")
# 添加一个等待动作,表示干燥过程(模拟)
action_sequence.append({
"action_name": "wait",
"action_kwargs": {
- "time": 60.0, # 等待1分钟
+ "time": 10.0, # 模拟等待时间
"description": f"模拟干燥 {compound} (无加热器可用)"
}
})
+ print(f"📄 DRY: 协议生成完成,共 {len(action_sequence)} 个动作 🎯")
return action_sequence
+ print(f"🎉 找到加热器: {heater_id}!")
+
# 3. 启动加热器进行干燥
- print(f"DRY: 启动加热器 {heater_id} 进行干燥")
+ print(f"\n🚀 步骤3: 开始执行干燥流程...")
+ print(f"🔥 启动加热器 {heater_id} 进行干燥")
# 3.1 启动加热
+ print(f" ⚡ 动作1: 启动加热到 {dry_temp}°C...")
action_sequence.append({
"device_id": heater_id,
"action_name": "heat_chill_start",
@@ -107,29 +117,35 @@ def generate_dry_protocol(
"purpose": f"干燥 {compound}"
}
})
+ print(f" ✅ 加热器启动命令已添加 🔥")
# 3.2 等待温度稳定
+ print(f" ⏳ 动作2: 等待温度稳定...")
action_sequence.append({
"action_name": "wait",
"action_kwargs": {
- "time": 60.0,
+ "time": 10.0,
"description": f"等待温度稳定到 {dry_temp}°C"
}
})
+ print(f" ✅ 温度稳定等待命令已添加 🌡️")
# 3.3 保持干燥温度
+ print(f" 🔄 动作3: 保持干燥温度 {simulation_time/60:.0f} 分钟...")
action_sequence.append({
"device_id": heater_id,
"action_name": "heat_chill",
"action_kwargs": {
"vessel": vessel,
"temp": dry_temp,
- "time": dry_time,
+ "time": simulation_time,
"purpose": f"干燥 {compound},保持温度 {dry_temp}°C"
}
})
+ print(f" ✅ 温度保持命令已添加 🌡️⏰")
# 3.4 停止加热
+ print(f" ⏹️ 动作4: 停止加热...")
action_sequence.append({
"device_id": heater_id,
"action_name": "heat_chill_stop",
@@ -138,18 +154,22 @@ def generate_dry_protocol(
"purpose": f"干燥完成,停止加热"
}
})
+ print(f" ✅ 停止加热命令已添加 🛑")
# 3.5 等待冷却
+ print(f" ❄️ 动作5: 等待冷却...")
action_sequence.append({
"action_name": "wait",
"action_kwargs": {
- "time": 300.0, # 等待5分钟冷却
+ "time": 10.0, # 等待10秒冷却
"description": f"等待 {compound} 冷却"
}
})
+ print(f" ✅ 冷却等待命令已添加 🧊")
- print(f"DRY: 协议生成完成,共 {len(action_sequence)} 个动作")
- print(f"DRY: 预计总时间: {(dry_time + 360)/60:.0f} 分钟")
+ print(f"\n🎊 DRY: 协议生成完成,共 {len(action_sequence)} 个动作 🎯")
+ print(f"⏱️ DRY: 预计总时间: {(dry_time + 360)/60:.0f} 分钟 ⌛")
+ print(f"🏁 所有动作序列准备就绪! ✨")
return action_sequence
diff --git a/unilabos/compile/evaporate_protocol.py b/unilabos/compile/evaporate_protocol.py
index 4eedb3cdd..6a2d6f69a 100644
--- a/unilabos/compile/evaporate_protocol.py
+++ b/unilabos/compile/evaporate_protocol.py
@@ -7,7 +7,7 @@
def debug_print(message):
"""调试输出"""
- print(f"[EVAPORATE] {message}", flush=True)
+ print(f"🧪 [EVAPORATE] {message}", flush=True)
logger.info(f"[EVAPORATE] {message}")
def parse_time_input(time_input: Union[str, float]) -> float:
@@ -21,18 +21,20 @@ def parse_time_input(time_input: Union[str, float]) -> float:
float: 时间(秒)
"""
if isinstance(time_input, (int, float)):
+ debug_print(f"⏱️ 时间输入为数字: {time_input}s ✨")
return float(time_input)
if not time_input or not str(time_input).strip():
+ debug_print(f"⚠️ 时间输入为空,使用默认值: 180s (3分钟) 🕐")
return 180.0 # 默认3分钟
time_str = str(time_input).lower().strip()
- debug_print(f"解析时间输入: '{time_str}'")
+ debug_print(f"🔍 解析时间输入: '{time_str}' 📝")
# 处理未知时间
if time_str in ['?', 'unknown', 'tbd']:
default_time = 180.0 # 默认3分钟
- debug_print(f"检测到未知时间,使用默认值: {default_time}s")
+ debug_print(f"❓ 检测到未知时间,使用默认值: {default_time}s (3分钟) 🤷♀️")
return default_time
# 移除空格并提取数字和单位
@@ -45,10 +47,10 @@ def parse_time_input(time_input: Union[str, float]) -> float:
# 如果无法解析,尝试直接转换为数字(默认秒)
try:
value = float(time_str)
- debug_print(f"时间解析: {time_str} → {value}s(无单位,默认秒)")
+ debug_print(f"✅ 时间解析成功: {time_str} → {value}s(无单位,默认秒)⏰")
return value
except ValueError:
- debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值180s")
+ debug_print(f"❌ 无法解析时间: '{time_str}',使用默认值180s (3分钟) 😅")
return 180.0
value = float(match.group(1))
@@ -57,14 +59,17 @@ def parse_time_input(time_input: Union[str, float]) -> float:
# 转换为秒
if unit in ['min', 'minute']:
time_sec = value * 60.0 # min -> s
+ debug_print(f"🕐 时间转换: {value} 分钟 → {time_sec}s ⏰")
elif unit in ['h', 'hr', 'hour']:
time_sec = value * 3600.0 # h -> s
+ debug_print(f"🕐 时间转换: {value} 小时 → {time_sec}s ({time_sec/60:.1f}分钟) ⏰")
elif unit in ['d', 'day']:
time_sec = value * 86400.0 # d -> s
+ debug_print(f"🕐 时间转换: {value} 天 → {time_sec}s ({time_sec/3600:.1f}小时) ⏰")
else: # s, sec, second 或默认
time_sec = value # 已经是s
+ debug_print(f"🕐 时间转换: {value}s → {time_sec}s (已是秒) ⏰")
- debug_print(f"时间转换: {value}{unit} → {time_sec}s")
return time_sec
def find_rotavap_device(G: nx.DiGraph, vessel: str = None) -> Optional[str]:
@@ -78,28 +83,30 @@ def find_rotavap_device(G: nx.DiGraph, vessel: str = None) -> Optional[str]:
Returns:
str: 找到的旋转蒸发仪设备ID,如果没找到返回None
"""
- debug_print("查找旋转蒸发仪设备...")
+ debug_print("🔍 开始查找旋转蒸发仪设备... 🌪️")
# 如果指定了vessel,先检查是否存在且是旋转蒸发仪
if vessel:
+ debug_print(f"🎯 检查指定设备: {vessel} 🔧")
if vessel in G.nodes():
node_data = G.nodes[vessel]
node_class = node_data.get('class', '')
node_type = node_data.get('type', '')
- debug_print(f"检查指定设备 {vessel}: class={node_class}, type={node_type}")
+ debug_print(f"📋 设备信息 {vessel}: class={node_class}, type={node_type}")
# 检查是否为旋转蒸发仪
if any(keyword in str(node_class).lower() for keyword in ['rotavap', 'rotary', 'evaporat']):
- debug_print(f"✓ 找到指定的旋转蒸发仪: {vessel}")
+ debug_print(f"🎉 找到指定的旋转蒸发仪: {vessel} ✨")
return vessel
elif node_type == 'device':
- debug_print(f"✓ 指定设备存在,尝试直接使用: {vessel}")
+ debug_print(f"✅ 指定设备存在,尝试直接使用: {vessel} 🔧")
return vessel
else:
- debug_print(f"✗ 指定的设备 {vessel} 不存在")
+ debug_print(f"❌ 指定的设备 {vessel} 不存在 😞")
# 在所有设备中查找旋转蒸发仪
+ debug_print("🔎 在所有设备中搜索旋转蒸发仪... 🕵️♀️")
rotavap_candidates = []
for node_id, node_data in G.nodes(data=True):
@@ -113,17 +120,17 @@ def find_rotavap_device(G: nx.DiGraph, vessel: str = None) -> Optional[str]:
# 检查设备类型
if any(keyword in str(node_class).lower() for keyword in ['rotavap', 'rotary', 'evaporat']):
rotavap_candidates.append(node_id)
- debug_print(f"✓ 找到旋转蒸发仪候选: {node_id} (class: {node_class})")
+ debug_print(f"🌟 找到旋转蒸发仪候选: {node_id} (class: {node_class}) 🌪️")
elif any(keyword in str(node_id).lower() for keyword in ['rotavap', 'rotary', 'evaporat']):
rotavap_candidates.append(node_id)
- debug_print(f"✓ 找到旋转蒸发仪候选 (按名称): {node_id}")
+ debug_print(f"🌟 找到旋转蒸发仪候选 (按名称): {node_id} 🌪️")
if rotavap_candidates:
selected = rotavap_candidates[0] # 选择第一个找到的
- debug_print(f"✓ 选择旋转蒸发仪: {selected}")
+ debug_print(f"🎯 选择旋转蒸发仪: {selected} 🏆")
return selected
- debug_print("✗ 未找到旋转蒸发仪设备")
+ debug_print("😭 未找到旋转蒸发仪设备 💔")
return None
def find_connected_vessel(G: nx.DiGraph, rotavap_device: str) -> Optional[str]:
@@ -137,31 +144,33 @@ def find_connected_vessel(G: nx.DiGraph, rotavap_device: str) -> Optional[str]:
Returns:
str: 连接的容器ID,如果没找到返回None
"""
- debug_print(f"查找与 {rotavap_device} 连接的容器...")
+ debug_print(f"🔗 查找与 {rotavap_device} 连接的容器... 🥽")
# 查看旋转蒸发仪的子设备
rotavap_data = G.nodes[rotavap_device]
children = rotavap_data.get('children', [])
+ debug_print(f"👶 检查子设备: {children}")
for child_id in children:
if child_id in G.nodes():
child_data = G.nodes[child_id]
child_type = child_data.get('type', '')
if child_type == 'container':
- debug_print(f"✓ 找到连接的容器: {child_id}")
+ debug_print(f"🎉 找到连接的容器: {child_id} 🥽✨")
return child_id
# 查看邻接的容器
+ debug_print("🤝 检查邻接设备...")
for neighbor in G.neighbors(rotavap_device):
neighbor_data = G.nodes[neighbor]
neighbor_type = neighbor_data.get('type', '')
if neighbor_type == 'container':
- debug_print(f"✓ 找到邻接的容器: {neighbor}")
+ debug_print(f"🎉 找到邻接的容器: {neighbor} 🥽✨")
return neighbor
- debug_print("✗ 未找到连接的容器")
+ debug_print("😞 未找到连接的容器 💔")
return None
def generate_evaporate_protocol(
@@ -191,110 +200,136 @@ def generate_evaporate_protocol(
List[Dict[str, Any]]: 动作序列
"""
- debug_print("=" * 50)
- debug_print("开始生成蒸发协议(支持单位)")
- debug_print(f"输入参数:")
- debug_print(f" - vessel: {vessel}")
- debug_print(f" - pressure: {pressure} bar")
- debug_print(f" - temp: {temp}°C")
- debug_print(f" - time: {time} (类型: {type(time)})")
- debug_print(f" - stir_speed: {stir_speed} RPM")
- debug_print(f" - solvent: '{solvent}'")
- debug_print("=" * 50)
+ debug_print("🌟" * 20)
+ debug_print("🌪️ 开始生成蒸发协议(支持单位)✨")
+ debug_print(f"📝 输入参数:")
+ debug_print(f" 🥽 vessel: {vessel}")
+ debug_print(f" 💨 pressure: {pressure} bar")
+ debug_print(f" 🌡️ temp: {temp}°C")
+ debug_print(f" ⏰ time: {time} (类型: {type(time)})")
+ debug_print(f" 🌪️ stir_speed: {stir_speed} RPM")
+ debug_print(f" 🧪 solvent: '{solvent}'")
+ debug_print("🌟" * 20)
# === 步骤1: 查找旋转蒸发仪设备 ===
- debug_print("步骤1: 查找旋转蒸发仪设备...")
+ debug_print("📍 步骤1: 查找旋转蒸发仪设备... 🔍")
# 验证vessel参数
if not vessel:
+ debug_print("❌ vessel 参数不能为空! 😱")
raise ValueError("vessel 参数不能为空")
# 查找旋转蒸发仪设备
rotavap_device = find_rotavap_device(G, vessel)
if not rotavap_device:
+ debug_print("💥 未找到旋转蒸发仪设备! 😭")
raise ValueError(f"未找到旋转蒸发仪设备。请检查组态图中是否包含 class 包含 'rotavap'、'rotary' 或 'evaporat' 的设备")
+ debug_print(f"🎉 成功找到旋转蒸发仪: {rotavap_device} ✨")
+
# === 步骤2: 确定目标容器 ===
- debug_print("步骤2: 确定目标容器...")
+ debug_print("📍 步骤2: 确定目标容器... 🥽")
target_vessel = vessel
# 如果vessel就是旋转蒸发仪设备,查找连接的容器
if vessel == rotavap_device:
+ debug_print("🔄 vessel就是旋转蒸发仪,查找连接的容器...")
connected_vessel = find_connected_vessel(G, rotavap_device)
if connected_vessel:
target_vessel = connected_vessel
- debug_print(f"使用连接的容器: {target_vessel}")
+ debug_print(f"✅ 使用连接的容器: {target_vessel} 🥽✨")
else:
- debug_print(f"未找到连接的容器,使用设备本身: {rotavap_device}")
+ debug_print(f"⚠️ 未找到连接的容器,使用设备本身: {rotavap_device} 🔧")
target_vessel = rotavap_device
elif vessel in G.nodes() and G.nodes[vessel].get('type') == 'container':
- debug_print(f"使用指定的容器: {vessel}")
+ debug_print(f"✅ 使用指定的容器: {vessel} 🥽✨")
target_vessel = vessel
else:
- debug_print(f"容器 '{vessel}' 不存在或类型不正确,使用旋转蒸发仪设备: {rotavap_device}")
+ debug_print(f"⚠️ 容器 '{vessel}' 不存在或类型不正确,使用旋转蒸发仪设备: {rotavap_device} 🔧")
target_vessel = rotavap_device
# === 🔧 新增:步骤3:单位解析处理 ===
- debug_print("步骤3: 单位解析处理...")
+ debug_print("📍 步骤3: 单位解析处理... ⚡")
# 解析时间
final_time = parse_time_input(time)
- debug_print(f"时间解析: {time} → {final_time}s ({final_time/60:.1f}分钟)")
+ debug_print(f"🎯 时间解析完成: {time} → {final_time}s ({final_time/60:.1f}分钟) ⏰✨")
# === 步骤4: 参数验证和修正 ===
- debug_print("步骤4: 参数验证和修正...")
+ debug_print("📍 步骤4: 参数验证和修正... 🔧")
# 修正参数范围
if pressure <= 0 or pressure > 1.0:
- debug_print(f"真空度 {pressure} bar 超出范围,修正为 0.1 bar")
+ debug_print(f"⚠️ 真空度 {pressure} bar 超出范围,修正为 0.1 bar 💨")
pressure = 0.1
+ else:
+ debug_print(f"✅ 真空度 {pressure} bar 在正常范围内 💨")
if temp < 10.0 or temp > 200.0:
- debug_print(f"温度 {temp}°C 超出范围,修正为 60°C")
+ debug_print(f"⚠️ 温度 {temp}°C 超出范围,修正为 60°C 🌡️")
temp = 60.0
+ else:
+ debug_print(f"✅ 温度 {temp}°C 在正常范围内 🌡️")
if final_time <= 0:
- debug_print(f"时间 {final_time}s 无效,修正为 180s")
+ debug_print(f"⚠️ 时间 {final_time}s 无效,修正为 180s (3分钟) ⏰")
final_time = 180.0
+ else:
+ debug_print(f"✅ 时间 {final_time}s ({final_time/60:.1f}分钟) 有效 ⏰")
if stir_speed < 10.0 or stir_speed > 300.0:
- debug_print(f"旋转速度 {stir_speed} RPM 超出范围,修正为 100 RPM")
+ debug_print(f"⚠️ 旋转速度 {stir_speed} RPM 超出范围,修正为 100 RPM 🌪️")
stir_speed = 100.0
+ else:
+ debug_print(f"✅ 旋转速度 {stir_speed} RPM 在正常范围内 🌪️")
# 根据溶剂优化参数
if solvent:
- debug_print(f"根据溶剂 '{solvent}' 优化参数...")
+ debug_print(f"🧪 根据溶剂 '{solvent}' 优化参数... 🔬")
solvent_lower = solvent.lower()
if any(s in solvent_lower for s in ['water', 'aqueous', 'h2o']):
temp = max(temp, 80.0)
pressure = max(pressure, 0.2)
- debug_print("水系溶剂:提高温度和真空度")
+ debug_print("💧 水系溶剂:提高温度和真空度 🌡️💨")
elif any(s in solvent_lower for s in ['ethanol', 'methanol', 'acetone']):
temp = min(temp, 50.0)
pressure = min(pressure, 0.05)
- debug_print("易挥发溶剂:降低温度和真空度")
+ debug_print("🍺 易挥发溶剂:降低温度和真空度 🌡️💨")
elif any(s in solvent_lower for s in ['dmso', 'dmi', 'toluene']):
temp = max(temp, 100.0)
pressure = min(pressure, 0.01)
- debug_print("高沸点溶剂:提高温度,降低真空度")
+ debug_print("🔥 高沸点溶剂:提高温度,降低真空度 🌡️💨")
+ else:
+ debug_print("🧪 通用溶剂,使用标准参数 ✨")
+ else:
+ debug_print("🤷♀️ 未指定溶剂,使用默认参数 ✨")
- debug_print(f"最终参数: pressure={pressure}, temp={temp}, time={final_time}, stir_speed={stir_speed}")
+ debug_print(f"🎯 最终参数: pressure={pressure} bar 💨, temp={temp}°C 🌡️, time={final_time}s ⏰, stir_speed={stir_speed} RPM 🌪️")
# === 步骤5: 生成动作序列 ===
- debug_print("步骤5: 生成动作序列...")
+ debug_print("📍 步骤5: 生成动作序列... 🎬")
action_sequence = []
- # 等待稳定
+ # 1. 等待稳定
+ debug_print(" 🔄 动作1: 添加初始等待稳定... ⏳")
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 10}
})
+ debug_print(" ✅ 初始等待动作已添加 ⏳✨")
+
+ # 2. 执行蒸发
+ debug_print(f" 🌪️ 动作2: 执行蒸发操作...")
+ debug_print(f" 🔧 设备: {rotavap_device}")
+ debug_print(f" 🥽 容器: {target_vessel}")
+ debug_print(f" 💨 真空度: {pressure} bar")
+ debug_print(f" 🌡️ 温度: {temp}°C")
+ debug_print(f" ⏰ 时间: {final_time}s ({final_time/60:.1f}分钟)")
+ debug_print(f" 🌪️ 旋转速度: {stir_speed} RPM")
- # 执行蒸发
- debug_print(f"执行蒸发: 设备={rotavap_device}, 容器={target_vessel}")
evaporate_action = {
"device_id": rotavap_device,
"action_name": "evaporate",
@@ -308,97 +343,24 @@ def generate_evaporate_protocol(
}
}
action_sequence.append(evaporate_action)
+ debug_print(" ✅ 蒸发动作已添加 🌪️✨")
- # 蒸发后等待
+ # 3. 蒸发后等待
+ debug_print(" 🔄 动作3: 添加蒸发后等待... ⏳")
action_sequence.append({
"action_name": "wait",
- "action_kwargs": {"time": 30}
+ "action_kwargs": {"time": 10}
})
+ debug_print(" ✅ 蒸发后等待动作已添加 ⏳✨")
# === 总结 ===
- debug_print("=" * 50)
- debug_print(f"蒸发协议生成完成")
- debug_print(f"总动作数: {len(action_sequence)}")
- debug_print(f"旋转蒸发仪: {rotavap_device}")
- debug_print(f"目标容器: {target_vessel}")
- debug_print(f"蒸发参数: {pressure} bar, {temp}°C, {final_time}s, {stir_speed} RPM")
- debug_print("=" * 50)
+ debug_print("🎊" * 20)
+ debug_print(f"🎉 蒸发协议生成完成! ✨")
+ debug_print(f"📊 总动作数: {len(action_sequence)} 个 📝")
+ debug_print(f"🌪️ 旋转蒸发仪: {rotavap_device} 🔧")
+ debug_print(f"🥽 目标容器: {target_vessel} 🧪")
+ debug_print(f"⚙️ 蒸发参数: {pressure} bar 💨, {temp}°C 🌡️, {final_time}s ⏰, {stir_speed} RPM 🌪️")
+ debug_print(f"⏱️ 预计总时间: {(final_time + 20)/60:.1f} 分钟 ⌛")
+ debug_print("🎊" * 20)
return action_sequence
-
-# === 便捷函数 ===
-
-def generate_quick_evaporate_protocol(
- G: nx.DiGraph,
- vessel: str,
- **kwargs
-) -> List[Dict[str, Any]]:
- """快速蒸发:低温短时间"""
- return generate_evaporate_protocol(
- G, vessel,
- pressure=0.2,
- temp=40.0,
- time="15 min", # 🔧 使用带单位的时间
- stir_speed=80.0,
- **kwargs
- )
-
-def generate_gentle_evaporate_protocol(
- G: nx.DiGraph,
- vessel: str,
- **kwargs
-) -> List[Dict[str, Any]]:
- """温和蒸发:中等条件"""
- return generate_evaporate_protocol(
- G, vessel,
- pressure=0.1,
- temp=50.0,
- time="45 min", # 🔧 使用带单位的时间
- stir_speed=60.0,
- **kwargs
- )
-
-def generate_high_vacuum_evaporate_protocol(
- G: nx.DiGraph,
- vessel: str,
- **kwargs
-) -> List[Dict[str, Any]]:
- """高真空蒸发:低温高真空"""
- return generate_evaporate_protocol(
- G, vessel,
- pressure=0.01,
- temp=35.0,
- time="1 h", # 🔧 使用带单位的时间
- stir_speed=120.0,
- **kwargs
- )
-
-def generate_standard_evaporate_protocol(
- G: nx.DiGraph,
- vessel: str,
- **kwargs
-) -> List[Dict[str, Any]]:
- """标准蒸发:常用参数"""
- return generate_evaporate_protocol(
- G, vessel,
- pressure=0.1,
- temp=60.0,
- time="3 min", # 🔧 使用带单位的时间
- stir_speed=100.0,
- **kwargs
- )
-
-# 测试函数
-def test_time_parsing():
- """测试时间解析功能"""
- print("=== EVAPORATE 时间解析测试 ===")
-
- test_times = ["3 min", "180", "0.5 h", "2 hours", "?", "unknown", "1.5", "30 s"]
- for time_str in test_times:
- result = parse_time_input(time_str)
- print(f"时间解析: '{time_str}' → {result}s ({result/60:.1f}分钟)")
-
- print("✅ 测试完成")
-
-if __name__ == "__main__":
- test_time_parsing()
diff --git a/unilabos/compile/filter_protocol.py b/unilabos/compile/filter_protocol.py
index 662968480..d974a416e 100644
--- a/unilabos/compile/filter_protocol.py
+++ b/unilabos/compile/filter_protocol.py
@@ -7,12 +7,12 @@
def debug_print(message):
"""调试输出"""
- print(f"[FILTER] {message}", flush=True)
+ print(f"🧪 [FILTER] {message}", flush=True)
logger.info(f"[FILTER] {message}")
def find_filter_device(G: nx.DiGraph) -> str:
"""查找过滤器设备"""
- debug_print("查找过滤器设备...")
+ debug_print("🔍 查找过滤器设备... 🌊")
# 查找过滤器设备
for node in G.nodes():
@@ -20,27 +20,33 @@ def find_filter_device(G: nx.DiGraph) -> str:
node_class = node_data.get('class', '') or ''
if 'filter' in node_class.lower() or 'filter' in node.lower():
- debug_print(f"找到过滤器设备: {node}")
+ debug_print(f"🎉 找到过滤器设备: {node} ✨")
return node
# 如果没找到,寻找可能的过滤器名称
+ debug_print("🔎 在预定义名称中搜索过滤器... 📋")
possible_names = ["filter", "filter_1", "virtual_filter", "filtration_unit"]
for name in possible_names:
if name in G.nodes():
- debug_print(f"找到过滤器设备: {name}")
+ debug_print(f"🎉 找到过滤器设备: {name} ✨")
return name
+ debug_print("😭 未找到过滤器设备 💔")
raise ValueError("未找到过滤器设备")
def validate_vessel(G: nx.DiGraph, vessel: str, vessel_type: str = "容器") -> None:
"""验证容器是否存在"""
+ debug_print(f"🔍 验证{vessel_type}: '{vessel}' 🧪")
+
if not vessel:
+ debug_print(f"❌ {vessel_type}不能为空! 😱")
raise ValueError(f"{vessel_type}不能为空")
if vessel not in G.nodes():
+ debug_print(f"❌ {vessel_type} '{vessel}' 不存在于系统中! 😞")
raise ValueError(f"{vessel_type} '{vessel}' 不存在于系统中")
- debug_print(f"✅ {vessel_type} '{vessel}' 验证通过")
+ debug_print(f"✅ {vessel_type} '{vessel}' 验证通过 🎯")
def generate_filter_protocol(
G: nx.DiGraph,
@@ -61,47 +67,53 @@ def generate_filter_protocol(
List[Dict[str, Any]]: 过滤操作的动作序列
"""
- debug_print("=" * 60)
- debug_print("开始生成过滤协议")
- debug_print(f"输入参数:")
- debug_print(f" - vessel: {vessel}")
- debug_print(f" - filtrate_vessel: {filtrate_vessel}")
- debug_print(f" - 其他参数: {kwargs}")
- debug_print("=" * 60)
+ debug_print("🌊" * 20)
+ debug_print("🚀 开始生成过滤协议 ✨")
+ debug_print(f"📝 输入参数:")
+ debug_print(f" 🥽 vessel: {vessel}")
+ debug_print(f" 🧪 filtrate_vessel: {filtrate_vessel}")
+ debug_print(f" ⚙️ 其他参数: {kwargs}")
+ debug_print("🌊" * 20)
action_sequence = []
# === 参数验证 ===
- debug_print("步骤1: 参数验证...")
+ debug_print("📍 步骤1: 参数验证... 🔧")
# 验证必需参数
+ debug_print(" 🔍 验证必需参数...")
validate_vessel(G, vessel, "过滤容器")
+ debug_print(" ✅ 必需参数验证完成 🎯")
# 验证可选参数
+ debug_print(" 🔍 验证可选参数...")
if filtrate_vessel:
validate_vessel(G, filtrate_vessel, "滤液容器")
- debug_print("模式: 过滤并收集滤液")
+ debug_print(" 🌊 模式: 过滤并收集滤液 💧")
else:
- debug_print("模式: 过滤并收集固体")
+ debug_print(" 🧱 模式: 过滤并收集固体 🔬")
+ debug_print(" ✅ 可选参数验证完成 🎯")
# === 查找设备 ===
- debug_print("步骤2: 查找设备...")
+ debug_print("📍 步骤2: 查找设备... 🔍")
try:
+ debug_print(" 🔎 搜索过滤器设备...")
filter_device = find_filter_device(G)
- debug_print(f"使用过滤器设备: {filter_device}")
+ debug_print(f" 🎉 使用过滤器设备: {filter_device} 🌊✨")
except Exception as e:
- debug_print(f"❌ 设备查找失败: {str(e)}")
+ debug_print(f" ❌ 设备查找失败: {str(e)} 😭")
raise ValueError(f"设备查找失败: {str(e)}")
# === 转移到过滤器(如果需要)===
- debug_print("步骤3: 转移到过滤器...")
+ debug_print("📍 步骤3: 转移到过滤器... 🚚")
if vessel != filter_device:
- debug_print(f"需要转移: {vessel} → {filter_device}")
+ debug_print(f" 🚛 需要转移: {vessel} → {filter_device} 📦")
try:
+ debug_print(" 🔄 开始执行转移操作...")
# 使用pump protocol转移液体到过滤器
transfer_actions = generate_pump_protocol_with_rinsing(
G=G,
@@ -121,20 +133,21 @@ def generate_filter_protocol(
if transfer_actions:
action_sequence.extend(transfer_actions)
- debug_print(f"✅ 添加了 {len(transfer_actions)} 个转移动作")
+ debug_print(f" ✅ 添加了 {len(transfer_actions)} 个转移动作 🚚✨")
else:
- debug_print("⚠️ 转移协议返回空序列")
+ debug_print(" ⚠️ 转移协议返回空序列 🤔")
except Exception as e:
- debug_print(f"❌ 转移失败: {str(e)}")
- # 继续执行,可能是直接连接的过滤器
+ debug_print(f" ❌ 转移失败: {str(e)} 😞")
+ debug_print(" 🔄 继续执行,可能是直接连接的过滤器 🤞")
else:
- debug_print("过滤容器就是过滤器,无需转移")
+ debug_print(" ✅ 过滤容器就是过滤器,无需转移 🎯")
# === 执行过滤操作 ===
- debug_print("步骤4: 执行过滤操作...")
+ debug_print("📍 步骤4: 执行过滤操作... 🌊")
# 构建过滤动作参数
+ debug_print(" ⚙️ 构建过滤参数...")
filter_kwargs = {
"vessel": filter_device, # 过滤器设备
"filtrate_vessel": filtrate_vessel, # 滤液容器(可能为空)
@@ -145,7 +158,8 @@ def generate_filter_protocol(
"volume": kwargs.get("volume", 0.0) # 0表示过滤所有
}
- debug_print(f"过滤参数: {filter_kwargs}")
+ debug_print(f" 📋 过滤参数: {filter_kwargs}")
+ debug_print(" 🌊 开始过滤操作...")
# 过滤动作
filter_action = {
@@ -154,20 +168,24 @@ def generate_filter_protocol(
"action_kwargs": filter_kwargs
}
action_sequence.append(filter_action)
+ debug_print(" ✅ 过滤动作已添加 🌊✨")
# 过滤后等待
+ debug_print(" ⏳ 添加过滤后等待...")
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 10.0}
})
+ debug_print(" ✅ 过滤后等待动作已添加 ⏰✨")
# === 收集滤液(如果需要)===
- debug_print("步骤5: 收集滤液...")
+ debug_print("📍 步骤5: 收集滤液... 💧")
if filtrate_vessel:
- debug_print(f"收集滤液: {filter_device} → {filtrate_vessel}")
+ debug_print(f" 🧪 收集滤液: {filter_device} → {filtrate_vessel} 💧")
try:
+ debug_print(" 🔄 开始执行收集操作...")
# 使用pump protocol收集滤液
collect_actions = generate_pump_protocol_with_rinsing(
G=G,
@@ -187,29 +205,32 @@ def generate_filter_protocol(
if collect_actions:
action_sequence.extend(collect_actions)
- debug_print(f"✅ 添加了 {len(collect_actions)} 个收集动作")
+ debug_print(f" ✅ 添加了 {len(collect_actions)} 个收集动作 🧪✨")
else:
- debug_print("⚠️ 收集协议返回空序列")
+ debug_print(" ⚠️ 收集协议返回空序列 🤔")
except Exception as e:
- debug_print(f"❌ 收集滤液失败: {str(e)}")
- # 继续执行,可能滤液直接流入指定容器
+ debug_print(f" ❌ 收集滤液失败: {str(e)} 😞")
+ debug_print(" 🔄 继续执行,可能滤液直接流入指定容器 🤞")
else:
- debug_print("未指定滤液容器,固体保留在过滤器中")
+ debug_print(" 🧱 未指定滤液容器,固体保留在过滤器中 🔬")
# === 最终等待 ===
+ debug_print("📍 步骤6: 最终等待... ⏰")
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5.0}
})
+ debug_print(" ✅ 最终等待动作已添加 ⏰✨")
# === 总结 ===
- debug_print("=" * 60)
- debug_print(f"过滤协议生成完成")
- debug_print(f"总动作数: {len(action_sequence)}")
- debug_print(f"过滤容器: {vessel}")
- debug_print(f"过滤器设备: {filter_device}")
- debug_print(f"滤液容器: {filtrate_vessel or '无(保留固体)'}")
- debug_print("=" * 60)
+ debug_print("🎊" * 20)
+ debug_print(f"🎉 过滤协议生成完成! ✨")
+ debug_print(f"📊 总动作数: {len(action_sequence)} 个 📝")
+ debug_print(f"🥽 过滤容器: {vessel} 🧪")
+ debug_print(f"🌊 过滤器设备: {filter_device} 🔧")
+ debug_print(f"💧 滤液容器: {filtrate_vessel or '无(保留固体)'} 🧱")
+ debug_print(f"⏱️ 预计总时间: {(len(action_sequence) * 5):.0f} 秒 ⌛")
+ debug_print("🎊" * 20)
return action_sequence
diff --git a/unilabos/compile/heatchill_protocol.py b/unilabos/compile/heatchill_protocol.py
index 297015e34..f8bcc110a 100644
--- a/unilabos/compile/heatchill_protocol.py
+++ b/unilabos/compile/heatchill_protocol.py
@@ -7,179 +7,129 @@
def debug_print(message):
"""调试输出"""
- print(f"[HEATCHILL] {message}", flush=True)
+ print(f"🌡️ [HEATCHILL] {message}", flush=True)
logger.info(f"[HEATCHILL] {message}")
-def parse_time_with_units(time_input: Union[str, float, int], default_unit: str = "s") -> float:
+def parse_time_input(time_input: Union[str, float, int]) -> float:
"""
- 解析带单位的时间输入
+ 解析时间输入(统一函数)
Args:
time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0)
- default_unit: 默认单位(默认为秒)
Returns:
float: 时间(秒)
"""
if not time_input:
- return 0.0
+ return 300.0
- # 处理数值输入
+ # 🔢 处理数值输入
if isinstance(time_input, (int, float)):
result = float(time_input)
- debug_print(f"数值时间输入: {time_input} → {result}s(默认单位)")
+ debug_print(f"⏰ 数值时间: {time_input} → {result}s")
return result
- # 处理字符串输入
+ # 📝 处理字符串输入
time_str = str(time_input).lower().strip()
- debug_print(f"解析时间字符串: '{time_str}'")
+ debug_print(f"🔍 解析时间: '{time_str}'")
- # 处理特殊值
- if time_str in ['?', 'unknown', 'tbd', 'to be determined']:
- default_time = 300.0 # 5分钟默认值
- debug_print(f"检测到未知时间,使用默认值: {default_time}s")
- return default_time
+ # ❓ 特殊值处理
+ special_times = {
+ '?': 300.0, 'unknown': 300.0, 'tbd': 300.0,
+ 'overnight': 43200.0, 'several hours': 10800.0,
+ 'few hours': 7200.0, 'long time': 3600.0, 'short time': 300.0
+ }
+
+ if time_str in special_times:
+ result = special_times[time_str]
+ debug_print(f"🎯 特殊时间: '{time_str}' → {result}s ({result/60:.1f}分钟)")
+ return result
- # 如果是纯数字,使用默认单位
+ # 🔢 纯数字处理
try:
- value = float(time_str)
- if default_unit == "s":
- result = value
- elif default_unit in ["min", "minute"]:
- result = value * 60.0
- elif default_unit in ["h", "hour"]:
- result = value * 3600.0
- else:
- result = value # 默认秒
- debug_print(f"纯数字输入: {time_str} → {result}s(单位: {default_unit})")
+ result = float(time_str)
+ debug_print(f"⏰ 纯数字: {time_str} → {result}s")
return result
except ValueError:
pass
- # 使用正则表达式匹配数字和单位
+ # 📐 正则表达式解析
pattern = r'(\d+\.?\d*)\s*([a-z]*)'
match = re.match(pattern, time_str)
if not match:
- debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 60s")
- return 60.0
+ debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 300s")
+ return 300.0
value = float(match.group(1))
- unit = match.group(2) or default_unit
+ unit = match.group(2) or 's'
- # 单位转换映射
+ # 📏 单位转换
unit_multipliers = {
- # 秒
- 's': 1.0,
- 'sec': 1.0,
- 'second': 1.0,
- 'seconds': 1.0,
-
- # 分钟
- 'm': 60.0,
- 'min': 60.0,
- 'mins': 60.0,
- 'minute': 60.0,
- 'minutes': 60.0,
-
- # 小时
- 'h': 3600.0,
- 'hr': 3600.0,
- 'hrs': 3600.0,
- 'hour': 3600.0,
- 'hours': 3600.0,
-
- # 天
- 'd': 86400.0,
- 'day': 86400.0,
- 'days': 86400.0,
+ 's': 1.0, 'sec': 1.0, 'second': 1.0, 'seconds': 1.0,
+ 'm': 60.0, 'min': 60.0, 'mins': 60.0, 'minute': 60.0, 'minutes': 60.0,
+ 'h': 3600.0, 'hr': 3600.0, 'hrs': 3600.0, 'hour': 3600.0, 'hours': 3600.0,
+ 'd': 86400.0, 'day': 86400.0, 'days': 86400.0
}
multiplier = unit_multipliers.get(unit, 1.0)
result = value * multiplier
- debug_print(f"时间解析: '{time_str}' → {value} {unit} → {result}s")
+ debug_print(f"✅ 时间解析: '{time_str}' → {value} {unit} → {result}s ({result/60:.1f}分钟)")
return result
-def parse_temp_spec(temp_spec: str) -> float:
- """解析温度规格为具体温度"""
- if not temp_spec:
- return 25.0
-
- temp_spec = temp_spec.strip().lower()
-
- # 特殊温度规格
- special_temps = {
- "room temperature": 25.0, # 室温
- "reflux": 78.0, # 默认回流温度
- "ice bath": 0.0, # 冰浴
- "boiling": 100.0, # 沸腾
- "hot": 60.0, # 热
- "warm": 40.0, # 温热
- "cold": 10.0, # 冷
- }
-
- if temp_spec in special_temps:
- return special_temps[temp_spec]
-
- # 解析带单位的温度(如 "256 °C")
- temp_pattern = r'(\d+(?:\.\d+)?)\s*°?[cf]?'
- match = re.search(temp_pattern, temp_spec)
+def parse_temp_input(temp_input: Union[str, float], default_temp: float = 25.0) -> float:
+ """
+ 解析温度输入(统一函数)
- if match:
- return float(match.group(1))
+ Args:
+ temp_input: 温度输入
+ default_temp: 默认温度
+
+ Returns:
+ float: 温度(°C)
+ """
+ if not temp_input:
+ return default_temp
- return 25.0
-
-def parse_time_spec(time_spec: str) -> float:
- """解析时间规格为秒数"""
- if not time_spec:
- return 300.0
+ # 🔢 数值输入
+ if isinstance(temp_input, (int, float)):
+ result = float(temp_input)
+ debug_print(f"🌡️ 数值温度: {temp_input} → {result}°C")
+ return result
- time_spec = time_spec.strip().lower()
+ # 📝 字符串输入
+ temp_str = str(temp_input).lower().strip()
+ debug_print(f"🔍 解析温度: '{temp_str}'")
- # 特殊时间规格
- special_times = {
- "overnight": 43200.0, # 12小时
- "several hours": 10800.0, # 3小时
- "few hours": 7200.0, # 2小时
- "long time": 3600.0, # 1小时
- "short time": 300.0, # 5分钟
+ # 🎯 特殊温度
+ special_temps = {
+ "room temperature": 25.0, "reflux": 78.0, "ice bath": 0.0,
+ "boiling": 100.0, "hot": 60.0, "warm": 40.0, "cold": 10.0
}
- if time_spec in special_times:
- return special_times[time_spec]
+ if temp_str in special_temps:
+ result = special_temps[temp_str]
+ debug_print(f"🎯 特殊温度: '{temp_str}' → {result}°C")
+ return result
- # 解析带单位的时间(如 "2 h")
- time_pattern = r'(\d+(?:\.\d+)?)\s*([a-zA-Z]+)'
- match = re.search(time_pattern, time_spec)
+ # 📐 正则解析(如 "256 °C")
+ temp_pattern = r'(\d+(?:\.\d+)?)\s*°?[cf]?'
+ match = re.search(temp_pattern, temp_str)
if match:
- value = float(match.group(1))
- unit = match.group(2).lower()
-
- unit_multipliers = {
- 's': 1.0,
- 'sec': 1.0,
- 'min': 60.0,
- 'minute': 60.0,
- 'minutes': 60.0,
- 'h': 3600.0,
- 'hr': 3600.0,
- 'hour': 3600.0,
- 'hours': 3600.0,
- }
-
- multiplier = unit_multipliers.get(unit, 3600.0)
- return value * multiplier
+ result = float(match.group(1))
+ debug_print(f"✅ 温度解析: '{temp_str}' → {result}°C")
+ return result
- return 300.0
+ debug_print(f"⚠️ 无法解析温度: '{temp_str}',使用默认值: {default_temp}°C")
+ return default_temp
def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str:
"""查找与指定容器相连的加热/冷却设备"""
- debug_print(f"查找加热设备,目标容器: {vessel}")
+ debug_print(f"🔍 查找加热设备,目标容器: {vessel}")
- # 查找所有加热/冷却设备节点
+ # 🔧 查找所有加热设备
heatchill_nodes = []
for node in G.nodes():
node_data = G.nodes[node]
@@ -187,28 +137,55 @@ def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str:
if 'heatchill' in node_class.lower() or 'virtual_heatchill' in node_class:
heatchill_nodes.append(node)
- debug_print(f"找到加热设备: {node}")
+ debug_print(f"🎉 找到加热设备: {node}")
- if vessel:
- # 检查哪个加热设备与目标容器相连
+ # 🔗 检查连接
+ if vessel and heatchill_nodes:
for heatchill in heatchill_nodes:
if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill):
- debug_print(f"加热设备 '{heatchill}' 与容器 '{vessel}' 相连")
+ debug_print(f"✅ 加热设备 '{heatchill}' 与容器 '{vessel}' 相连")
return heatchill
- # 如果没有指定容器或没有直接连接,返回第一个可用的加热设备
+ # 🎯 使用第一个可用设备
if heatchill_nodes:
- debug_print(f"使用第一个加热设备: {heatchill_nodes[0]}")
- return heatchill_nodes[0]
+ selected = heatchill_nodes[0]
+ debug_print(f"🔧 使用第一个加热设备: {selected}")
+ return selected
- debug_print("未找到加热设备,使用默认设备")
+ # 🆘 默认设备
+ debug_print("⚠️ 未找到加热设备,使用默认设备")
return "heatchill_1"
+def validate_and_fix_params(temp: float, time: float, stir_speed: float) -> tuple:
+ """验证和修正参数"""
+ # 🌡️ 温度范围验证
+ if temp < -50.0 or temp > 300.0:
+ debug_print(f"⚠️ 温度 {temp}°C 超出范围,修正为 25°C")
+ temp = 25.0
+ else:
+ debug_print(f"✅ 温度 {temp}°C 在正常范围内")
+
+ # ⏰ 时间验证
+ if time < 0:
+ debug_print(f"⚠️ 时间 {time}s 无效,修正为 300s")
+ time = 300.0
+ else:
+ debug_print(f"✅ 时间 {time}s ({time/60:.1f}分钟) 有效")
+
+ # 🌪️ 搅拌速度验证
+ if stir_speed < 0 or stir_speed > 1500.0:
+ debug_print(f"⚠️ 搅拌速度 {stir_speed} RPM 超出范围,修正为 300 RPM")
+ stir_speed = 300.0
+ else:
+ debug_print(f"✅ 搅拌速度 {stir_speed} RPM 在正常范围内")
+
+ return temp, time, stir_speed
+
def generate_heat_chill_protocol(
G: nx.DiGraph,
vessel: str,
temp: float = 25.0,
- time: Union[str, float] = "300", # 🔧 修改:支持字符串时间
+ time: Union[str, float] = "300",
temp_spec: str = "",
time_spec: str = "",
pressure: str = "",
@@ -219,249 +196,111 @@ def generate_heat_chill_protocol(
**kwargs
) -> List[Dict[str, Any]]:
"""
- 生成加热/冷却操作的协议序列 - 支持单位
+ 生成加热/冷却操作的协议序列
"""
- debug_print("=" * 50)
- debug_print("开始生成加热冷却协议(支持单位)")
- debug_print(f"输入参数:")
- debug_print(f" - vessel: {vessel}")
- debug_print(f" - temp: {temp}°C")
- debug_print(f" - time: {time} (类型: {type(time)})")
- debug_print(f" - temp_spec: {temp_spec}")
- debug_print(f" - time_spec: {time_spec}")
- debug_print(f" - pressure: {pressure}")
- debug_print(f" - reflux_solvent: {reflux_solvent}")
- debug_print(f" - stir: {stir}")
- debug_print(f" - stir_speed: {stir_speed} RPM")
- debug_print(f" - purpose: {purpose}")
- debug_print(f" - 其他参数: {kwargs}")
- debug_print("=" * 50)
-
- action_sequence = []
-
- # === 参数验证 ===
- debug_print("步骤1: 参数验证...")
-
- # 验证必需参数
+ debug_print("🌡️" * 20)
+ debug_print("🚀 开始生成加热冷却协议 ✨")
+ debug_print(f"📝 输入参数:")
+ debug_print(f" 🥽 vessel: {vessel}")
+ debug_print(f" 🌡️ temp: {temp}°C")
+ debug_print(f" ⏰ time: {time}")
+ debug_print(f" 🎯 temp_spec: {temp_spec}")
+ debug_print(f" ⏱️ time_spec: {time_spec}")
+ debug_print(f" 🌪️ stir: {stir} ({stir_speed} RPM)")
+ debug_print("🌡️" * 20)
+
+ # 📋 参数验证
+ debug_print("📍 步骤1: 参数验证... 🔧")
if not vessel:
+ debug_print("❌ vessel 参数不能为空! 😱")
raise ValueError("vessel 参数不能为空")
if vessel not in G.nodes():
+ debug_print(f"❌ 容器 '{vessel}' 不存在于系统中! 😞")
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
- # === 🔧 新增:单位解析处理 ===
- debug_print("步骤2: 单位解析处理...")
-
- # 温度解析:优先使用 temp_spec,然后是 temp
- final_temp = temp
- if temp_spec:
- final_temp = parse_temp_spec(temp_spec)
- debug_print(f"温度解析: '{temp_spec}' → {final_temp}°C")
+ debug_print("✅ 基础参数验证通过 🎯")
- # 时间解析:优先使用 time_spec,然后是 time
- if time_spec:
- final_time = parse_time_spec(time_spec) # 使用现有的time_spec解析
- debug_print(f"时间解析: '{time_spec}' → {final_time}s")
- else:
- final_time = parse_time_with_units(time, "s")
- debug_print(f"时间解析: {time} → {final_time}s ({final_time/60:.1f}分钟)")
-
- # 参数范围验证
- if final_temp < -50.0 or final_temp > 300.0:
- debug_print(f"温度 {final_temp}°C 超出范围,修正为 25°C")
- final_temp = 25.0
+ # 🔄 参数解析
+ debug_print("📍 步骤2: 参数解析... ⚡")
- if final_time < 0:
- debug_print(f"时间 {final_time}s 无效,修正为 300s")
- final_time = 300.0
+ #温度解析:优先使用 temp_spec
+ final_temp = parse_temp_input(temp_spec, temp) if temp_spec else temp
- if stir_speed < 0 or stir_speed > 1500.0:
- debug_print(f"搅拌速度 {stir_speed} RPM 超出范围,修正为 300 RPM")
- stir_speed = 300.0
+ # 时间解析:优先使用 time_spec
+ final_time = parse_time_input(time_spec) if time_spec else parse_time_input(time)
- debug_print(f"✅ 单位解析和参数验证通过")
+ # 参数修正
+ final_temp, final_time, stir_speed = validate_and_fix_params(final_temp, final_time, stir_speed)
- # === 查找加热设备 ===
- debug_print("步骤3: 查找加热设备...")
+ debug_print(f"🎯 最终参数: temp={final_temp}°C, time={final_time}s, stir_speed={stir_speed} RPM")
+ # 🔍 查找设备
+ debug_print("📍 步骤3: 查找加热设备... 🔍")
try:
heatchill_id = find_connected_heatchill(G, vessel)
- debug_print(f"设备配置: 加热设备 = {heatchill_id}")
-
+ debug_print(f"🎉 使用加热设备: {heatchill_id} ✨")
except Exception as e:
- debug_print(f"❌ 设备查找失败: {str(e)}")
+ debug_print(f"❌ 设备查找失败: {str(e)} 😭")
raise ValueError(f"无法找到加热设备: {str(e)}")
- # === 执行加热操作 ===
- debug_print("步骤4: 执行加热操作...")
+ # 🚀 生成动作
+ debug_print("📍 步骤4: 生成加热动作... 🔥")
+
+ # 🕐 模拟运行时间优化
+ debug_print(" ⏱️ 检查模拟运行时间限制...")
+ original_time = final_time
+ simulation_time_limit = 100.0 # 模拟运行时间限制:100秒
+ if final_time > simulation_time_limit:
+ final_time = simulation_time_limit
+ debug_print(f" 🎮 模拟运行优化: {original_time}s → {final_time}s (限制为{simulation_time_limit}s) ⚡")
+ debug_print(f" 📊 时间缩短: {original_time/60:.1f}分钟 → {final_time/60:.1f}分钟 🚀")
+ else:
+ debug_print(f" ✅ 时间在限制内: {final_time}s ({final_time/60:.1f}分钟) 保持不变 🎯")
+
+ action_sequence = []
heatchill_action = {
"device_id": heatchill_id,
"action_name": "heat_chill",
"action_kwargs": {
"vessel": vessel,
- "temp": float(final_temp), # 🔧 确保是浮点数
- "time": float(final_time), # 🔧 确保是浮点数
- "stir": bool(stir), # 🔧 确保是布尔值
- "stir_speed": float(stir_speed), # 🔧 确保是浮点数
- "purpose": str(purpose or f"加热到 {final_temp}°C") # 🔧 确保是字符串
+ "temp": float(final_temp),
+ "time": float(final_time),
+ "stir": bool(stir),
+ "stir_speed": float(stir_speed),
+ "purpose": str(purpose or f"加热到 {final_temp}°C") + (f" (模拟时间: {final_time}s)" if original_time != final_time else "")
}
}
-
action_sequence.append(heatchill_action)
+ debug_print("✅ 加热动作已添加 🔥✨")
+
+ # 显示时间调整信息
+ if original_time != final_time:
+ debug_print(f" 🎭 模拟优化说明: 原计划 {original_time/60:.1f}分钟,实际模拟 {final_time/60:.1f}分钟 ⚡")
- # === 总结 ===
- debug_print("=" * 50)
- debug_print(f"加热冷却协议生成完成(支持单位)")
- debug_print(f"总动作数: {len(action_sequence)}")
- debug_print(f"加热容器: {vessel}")
- debug_print(f"目标温度: {final_temp}°C")
- debug_print(f"加热时间: {final_time}s ({final_time/60:.1f}分钟)")
- if pressure:
- debug_print(f"压力参数: {pressure} (已接收,不做特殊处理)")
- if reflux_solvent:
- debug_print(f"回流溶剂: {reflux_solvent} (已接收,不做特殊处理)")
- debug_print("=" * 50)
+ # 🎊 总结
+ debug_print("🎊" * 20)
+ debug_print(f"🎉 加热冷却协议生成完成! ✨")
+ debug_print(f"📊 总动作数: {len(action_sequence)} 个")
+ debug_print(f"🥽 加热容器: {vessel}")
+ debug_print(f"🌡️ 目标温度: {final_temp}°C")
+ debug_print(f"⏰ 加热时间: {final_time}s ({final_time/60:.1f}分钟)")
+ debug_print("🎊" * 20)
return action_sequence
def generate_heat_chill_to_temp_protocol(
- G: nx.DiGraph,
- vessel: str,
- temp: float = 25.0,
- time: Union[str, float] = 300.0, # 🔧 也支持字符串
- temp_spec: str = "",
- time_spec: str = "",
- pressure: str = "",
- reflux_solvent: str = "",
- stir: bool = False,
- stir_speed: float = 300.0,
- purpose: str = "",
- **kwargs # 🔧 接受额外参数,增强兼容性
+ G: nx.DiGraph,
+ vessel: str,
+ temp: float = 25.0,
+ time: Union[str, float] = 100.0,
+ **kwargs
) -> List[Dict[str, Any]]:
- """
- 生成加热/冷却操作的协议序列
-
- Args:
- G: 设备图
- vessel: 加热容器名称(必需)
- temp: 目标温度 (°C)
- time: 加热时间(支持字符串和数字)
- temp_spec: 温度规格(如 'room temperature', 'reflux')
- time_spec: 时间规格(如 'overnight', '2 h')
- pressure: 压力规格(如 '1 mbar'),不做特殊处理
- reflux_solvent: 回流溶剂名称,不做特殊处理
- stir: 是否搅拌
- stir_speed: 搅拌速度 (RPM)
- purpose: 操作目的
- **kwargs: 其他参数(兼容性)
-
- Returns:
- List[Dict[str, Any]]: 加热操作的动作序列
- """
-
- debug_print("=" * 50)
- debug_print("开始生成加热冷却协议")
- debug_print(f"输入参数:")
- debug_print(f" - vessel: {vessel}")
- debug_print(f" - temp: {temp}°C")
- debug_print(f" - time: {time} (类型: {type(time)})")
- debug_print(f" - temp_spec: {temp_spec}")
- debug_print(f" - time_spec: {time_spec}")
- debug_print(f" - pressure: {pressure}")
- debug_print(f" - reflux_solvent: {reflux_solvent}")
- debug_print(f" - stir: {stir}")
- debug_print(f" - stir_speed: {stir_speed} RPM")
- debug_print(f" - purpose: {purpose}")
- debug_print(f" - 其他参数: {kwargs}")
- debug_print("=" * 50)
-
- action_sequence = []
-
- # === 参数验证 ===
- debug_print("步骤1: 参数验证...")
-
- # 验证必需参数
- if not vessel:
- raise ValueError("vessel 参数不能为空")
-
- if vessel not in G.nodes():
- raise ValueError(f"容器 '{vessel}' 不存在于系统中")
-
- # 温度解析:优先使用 temp_spec,然后是 temp
- final_temp = temp
- if temp_spec:
- final_temp = parse_temp_spec(temp_spec)
- debug_print(f"温度解析: '{temp_spec}' → {final_temp}°C")
-
- # 🔧 修复:时间解析,支持字符串输入
- if time_spec:
- final_time = parse_time_spec(time_spec)
- debug_print(f"时间解析: '{time_spec}' → {final_time}s ({final_time / 60:.1f}分钟)")
- else:
- final_time = parse_time_with_units(time, "s")
- debug_print(f"时间解析: {time} → {final_time}s ({final_time/60:.1f}分钟)")
-
- # 参数范围验证
- if final_temp < -50.0 or final_temp > 300.0:
- debug_print(f"温度 {final_temp}°C 超出范围,修正为 25°C")
- final_temp = 25.0
-
- if final_time < 0:
- debug_print(f"时间 {final_time}s 无效,修正为 300s")
- final_time = 300.0
-
- if stir_speed < 0 or stir_speed > 1500.0:
- debug_print(f"搅拌速度 {stir_speed} RPM 超出范围,修正为 300 RPM")
- stir_speed = 300.0
-
- debug_print(f"✅ 参数验证通过")
-
- # === 查找加热设备 ===
- debug_print("步骤2: 查找加热设备...")
-
- try:
- heatchill_id = find_connected_heatchill(G, vessel)
- debug_print(f"设备配置: 加热设备 = {heatchill_id}")
-
- except Exception as e:
- debug_print(f"❌ 设备查找失败: {str(e)}")
- raise ValueError(f"无法找到加热设备: {str(e)}")
-
- # === 执行加热操作 ===
- debug_print("步骤3: 执行加热操作...")
-
- heatchill_action = {
- "device_id": heatchill_id,
- "action_name": "heat_chill",
- "action_kwargs": {
- "vessel": vessel,
- "temp": float(final_temp), # 🔧 确保是浮点数
- "time": float(final_time), # 🔧 确保是浮点数
- "stir": bool(stir), # 🔧 确保是布尔值
- "stir_speed": float(stir_speed), # 🔧 确保是浮点数
- "purpose": str(purpose or f"加热到 {final_temp}°C") # 🔧 确保是字符串
- }
- }
-
- action_sequence.append(heatchill_action)
-
- # === 总结 ===
- debug_print("=" * 50)
- debug_print(f"加热冷却协议生成完成")
- debug_print(f"总动作数: {len(action_sequence)}")
- debug_print(f"加热容器: {vessel}")
- debug_print(f"目标温度: {final_temp}°C")
- debug_print(f"加热时间: {final_time}s ({final_time / 60:.1f}分钟)")
- if pressure:
- debug_print(f"压力参数: {pressure} (已接收,不做特殊处理)")
- if reflux_solvent:
- debug_print(f"回流溶剂: {reflux_solvent} (已接收,不做特殊处理)")
- debug_print("=" * 50)
-
- return action_sequence
-
+ """生成加热到指定温度的协议(简化版)"""
+ debug_print(f"🌡️ 生成加热到温度协议: {vessel} → {temp}°C")
+ return generate_heat_chill_protocol(G, vessel, temp, time, **kwargs)
def generate_heat_chill_start_protocol(
G: nx.DiGraph,
@@ -472,33 +311,19 @@ def generate_heat_chill_start_protocol(
) -> List[Dict[str, Any]]:
"""生成开始加热操作的协议序列"""
- debug_print("=" * 50)
- debug_print("开始生成启动加热协议")
- debug_print(f"输入参数:")
- debug_print(f" - vessel: {vessel}")
- debug_print(f" - temp: {temp}°C")
- debug_print(f" - purpose: {purpose}")
- debug_print("=" * 50)
+ debug_print("🔥 开始生成启动加热协议 ✨")
+ debug_print(f"🥽 vessel: {vessel}, 🌡️ temp: {temp}°C")
- action_sequence = []
+ # 基础验证
+ if not vessel or vessel not in G.nodes():
+ debug_print("❌ 容器验证失败!")
+ raise ValueError("vessel 参数无效")
- # 验证参数
- if not vessel:
- raise ValueError("vessel 参数不能为空")
-
- if vessel not in G.nodes():
- raise ValueError(f"容器 '{vessel}' 不存在于系统中")
-
- # 查找加热设备
- try:
- heatchill_id = find_connected_heatchill(G, vessel)
- debug_print(f"设备配置: 加热设备 = {heatchill_id}")
- except Exception as e:
- debug_print(f"❌ 设备查找失败: {str(e)}")
- raise ValueError(f"无法找到加热设备: {str(e)}")
+ # 查找设备
+ heatchill_id = find_connected_heatchill(G, vessel)
- # 执行开始加热操作
- start_action = {
+ # 生成动作
+ action_sequence = [{
"device_id": heatchill_id,
"action_name": "heat_chill_start",
"action_kwargs": {
@@ -506,11 +331,9 @@ def generate_heat_chill_start_protocol(
"temp": temp,
"purpose": purpose or f"开始加热到 {temp}°C"
}
- }
+ }]
- action_sequence.append(start_action)
-
- debug_print(f"启动加热协议生成完成,动作数: {len(action_sequence)}")
+ debug_print(f"✅ 启动加热协议生成完成 🎯")
return action_sequence
def generate_heat_chill_stop_protocol(
@@ -520,48 +343,34 @@ def generate_heat_chill_stop_protocol(
) -> List[Dict[str, Any]]:
"""生成停止加热操作的协议序列"""
- debug_print("=" * 50)
- debug_print("开始生成停止加热协议")
- debug_print(f"输入参数:")
- debug_print(f" - vessel: {vessel}")
- debug_print("=" * 50)
+ debug_print("🛑 开始生成停止加热协议 ✨")
+ debug_print(f"🥽 vessel: {vessel}")
- action_sequence = []
+ # 基础验证
+ if not vessel or vessel not in G.nodes():
+ debug_print("❌ 容器验证失败!")
+ raise ValueError("vessel 参数无效")
- # 验证参数
- if not vessel:
- raise ValueError("vessel 参数不能为空")
+ # 查找设备
+ heatchill_id = find_connected_heatchill(G, vessel)
- if vessel not in G.nodes():
- raise ValueError(f"容器 '{vessel}' 不存在于系统中")
-
- # 查找加热设备
- try:
- heatchill_id = find_connected_heatchill(G, vessel)
- debug_print(f"设备配置: 加热设备 = {heatchill_id}")
- except Exception as e:
- debug_print(f"❌ 设备查找失败: {str(e)}")
- raise ValueError(f"无法找到加热设备: {str(e)}")
-
- # 执行停止加热操作
- stop_action = {
+ # 生成动作
+ action_sequence = [{
"device_id": heatchill_id,
"action_name": "heat_chill_stop",
"action_kwargs": {
"vessel": vessel
}
- }
-
- action_sequence.append(stop_action)
+ }]
- debug_print(f"停止加热协议生成完成,动作数: {len(action_sequence)}")
+ debug_print(f"✅ 停止加热协议生成完成 🎯")
return action_sequence
# 测试函数
def test_heatchill_protocol():
"""测试加热协议"""
- debug_print("=== HEATCHILL PROTOCOL 测试 ===")
- debug_print("✅ 测试完成")
+ debug_print("🧪 === HEATCHILL PROTOCOL 测试 === ✨")
+ debug_print("✅ 测试完成 🎉")
if __name__ == "__main__":
test_heatchill_protocol()
\ No newline at end of file
diff --git a/unilabos/compile/hydrogenate_protocol.py b/unilabos/compile/hydrogenate_protocol.py
index 80707057d..81cd926c5 100644
--- a/unilabos/compile/hydrogenate_protocol.py
+++ b/unilabos/compile/hydrogenate_protocol.py
@@ -255,11 +255,23 @@ def generate_hydrogenate_protocol(
action_sequence.append({
"action_name": "wait",
"action_kwargs": {
- "time": 120.0,
+ "time": 20.0,
"description": f"等待温度稳定到 {temperature}°C"
}
})
+ # 🕐 模拟运行时间优化
+ print("HYDROGENATE: 检查模拟运行时间限制...")
+ original_reaction_time = reaction_time
+ simulation_time_limit = 60.0 # 模拟运行时间限制:60秒
+
+ if reaction_time > simulation_time_limit:
+ reaction_time = simulation_time_limit
+ print(f"HYDROGENATE: 模拟运行优化: {original_reaction_time}s → {reaction_time}s (限制为{simulation_time_limit}s)")
+ print(f"HYDROGENATE: 时间缩短: {original_reaction_time/3600:.2f}小时 → {reaction_time/60:.1f}分钟")
+ else:
+ print(f"HYDROGENATE: 时间在限制内: {reaction_time}s ({reaction_time/60:.1f}分钟) 保持不变")
+
# 保持反应温度
action_sequence.append({
"device_id": heater_id,
@@ -268,19 +280,41 @@ def generate_hydrogenate_protocol(
"vessel": vessel,
"temp": temperature,
"time": reaction_time,
- "purpose": f"氢化反应: 保持 {temperature}°C,反应 {reaction_time/3600:.1f} 小时"
+ "purpose": f"氢化反应: 保持 {temperature}°C,反应 {reaction_time/60:.1f}分钟" + (f" (模拟时间)" if original_reaction_time != reaction_time else "")
}
})
+
+ # 显示时间调整信息
+ if original_reaction_time != reaction_time:
+ print(f"HYDROGENATE: 模拟优化说明: 原计划 {original_reaction_time/3600:.2f}小时,实际模拟 {reaction_time/60:.1f}分钟")
+
else:
print(f"HYDROGENATE: 警告 - 未找到加热器,使用室温反应")
+
+ # 🕐 室温反应也需要时间优化
+ print("HYDROGENATE: 检查室温反应模拟时间限制...")
+ original_reaction_time = reaction_time
+ simulation_time_limit = 60.0 # 模拟运行时间限制:60秒
+
+ if reaction_time > simulation_time_limit:
+ reaction_time = simulation_time_limit
+ print(f"HYDROGENATE: 室温反应时间优化: {original_reaction_time}s → {reaction_time}s")
+ print(f"HYDROGENATE: 时间缩短: {original_reaction_time/3600:.2f}小时 → {reaction_time/60:.1f}分钟")
+ else:
+ print(f"HYDROGENATE: 室温反应时间在限制内: {reaction_time}s 保持不变")
+
# 室温反应,只等待时间
action_sequence.append({
"action_name": "wait",
"action_kwargs": {
"time": reaction_time,
- "description": f"室温氢化反应 {reaction_time/3600:.1f} 小时"
+ "description": f"室温氢化反应 {reaction_time/60:.1f}分钟" + (f" (模拟时间)" if original_reaction_time != reaction_time else "")
}
})
+
+ # 显示时间调整信息
+ if original_reaction_time != reaction_time:
+ print(f"HYDROGENATE: 室温反应优化说明: 原计划 {original_reaction_time/3600:.2f}小时,实际模拟 {reaction_time/60:.1f}分钟")
# 7. 停止加热
if heater_id:
diff --git a/unilabos/compile/recrystallize_protocol.py b/unilabos/compile/recrystallize_protocol.py
index 4e5f5923a..569a79867 100644
--- a/unilabos/compile/recrystallize_protocol.py
+++ b/unilabos/compile/recrystallize_protocol.py
@@ -4,6 +4,11 @@
from .pump_protocol import generate_pump_protocol_with_rinsing
+def debug_print(message):
+ """调试输出"""
+ print(f"💎 [RECRYSTALLIZE] {message}", flush=True)
+
+
def parse_volume_with_units(volume_input: Union[str, float, int], default_unit: str = "mL") -> float:
"""
解析带单位的体积输入
@@ -16,22 +21,23 @@ def parse_volume_with_units(volume_input: Union[str, float, int], default_unit:
float: 体积(毫升)
"""
if not volume_input:
+ debug_print("⚠️ 体积输入为空,返回 0.0mL 📦")
return 0.0
# 处理数值输入
if isinstance(volume_input, (int, float)):
result = float(volume_input)
- print(f"RECRYSTALLIZE: 数值体积输入: {volume_input} → {result}mL(默认单位)")
+ debug_print(f"🔢 数值体积输入: {volume_input} → {result}mL(默认单位)💧")
return result
# 处理字符串输入
volume_str = str(volume_input).lower().strip()
- print(f"RECRYSTALLIZE: 解析体积字符串: '{volume_str}'")
+ debug_print(f"🔍 解析体积字符串: '{volume_str}' 📝")
# 处理特殊值
if volume_str in ['?', 'unknown', 'tbd', 'to be determined']:
default_volume = 50.0 # 50mL默认值
- print(f"RECRYSTALLIZE: 检测到未知体积,使用默认值: {default_volume}mL")
+ debug_print(f"❓ 检测到未知体积,使用默认值: {default_volume}mL 🎯")
return default_volume
# 如果是纯数字,使用默认单位
@@ -45,7 +51,7 @@ def parse_volume_with_units(volume_input: Union[str, float, int], default_unit:
result = value / 1000.0
else:
result = value # 默认mL
- print(f"RECRYSTALLIZE: 纯数字输入: {volume_str} → {result}mL(单位: {default_unit})")
+ debug_print(f"🔢 纯数字输入: {volume_str} → {result}mL(单位: {default_unit})📏")
return result
except ValueError:
pass
@@ -57,7 +63,7 @@ def parse_volume_with_units(volume_input: Union[str, float, int], default_unit:
match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
if not match:
- print(f"RECRYSTALLIZE: ⚠️ 无法解析体积: '{volume_str}',使用默认值: 50mL")
+ debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值: 50mL 🎯")
return 50.0
value = float(match.group(1))
@@ -66,12 +72,15 @@ def parse_volume_with_units(volume_input: Union[str, float, int], default_unit:
# 转换为毫升
if unit in ['l', 'liter']:
volume = value * 1000.0 # L -> mL
+ debug_print(f"📏 升转毫升: {value}L → {volume}mL 💧")
elif unit in ['μl', 'ul', 'microliter']:
volume = value / 1000.0 # μL -> mL
+ debug_print(f"📏 微升转毫升: {value}μL → {volume}mL 💧")
else: # ml, milliliter 或默认
volume = value # 已经是mL
+ debug_print(f"📏 毫升单位: {value}mL → {volume}mL 💧")
- print(f"RECRYSTALLIZE: 体积解析: '{volume_str}' → {value} {unit} → {volume}mL")
+ debug_print(f"✅ 体积解析完成: '{volume_str}' → {volume}mL ✨")
return volume
@@ -85,6 +94,8 @@ def parse_ratio(ratio_str: str) -> Tuple[float, float]:
Returns:
Tuple[float, float]: 比例元组 (ratio1, ratio2)
"""
+ debug_print(f"⚖️ 开始解析比例: '{ratio_str}' 📊")
+
try:
# 处理 "1:1", "3:7", "50:50" 等格式
if ":" in ratio_str:
@@ -92,6 +103,7 @@ def parse_ratio(ratio_str: str) -> Tuple[float, float]:
if len(parts) == 2:
ratio1 = float(parts[0])
ratio2 = float(parts[1])
+ debug_print(f"✅ 冒号格式解析成功: {ratio1}:{ratio2} 🎯")
return ratio1, ratio2
# 处理 "1-1", "3-7" 等格式
@@ -100,6 +112,7 @@ def parse_ratio(ratio_str: str) -> Tuple[float, float]:
if len(parts) == 2:
ratio1 = float(parts[0])
ratio2 = float(parts[1])
+ debug_print(f"✅ 横线格式解析成功: {ratio1}:{ratio2} 🎯")
return ratio1, ratio2
# 处理 "1,1", "3,7" 等格式
@@ -108,14 +121,15 @@ def parse_ratio(ratio_str: str) -> Tuple[float, float]:
if len(parts) == 2:
ratio1 = float(parts[0])
ratio2 = float(parts[1])
+ debug_print(f"✅ 逗号格式解析成功: {ratio1}:{ratio2} 🎯")
return ratio1, ratio2
# 默认 1:1
- print(f"RECRYSTALLIZE: 无法解析比例 '{ratio_str}',使用默认比例 1:1")
+ debug_print(f"⚠️ 无法解析比例 '{ratio_str}',使用默认比例 1:1 🎭")
return 1.0, 1.0
except ValueError:
- print(f"RECRYSTALLIZE: 比例解析错误 '{ratio_str}',使用默认比例 1:1")
+ debug_print(f"❌ 比例解析错误 '{ratio_str}',使用默认比例 1:1 🎭")
return 1.0, 1.0
@@ -130,7 +144,7 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
Returns:
str: 溶剂容器ID
"""
- print(f"RECRYSTALLIZE: 正在查找溶剂 '{solvent}' 的容器...")
+ debug_print(f"🔍 正在查找溶剂 '{solvent}' 的容器... 🧪")
# 构建可能的容器名称
possible_names = [
@@ -144,22 +158,27 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
f"vessel_{solvent}",
]
+ debug_print(f"📋 候选容器名称: {possible_names[:3]}... (共{len(possible_names)}个) 📝")
+
# 第一步:通过容器名称匹配
+ debug_print(" 🎯 步骤1: 精确名称匹配...")
for vessel_name in possible_names:
if vessel_name in G.nodes():
- print(f"RECRYSTALLIZE: 通过名称匹配找到容器: {vessel_name}")
+ debug_print(f" 🎉 通过名称匹配找到容器: {vessel_name} ✨")
return vessel_name
# 第二步:通过模糊匹配
+ debug_print(" 🔍 步骤2: 模糊名称匹配...")
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
node_name = G.nodes[node_id].get('name', '').lower()
if solvent.lower() in node_id.lower() or solvent.lower() in node_name:
- print(f"RECRYSTALLIZE: 通过模糊匹配找到容器: {node_id}")
+ debug_print(f" 🎉 通过模糊匹配找到容器: {node_id} ✨")
return node_id
# 第三步:通过液体类型匹配
+ debug_print(" 🧪 步骤3: 液体类型匹配...")
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
vessel_data = G.nodes[node_id].get('data', {})
@@ -171,9 +190,10 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
reagent_name = vessel_data.get('reagent_name', '').lower()
if solvent.lower() in liquid_type or solvent.lower() in reagent_name:
- print(f"RECRYSTALLIZE: 通过液体类型匹配找到容器: {node_id}")
+ debug_print(f" 🎉 通过液体类型匹配找到容器: {node_id} ✨")
return node_id
+ debug_print(f"❌ 找不到溶剂 '{solvent}' 对应的容器 😭")
raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器")
@@ -203,61 +223,80 @@ def generate_recrystallize_protocol(
"""
action_sequence = []
- print(f"RECRYSTALLIZE: 开始生成重结晶协议(支持单位)")
- print(f" - 比例: {ratio}")
- print(f" - 溶剂1: {solvent1}")
- print(f" - 溶剂2: {solvent2}")
- print(f" - 容器: {vessel}")
- print(f" - 总体积: {volume} (类型: {type(volume)})")
+ debug_print("💎" * 20)
+ debug_print("🚀 开始生成重结晶协议(支持单位)✨")
+ debug_print(f"📝 输入参数:")
+ debug_print(f" ⚖️ 比例: {ratio}")
+ debug_print(f" 🧪 溶剂1: {solvent1}")
+ debug_print(f" 🧪 溶剂2: {solvent2}")
+ debug_print(f" 🥽 容器: {vessel}")
+ debug_print(f" 💧 总体积: {volume} (类型: {type(volume)})")
+ debug_print("💎" * 20)
# 1. 验证目标容器存在
+ debug_print("📍 步骤1: 验证目标容器... 🔧")
if vessel not in G.nodes():
+ debug_print(f"❌ 目标容器 '{vessel}' 不存在于系统中! 😱")
raise ValueError(f"目标容器 '{vessel}' 不存在于系统中")
+ debug_print(f"✅ 目标容器 '{vessel}' 验证通过 🎯")
# 2. 🔧 新增:解析体积(支持单位)
+ debug_print("📍 步骤2: 解析体积(支持单位)... 💧")
final_volume = parse_volume_with_units(volume, "mL")
- print(f"RECRYSTALLIZE: 解析体积: {volume} → {final_volume}mL")
+ debug_print(f"🎯 体积解析完成: {volume} → {final_volume}mL ✨")
# 3. 解析比例
+ debug_print("📍 步骤3: 解析比例... ⚖️")
ratio1, ratio2 = parse_ratio(ratio)
total_ratio = ratio1 + ratio2
+ debug_print(f"🎯 比例解析完成: {ratio1}:{ratio2} (总比例: {total_ratio}) ✨")
# 4. 计算各溶剂体积
+ debug_print("📍 步骤4: 计算各溶剂体积... 🧮")
volume1 = final_volume * (ratio1 / total_ratio)
volume2 = final_volume * (ratio2 / total_ratio)
- print(f"RECRYSTALLIZE: 解析比例: {ratio1}:{ratio2}")
- print(f"RECRYSTALLIZE: {solvent1} 体积: {volume1:.2f} mL")
- print(f"RECRYSTALLIZE: {solvent2} 体积: {volume2:.2f} mL")
+ debug_print(f"🧪 {solvent1} 体积: {volume1:.2f} mL ({ratio1}/{total_ratio} × {final_volume})")
+ debug_print(f"🧪 {solvent2} 体积: {volume2:.2f} mL ({ratio2}/{total_ratio} × {final_volume})")
+ debug_print(f"✅ 体积计算完成: 总计 {volume1 + volume2:.2f} mL 🎯")
# 5. 查找溶剂容器
+ debug_print("📍 步骤5: 查找溶剂容器... 🔍")
try:
+ debug_print(f" 🔍 查找溶剂1容器...")
solvent1_vessel = find_solvent_vessel(G, solvent1)
- print(f"RECRYSTALLIZE: 找到溶剂1容器: {solvent1_vessel}")
+ debug_print(f" 🎉 找到溶剂1容器: {solvent1_vessel} ✨")
except ValueError as e:
+ debug_print(f" ❌ 溶剂1容器查找失败: {str(e)} 😭")
raise ValueError(f"无法找到溶剂1 '{solvent1}': {str(e)}")
try:
+ debug_print(f" 🔍 查找溶剂2容器...")
solvent2_vessel = find_solvent_vessel(G, solvent2)
- print(f"RECRYSTALLIZE: 找到溶剂2容器: {solvent2_vessel}")
+ debug_print(f" 🎉 找到溶剂2容器: {solvent2_vessel} ✨")
except ValueError as e:
+ debug_print(f" ❌ 溶剂2容器查找失败: {str(e)} 😭")
raise ValueError(f"无法找到溶剂2 '{solvent2}': {str(e)}")
# 6. 验证路径存在
+ debug_print("📍 步骤6: 验证传输路径... 🛤️")
try:
path1 = nx.shortest_path(G, source=solvent1_vessel, target=vessel)
- print(f"RECRYSTALLIZE: 溶剂1路径: {' → '.join(path1)}")
+ debug_print(f" 🛤️ 溶剂1路径: {' → '.join(path1)} ✅")
except nx.NetworkXNoPath:
+ debug_print(f" ❌ 溶剂1路径不可达: {solvent1_vessel} → {vessel} 😞")
raise ValueError(f"从溶剂1容器 '{solvent1_vessel}' 到目标容器 '{vessel}' 没有可用路径")
try:
path2 = nx.shortest_path(G, source=solvent2_vessel, target=vessel)
- print(f"RECRYSTALLIZE: 溶剂2路径: {' → '.join(path2)}")
+ debug_print(f" 🛤️ 溶剂2路径: {' → '.join(path2)} ✅")
except nx.NetworkXNoPath:
+ debug_print(f" ❌ 溶剂2路径不可达: {solvent2_vessel} → {vessel} 😞")
raise ValueError(f"从溶剂2容器 '{solvent2_vessel}' 到目标容器 '{vessel}' 没有可用路径")
# 7. 添加第一种溶剂
- print(f"RECRYSTALLIZE: 开始添加溶剂1 {volume1:.2f} mL")
+ debug_print("📍 步骤7: 添加第一种溶剂... 🧪")
+ debug_print(f" 🚰 开始添加溶剂1: {solvent1} ({volume1:.2f} mL)")
try:
pump_actions1 = generate_pump_protocol_with_rinsing(
@@ -277,21 +316,26 @@ def generate_recrystallize_protocol(
)
action_sequence.extend(pump_actions1)
+ debug_print(f" ✅ 溶剂1泵送动作已添加: {len(pump_actions1)} 个动作 🚰✨")
except Exception as e:
+ debug_print(f" ❌ 溶剂1泵协议生成失败: {str(e)} 😭")
raise ValueError(f"生成溶剂1泵协议时出错: {str(e)}")
# 8. 等待溶剂1稳定
+ debug_print(" ⏳ 添加溶剂1稳定等待...")
action_sequence.append({
"action_name": "wait",
"action_kwargs": {
- "time": 10.0,
+ "time": 5.0, # 🕐 缩短等待时间:10.0s → 5.0s
"description": f"等待溶剂1 {solvent1} 稳定"
}
})
+ debug_print(" ✅ 溶剂1稳定等待已添加 ⏰✨")
# 9. 添加第二种溶剂
- print(f"RECRYSTALLIZE: 开始添加溶剂2 {volume2:.2f} mL")
+ debug_print("📍 步骤8: 添加第二种溶剂... 🧪")
+ debug_print(f" 🚰 开始添加溶剂2: {solvent2} ({volume2:.2f} mL)")
try:
pump_actions2 = generate_pump_protocol_with_rinsing(
@@ -311,31 +355,63 @@ def generate_recrystallize_protocol(
)
action_sequence.extend(pump_actions2)
+ debug_print(f" ✅ 溶剂2泵送动作已添加: {len(pump_actions2)} 个动作 🚰✨")
except Exception as e:
+ debug_print(f" ❌ 溶剂2泵协议生成失败: {str(e)} 😭")
raise ValueError(f"生成溶剂2泵协议时出错: {str(e)}")
# 10. 等待溶剂2稳定
+ debug_print(" ⏳ 添加溶剂2稳定等待...")
action_sequence.append({
"action_name": "wait",
"action_kwargs": {
- "time": 10.0,
+ "time": 5.0, # 🕐 缩短等待时间:10.0s → 5.0s
"description": f"等待溶剂2 {solvent2} 稳定"
}
})
+ debug_print(" ✅ 溶剂2稳定等待已添加 ⏰✨")
# 11. 等待重结晶完成
+ debug_print("📍 步骤9: 等待重结晶完成... 💎")
+
+ # 🕐 模拟运行时间优化
+ debug_print(" ⏱️ 检查模拟运行时间限制...")
+ original_crystallize_time = 600.0 # 原始重结晶时间
+ simulation_time_limit = 60.0 # 模拟运行时间限制:60秒
+
+ final_crystallize_time = min(original_crystallize_time, simulation_time_limit)
+
+ if original_crystallize_time > simulation_time_limit:
+ debug_print(f" 🎮 模拟运行优化: {original_crystallize_time}s → {final_crystallize_time}s ⚡")
+ debug_print(f" 📊 时间缩短: {original_crystallize_time/60:.1f}分钟 → {final_crystallize_time/60:.1f}分钟 🚀")
+ else:
+ debug_print(f" ✅ 时间在限制内: {final_crystallize_time}s 保持不变 🎯")
+
action_sequence.append({
"action_name": "wait",
"action_kwargs": {
- "time": 600.0, # 等待10分钟进行重结晶
- "description": f"等待重结晶完成({solvent1}:{solvent2} = {ratio},总体积 {final_volume}mL)"
+ "time": final_crystallize_time,
+ "description": f"等待重结晶完成({solvent1}:{solvent2} = {ratio},总体积 {final_volume}mL)" + (f" (模拟时间)" if original_crystallize_time != final_crystallize_time else "")
}
})
-
- print(f"RECRYSTALLIZE: 协议生成完成,共 {len(action_sequence)} 个动作")
- print(f"RECRYSTALLIZE: 预计总时间: {620/60:.1f} 分钟")
- print(f"RECRYSTALLIZE: 总体积: {final_volume}mL")
+ debug_print(f" ✅ 重结晶等待已添加: {final_crystallize_time}s 💎✨")
+
+ # 显示时间调整信息
+ if original_crystallize_time != final_crystallize_time:
+ debug_print(f" 🎭 模拟优化说明: 原计划 {original_crystallize_time/60:.1f}分钟,实际模拟 {final_crystallize_time/60:.1f}分钟 ⚡")
+
+ # 🎊 总结
+ debug_print("💎" * 20)
+ debug_print(f"🎉 重结晶协议生成完成! ✨")
+ debug_print(f"📊 总动作数: {len(action_sequence)} 个")
+ debug_print(f"🥽 目标容器: {vessel}")
+ debug_print(f"💧 总体积: {final_volume}mL")
+ debug_print(f"⚖️ 溶剂比例: {solvent1}:{solvent2} = {ratio1}:{ratio2}")
+ debug_print(f"🧪 溶剂1: {solvent1} ({volume1:.2f}mL)")
+ debug_print(f"🧪 溶剂2: {solvent2} ({volume2:.2f}mL)")
+ debug_print(f"⏱️ 预计总时间: {(final_crystallize_time + 10)/60:.1f} 分钟 ⌛")
+ debug_print("💎" * 20)
return action_sequence
@@ -343,15 +419,16 @@ def generate_recrystallize_protocol(
# 测试函数
def test_recrystallize_protocol():
"""测试重结晶协议"""
- print("=== RECRYSTALLIZE PROTOCOL 测试 ===")
+ debug_print("🧪 === RECRYSTALLIZE PROTOCOL 测试 === ✨")
# 测试比例解析
+ debug_print("⚖️ 测试比例解析...")
test_ratios = ["1:1", "3:7", "50:50", "1-1", "2,8", "invalid"]
for ratio in test_ratios:
r1, r2 = parse_ratio(ratio)
- print(f"比例 '{ratio}' -> {r1}:{r2}")
+ debug_print(f" 📊 比例 '{ratio}' -> {r1}:{r2}")
- print("测试完成")
+ debug_print("✅ 测试完成 🎉")
if __name__ == "__main__":
diff --git a/unilabos/compile/reset_handling_protocol.py b/unilabos/compile/reset_handling_protocol.py
index 0fa55c235..2e51da337 100644
--- a/unilabos/compile/reset_handling_protocol.py
+++ b/unilabos/compile/reset_handling_protocol.py
@@ -3,6 +3,11 @@
from .pump_protocol import generate_pump_protocol_with_rinsing
+def debug_print(message):
+ """调试输出"""
+ print(f"🔄 [RESET_HANDLING] {message}", flush=True)
+
+
def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
"""
查找溶剂容器,支持多种匹配模式
@@ -14,7 +19,7 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
Returns:
str: 溶剂容器ID
"""
- print(f"RESET_HANDLING: 正在查找溶剂 '{solvent}' 的容器...")
+ debug_print(f"🔍 正在查找溶剂 '{solvent}' 的容器... 🧪")
# 构建可能的容器名称
possible_names = [
@@ -28,23 +33,30 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
f"vessel_{solvent}", # vessel_methanol
]
+ debug_print(f"📋 候选容器名称: {possible_names[:3]}... (共{len(possible_names)}个) 📝")
+
# 第一步:通过容器名称匹配
+ debug_print(" 🎯 步骤1: 精确名称匹配...")
for vessel_name in possible_names:
if vessel_name in G.nodes():
- print(f"RESET_HANDLING: 通过名称匹配找到容器: {vessel_name}")
+ debug_print(f" 🎉 通过名称匹配找到容器: {vessel_name} ✨")
return vessel_name
+ debug_print(" 😞 精确名称匹配失败,尝试模糊匹配... 🔍")
# 第二步:通过模糊匹配
+ debug_print(" 🔍 步骤2: 模糊名称匹配...")
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
node_name = G.nodes[node_id].get('name', '').lower()
# 检查是否包含溶剂名称
if solvent.lower() in node_id.lower() or solvent.lower() in node_name:
- print(f"RESET_HANDLING: 通过模糊匹配找到容器: {node_id}")
+ debug_print(f" 🎉 通过模糊匹配找到容器: {node_id} ✨")
return node_id
+ debug_print(" 😞 模糊匹配失败,尝试液体类型匹配... 🧪")
# 第三步:通过液体类型匹配
+ debug_print(" 🧪 步骤3: 液体类型匹配...")
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
vessel_data = G.nodes[node_id].get('data', {})
@@ -56,10 +68,11 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
reagent_name = vessel_data.get('reagent_name', '').lower()
if solvent.lower() in liquid_type or solvent.lower() in reagent_name:
- print(f"RESET_HANDLING: 通过液体类型匹配找到容器: {node_id}")
+ debug_print(f" 🎉 通过液体类型匹配找到容器: {node_id} ✨")
return node_id
# 列出可用容器帮助调试
+ debug_print(" 📊 显示可用容器信息...")
available_containers = []
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
@@ -75,13 +88,17 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
'reagent_name': vessel_data.get('reagent_name', '')
})
- print(f"RESET_HANDLING: 可用容器列表:")
- for container in available_containers:
- print(f" - {container['id']}: {container['name']}")
- print(f" 液体: {container['liquids']}")
- print(f" 试剂: {container['reagent_name']}")
+ debug_print(f" 📋 可用容器列表 (共{len(available_containers)}个):")
+ for i, container in enumerate(available_containers[:5]): # 只显示前5个
+ debug_print(f" {i+1}. 🥽 {container['id']}: {container['name']}")
+ debug_print(f" 💧 液体: {container['liquids']}")
+ debug_print(f" 🧪 试剂: {container['reagent_name']}")
+
+ if len(available_containers) > 5:
+ debug_print(f" ... 还有 {len(available_containers)-5} 个容器 📦")
- raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器。尝试了: {possible_names}")
+ debug_print(f"❌ 找不到溶剂 '{solvent}' 对应的容器 😭")
+ raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器。尝试了: {possible_names[:3]}...")
def generate_reset_handling_protocol(
@@ -104,35 +121,49 @@ def generate_reset_handling_protocol(
# 固定参数
target_vessel = "main_reactor" # 默认目标容器
- volume = 100.0 # 默认体积 100 mL
-
- print(f"RESET_HANDLING: 开始生成重置处理协议")
- print(f" - 溶剂: {solvent}")
- print(f" - 目标容器: {target_vessel}")
- print(f" - 体积: {volume} mL")
+ volume = 50.0 # 默认体积 50 mL
+
+ debug_print("🔄" * 20)
+ debug_print("🚀 开始生成重置处理协议 ✨")
+ debug_print(f"📝 输入参数:")
+ debug_print(f" 🧪 溶剂: {solvent}")
+ debug_print(f" 🥽 目标容器: {target_vessel}")
+ debug_print(f" 💧 体积: {volume} mL")
+ debug_print(f" ⚙️ 其他参数: {kwargs}")
+ debug_print("🔄" * 20)
# 1. 验证目标容器存在
+ debug_print("📍 步骤1: 验证目标容器... 🔧")
if target_vessel not in G.nodes():
+ debug_print(f"❌ 目标容器 '{target_vessel}' 不存在于系统中! 😱")
raise ValueError(f"目标容器 '{target_vessel}' 不存在于系统中")
+ debug_print(f"✅ 目标容器 '{target_vessel}' 验证通过 🎯")
# 2. 查找溶剂容器
+ debug_print("📍 步骤2: 查找溶剂容器... 🔍")
try:
solvent_vessel = find_solvent_vessel(G, solvent)
- print(f"RESET_HANDLING: 找到溶剂容器: {solvent_vessel}")
+ debug_print(f"🎉 找到溶剂容器: {solvent_vessel} ✨")
except ValueError as e:
+ debug_print(f"❌ 溶剂容器查找失败: {str(e)} 😭")
raise ValueError(f"无法找到溶剂 '{solvent}': {str(e)}")
# 3. 验证路径存在
+ debug_print("📍 步骤3: 验证传输路径... 🛤️")
try:
path = nx.shortest_path(G, source=solvent_vessel, target=target_vessel)
- print(f"RESET_HANDLING: 找到路径: {' → '.join(path)}")
+ debug_print(f"🛤️ 找到路径: {' → '.join(path)} ✅")
except nx.NetworkXNoPath:
+ debug_print(f"❌ 路径不可达: {solvent_vessel} → {target_vessel} 😞")
raise ValueError(f"从溶剂容器 '{solvent_vessel}' 到目标容器 '{target_vessel}' 没有可用路径")
# 4. 使用pump_protocol转移溶剂
- print(f"RESET_HANDLING: 开始转移溶剂 {volume} mL")
+ debug_print("📍 步骤4: 转移溶剂... 🚰")
+ debug_print(f" 🚛 开始转移: {solvent_vessel} → {target_vessel}")
+ debug_print(f" 💧 转移体积: {volume} mL")
try:
+ debug_print(" 🔄 生成泵送协议...")
pump_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=solvent_vessel,
@@ -150,21 +181,52 @@ def generate_reset_handling_protocol(
)
action_sequence.extend(pump_actions)
+ debug_print(f" ✅ 泵送协议已添加: {len(pump_actions)} 个动作 🚰✨")
except Exception as e:
+ debug_print(f" ❌ 泵送协议生成失败: {str(e)} 😭")
raise ValueError(f"生成泵协议时出错: {str(e)}")
# 5. 等待溶剂稳定
+ debug_print("📍 步骤5: 等待溶剂稳定... ⏳")
+
+ # 🕐 模拟运行时间优化
+ debug_print(" ⏱️ 检查模拟运行时间限制...")
+ original_wait_time = 10.0 # 原始等待时间
+ simulation_time_limit = 5.0 # 模拟运行时间限制:5秒
+
+ final_wait_time = min(original_wait_time, simulation_time_limit)
+
+ if original_wait_time > simulation_time_limit:
+ debug_print(f" 🎮 模拟运行优化: {original_wait_time}s → {final_wait_time}s ⚡")
+ debug_print(f" 📊 时间缩短: {original_wait_time}s → {final_wait_time}s 🚀")
+ else:
+ debug_print(f" ✅ 时间在限制内: {final_wait_time}s 保持不变 🎯")
+
action_sequence.append({
"action_name": "wait",
"action_kwargs": {
- "time": 10.0,
- "description": f"等待溶剂 {solvent} 稳定"
+ "time": final_wait_time,
+ "description": f"等待溶剂 {solvent} 稳定" + (f" (模拟时间)" if original_wait_time != final_wait_time else "")
}
})
-
- print(f"RESET_HANDLING: 协议生成完成,共 {len(action_sequence)} 个动作")
- print(f"RESET_HANDLING: 已添加 {volume} mL {solvent} 到 {target_vessel}")
+ debug_print(f" ✅ 稳定等待已添加: {final_wait_time}s ⏰✨")
+
+ # 显示时间调整信息
+ if original_wait_time != final_wait_time:
+ debug_print(f" 🎭 模拟优化说明: 原计划 {original_wait_time}s,实际模拟 {final_wait_time}s ⚡")
+
+ # 🎊 总结
+ debug_print("🔄" * 20)
+ debug_print(f"🎉 重置处理协议生成完成! ✨")
+ debug_print(f"📊 总动作数: {len(action_sequence)} 个")
+ debug_print(f"🧪 溶剂: {solvent}")
+ debug_print(f"🥽 源容器: {solvent_vessel}")
+ debug_print(f"🥽 目标容器: {target_vessel}")
+ debug_print(f"💧 转移体积: {volume} mL")
+ debug_print(f"⏱️ 预计总时间: {(final_wait_time + 5):.0f} 秒 ⌛")
+ debug_print(f"🎯 已添加 {volume} mL {solvent} 到 {target_vessel} 🚰✨")
+ debug_print("🔄" * 20)
return action_sequence
@@ -172,8 +234,15 @@ def generate_reset_handling_protocol(
# 测试函数
def test_reset_handling_protocol():
"""测试重置处理协议"""
- print("=== RESET HANDLING PROTOCOL 测试 ===")
- print("测试完成")
+ debug_print("🧪 === RESET HANDLING PROTOCOL 测试 === ✨")
+
+ # 测试溶剂名称
+ debug_print("🧪 测试常用溶剂名称...")
+ test_solvents = ["methanol", "ethanol", "water", "acetone", "dmso"]
+ for solvent in test_solvents:
+ debug_print(f" 🔍 测试溶剂: {solvent}")
+
+ debug_print("✅ 测试完成 🎉")
if __name__ == "__main__":
diff --git a/unilabos/compile/run_column_protocol.py b/unilabos/compile/run_column_protocol.py
index c60e24093..cb55f8650 100644
--- a/unilabos/compile/run_column_protocol.py
+++ b/unilabos/compile/run_column_protocol.py
@@ -277,13 +277,13 @@ def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:
# 查找是否有内置容器数据
config_data = G.nodes[vessel].get('config', {})
if 'volume' in config_data:
- default_volume = config_data.get('volume', 100.0)
+ default_volume = config_data.get('volume', 50.0)
debug_print(f"使用设备默认容量: {default_volume}mL")
return default_volume
# 对于旋蒸等设备,使用默认值
if 'rotavap' in vessel.lower():
- default_volume = 100.0
+ default_volume = 50.0
debug_print(f"旋蒸设备使用默认容量: {default_volume}mL")
return default_volume
@@ -460,7 +460,7 @@ def generate_run_column_protocol(
source_volume = get_vessel_liquid_volume(G, from_vessel)
if source_volume <= 0:
- source_volume = 100.0 # 默认体积
+ source_volume = 50.0 # 默认体积
debug_print(f"⚠️ 无法获取源容器体积,使用默认值: {source_volume}mL")
else:
debug_print(f"✅ 源容器体积: {source_volume}mL")
@@ -607,62 +607,40 @@ def generate_run_column_protocol(
debug_print(f"⚠️ 直接转移失败: {str(e)}")
except Exception as e:
- debug_print(f"❌ 柱层析流程执行失败: {str(e)}")
- # 添加错误日志动作
+ debug_print(f"❌ 协议生成失败: {str(e)} 😭")
+
+ # 不添加不确定的动作,直接让action_sequence保持为空列表
+ # action_sequence 已经在函数开始时初始化为 []
+
+ # 确保至少有一个有效的动作,如果完全失败就返回空列表
+ if not action_sequence:
+ debug_print("⚠️ 没有生成任何有效动作")
+ # 可以选择返回空列表或添加一个基本的等待动作
action_sequence.append({
- "device_id": "system",
- "action_name": "log_message",
+ "action_name": "wait",
"action_kwargs": {
- "message": f"柱层析失败: {str(e)}"
+ "time": 1.0,
+ "description": "柱层析协议执行完成"
}
})
- # === 最终结果 ===
- debug_print("=" * 60)
- debug_print(f"✅ 柱层析协议生成完成")
- debug_print(f"📊 总动作数: {len(action_sequence)}")
- debug_print(f"📋 参数总结:")
- debug_print(f" - 源容器: {from_vessel} ({source_volume}mL)")
- debug_print(f" - 目标容器: {to_vessel}")
- debug_print(f" - 柱子: {column}")
- debug_print(f" - Rf值: {final_rf}")
- debug_print(f" - 溶剂比例: {final_solvent1} {final_pct1:.1f}% : {final_solvent2} {final_pct2:.1f}%")
- debug_print(f" - 洗脱体积: {solvent1_volume:.1f}mL + {solvent2_volume:.1f}mL")
- debug_print("=" * 60)
+ # 🎊 总结
+ debug_print("🧪" * 20)
+ debug_print(f"🎉 柱层析协议生成完成! ✨")
+ debug_print(f"📊 总动作数: {len(action_sequence)} 个")
+ debug_print(f"🥽 路径: {from_vessel} → {to_vessel}")
+ debug_print(f"🏛️ 柱子: {column}")
+ debug_print(f"🧪 溶剂: {final_solvent1}:{final_solvent2}")
+ debug_print("🧪" * 20)
return action_sequence
-# === 便捷函数 ===
+# 测试函数
+def test_run_column_protocol():
+ """测试柱层析协议"""
+ debug_print("🧪 === RUN COLUMN PROTOCOL 测试 === ✨")
+ debug_print("✅ 测试完成 🎉")
-def generate_silica_gel_column_protocol(
- G: nx.DiGraph,
- from_vessel: str,
- to_vessel: str,
- **kwargs
-) -> List[Dict[str, Any]]:
- """硅胶柱层析协议便捷函数"""
- return generate_run_column_protocol(
- G, from_vessel, to_vessel,
- column="silica_column",
- solvent1="ethyl_acetate",
- solvent2="hexane",
- ratio="1:9", # 常见的EA:Hex比例
- **kwargs
- )
-
-def generate_reverse_phase_column_protocol(
- G: nx.DiGraph,
- from_vessel: str,
- to_vessel: str,
- **kwargs
-) -> List[Dict[str, Any]]:
- """反相柱层析协议便捷函数"""
- return generate_run_column_protocol(
- G, from_vessel, to_vessel,
- column="c18_column",
- solvent1="methanol",
- solvent2="water",
- ratio="7:3", # 常见的MeOH:H2O比例
- **kwargs
- )
+if __name__ == "__main__":
+ test_run_column_protocol()
diff --git a/unilabos/compile/stir_protocol.py b/unilabos/compile/stir_protocol.py
index 5343a2a47..f7a6a741b 100644
--- a/unilabos/compile/stir_protocol.py
+++ b/unilabos/compile/stir_protocol.py
@@ -7,12 +7,12 @@
def debug_print(message):
"""调试输出"""
- print(f"[STIR] {message}", flush=True)
+ print(f"🌪️ [STIR] {message}", flush=True)
logger.info(f"[STIR] {message}")
-def parse_time_with_units(time_input: Union[str, float, int], default_unit: str = "s") -> float:
+def parse_time_input(time_input: Union[str, float, int], default_unit: str = "s") -> float:
"""
- 解析带单位的时间输入
+ 统一的时间解析函数(精简版)
Args:
time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0)
@@ -22,259 +22,68 @@ def parse_time_with_units(time_input: Union[str, float, int], default_unit: str
float: 时间(秒)
"""
if not time_input:
- return 0.0
+ return 100.0 # 默认100秒
- # 处理数值输入
+ # 🔢 处理数值输入
if isinstance(time_input, (int, float)):
result = float(time_input)
- debug_print(f"数值时间输入: {time_input} → {result}s(默认单位)")
+ debug_print(f"⏰ 数值时间: {time_input} → {result}s")
return result
- # 处理字符串输入
+ # 📝 处理字符串输入
time_str = str(time_input).lower().strip()
- debug_print(f"解析时间字符串: '{time_str}'")
+ debug_print(f"🔍 解析时间: '{time_str}'")
- # 处理特殊值
- if time_str in ['?', 'unknown', 'tbd', 'to be determined']:
- default_time = 300.0 # 5分钟默认值
- debug_print(f"检测到未知时间,使用默认值: {default_time}s")
- return default_time
+ # ❓ 特殊值处理
+ special_times = {
+ '?': 300.0, 'unknown': 300.0, 'tbd': 300.0,
+ 'briefly': 30.0, 'quickly': 60.0, 'slowly': 600.0,
+ 'several minutes': 300.0, 'few minutes': 180.0, 'overnight': 3600.0
+ }
+
+ if time_str in special_times:
+ result = special_times[time_str]
+ debug_print(f"🎯 特殊时间: '{time_str}' → {result}s ({result/60:.1f}分钟)")
+ return result
- # 如果是纯数字,使用默认单位
+ # 🔢 纯数字处理
try:
- value = float(time_str)
- if default_unit == "s":
- result = value
- elif default_unit in ["min", "minute"]:
- result = value * 60.0
- elif default_unit in ["h", "hour"]:
- result = value * 3600.0
- else:
- result = value # 默认秒
- debug_print(f"纯数字输入: {time_str} → {result}s(单位: {default_unit})")
+ result = float(time_str)
+ debug_print(f"⏰ 纯数字: {time_str} → {result}s")
return result
except ValueError:
pass
- # 使用正则表达式匹配数字和单位
+ # 📐 正则表达式解析
pattern = r'(\d+\.?\d*)\s*([a-z]*)'
match = re.match(pattern, time_str)
if not match:
- debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 60s")
- return 60.0
+ debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 100s")
+ return 100.0
value = float(match.group(1))
unit = match.group(2) or default_unit
- # 单位转换映射
+ # 📏 单位转换
unit_multipliers = {
- # 秒
- 's': 1.0,
- 'sec': 1.0,
- 'second': 1.0,
- 'seconds': 1.0,
-
- # 分钟
- 'm': 60.0,
- 'min': 60.0,
- 'mins': 60.0,
- 'minute': 60.0,
- 'minutes': 60.0,
-
- # 小时
- 'h': 3600.0,
- 'hr': 3600.0,
- 'hrs': 3600.0,
- 'hour': 3600.0,
- 'hours': 3600.0,
-
- # 天
- 'd': 86400.0,
- 'day': 86400.0,
- 'days': 86400.0,
+ 's': 1.0, 'sec': 1.0, 'second': 1.0, 'seconds': 1.0,
+ 'm': 60.0, 'min': 60.0, 'mins': 60.0, 'minute': 60.0, 'minutes': 60.0,
+ 'h': 3600.0, 'hr': 3600.0, 'hrs': 3600.0, 'hour': 3600.0, 'hours': 3600.0,
+ 'd': 86400.0, 'day': 86400.0, 'days': 86400.0
}
multiplier = unit_multipliers.get(unit, 1.0)
result = value * multiplier
- debug_print(f"时间解析: '{time_str}' → {value} {unit} → {result}s")
+ debug_print(f"✅ 时间解析: '{time_str}' → {value} {unit} → {result}s ({result/60:.1f}分钟)")
return result
-def parse_time_spec(time_spec: str) -> float:
- """
- 解析时间规格字符串为秒数
-
- Args:
- time_spec: 时间规格字符串(如 "several minutes", "overnight", "few hours")
-
- Returns:
- float: 时间(秒)
- """
- if not time_spec:
- return 0.0
-
- time_spec = time_spec.lower().strip()
-
- # 预定义的时间规格映射
- time_spec_map = {
- # 几分钟
- "several minutes": 5.0 * 60, # 5分钟
- "few minutes": 3.0 * 60, # 3分钟
- "couple of minutes": 2.0 * 60, # 2分钟
- "a few minutes": 3.0 * 60, # 3分钟
- "some minutes": 5.0 * 60, # 5分钟
-
- # 几小时
- "several hours": 3.0 * 3600, # 3小时
- "few hours": 2.0 * 3600, # 2小时
- "couple of hours": 2.0 * 3600, # 2小时
- "a few hours": 3.0 * 3600, # 3小时
- "some hours": 4.0 * 3600, # 4小时
-
- # 特殊时间
- "overnight": 12.0 * 3600, # 12小时
- "over night": 12.0 * 3600, # 12小时
- "morning": 4.0 * 3600, # 4小时
- "afternoon": 6.0 * 3600, # 6小时
- "evening": 4.0 * 3600, # 4小时
-
- # 短时间
- "briefly": 30.0, # 30秒
- "momentarily": 10.0, # 10秒
- "quickly": 60.0, # 1分钟
- "slowly": 10.0 * 60, # 10分钟
-
- # 长时间
- "extended": 6.0 * 3600, # 6小时
- "prolonged": 8.0 * 3600, # 8小时
- "extensively": 12.0 * 3600, # 12小时
- }
-
- # 直接匹配
- if time_spec in time_spec_map:
- result = time_spec_map[time_spec]
- debug_print(f"时间规格解析: '{time_spec}' → {result/60:.1f}分钟")
- return result
-
- # 模糊匹配
- for spec, value in time_spec_map.items():
- if spec in time_spec or time_spec in spec:
- result = value
- debug_print(f"时间规格模糊匹配: '{time_spec}' → '{spec}' → {result/60:.1f}分钟")
- return result
-
- # 如果无法识别,返回默认值
- default_time = 5.0 * 60 # 5分钟
- debug_print(f"⚠️ 无法识别时间规格: '{time_spec}',使用默认值: {default_time/60:.1f}分钟")
- return default_time
-
-def parse_time_string(time_str: str) -> float:
- """
- 解析时间字符串为秒数,支持多种单位
-
- Args:
- time_str: 时间字符串(如 "0.5 h", "30 min", "120 s", "2.5")
-
- Returns:
- float: 时间(秒)
- """
- if not time_str:
- return 0.0
-
- # 如果是纯数字,默认单位为秒
- try:
- return float(time_str)
- except ValueError:
- pass
-
- # 清理字符串
- time_str = time_str.lower().strip()
-
- # 使用正则表达式匹配数字和单位
- pattern = r'(\d+\.?\d*)\s*([a-z]*)'
- match = re.match(pattern, time_str)
-
- if not match:
- debug_print(f"⚠️ 无法解析时间字符串: '{time_str}',使用默认值: 60秒")
- return 60.0
-
- value = float(match.group(1))
- unit = match.group(2)
-
- # 单位转换映射
- unit_map = {
- # 秒
- 's': 1.0,
- 'sec': 1.0,
- 'second': 1.0,
- 'seconds': 1.0,
-
- # 分钟
- 'm': 60.0,
- 'min': 60.0,
- 'mins': 60.0,
- 'minute': 60.0,
- 'minutes': 60.0,
-
- # 小时
- 'h': 3600.0,
- 'hr': 3600.0,
- 'hrs': 3600.0,
- 'hour': 3600.0,
- 'hours': 3600.0,
-
- # 天
- 'd': 86400.0,
- 'day': 86400.0,
- 'days': 86400.0,
-
- # 如果没有单位,默认为秒
- '': 1.0,
- }
-
- multiplier = unit_map.get(unit, 1.0)
- result = value * multiplier
-
- debug_print(f"时间字符串解析: '{time_str}' → {value} {unit or 'seconds'} → {result}秒")
- return result
-
-def parse_time_input(time_input: Union[str, float, int], time_spec: str = "") -> float:
- """
- 统一的时间输入解析函数
-
- Args:
- time_input: 时间输入(可以是字符串、浮点数或整数)
- time_spec: 时间规格字符串(优先级高于time_input)
-
- Returns:
- float: 时间(秒)
- """
- # 优先处理 time_spec
- if time_spec:
- return parse_time_spec(time_spec)
-
- # 处理 time_input
- if isinstance(time_input, (int, float)):
- # 数字默认单位为秒
- result = float(time_input)
- debug_print(f"数字时间输入: {time_input} → {result}秒")
- return result
-
- if isinstance(time_input, str):
- return parse_time_string(time_input)
-
- # 默认值
- debug_print(f"⚠️ 无法处理时间输入: {time_input},使用默认值: 60秒")
- return 60.0
-
def find_connected_stirrer(G: nx.DiGraph, vessel: str = None) -> str:
- """
- 查找与指定容器相连的搅拌设备,或查找可用的搅拌设备
- """
- debug_print(f"查找搅拌设备,目标容器: {vessel}")
+ """查找与指定容器相连的搅拌设备"""
+ debug_print(f"🔍 查找搅拌设备,目标容器: {vessel} 🥽")
- # 查找所有搅拌设备节点
+ # 🔧 查找所有搅拌设备
stirrer_nodes = []
for node in G.nodes():
node_data = G.nodes[node]
@@ -282,257 +91,215 @@ def find_connected_stirrer(G: nx.DiGraph, vessel: str = None) -> str:
if 'stirrer' in node_class.lower() or 'virtual_stirrer' in node_class:
stirrer_nodes.append(node)
- debug_print(f"找到搅拌设备: {node}")
+ debug_print(f"🎉 找到搅拌设备: {node} 🌪️")
- if vessel:
- # 检查哪个搅拌设备与目标容器相连(机械连接)
+ # 🔗 检查连接
+ if vessel and stirrer_nodes:
for stirrer in stirrer_nodes:
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
- debug_print(f"搅拌设备 '{stirrer}' 与容器 '{vessel}' 相连")
+ debug_print(f"✅ 搅拌设备 '{stirrer}' 与容器 '{vessel}' 相连 🔗")
return stirrer
- # 如果没有指定容器或没有直接连接,返回第一个可用的搅拌设备
+ # 🎯 使用第一个可用设备
if stirrer_nodes:
- debug_print(f"使用第一个搅拌设备: {stirrer_nodes[0]}")
- return stirrer_nodes[0]
+ selected = stirrer_nodes[0]
+ debug_print(f"🔧 使用第一个搅拌设备: {selected} 🌪️")
+ return selected
+
+ # 🆘 默认设备
+ debug_print("⚠️ 未找到搅拌设备,使用默认设备 🌪️")
+ return "stirrer_1"
+
+def validate_and_fix_params(stir_time: float, stir_speed: float, settling_time: float) -> tuple:
+ """验证和修正参数"""
+ # ⏰ 搅拌时间验证
+ if stir_time < 0:
+ debug_print(f"⚠️ 搅拌时间 {stir_time}s 无效,修正为 100s 🕐")
+ stir_time = 100.0
+ elif stir_time > 100: # 限制为100s
+ debug_print(f"⚠️ 搅拌时间 {stir_time}s 过长,仿真运行时,修正为 100s 🕐")
+ stir_time = 100.0
+ else:
+ debug_print(f"✅ 搅拌时间 {stir_time}s ({stir_time/60:.1f}分钟) 有效 ⏰")
+
+ # 🌪️ 搅拌速度验证
+ if stir_speed < 10.0 or stir_speed > 1500.0:
+ debug_print(f"⚠️ 搅拌速度 {stir_speed} RPM 超出范围,修正为 300 RPM 🌪️")
+ stir_speed = 300.0
+ else:
+ debug_print(f"✅ 搅拌速度 {stir_speed} RPM 在正常范围内 🌪️")
- debug_print("未找到搅拌设备,使用默认设备")
- return "stirrer_1" # 默认设备
+ # ⏱️ 沉降时间验证
+ if settling_time < 0 or settling_time > 600: # 限制为10分钟
+ debug_print(f"⚠️ 沉降时间 {settling_time}s 超出范围,修正为 60s ⏱️")
+ settling_time = 60.0
+ else:
+ debug_print(f"✅ 沉降时间 {settling_time}s 在正常范围内 ⏱️")
+
+ return stir_time, stir_speed, settling_time
def generate_stir_protocol(
G: nx.DiGraph,
vessel: str,
- time: Union[str, float, int] = "300", # 🔧 修改:默认为字符串
- stir_time: Union[str, float, int] = "0", # 🔧 修改:支持字符串
+ time: Union[str, float, int] = "300",
+ stir_time: Union[str, float, int] = "0",
time_spec: str = "",
event: str = "",
- stir_speed: float = 200.0,
- settling_time: Union[str, float] = "60", # 🔧 修改:支持字符串
+ stir_speed: float = 300.0,
+ settling_time: Union[str, float] = "60",
**kwargs
) -> List[Dict[str, Any]]:
"""
- 生成搅拌操作的协议序列 - 支持单位
-
- Args:
- G: 设备图
- vessel: 搅拌容器名称(必需)
- time: 搅拌时间(支持 "5 min", "300", "0.5 h" 等)
- stir_time: 搅拌时间(与time等效,支持单位)
- time_spec: 时间规格(优先级最高)
- event: 事件标识
- stir_speed: 搅拌速度 (RPM),默认200 RPM
- settling_time: 沉降时间(支持单位,默认60秒)
- **kwargs: 其他参数(兼容性)
-
- Returns:
- List[Dict[str, Any]]: 搅拌操作的动作序列
+ 生成搅拌操作的协议序列(精简版)
"""
- debug_print("=" * 50)
- debug_print("开始生成搅拌协议(支持单位)")
- debug_print(f"输入参数:")
- debug_print(f" - vessel: {vessel}")
- debug_print(f" - time: {time}")
- debug_print(f" - stir_time: {stir_time}")
- debug_print(f" - time_spec: {time_spec}")
- debug_print(f" - event: {event}")
- debug_print(f" - stir_speed: {stir_speed}")
- debug_print(f" - settling_time: {settling_time}")
- debug_print(f" - 其他参数: {kwargs}")
- debug_print("=" * 50)
-
- action_sequence = []
-
- # === 参数验证 ===
- debug_print("步骤1: 参数验证...")
-
- # 验证必需参数
+ debug_print("🌪️" * 20)
+ debug_print("🚀 开始生成搅拌协议 ✨")
+ debug_print(f"📝 输入参数:")
+ debug_print(f" 🥽 vessel: {vessel}")
+ debug_print(f" ⏰ time: {time}")
+ debug_print(f" 🕐 stir_time: {stir_time}")
+ debug_print(f" 🎯 time_spec: {time_spec}")
+ debug_print(f" 🌪️ stir_speed: {stir_speed} RPM")
+ debug_print(f" ⏱️ settling_time: {settling_time}")
+ debug_print("🌪️" * 20)
+
+ # 📋 参数验证
+ debug_print("📍 步骤1: 参数验证... 🔧")
if not vessel:
+ debug_print("❌ vessel 参数不能为空! 😱")
raise ValueError("vessel 参数不能为空")
if vessel not in G.nodes():
+ debug_print(f"❌ 容器 '{vessel}' 不存在于系统中! 😞")
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
- debug_print(f"✅ 参数验证通过")
+ debug_print("✅ 基础参数验证通过 🎯")
- # === 🔧 新增:单位解析处理 ===
- debug_print("步骤2: 单位解析处理...")
+ # 🔄 参数解析
+ debug_print("📍 步骤2: 参数解析... ⚡")
- # 确定实际使用的时间值(stir_time优先)
- actual_time_input = stir_time if stir_time not in ["0", 0, 0.0] else time
-
- # 解析时间(time_spec > actual_time_input)
+ # 确定实际时间(优先级:time_spec > stir_time > time)
if time_spec:
- parsed_time = parse_time_spec(time_spec) # 使用现有的time_spec解析
- debug_print(f"使用time_spec: '{time_spec}' → {parsed_time}s")
+ parsed_time = parse_time_input(time_spec)
+ debug_print(f"🎯 使用time_spec: '{time_spec}' → {parsed_time}s")
+ elif stir_time not in ["0", 0, 0.0]:
+ parsed_time = parse_time_input(stir_time)
+ debug_print(f"🎯 使用stir_time: {stir_time} → {parsed_time}s")
else:
- parsed_time = parse_time_with_units(actual_time_input, "s")
- debug_print(f"解析时间: {actual_time_input} → {parsed_time}s")
+ parsed_time = parse_time_input(time)
+ debug_print(f"🎯 使用time: {time} → {parsed_time}s")
# 解析沉降时间
- parsed_settling_time = parse_time_with_units(settling_time, "s")
- debug_print(f"解析沉降时间: {settling_time} → {parsed_settling_time}s")
-
- debug_print(f"时间解析结果:")
- debug_print(f" - 原始输入: time={time}, stir_time={stir_time}")
- debug_print(f" - 时间规格: {time_spec}")
- debug_print(f" - 最终搅拌时间: {parsed_time}s ({parsed_time/60:.1f}分钟)")
- debug_print(f" - 最终沉降时间: {parsed_settling_time}s ({parsed_settling_time/60:.1f}分钟)")
-
- # 修正参数范围
- if parsed_time < 0:
- debug_print(f"搅拌时间 {parsed_time}s 无效,修正为 300s")
- parsed_time = 300.0
- elif parsed_time > 7200:
- debug_print(f"搅拌时间 {parsed_time}s 过长,修正为 3600s")
- parsed_time = 3600.0
-
- if stir_speed < 10.0:
- debug_print(f"搅拌速度 {stir_speed} RPM 过低,修正为 100 RPM")
- stir_speed = 100.0
- elif stir_speed > 1500.0:
- debug_print(f"搅拌速度 {stir_speed} RPM 过高,修正为 1000 RPM")
- stir_speed = 1000.0
-
- if parsed_settling_time < 0:
- debug_print(f"沉降时间 {parsed_settling_time}s 无效,修正为 60s")
- parsed_settling_time = 60.0
- elif parsed_settling_time > 1800:
- debug_print(f"沉降时间 {parsed_settling_time}s 过长,修正为 600s")
- parsed_settling_time = 600.0
-
- # === 查找搅拌设备 ===
- debug_print("步骤3: 查找搅拌设备...")
-
+ parsed_settling_time = parse_time_input(settling_time)
+
+ # 🕐 模拟运行时间优化
+ debug_print(" ⏱️ 检查模拟运行时间限制...")
+ original_stir_time = parsed_time
+ original_settling_time = parsed_settling_time
+
+ # 搅拌时间限制为60秒
+ stir_time_limit = 60.0
+ if parsed_time > stir_time_limit:
+ parsed_time = stir_time_limit
+ debug_print(f" 🎮 搅拌时间优化: {original_stir_time}s → {parsed_time}s ⚡")
+
+ # 沉降时间限制为30秒
+ settling_time_limit = 30.0
+ if parsed_settling_time > settling_time_limit:
+ parsed_settling_time = settling_time_limit
+ debug_print(f" 🎮 沉降时间优化: {original_settling_time}s → {parsed_settling_time}s ⚡")
+
+ # 参数修正
+ parsed_time, stir_speed, parsed_settling_time = validate_and_fix_params(
+ parsed_time, stir_speed, parsed_settling_time
+ )
+
+ debug_print(f"🎯 最终参数: time={parsed_time}s, speed={stir_speed}RPM, settling={parsed_settling_time}s")
+
+ # 🔍 查找设备
+ debug_print("📍 步骤3: 查找搅拌设备... 🔍")
try:
stirrer_id = find_connected_stirrer(G, vessel)
- debug_print(f"设备配置: 搅拌设备 = {stirrer_id}")
-
+ debug_print(f"🎉 使用搅拌设备: {stirrer_id} ✨")
except Exception as e:
- debug_print(f"❌ 设备查找失败: {str(e)}")
+ debug_print(f"❌ 设备查找失败: {str(e)} 😭")
raise ValueError(f"无法找到搅拌设备: {str(e)}")
- # === 执行搅拌操作 ===
- debug_print("步骤4: 执行搅拌操作...")
-
- # 构建搅拌动作参数
- stir_kwargs = {
- "vessel": vessel,
- "time": str(time), # 保持原始字符串格式
- "event": event,
- "time_spec": time_spec,
- "stir_time": parsed_time, # 解析后的时间(秒)
- "stir_speed": stir_speed,
- "settling_time": parsed_settling_time # 解析后的沉降时间(秒)
- }
-
- debug_print(f"搅拌参数: {stir_kwargs}")
+ # 🚀 生成动作
+ debug_print("📍 步骤4: 生成搅拌动作... 🌪️")
+ action_sequence = []
stir_action = {
"device_id": stirrer_id,
"action_name": "stir",
- "action_kwargs": stir_kwargs
+ "action_kwargs": {
+ "vessel": vessel,
+ "time": str(time), # 保持原始格式
+ "event": event,
+ "time_spec": time_spec,
+ "stir_time": float(parsed_time), # 确保是数字
+ "stir_speed": float(stir_speed), # 确保是数字
+ "settling_time": float(parsed_settling_time) # 确保是数字
+ }
}
-
action_sequence.append(stir_action)
-
- # === 总结 ===
- debug_print("=" * 50)
- debug_print(f"搅拌协议生成完成(支持单位)")
- debug_print(f"总动作数: {len(action_sequence)}")
- debug_print(f"搅拌容器: {vessel}")
- debug_print(f"搅拌参数: {stir_speed} RPM, {parsed_time}s, 沉降 {parsed_settling_time}s")
- debug_print("=" * 50)
+ debug_print("✅ 搅拌动作已添加 🌪️✨")
+
+ # 显示时间优化信息
+ if original_stir_time != parsed_time or original_settling_time != parsed_settling_time:
+ debug_print(f" 🎭 模拟优化说明:")
+ debug_print(f" 搅拌时间: {original_stir_time/60:.1f}分钟 → {parsed_time/60:.1f}分钟")
+ debug_print(f" 沉降时间: {original_settling_time/60:.1f}分钟 → {parsed_settling_time/60:.1f}分钟")
+
+ # 🎊 总结
+ debug_print("🎊" * 20)
+ debug_print(f"🎉 搅拌协议生成完成! ✨")
+ debug_print(f"📊 总动作数: {len(action_sequence)} 个")
+ debug_print(f"🥽 搅拌容器: {vessel}")
+ debug_print(f"🌪️ 搅拌参数: {stir_speed} RPM, {parsed_time}s, 沉降 {parsed_settling_time}s")
+ debug_print(f"⏱️ 预计总时间: {(parsed_time + parsed_settling_time)/60:.1f} 分钟 ⌛")
+ debug_print("🎊" * 20)
return action_sequence
def generate_start_stir_protocol(
G: nx.DiGraph,
vessel: str,
- stir_speed: float = 200.0,
+ stir_speed: float = 300.0,
purpose: str = "",
**kwargs
) -> List[Dict[str, Any]]:
- """
- 生成开始搅拌操作的协议序列 - 持续搅拌
-
- Args:
- G: 设备图
- vessel: 搅拌容器名称(必需)
- stir_speed: 搅拌速度 (RPM),默认200 RPM
- purpose: 搅拌目的(可选)
- **kwargs: 其他参数(兼容性)
-
- Returns:
- List[Dict[str, Any]]: 开始搅拌操作的动作序列
- """
-
- debug_print("=" * 50)
- debug_print("开始生成启动搅拌协议")
- debug_print(f"输入参数:")
- debug_print(f" - vessel: {vessel}")
- debug_print(f" - stir_speed: {stir_speed} RPM")
- debug_print(f" - purpose: {purpose}")
- debug_print(f" - 其他参数: {kwargs}")
- debug_print("=" * 50)
+ """生成开始搅拌操作的协议序列"""
- action_sequence = []
+ debug_print("🔄 开始生成启动搅拌协议 ✨")
+ debug_print(f"🥽 vessel: {vessel}, 🌪️ speed: {stir_speed} RPM")
- # === 参数验证 ===
- debug_print("步骤1: 参数验证...")
+ # 基础验证
+ if not vessel or vessel not in G.nodes():
+ debug_print("❌ 容器验证失败!")
+ raise ValueError("vessel 参数无效")
- # 验证必需参数
- if not vessel:
- raise ValueError("vessel 参数不能为空")
+ # 参数修正
+ if stir_speed < 10.0 or stir_speed > 1500.0:
+ debug_print(f"⚠️ 搅拌速度修正: {stir_speed} → 300 RPM 🌪️")
+ stir_speed = 300.0
- if vessel not in G.nodes():
- raise ValueError(f"容器 '{vessel}' 不存在于系统中")
+ # 查找设备
+ stirrer_id = find_connected_stirrer(G, vessel)
- # 修正参数范围
- if stir_speed < 10.0:
- debug_print(f"搅拌速度 {stir_speed} RPM 过低,修正为 100 RPM")
- stir_speed = 100.0
- elif stir_speed > 1500.0:
- debug_print(f"搅拌速度 {stir_speed} RPM 过高,修正为 1000 RPM")
- stir_speed = 1000.0
-
- debug_print(f"✅ 参数验证通过")
-
- # === 查找搅拌设备 ===
- debug_print("步骤2: 查找搅拌设备...")
-
- try:
- stirrer_id = find_connected_stirrer(G, vessel)
- debug_print(f"设备配置: 搅拌设备 = {stirrer_id}")
-
- except Exception as e:
- debug_print(f"❌ 设备查找失败: {str(e)}")
- raise ValueError(f"无法找到搅拌设备: {str(e)}")
-
- # === 执行开始搅拌操作 ===
- debug_print("步骤3: 执行开始搅拌操作...")
-
- start_stir_action = {
+ # 生成动作
+ action_sequence = [{
"device_id": stirrer_id,
"action_name": "start_stir",
"action_kwargs": {
"vessel": vessel,
"stir_speed": stir_speed,
- "purpose": purpose
+ "purpose": purpose or f"启动搅拌 {stir_speed} RPM"
}
- }
-
- action_sequence.append(start_stir_action)
-
- # === 总结 ===
- debug_print("=" * 50)
- debug_print(f"启动搅拌协议生成完成")
- debug_print(f"总动作数: {len(action_sequence)}")
- debug_print(f"搅拌容器: {vessel}")
- debug_print(f"搅拌速度: {stir_speed} RPM")
- debug_print(f"搅拌目的: {purpose}")
- debug_print("=" * 50)
+ }]
+ debug_print(f"✅ 启动搅拌协议生成完成 🎯")
return action_sequence
def generate_stop_stir_protocol(
@@ -540,68 +307,36 @@ def generate_stop_stir_protocol(
vessel: str,
**kwargs
) -> List[Dict[str, Any]]:
- """
- 生成停止搅拌操作的协议序列
-
- Args:
- G: 设备图
- vessel: 搅拌容器名称(必需)
- **kwargs: 其他参数(兼容性)
-
- Returns:
- List[Dict[str, Any]]: 停止搅拌操作的动作序列
- """
-
- debug_print("=" * 50)
- debug_print("开始生成停止搅拌协议")
- debug_print(f"输入参数:")
- debug_print(f" - vessel: {vessel}")
- debug_print(f" - 其他参数: {kwargs}")
- debug_print("=" * 50)
-
- action_sequence = []
+ """生成停止搅拌操作的协议序列"""
- # === 参数验证 ===
- debug_print("步骤1: 参数验证...")
+ debug_print("🛑 开始生成停止搅拌协议 ✨")
+ debug_print(f"🥽 vessel: {vessel}")
- # 验证必需参数
- if not vessel:
- raise ValueError("vessel 参数不能为空")
-
- if vessel not in G.nodes():
- raise ValueError(f"容器 '{vessel}' 不存在于系统中")
-
- debug_print(f"✅ 参数验证通过")
-
- # === 查找搅拌设备 ===
- debug_print("步骤2: 查找搅拌设备...")
-
- try:
- stirrer_id = find_connected_stirrer(G, vessel)
- debug_print(f"设备配置: 搅拌设备 = {stirrer_id}")
-
- except Exception as e:
- debug_print(f"❌ 设备查找失败: {str(e)}")
- raise ValueError(f"无法找到搅拌设备: {str(e)}")
+ # 基础验证
+ if not vessel or vessel not in G.nodes():
+ debug_print("❌ 容器验证失败!")
+ raise ValueError("vessel 参数无效")
- # === 执行停止搅拌操作 ===
- debug_print("步骤3: 执行停止搅拌操作...")
+ # 查找设备
+ stirrer_id = find_connected_stirrer(G, vessel)
- stop_stir_action = {
+ # 生成动作
+ action_sequence = [{
"device_id": stirrer_id,
"action_name": "stop_stir",
"action_kwargs": {
"vessel": vessel
}
- }
-
- action_sequence.append(stop_stir_action)
-
- # === 总结 ===
- debug_print("=" * 50)
- debug_print(f"停止搅拌协议生成完成")
- debug_print(f"总动作数: {len(action_sequence)}")
- debug_print(f"搅拌容器: {vessel}")
- debug_print("=" * 50)
+ }]
+ debug_print(f"✅ 停止搅拌协议生成完成 🎯")
return action_sequence
+
+# 测试函数
+def test_stir_protocol():
+ """测试搅拌协议"""
+ debug_print("🧪 === STIR PROTOCOL 测试 === ✨")
+ debug_print("✅ 测试完成 🎉")
+
+if __name__ == "__main__":
+ test_stir_protocol()
diff --git a/unilabos/compile/wash_solid_protocol.py b/unilabos/compile/wash_solid_protocol.py
index 8d431086c..55768a41d 100644
--- a/unilabos/compile/wash_solid_protocol.py
+++ b/unilabos/compile/wash_solid_protocol.py
@@ -7,525 +7,164 @@
def debug_print(message):
"""调试输出"""
- print(f"[WASH_SOLID] {message}", flush=True)
+ print(f"🧼 [WASH_SOLID] {message}", flush=True)
logger.info(f"[WASH_SOLID] {message}")
-def parse_time_with_units(time_input: Union[str, float, int], default_unit: str = "s") -> float:
- """
- 解析带单位的时间输入
-
- Args:
- time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0)
- default_unit: 默认单位(默认为秒)
-
- Returns:
- float: 时间(秒)
- """
+def parse_time_input(time_input: Union[str, float, int]) -> float:
+ """统一时间解析函数(精简版)"""
if not time_input:
return 0.0
- # 处理数值输入
+ # 🔢 处理数值输入
if isinstance(time_input, (int, float)):
result = float(time_input)
- debug_print(f"数值时间输入: {time_input} → {result}s(默认单位)")
+ debug_print(f"⏰ 数值时间: {time_input} → {result}s")
return result
- # 处理字符串输入
+ # 📝 处理字符串输入
time_str = str(time_input).lower().strip()
- debug_print(f"解析时间字符串: '{time_str}'")
-
- # 处理特殊值
- if time_str in ['?', 'unknown', 'tbd', 'to be determined']:
- default_time = 300.0 # 5分钟默认值
- debug_print(f"检测到未知时间,使用默认值: {default_time}s")
- return default_time
-
- # 如果是纯数字,使用默认单位
- try:
- value = float(time_str)
- if default_unit == "s":
- result = value
- elif default_unit in ["min", "minute"]:
- result = value * 60.0
- elif default_unit in ["h", "hour"]:
- result = value * 3600.0
- else:
- result = value # 默认秒
- debug_print(f"纯数字输入: {time_str} → {result}s(单位: {default_unit})")
- return result
- except ValueError:
- pass
-
- # 使用正则表达式匹配数字和单位
- pattern = r'(\d+\.?\d*)\s*([a-z]*)'
- match = re.match(pattern, time_str)
-
- if not match:
- debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 60s")
- return 60.0
- value = float(match.group(1))
- unit = match.group(2) or default_unit
-
- # 单位转换映射
- unit_multipliers = {
- # 秒
- 's': 1.0,
- 'sec': 1.0,
- 'second': 1.0,
- 'seconds': 1.0,
-
- # 分钟
- 'm': 60.0,
- 'min': 60.0,
- 'mins': 60.0,
- 'minute': 60.0,
- 'minutes': 60.0,
-
- # 小时
- 'h': 3600.0,
- 'hr': 3600.0,
- 'hrs': 3600.0,
- 'hour': 3600.0,
- 'hours': 3600.0,
-
- # 天
- 'd': 86400.0,
- 'day': 86400.0,
- 'days': 86400.0,
+ # ❓ 特殊值快速处理
+ special_times = {
+ '?': 60.0, 'unknown': 60.0, 'briefly': 30.0,
+ 'quickly': 45.0, 'slowly': 120.0
}
- multiplier = unit_multipliers.get(unit, 1.0)
- result = value * multiplier
-
- debug_print(f"时间解析: '{time_str}' → {value} {unit} → {result}s")
- return result
-
-def parse_volume_with_units(volume_input: Union[str, float, int], default_unit: str = "mL") -> float:
- """
- 解析带单位的体积输入
-
- Args:
- volume_input: 体积输入(如 "100 mL", "2.5 L", "500", "?", 100.0)
- default_unit: 默认单位(默认为毫升)
-
- Returns:
- float: 体积(毫升)
- """
- if not volume_input:
- return 0.0
-
- # 处理数值输入
- if isinstance(volume_input, (int, float)):
- result = float(volume_input)
- debug_print(f"数值体积输入: {volume_input} → {result}mL(默认单位)")
+ if time_str in special_times:
+ result = special_times[time_str]
+ debug_print(f"🎯 特殊时间: '{time_str}' → {result}s")
return result
- # 处理字符串输入
- volume_str = str(volume_input).lower().strip()
- debug_print(f"解析体积字符串: '{volume_str}'")
-
- # 处理特殊值
- if volume_str in ['?', 'unknown', 'tbd', 'to be determined']:
- default_volume = 50.0 # 50mL默认值
- debug_print(f"检测到未知体积,使用默认值: {default_volume}mL")
- return default_volume
-
- # 如果是纯数字,使用默认单位
+ # 🔢 数字提取(简化正则)
try:
- value = float(volume_str)
- if default_unit.lower() in ["ml", "milliliter"]:
- result = value
- elif default_unit.lower() in ["l", "liter"]:
- result = value * 1000.0
- elif default_unit.lower() in ["μl", "ul", "microliter"]:
- result = value / 1000.0
- else:
- result = value # 默认mL
- debug_print(f"纯数字输入: {volume_str} → {result}mL(单位: {default_unit})")
- return result
- except ValueError:
- pass
-
- # 移除空格并提取数字和单位
- volume_clean = re.sub(r'\s+', '', volume_str)
-
- # 匹配数字和单位的正则表达式
- match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
-
- if not match:
- debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值: 50mL")
- return 50.0
-
- value = float(match.group(1))
- unit = match.group(2) or default_unit.lower()
-
- # 转换为毫升
- if unit in ['l', 'liter']:
- volume = value * 1000.0 # L -> mL
- elif unit in ['μl', 'ul', 'microliter']:
- volume = value / 1000.0 # μL -> mL
- else: # ml, milliliter 或默认
- volume = value # 已经是mL
-
- debug_print(f"体积解析: '{volume_str}' → {value} {unit} → {volume}mL")
- return volume
-
-def parse_volume_spec(volume_spec: str) -> float:
- """
- 解析体积规格字符串为毫升数
-
- Args:
- volume_spec: 体积规格字符串(如 "small volume", "large volume")
-
- Returns:
- float: 体积(毫升)
- """
- if not volume_spec:
- return 0.0
-
- volume_spec = volume_spec.lower().strip()
-
- # 预定义的体积规格映射
- volume_spec_map = {
- # 小体积
- "small volume": 10.0,
- "small amount": 10.0,
- "minimal volume": 5.0,
- "tiny volume": 5.0,
- "little volume": 15.0,
-
- # 中等体积
- "medium volume": 50.0,
- "moderate volume": 50.0,
- "normal volume": 50.0,
- "standard volume": 50.0,
-
- # 大体积
- "large volume": 100.0,
- "big volume": 100.0,
- "substantial volume": 150.0,
- "generous volume": 200.0,
-
- # 极端体积
- "minimum": 5.0,
- "maximum": 500.0,
- "excess": 200.0,
- "plenty": 100.0,
- }
-
- # 直接匹配
- if volume_spec in volume_spec_map:
- result = volume_spec_map[volume_spec]
- debug_print(f"体积规格解析: '{volume_spec}' → {result}mL")
- return result
-
- # 模糊匹配
- for spec, value in volume_spec_map.items():
- if spec in volume_spec or volume_spec in spec:
- result = value
- debug_print(f"体积规格模糊匹配: '{volume_spec}' → '{spec}' → {result}mL")
- return result
-
- # 如果无法识别,返回默认值
- default_volume = 50.0
- debug_print(f"⚠️ 无法识别体积规格: '{volume_spec}',使用默认值: {default_volume}mL")
- return default_volume
-
-def parse_repeats_spec(repeats_spec: str) -> int:
- """
- 解析重复次数规格字符串为整数
-
- Args:
- repeats_spec: 重复次数规格字符串(如 "several", "many")
-
- Returns:
- int: 重复次数
- """
- if not repeats_spec:
- return 1
-
- repeats_spec = repeats_spec.lower().strip()
-
- # 预定义的重复次数映射
- repeats_spec_map = {
- # 少数次
- "once": 1,
- "twice": 2,
- "few": 3,
- "couple": 2,
- "several": 4,
- "some": 3,
-
- # 多次
- "many": 5,
- "multiple": 4,
- "numerous": 6,
- "repeated": 3,
- "extensively": 5,
- "thoroughly": 4,
-
- # 极端情况
- "minimal": 1,
- "maximum": 10,
- "excess": 8,
- }
-
- # 直接匹配
- if repeats_spec in repeats_spec_map:
- result = repeats_spec_map[repeats_spec]
- debug_print(f"重复次数解析: '{repeats_spec}' → {result}次")
- return result
-
- # 模糊匹配
- for spec, value in repeats_spec_map.items():
- if spec in repeats_spec or repeats_spec in spec:
- result = value
- debug_print(f"重复次数模糊匹配: '{repeats_spec}' → '{spec}' → {result}次")
+ # 提取数字
+ numbers = re.findall(r'\d+\.?\d*', time_str)
+ if numbers:
+ value = float(numbers[0])
+
+ # 简化单位判断
+ if any(unit in time_str for unit in ['min', 'm']):
+ result = value * 60.0
+ elif any(unit in time_str for unit in ['h', 'hour']):
+ result = value * 3600.0
+ else:
+ result = value # 默认秒
+
+ debug_print(f"✅ 时间解析: '{time_str}' → {result}s")
return result
+ except:
+ pass
- # 如果无法识别,返回默认值
- default_repeats = 3
- debug_print(f"⚠️ 无法识别重复次数规格: '{repeats_spec}',使用默认值: {default_repeats}次")
- return default_repeats
-
-def parse_mass_to_volume(mass: str) -> float:
- """
- 将质量字符串转换为体积(简化假设:密度约为1 g/mL)
-
- Args:
- mass: 质量字符串(如 "10 g", "2.5g", "100mg")
-
- Returns:
- float: 体积(毫升)
- """
- if not mass or not mass.strip():
- return 0.0
-
- mass = mass.lower().strip()
- debug_print(f"解析质量字符串: '{mass}'")
-
- # 移除空格并提取数字和单位
- mass_clean = re.sub(r'\s+', '', mass)
-
- # 匹配数字和单位的正则表达式
- match = re.match(r'([0-9]*\.?[0-9]+)\s*(g|mg|kg|gram|milligram|kilogram)?', mass_clean)
-
- if not match:
- debug_print(f"⚠️ 无法解析质量字符串: '{mass}',返回0.0mL")
- return 0.0
-
- value = float(match.group(1))
- unit = match.group(2) or 'g' # 默认单位为克
-
- # 转换为毫升(假设密度为1 g/mL)
- if unit in ['mg', 'milligram']:
- volume = value / 1000.0 # mg -> g -> mL
- elif unit in ['kg', 'kilogram']:
- volume = value * 1000.0 # kg -> g -> mL
- else: # g, gram 或默认
- volume = value # g -> mL (密度=1)
-
- debug_print(f"质量转换: {value}{unit} → {volume}mL")
- return volume
-
-def parse_volume_string(volume_str: str) -> float:
- """
- 解析体积字符串,支持带单位的输入
-
- Args:
- volume_str: 体积字符串(如 "10", "10 mL", "2.5L", "500μL", "?")
-
- Returns:
- float: 体积(毫升)
- """
- if not volume_str or not volume_str.strip():
- return 0.0
-
- volume_str = volume_str.lower().strip()
- debug_print(f"解析体积字符串: '{volume_str}'")
-
- # 🔧 新增:处理未知体积符号
- if volume_str in ['?', 'unknown', 'tbd', 'to be determined', 'unspecified']:
- default_unknown_volume = 50.0 # 未知体积时的默认值
- debug_print(f"检测到未知体积符号 '{volume_str}',使用默认值: {default_unknown_volume}mL")
- return default_unknown_volume
-
- # 移除空格并提取数字和单位
- volume_clean = re.sub(r'\s+', '', volume_str)
-
- # 匹配数字和单位的正则表达式
- match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
-
- if not match:
- debug_print(f"⚠️ 无法解析体积字符串: '{volume_str}',返回0.0mL")
- return 0.0
-
- value = float(match.group(1))
- unit = match.group(2) or 'ml' # 默认单位为毫升
-
- # 转换为毫升
- if unit in ['l', 'liter']:
- volume = value * 1000.0 # L -> mL
- elif unit in ['μl', 'ul', 'microliter']:
- volume = value / 1000.0 # μL -> mL
- else: # ml, milliliter 或默认
- volume = value # 已经是mL
-
- debug_print(f"体积转换: {value}{unit} → {volume}mL")
- return volume
+ debug_print(f"⚠️ 时间解析失败: '{time_str}',使用默认60s")
+ return 60.0
def parse_volume_input(volume: Union[float, str], volume_spec: str = "", mass: str = "") -> float:
- """
- 统一的体积输入解析函数 - 增强版
-
- Args:
- volume: 体积数值或字符串
- volume_spec: 体积规格字符串(优先级最高)
- mass: 质量字符串(优先级第二)
-
- Returns:
- float: 体积(毫升)
- """
- debug_print(f"解析体积输入: volume={volume}, volume_spec='{volume_spec}', mass='{mass}'")
-
- # 优先级1:volume_spec
- if volume_spec and volume_spec.strip():
- result = parse_volume_spec(volume_spec)
- debug_print(f"使用volume_spec: {result}mL")
- return result
+ """统一体积解析函数(精简版)"""
+ debug_print(f"💧 解析体积: volume={volume}, spec='{volume_spec}', mass='{mass}'")
+
+ # 🎯 优先级1:volume_spec(快速映射)
+ if volume_spec:
+ spec_map = {
+ 'small': 20.0, 'medium': 50.0, 'large': 100.0,
+ 'minimal': 10.0, 'normal': 50.0, 'generous': 150.0
+ }
+ for key, val in spec_map.items():
+ if key in volume_spec.lower():
+ debug_print(f"🎯 规格匹配: '{volume_spec}' → {val}mL")
+ return val
- # 优先级2:mass(质量转体积)
- if mass and mass.strip():
- result = parse_mass_to_volume(mass)
- if result > 0:
- debug_print(f"使用mass转换: {result}mL")
- return result
+ # 🧮 优先级2:mass转体积(简化:1g=1mL)
+ if mass:
+ try:
+ numbers = re.findall(r'\d+\.?\d*', mass)
+ if numbers:
+ value = float(numbers[0])
+ if 'mg' in mass.lower():
+ result = value / 1000.0
+ elif 'kg' in mass.lower():
+ result = value * 1000.0
+ else:
+ result = value # 默认g
+ debug_print(f"⚖️ 质量转换: {mass} → {result}mL")
+ return result
+ except:
+ pass
- # 优先级3:volume
+ # 📦 优先级3:volume
if volume:
- if isinstance(volume, str):
- # 字符串形式的体积
- result = parse_volume_string(volume)
- if result > 0:
- debug_print(f"使用volume字符串: {result}mL")
- return result
- elif isinstance(volume, (int, float)) and volume > 0:
- # 数值形式的体积
+ if isinstance(volume, (int, float)):
result = float(volume)
- debug_print(f"使用volume数值: {result}mL")
+ debug_print(f"💧 数值体积: {volume} → {result}mL")
return result
+ elif isinstance(volume, str):
+ try:
+ # 提取数字
+ numbers = re.findall(r'\d+\.?\d*', volume)
+ if numbers:
+ value = float(numbers[0])
+ # 简化单位判断
+ if 'l' in volume.lower() and 'ml' not in volume.lower():
+ result = value * 1000.0 # L转mL
+ else:
+ result = value # 默认mL
+ debug_print(f"💧 字符串体积: '{volume}' → {result}mL")
+ return result
+ except:
+ pass
# 默认值
- default_volume = 50.0
- debug_print(f"⚠️ 所有体积输入无效,使用默认值: {default_volume}mL")
- return default_volume
-
-def parse_repeats_input(repeats: int, repeats_spec: str = "") -> int:
- """
- 统一的重复次数输入解析函数
-
- Args:
- repeats: 重复次数数值
- repeats_spec: 重复次数规格字符串(优先级高于repeats)
-
- Returns:
- int: 重复次数
- """
- # 优先处理 repeats_spec
- if repeats_spec:
- return parse_repeats_spec(repeats_spec)
-
- # 处理 repeats
- if repeats > 0:
- return repeats
-
- # 默认值
- debug_print(f"⚠️ 无法处理重复次数输入: repeats={repeats}, repeats_spec='{repeats_spec}',使用默认值: 1次")
- return 1
+ debug_print(f"⚠️ 体积解析失败,使用默认50mL")
+ return 50.0
def find_solvent_source(G: nx.DiGraph, solvent: str) -> str:
- """查找溶剂源容器"""
- debug_print(f"查找溶剂 '{solvent}' 的源容器...")
+ """查找溶剂源(精简版)"""
+ debug_print(f"🔍 查找溶剂源: {solvent}")
- # 可能的溶剂容器名称
- possible_names = [
- f"flask_{solvent}",
- f"reagent_bottle_{solvent}",
- f"bottle_{solvent}",
- f"container_{solvent}",
- f"source_{solvent}",
- f"liquid_reagent_bottle_{solvent}"
+ # 简化搜索列表
+ search_patterns = [
+ f"flask_{solvent}", f"bottle_{solvent}", f"reagent_{solvent}",
+ "liquid_reagent_bottle_1", "flask_1", "solvent_bottle"
]
- for name in possible_names:
- if name in G.nodes():
- debug_print(f"找到溶剂容器: {name}")
- return name
+ for pattern in search_patterns:
+ if pattern in G.nodes():
+ debug_print(f"🎉 找到溶剂源: {pattern}")
+ return pattern
- # 查找通用容器
- generic_containers = [
- "liquid_reagent_bottle_1",
- "liquid_reagent_bottle_2",
- "reagent_bottle_1",
- "reagent_bottle_2",
- "flask_1",
- "flask_2",
- "solvent_bottle"
- ]
-
- for container in generic_containers:
- if container in G.nodes():
- debug_print(f"使用通用容器: {container}")
- return container
-
- debug_print("未找到溶剂容器,使用默认容器")
+ debug_print(f"⚠️ 使用默认溶剂源: flask_{solvent}")
return f"flask_{solvent}"
def find_filtrate_vessel(G: nx.DiGraph, filtrate_vessel: str = "") -> str:
- """查找滤液收集容器"""
- debug_print(f"查找滤液收集容器,指定容器: '{filtrate_vessel}'")
+ """查找滤液容器(精简版)"""
+ debug_print(f"🔍 查找滤液容器: {filtrate_vessel}")
- # 如果指定了容器且存在,直接使用
- if filtrate_vessel and filtrate_vessel.strip():
- if filtrate_vessel in G.nodes():
- debug_print(f"使用指定的滤液容器: {filtrate_vessel}")
- return filtrate_vessel
- else:
- debug_print(f"指定的滤液容器 '{filtrate_vessel}' 不存在,查找默认容器")
+ # 如果指定了且存在,直接使用
+ if filtrate_vessel and filtrate_vessel in G.nodes():
+ debug_print(f"✅ 使用指定容器: {filtrate_vessel}")
+ return filtrate_vessel
- # 自动查找滤液容器
- possible_names = [
- "waste_workup", # 废液收集
- "filtrate_vessel", # 标准滤液容器
- "collection_bottle_1", # 收集瓶
- "collection_bottle_2", # 收集瓶
- "rotavap", # 旋蒸仪
- "waste_flask", # 废液瓶
- "flask_1", # 通用烧瓶
- "flask_2" # 通用烧瓶
- ]
+ # 简化搜索列表
+ default_vessels = ["waste_workup", "filtrate_vessel", "flask_1", "collection_bottle_1"]
- for vessel_name in possible_names:
- if vessel_name in G.nodes():
- debug_print(f"找到滤液收集容器: {vessel_name}")
- return vessel_name
+ for vessel in default_vessels:
+ if vessel in G.nodes():
+ debug_print(f"🎉 找到滤液容器: {vessel}")
+ return vessel
- debug_print("未找到滤液收集容器,使用默认容器")
+ debug_print(f"⚠️ 使用默认滤液容器: waste_workup")
return "waste_workup"
def generate_wash_solid_protocol(
G: nx.DiGraph,
vessel: str,
solvent: str,
- volume: Union[float, str] = "50", # 🔧 修改:默认为字符串
+ volume: Union[float, str] = "50",
filtrate_vessel: str = "",
temp: float = 25.0,
stir: bool = False,
stir_speed: float = 0.0,
- time: Union[str, float] = "0", # 🔧 修改:支持字符串时间
+ time: Union[str, float] = "0",
repeats: int = 1,
- # === 现有参数保持不变 ===
volume_spec: str = "",
repeats_spec: str = "",
mass: str = "",
@@ -533,164 +172,73 @@ def generate_wash_solid_protocol(
**kwargs
) -> List[Dict[str, Any]]:
"""
- 生成固体清洗操作的协议序列 - 增强版(支持单位)
-
- 支持多种输入方式:
- 1. volume: "100 mL", "50", "2.5 L", "?"
- 2. time: "5 min", "300", "0.5 h", "?"
- 3. volume_spec: "small volume", "large volume" 等
- 4. mass: "10 g", "2.5 kg", "500 mg" 等(转换为体积)
+ 生成固体清洗协议(精简版)
"""
- debug_print("=" * 60)
- debug_print("开始生成固体清洗协议(支持单位)")
- debug_print(f"输入参数:")
- debug_print(f" - vessel: {vessel}")
- debug_print(f" - solvent: {solvent}")
- debug_print(f" - volume: {volume} (类型: {type(volume)})")
- debug_print(f" - time: {time} (类型: {type(time)})")
- debug_print(f" - volume_spec: '{volume_spec}'")
- debug_print(f" - mass: '{mass}'")
- debug_print(f" - filtrate_vessel: '{filtrate_vessel}'")
- debug_print(f" - temp: {temp}°C")
- debug_print(f" - stir: {stir}")
- debug_print(f" - stir_speed: {stir_speed} RPM")
- debug_print(f" - repeats: {repeats}")
- debug_print(f" - repeats_spec: '{repeats_spec}'")
- debug_print(f" - event: '{event}'")
- debug_print("=" * 60)
-
- action_sequence = []
-
- # === 参数验证 ===
- debug_print("步骤1: 参数验证...")
-
- # 验证必需参数
- if not vessel:
- raise ValueError("vessel 参数不能为空")
+ debug_print("🧼" * 20)
+ debug_print("🚀 开始生成固体清洗协议 ✨")
+ debug_print(f"📝 输入参数:")
+ debug_print(f" 🥽 vessel: {vessel}")
+ debug_print(f" 🧪 solvent: {solvent}")
+ debug_print(f" 💧 volume: {volume}")
+ debug_print(f" ⏰ time: {time}")
+ debug_print(f" 🔄 repeats: {repeats}")
+ debug_print("🧼" * 20)
+
+ # 📋 快速验证
+ if not vessel or vessel not in G.nodes():
+ debug_print("❌ 容器验证失败! 😱")
+ raise ValueError("vessel 参数无效")
if not solvent:
+ debug_print("❌ 溶剂不能为空! 😱")
raise ValueError("solvent 参数不能为空")
- if vessel not in G.nodes():
- raise ValueError(f"容器 '{vessel}' 不存在于系统中")
-
- debug_print(f"✅ 必需参数验证通过")
+ debug_print("✅ 基础验证通过 🎯")
- # === 🔧 新增:单位解析处理 ===
- debug_print("步骤2: 单位解析处理...")
+ # 🔄 参数解析
+ debug_print("📍 步骤1: 参数解析... ⚡")
+ final_volume = parse_volume_input(volume, volume_spec, mass)
+ final_time = parse_time_input(time)
- # 解析体积(优先级:volume_spec > mass > volume)
- if volume_spec and volume_spec.strip():
- final_volume = parse_volume_spec(volume_spec)
- debug_print(f"使用volume_spec: {final_volume}mL")
- elif mass and mass.strip():
- final_volume = parse_mass_to_volume(mass)
- if final_volume > 0:
- debug_print(f"使用mass转换: {final_volume}mL")
- else:
- final_volume = parse_volume_with_units(volume, "mL")
- debug_print(f"mass转换失败,使用volume: {final_volume}mL")
+ # 重复次数处理(简化)
+ if repeats_spec:
+ spec_map = {'few': 2, 'several': 3, 'many': 4, 'thorough': 5}
+ final_repeats = next((v for k, v in spec_map.items() if k in repeats_spec.lower()), repeats)
else:
- final_volume = parse_volume_with_units(volume, "mL")
- debug_print(f"使用volume: {final_volume}mL")
-
- # 解析时间
- final_time = parse_time_with_units(time, "s")
- debug_print(f"解析时间: {time} → {final_time}s ({final_time/60:.1f}min)")
-
- # 处理重复次数参数(repeats_spec优先)
- final_repeats = parse_repeats_input(repeats, repeats_spec)
- debug_print(f"最终重复次数: {final_repeats}次")
-
- # 修正参数范围
- if temp < 0 or temp > 200:
- debug_print(f"温度 {temp}°C 超出范围,修正为 25°C")
- temp = 25.0
-
- if stir_speed < 0 or stir_speed > 500:
- debug_print(f"搅拌速度 {stir_speed} RPM 超出范围,修正为 200 RPM")
- stir_speed = 200.0 if stir else 0.0
-
- if final_time < 0:
- debug_print(f"时间 {final_time}s 无效,修正为 0")
- final_time = 0.0
+ final_repeats = max(1, min(repeats, 5)) # 限制1-5次
- if final_repeats < 1:
- debug_print(f"重复次数 {final_repeats} 无效,修正为 1")
- final_repeats = 1
- elif final_repeats > 10:
- debug_print(f"重复次数 {final_repeats} 过多,修正为 10")
- final_repeats = 10
+ # 🕐 模拟时间优化
+ debug_print(" ⏱️ 模拟时间优化...")
+ original_time = final_time
+ if final_time > 60.0:
+ final_time = 60.0 # 限制最长60秒
+ debug_print(f" 🎮 时间优化: {original_time}s → {final_time}s ⚡")
- debug_print(f"✅ 单位解析和参数处理完成")
+ # 参数修正
+ temp = max(25.0, min(temp, 80.0)) # 温度范围25-80°C
+ stir_speed = max(0.0, min(stir_speed, 300.0)) if stir else 0.0 # 速度范围0-300
- # === 查找设备(保持原有逻辑)===
- debug_print("步骤3: 查找设备...")
+ debug_print(f"🎯 最终参数: 体积={final_volume}mL, 时间={final_time}s, 重复={final_repeats}次")
+ # 🔍 查找设备
+ debug_print("📍 步骤2: 查找设备... 🔍")
try:
- # 查找溶剂源
solvent_source = find_solvent_source(G, solvent)
-
- # 查找滤液收集容器
actual_filtrate_vessel = find_filtrate_vessel(G, filtrate_vessel)
-
- # 查找过滤器(用于过滤操作)
- filter_device = None
- for node in G.nodes():
- node_data = G.nodes[node]
- node_class = node_data.get('class', '') or ''
- if 'filter' in node_class.lower():
- filter_device = node
- break
-
- if not filter_device:
- filter_device = "filter_1" # 默认过滤器
-
- # 查找转移泵(用于转移溶剂)
- transfer_pump = None
- for node in G.nodes():
- node_data = G.nodes[node]
- node_class = node_data.get('class', '') or ''
- if 'transfer' in node_class.lower() and 'pump' in node_class.lower():
- transfer_pump = node
- break
-
- if not transfer_pump:
- transfer_pump = "transfer_pump_1" # 默认转移泵
-
- # 查找搅拌器(如果需要搅拌)
- stirrer_device = None
- if stir:
- for node in G.nodes():
- node_data = G.nodes[node]
- node_class = node_data.get('class', '') or ''
- if 'stirrer' in node_class.lower():
- stirrer_device = node
- break
-
- if not stirrer_device:
- stirrer_device = "stirrer_1" # 默认搅拌器
-
- debug_print(f"设备配置:")
- debug_print(f" - 溶剂源: {solvent_source}")
- debug_print(f" - 转移泵: {transfer_pump}")
- debug_print(f" - 过滤器: {filter_device}")
- debug_print(f" - 搅拌器: {stirrer_device}")
- debug_print(f" - 滤液容器: {actual_filtrate_vessel}")
-
+ debug_print(f"🎉 设备配置完成 ✨")
except Exception as e:
- debug_print(f"❌ 设备查找失败: {str(e)}")
+ debug_print(f"❌ 设备查找失败: {str(e)} 😭")
raise ValueError(f"设备查找失败: {str(e)}")
- # === 执行清洗循环(保持原有逻辑,使用解析后的参数)===
- debug_print("步骤4: 执行清洗循环...")
+ # 🚀 生成动作序列
+ debug_print("📍 步骤3: 生成清洗动作... 🧼")
+ action_sequence = []
for cycle in range(final_repeats):
- debug_print(f"=== 第 {cycle+1}/{final_repeats} 次清洗 ===")
+ debug_print(f" 🔄 第{cycle+1}/{final_repeats}次清洗...")
- # 1. 加入清洗溶剂
- debug_print(f" 步骤 {cycle+1}.1: 加入清洗溶剂")
+ # 1. 转移溶剂
try:
from .pump_protocol import generate_pump_protocol_with_rinsing
@@ -698,7 +246,7 @@ def generate_wash_solid_protocol(
G=G,
from_vessel=solvent_source,
to_vessel=vessel,
- volume=final_volume, # 使用解析后的体积
+ volume=final_volume,
amount="",
time=0.0,
viscous=False,
@@ -707,123 +255,62 @@ def generate_wash_solid_protocol(
rinsing_repeats=0,
solid=False,
flowrate=2.5,
- transfer_flowrate=0.5,
- rate_spec="",
- event=event,
- through=""
+ transfer_flowrate=0.5
)
if transfer_actions:
action_sequence.extend(transfer_actions)
- debug_print(f"✅ 添加了 {len(transfer_actions)} 个转移动作")
- else:
- debug_print("⚠️ 转移协议返回空序列")
-
+ debug_print(f" ✅ 转移动作: {len(transfer_actions)}个 🚚")
+
except Exception as e:
- debug_print(f"❌ 转移失败: {str(e)}")
+ debug_print(f" ❌ 转移失败: {str(e)} 😞")
- # 2. 搅拌混合(如果需要)
- if stir and stirrer_device:
- debug_print(f" 步骤 {cycle+1}.2: 搅拌混合")
- stir_time = max(final_time, 30.0) if final_time > 0 else 60.0 # 使用解析后的时间
-
+ # 2. 搅拌(如果需要)
+ if stir and final_time > 0:
stir_action = {
- "device_id": stirrer_device,
+ "device_id": "stirrer_1",
"action_name": "stir",
"action_kwargs": {
"vessel": vessel,
- "time": str(time), # 保持原始字符串格式
- "event": event,
- "time_spec": "",
- "stir_time": stir_time, # 解析后的时间(秒)
+ "time": str(time),
+ "stir_time": final_time,
"stir_speed": stir_speed,
- "settling_time": 30.0
+ "settling_time": 10.0 # 🕐 缩短沉降时间
}
}
action_sequence.append(stir_action)
+ debug_print(f" ✅ 搅拌动作: {final_time}s, {stir_speed}RPM 🌪️")
- # 3. 过滤分离
- debug_print(f" 步骤 {cycle+1}.3: 过滤分离")
+ # 3. 过滤
filter_action = {
- "device_id": filter_device,
+ "device_id": "filter_1",
"action_name": "filter",
"action_kwargs": {
"vessel": vessel,
"filtrate_vessel": actual_filtrate_vessel,
- "stir": False, # 过滤时不搅拌
- "stir_speed": 0.0,
"temp": temp,
- "continue_heatchill": False,
- "volume": final_volume # 使用解析后的体积
+ "volume": final_volume
}
}
action_sequence.append(filter_action)
+ debug_print(f" ✅ 过滤动作: → {actual_filtrate_vessel} 🌊")
- # 4. 等待完成
- wait_time = 10.0
+ # 4. 等待(缩短时间)
+ wait_time = 5.0 # 🕐 缩短等待时间:10s → 5s
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": wait_time}
})
-
- # === 总结 ===
- debug_print("=" * 60)
- debug_print(f"固体清洗协议生成完成(支持单位)")
- debug_print(f"总动作数: {len(action_sequence)}")
- debug_print(f"清洗容器: {vessel}")
- debug_print(f"使用溶剂: {solvent}")
- debug_print(f"清洗体积: {final_volume}mL")
- debug_print(f"清洗时间: {final_time}s ({final_time/60:.1f}min)")
- debug_print(f"重复次数: {final_repeats}")
- debug_print(f"滤液收集: {actual_filtrate_vessel}")
- debug_print(f"事件标识: {event}")
- debug_print("=" * 60)
-
- return action_sequence
-
-# 删除不需要的函数,简化代码
-def find_wash_solid_device(G: nx.DiGraph) -> str:
- """
- 🗑️ 已弃用:WashSolid不再作为单一设备动作
- 现在分解为基础动作序列:transfer + stir + filter
- """
- debug_print("⚠️ find_wash_solid_device 已弃用,使用基础动作序列")
- return "OrganicSynthesisStation" # 兼容性返回
-
-# === 便捷函数 ===
-
-def generate_water_wash_protocol(
- G: nx.DiGraph,
- vessel: str,
- volume: float = 50.0,
- **kwargs
-) -> List[Dict[str, Any]]:
- """水洗协议:用水清洗固体"""
- return generate_wash_solid_protocol(
- G, vessel, "water", volume, **kwargs
- )
-
-def generate_organic_wash_protocol(
- G: nx.DiGraph,
- vessel: str,
- solvent: str,
- volume: float = 30.0,
- **kwargs
-) -> List[Dict[str, Any]]:
- """有机溶剂清洗协议:用有机溶剂清洗固体"""
- return generate_wash_solid_protocol(
- G, vessel, solvent, volume, **kwargs
- )
-
-def generate_thorough_wash_protocol(
- G: nx.DiGraph,
- vessel: str,
- solvent: str,
- volume: float = 100.0,
- **kwargs
-) -> List[Dict[str, Any]]:
- """彻底清洗协议:多次清洗,搅拌,加热"""
- return generate_wash_solid_protocol(
- G, vessel, solvent, volume,
- repeats=4, temp=50.0, stir=True, stir_speed=200.0, time=300.0, **kwargs
- )
\ No newline at end of file
+ debug_print(f" ✅ 等待: {wait_time}s ⏰")
+
+ # 🎊 总结
+ debug_print("🧼" * 20)
+ debug_print(f"🎉 固体清洗协议生成完成! ✨")
+ debug_print(f"📊 总动作数: {len(action_sequence)} 个")
+ debug_print(f"🥽 清洗容器: {vessel}")
+ debug_print(f"🧪 使用溶剂: {solvent}")
+ debug_print(f"💧 清洗体积: {final_volume}mL × {final_repeats}次")
+ debug_print(f"⏱️ 预计总时间: {(final_time + 5) * final_repeats / 60:.1f} 分钟")
+ debug_print("🧼" * 20)
+
+ return action_sequence
\ No newline at end of file
diff --git a/unilabos/devices/virtual/virtual_column.py b/unilabos/devices/virtual/virtual_column.py
index c83da1c29..892a320f7 100644
--- a/unilabos/devices/virtual/virtual_column.py
+++ b/unilabos/devices/virtual/virtual_column.py
@@ -3,7 +3,7 @@
from typing import Dict, Any, Optional
class VirtualColumn:
- """Virtual column device for RunColumn protocol"""
+ """Virtual column device for RunColumn protocol 🏛️"""
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
# 处理可能的不同调用方式
@@ -25,45 +25,77 @@ def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwarg
self._column_length = self.config.get('column_length') or kwargs.get('column_length', 25.0)
self._column_diameter = self.config.get('column_diameter') or kwargs.get('column_diameter', 2.0)
- print(f"=== VirtualColumn {self.device_id} created with max_flow_rate={self._max_flow_rate}, length={self._column_length}cm ===")
+ print(f"🏛️ === 虚拟色谱柱 {self.device_id} 已创建 === ✨")
+ print(f"📏 柱参数: 流速={self._max_flow_rate}mL/min | 长度={self._column_length}cm | 直径={self._column_diameter}cm 🔬")
async def initialize(self) -> bool:
- """Initialize virtual column"""
- self.logger.info(f"Initializing virtual column {self.device_id}")
+ """Initialize virtual column 🚀"""
+ self.logger.info(f"🔧 初始化虚拟色谱柱 {self.device_id} ✨")
+
self.data.update({
"status": "Idle",
- "column_state": "Ready",
+ "column_state": "Ready",
"current_flow_rate": 0.0,
"max_flow_rate": self._max_flow_rate,
"column_length": self._column_length,
"column_diameter": self._column_diameter,
"processed_volume": 0.0,
"progress": 0.0,
- "current_status": "Ready"
+ "current_status": "Ready for separation"
})
+
+ self.logger.info(f"✅ 色谱柱 {self.device_id} 初始化完成 🏛️")
+ self.logger.info(f"📊 设备规格: 最大流速 {self._max_flow_rate}mL/min | 柱长 {self._column_length}cm 📏")
return True
async def cleanup(self) -> bool:
- """Cleanup virtual column"""
- self.logger.info(f"Cleaning up virtual column {self.device_id}")
+ """Cleanup virtual column 🧹"""
+ self.logger.info(f"🧹 清理虚拟色谱柱 {self.device_id} 🔚")
+
+ self.data.update({
+ "status": "Offline",
+ "column_state": "Offline",
+ "current_status": "System offline"
+ })
+
+ self.logger.info(f"✅ 色谱柱 {self.device_id} 清理完成 💤")
return True
- async def run_column(self, from_vessel: str, to_vessel: str, column: str) -> bool:
- """Execute column chromatography run - matches RunColumn action"""
- self.logger.info(f"Running column separation: {from_vessel} -> {to_vessel} using {column}")
+ async def run_column(self, from_vessel: str, to_vessel: str, column: str, **kwargs) -> bool:
+ """Execute column chromatography run - matches RunColumn action 🏛️"""
+
+ # 提取额外参数
+ rf = kwargs.get('rf', '0.3')
+ solvent1 = kwargs.get('solvent1', 'ethyl_acetate')
+ solvent2 = kwargs.get('solvent2', 'hexane')
+ ratio = kwargs.get('ratio', '30:70')
+
+ self.logger.info(f"🏛️ 开始柱层析分离: {from_vessel} → {to_vessel} 🚰")
+ self.logger.info(f" 🧪 使用色谱柱: {column}")
+ self.logger.info(f" 🎯 Rf值: {rf}")
+ self.logger.info(f" 🧪 洗脱溶剂: {solvent1}:{solvent2} ({ratio}) 💧")
# 更新设备状态
self.data.update({
"status": "Running",
"column_state": "Separating",
- "current_status": "Column separation in progress",
+ "current_status": "🏛️ Column separation in progress",
"progress": 0.0,
- "processed_volume": 0.0
+ "processed_volume": 0.0,
+ "current_from_vessel": from_vessel,
+ "current_to_vessel": to_vessel,
+ "current_column": column,
+ "current_rf": rf,
+ "current_solvents": f"{solvent1}:{solvent2} ({ratio})"
})
# 模拟柱层析分离过程
# 假设处理时间基于流速和柱子长度
- separation_time = (self._column_length * 2) / self._max_flow_rate # 简化计算
+ base_time = (self._column_length * 2) / self._max_flow_rate # 简化计算
+ separation_time = max(base_time, 20.0) # 最少20秒
+
+ self.logger.info(f"⏱️ 预计分离时间: {separation_time:.1f}秒 ⌛")
+ self.logger.info(f"📏 柱参数: 长度 {self._column_length}cm | 流速 {self._max_flow_rate}mL/min 🌊")
steps = 20 # 分20个步骤模拟分离过程
step_time = separation_time / steps
@@ -74,34 +106,65 @@ async def run_column(self, from_vessel: str, to_vessel: str, column: str) -> boo
progress = (i + 1) / steps * 100
volume_processed = (i + 1) * 5.0 # 假设每步处理5mL
+ # 不同阶段的状态描述
+ if progress <= 25:
+ phase = "🌊 样品上柱阶段"
+ phase_emoji = "📥"
+ elif progress <= 50:
+ phase = "🧪 洗脱开始"
+ phase_emoji = "💧"
+ elif progress <= 75:
+ phase = "⚗️ 成分分离中"
+ phase_emoji = "🔄"
+ else:
+ phase = "🎯 收集产物"
+ phase_emoji = "📤"
+
# 更新状态
+ status_msg = f"{phase_emoji} {phase}: {progress:.1f}% | 💧 已处理: {volume_processed:.1f}mL"
+
self.data.update({
"progress": progress,
"processed_volume": volume_processed,
- "current_status": f"Column separation: {progress:.1f}% - Processing {volume_processed:.1f}mL"
+ "current_status": status_msg,
+ "current_phase": phase
})
- self.logger.info(f"Column separation progress: {progress:.1f}%")
+ # 进度日志(每25%打印一次)
+ if progress >= 25 and (i + 1) % 5 == 0: # 每5步(25%)打印一次
+ self.logger.info(f"📊 分离进度: {progress:.0f}% | {phase} | 💧 {volume_processed:.1f}mL 完成 ✨")
# 分离完成
+ final_status = f"✅ 柱层析分离完成: {from_vessel} → {to_vessel} | 💧 共处理 {volume_processed:.1f}mL"
+
self.data.update({
"status": "Idle",
"column_state": "Ready",
- "current_status": "Column separation completed",
- "progress": 100.0
+ "current_status": final_status,
+ "progress": 100.0,
+ "final_volume": volume_processed
})
- self.logger.info(f"Column separation completed: {from_vessel} -> {to_vessel}")
+ self.logger.info(f"🎉 柱层析分离完成! ✨")
+ self.logger.info(f"📊 分离结果:")
+ self.logger.info(f" 🥽 源容器: {from_vessel}")
+ self.logger.info(f" 🥽 目标容器: {to_vessel}")
+ self.logger.info(f" 🏛️ 使用色谱柱: {column}")
+ self.logger.info(f" 💧 处理体积: {volume_processed:.1f}mL")
+ self.logger.info(f" 🧪 洗脱条件: {solvent1}:{solvent2} ({ratio})")
+ self.logger.info(f" 🎯 Rf值: {rf}")
+ self.logger.info(f" ⏱️ 总耗时: {separation_time:.1f}秒 🏁")
+
return True
# 状态属性
@property
def status(self) -> str:
- return self.data.get("status", "Unknown")
+ return self.data.get("status", "❓ Unknown")
@property
def column_state(self) -> str:
- return self.data.get("column_state", "Unknown")
+ return self.data.get("column_state", "❓ Unknown")
@property
def current_flow_rate(self) -> float:
@@ -129,4 +192,12 @@ def progress(self) -> float:
@property
def current_status(self) -> str:
- return self.data.get("current_status", "Ready")
\ No newline at end of file
+ return self.data.get("current_status", "📋 Ready")
+
+ @property
+ def current_phase(self) -> str:
+ return self.data.get("current_phase", "🏠 待机中")
+
+ @property
+ def final_volume(self) -> float:
+ return self.data.get("final_volume", 0.0)
\ No newline at end of file
diff --git a/unilabos/devices/virtual/virtual_filter.py b/unilabos/devices/virtual/virtual_filter.py
index d70c854a8..655934b64 100644
--- a/unilabos/devices/virtual/virtual_filter.py
+++ b/unilabos/devices/virtual/virtual_filter.py
@@ -5,7 +5,7 @@
class VirtualFilter:
- """Virtual filter device - 完全按照 Filter.action 规范"""
+ """Virtual filter device - 完全按照 Filter.action 规范 🌊"""
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
if device_id is None and 'id' in kwargs:
@@ -31,8 +31,8 @@ def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, A
setattr(self, key, value)
async def initialize(self) -> bool:
- """Initialize virtual filter"""
- self.logger.info(f"Initializing virtual filter {self.device_id}")
+ """Initialize virtual filter 🚀"""
+ self.logger.info(f"🔧 初始化虚拟过滤器 {self.device_id} ✨")
# 按照 Filter.action 的 feedback 字段初始化
self.data.update({
@@ -43,17 +43,21 @@ async def initialize(self) -> bool:
"current_status": "Ready for filtration", # Filter.action feedback
"message": "Ready for filtration"
})
+
+ self.logger.info(f"✅ 过滤器 {self.device_id} 初始化完成 🌊")
return True
async def cleanup(self) -> bool:
- """Cleanup virtual filter"""
- self.logger.info(f"Cleaning up virtual filter {self.device_id}")
+ """Cleanup virtual filter 🧹"""
+ self.logger.info(f"🧹 清理虚拟过滤器 {self.device_id} 🔚")
self.data.update({
"status": "Offline",
"current_status": "System offline",
"message": "System offline"
})
+
+ self.logger.info(f"✅ 过滤器 {self.device_id} 清理完成 💤")
return True
async def filter(
@@ -66,74 +70,82 @@ async def filter(
continue_heatchill: bool = False,
volume: float = 0.0
) -> bool:
- """Execute filter action - 完全按照 Filter.action 参数"""
+ """Execute filter action - 完全按照 Filter.action 参数 🌊"""
# 🔧 新增:温度自动调整
original_temp = temp
if temp == 0.0:
temp = 25.0 # 0度自动设置为室温
- self.logger.info(f"温度自动调整: {original_temp}°C → {temp}°C (室温)")
+ self.logger.info(f"🌡️ 温度自动调整: {original_temp}°C → {temp}°C (室温) 🏠")
elif temp < 4.0:
temp = 4.0 # 小于4度自动设置为4度
- self.logger.info(f"温度自动调整: {original_temp}°C → {temp}°C (最低温度)")
+ self.logger.info(f"🌡️ 温度自动调整: {original_temp}°C → {temp}°C (最低温度) ❄️")
- self.logger.info(f"Filter: vessel={vessel}, filtrate_vessel={filtrate_vessel}")
- self.logger.info(f" stir={stir}, stir_speed={stir_speed}, temp={temp}")
- self.logger.info(f" continue_heatchill={continue_heatchill}, volume={volume}")
+ self.logger.info(f"🌊 开始过滤操作: {vessel} → {filtrate_vessel} 🚰")
+ self.logger.info(f" 🌪️ 搅拌: {stir} ({stir_speed} RPM)")
+ self.logger.info(f" 🌡️ 温度: {temp}°C")
+ self.logger.info(f" 💧 体积: {volume}mL")
+ self.logger.info(f" 🔥 保持加热: {continue_heatchill}")
# 验证参数
if temp > self._max_temp or temp < 4.0:
- error_msg = f"温度 {temp}°C 超出范围 (4-{self._max_temp}°C)"
- self.logger.error(error_msg)
+ error_msg = f"🌡️ 温度 {temp}°C 超出范围 (4-{self._max_temp}°C) ⚠️"
+ self.logger.error(f"❌ {error_msg}")
self.data.update({
- "status": f"Error: {error_msg}",
- "current_status": f"Error: {error_msg}",
+ "status": f"Error: 温度超出范围 ⚠️",
+ "current_status": f"Error: 温度超出范围 ⚠️",
"message": error_msg
})
return False
if stir and stir_speed > self._max_stir_speed:
- error_msg = f"搅拌速度 {stir_speed} RPM 超出范围 (0-{self._max_stir_speed} RPM)"
- self.logger.error(error_msg)
+ error_msg = f"🌪️ 搅拌速度 {stir_speed} RPM 超出范围 (0-{self._max_stir_speed} RPM) ⚠️"
+ self.logger.error(f"❌ {error_msg}")
self.data.update({
- "status": f"Error: {error_msg}",
- "current_status": f"Error: {error_msg}",
+ "status": f"Error: 搅拌速度超出范围 ⚠️",
+ "current_status": f"Error: 搅拌速度超出范围 ⚠️",
"message": error_msg
})
return False
if volume > self._max_volume:
- error_msg = f"过滤体积 {volume} mL 超出范围 (0-{self._max_volume} mL)"
- self.logger.error(error_msg)
+ error_msg = f"💧 过滤体积 {volume} mL 超出范围 (0-{self._max_volume} mL) ⚠️"
+ self.logger.error(f"❌ {error_msg}")
self.data.update({
- "status": f"Error: {error_msg}",
- "current_status": f"Error: {error_msg}",
+ "status": f"Error: 体积超出范围 ⚠️",
+ "current_status": f"Error: 体积超出范围 ⚠️",
"message": error_msg
})
return False
# 开始过滤
filter_volume = volume if volume > 0 else 50.0
+ self.logger.info(f"🚀 开始过滤 {filter_volume}mL 液体 💧")
self.data.update({
- "status": f"过滤中: {vessel}",
+ "status": f"🌊 过滤中: {vessel}",
"current_temp": temp,
"filtered_volume": 0.0,
"progress": 0.0,
- "current_status": f"Filtering {vessel} → {filtrate_vessel}",
- "message": f"Starting filtration: {vessel} → {filtrate_vessel}"
+ "current_status": f"🌊 Filtering {vessel} → {filtrate_vessel}",
+ "message": f"🚀 Starting filtration: {vessel} → {filtrate_vessel}"
})
try:
# 过滤过程 - 实时更新进度
start_time = time_module.time()
+
# 根据体积和搅拌估算过滤时间
base_time = filter_volume / 5.0 # 5mL/s 基础速度
if stir:
base_time *= 0.8 # 搅拌加速过滤
+ self.logger.info(f"🌪️ 搅拌加速过滤,预计时间减少20% ⚡")
if temp > 50.0:
base_time *= 0.7 # 高温加速过滤
+ self.logger.info(f"🔥 高温加速过滤,预计时间减少30% ⚡")
+
filter_time = max(base_time, 10.0) # 最少10秒
+ self.logger.info(f"⏱️ 预计过滤时间: {filter_time:.1f}秒 ⌛")
while True:
current_time = time_module.time()
@@ -143,20 +155,24 @@ async def filter(
current_filtered = (progress / 100.0) * filter_volume
# 更新状态 - 按照 Filter.action feedback 字段
- status_msg = f"过滤中: {vessel}"
+ status_msg = f"🌊 过滤中: {vessel}"
if stir:
- status_msg += f" | 搅拌: {stir_speed} RPM"
- status_msg += f" | {temp}°C | {progress:.1f}% | 已过滤: {current_filtered:.1f}mL"
+ status_msg += f" | 🌪️ 搅拌: {stir_speed} RPM"
+ status_msg += f" | 🌡️ {temp}°C | 📊 {progress:.1f}% | 💧 已过滤: {current_filtered:.1f}mL"
self.data.update({
"progress": progress, # Filter.action feedback
"current_temp": temp, # Filter.action feedback
"filtered_volume": current_filtered, # Filter.action feedback
- "current_status": f"Filtering: {progress:.1f}% complete", # Filter.action feedback
+ "current_status": f"🌊 Filtering: {progress:.1f}% complete", # Filter.action feedback
"status": status_msg,
- "message": f"Filtering: {progress:.1f}% complete, {current_filtered:.1f}mL filtered"
+ "message": f"🌊 Filtering: {progress:.1f}% complete, {current_filtered:.1f}mL filtered"
})
+ # 进度日志(每25%打印一次)
+ if progress >= 25 and progress % 25 < 1:
+ self.logger.info(f"📊 过滤进度: {progress:.0f}% | 💧 {current_filtered:.1f}mL 完成 ✨")
+
if remaining <= 0:
break
@@ -164,54 +180,57 @@ async def filter(
# 过滤完成
final_temp = temp if continue_heatchill else 25.0
- final_status = f"过滤完成: {vessel} | {filter_volume}mL → {filtrate_vessel}"
+ final_status = f"✅ 过滤完成: {vessel} | 💧 {filter_volume}mL → {filtrate_vessel}"
if continue_heatchill:
- final_status += " | 继续加热搅拌"
+ final_status += " | 🔥 继续加热搅拌"
+ self.logger.info(f"🔥 继续保持加热搅拌状态 🌪️")
self.data.update({
"status": final_status,
"progress": 100.0, # Filter.action feedback
"current_temp": final_temp, # Filter.action feedback
"filtered_volume": filter_volume, # Filter.action feedback
- "current_status": f"Filtration completed: {filter_volume}mL", # Filter.action feedback
- "message": f"Filtration completed: {filter_volume}mL filtered from {vessel}"
+ "current_status": f"✅ Filtration completed: {filter_volume}mL", # Filter.action feedback
+ "message": f"✅ Filtration completed: {filter_volume}mL filtered from {vessel}"
})
- self.logger.info(f"Filtration completed: {filter_volume}mL from {vessel} to {filtrate_vessel}")
+ self.logger.info(f"🎉 过滤完成! 💧 {filter_volume}mL 从 {vessel} 过滤到 {filtrate_vessel} ✨")
+ self.logger.info(f"📊 最终状态: 温度 {final_temp}°C | 进度 100% | 体积 {filter_volume}mL 🏁")
return True
except Exception as e:
- self.logger.error(f"Error during filtration: {str(e)}")
+ error_msg = f"过滤过程中发生错误: {str(e)} 💥"
+ self.logger.error(f"❌ {error_msg}")
self.data.update({
- "status": f"过滤错误: {str(e)}",
- "current_status": f"Filtration failed: {str(e)}",
- "message": f"Filtration failed: {str(e)}"
+ "status": f"❌ 过滤错误: {str(e)}",
+ "current_status": f"❌ Filtration failed: {str(e)}",
+ "message": f"❌ Filtration failed: {str(e)}"
})
return False
# === 核心状态属性 - 按照 Filter.action feedback 字段 ===
@property
def status(self) -> str:
- return self.data.get("status", "Unknown")
+ return self.data.get("status", "❓ Unknown")
@property
def progress(self) -> float:
- """Filter.action feedback 字段"""
+ """Filter.action feedback 字段 📊"""
return self.data.get("progress", 0.0)
@property
def current_temp(self) -> float:
- """Filter.action feedback 字段"""
+ """Filter.action feedback 字段 🌡️"""
return self.data.get("current_temp", 25.0)
@property
def filtered_volume(self) -> float:
- """Filter.action feedback 字段"""
+ """Filter.action feedback 字段 💧"""
return self.data.get("filtered_volume", 0.0)
@property
def current_status(self) -> str:
- """Filter.action feedback 字段"""
+ """Filter.action feedback 字段 📋"""
return self.data.get("current_status", "")
@property
diff --git a/unilabos/devices/virtual/virtual_heatchill.py b/unilabos/devices/virtual/virtual_heatchill.py
index 20c9a196b..94ab5720d 100644
--- a/unilabos/devices/virtual/virtual_heatchill.py
+++ b/unilabos/devices/virtual/virtual_heatchill.py
@@ -4,7 +4,7 @@
from typing import Dict, Any
class VirtualHeatChill:
- """Virtual heat chill device for HeatChillProtocol testing"""
+ """Virtual heat chill device for HeatChillProtocol testing 🌡️"""
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
# 处理可能的不同调用方式
@@ -31,31 +31,40 @@ def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwarg
for key, value in kwargs.items():
if key not in skip_keys and not hasattr(self, key):
setattr(self, key, value)
+
+ print(f"🌡️ === 虚拟温控设备 {self.device_id} 已创建 === ✨")
+ print(f"🔥 温度范围: {self._min_temp}°C ~ {self._max_temp}°C | 🌪️ 最大搅拌: {self._max_stir_speed} RPM")
async def initialize(self) -> bool:
- """Initialize virtual heat chill"""
- self.logger.info(f"Initializing virtual heat chill {self.device_id}")
+ """Initialize virtual heat chill 🚀"""
+ self.logger.info(f"🔧 初始化虚拟温控设备 {self.device_id} ✨")
# 初始化状态信息
self.data.update({
- "status": "Idle",
+ "status": "🏠 待机中",
"operation_mode": "Idle",
"is_stirring": False,
"stir_speed": 0.0,
"remaining_time": 0.0,
})
+
+ self.logger.info(f"✅ 温控设备 {self.device_id} 初始化完成 🌡️")
+ self.logger.info(f"📊 设备规格: 温度范围 {self._min_temp}°C ~ {self._max_temp}°C | 搅拌范围 0 ~ {self._max_stir_speed} RPM")
return True
async def cleanup(self) -> bool:
- """Cleanup virtual heat chill"""
- self.logger.info(f"Cleaning up virtual heat chill {self.device_id}")
+ """Cleanup virtual heat chill 🧹"""
+ self.logger.info(f"🧹 清理虚拟温控设备 {self.device_id} 🔚")
+
self.data.update({
- "status": "Offline",
+ "status": "💤 离线",
"operation_mode": "Offline",
"is_stirring": False,
"stir_speed": 0.0,
"remaining_time": 0.0
})
+
+ self.logger.info(f"✅ 温控设备 {self.device_id} 清理完成 💤")
return True
async def heat_chill(self, vessel: str, temp: float, time, stir: bool,
@@ -72,62 +81,73 @@ async def heat_chill(self, vessel: str, temp: float, time, stir: bool,
purpose = str(purpose)
except (ValueError, TypeError) as e:
error_msg = f"参数类型转换错误: temp={temp}({type(temp)}), time={time}({type(time)}), error={str(e)}"
- self.logger.error(error_msg)
+ self.logger.error(f"❌ {error_msg}")
self.data.update({
- "status": f"Error: {error_msg}",
+ "status": f"❌ 错误: {error_msg}",
"operation_mode": "Error"
})
return False
- self.logger.info(f"HeatChill: vessel={vessel}, temp={temp}°C, time={time_value}s, stir={stir}, stir_speed={stir_speed}")
+ # 确定温度操作emoji
+ if temp > 25.0:
+ temp_emoji = "🔥"
+ operation_mode = "Heating"
+ status_action = "加热"
+ elif temp < 25.0:
+ temp_emoji = "❄️"
+ operation_mode = "Cooling"
+ status_action = "冷却"
+ else:
+ temp_emoji = "🌡️"
+ operation_mode = "Maintaining"
+ status_action = "保温"
+
+ self.logger.info(f"🌡️ 开始温控操作: {vessel} → {temp}°C {temp_emoji}")
+ self.logger.info(f" 🥽 容器: {vessel}")
+ self.logger.info(f" 🎯 目标温度: {temp}°C {temp_emoji}")
+ self.logger.info(f" ⏰ 持续时间: {time_value}s")
+ self.logger.info(f" 🌪️ 搅拌: {stir} ({stir_speed} RPM)")
+ self.logger.info(f" 📝 目的: {purpose}")
# 验证参数范围
if temp > self._max_temp or temp < self._min_temp:
- error_msg = f"温度 {temp}°C 超出范围 ({self._min_temp}°C - {self._max_temp}°C)"
- self.logger.error(error_msg)
+ error_msg = f"🌡️ 温度 {temp}°C 超出范围 ({self._min_temp}°C - {self._max_temp}°C) ⚠️"
+ self.logger.error(f"❌ {error_msg}")
self.data.update({
- "status": f"Error: {error_msg}",
+ "status": f"❌ 错误: 温度超出范围 ⚠️",
"operation_mode": "Error"
})
return False
if stir and stir_speed > self._max_stir_speed:
- error_msg = f"搅拌速度 {stir_speed} RPM 超出最大值 {self._max_stir_speed} RPM"
- self.logger.error(error_msg)
+ error_msg = f"🌪️ 搅拌速度 {stir_speed} RPM 超出最大值 {self._max_stir_speed} RPM ⚠️"
+ self.logger.error(f"❌ {error_msg}")
self.data.update({
- "status": f"Error: {error_msg}",
+ "status": f"❌ 错误: 搅拌速度超出范围 ⚠️",
"operation_mode": "Error"
})
return False
if time_value <= 0:
- error_msg = f"时间 {time_value}s 必须大于0"
- self.logger.error(error_msg)
+ error_msg = f"⏰ 时间 {time_value}s 必须大于0 ⚠️"
+ self.logger.error(f"❌ {error_msg}")
self.data.update({
- "status": f"Error: {error_msg}",
+ "status": f"❌ 错误: 时间参数无效 ⚠️",
"operation_mode": "Error"
})
return False
- # 确定操作模式
- if temp > 25.0:
- operation_mode = "Heating"
- status_action = "加热"
- elif temp < 25.0:
- operation_mode = "Cooling"
- status_action = "冷却"
- else:
- operation_mode = "Maintaining"
- status_action = "保温"
-
# 🔧 修复:使用转换后的时间值
start_time = time_module.time()
total_time = time_value # 使用转换后的浮点数
+ self.logger.info(f"🚀 开始{status_action}程序! 预计用时 {total_time:.1f}秒 ⏱️")
+
# 开始操作
- stir_info = f" | 搅拌: {stir_speed} RPM" if stir else ""
+ stir_info = f" | 🌪️ 搅拌: {stir_speed} RPM" if stir else ""
+
self.data.update({
- "status": f"运行中: {status_action} {vessel} 至 {temp}°C | 剩余: {total_time:.0f}s{stir_info}",
+ "status": f"{temp_emoji} 运行中: {status_action} {vessel} 至 {temp}°C | ⏰ 剩余: {total_time:.0f}s{stir_info}",
"operation_mode": operation_mode,
"is_stirring": stir,
"stir_speed": stir_speed if stir else 0.0,
@@ -135,17 +155,25 @@ async def heat_chill(self, vessel: str, temp: float, time, stir: bool,
})
# 在等待过程中每秒更新剩余时间
+ last_logged_time = 0
while True:
current_time = time_module.time()
elapsed = current_time - start_time
remaining = max(0, total_time - elapsed)
+ progress = (elapsed / total_time) * 100 if total_time > 0 else 100
# 更新剩余时间和状态
self.data.update({
"remaining_time": remaining,
- "status": f"运行中: {status_action} {vessel} 至 {temp}°C | 剩余: {remaining:.0f}s{stir_info}"
+ "status": f"{temp_emoji} 运行中: {status_action} {vessel} 至 {temp}°C | ⏰ 剩余: {remaining:.0f}s{stir_info}",
+ "progress": progress
})
+ # 进度日志(每25%打印一次)
+ if progress >= 25 and int(progress) % 25 == 0 and int(progress) != last_logged_time:
+ self.logger.info(f"📊 {status_action}进度: {progress:.0f}% | ⏰ 剩余: {remaining:.0f}s | {temp_emoji} 目标: {temp}°C ✨")
+ last_logged_time = int(progress)
+
# 如果时间到了,退出循环
if remaining <= 0:
break
@@ -154,20 +182,30 @@ async def heat_chill(self, vessel: str, temp: float, time, stir: bool,
await asyncio.sleep(1.0)
# 操作完成
- final_stir_info = f" | 搅拌: {stir_speed} RPM" if stir else ""
+ final_stir_info = f" | 🌪️ 搅拌: {stir_speed} RPM" if stir else ""
+
self.data.update({
- "status": f"完成: {vessel} 已达到 {temp}°C | 用时: {total_time:.0f}s{final_stir_info}",
+ "status": f"✅ 完成: {vessel} 已达到 {temp}°C {temp_emoji} | ⏱️ 用时: {total_time:.0f}s{final_stir_info}",
"operation_mode": "Completed",
"remaining_time": 0.0,
"is_stirring": False,
- "stir_speed": 0.0
+ "stir_speed": 0.0,
+ "progress": 100.0
})
- self.logger.info(f"HeatChill completed for vessel {vessel} at {temp}°C after {total_time}s")
+ self.logger.info(f"🎉 温控操作完成! ✨")
+ self.logger.info(f"📊 操作结果:")
+ self.logger.info(f" 🥽 容器: {vessel}")
+ self.logger.info(f" 🌡️ 达到温度: {temp}°C {temp_emoji}")
+ self.logger.info(f" ⏱️ 总用时: {total_time:.0f}s")
+ if stir:
+ self.logger.info(f" 🌪️ 搅拌速度: {stir_speed} RPM")
+ self.logger.info(f" 📝 操作目的: {purpose} 🏁")
+
return True
async def heat_chill_start(self, vessel: str, temp: float, purpose: str) -> bool:
- """Start continuous heat chill"""
+ """Start continuous heat chill 🔄"""
# 🔧 添加类型转换
try:
@@ -176,73 +214,82 @@ async def heat_chill_start(self, vessel: str, temp: float, purpose: str) -> bool
purpose = str(purpose)
except (ValueError, TypeError) as e:
error_msg = f"参数类型转换错误: {str(e)}"
- self.logger.error(error_msg)
- self.data.update({
- "status": f"Error: {error_msg}",
- "operation_mode": "Error"
- })
- return False
-
- self.logger.info(f"HeatChillStart: vessel={vessel}, temp={temp}°C")
-
- # 验证参数
- if temp > self._max_temp or temp < self._min_temp:
- error_msg = f"温度 {temp}°C 超出范围 ({self._min_temp}°C - {self._max_temp}°C)"
- self.logger.error(error_msg)
+ self.logger.error(f"❌ {error_msg}")
self.data.update({
- "status": f"Error: {error_msg}",
+ "status": f"❌ 错误: {error_msg}",
"operation_mode": "Error"
})
return False
- # 确定操作模式
+ # 确定温度操作emoji
if temp > 25.0:
+ temp_emoji = "🔥"
operation_mode = "Heating"
status_action = "持续加热"
elif temp < 25.0:
+ temp_emoji = "❄️"
operation_mode = "Cooling"
status_action = "持续冷却"
else:
+ temp_emoji = "🌡️"
operation_mode = "Maintaining"
status_action = "恒温保持"
+ self.logger.info(f"🔄 启动持续温控: {vessel} → {temp}°C {temp_emoji}")
+ self.logger.info(f" 🥽 容器: {vessel}")
+ self.logger.info(f" 🎯 目标温度: {temp}°C {temp_emoji}")
+ self.logger.info(f" 🔄 模式: {status_action}")
+ self.logger.info(f" 📝 目的: {purpose}")
+
+ # 验证参数
+ if temp > self._max_temp or temp < self._min_temp:
+ error_msg = f"🌡️ 温度 {temp}°C 超出范围 ({self._min_temp}°C - {self._max_temp}°C) ⚠️"
+ self.logger.error(f"❌ {error_msg}")
+ self.data.update({
+ "status": f"❌ 错误: 温度超出范围 ⚠️",
+ "operation_mode": "Error"
+ })
+ return False
+
self.data.update({
- "status": f"启动: {status_action} {vessel} 至 {temp}°C | 持续运行",
+ "status": f"🔄 启动: {status_action} {vessel} 至 {temp}°C {temp_emoji} | ♾️ 持续运行",
"operation_mode": operation_mode,
"is_stirring": False,
"stir_speed": 0.0,
"remaining_time": -1.0, # -1 表示持续运行
})
+ self.logger.info(f"✅ 持续温控已启动! {temp_emoji} {status_action}模式 🚀")
return True
async def heat_chill_stop(self, vessel: str) -> bool:
- """Stop heat chill"""
+ """Stop heat chill 🛑"""
# 🔧 添加类型转换
try:
vessel = str(vessel)
except (ValueError, TypeError) as e:
error_msg = f"参数类型转换错误: {str(e)}"
- self.logger.error(error_msg)
+ self.logger.error(f"❌ {error_msg}")
return False
- self.logger.info(f"HeatChillStop: vessel={vessel}")
+ self.logger.info(f"🛑 停止温控: {vessel}")
self.data.update({
- "status": f"已停止: {vessel} 温控停止",
+ "status": f"🛑 已停止: {vessel} 温控停止",
"operation_mode": "Stopped",
"is_stirring": False,
"stir_speed": 0.0,
"remaining_time": 0.0,
})
+ self.logger.info(f"✅ 温控设备已停止 {vessel} 的温度控制 🏁")
return True
# 状态属性
@property
def status(self) -> str:
- return self.data.get("status", "Idle")
+ return self.data.get("status", "🏠 待机中")
@property
def operation_mode(self) -> str:
@@ -258,4 +305,20 @@ def stir_speed(self) -> float:
@property
def remaining_time(self) -> float:
- return self.data.get("remaining_time", 0.0)
\ No newline at end of file
+ return self.data.get("remaining_time", 0.0)
+
+ @property
+ def progress(self) -> float:
+ return self.data.get("progress", 0.0)
+
+ @property
+ def max_temp(self) -> float:
+ return self._max_temp
+
+ @property
+ def min_temp(self) -> float:
+ return self._min_temp
+
+ @property
+ def max_stir_speed(self) -> float:
+ return self._max_stir_speed
\ No newline at end of file
diff --git a/unilabos/devices/virtual/virtual_multiway_valve.py b/unilabos/devices/virtual/virtual_multiway_valve.py
index c24b7b1c5..c12aa7fe6 100644
--- a/unilabos/devices/virtual/virtual_multiway_valve.py
+++ b/unilabos/devices/virtual/virtual_multiway_valve.py
@@ -1,16 +1,20 @@
import time
+import logging
from typing import Union, Dict, Optional
class VirtualMultiwayValve:
"""
- 虚拟九通阀门 - 0号位连接transfer pump,1-8号位连接其他设备
+ 虚拟九通阀门 - 0号位连接transfer pump,1-8号位连接其他设备 🔄
"""
def __init__(self, port: str = "VIRTUAL", positions: int = 8):
self.port = port
self.max_positions = positions # 1-8号位
self.total_positions = positions + 1 # 0-8号位,共9个位置
+ # 添加日志记录器
+ self.logger = logging.getLogger(f"VirtualMultiwayValve.{port}")
+
# 状态属性
self._status = "Idle"
self._valve_state = "Ready"
@@ -29,6 +33,10 @@ def __init__(self, port: str = "VIRTUAL", positions: int = 8):
7: "port_7", # 7号位
8: "port_8" # 8号位
}
+
+ print(f"🔄 === 虚拟多通阀门已创建 === ✨")
+ print(f"🎯 端口: {port} | 📊 位置范围: 0-{self.max_positions} | 🏠 初始位置: 0 (transfer_pump)")
+ self.logger.info(f"🔧 多通阀门初始化: 端口={port}, 最大位置={self.max_positions}")
@property
def status(self) -> str:
@@ -47,16 +55,16 @@ def target_position(self) -> int:
return self._target_position
def get_current_position(self) -> int:
- """获取当前阀门位置"""
+ """获取当前阀门位置 📍"""
return self._current_position
def get_current_port(self) -> str:
- """获取当前连接的端口名称"""
+ """获取当前连接的端口名称 🔌"""
return self.position_map.get(self._current_position, "unknown")
def set_position(self, command: Union[int, str]):
"""
- 设置阀门位置 - 支持0-8位置
+ 设置阀门位置 - 支持0-8位置 🎯
Args:
command: 目标位置 (0-8) 或位置字符串
@@ -71,112 +79,162 @@ def set_position(self, command: Union[int, str]):
pos = int(command)
if pos < 0 or pos > self.max_positions:
- raise ValueError(f"Position must be between 0 and {self.max_positions}")
+ error_msg = f"位置必须在 0-{self.max_positions} 范围内"
+ self.logger.error(f"❌ {error_msg}: 请求位置={pos}")
+ raise ValueError(error_msg)
+
+ # 获取位置描述emoji
+ if pos == 0:
+ pos_emoji = "🚰"
+ pos_desc = "泵位置"
+ else:
+ pos_emoji = "🔌"
+ pos_desc = f"端口{pos}"
+
+ old_position = self._current_position
+ old_port = self.get_current_port()
+
+ self.logger.info(f"🔄 阀门切换: {old_position}({old_port}) → {pos}({self.position_map.get(pos, 'unknown')}) {pos_emoji}")
self._status = "Busy"
self._valve_state = "Moving"
self._target_position = pos
# 模拟阀门切换时间
- switch_time = abs(self._current_position - pos) * 0.5 # 每个位置0.5秒
- time.sleep(switch_time)
+ switch_time = abs(self._current_position - pos) * 0.5 # 每个位置0.1秒
+
+ if switch_time > 0:
+ self.logger.info(f"⏱️ 阀门移动中... 预计用时: {switch_time:.1f}秒 🔄")
+ time.sleep(switch_time)
self._current_position = pos
self._status = "Idle"
self._valve_state = "Ready"
current_port = self.get_current_port()
- return f"Position set to {pos} ({current_port})"
+ success_msg = f"✅ 阀门已切换到位置 {pos} ({current_port}) {pos_emoji}"
+
+ self.logger.info(success_msg)
+ return success_msg
except ValueError as e:
+ error_msg = f"❌ 阀门切换失败: {str(e)}"
self._status = "Error"
self._valve_state = "Error"
- return f"Error: {str(e)}"
+ self.logger.error(error_msg)
+ return error_msg
def set_to_pump_position(self):
- """切换到transfer pump位置(0号位)"""
+ """切换到transfer pump位置(0号位)🚰"""
+ self.logger.info(f"🚰 切换到泵位置...")
return self.set_position(0)
def set_to_port(self, port_number: int):
"""
- 切换到指定端口位置
+ 切换到指定端口位置 🔌
Args:
port_number: 端口号 (1-8)
"""
if port_number < 1 or port_number > self.max_positions:
- raise ValueError(f"Port number must be between 1 and {self.max_positions}")
+ error_msg = f"端口号必须在 1-{self.max_positions} 范围内"
+ self.logger.error(f"❌ {error_msg}: 请求端口={port_number}")
+ raise ValueError(error_msg)
+
+ self.logger.info(f"🔌 切换到端口 {port_number}...")
return self.set_position(port_number)
def open(self):
- """打开阀门 - 设置到transfer pump位置(0号位)"""
+ """打开阀门 - 设置到transfer pump位置(0号位)🔓"""
+ self.logger.info(f"🔓 打开阀门,设置到泵位置...")
return self.set_to_pump_position()
def close(self):
- """关闭阀门 - 对于多通阀门,设置到一个"关闭"状态"""
+ """关闭阀门 - 对于多通阀门,设置到一个"关闭"状态 🔒"""
+ self.logger.info(f"🔒 关闭阀门...")
+
self._status = "Busy"
self._valve_state = "Closing"
time.sleep(0.5)
-
+
# 可以选择保持当前位置或设置特殊关闭状态
self._status = "Idle"
self._valve_state = "Closed"
- return f"Valve closed at position {self._current_position}"
+ close_msg = f"🔒 阀门已关闭,保持在位置 {self._current_position} ({self.get_current_port()})"
+ self.logger.info(close_msg)
+ return close_msg
def get_valve_position(self) -> int:
- """获取阀门位置 - 兼容性方法"""
+ """获取阀门位置 - 兼容性方法 📍"""
return self._current_position
def is_at_position(self, position: int) -> bool:
- """检查是否在指定位置"""
- return self._current_position == position
+ """检查是否在指定位置 🎯"""
+ result = self._current_position == position
+ self.logger.debug(f"🎯 位置检查: 当前={self._current_position}, 目标={position}, 匹配={result}")
+ return result
def is_at_pump_position(self) -> bool:
- """检查是否在transfer pump位置"""
- return self._current_position == 0
+ """检查是否在transfer pump位置 🚰"""
+ result = self._current_position == 0
+ pump_status = "是" if result else "否"
+ self.logger.debug(f"🚰 泵位置检查: {pump_status} (当前位置: {self._current_position})")
+ return result
def is_at_port(self, port_number: int) -> bool:
- """检查是否在指定端口位置"""
- return self._current_position == port_number
+ """检查是否在指定端口位置 🔌"""
+ result = self._current_position == port_number
+ port_status = "是" if result else "否"
+ self.logger.debug(f"🔌 端口{port_number}检查: {port_status} (当前位置: {self._current_position})")
+ return result
def get_available_positions(self) -> list:
- """获取可用位置列表"""
- return list(range(0, self.max_positions + 1))
+ """获取可用位置列表 📋"""
+ positions = list(range(0, self.max_positions + 1))
+ self.logger.debug(f"📋 可用位置: {positions}")
+ return positions
def get_available_ports(self) -> Dict[int, str]:
- """获取可用端口映射"""
+ """获取可用端口映射 🗺️"""
+ self.logger.debug(f"🗺️ 端口映射: {self.position_map}")
return self.position_map.copy()
def reset(self):
- """重置阀门到transfer pump位置(0号位)"""
+ """重置阀门到transfer pump位置(0号位)🔄"""
+ self.logger.info(f"🔄 重置阀门到泵位置...")
return self.set_position(0)
def switch_between_pump_and_port(self, port_number: int):
"""
- 在transfer pump位置和指定端口之间切换
+ 在transfer pump位置和指定端口之间切换 🔄
Args:
port_number: 目标端口号 (1-8)
"""
if self._current_position == 0:
# 当前在pump位置,切换到指定端口
+ self.logger.info(f"🔄 从泵位置切换到端口 {port_number}...")
return self.set_to_port(port_number)
else:
# 当前在某个端口,切换到pump位置
+ self.logger.info(f"🔄 从端口 {self._current_position} 切换到泵位置...")
return self.set_to_pump_position()
def get_flow_path(self) -> str:
- """获取当前流路路径描述"""
+ """获取当前流路路径描述 🌊"""
current_port = self.get_current_port()
if self._current_position == 0:
- return f"Transfer pump connected (position {self._current_position})"
+ flow_path = f"🚰 转移泵已连接 (位置 {self._current_position})"
else:
- return f"Port {self._current_position} connected ({current_port})"
+ flow_path = f"🔌 端口 {self._current_position} 已连接 ({current_port})"
+
+ self.logger.debug(f"🌊 当前流路: {flow_path}")
+ return flow_path
def get_info(self) -> dict:
- """获取阀门详细信息"""
- return {
+ """获取阀门详细信息 📊"""
+ info = {
"port": self.port,
"max_positions": self.max_positions,
"total_positions": self.total_positions,
@@ -188,18 +246,25 @@ def get_info(self) -> dict:
"flow_path": self.get_flow_path(),
"position_map": self.position_map
}
+
+ self.logger.debug(f"📊 阀门信息: 位置={self._current_position}, 状态={self._status}, 端口={self.get_current_port()}")
+ return info
def __str__(self):
- return f"VirtualMultiwayValve(Position: {self._current_position}/{self.max_positions}, Port: {self.get_current_port()}, Status: {self._status})"
+ current_port = self.get_current_port()
+ status_emoji = "✅" if self._status == "Idle" else "🔄" if self._status == "Busy" else "❌"
+
+ return f"🔄 VirtualMultiwayValve({status_emoji} 位置: {self._current_position}/{self.max_positions}, 端口: {current_port}, 状态: {self._status})"
def set_valve_position(self, command: Union[int, str]):
"""
- 设置阀门位置 - 兼容pump_protocol调用
+ 设置阀门位置 - 兼容pump_protocol调用 🎯
这是set_position的别名方法,用于兼容pump_protocol.py
Args:
command: 目标位置 (0-8) 或位置字符串
"""
+ self.logger.debug(f"🎯 兼容性调用: set_valve_position({command})")
return self.set_position(command)
@@ -207,25 +272,35 @@ def set_valve_position(self, command: Union[int, str]):
if __name__ == "__main__":
valve = VirtualMultiwayValve()
- print("=== 虚拟九通阀门测试 ===")
- print(f"初始状态: {valve}")
- print(f"当前流路: {valve.get_flow_path()}")
+ print("🔄 === 虚拟九通阀门测试 === ✨")
+ print(f"🏠 初始状态: {valve}")
+ print(f"🌊 当前流路: {valve.get_flow_path()}")
# 切换到试剂瓶1(1号位)
- print(f"\n切换到1号位: {valve.set_position(1)}")
- print(f"当前状态: {valve}")
+ print(f"\n🔌 切换到1号位: {valve.set_position(1)}")
+ print(f"📍 当前状态: {valve}")
# 切换到transfer pump位置(0号位)
- print(f"\n切换到pump位置: {valve.set_to_pump_position()}")
- print(f"当前状态: {valve}")
+ print(f"\n🚰 切换到pump位置: {valve.set_to_pump_position()}")
+ print(f"📍 当前状态: {valve}")
# 切换到试剂瓶2(2号位)
- print(f"\n切换到2号位: {valve.set_to_port(2)}")
- print(f"当前状态: {valve}")
+ print(f"\n🔌 切换到2号位: {valve.set_to_port(2)}")
+ print(f"📍 当前状态: {valve}")
# 显示所有可用位置
- print(f"\n可用位置: {valve.get_available_positions()}")
- print(f"端口映射: {valve.get_available_ports()}")
+ print(f"\n📋 可用位置: {valve.get_available_positions()}")
+ print(f"🗺️ 端口映射: {valve.get_available_ports()}")
# 获取详细信息
- print(f"\n详细信息: {valve.get_info()}")
\ No newline at end of file
+ print(f"\n📊 详细信息: {valve.get_info()}")
+
+ # 测试切换功能
+ print(f"\n🔄 智能切换测试:")
+ print(f"当前位置: {valve._current_position}")
+ print(f"切换结果: {valve.switch_between_pump_and_port(3)}")
+ print(f"新位置: {valve._current_position}")
+
+ # 重置测试
+ print(f"\n🔄 重置测试: {valve.reset()}")
+ print(f"📍 重置后状态: {valve}")
\ No newline at end of file
diff --git a/unilabos/devices/virtual/virtual_rotavap.py b/unilabos/devices/virtual/virtual_rotavap.py
index bfd64947e..dd1cca43d 100644
--- a/unilabos/devices/virtual/virtual_rotavap.py
+++ b/unilabos/devices/virtual/virtual_rotavap.py
@@ -4,11 +4,11 @@
from typing import Dict, Any, Optional
def debug_print(message):
- """调试输出"""
- print(f"[ROTAVAP] {message}", flush=True)
+ """调试输出 🔍"""
+ print(f"🌪️ [ROTAVAP] {message}", flush=True)
class VirtualRotavap:
- """Virtual rotary evaporator device - 简化版,只保留核心功能"""
+ """Virtual rotary evaporator device - 简化版,只保留核心功能 🌪️"""
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
# 处理可能的不同调用方式
@@ -35,13 +35,16 @@ def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, A
if key not in skip_keys and not hasattr(self, key):
setattr(self, key, value)
+ print(f"🌪️ === 虚拟旋转蒸发仪 {self.device_id} 已创建 === ✨")
+ print(f"🔥 温度范围: 10°C ~ {self._max_temp}°C | 🌀 转速范围: 10 ~ {self._max_rotation_speed} RPM")
+
async def initialize(self) -> bool:
- """Initialize virtual rotary evaporator"""
- self.logger.info(f"Initializing virtual rotary evaporator {self.device_id}")
+ """Initialize virtual rotary evaporator 🚀"""
+ self.logger.info(f"🔧 初始化虚拟旋转蒸发仪 {self.device_id} ✨")
# 只保留核心状态
self.data.update({
- "status": "Idle",
+ "status": "🏠 待机中",
"rotavap_state": "Ready", # Ready, Evaporating, Completed, Error
"current_temp": 25.0,
"target_temp": 25.0,
@@ -50,22 +53,27 @@ async def initialize(self) -> bool:
"evaporated_volume": 0.0,
"progress": 0.0,
"remaining_time": 0.0,
- "message": "Ready for evaporation"
+ "message": "🌪️ Ready for evaporation"
})
+
+ self.logger.info(f"✅ 旋转蒸发仪 {self.device_id} 初始化完成 🌪️")
+ self.logger.info(f"📊 设备规格: 温度范围 10°C ~ {self._max_temp}°C | 转速范围 10 ~ {self._max_rotation_speed} RPM")
return True
async def cleanup(self) -> bool:
- """Cleanup virtual rotary evaporator"""
- self.logger.info(f"Cleaning up virtual rotary evaporator {self.device_id}")
+ """Cleanup virtual rotary evaporator 🧹"""
+ self.logger.info(f"🧹 清理虚拟旋转蒸发仪 {self.device_id} 🔚")
self.data.update({
- "status": "Offline",
+ "status": "💤 离线",
"rotavap_state": "Offline",
"current_temp": 25.0,
"rotation_speed": 0.0,
"vacuum_pressure": 1.0,
- "message": "System offline"
+ "message": "💤 System offline"
})
+
+ self.logger.info(f"✅ 旋转蒸发仪 {self.device_id} 清理完成 💤")
return True
async def evaporate(
@@ -78,37 +86,44 @@ async def evaporate(
solvent: str = "",
**kwargs
) -> bool:
- """Execute evaporate action - 简化版"""
+ """Execute evaporate action - 简化版 🌪️"""
# 🔧 简化处理:如果vessel就是设备自己,直接操作
if vessel == self.device_id:
- debug_print(f"在设备 {self.device_id} 上直接执行蒸发操作")
+ debug_print(f"🎯 在设备 {self.device_id} 上直接执行蒸发操作")
actual_vessel = self.device_id
else:
actual_vessel = vessel
# 参数预处理
if solvent:
- self.logger.info(f"识别到溶剂: {solvent}")
+ self.logger.info(f"🧪 识别到溶剂: {solvent}")
# 根据溶剂调整参数
solvent_lower = solvent.lower()
if any(s in solvent_lower for s in ['water', 'aqueous']):
temp = max(temp, 80.0)
pressure = max(pressure, 0.2)
- self.logger.info("水系溶剂:调整参数")
+ self.logger.info(f"💧 水系溶剂:调整参数 → 温度 {temp}°C, 压力 {pressure} bar")
elif any(s in solvent_lower for s in ['ethanol', 'methanol', 'acetone']):
temp = min(temp, 50.0)
pressure = min(pressure, 0.05)
- self.logger.info("易挥发溶剂:调整参数")
+ self.logger.info(f"⚡ 易挥发溶剂:调整参数 → 温度 {temp}°C, 压力 {pressure} bar")
- self.logger.info(f"Evaporate: vessel={actual_vessel}, pressure={pressure} bar, temp={temp}°C, time={time}s, rotation={stir_speed} RPM, solvent={solvent}")
+ self.logger.info(f"🌪️ 开始蒸发操作: {actual_vessel}")
+ self.logger.info(f" 🥽 容器: {actual_vessel}")
+ self.logger.info(f" 🌡️ 温度: {temp}°C")
+ self.logger.info(f" 💨 真空度: {pressure} bar")
+ self.logger.info(f" ⏰ 时间: {time}s")
+ self.logger.info(f" 🌀 转速: {stir_speed} RPM")
+ if solvent:
+ self.logger.info(f" 🧪 溶剂: {solvent}")
# 验证参数
if temp > self._max_temp or temp < 10.0:
- error_msg = f"温度 {temp}°C 超出范围 (10-{self._max_temp}°C)"
- self.logger.error(error_msg)
+ error_msg = f"🌡️ 温度 {temp}°C 超出范围 (10-{self._max_temp}°C) ⚠️"
+ self.logger.error(f"❌ {error_msg}")
self.data.update({
- "status": f"Error: {error_msg}",
+ "status": f"❌ 错误: 温度超出范围",
"rotavap_state": "Error",
"current_temp": 25.0,
"progress": 0.0,
@@ -118,10 +133,10 @@ async def evaporate(
return False
if stir_speed > self._max_rotation_speed or stir_speed < 10.0:
- error_msg = f"旋转速度 {stir_speed} RPM 超出范围 (10-{self._max_rotation_speed} RPM)"
- self.logger.error(error_msg)
+ error_msg = f"🌀 旋转速度 {stir_speed} RPM 超出范围 (10-{self._max_rotation_speed} RPM) ⚠️"
+ self.logger.error(f"❌ {error_msg}")
self.data.update({
- "status": f"Error: {error_msg}",
+ "status": f"❌ 错误: 转速超出范围",
"rotavap_state": "Error",
"current_temp": 25.0,
"progress": 0.0,
@@ -131,10 +146,10 @@ async def evaporate(
return False
if pressure < 0.01 or pressure > 1.0:
- error_msg = f"真空度 {pressure} bar 超出范围 (0.01-1.0 bar)"
- self.logger.error(error_msg)
+ error_msg = f"💨 真空度 {pressure} bar 超出范围 (0.01-1.0 bar) ⚠️"
+ self.logger.error(f"❌ {error_msg}")
self.data.update({
- "status": f"Error: {error_msg}",
+ "status": f"❌ 错误: 压力超出范围",
"rotavap_state": "Error",
"current_temp": 25.0,
"progress": 0.0,
@@ -144,8 +159,10 @@ async def evaporate(
return False
# 开始蒸发
+ self.logger.info(f"🚀 启动蒸发程序! 预计用时 {time/60:.1f}分钟 ⏱️")
+
self.data.update({
- "status": f"蒸发中: {actual_vessel}",
+ "status": f"🌪️ 蒸发中: {actual_vessel}",
"rotavap_state": "Evaporating",
"current_temp": temp,
"target_temp": temp,
@@ -154,13 +171,14 @@ async def evaporate(
"remaining_time": time,
"progress": 0.0,
"evaporated_volume": 0.0,
- "message": f"Evaporating {actual_vessel} at {temp}°C, {pressure} bar, {stir_speed} RPM"
+ "message": f"🌪️ Evaporating {actual_vessel} at {temp}°C, {pressure} bar, {stir_speed} RPM"
})
try:
# 蒸发过程 - 实时更新进度
start_time = time_module.time()
total_time = time
+ last_logged_progress = 0
while True:
current_time = time_module.time()
@@ -168,19 +186,31 @@ async def evaporate(
remaining = max(0, total_time - elapsed)
progress = min(100.0, (elapsed / total_time) * 100)
- # 模拟蒸发体积
- evaporated_vol = progress * 0.8 # 假设最多蒸发80mL
+ # 模拟蒸发体积 - 根据溶剂类型调整
+ if solvent and any(s in solvent.lower() for s in ['water', 'aqueous']):
+ evaporated_vol = progress * 0.6 # 水系溶剂蒸发慢
+ elif solvent and any(s in solvent.lower() for s in ['ethanol', 'methanol', 'acetone']):
+ evaporated_vol = progress * 1.0 # 易挥发溶剂蒸发快
+ else:
+ evaporated_vol = progress * 0.8 # 默认蒸发量
# 🔧 更新状态 - 确保包含所有必需字段
+ status_msg = f"🌪️ 蒸发中: {actual_vessel} | 🌡️ {temp}°C | 💨 {pressure} bar | 🌀 {stir_speed} RPM | 📊 {progress:.1f}% | ⏰ 剩余: {remaining:.0f}s"
+
self.data.update({
"remaining_time": remaining,
- "progress": progress, # 确保这个字段存在
- "evaporated_volume": evaporated_vol, # 确保这个字段存在
- "current_temp": temp, # 确保这个字段存在
- "status": f"蒸发中: {actual_vessel} | {temp}°C | {pressure} bar | {progress:.1f}% | 剩余: {remaining:.0f}s",
- "message": f"Evaporating: {progress:.1f}% complete, {remaining:.0f}s remaining"
+ "progress": progress,
+ "evaporated_volume": evaporated_vol,
+ "current_temp": temp,
+ "status": status_msg,
+ "message": f"🌪️ Evaporating: {progress:.1f}% complete, 💧 {evaporated_vol:.1f}mL evaporated, ⏰ {remaining:.0f}s remaining"
})
+ # 进度日志(每25%打印一次)
+ if progress >= 25 and int(progress) % 25 == 0 and int(progress) != last_logged_progress:
+ self.logger.info(f"📊 蒸发进度: {progress:.0f}% | 💧 已蒸发: {evaporated_vol:.1f}mL | ⏰ 剩余: {remaining:.0f}s ✨")
+ last_logged_progress = int(progress)
+
# 时间到了,退出循环
if remaining <= 0:
break
@@ -189,42 +219,59 @@ async def evaporate(
await asyncio.sleep(1.0)
# 蒸发完成
- final_evaporated = 80.0
+ if solvent and any(s in solvent.lower() for s in ['water', 'aqueous']):
+ final_evaporated = 60.0 # 水系溶剂
+ elif solvent and any(s in solvent.lower() for s in ['ethanol', 'methanol', 'acetone']):
+ final_evaporated = 100.0 # 易挥发溶剂
+ else:
+ final_evaporated = 80.0 # 默认
+
self.data.update({
- "status": f"蒸发完成: {actual_vessel} | 蒸发量: {final_evaporated:.1f}mL",
+ "status": f"✅ 蒸发完成: {actual_vessel} | 💧 蒸发量: {final_evaporated:.1f}mL",
"rotavap_state": "Completed",
"evaporated_volume": final_evaporated,
"progress": 100.0,
- "current_temp": temp, # 保持温度信息
+ "current_temp": temp,
"remaining_time": 0.0,
- "rotation_speed": 0.0, # 停止旋转
- "vacuum_pressure": 1.0, # 恢复大气压
- "message": f"Evaporation completed: {final_evaporated}mL evaporated from {actual_vessel}"
+ "rotation_speed": 0.0,
+ "vacuum_pressure": 1.0,
+ "message": f"✅ Evaporation completed: {final_evaporated}mL evaporated from {actual_vessel}"
})
- self.logger.info(f"Evaporation completed: {final_evaporated}mL evaporated from {actual_vessel}")
+ self.logger.info(f"🎉 蒸发操作完成! ✨")
+ self.logger.info(f"📊 蒸发结果:")
+ self.logger.info(f" 🥽 容器: {actual_vessel}")
+ self.logger.info(f" 💧 蒸发量: {final_evaporated:.1f}mL")
+ self.logger.info(f" 🌡️ 蒸发温度: {temp}°C")
+ self.logger.info(f" 💨 真空度: {pressure} bar")
+ self.logger.info(f" 🌀 旋转速度: {stir_speed} RPM")
+ self.logger.info(f" ⏱️ 总用时: {total_time:.0f}s")
+ if solvent:
+ self.logger.info(f" 🧪 处理溶剂: {solvent} 🏁")
+
return True
except Exception as e:
# 出错处理
- self.logger.error(f"Error during evaporation: {str(e)}")
+ error_msg = f"蒸发过程中发生错误: {str(e)} 💥"
+ self.logger.error(f"❌ {error_msg}")
self.data.update({
- "status": f"蒸发错误: {str(e)}",
+ "status": f"❌ 蒸发错误: {str(e)}",
"rotavap_state": "Error",
"current_temp": 25.0,
"progress": 0.0,
"evaporated_volume": 0.0,
"rotation_speed": 0.0,
"vacuum_pressure": 1.0,
- "message": f"Evaporation failed: {str(e)}"
+ "message": f"❌ Evaporation failed: {str(e)}"
})
return False
# === 核心状态属性 ===
@property
def status(self) -> str:
- return self.data.get("status", "Unknown")
+ return self.data.get("status", "❓ Unknown")
@property
def rotavap_state(self) -> str:
diff --git a/unilabos/devices/virtual/virtual_solid_dispenser.py b/unilabos/devices/virtual/virtual_solid_dispenser.py
index 48ad9f560..439c34837 100644
--- a/unilabos/devices/virtual/virtual_solid_dispenser.py
+++ b/unilabos/devices/virtual/virtual_solid_dispenser.py
@@ -5,12 +5,12 @@
class VirtualSolidDispenser:
"""
- 虚拟固体粉末加样器 - 用于处理 Add Protocol 中的固体试剂添加
+ 虚拟固体粉末加样器 - 用于处理 Add Protocol 中的固体试剂添加 ⚗️
特点:
- - 高兼容性:缺少参数不报错
- - 智能识别:自动查找固体试剂瓶
- - 简单反馈:成功/失败 + 消息
+ - 高兼容性:缺少参数不报错 ✅
+ - 智能识别:自动查找固体试剂瓶 🔍
+ - 简单反馈:成功/失败 + 消息 📊
"""
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
@@ -29,26 +29,30 @@ def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwarg
self.logger = logging.getLogger(f"VirtualSolidDispenser.{self.device_id}")
- print(f"=== VirtualSolidDispenser {self.device_id} 创建成功! ===")
- print(f"=== 最大容量: {self.max_capacity}g, 精度: {self.precision}g ===")
+ print(f"⚗️ === 虚拟固体分配器 {self.device_id} 创建成功! === ✨")
+ print(f"📊 设备规格: 最大容量 {self.max_capacity}g | 精度 {self.precision}g 🎯")
async def initialize(self) -> bool:
- """初始化固体加样器"""
- self.logger.info(f"初始化固体加样器 {self.device_id}")
+ """初始化固体加样器 🚀"""
+ self.logger.info(f"🔧 初始化固体分配器 {self.device_id} ✨")
self._status = "Ready"
self._current_reagent = ""
self._dispensed_amount = 0.0
+
+ self.logger.info(f"✅ 固体分配器 {self.device_id} 初始化完成 ⚗️")
return True
async def cleanup(self) -> bool:
- """清理固体加样器"""
- self.logger.info(f"清理固体加样器 {self.device_id}")
+ """清理固体加样器 🧹"""
+ self.logger.info(f"🧹 清理固体分配器 {self.device_id} 🔚")
self._status = "Idle"
+
+ self.logger.info(f"✅ 固体分配器 {self.device_id} 清理完成 💤")
return True
def parse_mass_string(self, mass_str: str) -> float:
"""
- 解析质量字符串为数值 (g)
+ 解析质量字符串为数值 (g) ⚖️
支持格式: "2.9 g", "19.3g", "4.5 mg", "1.2 kg" 等
"""
@@ -63,6 +67,7 @@ def parse_mass_string(self, mass_str: str) -> float:
match = re.search(pattern, mass_clean)
if not match:
+ self.logger.debug(f"🔍 无法解析质量字符串: {mass_str}")
return 0.0
try:
@@ -87,15 +92,18 @@ def parse_mass_string(self, mass_str: str) -> float:
}
multiplier = unit_multipliers.get(unit, 1.0)
- return value * multiplier
+ result = value * multiplier
+
+ self.logger.debug(f"⚖️ 质量解析: {mass_str} → {result:.6f}g (原值: {value} {unit})")
+ return result
except (ValueError, TypeError):
- self.logger.warning(f"无法解析质量字符串: {mass_str}")
+ self.logger.warning(f"⚠️ 无法解析质量字符串: {mass_str}")
return 0.0
def parse_mol_string(self, mol_str: str) -> float:
"""
- 解析摩尔数字符串为数值 (mol)
+ 解析摩尔数字符串为数值 (mol) 🧮
支持格式: "0.12 mol", "16.2 mmol", "25.2mmol" 等
"""
@@ -110,6 +118,7 @@ def parse_mol_string(self, mol_str: str) -> float:
match = re.search(pattern, mol_clean)
if not match:
+ self.logger.debug(f"🔍 无法解析摩尔数字符串: {mol_str}")
return 0.0
try:
@@ -118,21 +127,25 @@ def parse_mol_string(self, mol_str: str) -> float:
# 单位转换为 mol
if unit == 'mmol':
- return value * 0.001
+ result = value * 0.001
else: # mol
- return value
+ result = value
+
+ self.logger.debug(f"🧮 摩尔数解析: {mol_str} → {result:.6f}mol (原值: {value} {unit})")
+ return result
except (ValueError, TypeError):
- self.logger.warning(f"无法解析摩尔数字符串: {mol_str}")
+ self.logger.warning(f"⚠️ 无法解析摩尔数字符串: {mol_str}")
return 0.0
def find_solid_reagent_bottle(self, reagent_name: str) -> str:
"""
- 查找固体试剂瓶
+ 查找固体试剂瓶 🔍
这是一个简化版本,实际使用时应该连接到系统的设备图
"""
if not reagent_name:
+ self.logger.debug(f"🔍 未指定试剂名称,使用默认瓶")
return "unknown_solid_bottle"
# 可能的固体试剂瓶命名模式
@@ -146,7 +159,9 @@ def find_solid_reagent_bottle(self, reagent_name: str) -> str:
]
# 这里简化处理,实际应该查询设备图
- return possible_names[0]
+ selected_bottle = possible_names[0]
+ self.logger.debug(f"🔍 为试剂 {reagent_name} 选择试剂瓶: {selected_bottle}")
+ return selected_bottle
async def add_solid(
self,
@@ -158,7 +173,7 @@ async def add_solid(
**kwargs # 兼容额外参数
) -> Dict[str, Any]:
"""
- 添加固体试剂的主要方法
+ 添加固体试剂的主要方法 ⚗️
Args:
vessel: 目标容器
@@ -172,22 +187,24 @@ async def add_solid(
Dict: 操作结果
"""
try:
- self.logger.info(f"=== 开始固体加样操作 ===")
- self.logger.info(f"目标容器: {vessel}")
- self.logger.info(f"试剂: {reagent}")
- self.logger.info(f"质量: {mass}")
- self.logger.info(f"摩尔数: {mol}")
- self.logger.info(f"目的: {purpose}")
+ self.logger.info(f"⚗️ === 开始固体加样操作 === ✨")
+ self.logger.info(f" 🥽 目标容器: {vessel}")
+ self.logger.info(f" 🧪 试剂: {reagent}")
+ self.logger.info(f" ⚖️ 质量: {mass}")
+ self.logger.info(f" 🧮 摩尔数: {mol}")
+ self.logger.info(f" 📝 目的: {purpose}")
# 参数验证 - 宽松处理
if not vessel:
vessel = "main_reactor" # 默认容器
- self.logger.warning(f"未指定容器,使用默认容器: {vessel}")
+ self.logger.warning(f"⚠️ 未指定容器,使用默认容器: {vessel} 🏠")
if not reagent:
+ error_msg = "❌ 错误: 必须指定试剂名称"
+ self.logger.error(error_msg)
return {
"success": False,
- "message": "错误: 必须指定试剂名称",
+ "message": error_msg,
"return_info": "missing_reagent"
}
@@ -195,36 +212,41 @@ async def add_solid(
mass_value = self.parse_mass_string(mass)
mol_value = self.parse_mol_string(mol)
- self.logger.info(f"解析后 - 质量: {mass_value}g, 摩尔数: {mol_value}mol")
+ self.logger.info(f"📊 解析结果 - 质量: {mass_value:.6f}g | 摩尔数: {mol_value:.6f}mol")
# 确定实际加样量
if mass_value > 0:
actual_amount = mass_value
amount_unit = "g"
- self.logger.info(f"按质量加样: {actual_amount} {amount_unit}")
+ amount_emoji = "⚖️"
+ self.logger.info(f"⚖️ 按质量加样: {actual_amount:.6f} {amount_unit}")
elif mol_value > 0:
# 简化处理:假设分子量为100 g/mol
assumed_mw = 100.0
actual_amount = mol_value * assumed_mw
amount_unit = "g (from mol)"
- self.logger.info(f"按摩尔数加样: {mol_value} mol → {actual_amount} g (假设分子量 {assumed_mw})")
+ amount_emoji = "🧮"
+ self.logger.info(f"🧮 按摩尔数加样: {mol_value:.6f} mol → {actual_amount:.6f} g (假设分子量 {assumed_mw})")
else:
# 没有指定量,使用默认值
actual_amount = 1.0
amount_unit = "g (default)"
- self.logger.warning(f"未指定质量或摩尔数,使用默认值: {actual_amount} {amount_unit}")
+ amount_emoji = "🎯"
+ self.logger.warning(f"⚠️ 未指定质量或摩尔数,使用默认值: {actual_amount} {amount_unit} 🎯")
# 检查容量限制
if actual_amount > self.max_capacity:
+ error_msg = f"❌ 错误: 请求量 {actual_amount:.3f}g 超过最大容量 {self.max_capacity}g"
+ self.logger.error(error_msg)
return {
"success": False,
- "message": f"错误: 请求量 {actual_amount}g 超过最大容量 {self.max_capacity}g",
+ "message": error_msg,
"return_info": "exceeds_capacity"
}
# 查找试剂瓶
reagent_bottle = self.find_solid_reagent_bottle(reagent)
- self.logger.info(f"使用试剂瓶: {reagent_bottle}")
+ self.logger.info(f"🔍 使用试剂瓶: {reagent_bottle}")
# 模拟加样过程
self._status = "Dispensing"
@@ -233,8 +255,17 @@ async def add_solid(
# 计算操作时间 (基于质量)
operation_time = max(0.5, actual_amount * 0.1) # 每克0.1秒,最少0.5秒
- self.logger.info(f"开始加样,预计时间: {operation_time:.1f}秒")
- await asyncio.sleep(operation_time)
+ self.logger.info(f"🚀 开始加样,预计时间: {operation_time:.1f}秒 ⏱️")
+
+ # 显示进度的模拟
+ steps = max(3, int(operation_time))
+ step_time = operation_time / steps
+
+ for i in range(steps):
+ progress = (i + 1) / steps * 100
+ await asyncio.sleep(step_time)
+ if i % 2 == 0: # 每隔一步显示进度
+ self.logger.debug(f"📊 加样进度: {progress:.0f}% | {amount_emoji} 正在分配 {reagent}...")
# 更新状态
self._dispensed_amount = actual_amount
@@ -242,19 +273,26 @@ async def add_solid(
self._status = "Ready"
# 成功结果
- success_message = f"成功添加 {reagent} {actual_amount:.3f} {amount_unit} 到 {vessel}"
+ success_message = f"✅ 成功添加 {reagent} {actual_amount:.6f} {amount_unit} 到 {vessel}"
- self.logger.info(f"=== 固体加样完成 ===")
- self.logger.info(success_message)
+ self.logger.info(f"🎉 === 固体加样完成 === ✨")
+ self.logger.info(f"📊 操作结果:")
+ self.logger.info(f" ✅ {success_message}")
+ self.logger.info(f" 🧪 试剂瓶: {reagent_bottle}")
+ self.logger.info(f" ⏱️ 用时: {operation_time:.1f}秒")
+ self.logger.info(f" 🎯 总操作次数: {self._total_operations} 🏁")
return {
"success": True,
"message": success_message,
- "return_info": f"dispensed_{actual_amount:.3f}g"
+ "return_info": f"dispensed_{actual_amount:.6f}g",
+ "dispensed_amount": actual_amount,
+ "reagent": reagent,
+ "vessel": vessel
}
except Exception as e:
- error_message = f"固体加样失败: {str(e)}"
+ error_message = f"❌ 固体加样失败: {str(e)} 💥"
self.logger.error(error_message)
self._status = "Error"
@@ -282,8 +320,8 @@ def total_operations(self) -> int:
return self._total_operations
def get_device_info(self) -> Dict[str, Any]:
- """获取设备状态信息"""
- return {
+ """获取设备状态信息 📊"""
+ info = {
"device_id": self.device_id,
"status": self._status,
"current_reagent": self._current_reagent,
@@ -292,43 +330,59 @@ def get_device_info(self) -> Dict[str, Any]:
"max_capacity": self.max_capacity,
"precision": self.precision
}
+
+ self.logger.debug(f"📊 设备信息: 状态={self._status}, 试剂={self._current_reagent}, 加样量={self._dispensed_amount:.6f}g")
+ return info
def __str__(self):
- return f"VirtualSolidDispenser({self.device_id}: {self._status}, 最后加样 {self._dispensed_amount:.3f}g)"
+ status_emoji = "✅" if self._status == "Ready" else "🔄" if self._status == "Dispensing" else "❌" if self._status == "Error" else "🏠"
+ return f"⚗️ VirtualSolidDispenser({status_emoji} {self.device_id}: {self._status}, 最后加样 {self._dispensed_amount:.3f}g)"
# 测试函数
async def test_solid_dispenser():
- """测试固体加样器"""
- print("=== 固体加样器测试 ===")
+ """测试固体加样器 🧪"""
+ print("⚗️ === 固体加样器测试开始 === 🧪")
dispenser = VirtualSolidDispenser("test_dispenser")
await dispenser.initialize()
# 测试1: 按质量加样
+ print(f"\n🧪 测试1: 按质量加样...")
result1 = await dispenser.add_solid(
vessel="main_reactor",
reagent="magnesium",
mass="2.9 g"
)
- print(f"测试1结果: {result1}")
+ print(f"📊 测试1结果: {result1}")
# 测试2: 按摩尔数加样
+ print(f"\n🧮 测试2: 按摩尔数加样...")
result2 = await dispenser.add_solid(
vessel="main_reactor",
reagent="sodium_nitrite",
mol="0.28 mol"
)
- print(f"测试2结果: {result2}")
+ print(f"📊 测试2结果: {result2}")
# 测试3: 缺少参数
+ print(f"\n⚠️ 测试3: 缺少参数测试...")
result3 = await dispenser.add_solid(
reagent="test_compound"
)
- print(f"测试3结果: {result3}")
+ print(f"📊 测试3结果: {result3}")
+
+ # 测试4: 超容量测试
+ print(f"\n❌ 测试4: 超容量测试...")
+ result4 = await dispenser.add_solid(
+ vessel="main_reactor",
+ reagent="heavy_compound",
+ mass="150 g" # 超过100g限制
+ )
+ print(f"📊 测试4结果: {result4}")
- print(f"设备信息: {dispenser.get_device_info()}")
- print("=== 测试完成 ===")
+ print(f"\n📊 最终设备信息: {dispenser.get_device_info()}")
+ print(f"✅ === 测试完成 === 🎉")
if __name__ == "__main__":
diff --git a/unilabos/devices/virtual/virtual_stirrer.py b/unilabos/devices/virtual/virtual_stirrer.py
index 874f997a6..2b9058be5 100644
--- a/unilabos/devices/virtual/virtual_stirrer.py
+++ b/unilabos/devices/virtual/virtual_stirrer.py
@@ -4,7 +4,7 @@
from typing import Dict, Any
class VirtualStirrer:
- """Virtual stirrer device for StirProtocol testing - 功能完整版"""
+ """Virtual stirrer device for StirProtocol testing - 功能完整版 🌪️"""
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
# 处理可能的不同调用方式
@@ -30,45 +30,69 @@ def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwarg
for key, value in kwargs.items():
if key not in skip_keys and not hasattr(self, key):
setattr(self, key, value)
+
+ print(f"🌪️ === 虚拟搅拌器 {self.device_id} 已创建 === ✨")
+ print(f"🔧 速度范围: {self._min_speed} ~ {self._max_speed} RPM | 📱 端口: {self.port}")
async def initialize(self) -> bool:
- """Initialize virtual stirrer"""
- self.logger.info(f"Initializing virtual stirrer {self.device_id}")
+ """Initialize virtual stirrer 🚀"""
+ self.logger.info(f"🔧 初始化虚拟搅拌器 {self.device_id} ✨")
# 初始化状态信息
self.data.update({
- "status": "Idle",
+ "status": "🏠 待机中",
"operation_mode": "Idle", # 操作模式: Idle, Stirring, Settling, Completed, Error
"current_vessel": "", # 当前搅拌的容器
"current_speed": 0.0, # 当前搅拌速度
"is_stirring": False, # 是否正在搅拌
"remaining_time": 0.0, # 剩余时间
})
+
+ self.logger.info(f"✅ 搅拌器 {self.device_id} 初始化完成 🌪️")
+ self.logger.info(f"📊 设备规格: 速度范围 {self._min_speed} ~ {self._max_speed} RPM")
return True
async def cleanup(self) -> bool:
- """Cleanup virtual stirrer"""
- self.logger.info(f"Cleaning up virtual stirrer {self.device_id}")
+ """Cleanup virtual stirrer 🧹"""
+ self.logger.info(f"🧹 清理虚拟搅拌器 {self.device_id} 🔚")
+
self.data.update({
- "status": "Offline",
+ "status": "💤 离线",
"operation_mode": "Offline",
"current_vessel": "",
"current_speed": 0.0,
"is_stirring": False,
"remaining_time": 0.0,
})
+
+ self.logger.info(f"✅ 搅拌器 {self.device_id} 清理完成 💤")
return True
- async def stir(self, stir_time: float, stir_speed: float, settling_time: float) -> bool:
- """Execute stir action - 定时搅拌 + 沉降"""
- self.logger.info(f"Stir: speed={stir_speed} RPM, time={stir_time}s, settling={settling_time}s")
+ async def stir(self, stir_time: float, stir_speed: float, settling_time: float, **kwargs) -> bool:
+ """Execute stir action - 定时搅拌 + 沉降 🌪️"""
+
+ # 🔧 类型转换 - 确保所有参数都是数字类型
+ try:
+ stir_time = float(stir_time)
+ stir_speed = float(stir_speed)
+ settling_time = float(settling_time)
+ except (ValueError, TypeError) as e:
+ error_msg = f"参数类型转换失败: stir_time={stir_time}, stir_speed={stir_speed}, settling_time={settling_time}, error={e}"
+ self.logger.error(f"❌ {error_msg}")
+ self.data.update({
+ "status": f"❌ 错误: {error_msg}",
+ "operation_mode": "Error"
+ })
+ return False
+
+ self.logger.info(f"🌪️ 开始搅拌操作: 速度 {stir_speed} RPM | 时间 {stir_time}s | 沉降 {settling_time}s")
# 验证参数
if stir_speed > self._max_speed or stir_speed < self._min_speed:
- error_msg = f"搅拌速度 {stir_speed} RPM 超出范围 ({self._min_speed} - {self._max_speed} RPM)"
- self.logger.error(error_msg)
+ error_msg = f"🌪️ 搅拌速度 {stir_speed} RPM 超出范围 ({self._min_speed} - {self._max_speed} RPM) ⚠️"
+ self.logger.error(f"❌ {error_msg}")
self.data.update({
- "status": f"Error: {error_msg}",
+ "status": f"❌ 错误: 速度超出范围",
"operation_mode": "Error"
})
return False
@@ -77,8 +101,10 @@ async def stir(self, stir_time: float, stir_speed: float, settling_time: float)
start_time = time_module.time()
total_stir_time = stir_time
+ self.logger.info(f"🚀 开始搅拌阶段: {stir_speed} RPM × {total_stir_time}s ⏱️")
+
self.data.update({
- "status": f"搅拌中: {stir_speed} RPM | 剩余: {total_stir_time:.0f}s",
+ "status": f"🌪️ 搅拌中: {stir_speed} RPM | ⏰ 剩余: {total_stir_time:.0f}s",
"operation_mode": "Stirring",
"current_speed": stir_speed,
"is_stirring": True,
@@ -86,30 +112,41 @@ async def stir(self, stir_time: float, stir_speed: float, settling_time: float)
})
# 搅拌过程 - 实时更新剩余时间
+ last_logged_time = 0
while True:
current_time = time_module.time()
elapsed = current_time - start_time
remaining = max(0, total_stir_time - elapsed)
+ progress = (elapsed / total_stir_time) * 100 if total_stir_time > 0 else 100
# 更新状态
self.data.update({
"remaining_time": remaining,
- "status": f"搅拌中: {stir_speed} RPM | 剩余: {remaining:.0f}s"
+ "status": f"🌪️ 搅拌中: {stir_speed} RPM | ⏰ 剩余: {remaining:.0f}s"
})
+ # 进度日志(每25%打印一次)
+ if progress >= 25 and int(progress) % 25 == 0 and int(progress) != last_logged_time:
+ self.logger.info(f"📊 搅拌进度: {progress:.0f}% | 🌪️ {stir_speed} RPM | ⏰ 剩余: {remaining:.0f}s ✨")
+ last_logged_time = int(progress)
+
# 搅拌时间到了
if remaining <= 0:
break
await asyncio.sleep(1.0)
+ self.logger.info(f"✅ 搅拌阶段完成! 🌪️ {stir_speed} RPM × {stir_time}s")
+
# === 第二阶段:沉降(如果需要)===
if settling_time > 0:
start_settling_time = time_module.time()
total_settling_time = settling_time
+ self.logger.info(f"🛑 开始沉降阶段: 停止搅拌 × {total_settling_time}s ⏱️")
+
self.data.update({
- "status": f"沉降中: 停止搅拌 | 剩余: {total_settling_time:.0f}s",
+ "status": f"🛑 沉降中: 停止搅拌 | ⏰ 剩余: {total_settling_time:.0f}s",
"operation_mode": "Settling",
"current_speed": 0.0,
"is_stirring": False,
@@ -117,52 +154,87 @@ async def stir(self, stir_time: float, stir_speed: float, settling_time: float)
})
# 沉降过程 - 实时更新剩余时间
+ last_logged_settling = 0
while True:
current_time = time_module.time()
elapsed = current_time - start_settling_time
remaining = max(0, total_settling_time - elapsed)
+ progress = (elapsed / total_settling_time) * 100 if total_settling_time > 0 else 100
# 更新状态
self.data.update({
"remaining_time": remaining,
- "status": f"沉降中: 停止搅拌 | 剩余: {remaining:.0f}s"
+ "status": f"🛑 沉降中: 停止搅拌 | ⏰ 剩余: {remaining:.0f}s"
})
+ # 进度日志(每25%打印一次)
+ if progress >= 25 and int(progress) % 25 == 0 and int(progress) != last_logged_settling:
+ self.logger.info(f"📊 沉降进度: {progress:.0f}% | 🛑 静置中 | ⏰ 剩余: {remaining:.0f}s ✨")
+ last_logged_settling = int(progress)
+
# 沉降时间到了
if remaining <= 0:
break
await asyncio.sleep(1.0)
+
+ self.logger.info(f"✅ 沉降阶段完成! 🛑 静置 {settling_time}s")
# === 操作完成 ===
- settling_info = f" | 沉降: {settling_time:.0f}s" if settling_time > 0 else ""
+ settling_info = f" | 🛑 沉降: {settling_time:.0f}s" if settling_time > 0 else ""
+
self.data.update({
- "status": f"完成: 搅拌 {stir_speed} RPM, {stir_time:.0f}s{settling_info}",
+ "status": f"✅ 完成: 🌪️ 搅拌 {stir_speed} RPM × {stir_time:.0f}s{settling_info}",
"operation_mode": "Completed",
"current_speed": 0.0,
"is_stirring": False,
"remaining_time": 0.0,
})
- self.logger.info(f"Stir completed: {stir_speed} RPM for {stir_time}s + settling {settling_time}s")
+ self.logger.info(f"🎉 搅拌操作完成! ✨")
+ self.logger.info(f"📊 操作总结:")
+ self.logger.info(f" 🌪️ 搅拌: {stir_speed} RPM × {stir_time}s")
+ if settling_time > 0:
+ self.logger.info(f" 🛑 沉降: {settling_time}s")
+ self.logger.info(f" ⏱️ 总用时: {(stir_time + settling_time):.0f}s 🏁")
+
return True
- async def start_stir(self, vessel: str, stir_speed: float, purpose: str) -> bool:
- """Start stir action - 开始持续搅拌"""
- self.logger.info(f"StartStir: vessel={vessel}, speed={stir_speed} RPM, purpose={purpose}")
+ async def start_stir(self, vessel: str, stir_speed: float, purpose: str = "") -> bool:
+ """Start stir action - 开始持续搅拌 🔄"""
+
+ # 🔧 类型转换
+ try:
+ stir_speed = float(stir_speed)
+ vessel = str(vessel)
+ purpose = str(purpose)
+ except (ValueError, TypeError) as e:
+ error_msg = f"参数类型转换错误: {str(e)}"
+ self.logger.error(f"❌ {error_msg}")
+ self.data.update({
+ "status": f"❌ 错误: {error_msg}",
+ "operation_mode": "Error"
+ })
+ return False
+
+ self.logger.info(f"🔄 启动持续搅拌: {vessel} | 🌪️ {stir_speed} RPM")
+ if purpose:
+ self.logger.info(f"📝 搅拌目的: {purpose}")
# 验证参数
if stir_speed > self._max_speed or stir_speed < self._min_speed:
- error_msg = f"搅拌速度 {stir_speed} RPM 超出范围 ({self._min_speed} - {self._max_speed} RPM)"
- self.logger.error(error_msg)
+ error_msg = f"🌪️ 搅拌速度 {stir_speed} RPM 超出范围 ({self._min_speed} - {self._max_speed} RPM) ⚠️"
+ self.logger.error(f"❌ {error_msg}")
self.data.update({
- "status": f"Error: {error_msg}",
+ "status": f"❌ 错误: 速度超出范围",
"operation_mode": "Error"
})
return False
+ purpose_info = f" | 📝 {purpose}" if purpose else ""
+
self.data.update({
- "status": f"启动: 持续搅拌 {vessel} at {stir_speed} RPM | {purpose}",
+ "status": f"🔄 启动: 持续搅拌 {vessel} | 🌪️ {stir_speed} RPM{purpose_info}",
"operation_mode": "Stirring",
"current_vessel": vessel,
"current_speed": stir_speed,
@@ -170,16 +242,28 @@ async def start_stir(self, vessel: str, stir_speed: float, purpose: str) -> bool
"remaining_time": -1.0, # -1 表示持续运行
})
+ self.logger.info(f"✅ 持续搅拌已启动! 🌪️ {stir_speed} RPM × ♾️ 🚀")
return True
async def stop_stir(self, vessel: str) -> bool:
- """Stop stir action - 停止搅拌"""
- self.logger.info(f"StopStir: vessel={vessel}")
+ """Stop stir action - 停止搅拌 🛑"""
+
+ # 🔧 类型转换
+ try:
+ vessel = str(vessel)
+ except (ValueError, TypeError) as e:
+ error_msg = f"参数类型转换错误: {str(e)}"
+ self.logger.error(f"❌ {error_msg}")
+ return False
current_speed = self.data.get("current_speed", 0.0)
+ self.logger.info(f"🛑 停止搅拌: {vessel}")
+ if current_speed > 0:
+ self.logger.info(f"🌪️ 之前搅拌速度: {current_speed} RPM")
+
self.data.update({
- "status": f"已停止: {vessel} 搅拌停止 | 之前速度: {current_speed} RPM",
+ "status": f"🛑 已停止: {vessel} 搅拌停止 | 之前速度: {current_speed} RPM",
"operation_mode": "Stopped",
"current_vessel": "",
"current_speed": 0.0,
@@ -187,12 +271,13 @@ async def stop_stir(self, vessel: str) -> bool:
"remaining_time": 0.0,
})
+ self.logger.info(f"✅ 搅拌器已停止 {vessel} 的搅拌操作 🏁")
return True
# 状态属性
@property
def status(self) -> str:
- return self.data.get("status", "Idle")
+ return self.data.get("status", "🏠 待机中")
@property
def operation_mode(self) -> str:
@@ -212,4 +297,33 @@ def is_stirring(self) -> bool:
@property
def remaining_time(self) -> float:
- return self.data.get("remaining_time", 0.0)
\ No newline at end of file
+ return self.data.get("remaining_time", 0.0)
+
+ @property
+ def max_speed(self) -> float:
+ return self._max_speed
+
+ @property
+ def min_speed(self) -> float:
+ return self._min_speed
+
+ def get_device_info(self) -> Dict[str, Any]:
+ """获取设备状态信息 📊"""
+ info = {
+ "device_id": self.device_id,
+ "status": self.status,
+ "operation_mode": self.operation_mode,
+ "current_vessel": self.current_vessel,
+ "current_speed": self.current_speed,
+ "is_stirring": self.is_stirring,
+ "remaining_time": self.remaining_time,
+ "max_speed": self._max_speed,
+ "min_speed": self._min_speed
+ }
+
+ self.logger.debug(f"📊 设备信息: 模式={self.operation_mode}, 速度={self.current_speed} RPM, 搅拌={self.is_stirring}")
+ return info
+
+ def __str__(self):
+ status_emoji = "✅" if self.operation_mode == "Idle" else "🌪️" if self.operation_mode == "Stirring" else "🛑" if self.operation_mode == "Settling" else "❌"
+ return f"🌪️ VirtualStirrer({status_emoji} {self.device_id}: {self.operation_mode}, {self.current_speed} RPM)"
\ No newline at end of file
From ed3b22a7380ae19d7f3d4dceec5fd718ee33cab7 Mon Sep 17 00:00:00 2001
From: KCFeng425 <2100011801@stu.pku.edu.cn>
Date: Wed, 16 Jul 2025 10:38:12 +0800
Subject: [PATCH 9/9] =?UTF-8?q?=E8=A1=A5=E5=85=85=E4=BA=86=E5=89=A9?=
=?UTF-8?q?=E4=B8=8B=E7=9A=84=E5=87=A0=E4=B8=AAprotocol?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
unilabos/app/mq.py | 2 +-
unilabos/compile/add_protocol.py | 224 +++-
unilabos/compile/adjustph_protocol.py | 313 +++--
unilabos/compile/dissolve_protocol.py | 315 +++--
.../compile/evacuateandrefill_protocol.py | 398 ++++--
unilabos/compile/pump_protocol.py | 1070 ++++++++++++++---
unilabos/compile/separate_protocol.py | 424 +++++--
.../devices/virtual/virtual_multiway_valve.py | 22 +-
.../devices/virtual/virtual_transferpump.py | 205 +++-
unilabos/ros/nodes/presets/protocol_node.py | 2 +-
10 files changed, 2334 insertions(+), 641 deletions(-)
diff --git a/unilabos/app/mq.py b/unilabos/app/mq.py
index 061add291..12ec2c0d8 100644
--- a/unilabos/app/mq.py
+++ b/unilabos/app/mq.py
@@ -166,7 +166,7 @@ def publish_device_status(self, device_status: dict, device_id, property_name):
status = {"data": device_status.get(device_id, {}), "device_id": device_id}
address = f"labs/{MQConfig.lab_id}/devices/"
self.client.publish(address, json.dumps(status), qos=2)
- logger.debug(f"Device status published: address: {address}, {status}")
+ # logger.debug(f"Device status published: address: {address}, {status}")
def publish_job_status(self, feedback_data: dict, job_id: str, status: str, return_info: Optional[str] = None):
if self.mqtt_disable:
diff --git a/unilabos/compile/add_protocol.py b/unilabos/compile/add_protocol.py
index b5b017eb0..c46befeff 100644
--- a/unilabos/compile/add_protocol.py
+++ b/unilabos/compile/add_protocol.py
@@ -22,18 +22,20 @@ def parse_volume_input(volume_input: Union[str, float]) -> float:
float: 体积(毫升)
"""
if isinstance(volume_input, (int, float)):
+ debug_print(f"📏 体积输入为数值: {volume_input}")
return float(volume_input)
if not volume_input or not str(volume_input).strip():
+ debug_print(f"⚠️ 体积输入为空,返回0.0mL")
return 0.0
volume_str = str(volume_input).lower().strip()
- debug_print(f"解析体积输入: '{volume_str}'")
+ debug_print(f"🔍 解析体积输入: '{volume_str}'")
# 处理未知体积
if volume_str in ['?', 'unknown', 'tbd', 'to be determined']:
default_volume = 10.0 # 默认10mL
- debug_print(f"检测到未知体积,使用默认值: {default_volume}mL")
+ debug_print(f"❓ 检测到未知体积,使用默认值: {default_volume}mL 🎯")
return default_volume
# 移除空格并提取数字和单位
@@ -43,7 +45,7 @@ def parse_volume_input(volume_input: Union[str, float]) -> float:
match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
if not match:
- debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值10mL")
+ debug_print(f"❌ 无法解析体积: '{volume_str}',使用默认值10mL")
return 10.0
value = float(match.group(1))
@@ -52,12 +54,14 @@ def parse_volume_input(volume_input: Union[str, float]) -> float:
# 转换为毫升
if unit in ['l', 'liter']:
volume = value * 1000.0 # L -> mL
+ debug_print(f"🔄 体积转换: {value}L → {volume}mL")
elif unit in ['μl', 'ul', 'microliter']:
volume = value / 1000.0 # μL -> mL
+ debug_print(f"🔄 体积转换: {value}μL → {volume}mL")
else: # ml, milliliter 或默认
volume = value # 已经是mL
+ debug_print(f"✅ 体积已为mL: {volume}mL")
- debug_print(f"体积转换: {value}{unit} → {volume}mL")
return volume
def parse_mass_input(mass_input: Union[str, float]) -> float:
@@ -71,13 +75,15 @@ def parse_mass_input(mass_input: Union[str, float]) -> float:
float: 质量(克)
"""
if isinstance(mass_input, (int, float)):
+ debug_print(f"⚖️ 质量输入为数值: {mass_input}g")
return float(mass_input)
if not mass_input or not str(mass_input).strip():
+ debug_print(f"⚠️ 质量输入为空,返回0.0g")
return 0.0
mass_str = str(mass_input).lower().strip()
- debug_print(f"解析质量输入: '{mass_str}'")
+ debug_print(f"🔍 解析质量输入: '{mass_str}'")
# 移除空格并提取数字和单位
mass_clean = re.sub(r'\s+', '', mass_str)
@@ -86,7 +92,7 @@ def parse_mass_input(mass_input: Union[str, float]) -> float:
match = re.match(r'([0-9]*\.?[0-9]+)\s*(g|mg|kg|gram|milligram|kilogram)?', mass_clean)
if not match:
- debug_print(f"⚠️ 无法解析质量: '{mass_str}',返回0.0g")
+ debug_print(f"❌ 无法解析质量: '{mass_str}',返回0.0g")
return 0.0
value = float(match.group(1))
@@ -95,12 +101,14 @@ def parse_mass_input(mass_input: Union[str, float]) -> float:
# 转换为克
if unit in ['mg', 'milligram']:
mass = value / 1000.0 # mg -> g
+ debug_print(f"🔄 质量转换: {value}mg → {mass}g")
elif unit in ['kg', 'kilogram']:
mass = value * 1000.0 # kg -> g
+ debug_print(f"🔄 质量转换: {value}kg → {mass}g")
else: # g, gram 或默认
mass = value # 已经是g
+ debug_print(f"✅ 质量已为g: {mass}g")
- debug_print(f"质量转换: {value}{unit} → {mass}g")
return mass
def parse_time_input(time_input: Union[str, float]) -> float:
@@ -114,18 +122,20 @@ def parse_time_input(time_input: Union[str, float]) -> float:
float: 时间(秒)
"""
if isinstance(time_input, (int, float)):
+ debug_print(f"⏱️ 时间输入为数值: {time_input}秒")
return float(time_input)
if not time_input or not str(time_input).strip():
+ debug_print(f"⚠️ 时间输入为空,返回0秒")
return 0.0
time_str = str(time_input).lower().strip()
- debug_print(f"解析时间输入: '{time_str}'")
+ debug_print(f"🔍 解析时间输入: '{time_str}'")
# 处理未知时间
if time_str in ['?', 'unknown', 'tbd']:
default_time = 60.0 # 默认1分钟
- debug_print(f"检测到未知时间,使用默认值: {default_time}s")
+ debug_print(f"❓ 检测到未知时间,使用默认值: {default_time}s (1分钟) ⏰")
return default_time
# 移除空格并提取数字和单位
@@ -135,7 +145,7 @@ def parse_time_input(time_input: Union[str, float]) -> float:
match = re.match(r'([0-9]*\.?[0-9]+)\s*(s|sec|second|min|minute|h|hr|hour|d|day)?', time_clean)
if not match:
- debug_print(f"⚠️ 无法解析时间: '{time_str}',返回0s")
+ debug_print(f"❌ 无法解析时间: '{time_str}',返回0s")
return 0.0
value = float(match.group(1))
@@ -144,21 +154,25 @@ def parse_time_input(time_input: Union[str, float]) -> float:
# 转换为秒
if unit in ['min', 'minute']:
time_sec = value * 60.0 # min -> s
+ debug_print(f"🔄 时间转换: {value}分钟 → {time_sec}秒")
elif unit in ['h', 'hr', 'hour']:
time_sec = value * 3600.0 # h -> s
+ debug_print(f"🔄 时间转换: {value}小时 → {time_sec}秒")
elif unit in ['d', 'day']:
time_sec = value * 86400.0 # d -> s
+ debug_print(f"🔄 时间转换: {value}天 → {time_sec}秒")
else: # s, sec, second 或默认
time_sec = value # 已经是s
+ debug_print(f"✅ 时间已为秒: {time_sec}秒")
- debug_print(f"时间转换: {value}{unit} → {time_sec}s")
return time_sec
def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str:
"""增强版试剂容器查找,支持固体和液体"""
- debug_print(f"查找试剂 '{reagent}' 的容器...")
+ debug_print(f"🔍 开始查找试剂 '{reagent}' 的容器...")
# 🔧 方法1:直接搜索 data.reagent_name 和 config.reagent
+ debug_print(f"📋 方法1: 搜索reagent字段...")
for node in G.nodes():
node_data = G.nodes[node].get('data', {})
node_type = G.nodes[node].get('type', '')
@@ -171,16 +185,17 @@ def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str:
# 精确匹配
if reagent_name == reagent.lower() or config_reagent == reagent.lower():
- debug_print(f"✅ 通过reagent字段找到容器: {node}")
+ debug_print(f"✅ 通过reagent字段精确匹配到容器: {node} 🎯")
return node
# 模糊匹配
if (reagent.lower() in reagent_name and reagent_name) or \
(reagent.lower() in config_reagent and config_reagent):
- debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node}")
+ debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node} 🔍")
return node
# 🔧 方法2:常见的容器命名规则
+ debug_print(f"📋 方法2: 使用命名规则查找...")
reagent_clean = reagent.lower().replace(' ', '_').replace('-', '_')
possible_names = [
reagent_clean,
@@ -197,20 +212,23 @@ def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str:
f"reagent_bottle_3"
]
+ debug_print(f"🔍 尝试的容器名称: {possible_names[:5]}... (共{len(possible_names)}个)")
+
for name in possible_names:
if name in G.nodes():
node_type = G.nodes[name].get('type', '')
if node_type == 'container':
- debug_print(f"✅ 通过命名规则找到容器: {name}")
+ debug_print(f"✅ 通过命名规则找到容器: {name} 📝")
return name
# 🔧 方法3:节点名称模糊匹配
+ debug_print(f"📋 方法3: 节点名称模糊匹配...")
for node_id in G.nodes():
node_data = G.nodes[node_id]
if node_data.get('type') == 'container':
# 检查节点名称是否包含试剂名称
if reagent_clean in node_id.lower():
- debug_print(f"✅ 通过节点名称模糊匹配到容器: {node_id}")
+ debug_print(f"✅ 通过节点名称模糊匹配到容器: {node_id} 🔍")
return node_id
# 检查液体类型匹配
@@ -220,51 +238,77 @@ def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str:
if isinstance(liquid, dict):
liquid_type = liquid.get('liquid_type') or liquid.get('name', '')
if liquid_type.lower() == reagent.lower():
- debug_print(f"✅ 通过液体类型匹配到容器: {node_id}")
+ debug_print(f"✅ 通过液体类型匹配到容器: {node_id} 💧")
return node_id
# 🔧 方法4:使用第一个试剂瓶作为备选
+ debug_print(f"📋 方法4: 查找备选试剂瓶...")
for node_id in G.nodes():
node_data = G.nodes[node_id]
if (node_data.get('type') == 'container' and
('reagent' in node_id.lower() or 'bottle' in node_id.lower())):
- debug_print(f"⚠️ 未找到专用容器,使用备选试剂瓶: {node_id}")
+ debug_print(f"⚠️ 未找到专用容器,使用备选试剂瓶: {node_id} 🔄")
return node_id
+ debug_print(f"❌ 所有方法都失败了,无法找到容器!")
raise ValueError(f"找不到试剂 '{reagent}' 对应的容器")
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
"""查找连接到指定容器的搅拌器"""
+ debug_print(f"🔍 查找连接到容器 '{vessel}' 的搅拌器...")
+
stirrer_nodes = []
for node in G.nodes():
node_class = G.nodes[node].get('class', '').lower()
if 'stirrer' in node_class:
stirrer_nodes.append(node)
+ debug_print(f"📋 发现搅拌器: {node}")
+
+ debug_print(f"📊 共找到 {len(stirrer_nodes)} 个搅拌器")
# 查找连接到容器的搅拌器
for stirrer in stirrer_nodes:
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
- debug_print(f"找到连接的搅拌器: {stirrer}")
+ debug_print(f"✅ 找到连接的搅拌器: {stirrer} 🔗")
return stirrer
# 返回第一个搅拌器
if stirrer_nodes:
- debug_print(f"使用第一个搅拌器: {stirrer_nodes[0]}")
+ debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个: {stirrer_nodes[0]} 🔄")
return stirrer_nodes[0]
+ debug_print(f"❌ 未找到任何搅拌器")
return ""
def find_solid_dispenser(G: nx.DiGraph) -> str:
"""查找固体加样器"""
+ debug_print(f"🔍 查找固体加样器...")
+
for node in G.nodes():
node_class = G.nodes[node].get('class', '').lower()
if 'solid_dispenser' in node_class or 'dispenser' in node_class:
- debug_print(f"找到固体加样器: {node}")
+ debug_print(f"✅ 找到固体加样器: {node} 🥄")
return node
- debug_print("⚠️ 未找到固体加样器")
+ debug_print(f"❌ 未找到固体加样器")
return ""
+# 🆕 创建进度日志动作
+def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]:
+ """创建一个动作日志"""
+ full_message = f"{emoji} {message}"
+ debug_print(full_message)
+ logger.info(full_message)
+ print(f"[ACTION] {full_message}", flush=True)
+
+ return {
+ "action_name": "wait",
+ "action_kwargs": {
+ "time": 0.1,
+ "log_message": full_message
+ }
+ }
+
def generate_add_protocol(
G: nx.DiGraph,
vessel: str,
@@ -301,51 +345,58 @@ def generate_add_protocol(
"""
debug_print("=" * 60)
- debug_print("开始生成添加试剂协议")
- debug_print(f"原始参数:")
- debug_print(f" - vessel: '{vessel}'")
- debug_print(f" - reagent: '{reagent}'")
- debug_print(f" - volume: {volume} (类型: {type(volume)})")
- debug_print(f" - mass: {mass} (类型: {type(mass)})")
- debug_print(f" - time: {time} (类型: {type(time)})")
- debug_print(f" - mol: '{mol}'")
- debug_print(f" - event: '{event}'")
- debug_print(f" - rate_spec: '{rate_spec}'")
+ debug_print("🚀 开始生成添加试剂协议")
+ debug_print(f"📋 原始参数:")
+ debug_print(f" 🥼 vessel: '{vessel}'")
+ debug_print(f" 🧪 reagent: '{reagent}'")
+ debug_print(f" 📏 volume: {volume} (类型: {type(volume)})")
+ debug_print(f" ⚖️ mass: {mass} (类型: {type(mass)})")
+ debug_print(f" ⏱️ time: {time} (类型: {type(time)})")
+ debug_print(f" 🧬 mol: '{mol}'")
+ debug_print(f" 🎯 event: '{event}'")
+ debug_print(f" ⚡ rate_spec: '{rate_spec}'")
+ debug_print(f" 🌪️ stir: {stir}")
+ debug_print(f" 🔄 stir_speed: {stir_speed} rpm")
debug_print("=" * 60)
action_sequence = []
# === 参数验证 ===
- debug_print("步骤1: 参数验证...")
+ debug_print("🔍 步骤1: 参数验证...")
+ action_sequence.append(create_action_log(f"开始添加试剂 '{reagent}' 到容器 '{vessel}'", "🎬"))
if not vessel:
+ debug_print("❌ vessel 参数不能为空")
raise ValueError("vessel 参数不能为空")
if not reagent:
+ debug_print("❌ reagent 参数不能为空")
raise ValueError("reagent 参数不能为空")
if vessel not in G.nodes():
+ debug_print(f"❌ 容器 '{vessel}' 不存在于系统中")
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
debug_print("✅ 基本参数验证通过")
# === 🔧 关键修复:参数解析 ===
- debug_print("步骤2: 参数解析...")
+ debug_print("🔍 步骤2: 参数解析...")
+ action_sequence.append(create_action_log("正在解析添加参数...", "🔍"))
# 解析各种参数为数值
final_volume = parse_volume_input(volume)
final_mass = parse_mass_input(mass)
final_time = parse_time_input(time)
- debug_print(f"解析结果:")
- debug_print(f" - 体积: {final_volume}mL")
- debug_print(f" - 质量: {final_mass}g")
- debug_print(f" - 时间: {final_time}s")
- debug_print(f" - 摩尔: '{mol}'")
- debug_print(f" - 事件: '{event}'")
- debug_print(f" - 速率: '{rate_spec}'")
+ debug_print(f"📊 解析结果:")
+ debug_print(f" 📏 体积: {final_volume}mL")
+ debug_print(f" ⚖️ 质量: {final_mass}g")
+ debug_print(f" ⏱️ 时间: {final_time}s")
+ debug_print(f" 🧬 摩尔: '{mol}'")
+ debug_print(f" 🎯 事件: '{event}'")
+ debug_print(f" ⚡ 速率: '{rate_spec}'")
# === 判断添加类型 ===
- debug_print("步骤3: 判断添加类型...")
+ debug_print("🔍 步骤3: 判断添加类型...")
# 🔧 修复:现在使用解析后的数值进行比较
is_solid = (final_mass > 0 or (mol and mol.strip() != ""))
@@ -357,22 +408,34 @@ def generate_add_protocol(
final_volume = 10.0
debug_print("⚠️ 未指定体积或质量,默认为10mL液体")
- debug_print(f"添加类型: {'固体' if is_solid else '液体'}")
+ add_type = "固体" if is_solid else "液体"
+ add_emoji = "🧂" if is_solid else "💧"
+ debug_print(f"📋 添加类型: {add_type} {add_emoji}")
+
+ action_sequence.append(create_action_log(f"确定添加类型: {add_type} {add_emoji}", "📋"))
# === 执行添加流程 ===
- debug_print("步骤4: 执行添加流程...")
+ debug_print("🔍 步骤4: 执行添加流程...")
try:
if is_solid:
# === 固体添加路径 ===
- debug_print(f"使用固体添加路径")
+ debug_print(f"🧂 使用固体添加路径")
+ action_sequence.append(create_action_log("开始固体试剂添加流程", "🧂"))
solid_dispenser = find_solid_dispenser(G)
if solid_dispenser:
+ action_sequence.append(create_action_log(f"找到固体加样器: {solid_dispenser}", "🥄"))
+
# 启动搅拌
if stir:
+ debug_print("🌪️ 准备启动搅拌...")
+ action_sequence.append(create_action_log("准备启动搅拌器", "🌪️"))
+
stirrer_id = find_connected_stirrer(G, vessel)
if stirrer_id:
+ action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {stir_speed} rpm)", "🔄"))
+
action_sequence.append({
"device_id": stirrer_id,
"action_name": "start_stir",
@@ -383,6 +446,7 @@ def generate_add_protocol(
}
})
# 等待搅拌稳定
+ action_sequence.append(create_action_log("等待搅拌稳定...", "⏳"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 3}
@@ -399,19 +463,27 @@ def generate_add_protocol(
if final_mass > 0:
add_kwargs["mass"] = str(final_mass)
+ action_sequence.append(create_action_log(f"准备添加固体: {final_mass}g", "⚖️"))
if mol and mol.strip():
add_kwargs["mol"] = mol
+ action_sequence.append(create_action_log(f"按摩尔数添加: {mol}", "🧬"))
if equiv and equiv.strip():
add_kwargs["equiv"] = equiv
+ action_sequence.append(create_action_log(f"当量: {equiv}", "🔢"))
+ action_sequence.append(create_action_log("开始固体加样操作", "🥄"))
action_sequence.append({
"device_id": solid_dispenser,
"action_name": "add_solid",
"action_kwargs": add_kwargs
})
+ action_sequence.append(create_action_log("固体加样完成", "✅"))
+
# 添加后等待
if final_time > 0:
+ wait_minutes = final_time / 60
+ action_sequence.append(create_action_log(f"等待反应进行 ({wait_minutes:.1f}分钟)", "⏰"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": final_time}
@@ -419,19 +491,28 @@ def generate_add_protocol(
debug_print(f"✅ 固体添加完成")
else:
- debug_print("⚠️ 未找到固体加样器,跳过固体添加")
+ debug_print("❌ 未找到固体加样器,跳过固体添加")
+ action_sequence.append(create_action_log("未找到固体加样器,无法添加固体", "❌"))
else:
# === 液体添加路径 ===
- debug_print(f"使用液体添加路径")
+ debug_print(f"💧 使用液体添加路径")
+ action_sequence.append(create_action_log("开始液体试剂添加流程", "💧"))
# 查找试剂容器
+ action_sequence.append(create_action_log("正在查找试剂容器...", "🔍"))
reagent_vessel = find_reagent_vessel(G, reagent)
+ action_sequence.append(create_action_log(f"找到试剂容器: {reagent_vessel}", "🧪"))
# 启动搅拌
if stir:
+ debug_print("🌪️ 准备启动搅拌...")
+ action_sequence.append(create_action_log("准备启动搅拌器", "🌪️"))
+
stirrer_id = find_connected_stirrer(G, vessel)
if stirrer_id:
+ action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {stir_speed} rpm)", "🔄"))
+
action_sequence.append({
"device_id": stirrer_id,
"action_name": "start_stir",
@@ -442,6 +523,7 @@ def generate_add_protocol(
}
})
# 等待搅拌稳定
+ action_sequence.append(create_action_log("等待搅拌稳定...", "⏳"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5}
@@ -451,18 +533,23 @@ def generate_add_protocol(
if final_time > 0:
flowrate = final_volume / final_time * 60 # mL/min
transfer_flowrate = flowrate
+ debug_print(f"⚡ 根据时间计算流速: {flowrate:.2f} mL/min")
else:
if rate_spec == "dropwise":
flowrate = 0.5 # 滴加,很慢
transfer_flowrate = 0.2
+ debug_print(f"💧 滴加模式,流速: {flowrate} mL/min")
elif viscous:
flowrate = 1.0 # 粘性液体
transfer_flowrate = 0.3
+ debug_print(f"🍯 粘性液体,流速: {flowrate} mL/min")
else:
flowrate = 2.5 # 正常流速
transfer_flowrate = 0.5
+ debug_print(f"⚡ 正常流速: {flowrate} mL/min")
- debug_print(f"流速设置: {flowrate} mL/min")
+ action_sequence.append(create_action_log(f"设置流速: {flowrate:.2f} mL/min", "⚡"))
+ action_sequence.append(create_action_log(f"开始转移 {final_volume}mL 液体", "🚰"))
# 调用pump protocol
pump_actions = generate_pump_protocol_with_rinsing(
@@ -486,9 +573,11 @@ def generate_add_protocol(
)
action_sequence.extend(pump_actions)
debug_print(f"✅ 液体转移完成,添加了 {len(pump_actions)} 个动作")
+ action_sequence.append(create_action_log(f"液体转移完成 ({len(pump_actions)} 个操作)", "✅"))
except Exception as e:
- debug_print(f"⚠️ 试剂添加失败: {str(e)}")
+ debug_print(f"❌ 试剂添加失败: {str(e)}")
+ action_sequence.append(create_action_log(f"试剂添加失败: {str(e)}", "❌"))
# 添加错误日志
action_sequence.append({
"device_id": "system",
@@ -500,19 +589,28 @@ def generate_add_protocol(
# === 最终结果 ===
debug_print("=" * 60)
- debug_print(f"✅ 添加试剂协议生成完成")
+ debug_print(f"🎉 添加试剂协议生成完成")
debug_print(f"📊 总动作数: {len(action_sequence)}")
debug_print(f"📋 处理总结:")
- debug_print(f" - 试剂: {reagent}")
- debug_print(f" - 添加类型: {'固体' if is_solid else '液体'}")
- debug_print(f" - 目标容器: {vessel}")
+ debug_print(f" 🧪 试剂: {reagent}")
+ debug_print(f" {add_emoji} 添加类型: {add_type}")
+ debug_print(f" 🥼 目标容器: {vessel}")
if is_liquid:
- debug_print(f" - 体积: {final_volume}mL")
+ debug_print(f" 📏 体积: {final_volume}mL")
if is_solid:
- debug_print(f" - 质量: {final_mass}g")
- debug_print(f" - 摩尔: {mol}")
+ debug_print(f" ⚖️ 质量: {final_mass}g")
+ debug_print(f" 🧬 摩尔: {mol}")
debug_print("=" * 60)
+ # 添加完成日志
+ summary_msg = f"试剂添加协议完成: {reagent} → {vessel}"
+ if is_liquid:
+ summary_msg += f" ({final_volume}mL)"
+ if is_solid:
+ summary_msg += f" ({final_mass}g)"
+
+ action_sequence.append(create_action_log(summary_msg, "🎉"))
+
return action_sequence
# === 便捷函数 ===
@@ -520,6 +618,7 @@ def generate_add_protocol(
def add_liquid_volume(G: nx.DiGraph, vessel: str, reagent: str, volume: Union[str, float],
time: Union[str, float] = 0.0, rate_spec: str = "") -> List[Dict[str, Any]]:
"""添加指定体积的液体试剂"""
+ debug_print(f"💧 快速添加液体: {reagent} ({volume}) → {vessel}")
return generate_add_protocol(
G, vessel, reagent,
volume=volume,
@@ -530,6 +629,7 @@ def add_liquid_volume(G: nx.DiGraph, vessel: str, reagent: str, volume: Union[st
def add_solid_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, float],
event: str = "") -> List[Dict[str, Any]]:
"""添加指定质量的固体试剂"""
+ debug_print(f"🧂 快速添加固体: {reagent} ({mass}) → {vessel}")
return generate_add_protocol(
G, vessel, reagent,
mass=mass,
@@ -539,6 +639,7 @@ def add_solid_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, fl
def add_solid_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str,
event: str = "") -> List[Dict[str, Any]]:
"""按摩尔数添加固体试剂"""
+ debug_print(f"🧬 按摩尔数添加固体: {reagent} ({mol}) → {vessel}")
return generate_add_protocol(
G, vessel, reagent,
mol=mol,
@@ -548,6 +649,7 @@ def add_solid_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str,
def add_dropwise_liquid(G: nx.DiGraph, vessel: str, reagent: str, volume: Union[str, float],
time: Union[str, float] = "20 min", event: str = "") -> List[Dict[str, Any]]:
"""滴加液体试剂"""
+ debug_print(f"💧 滴加液体: {reagent} ({volume}) → {vessel} (用时: {time})")
return generate_add_protocol(
G, vessel, reagent,
volume=volume,
@@ -559,6 +661,7 @@ def add_dropwise_liquid(G: nx.DiGraph, vessel: str, reagent: str, volume: Union[
def add_portionwise_solid(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, float],
time: Union[str, float] = "1 h", event: str = "") -> List[Dict[str, Any]]:
"""分批添加固体试剂"""
+ debug_print(f"🧂 分批添加固体: {reagent} ({mass}) → {vessel} (用时: {time})")
return generate_add_protocol(
G, vessel, reagent,
mass=mass,
@@ -573,22 +676,25 @@ def test_add_protocol():
print("=== ADD PROTOCOL 增强版测试 ===")
# 测试体积解析
+ debug_print("🧪 测试体积解析...")
volumes = ["2.7 mL", "2.67 mL", "?", 10.0, "1 L", "500 μL"]
for vol in volumes:
result = parse_volume_input(vol)
- print(f"体积解析: {vol} → {result}mL")
+ print(f"📏 体积解析: {vol} → {result}mL")
# 测试质量解析
+ debug_print("⚖️ 测试质量解析...")
masses = ["19.3 g", "4.5 g", 2.5, "500 mg", "1 kg"]
for mass in masses:
result = parse_mass_input(mass)
- print(f"质量解析: {mass} → {result}g")
+ print(f"⚖️ 质量解析: {mass} → {result}g")
# 测试时间解析
+ debug_print("⏱️ 测试时间解析...")
times = ["1 h", "20 min", "30 s", 60.0, "?"]
for time in times:
result = parse_time_input(time)
- print(f"时间解析: {time} → {result}s")
+ print(f"⏱️ 时间解析: {time} → {result}s")
print("✅ 测试完成")
diff --git a/unilabos/compile/adjustph_protocol.py b/unilabos/compile/adjustph_protocol.py
index ce7c1c3f3..d8f1b1b64 100644
--- a/unilabos/compile/adjustph_protocol.py
+++ b/unilabos/compile/adjustph_protocol.py
@@ -1,7 +1,30 @@
import networkx as nx
+import logging
from typing import List, Dict, Any
from .pump_protocol import generate_pump_protocol_with_rinsing
+logger = logging.getLogger(__name__)
+
+def debug_print(message):
+ """调试输出"""
+ print(f"[ADJUST_PH] {message}", flush=True)
+ logger.info(f"[ADJUST_PH] {message}")
+
+# 🆕 创建进度日志动作
+def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]:
+ """创建一个动作日志"""
+ full_message = f"{emoji} {message}"
+ debug_print(full_message)
+ logger.info(full_message)
+ print(f"[ACTION] {full_message}", flush=True)
+
+ return {
+ "action_name": "wait",
+ "action_kwargs": {
+ "time": 0.1,
+ "log_message": full_message
+ }
+ }
def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str:
"""
@@ -14,7 +37,7 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str:
Returns:
str: 试剂容器ID
"""
- print(f"ADJUST_PH: 正在查找试剂 '{reagent}' 的容器...")
+ debug_print(f"🔍 正在查找试剂 '{reagent}' 的容器...")
# 常见酸碱试剂的别名映射
reagent_aliases = {
@@ -29,11 +52,16 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str:
# 构建搜索名称列表
search_names = [reagent.lower()]
+ debug_print(f"📋 基础搜索名称: {reagent.lower()}")
# 添加别名
for base_name, aliases in reagent_aliases.items():
if reagent.lower() in base_name.lower() or base_name.lower() in reagent.lower():
search_names.extend([alias.lower() for alias in aliases])
+ debug_print(f"🔗 添加别名: {aliases}")
+ break
+
+ debug_print(f"📝 完整搜索列表: {search_names}")
# 构建可能的容器名称
possible_names = []
@@ -49,13 +77,17 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str:
name_clean
])
+ debug_print(f"🎯 可能的容器名称 (前5个): {possible_names[:5]}... (共{len(possible_names)}个)")
+
# 第一步:通过容器名称匹配
+ debug_print(f"📋 方法1: 精确名称匹配...")
for vessel_name in possible_names:
if vessel_name in G.nodes():
- print(f"ADJUST_PH: 通过名称匹配找到容器: {vessel_name}")
+ debug_print(f"✅ 通过名称匹配找到容器: {vessel_name} 🎯")
return vessel_name
# 第二步:通过模糊匹配
+ debug_print(f"📋 方法2: 模糊名称匹配...")
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
node_name = G.nodes[node_id].get('name', '').lower()
@@ -63,10 +95,11 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str:
# 检查是否包含任何搜索名称
for search_name in search_names:
if search_name in node_id.lower() or search_name in node_name:
- print(f"ADJUST_PH: 通过模糊匹配找到容器: {node_id}")
+ debug_print(f"✅ 通过模糊匹配找到容器: {node_id} 🔍")
return node_id
# 第三步:通过液体类型匹配
+ debug_print(f"📋 方法3: 液体类型匹配...")
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
vessel_data = G.nodes[node_id].get('data', {})
@@ -79,10 +112,11 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str:
for search_name in search_names:
if search_name in liquid_type or search_name in reagent_name:
- print(f"ADJUST_PH: 通过液体类型匹配找到容器: {node_id}")
+ debug_print(f"✅ 通过液体类型匹配找到容器: {node_id} 💧")
return node_id
# 列出可用容器帮助调试
+ debug_print(f"📊 列出可用容器帮助调试...")
available_containers = []
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
@@ -98,67 +132,92 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str:
'reagent_name': vessel_data.get('reagent_name', '')
})
- print(f"ADJUST_PH: 可用容器列表:")
+ debug_print(f"📋 可用容器列表:")
for container in available_containers:
- print(f" - {container['id']}: {container['name']}")
- print(f" 液体: {container['liquids']}")
- print(f" 试剂: {container['reagent_name']}")
+ debug_print(f" - 🧪 {container['id']}: {container['name']}")
+ debug_print(f" 💧 液体: {container['liquids']}")
+ debug_print(f" 🏷️ 试剂: {container['reagent_name']}")
- raise ValueError(f"找不到试剂 '{reagent}' 对应的容器。尝试了: {possible_names}")
-
+ debug_print(f"❌ 所有匹配方法都失败了")
+ raise ValueError(f"找不到试剂 '{reagent}' 对应的容器。尝试了: {possible_names[:10]}...")
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
"""查找与容器相连的搅拌器"""
+ debug_print(f"🔍 查找连接到容器 '{vessel}' 的搅拌器...")
+
stirrer_nodes = [node for node in G.nodes()
if (G.nodes[node].get('class') or '') == 'virtual_stirrer']
+ debug_print(f"📊 发现 {len(stirrer_nodes)} 个搅拌器: {stirrer_nodes}")
+
for stirrer in stirrer_nodes:
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
+ debug_print(f"✅ 找到连接的搅拌器: {stirrer} 🔗")
return stirrer
- return stirrer_nodes[0] if stirrer_nodes else None
-
+ if stirrer_nodes:
+ debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个: {stirrer_nodes[0]} 🔄")
+ return stirrer_nodes[0]
+
+ debug_print(f"❌ 未找到任何搅拌器")
+ return None
-def calculate_reagent_volume(target_ph_value: float, reagent: str, vessel_volume: float = 100.0) -> float: # 改为 target_ph_value
+def calculate_reagent_volume(target_ph_value: float, reagent: str, vessel_volume: float = 100.0) -> float:
"""
估算需要的试剂体积来调节pH
Args:
- target_ph_value: 目标pH值 # 改为 target_ph_value
+ target_ph_value: 目标pH值
reagent: 试剂名称
vessel_volume: 容器体积 (mL)
Returns:
float: 估算的试剂体积 (mL)
"""
+ debug_print(f"🧮 计算试剂体积...")
+ debug_print(f" 📍 目标pH: {target_ph_value}")
+ debug_print(f" 🧪 试剂: {reagent}")
+ debug_print(f" 📏 容器体积: {vessel_volume}mL")
+
# 简化的pH调节体积估算(实际应用中需要更精确的计算)
if "acid" in reagent.lower() or "hcl" in reagent.lower():
+ debug_print(f"🍋 检测到酸性试剂")
# 酸性试剂:pH越低需要的体积越大
- if target_ph_value < 3: # 改为 target_ph_value
- return vessel_volume * 0.05 # 5%
- elif target_ph_value < 5: # 改为 target_ph_value
- return vessel_volume * 0.02 # 2%
+ if target_ph_value < 3:
+ volume = vessel_volume * 0.05 # 5%
+ debug_print(f" 💪 强酸性 (pH<3): 使用 5% 体积")
+ elif target_ph_value < 5:
+ volume = vessel_volume * 0.02 # 2%
+ debug_print(f" 🔸 中酸性 (pH<5): 使用 2% 体积")
else:
- return vessel_volume * 0.01 # 1%
+ volume = vessel_volume * 0.01 # 1%
+ debug_print(f" 🔹 弱酸性 (pH≥5): 使用 1% 体积")
elif "hydroxide" in reagent.lower() or "naoh" in reagent.lower():
+ debug_print(f"🧂 检测到碱性试剂")
# 碱性试剂:pH越高需要的体积越大
- if target_ph_value > 11: # 改为 target_ph_value
- return vessel_volume * 0.05 # 5%
- elif target_ph_value > 9: # 改为 target_ph_value
- return vessel_volume * 0.02 # 2%
+ if target_ph_value > 11:
+ volume = vessel_volume * 0.05 # 5%
+ debug_print(f" 💪 强碱性 (pH>11): 使用 5% 体积")
+ elif target_ph_value > 9:
+ volume = vessel_volume * 0.02 # 2%
+ debug_print(f" 🔸 中碱性 (pH>9): 使用 2% 体积")
else:
- return vessel_volume * 0.01 # 1%
+ volume = vessel_volume * 0.01 # 1%
+ debug_print(f" 🔹 弱碱性 (pH≤9): 使用 1% 体积")
else:
# 未知试剂,使用默认值
- return vessel_volume * 0.01
-
+ volume = vessel_volume * 0.01
+ debug_print(f"❓ 未知试剂类型,使用默认 1% 体积")
+
+ debug_print(f"📊 计算结果: {volume:.2f}mL")
+ return volume
def generate_adjust_ph_protocol(
G: nx.DiGraph,
vessel: str,
- ph_value: float, # 改为 ph_value
+ ph_value: float,
reagent: str,
**kwargs
) -> List[Dict[str, Any]]:
@@ -168,13 +227,23 @@ def generate_adjust_ph_protocol(
Args:
G: 有向图,节点为容器和设备
vessel: 目标容器(需要调节pH的容器)
- ph_value: 目标pH值(从XDL传入) # 改为 ph_value
+ ph_value: 目标pH值(从XDL传入)
reagent: 酸碱试剂名称(从XDL传入)
**kwargs: 其他可选参数,使用默认值
Returns:
List[Dict[str, Any]]: 动作序列
"""
+
+ debug_print("=" * 60)
+ debug_print("🧪 开始生成pH调节协议")
+ debug_print(f"📋 原始参数:")
+ debug_print(f" 🥼 vessel: '{vessel}'")
+ debug_print(f" 📊 ph_value: {ph_value}")
+ debug_print(f" 🧪 reagent: '{reagent}'")
+ debug_print(f" 📦 kwargs: {kwargs}")
+ debug_print("=" * 60)
+
action_sequence = []
# 从kwargs中获取可选参数,如果没有则使用默认值
@@ -184,48 +253,84 @@ def generate_adjust_ph_protocol(
stir_time = kwargs.get('stir_time', 60.0) # 默认搅拌时间
settling_time = kwargs.get('settling_time', 30.0) # 默认平衡时间
- print(f"ADJUST_PH: 开始生成pH调节协议")
- print(f" - 目标容器: {vessel}")
- print(f" - 目标pH: {ph_value}") # 改为 ph_value
- print(f" - 试剂: {reagent}")
- print(f" - 使用默认参数: 体积=自动估算, 搅拌=True, 搅拌速度=300RPM")
+ debug_print(f"🔧 处理后的参数:")
+ debug_print(f" 📏 volume: {volume}mL (0.0表示自动估算)")
+ debug_print(f" 🌪️ stir: {stir}")
+ debug_print(f" 🔄 stir_speed: {stir_speed}rpm")
+ debug_print(f" ⏱️ stir_time: {stir_time}s")
+ debug_print(f" ⏳ settling_time: {settling_time}s")
+
+ # 开始处理
+ action_sequence.append(create_action_log(f"开始调节pH至 {ph_value}", "🧪"))
+ action_sequence.append(create_action_log(f"目标容器: {vessel}", "🥼"))
+ action_sequence.append(create_action_log(f"使用试剂: {reagent}", "⚗️"))
# 1. 验证目标容器存在
+ debug_print(f"🔍 步骤1: 验证目标容器...")
if vessel not in G.nodes():
+ debug_print(f"❌ 目标容器 '{vessel}' 不存在于系统中")
raise ValueError(f"目标容器 '{vessel}' 不存在于系统中")
+ debug_print(f"✅ 目标容器验证通过")
+ action_sequence.append(create_action_log("目标容器验证通过", "✅"))
+
# 2. 查找酸碱试剂容器
+ debug_print(f"🔍 步骤2: 查找试剂容器...")
+ action_sequence.append(create_action_log("正在查找试剂容器...", "🔍"))
+
try:
reagent_vessel = find_acid_base_vessel(G, reagent)
- print(f"ADJUST_PH: 找到试剂容器: {reagent_vessel}")
+ debug_print(f"✅ 找到试剂容器: {reagent_vessel}")
+ action_sequence.append(create_action_log(f"找到试剂容器: {reagent_vessel}", "🧪"))
except ValueError as e:
+ debug_print(f"❌ 无法找到试剂容器: {str(e)}")
+ action_sequence.append(create_action_log(f"试剂容器查找失败: {str(e)}", "❌"))
raise ValueError(f"无法找到试剂 '{reagent}': {str(e)}")
- # 3. 如果未指定体积,自动估算
+ # 3. 体积估算
+ debug_print(f"🔍 步骤3: 体积处理...")
if volume <= 0:
+ action_sequence.append(create_action_log("开始自动估算试剂体积", "🧮"))
+
# 获取目标容器的体积信息
vessel_data = G.nodes[vessel].get('data', {})
vessel_volume = vessel_data.get('max_volume', 100.0) # 默认100mL
+ debug_print(f"📏 容器最大体积: {vessel_volume}mL")
- estimated_volume = calculate_reagent_volume(ph_value, reagent, vessel_volume) # 改为 ph_value
+ estimated_volume = calculate_reagent_volume(ph_value, reagent, vessel_volume)
volume = estimated_volume
- print(f"ADJUST_PH: 自动估算试剂体积: {volume:.2f} mL")
+ debug_print(f"✅ 自动估算试剂体积: {volume:.2f} mL")
+ action_sequence.append(create_action_log(f"估算试剂体积: {volume:.2f}mL", "📊"))
+ else:
+ debug_print(f"📏 使用指定体积: {volume}mL")
+ action_sequence.append(create_action_log(f"使用指定体积: {volume}mL", "📏"))
# 4. 验证路径存在
+ debug_print(f"🔍 步骤4: 路径验证...")
+ action_sequence.append(create_action_log("验证转移路径...", "🛤️"))
+
try:
path = nx.shortest_path(G, source=reagent_vessel, target=vessel)
- print(f"ADJUST_PH: 找到路径: {' → '.join(path)}")
+ debug_print(f"✅ 找到路径: {' → '.join(path)}")
+ action_sequence.append(create_action_log(f"找到转移路径: {' → '.join(path)}", "🛤️"))
except nx.NetworkXNoPath:
+ debug_print(f"❌ 无法找到转移路径")
+ action_sequence.append(create_action_log("转移路径不存在", "❌"))
raise ValueError(f"从试剂容器 '{reagent_vessel}' 到目标容器 '{vessel}' 没有可用路径")
- # 5. 先启动搅拌(如果需要)
+ # 5. 搅拌器设置
+ debug_print(f"🔍 步骤5: 搅拌器设置...")
stirrer_id = None
if stir:
+ action_sequence.append(create_action_log("准备启动搅拌器", "🌪️"))
+
try:
stirrer_id = find_connected_stirrer(G, vessel)
if stirrer_id:
- print(f"ADJUST_PH: 找到搅拌器 {stirrer_id},启动搅拌")
+ debug_print(f"✅ 找到搅拌器 {stirrer_id},启动搅拌")
+ action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {stir_speed}rpm)", "🔄"))
+
action_sequence.append({
"device_id": stirrer_id,
"action_name": "start_stir",
@@ -237,23 +342,34 @@ def generate_adjust_ph_protocol(
})
# 等待搅拌稳定
+ action_sequence.append(create_action_log("等待搅拌稳定...", "⏳"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5}
})
else:
- print(f"ADJUST_PH: 警告 - 未找到搅拌器,继续执行")
+ debug_print(f"⚠️ 未找到搅拌器,继续执行")
+ action_sequence.append(create_action_log("未找到搅拌器,跳过搅拌", "⚠️"))
except Exception as e:
- print(f"ADJUST_PH: 搅拌器配置出错: {str(e)}")
+ debug_print(f"❌ 搅拌器配置出错: {str(e)}")
+ action_sequence.append(create_action_log(f"搅拌器配置失败: {str(e)}", "❌"))
+ else:
+ debug_print(f"📋 跳过搅拌设置")
+ action_sequence.append(create_action_log("跳过搅拌设置", "⏭️"))
- # 6. 缓慢添加试剂 - 使用pump_protocol
- print(f"ADJUST_PH: 开始添加试剂 {volume:.2f} mL")
+ # 6. 试剂添加
+ debug_print(f"🔍 步骤6: 试剂添加...")
+ action_sequence.append(create_action_log(f"开始添加试剂 {volume:.2f}mL", "🚰"))
# 计算添加时间(pH调节需要缓慢添加)
addition_time = max(30.0, volume * 2.0) # 至少30秒,每mL需要2秒
+ debug_print(f"⏱️ 计算添加时间: {addition_time}s (缓慢注入)")
+ action_sequence.append(create_action_log(f"设置添加时间: {addition_time:.0f}s (缓慢注入)", "⏱️"))
try:
+ action_sequence.append(create_action_log("调用泵协议进行试剂转移", "🔄"))
+
pump_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=reagent_vessel,
@@ -266,17 +382,24 @@ def generate_adjust_ph_protocol(
rinsing_volume=0.0,
rinsing_repeats=0,
solid=False,
- flowrate=0.5 # 缓慢注入
+ flowrate=0.5, # 缓慢注入
+ transfer_flowrate=0.3
)
action_sequence.extend(pump_actions)
+ debug_print(f"✅ 泵协议生成完成,添加了 {len(pump_actions)} 个动作")
+ action_sequence.append(create_action_log(f"试剂转移完成 ({len(pump_actions)} 个操作)", "✅"))
except Exception as e:
+ debug_print(f"❌ 生成泵协议时出错: {str(e)}")
+ action_sequence.append(create_action_log(f"泵协议生成失败: {str(e)}", "❌"))
raise ValueError(f"生成泵协议时出错: {str(e)}")
- # 7. 持续搅拌以混合和平衡
+ # 7. 混合搅拌
if stir and stirrer_id:
- print(f"ADJUST_PH: 持续搅拌 {stir_time} 秒以混合试剂")
+ debug_print(f"🔍 步骤7: 混合搅拌...")
+ action_sequence.append(create_action_log(f"开始混合搅拌 {stir_time:.0f}s", "🌀"))
+
action_sequence.append({
"device_id": stirrer_id,
"action_name": "stir",
@@ -284,25 +407,47 @@ def generate_adjust_ph_protocol(
"stir_time": stir_time,
"stir_speed": stir_speed,
"settling_time": settling_time,
- "purpose": f"pH调节: 混合试剂,目标pH={ph_value}" # 改为 ph_value
+ "purpose": f"pH调节: 混合试剂,目标pH={ph_value}"
}
})
+
+ debug_print(f"✅ 混合搅拌设置完成")
+ else:
+ debug_print(f"⏭️ 跳过混合搅拌")
+ action_sequence.append(create_action_log("跳过混合搅拌", "⏭️"))
+
+ # 8. 等待平衡
+ debug_print(f"🔍 步骤8: 反应平衡...")
+ action_sequence.append(create_action_log(f"等待pH平衡 {settling_time:.0f}s", "⚖️"))
- # 8. 等待反应平衡
action_sequence.append({
"action_name": "wait",
"action_kwargs": {
"time": settling_time,
- "description": f"等待pH平衡到目标值 {ph_value}" # 改为 ph_value
+ "description": f"等待pH平衡到目标值 {ph_value}"
}
})
- print(f"ADJUST_PH: 协议生成完成,共 {len(action_sequence)} 个动作")
- print(f"ADJUST_PH: 预计总时间: {addition_time + stir_time + settling_time:.0f} 秒")
+ # 9. 完成总结
+ total_time = addition_time + stir_time + settling_time
+
+ debug_print("=" * 60)
+ debug_print(f"🎉 pH调节协议生成完成")
+ debug_print(f"📊 协议统计:")
+ debug_print(f" 📋 总动作数: {len(action_sequence)}")
+ debug_print(f" ⏱️ 预计总时间: {total_time:.0f}s ({total_time/60:.1f}分钟)")
+ debug_print(f" 🧪 试剂: {reagent}")
+ debug_print(f" 📏 体积: {volume:.2f}mL")
+ debug_print(f" 📊 目标pH: {ph_value}")
+ debug_print(f" 🥼 目标容器: {vessel}")
+ debug_print("=" * 60)
+
+ # 添加完成日志
+ summary_msg = f"pH调节协议完成: {vessel} → pH {ph_value} (使用 {volume:.2f}mL {reagent})"
+ action_sequence.append(create_action_log(summary_msg, "🎉"))
return action_sequence
-
def generate_adjust_ph_protocol_stepwise(
G: nx.DiGraph,
vessel: str,
@@ -317,7 +462,7 @@ def generate_adjust_ph_protocol_stepwise(
Args:
G: 网络图
vessel: 目标容器
- pH: 目标pH值
+ ph_value: 目标pH值
reagent: 酸碱试剂
max_volume: 最大试剂体积
steps: 分步数量
@@ -325,15 +470,28 @@ def generate_adjust_ph_protocol_stepwise(
Returns:
List[Dict[str, Any]]: 动作序列
"""
- action_sequence = []
+ debug_print("=" * 60)
+ debug_print(f"🔄 开始分步pH调节")
+ debug_print(f"📋 分步参数:")
+ debug_print(f" 🥼 vessel: {vessel}")
+ debug_print(f" 📊 ph_value: {ph_value}")
+ debug_print(f" 🧪 reagent: {reagent}")
+ debug_print(f" 📏 max_volume: {max_volume}mL")
+ debug_print(f" 🔢 steps: {steps}")
+ debug_print("=" * 60)
- print(f"ADJUST_PH: 开始分步pH调节({steps}步)")
+ action_sequence = []
# 每步添加的体积
step_volume = max_volume / steps
+ debug_print(f"📊 每步体积: {step_volume:.2f}mL")
+
+ action_sequence.append(create_action_log(f"开始分步pH调节 ({steps}步)", "🔄"))
+ action_sequence.append(create_action_log(f"每步添加: {step_volume:.2f}mL", "📏"))
for i in range(steps):
- print(f"ADJUST_PH: 第 {i+1}/{steps} 步,添加 {step_volume} mL")
+ debug_print(f"🔄 执行第 {i+1}/{steps} 步,添加 {step_volume:.2f}mL")
+ action_sequence.append(create_action_log(f"第 {i+1}/{steps} 步开始", "🚀"))
# 生成单步协议
step_actions = generate_adjust_ph_protocol(
@@ -349,9 +507,13 @@ def generate_adjust_ph_protocol_stepwise(
)
action_sequence.extend(step_actions)
+ debug_print(f"✅ 第 {i+1}/{steps} 步完成,添加了 {len(step_actions)} 个动作")
+ action_sequence.append(create_action_log(f"第 {i+1}/{steps} 步完成", "✅"))
# 步骤间等待
if i < steps - 1:
+ debug_print(f"⏳ 步骤间等待30s")
+ action_sequence.append(create_action_log("步骤间等待...", "⏳"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {
@@ -360,10 +522,11 @@ def generate_adjust_ph_protocol_stepwise(
}
})
- print(f"ADJUST_PH: 分步pH调节完成")
+ debug_print(f"🎉 分步pH调节完成,共 {len(action_sequence)} 个动作")
+ action_sequence.append(create_action_log("分步pH调节全部完成", "🎉"))
+
return action_sequence
-
# 便捷函数:常用pH调节
def generate_acidify_protocol(
G: nx.DiGraph,
@@ -372,11 +535,11 @@ def generate_acidify_protocol(
acid: str = "hydrochloric acid"
) -> List[Dict[str, Any]]:
"""酸化协议"""
+ debug_print(f"🍋 生成酸化协议: {vessel} → pH {target_ph} (使用 {acid})")
return generate_adjust_ph_protocol(
- G, vessel, target_ph, acid, 0.0, True, 300.0, 120.0, 60.0
+ G, vessel, target_ph, acid
)
-
def generate_basify_protocol(
G: nx.DiGraph,
vessel: str,
@@ -384,28 +547,42 @@ def generate_basify_protocol(
base: str = "sodium hydroxide"
) -> List[Dict[str, Any]]:
"""碱化协议"""
+ debug_print(f"🧂 生成碱化协议: {vessel} → pH {target_ph} (使用 {base})")
return generate_adjust_ph_protocol(
- G, vessel, target_ph, base, 0.0, True, 300.0, 120.0, 60.0
+ G, vessel, target_ph, base
)
-
def generate_neutralize_protocol(
G: nx.DiGraph,
vessel: str,
reagent: str = "sodium hydroxide"
) -> List[Dict[str, Any]]:
"""中和协议(pH=7)"""
+ debug_print(f"⚖️ 生成中和协议: {vessel} → pH 7.0 (使用 {reagent})")
return generate_adjust_ph_protocol(
- G, vessel, 7.0, reagent, 0.0, True, 350.0, 180.0, 90.0
+ G, vessel, 7.0, reagent
)
-
# 测试函数
def test_adjust_ph_protocol():
"""测试pH调节协议"""
- print("=== ADJUST PH PROTOCOL 测试 ===")
- print("测试完成")
-
+ debug_print("=== ADJUST PH PROTOCOL 增强版测试 ===")
+
+ # 测试体积计算
+ debug_print("🧮 测试体积计算...")
+ test_cases = [
+ (2.0, "hydrochloric acid", 100.0),
+ (4.0, "hydrochloric acid", 100.0),
+ (12.0, "sodium hydroxide", 100.0),
+ (10.0, "sodium hydroxide", 100.0),
+ (7.0, "unknown reagent", 100.0)
+ ]
+
+ for ph, reagent, volume in test_cases:
+ result = calculate_reagent_volume(ph, reagent, volume)
+ debug_print(f"📊 {reagent} → pH {ph}: {result:.2f}mL")
+
+ debug_print("✅ 测试完成")
if __name__ == "__main__":
test_adjust_ph_protocol()
\ No newline at end of file
diff --git a/unilabos/compile/dissolve_protocol.py b/unilabos/compile/dissolve_protocol.py
index 9008e6d57..065196ef4 100644
--- a/unilabos/compile/dissolve_protocol.py
+++ b/unilabos/compile/dissolve_protocol.py
@@ -11,6 +11,22 @@ def debug_print(message):
print(f"[DISSOLVE] {message}", flush=True)
logger.info(f"[DISSOLVE] {message}")
+# 🆕 创建进度日志动作
+def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]:
+ """创建一个动作日志"""
+ full_message = f"{emoji} {message}"
+ debug_print(full_message)
+ logger.info(full_message)
+ print(f"[ACTION] {full_message}", flush=True)
+
+ return {
+ "action_name": "wait",
+ "action_kwargs": {
+ "time": 0.1,
+ "log_message": full_message
+ }
+ }
+
def parse_volume_input(volume_input: Union[str, float]) -> float:
"""
解析体积输入,支持带单位的字符串
@@ -22,18 +38,20 @@ def parse_volume_input(volume_input: Union[str, float]) -> float:
float: 体积(毫升)
"""
if isinstance(volume_input, (int, float)):
+ debug_print(f"📏 体积输入为数值: {volume_input}")
return float(volume_input)
if not volume_input or not str(volume_input).strip():
+ debug_print(f"⚠️ 体积输入为空,返回0.0mL")
return 0.0
volume_str = str(volume_input).lower().strip()
- debug_print(f"解析体积输入: '{volume_str}'")
+ debug_print(f"🔍 解析体积输入: '{volume_str}'")
# 处理未知体积
if volume_str in ['?', 'unknown', 'tbd', 'to be determined']:
default_volume = 50.0 # 默认50mL
- debug_print(f"检测到未知体积,使用默认值: {default_volume}mL")
+ debug_print(f"❓ 检测到未知体积,使用默认值: {default_volume}mL 🎯")
return default_volume
# 移除空格并提取数字和单位
@@ -43,7 +61,7 @@ def parse_volume_input(volume_input: Union[str, float]) -> float:
match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
if not match:
- debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值50mL")
+ debug_print(f"❌ 无法解析体积: '{volume_str}',使用默认值50mL")
return 50.0
value = float(match.group(1))
@@ -52,12 +70,14 @@ def parse_volume_input(volume_input: Union[str, float]) -> float:
# 转换为毫升
if unit in ['l', 'liter']:
volume = value * 1000.0 # L -> mL
+ debug_print(f"🔄 体积转换: {value}L → {volume}mL")
elif unit in ['μl', 'ul', 'microliter']:
volume = value / 1000.0 # μL -> mL
+ debug_print(f"🔄 体积转换: {value}μL → {volume}mL")
else: # ml, milliliter 或默认
volume = value # 已经是mL
+ debug_print(f"✅ 体积已为mL: {volume}mL")
- debug_print(f"体积转换: {value}{unit} → {volume}mL")
return volume
def parse_mass_input(mass_input: Union[str, float]) -> float:
@@ -71,18 +91,20 @@ def parse_mass_input(mass_input: Union[str, float]) -> float:
float: 质量(克)
"""
if isinstance(mass_input, (int, float)):
+ debug_print(f"⚖️ 质量输入为数值: {mass_input}g")
return float(mass_input)
if not mass_input or not str(mass_input).strip():
+ debug_print(f"⚠️ 质量输入为空,返回0.0g")
return 0.0
mass_str = str(mass_input).lower().strip()
- debug_print(f"解析质量输入: '{mass_str}'")
+ debug_print(f"🔍 解析质量输入: '{mass_str}'")
# 处理未知质量
if mass_str in ['?', 'unknown', 'tbd', 'to be determined']:
default_mass = 1.0 # 默认1g
- debug_print(f"检测到未知质量,使用默认值: {default_mass}g")
+ debug_print(f"❓ 检测到未知质量,使用默认值: {default_mass}g 🎯")
return default_mass
# 移除空格并提取数字和单位
@@ -92,7 +114,7 @@ def parse_mass_input(mass_input: Union[str, float]) -> float:
match = re.match(r'([0-9]*\.?[0-9]+)\s*(g|mg|kg|gram|milligram|kilogram)?', mass_clean)
if not match:
- debug_print(f"⚠️ 无法解析质量: '{mass_str}',返回0.0g")
+ debug_print(f"❌ 无法解析质量: '{mass_str}',返回0.0g")
return 0.0
value = float(match.group(1))
@@ -101,12 +123,14 @@ def parse_mass_input(mass_input: Union[str, float]) -> float:
# 转换为克
if unit in ['mg', 'milligram']:
mass = value / 1000.0 # mg -> g
+ debug_print(f"🔄 质量转换: {value}mg → {mass}g")
elif unit in ['kg', 'kilogram']:
mass = value * 1000.0 # kg -> g
+ debug_print(f"🔄 质量转换: {value}kg → {mass}g")
else: # g, gram 或默认
mass = value # 已经是g
+ debug_print(f"✅ 质量已为g: {mass}g")
- debug_print(f"质量转换: {value}{unit} → {mass}g")
return mass
def parse_time_input(time_input: Union[str, float]) -> float:
@@ -120,18 +144,20 @@ def parse_time_input(time_input: Union[str, float]) -> float:
float: 时间(秒)
"""
if isinstance(time_input, (int, float)):
+ debug_print(f"⏱️ 时间输入为数值: {time_input}秒")
return float(time_input)
if not time_input or not str(time_input).strip():
+ debug_print(f"⚠️ 时间输入为空,返回0秒")
return 0.0
time_str = str(time_input).lower().strip()
- debug_print(f"解析时间输入: '{time_str}'")
+ debug_print(f"🔍 解析时间输入: '{time_str}'")
# 处理未知时间
if time_str in ['?', 'unknown', 'tbd']:
default_time = 600.0 # 默认10分钟
- debug_print(f"检测到未知时间,使用默认值: {default_time}s")
+ debug_print(f"❓ 检测到未知时间,使用默认值: {default_time}s (10分钟) ⏰")
return default_time
# 移除空格并提取数字和单位
@@ -141,7 +167,7 @@ def parse_time_input(time_input: Union[str, float]) -> float:
match = re.match(r'([0-9]*\.?[0-9]+)\s*(s|sec|second|min|minute|h|hr|hour|d|day)?', time_clean)
if not match:
- debug_print(f"⚠️ 无法解析时间: '{time_str}',返回0s")
+ debug_print(f"❌ 无法解析时间: '{time_str}',返回0s")
return 0.0
value = float(match.group(1))
@@ -150,14 +176,17 @@ def parse_time_input(time_input: Union[str, float]) -> float:
# 转换为秒
if unit in ['min', 'minute']:
time_sec = value * 60.0 # min -> s
+ debug_print(f"🔄 时间转换: {value}分钟 → {time_sec}秒")
elif unit in ['h', 'hr', 'hour']:
time_sec = value * 3600.0 # h -> s
+ debug_print(f"🔄 时间转换: {value}小时 → {time_sec}秒")
elif unit in ['d', 'day']:
time_sec = value * 86400.0 # d -> s
+ debug_print(f"🔄 时间转换: {value}天 → {time_sec}秒")
else: # s, sec, second 或默认
time_sec = value # 已经是s
+ debug_print(f"✅ 时间已为秒: {time_sec}秒")
- debug_print(f"时间转换: {value}{unit} → {time_sec}s")
return time_sec
def parse_temperature_input(temp_input: Union[str, float]) -> float:
@@ -171,13 +200,15 @@ def parse_temperature_input(temp_input: Union[str, float]) -> float:
float: 温度(摄氏度)
"""
if isinstance(temp_input, (int, float)):
+ debug_print(f"🌡️ 温度输入为数值: {temp_input}°C")
return float(temp_input)
if not temp_input or not str(temp_input).strip():
+ debug_print(f"⚠️ 温度输入为空,使用默认室温25°C")
return 25.0 # 默认室温
temp_str = str(temp_input).lower().strip()
- debug_print(f"解析温度输入: '{temp_str}'")
+ debug_print(f"🔍 解析温度输入: '{temp_str}'")
# 处理特殊温度描述
temp_aliases = {
@@ -193,7 +224,7 @@ def parse_temperature_input(temp_input: Union[str, float]) -> float:
if temp_str in temp_aliases:
result = temp_aliases[temp_str]
- debug_print(f"温度别名解析: '{temp_str}' → {result}°C")
+ debug_print(f"🏷️ 温度别名解析: '{temp_str}' → {result}°C")
return result
# 移除空格并提取数字和单位
@@ -203,7 +234,7 @@ def parse_temperature_input(temp_input: Union[str, float]) -> float:
match = re.match(r'([0-9]*\.?[0-9]+)\s*(°c|c|celsius|°f|f|fahrenheit|k|kelvin)?', temp_clean)
if not match:
- debug_print(f"⚠️ 无法解析温度: '{temp_str}',使用默认值25°C")
+ debug_print(f"❌ 无法解析温度: '{temp_str}',使用默认值25°C")
return 25.0
value = float(match.group(1))
@@ -212,19 +243,22 @@ def parse_temperature_input(temp_input: Union[str, float]) -> float:
# 转换为摄氏度
if unit in ['°f', 'f', 'fahrenheit']:
temp_c = (value - 32) * 5/9 # F -> C
+ debug_print(f"🔄 温度转换: {value}°F → {temp_c:.1f}°C")
elif unit in ['k', 'kelvin']:
temp_c = value - 273.15 # K -> C
+ debug_print(f"🔄 温度转换: {value}K → {temp_c:.1f}°C")
else: # °c, c, celsius 或默认
temp_c = value # 已经是C
+ debug_print(f"✅ 温度已为°C: {temp_c}°C")
- debug_print(f"温度转换: {value}{unit} → {temp_c}°C")
return temp_c
def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
- """增强版溶剂容器查找"""
- debug_print(f"查找溶剂 '{solvent}' 的容器...")
+ """增强版溶剂容器查找,支持多种匹配模式"""
+ debug_print(f"🔍 开始查找溶剂 '{solvent}' 的容器...")
# 🔧 方法1:直接搜索 data.reagent_name 和 config.reagent
+ debug_print(f"📋 方法1: 搜索reagent字段...")
for node in G.nodes():
node_data = G.nodes[node].get('data', {})
node_type = G.nodes[node].get('type', '')
@@ -237,18 +271,20 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
# 精确匹配
if reagent_name == solvent.lower() or config_reagent == solvent.lower():
- debug_print(f"✅ 通过reagent字段找到容器: {node}")
+ debug_print(f"✅ 通过reagent字段精确匹配到容器: {node} 🎯")
return node
# 模糊匹配
if (solvent.lower() in reagent_name and reagent_name) or \
(solvent.lower() in config_reagent and config_reagent):
- debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node}")
+ debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node} 🔍")
return node
# 🔧 方法2:常见的容器命名规则
+ debug_print(f"📋 方法2: 使用命名规则查找...")
solvent_clean = solvent.lower().replace(' ', '_').replace('-', '_')
possible_names = [
+ solvent_clean,
f"flask_{solvent_clean}",
f"bottle_{solvent_clean}",
f"vessel_{solvent_clean}",
@@ -256,77 +292,118 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
f"{solvent_clean}_bottle",
f"solvent_{solvent_clean}",
f"reagent_{solvent_clean}",
- f"reagent_bottle_{solvent_clean}"
+ f"reagent_bottle_{solvent_clean}",
+ f"reagent_bottle_1", # 通用试剂瓶
+ f"reagent_bottle_2",
+ f"reagent_bottle_3"
]
+ debug_print(f"🔍 尝试的容器名称: {possible_names[:5]}... (共{len(possible_names)}个)")
+
for name in possible_names:
if name in G.nodes():
node_type = G.nodes[name].get('type', '')
if node_type == 'container':
- debug_print(f"✅ 通过命名规则找到容器: {name}")
+ debug_print(f"✅ 通过命名规则找到容器: {name} 📝")
return name
- # 🔧 方法3:使用第一个试剂瓶作为备选
+ # 🔧 方法3:节点名称模糊匹配
+ debug_print(f"📋 方法3: 节点名称模糊匹配...")
+ for node_id in G.nodes():
+ node_data = G.nodes[node_id]
+ if node_data.get('type') == 'container':
+ # 检查节点名称是否包含溶剂名称
+ if solvent_clean in node_id.lower():
+ debug_print(f"✅ 通过节点名称模糊匹配到容器: {node_id} 🔍")
+ return node_id
+
+ # 检查液体类型匹配
+ vessel_data = node_data.get('data', {})
+ liquids = vessel_data.get('liquid', [])
+ for liquid in liquids:
+ if isinstance(liquid, dict):
+ liquid_type = liquid.get('liquid_type') or liquid.get('name', '')
+ if liquid_type.lower() == solvent.lower():
+ debug_print(f"✅ 通过液体类型匹配到容器: {node_id} 💧")
+ return node_id
+
+ # 🔧 方法4:使用第一个试剂瓶作为备选
+ debug_print(f"📋 方法4: 查找备选试剂瓶...")
for node_id in G.nodes():
node_data = G.nodes[node_id]
if (node_data.get('type') == 'container' and
('reagent' in node_id.lower() or 'bottle' in node_id.lower() or 'flask' in node_id.lower())):
- debug_print(f"⚠️ 未找到专用容器,使用备选容器: {node_id}")
+ debug_print(f"⚠️ 未找到专用容器,使用备选试剂瓶: {node_id} 🔄")
return node_id
+ debug_print(f"❌ 所有方法都失败了,无法找到容器!")
raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器")
def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str:
"""查找连接到指定容器的加热搅拌器"""
+ debug_print(f"🔍 查找连接到容器 '{vessel}' 的加热搅拌器...")
+
heatchill_nodes = []
for node in G.nodes():
node_class = G.nodes[node].get('class', '').lower()
if 'heatchill' in node_class:
heatchill_nodes.append(node)
+ debug_print(f"📋 发现加热搅拌器: {node}")
+
+ debug_print(f"📊 共找到 {len(heatchill_nodes)} 个加热搅拌器")
# 查找连接到容器的加热器
for heatchill in heatchill_nodes:
if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill):
- debug_print(f"找到连接的加热器: {heatchill}")
+ debug_print(f"✅ 找到连接的加热搅拌器: {heatchill} 🔗")
return heatchill
# 返回第一个加热器
if heatchill_nodes:
- debug_print(f"使用第一个加热器: {heatchill_nodes[0]}")
+ debug_print(f"⚠️ 未找到直接连接的加热搅拌器,使用第一个: {heatchill_nodes[0]} 🔄")
return heatchill_nodes[0]
+ debug_print(f"❌ 未找到任何加热搅拌器")
return ""
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
"""查找连接到指定容器的搅拌器"""
+ debug_print(f"🔍 查找连接到容器 '{vessel}' 的搅拌器...")
+
stirrer_nodes = []
for node in G.nodes():
node_class = G.nodes[node].get('class', '').lower()
if 'stirrer' in node_class:
stirrer_nodes.append(node)
+ debug_print(f"📋 发现搅拌器: {node}")
+
+ debug_print(f"📊 共找到 {len(stirrer_nodes)} 个搅拌器")
# 查找连接到容器的搅拌器
for stirrer in stirrer_nodes:
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
- debug_print(f"找到连接的搅拌器: {stirrer}")
+ debug_print(f"✅ 找到连接的搅拌器: {stirrer} 🔗")
return stirrer
# 返回第一个搅拌器
if stirrer_nodes:
- debug_print(f"使用第一个搅拌器: {stirrer_nodes[0]}")
+ debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个: {stirrer_nodes[0]} 🔄")
return stirrer_nodes[0]
+ debug_print(f"❌ 未找到任何搅拌器")
return ""
def find_solid_dispenser(G: nx.DiGraph) -> str:
"""查找固体加样器"""
+ debug_print(f"🔍 查找固体加样器...")
+
for node in G.nodes():
node_class = G.nodes[node].get('class', '').lower()
if 'solid_dispenser' in node_class or 'dispenser' in node_class:
- debug_print(f"找到固体加样器: {node}")
+ debug_print(f"✅ 找到固体加样器: {node} 🥄")
return node
- debug_print("⚠️ 未找到固体加样器")
+ debug_print(f"❌ 未找到固体加样器")
return ""
def generate_dissolve_protocol(
@@ -347,12 +424,13 @@ def generate_dissolve_protocol(
**kwargs # 🔧 关键:接受所有其他参数,防止unexpected keyword错误
) -> List[Dict[str, Any]]:
"""
- 生成溶解操作的协议序列 - 修复版
+ 生成溶解操作的协议序列 - 增强版
🔧 修复要点:
1. 添加action文件中的所有参数(mass, mol, reagent, event)
2. 使用 **kwargs 接受所有额外参数,防止 unexpected keyword argument 错误
3. 支持固体溶解和液体溶解两种模式
+ 4. 添加详细的emoji日志系统
支持两种溶解模式:
1. 液体溶解:指定 solvent + volume,使用pump protocol转移溶剂
@@ -367,35 +445,40 @@ def generate_dissolve_protocol(
"""
debug_print("=" * 60)
- debug_print("开始生成溶解协议 - 修复版")
- debug_print(f"原始参数:")
- debug_print(f" - vessel: '{vessel}'")
- debug_print(f" - solvent: '{solvent}'")
- debug_print(f" - volume: {volume} (类型: {type(volume)})")
- debug_print(f" - mass: {mass} (类型: {type(mass)})")
- debug_print(f" - temp: {temp} (类型: {type(temp)})")
- debug_print(f" - time: {time} (类型: {type(time)})")
- debug_print(f" - reagent: '{reagent}'")
- debug_print(f" - mol: '{mol}'")
- debug_print(f" - event: '{event}'")
- debug_print(f" - kwargs: {kwargs}") # 显示额外参数
+ debug_print("🧪 开始生成溶解协议")
+ debug_print(f"📋 原始参数:")
+ debug_print(f" 🥼 vessel: '{vessel}'")
+ debug_print(f" 💧 solvent: '{solvent}'")
+ debug_print(f" 📏 volume: {volume} (类型: {type(volume)})")
+ debug_print(f" ⚖️ mass: {mass} (类型: {type(mass)})")
+ debug_print(f" 🌡️ temp: {temp} (类型: {type(temp)})")
+ debug_print(f" ⏱️ time: {time} (类型: {type(time)})")
+ debug_print(f" 🧪 reagent: '{reagent}'")
+ debug_print(f" 🧬 mol: '{mol}'")
+ debug_print(f" 🎯 event: '{event}'")
+ debug_print(f" 📦 kwargs: {kwargs}") # 显示额外参数
debug_print("=" * 60)
action_sequence = []
# === 参数验证 ===
- debug_print("步骤1: 参数验证...")
+ debug_print("🔍 步骤1: 参数验证...")
+ action_sequence.append(create_action_log(f"开始溶解操作 - 容器: {vessel}", "🎬"))
if not vessel:
+ debug_print("❌ vessel 参数不能为空")
raise ValueError("vessel 参数不能为空")
if vessel not in G.nodes():
+ debug_print(f"❌ 容器 '{vessel}' 不存在于系统中")
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
debug_print("✅ 基本参数验证通过")
+ action_sequence.append(create_action_log("参数验证通过", "✅"))
# === 🔧 关键修复:参数解析 ===
- debug_print("步骤2: 参数解析...")
+ debug_print("🔍 步骤2: 参数解析...")
+ action_sequence.append(create_action_log("正在解析溶解参数...", "🔍"))
# 解析各种参数为数值
final_volume = parse_volume_input(volume)
@@ -403,17 +486,18 @@ def generate_dissolve_protocol(
final_temp = parse_temperature_input(temp)
final_time = parse_time_input(time)
- debug_print(f"解析结果:")
- debug_print(f" - 体积: {final_volume}mL")
- debug_print(f" - 质量: {final_mass}g")
- debug_print(f" - 温度: {final_temp}°C")
- debug_print(f" - 时间: {final_time}s")
- debug_print(f" - 试剂: '{reagent}'")
- debug_print(f" - 摩尔: '{mol}'")
- debug_print(f" - 事件: '{event}'")
+ debug_print(f"📊 解析结果:")
+ debug_print(f" 📏 体积: {final_volume}mL")
+ debug_print(f" ⚖️ 质量: {final_mass}g")
+ debug_print(f" 🌡️ 温度: {final_temp}°C")
+ debug_print(f" ⏱️ 时间: {final_time}s")
+ debug_print(f" 🧪 试剂: '{reagent}'")
+ debug_print(f" 🧬 摩尔: '{mol}'")
+ debug_print(f" 🎯 事件: '{event}'")
# === 判断溶解类型 ===
- debug_print("步骤3: 判断溶解类型...")
+ debug_print("🔍 步骤3: 判断溶解类型...")
+ action_sequence.append(create_action_log("正在判断溶解类型...", "🔍"))
# 判断是固体溶解还是液体溶解
is_solid_dissolve = (final_mass > 0 or (mol and mol.strip() != "") or (reagent and reagent.strip() != ""))
@@ -427,10 +511,15 @@ def generate_dissolve_protocol(
solvent = "water" # 默认溶剂
debug_print("⚠️ 未明确指定溶解参数,默认为50mL水溶解")
- debug_print(f"溶解类型: {'固体溶解' if is_solid_dissolve else '液体溶解'}")
+ dissolve_type = "固体溶解" if is_solid_dissolve else "液体溶解"
+ dissolve_emoji = "🧂" if is_solid_dissolve else "💧"
+ debug_print(f"📋 溶解类型: {dissolve_type} {dissolve_emoji}")
+
+ action_sequence.append(create_action_log(f"确定溶解类型: {dissolve_type} {dissolve_emoji}", "📋"))
# === 查找设备 ===
- debug_print("步骤4: 查找设备...")
+ debug_print("🔍 步骤4: 查找设备...")
+ action_sequence.append(create_action_log("正在查找相关设备...", "🔍"))
# 查找加热搅拌器
heatchill_id = find_connected_heatchill(G, vessel)
@@ -439,21 +528,31 @@ def generate_dissolve_protocol(
# 优先使用加热搅拌器,否则使用独立搅拌器
stir_device_id = heatchill_id or stirrer_id
- debug_print(f"设备映射:")
- debug_print(f" - 加热器: '{heatchill_id}'")
- debug_print(f" - 搅拌器: '{stirrer_id}'")
- debug_print(f" - 使用设备: '{stir_device_id}'")
+ debug_print(f"📊 设备映射:")
+ debug_print(f" 🔥 加热器: '{heatchill_id}'")
+ debug_print(f" 🌪️ 搅拌器: '{stirrer_id}'")
+ debug_print(f" 🎯 使用设备: '{stir_device_id}'")
+
+ if heatchill_id:
+ action_sequence.append(create_action_log(f"找到加热搅拌器: {heatchill_id}", "🔥"))
+ elif stirrer_id:
+ action_sequence.append(create_action_log(f"找到搅拌器: {stirrer_id}", "🌪️"))
+ else:
+ action_sequence.append(create_action_log("未找到搅拌设备,将跳过搅拌", "⚠️"))
# === 执行溶解流程 ===
- debug_print("步骤5: 执行溶解流程...")
+ debug_print("🔍 步骤5: 执行溶解流程...")
try:
# 步骤5.1: 启动加热搅拌(如果需要)
if stir_device_id and (final_temp > 25.0 or final_time > 0 or stir_speed > 0):
- debug_print(f"5.1: 启动加热搅拌,温度: {final_temp}°C")
+ debug_print(f"🔍 5.1: 启动加热搅拌,温度: {final_temp}°C")
+ action_sequence.append(create_action_log(f"准备加热搅拌 (目标温度: {final_temp}°C)", "🔥"))
if heatchill_id and (final_temp > 25.0 or final_time > 0):
# 使用加热搅拌器
+ action_sequence.append(create_action_log(f"启动加热搅拌器 {heatchill_id}", "🔥"))
+
heatchill_action = {
"device_id": heatchill_id,
"action_name": "heat_chill_start",
@@ -468,6 +567,7 @@ def generate_dissolve_protocol(
# 等待温度稳定
if final_temp > 25.0:
wait_time = min(60, abs(final_temp - 25.0) * 1.5)
+ action_sequence.append(create_action_log(f"等待温度稳定 ({wait_time:.0f}秒)", "⏳"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": wait_time}
@@ -475,6 +575,8 @@ def generate_dissolve_protocol(
elif stirrer_id:
# 使用独立搅拌器
+ action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {stir_speed}rpm)", "🌪️"))
+
stir_action = {
"device_id": stirrer_id,
"action_name": "start_stir",
@@ -487,6 +589,7 @@ def generate_dissolve_protocol(
action_sequence.append(stir_action)
# 等待搅拌稳定
+ action_sequence.append(create_action_log("等待搅拌稳定...", "⏳"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5}
@@ -494,10 +597,13 @@ def generate_dissolve_protocol(
if is_solid_dissolve:
# === 固体溶解路径 ===
- debug_print(f"5.2: 使用固体溶解路径")
+ debug_print(f"🔍 5.2: 使用固体溶解路径")
+ action_sequence.append(create_action_log("开始固体溶解流程", "🧂"))
solid_dispenser = find_solid_dispenser(G)
if solid_dispenser:
+ action_sequence.append(create_action_log(f"找到固体加样器: {solid_dispenser}", "🥄"))
+
# 固体加样
add_kwargs = {
"vessel": vessel,
@@ -508,9 +614,12 @@ def generate_dissolve_protocol(
if final_mass > 0:
add_kwargs["mass"] = str(final_mass)
+ action_sequence.append(create_action_log(f"准备添加固体: {final_mass}g", "⚖️"))
if mol and mol.strip():
add_kwargs["mol"] = mol
+ action_sequence.append(create_action_log(f"按摩尔数添加: {mol}", "🧬"))
+ action_sequence.append(create_action_log("开始固体加样操作", "🥄"))
action_sequence.append({
"device_id": solid_dispenser,
"action_name": "add_solid",
@@ -518,18 +627,24 @@ def generate_dissolve_protocol(
})
debug_print(f"✅ 固体加样完成")
+ action_sequence.append(create_action_log("固体加样完成", "✅"))
else:
debug_print("⚠️ 未找到固体加样器,跳过固体添加")
+ action_sequence.append(create_action_log("未找到固体加样器,无法添加固体", "❌"))
elif is_liquid_dissolve:
# === 液体溶解路径 ===
- debug_print(f"5.3: 使用液体溶解路径")
+ debug_print(f"🔍 5.3: 使用液体溶解路径")
+ action_sequence.append(create_action_log("开始液体溶解流程", "💧"))
# 查找溶剂容器
+ action_sequence.append(create_action_log("正在查找溶剂容器...", "🔍"))
try:
solvent_vessel = find_solvent_vessel(G, solvent)
+ action_sequence.append(create_action_log(f"找到溶剂容器: {solvent_vessel}", "🧪"))
except ValueError as e:
debug_print(f"⚠️ {str(e)},跳过溶剂添加")
+ action_sequence.append(create_action_log(f"溶剂容器查找失败: {str(e)}", "❌"))
solvent_vessel = None
if solvent_vessel:
@@ -537,6 +652,9 @@ def generate_dissolve_protocol(
flowrate = 1.0 # 较慢的注入速度
transfer_flowrate = 0.5 # 较慢的转移速度
+ action_sequence.append(create_action_log(f"设置流速: {flowrate}mL/min (缓慢注入)", "⚡"))
+ action_sequence.append(create_action_log(f"开始转移 {final_volume}mL {solvent}", "🚰"))
+
# 调用pump protocol
pump_actions = generate_pump_protocol_with_rinsing(
G=G,
@@ -559,8 +677,10 @@ def generate_dissolve_protocol(
)
action_sequence.extend(pump_actions)
debug_print(f"✅ 溶剂转移完成,添加了 {len(pump_actions)} 个动作")
+ action_sequence.append(create_action_log(f"溶剂转移完成 ({len(pump_actions)} 个操作)", "✅"))
# 溶剂添加后等待
+ action_sequence.append(create_action_log("溶剂添加后短暂等待...", "⏳"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5}
@@ -568,10 +688,14 @@ def generate_dissolve_protocol(
# 步骤5.4: 等待溶解完成
if final_time > 0:
- debug_print(f"5.4: 等待溶解完成 - {final_time}s")
+ debug_print(f"🔍 5.4: 等待溶解完成 - {final_time}s")
+ wait_minutes = final_time / 60
+ action_sequence.append(create_action_log(f"开始溶解等待 ({wait_minutes:.1f}分钟)", "⏰"))
if heatchill_id:
# 使用定时加热搅拌
+ action_sequence.append(create_action_log(f"使用加热搅拌器进行定时溶解", "🔥"))
+
dissolve_action = {
"device_id": heatchill_id,
"action_name": "heat_chill",
@@ -588,6 +712,8 @@ def generate_dissolve_protocol(
elif stirrer_id:
# 使用定时搅拌
+ action_sequence.append(create_action_log(f"使用搅拌器进行定时溶解", "🌪️"))
+
stir_action = {
"device_id": stirrer_id,
"action_name": "stir",
@@ -603,6 +729,7 @@ def generate_dissolve_protocol(
else:
# 简单等待
+ action_sequence.append(create_action_log(f"简单等待溶解完成", "⏳"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": final_time}
@@ -610,7 +737,8 @@ def generate_dissolve_protocol(
# 步骤5.5: 停止加热搅拌(如果需要)
if heatchill_id and final_time == 0 and final_temp > 25.0:
- debug_print(f"5.5: 停止加热器")
+ debug_print(f"🔍 5.5: 停止加热器")
+ action_sequence.append(create_action_log("停止加热搅拌器", "🛑"))
stop_action = {
"device_id": heatchill_id,
@@ -622,7 +750,8 @@ def generate_dissolve_protocol(
action_sequence.append(stop_action)
except Exception as e:
- debug_print(f"⚠️ 溶解流程执行失败: {str(e)}")
+ debug_print(f"❌ 溶解流程执行失败: {str(e)}")
+ action_sequence.append(create_action_log(f"溶解流程失败: {str(e)}", "❌"))
# 添加错误日志
action_sequence.append({
"device_id": "system",
@@ -634,21 +763,30 @@ def generate_dissolve_protocol(
# === 最终结果 ===
debug_print("=" * 60)
- debug_print(f"✅ 溶解协议生成完成")
- debug_print(f"📊 总动作数: {len(action_sequence)}")
- debug_print(f"📋 处理总结:")
- debug_print(f" - 容器: {vessel}")
- debug_print(f" - 溶解类型: {'固体溶解' if is_solid_dissolve else '液体溶解'}")
+ debug_print(f"🎉 溶解协议生成完成")
+ debug_print(f"📊 协议统计:")
+ debug_print(f" 📋 总动作数: {len(action_sequence)}")
+ debug_print(f" 🥼 容器: {vessel}")
+ debug_print(f" {dissolve_emoji} 溶解类型: {dissolve_type}")
if is_liquid_dissolve:
- debug_print(f" - 溶剂: {solvent} ({final_volume}mL)")
+ debug_print(f" 💧 溶剂: {solvent} ({final_volume}mL)")
if is_solid_dissolve:
- debug_print(f" - 试剂: {reagent}")
- debug_print(f" - 质量: {final_mass}g")
- debug_print(f" - 摩尔: {mol}")
- debug_print(f" - 温度: {final_temp}°C")
- debug_print(f" - 时间: {final_time}s")
+ debug_print(f" 🧪 试剂: {reagent}")
+ debug_print(f" ⚖️ 质量: {final_mass}g")
+ debug_print(f" 🧬 摩尔: {mol}")
+ debug_print(f" 🌡️ 温度: {final_temp}°C")
+ debug_print(f" ⏱️ 时间: {final_time}s")
debug_print("=" * 60)
+ # 添加完成日志
+ summary_msg = f"溶解协议完成: {vessel}"
+ if is_liquid_dissolve:
+ summary_msg += f" (使用 {final_volume}mL {solvent})"
+ if is_solid_dissolve:
+ summary_msg += f" (溶解 {final_mass}g {reagent})"
+
+ action_sequence.append(create_action_log(summary_msg, "🎉"))
+
return action_sequence
# === 便捷函数 ===
@@ -656,6 +794,7 @@ def generate_dissolve_protocol(
def dissolve_solid_by_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, float],
temp: Union[str, float] = 25.0, time: Union[str, float] = "10 min") -> List[Dict[str, Any]]:
"""按质量溶解固体"""
+ debug_print(f"🧂 快速固体溶解: {reagent} ({mass}) → {vessel}")
return generate_dissolve_protocol(
G, vessel,
mass=mass,
@@ -667,6 +806,7 @@ def dissolve_solid_by_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union
def dissolve_solid_by_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str,
temp: Union[str, float] = 25.0, time: Union[str, float] = "10 min") -> List[Dict[str, Any]]:
"""按摩尔数溶解固体"""
+ debug_print(f"🧬 按摩尔数溶解固体: {reagent} ({mol}) → {vessel}")
return generate_dissolve_protocol(
G, vessel,
mol=mol,
@@ -678,6 +818,7 @@ def dissolve_solid_by_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str,
def dissolve_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float],
temp: Union[str, float] = 25.0, time: Union[str, float] = "5 min") -> List[Dict[str, Any]]:
"""用溶剂溶解"""
+ debug_print(f"💧 溶剂溶解: {solvent} ({volume}) → {vessel}")
return generate_dissolve_protocol(
G, vessel,
solvent=solvent,
@@ -688,6 +829,7 @@ def dissolve_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Unio
def dissolve_at_room_temp(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float]) -> List[Dict[str, Any]]:
"""室温溶解"""
+ debug_print(f"🌡️ 室温溶解: {solvent} ({volume}) → {vessel}")
return generate_dissolve_protocol(
G, vessel,
solvent=solvent,
@@ -699,6 +841,7 @@ def dissolve_at_room_temp(G: nx.DiGraph, vessel: str, solvent: str, volume: Unio
def dissolve_with_heating(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float],
temp: Union[str, float] = "60 °C", time: Union[str, float] = "15 min") -> List[Dict[str, Any]]:
"""加热溶解"""
+ debug_print(f"🔥 加热溶解: {solvent} ({volume}) → {vessel} @ {temp}")
return generate_dissolve_protocol(
G, vessel,
solvent=solvent,
@@ -710,33 +853,37 @@ def dissolve_with_heating(G: nx.DiGraph, vessel: str, solvent: str, volume: Unio
# 测试函数
def test_dissolve_protocol():
"""测试溶解协议的各种参数解析"""
- print("=== DISSOLVE PROTOCOL 修复版测试 ===")
+ debug_print("=== DISSOLVE PROTOCOL 增强版测试 ===")
# 测试体积解析
+ debug_print("💧 测试体积解析...")
volumes = ["10 mL", "?", 10.0, "1 L", "500 μL"]
for vol in volumes:
result = parse_volume_input(vol)
- print(f"体积解析: {vol} → {result}mL")
+ debug_print(f"📏 体积解析: {vol} → {result}mL")
# 测试质量解析
+ debug_print("⚖️ 测试质量解析...")
masses = ["2.9 g", "?", 2.5, "500 mg"]
for mass in masses:
result = parse_mass_input(mass)
- print(f"质量解析: {mass} → {result}g")
+ debug_print(f"⚖️ 质量解析: {mass} → {result}g")
# 测试温度解析
+ debug_print("🌡️ 测试温度解析...")
temps = ["60 °C", "room temperature", "?", 25.0, "reflux"]
for temp in temps:
result = parse_temperature_input(temp)
- print(f"温度解析: {temp} → {result}°C")
+ debug_print(f"🌡️ 温度解析: {temp} → {result}°C")
# 测试时间解析
+ debug_print("⏱️ 测试时间解析...")
times = ["30 min", "1 h", "?", 60.0]
for time in times:
result = parse_time_input(time)
- print(f"时间解析: {time} → {result}s")
+ debug_print(f"⏱️ 时间解析: {time} → {result}s")
- print("✅ 测试完成")
+ debug_print("✅ 测试完成")
if __name__ == "__main__":
test_dissolve_protocol()
\ No newline at end of file
diff --git a/unilabos/compile/evacuateandrefill_protocol.py b/unilabos/compile/evacuateandrefill_protocol.py
index c50b88492..cbcf19bb9 100644
--- a/unilabos/compile/evacuateandrefill_protocol.py
+++ b/unilabos/compile/evacuateandrefill_protocol.py
@@ -1,16 +1,68 @@
import networkx as nx
import logging
-import uuid # 🔧 移到顶部
+import uuid
+import sys
from typing import List, Dict, Any, Optional
from .pump_protocol import generate_pump_protocol_with_rinsing, generate_pump_protocol
# 设置日志
logger = logging.getLogger(__name__)
+# 确保输出编码为UTF-8
+if hasattr(sys.stdout, 'reconfigure'):
+ try:
+ sys.stdout.reconfigure(encoding='utf-8')
+ sys.stderr.reconfigure(encoding='utf-8')
+ except:
+ pass
+
def debug_print(message):
- """调试输出函数"""
- print(f"[EVACUATE_REFILL] {message}", flush=True)
- logger.info(f"[EVACUATE_REFILL] {message}")
+ """调试输出函数 - 支持中文"""
+ try:
+ # 确保消息是字符串格式
+ safe_message = str(message)
+ print(f"[抽真空充气] {safe_message}", flush=True)
+ logger.info(f"[抽真空充气] {safe_message}")
+ except UnicodeEncodeError:
+ # 如果编码失败,尝试替换不支持的字符
+ safe_message = str(message).encode('utf-8', errors='replace').decode('utf-8')
+ print(f"[抽真空充气] {safe_message}", flush=True)
+ logger.info(f"[抽真空充气] {safe_message}")
+ except Exception as e:
+ # 最后的安全措施
+ fallback_message = f"日志输出错误: {repr(message)}"
+ print(f"[抽真空充气] {fallback_message}", flush=True)
+ logger.info(f"[抽真空充气] {fallback_message}")
+
+def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]:
+ """创建一个动作日志 - 支持中文和emoji"""
+ try:
+ full_message = f"{emoji} {message}"
+ debug_print(full_message)
+ logger.info(full_message)
+
+ return {
+ "action_name": "wait",
+ "action_kwargs": {
+ "time": 0.1,
+ "log_message": full_message,
+ "progress_message": full_message
+ }
+ }
+ except Exception as e:
+ # 如果emoji有问题,使用纯文本
+ safe_message = f"[日志] {message}"
+ debug_print(safe_message)
+ logger.info(safe_message)
+
+ return {
+ "action_name": "wait",
+ "action_kwargs": {
+ "time": 0.1,
+ "log_message": safe_message,
+ "progress_message": safe_message
+ }
+ }
def find_gas_source(G: nx.DiGraph, gas: str) -> str:
"""
@@ -19,9 +71,10 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str:
2. 气体类型匹配(data.gas_type)
3. 默认气源
"""
- debug_print(f"正在查找气体 '{gas}' 的气源...")
+ debug_print(f"🔍 正在查找气体 '{gas}' 的气源...")
# 第一步:通过容器名称匹配
+ debug_print(f"📋 方法1: 容器名称匹配...")
gas_source_patterns = [
f"gas_source_{gas}",
f"gas_{gas}",
@@ -32,12 +85,15 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str:
f"bottle_{gas}"
]
+ debug_print(f"🎯 尝试的容器名称: {gas_source_patterns}")
+
for pattern in gas_source_patterns:
if pattern in G.nodes():
- debug_print(f"通过名称匹配找到气源: {pattern}")
+ debug_print(f"✅ 通过名称找到气源: {pattern}")
return pattern
# 第二步:通过气体类型匹配 (data.gas_type)
+ debug_print(f"📋 方法2: 气体类型匹配...")
for node_id in G.nodes():
node_data = G.nodes[node_id]
node_class = node_data.get('class', '') or ''
@@ -52,7 +108,7 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str:
gas_type = data.get('gas_type', '')
if gas_type.lower() == gas.lower():
- debug_print(f"通过气体类型匹配找到气源: {node_id} (gas_type: {gas_type})")
+ debug_print(f"✅ 通过气体类型找到气源: {node_id} (气体类型: {gas_type})")
return node_id
# 检查 config.gas_type
@@ -60,10 +116,11 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str:
config_gas_type = config.get('gas_type', '')
if config_gas_type.lower() == gas.lower():
- debug_print(f"通过配置气体类型匹配找到气源: {node_id} (config.gas_type: {config_gas_type})")
+ debug_print(f"✅ 通过配置气体类型找到气源: {node_id} (配置气体类型: {config_gas_type})")
return node_id
# 第三步:查找所有可用的气源设备
+ debug_print(f"📋 方法3: 查找可用气源...")
available_gas_sources = []
for node_id in G.nodes():
node_data = G.nodes[node_id]
@@ -74,12 +131,13 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str:
(node_id.startswith('flask_') and any(g in node_id.lower() for g in ['air', 'nitrogen', 'argon']))):
data = node_data.get('data', {})
- gas_type = data.get('gas_type', 'unknown')
- available_gas_sources.append(f"{node_id} (gas_type: {gas_type})")
+ gas_type = data.get('gas_type', '未知')
+ available_gas_sources.append(f"{node_id} (气体类型: {gas_type})")
- debug_print(f"可用气源列表: {available_gas_sources}")
+ debug_print(f"📊 可用气源: {available_gas_sources}")
# 第四步:如果找不到特定气体,使用默认的第一个气源
+ debug_print(f"📋 方法4: 查找默认气源...")
default_gas_sources = [
node for node in G.nodes()
if ((G.nodes[node].get('class') or '').find('virtual_gas_source') != -1
@@ -91,11 +149,12 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str:
debug_print(f"⚠️ 未找到特定气体 '{gas}',使用默认气源: {default_source}")
return default_source
- raise ValueError(f"找不到气体 '{gas}' 对应的气源。可用气源: {available_gas_sources}")
+ debug_print(f"❌ 所有方法都失败了!")
+ raise ValueError(f"无法找到气体 '{gas}' 的气源。可用气源: {available_gas_sources}")
def find_vacuum_pump(G: nx.DiGraph) -> str:
"""查找真空泵设备"""
- debug_print("查找真空泵设备...")
+ debug_print("🔍 正在查找真空泵...")
vacuum_pumps = []
for node in G.nodes():
@@ -106,16 +165,18 @@ def find_vacuum_pump(G: nx.DiGraph) -> str:
'vacuum_pump' in node.lower() or
'vacuum' in node_class.lower()):
vacuum_pumps.append(node)
+ debug_print(f"📋 发现真空泵: {node}")
if not vacuum_pumps:
- raise ValueError("系统中未找到真空泵设备")
+ debug_print(f"❌ 系统中未找到真空泵")
+ raise ValueError("系统中未找到真空泵")
- debug_print(f"找到真空泵: {vacuum_pumps[0]}")
+ debug_print(f"✅ 使用真空泵: {vacuum_pumps[0]}")
return vacuum_pumps[0]
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> Optional[str]:
"""查找与指定容器相连的搅拌器"""
- debug_print(f"查找与容器 {vessel} 相连的搅拌器...")
+ debug_print(f"🔍 正在查找与容器 {vessel} 连接的搅拌器...")
stirrer_nodes = []
for node in G.nodes():
@@ -124,24 +185,27 @@ def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> Optional[str]:
if 'virtual_stirrer' in node_class or 'stirrer' in node.lower():
stirrer_nodes.append(node)
+ debug_print(f"📋 发现搅拌器: {node}")
+
+ debug_print(f"📊 找到的搅拌器总数: {len(stirrer_nodes)}")
# 检查哪个搅拌器与目标容器相连
for stirrer in stirrer_nodes:
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
- debug_print(f"找到连接的搅拌器: {stirrer}")
+ debug_print(f"✅ 找到连接的搅拌器: {stirrer}")
return stirrer
# 如果没有连接的搅拌器,返回第一个可用的
if stirrer_nodes:
- debug_print(f"未找到直接连接的搅拌器,使用第一个可用的: {stirrer_nodes[0]}")
+ debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个可用的: {stirrer_nodes[0]}")
return stirrer_nodes[0]
- debug_print("未找到搅拌器")
+ debug_print("❌ 未找到搅拌器")
return None
def find_vacuum_solenoid_valve(G: nx.DiGraph, vacuum_pump: str) -> Optional[str]:
- """查找真空泵相关的电磁阀 - 根据实际连接逻辑"""
- debug_print(f"查找真空泵 {vacuum_pump} 相关的电磁阀...")
+ """查找真空泵相关的电磁阀"""
+ debug_print(f"🔍 正在查找真空泵 {vacuum_pump} 的电磁阀...")
# 查找所有电磁阀
solenoid_valves = []
@@ -151,29 +215,30 @@ def find_vacuum_solenoid_valve(G: nx.DiGraph, vacuum_pump: str) -> Optional[str]
if ('solenoid' in node_class.lower() or 'solenoid_valve' in node.lower()):
solenoid_valves.append(node)
+ debug_print(f"📋 发现电磁阀: {node}")
- debug_print(f"找到的电磁阀: {solenoid_valves}")
+ debug_print(f"📊 找到的电磁阀: {solenoid_valves}")
- # 🔧 修复:根据实际组态图连接逻辑查找
- # vacuum_pump_1 <- solenoid_valve_1 <- multiway_valve_2
+ # 检查连接关系
+ debug_print(f"📋 方法1: 检查连接关系...")
for solenoid in solenoid_valves:
- # 检查电磁阀是否连接到真空泵
if G.has_edge(solenoid, vacuum_pump) or G.has_edge(vacuum_pump, solenoid):
- debug_print(f"✓ 找到连接真空泵的电磁阀: {solenoid}")
+ debug_print(f"✅ 找到连接的真空电磁阀: {solenoid}")
return solenoid
- # 通过命名规则查找(备选方案)
+ # 通过命名规则查找
+ debug_print(f"📋 方法2: 检查命名规则...")
for solenoid in solenoid_valves:
if 'vacuum' in solenoid.lower() or solenoid == 'solenoid_valve_1':
- debug_print(f"✓ 通过命名规则找到真空电磁阀: {solenoid}")
+ debug_print(f"✅ 通过命名找到真空电磁阀: {solenoid}")
return solenoid
debug_print("⚠️ 未找到真空电磁阀")
return None
def find_gas_solenoid_valve(G: nx.DiGraph, gas_source: str) -> Optional[str]:
- """查找气源相关的电磁阀 - 根据实际连接逻辑"""
- debug_print(f"查找气源 {gas_source} 相关的电磁阀...")
+ """查找气源相关的电磁阀"""
+ debug_print(f"🔍 正在查找气源 {gas_source} 的电磁阀...")
# 查找所有电磁阀
solenoid_valves = []
@@ -184,18 +249,20 @@ def find_gas_solenoid_valve(G: nx.DiGraph, gas_source: str) -> Optional[str]:
if ('solenoid' in node_class.lower() or 'solenoid_valve' in node.lower()):
solenoid_valves.append(node)
- # 🔧 修复:根据实际组态图连接逻辑查找
- # gas_source_1 -> solenoid_valve_2 -> multiway_valve_2
+ debug_print(f"📊 找到的电磁阀: {solenoid_valves}")
+
+ # 检查连接关系
+ debug_print(f"📋 方法1: 检查连接关系...")
for solenoid in solenoid_valves:
- # 检查气源是否连接到电磁阀
if G.has_edge(gas_source, solenoid) or G.has_edge(solenoid, gas_source):
- debug_print(f"✓ 找到连接气源的电磁阀: {solenoid}")
+ debug_print(f"✅ 找到连接的气源电磁阀: {solenoid}")
return solenoid
- # 通过命名规则查找(备选方案)
+ # 通过命名规则查找
+ debug_print(f"📋 方法2: 检查命名规则...")
for solenoid in solenoid_valves:
if 'gas' in solenoid.lower() or solenoid == 'solenoid_valve_2':
- debug_print(f"✓ 通过命名规则找到气源电磁阀: {solenoid}")
+ debug_print(f"✅ 通过命名找到气源电磁阀: {solenoid}")
return solenoid
debug_print("⚠️ 未找到气源电磁阀")
@@ -208,7 +275,7 @@ def generate_evacuateandrefill_protocol(
**kwargs
) -> List[Dict[str, Any]]:
"""
- 生成抽真空和充气操作的动作序列 - 最终修复版本
+ 生成抽真空和充气操作的动作序列 - 中文版
Args:
G: 设备图
@@ -223,75 +290,113 @@ def generate_evacuateandrefill_protocol(
# 硬编码重复次数为 3
repeats = 3
- # 🔧 修复:在函数开始就生成协议ID
+ # 生成协议ID
protocol_id = str(uuid.uuid4())
- debug_print(f"生成协议ID: {protocol_id}")
+ debug_print(f"🆔 生成协议ID: {protocol_id}")
debug_print("=" * 60)
- debug_print("开始生成抽真空充气协议")
- debug_print(f"输入参数:")
- debug_print(f" - vessel: {vessel}")
- debug_print(f" - gas: {gas}")
- debug_print(f" - repeats: {repeats} (硬编码)")
- debug_print(f" - 其他参数: {kwargs}")
+ debug_print("🧪 开始生成抽真空充气协议")
+ debug_print(f"📋 原始参数:")
+ debug_print(f" 🥼 容器: '{vessel}'")
+ debug_print(f" 💨 气体: '{gas}'")
+ debug_print(f" 🔄 循环次数: {repeats} (硬编码)")
+ debug_print(f" 📦 其他参数: {kwargs}")
debug_print("=" * 60)
action_sequence = []
# === 参数验证和修正 ===
- debug_print("步骤1: 参数验证和修正...")
+ debug_print("🔍 步骤1: 参数验证和修正...")
+ action_sequence.append(create_action_log(f"开始抽真空充气操作 - 容器: {vessel}", "🎬"))
+ action_sequence.append(create_action_log(f"目标气体: {gas}", "💨"))
+ action_sequence.append(create_action_log(f"循环次数: {repeats}", "🔄"))
# 验证必需参数
if not vessel:
- raise ValueError("vessel 参数不能为空")
+ debug_print("❌ 容器参数不能为空")
+ raise ValueError("容器参数不能为空")
if not gas:
- raise ValueError("gas 参数不能为空")
+ debug_print("❌ 气体参数不能为空")
+ raise ValueError("气体参数不能为空")
if vessel not in G.nodes():
- raise ValueError(f"容器 '{vessel}' 不存在于系统中")
+ debug_print(f"❌ 容器 '{vessel}' 在系统中不存在")
+ raise ValueError(f"容器 '{vessel}' 在系统中不存在")
+
+ debug_print("✅ 基本参数验证通过")
+ action_sequence.append(create_action_log("参数验证通过", "✅"))
# 标准化气体名称
+ debug_print("🔧 标准化气体名称...")
gas_aliases = {
'n2': 'nitrogen',
'ar': 'argon',
'air': 'air',
'o2': 'oxygen',
'co2': 'carbon_dioxide',
- 'h2': 'hydrogen'
+ 'h2': 'hydrogen',
+ '氮气': 'nitrogen',
+ '氩气': 'argon',
+ '空气': 'air',
+ '氧气': 'oxygen',
+ '二氧化碳': 'carbon_dioxide',
+ '氢气': 'hydrogen'
}
original_gas = gas
gas_lower = gas.lower().strip()
if gas_lower in gas_aliases:
gas = gas_aliases[gas_lower]
- debug_print(f"标准化气体名称: {original_gas} -> {gas}")
+ debug_print(f"🔄 标准化气体名称: {original_gas} -> {gas}")
+ action_sequence.append(create_action_log(f"气体名称标准化: {original_gas} -> {gas}", "🔄"))
- debug_print(f"最终参数: vessel={vessel}, gas={gas}, repeats={repeats}")
+ debug_print(f"📋 最终参数: 容器={vessel}, 气体={gas}, 重复={repeats}")
# === 查找设备 ===
- debug_print("步骤2: 查找设备...")
+ debug_print("🔍 步骤2: 查找设备...")
+ action_sequence.append(create_action_log("正在查找相关设备...", "🔍"))
try:
vacuum_pump = find_vacuum_pump(G)
+ action_sequence.append(create_action_log(f"找到真空泵: {vacuum_pump}", "🌪️"))
+
gas_source = find_gas_source(G, gas)
+ action_sequence.append(create_action_log(f"找到气源: {gas_source}", "💨"))
+
vacuum_solenoid = find_vacuum_solenoid_valve(G, vacuum_pump)
+ if vacuum_solenoid:
+ action_sequence.append(create_action_log(f"找到真空电磁阀: {vacuum_solenoid}", "🚪"))
+ else:
+ action_sequence.append(create_action_log("未找到真空电磁阀", "⚠️"))
+
gas_solenoid = find_gas_solenoid_valve(G, gas_source)
+ if gas_solenoid:
+ action_sequence.append(create_action_log(f"找到气源电磁阀: {gas_solenoid}", "🚪"))
+ else:
+ action_sequence.append(create_action_log("未找到气源电磁阀", "⚠️"))
+
stirrer_id = find_connected_stirrer(G, vessel)
+ if stirrer_id:
+ action_sequence.append(create_action_log(f"找到搅拌器: {stirrer_id}", "🌪️"))
+ else:
+ action_sequence.append(create_action_log("未找到搅拌器", "⚠️"))
- debug_print(f"设备配置:")
- debug_print(f" - 真空泵: {vacuum_pump}")
- debug_print(f" - 气源: {gas_source}")
- debug_print(f" - 真空电磁阀: {vacuum_solenoid}")
- debug_print(f" - 气源电磁阀: {gas_solenoid}")
- debug_print(f" - 搅拌器: {stirrer_id}")
+ debug_print(f"📊 设备配置:")
+ debug_print(f" 🌪️ 真空泵: {vacuum_pump}")
+ debug_print(f" 💨 气源: {gas_source}")
+ debug_print(f" 🚪 真空电磁阀: {vacuum_solenoid}")
+ debug_print(f" 🚪 气源电磁阀: {gas_solenoid}")
+ debug_print(f" 🌪️ 搅拌器: {stirrer_id}")
except Exception as e:
debug_print(f"❌ 设备查找失败: {str(e)}")
+ action_sequence.append(create_action_log(f"设备查找失败: {str(e)}", "❌"))
raise ValueError(f"设备查找失败: {str(e)}")
# === 参数设置 ===
- debug_print("步骤3: 参数设置...")
+ debug_print("🔍 步骤3: 参数设置...")
+ action_sequence.append(create_action_log("设置操作参数...", "⚙️"))
# 根据气体类型调整参数
if gas.lower() in ['nitrogen', 'argon']:
@@ -300,87 +405,108 @@ def generate_evacuateandrefill_protocol(
PUMP_FLOW_RATE = 2.0
VACUUM_TIME = 30.0
REFILL_TIME = 20.0
- debug_print("惰性气体:使用标准参数")
+ debug_print("💨 惰性气体: 使用标准参数")
+ action_sequence.append(create_action_log("检测到惰性气体,使用标准参数", "💨"))
elif gas.lower() in ['air', 'oxygen']:
VACUUM_VOLUME = 20.0
REFILL_VOLUME = 20.0
PUMP_FLOW_RATE = 1.5
VACUUM_TIME = 45.0
REFILL_TIME = 25.0
- debug_print("活性气体:使用保守参数")
+ debug_print("🔥 活性气体: 使用保守参数")
+ action_sequence.append(create_action_log("检测到活性气体,使用保守参数", "🔥"))
else:
VACUUM_VOLUME = 15.0
REFILL_VOLUME = 15.0
PUMP_FLOW_RATE = 1.0
VACUUM_TIME = 60.0
REFILL_TIME = 30.0
- debug_print("未知气体:使用安全参数")
+ debug_print("❓ 未知气体: 使用安全参数")
+ action_sequence.append(create_action_log("未知气体类型,使用安全参数", "❓"))
STIR_SPEED = 200.0
- debug_print(f"操作参数:")
- debug_print(f" - 抽真空体积: {VACUUM_VOLUME}mL")
- debug_print(f" - 充气体积: {REFILL_VOLUME}mL")
- debug_print(f" - 泵流速: {PUMP_FLOW_RATE}mL/s")
- debug_print(f" - 抽真空时间: {VACUUM_TIME}s")
- debug_print(f" - 充气时间: {REFILL_TIME}s")
- debug_print(f" - 搅拌速度: {STIR_SPEED}RPM")
+ debug_print(f"⚙️ 操作参数:")
+ debug_print(f" 📏 真空体积: {VACUUM_VOLUME}mL")
+ debug_print(f" 📏 充气体积: {REFILL_VOLUME}mL")
+ debug_print(f" ⚡ 泵流速: {PUMP_FLOW_RATE}mL/s")
+ debug_print(f" ⏱️ 真空时间: {VACUUM_TIME}s")
+ debug_print(f" ⏱️ 充气时间: {REFILL_TIME}s")
+ debug_print(f" 🌪️ 搅拌速度: {STIR_SPEED}RPM")
+
+ action_sequence.append(create_action_log(f"真空体积: {VACUUM_VOLUME}mL", "📏"))
+ action_sequence.append(create_action_log(f"充气体积: {REFILL_VOLUME}mL", "📏"))
+ action_sequence.append(create_action_log(f"泵流速: {PUMP_FLOW_RATE}mL/s", "⚡"))
# === 路径验证 ===
- debug_print("步骤4: 路径验证...")
+ debug_print("🔍 步骤4: 路径验证...")
+ action_sequence.append(create_action_log("验证传输路径...", "🛤️"))
try:
- # 验证抽真空路径: vessel -> vacuum_pump (通过八通阀和电磁阀)
+ # 验证抽真空路径
if nx.has_path(G, vessel, vacuum_pump):
vacuum_path = nx.shortest_path(G, source=vessel, target=vacuum_pump)
- debug_print(f"抽真空路径: {' → '.join(vacuum_path)}")
+ debug_print(f"✅ 真空路径: {' -> '.join(vacuum_path)}")
+ action_sequence.append(create_action_log(f"真空路径: {' -> '.join(vacuum_path)}", "🛤️"))
else:
- debug_print(f"⚠️ 抽真空路径不存在,继续执行但可能有问题")
+ debug_print(f"⚠️ 真空路径不存在,继续执行但可能有问题")
+ action_sequence.append(create_action_log("真空路径检查: 路径不存在", "⚠️"))
- # 验证充气路径: gas_source -> vessel (通过电磁阀和八通阀)
+ # 验证充气路径
if nx.has_path(G, gas_source, vessel):
gas_path = nx.shortest_path(G, source=gas_source, target=vessel)
- debug_print(f"充气路径: {' → '.join(gas_path)}")
+ debug_print(f"✅ 气体路径: {' -> '.join(gas_path)}")
+ action_sequence.append(create_action_log(f"气体路径: {' -> '.join(gas_path)}", "🛤️"))
else:
- debug_print(f"⚠️ 充气路径不存在,继续执行但可能有问题")
+ debug_print(f"⚠️ 气体路径不存在,继续执行但可能有问题")
+ action_sequence.append(create_action_log("气体路径检查: 路径不存在", "⚠️"))
except Exception as e:
debug_print(f"⚠️ 路径验证失败: {str(e)},继续执行")
+ action_sequence.append(create_action_log(f"路径验证失败: {str(e)}", "⚠️"))
# === 启动搅拌器 ===
- debug_print("步骤5: 启动搅拌器...")
+ debug_print("🔍 步骤5: 启动搅拌器...")
if stirrer_id:
- debug_print(f"启动搅拌器: {stirrer_id}")
+ debug_print(f"🌪️ 启动搅拌器: {stirrer_id}")
+ action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {STIR_SPEED}rpm)", "🌪️"))
+
action_sequence.append({
"device_id": stirrer_id,
"action_name": "start_stir",
"action_kwargs": {
"vessel": vessel,
"stir_speed": STIR_SPEED,
- "purpose": "抽真空充气操作前启动搅拌"
+ "purpose": "抽真空充气前预搅拌"
}
})
# 等待搅拌稳定
+ action_sequence.append(create_action_log("等待搅拌稳定...", "⏳"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5.0}
})
else:
- debug_print("未找到搅拌器,跳过搅拌启动")
+ debug_print("⚠️ 未找到搅拌器,跳过搅拌器启动")
+ action_sequence.append(create_action_log("跳过搅拌器启动", "⏭️"))
- # === 执行 3 次抽真空-充气循环 ===
- debug_print("步骤6: 执行抽真空-充气循环...")
+ # === 执行循环 ===
+ debug_print("🔍 步骤6: 执行抽真空-充气循环...")
+ action_sequence.append(create_action_log(f"开始 {repeats} 次抽真空-充气循环", "🔄"))
for cycle in range(repeats):
- debug_print(f"=== 第 {cycle+1}/{repeats} 次循环 ===")
+ debug_print(f"=== 第 {cycle+1}/{repeats} 轮循环 ===")
+ action_sequence.append(create_action_log(f"第 {cycle+1}/{repeats} 轮循环开始", "🚀"))
# ============ 抽真空阶段 ============
- debug_print(f"抽真空阶段开始")
+ debug_print(f"🌪️ 抽真空阶段开始")
+ action_sequence.append(create_action_log("开始抽真空阶段", "🌪️"))
# 启动真空泵
- debug_print(f"启动真空泵: {vacuum_pump}")
+ debug_print(f"🔛 启动真空泵: {vacuum_pump}")
+ action_sequence.append(create_action_log(f"启动真空泵: {vacuum_pump}", "🔛"))
action_sequence.append({
"device_id": vacuum_pump,
"action_name": "set_status",
@@ -389,17 +515,19 @@ def generate_evacuateandrefill_protocol(
# 开启真空电磁阀
if vacuum_solenoid:
- debug_print(f"开启真空电磁阀: {vacuum_solenoid}")
+ debug_print(f"🚪 打开真空电磁阀: {vacuum_solenoid}")
+ action_sequence.append(create_action_log(f"打开真空电磁阀: {vacuum_solenoid}", "🚪"))
action_sequence.append({
"device_id": vacuum_solenoid,
"action_name": "set_valve_position",
"action_kwargs": {"command": "OPEN"}
})
- # 抽真空操作 - 使用液体转移协议
- debug_print(f"抽真空操作: {vessel} → {vacuum_pump}")
+ # 抽真空操作
+ debug_print(f"🌪️ 抽真空操作: {vessel} -> {vacuum_pump}")
+ action_sequence.append(create_action_log(f"开始抽真空: {vessel} -> {vacuum_pump}", "🌪️"))
+
try:
-
vacuum_transfer_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=vessel,
@@ -419,8 +547,10 @@ def generate_evacuateandrefill_protocol(
if vacuum_transfer_actions:
action_sequence.extend(vacuum_transfer_actions)
debug_print(f"✅ 添加了 {len(vacuum_transfer_actions)} 个抽真空动作")
+ action_sequence.append(create_action_log(f"抽真空协议完成 ({len(vacuum_transfer_actions)} 个操作)", "✅"))
else:
debug_print("⚠️ 抽真空协议返回空序列,添加手动动作")
+ action_sequence.append(create_action_log("抽真空协议为空,使用手动等待", "⚠️"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": VACUUM_TIME}
@@ -428,13 +558,15 @@ def generate_evacuateandrefill_protocol(
except Exception as e:
debug_print(f"❌ 抽真空失败: {str(e)}")
- # 添加等待时间作为备选
+ action_sequence.append(create_action_log(f"抽真空失败: {str(e)}", "❌"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": VACUUM_TIME}
})
# 抽真空后等待
+ wait_minutes = VACUUM_TIME / 60
+ action_sequence.append(create_action_log(f"抽真空后等待 ({wait_minutes:.1f} 分钟)", "⏳"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": VACUUM_TIME}
@@ -442,7 +574,8 @@ def generate_evacuateandrefill_protocol(
# 关闭真空电磁阀
if vacuum_solenoid:
- debug_print(f"关闭真空电磁阀: {vacuum_solenoid}")
+ debug_print(f"🚪 关闭真空电磁阀: {vacuum_solenoid}")
+ action_sequence.append(create_action_log(f"关闭真空电磁阀: {vacuum_solenoid}", "🚪"))
action_sequence.append({
"device_id": vacuum_solenoid,
"action_name": "set_valve_position",
@@ -450,24 +583,28 @@ def generate_evacuateandrefill_protocol(
})
# 关闭真空泵
- debug_print(f"关闭真空泵: {vacuum_pump}")
+ debug_print(f"🔴 停止真空泵: {vacuum_pump}")
+ action_sequence.append(create_action_log(f"停止真空泵: {vacuum_pump}", "🔴"))
action_sequence.append({
"device_id": vacuum_pump,
"action_name": "set_status",
"action_kwargs": {"string": "OFF"}
})
- # 抽真空后等待
+ # 阶段间等待
+ action_sequence.append(create_action_log("抽真空阶段完成,短暂等待", "⏳"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5.0}
})
# ============ 充气阶段 ============
- debug_print(f"充气阶段开始")
+ debug_print(f"💨 充气阶段开始")
+ action_sequence.append(create_action_log("开始气体充气阶段", "💨"))
# 启动气源
- debug_print(f"启动气源: {gas_source}")
+ debug_print(f"🔛 启动气源: {gas_source}")
+ action_sequence.append(create_action_log(f"启动气源: {gas_source}", "🔛"))
action_sequence.append({
"device_id": gas_source,
"action_name": "set_status",
@@ -476,17 +613,19 @@ def generate_evacuateandrefill_protocol(
# 开启气源电磁阀
if gas_solenoid:
- debug_print(f"开启气源电磁阀: {gas_solenoid}")
+ debug_print(f"🚪 打开气源电磁阀: {gas_solenoid}")
+ action_sequence.append(create_action_log(f"打开气源电磁阀: {gas_solenoid}", "🚪"))
action_sequence.append({
"device_id": gas_solenoid,
"action_name": "set_valve_position",
"action_kwargs": {"command": "OPEN"}
})
- # 充气操作 - 使用液体转移协议
- debug_print(f"充气操作: {gas_source} → {vessel}")
+ # 充气操作
+ debug_print(f"💨 充气操作: {gas_source} -> {vessel}")
+ action_sequence.append(create_action_log(f"开始气体充气: {gas_source} -> {vessel}", "💨"))
+
try:
-
gas_transfer_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=gas_source,
@@ -506,22 +645,26 @@ def generate_evacuateandrefill_protocol(
if gas_transfer_actions:
action_sequence.extend(gas_transfer_actions)
debug_print(f"✅ 添加了 {len(gas_transfer_actions)} 个充气动作")
+ action_sequence.append(create_action_log(f"气体充气协议完成 ({len(gas_transfer_actions)} 个操作)", "✅"))
else:
debug_print("⚠️ 充气协议返回空序列,添加手动动作")
+ action_sequence.append(create_action_log("充气协议为空,使用手动等待", "⚠️"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": REFILL_TIME}
})
except Exception as e:
- debug_print(f"❌ 充气失败: {str(e)}")
- # 添加等待时间作为备选
+ debug_print(f"❌ 气体充气失败: {str(e)}")
+ action_sequence.append(create_action_log(f"气体充气失败: {str(e)}", "❌"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": REFILL_TIME}
})
# 充气后等待
+ refill_wait_minutes = REFILL_TIME / 60
+ action_sequence.append(create_action_log(f"充气后等待 ({refill_wait_minutes:.1f} 分钟)", "⏳"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": REFILL_TIME}
@@ -529,7 +672,8 @@ def generate_evacuateandrefill_protocol(
# 关闭气源电磁阀
if gas_solenoid:
- debug_print(f"关闭气源电磁阀: {gas_solenoid}")
+ debug_print(f"🚪 关闭气源电磁阀: {gas_solenoid}")
+ action_sequence.append(create_action_log(f"关闭气源电磁阀: {gas_solenoid}", "🚪"))
action_sequence.append({
"device_id": gas_solenoid,
"action_name": "set_valve_position",
@@ -537,68 +681,92 @@ def generate_evacuateandrefill_protocol(
})
# 关闭气源
- debug_print(f"关闭气源: {gas_source}")
+ debug_print(f"🔴 停止气源: {gas_source}")
+ action_sequence.append(create_action_log(f"停止气源: {gas_source}", "🔴"))
action_sequence.append({
"device_id": gas_source,
"action_name": "set_status",
"action_kwargs": {"string": "OFF"}
})
- # 等待下一次循环
+ # 循环间等待
if cycle < repeats - 1:
- debug_print(f"等待下一次循环...")
+ debug_print(f"⏳ 等待下一个循环...")
+ action_sequence.append(create_action_log("等待下一个循环...", "⏳"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 10.0}
})
+ else:
+ action_sequence.append(create_action_log(f"第 {cycle+1}/{repeats} 轮循环完成", "✅"))
# === 停止搅拌器 ===
- debug_print("步骤7: 停止搅拌器...")
+ debug_print("🔍 步骤7: 停止搅拌器...")
if stirrer_id:
- debug_print(f"停止搅拌器: {stirrer_id}")
+ debug_print(f"🛑 停止搅拌器: {stirrer_id}")
+ action_sequence.append(create_action_log(f"停止搅拌器: {stirrer_id}", "🛑"))
action_sequence.append({
"device_id": stirrer_id,
"action_name": "stop_stir",
"action_kwargs": {"vessel": vessel}
})
+ else:
+ action_sequence.append(create_action_log("跳过搅拌器停止", "⏭️"))
# === 最终等待 ===
+ action_sequence.append(create_action_log("最终稳定等待...", "⏳"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 10.0}
})
# === 总结 ===
+ total_time = (VACUUM_TIME + REFILL_TIME + 25) * repeats + 20
+
debug_print("=" * 60)
- debug_print(f"抽真空充气协议生成完成")
- debug_print(f"总动作数: {len(action_sequence)}")
- debug_print(f"处理容器: {vessel}")
- debug_print(f"使用气体: {gas}")
- debug_print(f"重复次数: {repeats} (硬编码)")
+ debug_print(f"🎉 抽真空充气协议生成完成")
+ debug_print(f"📊 协议统计:")
+ debug_print(f" 📋 总动作数: {len(action_sequence)}")
+ debug_print(f" ⏱️ 预计总时间: {total_time:.0f}s ({total_time/60:.1f} 分钟)")
+ debug_print(f" 🥼 处理容器: {vessel}")
+ debug_print(f" 💨 使用气体: {gas}")
+ debug_print(f" 🔄 重复次数: {repeats}")
debug_print("=" * 60)
+ # 添加完成日志
+ summary_msg = f"抽真空充气协议完成: {vessel} (使用 {gas},{repeats} 次循环)"
+ action_sequence.append(create_action_log(summary_msg, "🎉"))
+
return action_sequence
# === 便捷函数 ===
def generate_nitrogen_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]:
"""生成氮气置换协议"""
+ debug_print(f"💨 生成氮气置换协议: {vessel}")
return generate_evacuateandrefill_protocol(G, vessel, "nitrogen", **kwargs)
def generate_argon_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]:
"""生成氩气置换协议"""
+ debug_print(f"💨 生成氩气置换协议: {vessel}")
return generate_evacuateandrefill_protocol(G, vessel, "argon", **kwargs)
def generate_air_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]:
"""生成空气置换协议"""
+ debug_print(f"💨 生成空气置换协议: {vessel}")
return generate_evacuateandrefill_protocol(G, vessel, "air", **kwargs)
+def generate_inert_atmosphere_protocol(G: nx.DiGraph, vessel: str, gas: str = "nitrogen", **kwargs) -> List[Dict[str, Any]]:
+ """生成惰性气氛协议"""
+ debug_print(f"🛡️ 生成惰性气氛协议: {vessel} (使用 {gas})")
+ return generate_evacuateandrefill_protocol(G, vessel, gas, **kwargs)
+
# 测试函数
def test_evacuateandrefill_protocol():
"""测试抽真空充气协议"""
- debug_print("=== EVACUATE AND REFILL PROTOCOL 测试 ===")
- debug_print("测试完成")
+ debug_print("=== 抽真空充气协议增强中文版测试 ===")
+ debug_print("✅ 测试完成")
if __name__ == "__main__":
test_evacuateandrefill_protocol()
\ No newline at end of file
diff --git a/unilabos/compile/pump_protocol.py b/unilabos/compile/pump_protocol.py
index f214ff484..a54218e30 100644
--- a/unilabos/compile/pump_protocol.py
+++ b/unilabos/compile/pump_protocol.py
@@ -273,7 +273,6 @@ def generate_pump_protocol(
if not pump_backbone:
debug_print("PUMP_TRANSFER: 没有泵骨架节点,可能是直接容器连接或只有电磁阀")
- # 🔧 对于气体传输,这是正常的,直接返回空序列
return pump_action_sequence
if transfer_flowrate == 0:
@@ -319,10 +318,31 @@ def generate_pump_protocol(
volume_left = volume
debug_print(f"PUMP_TRANSFER: 需要 {repeats} 次转移,单次最大体积 {min_transfer_volume} mL")
+ # 🆕 只在开头打印总体概览
+ if repeats > 1:
+ debug_print(f"🔄 分批转移概览: 总体积 {volume:.2f}mL,需要 {repeats} 次转移")
+ logger.info(f"🔄 分批转移概览: 总体积 {volume:.2f}mL,需要 {repeats} 次转移")
+
+ # 🔧 创建一个自定义的wait动作,用于在执行时打印日志
+ def create_progress_log_action(message: str) -> Dict[str, Any]:
+ """创建一个特殊的等待动作,在执行时打印进度日志"""
+ return {
+ "action_name": "wait",
+ "action_kwargs": {
+ "time": 0.1, # 很短的等待时间
+ "progress_message": message # 自定义字段,用于进度日志
+ }
+ }
+
# 生成泵操作序列
for i in range(repeats):
current_volume = min(volume_left, min_transfer_volume)
+ # 🆕 在每次循环开始时添加进度日志
+ if repeats > 1:
+ start_message = f"🚀 准备开始第 {i+1}/{repeats} 次转移: {current_volume:.2f}mL ({from_vessel} → {to_vessel}) 🚰"
+ pump_action_sequence.append(create_progress_log_action(start_message))
+
# 🔧 修复:安全地获取边数据
def get_safe_edge_data(node_a, node_b, key):
try:
@@ -426,6 +446,16 @@ def get_safe_edge_data(node_a, node_b, key):
])
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}})
+ # 🆕 在每次循环结束时添加完成日志
+ if repeats > 1:
+ remaining_volume = volume_left - current_volume
+ if remaining_volume > 0:
+ end_message = f"✅ 第 {i+1}/{repeats} 次转移完成! 剩余 {remaining_volume:.2f}mL 待转移 ⏳"
+ else:
+ end_message = f"🎉 第 {i+1}/{repeats} 次转移完成! 全部 {volume:.2f}mL 转移完毕 ✨"
+
+ pump_action_sequence.append(create_progress_log_action(end_message))
+
volume_left -= current_volume
return pump_action_sequence
@@ -437,7 +467,745 @@ def generate_pump_protocol_with_rinsing(
to_vessel: str,
volume: float = 0.0,
amount: str = "",
- time: float = 0.0, # 🔧 修复:统一使用 time
+ time: float = 0.0, # 🔧 修复:统一使用 time
+ viscous: bool = False,
+ rinsing_solvent: str = "",
+ rinsing_volume: float = 0.0,
+ rinsing_repeats: int = 0,
+ solid: bool = False,
+ flowrate: float = 2.5,
+ transfer_flowrate: float = 0.5,
+ rate_spec: str = "",
+ event: str = "",
+ through: str = "",
+ **kwargs
+) -> List[Dict[str, Any]]:
+ """
+ 原有的同步版本,添加防冲突机制
+ """
+
+ # 添加执行锁,防止并发调用
+ import threading
+ if not hasattr(generate_pump_protocol_with_rinsing, '_lock'):
+ generate_pump_protocol_with_rinsing._lock = threading.Lock()
+
+ with generate_pump_protocol_with_rinsing._lock:
+ debug_print("=" * 60)
+ debug_print(f"PUMP_TRANSFER: 🚀 开始生成协议 (同步版本)")
+ debug_print(f" 📍 路径: {from_vessel} -> {to_vessel}")
+ debug_print(f" 🕐 时间戳: {time_module.time()}")
+ debug_print(f" 🔒 获得执行锁")
+ debug_print("=" * 60)
+
+ # 短暂延迟,避免快速重复调用
+ time_module.sleep(0.01)
+
+ debug_print("🔍 步骤1: 开始体积处理...")
+
+ # 1. 处理体积参数
+ final_volume = volume
+ debug_print(f"📋 初始设置: final_volume = {final_volume}")
+
+ # 🔧 修复:如果volume为0(ROS2传入的空值),从容器读取实际体积
+ if volume == 0.0:
+ debug_print("🎯 检测到 volume=0.0,开始自动体积检测...")
+
+ # 直接从源容器读取实际体积
+ actual_volume = get_vessel_liquid_volume(G, from_vessel)
+ debug_print(f"📖 从容器 '{from_vessel}' 读取到体积: {actual_volume}mL")
+
+ if actual_volume > 0:
+ final_volume = actual_volume
+ debug_print(f"✅ 成功设置体积为: {final_volume}mL")
+ else:
+ final_volume = 10.0 # 如果读取失败,使用默认值
+ logger.warning(f"⚠️ 无法从容器读取体积,使用默认值: {final_volume}mL")
+ else:
+ debug_print(f"📌 体积非零,直接使用: {final_volume}mL")
+
+ # 处理 amount 参数
+ if amount and amount.strip():
+ debug_print(f"🔍 检测到 amount 参数: '{amount}',开始解析...")
+ parsed_volume = _parse_amount_to_volume(amount)
+ debug_print(f"📖 从 amount 解析得到体积: {parsed_volume}mL")
+
+ if parsed_volume > 0:
+ final_volume = parsed_volume
+ debug_print(f"✅ 使用从 amount 解析的体积: {final_volume}mL")
+ elif parsed_volume == 0.0 and amount.lower().strip() == "all":
+ debug_print("🎯 检测到 amount='all',从容器读取全部体积...")
+ actual_volume = get_vessel_liquid_volume(G, from_vessel)
+ if actual_volume > 0:
+ final_volume = actual_volume
+ debug_print(f"✅ amount='all',设置体积为: {final_volume}mL")
+
+ # 最终体积验证
+ debug_print(f"🔍 步骤2: 最终体积验证...")
+ if final_volume <= 0:
+ logger.error(f"❌ 体积无效: {final_volume}mL")
+ final_volume = 10.0
+ logger.warning(f"⚠️ 强制设置为默认值: {final_volume}mL")
+
+ debug_print(f"✅ 最终确定体积: {final_volume}mL")
+
+ # 2. 处理流速参数
+ debug_print(f"🔍 步骤3: 处理流速参数...")
+ debug_print(f" - 原始 flowrate: {flowrate}")
+ debug_print(f" - 原始 transfer_flowrate: {transfer_flowrate}")
+
+ final_flowrate = flowrate if flowrate > 0 else 2.5
+ final_transfer_flowrate = transfer_flowrate if transfer_flowrate > 0 else 0.5
+
+ if flowrate <= 0:
+ logger.warning(f"⚠️ flowrate <= 0,修正为: {final_flowrate}mL/s")
+ if transfer_flowrate <= 0:
+ logger.warning(f"⚠️ transfer_flowrate <= 0,修正为: {final_transfer_flowrate}mL/s")
+
+ debug_print(f"✅ 修正后流速: flowrate={final_flowrate}mL/s, transfer_flowrate={final_transfer_flowrate}mL/s")
+
+ # 3. 根据时间计算流速
+ if time > 0 and final_volume > 0:
+ debug_print(f"🔍 步骤4: 根据时间计算流速...")
+ calculated_flowrate = final_volume / time
+ debug_print(f" - 计算得到流速: {calculated_flowrate}mL/s")
+
+ if flowrate <= 0 or flowrate == 2.5:
+ final_flowrate = min(calculated_flowrate, 10.0)
+ debug_print(f" - 调整 flowrate 为: {final_flowrate}mL/s")
+ if transfer_flowrate <= 0 or transfer_flowrate == 0.5:
+ final_transfer_flowrate = min(calculated_flowrate, 5.0)
+ debug_print(f" - 调整 transfer_flowrate 为: {final_transfer_flowrate}mL/s")
+
+ # 4. 根据速度规格调整
+ if rate_spec:
+ debug_print(f"🔍 步骤5: 根据速度规格调整...")
+ debug_print(f" - 速度规格: '{rate_spec}'")
+
+ if rate_spec == "dropwise":
+ final_flowrate = min(final_flowrate, 0.1)
+ final_transfer_flowrate = min(final_transfer_flowrate, 0.1)
+ debug_print(f" - dropwise模式,流速调整为: {final_flowrate}mL/s")
+ elif rate_spec == "slowly":
+ final_flowrate = min(final_flowrate, 0.5)
+ final_transfer_flowrate = min(final_transfer_flowrate, 0.3)
+ debug_print(f" - slowly模式,流速调整为: {final_flowrate}mL/s")
+ elif rate_spec == "quickly":
+ final_flowrate = max(final_flowrate, 5.0)
+ final_transfer_flowrate = max(final_transfer_flowrate, 2.0)
+ debug_print(f" - quickly模式,流速调整为: {final_flowrate}mL/s")
+
+ try:
+ # 🆕 修复:在这里调用带有循环日志的generate_pump_protocol_with_loop_logging函数
+ pump_action_sequence = generate_pump_protocol_with_loop_logging(
+ G, from_vessel, to_vessel, final_volume,
+ final_flowrate, final_transfer_flowrate
+ )
+
+ debug_print(f"🔓 释放执行锁")
+ return pump_action_sequence
+
+ except Exception as e:
+ logger.error(f"❌ 协议生成失败: {str(e)}")
+ return [
+ {
+ "device_id": "system",
+ "action_name": "log_message",
+ "action_kwargs": {
+ "message": f"❌ 协议生成失败: {str(e)}"
+ }
+ }
+ ]
+
+
+def generate_pump_protocol_with_loop_logging(
+ G: nx.DiGraph,
+ from_vessel: str,
+ to_vessel: str,
+ volume: float,
+ flowrate: float = 2.5,
+ transfer_flowrate: float = 0.5,
+) -> List[Dict[str, Any]]:
+ """
+ 生成泵操作的动作序列 - 带循环日志版本
+ 🔧 修复:正确处理包含电磁阀的路径,并在合适时机打印循环日志
+ """
+ pump_action_sequence = []
+ nodes = G.nodes(data=True)
+
+ # 验证输入参数
+ if volume <= 0:
+ logger.error(f"无效的体积参数: {volume}mL")
+ return pump_action_sequence
+
+ if flowrate <= 0:
+ flowrate = 2.5
+ logger.warning(f"flowrate <= 0,使用默认值 {flowrate}mL/s")
+
+ if transfer_flowrate <= 0:
+ transfer_flowrate = 0.5
+ logger.warning(f"transfer_flowrate <= 0,使用默认值 {transfer_flowrate}mL/s")
+
+ # 验证容器存在
+ if from_vessel not in G.nodes():
+ logger.error(f"源容器 '{from_vessel}' 不存在")
+ return pump_action_sequence
+
+ if to_vessel not in G.nodes():
+ logger.error(f"目标容器 '{to_vessel}' 不存在")
+ return pump_action_sequence
+
+ try:
+ shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
+ debug_print(f"PUMP_TRANSFER: 路径 {from_vessel} -> {to_vessel}: {shortest_path}")
+ except nx.NetworkXNoPath:
+ logger.error(f"无法找到从 '{from_vessel}' 到 '{to_vessel}' 的路径")
+ return pump_action_sequence
+
+ # 🔧 关键修复:正确构建泵骨架,排除容器和电磁阀
+ pump_backbone = []
+ for node in shortest_path:
+ # 跳过起始和结束容器
+ if node == from_vessel or node == to_vessel:
+ continue
+
+ # 跳过电磁阀(电磁阀不参与泵操作)
+ node_data = G.nodes.get(node, {})
+ node_class = node_data.get("class", "") or ""
+ if ("solenoid" in node_class.lower() or "solenoid_valve" in node.lower()):
+ debug_print(f"PUMP_TRANSFER: 跳过电磁阀 {node}")
+ continue
+
+ # 只包含多通阀和泵
+ if ("multiway" in node_class.lower() or "valve" in node_class.lower() or "pump" in node_class.lower()):
+ pump_backbone.append(node)
+
+ debug_print(f"PUMP_TRANSFER: 过滤后的泵骨架: {pump_backbone}")
+
+ if not pump_backbone:
+ debug_print("PUMP_TRANSFER: 没有泵骨架节点,可能是直接容器连接或只有电磁阀")
+ return pump_action_sequence
+
+ if transfer_flowrate == 0:
+ transfer_flowrate = flowrate
+
+ try:
+ pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone)
+ except Exception as e:
+ debug_print(f"PUMP_TRANSFER: 构建泵-阀门映射失败: {str(e)}")
+ return pump_action_sequence
+
+ if not pumps_from_node:
+ debug_print("PUMP_TRANSFER: 没有可用的泵映射")
+ return pump_action_sequence
+
+ # 🔧 修复:安全地获取最小转移体积
+ try:
+ min_transfer_volumes = []
+ for node in pump_backbone:
+ if node in pumps_from_node:
+ pump_node = pumps_from_node[node]
+ if pump_node in nodes:
+ pump_config = nodes[pump_node].get("config", {})
+ max_volume = pump_config.get("max_volume")
+ if max_volume is not None:
+ min_transfer_volumes.append(max_volume)
+
+ if min_transfer_volumes:
+ min_transfer_volume = min(min_transfer_volumes)
+ else:
+ min_transfer_volume = 25.0 # 默认值
+ debug_print(f"PUMP_TRANSFER: 无法获取泵的最大体积,使用默认值: {min_transfer_volume}mL")
+ except Exception as e:
+ debug_print(f"PUMP_TRANSFER: 获取最小转移体积失败: {str(e)}")
+ min_transfer_volume = 25.0 # 默认值
+
+ repeats = int(np.ceil(volume / min_transfer_volume))
+
+ if repeats > 1 and (from_vessel.startswith("pump") or to_vessel.startswith("pump")):
+ logger.error("Cannot transfer volume larger than min_transfer_volume between two pumps.")
+ return pump_action_sequence
+
+ volume_left = volume
+ debug_print(f"PUMP_TRANSFER: 需要 {repeats} 次转移,单次最大体积 {min_transfer_volume} mL")
+
+ # 🆕 只在开头打印总体概览
+ if repeats > 1:
+ debug_print(f"🔄 分批转移概览: 总体积 {volume:.2f}mL,需要 {repeats} 次转移")
+ logger.info(f"🔄 分批转移概览: 总体积 {volume:.2f}mL,需要 {repeats} 次转移")
+
+ # 🔧 创建一个自定义的wait动作,用于在执行时打印日志
+ def create_progress_log_action(message: str) -> Dict[str, Any]:
+ """创建一个特殊的等待动作,在执行时打印进度日志"""
+ return {
+ "action_name": "wait",
+ "action_kwargs": {
+ "time": 0.1, # 很短的等待时间
+ "progress_message": message # 自定义字段,用于进度日志
+ }
+ }
+
+ # 生成泵操作序列
+ for i in range(repeats):
+ current_volume = min(volume_left, min_transfer_volume)
+
+ # 🆕 在每次循环开始时添加进度日志
+ if repeats > 1:
+ start_message = f"🚀 准备开始第 {i+1}/{repeats} 次转移: {current_volume:.2f}mL ({from_vessel} → {to_vessel}) 🚰"
+ pump_action_sequence.append(create_progress_log_action(start_message))
+
+ # 🔧 修复:安全地获取边数据
+ def get_safe_edge_data(node_a, node_b, key):
+ try:
+ edge_data = G.get_edge_data(node_a, node_b)
+ if edge_data and "port" in edge_data:
+ port_data = edge_data["port"]
+ if isinstance(port_data, dict) and key in port_data:
+ return port_data[key]
+ return "default"
+ except Exception as e:
+ debug_print(f"PUMP_TRANSFER: 获取边数据失败 {node_a}->{node_b}: {str(e)}")
+ return "default"
+
+ # 从源容器吸液
+ if not from_vessel.startswith("pump") and pump_backbone:
+ first_pump_node = pump_backbone[0]
+ if first_pump_node in valve_from_node and first_pump_node in pumps_from_node:
+ port_command = get_safe_edge_data(first_pump_node, from_vessel, first_pump_node)
+ pump_action_sequence.extend([
+ {
+ "device_id": valve_from_node[first_pump_node],
+ "action_name": "set_valve_position",
+ "action_kwargs": {
+ "command": port_command
+ }
+ },
+ {
+ "device_id": pumps_from_node[first_pump_node],
+ "action_name": "set_position",
+ "action_kwargs": {
+ "position": float(current_volume),
+ "max_velocity": transfer_flowrate
+ }
+ }
+ ])
+ pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}})
+
+ # 泵间转移
+ for nodeA, nodeB in zip(pump_backbone[:-1], pump_backbone[1:]):
+ if nodeA in valve_from_node and nodeB in valve_from_node and nodeA in pumps_from_node and nodeB in pumps_from_node:
+ port_a = get_safe_edge_data(nodeA, nodeB, nodeA)
+ port_b = get_safe_edge_data(nodeB, nodeA, nodeB)
+
+ pump_action_sequence.append([
+ {
+ "device_id": valve_from_node[nodeA],
+ "action_name": "set_valve_position",
+ "action_kwargs": {
+ "command": port_a
+ }
+ },
+ {
+ "device_id": valve_from_node[nodeB],
+ "action_name": "set_valve_position",
+ "action_kwargs": {
+ "command": port_b
+ }
+ }
+ ])
+ pump_action_sequence.append([
+ {
+ "device_id": pumps_from_node[nodeA],
+ "action_name": "set_position",
+ "action_kwargs": {
+ "position": 0.0,
+ "max_velocity": transfer_flowrate
+ }
+ },
+ {
+ "device_id": pumps_from_node[nodeB],
+ "action_name": "set_position",
+ "action_kwargs": {
+ "position": float(current_volume),
+ "max_velocity": transfer_flowrate
+ }
+ }
+ ])
+ pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}})
+
+ # 排液到目标容器
+ if not to_vessel.startswith("pump") and pump_backbone:
+ last_pump_node = pump_backbone[-1]
+ if last_pump_node in valve_from_node and last_pump_node in pumps_from_node:
+ port_command = get_safe_edge_data(last_pump_node, to_vessel, last_pump_node)
+ pump_action_sequence.extend([
+ {
+ "device_id": valve_from_node[last_pump_node],
+ "action_name": "set_valve_position",
+ "action_kwargs": {
+ "command": port_command
+ }
+ },
+ {
+ "device_id": pumps_from_node[last_pump_node],
+ "action_name": "set_position",
+ "action_kwargs": {
+ "position": 0.0,
+ "max_velocity": flowrate
+ }
+ }
+ ])
+ pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}})
+
+ # 🆕 在每次循环结束时添加完成日志
+ if repeats > 1:
+ remaining_volume = volume_left - current_volume
+ if remaining_volume > 0:
+ end_message = f"✅ 第 {i+1}/{repeats} 次转移完成! 剩余 {remaining_volume:.2f}mL 待转移 ⏳"
+ else:
+ end_message = f"🎉 第 {i+1}/{repeats} 次转移完成! 全部 {volume:.2f}mL 转移完毕 ✨"
+
+ pump_action_sequence.append(create_progress_log_action(end_message))
+
+ volume_left -= current_volume
+
+ return pump_action_sequence
+
+
+def generate_pump_protocol_with_rinsing(
+ G: nx.DiGraph,
+ from_vessel: str,
+ to_vessel: str,
+ volume: float = 0.0,
+ amount: str = "",
+ time: float = 0.0, # 🔧 修复:统一使用 time
+ viscous: bool = False,
+ rinsing_solvent: str = "",
+ rinsing_volume: float = 0.0,
+ rinsing_repeats: int = 0,
+ solid: bool = False,
+ flowrate: float = 2.5,
+ transfer_flowrate: float = 0.5,
+ rate_spec: str = "",
+ event: str = "",
+ through: str = "",
+ **kwargs
+) -> List[Dict[str, Any]]:
+ """
+ 增强兼容性的泵转移协议生成器,支持自动体积检测
+ """
+ debug_print("=" * 60)
+ debug_print(f"PUMP_TRANSFER: 🚀 开始生成协议")
+ debug_print(f" 📍 路径: {from_vessel} -> {to_vessel}")
+ debug_print(f" 🕐 时间戳: {time_module.time()}")
+ debug_print(f" 📊 原始参数:")
+ debug_print(f" - volume: {volume} (类型: {type(volume)})")
+ debug_print(f" - amount: '{amount}'")
+ debug_print(f" - time: {time}") # 🔧 修复:统一使用 time
+ debug_print(f" - flowrate: {flowrate}")
+ debug_print(f" - transfer_flowrate: {transfer_flowrate}")
+ debug_print(f" - rate_spec: '{rate_spec}'")
+ debug_print("=" * 60)
+
+ # ========== 🔧 核心修复:智能体积处理 ==========
+
+ debug_print("🔍 步骤1: 开始体积处理...")
+
+ # 1. 处理体积参数
+ final_volume = volume
+ debug_print(f"📋 初始设置: final_volume = {final_volume}")
+
+ # 🔧 修复:如果volume为0(ROS2传入的空值),从容器读取实际体积
+ if volume == 0.0:
+ debug_print("🎯 检测到 volume=0.0,开始自动体积检测...")
+
+ # 直接从源容器读取实际体积
+ actual_volume = get_vessel_liquid_volume(G, from_vessel)
+ debug_print(f"📖 从容器 '{from_vessel}' 读取到体积: {actual_volume}mL")
+
+ if actual_volume > 0:
+ final_volume = actual_volume
+ debug_print(f"✅ 成功设置体积为: {final_volume}mL")
+ else:
+ final_volume = 10.0 # 如果读取失败,使用默认值
+ debug_print(f"⚠️ 无法从容器读取体积,使用默认值: {final_volume}mL")
+ else:
+ debug_print(f"📌 体积非零,直接使用: {final_volume}mL")
+
+ # 处理 amount 参数
+ if amount and amount.strip():
+ debug_print(f"🔍 检测到 amount 参数: '{amount}',开始解析...")
+ parsed_volume = _parse_amount_to_volume(amount)
+ debug_print(f"📖 从 amount 解析得到体积: {parsed_volume}mL")
+
+ if parsed_volume > 0:
+ final_volume = parsed_volume
+ debug_print(f"✅ 使用从 amount 解析的体积: {final_volume}mL")
+ elif parsed_volume == 0.0 and amount.lower().strip() == "all":
+ debug_print("🎯 检测到 amount='all',从容器读取全部体积...")
+ actual_volume = get_vessel_liquid_volume(G, from_vessel)
+ if actual_volume > 0:
+ final_volume = actual_volume
+ debug_print(f"✅ amount='all',设置体积为: {final_volume}mL")
+
+ # 最终体积验证
+ debug_print(f"🔍 步骤2: 最终体积验证...")
+ if final_volume <= 0:
+ debug_print(f"❌ 体积无效: {final_volume}mL")
+ final_volume = 10.0
+ debug_print(f"⚠️ 强制设置为默认值: {final_volume}mL")
+
+ debug_print(f"✅ 最终确定体积: {final_volume}mL")
+
+ # 2. 处理流速参数
+ debug_print(f"🔍 步骤3: 处理流速参数...")
+ debug_print(f" - 原始 flowrate: {flowrate}")
+ debug_print(f" - 原始 transfer_flowrate: {transfer_flowrate}")
+
+ final_flowrate = flowrate if flowrate > 0 else 2.5
+ final_transfer_flowrate = transfer_flowrate if transfer_flowrate > 0 else 0.5
+
+ if flowrate <= 0:
+ debug_print(f"⚠️ flowrate <= 0,修正为: {final_flowrate}mL/s")
+ if transfer_flowrate <= 0:
+ debug_print(f"⚠️ transfer_flowrate <= 0,修正为: {final_transfer_flowrate}mL/s")
+
+ debug_print(f"✅ 修正后流速: flowrate={final_flowrate}mL/s, transfer_flowrate={final_transfer_flowrate}mL/s")
+
+ # 3. 根据时间计算流速
+ if time > 0 and final_volume > 0: # 🔧 修复:统一使用 time
+ debug_print(f"🔍 步骤4: 根据时间计算流速...")
+ calculated_flowrate = final_volume / time
+ debug_print(f" - 计算得到流速: {calculated_flowrate}mL/s")
+
+ if flowrate <= 0 or flowrate == 2.5:
+ final_flowrate = min(calculated_flowrate, 10.0)
+ debug_print(f" - 调整 flowrate 为: {final_flowrate}mL/s")
+ if transfer_flowrate <= 0 or transfer_flowrate == 0.5:
+ final_transfer_flowrate = min(calculated_flowrate, 5.0)
+ debug_print(f" - 调整 transfer_flowrate 为: {final_transfer_flowrate}mL/s")
+
+ # 4. 根据速度规格调整
+ if rate_spec:
+ debug_print(f"🔍 步骤5: 根据速度规格调整...")
+ debug_print(f" - 速度规格: '{rate_spec}'")
+
+ if rate_spec == "dropwise":
+ final_flowrate = min(final_flowrate, 0.1)
+ final_transfer_flowrate = min(final_transfer_flowrate, 0.1)
+ debug_print(f" - dropwise模式,流速调整为: {final_flowrate}mL/s")
+ elif rate_spec == "slowly":
+ final_flowrate = min(final_flowrate, 0.5)
+ final_transfer_flowrate = min(final_transfer_flowrate, 0.3)
+ debug_print(f" - slowly模式,流速调整为: {final_flowrate}mL/s")
+ elif rate_spec == "quickly":
+ final_flowrate = max(final_flowrate, 5.0)
+ final_transfer_flowrate = max(final_transfer_flowrate, 2.0)
+ debug_print(f" - quickly模式,流速调整为: {final_flowrate}mL/s")
+
+ # # 5. 处理冲洗参数
+ # debug_print(f"🔍 步骤6: 处理冲洗参数...")
+ # final_rinsing_solvent = rinsing_solvent
+ # final_rinsing_volume = rinsing_volume if rinsing_volume > 0 else 5.0
+ # final_rinsing_repeats = rinsing_repeats if rinsing_repeats > 0 else 2
+
+ # if rinsing_volume <= 0:
+ # debug_print(f"⚠️ rinsing_volume <= 0,修正为: {final_rinsing_volume}mL")
+ # if rinsing_repeats <= 0:
+ # debug_print(f"⚠️ rinsing_repeats <= 0,修正为: {final_rinsing_repeats}次")
+
+ # # 根据物理属性调整冲洗参数
+ # if viscous or solid:
+ # final_rinsing_repeats = max(final_rinsing_repeats, 3)
+ # final_rinsing_volume = max(final_rinsing_volume, 10.0)
+ # debug_print(f"🧪 粘稠/固体物质,调整冲洗参数:{final_rinsing_repeats}次,{final_rinsing_volume}mL")
+
+ # 参数总结
+ debug_print("📊 最终参数总结:")
+ debug_print(f" - 体积: {final_volume}mL")
+ debug_print(f" - 流速: {final_flowrate}mL/s")
+ debug_print(f" - 转移流速: {final_transfer_flowrate}mL/s")
+ # debug_print(f" - 冲洗溶剂: '{final_rinsing_solvent}'")
+ # debug_print(f" - 冲洗体积: {final_rinsing_volume}mL")
+ # debug_print(f" - 冲洗次数: {final_rinsing_repeats}次")
+
+ # ========== 执行基础转移 ==========
+
+ debug_print("🔧 步骤7: 开始执行基础转移...")
+
+ try:
+ debug_print(f" - 调用 generate_pump_protocol...")
+ debug_print(f" - 参数: G, '{from_vessel}', '{to_vessel}', {final_volume}, {final_flowrate}, {final_transfer_flowrate}")
+
+ pump_action_sequence = generate_pump_protocol(
+ G, from_vessel, to_vessel, final_volume,
+ final_flowrate, final_transfer_flowrate
+ )
+
+ debug_print(f" - generate_pump_protocol 返回结果:")
+ debug_print(f" - 动作序列长度: {len(pump_action_sequence)}")
+ debug_print(f" - 动作序列是否为空: {len(pump_action_sequence) == 0}")
+
+ if not pump_action_sequence:
+ debug_print("❌ 基础转移协议生成为空,可能是路径问题")
+ debug_print(f" - 源容器存在: {from_vessel in G.nodes()}")
+ debug_print(f" - 目标容器存在: {to_vessel in G.nodes()}")
+
+ if from_vessel in G.nodes() and to_vessel in G.nodes():
+ try:
+ path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
+ debug_print(f" - 路径存在: {path}")
+ except Exception as path_error:
+ debug_print(f" - 无法找到路径: {str(path_error)}")
+
+ return [
+ {
+ "device_id": "system",
+ "action_name": "log_message",
+ "action_kwargs": {
+ "message": f"⚠️ 路径问题,无法转移: {final_volume}mL 从 {from_vessel} 到 {to_vessel}"
+ }
+ }
+ ]
+
+ debug_print(f"✅ 基础转移生成了 {len(pump_action_sequence)} 个动作")
+
+ # 打印前几个动作用于调试
+ if len(pump_action_sequence) > 0:
+ debug_print("🔍 前几个动作预览:")
+ for i, action in enumerate(pump_action_sequence[:3]):
+ debug_print(f" 动作 {i+1}: {action}")
+ if len(pump_action_sequence) > 3:
+ debug_print(f" ... 还有 {len(pump_action_sequence) - 3} 个动作")
+
+ except Exception as e:
+ debug_print(f"❌ 基础转移失败: {str(e)}")
+ import traceback
+ debug_print(f"详细错误: {traceback.format_exc()}")
+ return [
+ {
+ "device_id": "system",
+ "action_name": "log_message",
+ "action_kwargs": {
+ "message": f"❌ 转移失败: {final_volume}mL 从 {from_vessel} 到 {to_vessel}, 错误: {str(e)}"
+ }
+ }
+ ]
+
+ # ========== 执行冲洗操作 ==========
+
+ # debug_print("🔧 步骤8: 检查冲洗操作...")
+
+ # if final_rinsing_solvent and final_rinsing_solvent.strip() and final_rinsing_repeats > 0:
+ # debug_print(f"🧽 开始冲洗操作,溶剂: '{final_rinsing_solvent}'")
+
+ # try:
+ # if final_rinsing_solvent.strip() != "air":
+ # debug_print(" - 执行液体冲洗...")
+ # rinsing_actions = _generate_rinsing_sequence(
+ # G, from_vessel, to_vessel, final_rinsing_solvent,
+ # final_rinsing_volume, final_rinsing_repeats,
+ # final_flowrate, final_transfer_flowrate
+ # )
+ # pump_action_sequence.extend(rinsing_actions)
+ # debug_print(f" - 添加了 {len(rinsing_actions)} 个冲洗动作")
+ # else:
+ # debug_print(" - 执行空气冲洗...")
+ # air_rinsing_actions = _generate_air_rinsing_sequence(
+ # G, from_vessel, to_vessel, final_rinsing_volume, final_rinsing_repeats,
+ # final_flowrate, final_transfer_flowrate
+ # )
+ # pump_action_sequence.extend(air_rinsing_actions)
+ # debug_print(f" - 添加了 {len(air_rinsing_actions)} 个空气冲洗动作")
+ # except Exception as e:
+ # debug_print(f"⚠️ 冲洗操作失败: {str(e)},跳过冲洗")
+ # else:
+ # debug_print(f"⏭️ 跳过冲洗操作")
+ # debug_print(f" - 溶剂: '{final_rinsing_solvent}'")
+ # debug_print(f" - 次数: {final_rinsing_repeats}")
+ # debug_print(f" - 条件满足: {bool(final_rinsing_solvent and final_rinsing_solvent.strip() and final_rinsing_repeats > 0)}")
+
+ # ========== 最终结果 ==========
+
+ debug_print("=" * 60)
+ debug_print(f"🎉 PUMP_TRANSFER: 协议生成完成")
+ debug_print(f" 📊 总动作数: {len(pump_action_sequence)}")
+ debug_print(f" 📋 最终体积: {final_volume}mL")
+ debug_print(f" 🚀 执行路径: {from_vessel} -> {to_vessel}")
+
+ # 最终验证
+ if len(pump_action_sequence) == 0:
+ debug_print("🚨 协议生成结果为空!这是异常情况")
+ return [
+ {
+ "device_id": "system",
+ "action_name": "log_message",
+ "action_kwargs": {
+ "message": f"🚨 协议生成失败: 无法生成任何动作序列"
+ }
+ }
+ ]
+
+ debug_print("=" * 60)
+ return pump_action_sequence
+
+
+async def generate_pump_protocol_with_rinsing_async(
+ G: nx.DiGraph,
+ from_vessel: str,
+ to_vessel: str,
+ volume: float = 0.0,
+ amount: str = "",
+ time: float = 0.0,
+ viscous: bool = False,
+ rinsing_solvent: str = "",
+ rinsing_volume: float = 0.0,
+ rinsing_repeats: int = 0,
+ solid: bool = False,
+ flowrate: float = 2.5,
+ transfer_flowrate: float = 0.5,
+ rate_spec: str = "",
+ event: str = "",
+ through: str = "",
+ **kwargs
+) -> List[Dict[str, Any]]:
+ """
+ 异步版本的泵转移协议生成器,避免并发问题
+ """
+ debug_print("=" * 60)
+ debug_print(f"PUMP_TRANSFER: 🚀 开始生成协议 (异步版本)")
+ debug_print(f" 📍 路径: {from_vessel} -> {to_vessel}")
+ debug_print(f" 🕐 时间戳: {time_module.time()}")
+ debug_print("=" * 60)
+
+ # 添加唯一标识符
+ protocol_id = f"pump_transfer_{int(time_module.time() * 1000000)}"
+ debug_print(f"📋 协议ID: {protocol_id}")
+
+ # 调用原有的同步版本
+ result = generate_pump_protocol_with_rinsing(
+ G, from_vessel, to_vessel, volume, amount, time, viscous,
+ rinsing_solvent, rinsing_volume, rinsing_repeats, solid,
+ flowrate, transfer_flowrate, rate_spec, event, through, **kwargs
+ )
+
+ # 为每个动作添加唯一标识
+ for i, action in enumerate(result):
+ if isinstance(action, dict):
+ action['_protocol_id'] = protocol_id
+ action['_action_sequence'] = i
+ action['_timestamp'] = time_module.time()
+
+ debug_print(f"📊 协议 {protocol_id} 生成完成,共 {len(result)} 个动作")
+ return result
+
+# 保持原有的同步版本兼容性
+def generate_pump_protocol_with_rinsing(
+ G: nx.DiGraph,
+ from_vessel: str,
+ to_vessel: str,
+ volume: float = 0.0,
+ amount: str = "",
+ time: float = 0.0,
viscous: bool = False,
rinsing_solvent: str = "",
rinsing_volume: float = 0.0,
@@ -451,116 +1219,118 @@ def generate_pump_protocol_with_rinsing(
**kwargs
) -> List[Dict[str, Any]]:
"""
- 增强兼容性的泵转移协议生成器,支持自动体积检测
+ 原有的同步版本,添加防冲突机制
"""
- debug_print("=" * 60)
- debug_print(f"PUMP_TRANSFER: 🚀 开始生成协议")
- debug_print(f" 📍 路径: {from_vessel} -> {to_vessel}")
- debug_print(f" 🕐 时间戳: {time_module.time()}")
- debug_print(f" 📊 原始参数:")
- debug_print(f" - volume: {volume} (类型: {type(volume)})")
- debug_print(f" - amount: '{amount}'")
- debug_print(f" - time: {time}") # 🔧 修复:统一使用 time
- debug_print(f" - flowrate: {flowrate}")
- debug_print(f" - transfer_flowrate: {transfer_flowrate}")
- debug_print(f" - rate_spec: '{rate_spec}'")
- debug_print("=" * 60)
-
- # ========== 🔧 核心修复:智能体积处理 ==========
-
- debug_print("🔍 步骤1: 开始体积处理...")
- # 1. 处理体积参数
- final_volume = volume
- debug_print(f"📋 初始设置: final_volume = {final_volume}")
+ # 添加执行锁,防止并发调用
+ import threading
+ if not hasattr(generate_pump_protocol_with_rinsing, '_lock'):
+ generate_pump_protocol_with_rinsing._lock = threading.Lock()
- # 🔧 修复:如果volume为0(ROS2传入的空值),从容器读取实际体积
- if volume == 0.0:
- debug_print("🎯 检测到 volume=0.0,开始自动体积检测...")
+ with generate_pump_protocol_with_rinsing._lock:
+ debug_print("=" * 60)
+ debug_print(f"PUMP_TRANSFER: 🚀 开始生成协议 (同步版本)")
+ debug_print(f" 📍 路径: {from_vessel} -> {to_vessel}")
+ debug_print(f" 🕐 时间戳: {time_module.time()}")
+ debug_print(f" 🔒 获得执行锁")
+ debug_print("=" * 60)
- # 直接从源容器读取实际体积
- actual_volume = get_vessel_liquid_volume(G, from_vessel)
- debug_print(f"📖 从容器 '{from_vessel}' 读取到体积: {actual_volume}mL")
+ # 短暂延迟,避免快速重复调用
+ time_module.sleep(0.01)
- if actual_volume > 0:
- final_volume = actual_volume
- debug_print(f"✅ 成功设置体积为: {final_volume}mL")
- else:
- final_volume = 10.0 # 如果读取失败,使用默认值
- debug_print(f"⚠️ 无法从容器读取体积,使用默认值: {final_volume}mL")
- else:
- debug_print(f"📌 体积非零,直接使用: {final_volume}mL")
-
- # 处理 amount 参数
- if amount and amount.strip():
- debug_print(f"🔍 检测到 amount 参数: '{amount}',开始解析...")
- parsed_volume = _parse_amount_to_volume(amount)
- debug_print(f"📖 从 amount 解析得到体积: {parsed_volume}mL")
+ debug_print("🔍 步骤1: 开始体积处理...")
- if parsed_volume > 0:
- final_volume = parsed_volume
- debug_print(f"✅ 使用从 amount 解析的体积: {final_volume}mL")
- elif parsed_volume == 0.0 and amount.lower().strip() == "all":
- debug_print("🎯 检测到 amount='all',从容器读取全部体积...")
+ # 1. 处理体积参数
+ final_volume = volume
+ debug_print(f"📋 初始设置: final_volume = {final_volume}")
+
+ # 🔧 修复:如果volume为0(ROS2传入的空值),从容器读取实际体积
+ if volume == 0.0:
+ debug_print("🎯 检测到 volume=0.0,开始自动体积检测...")
+
+ # 直接从源容器读取实际体积
actual_volume = get_vessel_liquid_volume(G, from_vessel)
+ debug_print(f"📖 从容器 '{from_vessel}' 读取到体积: {actual_volume}mL")
+
if actual_volume > 0:
final_volume = actual_volume
- debug_print(f"✅ amount='all',设置体积为: {final_volume}mL")
-
- # 最终体积验证
- debug_print(f"🔍 步骤2: 最终体积验证...")
- if final_volume <= 0:
- debug_print(f"❌ 体积无效: {final_volume}mL")
- final_volume = 10.0
- debug_print(f"⚠️ 强制设置为默认值: {final_volume}mL")
-
- debug_print(f"✅ 最终确定体积: {final_volume}mL")
-
- # 2. 处理流速参数
- debug_print(f"🔍 步骤3: 处理流速参数...")
- debug_print(f" - 原始 flowrate: {flowrate}")
- debug_print(f" - 原始 transfer_flowrate: {transfer_flowrate}")
-
- final_flowrate = flowrate if flowrate > 0 else 2.5
- final_transfer_flowrate = transfer_flowrate if transfer_flowrate > 0 else 0.5
-
- if flowrate <= 0:
- debug_print(f"⚠️ flowrate <= 0,修正为: {final_flowrate}mL/s")
- if transfer_flowrate <= 0:
- debug_print(f"⚠️ transfer_flowrate <= 0,修正为: {final_transfer_flowrate}mL/s")
-
- debug_print(f"✅ 修正后流速: flowrate={final_flowrate}mL/s, transfer_flowrate={final_transfer_flowrate}mL/s")
-
- # 3. 根据时间计算流速
- if time > 0 and final_volume > 0: # 🔧 修复:统一使用 time
- debug_print(f"🔍 步骤4: 根据时间计算流速...")
- calculated_flowrate = final_volume / time
- debug_print(f" - 计算得到流速: {calculated_flowrate}mL/s")
+ debug_print(f"✅ 成功设置体积为: {final_volume}mL")
+ else:
+ final_volume = 10.0 # 如果读取失败,使用默认值
+ logger.warning(f"⚠️ 无法从容器读取体积,使用默认值: {final_volume}mL")
+ else:
+ debug_print(f"📌 体积非零,直接使用: {final_volume}mL")
- if flowrate <= 0 or flowrate == 2.5:
- final_flowrate = min(calculated_flowrate, 10.0)
- debug_print(f" - 调整 flowrate 为: {final_flowrate}mL/s")
- if transfer_flowrate <= 0 or transfer_flowrate == 0.5:
- final_transfer_flowrate = min(calculated_flowrate, 5.0)
- debug_print(f" - 调整 transfer_flowrate 为: {final_transfer_flowrate}mL/s")
-
- # 4. 根据速度规格调整
- if rate_spec:
- debug_print(f"🔍 步骤5: 根据速度规格调整...")
- debug_print(f" - 速度规格: '{rate_spec}'")
+ # 处理 amount 参数
+ if amount and amount.strip():
+ debug_print(f"🔍 检测到 amount 参数: '{amount}',开始解析...")
+ parsed_volume = _parse_amount_to_volume(amount)
+ debug_print(f"📖 从 amount 解析得到体积: {parsed_volume}mL")
+
+ if parsed_volume > 0:
+ final_volume = parsed_volume
+ debug_print(f"✅ 使用从 amount 解析的体积: {final_volume}mL")
+ elif parsed_volume == 0.0 and amount.lower().strip() == "all":
+ debug_print("🎯 检测到 amount='all',从容器读取全部体积...")
+ actual_volume = get_vessel_liquid_volume(G, from_vessel)
+ if actual_volume > 0:
+ final_volume = actual_volume
+ debug_print(f"✅ amount='all',设置体积为: {final_volume}mL")
- if rate_spec == "dropwise":
- final_flowrate = min(final_flowrate, 0.1)
- final_transfer_flowrate = min(final_transfer_flowrate, 0.1)
- debug_print(f" - dropwise模式,流速调整为: {final_flowrate}mL/s")
- elif rate_spec == "slowly":
- final_flowrate = min(final_flowrate, 0.5)
- final_transfer_flowrate = min(final_transfer_flowrate, 0.3)
- debug_print(f" - slowly模式,流速调整为: {final_flowrate}mL/s")
- elif rate_spec == "quickly":
- final_flowrate = max(final_flowrate, 5.0)
- final_transfer_flowrate = max(final_transfer_flowrate, 2.0)
- debug_print(f" - quickly模式,流速调整为: {final_flowrate}mL/s")
+ # 最终体积验证
+ debug_print(f"🔍 步骤2: 最终体积验证...")
+ if final_volume <= 0:
+ logger.error(f"❌ 体积无效: {final_volume}mL")
+ final_volume = 10.0
+ logger.warning(f"⚠️ 强制设置为默认值: {final_volume}mL")
+
+ debug_print(f"✅ 最终确定体积: {final_volume}mL")
+
+ # 2. 处理流速参数
+ debug_print(f"🔍 步骤3: 处理流速参数...")
+ debug_print(f" - 原始 flowrate: {flowrate}")
+ debug_print(f" - 原始 transfer_flowrate: {transfer_flowrate}")
+
+ final_flowrate = flowrate if flowrate > 0 else 2.5
+ final_transfer_flowrate = transfer_flowrate if transfer_flowrate > 0 else 0.5
+
+ if flowrate <= 0:
+ logger.warning(f"⚠️ flowrate <= 0,修正为: {final_flowrate}mL/s")
+ if transfer_flowrate <= 0:
+ logger.warning(f"⚠️ transfer_flowrate <= 0,修正为: {final_transfer_flowrate}mL/s")
+
+ debug_print(f"✅ 修正后流速: flowrate={final_flowrate}mL/s, transfer_flowrate={final_transfer_flowrate}mL/s")
+
+ # 3. 根据时间计算流速
+ if time > 0 and final_volume > 0:
+ debug_print(f"🔍 步骤4: 根据时间计算流速...")
+ calculated_flowrate = final_volume / time
+ debug_print(f" - 计算得到流速: {calculated_flowrate}mL/s")
+
+ if flowrate <= 0 or flowrate == 2.5:
+ final_flowrate = min(calculated_flowrate, 10.0)
+ debug_print(f" - 调整 flowrate 为: {final_flowrate}mL/s")
+ if transfer_flowrate <= 0 or transfer_flowrate == 0.5:
+ final_transfer_flowrate = min(calculated_flowrate, 5.0)
+ debug_print(f" - 调整 transfer_flowrate 为: {final_transfer_flowrate}mL/s")
+
+ # 4. 根据速度规格调整
+ if rate_spec:
+ debug_print(f"🔍 步骤5: 根据速度规格调整...")
+ debug_print(f" - 速度规格: '{rate_spec}'")
+
+ if rate_spec == "dropwise":
+ final_flowrate = min(final_flowrate, 0.1)
+ final_transfer_flowrate = min(final_transfer_flowrate, 0.1)
+ debug_print(f" - dropwise模式,流速调整为: {final_flowrate}mL/s")
+ elif rate_spec == "slowly":
+ final_flowrate = min(final_flowrate, 0.5)
+ final_transfer_flowrate = min(final_transfer_flowrate, 0.3)
+ debug_print(f" - slowly模式,流速调整为: {final_flowrate}mL/s")
+ elif rate_spec == "quickly":
+ final_flowrate = max(final_flowrate, 5.0)
+ final_transfer_flowrate = max(final_transfer_flowrate, 2.0)
+ debug_print(f" - quickly模式,流速调整为: {final_flowrate}mL/s")
# # 5. 处理冲洗参数
# debug_print(f"🔍 步骤6: 处理冲洗参数...")
@@ -569,9 +1339,9 @@ def generate_pump_protocol_with_rinsing(
# final_rinsing_repeats = rinsing_repeats if rinsing_repeats > 0 else 2
# if rinsing_volume <= 0:
- # debug_print(f"⚠️ rinsing_volume <= 0,修正为: {final_rinsing_volume}mL")
+ # logger.warning(f"⚠️ rinsing_volume <= 0,修正为: {final_rinsing_volume}mL")
# if rinsing_repeats <= 0:
- # debug_print(f"⚠️ rinsing_repeats <= 0,修正为: {final_rinsing_repeats}次")
+ # logger.warning(f"⚠️ rinsing_repeats <= 0,修正为: {final_rinsing_repeats}次")
# # 根据物理属性调整冲洗参数
# if viscous or solid:
@@ -726,7 +1496,7 @@ async def generate_pump_protocol_with_rinsing_async(
rate_spec: str = "",
event: str = "",
through: str = "",
- **kwargs
+ **kwargs
) -> List[Dict[str, Any]]:
"""
异步版本的泵转移协议生成器,避免并发问题
@@ -891,58 +1661,56 @@ def generate_pump_protocol_with_rinsing(
final_flowrate = max(final_flowrate, 5.0)
final_transfer_flowrate = max(final_transfer_flowrate, 2.0)
debug_print(f" - quickly模式,流速调整为: {final_flowrate}mL/s")
+
+ # # 5. 处理冲洗参数
+ # debug_print(f"🔍 步骤6: 处理冲洗参数...")
+ # final_rinsing_solvent = rinsing_solvent
+ # final_rinsing_volume = rinsing_volume if rinsing_volume > 0 else 5.0
+ # final_rinsing_repeats = rinsing_repeats if rinsing_repeats > 0 else 2
+
+ # if rinsing_volume <= 0:
+ # logger.warning(f"⚠️ rinsing_volume <= 0,修正为: {final_rinsing_volume}mL")
+ # if rinsing_repeats <= 0:
+ # logger.warning(f"⚠️ rinsing_repeats <= 0,修正为: {final_rinsing_repeats}次")
+
+ # # 根据物理属性调整冲洗参数
+ # if viscous or solid:
+ # final_rinsing_repeats = max(final_rinsing_repeats, 3)
+ # final_rinsing_volume = max(final_rinsing_volume, 10.0)
+ # debug_print(f"🧪 粘稠/固体物质,调整冲洗参数:{final_rinsing_repeats}次,{final_rinsing_volume}mL")
+
+ try:
+ pump_action_sequence = generate_pump_protocol(
+ G, from_vessel, to_vessel, final_volume,
+ flowrate, transfer_flowrate
+ )
- # # 5. 处理冲洗参数
- # debug_print(f"🔍 步骤6: 处理冲洗参数...")
- # final_rinsing_solvent = rinsing_solvent
- # final_rinsing_volume = rinsing_volume if rinsing_volume > 0 else 5.0
- # final_rinsing_repeats = rinsing_repeats if rinsing_repeats > 0 else 2
-
- # if rinsing_volume <= 0:
- # logger.warning(f"⚠️ rinsing_volume <= 0,修正为: {final_rinsing_volume}mL")
- # if rinsing_repeats <= 0:
- # logger.warning(f"⚠️ rinsing_repeats <= 0,修正为: {final_rinsing_repeats}次")
-
- # # 根据物理属性调整冲洗参数
- # if viscous or solid:
- # final_rinsing_repeats = max(final_rinsing_repeats, 3)
- # final_rinsing_volume = max(final_rinsing_volume, 10.0)
- # debug_print(f"🧪 粘稠/固体物质,调整冲洗参数:{final_rinsing_repeats}次,{final_rinsing_volume}mL")
-
- try:
- pump_action_sequence = generate_pump_protocol(
- G, from_vessel, to_vessel, final_volume,
- flowrate, transfer_flowrate
- )
-
- # 为每个动作添加唯一标识
- # for i, action in enumerate(pump_action_sequence):
- # if isinstance(action, dict):
- # action['_protocol_id'] = protocol_id
- # action['_action_sequence'] = i
- # elif isinstance(action, list):
- # for j, sub_action in enumerate(action):
- # if isinstance(sub_action, dict):
- # sub_action['_protocol_id'] = protocol_id
- # sub_action['_action_sequence'] = f"{i}_{j}"
+ # 为每个动作添加唯一标识
+ # for i, action in enumerate(pump_action_sequence):
+ # if isinstance(action, dict):
+ # action['_protocol_id'] = protocol_id
+ # action['_action_sequence'] = i
+ # elif isinstance(action, list):
+ # for j, sub_action in enumerate(action):
+ # if isinstance(sub_action, dict):
+ # sub_action['_protocol_id'] = protocol_id
+ # sub_action['_action_sequence'] = f"{i}_{j}"
#
# debug_print(f"📊 协议 {protocol_id} 生成完成,共 {len(pump_action_sequence)} 个动作")
- debug_print(f"🔓 释放执行锁")
- return pump_action_sequence
+ debug_print(f"🔓 释放执行锁")
+ return pump_action_sequence
- except Exception as e:
- logger.error(f"❌ 协议生成失败: {str(e)}")
- return [
- {
- "device_id": "system",
- "action_name": "log_message",
- "action_kwargs": {
- "message": f"❌ 协议生成失败: {str(e)}"
- },
- '_protocol_id': protocol_id,
- '_action_sequence': 0
+ except Exception as e:
+ logger.error(f"❌ 协议生成失败: {str(e)}")
+ return [
+ {
+ "device_id": "system",
+ "action_name": "log_message",
+ "action_kwargs": {
+ "message": f"❌ 协议生成失败: {str(e)}"
}
- ]
+ }
+ ]
def _parse_amount_to_volume(amount: str) -> float:
"""解析 amount 字符串为体积"""
diff --git a/unilabos/compile/separate_protocol.py b/unilabos/compile/separate_protocol.py
index 07d9ab4e8..258c37d28 100644
--- a/unilabos/compile/separate_protocol.py
+++ b/unilabos/compile/separate_protocol.py
@@ -1,15 +1,67 @@
import networkx as nx
import re
import logging
+import sys
from typing import List, Dict, Any, Union
from .pump_protocol import generate_pump_protocol_with_rinsing
logger = logging.getLogger(__name__)
+# 确保输出编码为UTF-8
+if hasattr(sys.stdout, 'reconfigure'):
+ try:
+ sys.stdout.reconfigure(encoding='utf-8')
+ sys.stderr.reconfigure(encoding='utf-8')
+ except:
+ pass
+
def debug_print(message):
- """调试输出"""
- print(f"[SEPARATE] {message}", flush=True)
- logger.info(f"[SEPARATE] {message}")
+ """调试输出函数 - 支持中文"""
+ try:
+ # 确保消息是字符串格式
+ safe_message = str(message)
+ print(f"[分离协议] {safe_message}", flush=True)
+ logger.info(f"[分离协议] {safe_message}")
+ except UnicodeEncodeError:
+ # 如果编码失败,尝试替换不支持的字符
+ safe_message = str(message).encode('utf-8', errors='replace').decode('utf-8')
+ print(f"[分离协议] {safe_message}", flush=True)
+ logger.info(f"[分离协议] {safe_message}")
+ except Exception as e:
+ # 最后的安全措施
+ fallback_message = f"日志输出错误: {repr(message)}"
+ print(f"[分离协议] {fallback_message}", flush=True)
+ logger.info(f"[分离协议] {fallback_message}")
+
+def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]:
+ """创建一个动作日志 - 支持中文和emoji"""
+ try:
+ full_message = f"{emoji} {message}"
+ debug_print(full_message)
+ logger.info(full_message)
+
+ return {
+ "action_name": "wait",
+ "action_kwargs": {
+ "time": 0.1,
+ "log_message": full_message,
+ "progress_message": full_message
+ }
+ }
+ except Exception as e:
+ # 如果emoji有问题,使用纯文本
+ safe_message = f"[日志] {message}"
+ debug_print(safe_message)
+ logger.info(safe_message)
+
+ return {
+ "action_name": "wait",
+ "action_kwargs": {
+ "time": 0.1,
+ "log_message": safe_message,
+ "progress_message": safe_message
+ }
+ }
def parse_volume_input(volume_input: Union[str, float]) -> float:
"""
@@ -22,52 +74,58 @@ def parse_volume_input(volume_input: Union[str, float]) -> float:
float: 体积(毫升)
"""
if isinstance(volume_input, (int, float)):
+ debug_print(f"📏 体积输入为数值: {volume_input}")
return float(volume_input)
if not volume_input or not str(volume_input).strip():
+ debug_print(f"⚠️ 体积输入为空,返回 0.0mL")
return 0.0
volume_str = str(volume_input).lower().strip()
- debug_print(f"解析体积输入: '{volume_str}'")
+ debug_print(f"🔍 解析体积输入: '{volume_str}'")
# 处理未知体积
- if volume_str in ['?', 'unknown', 'tbd', 'to be determined']:
+ if volume_str in ['?', 'unknown', 'tbd', 'to be determined', '未知', '待定']:
default_volume = 100.0 # 默认100mL
- debug_print(f"检测到未知体积,使用默认值: {default_volume}mL")
+ debug_print(f"❓ 检测到未知体积,使用默认值: {default_volume}mL")
return default_volume
# 移除空格并提取数字和单位
volume_clean = re.sub(r'\s+', '', volume_str)
# 匹配数字和单位的正则表达式
- match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
+ match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter|毫升|升|微升)?', volume_clean)
if not match:
- debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值100mL")
+ debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值 100mL")
return 100.0
value = float(match.group(1))
unit = match.group(2) or 'ml' # 默认单位为毫升
# 转换为毫升
- if unit in ['l', 'liter']:
+ if unit in ['l', 'liter', '升']:
volume = value * 1000.0 # L -> mL
- elif unit in ['μl', 'ul', 'microliter']:
+ debug_print(f"🔄 体积转换: {value}L -> {volume}mL")
+ elif unit in ['μl', 'ul', 'microliter', '微升']:
volume = value / 1000.0 # μL -> mL
- else: # ml, milliliter 或默认
+ debug_print(f"🔄 体积转换: {value}μL -> {volume}mL")
+ else: # ml, milliliter, 毫升 或默认
volume = value # 已经是mL
+ debug_print(f"✅ 体积已为毫升单位: {volume}mL")
- debug_print(f"体积转换: {value}{unit} → {volume}mL")
return volume
def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
- """查找溶剂容器"""
+ """查找溶剂容器,支持多种匹配模式"""
if not solvent or not solvent.strip():
+ debug_print("⏭️ 未指定溶剂,跳过溶剂容器查找")
return ""
- debug_print(f"查找溶剂 '{solvent}' 的容器...")
+ debug_print(f"🔍 正在查找溶剂 '{solvent}' 的容器...")
# 🔧 方法1:直接搜索 data.reagent_name 和 config.reagent
+ debug_print(f"📋 方法1: 搜索试剂字段...")
for node in G.nodes():
node_data = G.nodes[node].get('data', {})
node_type = G.nodes[node].get('type', '')
@@ -80,16 +138,17 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
# 精确匹配
if reagent_name == solvent.lower() or config_reagent == solvent.lower():
- debug_print(f"✅ 通过reagent字段找到容器: {node}")
+ debug_print(f"✅ 通过试剂字段精确匹配找到容器: {node}")
return node
# 模糊匹配
if (solvent.lower() in reagent_name and reagent_name) or \
(solvent.lower() in config_reagent and config_reagent):
- debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node}")
+ debug_print(f"✅ 通过试剂字段模糊匹配找到容器: {node}")
return node
# 🔧 方法2:常见的容器命名规则
+ debug_print(f"📋 方法2: 使用命名规则...")
solvent_clean = solvent.lower().replace(' ', '_').replace('-', '_')
possible_names = [
f"flask_{solvent_clean}",
@@ -99,9 +158,14 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
f"{solvent_clean}_bottle",
f"solvent_{solvent_clean}",
f"reagent_{solvent_clean}",
- f"reagent_bottle_{solvent_clean}"
+ f"reagent_bottle_{solvent_clean}",
+ f"reagent_bottle_1", # 通用试剂瓶
+ f"reagent_bottle_2",
+ f"reagent_bottle_3"
]
+ debug_print(f"🎯 尝试的容器名称: {possible_names[:5]}... (共 {len(possible_names)} 个)")
+
for name in possible_names:
if name in G.nodes():
node_type = G.nodes[name].get('type', '')
@@ -110,53 +174,94 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
return name
# 🔧 方法3:使用第一个试剂瓶作为备选
+ debug_print(f"📋 方法3: 查找备用试剂瓶...")
for node_id in G.nodes():
node_data = G.nodes[node_id]
if (node_data.get('type') == 'container' and
('reagent' in node_id.lower() or 'bottle' in node_id.lower())):
- debug_print(f"⚠️ 未找到专用容器,使用备选容器: {node_id}")
+ debug_print(f"⚠️ 未找到专用容器,使用备用容器: {node_id}")
return node_id
- debug_print(f"⚠️ 未找到溶剂 '{solvent}' 的容器")
+ debug_print(f"❌ 无法找到溶剂 '{solvent}' 的容器")
return ""
def find_separator_device(G: nx.DiGraph, vessel: str) -> str:
- """查找分离器设备"""
- debug_print(f"查找容器 '{vessel}' 对应的分离器设备...")
+ """查找分离器设备,支持多种查找方式"""
+ debug_print(f"🔍 正在查找容器 '{vessel}' 的分离器设备...")
# 方法1:查找连接到容器的分离器设备
+ debug_print(f"📋 方法1: 检查连接的分离器...")
+ separator_nodes = []
for node in G.nodes():
node_class = G.nodes[node].get('class', '').lower()
if 'separator' in node_class:
+ separator_nodes.append(node)
+ debug_print(f"📋 发现分离器设备: {node}")
+
# 检查是否连接到目标容器
if G.has_edge(node, vessel) or G.has_edge(vessel, node):
debug_print(f"✅ 找到连接的分离器: {node}")
return node
+ debug_print(f"📊 找到的分离器总数: {len(separator_nodes)}")
+
# 方法2:根据命名规则查找
+ debug_print(f"📋 方法2: 使用命名规则...")
possible_names = [
f"{vessel}_controller",
f"{vessel}_separator",
vessel, # 容器本身可能就是分离器
"separator_1",
- "virtual_separator"
+ "virtual_separator",
+ "liquid_handler_1", # 液体处理器也可能用于分离
+ "controller_1"
]
+ debug_print(f"🎯 尝试的分离器名称: {possible_names}")
+
for name in possible_names:
if name in G.nodes():
node_class = G.nodes[name].get('class', '').lower()
- if 'separator' in node_class:
+ if 'separator' in node_class or 'controller' in node_class:
debug_print(f"✅ 通过命名规则找到分离器: {name}")
return name
# 方法3:查找第一个分离器设备
+ debug_print(f"📋 方法3: 使用第一个可用分离器...")
+ if separator_nodes:
+ debug_print(f"⚠️ 使用第一个分离器设备: {separator_nodes[0]}")
+ return separator_nodes[0]
+
+ debug_print(f"❌ 未找到分离器设备")
+ return ""
+
+def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
+ """查找连接到指定容器的搅拌器"""
+ debug_print(f"🔍 正在查找与容器 {vessel} 连接的搅拌器...")
+
+ stirrer_nodes = []
for node in G.nodes():
- node_class = G.nodes[node].get('class', '').lower()
- if 'separator' in node_class:
- debug_print(f"⚠️ 使用第一个分离器设备: {node}")
- return node
+ node_data = G.nodes[node]
+ node_class = node_data.get('class', '') or ''
+
+ if 'stirrer' in node_class.lower():
+ stirrer_nodes.append(node)
+ debug_print(f"📋 发现搅拌器: {node}")
+
+ debug_print(f"📊 找到的搅拌器总数: {len(stirrer_nodes)}")
- debug_print(f"⚠️ 未找到分离器设备")
+ # 检查哪个搅拌器与目标容器相连
+ for stirrer in stirrer_nodes:
+ if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
+ debug_print(f"✅ 找到连接的搅拌器: {stirrer}")
+ return stirrer
+
+ # 如果没有连接的搅拌器,返回第一个可用的
+ if stirrer_nodes:
+ debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个可用的: {stirrer_nodes[0]}")
+ return stirrer_nodes[0]
+
+ debug_print("❌ 未找到搅拌器")
return ""
def generate_separate_protocol(
@@ -185,7 +290,7 @@ def generate_separate_protocol(
**kwargs
) -> List[Dict[str, Any]]:
"""
- 生成分离操作的协议序列 - 修复版
+ 生成分离操作的协议序列 - 增强中文版
支持XDL参数格式:
- vessel: 分离容器(必需)
@@ -206,26 +311,31 @@ def generate_separate_protocol(
"""
debug_print("=" * 60)
- debug_print("开始生成分离协议 - 修复版")
- debug_print(f"原始参数:")
- debug_print(f" - vessel: '{vessel}'")
- debug_print(f" - purpose: '{purpose}'")
- debug_print(f" - product_phase: '{product_phase}'")
- debug_print(f" - solvent: '{solvent}'")
- debug_print(f" - volume: {volume} (类型: {type(volume)})")
- debug_print(f" - repeats: {repeats}")
- debug_print(f" - product_vessel: '{product_vessel}'")
- debug_print(f" - waste_vessel: '{waste_vessel}'")
+ debug_print("🧪 开始生成分离协议 - 增强中文版")
+ debug_print(f"📋 原始参数:")
+ debug_print(f" 🥼 容器: '{vessel}'")
+ debug_print(f" 🎯 分离目的: '{purpose}'")
+ debug_print(f" 📊 产物相: '{product_phase}'")
+ debug_print(f" 💧 溶剂: '{solvent}'")
+ debug_print(f" 📏 体积: {volume} (类型: {type(volume)})")
+ debug_print(f" 🔄 重复次数: {repeats}")
+ debug_print(f" 🎯 产物容器: '{product_vessel}'")
+ debug_print(f" 🗑️ 废液容器: '{waste_vessel}'")
+ debug_print(f" 📦 其他参数: {kwargs}")
debug_print("=" * 60)
action_sequence = []
# === 参数验证和标准化 ===
- debug_print("步骤1: 参数验证和标准化...")
+ debug_print("🔍 步骤1: 参数验证和标准化...")
+ action_sequence.append(create_action_log(f"开始分离操作 - 容器: {vessel}", "🎬"))
+ action_sequence.append(create_action_log(f"分离目的: {purpose}", "🧪"))
+ action_sequence.append(create_action_log(f"产物相: {product_phase}", "📊"))
# 统一容器参数
final_vessel = vessel or separation_vessel
if not final_vessel:
+ debug_print("❌ 必须指定分离容器")
raise ValueError("必须指定分离容器 (vessel 或 separation_vessel)")
final_to_vessel = to_vessel or product_vessel
@@ -237,14 +347,18 @@ def generate_separate_protocol(
# 🔧 修复:确保repeats至少为1
if repeats <= 0:
repeats = 1
- debug_print(f"⚠️ repeats参数 <= 0,自动设置为1")
+ debug_print(f"⚠️ 重复次数参数 <= 0,自动设置为 1")
+
+ debug_print(f"🔧 标准化后的参数:")
+ debug_print(f" 🥼 分离容器: '{final_vessel}'")
+ debug_print(f" 🎯 产物容器: '{final_to_vessel}'")
+ debug_print(f" 🗑️ 废液容器: '{final_waste_vessel}'")
+ debug_print(f" 📏 溶剂体积: {final_volume}mL")
+ debug_print(f" 🔄 重复次数: {repeats}")
- debug_print(f"标准化参数:")
- debug_print(f" - 分离容器: '{final_vessel}'")
- debug_print(f" - 产物容器: '{final_to_vessel}'")
- debug_print(f" - 废液容器: '{final_waste_vessel}'")
- debug_print(f" - 溶剂体积: {final_volume}mL")
- debug_print(f" - 重复次数: {repeats}")
+ action_sequence.append(create_action_log(f"分离容器: {final_vessel}", "🧪"))
+ action_sequence.append(create_action_log(f"溶剂体积: {final_volume}mL", "📏"))
+ action_sequence.append(create_action_log(f"重复次数: {repeats}", "🔄"))
# 验证必需参数
if not purpose:
@@ -254,66 +368,147 @@ def generate_separate_protocol(
if purpose not in ["wash", "extract", "separate"]:
debug_print(f"⚠️ 未知的分离目的 '{purpose}',使用默认值 'separate'")
purpose = "separate"
+ action_sequence.append(create_action_log(f"未知目的,使用: {purpose}", "⚠️"))
if product_phase not in ["top", "bottom"]:
debug_print(f"⚠️ 未知的产物相 '{product_phase}',使用默认值 'top'")
product_phase = "top"
+ action_sequence.append(create_action_log(f"未知相别,使用: {product_phase}", "⚠️"))
debug_print("✅ 参数验证通过")
+ action_sequence.append(create_action_log("参数验证通过", "✅"))
# === 查找设备 ===
- debug_print("步骤2: 查找设备...")
+ debug_print("🔍 步骤2: 查找设备...")
+ action_sequence.append(create_action_log("正在查找相关设备...", "🔍"))
# 查找分离器设备
separator_device = find_separator_device(G, final_vessel)
- if not separator_device:
- debug_print("⚠️ 未找到分离器设备,可能无法执行分离操作")
+ if separator_device:
+ action_sequence.append(create_action_log(f"找到分离器设备: {separator_device}", "🧪"))
+ else:
+ debug_print("⚠️ 未找到分离器设备,可能无法执行分离")
+ action_sequence.append(create_action_log("未找到分离器设备", "⚠️"))
+
+ # 查找搅拌器
+ stirrer_device = find_connected_stirrer(G, final_vessel)
+ if stirrer_device:
+ action_sequence.append(create_action_log(f"找到搅拌器: {stirrer_device}", "🌪️"))
+ else:
+ action_sequence.append(create_action_log("未找到搅拌器", "⚠️"))
# 查找溶剂容器(如果需要)
solvent_vessel = ""
if solvent and solvent.strip():
solvent_vessel = find_solvent_vessel(G, solvent)
+ if solvent_vessel:
+ action_sequence.append(create_action_log(f"找到溶剂容器: {solvent_vessel}", "💧"))
+ else:
+ action_sequence.append(create_action_log(f"未找到溶剂容器: {solvent}", "⚠️"))
- debug_print(f"设备映射:")
- debug_print(f" - 分离器设备: '{separator_device}'")
- debug_print(f" - 溶剂容器: '{solvent_vessel}'")
+ debug_print(f"📊 设备配置:")
+ debug_print(f" 🧪 分离器设备: '{separator_device}'")
+ debug_print(f" 🌪️ 搅拌器设备: '{stirrer_device}'")
+ debug_print(f" 💧 溶剂容器: '{solvent_vessel}'")
# === 执行分离流程 ===
- debug_print("步骤3: 执行分离流程...")
+ debug_print("🔍 步骤3: 执行分离流程...")
+ action_sequence.append(create_action_log("开始分离工作流程", "🎯"))
try:
for repeat_idx in range(repeats):
- debug_print(f"3.{repeat_idx+1}: 第 {repeat_idx+1}/{repeats} 次分离")
+ cycle_num = repeat_idx + 1
+ debug_print(f"🔄 第{cycle_num}轮: 开始分离循环 {cycle_num}/{repeats}")
+ action_sequence.append(create_action_log(f"分离循环 {cycle_num}/{repeats} 开始", "🔄"))
# 步骤3.1: 添加溶剂(如果需要)
if solvent_vessel and final_volume > 0:
- debug_print(f"3.{repeat_idx+1}.1: 添加溶剂 {solvent} ({final_volume}mL)")
+ debug_print(f"🔄 第{cycle_num}轮 步骤1: 添加溶剂 {solvent} ({final_volume}mL)")
+ action_sequence.append(create_action_log(f"向分离容器添加 {final_volume}mL {solvent}", "💧"))
- # 使用pump protocol添加溶剂
- pump_actions = generate_pump_protocol_with_rinsing(
- G=G,
- from_vessel=solvent_vessel,
- to_vessel=final_vessel,
- volume=final_volume,
- amount="",
- time=0.0,
- viscous=False,
- rinsing_solvent="",
- rinsing_volume=0.0,
- rinsing_repeats=0,
- solid=False,
- flowrate=2.5,
- transfer_flowrate=0.5,
- rate_spec="",
- event="",
- through="",
- **kwargs
- )
- action_sequence.extend(pump_actions)
- debug_print(f"✅ 溶剂添加完成,添加了 {len(pump_actions)} 个动作")
+ try:
+ # 使用pump protocol添加溶剂
+ pump_actions = generate_pump_protocol_with_rinsing(
+ G=G,
+ from_vessel=solvent_vessel,
+ to_vessel=final_vessel,
+ volume=final_volume,
+ amount="",
+ time=0.0,
+ viscous=False,
+ rinsing_solvent="",
+ rinsing_volume=0.0,
+ rinsing_repeats=0,
+ solid=False,
+ flowrate=2.5,
+ transfer_flowrate=0.5,
+ rate_spec="",
+ event="",
+ through="",
+ **kwargs
+ )
+ action_sequence.extend(pump_actions)
+ debug_print(f"✅ 溶剂添加完成,添加了 {len(pump_actions)} 个动作")
+ action_sequence.append(create_action_log(f"溶剂转移完成 ({len(pump_actions)} 个操作)", "✅"))
+
+ except Exception as e:
+ debug_print(f"❌ 溶剂添加失败: {str(e)}")
+ action_sequence.append(create_action_log(f"溶剂添加失败: {str(e)}", "❌"))
+ else:
+ debug_print(f"🔄 第{cycle_num}轮 步骤1: 无需添加溶剂")
+ action_sequence.append(create_action_log("无需添加溶剂", "⏭️"))
- # 步骤3.2: 执行分离操作
+ # 步骤3.2: 启动搅拌(如果有搅拌器)
+ if stirrer_device and stir_time > 0:
+ debug_print(f"🔄 第{cycle_num}轮 步骤2: 开始搅拌 ({stir_speed}rpm,持续 {stir_time}s)")
+ action_sequence.append(create_action_log(f"开始搅拌: {stir_speed}rpm,持续 {stir_time}s", "🌪️"))
+
+ action_sequence.append({
+ "device_id": stirrer_device,
+ "action_name": "start_stir",
+ "action_kwargs": {
+ "vessel": final_vessel,
+ "stir_speed": stir_speed,
+ "purpose": f"分离混合 - {purpose}"
+ }
+ })
+
+ # 搅拌等待
+ stir_minutes = stir_time / 60
+ action_sequence.append(create_action_log(f"搅拌中,持续 {stir_minutes:.1f} 分钟", "⏱️"))
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": stir_time}
+ })
+
+ # 停止搅拌
+ action_sequence.append(create_action_log("停止搅拌器", "🛑"))
+ action_sequence.append({
+ "device_id": stirrer_device,
+ "action_name": "stop_stir",
+ "action_kwargs": {"vessel": final_vessel}
+ })
+
+ else:
+ debug_print(f"🔄 第{cycle_num}轮 步骤2: 无需搅拌")
+ action_sequence.append(create_action_log("无需搅拌", "⏭️"))
+
+ # 步骤3.3: 静置分层
+ if settling_time > 0:
+ debug_print(f"🔄 第{cycle_num}轮 步骤3: 静置分层 ({settling_time}s)")
+ settling_minutes = settling_time / 60
+ action_sequence.append(create_action_log(f"静置分层 ({settling_minutes:.1f} 分钟)", "⚖️"))
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": settling_time}
+ })
+ else:
+ debug_print(f"🔄 第{cycle_num}轮 步骤3: 未指定静置时间")
+ action_sequence.append(create_action_log("未指定静置时间", "⏭️"))
+
+ # 步骤3.4: 执行分离操作
if separator_device:
- debug_print(f"3.{repeat_idx+1}.2: 执行分离操作")
+ debug_print(f"🔄 第{cycle_num}轮 步骤4: 执行分离操作")
+ action_sequence.append(create_action_log(f"执行分离: 收集{product_phase}相", "🧪"))
# 调用分离器设备的separate方法
separate_action = {
@@ -330,31 +525,44 @@ def generate_separate_protocol(
"solvent_volume": final_volume,
"through": through,
"repeats": 1, # 每次调用只做一次分离
- "stir_time": stir_time,
+ "stir_time": 0, # 已经在上面完成
"stir_speed": stir_speed,
- "settling_time": settling_time
+ "settling_time": 0 # 已经在上面完成
}
}
action_sequence.append(separate_action)
- debug_print(f"✅ 分离操作添加完成")
+ debug_print(f"✅ 分离操作已添加")
+ action_sequence.append(create_action_log("分离操作完成", "✅"))
+
+ # 收集结果
+ if final_to_vessel:
+ action_sequence.append(create_action_log(f"产物 ({product_phase}相) 收集到: {final_to_vessel}", "📦"))
+ if final_waste_vessel:
+ action_sequence.append(create_action_log(f"废相收集到: {final_waste_vessel}", "🗑️"))
else:
- debug_print(f"3.{repeat_idx+1}.2: 无分离器设备,跳过分离操作")
+ debug_print(f"🔄 第{cycle_num}轮 步骤4: 无分离器设备,跳过分离")
+ action_sequence.append(create_action_log("无分离器设备可用", "❌"))
# 添加等待时间模拟分离
action_sequence.append({
"action_name": "wait",
- "action_kwargs": {"time": stir_time + settling_time}
+ "action_kwargs": {"time": 10.0}
})
- # 等待间隔(除了最后一次)
+ # 循环间等待(除了最后一次)
if repeat_idx < repeats - 1:
+ debug_print(f"🔄 第{cycle_num}轮: 等待下一次循环...")
+ action_sequence.append(create_action_log("等待下一次循环...", "⏳"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5}
})
+ else:
+ action_sequence.append(create_action_log(f"分离循环 {cycle_num}/{repeats} 完成", "🌟"))
except Exception as e:
- debug_print(f"⚠️ 分离流程执行失败: {str(e)}")
+ debug_print(f"❌ 分离工作流程执行失败: {str(e)}")
+ action_sequence.append(create_action_log(f"分离工作流程失败: {str(e)}", "❌"))
# 添加错误日志
action_sequence.append({
"device_id": "system",
@@ -365,22 +573,31 @@ def generate_separate_protocol(
})
# === 最终结果 ===
+ total_time = (stir_time + settling_time + 15) * repeats # 估算总时间
+
debug_print("=" * 60)
- debug_print(f"✅ 分离协议生成完成")
- debug_print(f"📊 总动作数: {len(action_sequence)}")
- debug_print(f"📋 处理总结:")
- debug_print(f" - 分离容器: {final_vessel}")
- debug_print(f" - 分离目的: {purpose}")
- debug_print(f" - 产物相: {product_phase}")
- debug_print(f" - 重复次数: {repeats}")
+ debug_print(f"🎉 分离协议生成完成")
+ debug_print(f"📊 协议统计:")
+ debug_print(f" 📋 总动作数: {len(action_sequence)}")
+ debug_print(f" ⏱️ 预计总时间: {total_time:.0f}s ({total_time/60:.1f} 分钟)")
+ debug_print(f" 🥼 分离容器: {final_vessel}")
+ debug_print(f" 🎯 分离目的: {purpose}")
+ debug_print(f" 📊 产物相: {product_phase}")
+ debug_print(f" 🔄 重复次数: {repeats}")
if solvent:
- debug_print(f" - 溶剂: {solvent} ({final_volume}mL)")
+ debug_print(f" 💧 溶剂: {solvent} ({final_volume}mL)")
if final_to_vessel:
- debug_print(f" - 产物容器: {final_to_vessel}")
+ debug_print(f" 🎯 产物容器: {final_to_vessel}")
if final_waste_vessel:
- debug_print(f" - 废液容器: {final_waste_vessel}")
+ debug_print(f" 🗑️ 废液容器: {final_waste_vessel}")
debug_print("=" * 60)
+ # 添加完成日志
+ summary_msg = f"分离协议完成: {final_vessel} ({purpose},{repeats} 次循环)"
+ if solvent:
+ summary_msg += f",使用 {final_volume}mL {solvent}"
+ action_sequence.append(create_action_log(summary_msg, "🎉"))
+
return action_sequence
# === 便捷函数 ===
@@ -388,6 +605,7 @@ def generate_separate_protocol(
def separate_phases_only(G: nx.DiGraph, vessel: str, product_phase: str = "top",
product_vessel: str = "", waste_vessel: str = "") -> List[Dict[str, Any]]:
"""仅进行相分离(不添加溶剂)"""
+ debug_print(f"⚡ 快速相分离: {vessel} ({product_phase}相)")
return generate_separate_protocol(
G, vessel=vessel,
purpose="separate",
@@ -399,6 +617,7 @@ def separate_phases_only(G: nx.DiGraph, vessel: str, product_phase: str = "top",
def wash_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float],
product_phase: str = "top", repeats: int = 1) -> List[Dict[str, Any]]:
"""用溶剂洗涤"""
+ debug_print(f"🧽 用{solvent}洗涤: {vessel} ({repeats} 次)")
return generate_separate_protocol(
G, vessel=vessel,
purpose="wash",
@@ -411,6 +630,7 @@ def wash_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[st
def extract_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float],
product_phase: str = "bottom", repeats: int = 3) -> List[Dict[str, Any]]:
"""用溶剂萃取"""
+ debug_print(f"🧪 用{solvent}萃取: {vessel} ({repeats} 次)")
return generate_separate_protocol(
G, vessel=vessel,
purpose="extract",
@@ -423,6 +643,7 @@ def extract_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union
def separate_aqueous_organic(G: nx.DiGraph, vessel: str, organic_phase: str = "top",
product_vessel: str = "", waste_vessel: str = "") -> List[Dict[str, Any]]:
"""水-有机相分离"""
+ debug_print(f"💧 水-有机相分离: {vessel} (有机相: {organic_phase})")
return generate_separate_protocol(
G, vessel=vessel,
purpose="separate",
@@ -434,15 +655,16 @@ def separate_aqueous_organic(G: nx.DiGraph, vessel: str, organic_phase: str = "t
# 测试函数
def test_separate_protocol():
"""测试分离协议的各种参数解析"""
- print("=== SEPARATE PROTOCOL 增强版测试 ===")
+ debug_print("=== 分离协议增强中文版测试 ===")
# 测试体积解析
- volumes = ["200 mL", "?", 100.0, "1 L", "500 μL"]
+ debug_print("🧪 测试体积解析...")
+ volumes = ["200 mL", "?", 100.0, "1 L", "500 μL", "未知", "50毫升"]
for vol in volumes:
result = parse_volume_input(vol)
- print(f"体积解析: {vol} → {result}mL")
+ debug_print(f"📊 体积解析结果: {vol} -> {result}mL")
- print("✅ 测试完成")
+ debug_print("✅ 测试完成")
if __name__ == "__main__":
test_separate_protocol()
diff --git a/unilabos/devices/virtual/virtual_multiway_valve.py b/unilabos/devices/virtual/virtual_multiway_valve.py
index c12aa7fe6..468175cee 100644
--- a/unilabos/devices/virtual/virtual_multiway_valve.py
+++ b/unilabos/devices/virtual/virtual_multiway_valve.py
@@ -101,7 +101,7 @@ def set_position(self, command: Union[int, str]):
self._target_position = pos
# 模拟阀门切换时间
- switch_time = abs(self._current_position - pos) * 0.5 # 每个位置0.1秒
+ switch_time = abs(self._current_position - pos) * 0.5 # 每个位置0.5秒
if switch_time > 0:
self.logger.info(f"⏱️ 阀门移动中... 预计用时: {switch_time:.1f}秒 🔄")
@@ -172,32 +172,32 @@ def get_valve_position(self) -> int:
def is_at_position(self, position: int) -> bool:
"""检查是否在指定位置 🎯"""
result = self._current_position == position
- self.logger.debug(f"🎯 位置检查: 当前={self._current_position}, 目标={position}, 匹配={result}")
+ # 删除debug日志:self.logger.debug(f"🎯 位置检查: 当前={self._current_position}, 目标={position}, 匹配={result}")
return result
def is_at_pump_position(self) -> bool:
"""检查是否在transfer pump位置 🚰"""
result = self._current_position == 0
- pump_status = "是" if result else "否"
- self.logger.debug(f"🚰 泵位置检查: {pump_status} (当前位置: {self._current_position})")
+ # 删除debug日志:pump_status = "是" if result else "否"
+ # 删除debug日志:self.logger.debug(f"🚰 泵位置检查: {pump_status} (当前位置: {self._current_position})")
return result
def is_at_port(self, port_number: int) -> bool:
"""检查是否在指定端口位置 🔌"""
result = self._current_position == port_number
- port_status = "是" if result else "否"
- self.logger.debug(f"🔌 端口{port_number}检查: {port_status} (当前位置: {self._current_position})")
+ # 删除debug日志:port_status = "是" if result else "否"
+ # 删除debug日志:self.logger.debug(f"🔌 端口{port_number}检查: {port_status} (当前位置: {self._current_position})")
return result
def get_available_positions(self) -> list:
"""获取可用位置列表 📋"""
positions = list(range(0, self.max_positions + 1))
- self.logger.debug(f"📋 可用位置: {positions}")
+ # 删除debug日志:self.logger.debug(f"📋 可用位置: {positions}")
return positions
def get_available_ports(self) -> Dict[int, str]:
"""获取可用端口映射 🗺️"""
- self.logger.debug(f"🗺️ 端口映射: {self.position_map}")
+ # 删除debug日志:self.logger.debug(f"🗺️ 端口映射: {self.position_map}")
return self.position_map.copy()
def reset(self):
@@ -229,7 +229,7 @@ def get_flow_path(self) -> str:
else:
flow_path = f"🔌 端口 {self._current_position} 已连接 ({current_port})"
- self.logger.debug(f"🌊 当前流路: {flow_path}")
+ # 删除debug日志:self.logger.debug(f"🌊 当前流路: {flow_path}")
return flow_path
def get_info(self) -> dict:
@@ -247,7 +247,7 @@ def get_info(self) -> dict:
"position_map": self.position_map
}
- self.logger.debug(f"📊 阀门信息: 位置={self._current_position}, 状态={self._status}, 端口={self.get_current_port()}")
+ # 删除debug日志:self.logger.debug(f"📊 阀门信息: 位置={self._current_position}, 状态={self._status}, 端口={self.get_current_port()}")
return info
def __str__(self):
@@ -264,7 +264,7 @@ def set_valve_position(self, command: Union[int, str]):
Args:
command: 目标位置 (0-8) 或位置字符串
"""
- self.logger.debug(f"🎯 兼容性调用: set_valve_position({command})")
+ # 删除debug日志:self.logger.debug(f"🎯 兼容性调用: set_valve_position({command})")
return self.set_position(command)
diff --git a/unilabos/devices/virtual/virtual_transferpump.py b/unilabos/devices/virtual/virtual_transferpump.py
index a2cba9c4b..7d80744c3 100644
--- a/unilabos/devices/virtual/virtual_transferpump.py
+++ b/unilabos/devices/virtual/virtual_transferpump.py
@@ -12,7 +12,7 @@ class VirtualPumpMode(Enum):
class VirtualTransferPump:
- """虚拟转移泵类 - 模拟泵的基本功能,无需实际硬件"""
+ """虚拟转移泵类 - 模拟泵的基本功能,无需实际硬件 🚰"""
def __init__(self, device_id: str = None, config: dict = None, **kwargs):
"""
@@ -42,20 +42,31 @@ def __init__(self, device_id: str = None, config: dict = None, **kwargs):
self._max_velocity = 5.0 # float
self._current_volume = 0.0 # float
+ # 🚀 新增:快速模式设置 - 大幅缩短执行时间
+ self._fast_mode = True # 是否启用快速模式
+ self._fast_move_time = 1.0 # 快速移动时间(秒)
+ self._fast_dispense_time = 1.0 # 快速喷射时间(秒)
+
self.logger = logging.getLogger(f"VirtualTransferPump.{self.device_id}")
+
+ print(f"🚰 === 虚拟转移泵 {self.device_id} 已创建 === ✨")
+ print(f"💨 快速模式: {'启用' if self._fast_mode else '禁用'} | 移动时间: {self._fast_move_time}s | 喷射时间: {self._fast_dispense_time}s")
+ print(f"📊 最大容量: {self.max_volume}mL | 端口: {self.port}")
async def initialize(self) -> bool:
- """初始化虚拟泵"""
- self.logger.info(f"Initializing virtual pump {self.device_id}")
+ """初始化虚拟泵 🚀"""
+ self.logger.info(f"🔧 初始化虚拟转移泵 {self.device_id} ✨")
self._status = "Idle"
self._position = 0.0
self._current_volume = 0.0
+ self.logger.info(f"✅ 转移泵 {self.device_id} 初始化完成 🚰")
return True
async def cleanup(self) -> bool:
- """清理虚拟泵"""
- self.logger.info(f"Cleaning up virtual pump {self.device_id}")
+ """清理虚拟泵 🧹"""
+ self.logger.info(f"🧹 清理虚拟转移泵 {self.device_id} 🔚")
self._status = "Idle"
+ self.logger.info(f"✅ 转移泵 {self.device_id} 清理完成 💤")
return True
# 基本属性
@@ -65,12 +76,12 @@ def status(self) -> str:
@property
def position(self) -> float:
- """当前柱塞位置 (ml)"""
+ """当前柱塞位置 (ml) 📍"""
return self._position
@property
def current_volume(self) -> float:
- """当前注射器中的体积 (ml)"""
+ """当前注射器中的体积 (ml) 💧"""
return self._current_volume
@property
@@ -82,22 +93,50 @@ def transfer_rate(self) -> float:
return self._transfer_rate
def set_max_velocity(self, velocity: float):
- """设置最大速度 (ml/s)"""
+ """设置最大速度 (ml/s) 🌊"""
self._max_velocity = max(0.1, min(50.0, velocity)) # 限制在合理范围内
- self.logger.info(f"Set max velocity to {self._max_velocity} ml/s")
+ self.logger.info(f"🌊 设置最大速度为 {self._max_velocity} mL/s")
def get_status(self) -> str:
- """获取泵状态"""
+ """获取泵状态 📋"""
return self._status
async def _simulate_operation(self, duration: float):
- """模拟操作延时"""
+ """模拟操作延时 ⏱️"""
self._status = "Busy"
await asyncio.sleep(duration)
self._status = "Idle"
def _calculate_duration(self, volume: float, velocity: float = None) -> float:
- """计算操作持续时间"""
+ """
+ 计算操作持续时间 ⏰
+ 🚀 快速模式:保留计算逻辑用于日志显示,但实际使用固定的快速时间
+ """
+ if velocity is None:
+ velocity = self._max_velocity
+
+ # 📊 计算理论时间(用于日志显示)
+ theoretical_duration = abs(volume) / velocity
+
+ # 🚀 如果启用快速模式,使用固定的快速时间
+ if self._fast_mode:
+ # 根据操作类型选择快速时间
+ if abs(volume) > 0.1: # 大于0.1mL的操作
+ actual_duration = self._fast_move_time
+ else: # 很小的操作
+ actual_duration = 0.5
+
+ self.logger.debug(f"⚡ 快速模式: 理论时间 {theoretical_duration:.2f}s → 实际时间 {actual_duration:.2f}s")
+ return actual_duration
+ else:
+ # 正常模式使用理论时间
+ return theoretical_duration
+
+ def _calculate_display_duration(self, volume: float, velocity: float = None) -> float:
+ """
+ 计算显示用的持续时间(用于日志) 📊
+ 这个函数返回理论计算时间,用于日志显示
+ """
if velocity is None:
velocity = self._max_velocity
return abs(volume) / velocity
@@ -105,7 +144,7 @@ def _calculate_duration(self, volume: float, velocity: float = None) -> float:
# 新的set_position方法 - 专门用于SetPumpPosition动作
async def set_position(self, position: float, max_velocity: float = None):
"""
- 移动到绝对位置 - 专门用于SetPumpPosition动作
+ 移动到绝对位置 - 专门用于SetPumpPosition动作 🎯
Args:
position (float): 目标位置 (ml)
@@ -122,56 +161,107 @@ async def set_position(self, position: float, max_velocity: float = None):
# 限制位置在有效范围内
target_position = max(0.0, min(float(self.max_volume), target_position))
- # 计算移动距离和时间
+ # 计算移动距离
volume_to_move = abs(target_position - self._position)
- duration = self._calculate_duration(volume_to_move, velocity)
- self.logger.info(f"SET_POSITION: Moving to {target_position} ml (current: {self._position} ml), velocity: {velocity} ml/s")
+ # 📊 计算显示用的时间(用于日志)
+ display_duration = self._calculate_display_duration(volume_to_move, velocity)
+
+ # ⚡ 计算实际执行时间(快速模式)
+ actual_duration = self._calculate_duration(volume_to_move, velocity)
+
+ # 🎯 确定操作类型和emoji
+ if target_position > self._position:
+ operation_type = "吸液"
+ operation_emoji = "📥"
+ elif target_position < self._position:
+ operation_type = "排液"
+ operation_emoji = "📤"
+ else:
+ operation_type = "保持"
+ operation_emoji = "📍"
- # 模拟移动过程
- start_position = self._position
- steps = 10 if duration > 0.1 else 1 # 如果移动距离很小,只用1步
- step_duration = duration / steps if steps > 1 else duration
+ self.logger.info(f"🎯 SET_POSITION: {operation_type} {operation_emoji}")
+ self.logger.info(f" 📍 位置: {self._position:.2f}mL → {target_position:.2f}mL (移动 {volume_to_move:.2f}mL)")
+ self.logger.info(f" 🌊 速度: {velocity:.2f} mL/s")
+ self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s")
- for i in range(steps + 1):
- # 计算当前位置和进度
- progress = (i / steps) * 100 if steps > 0 else 100
- current_pos = start_position + (target_position - start_position) * (i / steps) if steps > 0 else target_position
+ if self._fast_mode:
+ self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s")
+
+ # 🚀 模拟移动过程
+ if volume_to_move > 0.01: # 只有当移动距离足够大时才显示进度
+ start_position = self._position
+ steps = 5 if actual_duration > 0.5 else 2 # 根据实际时间调整步数
+ step_duration = actual_duration / steps
- # 更新状态
- self._status = "Moving" if i < steps else "Idle"
- self._position = current_pos
- self._current_volume = current_pos
+ self.logger.info(f"🚀 开始{operation_type}... {operation_emoji}")
- # 等待一小步时间
- if i < steps and step_duration > 0:
- await asyncio.sleep(step_duration)
+ for i in range(steps + 1):
+ # 计算当前位置和进度
+ progress = (i / steps) * 100 if steps > 0 else 100
+ current_pos = start_position + (target_position - start_position) * (i / steps) if steps > 0 else target_position
+
+ # 更新状态
+ if i < steps:
+ self._status = f"{operation_type}中"
+ status_emoji = "🔄"
+ else:
+ self._status = "Idle"
+ status_emoji = "✅"
+
+ self._position = current_pos
+ self._current_volume = current_pos
+
+ # 显示进度(每25%或最后一步)
+ if i == 0:
+ self.logger.debug(f" 🔄 {operation_type}开始: {progress:.0f}%")
+ elif progress >= 50 and i == steps // 2:
+ self.logger.debug(f" 🔄 {operation_type}进度: {progress:.0f}%")
+ elif i == steps:
+ self.logger.info(f" ✅ {operation_type}完成: {progress:.0f}% | 当前位置: {current_pos:.2f}mL")
+
+ # 等待一小步时间
+ if i < steps and step_duration > 0:
+ await asyncio.sleep(step_duration)
+ else:
+ # 移动距离很小,直接完成
+ self._position = target_position
+ self._current_volume = target_position
+ self.logger.info(f" 📍 微调完成: {target_position:.2f}mL")
# 确保最终位置准确
self._position = target_position
self._current_volume = target_position
self._status = "Idle"
- self.logger.info(f"SET_POSITION: Reached position {self._position} ml, current volume: {self._current_volume} ml")
+ # 📊 最终状态日志
+ if volume_to_move > 0.01:
+ self.logger.info(f"🎉 SET_POSITION 完成! 📍 最终位置: {self._position:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL")
# 返回符合action定义的结果
return {
"success": True,
- "message": f"Successfully moved to position {self._position} ml"
+ "message": f"✅ 成功移动到位置 {self._position:.2f}mL ({operation_type})",
+ "final_position": self._position,
+ "final_volume": self._current_volume,
+ "operation_type": operation_type
}
except Exception as e:
- error_msg = f"Failed to set position: {str(e)}"
+ error_msg = f"❌ 设置位置失败: {str(e)}"
self.logger.error(error_msg)
return {
"success": False,
- "message": error_msg
+ "message": error_msg,
+ "final_position": self._position,
+ "final_volume": self._current_volume
}
# 其他泵操作方法
async def pull_plunger(self, volume: float, velocity: float = None):
"""
- 拉取柱塞(吸液)
+ 拉取柱塞(吸液) 📥
Args:
volume (float): 要拉取的体积 (ml)
@@ -181,23 +271,29 @@ async def pull_plunger(self, volume: float, velocity: float = None):
actual_volume = new_position - self._position
if actual_volume <= 0:
- self.logger.warning("Cannot pull - already at maximum volume")
+ self.logger.warning("⚠️ 无法吸液 - 已达到最大容量")
return
- duration = self._calculate_duration(actual_volume, velocity)
+ display_duration = self._calculate_display_duration(actual_volume, velocity)
+ actual_duration = self._calculate_duration(actual_volume, velocity)
+
+ self.logger.info(f"📥 开始吸液: {actual_volume:.2f}mL")
+ self.logger.info(f" 📍 位置: {self._position:.2f}mL → {new_position:.2f}mL")
+ self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s")
- self.logger.info(f"Pulling {actual_volume} ml (from {self._position} to {new_position})")
+ if self._fast_mode:
+ self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s")
- await self._simulate_operation(duration)
+ await self._simulate_operation(actual_duration)
self._position = new_position
self._current_volume = new_position
- self.logger.info(f"Pulled {actual_volume} ml, current volume: {self._current_volume} ml")
+ self.logger.info(f"✅ 吸液完成: {actual_volume:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL")
async def push_plunger(self, volume: float, velocity: float = None):
"""
- 推出柱塞(排液)
+ 推出柱塞(排液) 📤
Args:
volume (float): 要推出的体积 (ml)
@@ -207,35 +303,44 @@ async def push_plunger(self, volume: float, velocity: float = None):
actual_volume = self._position - new_position
if actual_volume <= 0:
- self.logger.warning("Cannot push - already at minimum volume")
+ self.logger.warning("⚠️ 无法排液 - 已达到最小容量")
return
- duration = self._calculate_duration(actual_volume, velocity)
+ display_duration = self._calculate_display_duration(actual_volume, velocity)
+ actual_duration = self._calculate_duration(actual_volume, velocity)
- self.logger.info(f"Pushing {actual_volume} ml (from {self._position} to {new_position})")
+ self.logger.info(f"📤 开始排液: {actual_volume:.2f}mL")
+ self.logger.info(f" 📍 位置: {self._position:.2f}mL → {new_position:.2f}mL")
+ self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s")
- await self._simulate_operation(duration)
+ if self._fast_mode:
+ self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s")
+
+ await self._simulate_operation(actual_duration)
self._position = new_position
self._current_volume = new_position
- self.logger.info(f"Pushed {actual_volume} ml, current volume: {self._current_volume} ml")
+ self.logger.info(f"✅ 排液完成: {actual_volume:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL")
# 便捷操作方法
async def aspirate(self, volume: float, velocity: float = None):
- """吸液操作"""
+ """吸液操作 📥"""
await self.pull_plunger(volume, velocity)
async def dispense(self, volume: float, velocity: float = None):
- """排液操作"""
+ """排液操作 📤"""
await self.push_plunger(volume, velocity)
async def transfer(self, volume: float, aspirate_velocity: float = None, dispense_velocity: float = None):
- """转移操作(先吸后排)"""
+ """转移操作(先吸后排) 🔄"""
+ self.logger.info(f"🔄 开始转移操作: {volume:.2f}mL")
+
# 吸液
await self.aspirate(volume, aspirate_velocity)
# 短暂停顿
+ self.logger.debug("⏸️ 短暂停顿...")
await asyncio.sleep(0.1)
# 排液
diff --git a/unilabos/ros/nodes/presets/protocol_node.py b/unilabos/ros/nodes/presets/protocol_node.py
index f8921dadf..033cbd3b4 100644
--- a/unilabos/ros/nodes/presets/protocol_node.py
+++ b/unilabos/ros/nodes/presets/protocol_node.py
@@ -211,7 +211,7 @@ async def execute_protocol(goal_handle: ServerGoalHandle):
# 逐步执行工作流
step_results = []
for i, action in enumerate(protocol_steps):
- self.get_logger().info(f"Running step {i + 1}: {action}")
+ # self.get_logger().info(f"Running step {i + 1}: {action}")
if isinstance(action, dict):
# 如果是单个动作,直接执行
if action["action_name"] == "wait":