fix all protocol_compilers and remove deprecated devices

This commit is contained in:
Junhan Chang
2025-08-11 15:01:04 +08:00
parent fe1a029a9b
commit 50e93cb1af
28 changed files with 1051 additions and 2014 deletions

View File

@@ -49,7 +49,6 @@
"config": {
"protocol_type": [
"AddProtocol",
"TransferProtocol",
"StartStirProtocol",
"StopStirProtocol",
"StirProtocol",
@@ -1077,7 +1076,7 @@
"target": "solid_dispenser_1",
"type": "resource",
"port": {
"solid_reagent_bottle_1": "top",
"solid_reagent_bottle_1": "bottom",
"solid_dispenser_1": "SolidIn"
}
},
@@ -1087,7 +1086,7 @@
"target": "solid_dispenser_1",
"type": "resource",
"port": {
"solid_reagent_bottle_2": "top",
"solid_reagent_bottle_2": "bottom",
"solid_dispenser_1": "SolidIn"
}
},
@@ -1097,7 +1096,7 @@
"target": "solid_dispenser_1",
"type": "resource",
"port": {
"solid_reagent_bottle_3": "top",
"solid_reagent_bottle_3": "bottom",
"solid_dispenser_1": "SolidIn"
}
}

View File

@@ -15,7 +15,6 @@ from .heatchill_protocol import (
generate_heat_chill_to_temp_protocol # 保留导入,但不注册为协议
)
from .stir_protocol import generate_stir_protocol, generate_start_stir_protocol, generate_stop_stir_protocol
from .transfer_protocol import generate_transfer_protocol
from .clean_vessel_protocol import generate_clean_vessel_protocol
from .dissolve_protocol import generate_dissolve_protocol
from .filter_through_protocol import generate_filter_through_protocol
@@ -54,6 +53,5 @@ action_protocol_generators = {
StartStirProtocol: generate_start_stir_protocol,
StirProtocol: generate_stir_protocol,
StopStirProtocol: generate_stop_stir_protocol,
TransferProtocol: generate_transfer_protocol,
WashSolidProtocol: generate_wash_solid_protocol,
}

View File

