mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-25 09:59:16 +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
|
||||
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}")
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
"""创建一个动作日志"""
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
||||
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():
|
||||
"""测试单位解析功能"""
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
import networkx as nx
|
||||
|
||||
from .logger_util import debug_print
|
||||
from .resource_helper import get_resource_id, get_resource_data
|
||||
|
||||
|
||||
def get_vessel(vessel):
|
||||
"""
|
||||
统一处理vessel参数,返回vessel_id和vessel_data。
|
||||
支持 dict、str、ResourceDictInstance。
|
||||
|
||||
Args:
|
||||
vessel: 可以是一个字典或字符串,表示vessel的ID或数据。
|
||||
vessel: 可以是一个字典、字符串或 ResourceDictInstance,表示vessel的ID或数据。
|
||||
|
||||
Returns:
|
||||
tuple: 包含vessel_id和vessel_data。
|
||||
"""
|
||||
if isinstance(vessel, dict):
|
||||
if "id" not in vessel:
|
||||
vessel_id = list(vessel.values())[0].get("id", "")
|
||||
else:
|
||||
vessel_id = vessel.get("id", "")
|
||||
vessel_data = vessel.get("data", {})
|
||||
else:
|
||||
vessel_id = str(vessel)
|
||||
vessel_data = {}
|
||||
# 统一使用 resource_helper 处理
|
||||
vessel_id = get_resource_id(vessel)
|
||||
vessel_data = get_resource_data(vessel)
|
||||
return vessel_id, vessel_data
|
||||
|
||||
|
||||
@@ -278,4 +274,31 @@ def find_solid_dispenser(G: nx.DiGraph) -> str:
|
||||
return node
|
||||
|
||||
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