mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-29 14:48:13 +00:00
refactor: 升级编译器共享工具库(logger_util, unit_parser, vessel_parser, resource_helper)
- logger_util: 重写debug_print,支持自动检测调用模块并设置前缀 - unit_parser: 新增parse_temperature_input,统一温度字符串解析 - vessel_parser: 新增find_connected_heatchill,统一加热设备查找 - resource_helper: 新增update_vessel_volume/get_resource_liquid_volume等共享函数 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,36 +1,57 @@
|
|||||||
# 🆕 创建进度日志动作
|
"""编译器共享日志工具"""
|
||||||
|
|
||||||
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
# 模块名到前缀的映射
|
||||||
|
_MODULE_PREFIXES = {
|
||||||
|
"add_protocol": "[ADD]",
|
||||||
|
"adjustph_protocol": "[ADJUSTPH]",
|
||||||
|
"clean_vessel_protocol": "[CLEAN_VESSEL]",
|
||||||
|
"dissolve_protocol": "[DISSOLVE]",
|
||||||
|
"dry_protocol": "[DRY]",
|
||||||
|
"evacuateandrefill_protocol": "[EVACUATE]",
|
||||||
|
"evaporate_protocol": "[EVAPORATE]",
|
||||||
|
"filter_protocol": "[FILTER]",
|
||||||
|
"heatchill_protocol": "[HEATCHILL]",
|
||||||
|
"hydrogenate_protocol": "[HYDROGENATE]",
|
||||||
|
"pump_protocol": "[PUMP]",
|
||||||
|
"recrystallize_protocol": "[RECRYSTALLIZE]",
|
||||||
|
"reset_handling_protocol": "[RESET]",
|
||||||
|
"run_column_protocol": "[RUN_COLUMN]",
|
||||||
|
"separate_protocol": "[SEPARATE]",
|
||||||
|
"stir_protocol": "[STIR]",
|
||||||
|
"wash_solid_protocol": "[WASH_SOLID]",
|
||||||
|
"vessel_parser": "[VESSEL_PARSER]",
|
||||||
|
"unit_parser": "[UNIT_PARSER]",
|
||||||
|
"resource_helper": "[RESOURCE_HELPER]",
|
||||||
|
}
|
||||||
|
|
||||||
def debug_print(message, prefix="[UNIT_PARSER]"):
|
|
||||||
"""调试输出"""
|
def debug_print(message, prefix=None):
|
||||||
|
"""调试输出 — 自动根据调用模块设置前缀"""
|
||||||
|
if prefix is None:
|
||||||
|
frame = inspect.currentframe()
|
||||||
|
caller = frame.f_back if frame else None
|
||||||
|
module_name = ""
|
||||||
|
if caller:
|
||||||
|
module_name = caller.f_globals.get("__name__", "")
|
||||||
|
# 取最后一段作为模块短名
|
||||||
|
module_name = module_name.rsplit(".", 1)[-1]
|
||||||
|
prefix = _MODULE_PREFIXES.get(module_name, f"[{module_name.upper()}]")
|
||||||
|
logger = logging.getLogger("unilabos.compile")
|
||||||
logger.info(f"{prefix} {message}")
|
logger.info(f"{prefix} {message}")
|
||||||
|
|
||||||
|
|
||||||
def action_log(message: str, emoji: str = "📝", prefix="[HIGH-LEVEL OPERATION]") -> Dict[str, Any]:
|
def action_log(message: str, emoji: str = "📝", prefix="[HIGH-LEVEL OPERATION]") -> Dict[str, Any]:
|
||||||
"""创建一个动作日志 - 支持中文和emoji"""
|
"""创建一个动作日志"""
|
||||||
try:
|
full_message = f"{prefix} {emoji} {message}"
|
||||||
full_message = f"{prefix} {emoji} {message}"
|
return {
|
||||||
|
"action_name": "wait",
|
||||||
return {
|
"action_kwargs": {
|
||||||
"action_name": "wait",
|
"time": 0.1,
|
||||||
"action_kwargs": {
|
"log_message": full_message,
|
||||||
"time": 0.1,
|
"progress_message": full_message
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
172
unilabos/compile/utils/resource_helper.py
Normal file
172
unilabos/compile/utils/resource_helper.py
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
"""
|
||||||
|
资源实例兼容层
|
||||||
|
|
||||||
|
提供 ensure_resource_instance() 将 dict / ResourceDictInstance 统一转为
|
||||||
|
ResourceDictInstance,使编译器可以渐进式迁移到强类型资源。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any, Dict, Optional, Union
|
||||||
|
|
||||||
|
from unilabos.resources.resource_tracker import ResourceDictInstance
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_resource_instance(
|
||||||
|
resource: Union[Dict[str, Any], ResourceDictInstance, None],
|
||||||
|
) -> Optional[ResourceDictInstance]:
|
||||||
|
"""将 dict 或 ResourceDictInstance 统一转为 ResourceDictInstance
|
||||||
|
|
||||||
|
编译器入口统一调用此函数,即可同时兼容旧 dict 传参和新 ResourceDictInstance 传参。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resource: 资源数据,可以是 plain dict、ResourceDictInstance 或 None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ResourceDictInstance 或 None(当输入为 None 时)
|
||||||
|
"""
|
||||||
|
if resource is None:
|
||||||
|
return None
|
||||||
|
if isinstance(resource, ResourceDictInstance):
|
||||||
|
return resource
|
||||||
|
if isinstance(resource, dict):
|
||||||
|
return ResourceDictInstance.get_resource_instance_from_dict(resource)
|
||||||
|
raise TypeError(f"不支持的资源类型: {type(resource)}, 期望 dict 或 ResourceDictInstance")
|
||||||
|
|
||||||
|
|
||||||
|
def resource_to_dict(resource: Union[Dict[str, Any], ResourceDictInstance]) -> Dict[str, Any]:
|
||||||
|
"""将 ResourceDictInstance 或 dict 统一转为 plain dict
|
||||||
|
|
||||||
|
用于需要 dict 操作的场景(如 children dict 操作)。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resource: ResourceDictInstance 或 dict
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
plain dict
|
||||||
|
"""
|
||||||
|
if isinstance(resource, dict):
|
||||||
|
return resource
|
||||||
|
if isinstance(resource, ResourceDictInstance):
|
||||||
|
return resource.get_plr_nested_dict()
|
||||||
|
raise TypeError(f"不支持的资源类型: {type(resource)}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_resource_id(resource: Union[str, Dict[str, Any], ResourceDictInstance]) -> str:
|
||||||
|
"""从资源对象中提取 ID
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resource: 字符串 ID、dict 或 ResourceDictInstance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
资源 ID 字符串
|
||||||
|
"""
|
||||||
|
if isinstance(resource, str):
|
||||||
|
return resource
|
||||||
|
if isinstance(resource, ResourceDictInstance):
|
||||||
|
return resource.res_content.id
|
||||||
|
if isinstance(resource, dict):
|
||||||
|
if "id" in resource:
|
||||||
|
return resource["id"]
|
||||||
|
# 兼容 {station_id: {...}} 格式
|
||||||
|
first_val = next(iter(resource.values()), {})
|
||||||
|
if isinstance(first_val, dict):
|
||||||
|
return first_val.get("id", "")
|
||||||
|
return ""
|
||||||
|
raise TypeError(f"不支持的资源类型: {type(resource)}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_resource_data(resource: Union[str, Dict[str, Any], ResourceDictInstance]) -> Dict[str, Any]:
|
||||||
|
"""从资源对象中提取 data 字段
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resource: 字符串、dict 或 ResourceDictInstance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
data 字典
|
||||||
|
"""
|
||||||
|
if isinstance(resource, str):
|
||||||
|
return {}
|
||||||
|
if isinstance(resource, ResourceDictInstance):
|
||||||
|
return dict(resource.res_content.data)
|
||||||
|
if isinstance(resource, dict):
|
||||||
|
return resource.get("data", {})
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_resource_display_info(resource: Union[str, Dict[str, Any], ResourceDictInstance]) -> str:
|
||||||
|
"""获取资源的显示信息(用于日志)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resource: 字符串 ID、dict 或 ResourceDictInstance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
显示信息字符串
|
||||||
|
"""
|
||||||
|
if isinstance(resource, str):
|
||||||
|
return resource
|
||||||
|
if isinstance(resource, ResourceDictInstance):
|
||||||
|
res = resource.res_content
|
||||||
|
return f"{res.id} ({res.name})" if res.name and res.name != res.id else res.id
|
||||||
|
if isinstance(resource, dict):
|
||||||
|
res_id = resource.get("id", "unknown")
|
||||||
|
res_name = resource.get("name", "")
|
||||||
|
if res_name and res_name != res_id:
|
||||||
|
return f"{res_id} ({res_name})"
|
||||||
|
return res_id
|
||||||
|
return str(resource)
|
||||||
|
|
||||||
|
|
||||||
|
def get_resource_liquid_volume(resource: Union[Dict[str, Any], ResourceDictInstance]) -> float:
|
||||||
|
"""从资源中获取液体体积
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resource: dict 或 ResourceDictInstance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
液体总体积 (mL)
|
||||||
|
"""
|
||||||
|
data = get_resource_data(resource)
|
||||||
|
liquids = data.get("liquid", [])
|
||||||
|
if isinstance(liquids, list):
|
||||||
|
return sum(l.get("volume", 0.0) for l in liquids if isinstance(l, dict))
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def update_vessel_volume(vessel, G, new_volume: float, description: str = "") -> None:
|
||||||
|
"""
|
||||||
|
更新容器体积(同时更新vessel字典和图节点)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vessel: 容器字典或 ResourceDictInstance
|
||||||
|
G: 网络图 (nx.DiGraph)
|
||||||
|
new_volume: 新体积 (mL)
|
||||||
|
description: 更新描述(用于日志)
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger("unilabos.compile")
|
||||||
|
|
||||||
|
vessel_id = get_resource_id(vessel)
|
||||||
|
|
||||||
|
if description:
|
||||||
|
logger.info(f"[RESOURCE] 更新容器体积 - {description}")
|
||||||
|
|
||||||
|
# 更新 vessel 字典中的体积
|
||||||
|
if isinstance(vessel, dict):
|
||||||
|
if "data" not in vessel:
|
||||||
|
vessel["data"] = {}
|
||||||
|
lv = vessel["data"].get("liquid_volume")
|
||||||
|
if isinstance(lv, list) and len(lv) > 0:
|
||||||
|
vessel["data"]["liquid_volume"][0] = new_volume
|
||||||
|
else:
|
||||||
|
vessel["data"]["liquid_volume"] = new_volume
|
||||||
|
|
||||||
|
# 同时更新图中的容器数据
|
||||||
|
if vessel_id and vessel_id in G.nodes():
|
||||||
|
if "data" not in G.nodes[vessel_id]:
|
||||||
|
G.nodes[vessel_id]["data"] = {}
|
||||||
|
node_lv = G.nodes[vessel_id]["data"].get("liquid_volume")
|
||||||
|
if isinstance(node_lv, list) and len(node_lv) > 0:
|
||||||
|
G.nodes[vessel_id]["data"]["liquid_volume"][0] = new_volume
|
||||||
|
else:
|
||||||
|
G.nodes[vessel_id]["data"]["liquid_volume"] = new_volume
|
||||||
|
|
||||||
|
logger.info(f"[RESOURCE] 容器 '{vessel_id}' 体积已更新为: {new_volume:.2f}mL")
|
||||||
@@ -184,6 +184,42 @@ def parse_time_input(time_input: Union[str, float]) -> float:
|
|||||||
|
|
||||||
return time_sec
|
return time_sec
|
||||||
|
|
||||||
|
|
||||||
|
def parse_temperature_input(temp_input: Union[str, float], default_temp: float = 25.0) -> float:
|
||||||
|
"""
|
||||||
|
解析温度输入,支持字符串和数值
|
||||||
|
|
||||||
|
Args:
|
||||||
|
temp_input: 温度输入(如 "256 °C", "reflux", 45.0)
|
||||||
|
default_temp: 默认温度
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: 温度(°C)
|
||||||
|
"""
|
||||||
|
if not temp_input:
|
||||||
|
return default_temp
|
||||||
|
|
||||||
|
if isinstance(temp_input, (int, float)):
|
||||||
|
return float(temp_input)
|
||||||
|
|
||||||
|
temp_str = str(temp_input).lower().strip()
|
||||||
|
|
||||||
|
# 特殊温度关键词
|
||||||
|
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:
|
||||||
|
return special_temps[temp_str]
|
||||||
|
|
||||||
|
# 正则解析(如 "256 °C", "45°C", "45")
|
||||||
|
match = re.search(r'(\d+(?:\.\d+)?)\s*°?[cf]?', temp_str)
|
||||||
|
if match:
|
||||||
|
return float(match.group(1))
|
||||||
|
|
||||||
|
debug_print(f"无法解析温度: '{temp_str}',使用默认值: {default_temp}°C")
|
||||||
|
return default_temp
|
||||||
|
|
||||||
# 测试函数
|
# 测试函数
|
||||||
def test_unit_parser():
|
def test_unit_parser():
|
||||||
"""测试单位解析功能"""
|
"""测试单位解析功能"""
|
||||||
|
|||||||
@@ -1,27 +1,23 @@
|
|||||||
import networkx as nx
|
import networkx as nx
|
||||||
|
|
||||||
from .logger_util import debug_print
|
from .logger_util import debug_print
|
||||||
|
from .resource_helper import get_resource_id, get_resource_data
|
||||||
|
|
||||||
|
|
||||||
def get_vessel(vessel):
|
def get_vessel(vessel):
|
||||||
"""
|
"""
|
||||||
统一处理vessel参数,返回vessel_id和vessel_data。
|
统一处理vessel参数,返回vessel_id和vessel_data。
|
||||||
|
支持 dict、str、ResourceDictInstance。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
vessel: 可以是一个字典或字符串,表示vessel的ID或数据。
|
vessel: 可以是一个字典、字符串或 ResourceDictInstance,表示vessel的ID或数据。
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple: 包含vessel_id和vessel_data。
|
tuple: 包含vessel_id和vessel_data。
|
||||||
"""
|
"""
|
||||||
if isinstance(vessel, dict):
|
# 统一使用 resource_helper 处理
|
||||||
if "id" not in vessel:
|
vessel_id = get_resource_id(vessel)
|
||||||
vessel_id = list(vessel.values())[0].get("id", "")
|
vessel_data = get_resource_data(vessel)
|
||||||
else:
|
|
||||||
vessel_id = vessel.get("id", "")
|
|
||||||
vessel_data = vessel.get("data", {})
|
|
||||||
else:
|
|
||||||
vessel_id = str(vessel)
|
|
||||||
vessel_data = {}
|
|
||||||
return vessel_id, vessel_data
|
return vessel_id, vessel_data
|
||||||
|
|
||||||
|
|
||||||
@@ -279,3 +275,30 @@ def find_solid_dispenser(G: nx.DiGraph) -> str:
|
|||||||
|
|
||||||
debug_print(f"❌ 未找到固体加样器")
|
debug_print(f"❌ 未找到固体加样器")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str:
|
||||||
|
"""查找与指定容器相连的加热/冷却设备"""
|
||||||
|
heatchill_nodes = []
|
||||||
|
for node in G.nodes():
|
||||||
|
node_data = G.nodes[node]
|
||||||
|
node_class = node_data.get('class', '') or ''
|
||||||
|
node_name = node.lower()
|
||||||
|
if ('heatchill' in node_class.lower() or 'virtual_heatchill' in node_class
|
||||||
|
or 'heater' in node_name or 'heat' in node_name):
|
||||||
|
heatchill_nodes.append(node)
|
||||||
|
|
||||||
|
# 检查连接
|
||||||
|
if vessel and heatchill_nodes:
|
||||||
|
for hc in heatchill_nodes:
|
||||||
|
if G.has_edge(hc, vessel) or G.has_edge(vessel, hc):
|
||||||
|
debug_print(f"加热设备 '{hc}' 与容器 '{vessel}' 相连")
|
||||||
|
return hc
|
||||||
|
|
||||||
|
# 使用第一个可用设备
|
||||||
|
if heatchill_nodes:
|
||||||
|
debug_print(f"使用第一个加热设备: {heatchill_nodes[0]}")
|
||||||
|
return heatchill_nodes[0]
|
||||||
|
|
||||||
|
debug_print("未找到加热设备,使用默认设备")
|
||||||
|
return "heatchill_1"
|
||||||
Reference in New Issue
Block a user