diff --git a/README.md b/README.md
index 0d02849..918e6fa 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.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 a80f06c..4163853 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.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 dde6acc..5807be8 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.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 c54e141..2f72c32 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.11"
source:
path: ../..
diff --git a/setup.py b/setup.py
index 4dde107..8a7eebd 100644
--- a/setup.py
+++ b/setup.py
@@ -4,7 +4,7 @@ package_name = 'unilabos'
setup(
name=package_name,
- version='0.9.8',
+ 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 5bb14ae..67b4c3b 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
@@ -51,9 +60,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 测完了三个都能跑✅
flowrate: float = 500
transfer_flowrate: float = 2500
@@ -66,25 +75,25 @@ 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
+ 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
@@ -95,21 +104,27 @@ class AddProtocol(BaseModel):
time: float
stir: bool
stir_speed: float
+
+
+
+
+
viscous: bool
- purpose: str
+ 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,7 +133,10 @@ class HeatChillProtocol(BaseModel):
temp: float
time: float
- stir: bool
+
+
+
+ stir: bool 测完了能跑✅
stir_speed: float
purpose: str
@@ -133,7 +151,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 +169,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
@@ -165,11 +185,11 @@ class CleanVesselProtocol(BaseModel):
class DissolveProtocol(BaseModel):
vessel: str
solvent: str
- volume: float
- amount: str = ""
- temp: float = 25.0
- time: float = 0.0
- stir_speed: float = 0.0
+ volume: float
+ amount: str = ""
+ temp: float = 25.0
+ time: float = 0.0
+ stir_speed: float = 0.0 测完了能跑✅
class FilterThroughProtocol(BaseModel):
from_vessel: str
@@ -183,16 +203,19 @@ class FilterThroughProtocol(BaseModel):
class RunColumnProtocol(BaseModel):
from_vessel: str
to_vessel: str
- column: str
+ 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: bool = False
+
+
+
+ stir_speed: float = 0.0 测完了能跑✅
time: float = 0.0
repeats: int = 1
@@ -219,4 +242,17 @@ 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="反应容器")
+
+ 还差
+
+
+
+
+
+单位修复:
+ evaporate
+ heatchill
+ recrysitallize
+ stir
+ wash solid
\ 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 d0f5c6a..43e4cc6 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": [
@@ -802,7 +898,7 @@
"type": "fluid",
"port": {
"multiway_valve_2": "3",
- "solenoid_valve_2": "in"
+ "solenoid_valve_2": "out"
}
},
{
@@ -812,7 +908,7 @@
"type": "fluid",
"port": {
"gas_source_1": "gassource",
- "solenoid_valve_2": "out"
+ "solenoid_valve_2": "in"
}
},
{
@@ -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/app/mq.py b/unilabos/app/mq.py
index c569c04..3969ec4 100644
--- a/unilabos/app/mq.py
+++ b/unilabos/app/mq.py
@@ -166,7 +166,7 @@ class MQTTClient:
status = {"data": device_status.get(device_id, {}), "device_id": device_id, "timestamp": time.time()}
address = f"labs/{MQConfig.lab_id}/devices/"
self.client.publish(address, json.dumps(status), qos=2)
- logger.info(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 144ec96..c46befe 100644
--- a/unilabos/compile/add_protocol.py
+++ b/unilabos/compile/add_protocol.py
@@ -1,627 +1,702 @@
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 find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str:
+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:
"""
- 根据试剂名称查找对应的试剂瓶,支持多种匹配模式:
- 1. 容器名称匹配(如 flask_DMF, reagent_bottle_1-DMF)
- 2. 容器内液体类型匹配(如 liquid_type: "DMF", name: "ethanol")
- 3. 试剂名称匹配(如 reagent_name: "DMF", config.reagent: "ethyl_acetate")
+ 解析体积输入,支持带单位的字符串
Args:
- G: 网络图
- reagent: 试剂名称
+ volume_input: 体积输入(如 "2.7 mL", "2.67 mL", "?", 10.0)
Returns:
- str: 试剂瓶的vessel ID
-
- Raises:
- ValueError: 如果找不到对应的试剂瓶
+ float: 体积(毫升)
"""
- print(f"ADD_PROTOCOL: 正在查找试剂 '{reagent}' 的容器...")
+ if isinstance(volume_input, (int, float)):
+ debug_print(f"📏 体积输入为数值: {volume_input}")
+ return float(volume_input)
- # 第一步:通过容器名称匹配
- 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
- ]
-
- # 尝试名称匹配
- for vessel_name in possible_names:
- if vessel_name in G.nodes():
- print(f"ADD_PROTOCOL: 通过名称匹配找到容器: {vessel_name}")
- return vessel_name
-
- # 第二步:通过模糊名称匹配(名称中包含试剂名)
- 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)
-
- 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():
+ if not volume_input or not str(volume_input).strip():
+ debug_print(f"⚠️ 体积输入为空,返回0.0mL")
return 0.0
- vessel_data = G.nodes[vessel].get('data', {})
- liquids = vessel_data.get('liquid', [])
+ volume_str = str(volume_input).lower().strip()
+ debug_print(f"🔍 解析体积输入: '{volume_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
+ # 处理未知体积
+ if volume_str in ['?', 'unknown', 'tbd', 'to be determined']:
+ default_volume = 10.0 # 默认10mL
+ debug_print(f"❓ 检测到未知体积,使用默认值: {default_volume}mL 🎯")
+ return default_volume
- return total_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
+ 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")
+
+ return 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]:
+def parse_mass_input(mass_input: Union[str, float]) -> float:
"""
- 根据试剂类型查找所有匹配的容器
- 返回匹配容器的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
-
- # 检查试剂类型匹配
- vessel_data = G.nodes[node_id].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()
+ Args:
+ mass_input: 质量输入(如 "19.3 g", "4.5 g", 2.5)
+
+ Returns:
+ 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}'")
+
+ # 移除空格并提取数字和单位
+ 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
+ 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")
+
+ 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)):
+ 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}'")
+
+ # 处理未知时间
+ if time_str in ['?', 'unknown', 'tbd']:
+ default_time = 60.0 # 默认1分钟
+ debug_print(f"❓ 检测到未知时间,使用默认值: {default_time}s (1分钟) ⏰")
+ 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
+ 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}秒")
+
+ return time_sec
+
+def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str:
+ """增强版试剂容器查找,支持固体和液体"""
+ 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', '')
+ 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.lower() == reagent_name or
- reagent.lower() == config_reagent):
- matching_vessels.append(node_id)
- continue
+ # 精确匹配
+ 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:常见的容器命名规则
+ debug_print(f"📋 方法2: 使用命名规则查找...")
+ reagent_clean = reagent.lower().replace(' ', '_').replace('-', '_')
+ possible_names = [
+ 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"
+ ]
+
+ 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} 📝")
+ 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} 🔍")
+ 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():
- matching_vessels.append(node_id)
- break
+ debug_print(f"✅ 通过液体类型匹配到容器: {node_id} 💧")
+ return node_id
- return matching_vessels
-
+ # 🔧 方法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} 🔄")
+ return node_id
+
+ debug_print(f"❌ 所有方法都失败了,无法找到容器!")
+ raise ValueError(f"找不到试剂 '{reagent}' 对应的容器")
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
- """
- 查找与指定容器相连的搅拌器
+ """查找连接到指定容器的搅拌器"""
+ debug_print(f"🔍 查找连接到容器 '{vessel}' 的搅拌器...")
- Args:
- G: 网络图
- vessel: 容器ID
+ 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}")
- Returns:
- str: 搅拌器ID,如果找不到则返回None
- """
- # 查找所有搅拌器节点
- stirrer_nodes = [node for node in G.nodes()
- if (G.nodes[node].get('class') or '') == 'virtual_stirrer']
+ 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} 🔗")
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 ""
+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} 🥄")
+ return node
+
+ 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,
reagent: str,
- volume: float,
- mass: float = 0.0,
+ # 🔧 修复:所有参数都用 Union 类型,支持字符串和数值
+ volume: Union[str, float] = 0.0,
+ mass: Union[str, float] = 0.0,
amount: str = "",
- time: float = 0.0,
- stir: bool = False,
- stir_speed: float = 300.0,
- viscous: bool = False,
- purpose: str = "添加试剂"
-) -> 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: 当找不到必要的设备或容器时
- """
- 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}")
-
- # 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)}")
-
- # 3. 验证试剂容器中的试剂体积
- available_volume = get_vessel_reagent_volume(G, reagent_vessel)
- print(f"ADD_PROTOCOL: 试剂容器 {reagent_vessel} 中有 {available_volume} mL 试剂")
-
- 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}' 没有可用路径")
-
- # 5. **先启动搅拌** - 关键改进!
- if stir:
- try:
- stirrer_id = find_connected_stirrer(G, vessel)
-
- if stirrer_id:
- print(f"ADD_PROTOCOL: 找到搅拌器 {stirrer_id},将在添加前启动搅拌")
-
- # 先启动搅拌
- stir_action = {
- "device_id": stirrer_id,
- "action_name": "start_stir",
- "action_kwargs": {
- "vessel": vessel,
- "stir_speed": stir_speed,
- "purpose": f"{purpose}: 启动搅拌,准备添加 {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
- else:
- # 使用默认流速
- if viscous:
- transfer_flowrate = 0.3 # 粘稠液体用较慢速度
- flowrate = 1.0
- else:
- transfer_flowrate = 0.5 # 普通液体默认速度
- flowrate = 2.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的核心算法实现液体转移
- try:
- 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="", # 添加试剂通常不需要清洗
- rinsing_volume=0.0,
- rinsing_repeats=0,
- solid=False,
- flowrate=flowrate,
- transfer_flowrate=transfer_flowrate
- )
-
- # 添加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: 添加试剂协议生成完成")
-
- 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,
+ time: Union[str, 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
+ # XDL扩展参数
+ mol: str = "",
+ event: str = "",
+ rate_spec: str = "",
+ equiv: str = "",
+ ratio: str = "",
+ **kwargs
) -> 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]]: 动作序列
+ 支持所有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"
"""
+
+ 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(f" 🌪️ stir: {stir}")
+ debug_print(f" 🔄 stir_speed: {stir_speed} rpm")
+ debug_print("=" * 60)
+
action_sequence = []
- # 1. 智能查找试剂瓶
- reagent_vessel = find_reagent_vessel(G, reagent)
+ # === 参数验证 ===
+ debug_print("🔍 步骤1: 参数验证...")
+ action_sequence.append(create_action_log(f"开始添加试剂 '{reagent}' 到容器 '{vessel}'", "🎬"))
- # 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}"
- }
- })
+ 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: 参数解析...")
+ 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("🔍 步骤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液体")
+
+ 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: 执行添加流程...")
+
+ try:
+ if is_solid:
+ # === 固体添加路径 ===
+ debug_print(f"🧂 使用固体添加路径")
+ action_sequence.append(create_action_log("开始固体试剂添加流程", "🧂"))
- # 等待搅拌稳定
- 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
- )
-
- 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
- }
+ 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",
+ "action_kwargs": {
+ "vessel": vessel,
+ "stir_speed": stir_speed,
+ "purpose": f"准备添加固体 {reagent}"
+ }
+ })
+ # 等待搅拌稳定
+ action_sequence.append(create_action_log("等待搅拌稳定...", "⏳"))
+ 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)
+ 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}
+ })
+
+ debug_print(f"✅ 固体添加完成")
+ else:
+ debug_print("❌ 未找到固体加样器,跳过固体添加")
+ action_sequence.append(create_action_log("未找到固体加样器,无法添加固体", "❌"))
+
+ else:
+ # === 液体添加路径 ===
+ 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",
+ "action_kwargs": {
+ "vessel": vessel,
+ "stir_speed": stir_speed,
+ "purpose": f"准备添加液体 {reagent}"
+ }
+ })
+ # 等待搅拌稳定
+ action_sequence.append(create_action_log("等待搅拌稳定...", "⏳"))
+ 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
+ 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")
+
+ 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(
+ G=G,
+ from_vessel=reagent_vessel,
+ to_vessel=vessel,
+ volume=final_volume,
+ amount=amount,
+ time=final_time,
+ viscous=viscous,
+ rinsing_solvent="",
+ rinsing_volume=0.0,
+ rinsing_repeats=0,
+ solid=False,
+ flowrate=flowrate,
+ transfer_flowrate=transfer_flowrate,
+ rate_spec=rate_spec,
+ event=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)}", "❌"))
+ # 添加错误日志
+ 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" {add_emoji} 添加类型: {add_type}")
+ 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)
+
+ # 添加完成日志
+ 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, "🎉"))
- print(f"ADD_PROTOCOL: 连续添加协议生成完成,共 {len(action_sequence)} 个动作")
return action_sequence
+# === 便捷函数 ===
-# 便捷函数:常用添加方案
-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_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, organic_reagent, volume, 0.0, "", 0.0,
- True, stir_speed, False, f"添加有机试剂 {organic_reagent}"
+ G, vessel, reagent,
+ volume=volume,
+ time=time,
+ rate_spec=rate_spec
)
-
-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_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, viscous_reagent, volume, 0.0, "", addition_time,
- True, 250.0, True, f"缓慢添加粘稠试剂 {viscous_reagent}"
+ G, vessel, reagent,
+ mass=mass,
+ event=event
)
-
-def generate_solvent_add_protocol(
- G: nx.DiGraph,
- vessel: str,
- solvent: str,
- volume: float
-) -> List[Dict[str, Any]]:
- """溶剂添加:快速、无需特殊处理"""
+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, solvent, volume, 0.0, "", 0.0,
- False, 300.0, False, f"添加溶剂 {solvent}"
+ G, vessel, reagent,
+ mol=mol,
+ event=event
)
+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,
+ time=time,
+ rate_spec="dropwise",
+ event=event
+ )
-# 使用示例和测试函数
+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,
+ time=time,
+ rate_spec="portionwise",
+ event=event
+ )
+
+# 测试函数
def test_add_protocol():
- """测试添加协议的示例"""
- print("=== ADD PROTOCOL 智能匹配测试 ===")
- print("测试完成")
-
+ """测试添加协议的各种参数解析"""
+ 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")
+
+ # 测试质量解析
+ 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")
+
+ # 测试时间解析
+ 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("✅ 测试完成")
if __name__ == "__main__":
test_add_protocol()
\ No newline at end of file
diff --git a/unilabos/compile/adjustph_protocol.py b/unilabos/compile/adjustph_protocol.py
index ce7c1c3..d8f1b1b 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 3da0d53..065196e 100644
--- a/unilabos/compile/dissolve_protocol.py
+++ b/unilabos/compile/dissolve_protocol.py
@@ -1,359 +1,889 @@
-from typing import List, Dict, Any
import networkx as nx
-from .pump_protocol import generate_pump_protocol
+import re
+import logging
+from typing import List, Dict, Any, Union
+from .pump_protocol import generate_pump_protocol_with_rinsing
+logger = logging.getLogger(__name__)
+
+def debug_print(message):
+ """调试输出"""
+ print(f"[DISSOLVE] {message}", flush=True)
+ logger.info(f"[DISSOLVE] {message}")
+
+# 🆕 创建进度日志动作
+def 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:
+ """
+ 解析体积输入,支持带单位的字符串
+
+ Args:
+ volume_input: 体积输入(如 "10 mL", "?", 10.0)
+
+ Returns:
+ 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}'")
+
+ # 处理未知体积
+ if volume_str in ['?', 'unknown', 'tbd', 'to be determined']:
+ default_volume = 50.0 # 默认50mL
+ debug_print(f"❓ 检测到未知体积,使用默认值: {default_volume}mL 🎯")
+ return default_volume
+
+ # 移除空格并提取数字和单位
+ volume_clean = re.sub(r'\s+', '', volume_str)
+
+ # 匹配数字和单位的正则表达式
+ match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
+
+ if not match:
+ debug_print(f"❌ 无法解析体积: '{volume_str}',使用默认值50mL")
+ return 50.0
+
+ value = float(match.group(1))
+ unit = match.group(2) or 'ml' # 默认单位为毫升
+
+ # 转换为毫升
+ if unit in ['l', 'liter']:
+ volume = value * 1000.0 # L -> mL
+ 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")
+
+ 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)):
+ 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}'")
+
+ # 处理未知质量
+ 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
+ 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")
+
+ return mass
+
+def parse_time_input(time_input: Union[str, float]) -> float:
+ """
+ 解析时间输入,支持带单位的字符串
+
+ Args:
+ time_input: 时间输入(如 "30 min", "1 h", "?", 60.0)
+
+ Returns:
+ float: 时间(秒)
+ """
+ if isinstance(time_input, (int, float)):
+ 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}'")
+
+ # 处理未知时间
+ if time_str in ['?', 'unknown', 'tbd']:
+ default_time = 600.0 # 默认10分钟
+ debug_print(f"❓ 检测到未知时间,使用默认值: {default_time}s (10分钟) ⏰")
+ 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
+ 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}秒")
+
+ return time_sec
+
+def parse_temperature_input(temp_input: Union[str, float]) -> float:
+ """
+ 解析温度输入,支持带单位的字符串
+
+ Args:
+ temp_input: 温度输入(如 "60 °C", "room temperature", "?", 25.0)
+
+ Returns:
+ float: 温度(摄氏度)
+ """
+ if isinstance(temp_input, (int, float)):
+ 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}'")
+
+ # 处理特殊温度描述
+ 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
+ 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")
+
+ return temp_c
def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
- """
- 查找溶剂容器
- """
- # 按照pump_protocol的命名规则查找溶剂瓶
- solvent_vessel_id = f"flask_{solvent}"
+ """增强版溶剂容器查找,支持多种匹配模式"""
+ debug_print(f"🔍 开始查找溶剂 '{solvent}' 的容器...")
- if solvent_vessel_id in G.nodes():
- return solvent_vessel_id
-
- # 如果直接匹配失败,尝试模糊匹配
+ # 🔧 方法1:直接搜索 data.reagent_name 和 config.reagent
+ debug_print(f"📋 方法1: 搜索reagent字段...")
for node in G.nodes():
- if node.startswith('flask_') and solvent.lower() in node.lower():
- return node
+ node_data = G.nodes[node].get('data', {})
+ node_type = G.nodes[node].get('type', '')
+ config_data = G.nodes[node].get('config', {})
+
+ # 只搜索容器类型的节点
+ if node_type == 'container':
+ reagent_name = node_data.get('reagent_name', '').lower()
+ config_reagent = config_data.get('reagent', '').lower()
+
+ # 精确匹配
+ if reagent_name == solvent.lower() or config_reagent == solvent.lower():
+ debug_print(f"✅ 通过reagent字段精确匹配到容器: {node} 🎯")
+ return node
+
+ # 模糊匹配
+ if (solvent.lower() in reagent_name and reagent_name) or \
+ (solvent.lower() in config_reagent and config_reagent):
+ debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node} 🔍")
+ return node
- # 如果还是找不到,列出所有可用的溶剂瓶
- available_flasks = [node for node in G.nodes()
- if node.startswith('flask_')
- and G.nodes[node].get('type') == 'container']
+ # 🔧 方法2:常见的容器命名规则
+ 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}",
+ f"{solvent_clean}_flask",
+ f"{solvent_clean}_bottle",
+ f"solvent_{solvent_clean}",
+ f"reagent_{solvent_clean}",
+ f"reagent_bottle_{solvent_clean}",
+ f"reagent_bottle_1", # 通用试剂瓶
+ f"reagent_bottle_2",
+ f"reagent_bottle_3"
+ ]
- raise ValueError(f"找不到溶剂 '{solvent}' 对应的溶剂瓶。可用溶剂瓶: {available_flasks}")
-
+ 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} 📝")
+ 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 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} 🔄")
+ return node_id
+
+ debug_print(f"❌ 所有方法都失败了,无法找到容器!")
+ raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器")
def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str:
- """
- 查找与指定容器相连的加热搅拌器
- """
- # 查找所有加热搅拌器节点
- heatchill_nodes = [node for node in G.nodes()
- if G.nodes[node].get('class') == 'virtual_heatchill']
+ """查找连接到指定容器的加热搅拌器"""
+ 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} 🔗")
return heatchill
- # 如果没有直接连接,返回第一个可用的加热器
- return heatchill_nodes[0] if heatchill_nodes else None
+ # 返回第一个加热器
+ if heatchill_nodes:
+ 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} 🔗")
+ return stirrer
+
+ # 返回第一个搅拌器
+ if stirrer_nodes:
+ 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} 🥄")
+ return node
+
+ debug_print(f"❌ 未找到固体加样器")
+ 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. 支持固体溶解和液体溶解两种模式
+ 4. 添加详细的emoji日志系统
- Args:
- G: 有向图,节点为设备和容器,边为流体管道
- vessel: 目标容器(要进行溶解的容器)
- solvent: 溶剂名称(用于查找对应的溶剂瓶)
- volume: 溶剂体积 (mL)
- amount: 要溶解的物质描述
- temp: 溶解温度 (°C),默认25°C(室温)
- time: 溶解时间 (秒),默认0(立即完成)
- stir_speed: 搅拌速度 (RPM),默认300 RPM
+ 支持两种溶解模式:
+ 1. 液体溶解:指定 solvent + volume,使用pump protocol转移溶剂
+ 2. 固体溶解:指定 mass/mol + reagent,使用固体加样器添加固体试剂
- Returns:
- List[Dict[str, Any]]: 溶解操作的动作序列
-
- Raises:
- ValueError: 当找不到必要的设备或容器时
-
- Examples:
- dissolve_actions = generate_dissolve_protocol(G, "reaction_mixture", "DMF", 10.0, "NaCl 2g", 60.0, 600.0, 400.0)
+ 支持所有XDL参数和单位:
+ - volume: "10 mL", "?" 或数值
+ - mass: "2.9 g", "?" 或数值
+ - temp: "60 °C", "room temperature", "?" 或数值
+ - time: "30 min", "1 h", "?" 或数值
+ - mol: "0.12 mol", "16.2 mmol"
"""
+
+ debug_print("=" * 60)
+ debug_print("🧪 开始生成溶解协议")
+ debug_print(f"📋 原始参数:")
+ debug_print(f" 🥼 vessel: '{vessel}'")
+ debug_print(f" 💧 solvent: '{solvent}'")
+ debug_print(f" 📏 volume: {volume} (类型: {type(volume)})")
+ debug_print(f" ⚖️ mass: {mass} (类型: {type(mass)})")
+ debug_print(f" 🌡️ temp: {temp} (类型: {type(temp)})")
+ debug_print(f" ⏱️ time: {time} (类型: {type(time)})")
+ debug_print(f" 🧪 reagent: '{reagent}'")
+ debug_print(f" 🧬 mol: '{mol}'")
+ debug_print(f" 🎯 event: '{event}'")
+ debug_print(f" 📦 kwargs: {kwargs}") # 显示额外参数
+ debug_print("=" * 60)
+
action_sequence = []
- print(f"DISSOLVE: 开始生成溶解协议")
- print(f" - 目标容器: {vessel}")
- print(f" - 溶剂: {solvent}")
- print(f" - 溶剂体积: {volume} mL")
- print(f" - 要溶解的物质: {amount}")
- print(f" - 溶解温度: {temp}°C")
- print(f" - 溶解时间: {time}s ({time/60:.1f}分钟)" if time > 0 else " - 溶解时间: 立即完成")
- print(f" - 搅拌速度: {stir_speed} RPM")
+ # === 参数验证 ===
+ debug_print("🔍 步骤1: 参数验证...")
+ action_sequence.append(create_action_log(f"开始溶解操作 - 容器: {vessel}", "🎬"))
+
+ if not vessel:
+ debug_print("❌ vessel 参数不能为空")
+ raise ValueError("vessel 参数不能为空")
- # 验证目标容器存在
if vessel not in G.nodes():
- raise ValueError(f"目标容器 '{vessel}' 不存在于系统中")
+ debug_print(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("✅ 基本参数验证通过")
+ action_sequence.append(create_action_log("参数验证通过", "✅"))
- # 验证是否存在从溶剂瓶到目标容器的路径
- 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: 参数解析...")
+ action_sequence.append(create_action_log("正在解析溶解参数...", "🔍"))
+
+ # 解析各种参数为数值
+ 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: 判断溶解类型...")
+ action_sequence.append(create_action_log("正在判断溶解类型...", "🔍"))
+
+ # 判断是固体溶解还是液体溶解
+ 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水溶解")
+
+ 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: 查找设备...")
+ action_sequence.append(create_action_log("正在查找相关设备...", "🔍"))
# 查找加热搅拌器
- heatchill_id = None
- if temp > 25.0 or stir_speed > 0 or time > 0:
- try:
- heatchill_id = find_connected_heatchill(G, vessel)
- if heatchill_id:
- print(f"DISSOLVE: 找到加热搅拌器: {heatchill_id}")
+ heatchill_id = find_connected_heatchill(G, vessel)
+ stirrer_id = find_connected_stirrer(G, vessel)
+
+ # 优先使用加热搅拌器,否则使用独立搅拌器
+ stir_device_id = heatchill_id or stirrer_id
+
+ debug_print(f"📊 设备映射:")
+ debug_print(f" 🔥 加热器: '{heatchill_id}'")
+ debug_print(f" 🌪️ 搅拌器: '{stirrer_id}'")
+ debug_print(f" 🎯 使用设备: '{stir_device_id}'")
+
+ 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: 执行溶解流程...")
+
+ 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")
+ 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",
+ "action_kwargs": {
+ "vessel": vessel,
+ "temp": final_temp,
+ "purpose": f"溶解准备 - {event}" if event else "溶解准备"
+ }
+ }
+ action_sequence.append(heatchill_action)
+
+ # 等待温度稳定
+ if final_temp > 25.0:
+ wait_time = min(60, abs(final_temp - 25.0) * 1.5)
+ action_sequence.append(create_action_log(f"等待温度稳定 ({wait_time:.0f}秒)", "⏳"))
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": wait_time}
+ })
+
+ 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",
+ "action_kwargs": {
+ "vessel": vessel,
+ "stir_speed": stir_speed,
+ "purpose": f"溶解搅拌 - {event}" if event else "溶解搅拌"
+ }
+ }
+ action_sequence.append(stir_action)
+
+ # 等待搅拌稳定
+ action_sequence.append(create_action_log("等待搅拌稳定...", "⏳"))
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": 5}
+ })
+
+ if is_solid_dissolve:
+ # === 固体溶解路径 ===
+ 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,
+ "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)
+ 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",
+ "action_kwargs": add_kwargs
+ })
+
+ debug_print(f"✅ 固体加样完成")
+ action_sequence.append(create_action_log("固体加样完成", "✅"))
else:
- print(f"DISSOLVE: 警告 - 需要加热/搅拌但未找到与容器 {vessel} 相连的加热搅拌器")
- except Exception as e:
- print(f"DISSOLVE: 加热搅拌器配置出错: {str(e)}")
-
- # === 第一步:启动加热搅拌(在添加溶剂前) ===
- if heatchill_id and (temp > 25.0 or time > 0):
- print(f"DISSOLVE: 启动加热搅拌器,温度: {temp}°C")
+ debug_print("⚠️ 未找到固体加样器,跳过固体添加")
+ action_sequence.append(create_action_log("未找到固体加样器,无法添加固体", "❌"))
- if time > 0:
- # 如果指定了时间,使用定时加热搅拌
- heatchill_action = {
+ elif is_liquid_dissolve:
+ # === 液体溶解路径 ===
+ 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:
+ # 计算流速 - 溶解时通常用较慢的速度,避免飞溅
+ 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,
+ 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(create_action_log(f"溶剂转移完成 ({len(pump_actions)} 个操作)", "✅"))
+
+ # 溶剂添加后等待
+ action_sequence.append(create_action_log("溶剂添加后短暂等待...", "⏳"))
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": 5}
+ })
+
+ # 步骤5.4: 等待溶解完成
+ if final_time > 0:
+ 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",
+ "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:
+ # 使用定时搅拌
+ action_sequence.append(create_action_log(f"使用搅拌器进行定时溶解", "🌪️"))
+
+ 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(create_action_log(f"简单等待溶解完成", "⏳"))
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": final_time}
+ })
+
+ # 步骤5.5: 停止加热搅拌(如果需要)
+ if heatchill_id and final_time == 0 and final_temp > 25.0:
+ debug_print(f"🔍 5.5: 停止加热器")
+ action_sequence.append(create_action_log("停止加热搅拌器", "🛑"))
+
+ stop_action = {
"device_id": heatchill_id,
- "action_name": "heat_chill",
+ "action_name": "heat_chill_stop",
"action_kwargs": {
- "vessel": vessel,
- "temp": temp,
- "time": time,
- "stir": True,
- "stir_speed": stir_speed,
- "purpose": f"溶解 {amount} 在 {solvent} 中"
- }
- }
- else:
- # 如果没有指定时间,使用持续加热搅拌
- heatchill_action = {
- "device_id": heatchill_id,
- "action_name": "heat_chill_start",
- "action_kwargs": {
- "vessel": vessel,
- "temp": temp,
- "purpose": f"溶解 {amount} 在 {solvent} 中"
+ "vessel": vessel
}
}
+ action_sequence.append(stop_action)
- action_sequence.append(heatchill_action)
-
- # 等待温度稳定
- if temp > 25.0:
- wait_time = min(60, abs(temp - 25.0) * 1.5) # 根据温差估算预热时间
- action_sequence.append({
- "action_name": "wait",
- "action_kwargs": {"time": wait_time}
- })
-
- # === 第二步:添加溶剂到目标容器 ===
- if volume > 0:
- print(f"DISSOLVE: 将 {volume} mL {solvent} 从 {solvent_vessel} 转移到 {vessel}")
-
- # 计算流速 - 溶解时通常用较慢的速度,避免飞溅
- transfer_flowrate = 1.0 # 较慢的转移速度
- flowrate = 0.5 # 较慢的注入速度
-
- try:
- # 使用成熟的 pump_protocol 算法进行液体转移
- pump_actions = generate_pump_protocol(
- G=G,
- from_vessel=solvent_vessel,
- to_vessel=vessel,
- volume=volume,
- flowrate=flowrate, # 注入速度 - 较慢避免飞溅
- transfer_flowrate=transfer_flowrate # 转移速度
- )
-
- action_sequence.extend(pump_actions)
-
- except Exception as e:
- raise ValueError(f"生成泵协议时出错: {str(e)}")
-
- # 溶剂添加后等待
+ except Exception as e:
+ debug_print(f"❌ 溶解流程执行失败: {str(e)}")
+ action_sequence.append(create_action_log(f"溶解流程失败: {str(e)}", "❌"))
+ # 添加错误日志
action_sequence.append({
- "action_name": "wait",
- "action_kwargs": {"time": 5}
+ "device_id": "system",
+ "action_name": "log_message",
+ "action_kwargs": {
+ "message": f"溶解失败: {str(e)}"
+ }
})
- # === 第三步:如果没有使用定时加热搅拌,但需要等待溶解 ===
- if time > 0 and heatchill_id and temp <= 25.0:
- # 只需要搅拌等待,不需要加热
- print(f"DISSOLVE: 室温搅拌 {time}s 等待溶解")
-
- stir_action = {
- "device_id": heatchill_id,
- "action_name": "heat_chill",
- "action_kwargs": {
- "vessel": vessel,
- "temp": 25.0, # 室温
- "time": time,
- "stir": True,
- "stir_speed": stir_speed,
- "purpose": f"室温搅拌溶解 {amount}"
- }
- }
- action_sequence.append(stir_action)
+ # === 最终结果 ===
+ debug_print("=" * 60)
+ 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)")
+ 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)
- # === 第四步:如果使用了持续加热,需要手动停止 ===
- if heatchill_id and time == 0 and temp > 25.0:
- print(f"DISSOLVE: 停止加热搅拌器")
-
- stop_action = {
- "device_id": heatchill_id,
- "action_name": "heat_chill_stop",
- "action_kwargs": {
- "vessel": vessel
- }
- }
- action_sequence.append(stop_action)
+ # 添加完成日志
+ 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})"
- print(f"DISSOLVE: 生成了 {len(action_sequence)} 个动作")
- print(f"DISSOLVE: 溶解协议生成完成")
+ action_sequence.append(create_action_log(summary_msg, "🎉"))
return action_sequence
+# === 便捷函数 ===
-# 便捷函数:常用溶解方案
-def generate_room_temp_dissolve_protocol(
- G: nx.DiGraph,
- vessel: str,
- solvent: str,
- volume: float,
- amount: str = "",
- stir_time: float = 300.0 # 5分钟
-) -> List[Dict[str, Any]]:
- """室温溶解:快速搅拌,短时间"""
- return generate_dissolve_protocol(G, vessel, solvent, volume, amount, 25.0, stir_time, 400.0)
+def dissolve_solid_by_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, float],
+ temp: Union[str, float] = 25.0, time: Union[str, float] = "10 min") -> List[Dict[str, Any]]:
+ """按质量溶解固体"""
+ debug_print(f"🧂 快速固体溶解: {reagent} ({mass}) → {vessel}")
+ return generate_dissolve_protocol(
+ G, vessel,
+ mass=mass,
+ reagent=reagent,
+ temp=temp,
+ time=time
+ )
+def dissolve_solid_by_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str,
+ temp: Union[str, float] = 25.0, time: Union[str, float] = "10 min") -> List[Dict[str, Any]]:
+ """按摩尔数溶解固体"""
+ debug_print(f"🧬 按摩尔数溶解固体: {reagent} ({mol}) → {vessel}")
+ return generate_dissolve_protocol(
+ G, vessel,
+ mol=mol,
+ reagent=reagent,
+ temp=temp,
+ time=time
+ )
-def generate_heated_dissolve_protocol(
- G: nx.DiGraph,
- vessel: str,
- solvent: str,
- volume: float,
- amount: str = "",
- temp: float = 60.0,
- dissolve_time: float = 900.0 # 15分钟
-) -> List[Dict[str, Any]]:
- """加热溶解:中等温度,较长时间"""
- return generate_dissolve_protocol(G, vessel, solvent, volume, amount, temp, dissolve_time, 300.0)
+def dissolve_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float],
+ temp: Union[str, float] = 25.0, time: Union[str, float] = "5 min") -> List[Dict[str, Any]]:
+ """用溶剂溶解"""
+ debug_print(f"💧 溶剂溶解: {solvent} ({volume}) → {vessel}")
+ return generate_dissolve_protocol(
+ G, vessel,
+ solvent=solvent,
+ volume=volume,
+ temp=temp,
+ time=time
+ )
+def dissolve_at_room_temp(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float]) -> List[Dict[str, Any]]:
+ """室温溶解"""
+ debug_print(f"🌡️ 室温溶解: {solvent} ({volume}) → {vessel}")
+ return generate_dissolve_protocol(
+ G, vessel,
+ solvent=solvent,
+ volume=volume,
+ temp="room temperature",
+ time="5 min"
+ )
-def generate_gentle_dissolve_protocol(
- G: nx.DiGraph,
- vessel: str,
- solvent: str,
- volume: float,
- amount: str = "",
- temp: float = 40.0,
- dissolve_time: float = 1800.0 # 30分钟
-) -> List[Dict[str, Any]]:
- """温和溶解:低温,长时间,慢搅拌"""
- return generate_dissolve_protocol(G, vessel, solvent, volume, amount, temp, dissolve_time, 200.0)
-
-
-def generate_hot_dissolve_protocol(
- G: nx.DiGraph,
- vessel: str,
- solvent: str,
- volume: float,
- amount: str = "",
- temp: float = 80.0,
- dissolve_time: float = 600.0 # 10分钟
-) -> List[Dict[str, Any]]:
- """高温溶解:高温,中等时间,快搅拌"""
- return generate_dissolve_protocol(G, vessel, solvent, volume, amount, temp, dissolve_time, 500.0)
-
-
-def generate_sequential_dissolve_protocol(
- G: nx.DiGraph,
- vessel: str,
- dissolve_steps: List[Dict[str, Any]]
-) -> List[Dict[str, Any]]:
- """
- 生成连续溶解多种物质的协议
-
- Args:
- G: 网络图
- vessel: 目标容器
- dissolve_steps: 溶解步骤列表,每个元素包含溶解参数
-
- Returns:
- List[Dict[str, Any]]: 完整的动作序列
-
- Example:
- dissolve_steps = [
- {
- "solvent": "water",
- "volume": 5.0,
- "amount": "NaCl 1g",
- "temp": 25.0,
- "time": 300.0,
- "stir_speed": 300.0
- },
- {
- "solvent": "ethanol",
- "volume": 2.0,
- "amount": "organic compound 0.5g",
- "temp": 40.0,
- "time": 600.0,
- "stir_speed": 400.0
- }
- ]
- """
- action_sequence = []
-
- for i, step in enumerate(dissolve_steps):
- print(f"DISSOLVE: 处理第 {i+1}/{len(dissolve_steps)} 个溶解步骤")
-
- # 生成单个溶解步骤的协议
- dissolve_actions = generate_dissolve_protocol(
- G=G,
- vessel=vessel,
- solvent=step.get('solvent'),
- volume=step.get('volume', 0.0),
- amount=step.get('amount', ''),
- temp=step.get('temp', 25.0),
- time=step.get('time', 0.0),
- stir_speed=step.get('stir_speed', 300.0)
- )
-
- action_sequence.extend(dissolve_actions)
-
- # 在步骤之间加入等待时间
- if i < len(dissolve_steps) - 1: # 不是最后一个步骤
- action_sequence.append({
- "action_name": "wait",
- "action_kwargs": {"time": 10}
- })
-
- print(f"DISSOLVE: 连续溶解协议生成完成,共 {len(action_sequence)} 个动作")
- return action_sequence
-
+def dissolve_with_heating(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float],
+ temp: Union[str, float] = "60 °C", time: Union[str, float] = "15 min") -> List[Dict[str, Any]]:
+ """加热溶解"""
+ debug_print(f"🔥 加热溶解: {solvent} ({volume}) → {vessel} @ {temp}")
+ return generate_dissolve_protocol(
+ G, vessel,
+ solvent=solvent,
+ volume=volume,
+ temp=temp,
+ time=time
+ )
# 测试函数
def test_dissolve_protocol():
- """测试溶解协议的示例"""
- print("=== DISSOLVE PROTOCOL 测试 ===")
- print("测试完成")
-
+ """测试溶解协议的各种参数解析"""
+ debug_print("=== DISSOLVE PROTOCOL 增强版测试 ===")
+
+ # 测试体积解析
+ debug_print("💧 测试体积解析...")
+ volumes = ["10 mL", "?", 10.0, "1 L", "500 μL"]
+ for vol in volumes:
+ result = parse_volume_input(vol)
+ 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)
+ 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)
+ debug_print(f"🌡️ 温度解析: {temp} → {result}°C")
+
+ # 测试时间解析
+ debug_print("⏱️ 测试时间解析...")
+ times = ["30 min", "1 h", "?", 60.0]
+ for time in times:
+ result = parse_time_input(time)
+ debug_print(f"⏱️ 时间解析: {time} → {result}s")
+
+ debug_print("✅ 测试完成")
if __name__ == "__main__":
test_dissolve_protocol()
\ No newline at end of file
diff --git a/unilabos/compile/dry_protocol.py b/unilabos/compile/dry_protocol.py
index 34044eb..1f06069 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/evacuateandrefill_protocol.py b/unilabos/compile/evacuateandrefill_protocol.py
index aa44df6..cbcf19b 100644
--- a/unilabos/compile/evacuateandrefill_protocol.py
+++ b/unilabos/compile/evacuateandrefill_protocol.py
@@ -1,8 +1,68 @@
-import numpy as np
import networkx as nx
+import logging
+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):
+ """调试输出函数 - 支持中文"""
+ 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:
"""
@@ -11,9 +71,10 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str:
2. 气体类型匹配(data.gas_type)
3. 默认气源
"""
- print(f"EVACUATE_REFILL: 正在查找气体 '{gas}' 的气源...")
+ debug_print(f"🔍 正在查找气体 '{gas}' 的气源...")
# 第一步:通过容器名称匹配
+ debug_print(f"📋 方法1: 容器名称匹配...")
gas_source_patterns = [
f"gas_source_{gas}",
f"gas_{gas}",
@@ -24,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():
- print(f"EVACUATE_REFILL: 通过名称匹配找到气源: {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 ''
@@ -44,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():
- print(f"EVACUATE_REFILL: 通过气体类型匹配找到气源: {node_id} (gas_type: {gas_type})")
+ debug_print(f"✅ 通过气体类型找到气源: {node_id} (气体类型: {gas_type})")
return node_id
# 检查 config.gas_type
@@ -52,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():
- print(f"EVACUATE_REFILL: 通过配置气体类型匹配找到气源: {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]
@@ -66,226 +131,382 @@ 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})")
- print(f"EVACUATE_REFILL: 可用气源列表: {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 '').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
-
+ debug_print(f"❌ 所有方法都失败了!")
+ raise ValueError(f"无法找到气体 '{gas}' 的气源。可用气源: {available_gas_sources}")
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)
+ debug_print(f"📋 发现真空泵: {node}")
if not vacuum_pumps:
- raise ValueError("系统中未找到真空泵设备")
+ debug_print(f"❌ 系统中未找到真空泵")
+ 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)
+ 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}")
return stirrer
- return stirrer_nodes[0] if stirrer_nodes else None
-
-
-def find_associated_solenoid_valve(G: nx.DiGraph, device_id: str) -> Optional[str]:
- """查找与指定设备相关联的电磁阀"""
- solenoid_valves = [
- node for node in G.nodes()
- if ('solenoid' in (G.nodes[node].get('class') or '').lower()
- or 'solenoid_valve' in node)
- ]
-
- # 通过网络连接查找直接相连的电磁阀
- for solenoid in solenoid_valves:
- if G.has_edge(device_id, solenoid) or G.has_edge(solenoid, device_id):
- return solenoid
-
- # 通过命名规则查找关联的电磁阀
- device_type = ""
- if 'vacuum' in device_id.lower():
- device_type = "vacuum"
- elif 'gas' in device_id.lower():
- device_type = "gas"
-
- if device_type:
- for solenoid in solenoid_valves:
- if device_type in solenoid.lower():
- return solenoid
+ # 如果没有连接的搅拌器,返回第一个可用的
+ if stirrer_nodes:
+ debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个可用的: {stirrer_nodes[0]}")
+ return stirrer_nodes[0]
+ debug_print("❌ 未找到搅拌器")
return None
+def find_vacuum_solenoid_valve(G: nx.DiGraph, vacuum_pump: str) -> Optional[str]:
+ """查找真空泵相关的电磁阀"""
+ debug_print(f"🔍 正在查找真空泵 {vacuum_pump} 的电磁阀...")
+
+ # 查找所有电磁阀
+ solenoid_valves = []
+ for node in G.nodes():
+ node_data = G.nodes[node]
+ node_class = node_data.get('class', '') or ''
+
+ if ('solenoid' in node_class.lower() or 'solenoid_valve' in node.lower()):
+ solenoid_valves.append(node)
+ debug_print(f"📋 发现电磁阀: {node}")
+
+ debug_print(f"📊 找到的电磁阀: {solenoid_valves}")
+
+ # 检查连接关系
+ 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}")
+ 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}")
+ 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)
+
+ 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}")
+ 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}")
+ return solenoid
+
+ debug_print("⚠️ 未找到气源电磁阀")
+ return None
def generate_evacuateandrefill_protocol(
G: nx.DiGraph,
vessel: str,
gas: str,
- repeats: int = 1
+ **kwargs
) -> List[Dict[str, Any]]:
"""
- 生成抽真空和充气操作的动作序列
+ 生成抽真空和充气操作的动作序列 - 中文版
- **修复版本**: 正确调用 pump_protocol 并处理异常
+ Args:
+ G: 设备图
+ vessel: 目标容器名称(必需)
+ gas: 气体名称(必需)
+ **kwargs: 其他参数(兼容性)
+
+ Returns:
+ List[Dict[str, Any]]: 动作序列
"""
+
+ # 硬编码重复次数为 3
+ repeats = 3
+
+ # 生成协议ID
+ protocol_id = str(uuid.uuid4())
+ debug_print(f"🆔 生成协议ID: {protocol_id}")
+
+ debug_print("=" * 60)
+ 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 = []
- # 参数设置 - 关键修复:减小体积避免超出泵容量
- VACUUM_VOLUME = 20.0 # 减小抽真空体积
- REFILL_VOLUME = 20.0 # 减小充气体积
- PUMP_FLOW_RATE = 2.5 # 降低流速
- STIR_SPEED = 300.0
+ # === 参数验证和修正 ===
+ 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}", "🔄"))
- print(f"EVACUATE_REFILL: 开始生成协议,目标容器: {vessel}, 气体: {gas}, 重复次数: {repeats}")
+ # 验证必需参数
+ if not vessel:
+ debug_print("❌ 容器参数不能为空")
+ raise ValueError("容器参数不能为空")
+
+ if not gas:
+ debug_print("❌ 气体参数不能为空")
+ raise ValueError("气体参数不能为空")
- # 1. 验证设备存在
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',
+ '氮气': '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}")
+ action_sequence.append(create_action_log(f"气体名称标准化: {original_gas} -> {gas}", "🔄"))
+
+ debug_print(f"📋 最终参数: 容器={vessel}, 气体={gas}, 重复={repeats}")
+
+ # === 查找设备 ===
+ debug_print("🔍 步骤2: 查找设备...")
+ action_sequence.append(create_action_log("正在查找相关设备...", "🔍"))
- # 2. 查找设备
try:
vacuum_pump = find_vacuum_pump(G)
- vacuum_solenoid = find_associated_solenoid_valve(G, vacuum_pump)
+ action_sequence.append(create_action_log(f"找到真空泵: {vacuum_pump}", "🌪️"))
+
gas_source = find_gas_source(G, gas)
- gas_solenoid = find_associated_solenoid_valve(G, gas_source)
+ 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("未找到搅拌器", "⚠️"))
- 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)}")
+ action_sequence.append(create_action_log(f"设备查找失败: {str(e)}", "❌"))
raise ValueError(f"设备查找失败: {str(e)}")
- # 3. **关键修复**: 验证路径存在性
+ # === 参数设置 ===
+ debug_print("🔍 步骤3: 参数设置...")
+ action_sequence.append(create_action_log("设置操作参数...", "⚙️"))
+
+ # 根据气体类型调整参数
+ 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("💨 惰性气体: 使用标准参数")
+ 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("🔥 活性气体: 使用保守参数")
+ 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("❓ 未知气体: 使用安全参数")
+ 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")
+
+ 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: 路径验证...")
+ action_sequence.append(create_action_log("验证传输路径...", "🛤️"))
+
try:
# 验证抽真空路径
- vacuum_path = nx.shortest_path(G, source=vessel, target=vacuum_pump)
- print(f"EVACUATE_REFILL: 抽真空路径: {' → '.join(vacuum_path)}")
+ 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)}")
+ action_sequence.append(create_action_log(f"真空路径: {' -> '.join(vacuum_path)}", "🛤️"))
+ else:
+ debug_print(f"⚠️ 真空路径不存在,继续执行但可能有问题")
+ action_sequence.append(create_action_log("真空路径检查: 路径不存在", "⚠️"))
# 验证充气路径
- gas_path = nx.shortest_path(G, source=gas_source, target=vessel)
- print(f"EVACUATE_REFILL: 充气路径: {' → '.join(gas_path)}")
+ 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)}")
+ action_sequence.append(create_action_log(f"气体路径: {' -> '.join(gas_path)}", "🛤️"))
+ else:
+ debug_print(f"⚠️ 气体路径不存在,继续执行但可能有问题")
+ action_sequence.append(create_action_log("气体路径检查: 路径不存在", "⚠️"))
- # **新增**: 检查路径中的边数据
- 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}")
-
- 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:
- raise ValueError(f"路径不存在: {str(e)}")
except Exception as e:
- raise ValueError(f"路径验证失败: {str(e)}")
+ debug_print(f"⚠️ 路径验证失败: {str(e)},继续执行")
+ action_sequence.append(create_action_log(f"路径验证失败: {str(e)}", "⚠️"))
+
+ # === 启动搅拌器 ===
+ debug_print("🔍 步骤5: 启动搅拌器...")
- # 4. 启动搅拌器
if 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("⚠️ 未找到搅拌器,跳过搅拌器启动")
+ action_sequence.append(create_action_log("跳过搅拌器启动", "⏭️"))
+
+ # === 执行循环 ===
+ debug_print("🔍 步骤6: 执行抽真空-充气循环...")
+ action_sequence.append(create_action_log(f"开始 {repeats} 次抽真空-充气循环", "🔄"))
- # 5. 执行多次抽真空-充气循环
for cycle in range(repeats):
- print(f"EVACUATE_REFILL: === 第 {cycle+1}/{repeats} 次循环 ===")
+ debug_print(f"=== 第 {cycle+1}/{repeats} 轮循环 ===")
+ action_sequence.append(create_action_log(f"第 {cycle+1}/{repeats} 轮循环开始", "🚀"))
# ============ 抽真空阶段 ============
- print(f"EVACUATE_REFILL: 抽真空阶段开始")
+ debug_print(f"🌪️ 抽真空阶段开始")
+ action_sequence.append(create_action_log("开始抽真空阶段", "🌪️"))
# 启动真空泵
+ 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",
@@ -294,16 +515,17 @@ def generate_evacuateandrefill_protocol(
# 开启真空电磁阀
if 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"}
})
- # **关键修复**: 改进 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}")
+ action_sequence.append(create_action_log(f"开始抽真空: {vessel} -> {vacuum_pump}", "🌪️"))
try:
vacuum_transfer_actions = generate_pump_protocol_with_rinsing(
@@ -314,7 +536,7 @@ def generate_evacuateandrefill_protocol(
amount="",
time=0.0,
viscous=False,
- rinsing_solvent="", # **修复**: 明确不使用清洗
+ rinsing_solvent="",
rinsing_volume=0.0,
rinsing_repeats=0,
solid=False,
@@ -324,52 +546,36 @@ 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)} 个抽真空动作")
+ action_sequence.append(create_action_log(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(create_action_log("抽真空协议为空,使用手动等待", "⚠️"))
+ 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(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}
+ })
# 关闭真空电磁阀
if 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",
@@ -377,32 +583,47 @@ def generate_evacuateandrefill_protocol(
})
# 关闭真空泵
+ 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}
+ })
+
# ============ 充气阶段 ============
- print(f"EVACUATE_REFILL: 充气阶段开始")
+ debug_print(f"💨 充气阶段开始")
+ action_sequence.append(create_action_log("开始气体充气阶段", "💨"))
# 启动气源
+ 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_name": "set_status",
"action_kwargs": {"string": "ON"}
})
# 开启气源电磁阀
if 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"}
})
- # **关键修复**: 改进充气 pump_protocol 调用
- print(f"EVACUATE_REFILL: 调用充气 pump_protocol: {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(
@@ -413,7 +634,7 @@ def generate_evacuateandrefill_protocol(
amount="",
time=0.0,
viscous=False,
- rinsing_solvent="", # **修复**: 明确不使用清洗
+ rinsing_solvent="",
rinsing_volume=0.0,
rinsing_repeats=0,
solid=False,
@@ -423,77 +644,36 @@ 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)} 个充气动作")
+ action_sequence.append(create_action_log(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(create_action_log("充气协议为空,使用手动等待", "⚠️"))
+ 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(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}
+ })
# 关闭气源电磁阀
if 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",
@@ -501,37 +681,92 @@ def generate_evacuateandrefill_protocol(
})
# 关闭气源
+ 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"⏳ 等待下一个循环...")
+ action_sequence.append(create_action_log("等待下一个循环...", "⏳"))
action_sequence.append({
"action_name": "wait",
- "action_kwargs": {"time": 2.0}
+ "action_kwargs": {"time": 10.0}
})
+ else:
+ action_sequence.append(create_action_log(f"第 {cycle+1}/{repeats} 轮循环完成", "✅"))
+
+ # === 停止搅拌器 ===
+ debug_print("🔍 步骤7: 停止搅拌器...")
- # 停止搅拌器
if 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"📊 协议统计:")
+ 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, "🎉"))
- print(f"EVACUATE_REFILL: 协议生成完成,共 {len(action_sequence)} 个动作")
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():
"""测试抽真空充气协议"""
- print("=== EVACUATE AND REFILL PROTOCOL 测试 ===")
- print("测试完成")
-
+ debug_print("=== 抽真空充气协议增强中文版测试 ===")
+ 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 4cee78d..6a2d6f6 100644
--- a/unilabos/compile/evaporate_protocol.py
+++ b/unilabos/compile/evaporate_protocol.py
@@ -1,326 +1,366 @@
-from typing import List, Dict, Any
+from typing import List, Dict, Any, Optional, Union
import networkx as nx
-from .pump_protocol import generate_pump_protocol
+import logging
+import re
+logger = logging.getLogger(__name__)
-def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:
+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 vessel not in G.nodes():
- return 0.0
+ if isinstance(time_input, (int, float)):
+ debug_print(f"⏱️ 时间输入为数字: {time_input}s ✨")
+ return float(time_input)
- vessel_data = G.nodes[vessel].get('data', {})
- liquids = vessel_data.get('liquid', [])
+ if not time_input or not str(time_input).strip():
+ debug_print(f"⚠️ 时间输入为空,使用默认值: 180s (3分钟) 🕐")
+ return 180.0 # 默认3分钟
- total_volume = 0.0
- for liquid in liquids:
- if isinstance(liquid, dict) and 'liquid_volume' in liquid:
- total_volume += liquid['liquid_volume']
+ time_str = str(time_input).lower().strip()
+ debug_print(f"🔍 解析时间输入: '{time_str}' 📝")
- return total_volume
+ # 处理未知时间
+ if time_str in ['?', 'unknown', 'tbd']:
+ default_time = 180.0 # 默认3分钟
+ debug_print(f"❓ 检测到未知时间,使用默认值: {default_time}s (3分钟) 🤷♀️")
+ 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 (3分钟) 😅")
+ 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
+ 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 (已是秒) ⏰")
+
+ return time_sec
+def find_rotavap_device(G: nx.DiGraph, vessel: str = None) -> Optional[str]:
+ """
+ 在组态图中查找旋转蒸发仪设备
+
+ Args:
+ G: 设备图
+ vessel: 指定的设备名称(可选)
+
+ Returns:
+ str: 找到的旋转蒸发仪设备ID,如果没找到返回None
+ """
+ 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}")
+
+ # 检查是否为旋转蒸发仪
+ 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} 不存在 😞")
+
+ # 在所有设备中查找旋转蒸发仪
+ debug_print("🔎 在所有设备中搜索旋转蒸发仪... 🕵️♀️")
+ 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_rotavap_device(G: nx.DiGraph) -> str:
- """查找旋转蒸发仪设备"""
- rotavap_nodes = [node for node in G.nodes()
- if (G.nodes[node].get('class') or '') == 'virtual_rotavap']
+def find_connected_vessel(G: nx.DiGraph, rotavap_device: str) -> Optional[str]:
+ """
+ 查找与旋转蒸发仪连接的容器
- if rotavap_nodes:
- return rotavap_nodes[0]
+ Args:
+ G: 设备图
+ rotavap_device: 旋转蒸发仪设备ID
- raise ValueError("系统中未找到旋转蒸发仪设备")
-
-
-def find_solvent_recovery_vessel(G: nx.DiGraph) -> str:
- """查找溶剂回收容器"""
- possible_names = [
- "flask_distillate",
- "bottle_distillate",
- "vessel_distillate",
- "distillate",
- "solvent_recovery",
- "flask_solvent_recovery",
- "collection_flask"
- ]
+ Returns:
+ str: 连接的容器ID,如果没找到返回None
+ """
+ debug_print(f"🔗 查找与 {rotavap_device} 连接的容器... 🥽")
- for vessel_name in possible_names:
- if vessel_name in G.nodes():
- return vessel_name
+ # 查看旋转蒸发仪的子设备
+ rotavap_data = G.nodes[rotavap_device]
+ children = rotavap_data.get('children', [])
- # 如果找不到专门的回收容器,使用废液容器
- waste_names = ["waste_workup", "flask_waste", "bottle_waste", "waste"]
- for vessel_name in waste_names:
- if vessel_name in G.nodes():
- return vessel_name
+ 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} 🥽✨")
+ return child_id
- raise ValueError(f"未找到溶剂回收容器。尝试了以下名称: {possible_names + waste_names}")
-
+ # 查看邻接的容器
+ 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} 🥽✨")
+ return neighbor
+
+ debug_print("😞 未找到连接的容器 💔")
+ return None
def generate_evaporate_protocol(
G: nx.DiGraph,
vessel: str,
pressure: float = 0.1,
temp: float = 60.0,
- time: float = 1800.0,
- stir_speed: float = 100.0
+ time: Union[str, float] = "180", # 🔧 修改:支持字符串时间
+ 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: 蒸发时间(支持 "3 min", "180", "0.5 h" 等)
+ stir_speed: 旋转速度 (RPM),默认100
+ solvent: 溶剂名称(用于参数优化)
+ **kwargs: 其他参数(兼容性)
Returns:
- List[Dict[str, Any]]: 蒸发操作的动作序列
-
- Raises:
- ValueError: 当找不到必要的设备时抛出异常
-
- Examples:
- evaporate_actions = generate_evaporate_protocol(G, "reaction_mixture", 0.05, 80.0, 3600.0)
+ List[Dict[str, Any]]: 动作序列
"""
+
+ 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: 查找旋转蒸发仪设备... 🔍")
+
+ # 验证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: 确定目标容器... 🥽")
+
+ 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} 🥽✨")
+ 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: 单位解析处理... ⚡")
+
+ # 解析时间
+ 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:
+ 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 🌡️")
+ temp = 60.0
+ else:
+ debug_print(f"✅ 温度 {temp}°C 在正常范围内 🌡️")
+
+ if final_time <= 0:
+ 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 🌪️")
+ stir_speed = 100.0
+ else:
+ debug_print(f"✅ 旋转速度 {stir_speed} RPM 在正常范围内 🌪️")
+
+ # 根据溶剂优化参数
+ 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("🔥 高沸点溶剂:提高温度,降低真空度 🌡️💨")
+ else:
+ debug_print("🧪 通用溶剂,使用标准参数 ✨")
+ else:
+ debug_print("🤷♀️ 未指定溶剂,使用默认参数 ✨")
+
+ debug_print(f"🎯 最终参数: pressure={pressure} bar 💨, temp={temp}°C 🌡️, time={final_time}s ⏰, stir_speed={stir_speed} RPM 🌪️")
+
+ # === 步骤5: 生成动作序列 ===
+ debug_print("📍 步骤5: 生成动作序列... 🎬")
+
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")
-
- # 验证源容器存在
- if vessel not in G.nodes():
- raise ValueError(f"源容器 '{vessel}' 不存在于系统中")
-
- # 获取源容器中的液体体积
- source_volume = get_vessel_liquid_volume(G, vessel)
- print(f"EVAPORATE: 源容器 {vessel} 中有 {source_volume} mL 液体")
-
- # 查找旋转蒸发仪
- try:
- rotavap_id = find_rotavap_device(G)
- print(f"EVAPORATE: 找到旋转蒸发仪: {rotavap_id}")
- except ValueError as e:
- raise ValueError(f"无法找到旋转蒸发仪: {str(e)}")
-
- # 查找旋转蒸发仪样品容器
- 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 not rotavap_vessel:
- raise ValueError(f"未找到旋转蒸发仪样品容器。尝试了: {possible_rotavap_vessels}")
-
- print(f"EVAPORATE: 找到旋转蒸发仪样品容器: {rotavap_vessel}")
-
- # 查找溶剂回收容器
- 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
-
- # === 简化的体积计算策略 ===
- if source_volume > 0:
- # 如果能检测到液体体积,使用实际体积的大部分
- transfer_volume = min(source_volume * 0.9, 250.0) # 90%或最多250mL
- print(f"EVAPORATE: 检测到液体体积,将转移 {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 = {
+ # 1. 等待稳定
+ debug_print(" 🔄 动作1: 添加初始等待稳定... ⏳")
+ action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 10}
- }
- action_sequence.append(wait_action)
+ })
+ 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")
- # === 第二步:执行旋转蒸发 ===
- print(f"EVAPORATE: 执行旋转蒸发操作")
evaporate_action = {
- "device_id": rotavap_id,
+ "device_id": rotavap_device,
"action_name": "evaporate",
"action_kwargs": {
- "vessel": rotavap_vessel,
+ "vessel": target_vessel,
"pressure": pressure,
"temp": temp,
- "time": time,
- "stir_speed": stir_speed
+ "time": final_time,
+ "stir_speed": stir_speed,
+ "solvent": solvent
}
}
action_sequence.append(evaporate_action)
+ debug_print(" ✅ 蒸发动作已添加 🌪️✨")
- # 蒸发后等待系统稳定
- wait_action = {
+ # 3. 蒸发后等待
+ debug_print(" 🔄 动作3: 添加蒸发后等待... ⏳")
+ action_sequence.append({
"action_name": "wait",
- "action_kwargs": {"time": 30}
- }
- action_sequence.append(wait_action)
+ "action_kwargs": {"time": 10}
+ })
+ debug_print(" ✅ 蒸发后等待动作已添加 ⏳✨")
- # === 第三步:溶剂回收(如果有回收容器)===
- if distillate_vessel:
- print(f"EVAPORATE: 回收冷凝溶剂到 {distillate_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(
- G=G,
- from_vessel=cleaning_solvent,
- to_vessel=rotavap_vessel,
- volume=cleaning_volume,
- flowrate=2.0,
- transfer_flowrate=2.0
- )
- action_sequence.extend(cleaning_actions)
-
- # 将清洗液转移到废液/回收容器
- waste_actions = generate_pump_protocol(
- G=G,
- from_vessel=rotavap_vessel,
- to_vessel=distillate_vessel, # 使用回收容器作为废液
- volume=cleaning_volume,
- flowrate=2.0,
- transfer_flowrate=2.0
- )
- action_sequence.extend(waste_actions)
-
- except Exception as e:
- print(f"EVAPORATE: 清洗步骤失败: {str(e)}")
-
- print(f"EVAPORATE: 生成了 {len(action_sequence)} 个动作")
- print(f"EVAPORATE: 蒸发协议生成完成")
- print(f"EVAPORATE: 总处理体积: {transfer_volume} mL")
+ # === 总结 ===
+ 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
-
-
-# 便捷函数:常用蒸发方案 - 都使用250mL标准瓶体积
-def generate_quick_evaporate_protocol(
- G: nx.DiGraph,
- vessel: str,
- temp: float = 40.0,
- time: float = 900.0 # 15分钟
-) -> List[Dict[str, Any]]:
- """快速蒸发:低温、短时间、整瓶处理"""
- return generate_evaporate_protocol(G, vessel, 0.2, temp, time, 80.0)
-
-
-def generate_gentle_evaporate_protocol(
- G: nx.DiGraph,
- vessel: str,
- temp: float = 50.0,
- time: float = 2700.0 # 45分钟
-) -> List[Dict[str, Any]]:
- """温和蒸发:中等条件、较长时间、整瓶处理"""
- return generate_evaporate_protocol(G, vessel, 0.1, temp, time, 60.0)
-
-
-def generate_high_vacuum_evaporate_protocol(
- G: nx.DiGraph,
- vessel: str,
- temp: float = 35.0,
- time: float = 3600.0 # 1小时
-) -> List[Dict[str, Any]]:
- """高真空蒸发:低温、高真空、长时间、整瓶处理"""
- return generate_evaporate_protocol(G, vessel, 0.01, temp, time, 120.0)
-
-
-def generate_standard_evaporate_protocol(
- G: nx.DiGraph,
- vessel: str
-) -> 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 # 适中旋转速度
- )
diff --git a/unilabos/compile/filter_protocol.py b/unilabos/compile/filter_protocol.py
index 7e3ca6b..d974a41 100644
--- a/unilabos/compile/filter_protocol.py
+++ b/unilabos/compile/filter_protocol.py
@@ -1,304 +1,236 @@
-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
+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():
- 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
-
+def debug_print(message):
+ """调试输出"""
+ print(f"🧪 [FILTER] {message}", flush=True)
+ logger.info(f"[FILTER] {message}")
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']
+ debug_print("🔍 查找过滤器设备... 🌊")
- if filter_nodes:
- return filter_nodes[0]
+ # 查找过滤器设备
+ for node in G.nodes():
+ node_data = G.nodes[node]
+ node_class = node_data.get('class', '') or ''
+
+ if 'filter' in node_class.lower() or 'filter' in node.lower():
+ debug_print(f"🎉 找到过滤器设备: {node} ✨")
+ return node
- raise ValueError("系统中未找到过滤器设备")
+ # 如果没找到,寻找可能的过滤器名称
+ 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} ✨")
+ return name
+
+ debug_print("😭 未找到过滤器设备 💔")
+ raise ValueError("未找到过滤器设备")
-
-def find_filter_vessel(G: nx.DiGraph) -> str:
- """查找过滤器专用容器"""
- possible_names = [
- "filter_vessel", # 标准过滤器容器
- "filtration_vessel", # 备选名称
- "vessel_filter", # 备选名称
- "filter_unit", # 备选名称
- "filter" # 简单名称
- ]
+def validate_vessel(G: nx.DiGraph, vessel: str, vessel_type: str = "容器") -> None:
+ """验证容器是否存在"""
+ debug_print(f"🔍 验证{vessel_type}: '{vessel}' 🧪")
- for vessel_name in possible_names:
- if vessel_name in G.nodes():
- return vessel_name
+ if not vessel:
+ debug_print(f"❌ {vessel_type}不能为空! 😱")
+ raise ValueError(f"{vessel_type}不能为空")
- raise ValueError(f"未找到过滤器容器。尝试了以下名称: {possible_names}")
-
-
-def find_filtrate_vessel(G: nx.DiGraph, filtrate_vessel: str = "") -> str:
- """查找滤液收集容器"""
- if filtrate_vessel and filtrate_vessel in G.nodes():
- return filtrate_vessel
+ if vessel not in G.nodes():
+ debug_print(f"❌ {vessel_type} '{vessel}' 不存在于系统中! 😞")
+ raise ValueError(f"{vessel_type} '{vessel}' 不存在于系统中")
- # 自动查找滤液容器
- possible_names = [
- "filtrate_vessel",
- "collection_bottle_1",
- "collection_bottle_2",
- "waste_workup"
- ]
-
- for vessel_name in possible_names:
- if vessel_name in G.nodes():
- 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(f"✅ {vessel_type} '{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("🌊" * 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 = []
- 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 vessel not in G.nodes():
- raise ValueError(f"源容器 '{vessel}' 不存在于系统中")
+ # 验证必需参数
+ debug_print(" 🔍 验证必需参数...")
+ validate_vessel(G, vessel, "过滤容器")
+ debug_print(" ✅ 必需参数验证完成 🎯")
- # 获取源容器中的液体体积
- source_volume = get_vessel_liquid_volume(G, vessel)
- print(f"FILTER: 源容器 {vessel} 中有 {source_volume} mL 液体")
-
- # 查找过滤器设备
- try:
- filter_id = find_filter_device(G)
- print(f"FILTER: 找到过滤器: {filter_id}")
- except ValueError as e:
- raise ValueError(f"无法找到过滤器: {str(e)}")
-
- # 查找过滤器容器
- try:
- filter_vessel_id = find_filter_vessel(G)
- print(f"FILTER: 找到过滤器容器: {filter_vessel_id}")
- except ValueError as e:
- raise ValueError(f"无法找到过滤器容器: {str(e)}")
-
- # 查找滤液收集容器
- try:
- actual_filtrate_vessel = find_filtrate_vessel(G, filtrate_vessel)
- print(f"FILTER: 找到滤液收集容器: {actual_filtrate_vessel}")
- except ValueError as 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)}")
-
- # === 简化的体积计算策略 ===
- 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")
+ # 验证可选参数
+ debug_print(" 🔍 验证可选参数...")
+ if filtrate_vessel:
+ validate_vessel(G, filtrate_vessel, "滤液容器")
+ debug_print(" 🌊 模式: 过滤并收集滤液 💧")
else:
- transfer_volume = 50.0 # 默认过滤量
- print(f"FILTER: 未检测到液体体积,默认过滤 {transfer_volume} mL")
+ debug_print(" 🧱 模式: 过滤并收集固体 🔬")
+ debug_print(" ✅ 可选参数验证完成 🎯")
- # === 第一步:启动加热搅拌器(在转移前预热) ===
- 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}
- })
+ # === 查找设备 ===
+ debug_print("📍 步骤2: 查找设备... 🔍")
- # === 第二步:将待过滤溶液转移到过滤器 ===
- 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)
+ debug_print(" 🔎 搜索过滤器设备...")
+ filter_device = find_filter_device(G)
+ debug_print(f" 🎉 使用过滤器设备: {filter_device} 🌊✨")
+
except Exception as e:
- raise ValueError(f"无法将溶液转移到过滤器: {str(e)}")
+ debug_print(f" ❌ 设备查找失败: {str(e)} 😭")
+ raise ValueError(f"设备查找失败: {str(e)}")
- # 转移后等待
- action_sequence.append({
- "action_name": "wait",
- "action_kwargs": {"time": 5}
- })
+ # === 转移到过滤器(如果需要)===
+ debug_print("📍 步骤3: 转移到过滤器... 🚚")
- # === 第三步:执行过滤操作(完全按照 Filter.action 参数) ===
- print(f"FILTER: 执行过滤操作")
+ if vessel != filter_device:
+ debug_print(f" 🚛 需要转移: {vessel} → {filter_device} 📦")
+
+ try:
+ debug_print(" 🔄 开始执行转移操作...")
+ # 使用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)} 😞")
+ debug_print(" 🔄 继续执行,可能是直接连接的过滤器 🤞")
+ else:
+ debug_print(" ✅ 过滤容器就是过滤器,无需转移 🎯")
+
+ # === 执行过滤操作 ===
+ debug_print("📍 步骤4: 执行过滤操作... 🌊")
+
+ # 构建过滤动作参数
+ debug_print(" ⚙️ 构建过滤参数...")
+ 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}")
+ debug_print(" 🌊 开始过滤操作...")
+
+ # 过滤动作
filter_action = {
- "device_id": filter_id,
+ "device_id": filter_device,
"action_name": "filter",
- "action_kwargs": {
- "vessel": filter_vessel_id,
- "filtrate_vessel": actual_filtrate_vessel,
- "stir": stir,
- "stir_speed": stir_speed,
- "temp": temp,
- "continue_heatchill": continue_heatchill,
- "volume": transfer_volume
- }
+ "action_kwargs": filter_kwargs
}
action_sequence.append(filter_action)
+ debug_print(" ✅ 过滤动作已添加 🌊✨")
# 过滤后等待
+ debug_print(" ⏳ 添加过滤后等待...")
action_sequence.append({
"action_name": "wait",
- "action_kwargs": {"time": 10}
+ "action_kwargs": {"time": 10.0}
})
+ debug_print(" ✅ 过滤后等待动作已添加 ⏰✨")
- # === 第四步:如果不继续加热搅拌,停止加热器 ===
- if heatchill_id and not continue_heatchill and (temp != 25.0 or stir):
- print(f"FILTER: 停止加热搅拌器")
+ # === 收集滤液(如果需要)===
+ debug_print("📍 步骤5: 收集滤液... 💧")
+
+ if filtrate_vessel:
+ debug_print(f" 🧪 收集滤液: {filter_device} → {filtrate_vessel} 💧")
- stop_action = {
- "device_id": heatchill_id,
- "action_name": "heat_chill_stop",
- "action_kwargs": {
- "vessel": filter_vessel_id
- }
- }
- action_sequence.append(stop_action)
+ try:
+ debug_print(" 🔄 开始执行收集操作...")
+ # 使用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)} 😞")
+ debug_print(" 🔄 继续执行,可能滤液直接流入指定容器 🤞")
+ else:
+ debug_print(" 🧱 未指定滤液容器,固体保留在过滤器中 🔬")
- print(f"FILTER: 生成了 {len(action_sequence)} 个动作")
- print(f"FILTER: 过滤协议生成完成")
+ # === 最终等待 ===
+ debug_print("📍 步骤6: 最终等待... ⏰")
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": 5.0}
+ })
+ debug_print(" ✅ 最终等待动作已添加 ⏰✨")
+
+ # === 总结 ===
+ 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
-
-
-# 便捷函数:常用过滤方案
-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
diff --git a/unilabos/compile/heatchill_protocol.py b/unilabos/compile/heatchill_protocol.py
index 5ce0992..f8bcc11 100644
--- a/unilabos/compile/heatchill_protocol.py
+++ b/unilabos/compile/heatchill_protocol.py
@@ -1,373 +1,376 @@
-from typing import List, Dict, Any, Optional
+from typing import List, Dict, Any, Union
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_time_input(time_input: Union[str, float, int]) -> float:
+ """
+ 解析时间输入(统一函数)
+
+ Args:
+ time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0)
+
+ Returns:
+ float: 时间(秒)
+ """
+ if not time_input:
+ return 300.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}'")
+
+ # ❓ 特殊值处理
+ 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:
+ 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}',使用默认值: 300s")
+ return 300.0
+
+ value = float(match.group(1))
+ 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
+ }
+
+ multiplier = unit_multipliers.get(unit, 1.0)
+ result = value * multiplier
+
+ debug_print(f"✅ 时间解析: '{time_str}' → {value} {unit} → {result}s ({result/60:.1f}分钟)")
+ return result
+
+def parse_temp_input(temp_input: Union[str, float], default_temp: float = 25.0) -> float:
+ """
+ 解析温度输入(统一函数)
+
+ Args:
+ temp_input: 温度输入
+ default_temp: 默认温度
+
+ Returns:
+ float: 温度(°C)
+ """
+ if not temp_input:
+ return default_temp
+
+ # 🔢 数值输入
+ if isinstance(temp_input, (int, float)):
+ result = float(temp_input)
+ debug_print(f"🌡️ 数值温度: {temp_input} → {result}°C")
+ return result
+
+ # 📝 字符串输入
+ temp_str = str(temp_input).lower().strip()
+ debug_print(f"🔍 解析温度: '{temp_str}'")
+
+ # 🎯 特殊温度
+ 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_str in special_temps:
+ result = special_temps[temp_str]
+ debug_print(f"🎯 特殊温度: '{temp_str}' → {result}°C")
+ return result
+
+ # 📐 正则解析(如 "256 °C")
+ temp_pattern = r'(\d+(?:\.\d+)?)\s*°?[cf]?'
+ match = re.search(temp_pattern, temp_str)
+
+ if match:
+ result = float(match.group(1))
+ debug_print(f"✅ 温度解析: '{temp_str}' → {result}°C")
+ return result
+
+ debug_print(f"⚠️ 无法解析温度: '{temp_str}',使用默认值: {default_temp}°C")
+ return default_temp
def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str:
- """
- 查找与指定容器相连的加热/冷却设备
- """
- # 查找所有加热/冷却设备节点
- heatchill_nodes = [node for node in G.nodes()
- if (G.nodes[node].get('class') or '') == 'virtual_heatchill']
+ """查找与指定容器相连的加热/冷却设备"""
+ debug_print(f"🔍 查找加热设备,目标容器: {vessel}")
- # 检查哪个加热/冷却设备与目标容器相连(机械连接)
- for heatchill in heatchill_nodes:
- if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill):
- return 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}")
- # 如果没有直接连接,返回第一个可用的加热/冷却设备
+ # 🔗 检查连接
+ 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}' 相连")
+ return heatchill
+
+ # 🎯 使用第一个可用设备
if heatchill_nodes:
- return heatchill_nodes[0]
+ selected = heatchill_nodes[0]
+ debug_print(f"🔧 使用第一个加热设备: {selected}")
+ return selected
- raise ValueError("系统中未找到可用的加热/冷却设备")
+ # 🆘 默认设备
+ 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,
- time: float,
+ temp: float = 25.0,
+ time: Union[str, float] = "300",
+ 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]]:
"""
- 生成加热/冷却操作的协议序列 - 带时间限制的完整操作
+ 生成加热/冷却操作的协议序列
"""
- 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("🌡️" * 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 参数不能为空")
- # 1. 验证容器存在
if vessel not in G.nodes():
+ debug_print(f"❌ 容器 '{vessel}' 不存在于系统中! 😞")
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
- # 2. 查找加热/冷却设备
+ debug_print("✅ 基础参数验证通过 🎯")
+
+ # 🔄 参数解析
+ debug_print("📍 步骤2: 参数解析... ⚡")
+
+ #温度解析:优先使用 temp_spec
+ final_temp = parse_temp_input(temp_spec, temp) if temp_spec else temp
+
+ # 时间解析:优先使用 time_spec
+ final_time = parse_time_input(time_spec) if time_spec else parse_time_input(time)
+
+ # 参数修正
+ final_temp, final_time, stir_speed = validate_and_fix_params(final_temp, final_time, stir_speed)
+
+ 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)
- 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)}")
- # 3. 执行加热/冷却操作
+ # 🚀 生成动作
+ 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": temp,
- "time": time,
- "stir": stir,
- "stir_speed": stir_speed,
- "status": "start"
+ "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("🎊" * 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)
- print(f"HEATCHILL: 生成了 {len(action_sequence)} 个动作")
return action_sequence
+def generate_heat_chill_to_temp_protocol(
+ G: nx.DiGraph,
+ vessel: str,
+ temp: float = 25.0,
+ time: Union[str, float] = 100.0,
+ **kwargs
+) -> List[Dict[str, Any]]:
+ """生成加热到指定温度的协议(简化版)"""
+ 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,
vessel: str,
- temp: float,
- purpose: str = "开始加热/冷却"
+ temp: float = 25.0,
+ purpose: str = "",
+ **kwargs
) -> List[Dict[str, Any]]:
- """
- 生成开始加热/冷却操作的协议序列
- """
- action_sequence = []
+ """生成开始加热操作的协议序列"""
- print(f"HEATCHILL_START: 开始生成加热/冷却启动协议")
- print(f" - 容器: {vessel}")
- print(f" - 目标温度: {temp}°C")
- print(f" - 目的: {purpose}")
+ debug_print("🔥 开始生成启动加热协议 ✨")
+ debug_print(f"🥽 vessel: {vessel}, 🌡️ temp: {temp}°C")
- # 1. 验证容器存在
- if vessel not in G.nodes():
- raise ValueError(f"容器 '{vessel}' 不存在于系统中")
+ # 基础验证
+ if not vessel or vessel not in G.nodes():
+ debug_print("❌ 容器验证失败!")
+ raise ValueError("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)}")
+ # 查找设备
+ heatchill_id = find_connected_heatchill(G, vessel)
- # 3. 执行开始加热/冷却操作
- heatchill_start_action = {
+ # 生成动作
+ action_sequence = [{
"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)
-
- print(f"HEATCHILL_START: 生成了 {len(action_sequence)} 个动作")
+ debug_print(f"✅ 启动加热协议生成完成 🎯")
return action_sequence
-
def generate_heat_chill_stop_protocol(
G: nx.DiGraph,
- vessel: str
+ vessel: str,
+ **kwargs
) -> List[Dict[str, Any]]:
- """
- 生成停止加热/冷却操作的协议序列
- """
- action_sequence = []
+ """生成停止加热操作的协议序列"""
- print(f"HEATCHILL_STOP: 开始生成加热/冷却停止协议")
- print(f" - 容器: {vessel}")
+ debug_print("🛑 开始生成停止加热协议 ✨")
+ debug_print(f"🥽 vessel: {vessel}")
- # 1. 验证容器存在
- if vessel not in G.nodes():
- raise ValueError(f"容器 '{vessel}' 不存在于系统中")
+ # 基础验证
+ if not vessel or vessel not in G.nodes():
+ debug_print("❌ 容器验证失败!")
+ raise ValueError("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)}")
+ # 查找设备
+ heatchill_id = find_connected_heatchill(G, vessel)
- # 3. 执行停止加热/冷却操作
- heatchill_stop_action = {
+ # 生成动作
+ action_sequence = [{
"device_id": heatchill_id,
"action_name": "heat_chill_stop",
"action_kwargs": {
"vessel": vessel
}
- }
+ }]
- action_sequence.append(heatchill_stop_action)
-
- print(f"HEATCHILL_STOP: 生成了 {len(action_sequence)} 个动作")
+ debug_print(f"✅ 停止加热协议生成完成 🎯")
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}' 不存在于系统中")
-
- # 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)} 个动作")
- 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/hydrogenate_protocol.py b/unilabos/compile/hydrogenate_protocol.py
index 8070705..81cd926 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/pump_protocol.py b/unilabos/compile/pump_protocol.py
index cddb863..a54218e 100644
--- a/unilabos/compile/pump_protocol.py
+++ b/unilabos/compile/pump_protocol.py
@@ -1,255 +1,1853 @@
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
def find_connected_pump(G, valve_node):
- for neighbor in G.neighbors(valve_node):
- node_class = G.nodes[neighbor].get("class") or "" # 防止 None
- 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
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"):
- 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("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_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 # 默认值
- min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone])
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")
- # 生成泵操作的动作序列
+ # 🆕 只在开头打印总体概览
+ 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):
- # 单泵依次执行阀指令、活塞指令,将液体吸入与之相连的第一台泵
- 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]]
+ 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
+ }
}
- },
- {
- "device_id": pumps_from_node[pump_backbone[0]],
- "action_name": "set_position",
- "action_kwargs": {
- "position": float(min(volume_left, min_transfer_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],
- "action_name": "set_valve_position",
- "action_kwargs": {
- "command": G.get_edge_data(nodeA, nodeB)["port"][nodeA]
+ if nodeA in valve_from_node and nodeB in valve_from_node and nodeA in pumps_from_node and nodeB in pumps_from_node:
+ port_a = get_safe_edge_data(nodeA, nodeB, nodeA)
+ port_b = get_safe_edge_data(nodeB, nodeA, nodeB)
+
+ pump_action_sequence.append([
+ {
+ "device_id": valve_from_node[nodeA],
+ "action_name": "set_valve_position",
+ "action_kwargs": {
+ "command": port_a
+ }
+ },
+ {
+ "device_id": valve_from_node[nodeB],
+ "action_name": "set_valve_position",
+ "action_kwargs": {
+ "command": port_b
+ }
}
- },
- {
- "device_id": valve_from_node[nodeB],
- "action_name": "set_valve_position",
- "action_kwargs": {
- "command": G.get_edge_data(nodeB, nodeA)["port"][nodeB],
+ ])
+ pump_action_sequence.append([
+ {
+ "device_id": pumps_from_node[nodeA],
+ "action_name": "set_position",
+ "action_kwargs": {
+ "position": 0.0,
+ "max_velocity": transfer_flowrate
+ }
+ },
+ {
+ "device_id": pumps_from_node[nodeB],
+ "action_name": "set_position",
+ "action_kwargs": {
+ "position": float(current_volume),
+ "max_velocity": transfer_flowrate
+ }
}
- }
- ])
- # 相邻两泵液体转移:泵A排出液体,泵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(min(volume_left, min_transfer_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]],
- "action_name": "set_valve_position",
- "action_kwargs": {
- "command": G.get_edge_data(pump_backbone[-1], to_vessel)["port"][pump_backbone[-1]]
+ # 排液到目标容器
+ if not to_vessel.startswith("pump") and pump_backbone:
+ last_pump_node = pump_backbone[-1]
+ if last_pump_node in valve_from_node and last_pump_node in pumps_from_node:
+ port_command = get_safe_edge_data(last_pump_node, to_vessel, last_pump_node)
+ pump_action_sequence.extend([
+ {
+ "device_id": valve_from_node[last_pump_node],
+ "action_name": "set_valve_position",
+ "action_kwargs": {
+ "command": port_command
+ }
+ },
+ {
+ "device_id": pumps_from_node[last_pump_node],
+ "action_name": "set_position",
+ "action_kwargs": {
+ "position": 0.0,
+ "max_velocity": flowrate
+ }
}
- },
- {
- "device_id": pumps_from_node[pump_backbone[-1]],
- "action_name": "set_position",
- "action_kwargs": {
- "position": 0.0,
- "max_velocity": flowrate
- }
- }
- ])
- pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
+ ])
+ pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}})
- volume_left -= min_transfer_volume
+ # 🆕 在每次循环结束时添加完成日志
+ 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
-# 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 = "",
+ 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]]:
"""
- 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"
+
+ # 添加执行锁,防止并发调用
+ 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)}"
+ }
+ }
+ ]
- shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
- pump_backbone = shortest_path[1: -1]
+
+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
- pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone)
+ # 🔧 关键修复:正确构建泵骨架,排除容器和电磁阀
+ 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}")
- 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
+ if not pump_backbone:
+ debug_print("PUMP_TRANSFER: 没有泵骨架节点,可能是直接容器连接或只有电磁阀")
+ return pump_action_sequence
- pump_action_sequence = generate_pump_protocol(G, from_vessel, to_vessel, float(volume), flowrate, transfer_flowrate)
- if rinsing_solvent != "air" and rinsing_solvent != "":
+ 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,
+ 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}次")
+
+ # ========== 执行基础转移 ==========
+
+ 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,
+ 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")
+
+ 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)}"
+ }
+ }
+ ]
+
+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
+
+ 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)
- )
- # 如果转移的是溶液,第一种冲洗溶剂请选用溶液的溶剂,稀释泵内、转移管道内的溶液。后续冲洗溶剂不需要此操作。
- 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)
+ for solvent in rinsing_solvents:
+ solvent_vessel = f"flask_{solvent.strip()}"
- return pump_action_sequence
-# End Protocols
+ # 检查溶剂容器是否存在
+ 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 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/recrystallize_protocol.py b/unilabos/compile/recrystallize_protocol.py
index b69d88b..569a798 100644
--- a/unilabos/compile/recrystallize_protocol.py
+++ b/unilabos/compile/recrystallize_protocol.py
@@ -1,8 +1,89 @@
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 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:
+ """
+ 解析带单位的体积输入
+
+ Args:
+ volume_input: 体积输入(如 "100 mL", "2.5 L", "500", "?", 100.0)
+ default_unit: 默认单位(默认为毫升)
+
+ Returns:
+ float: 体积(毫升)
+ """
+ if not volume_input:
+ debug_print("⚠️ 体积输入为空,返回 0.0mL 📦")
+ 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
+ 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 💧")
+
+ debug_print(f"✅ 体积解析完成: '{volume_str}' → {volume}mL ✨")
+ return volume
+
+
def parse_ratio(ratio_str: str) -> Tuple[float, float]:
"""
解析比例字符串,支持多种格式
@@ -13,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:
@@ -20,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" 等格式
@@ -28,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" 等格式
@@ -36,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
@@ -58,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 = [
@@ -72,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', {})
@@ -99,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}' 对应的容器")
@@ -111,11 +203,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,72 +215,95 @@ 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" - 比例: {ratio}")
- print(f" - 溶剂1: {solvent1}")
- print(f" - 溶剂2: {solvent2}")
- print(f" - 容器: {vessel}")
- print(f" - 总体积: {volume} mL")
+ 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. 解析比例
+ # 2. 🔧 新增:解析体积(支持单位)
+ debug_print("📍 步骤2: 解析体积(支持单位)... 💧")
+ final_volume = parse_volume_with_units(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}) ✨")
- # 3. 计算各溶剂体积
- volume1 = volume * (ratio1 / total_ratio)
- volume2 = volume * (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 🎯")
- # 4. 查找溶剂容器
+ # 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)}")
- # 5. 验证路径存在
+ # 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}' 没有可用路径")
- # 6. 添加第一种溶剂
- print(f"RECRYSTALLIZE: 开始添加溶剂1 {volume1:.2f} mL")
+ # 7. 添加第一种溶剂
+ debug_print("📍 步骤7: 添加第一种溶剂... 🧪")
+ debug_print(f" 🚰 开始添加溶剂1: {solvent1} ({volume1:.2f} mL)")
try:
pump_actions1 = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=solvent1_vessel,
to_vessel=vessel,
- volume=volume1,
+ volume=volume1, # 使用解析后的体积
amount="",
time=0.0,
viscous=False,
@@ -201,28 +316,33 @@ 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)}")
- # 7. 等待溶剂1稳定
+ # 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稳定等待已添加 ⏰✨")
- # 8. 添加第二种溶剂
- print(f"RECRYSTALLIZE: 开始添加溶剂2 {volume2:.2f} mL")
+ # 9. 添加第二种溶剂
+ debug_print("📍 步骤8: 添加第二种溶剂... 🧪")
+ debug_print(f" 🚰 开始添加溶剂2: {solvent2} ({volume2:.2f} mL)")
try:
pump_actions2 = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=solvent2_vessel,
to_vessel=vessel,
- volume=volume2,
+ volume=volume2, # 使用解析后的体积
amount="",
time=0.0,
viscous=False,
@@ -235,30 +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)}")
- # 9. 等待溶剂2稳定
+ # 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 保持不变 🎯")
- # 10. 等待重结晶完成
action_sequence.append({
"action_name": "wait",
"action_kwargs": {
- "time": 600.0, # 等待10分钟进行重结晶
- "description": f"等待重结晶完成({solvent1}:{solvent2} = {ratio})"
+ "time": final_crystallize_time,
+ "description": f"等待重结晶完成({solvent1}:{solvent2} = {ratio},总体积 {final_volume}mL)" + (f" (模拟时间)" if original_crystallize_time != final_crystallize_time else "")
}
})
+ debug_print(f" ✅ 重结晶等待已添加: {final_crystallize_time}s 💎✨")
- print(f"RECRYSTALLIZE: 协议生成完成,共 {len(action_sequence)} 个动作")
- print(f"RECRYSTALLIZE: 预计总时间: {620/60:.1f} 分钟")
+ # 显示时间调整信息
+ 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
@@ -266,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 0fa55c2..2e51da3 100644
--- a/unilabos/compile/reset_handling_protocol.py
+++ b/unilabos/compile/reset_handling_protocol.py
@@ -3,6 +3,11 @@ from typing import List, Dict, Any
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']}")
- raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器。尝试了: {possible_names}")
+ if len(available_containers) > 5:
+ debug_print(f" ... 还有 {len(available_containers)-5} 个容器 📦")
+
+ 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 "")
}
})
+ debug_print(f" ✅ 稳定等待已添加: {final_wait_time}s ⏰✨")
- print(f"RESET_HANDLING: 协议生成完成,共 {len(action_sequence)} 个动作")
- print(f"RESET_HANDLING: 已添加 {volume} mL {solvent} 到 {target_vessel}")
+ # 显示时间调整信息
+ 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 f6b9214..cb55f86 100644
--- a/unilabos/compile/run_column_protocol.py
+++ b/unilabos/compile/run_column_protocol.py
@@ -1,312 +1,646 @@
-from typing import List, Dict, Any
+from typing import List, Dict, Any, Union
import networkx as nx
-from .pump_protocol import generate_pump_protocol
+import logging
+import re
+from .pump_protocol import generate_pump_protocol_with_rinsing
+logger = logging.getLogger(__name__)
-def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:
- """获取容器中的液体体积"""
- if vessel not in G.nodes():
+def debug_print(message):
+ """调试输出"""
+ print(f"[RUN_COLUMN] {message}", flush=True)
+ logger.info(f"[RUN_COLUMN] {message}")
+
+def parse_percentage(pct_str: str) -> float:
+ """
+ 解析百分比字符串为数值
+
+ Args:
+ pct_str: 百分比字符串(如 "40 %", "40%", "40")
+
+ Returns:
+ float: 百分比数值(0-100)
+ """
+ if not pct_str or not pct_str.strip():
return 0.0
- vessel_data = G.nodes[vessel].get('data', {})
- liquids = vessel_data.get('liquid', [])
+ pct_str = pct_str.strip().lower()
+ debug_print(f"解析百分比: '{pct_str}'")
- total_volume = 0.0
- for liquid in liquids:
- if isinstance(liquid, dict):
- # 支持两种格式:新格式 (name, volume) 和旧格式 (liquid_type, liquid_volume)
- volume = liquid.get('volume') or liquid.get('liquid_volume', 0.0)
- total_volume += volume
+ # 移除百分号和空格
+ pct_clean = re.sub(r'[%\s]', '', pct_str)
- return total_volume
+ # 提取数字
+ match = re.search(r'([0-9]*\.?[0-9]+)', pct_clean)
+ if match:
+ value = float(match.group(1))
+ debug_print(f"百分比解析结果: {value}%")
+ return value
+
+ debug_print(f"⚠️ 无法解析百分比: '{pct_str}',返回0.0")
+ return 0.0
+def parse_ratio(ratio_str: str) -> tuple:
+ """
+ 解析比例字符串为两个数值
+
+ Args:
+ ratio_str: 比例字符串(如 "5:95", "1:1", "40:60")
+
+ Returns:
+ tuple: (ratio1, ratio2) 两个比例值
+ """
+ if not ratio_str or not ratio_str.strip():
+ return (50.0, 50.0) # 默认1:1
+
+ ratio_str = ratio_str.strip()
+ debug_print(f"解析比例: '{ratio_str}'")
+
+ # 支持多种分隔符:: / -
+ if ':' in ratio_str:
+ parts = ratio_str.split(':')
+ elif '/' in ratio_str:
+ parts = ratio_str.split('/')
+ elif '-' in ratio_str:
+ parts = ratio_str.split('-')
+ elif 'to' in ratio_str.lower():
+ parts = ratio_str.lower().split('to')
+ else:
+ debug_print(f"⚠️ 无法解析比例格式: '{ratio_str}',使用默认1:1")
+ return (50.0, 50.0)
+
+ if len(parts) >= 2:
+ try:
+ ratio1 = float(parts[0].strip())
+ ratio2 = float(parts[1].strip())
+ total = ratio1 + ratio2
+
+ # 转换为百分比
+ pct1 = (ratio1 / total) * 100
+ pct2 = (ratio2 / total) * 100
+
+ debug_print(f"比例解析结果: {ratio1}:{ratio2} -> {pct1:.1f}%:{pct2:.1f}%")
+ return (pct1, pct2)
+ except ValueError as e:
+ debug_print(f"⚠️ 比例数值转换失败: {str(e)}")
+
+ debug_print(f"⚠️ 比例解析失败,使用默认1:1")
+ return (50.0, 50.0)
-def find_column_device(G: nx.DiGraph, column: str) -> str:
+def parse_rf_value(rf_str: str) -> float:
+ """
+ 解析Rf值字符串
+
+ Args:
+ rf_str: Rf值字符串(如 "0.3", "0.45", "?")
+
+ Returns:
+ float: Rf值(0-1)
+ """
+ if not rf_str or not rf_str.strip():
+ return 0.3 # 默认Rf值
+
+ rf_str = rf_str.strip().lower()
+ debug_print(f"解析Rf值: '{rf_str}'")
+
+ # 处理未知Rf值
+ if rf_str in ['?', 'unknown', 'tbd', 'to be determined']:
+ default_rf = 0.3
+ debug_print(f"检测到未知Rf值,使用默认值: {default_rf}")
+ return default_rf
+
+ # 提取数字
+ match = re.search(r'([0-9]*\.?[0-9]+)', rf_str)
+ if match:
+ value = float(match.group(1))
+ # 确保Rf值在0-1范围内
+ if value > 1.0:
+ value = value / 100.0 # 可能是百分比形式
+ value = max(0.0, min(1.0, value)) # 限制在0-1范围
+ debug_print(f"Rf值解析结果: {value}")
+ return value
+
+ debug_print(f"⚠️ 无法解析Rf值: '{rf_str}',使用默认值0.3")
+ return 0.3
+
+def find_column_device(G: nx.DiGraph) -> str:
"""查找柱层析设备"""
- # 首先检查是否有虚拟柱设备
- column_nodes = [node for node in G.nodes()
- if (G.nodes[node].get('class') or '') == 'virtual_column']
+ debug_print("查找柱层析设备...")
- if column_nodes:
- return column_nodes[0]
+ # 查找虚拟柱设备
+ for node in G.nodes():
+ node_data = G.nodes[node]
+ node_class = node_data.get('class', '') or ''
+
+ if 'virtual_column' in node_class.lower() or 'column' in node_class.lower():
+ debug_print(f"✅ 找到柱层析设备: {node}")
+ return node
- # 如果没有虚拟柱设备,抛出异常
- raise ValueError(f"系统中未找到柱层析设备。请确保配置了 virtual_column 设备")
-
+ # 如果没有找到,尝试创建虚拟设备名称
+ possible_names = ['column_1', 'virtual_column_1', 'chromatography_column_1']
+ for name in possible_names:
+ if name in G.nodes():
+ debug_print(f"✅ 找到柱设备: {name}")
+ return name
+
+ debug_print("⚠️ 未找到柱层析设备,将使用pump protocol直接转移")
+ return ""
def find_column_vessel(G: nx.DiGraph, column: str) -> str:
"""查找柱容器"""
- # 直接使用 column 参数作为容器名称
- if column in G.nodes():
- return column
+ debug_print(f"查找柱容器: '{column}'")
- # 尝试常见的柱容器命名规则
+ # 直接检查column参数是否是容器
+ if column in G.nodes():
+ node_type = G.nodes[column].get('type', '')
+ if node_type == 'container':
+ debug_print(f"✅ 找到柱容器: {column}")
+ return column
+
+ # 尝试常见的命名规则
possible_names = [
f"column_{column}",
- f"{column}_column",
+ f"{column}_column",
f"vessel_{column}",
f"{column}_vessel",
"column_vessel",
"chromatography_column",
"silica_column",
- "preparative_column"
+ "preparative_column",
+ "column"
]
for vessel_name in possible_names:
if vessel_name in G.nodes():
- return vessel_name
+ node_type = G.nodes[vessel_name].get('type', '')
+ if node_type == 'container':
+ debug_print(f"✅ 找到柱容器: {vessel_name}")
+ return vessel_name
- raise ValueError(f"未找到柱容器 '{column}'。尝试了以下名称: {[column] + possible_names}")
+ debug_print(f"⚠️ 未找到柱容器,将直接在源容器中进行分离")
+ return ""
-
-def find_eluting_solvent_vessel(G: nx.DiGraph, eluting_solvent: str) -> str:
- """查找洗脱溶剂容器"""
- if not eluting_solvent:
+def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
+ """查找溶剂容器 - 增强版"""
+ if not solvent or not solvent.strip():
return ""
- # 按照命名规则查找溶剂瓶
- solvent_vessel_id = f"flask_{eluting_solvent}"
+ solvent = solvent.strip().replace(' ', '_').lower()
+ debug_print(f"查找溶剂容器: '{solvent}'")
- if solvent_vessel_id in G.nodes():
- return solvent_vessel_id
-
- # 如果直接匹配失败,尝试模糊匹配
+ # 🔧 方法1:直接搜索 data.reagent_name
for node in G.nodes():
- if node.startswith('flask_') and eluting_solvent.lower() in node.lower():
- return node
+ node_data = G.nodes[node].get('data', {})
+ node_type = G.nodes[node].get('type', '')
+
+ # 只搜索容器类型的节点
+ if node_type == 'container':
+ reagent_name = node_data.get('reagent_name', '').lower()
+ reagent_config = G.nodes[node].get('config', {}).get('reagent', '').lower()
+
+ # 检查 data.reagent_name 和 config.reagent
+ if reagent_name == solvent or reagent_config == solvent:
+ debug_print(f"✅ 通过reagent_name找到溶剂容器: {node} (reagent: {reagent_name or reagent_config})")
+ return node
+
+ # 模糊匹配 reagent_name
+ if solvent in reagent_name or reagent_name in solvent:
+ debug_print(f"✅ 通过reagent_name模糊匹配到溶剂容器: {node} (reagent: {reagent_name})")
+ return node
+
+ if solvent in reagent_config or reagent_config in solvent:
+ debug_print(f"✅ 通过config.reagent模糊匹配到溶剂容器: {node} (reagent: {reagent_config})")
+ return node
- # 如果还是找不到,列出所有可用的溶剂瓶
- available_flasks = [node for node in G.nodes()
- if node.startswith('flask_')
- and G.nodes[node].get('type') == 'container']
+ # 🔧 方法2:常见的溶剂容器命名规则
+ possible_names = [
+ f"flask_{solvent}",
+ f"bottle_{solvent}",
+ f"reagent_{solvent}",
+ f"{solvent}_bottle",
+ f"{solvent}_flask",
+ f"solvent_{solvent}",
+ f"reagent_bottle_{solvent}"
+ ]
- raise ValueError(f"找不到洗脱溶剂 '{eluting_solvent}' 对应的溶剂瓶。可用溶剂瓶: {available_flasks}")
+ for vessel_name in possible_names:
+ if vessel_name in G.nodes():
+ node_type = G.nodes[vessel_name].get('type', '')
+ if node_type == 'container':
+ debug_print(f"✅ 通过命名规则找到溶剂容器: {vessel_name}")
+ return vessel_name
+
+ # 🔧 方法3:节点名称模糊匹配
+ for node in G.nodes():
+ node_type = G.nodes[node].get('type', '')
+ if node_type == 'container':
+ if ('flask_' in node or 'bottle_' in node or 'reagent_' in node) and solvent in node.lower():
+ debug_print(f"✅ 通过节点名称模糊匹配到溶剂容器: {node}")
+ return node
+
+ # 🔧 方法4:特殊溶剂名称映射
+ solvent_mapping = {
+ 'dmf': ['dmf', 'dimethylformamide', 'n,n-dimethylformamide'],
+ 'ethyl_acetate': ['ethyl_acetate', 'ethylacetate', 'etoac', 'ea'],
+ 'hexane': ['hexane', 'hexanes', 'n-hexane'],
+ 'methanol': ['methanol', 'meoh', 'ch3oh'],
+ 'water': ['water', 'h2o', 'distilled_water'],
+ 'acetone': ['acetone', 'ch3coch3', '2-propanone'],
+ 'dichloromethane': ['dichloromethane', 'dcm', 'ch2cl2', 'methylene_chloride'],
+ 'chloroform': ['chloroform', 'chcl3', 'trichloromethane']
+ }
+
+ # 查找映射的同义词
+ for canonical_name, synonyms in solvent_mapping.items():
+ if solvent in synonyms:
+ debug_print(f"检测到溶剂同义词: '{solvent}' -> '{canonical_name}'")
+ return find_solvent_vessel(G, canonical_name) # 递归搜索
+
+ debug_print(f"⚠️ 未找到溶剂 '{solvent}' 的容器")
+ return ""
+def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:
+ """获取容器中的液体体积 - 增强版"""
+ if vessel not in G.nodes():
+ debug_print(f"⚠️ 节点 '{vessel}' 不存在")
+ return 0.0
+
+ node_type = G.nodes[vessel].get('type', '')
+ vessel_data = G.nodes[vessel].get('data', {})
+
+ debug_print(f"读取节点 '{vessel}' (类型: {node_type}) 体积数据: {vessel_data}")
+
+ # 🔧 如果是设备类型,尝试查找关联的容器
+ if node_type == 'device':
+ debug_print(f"'{vessel}' 是设备,尝试查找关联容器...")
+
+ # 查找是否有内置容器数据
+ config_data = G.nodes[vessel].get('config', {})
+ if 'volume' in config_data:
+ default_volume = config_data.get('volume', 50.0)
+ debug_print(f"使用设备默认容量: {default_volume}mL")
+ return default_volume
+
+ # 对于旋蒸等设备,使用默认值
+ if 'rotavap' in vessel.lower():
+ default_volume = 50.0
+ debug_print(f"旋蒸设备使用默认容量: {default_volume}mL")
+ return default_volume
+
+ debug_print(f"⚠️ 设备 '{vessel}' 无法确定容量,返回0")
+ return 0.0
+
+ # 🔧 如果是容器类型,正常读取体积
+ total_volume = 0.0
+
+ # 方法1:检查液体列表
+ liquids = vessel_data.get('liquid', [])
+ if isinstance(liquids, list):
+ for liquid in liquids:
+ if isinstance(liquid, dict):
+ volume = liquid.get('volume') or liquid.get('liquid_volume', 0.0)
+ total_volume += volume
+
+ # 方法2:检查直接体积字段
+ if total_volume == 0.0:
+ volume_keys = ['current_volume', 'total_volume', 'volume', 'liquid_volume']
+ for key in volume_keys:
+ if key in vessel_data:
+ try:
+ total_volume = float(vessel_data[key])
+ if total_volume > 0:
+ break
+ except (ValueError, TypeError):
+ continue
+
+ # 方法3:检查配置中的初始体积
+ if total_volume == 0.0:
+ config_data = G.nodes[vessel].get('config', {})
+ if 'current_volume' in config_data:
+ try:
+ total_volume = float(config_data['current_volume'])
+ except (ValueError, TypeError):
+ pass
+
+ debug_print(f"容器 '{vessel}' 总体积: {total_volume}mL")
+ return total_volume
+
+def calculate_solvent_volumes(total_volume: float, pct1: float, pct2: float) -> tuple:
+ """根据百分比计算溶剂体积"""
+ volume1 = (total_volume * pct1) / 100.0
+ volume2 = (total_volume * pct2) / 100.0
+
+ debug_print(f"溶剂体积计算: 总体积{total_volume}mL")
+ debug_print(f" - 溶剂1: {pct1}% = {volume1}mL")
+ debug_print(f" - 溶剂2: {pct2}% = {volume2}mL")
+
+ return (volume1, volume2)
def generate_run_column_protocol(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
- column: str
+ column: str,
+ rf: str = "",
+ pct1: str = "",
+ pct2: str = "",
+ solvent1: str = "",
+ solvent2: str = "",
+ ratio: str = "",
+ **kwargs
) -> List[Dict[str, Any]]:
"""
- 生成柱层析分离的协议序列
+ 生成柱层析分离的协议序列 - 增强版
+
+ 支持新版XDL的所有参数,具有高兼容性和容错性
Args:
G: 有向图,节点为设备和容器,边为流体管道
- from_vessel: 源容器的名称,即样品起始所在的容器
- to_vessel: 目标容器的名称,分离后的样品要到达的容器
- column: 所使用的柱子的名称
+ from_vessel: 源容器的名称,即样品起始所在的容器(必需)
+ to_vessel: 目标容器的名称,分离后的样品要到达的容器(必需)
+ column: 所使用的柱子的名称(必需)
+ rf: Rf值(可选,支持 "?" 表示未知)
+ pct1: 第一种溶剂百分比(如 "40 %",可选)
+ pct2: 第二种溶剂百分比(如 "50 %",可选)
+ solvent1: 第一种溶剂名称(可选)
+ solvent2: 第二种溶剂名称(可选)
+ ratio: 溶剂比例(如 "5:95",可选,优先级高于pct1/pct2)
+ **kwargs: 其他可选参数
Returns:
List[Dict[str, Any]]: 柱层析分离操作的动作序列
"""
+
+ debug_print("=" * 60)
+ debug_print("开始生成柱层析协议")
+ debug_print(f"输入参数:")
+ debug_print(f" - from_vessel: '{from_vessel}'")
+ debug_print(f" - to_vessel: '{to_vessel}'")
+ debug_print(f" - column: '{column}'")
+ debug_print(f" - rf: '{rf}'")
+ debug_print(f" - pct1: '{pct1}'")
+ debug_print(f" - pct2: '{pct2}'")
+ debug_print(f" - solvent1: '{solvent1}'")
+ debug_print(f" - solvent2: '{solvent2}'")
+ debug_print(f" - ratio: '{ratio}'")
+ debug_print(f" - 其他参数: {kwargs}")
+ debug_print("=" * 60)
+
action_sequence = []
- print(f"RUN_COLUMN: 开始生成柱层析协议")
- print(f" - 源容器: {from_vessel}")
- print(f" - 目标容器: {to_vessel}")
- print(f" - 柱子: {column}")
+ # === 参数验证 ===
+ debug_print("步骤1: 参数验证...")
+
+ if not from_vessel:
+ raise ValueError("from_vessel 参数不能为空")
+ if not to_vessel:
+ raise ValueError("to_vessel 参数不能为空")
+ if not column:
+ raise ValueError("column 参数不能为空")
- # 验证源容器和目标容器存在
if from_vessel not in G.nodes():
raise ValueError(f"源容器 '{from_vessel}' 不存在于系统中")
-
if to_vessel not in G.nodes():
raise ValueError(f"目标容器 '{to_vessel}' 不存在于系统中")
- # 查找柱层析设备
- column_device_id = None
- column_nodes = [node for node in G.nodes()
- if (G.nodes[node].get('class') or '') == 'virtual_column']
+ debug_print("✅ 基本参数验证通过")
- if column_nodes:
- column_device_id = column_nodes[0]
- print(f"RUN_COLUMN: 找到柱层析设备: {column_device_id}")
+ # === 参数解析 ===
+ debug_print("步骤2: 参数解析...")
+
+ # 解析Rf值
+ final_rf = parse_rf_value(rf)
+ debug_print(f"最终Rf值: {final_rf}")
+
+ # 解析溶剂比例(ratio优先级高于pct1/pct2)
+ if ratio and ratio.strip():
+ final_pct1, final_pct2 = parse_ratio(ratio)
+ debug_print(f"使用ratio参数: {final_pct1:.1f}% : {final_pct2:.1f}%")
else:
- print(f"RUN_COLUMN: 警告 - 未找到柱层析设备")
+ final_pct1 = parse_percentage(pct1) if pct1 else 50.0
+ final_pct2 = parse_percentage(pct2) if pct2 else 50.0
+
+ # 如果百分比和不是100%,进行归一化
+ total_pct = final_pct1 + final_pct2
+ if total_pct == 0:
+ final_pct1, final_pct2 = 50.0, 50.0
+ elif total_pct != 100.0:
+ final_pct1 = (final_pct1 / total_pct) * 100
+ final_pct2 = (final_pct2 / total_pct) * 100
+
+ debug_print(f"使用百分比参数: {final_pct1:.1f}% : {final_pct2:.1f}%")
+
+ # 设置默认溶剂(如果未指定)
+ final_solvent1 = solvent1.strip() if solvent1 else "ethyl_acetate"
+ final_solvent2 = solvent2.strip() if solvent2 else "hexane"
+
+ debug_print(f"最终溶剂: {final_solvent1} : {final_solvent2}")
+
+ # === 查找设备和容器 ===
+ debug_print("步骤3: 查找设备和容器...")
+
+ # 查找柱层析设备
+ column_device_id = find_column_device(G)
+
+ # 查找柱容器
+ column_vessel = find_column_vessel(G, column)
+
+ # 查找溶剂容器
+ solvent1_vessel = find_solvent_vessel(G, final_solvent1)
+ solvent2_vessel = find_solvent_vessel(G, final_solvent2)
+
+ debug_print(f"设备映射:")
+ debug_print(f" - 柱设备: '{column_device_id}'")
+ debug_print(f" - 柱容器: '{column_vessel}'")
+ debug_print(f" - 溶剂1容器: '{solvent1_vessel}'")
+ debug_print(f" - 溶剂2容器: '{solvent2_vessel}'")
+
+ # === 获取源容器体积 ===
+ debug_print("步骤4: 获取源容器体积...")
- # 获取源容器中的液体体积
source_volume = get_vessel_liquid_volume(G, from_vessel)
- print(f"RUN_COLUMN: 源容器 {from_vessel} 中有 {source_volume} mL 液体")
+ if source_volume <= 0:
+ source_volume = 50.0 # 默认体积
+ debug_print(f"⚠️ 无法获取源容器体积,使用默认值: {source_volume}mL")
+ else:
+ debug_print(f"✅ 源容器体积: {source_volume}mL")
- # === 第一步:样品转移到柱子(如果柱子是容器) ===
- if column in G.nodes() and G.nodes[column].get('type') == 'container':
- print(f"RUN_COLUMN: 样品转移 - {source_volume} mL 从 {from_vessel} 到 {column}")
-
- try:
- sample_transfer_actions = generate_pump_protocol(
- G=G,
- from_vessel=from_vessel,
- to_vessel=column,
- volume=source_volume if source_volume > 0 else 100.0,
- flowrate=2.0
- )
- action_sequence.extend(sample_transfer_actions)
- except Exception as e:
- print(f"RUN_COLUMN: 样品转移失败: {str(e)}")
+ # === 计算溶剂体积 ===
+ debug_print("步骤5: 计算溶剂体积...")
- # === 第二步:使用柱层析设备执行分离 ===
- if column_device_id:
- print(f"RUN_COLUMN: 使用柱层析设备执行分离")
+ # 洗脱溶剂通常是样品体积的2-5倍
+ total_elution_volume = source_volume * 3.0
+ solvent1_volume, solvent2_volume = calculate_solvent_volumes(
+ total_elution_volume, final_pct1, final_pct2
+ )
+
+ # === 执行柱层析流程 ===
+ debug_print("步骤6: 执行柱层析流程...")
+
+ try:
+ # 步骤6.1: 样品上柱(如果有独立的柱容器)
+ if column_vessel and column_vessel != from_vessel:
+ debug_print(f"6.1: 样品上柱 - {source_volume}mL 从 {from_vessel} 到 {column_vessel}")
+
+ try:
+ sample_transfer_actions = generate_pump_protocol_with_rinsing(
+ G=G,
+ from_vessel=from_vessel,
+ to_vessel=column_vessel,
+ volume=source_volume,
+ flowrate=1.0, # 慢速上柱
+ transfer_flowrate=0.5,
+ rinsing_solvent="", # 暂不冲洗
+ rinsing_volume=0.0,
+ rinsing_repeats=0
+ )
+ action_sequence.extend(sample_transfer_actions)
+ debug_print(f"✅ 样品上柱完成,添加了 {len(sample_transfer_actions)} 个动作")
+ except Exception as e:
+ debug_print(f"⚠️ 样品上柱失败: {str(e)}")
- column_separation_action = {
- "device_id": column_device_id,
- "action_name": "run_column",
- "action_kwargs": {
- "from_vessel": from_vessel,
- "to_vessel": to_vessel,
- "column": column
+ # 步骤6.2: 添加洗脱溶剂1(如果有溶剂容器)
+ if solvent1_vessel and solvent1_volume > 0:
+ debug_print(f"6.2: 添加洗脱溶剂1 - {solvent1_volume:.1f}mL {final_solvent1}")
+
+ try:
+ target_vessel = column_vessel if column_vessel else from_vessel
+ solvent1_transfer_actions = generate_pump_protocol_with_rinsing(
+ G=G,
+ from_vessel=solvent1_vessel,
+ to_vessel=target_vessel,
+ volume=solvent1_volume,
+ flowrate=2.0,
+ transfer_flowrate=1.0
+ )
+ action_sequence.extend(solvent1_transfer_actions)
+ debug_print(f"✅ 溶剂1添加完成,添加了 {len(solvent1_transfer_actions)} 个动作")
+ except Exception as e:
+ debug_print(f"⚠️ 溶剂1添加失败: {str(e)}")
+
+ # 步骤6.3: 添加洗脱溶剂2(如果有溶剂容器)
+ if solvent2_vessel and solvent2_volume > 0:
+ debug_print(f"6.3: 添加洗脱溶剂2 - {solvent2_volume:.1f}mL {final_solvent2}")
+
+ try:
+ target_vessel = column_vessel if column_vessel else from_vessel
+ solvent2_transfer_actions = generate_pump_protocol_with_rinsing(
+ G=G,
+ from_vessel=solvent2_vessel,
+ to_vessel=target_vessel,
+ volume=solvent2_volume,
+ flowrate=2.0,
+ transfer_flowrate=1.0
+ )
+ action_sequence.extend(solvent2_transfer_actions)
+ debug_print(f"✅ 溶剂2添加完成,添加了 {len(solvent2_transfer_actions)} 个动作")
+ except Exception as e:
+ debug_print(f"⚠️ 溶剂2添加失败: {str(e)}")
+
+ # 步骤6.4: 使用柱层析设备执行分离(如果有设备)
+ if column_device_id:
+ debug_print(f"6.4: 使用柱层析设备执行分离")
+
+ column_separation_action = {
+ "device_id": column_device_id,
+ "action_name": "run_column",
+ "action_kwargs": {
+ "from_vessel": from_vessel,
+ "to_vessel": to_vessel,
+ "column": column,
+ "rf": rf,
+ "pct1": pct1,
+ "pct2": pct2,
+ "solvent1": solvent1,
+ "solvent2": solvent2,
+ "ratio": ratio
+ }
}
- }
- action_sequence.append(column_separation_action)
-
- # 等待柱层析设备完成分离
- action_sequence.append({
- "action_name": "wait",
- "action_kwargs": {"time": 60}
- })
-
- # === 第三步:从柱子转移到目标容器(如果需要) ===
- if column in G.nodes() and column != to_vessel:
- print(f"RUN_COLUMN: 产物转移 - 从 {column} 到 {to_vessel}")
-
- try:
- product_transfer_actions = generate_pump_protocol(
- G=G,
- from_vessel=column,
- to_vessel=to_vessel,
- volume=source_volume * 0.8 if source_volume > 0 else 80.0, # 假设有一些损失
- flowrate=1.5
- )
- action_sequence.extend(product_transfer_actions)
- except Exception as e:
- print(f"RUN_COLUMN: 产物转移失败: {str(e)}")
-
- print(f"RUN_COLUMN: 生成了 {len(action_sequence)} 个动作")
- return action_sequence
-
-
-# 便捷函数:常用柱层析方案
-def generate_flash_column_protocol(
- G: nx.DiGraph,
- from_vessel: str,
- to_vessel: str,
- column_material: str = "silica_gel",
- mobile_phase: str = "ethyl_acetate",
- mobile_phase_volume: float = 100.0
-) -> List[Dict[str, Any]]:
- """快速柱层析:高流速分离"""
- return generate_run_column_protocol(
- G, from_vessel, to_vessel, column_material,
- mobile_phase, mobile_phase_volume, 1, "", 0.0, 3.0
- )
-
-
-def generate_preparative_column_protocol(
- G: nx.DiGraph,
- from_vessel: str,
- to_vessel: str,
- column_material: str = "silica_gel",
- equilibration_solvent: str = "hexane",
- eluting_solvent: str = "ethyl_acetate",
- eluting_volume: float = 50.0,
- eluting_repeats: int = 3
-) -> List[Dict[str, Any]]:
- """制备柱层析:带平衡和多次洗脱"""
- return generate_run_column_protocol(
- G, from_vessel, to_vessel, column_material,
- eluting_solvent, eluting_volume, eluting_repeats,
- equilibration_solvent, 30.0, 1.5
- )
-
-
-def generate_gradient_column_protocol(
- G: nx.DiGraph,
- from_vessel: str,
- to_vessel: str,
- column_material: str = "silica_gel",
- gradient_solvents: List[str] = None,
- gradient_volumes: List[float] = None
-) -> List[Dict[str, Any]]:
- """梯度洗脱柱层析:多种溶剂系统"""
- if gradient_solvents is None:
- gradient_solvents = ["hexane", "ethyl_acetate", "methanol"]
- if gradient_volumes is None:
- gradient_volumes = [50.0, 50.0, 30.0]
-
- action_sequence = []
-
- # 每种溶剂单独执行一次柱层析
- for i, (solvent, volume) in enumerate(zip(gradient_solvents, gradient_volumes)):
- print(f"RUN_COLUMN: 梯度洗脱第 {i+1}/{len(gradient_solvents)} 步: {volume} mL {solvent}")
-
- # 第一步使用源容器,后续步骤使用柱子作为源
- step_from_vessel = from_vessel if i == 0 else column_material
- # 最后一步使用目标容器,其他步骤使用柱子作为目标
- step_to_vessel = to_vessel if i == len(gradient_solvents) - 1 else column_material
-
- step_actions = generate_run_column_protocol(
- G, step_from_vessel, step_to_vessel, column_material,
- solvent, volume, 1, "", 0.0, 1.0
- )
- action_sequence.extend(step_actions)
-
- # 在梯度步骤之间加入等待时间
- if i < len(gradient_solvents) - 1:
+ action_sequence.append(column_separation_action)
+ debug_print(f"✅ 柱层析设备动作已添加")
+
+ # 等待分离完成
+ separation_time = max(30, int(total_elution_volume / 2)) # 基于体积估算时间
action_sequence.append({
"action_name": "wait",
- "action_kwargs": {"time": 20}
+ "action_kwargs": {"time": separation_time}
})
+ debug_print(f"✅ 等待分离完成: {separation_time}秒")
+
+ # 步骤6.5: 产物收集(从柱容器到目标容器)
+ if column_vessel and column_vessel != to_vessel:
+ debug_print(f"6.5: 产物收集 - 从 {column_vessel} 到 {to_vessel}")
+
+ try:
+ # 估算产物体积(原始样品体积的70-90%)
+ product_volume = source_volume * 0.8
+
+ product_transfer_actions = generate_pump_protocol_with_rinsing(
+ G=G,
+ from_vessel=column_vessel,
+ to_vessel=to_vessel,
+ volume=product_volume,
+ flowrate=1.5,
+ transfer_flowrate=0.8
+ )
+ action_sequence.extend(product_transfer_actions)
+ debug_print(f"✅ 产物收集完成,添加了 {len(product_transfer_actions)} 个动作")
+ except Exception as e:
+ debug_print(f"⚠️ 产物收集失败: {str(e)}")
+
+ # 步骤6.6: 如果没有独立的柱设备和容器,执行简化的直接转移
+ if not column_device_id and not column_vessel:
+ debug_print(f"6.6: 简化模式 - 直接转移 {source_volume}mL 从 {from_vessel} 到 {to_vessel}")
+
+ try:
+ direct_transfer_actions = generate_pump_protocol_with_rinsing(
+ G=G,
+ from_vessel=from_vessel,
+ to_vessel=to_vessel,
+ volume=source_volume,
+ flowrate=2.0,
+ transfer_flowrate=1.0
+ )
+ action_sequence.extend(direct_transfer_actions)
+ debug_print(f"✅ 直接转移完成,添加了 {len(direct_transfer_actions)} 个动作")
+ except Exception as e:
+ debug_print(f"⚠️ 直接转移失败: {str(e)}")
+
+ except Exception as e:
+ debug_print(f"❌ 协议生成失败: {str(e)} 😭")
+
+ # 不添加不确定的动作,直接让action_sequence保持为空列表
+ # action_sequence 已经在函数开始时初始化为 []
+
+ # 确保至少有一个有效的动作,如果完全失败就返回空列表
+ if not action_sequence:
+ debug_print("⚠️ 没有生成任何有效动作")
+ # 可以选择返回空列表或添加一个基本的等待动作
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {
+ "time": 1.0,
+ "description": "柱层析协议执行完成"
+ }
+ })
+
+ # 🎊 总结
+ 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 generate_reverse_phase_column_protocol(
- G: nx.DiGraph,
- from_vessel: str,
- to_vessel: str,
- column_material: str = "C18",
- aqueous_phase: str = "water",
- organic_phase: str = "methanol",
- gradient_ratio: float = 0.5
-) -> 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
- )
-
-
-def generate_ion_exchange_column_protocol(
- G: nx.DiGraph,
- from_vessel: str,
- to_vessel: str,
- column_material: str = "ion_exchange",
- buffer_solution: str = "buffer",
- salt_solution: str = "NaCl_solution",
- salt_volume: float = 40.0
-) -> List[Dict[str, Any]]:
- """离子交换柱层析:缓冲液平衡,盐溶液洗脱"""
- return generate_run_column_protocol(
- G, from_vessel, to_vessel, column_material,
- salt_solution, salt_volume, 1,
- buffer_solution, 25.0, 0.5
- )
-
-
# 测试函数
def test_run_column_protocol():
- """测试柱层析协议的示例"""
- print("=== RUN COLUMN PROTOCOL 测试 ===")
- print("测试完成")
-
+ """测试柱层析协议"""
+ debug_print("🧪 === RUN COLUMN PROTOCOL 测试 === ✨")
+ debug_print("✅ 测试完成 🎉")
if __name__ == "__main__":
- test_run_column_protocol()
\ No newline at end of file
+ test_run_column_protocol()
+
diff --git a/unilabos/compile/separate_protocol.py b/unilabos/compile/separate_protocol.py
index cbb028c..258c37d 100644
--- a/unilabos/compile/separate_protocol.py
+++ b/unilabos/compile/separate_protocol.py
@@ -1,230 +1,670 @@
-import numpy as np
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):
+ """调试输出函数 - 支持中文"""
+ 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:
+ """
+ 解析体积输入,支持带单位的字符串
+
+ Args:
+ volume_input: 体积输入(如 "200 mL", "?", 50.0)
+
+ Returns:
+ 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}'")
+
+ # 处理未知体积
+ 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
+ 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"✅ 体积已为毫升单位: {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}' 的容器...")
+
+ # 🔧 方法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', '')
+ 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"✅ 通过试剂字段精确匹配找到容器: {node}")
+ return node
+
+ # 模糊匹配
+ if (solvent.lower() in reagent_name and reagent_name) or \
+ (solvent.lower() in config_reagent and config_reagent):
+ debug_print(f"✅ 通过试剂字段模糊匹配找到容器: {node}")
+ return node
+
+ # 🔧 方法2:常见的容器命名规则
+ debug_print(f"📋 方法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}",
+ 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}")
+ 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}")
+ return node_id
+
+ debug_print(f"❌ 无法找到溶剂 '{solvent}' 的容器")
+ return ""
+
+def find_separator_device(G: nx.DiGraph, vessel: str) -> str:
+ """查找分离器设备,支持多种查找方式"""
+ 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",
+ "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 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_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)}")
+
+ # 检查哪个搅拌器与目标容器相连
+ 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(
- G: nx.DiGraph,
- purpose: str, # 'wash' or 'extract'. 'wash' means that product phase will not be the added solvent phase, 'extract' means product phase will be the added solvent phase. If no solvent is added just use 'extract'.
- product_phase: str, # 'top' or 'bottom'. Phase that product will be in.
- from_vessel: str, #Contents of from_vessel are transferred to separation_vessel and separation is performed.
- separation_vessel: str, # Vessel in which separation of phases will be carried out.
- to_vessel: str, # Vessel to send product phase to.
- waste_phase_to_vessel: str, # Optional. Vessel to send waste phase to.
- solvent: str, # Optional. Solvent to add to separation vessel after contents of from_vessel has been transferred to create two phases.
- solvent_volume: float = 50, # Optional. Volume of solvent to add (mL).
- through: str = "", # Optional. Solid chemical to send product phase through on way to to_vessel, e.g. 'celite'.
- repeats: int = 1, # Optional. Number of separations to perform.
- stir_time: float = 30, # Optional. Time stir for after adding solvent, before separation of phases.
- stir_speed: float = 300, # Optional. Speed to stir at after adding solvent, before separation of phases.
- settling_time: float = 300 # Optional. Time
-) -> list[dict]:
+ G: nx.DiGraph,
+ # 🔧 基础参数,支持XDL的vessel参数
+ vessel: str = "", # XDL: 分离容器
+ purpose: str = "separate", # 分离目的
+ product_phase: str = "top", # 产物相
+ # 🔧 可选的详细参数
+ from_vessel: str = "", # 源容器(通常在separate前已经transfer了)
+ separation_vessel: str = "", # 分离容器(与vessel同义)
+ to_vessel: str = "", # 目标容器(可选)
+ waste_phase_to_vessel: str = "", # 废相目标容器
+ product_vessel: str = "", # XDL: 产物容器(与to_vessel同义)
+ waste_vessel: str = "", # XDL: 废液容器(与waste_phase_to_vessel同义)
+ # 🔧 溶剂相关参数
+ solvent: str = "", # 溶剂名称
+ solvent_volume: Union[str, float] = 0.0, # 溶剂体积
+ volume: Union[str, float] = 0.0, # XDL: 体积(与solvent_volume同义)
+ # 🔧 操作参数
+ through: str = "", # 通过材料
+ repeats: int = 1, # 重复次数
+ stir_time: float = 30.0, # 搅拌时间(秒)
+ stir_speed: float = 300.0, # 搅拌速度
+ settling_time: float = 300.0, # 沉降时间(秒)
+ **kwargs
+) -> List[Dict[str, Any]]:
"""
- Generate a protocol to clean a vessel with a solvent.
+ 生成分离操作的协议序列 - 增强中文版
- :param G: Directed graph. Nodes are containers and pumps, edges are fluidic connections.
- :param vessel: Vessel to clean.
- :param solvent: Solvent to clean vessel with.
- :param volume: Volume of solvent to clean vessel with.
- :param temp: Temperature to heat vessel to while cleaning.
- :param repeats: Number of cleaning cycles to perform.
- :return: List of actions to clean vessel.
+ 支持XDL参数格式:
+ - vessel: 分离容器(必需)
+ - purpose: "wash", "extract", "separate"
+ - product_phase: "top", "bottom"
+ - product_vessel: 产物收集容器
+ - waste_vessel: 废液收集容器
+ - solvent: 溶剂名称
+ - volume: "200 mL", "?" 或数值
+ - repeats: 重复次数
+
+ 分离流程:
+ 1. (可选)添加溶剂到分离容器
+ 2. 搅拌混合
+ 3. 静置分层
+ 4. 收集指定相到目标容器
+ 5. 重复指定次数
"""
- # 生成泵操作的动作序列
- pump_action_sequence = []
- reactor_volume = 500.0
- waste_vessel = waste_phase_to_vessel
+ debug_print("=" * 60)
+ debug_print("🧪 开始生成分离协议 - 增强中文版")
+ debug_print(f"📋 原始参数:")
+ debug_print(f" 🥼 容器: '{vessel}'")
+ 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)
- # TODO:通过物料管理系统找到溶剂的容器
- if "," in solvent:
- solvents = solvent.split(",")
- assert len(solvents) == repeats, "Number of solvents must match number of repeats."
+ action_sequence = []
+
+ # === 参数验证和标准化 ===
+ 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
+ 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"⚠️ 重复次数参数 <= 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}")
+
+ 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:
+ purpose = "separate"
+ if not product_phase:
+ product_phase = "top"
+ 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: 查找设备...")
+ action_sequence.append(create_action_log("正在查找相关设备...", "🔍"))
+
+ # 查找分离器设备
+ separator_device = find_separator_device(G, final_vessel)
+ if separator_device:
+ action_sequence.append(create_action_log(f"找到分离器设备: {separator_device}", "🧪"))
else:
- solvents = [solvent] * repeats
+ debug_print("⚠️ 未找到分离器设备,可能无法执行分离")
+ action_sequence.append(create_action_log("未找到分离器设备", "⚠️"))
- # TODO: 通过设备连接图找到分离容器的控制器、底部出口
- separator_controller = f"{separation_vessel}_controller"
- separation_vessel_bottom = f"flask_{separation_vessel}"
+ # 查找搅拌器
+ 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("未找到搅拌器", "⚠️"))
- transfer_flowrate = flowrate = 2.5
+ # 查找溶剂容器(如果需要)
+ 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}", "⚠️"))
- 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,
+ debug_print(f"📊 设备配置:")
+ debug_print(f" 🧪 分离器设备: '{separator_device}'")
+ debug_print(f" 🌪️ 搅拌器设备: '{stirrer_device}'")
+ debug_print(f" 💧 溶剂容器: '{solvent_vessel}'")
+
+ # === 执行分离流程 ===
+ debug_print("🔍 步骤3: 执行分离流程...")
+ action_sequence.append(create_action_log("开始分离工作流程", "🎯"))
+
+ try:
+ for repeat_idx in range(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"🔄 第{cycle_num}轮 步骤1: 添加溶剂 {solvent} ({final_volume}mL)")
+ action_sequence.append(create_action_log(f"向分离容器添加 {final_volume}mL {solvent}", "💧"))
+
+ 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: 启动搅拌(如果有搅拌器)
+ 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"🔄 第{cycle_num}轮 步骤4: 执行分离操作")
+ action_sequence.append(create_action_log(f"执行分离: 收集{product_phase}相", "🧪"))
+
+ # 调用分离器设备的separate方法
+ separate_action = {
+ "device_id": separator_device,
+ "action_name": "separate",
+ "action_kwargs": {
+ "purpose": purpose,
+ "product_phase": product_phase,
+ "from_vessel": from_vessel or final_vessel,
+ "separation_vessel": final_vessel,
+ "to_vessel": final_to_vessel or final_vessel,
+ "waste_phase_to_vessel": final_waste_vessel or final_vessel,
+ "solvent": solvent,
+ "solvent_volume": final_volume,
+ "through": through,
+ "repeats": 1, # 每次调用只做一次分离
+ "stir_time": 0, # 已经在上面完成
+ "stir_speed": stir_speed,
+ "settling_time": 0 # 已经在上面完成
+ }
}
+ action_sequence.append(separate_action)
+ 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"🔄 第{cycle_num}轮 步骤4: 无分离器设备,跳过分离")
+ action_sequence.append(create_action_log("无分离器设备可用", "❌"))
+ # 添加等待时间模拟分离
+ action_sequence.append({
+ "action_name": "wait",
+ "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)}")
+ action_sequence.append(create_action_log(f"分离工作流程失败: {str(e)}", "❌"))
+ # 添加错误日志
+ action_sequence.append({
+ "device_id": "system",
+ "action_name": "log_message",
+ "action_kwargs": {
+ "message": f"分离操作失败: {str(e)}"
}
- )
-
- # for i in range(2):
- # pump_action_sequence.append(
- # {
- # "device_id": "",
- # "action_name": "CleanProtocol",
- # "action_kwargs": {
- # "vessel": from_vessel,
- # "solvent": "H2O", # Solvent to clean vessel with.
- # "volume": solvent_volume, # Optional. Volume of solvent to clean vessel with.
- # "temp": 25.0, # Optional. Temperature to heat vessel to while cleaning.
- # "repeats": 1
- # }
- # }
- # )
- # pump_action_sequence.append(
- # {
- # "device_id": "",
- # "action_name": "CleanProtocol",
- # "action_kwargs": {
- # "vessel": from_vessel,
- # "solvent": "CH2Cl2", # Solvent to clean vessel with.
- # "volume": solvent_volume, # Optional. Volume of solvent to clean vessel with.
- # "temp": 25.0, # Optional. Temperature to heat vessel to while cleaning.
- # "repeats": 1
- # }
- # }
- # )
+ })
- # 生成泵操作的动作序列
- for i in range(repeats):
- # 找到当次萃取所用溶剂
- solvent_thistime = solvents[i]
- solvent_vessel = f"flask_{solvent_thistime}"
-
- pump_action_sequence.append(
- {
- "device_id": "",
- "action_name": "PumpTransferProtocol",
- "action_kwargs": {
- "from_vessel": solvent_vessel,
- "to_vessel": separation_vessel,
- "volume": solvent_volume,
- "time": solvent_volume / flowrate,
- # "transfer_flowrate": transfer_flowrate,
- }
- }
- )
- pump_action_sequence.extend([
- # 搅拌、静置
- {
- "device_id": separator_controller,
- "action_name": "stir",
- "action_kwargs": {
- "stir_time": stir_time,
- "stir_speed": stir_speed,
- "settling_time": settling_time
- }
- },
- # 分液(判断电导突跃)
- {
- "device_id": separator_controller,
- "action_name": "valve_open",
- "action_kwargs": {
- "command": "delta > 0.05"
- }
- }
- ])
-
- if product_phase == "bottom":
- # 产物转移到目标瓶
- pump_action_sequence.append(
- {
- "device_id": "",
- "action_name": "PumpTransferProtocol",
- "action_kwargs": {
- "from_vessel": separation_vessel_bottom,
- "to_vessel": to_vessel,
- "volume": 250.0,
- "time": 250.0 / flowrate,
- # "transfer_flowrate": transfer_flowrate,
- }
- }
- )
- # 放出上面那一相,60秒后关阀门
- pump_action_sequence.append(
- {
- "device_id": separator_controller,
- "action_name": "valve_open",
- "action_kwargs": {
- "command": "time > 60"
- }
- }
- )
- # 弃去上面那一相进废液
- pump_action_sequence.append(
- {
- "device_id": "",
- "action_name": "PumpTransferProtocol",
- "action_kwargs": {
- "from_vessel": separation_vessel_bottom,
- "to_vessel": waste_vessel,
- "volume": 250.0,
- "time": 250.0 / flowrate,
- # "transfer_flowrate": transfer_flowrate,
- }
- }
- )
- elif product_phase == "top":
- # 弃去下面那一相进废液
- pump_action_sequence.append(
- {
- "device_id": "",
- "action_name": "PumpTransferProtocol",
- "action_kwargs": {
- "from_vessel": separation_vessel_bottom,
- "to_vessel": waste_vessel,
- "volume": 250.0,
- "time": 250.0 / flowrate,
- # "transfer_flowrate": transfer_flowrate,
- }
- }
- )
- # 放出上面那一相
- pump_action_sequence.append(
- {
- "device_id": separator_controller,
- "action_name": "valve_open",
- "action_kwargs": {
- "command": "time > 60"
- }
- }
- )
- # 产物转移到目标瓶
- pump_action_sequence.append(
- {
- "device_id": "",
- "action_name": "PumpTransferProtocol",
- "action_kwargs": {
- "from_vessel": separation_vessel_bottom,
- "to_vessel": to_vessel,
- "volume": 250.0,
- "time": 250.0 / flowrate,
- # "transfer_flowrate": transfer_flowrate,
- }
- }
- )
- elif product_phase == "organic":
- pass
-
- # 如果不是最后一次,从中转瓶转移回分液漏斗
- if i < repeats - 1:
- pump_action_sequence.append(
- {
- "device_id": "",
- "action_name": "PumpTransferProtocol",
- "action_kwargs": {
- "from_vessel": to_vessel,
- "to_vessel": separation_vessel,
- "volume": 250.0,
- "time": 250.0 / flowrate,
- # "transfer_flowrate": transfer_flowrate,
- }
- }
- )
- return pump_action_sequence
+ # === 最终结果 ===
+ total_time = (stir_time + settling_time + 15) * repeats # 估算总时间
+
+ debug_print("=" * 60)
+ 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)")
+ if final_to_vessel:
+ debug_print(f" 🎯 产物容器: {final_to_vessel}")
+ if 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
+
+# === 便捷函数 ===
+
+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",
+ 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]]:
+ """用溶剂洗涤"""
+ debug_print(f"🧽 用{solvent}洗涤: {vessel} ({repeats} 次)")
+ 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]]:
+ """用溶剂萃取"""
+ debug_print(f"🧪 用{solvent}萃取: {vessel} ({repeats} 次)")
+ 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]]:
+ """水-有机相分离"""
+ debug_print(f"💧 水-有机相分离: {vessel} (有机相: {organic_phase})")
+ 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():
+ """测试分离协议的各种参数解析"""
+ debug_print("=== 分离协议增强中文版测试 ===")
+
+ # 测试体积解析
+ debug_print("🧪 测试体积解析...")
+ volumes = ["200 mL", "?", 100.0, "1 L", "500 μL", "未知", "50毫升"]
+ for vol in volumes:
+ result = parse_volume_input(vol)
+ debug_print(f"📊 体积解析结果: {vol} -> {result}mL")
+
+ debug_print("✅ 测试完成")
+
+if __name__ == "__main__":
+ test_separate_protocol()
diff --git a/unilabos/compile/stir_protocol.py b/unilabos/compile/stir_protocol.py
index 6fc865c..f7a6a74 100644
--- a/unilabos/compile/stir_protocol.py
+++ b/unilabos/compile/stir_protocol.py
@@ -1,166 +1,342 @@
-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__)
+
+def debug_print(message):
+ """调试输出"""
+ print(f"🌪️ [STIR] {message}", flush=True)
+ logger.info(f"[STIR] {message}")
+
+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)
+ default_unit: 默认单位(默认为秒)
+
+ Returns:
+ float: 时间(秒)
+ """
+ if not time_input:
+ return 100.0 # 默认100秒
+
+ # 🔢 处理数值输入
+ 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}'")
+
+ # ❓ 特殊值处理
+ 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:
+ 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}',使用默认值: 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
+ }
+
+ multiplier = unit_multipliers.get(unit, 1.0)
+ result = value * multiplier
+
+ debug_print(f"✅ 时间解析: '{time_str}' → {value} {unit} → {result}s ({result/60:.1f}分钟)")
+ return result
def find_connected_stirrer(G: nx.DiGraph, vessel: str = None) -> str:
- """
- 查找与指定容器相连的搅拌设备,或查找可用的搅拌设备
- """
- # 查找所有搅拌设备节点
- stirrer_nodes = [node for node in G.nodes()
- if (G.nodes[node].get('class') or '') == 'virtual_stirrer']
+ """查找与指定容器相连的搅拌设备"""
+ debug_print(f"🔍 查找搅拌设备,目标容器: {vessel} 🥽")
- if vessel:
- # 检查哪个搅拌设备与目标容器相连(机械连接)
+ # 🔧 查找所有搅拌设备
+ 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 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}' 相连 🔗")
return stirrer
- # 如果没有指定容器或没有直接连接,返回第一个可用的搅拌设备
+ # 🎯 使用第一个可用设备
if stirrer_nodes:
- return stirrer_nodes[0]
+ selected = stirrer_nodes[0]
+ debug_print(f"🔧 使用第一个搅拌设备: {selected} 🌪️")
+ return selected
- raise ValueError("系统中未找到可用的搅拌设备")
+ # 🆘 默认设备
+ 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 在正常范围内 🌪️")
+
+ # ⏱️ 沉降时间验证
+ 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,
- stir_time: float,
- stir_speed: float,
- settling_time: float
+ vessel: str,
+ time: Union[str, float, int] = "300",
+ stir_time: Union[str, float, int] = "0",
+ time_spec: str = "",
+ event: str = "",
+ stir_speed: float = 300.0,
+ settling_time: Union[str, float] = "60",
+ **kwargs
) -> List[Dict[str, Any]]:
"""
- 生成搅拌操作的协议序列 - 定时搅拌 + 沉降
+ 生成搅拌操作的协议序列(精简版)
"""
- action_sequence = []
- print(f"STIR: 开始生成搅拌协议")
- print(f" - 搅拌时间: {stir_time}秒")
- print(f" - 搅拌速度: {stir_speed} RPM")
- print(f" - 沉降时间: {settling_time}秒")
+ 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("✅ 基础参数验证通过 🎯")
+
+ # 🔄 参数解析
+ debug_print("📍 步骤2: 参数解析... ⚡")
+
+ # 确定实际时间(优先级:time_spec > stir_time > time)
+ if time_spec:
+ 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_input(time)
+ debug_print(f"🎯 使用time: {time} → {parsed_time}s")
+
+ # 解析沉降时间
+ 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)
- 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("📍 步骤4: 生成搅拌动作... 🌪️")
+
+ action_sequence = []
stir_action = {
"device_id": stirrer_id,
"action_name": "stir",
"action_kwargs": {
- "stir_time": stir_time,
- "stir_speed": stir_speed,
- "settling_time": settling_time
+ "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("✅ 搅拌动作已添加 🌪️✨")
+
+ # 显示时间优化信息
+ 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)
- print(f"STIR: 生成了 {len(action_sequence)} 个动作")
return action_sequence
-
def generate_start_stir_protocol(
G: nx.DiGraph,
vessel: str,
- stir_speed: float,
- purpose: str
+ stir_speed: float = 300.0,
+ purpose: str = "",
+ **kwargs
) -> List[Dict[str, Any]]:
- """
- 生成开始搅拌操作的协议序列 - 持续搅拌
- """
- action_sequence = []
+ """生成开始搅拌操作的协议序列"""
- print(f"START_STIR: 开始生成启动搅拌协议")
- print(f" - 容器: {vessel}")
- print(f" - 搅拌速度: {stir_speed} RPM")
- print(f" - 目的: {purpose}")
+ debug_print("🔄 开始生成启动搅拌协议 ✨")
+ debug_print(f"🥽 vessel: {vessel}, 🌪️ speed: {stir_speed} RPM")
- # 验证容器存在
- if vessel not in G.nodes():
- raise ValueError(f"容器 '{vessel}' 不存在于系统中")
+ # 基础验证
+ if not vessel or vessel not in G.nodes():
+ debug_print("❌ 容器验证失败!")
+ raise ValueError("vessel 参数无效")
- # 查找搅拌设备
- try:
- stirrer_id = find_connected_stirrer(G, vessel)
- print(f"START_STIR: 找到搅拌设备: {stirrer_id}")
- except ValueError as e:
- raise ValueError(f"无法找到搅拌设备: {str(e)}")
+ # 参数修正
+ if stir_speed < 10.0 or stir_speed > 1500.0:
+ debug_print(f"⚠️ 搅拌速度修正: {stir_speed} → 300 RPM 🌪️")
+ stir_speed = 300.0
- # 执行开始搅拌操作
- start_stir_action = {
+ # 查找设备
+ stirrer_id = find_connected_stirrer(G, vessel)
+
+ # 生成动作
+ 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)
-
- print(f"START_STIR: 生成了 {len(action_sequence)} 个动作")
+ debug_print(f"✅ 启动搅拌协议生成完成 🎯")
return action_sequence
-
def generate_stop_stir_protocol(
G: nx.DiGraph,
- vessel: str
+ vessel: str,
+ **kwargs
) -> List[Dict[str, Any]]:
- """
- 生成停止搅拌操作的协议序列
- """
- action_sequence = []
+ """生成停止搅拌操作的协议序列"""
- print(f"STOP_STIR: 开始生成停止搅拌协议")
- print(f" - 容器: {vessel}")
+ debug_print("🛑 开始生成停止搅拌协议 ✨")
+ debug_print(f"🥽 vessel: {vessel}")
- # 验证容器存在
- if vessel not in G.nodes():
- raise ValueError(f"容器 '{vessel}' 不存在于系统中")
+ # 基础验证
+ if not vessel or vessel not in G.nodes():
+ debug_print("❌ 容器验证失败!")
+ raise ValueError("vessel 参数无效")
- # 查找搅拌设备
- try:
- stirrer_id = find_connected_stirrer(G, vessel)
- print(f"STOP_STIR: 找到搅拌设备: {stirrer_id}")
- except ValueError as e:
- raise ValueError(f"无法找到搅拌设备: {str(e)}")
+ # 查找设备
+ 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)
-
- print(f"STOP_STIR: 生成了 {len(action_sequence)} 个动作")
+ debug_print(f"✅ 停止搅拌协议生成完成 🎯")
return action_sequence
+# 测试函数
+def test_stir_protocol():
+ """测试搅拌协议"""
+ debug_print("🧪 === STIR PROTOCOL 测试 === ✨")
+ debug_print("✅ 测试完成 🎉")
-# 便捷函数
-def generate_fast_stir_protocol(
- G: nx.DiGraph,
- time: float = 300.0,
- speed: float = 800.0,
- settling: float = 60.0
-) -> List[Dict[str, Any]]:
- """快速搅拌的便捷函数"""
- return generate_stir_protocol(G, time, speed, settling)
-
-
-def generate_gentle_stir_protocol(
- G: nx.DiGraph,
- time: float = 600.0,
- speed: float = 200.0,
- settling: float = 120.0
-) -> List[Dict[str, Any]]:
- """温和搅拌的便捷函数"""
- return generate_stir_protocol(G, time, speed, settling)
\ No newline at end of file
+if __name__ == "__main__":
+ test_stir_protocol()
diff --git a/unilabos/compile/utils/unit_parser.py b/unilabos/compile/utils/unit_parser.py
new file mode 100644
index 0000000..d1d297c
--- /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 a792b8f..55768a4 100644
--- a/unilabos/compile/wash_solid_protocol.py
+++ b/unilabos/compile/wash_solid_protocol.py
@@ -1,216 +1,316 @@
-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__)
+
+def debug_print(message):
+ """调试输出"""
+ print(f"🧼 [WASH_SOLID] {message}", flush=True)
+ logger.info(f"[WASH_SOLID] {message}")
+
+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")
+ return result
+
+ # 📝 处理字符串输入
+ time_str = str(time_input).lower().strip()
+
+ # ❓ 特殊值快速处理
+ special_times = {
+ '?': 60.0, 'unknown': 60.0, 'briefly': 30.0,
+ 'quickly': 45.0, 'slowly': 120.0
+ }
+
+ if time_str in special_times:
+ result = special_times[time_str]
+ debug_print(f"🎯 特殊时间: '{time_str}' → {result}s")
+ return result
+
+ # 🔢 数字提取(简化正则)
+ try:
+ # 提取数字
+ 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
+
+ debug_print(f"⚠️ 时间解析失败: '{time_str}',使用默认60s")
+ return 60.0
+
+def parse_volume_input(volume: Union[float, str], volume_spec: str = "", mass: str = "") -> float:
+ """统一体积解析函数(精简版)"""
+ 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转体积(简化: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
+ if volume:
+ if isinstance(volume, (int, float)):
+ result = float(volume)
+ 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
+
+ # 默认值
+ debug_print(f"⚠️ 体积解析失败,使用默认50mL")
+ return 50.0
+
+def find_solvent_source(G: nx.DiGraph, solvent: str) -> str:
+ """查找溶剂源(精简版)"""
+ debug_print(f"🔍 查找溶剂源: {solvent}")
+
+ # 简化搜索列表
+ search_patterns = [
+ f"flask_{solvent}", f"bottle_{solvent}", f"reagent_{solvent}",
+ "liquid_reagent_bottle_1", "flask_1", "solvent_bottle"
+ ]
+
+ for pattern in search_patterns:
+ if pattern in G.nodes():
+ debug_print(f"🎉 找到溶剂源: {pattern}")
+ return pattern
+
+ 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}")
+
+ # 如果指定了且存在,直接使用
+ if filtrate_vessel and filtrate_vessel in G.nodes():
+ debug_print(f"✅ 使用指定容器: {filtrate_vessel}")
+ return filtrate_vessel
+
+ # 简化搜索列表
+ default_vessels = ["waste_workup", "filtrate_vessel", "flask_1", "collection_bottle_1"]
+
+ for vessel in default_vessels:
+ if vessel in G.nodes():
+ debug_print(f"🎉 找到滤液容器: {vessel}")
+ return vessel
+
+ debug_print(f"⚠️ 使用默认滤液容器: waste_workup")
+ return "waste_workup"
def generate_wash_solid_protocol(
G: nx.DiGraph,
vessel: str,
solvent: str,
- volume: float,
+ volume: Union[float, str] = "50",
filtrate_vessel: str = "",
temp: float = 25.0,
stir: bool = False,
stir_speed: float = 0.0,
- time: float = 0.0,
- repeats: int = 1
+ time: Union[str, float] = "0",
+ repeats: int = 1,
+ volume_spec: str = "",
+ repeats_spec: str = "",
+ mass: str = "",
+ event: str = "",
+ **kwargs
) -> List[Dict[str, Any]]:
"""
- 生成固体清洗的协议序列
-
- Args:
- G: 有向图,节点为设备和容器
- vessel: 装有固体物质的容器名称
- solvent: 用于清洗固体的溶剂名称
- volume: 清洗溶剂的体积
- filtrate_vessel: 滤液要收集到的容器名称,可选参数
- temp: 清洗时的温度,可选参数
- stir: 是否在清洗过程中搅拌,默认为 False
- stir_speed: 搅拌速度,可选参数
- time: 清洗的时间,可选参数
- repeats: 清洗操作的重复次数,默认为 1
-
- Returns:
- List[Dict[str, Any]]: 固体清洗操作的动作序列
-
- Raises:
- ValueError: 当找不到必要的设备时抛出异常
-
- Examples:
- wash_solid_protocol = generate_wash_solid_protocol(
- G, "reactor", "ethanol", 100.0, "waste_flask", 60.0, True, 300.0, 600.0, 3
- )
+ 生成固体清洗协议(精简版)
"""
+
+ 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 参数不能为空")
+
+ debug_print("✅ 基础验证通过 🎯")
+
+ # 🔄 参数解析
+ debug_print("📍 步骤1: 参数解析... ⚡")
+ final_volume = parse_volume_input(volume, volume_spec, mass)
+ final_time = parse_time_input(time)
+
+ # 重复次数处理(简化)
+ 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_repeats = max(1, min(repeats, 5)) # 限制1-5次
+
+ # 🕐 模拟时间优化
+ 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 ⚡")
+
+ # 参数修正
+ 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(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)
+ debug_print(f"🎉 设备配置完成 ✨")
+ except Exception as e:
+ debug_print(f"❌ 设备查找失败: {str(e)} 😭")
+ raise ValueError(f"设备查找失败: {str(e)}")
+
+ # 🚀 生成动作序列
+ debug_print("📍 步骤3: 生成清洗动作... 🧼")
action_sequence = []
- # 验证容器是否存在
- if vessel not in G.nodes():
- raise ValueError(f"固体容器 {vessel} 不存在于图中")
-
- if filtrate_vessel and filtrate_vessel not in G.nodes():
- raise ValueError(f"滤液容器 {filtrate_vessel} 不存在于图中")
-
- # 查找转移泵设备(用于添加溶剂和转移滤液)
- pump_nodes = [node for node in G.nodes()
- if G.nodes[node].get('class') == 'virtual_transfer_pump']
-
- if not pump_nodes:
- raise ValueError("没有找到可用的转移泵设备")
-
- pump_id = pump_nodes[0]
-
- # 查找加热设备(如果需要加热)
- heatchill_nodes = [node for node in G.nodes()
- if G.nodes[node].get('class') == 'virtual_heatchill']
-
- heatchill_id = heatchill_nodes[0] if heatchill_nodes else None
-
- # 查找搅拌设备(如果需要搅拌)
- stirrer_nodes = [node for node in G.nodes()
- if G.nodes[node].get('class') == 'virtual_stirrer']
-
- stirrer_id = stirrer_nodes[0] if stirrer_nodes else None
-
- # 查找过滤设备(用于分离固体和滤液)
- filter_nodes = [node for node in G.nodes()
- if G.nodes[node].get('class') == 'virtual_filter']
-
- filter_id = filter_nodes[0] if filter_nodes else None
-
- # 查找溶剂容器
- 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
+ for cycle in range(final_repeats):
+ debug_print(f" 🔄 第{cycle+1}/{final_repeats}次清洗...")
- # 步骤1:如果需要加热,先设置温度
- if temp > 25.0 and heatchill_id:
- action_sequence.append({
- "device_id": heatchill_id,
- "action_name": "heat_chill_start",
+ # 1. 转移溶剂
+ 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
+ )
+
+ if transfer_actions:
+ action_sequence.extend(transfer_actions)
+ debug_print(f" ✅ 转移动作: {len(transfer_actions)}个 🚚")
+
+ except Exception as e:
+ debug_print(f" ❌ 转移失败: {str(e)} 😞")
+
+ # 2. 搅拌(如果需要)
+ if stir and final_time > 0:
+ stir_action = {
+ "device_id": "stirrer_1",
+ "action_name": "stir",
"action_kwargs": {
"vessel": vessel,
- "temp": temp,
- "purpose": f"固体清洗 - 第 {repeat_num} 次"
+ "time": str(time),
+ "stir_time": final_time,
+ "stir_speed": stir_speed,
+ "settling_time": 10.0 # 🕐 缩短沉降时间
}
- })
-
- # 步骤2:添加清洗溶剂到固体容器
- action_sequence.append({
- "device_id": pump_id,
- "action_name": "transfer",
- "action_kwargs": {
- "from_vessel": solvent_vessel,
- "to_vessel": vessel,
- "volume": volume,
- "amount": f"清洗溶剂 {solvent} - 第 {repeat_num} 次",
- "time": 0.0,
- "viscous": False,
- "rinsing_solvent": "",
- "rinsing_volume": 0.0,
- "rinsing_repeats": 0,
- "solid": False
}
+ action_sequence.append(stir_action)
+ debug_print(f" ✅ 搅拌动作: {final_time}s, {stir_speed}RPM 🌪️")
+
+ # 3. 过滤
+ filter_action = {
+ "device_id": "filter_1",
+ "action_name": "filter",
+ "action_kwargs": {
+ "vessel": vessel,
+ "filtrate_vessel": actual_filtrate_vessel,
+ "temp": temp,
+ "volume": final_volume
+ }
+ }
+ action_sequence.append(filter_action)
+ debug_print(f" ✅ 过滤动作: → {actual_filtrate_vessel} 🌊")
+
+ # 4. 等待(缩短时间)
+ wait_time = 5.0 # 🕐 缩短等待时间:10s → 5s
+ action_sequence.append({
+ "action_name": "wait",
+ "action_kwargs": {"time": wait_time}
})
-
- # 步骤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
-
- # 步骤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
- }
- })
+ 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 c83da1c..892a320 100644
--- a/unilabos/devices/virtual/virtual_column.py
+++ b/unilabos/devices/virtual/virtual_column.py
@@ -3,7 +3,7 @@ import logging
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 @@ class VirtualColumn:
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 @@ class VirtualColumn:
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 @@ class VirtualColumn:
@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 ca2e8b2..655934b 100644
--- a/unilabos/devices/virtual/virtual_filter.py
+++ b/unilabos/devices/virtual/virtual_filter.py
@@ -5,7 +5,7 @@ from typing import Dict, Any, Optional
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 @@ class VirtualFilter:
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 @@ class VirtualFilter:
"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,64 +70,82 @@ class VirtualFilter:
continue_heatchill: bool = False,
volume: float = 0.0
) -> bool:
- """Execute filter action - 完全按照 Filter.action 参数"""
- 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}")
+ """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"🌊 开始过滤操作: {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()
@@ -133,20 +155,24 @@ class VirtualFilter:
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
@@ -154,54 +180,57 @@ class VirtualFilter:
# 过滤完成
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 541434a..94ab572 100644
--- a/unilabos/devices/virtual/virtual_heatchill.py
+++ b/unilabos/devices/virtual/virtual_heatchill.py
@@ -4,7 +4,7 @@ import time as time_module # 重命名time模块,避免与参数冲突
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,94 +31,149 @@ class VirtualHeatChill:
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: 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 - 🔧 修复:确保参数类型正确"""
- # 验证参数
- 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)
+ # 🔧 关键修复:确保所有参数类型正确
+ 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(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" ⏰ 持续时间: {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(f"❌ {error_msg}")
+ self.data.update({
+ "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 temp > 25.0:
- operation_mode = "Heating"
- status_action = "加热"
- elif temp < 25.0:
- operation_mode = "Cooling"
- status_action = "冷却"
- else:
- operation_mode = "Maintaining"
- status_action = "保温"
+ if time_value <= 0:
+ error_msg = f"⏰ 时间 {time_value}s 必须大于0 ⚠️"
+ self.logger.error(f"❌ {error_msg}")
+ self.data.update({
+ "status": f"❌ 错误: 时间参数无效 ⚠️",
+ "operation_mode": "Error"
+ })
+ return False
- # **修复**: 使用重命名的time模块
+ # 🔧 修复:使用转换后的时间值
start_time = time_module.time()
- total_time = 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,
"remaining_time": total_time,
})
- # **修复**: 在等待过程中每秒更新剩余时间
+ # 在等待过程中每秒更新剩余时间
+ last_logged_time = 0
while True:
- current_time = time_module.time() # 使用重命名的time模块
+ 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
@@ -127,71 +182,114 @@ class VirtualHeatChill:
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"""
- self.logger.info(f"HeatChillStart: vessel={vessel}, temp={temp}°C")
+ """Start continuous heat chill 🔄"""
- # 验证参数
- 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)
+ # 🔧 添加类型转换
+ try:
+ temp = float(temp)
+ 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: {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"""
- self.logger.info(f"HeatChillStop: vessel={vessel}")
+ """Stop heat chill 🛑"""
+
+ # 🔧 添加类型转换
+ try:
+ vessel = str(vessel)
+ except (ValueError, TypeError) as e:
+ error_msg = f"参数类型转换错误: {str(e)}"
+ self.logger.error(f"❌ {error_msg}")
+ return False
+
+ 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:
@@ -207,4 +305,20 @@ class VirtualHeatChill:
@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 c24b7b1..468175c 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 @@ class VirtualMultiwayValve:
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 @@ class VirtualMultiwayValve:
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,7 +79,22 @@ class VirtualMultiwayValve:
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"
@@ -79,104 +102,139 @@ class VirtualMultiwayValve:
# 模拟阀门切换时间
switch_time = abs(self._current_position - pos) * 0.5 # 每个位置0.5秒
- time.sleep(switch_time)
+
+ 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
+ # 删除debug日志: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
+ # 删除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:
- """检查是否在指定端口位置"""
- return self._current_position == port_number
+ """检查是否在指定端口位置 🔌"""
+ result = self._current_position == port_number
+ # 删除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:
- """获取可用位置列表"""
- return list(range(0, self.max_positions + 1))
+ """获取可用位置列表 📋"""
+ positions = list(range(0, self.max_positions + 1))
+ # 删除debug日志:self.logger.debug(f"📋 可用位置: {positions}")
+ return positions
def get_available_ports(self) -> Dict[int, str]:
- """获取可用端口映射"""
+ """获取可用端口映射 🗺️"""
+ # 删除debug日志: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})"
+
+ # 删除debug日志: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 @@ class VirtualMultiwayValve:
"flow_path": self.get_flow_path(),
"position_map": self.position_map
}
+
+ # 删除debug日志: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) 或位置字符串
"""
+ # 删除debug日志:self.logger.debug(f"🎯 兼容性调用: set_valve_position({command})")
return self.set_position(command)
@@ -207,25 +272,35 @@ class VirtualMultiwayValve:
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 ba01c7b..dd1cca4 100644
--- a/unilabos/devices/virtual/virtual_rotavap.py
+++ b/unilabos/devices/virtual/virtual_rotavap.py
@@ -3,9 +3,12 @@ import logging
import time as time_module
from typing import Dict, Any, Optional
+def debug_print(message):
+ """调试输出 🔍"""
+ print(f"🌪️ [ROTAVAP] {message}", flush=True)
class VirtualRotavap:
- """Virtual rotary evaporator device - 简化版,只保留核心功能"""
+ """Virtual rotary evaporator device - 简化版,只保留核心功能 🌪️"""
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
# 处理可能的不同调用方式
@@ -32,13 +35,16 @@ class VirtualRotavap:
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,
@@ -47,22 +53,27 @@ class VirtualRotavap:
"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(
@@ -70,46 +81,88 @@ class VirtualRotavap:
vessel: str,
pressure: float = 0.1,
temp: float = 60.0,
- time: float = 1800.0, # 30分钟默认
- stir_speed: float = 100.0
+ time: float = 180.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 - 简化版 🌪️"""
+
+ # 🔧 简化处理:如果vessel就是设备自己,直接操作
+ if vessel == 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}")
+ # 根据溶剂调整参数
+ 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(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(f"⚡ 易挥发溶剂:调整参数 → 温度 {temp}°C, 压力 {pressure} bar")
+
+ 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,
+ "evaporated_volume": 0.0,
"message": error_msg
})
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,
+ "evaporated_volume": 0.0,
"message": error_msg
})
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,
+ "evaporated_volume": 0.0,
"message": error_msg
})
return False
# 开始蒸发
+ self.logger.info(f"🚀 启动蒸发程序! 预计用时 {time/60:.1f}分钟 ⏱️")
+
self.data.update({
- "status": f"蒸发中: {vessel}",
+ "status": f"🌪️ 蒸发中: {actual_vessel}",
"rotavap_state": "Evaporating",
"current_temp": temp,
"target_temp": temp,
@@ -118,13 +171,14 @@ class VirtualRotavap:
"remaining_time": time,
"progress": 0.0,
"evaporated_volume": 0.0,
- "message": f"Evaporating {vessel} at {temp}°C, {pressure} bar, {stir_speed} RPM"
+ "message": f"🌪️ Evaporating {actual_vessel} at {temp}°C, {pressure} bar, {stir_speed} RPM"
})
try:
# 蒸发过程 - 实时更新进度
start_time = time_module.time()
total_time = time
+ last_logged_progress = 0
while True:
current_time = time_module.time()
@@ -132,18 +186,31 @@ class VirtualRotavap:
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,
- "status": f"蒸发中: {vessel} | {temp}°C | {pressure} bar | {progress:.1f}% | 剩余: {remaining:.0f}s",
- "message": f"Evaporating: {progress:.1f}% complete, {remaining:.0f}s remaining"
+ "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
@@ -152,40 +219,59 @@ class VirtualRotavap:
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"蒸发完成: {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}"
+ "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 {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_solenoid_valve.py b/unilabos/devices/virtual/virtual_solenoid_valve.py
index f25cc84..54a1e6d 100644
--- a/unilabos/devices/virtual/virtual_solenoid_valve.py
+++ b/unilabos/devices/virtual/virtual_solenoid_valve.py
@@ -43,10 +43,25 @@ class VirtualSolenoidValve:
def is_open(self) -> bool:
return self._is_open
- def get_valve_position(self) -> str:
+ @property
+ def valve_position(self) -> str:
"""获取阀门位置状态"""
return "OPEN" if self._is_open else "CLOSED"
+ @property
+ def state(self) -> dict:
+ """获取阀门完整状态"""
+ return {
+ "device_id": self.device_id,
+ "port": self.port,
+ "voltage": self.voltage,
+ "response_time": self.response_time,
+ "is_open": self._is_open,
+ "valve_state": self._valve_state,
+ "status": self._status,
+ "position": self.valve_position
+ }
+
async def set_valve_position(self, command: str = None, **kwargs):
"""
设置阀门位置 - ROS动作接口
@@ -91,7 +106,7 @@ class VirtualSolenoidValve:
return {
"success": True,
"message": result_msg,
- "valve_position": self.get_valve_position()
+ "valve_position": self.valve_position
}
async def open(self, **kwargs):
@@ -102,21 +117,25 @@ class VirtualSolenoidValve:
"""关闭电磁阀 - ROS动作接口"""
return await self.set_valve_position(command="CLOSED")
- async def set_state(self, command: Union[bool, str], **kwargs):
+ async def set_status(self, string: str = None, **kwargs):
"""
- 设置阀门状态 - 兼容 SendCmd 类型
+ 设置阀门状态 - 兼容 StrSingleInput 类型
Args:
- command: True/False 或 "open"/"close"
+ string: "ON"/"OFF" 或 "OPEN"/"CLOSED"
"""
- if isinstance(command, bool):
- cmd_str = "OPEN" if command else "CLOSED"
- elif isinstance(command, str):
- cmd_str = command
- else:
- return {"success": False, "message": "Invalid command type"}
+ if string is None:
+ return {"success": False, "message": "Missing string parameter"}
- return await self.set_valve_position(command=cmd_str)
+ # 将 string 参数转换为 command 参数
+ if string.upper() in ["ON", "OPEN"]:
+ command = "OPEN"
+ elif string.upper() in ["OFF", "CLOSED"]:
+ command = "CLOSED"
+ else:
+ command = string
+
+ return await self.set_valve_position(command=command)
def toggle(self):
"""切换阀门状态"""
@@ -129,19 +148,6 @@ class VirtualSolenoidValve:
"""检查阀门是否关闭"""
return not self._is_open
- def get_state(self) -> dict:
- """获取阀门完整状态"""
- return {
- "device_id": self.device_id,
- "port": self.port,
- "voltage": self.voltage,
- "response_time": self.response_time,
- "is_open": self._is_open,
- "valve_state": self._valve_state,
- "status": self._status,
- "position": self.get_valve_position()
- }
-
async def reset(self):
"""重置阀门到关闭状态"""
return await self.close()
\ No newline at end of file
diff --git a/unilabos/devices/virtual/virtual_solid_dispenser.py b/unilabos/devices/virtual/virtual_solid_dispenser.py
new file mode 100644
index 0000000..439c348
--- /dev/null
+++ b/unilabos/devices/virtual/virtual_solid_dispenser.py
@@ -0,0 +1,389 @@
+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"⚗️ === 虚拟固体分配器 {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
+
+ self.logger.info(f"✅ 固体分配器 {self.device_id} 初始化完成 ⚗️")
+ return True
+
+ async def cleanup(self) -> bool:
+ """清理固体加样器 🧹"""
+ 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) ⚖️
+
+ 支持格式: "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:
+ self.logger.debug(f"🔍 无法解析质量字符串: {mass_str}")
+ 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)
+ 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}")
+ 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:
+ self.logger.debug(f"🔍 无法解析摩尔数字符串: {mol_str}")
+ return 0.0
+
+ try:
+ value = float(match.group(1))
+ unit = match.group(2)
+
+ # 单位转换为 mol
+ if unit == 'mmol':
+ result = value * 0.001
+ else: # mol
+ result = value
+
+ self.logger.debug(f"🧮 摩尔数解析: {mol_str} → {result:.6f}mol (原值: {value} {unit})")
+ return result
+
+ 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:
+ self.logger.debug(f"🔍 未指定试剂名称,使用默认瓶")
+ 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}",
+ ]
+
+ # 这里简化处理,实际应该查询设备图
+ selected_bottle = possible_names[0]
+ self.logger.debug(f"🔍 为试剂 {reagent_name} 选择试剂瓶: {selected_bottle}")
+ return selected_bottle
+
+ 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:
+ error_msg = "❌ 错误: 必须指定试剂名称"
+ self.logger.error(error_msg)
+ return {
+ "success": False,
+ "message": error_msg,
+ "return_info": "missing_reagent"
+ }
+
+ # 解析质量和摩尔数
+ mass_value = self.parse_mass_string(mass)
+ mol_value = self.parse_mol_string(mol)
+
+ self.logger.info(f"📊 解析结果 - 质量: {mass_value:.6f}g | 摩尔数: {mol_value:.6f}mol")
+
+ # 确定实际加样量
+ if mass_value > 0:
+ actual_amount = mass_value
+ amount_unit = "g"
+ 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)"
+ 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)"
+ 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": error_msg,
+ "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}秒 ⏱️")
+
+ # 显示进度的模拟
+ 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
+ self._total_operations += 1
+ self._status = "Ready"
+
+ # 成功结果
+ success_message = f"✅ 成功添加 {reagent} {actual_amount:.6f} {amount_unit} 到 {vessel}"
+
+ 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:.6f}g",
+ "dispensed_amount": actual_amount,
+ "reagent": reagent,
+ "vessel": vessel
+ }
+
+ 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]:
+ """获取设备状态信息 📊"""
+ info = {
+ "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
+ }
+
+ self.logger.debug(f"📊 设备信息: 状态={self._status}, 试剂={self._current_reagent}, 加样量={self._dispensed_amount:.6f}g")
+ return info
+
+ def __str__(self):
+ 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("⚗️ === 固体加样器测试开始 === 🧪")
+
+ 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}")
+
+ # 测试2: 按摩尔数加样
+ print(f"\n🧮 测试2: 按摩尔数加样...")
+ result2 = await dispenser.add_solid(
+ vessel="main_reactor",
+ reagent="sodium_nitrite",
+ mol="0.28 mol"
+ )
+ print(f"📊 测试2结果: {result2}")
+
+ # 测试3: 缺少参数
+ print(f"\n⚠️ 测试3: 缺少参数测试...")
+ result3 = await dispenser.add_solid(
+ reagent="test_compound"
+ )
+ 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"\n📊 最终设备信息: {dispenser.get_device_info()}")
+ print(f"✅ === 测试完成 === 🎉")
+
+
+if __name__ == "__main__":
+ asyncio.run(test_solid_dispenser())
\ No newline at end of file
diff --git a/unilabos/devices/virtual/virtual_stirrer.py b/unilabos/devices/virtual/virtual_stirrer.py
index 874f997..2b9058b 100644
--- a/unilabos/devices/virtual/virtual_stirrer.py
+++ b/unilabos/devices/virtual/virtual_stirrer.py
@@ -4,7 +4,7 @@ import time as time_module
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 @@ class VirtualStirrer:
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 @@ class VirtualStirrer:
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 @@ class VirtualStirrer:
})
# 搅拌过程 - 实时更新剩余时间
+ 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 @@ class VirtualStirrer:
})
# 沉降过程 - 实时更新剩余时间
+ 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 - 开始持续搅拌 🔄"""
- # 验证参数
- 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)
+ # 🔧 类型转换
+ 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: {error_msg}",
+ "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(f"❌ {error_msg}")
+ self.data.update({
+ "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 @@ class VirtualStirrer:
"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 @@ class VirtualStirrer:
"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 @@ class VirtualStirrer:
@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
diff --git a/unilabos/devices/virtual/virtual_transferpump.py b/unilabos/devices/virtual/virtual_transferpump.py
index a2cba9c..7d80744 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 @@ class VirtualTransferPump:
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 @@ class VirtualTransferPump:
@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 @@ class VirtualTransferPump:
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 @@ class VirtualTransferPump:
# 新的set_position方法 - 专门用于SetPumpPosition动作
async def set_position(self, position: float, max_velocity: float = None):
"""
- 移动到绝对位置 - 专门用于SetPumpPosition动作
+ 移动到绝对位置 - 专门用于SetPumpPosition动作 🎯
Args:
position (float): 目标位置 (ml)
@@ -122,56 +161,107 @@ class VirtualTransferPump:
# 限制位置在有效范围内
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)
- # 模拟移动过程
- start_position = self._position
- steps = 10 if duration > 0.1 else 1 # 如果移动距离很小,只用1步
- step_duration = duration / steps if steps > 1 else duration
+ # ⚡ 计算实际执行时间(快速模式)
+ actual_duration = self._calculate_duration(volume_to_move, velocity)
- 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
+ # 🎯 确定操作类型和emoji
+ if target_position > self._position:
+ operation_type = "吸液"
+ operation_emoji = "📥"
+ elif target_position < self._position:
+ operation_type = "排液"
+ operation_emoji = "📤"
+ else:
+ operation_type = "保持"
+ operation_emoji = "📍"
+
+ 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")
+
+ 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 @@ class VirtualTransferPump:
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"Pulling {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"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 @@ class VirtualTransferPump:
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/messages/__init__.py b/unilabos/messages/__init__.py
index 47f21f1..f91f382 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(180.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,282 @@ 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
+ # === 必需参数 ===
+ 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 HeatChillStartProtocol(BaseModel):
- vessel: str
- temp: float
- purpose: str
+ # === 必需参数 ===
+ 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
+ # === 必需参数 ===
+ vessel: str = Field(..., description="加热容器名称")
+
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 +557,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
@@ -207,7 +632,8 @@ __all__ = [
"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 5bfe5d7..461fc73 100644
--- a/unilabos/registry/devices/mock_devices.yaml
+++ b/unilabos/registry/devices/mock_devices.yaml
@@ -539,11 +539,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:
@@ -561,22 +565,34 @@ mock_heater:
type: object
goal:
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
@@ -584,13 +600,16 @@ mock_heater:
type: object
result:
properties:
+ message:
+ type: string
return_info:
type: string
success:
type: boolean
required:
- - return_info
- success
+ - message
+ - return_info
title: HeatChill_Result
type: object
required:
@@ -836,13 +855,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: []
@@ -898,8 +922,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
@@ -910,10 +940,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:
@@ -929,6 +963,11 @@ mock_pump:
- rinsing_volume
- rinsing_repeats
- solid
+ - flowrate
+ - transfer_flowrate
+ - rate_spec
+ - event
+ - through
title: PumpTransfer_Goal
type: object
result:
@@ -1519,6 +1558,7 @@ mock_separator:
goal_default:
from_vessel: ''
product_phase: ''
+ product_vessel: ''
purpose: ''
repeats: 0
separation_vessel: ''
@@ -1529,7 +1569,10 @@ mock_separator:
stir_time: 0.0
through: ''
to_vessel: ''
+ vessel: ''
+ volume: ''
waste_phase_to_vessel: ''
+ waste_vessel: ''
handles: []
result:
success: success
@@ -1585,6 +1628,8 @@ mock_separator:
type: string
product_phase:
type: string
+ product_vessel:
+ type: string
purpose:
type: string
repeats:
@@ -1607,8 +1652,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
@@ -1623,17 +1674,24 @@ mock_separator:
- stir_time
- stir_speed
- settling_time
+ - vessel
+ - volume
+ - product_vessel
+ - waste_vessel
title: Separate_Goal
type: object
result:
properties:
+ message:
+ type: string
return_info:
type: string
success:
type: boolean
required:
- - return_info
- success
+ - message
+ - return_info
title: Separate_Result
type: object
required:
@@ -2410,9 +2468,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
@@ -2429,13 +2491,25 @@ mock_stirrer_new:
type: object
goal:
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
@@ -2443,13 +2517,16 @@ mock_stirrer_new:
type: object
result:
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 aca63df..3ac61fc 100644
--- a/unilabos/registry/devices/organic_miscellaneous.yaml
+++ b/unilabos/registry/devices/organic_miscellaneous.yaml
@@ -242,9 +242,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
@@ -261,13 +265,25 @@ separator.homemade:
type: object
goal:
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
@@ -275,13 +291,16 @@ separator.homemade:
type: object
result:
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 2da45fd..fe4ad5b 100644
--- a/unilabos/registry/devices/temperature.yaml
+++ b/unilabos/registry/devices/temperature.yaml
@@ -259,11 +259,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:
@@ -281,22 +285,34 @@ heaterstirrer.dalong:
type: object
goal:
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
@@ -304,13 +320,16 @@ heaterstirrer.dalong:
type: object
result:
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 d8f8f19..0a263b4 100644
--- a/unilabos/registry/devices/virtual_device.yaml
+++ b/unilabos/registry/devices/virtual_device.yaml
@@ -248,6 +248,12 @@ virtual_column:
goal_default:
column: ''
from_vessel: ''
+ pct1: ''
+ pct2: ''
+ ratio: ''
+ rf: ''
+ solvent1: ''
+ solvent2: ''
to_vessel: ''
handles: []
result:
@@ -274,12 +280,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:
@@ -861,11 +885,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:
@@ -883,22 +911,34 @@ virtual_heatchill:
type: object
goal:
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
@@ -906,13 +946,16 @@ virtual_heatchill:
type: object
result:
properties:
+ message:
+ type: string
return_info:
type: string
success:
type: boolean
required:
- - return_info
- success
+ - message
+ - return_info
title: HeatChill_Result
type: object
required:
@@ -1630,13 +1673,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: []
@@ -1692,8 +1740,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
@@ -1704,10 +1758,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:
@@ -1723,6 +1781,11 @@ virtual_pump:
- rinsing_volume
- rinsing_repeats
- solid
+ - flowrate
+ - transfer_flowrate
+ - rate_spec
+ - event
+ - through
title: PumpTransfer_Goal
type: object
result:
@@ -1853,10 +1916,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
@@ -1865,6 +1926,7 @@ virtual_rotavap:
vessel: vessel
goal_default:
pressure: 0.0
+ solvent: ''
stir_speed: 0.0
temp: 0.0
time: 0.0
@@ -1923,6 +1985,8 @@ virtual_rotavap:
properties:
pressure:
type: number
+ solvent:
+ type: string
stir_speed:
type: number
temp:
@@ -1937,6 +2001,7 @@ virtual_rotavap:
- temp
- time
- stir_speed
+ - solvent
title: Evaporate_Goal
type: object
result:
@@ -2107,6 +2172,7 @@ virtual_separator:
goal_default:
from_vessel: ''
product_phase: ''
+ product_vessel: ''
purpose: ''
repeats: 0
separation_vessel: ''
@@ -2117,7 +2183,10 @@ virtual_separator:
stir_time: 0.0
through: ''
to_vessel: ''
+ vessel: ''
+ volume: ''
waste_phase_to_vessel: ''
+ waste_vessel: ''
handles: []
result:
message: message
@@ -2174,6 +2243,8 @@ virtual_separator:
type: string
product_phase:
type: string
+ product_vessel:
+ type: string
purpose:
type: string
repeats:
@@ -2196,8 +2267,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
@@ -2212,17 +2289,24 @@ virtual_separator:
- stir_time
- stir_speed
- settling_time
+ - vessel
+ - volume
+ - product_vessel
+ - waste_vessel
title: Separate_Goal
type: object
result:
properties:
+ message:
+ type: string
return_info:
type: string
success:
type: boolean
required:
- - return_info
- success
+ - message
+ - return_info
title: Separate_Result
type: object
required:
@@ -2381,6 +2465,26 @@ virtual_solenoid_valve:
title: is_closed参数
type: object
type: UniLabJsonCommand
+ auto-open:
+ feedback: {}
+ goal: {}
+ goal_default: {}
+ handles: []
+ result: {}
+ schema:
+ description: open的参数schema
+ properties:
+ feedback: {}
+ goal:
+ properties: {}
+ required: []
+ type: object
+ result: {}
+ required:
+ - goal
+ title: open参数
+ type: object
+ type: UniLabJsonCommandAsync
auto-reset:
feedback: {}
goal: {}
@@ -2401,6 +2505,53 @@ virtual_solenoid_valve:
title: reset参数
type: object
type: UniLabJsonCommandAsync
+ auto-set_state:
+ feedback: {}
+ goal: {}
+ goal_default:
+ command: null
+ handles: []
+ result: {}
+ schema:
+ description: set_state的参数schema
+ properties:
+ feedback: {}
+ goal:
+ properties:
+ command:
+ 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: []
+ type: object
+ result: {}
+ required:
+ - goal
+ title: set_valve_position参数
+ type: object
+ type: UniLabJsonCommandAsync
auto-toggle:
feedback: {}
goal: {}
@@ -2431,111 +2582,86 @@ virtual_solenoid_valve:
result:
success: success
schema:
- description: ''
+ description: ROS Action SendCmd 的 JSON Schema
properties:
feedback:
- properties:
- status:
- type: string
- required:
- - status
- title: SendCmd_Feedback
+ properties: {}
+ required: []
+ title: EmptyIn_Feedback
type: object
goal:
- properties:
- command:
- type: string
- required:
- - command
- title: SendCmd_Goal
+ properties: {}
+ required: []
+ title: EmptyIn_Goal
type: object
result:
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: ''
+ goal: {}
+ goal_default: {}
handles: []
- result:
- success: success
+ result: {}
schema:
- description: ''
+ description: ROS Action SendCmd 的 JSON Schema
properties:
feedback:
- properties:
- status:
- type: string
- required:
- - status
- title: SendCmd_Feedback
+ properties: {}
+ required: []
+ title: EmptyIn_Feedback
type: object
goal:
- properties:
- command:
- type: string
- required:
- - command
- title: SendCmd_Goal
+ properties: {}
+ required: []
+ title: EmptyIn_Goal
type: object
result:
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_state:
+ type: EmptyIn
+ set_status:
feedback: {}
goal:
- command: command
+ string: string
goal_default:
- command: ''
+ string: ''
handles: []
- result:
- success: success
+ result: {}
schema:
- description: ''
+ description: ROS Action SendCmd 的 JSON Schema
properties:
feedback:
- properties:
- status:
- type: string
- required:
- - status
- title: SendCmd_Feedback
+ properties: {}
+ required: []
+ title: StrSingleInput_Feedback
type: object
goal:
properties:
- command:
+ string:
type: string
required:
- - command
- title: SendCmd_Goal
+ - string
+ title: StrSingleInput_Goal
type: object
result:
properties:
@@ -2546,13 +2672,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
set_valve_position:
feedback: {}
goal:
@@ -2562,15 +2688,14 @@ virtual_solenoid_valve:
handles: []
result:
success: success
+ message: message
+ valve_position: valve_position
schema:
description: ''
properties:
feedback:
- properties:
- status:
- type: string
- required:
- - status
+ properties: {}
+ required: []
title: SendCmd_Feedback
type: object
goal:
@@ -2583,13 +2708,15 @@ virtual_solenoid_valve:
type: object
result:
properties:
- return_info:
- type: string
success:
type: boolean
+ message:
+ type: string
+ valve_position:
+ type: string
required:
- - return_info
- success
+ - message
title: SendCmd_Result
type: object
required:
@@ -2652,7 +2779,6 @@ virtual_solenoid_valve:
- valve_position
- state
type: object
- version: 0.0.1
virtual_stirrer:
class:
action_value_mappings:
@@ -2768,9 +2894,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
@@ -2787,13 +2917,25 @@ virtual_stirrer:
type: object
goal:
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
@@ -2801,13 +2943,16 @@ virtual_stirrer:
type: object
result:
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/work_station.yaml b/unilabos/registry/devices/work_station.yaml
index 91e4c4f..91ea9bd 100644
--- a/unilabos/registry/devices/work_station.yaml
+++ b/unilabos/registry/devices/work_station.yaml
@@ -54,9 +54,10 @@ workstation:
handles: []
result: {}
schema:
- description: ''
+ description: ROS Action AGVTransfer 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -65,6 +66,7 @@ workstation:
title: AGVTransfer_Feedback
type: object
goal:
+ description: Action 目标 - 从客户端发送到服务器
properties:
from_repo:
properties:
@@ -224,6 +226,7 @@ workstation:
title: AGVTransfer_Goal
type: object
result:
+ description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -254,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
@@ -283,9 +291,10 @@ workstation:
label: Vessel
result: {}
schema:
- description: ''
+ description: ROS Action Add 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
current_status:
type: string
@@ -297,13 +306,24 @@ workstation:
title: Add_Feedback
type: object
goal:
+ description: Action 目标 - 从客户端发送到服务器
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:
@@ -329,9 +349,15 @@ workstation:
- stir_speed
- viscous
- purpose
+ - event
+ - mol
+ - rate_spec
+ - equiv
+ - ratio
title: Add_Goal
type: object
result:
+ description: Action 结果 - 完成后从服务器发送到客户端
properties:
message:
type: string
@@ -380,9 +406,10 @@ workstation:
label: Vessel
result: {}
schema:
- description: ''
+ description: ROS Action AdjustPH 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
progress:
type: number
@@ -394,6 +421,7 @@ workstation:
title: AdjustPH_Feedback
type: object
goal:
+ description: Action 目标 - 从客户端发送到服务器
properties:
ph_value:
type: number
@@ -408,6 +436,7 @@ workstation:
title: AdjustPH_Goal
type: object
result:
+ description: Action 结果 - 完成后从服务器发送到客户端
properties:
message:
type: string
@@ -453,9 +482,10 @@ workstation:
label: Vessel
result: {}
schema:
- description: ''
+ description: ROS Action Centrifuge 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
current_speed:
type: number
@@ -473,6 +503,7 @@ workstation:
title: Centrifuge_Feedback
type: object
goal:
+ description: Action 目标 - 从客户端发送到服务器
properties:
speed:
type: number
@@ -490,6 +521,7 @@ workstation:
title: Centrifuge_Goal
type: object
result:
+ description: Action 结果 - 完成后从服务器发送到客户端
properties:
message:
type: string
@@ -542,9 +574,10 @@ workstation:
label: Vessel
result: {}
schema:
- description: ''
+ description: ROS Action Clean 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
current_device:
type: string
@@ -588,6 +621,7 @@ workstation:
title: Clean_Feedback
type: object
goal:
+ description: Action 目标 - 从客户端发送到服务器
properties:
repeats:
maximum: 2147483647
@@ -610,6 +644,7 @@ workstation:
title: Clean_Goal
type: object
result:
+ description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -659,9 +694,10 @@ workstation:
label: Vessel
result: {}
schema:
- description: ''
+ description: ROS Action CleanVessel 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
progress:
type: number
@@ -673,6 +709,7 @@ workstation:
title: CleanVessel_Feedback
type: object
goal:
+ description: Action 目标 - 从客户端发送到服务器
properties:
repeats:
maximum: 2147483647
@@ -695,6 +732,7 @@ workstation:
title: CleanVessel_Goal
type: object
result:
+ description: Action 结果 - 完成后从服务器发送到客户端
properties:
message:
type: string
@@ -725,6 +763,9 @@ workstation:
volume: volume
goal_default:
amount: ''
+ mass: ''
+ mol: ''
+ reagent: ''
solvent: ''
stir_speed: 0.0
temp: 0.0
@@ -751,9 +792,10 @@ workstation:
label: Vessel
result: {}
schema:
- description: ''
+ description: ROS Action Dissolve 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
progress:
type: number
@@ -765,9 +807,16 @@ workstation:
title: Dissolve_Feedback
type: object
goal:
+ description: Action 目标 - 从客户端发送到服务器
properties:
amount:
type: string
+ mass:
+ type: string
+ mol:
+ type: string
+ reagent:
+ type: string
solvent:
type: string
stir_speed:
@@ -788,9 +837,13 @@ workstation:
- temp
- time
- stir_speed
+ - mass
+ - mol
+ - reagent
title: Dissolve_Goal
type: object
result:
+ description: Action 结果 - 完成后从服务器发送到客户端
properties:
message:
type: string
@@ -832,9 +885,10 @@ workstation:
label: Vessel
result: {}
schema:
- description: ''
+ description: ROS Action Dry 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
progress:
type: number
@@ -846,6 +900,7 @@ workstation:
title: Dry_Feedback
type: object
goal:
+ description: Action 目标 - 从客户端发送到服务器
properties:
compound:
type: string
@@ -857,6 +912,7 @@ workstation:
title: Dry_Goal
type: object
result:
+ description: Action 结果 - 完成后从服务器发送到客户端
properties:
message:
type: string
@@ -879,11 +935,9 @@ workstation:
feedback: {}
goal:
gas: gas
- repeats: repeats
vessel: vessel
goal_default:
gas: ''
- repeats: 0
vessel: ''
handles:
input:
@@ -900,9 +954,10 @@ workstation:
label: Vessel
result: {}
schema:
- description: ''
+ description: ROS Action EvacuateAndRefill 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
current_device:
type: string
@@ -946,22 +1001,19 @@ workstation:
title: EvacuateAndRefill_Feedback
type: object
goal:
+ description: Action 目标 - 从客户端发送到服务器
properties:
gas:
type: string
- repeats:
- maximum: 2147483647
- minimum: -2147483648
- type: integer
vessel:
type: string
required:
- vessel
- gas
- - repeats
title: EvacuateAndRefill_Goal
type: object
result:
+ description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -981,12 +1033,14 @@ workstation:
feedback: {}
goal:
pressure: pressure
+ solvent: solvent
stir_speed: stir_speed
temp: temp
time: time
vessel: vessel
goal_default:
pressure: 0.0
+ solvent: ''
stir_speed: 0.0
temp: 0.0
time: 0.0
@@ -996,19 +1050,20 @@ workstation:
- 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: ''
+ description: ROS Action Evaporate 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
current_device:
type: string
@@ -1052,9 +1107,12 @@ workstation:
title: Evaporate_Feedback
type: object
goal:
+ description: Action 目标 - 从客户端发送到服务器
properties:
pressure:
type: number
+ solvent:
+ type: string
stir_speed:
type: number
temp:
@@ -1069,9 +1127,11 @@ workstation:
- temp
- time
- stir_speed
+ - solvent
title: Evaporate_Goal
type: object
result:
+ description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -1112,7 +1172,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
@@ -1123,16 +1183,17 @@ 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
label: Filtrate Vessel
result: {}
schema:
- description: ''
+ description: ROS Action Filter 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
current_status:
type: string
@@ -1150,6 +1211,7 @@ workstation:
title: Filter_Feedback
type: object
goal:
+ description: Action 目标 - 从客户端发送到服务器
properties:
continue_heatchill:
type: boolean
@@ -1176,6 +1238,7 @@ workstation:
title: Filter_Goal
type: object
result:
+ description: Action 结果 - 完成后从服务器发送到客户端
properties:
message:
type: string
@@ -1242,9 +1305,10 @@ workstation:
label: To Vessel
result: {}
schema:
- description: ''
+ description: ROS Action FilterThrough 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
progress:
type: number
@@ -1256,6 +1320,7 @@ workstation:
title: FilterThrough_Feedback
type: object
goal:
+ description: Action 目标 - 从客户端发送到服务器
properties:
eluting_repeats:
maximum: 2147483647
@@ -1284,6 +1349,7 @@ workstation:
title: FilterThrough_Goal
type: object
result:
+ description: Action 结果 - 完成后从服务器发送到客户端
properties:
message:
type: string
@@ -1305,18 +1371,26 @@ workstation:
HeatChillProtocol:
feedback: {}
goal:
+ pressure: pressure
purpose: purpose
+ reflux_solvent: reflux_solvent
stir: stir
stir_speed: stir_speed
temp: temp
+ temp_spec: temp_spec
time: time
+ time_spec: time_spec
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:
input:
@@ -1333,9 +1407,10 @@ workstation:
label: Vessel
result: {}
schema:
- description: ''
+ description: ROS Action HeatChill 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -1344,37 +1419,54 @@ workstation:
title: HeatChill_Feedback
type: object
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
title: HeatChill_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: HeatChill_Result
type: object
required:
@@ -1407,9 +1499,10 @@ workstation:
label: Vessel
result: {}
schema:
- description: ''
+ description: ROS Action HeatChillStart 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -1418,6 +1511,7 @@ workstation:
title: HeatChillStart_Feedback
type: object
goal:
+ description: Action 目标 - 从客户端发送到服务器
properties:
purpose:
type: string
@@ -1432,6 +1526,7 @@ workstation:
title: HeatChillStart_Goal
type: object
result:
+ description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -1468,9 +1563,10 @@ workstation:
label: Vessel
result: {}
schema:
- description: ''
+ description: ROS Action HeatChillStop 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -1479,6 +1575,7 @@ workstation:
title: HeatChillStop_Feedback
type: object
goal:
+ description: Action 目标 - 从客户端发送到服务器
properties:
vessel:
type: string
@@ -1487,6 +1584,7 @@ workstation:
title: HeatChillStop_Goal
type: object
result:
+ description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -1527,9 +1625,10 @@ workstation:
label: Vessel
result: {}
schema:
- description: ''
+ description: ROS Action Hydrogenate 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
progress:
type: number
@@ -1541,6 +1640,7 @@ workstation:
title: Hydrogenate_Feedback
type: object
goal:
+ description: Action 目标 - 从客户端发送到服务器
properties:
temp:
type: string
@@ -1555,6 +1655,7 @@ workstation:
title: Hydrogenate_Goal
type: object
result:
+ description: Action 结果 - 完成后从服务器发送到客户端
properties:
message:
type: string
@@ -1577,24 +1678,34 @@ workstation:
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
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:
@@ -1627,9 +1738,10 @@ workstation:
label: To Vessel
result: {}
schema:
- description: ''
+ description: ROS Action PumpTransfer 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
current_device:
type: string
@@ -1673,11 +1785,18 @@ workstation:
title: PumpTransfer_Feedback
type: object
goal:
+ description: Action 目标 - 从客户端发送到服务器
properties:
amount:
type: string
+ event:
+ type: string
+ flowrate:
+ type: number
from_vessel:
type: string
+ rate_spec:
+ type: string
rinsing_repeats:
maximum: 2147483647
minimum: -2147483648
@@ -1688,10 +1807,14 @@ workstation:
type: number
solid:
type: boolean
+ through:
+ type: string
time:
type: number
to_vessel:
type: string
+ transfer_flowrate:
+ type: number
viscous:
type: boolean
volume:
@@ -1707,9 +1830,15 @@ workstation:
- rinsing_volume
- rinsing_repeats
- solid
+ - flowrate
+ - transfer_flowrate
+ - rate_spec
+ - event
+ - through
title: PumpTransfer_Goal
type: object
result:
+ description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -1746,12 +1875,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
@@ -1764,9 +1893,10 @@ workstation:
label: Vessel
result: {}
schema:
- description: ''
+ description: ROS Action Recrystallize 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
progress:
type: number
@@ -1778,6 +1908,7 @@ workstation:
title: Recrystallize_Feedback
type: object
goal:
+ description: Action 目标 - 从客户端发送到服务器
properties:
ratio:
type: string
@@ -1798,6 +1929,7 @@ workstation:
title: Recrystallize_Goal
type: object
result:
+ description: Action 结果 - 完成后从服务器发送到客户端
properties:
message:
type: string
@@ -1832,9 +1964,10 @@ workstation:
output: []
result: {}
schema:
- description: ''
+ description: ROS Action ResetHandling 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
progress:
type: number
@@ -1846,6 +1979,7 @@ workstation:
title: ResetHandling_Feedback
type: object
goal:
+ description: Action 目标 - 从客户端发送到服务器
properties:
solvent:
type: string
@@ -1854,6 +1988,7 @@ workstation:
title: ResetHandling_Goal
type: object
result:
+ description: Action 结果 - 完成后从服务器发送到客户端
properties:
message:
type: string
@@ -1881,6 +2016,12 @@ workstation:
goal_default:
column: ''
from_vessel: ''
+ pct1: ''
+ pct2: ''
+ ratio: ''
+ rf: ''
+ solvent1: ''
+ solvent2: ''
to_vessel: ''
handles:
input:
@@ -1907,9 +2048,10 @@ workstation:
label: To Vessel
result: {}
schema:
- description: ''
+ description: ROS Action RunColumn 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
progress:
type: number
@@ -1921,20 +2063,40 @@ workstation:
title: RunColumn_Feedback
type: object
goal:
+ description: Action 目标 - 从客户端发送到服务器
properties:
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:
+ description: Action 结果 - 完成后从服务器发送到客户端
properties:
message:
type: string
@@ -1972,6 +2134,7 @@ workstation:
goal_default:
from_vessel: ''
product_phase: ''
+ product_vessel: ''
purpose: ''
repeats: 0
separation_vessel: ''
@@ -1982,7 +2145,10 @@ workstation:
stir_time: 0.0
through: ''
to_vessel: ''
+ vessel: ''
+ volume: ''
waste_phase_to_vessel: ''
+ waste_vessel: ''
handles:
input:
- data_key: vessel
@@ -2013,9 +2179,10 @@ workstation:
label: To Vessel
result: {}
schema:
- description: ''
+ description: ROS Action Separate 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
current_device:
type: string
@@ -2059,11 +2226,14 @@ workstation:
title: Separate_Feedback
type: object
goal:
+ description: Action 目标 - 从客户端发送到服务器
properties:
from_vessel:
type: string
product_phase:
type: string
+ product_vessel:
+ type: string
purpose:
type: string
repeats:
@@ -2086,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
@@ -2102,17 +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:
@@ -2145,9 +2329,10 @@ workstation:
label: Vessel
result: {}
schema:
- description: ''
+ description: ROS Action StartStir 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
current_speed:
type: number
@@ -2162,6 +2347,7 @@ workstation:
title: StartStir_Feedback
type: object
goal:
+ description: Action 目标 - 从客户端发送到服务器
properties:
purpose:
type: string
@@ -2176,6 +2362,7 @@ workstation:
title: StartStir_Goal
type: object
result:
+ description: Action 结果 - 完成后从服务器发送到客户端
properties:
message:
type: string
@@ -2197,13 +2384,21 @@ workstation:
StirProtocol:
feedback: {}
goal:
+ event: event
settling_time: settling_time
stir_speed: stir_speed
stir_time: stir_time
+ time: time
+ time_spec: time_spec
+ vessel: vessel
goal_default:
+ event: ''
settling_time: 0.0
stir_speed: 0.0
stir_time: 0.0
+ time: ''
+ time_spec: ''
+ vessel: ''
handles:
input:
- data_key: vessel
@@ -2219,9 +2414,10 @@ workstation:
label: Vessel
result: {}
schema:
- description: ''
+ description: ROS Action Stir 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -2230,28 +2426,45 @@ workstation:
title: Stir_Feedback
type: object
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
title: Stir_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: Stir_Result
type: object
required:
@@ -2280,9 +2493,10 @@ workstation:
label: Vessel
result: {}
schema:
- description: ''
+ description: ROS Action StopStir 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
current_status:
type: string
@@ -2294,6 +2508,7 @@ workstation:
title: StopStir_Feedback
type: object
goal:
+ description: Action 目标 - 从客户端发送到服务器
properties:
vessel:
type: string
@@ -2302,6 +2517,7 @@ workstation:
title: StopStir_Goal
type: object
result:
+ description: Action 结果 - 完成后从服务器发送到客户端
properties:
message:
type: string
@@ -2374,9 +2590,10 @@ workstation:
label: To Vessel
result: {}
schema:
- description: ''
+ description: ROS Action Transfer 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
current_status:
type: string
@@ -2391,6 +2608,7 @@ workstation:
title: Transfer_Feedback
type: object
goal:
+ description: Action 目标 - 从客户端发送到服务器
properties:
amount:
type: string
@@ -2428,6 +2646,7 @@ workstation:
title: Transfer_Goal
type: object
result:
+ description: Action 结果 - 完成后从服务器发送到客户端
properties:
message:
type: string
@@ -2480,8 +2699,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
@@ -2491,16 +2710,17 @@ 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
label: Filtrate Vessel
result: {}
schema:
- description: ''
+ description: ROS Action WashSolid 的 JSON Schema
properties:
feedback:
+ description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
progress:
type: number
@@ -2512,6 +2732,7 @@ workstation:
title: WashSolid_Feedback
type: object
goal:
+ description: Action 目标 - 从客户端发送到服务器
properties:
filtrate_vessel:
type: string
@@ -2546,6 +2767,7 @@ workstation:
title: WashSolid_Goal
type: object
result:
+ description: Action 结果 - 完成后从服务器发送到客户端
properties:
message:
type: string
@@ -2679,4 +2901,3 @@ workstation:
properties: {}
required: []
type: object
- version: 0.0.1
diff --git a/unilabos/ros/nodes/presets/protocol_node.py b/unilabos/ros/nodes/presets/protocol_node.py
index d2eab27..5c8ffbd 100644
--- a/unilabos/ros/nodes/presets/protocol_node.py
+++ b/unilabos/ros/nodes/presets/protocol_node.py
@@ -211,7 +211,7 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
# 逐步执行工作流
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":
diff --git a/unilabos_msgs/CMakeLists.txt b/unilabos_msgs/CMakeLists.txt
index 7e8a146..b2c1bc7 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/Add.action b/unilabos_msgs/action/Add.action
index de06c6a..021199a 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 # 试剂名称(必需)
+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 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/AddSolid.action b/unilabos_msgs/action/AddSolid.action
new file mode 100644
index 0000000..8812441
--- /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
diff --git a/unilabos_msgs/action/Dissolve.action b/unilabos_msgs/action/Dissolve.action
index 6b860d0..f070a61 100644
--- a/unilabos_msgs/action/Dissolve.action
+++ b/unilabos_msgs/action/Dissolve.action
@@ -1,14 +1,21 @@
-string vessel # 装有要溶解物质的容器名称
-string solvent # 用于溶解物质的溶剂名称
-float64 volume # 溶剂的体积,可选参数
-string amount # 要溶解物质的量,可选参数
-float64 temp # 溶解时的温度,可选参数
-float64 time # 溶解的时间,可选参数
-float64 stir_speed # 搅拌速度,可选参数
+# Goal - 溶解操作的目标参数
+string vessel # 装有要溶解物质的容器名称(必需)
+string solvent # 用于溶解物质的溶剂名称(可选)
+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 # 操作是否成功
string message # 结果消息
string return_info
---
+# Feedback - 实时反馈
string status # 当前状态描述
float64 progress # 进度百分比 (0-100)
\ No newline at end of file
diff --git a/unilabos_msgs/action/EvacuateAndRefill.action b/unilabos_msgs/action/EvacuateAndRefill.action
index 22ffc65..461cb28 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 45887f2..9cecb62 100644
--- a/unilabos_msgs/action/Evaporate.action
+++ b/unilabos_msgs/action/Evaporate.action
@@ -1,9 +1,10 @@
-# Organic
-string vessel
-float64 pressure
-float64 temp
-float64 time
-float64 stir_speed
+# Organic Synthesis Station Evaporate Action
+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/Filter.action b/unilabos_msgs/action/Filter.action
index 564df1a..42a8913 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 87ebf52..1e7025e 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)
+string time # 🔧 加热时间(支持带单位,如"5 min","300",默认秒)
+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 69d22b6..c8ca445 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/Recrystallize.action b/unilabos_msgs/action/Recrystallize.action
index fe727e8..2ae42bf 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/RunColumn.action b/unilabos_msgs/action/RunColumn.action
index 3fba948..1f8e9ba 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 fe8976a..fc185b6 100644
--- a/unilabos_msgs/action/Separate.action
+++ b/unilabos_msgs/action/Separate.action
@@ -1,22 +1,27 @@
-# 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 vessel # 分离容器名称(XDL参数,必需)
+string purpose # 分离目的 ('wash', 'extract', 'separate',可选)
+string product_phase # 产物相 ('top', 'bottom',可选)
+string from_vessel # 源容器(可选)
+string separation_vessel # 分离容器(与vessel同义,可选)
+string to_vessel # 目标容器(可选)
+string waste_phase_to_vessel # 废相目标容器(可选)
+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 # 操作是否成功
+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 # 当前状态描述
+float64 progress # 进度百分比 (0-100)
diff --git a/unilabos_msgs/action/Stir.action b/unilabos_msgs/action/Stir.action
index 9542f9d..e3e5580 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", "300",默认秒)
+string event # 事件标识(如 "A", "B")
+string time_spec # 时间规格(如 "several minutes")
+float64 stir_time # 解析后的搅拌时间(秒)
+float64 stir_speed # 搅拌速度(默认200.0)
+string settling_time # 🔧 沉降时间(支持带单位,默认秒)
---
+# 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 cb57e5c..281ca4c 100644
--- a/unilabos_msgs/action/WashSolid.action
+++ b/unilabos_msgs/action/WashSolid.action
@@ -1,16 +1,23 @@
-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 # 清洗溶剂名称(必需)
+string volume # 🔧 体积(支持数字和带单位的字符串,如"100 mL","?")
+string filtrate_vessel # 滤液收集容器(可选,默认"")
+float64 temp # 清洗温度(可选,默认25.0)
+bool stir # 是否搅拌(可选,默认false)
+float64 stir_speed # 搅拌速度(可选,默认0.0)
+string time # 🔧 清洗时间(支持带单位,如"5 min","300 s",默认秒)
+int32 repeats # 重复次数(与repeats_spec二选一)
+string volume_spec # 体积规格(优先级高于volume)
+string repeats_spec # 重复次数规格(优先级高于repeats)
+string mass # 固体质量描述(可选)
+string event # 事件标识符(可选)
---
-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