From 992b8a03d06bcf432a30cf227df49cfa179ad911 Mon Sep 17 00:00:00 2001 From: KCFeng425 <2100011801@stu.pku.edu.cn> Date: Fri, 18 Jul 2025 00:28:12 +0800 Subject: [PATCH] =?UTF-8?q?stir=E5=92=8Cadjustph=E7=9A=84=E4=B8=AD?= =?UTF-8?q?=E7=9A=84bug=E4=BF=AE=E4=B8=8D=E5=A5=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- unilabos/compile/adjustph_protocol.py | 17 +- unilabos/compile/evaporate_protocol.py | 18 +- unilabos/compile/recrystallize_protocol.py | 77 +++++++- .../devices/virtual/virtual_multiway_valve.py | 25 ++- unilabos/devices/virtual/virtual_rotavap.py | 16 +- unilabos/registry/devices/virtual_device.yaml | 173 ++++++++++++++++++ 6 files changed, 302 insertions(+), 24 deletions(-) diff --git a/unilabos/compile/adjustph_protocol.py b/unilabos/compile/adjustph_protocol.py index 4d39e93..0f7dce2 100644 --- a/unilabos/compile/adjustph_protocol.py +++ b/unilabos/compile/adjustph_protocol.py @@ -1,6 +1,6 @@ import networkx as nx import logging -from typing import List, Dict, Any +from typing import List, Dict, Any, Union from .pump_protocol import generate_pump_protocol_with_rinsing logger = logging.getLogger(__name__) @@ -216,7 +216,7 @@ def calculate_reagent_volume(target_ph_value: float, reagent: str, vessel_volume def generate_adjust_ph_protocol( G: nx.DiGraph, - vessel: dict, # 🔧 修改:从字符串改为字典类型 + vessel:Union[dict,str], # 🔧 修改:从字符串改为字典类型 ph_value: float, reagent: str, **kwargs @@ -235,8 +235,17 @@ def generate_adjust_ph_protocol( List[Dict[str, Any]]: 动作序列 """ - # 🔧 核心修改:从字典中提取容器ID - vessel_id = vessel["id"] + # 统一处理vessel参数 + if isinstance(vessel, dict): + vessel_id = vessel.get("id", "") + vessel_data = vessel.get("data", {}) + else: + vessel_id = str(vessel) + vessel_data = G.nodes[vessel_id].get("data", {}) if vessel_id in G.nodes() else {} + + if not vessel_id: + debug_print("❌ vessel 参数无效,必须包含id字段或直接提供容器ID") + raise ValueError("vessel 参数无效,必须包含id字段或直接提供容器ID") debug_print("=" * 60) debug_print("🧪 开始生成pH调节协议") diff --git a/unilabos/compile/evaporate_protocol.py b/unilabos/compile/evaporate_protocol.py index 7f93192..56228c2 100644 --- a/unilabos/compile/evaporate_protocol.py +++ b/unilabos/compile/evaporate_protocol.py @@ -22,7 +22,7 @@ def parse_time_input(time_input: Union[str, float]) -> float: """ if isinstance(time_input, (int, float)): debug_print(f"⏱️ 时间输入为数字: {time_input}s ✨") - return float(time_input) + return float(time_input) # 🔧 确保返回float if not time_input or not str(time_input).strip(): debug_print(f"⚠️ 时间输入为空,使用默认值: 180s (3分钟) 🕐") @@ -48,7 +48,7 @@ def parse_time_input(time_input: Union[str, float]) -> float: try: value = float(time_str) debug_print(f"✅ 时间解析成功: {time_str} → {value}s(无单位,默认秒)⏰") - return value + return float(value) # 🔧 确保返回float except ValueError: debug_print(f"❌ 无法解析时间: '{time_str}',使用默认值180s (3分钟) 😅") return 180.0 @@ -70,7 +70,7 @@ def parse_time_input(time_input: Union[str, float]) -> float: time_sec = value # 已经是s debug_print(f"🕐 时间转换: {value}s → {time_sec}s (已是秒) ⏰") - return time_sec + return float(time_sec) # 🔧 确保返回float def find_rotavap_device(G: nx.DiGraph, vessel: str = None) -> Optional[str]: """ @@ -389,12 +389,12 @@ def generate_evaporate_protocol( "device_id": rotavap_device, "action_name": "evaporate", "action_kwargs": { - "vessel": target_vessel, # 使用 target_vessel - "pressure": pressure, - "temp": temp, - "time": final_time, - "stir_speed": stir_speed, - "solvent": solvent + "vessel": target_vessel, + "pressure": float(pressure), + "temp": float(temp), + "time": float(final_time), # 🔧 强制转换为float类型 + "stir_speed": float(stir_speed), + "solvent": str(solvent) } } action_sequence.append(evaporate_action) diff --git a/unilabos/compile/recrystallize_protocol.py b/unilabos/compile/recrystallize_protocol.py index b6f4aff..7c15310 100644 --- a/unilabos/compile/recrystallize_protocol.py +++ b/unilabos/compile/recrystallize_protocol.py @@ -170,33 +170,94 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: 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} ✨") + debug_print(f" 🎉 通过模糊匹配找到容器: {node_id} (名称: {node_name}) ✨") return node_id - # 第三步:通过液体类型匹配 - debug_print(" 🧪 步骤3: 液体类型匹配...") + # 第三步:通过配置中的试剂信息匹配 + 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', {}) - liquids = vessel_data.get('liquid', []) + # 检查 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() - reagent_name = vessel_data.get('reagent_name', '').lower() - if solvent.lower() in liquid_type or solvent.lower() in reagent_name: - debug_print(f" 🎉 通过液体类型匹配找到容器: {node_id} ✨") + 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}' 对应的容器") diff --git a/unilabos/devices/virtual/virtual_multiway_valve.py b/unilabos/devices/virtual/virtual_multiway_valve.py index 468175c..3cd68a1 100644 --- a/unilabos/devices/virtual/virtual_multiway_valve.py +++ b/unilabos/devices/virtual/virtual_multiway_valve.py @@ -70,11 +70,32 @@ class VirtualMultiwayValve: command: 目标位置 (0-8) 或位置字符串 0: transfer pump位置 1-8: 其他设备位置 + 'default': 默认位置(0号位) """ try: - # 如果是字符串形式的位置,先转换为数字 + # 🔧 处理特殊字符串命令 if isinstance(command, str): - pos = int(command) + command_lower = command.lower().strip() + + # 处理特殊命令 + if command_lower in ['default', 'pump', 'transfer_pump', 'home']: + pos = 0 # 默认位置为0号位(transfer pump) + self.logger.info(f"🔧 特殊命令 '{command}' 映射到位置 {pos}") + elif command_lower in ['open']: + pos = 0 # open命令也映射到0号位 + self.logger.info(f"🔧 OPEN命令映射到位置 {pos}") + elif command_lower in ['close', 'closed']: + # 关闭命令保持当前位置 + pos = self._current_position + self.logger.info(f"🔧 CLOSE命令保持当前位置 {pos}") + else: + # 尝试转换为数字 + try: + pos = int(command) + except ValueError: + error_msg = f"无法识别的命令: '{command}'" + self.logger.error(f"❌ {error_msg}") + raise ValueError(error_msg) else: pos = int(command) diff --git a/unilabos/devices/virtual/virtual_rotavap.py b/unilabos/devices/virtual/virtual_rotavap.py index dd1cca4..61e66a3 100644 --- a/unilabos/devices/virtual/virtual_rotavap.py +++ b/unilabos/devices/virtual/virtual_rotavap.py @@ -88,6 +88,20 @@ class VirtualRotavap: ) -> bool: """Execute evaporate action - 简化版 🌪️""" + # 🔧 新增:确保time参数是数值类型 + if isinstance(time, str): + try: + time = float(time) + except ValueError: + self.logger.error(f"❌ 无法转换时间参数 '{time}' 为数值,使用默认值180.0秒") + time = 180.0 + elif not isinstance(time, (int, float)): + self.logger.error(f"❌ 时间参数类型无效: {type(time)},使用默认值180.0秒") + time = 180.0 + + # 确保time是float类型 + time = float(time) + # 🔧 简化处理:如果vessel就是设备自己,直接操作 if vessel == self.device_id: debug_print(f"🎯 在设备 {self.device_id} 上直接执行蒸发操作") @@ -158,7 +172,7 @@ class VirtualRotavap: }) return False - # 开始蒸发 + # 开始蒸发 - 🔧 现在time已经确保是float类型 self.logger.info(f"🚀 启动蒸发程序! 预计用时 {time/60:.1f}分钟 ⏱️") self.data.update({ diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml index 73a5901..4823a2f 100644 --- a/unilabos/registry/devices/virtual_device.yaml +++ b/unilabos/registry/devices/virtual_device.yaml @@ -5993,3 +5993,176 @@ virtual_vacuum_pump: - status type: object version: 0.0.1 +virtual_solid_dispenser: + class: + action_value_mappings: + auto-cleanup: + feedback: {} + goal: {} + goal_default: {} + handles: [] + result: {} + schema: + description: cleanup的参数schema + properties: + feedback: {} + goal: + properties: {} + type: object + result: {} + required: + - goal + title: cleanup参数 + type: object + type: UniLabJsonCommandAsync + auto-initialize: + feedback: {} + goal: {} + goal_default: {} + handles: [] + result: {} + schema: + description: initialize的参数schema + properties: + feedback: {} + goal: + properties: {} + type: object + result: {} + required: + - goal + title: initialize参数 + type: object + type: UniLabJsonCommandAsync + add_solid: + feedback: + current_status: status + progress: progress + goal: + vessel: vessel + reagent: reagent + mass: mass + mol: mol + purpose: purpose + event: event + rate_spec: rate_spec + equiv: equiv + ratio: ratio + goal_default: + vessel: '' + reagent: '' + mass: '' + mol: '' + purpose: '' + event: '' + rate_spec: '' + equiv: '' + ratio: '' + handles: [] + result: + success: success + message: message + return_info: return_info + schema: + description: '' + properties: + feedback: + properties: + current_status: + type: string + progress: + type: number + type: object + goal: + properties: + vessel: + type: string + reagent: + type: string + mass: + type: string + mol: + type: string + purpose: + type: string + event: + type: string + rate_spec: + type: string + equiv: + type: string + ratio: + type: string + type: object + result: + properties: + success: + type: boolean + message: + type: string + return_info: + type: string + type: object + required: + - goal + title: AddSolid + type: object + type: Add # 🔧 使用 Add action type + module: unilabos.devices.virtual.virtual_solid_dispenser:VirtualSolidDispenser + status_types: + status: str + current_reagent: str + dispensed_amount: float + total_operations: int + type: python + description: Virtual Solid Dispenser for Add Protocol Testing - supports mass and molar additions + handles: + - data_key: solid_out + data_source: executor + data_type: resource + description: 固体试剂输出口 + handler_key: SolidOut + io_type: source + label: SolidOut + side: SOUTH + - data_key: solid_in + data_source: handle + data_type: resource + description: 固体试剂输入口(连接试剂瓶) + handler_key: SolidIn + io_type: target + label: SolidIn + side: NORTH + icon: '' + init_param_schema: + config: + properties: + max_capacity: + default: 100.0 + type: number + precision: + default: 0.001 + type: number + config: + type: object + device_id: + type: string + required: [] + type: object + data: + properties: + status: + type: string + current_reagent: + type: string + dispensed_amount: + type: number + total_operations: + type: integer + required: + - status + - current_reagent + - dispensed_amount + - total_operations + type: object + version: 0.0.1 \ No newline at end of file