@@ -1,314 +1,24 @@
from functools import partial
import networkx as nx
import re
import logging
from typing import List, Dict, Any, Union
from .utils.vessel_parser import get_vessel
from .utils.unit_parser import parse_volume_input, parse_mass_input, parse_time_input
from .utils.vessel_parser import get_vessel, find_solid_dispenser, find_connected_stirrer, find_reagent_vessel
from .utils.logger_util import action_log
from .pump_protocol import generate_pump_protocol_with_rinsing
logger = logging.getLogger(__name__)
def debug_print(message):
"""调试输出"""
print(f"[ADD] {message}", flush=True)
logger.info(f"[ADD] {message}")
def parse_volume_input(volume_input: Union[str, float]) -> float:
"""
解析体积输入,支持带单位的字符串
Args:
volume_input: 体积输入(如 "2.7 mL", "2.67 mL", "?", 10.0
Returns:
float: 体积(毫升)
"""
if isinstance(volume_input, (int, float)):
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 = 10.0 # 默认10mL
debug_print(f"❓ 检测到未知体积,使用默认值: {default_volume}mL 🎯")
return default_volume
# 移除空格并提取数字和单位
volume_clean = re.sub(r'\s+', '', volume_str)
# 匹配数字和单位的正则表达式
match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
if not match:
debug_print(f"❌ 无法解析体积: '{volume_str}'使用默认值10mL")
return 10.0
value = float(match.group(1))
unit = match.group(2) or 'ml' # 默认单位为毫升
# 转换为毫升
if unit in ['l', 'liter']:
volume = value * 1000.0 # L -> mL
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: 质量输入(如 "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_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():
debug_print(f"✅ 通过液体类型匹配到容器: {node_id} 💧")
return node_id
# 🔧 方法4使用第一个试剂瓶作为备选
debug_print(f"📋 方法4: 查找备选试剂瓶...")
for node_id in G.nodes():
node_data = G.nodes[node_id]
if (node_data.get('type') == 'container' and
('reagent' in node_id.lower() or 'bottle' in node_id.lower())):
debug_print(f"⚠️ 未找到专用容器,使用备选试剂瓶: {node_id} 🔄")
return node_id
debug_print(f"❌ 所有方法都失败了,无法找到容器!")
raise ValueError(f"找不到试剂 '{reagent}' 对应的容器")
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
"""查找连接到指定容器的搅拌器"""
debug_print(f"🔍 查找连接到容器 '{vessel}' 的搅拌器...")
stirrer_nodes = []
for node in G.nodes():
node_class = G.nodes[node].get('class', '').lower()
if 'stirrer' in node_class:
stirrer_nodes.append(node)
debug_print(f"📋 发现搅拌器: {node}")
debug_print(f"📊 共找到 {len(stirrer_nodes)} 个搅拌器")
# 查找连接到容器的搅拌器
for stirrer in stirrer_nodes:
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
debug_print(f"✅ 找到连接的搅拌器: {stirrer} 🔗")
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 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
}
}
create_action_log = partial(action_log, prefix="[ADD]")
def generate_add_protocol(
G: nx.DiGraph,
@@ -398,12 +108,7 @@ def generate_add_protocol(
final_time = parse_time_input(time)
debug_print(f"📊 解析结果:")
debug_print(f" 📏 体积: {final_volume}mL")
debug_print(f" ⚖️ 质量: {final_mass}g")
debug_print(f" ⏱️ 时间: {final_time}s")
debug_print(f" 🧬 摩尔: '{mol}'")
debug_print(f" 🎯 事件: '{event}'")
debug_print(f" ⚡ 速率: '{rate_spec}'")
debug_print(f" 体积: {final_volume}mL, 质量: {final_mass}g, 时间: {final_time}s, 摩尔: '{mol}', 事件: '{event}', 速率: '{rate_spec}'")
# === 判断添加类型 ===
debug_print("🔍 步骤3: 判断添加类型...")

View File

@@ -8,25 +8,8 @@ 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:
"""
查找酸碱试剂容器,支持多种匹配模式

View File

@@ -1,102 +1,9 @@
from typing import List, Dict, Any
import networkx as nx
from .utils.vessel_parser import get_vessel
from .utils.vessel_parser import get_vessel, find_solvent_vessel
from .pump_protocol import generate_pump_protocol
def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
"""
查找溶剂容器,支持多种匹配模式:
1. 容器名称匹配(如 flask_water, reagent_bottle_1-DMF
2. 容器内液体类型匹配(如 liquid_type: "DMF", "ethanol"
"""
print(f"CLEAN_VESSEL: 正在查找溶剂 '{solvent}' 的容器...")
# 第一步:通过容器名称匹配
possible_names = [
f"flask_{solvent}", # flask_water, flask_ethanol
f"bottle_{solvent}", # bottle_water, bottle_ethanol
f"vessel_{solvent}", # vessel_water, vessel_ethanol
f"{solvent}_flask", # water_flask, ethanol_flask
f"{solvent}_bottle", # water_bottle, ethanol_bottle
f"{solvent}", # 直接用溶剂名
f"solvent_{solvent}", # solvent_water, solvent_ethanol
f"reagent_bottle_{solvent}", # reagent_bottle_DMF
]
# 尝试名称匹配
for vessel_name in possible_names:
if vessel_name in G.nodes():
print(f"CLEAN_VESSEL: 通过名称匹配找到容器: {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 (solvent.lower() in node_id.lower() or
solvent.lower() in node_name):
print(f"CLEAN_VESSEL: 通过模糊名称匹配找到容器: {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() == solvent.lower() or
reagent_name.lower() == solvent.lower() or
config_reagent.lower() == solvent.lower()):
print(f"CLEAN_VESSEL: 通过液体类型匹配找到容器: {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"CLEAN_VESSEL: 可用容器列表:")
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"未找到溶剂 '{solvent}' 的容器。尝试了名称匹配: {possible_names}")
def find_solvent_vessel_by_any_match(G: nx.DiGraph, solvent: str) -> str:
"""
增强版溶剂容器查找,支持各种匹配方式的别名函数

View File

@@ -1,33 +1,22 @@
from functools import partial
import networkx as nx
import re
import logging
from typing import List, Dict, Any, Union
from unilabos.compile.utils.vessel_parser import get_vessel
from .utils.vessel_parser import get_vessel
from .utils.logger_util import action_log
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
}
}
create_action_log = partial(action_log, prefix="[DISSOLVE]")
def parse_volume_input(volume_input: Union[str, float]) -> float:
"""

View File

@@ -1,9 +1,12 @@
from functools import partial
import networkx as nx
import logging
import uuid
import sys
from typing import List, Dict, Any, Optional
from .utils.vessel_parser import get_vessel
from .utils.logger_util import action_log
from .pump_protocol import generate_pump_protocol_with_rinsing, generate_pump_protocol
# 设置日志
@@ -22,48 +25,17 @@ 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
}
}
create_action_log = partial(action_log, prefix="[抽真空充气]")
def find_gas_source(G: nx.DiGraph, gas: str) -> str:
"""

View File

@@ -3,75 +3,14 @@ import networkx as nx
import logging
import re
from .utils.vessel_parser import get_vessel
from .utils.unit_parser import parse_time_input
logger = logging.getLogger(__name__)
def debug_print(message):
"""调试输出"""
print(f"🧪 [EVAPORATE] {message}", flush=True)
logger.info(f"[EVAPORATE] {message}")
def parse_time_input(time_input: Union[str, float]) -> float:
"""
解析时间输入,支持带单位的字符串
Args:
time_input: 时间输入(如 "3 min", "180", "0.5 h" 等)
Returns:
float: 时间(秒)
"""
if isinstance(time_input, (int, float)):
debug_print(f"⏱️ 时间输入为数字: {time_input}s ✨")
return float(time_input) # 🔧 确保返回float
if not time_input or not str(time_input).strip():
debug_print(f"⚠️ 时间输入为空,使用默认值: 180s (3分钟) 🕐")
return 180.0 # 默认3分钟
time_str = str(time_input).lower().strip()
debug_print(f"🔍 解析时间输入: '{time_str}' 📝")
# 处理未知时间
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 float(value) # 🔧 确保返回float
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 float(time_sec) # 🔧 确保返回float
def find_rotavap_device(G: nx.DiGraph, vessel: str = None) -> Optional[str]:
"""

View File

@@ -8,7 +8,6 @@ logger = logging.getLogger(__name__)
def debug_print(message):
"""调试输出"""
print(f"🧪 [FILTER] {message}", flush=True)
logger.info(f"[FILTER] {message}")
def find_filter_device(G: nx.DiGraph) -> str:
@@ -52,7 +51,7 @@ def validate_vessel(G: nx.DiGraph, vessel: str, vessel_type: str = "容器") ->
def generate_filter_protocol(
G: nx.DiGraph,
vessel: dict, # 🔧 修改:从字符串改为字典类型
filtrate_vessel: str = "",
filtrate_vessel: dict = {"id": "waste"},
**kwargs
) -> List[Dict[str, Any]]:
"""
@@ -70,6 +69,7 @@ def generate_filter_protocol(
# 🔧 核心修改从字典中提取容器ID
vessel_id, vessel_data = get_vessel(vessel)
filtrate_vessel_id, filtrate_vessel_data = get_vessel(filtrate_vessel)
debug_print("🌊" * 20)
debug_print("🚀 开始生成过滤协议(支持体积运算)✨")
@@ -103,7 +103,7 @@ def generate_filter_protocol(
# 验证可选参数
debug_print(" 🔍 验证可选参数...")
if filtrate_vessel:
validate_vessel(G, filtrate_vessel, "滤液容器")
validate_vessel(G, filtrate_vessel_id, "滤液容器")
debug_print(" 🌊 模式: 过滤并收集滤液 💧")
else:
debug_print(" 🧱 模式: 过滤并收集固体 🔬")
@@ -213,7 +213,7 @@ def generate_filter_protocol(
debug_print(" ⚙️ 构建过滤参数...")
filter_kwargs = {
"vessel": filter_device, # 过滤器设备
"filtrate_vessel": filtrate_vessel, # 滤液容器(可能为空)
"filtrate_vessel": filtrate_vessel_id, # 滤液容器(可能为空)
"stir": kwargs.get("stir", False),
"stir_speed": kwargs.get("stir_speed", 0.0),
"temp": kwargs.get("temp", 25.0),
@@ -245,7 +245,7 @@ def generate_filter_protocol(
debug_print("📍 步骤5: 收集滤液... 💧")
if filtrate_vessel:
debug_print(f" 🧪 收集滤液: {filter_device}{filtrate_vessel} 💧")
debug_print(f" 🧪 收集滤液: {filter_device}{filtrate_vessel_id} 💧")
try:
debug_print(" 🔄 开始执行收集操作...")
@@ -274,20 +274,20 @@ def generate_filter_protocol(
debug_print(" 🔧 更新滤液容器体积...")
# 更新filtrate_vessel在图中的体积如果它是节点
if filtrate_vessel in G.nodes():
if 'data' not in G.nodes[filtrate_vessel]:
G.nodes[filtrate_vessel]['data'] = {}
if filtrate_vessel_id in G.nodes():
if 'data' not in G.nodes[filtrate_vessel_id]:
G.nodes[filtrate_vessel_id]['data'] = {}
current_filtrate_volume = G.nodes[filtrate_vessel]['data'].get('liquid_volume', 0.0)
current_filtrate_volume = G.nodes[filtrate_vessel_id]['data'].get('liquid_volume', 0.0)
if isinstance(current_filtrate_volume, list):
if len(current_filtrate_volume) > 0:
G.nodes[filtrate_vessel]['data']['liquid_volume'][0] += expected_filtrate_volume
G.nodes[filtrate_vessel_id]['data']['liquid_volume'][0] += expected_filtrate_volume
else:
G.nodes[filtrate_vessel]['data']['liquid_volume'] = [expected_filtrate_volume]
G.nodes[filtrate_vessel_id]['data']['liquid_volume'] = [expected_filtrate_volume]
else:
G.nodes[filtrate_vessel]['data']['liquid_volume'] = current_filtrate_volume + expected_filtrate_volume
G.nodes[filtrate_vessel_id]['data']['liquid_volume'] = current_filtrate_volume + expected_filtrate_volume
debug_print(f" 📊 滤液容器 {filtrate_vessel} 体积增加 {expected_filtrate_volume:.2f}mL")
debug_print(f" 📊 滤液容器 {filtrate_vessel_id} 体积增加 {expected_filtrate_volume:.2f}mL")
else:
debug_print(" ⚠️ 收集协议返回空序列 🤔")
@@ -352,7 +352,7 @@ def generate_filter_protocol(
debug_print(f"📊 总动作数: {len(action_sequence)} 个 📝")
debug_print(f"🥽 过滤容器: {vessel_id} 🧪")
debug_print(f"🌊 过滤器设备: {filter_device} 🔧")
debug_print(f"💧 滤液容器: {filtrate_vessel or '无(保留固体)'} 🧱")
debug_print(f"💧 滤液容器: {filtrate_vessel_id or '无(保留固体)'} 🧱")
debug_print(f"⏱️ 预计总时间: {(len(action_sequence) * 5):.0f} 秒 ⌛")
if original_liquid_volume > 0:
debug_print(f"📊 体积变化统计:")
@@ -364,4 +364,3 @@ def generate_filter_protocol(
debug_print("🎊" * 20)
return action_sequence

View File

@@ -3,81 +3,14 @@ import networkx as nx
import logging
import re
from .utils.vessel_parser import get_vessel
from .utils.unit_parser import parse_time_input
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:
"""
@@ -287,7 +220,6 @@ def generate_heat_chill_protocol(
"device_id": heatchill_id,
"action_name": "heat_chill",
"action_kwargs": {
"vessel": vessel_id, # 🔧 使用 vessel_id
"temp": float(final_temp),
"time": float(final_time),
"stir": bool(stir),
@@ -321,7 +253,7 @@ def generate_heat_chill_to_temp_protocol(
**kwargs
) -> List[Dict[str, Any]]:
"""生成加热到指定温度的协议(简化版)"""
vessel_id = vessel["id"]
vessel_id, _ = get_vessel(vessel)
debug_print(f"🌡️ 生成加热到温度协议: {vessel_id}{temp}°C")
return generate_heat_chill_protocol(G, vessel, temp, time, **kwargs)
@@ -335,7 +267,7 @@ def generate_heat_chill_start_protocol(
"""生成开始加热操作的协议序列"""
# 🔧 核心修改从字典中提取容器ID
vessel_id = vessel["id"]
vessel_id, _ = get_vessel(vessel)
debug_print("🔥 开始生成启动加热协议 ✨")
debug_print(f"🥽 vessel: {vessel} (ID: {vessel_id}), 🌡️ temp: {temp}°C")
@@ -353,7 +285,6 @@ def generate_heat_chill_start_protocol(
"device_id": heatchill_id,
"action_name": "heat_chill_start",
"action_kwargs": {
"vessel": vessel_id, # 🔧 使用 vessel_id
"temp": temp,
"purpose": purpose or f"开始加热到 {temp}°C"
}
@@ -370,7 +301,7 @@ def generate_heat_chill_stop_protocol(
"""生成停止加热操作的协议序列"""
# 🔧 核心修改从字典中提取容器ID
vessel_id = vessel["id"]
vessel_id, _ = get_vessel(vessel)
debug_print("🛑 开始生成停止加热协议 ✨")
debug_print(f"🥽 vessel: {vessel} (ID: {vessel_id})")
@@ -388,10 +319,8 @@ def generate_heat_chill_stop_protocol(
"device_id": heatchill_id,
"action_name": "heat_chill_stop",
"action_kwargs": {
"vessel": vessel_id # 🔧 使用 vessel_id
}
}]
debug_print(f"✅ 停止加热协议生成完成 🎯")
return action_sequence

View File

@@ -10,11 +10,7 @@ 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()
# 同时写入日志
output = f"[TRANSFER] {message}"
logger.info(output)
def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:

View File

@@ -3,91 +3,16 @@ import re
import logging
from typing import List, Dict, Any, Tuple, Union
from .utils.vessel_parser import get_vessel
from .utils.unit_parser import parse_volume_input
from .pump_protocol import generate_pump_protocol_with_rinsing
logger = logging.getLogger(__name__)
def debug_print(message):
"""调试输出"""
print(f"💎 [RECRYSTALLIZE] {message}", flush=True)
logger.info(f"[RECRYSTALLIZE] {message}")
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]:
"""
解析比例字符串,支持多种格式
@@ -137,131 +62,6 @@ def parse_ratio(ratio_str: str) -> Tuple[float, float]:
return 1.0, 1.0
def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
"""
查找溶剂容器
Args:
G: 网络图
solvent: 溶剂名称
Returns:
str: 溶剂容器ID
"""
debug_print(f"🔍 正在查找溶剂 '{solvent}' 的容器... 🧪")
# 构建可能的容器名称
possible_names = [
f"flask_{solvent}",
f"bottle_{solvent}",
f"reagent_{solvent}",
f"reagent_bottle_{solvent}",
f"{solvent}_flask",
f"{solvent}_bottle",
f"{solvent}",
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():
debug_print(f" 🎉 通过名称匹配找到容器: {vessel_name}")
return vessel_name
# 第二步通过模糊匹配节点ID和名称
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:
debug_print(f" 🎉 通过模糊匹配找到容器: {node_id} (名称: {node_name}) ✨")
return node_id
# 第三步:通过配置中的试剂信息匹配
debug_print(" 🧪 步骤3: 配置试剂信息匹配...")
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
# 检查 config 中的 reagent 字段
node_config = G.nodes[node_id].get('config', {})
config_reagent = node_config.get('reagent', '').lower()
if config_reagent and solvent.lower() == config_reagent:
debug_print(f" 🎉 通过config.reagent匹配找到容器: {node_id} (试剂: {config_reagent}) ✨")
return node_id
# 第四步:通过数据中的试剂信息匹配
debug_print(" 🧪 步骤4: 数据试剂信息匹配...")
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
vessel_data = G.nodes[node_id].get('data', {})
# 检查 data 中的 reagent_name 字段
reagent_name = vessel_data.get('reagent_name', '').lower()
if reagent_name and solvent.lower() == reagent_name:
debug_print(f" 🎉 通过data.reagent_name匹配找到容器: {node_id} (试剂: {reagent_name}) ✨")
return node_id
# 检查 data 中的液体信息
liquids = vessel_data.get('liquid', [])
for liquid in liquids:
if isinstance(liquid, dict):
liquid_type = (liquid.get('liquid_type') or liquid.get('name', '')).lower()
if solvent.lower() in liquid_type:
debug_print(f" 🎉 通过液体类型匹配找到容器: {node_id} (液体类型: {liquid_type}) ✨")
return node_id
# 第五步:部分匹配(如果前面都没找到)
debug_print(" 🔍 步骤5: 部分匹配...")
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
node_config = G.nodes[node_id].get('config', {})
node_data = G.nodes[node_id].get('data', {})
node_name = G.nodes[node_id].get('name', '').lower()
config_reagent = node_config.get('reagent', '').lower()
data_reagent = node_data.get('reagent_name', '').lower()
# 检查是否包含溶剂名称
if (solvent.lower() in config_reagent or
solvent.lower() in data_reagent or
solvent.lower() in node_name or
solvent.lower() in node_id.lower()):
debug_print(f" 🎉 通过部分匹配找到容器: {node_id}")
debug_print(f" - 节点名称: {node_name}")
debug_print(f" - 配置试剂: {config_reagent}")
debug_print(f" - 数据试剂: {data_reagent}")
return node_id
# 调试信息:列出所有容器
debug_print(" 🔎 调试信息:列出所有容器...")
container_list = []
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
node_config = G.nodes[node_id].get('config', {})
node_data = G.nodes[node_id].get('data', {})
node_name = G.nodes[node_id].get('name', '')
container_info = {
'id': node_id,
'name': node_name,
'config_reagent': node_config.get('reagent', ''),
'data_reagent': node_data.get('reagent_name', '')
}
container_list.append(container_info)
debug_print(f" - 容器: {node_id}, 名称: {node_name}, config试剂: {node_config.get('reagent', '')}, data试剂: {node_data.get('reagent_name', '')}")
debug_print(f"❌ 找不到溶剂 '{solvent}' 对应的容器 😭")
debug_print(f"🔍 查找的溶剂: '{solvent}' (小写: '{solvent.lower()}')")
debug_print(f"📊 总共发现 {len(container_list)} 个容器")
raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器")
def generate_recrystallize_protocol(
G: nx.DiGraph,
vessel: dict, # 🔧 修改:从字符串改为字典类型
@@ -322,7 +122,7 @@ def generate_recrystallize_protocol(
# 2. 解析体积(支持单位)
debug_print("📍 步骤2: 解析体积(支持单位)... 💧")
final_volume = parse_volume_with_units(volume, "mL")
final_volume = parse_volume_input(volume, "mL")
debug_print(f"🎯 体积解析完成: {volume}{final_volume}mL ✨")
# 3. 解析比例
@@ -574,7 +374,7 @@ def test_recrystallize_protocol():
debug_print("💧 测试体积解析...")
test_volumes = ["100 mL", "2.5 L", "500", "50.5", "?", "invalid"]
for vol in test_volumes:
parsed = parse_volume_with_units(vol)
parsed = parse_volume_input(vol)
debug_print(f" 📊 体积 '{vol}' -> {parsed}mL")
# 测试比例解析

View File

@@ -8,7 +8,6 @@ logger = logging.getLogger(__name__)
def debug_print(message):
"""调试输出"""
print(f"🏛️ [RUN_COLUMN] {message}", flush=True)
logger.info(f"[RUN_COLUMN] {message}")
def parse_percentage(pct_str: str) -> float:

View File

@@ -1,9 +1,12 @@
from functools import partial
import networkx as nx
import re
import logging
import sys
from typing import List, Dict, Any, Union
from .utils.vessel_parser import get_vessel
from .utils.logger_util import action_log
from .pump_protocol import generate_pump_protocol_with_rinsing
logger = logging.getLogger(__name__)
@@ -21,48 +24,17 @@ def debug_print(message):
try:
# 确保消息是字符串格式
safe_message = str(message)
print(f"🌀 [SEPARATE] {safe_message}", flush=True)
logger.info(f"[SEPARATE] {safe_message}")
except UnicodeEncodeError:
# 如果编码失败,尝试替换不支持的字符
safe_message = str(message).encode('utf-8', errors='replace').decode('utf-8')
print(f"🌀 [SEPARATE] {safe_message}", flush=True)
logger.info(f"[SEPARATE] {safe_message}")
except Exception as e:
# 最后的安全措施
fallback_message = f"日志输出错误: {repr(message)}"
print(f"🌀 [SEPARATE] {fallback_message}", flush=True)
logger.info(f"[SEPARATE] {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
}
}
create_action_log = partial(action_log, prefix="[SEPARATE]")
def generate_separate_protocol(

View File

@@ -3,81 +3,14 @@ import networkx as nx
import logging
import re
from .utils.unit_parser import parse_time_input
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:
"""查找与指定容器相连的搅拌设备"""

View File

@@ -1,79 +0,0 @@
from typing import List, Dict, Any
import networkx as nx
def generate_transfer_protocol(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
volume: float,
amount: str = "",
time: float = 0,
viscous: bool = False,
rinsing_solvent: str = "",
rinsing_volume: float = 0.0,
rinsing_repeats: int = 0,
solid: bool = False
) -> List[Dict[str, Any]]:
"""
生成液体转移操作的协议序列
Args:
G: 有向图,节点为设备和容器
from_vessel: 源容器
to_vessel: 目标容器
volume: 转移体积 (mL)
amount: 数量描述 (可选)
time: 转移时间 (秒,可选)
viscous: 是否为粘性液体
rinsing_solvent: 冲洗溶剂 (可选)
rinsing_volume: 冲洗体积 (mL可选)
rinsing_repeats: 冲洗重复次数
solid: 是否涉及固体
Returns:
List[Dict[str, Any]]: 转移操作的动作序列
Raises:
ValueError: 当找不到合适的转移设备时抛出异常
Examples:
transfer_protocol = generate_transfer_protocol(G, "flask_1", "reactor", 10.0)
"""
action_sequence = []
# 查找虚拟转移泵设备用于液体转移 - 修复:应该查找 virtual_transfer_pump
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]
# 验证容器是否存在
if from_vessel not in G.nodes():
raise ValueError(f"源容器 {from_vessel} 不存在于图中")
if to_vessel not in G.nodes():
raise ValueError(f"目标容器 {to_vessel} 不存在于图中")
# 执行液体转移操作 - 参数完全匹配Transfer.action
action_sequence.append({
"device_id": pump_id,
"action_name": "transfer",
"action_kwargs": {
"from_vessel": from_vessel,
"to_vessel": to_vessel,
"volume": volume,
"amount": amount,
"time": time,
"viscous": viscous,
"rinsing_solvent": rinsing_solvent,
"rinsing_volume": rinsing_volume,
"rinsing_repeats": rinsing_repeats,
"solid": solid
}
})
return action_sequence

View File

@@ -0,0 +1,36 @@
# 🆕 创建进度日志动作
import logging
from typing import Dict, Any
logger = logging.getLogger(__name__)
def debug_print(message, prefix="[UNIT_PARSER]"):
"""调试输出"""
logger.info(f"{prefix} {message}")
def action_log(message: str, emoji: str = "📝", prefix="[HIGH-LEVEL OPERATION]") -> Dict[str, Any]:
"""创建一个动作日志 - 支持中文和emoji"""
try:
full_message = f"{prefix} {emoji} {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"{prefix} {message}"
return {
"action_name": "wait",
"action_kwargs": {
"time": 0.1,
"log_message": safe_message,
"progress_message": safe_message
}
}

View File

@@ -4,108 +4,12 @@
"""
import re
import logging
from typing import Union
logger = logging.getLogger(__name__)
from .logger_util import debug_print
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:
def parse_volume_input(volume_input: Union[str, float, int], default_unit: str = "mL") -> float:
"""
解析带单位的体积输入
@@ -175,6 +79,111 @@ def parse_volume_with_units(volume_input: Union[str, float, int], default_unit:
debug_print(f"体积解析: '{volume_str}'{value} {unit}{volume}mL")
return volume
def parse_mass_input(mass_input: Union[str, float]) -> float:
"""
解析质量输入,支持带单位的字符串
Args:
mass_input: 质量输入(如 "19.3 g", "4.5 g", 2.5
Returns:
float: 质量(克)
"""
if isinstance(mass_input, (int, float)):
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 ['m', 'min', 'minute', 'mins', 'minutes']:
time_sec = value * 60.0 # min -> s
debug_print(f"🔄 时间转换: {value}分钟 → {time_sec}")
elif unit in ['h', 'hr', 'hour', 'hrs', 'hours']:
time_sec = value * 3600.0 # h -> s
debug_print(f"🔄 时间转换: {value}小时 → {time_sec}")
elif unit in ['d', 'day', 'days']:
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 test_unit_parser():
"""测试单位解析功能"""
@@ -187,7 +196,7 @@ def test_unit_parser():
print("\n时间解析测试:")
for time_input in time_tests:
result = parse_time_with_units(time_input)
result = parse_time_input(time_input)
print(f" {time_input}{result}s ({result/60:.1f}min)")
# 测试体积解析
@@ -197,7 +206,7 @@ def test_unit_parser():
print("\n体积解析测试:")
for volume_input in volume_tests:
result = parse_volume_with_units(volume_input)
result = parse_volume_input(volume_input)
print(f" {volume_input}{result}mL")
print("\n✅ 测试完成")

View File

@@ -1,3 +1,8 @@
import networkx as nx
from .logger_util import debug_print
def get_vessel(vessel):
"""
统一处理vessel参数返回vessel_id和vessel_data。
@@ -18,3 +23,258 @@ def get_vessel(vessel):
vessel_id = str(vessel)
vessel_data = {}
return vessel_id, vessel_data
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_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():
debug_print(f"✅ 通过液体类型匹配到容器: {node_id} 💧")
return node_id
# 🔧 方法4使用第一个试剂瓶作为备选
debug_print(f"📋 方法4: 查找备选试剂瓶...")
for node_id in G.nodes():
node_data = G.nodes[node_id]
if (node_data.get('type') == 'container' and
('reagent' in node_id.lower() or 'bottle' in node_id.lower())):
debug_print(f"⚠️ 未找到专用容器,使用备选试剂瓶: {node_id} 🔄")
return node_id
debug_print(f"❌ 所有方法都失败了,无法找到容器!")
raise ValueError(f"找不到试剂 '{reagent}' 对应的容器")
def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
"""
查找溶剂容器
Args:
G: 网络图
solvent: 溶剂名称
Returns:
str: 溶剂容器ID
"""
debug_print(f"🔍 正在查找溶剂 '{solvent}' 的容器... 🧪")
# 构建可能的容器名称
possible_names = [
f"flask_{solvent}",
f"bottle_{solvent}",
f"reagent_{solvent}",
f"reagent_bottle_{solvent}",
f"{solvent}_flask",
f"{solvent}_bottle",
f"{solvent}",
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():
debug_print(f" 🎉 通过名称匹配找到容器: {vessel_name}")
return vessel_name
# 第二步通过模糊匹配节点ID和名称
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:
debug_print(f" 🎉 通过模糊匹配找到容器: {node_id} (名称: {node_name}) ✨")
return node_id
# 第三步:通过配置中的试剂信息匹配
debug_print(" 🧪 步骤3: 配置试剂信息匹配...")
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
# 检查 config 中的 reagent 字段
node_config = G.nodes[node_id].get('config', {})
config_reagent = node_config.get('reagent', '').lower()
if config_reagent and solvent.lower() == config_reagent:
debug_print(f" 🎉 通过config.reagent匹配找到容器: {node_id} (试剂: {config_reagent}) ✨")
return node_id
# 第四步:通过数据中的试剂信息匹配
debug_print(" 🧪 步骤4: 数据试剂信息匹配...")
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
vessel_data = G.nodes[node_id].get('data', {})
# 检查 data 中的 reagent_name 字段
reagent_name = vessel_data.get('reagent_name', '').lower()
if reagent_name and solvent.lower() == reagent_name:
debug_print(f" 🎉 通过data.reagent_name匹配找到容器: {node_id} (试剂: {reagent_name}) ✨")
return node_id
# 检查 data 中的液体信息
liquids = vessel_data.get('liquid', [])
for liquid in liquids:
if isinstance(liquid, dict):
liquid_type = (liquid.get('liquid_type') or liquid.get('name', '')).lower()
if solvent.lower() in liquid_type:
debug_print(f" 🎉 通过液体类型匹配找到容器: {node_id} (液体类型: {liquid_type}) ✨")
return node_id
# 第五步:部分匹配(如果前面都没找到)
debug_print(" 🔍 步骤5: 部分匹配...")
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
node_config = G.nodes[node_id].get('config', {})
node_data = G.nodes[node_id].get('data', {})
node_name = G.nodes[node_id].get('name', '').lower()
config_reagent = node_config.get('reagent', '').lower()
data_reagent = node_data.get('reagent_name', '').lower()
# 检查是否包含溶剂名称
if (solvent.lower() in config_reagent or
solvent.lower() in data_reagent or
solvent.lower() in node_name or
solvent.lower() in node_id.lower()):
debug_print(f" 🎉 通过部分匹配找到容器: {node_id}")
debug_print(f" - 节点名称: {node_name}")
debug_print(f" - 配置试剂: {config_reagent}")
debug_print(f" - 数据试剂: {data_reagent}")
return node_id
# 调试信息:列出所有容器
debug_print(" 🔎 调试信息:列出所有容器...")
container_list = []
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
node_config = G.nodes[node_id].get('config', {})
node_data = G.nodes[node_id].get('data', {})
node_name = G.nodes[node_id].get('name', '')
container_info = {
'id': node_id,
'name': node_name,
'config_reagent': node_config.get('reagent', ''),
'data_reagent': node_data.get('reagent_name', '')
}
container_list.append(container_info)
debug_print(
f" - 容器: {node_id}, 名称: {node_name}, config试剂: {node_config.get('reagent', '')}, data试剂: {node_data.get('reagent_name', '')}")
debug_print(f"❌ 找不到溶剂 '{solvent}' 对应的容器 😭")
debug_print(f"🔍 查找的溶剂: '{solvent}' (小写: '{solvent.lower()}')")
debug_print(f"📊 总共发现 {len(container_list)} 个容器")
raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器")
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 ""

View File

@@ -3,118 +3,14 @@ import networkx as nx
import logging
import re
from .utils.unit_parser import parse_time_input, parse_volume_input
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}'")
# 🎯 优先级1volume_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
# 🧮 优先级2mass转体积简化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
# 📦 优先级3volume
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:
"""查找溶剂源(精简版)"""

View File

@@ -67,7 +67,7 @@ class VirtualHeatChill:
self.logger.info(f"✅ 温控设备 {self.device_id} 清理完成 💤")
return True
async def heat_chill(self, vessel: str, temp: float, time, stir: bool,
async def heat_chill(self, temp: float, time, stir: bool,
stir_speed: float, purpose: str) -> bool:
"""Execute heat chill action - 🔧 修复:确保参数类型正确"""
@@ -77,7 +77,6 @@ class VirtualHeatChill:
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)}"
@@ -102,8 +101,7 @@ class VirtualHeatChill:
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" 🎯 目标温度: {temp}°C {temp_emoji}")
self.logger.info(f" ⏰ 持续时间: {time_value}s")
self.logger.info(f" 🌪️ 搅拌: {stir} ({stir_speed} RPM)")
@@ -147,7 +145,7 @@ class VirtualHeatChill:
stir_info = f" | 🌪️ 搅拌: {stir_speed} RPM" if stir else ""
self.data.update({
"status": f"{temp_emoji} 运行中: {status_action} {vessel}{temp}°C | ⏰ 剩余: {total_time:.0f}s{stir_info}",
"status": f"{temp_emoji} 运行中: {status_action}{temp}°C | ⏰ 剩余: {total_time:.0f}s{stir_info}",
"operation_mode": operation_mode,
"is_stirring": stir,
"stir_speed": stir_speed if stir else 0.0,
@@ -165,7 +163,7 @@ class VirtualHeatChill:
# 更新剩余时间和状态
self.data.update({
"remaining_time": remaining,
"status": f"{temp_emoji} 运行中: {status_action} {vessel}{temp}°C | ⏰ 剩余: {remaining:.0f}s{stir_info}",
"status": f"{temp_emoji} 运行中: {status_action}{temp}°C | ⏰ 剩余: {remaining:.0f}s{stir_info}",
"progress": progress
})
@@ -185,7 +183,7 @@ class VirtualHeatChill:
final_stir_info = f" | 🌪️ 搅拌: {stir_speed} RPM" if stir else ""
self.data.update({
"status": f"✅ 完成: {vessel} 已达到 {temp}°C {temp_emoji} | ⏱️ 用时: {total_time:.0f}s{final_stir_info}",
"status": f"✅ 完成: 已达到 {temp}°C {temp_emoji} | ⏱️ 用时: {total_time:.0f}s{final_stir_info}",
"operation_mode": "Completed",
"remaining_time": 0.0,
"is_stirring": False,
@@ -195,7 +193,6 @@ class VirtualHeatChill:
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:
@@ -204,13 +201,12 @@ class VirtualHeatChill:
return True
async def heat_chill_start(self, vessel: str, temp: float, purpose: str) -> bool:
async def heat_chill_start(self, temp: float, purpose: str) -> bool:
"""Start continuous heat chill 🔄"""
# 🔧 添加类型转换
try:
temp = float(temp)
vessel = str(vessel)
purpose = str(purpose)
except (ValueError, TypeError) as e:
error_msg = f"参数类型转换错误: {str(e)}"
@@ -235,8 +231,7 @@ class VirtualHeatChill:
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" 🎯 目标温度: {temp}°C {temp_emoji}")
self.logger.info(f" 🔄 模式: {status_action}")
self.logger.info(f" 📝 目的: {purpose}")
@@ -252,7 +247,7 @@ class VirtualHeatChill:
return False
self.data.update({
"status": f"🔄 启动: {status_action} {vessel}{temp}°C {temp_emoji} | ♾️ 持续运行",
"status": f"🔄 启动: {status_action}{temp}°C {temp_emoji} | ♾️ 持续运行",
"operation_mode": operation_mode,
"is_stirring": False,
"stir_speed": 0.0,
@@ -262,28 +257,20 @@ class VirtualHeatChill:
self.logger.info(f"✅ 持续温控已启动! {temp_emoji} {status_action}模式 🚀")
return True
async def heat_chill_stop(self, vessel: str) -> bool:
async def heat_chill_stop(self) -> bool:
"""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.logger.info(f"🛑 停止温控:")
self.data.update({
"status": f"🛑 已停止: {vessel} 温控停止",
"status": f"🛑 {self.device_id} 温控停止",
"operation_mode": "Stopped",
"is_stirring": False,
"stir_speed": 0.0,
"remaining_time": 0.0,
})
self.logger.info(f"✅ 温控设备已停止 {vessel} 温度控制 🏁")
self.logger.info(f"✅ 温控设备已停止 {self.device_id} 温度控制 🏁")
return True
# 状态属性

View File

@@ -1,197 +0,0 @@
import asyncio
import logging
from typing import Dict, Any, Optional
class VirtualPump:
"""Virtual pump device for transfer and cleaning operations"""
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
# 处理可能的不同调用方式
if device_id is None and 'id' in kwargs:
device_id = kwargs.pop('id')
if config is None and 'config' in kwargs:
config = kwargs.pop('config')
# 设置默认值
self.device_id = device_id or "unknown_pump"
self.config = config or {}
self.logger = logging.getLogger(f"VirtualPump.{self.device_id}")
self.data = {}
# 从config或kwargs中获取配置参数
self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL')
self._max_volume = self.config.get('max_volume') or kwargs.get('max_volume', 50.0)
self._transfer_rate = self.config.get('transfer_rate') or kwargs.get('transfer_rate', 10.0)
print(f"=== VirtualPump {self.device_id} created with max_volume={self._max_volume}, transfer_rate={self._transfer_rate} ===")
async def initialize(self) -> bool:
"""Initialize virtual pump"""
self.logger.info(f"Initializing virtual pump {self.device_id}")
self.data.update({
"status": "Idle",
"valve_position": 0,
"current_volume": 0.0,
"max_volume": self._max_volume,
"transfer_rate": self._transfer_rate,
"from_vessel": "",
"to_vessel": "",
"progress": 0.0,
"transferred_volume": 0.0,
"current_status": "Ready"
})
return True
async def cleanup(self) -> bool:
"""Cleanup virtual pump"""
self.logger.info(f"Cleaning up virtual pump {self.device_id}")
return True
async def transfer(self, from_vessel: str, to_vessel: str, volume: float,
amount: str = "", time: float = 0.0, viscous: bool = False,
rinsing_solvent: str = "", rinsing_volume: float = 0.0,
rinsing_repeats: int = 0, solid: bool = False) -> bool:
"""Execute transfer operation"""
self.logger.info(f"Transferring {volume}mL from {from_vessel} to {to_vessel}")
# 计算转移时间
transfer_time = volume / self._transfer_rate if time == 0 else time
self.data.update({
"status": "Running",
"from_vessel": from_vessel,
"to_vessel": to_vessel,
"current_status": "Transferring",
"progress": 0.0,
"transferred_volume": 0.0
})
# 模拟转移过程
steps = 10
step_time = transfer_time / steps
step_volume = volume / steps
for i in range(steps):
await asyncio.sleep(step_time)
progress = (i + 1) / steps * 100
current_volume = step_volume * (i + 1)
self.data.update({
"progress": progress,
"transferred_volume": current_volume,
"current_status": f"Transferring: {progress:.1f}%"
})
self.logger.info(f"Transfer progress: {progress:.1f}%")
self.data.update({
"status": "Idle",
"current_status": "Transfer completed",
"progress": 100.0,
"transferred_volume": volume
})
return True
async def clean_vessel(self, vessel: str, solvent: str, volume: float,
temp: float, repeats: int = 1) -> bool:
"""Execute vessel cleaning operation - matches CleanVessel action"""
self.logger.info(f"Starting vessel cleaning: {vessel} with {solvent} ({volume}mL at {temp}°C, {repeats} repeats)")
# 更新设备状态
self.data.update({
"status": "Running",
"from_vessel": f"flask_{solvent}",
"to_vessel": vessel,
"current_status": "Cleaning in progress",
"progress": 0.0,
"transferred_volume": 0.0
})
# 计算清洗时间(基于体积和重复次数)
# 假设清洗速度为 transfer_rate 的一半(因为需要加载和排放)
cleaning_rate = self._transfer_rate / 2
cleaning_time_per_cycle = volume / cleaning_rate
total_cleaning_time = cleaning_time_per_cycle * repeats
# 模拟清洗过程
steps_per_repeat = 10 # 每次重复清洗分10个步骤
total_steps = steps_per_repeat * repeats
step_time = total_cleaning_time / total_steps
for repeat in range(repeats):
self.logger.info(f"Starting cleaning cycle {repeat + 1}/{repeats}")
for step in range(steps_per_repeat):
await asyncio.sleep(step_time)
# 计算当前进度
current_step = repeat * steps_per_repeat + step + 1
progress = (current_step / total_steps) * 100
# 计算已处理的体积
volume_processed = (current_step / total_steps) * volume * repeats
# 更新状态
self.data.update({
"progress": progress,
"transferred_volume": volume_processed,
"current_status": f"Cleaning cycle {repeat + 1}/{repeats} - Step {step + 1}/{steps_per_repeat} ({progress:.1f}%)"
})
self.logger.info(f"Cleaning progress: {progress:.1f}% (Cycle {repeat + 1}/{repeats})")
# 清洗完成
self.data.update({
"status": "Idle",
"current_status": "Cleaning completed successfully",
"progress": 100.0,
"transferred_volume": volume * repeats,
"from_vessel": "",
"to_vessel": ""
})
self.logger.info(f"Vessel cleaning completed: {vessel}")
return True
# 状态属性
@property
def status(self) -> str:
return self.data.get("status", "Unknown")
@property
def valve_position(self) -> int:
return self.data.get("valve_position", 0)
@property
def current_volume(self) -> float:
return self.data.get("current_volume", 0.0)
@property
def max_volume(self) -> float:
return self.data.get("max_volume", 0.0)
@property
def transfer_rate(self) -> float:
return self.data.get("transfer_rate", 0.0)
@property
def from_vessel(self) -> str:
return self.data.get("from_vessel", "")
@property
def to_vessel(self) -> str:
return self.data.get("to_vessel", "")
@property
def progress(self) -> float:
return self.data.get("progress", 0.0)
@property
def transferred_volume(self) -> float:
return self.data.get("transferred_volume", 0.0)
@property
def current_status(self) -> str:
return self.data.get("current_status", "Ready")

File diff suppressed because it is too large Load Diff

View File

@@ -225,7 +225,8 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
self.lab_logger().info(f"Working on physical setup: {physical_setup_graph}")
protocol_steps = protocol_steps_generator(G=physical_setup_graph, **protocol_kwargs)
self.lab_logger().info(f"Goal received: {protocol_kwargs}, running steps: \n{protocol_steps}")
self.lab_logger().info(f"Goal received: {protocol_kwargs}, running steps: "
f"{json.dumps([step for step in protocol_steps if 'log_message' not in step['action_kwargs']], indent=4)}")
time_start = time.time()
time_overall = 100

View File

@@ -6,6 +6,7 @@ from pylabrobot.resources import Resource as PLRResource, Plate, TipRack, Coordi
from unilabos.ros.nodes.presets.protocol_node import ROS2ProtocolNode
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
class WorkStationContainer(Plate, TipRack):
"""
WorkStation 专用 Container 类,继承自 Plate和TipRack
@@ -81,4 +82,5 @@ class WorkStationExample(ROS2ProtocolNode):
"""
to_base_plate.assign_child_resource(from_plate, Coordinate.zero())
pass
pass