From acf7b6d3f71222e3f070a50f06c0461352862263 Mon Sep 17 00:00:00 2001 From: Kongchang Feng <2100011801@stu.pku.edu.cn> Date: Wed, 16 Jul 2025 11:12:02 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E9=83=A8=E5=88=86?= =?UTF-8?q?=E7=9A=84protocol=E5=9B=A0=E4=B8=BAXDL=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E7=9A=84=E9=97=AE=E9=A2=98=20(#61)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 修复了部分的protocol因为XDL更新导致的问题 但是pumptransfer,add,dissolve,separate还没修,后续还需要写virtual固体加料器 * 补充了四个action * 添加了固体加样器,丰富了json,修改了add protocol * bump version to 0.9.9 * fix bugs from new actions * protocol完整修复版本& bump version to 0.9.10 * 修补了一些单位处理,bump version to 0.9.11 * 优化了全protocol的运行时间,除了pumptransfer相关的还没 * 补充了剩下的几个protocol --------- Co-authored-by: Junhan Chang Co-authored-by: Xuwznln <18435084+Xuwznln@users.noreply.github.com> --- README.md | 2 +- README_zh.md | 2 +- recipes/ros-humble-unilabos-msgs/recipe.yaml | 2 +- recipes/unilabos/recipe.yaml | 2 +- setup.py | 2 +- .../comprehensive_protocol/checklist.md | 90 +- .../comprehensive_station.json | 142 +- unilabos/app/mq.py | 2 +- unilabos/compile/add_protocol.py | 1183 +++++----- unilabos/compile/adjustph_protocol.py | 313 ++- unilabos/compile/dissolve_protocol.py | 1126 +++++++--- unilabos/compile/dry_protocol.py | 48 +- .../compile/evacuateandrefill_protocol.py | 779 ++++--- unilabos/compile/evaporate_protocol.py | 600 ++--- unilabos/compile/filter_protocol.py | 440 ++-- unilabos/compile/heatchill_protocol.py | 599 ++--- unilabos/compile/hydrogenate_protocol.py | 40 +- unilabos/compile/pump_protocol.py | 1982 +++++++++++++++-- unilabos/compile/recrystallize_protocol.py | 252 ++- unilabos/compile/reset_handling_protocol.py | 119 +- unilabos/compile/run_column_protocol.py | 806 +++++-- unilabos/compile/separate_protocol.py | 868 ++++++-- unilabos/compile/stir_protocol.py | 376 +++- unilabos/compile/utils/unit_parser.py | 206 ++ unilabos/compile/wash_solid_protocol.py | 486 ++-- unilabos/devices/virtual/virtual_column.py | 115 +- unilabos/devices/virtual/virtual_filter.py | 115 +- unilabos/devices/virtual/virtual_heatchill.py | 220 +- .../devices/virtual/virtual_multiway_valve.py | 167 +- unilabos/devices/virtual/virtual_rotavap.py | 168 +- .../devices/virtual/virtual_solenoid_valve.py | 56 +- .../virtual/virtual_solid_dispenser.py | 389 ++++ unilabos/devices/virtual/virtual_stirrer.py | 182 +- .../devices/virtual/virtual_transferpump.py | 205 +- unilabos/messages/__init__.py | 526 ++++- unilabos/registry/devices/mock_devices.yaml | 83 +- .../devices/organic_miscellaneous.yaml | 21 +- unilabos/registry/devices/temperature.yaml | 21 +- unilabos/registry/devices/virtual_device.yaml | 299 ++- unilabos/registry/devices/work_station.yaml | 319 ++- unilabos/ros/nodes/presets/protocol_node.py | 2 +- unilabos_msgs/CMakeLists.txt | 1 + unilabos_msgs/action/Add.action | 25 +- unilabos_msgs/action/AddSolid.action | 15 + unilabos_msgs/action/Dissolve.action | 21 +- unilabos_msgs/action/EvacuateAndRefill.action | 3 +- unilabos_msgs/action/Evaporate.action | 13 +- unilabos_msgs/action/Filter.action | 14 +- unilabos_msgs/action/HeatChill.action | 25 +- unilabos_msgs/action/PumpTransfer.action | 5 + unilabos_msgs/action/Recrystallize.action | 10 +- unilabos_msgs/action/RunColumn.action | 15 +- unilabos_msgs/action/Separate.action | 43 +- unilabos_msgs/action/Stir.action | 19 +- unilabos_msgs/action/WashSolid.action | 33 +- 55 files changed, 9928 insertions(+), 3669 deletions(-) create mode 100644 unilabos/compile/utils/unit_parser.py create mode 100644 unilabos/devices/virtual/virtual_solid_dispenser.py create mode 100644 unilabos_msgs/action/AddSolid.action diff --git a/README.md b/README.md index 0d02849..918e6fa 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n environment_name # Currently, you need to install the `unilabos_msgs` package # You can download the system-specific package from the Release page -conda install ros-humble-unilabos-msgs-0.9.8-xxxxx.tar.bz2 +conda install ros-humble-unilabos-msgs-0.9.10-xxxxx.tar.bz2 # Install PyLabRobot and other prerequisites git clone https://github.com/PyLabRobot/pylabrobot plr_repo diff --git a/README_zh.md b/README_zh.md index a80f06c..4163853 100644 --- a/README_zh.md +++ b/README_zh.md @@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n 环境名 # 现阶段,需要安装 `unilabos_msgs` 包 # 可以前往 Release 页面下载系统对应的包进行安装 -conda install ros-humble-unilabos-msgs-0.9.8-xxxxx.tar.bz2 +conda install ros-humble-unilabos-msgs-0.9.11-xxxxx.tar.bz2 # 安装PyLabRobot等前置 git clone https://github.com/PyLabRobot/pylabrobot plr_repo diff --git a/recipes/ros-humble-unilabos-msgs/recipe.yaml b/recipes/ros-humble-unilabos-msgs/recipe.yaml index dde6acc..5807be8 100644 --- a/recipes/ros-humble-unilabos-msgs/recipe.yaml +++ b/recipes/ros-humble-unilabos-msgs/recipe.yaml @@ -1,6 +1,6 @@ package: name: ros-humble-unilabos-msgs - version: 0.9.8 + version: 0.9.11 source: path: ../../unilabos_msgs folder: ros-humble-unilabos-msgs/src/work diff --git a/recipes/unilabos/recipe.yaml b/recipes/unilabos/recipe.yaml index c54e141..2f72c32 100644 --- a/recipes/unilabos/recipe.yaml +++ b/recipes/unilabos/recipe.yaml @@ -1,6 +1,6 @@ package: name: unilabos - version: "0.9.8" + version: "0.9.11" source: path: ../.. diff --git a/setup.py b/setup.py index 4dde107..8a7eebd 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ package_name = 'unilabos' setup( name=package_name, - version='0.9.8', + version='0.9.11', packages=find_packages(), include_package_data=True, install_requires=['setuptools'], diff --git a/test/experiments/comprehensive_protocol/checklist.md b/test/experiments/comprehensive_protocol/checklist.md index 5bb14ae..67b4c3b 100644 --- a/test/experiments/comprehensive_protocol/checklist.md +++ b/test/experiments/comprehensive_protocol/checklist.md @@ -43,6 +43,15 @@ Hydrogenate 4. 参数对齐 + + + + + + + + + class PumpTransferProtocol(BaseModel): from_vessel: str to_vessel: str @@ -51,9 +60,9 @@ class PumpTransferProtocol(BaseModel): time: float = 0 viscous: bool = False rinsing_solvent: str = "air" - rinsing_volume: float = 5000 - rinsing_repeats: int = 2 - solid: bool = False + rinsing_volume: float = 5000 + rinsing_repeats: int = 2 + solid: bool = False 测完了三个都能跑✅ flowrate: float = 500 transfer_flowrate: float = 2500 @@ -66,25 +75,25 @@ class SeparateProtocol(BaseModel): waste_phase_to_vessel: str solvent: str solvent_volume: float - through: str - repeats: int - stir_time: float + through: str + repeats: int + stir_time: float stir_speed: float - settling_time: float + settling_time: float 测完了能跑✅ class EvaporateProtocol(BaseModel): vessel: str pressure: float temp: float - time: float + time: float 测完了能跑✅ stir_speed: float class EvacuateAndRefillProtocol(BaseModel): vessel: str gas: str - repeats: int + repeats: int 测完了能跑✅ class AddProtocol(BaseModel): vessel: str @@ -95,21 +104,27 @@ class AddProtocol(BaseModel): time: float stir: bool stir_speed: float + + + + + viscous: bool - purpose: str + purpose: str 测完了能跑✅ class CentrifugeProtocol(BaseModel): vessel: str speed: float - time: float 自创的 + time: float 没毛病 temp: float class FilterProtocol(BaseModel): vessel: str filtrate_vessel: str stir: bool - stir_speed: float - temp: float + stir_speed: float + temp: float 测完了能跑✅ continue_heatchill: bool volume: float @@ -118,7 +133,10 @@ class HeatChillProtocol(BaseModel): temp: float time: float - stir: bool + + + + stir: bool 测完了能跑✅ stir_speed: float purpose: str @@ -133,7 +151,9 @@ class HeatChillStopProtocol(BaseModel): class StirProtocol(BaseModel): stir_time: float stir_speed: float - settling_time: float + + + settling_time: float 测完了能跑✅ class StartStirProtocol(BaseModel): vessel: str @@ -149,11 +169,11 @@ class TransferProtocol(BaseModel): volume: float amount: str = "" time: float = 0 - viscous: bool = False + viscous: bool = False rinsing_solvent: str = "" rinsing_volume: float = 0.0 rinsing_repeats: int = 0 - solid: bool = False + solid: bool = False 这个protocol早该删掉了 class CleanVesselProtocol(BaseModel): vessel: str @@ -165,11 +185,11 @@ class CleanVesselProtocol(BaseModel): class DissolveProtocol(BaseModel): vessel: str solvent: str - volume: float - amount: str = "" - temp: float = 25.0 - time: float = 0.0 - stir_speed: float = 0.0 + volume: float + amount: str = "" + temp: float = 25.0 + time: float = 0.0 + stir_speed: float = 0.0 测完了能跑✅ class FilterThroughProtocol(BaseModel): from_vessel: str @@ -183,16 +203,19 @@ class FilterThroughProtocol(BaseModel): class RunColumnProtocol(BaseModel): from_vessel: str to_vessel: str - column: str + column: str 测完了能跑✅ class WashSolidProtocol(BaseModel): vessel: str solvent: str volume: float - filtrate_vessel: str = "" + filtrate_vessel: str = "" temp: float = 25.0 - stir: bool = False - stir_speed: float = 0.0 + stir: bool = False + + + + stir_speed: float = 0.0 测完了能跑✅ time: float = 0.0 repeats: int = 1 @@ -219,4 +242,17 @@ class RecrystallizeProtocol(BaseModel): class HydrogenateProtocol(BaseModel): temp: str = Field(..., description="反应温度(如 '45 °C')") time: str = Field(..., description="反应时间(如 '2 h')") <新写的,没问题> - vessel: str = Field(..., description="反应容器") \ No newline at end of file + vessel: str = Field(..., description="反应容器") + + 还差 + + + + + +单位修复: + evaporate + heatchill + recrysitallize + stir + wash solid \ No newline at end of file diff --git a/test/experiments/comprehensive_protocol/comprehensive_station.json b/test/experiments/comprehensive_protocol/comprehensive_station.json index d0f5c6a..43e4cc6 100644 --- a/test/experiments/comprehensive_protocol/comprehensive_station.json +++ b/test/experiments/comprehensive_protocol/comprehensive_station.json @@ -32,7 +32,11 @@ "separator_1", "collection_bottle_1", "collection_bottle_2", - "collection_bottle_3" + "collection_bottle_3", + "solid_dispenser_1", + "solid_reagent_bottle_1", + "solid_reagent_bottle_2", + "solid_reagent_bottle_3" ], "parent": null, "type": "device", @@ -672,6 +676,98 @@ "data": { "current_volume": 0.0 } + }, + { + "id": "solid_dispenser_1", + "name": "固体粉末加样器", + "children": [], + "parent": "OrganicSynthesisStation", + "type": "device", + "class": "virtual_solid_dispenser", + "position": { + "x": 600, + "y": 300, + "z": 0 + }, + "config": { + "max_capacity": 100.0, + "precision": 0.001 + }, + "data": { + "status": "Ready", + "current_reagent": "", + "dispensed_amount": 0.0, + "total_operations": 0 + } + }, + { + "id": "solid_reagent_bottle_1", + "name": "固体试剂瓶1-氯化钠", + "children": [], + "parent": "OrganicSynthesisStation", + "type": "container", + "class": "container", + "position": { + "x": 550, + "y": 250, + "z": 0 + }, + "config": { + "volume": 500.0, + "reagent": "sodium_chloride", + "physical_state": "solid" + }, + "data": { + "current_mass": 500.0, + "reagent_name": "sodium_chloride", + "physical_state": "solid" + } + }, + { + "id": "solid_reagent_bottle_2", + "name": "固体试剂瓶2-碳酸钠", + "children": [], + "parent": "OrganicSynthesisStation", + "type": "container", + "class": "container", + "position": { + "x": 600, + "y": 250, + "z": 0 + }, + "config": { + "volume": 500.0, + "reagent": "sodium_carbonate", + "physical_state": "solid" + }, + "data": { + "current_mass": 500.0, + "reagent_name": "sodium_carbonate", + "physical_state": "solid" + } + }, + { + "id": "solid_reagent_bottle_3", + "name": "固体试剂瓶3-氯化镁", + "children": [], + "parent": "OrganicSynthesisStation", + "type": "container", + "class": "container", + "position": { + "x": 650, + "y": 250, + "z": 0 + }, + "config": { + "volume": 500.0, + "reagent": "magnesium_chloride", + "physical_state": "solid" + }, + "data": { + "current_mass": 500.0, + "reagent_name": "magnesium_chloride", + "physical_state": "solid" + } } ], "links": [ @@ -802,7 +898,7 @@ "type": "fluid", "port": { "multiway_valve_2": "3", - "solenoid_valve_2": "in" + "solenoid_valve_2": "out" } }, { @@ -812,7 +908,7 @@ "type": "fluid", "port": { "gas_source_1": "gassource", - "solenoid_valve_2": "out" + "solenoid_valve_2": "in" } }, { @@ -964,6 +1060,46 @@ "solenoid_valve_3": "out", "main_reactor": "top" } + }, + { + "id": "link_solid_dispenser_to_reactor", + "source": "solid_dispenser_1", + "target": "main_reactor", + "type": "resource", + "port": { + "solid_dispenser_1": "SolidOut", + "main_reactor": "top" + } + }, + { + "id": "link_solid_bottle1_to_dispenser", + "source": "solid_reagent_bottle_1", + "target": "solid_dispenser_1", + "type": "resource", + "port": { + "solid_reagent_bottle_1": "top", + "solid_dispenser_1": "SolidIn" + } + }, + { + "id": "link_solid_bottle2_to_dispenser", + "source": "solid_reagent_bottle_2", + "target": "solid_dispenser_1", + "type": "resource", + "port": { + "solid_reagent_bottle_2": "top", + "solid_dispenser_1": "SolidIn" + } + }, + { + "id": "link_solid_bottle3_to_dispenser", + "source": "solid_reagent_bottle_3", + "target": "solid_dispenser_1", + "type": "resource", + "port": { + "solid_reagent_bottle_3": "top", + "solid_dispenser_1": "SolidIn" + } } ] } \ No newline at end of file diff --git a/unilabos/app/mq.py b/unilabos/app/mq.py index c569c04..3969ec4 100644 --- a/unilabos/app/mq.py +++ b/unilabos/app/mq.py @@ -166,7 +166,7 @@ class MQTTClient: status = {"data": device_status.get(device_id, {}), "device_id": device_id, "timestamp": time.time()} address = f"labs/{MQConfig.lab_id}/devices/" self.client.publish(address, json.dumps(status), qos=2) - logger.info(f"Device status published: address: {address}, {status}") + logger.debug(f"Device status published: address: {address}, {status}") def publish_job_status(self, feedback_data: dict, job_id: str, status: str, return_info: Optional[str] = None): if self.mqtt_disable: diff --git a/unilabos/compile/add_protocol.py b/unilabos/compile/add_protocol.py index 144ec96..c46befe 100644 --- a/unilabos/compile/add_protocol.py +++ b/unilabos/compile/add_protocol.py @@ -1,627 +1,702 @@ import networkx as nx -from typing import List, Dict, Any +import re +import logging +from typing import List, Dict, Any, Union from .pump_protocol import generate_pump_protocol_with_rinsing +logger = logging.getLogger(__name__) -def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str: +def debug_print(message): + """调试输出""" + print(f"[ADD] {message}", flush=True) + logger.info(f"[ADD] {message}") + +def parse_volume_input(volume_input: Union[str, float]) -> float: """ - 根据试剂名称查找对应的试剂瓶,支持多种匹配模式: - 1. 容器名称匹配(如 flask_DMF, reagent_bottle_1-DMF) - 2. 容器内液体类型匹配(如 liquid_type: "DMF", name: "ethanol") - 3. 试剂名称匹配(如 reagent_name: "DMF", config.reagent: "ethyl_acetate") + 解析体积输入,支持带单位的字符串 Args: - G: 网络图 - reagent: 试剂名称 + volume_input: 体积输入(如 "2.7 mL", "2.67 mL", "?", 10.0) Returns: - str: 试剂瓶的vessel ID - - Raises: - ValueError: 如果找不到对应的试剂瓶 + float: 体积(毫升) """ - print(f"ADD_PROTOCOL: 正在查找试剂 '{reagent}' 的容器...") + if isinstance(volume_input, (int, float)): + debug_print(f"📏 体积输入为数值: {volume_input}") + return float(volume_input) - # 第一步:通过容器名称匹配 - possible_names = [ - f"flask_{reagent}", # flask_DMF, flask_ethanol - f"bottle_{reagent}", # bottle_DMF, bottle_ethanol - f"vessel_{reagent}", # vessel_DMF, vessel_ethanol - f"{reagent}_flask", # DMF_flask, ethanol_flask - f"{reagent}_bottle", # DMF_bottle, ethanol_bottle - f"{reagent}", # 直接用试剂名 - f"reagent_{reagent}", # reagent_DMF, reagent_ethanol - f"reagent_bottle_{reagent}", # reagent_bottle_DMF - ] - - # 尝试名称匹配 - for vessel_name in possible_names: - if vessel_name in G.nodes(): - print(f"ADD_PROTOCOL: 通过名称匹配找到容器: {vessel_name}") - return vessel_name - - # 第二步:通过模糊名称匹配(名称中包含试剂名) - for node_id in G.nodes(): - if G.nodes[node_id].get('type') == 'container': - # 检查节点ID或名称中是否包含试剂名 - node_name = G.nodes[node_id].get('name', '').lower() - if (reagent.lower() in node_id.lower() or - reagent.lower() in node_name): - print(f"ADD_PROTOCOL: 通过模糊名称匹配找到容器: {node_id} (名称: {node_name})") - return node_id - - # 第三步:通过液体类型匹配 - for node_id in G.nodes(): - if G.nodes[node_id].get('type') == 'container': - vessel_data = G.nodes[node_id].get('data', {}) - liquids = vessel_data.get('liquid', []) - - for liquid in liquids: - if isinstance(liquid, dict): - # 支持两种格式的液体类型字段 - liquid_type = liquid.get('liquid_type') or liquid.get('name', '') - reagent_name = vessel_data.get('reagent_name', '') - config_reagent = G.nodes[node_id].get('config', {}).get('reagent', '') - - # 检查多个可能的字段 - if (liquid_type.lower() == reagent.lower() or - reagent_name.lower() == reagent.lower() or - config_reagent.lower() == reagent.lower()): - print(f"ADD_PROTOCOL: 通过液体类型匹配找到容器: {node_id}") - print(f" - liquid_type: {liquid_type}") - print(f" - reagent_name: {reagent_name}") - print(f" - config.reagent: {config_reagent}") - return node_id - - # 第四步:列出所有可用的容器信息帮助调试 - available_containers = [] - for node_id in G.nodes(): - if G.nodes[node_id].get('type') == 'container': - vessel_data = G.nodes[node_id].get('data', {}) - config_data = G.nodes[node_id].get('config', {}) - liquids = vessel_data.get('liquid', []) - - container_info = { - 'id': node_id, - 'name': G.nodes[node_id].get('name', ''), - 'liquid_types': [], - 'reagent_name': vessel_data.get('reagent_name', ''), - 'config_reagent': config_data.get('reagent', '') - } - - for liquid in liquids: - if isinstance(liquid, dict): - liquid_type = liquid.get('liquid_type') or liquid.get('name', '') - if liquid_type: - container_info['liquid_types'].append(liquid_type) - - available_containers.append(container_info) - - print(f"ADD_PROTOCOL: 可用容器列表:") - for container in available_containers: - print(f" - {container['id']}: {container['name']}") - print(f" 液体类型: {container['liquid_types']}") - print(f" 试剂名称: {container['reagent_name']}") - print(f" 配置试剂: {container['config_reagent']}") - - raise ValueError(f"找不到试剂 '{reagent}' 对应的试剂瓶。尝试了名称匹配: {possible_names}") - - -def find_reagent_vessel_by_any_match(G: nx.DiGraph, reagent: str) -> str: - """ - 增强版试剂容器查找,支持各种匹配方式的别名函数 - """ - return find_reagent_vessel(G, reagent) - - -def get_vessel_reagent_volume(G: nx.DiGraph, vessel: str) -> float: - """获取容器中的试剂体积""" - if vessel not in G.nodes(): + if not volume_input or not str(volume_input).strip(): + debug_print(f"⚠️ 体积输入为空,返回0.0mL") return 0.0 - vessel_data = G.nodes[vessel].get('data', {}) - liquids = vessel_data.get('liquid', []) + volume_str = str(volume_input).lower().strip() + debug_print(f"🔍 解析体积输入: '{volume_str}'") - total_volume = 0.0 - for liquid in liquids: - if isinstance(liquid, dict): - # 支持两种格式:新格式 (name, volume) 和旧格式 (liquid_type, liquid_volume) - volume = liquid.get('volume') or liquid.get('liquid_volume', 0.0) - total_volume += volume + # 处理未知体积 + if volume_str in ['?', 'unknown', 'tbd', 'to be determined']: + default_volume = 10.0 # 默认10mL + debug_print(f"❓ 检测到未知体积,使用默认值: {default_volume}mL 🎯") + return default_volume - return total_volume + # 移除空格并提取数字和单位 + volume_clean = re.sub(r'\s+', '', volume_str) + + # 匹配数字和单位的正则表达式 + match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean) + + if not match: + debug_print(f"❌ 无法解析体积: '{volume_str}',使用默认值10mL") + return 10.0 + + value = float(match.group(1)) + unit = match.group(2) or 'ml' # 默认单位为毫升 + + # 转换为毫升 + if unit in ['l', 'liter']: + volume = value * 1000.0 # L -> mL + debug_print(f"🔄 体积转换: {value}L → {volume}mL") + elif unit in ['μl', 'ul', 'microliter']: + volume = value / 1000.0 # μL -> mL + debug_print(f"🔄 体积转换: {value}μL → {volume}mL") + else: # ml, milliliter 或默认 + volume = value # 已经是mL + debug_print(f"✅ 体积已为mL: {volume}mL") + + return volume - -def get_vessel_reagent_types(G: nx.DiGraph, vessel: str) -> List[str]: - """获取容器中所有试剂的类型""" - if vessel not in G.nodes(): - return [] - - vessel_data = G.nodes[vessel].get('data', {}) - liquids = vessel_data.get('liquid', []) - - reagent_types = [] - for liquid in liquids: - if isinstance(liquid, dict): - # 支持两种格式的试剂类型字段 - reagent_type = liquid.get('liquid_type') or liquid.get('name', '') - if reagent_type: - reagent_types.append(reagent_type) - - # 同时检查配置中的试剂信息 - config_reagent = G.nodes[vessel].get('config', {}).get('reagent', '') - reagent_name = vessel_data.get('reagent_name', '') - - if config_reagent and config_reagent not in reagent_types: - reagent_types.append(config_reagent) - if reagent_name and reagent_name not in reagent_types: - reagent_types.append(reagent_name) - - return reagent_types - - -def find_vessels_by_reagent(G: nx.DiGraph, reagent: str) -> List[str]: +def parse_mass_input(mass_input: Union[str, float]) -> float: """ - 根据试剂类型查找所有匹配的容器 - 返回匹配容器的ID列表 - """ - matching_vessels = [] + 解析质量输入,支持带单位的字符串 - for node_id in G.nodes(): - if G.nodes[node_id].get('type') == 'container': - # 检查容器名称匹配 - node_name = G.nodes[node_id].get('name', '').lower() - if reagent.lower() in node_id.lower() or reagent.lower() in node_name: - matching_vessels.append(node_id) - continue - - # 检查试剂类型匹配 - vessel_data = G.nodes[node_id].get('data', {}) - liquids = vessel_data.get('liquid', []) - config_data = G.nodes[node_id].get('config', {}) - - # 检查 reagent_name 和 config.reagent - reagent_name = vessel_data.get('reagent_name', '').lower() + Args: + mass_input: 质量输入(如 "19.3 g", "4.5 g", 2.5) + + Returns: + float: 质量(克) + """ + if isinstance(mass_input, (int, float)): + debug_print(f"⚖️ 质量输入为数值: {mass_input}g") + return float(mass_input) + + if not mass_input or not str(mass_input).strip(): + debug_print(f"⚠️ 质量输入为空,返回0.0g") + return 0.0 + + mass_str = str(mass_input).lower().strip() + debug_print(f"🔍 解析质量输入: '{mass_str}'") + + # 移除空格并提取数字和单位 + mass_clean = re.sub(r'\s+', '', mass_str) + + # 匹配数字和单位的正则表达式 + match = re.match(r'([0-9]*\.?[0-9]+)\s*(g|mg|kg|gram|milligram|kilogram)?', mass_clean) + + if not match: + debug_print(f"❌ 无法解析质量: '{mass_str}',返回0.0g") + return 0.0 + + value = float(match.group(1)) + unit = match.group(2) or 'g' # 默认单位为克 + + # 转换为克 + if unit in ['mg', 'milligram']: + mass = value / 1000.0 # mg -> g + debug_print(f"🔄 质量转换: {value}mg → {mass}g") + elif unit in ['kg', 'kilogram']: + mass = value * 1000.0 # kg -> g + debug_print(f"🔄 质量转换: {value}kg → {mass}g") + else: # g, gram 或默认 + mass = value # 已经是g + debug_print(f"✅ 质量已为g: {mass}g") + + return mass + +def parse_time_input(time_input: Union[str, float]) -> float: + """ + 解析时间输入,支持带单位的字符串 + + Args: + time_input: 时间输入(如 "1 h", "20 min", "30 s", 60.0) + + Returns: + float: 时间(秒) + """ + if isinstance(time_input, (int, float)): + debug_print(f"⏱️ 时间输入为数值: {time_input}秒") + return float(time_input) + + if not time_input or not str(time_input).strip(): + debug_print(f"⚠️ 时间输入为空,返回0秒") + return 0.0 + + time_str = str(time_input).lower().strip() + debug_print(f"🔍 解析时间输入: '{time_str}'") + + # 处理未知时间 + if time_str in ['?', 'unknown', 'tbd']: + default_time = 60.0 # 默认1分钟 + debug_print(f"❓ 检测到未知时间,使用默认值: {default_time}s (1分钟) ⏰") + return default_time + + # 移除空格并提取数字和单位 + time_clean = re.sub(r'\s+', '', time_str) + + # 匹配数字和单位的正则表达式 + match = re.match(r'([0-9]*\.?[0-9]+)\s*(s|sec|second|min|minute|h|hr|hour|d|day)?', time_clean) + + if not match: + debug_print(f"❌ 无法解析时间: '{time_str}',返回0s") + return 0.0 + + value = float(match.group(1)) + unit = match.group(2) or 's' # 默认单位为秒 + + # 转换为秒 + if unit in ['min', 'minute']: + time_sec = value * 60.0 # min -> s + debug_print(f"🔄 时间转换: {value}分钟 → {time_sec}秒") + elif unit in ['h', 'hr', 'hour']: + time_sec = value * 3600.0 # h -> s + debug_print(f"🔄 时间转换: {value}小时 → {time_sec}秒") + elif unit in ['d', 'day']: + time_sec = value * 86400.0 # d -> s + debug_print(f"🔄 时间转换: {value}天 → {time_sec}秒") + else: # s, sec, second 或默认 + time_sec = value # 已经是s + debug_print(f"✅ 时间已为秒: {time_sec}秒") + + return time_sec + +def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str: + """增强版试剂容器查找,支持固体和液体""" + debug_print(f"🔍 开始查找试剂 '{reagent}' 的容器...") + + # 🔧 方法1:直接搜索 data.reagent_name 和 config.reagent + debug_print(f"📋 方法1: 搜索reagent字段...") + for node in G.nodes(): + node_data = G.nodes[node].get('data', {}) + node_type = G.nodes[node].get('type', '') + config_data = G.nodes[node].get('config', {}) + + # 只搜索容器类型的节点 + if node_type == 'container': + reagent_name = node_data.get('reagent_name', '').lower() config_reagent = config_data.get('reagent', '').lower() - if (reagent.lower() == reagent_name or - reagent.lower() == config_reagent): - matching_vessels.append(node_id) - continue + # 精确匹配 + if reagent_name == reagent.lower() or config_reagent == reagent.lower(): + debug_print(f"✅ 通过reagent字段精确匹配到容器: {node} 🎯") + return node - # 检查液体列表 + # 模糊匹配 + if (reagent.lower() in reagent_name and reagent_name) or \ + (reagent.lower() in config_reagent and config_reagent): + debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node} 🔍") + return node + + # 🔧 方法2:常见的容器命名规则 + debug_print(f"📋 方法2: 使用命名规则查找...") + reagent_clean = reagent.lower().replace(' ', '_').replace('-', '_') + possible_names = [ + reagent_clean, + f"flask_{reagent_clean}", + f"bottle_{reagent_clean}", + f"vessel_{reagent_clean}", + f"{reagent_clean}_flask", + f"{reagent_clean}_bottle", + f"reagent_{reagent_clean}", + f"reagent_bottle_{reagent_clean}", + f"solid_reagent_bottle_{reagent_clean}", + f"reagent_bottle_1", # 通用试剂瓶 + f"reagent_bottle_2", + f"reagent_bottle_3" + ] + + debug_print(f"🔍 尝试的容器名称: {possible_names[:5]}... (共{len(possible_names)}个)") + + for name in possible_names: + if name in G.nodes(): + node_type = G.nodes[name].get('type', '') + if node_type == 'container': + debug_print(f"✅ 通过命名规则找到容器: {name} 📝") + return name + + # 🔧 方法3:节点名称模糊匹配 + debug_print(f"📋 方法3: 节点名称模糊匹配...") + for node_id in G.nodes(): + node_data = G.nodes[node_id] + if node_data.get('type') == 'container': + # 检查节点名称是否包含试剂名称 + if reagent_clean in node_id.lower(): + debug_print(f"✅ 通过节点名称模糊匹配到容器: {node_id} 🔍") + return node_id + + # 检查液体类型匹配 + vessel_data = node_data.get('data', {}) + liquids = vessel_data.get('liquid', []) for liquid in liquids: if isinstance(liquid, dict): liquid_type = liquid.get('liquid_type') or liquid.get('name', '') if liquid_type.lower() == reagent.lower(): - matching_vessels.append(node_id) - break + debug_print(f"✅ 通过液体类型匹配到容器: {node_id} 💧") + return node_id - return matching_vessels - + # 🔧 方法4:使用第一个试剂瓶作为备选 + debug_print(f"📋 方法4: 查找备选试剂瓶...") + for node_id in G.nodes(): + node_data = G.nodes[node_id] + if (node_data.get('type') == 'container' and + ('reagent' in node_id.lower() or 'bottle' in node_id.lower())): + debug_print(f"⚠️ 未找到专用容器,使用备选试剂瓶: {node_id} 🔄") + return node_id + + debug_print(f"❌ 所有方法都失败了,无法找到容器!") + raise ValueError(f"找不到试剂 '{reagent}' 对应的容器") def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str: - """ - 查找与指定容器相连的搅拌器 + """查找连接到指定容器的搅拌器""" + debug_print(f"🔍 查找连接到容器 '{vessel}' 的搅拌器...") - Args: - G: 网络图 - vessel: 容器ID + stirrer_nodes = [] + for node in G.nodes(): + node_class = G.nodes[node].get('class', '').lower() + if 'stirrer' in node_class: + stirrer_nodes.append(node) + debug_print(f"📋 发现搅拌器: {node}") - Returns: - str: 搅拌器ID,如果找不到则返回None - """ - # 查找所有搅拌器节点 - stirrer_nodes = [node for node in G.nodes() - if (G.nodes[node].get('class') or '') == 'virtual_stirrer'] + debug_print(f"📊 共找到 {len(stirrer_nodes)} 个搅拌器") - # 检查哪个搅拌器与目标容器相连 + # 查找连接到容器的搅拌器 for stirrer in stirrer_nodes: if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): + debug_print(f"✅ 找到连接的搅拌器: {stirrer} 🔗") return stirrer - # 如果没有直接连接,返回第一个可用的搅拌器 - return stirrer_nodes[0] if stirrer_nodes else None + # 返回第一个搅拌器 + if stirrer_nodes: + debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个: {stirrer_nodes[0]} 🔄") + return stirrer_nodes[0] + + debug_print(f"❌ 未找到任何搅拌器") + return "" +def find_solid_dispenser(G: nx.DiGraph) -> str: + """查找固体加样器""" + debug_print(f"🔍 查找固体加样器...") + + for node in G.nodes(): + node_class = G.nodes[node].get('class', '').lower() + if 'solid_dispenser' in node_class or 'dispenser' in node_class: + debug_print(f"✅ 找到固体加样器: {node} 🥄") + return node + + debug_print(f"❌ 未找到固体加样器") + return "" + +# 🆕 创建进度日志动作 +def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]: + """创建一个动作日志""" + full_message = f"{emoji} {message}" + debug_print(full_message) + logger.info(full_message) + print(f"[ACTION] {full_message}", flush=True) + + return { + "action_name": "wait", + "action_kwargs": { + "time": 0.1, + "log_message": full_message + } + } def generate_add_protocol( G: nx.DiGraph, vessel: str, reagent: str, - volume: float, - mass: float = 0.0, + # 🔧 修复:所有参数都用 Union 类型,支持字符串和数值 + volume: Union[str, float] = 0.0, + mass: Union[str, float] = 0.0, amount: str = "", - time: float = 0.0, - stir: bool = False, - stir_speed: float = 300.0, - viscous: bool = False, - purpose: str = "添加试剂" -) -> List[Dict[str, Any]]: - """ - 生成添加试剂的协议序列,支持智能试剂匹配 - - 基于pump_protocol的成熟算法,实现试剂添加功能: - 1. 智能查找试剂瓶(支持名称匹配、液体类型匹配、试剂配置匹配) - 2. **先启动搅拌,再进行转移** - 确保试剂添加更均匀 - 3. 使用pump_protocol实现液体转移 - - Args: - G: 有向图,节点为容器和设备,边为连接关系 - vessel: 目标容器(要添加试剂的容器) - reagent: 试剂名称(用于查找对应的试剂瓶) - volume: 要添加的体积 (mL) - mass: 要添加的质量 (g) - 暂时未使用,预留接口 - amount: 其他数量描述 - time: 添加时间 (s),如果指定则计算流速 - stir: 是否启用搅拌 - stir_speed: 搅拌速度 (RPM) - viscous: 是否为粘稠液体 - purpose: 添加目的描述 - - Returns: - List[Dict[str, Any]]: 动作序列 - - Raises: - ValueError: 当找不到必要的设备或容器时 - """ - action_sequence = [] - - print(f"ADD_PROTOCOL: 开始生成添加试剂协议") - print(f" - 目标容器: {vessel}") - print(f" - 试剂: {reagent}") - print(f" - 体积: {volume} mL") - print(f" - 质量: {mass} g") - print(f" - 搅拌: {stir} (速度: {stir_speed} RPM)") - print(f" - 粘稠: {viscous}") - print(f" - 目的: {purpose}") - - # 1. 验证目标容器存在 - if vessel not in G.nodes(): - raise ValueError(f"目标容器 '{vessel}' 不存在于系统中") - - # 2. 智能查找试剂瓶 - try: - reagent_vessel = find_reagent_vessel(G, reagent) - print(f"ADD_PROTOCOL: 找到试剂容器: {reagent_vessel}") - except ValueError as e: - raise ValueError(f"无法找到试剂 '{reagent}': {str(e)}") - - # 3. 验证试剂容器中的试剂体积 - available_volume = get_vessel_reagent_volume(G, reagent_vessel) - print(f"ADD_PROTOCOL: 试剂容器 {reagent_vessel} 中有 {available_volume} mL 试剂") - - if available_volume < volume: - print(f"ADD_PROTOCOL: 警告 - 试剂容器中的试剂不足!需要 {volume} mL,可用 {available_volume} mL") - - # 4. 验证是否存在从试剂瓶到目标容器的路径 - try: - path = nx.shortest_path(G, source=reagent_vessel, target=vessel) - print(f"ADD_PROTOCOL: 找到路径 {reagent_vessel} -> {vessel}: {path}") - except nx.NetworkXNoPath: - raise ValueError(f"从试剂瓶 '{reagent_vessel}' 到目标容器 '{vessel}' 没有可用路径") - - # 5. **先启动搅拌** - 关键改进! - if stir: - try: - stirrer_id = find_connected_stirrer(G, vessel) - - if stirrer_id: - print(f"ADD_PROTOCOL: 找到搅拌器 {stirrer_id},将在添加前启动搅拌") - - # 先启动搅拌 - stir_action = { - "device_id": stirrer_id, - "action_name": "start_stir", - "action_kwargs": { - "vessel": vessel, - "stir_speed": stir_speed, - "purpose": f"{purpose}: 启动搅拌,准备添加 {reagent}" - } - } - - action_sequence.append(stir_action) - print(f"ADD_PROTOCOL: 已添加搅拌动作,速度 {stir_speed} RPM") - - # 等待搅拌稳定 - action_sequence.append({ - "action_name": "wait", - "action_kwargs": {"time": 5} - }) - else: - print(f"ADD_PROTOCOL: 警告 - 需要搅拌但未找到与容器 {vessel} 相连的搅拌器") - - except Exception as e: - print(f"ADD_PROTOCOL: 搅拌器配置出错: {str(e)}") - - # 6. 如果指定了体积,执行液体转移 - if volume > 0: - # 6.1 计算流速参数 - if time > 0: - # 根据时间计算流速 - transfer_flowrate = volume / time - flowrate = transfer_flowrate - else: - # 使用默认流速 - if viscous: - transfer_flowrate = 0.3 # 粘稠液体用较慢速度 - flowrate = 1.0 - else: - transfer_flowrate = 0.5 # 普通液体默认速度 - flowrate = 2.5 - - print(f"ADD_PROTOCOL: 准备转移 {volume} mL 从 {reagent_vessel} 到 {vessel}") - print(f"ADD_PROTOCOL: 转移流速={transfer_flowrate} mL/s, 注入流速={flowrate} mL/s") - - # 6.2 使用pump_protocol的核心算法实现液体转移 - try: - pump_actions = generate_pump_protocol_with_rinsing( - G=G, - from_vessel=reagent_vessel, - to_vessel=vessel, - volume=volume, - amount=amount, - time=time, - viscous=viscous, - rinsing_solvent="", # 添加试剂通常不需要清洗 - rinsing_volume=0.0, - rinsing_repeats=0, - solid=False, - flowrate=flowrate, - transfer_flowrate=transfer_flowrate - ) - - # 添加pump actions到序列中 - action_sequence.extend(pump_actions) - - except Exception as e: - raise ValueError(f"生成泵协议时出错: {str(e)}") - - print(f"ADD_PROTOCOL: 生成了 {len(action_sequence)} 个动作") - print(f"ADD_PROTOCOL: 添加试剂协议生成完成") - - return action_sequence - - -def generate_add_protocol_with_cleaning( - G: nx.DiGraph, - vessel: str, - reagent: str, - volume: float, - mass: float = 0.0, - amount: str = "", - time: float = 0.0, + time: Union[str, float] = 0.0, stir: bool = False, stir_speed: float = 300.0, viscous: bool = False, purpose: str = "添加试剂", - cleaning_solvent: str = "air", - cleaning_volume: float = 5.0, - cleaning_repeats: int = 1 + # XDL扩展参数 + mol: str = "", + event: str = "", + rate_spec: str = "", + equiv: str = "", + ratio: str = "", + **kwargs ) -> List[Dict[str, Any]]: """ - 生成带清洗的添加试剂协议,支持智能试剂匹配 + 生成添加试剂协议 - 修复版 - 与普通添加协议的区别是会在添加后进行管道清洗 - - Args: - G: 有向图 - vessel: 目标容器 - reagent: 试剂名称 - volume: 添加体积 - mass: 添加质量(预留) - amount: 其他数量描述 - time: 添加时间 - stir: 是否搅拌 - stir_speed: 搅拌速度 - viscous: 是否粘稠 - purpose: 添加目的 - cleaning_solvent: 清洗溶剂("air"表示空气清洗) - cleaning_volume: 清洗体积 - cleaning_repeats: 清洗重复次数 - - Returns: - List[Dict[str, Any]]: 动作序列 + 支持所有XDL参数和单位: + - volume: "2.7 mL", "2.67 mL", "?" 或数值 + - mass: "19.3 g", "4.5 g" 或数值 + - time: "1 h", "20 min" 或数值(秒) + - mol: "0.28 mol", "16.2 mmol", "25.2 mmol" + - rate_spec: "portionwise", "dropwise" + - event: "A", "B" + - equiv: "1.1" + - ratio: "?", "1:1" """ + + debug_print("=" * 60) + debug_print("🚀 开始生成添加试剂协议") + debug_print(f"📋 原始参数:") + debug_print(f" 🥼 vessel: '{vessel}'") + debug_print(f" 🧪 reagent: '{reagent}'") + debug_print(f" 📏 volume: {volume} (类型: {type(volume)})") + debug_print(f" ⚖️ mass: {mass} (类型: {type(mass)})") + debug_print(f" ⏱️ time: {time} (类型: {type(time)})") + debug_print(f" 🧬 mol: '{mol}'") + debug_print(f" 🎯 event: '{event}'") + debug_print(f" ⚡ rate_spec: '{rate_spec}'") + debug_print(f" 🌪️ stir: {stir}") + debug_print(f" 🔄 stir_speed: {stir_speed} rpm") + debug_print("=" * 60) + action_sequence = [] - # 1. 智能查找试剂瓶 - reagent_vessel = find_reagent_vessel(G, reagent) + # === 参数验证 === + debug_print("🔍 步骤1: 参数验证...") + action_sequence.append(create_action_log(f"开始添加试剂 '{reagent}' 到容器 '{vessel}'", "🎬")) - # 2. **先启动搅拌** - if stir: - stirrer_id = find_connected_stirrer(G, vessel) - if stirrer_id: - action_sequence.append({ - "device_id": stirrer_id, - "action_name": "start_stir", - "action_kwargs": { - "vessel": vessel, - "stir_speed": stir_speed, - "purpose": f"{purpose}: 启动搅拌,准备添加 {reagent}" - } - }) + if not vessel: + debug_print("❌ vessel 参数不能为空") + raise ValueError("vessel 参数不能为空") + if not reagent: + debug_print("❌ reagent 参数不能为空") + raise ValueError("reagent 参数不能为空") + + if vessel not in G.nodes(): + debug_print(f"❌ 容器 '{vessel}' 不存在于系统中") + raise ValueError(f"容器 '{vessel}' 不存在于系统中") + + debug_print("✅ 基本参数验证通过") + + # === 🔧 关键修复:参数解析 === + debug_print("🔍 步骤2: 参数解析...") + action_sequence.append(create_action_log("正在解析添加参数...", "🔍")) + + # 解析各种参数为数值 + final_volume = parse_volume_input(volume) + final_mass = parse_mass_input(mass) + final_time = parse_time_input(time) + + debug_print(f"📊 解析结果:") + debug_print(f" 📏 体积: {final_volume}mL") + debug_print(f" ⚖️ 质量: {final_mass}g") + debug_print(f" ⏱️ 时间: {final_time}s") + debug_print(f" 🧬 摩尔: '{mol}'") + debug_print(f" 🎯 事件: '{event}'") + debug_print(f" ⚡ 速率: '{rate_spec}'") + + # === 判断添加类型 === + debug_print("🔍 步骤3: 判断添加类型...") + + # 🔧 修复:现在使用解析后的数值进行比较 + is_solid = (final_mass > 0 or (mol and mol.strip() != "")) + is_liquid = (final_volume > 0) + + if not is_solid and not is_liquid: + # 默认为液体,10mL + is_liquid = True + final_volume = 10.0 + debug_print("⚠️ 未指定体积或质量,默认为10mL液体") + + add_type = "固体" if is_solid else "液体" + add_emoji = "🧂" if is_solid else "💧" + debug_print(f"📋 添加类型: {add_type} {add_emoji}") + + action_sequence.append(create_action_log(f"确定添加类型: {add_type} {add_emoji}", "📋")) + + # === 执行添加流程 === + debug_print("🔍 步骤4: 执行添加流程...") + + try: + if is_solid: + # === 固体添加路径 === + debug_print(f"🧂 使用固体添加路径") + action_sequence.append(create_action_log("开始固体试剂添加流程", "🧂")) - # 等待搅拌稳定 - action_sequence.append({ - "action_name": "wait", - "action_kwargs": {"time": 5} - }) - - # 3. 计算流速 - if time > 0: - transfer_flowrate = volume / time - flowrate = transfer_flowrate - else: - if viscous: - transfer_flowrate = 0.3 - flowrate = 1.0 - else: - transfer_flowrate = 0.5 - flowrate = 2.5 - - # 4. 使用带清洗的pump_protocol - pump_actions = generate_pump_protocol_with_rinsing( - G=G, - from_vessel=reagent_vessel, - to_vessel=vessel, - volume=volume, - amount=amount, - time=time, - viscous=viscous, - rinsing_solvent=cleaning_solvent, - rinsing_volume=cleaning_volume, - rinsing_repeats=cleaning_repeats, - solid=False, - flowrate=flowrate, - transfer_flowrate=transfer_flowrate - ) - - action_sequence.extend(pump_actions) - - return action_sequence - - -def generate_sequential_add_protocol( - G: nx.DiGraph, - vessel: str, - reagents: List[Dict[str, Any]], - stir_between_additions: bool = True, - final_stir: bool = True, - final_stir_speed: float = 400.0, - final_stir_time: float = 300.0 -) -> List[Dict[str, Any]]: - """ - 生成连续添加多种试剂的协议,支持智能试剂匹配 - - Args: - G: 网络图 - vessel: 目标容器 - reagents: 试剂列表,每个元素包含试剂添加参数 - stir_between_additions: 是否在每次添加之间搅拌 - final_stir: 是否在所有添加完成后进行最终搅拌 - final_stir_speed: 最终搅拌速度 - final_stir_time: 最终搅拌时间 - - Returns: - List[Dict[str, Any]]: 完整的动作序列 - - Example: - reagents = [ - { - "reagent": "DMF", # 会匹配 reagent_bottle_1 (reagent_name: "DMF") - "volume": 10.0, - "viscous": False, - "stir_speed": 300.0 - }, - { - "reagent": "ethyl_acetate", # 会匹配 reagent_bottle_2 (reagent_name: "ethyl_acetate") - "volume": 5.0, - "viscous": False, - "stir_speed": 350.0 - } - ] - """ - action_sequence = [] - - print(f"ADD_PROTOCOL: 开始连续添加 {len(reagents)} 种试剂到容器 {vessel}") - - for i, reagent_params in enumerate(reagents): - reagent_name = reagent_params.get('reagent') - print(f"ADD_PROTOCOL: 处理第 {i+1}/{len(reagents)} 个试剂: {reagent_name}") - - # 生成单个试剂的添加协议 - add_actions = generate_add_protocol( - G=G, - vessel=vessel, - reagent=reagent_name, - volume=reagent_params.get('volume', 0.0), - mass=reagent_params.get('mass', 0.0), - amount=reagent_params.get('amount', ''), - time=reagent_params.get('time', 0.0), - stir=stir_between_additions, - stir_speed=reagent_params.get('stir_speed', 300.0), - viscous=reagent_params.get('viscous', False), - purpose=reagent_params.get('purpose', f'添加试剂 {reagent_name} ({i+1}/{len(reagents)})') - ) - - action_sequence.extend(add_actions) - - # 在添加之间加入等待时间 - if i < len(reagents) - 1: # 不是最后一个试剂 - action_sequence.append({ - "action_name": "wait", - "action_kwargs": {"time": 10} # 试剂混合时间 - }) - - # 最终搅拌 - if final_stir: - stirrer_id = find_connected_stirrer(G, vessel) - if stirrer_id: - print(f"ADD_PROTOCOL: 添加最终搅拌动作,速度 {final_stir_speed} RPM,时间 {final_stir_time} 秒") - action_sequence.extend([ - { - "device_id": stirrer_id, - "action_name": "stir", - "action_kwargs": { - "stir_time": final_stir_time, - "stir_speed": final_stir_speed, - "settling_time": 30.0 - } + solid_dispenser = find_solid_dispenser(G) + if solid_dispenser: + action_sequence.append(create_action_log(f"找到固体加样器: {solid_dispenser}", "🥄")) + + # 启动搅拌 + if stir: + debug_print("🌪️ 准备启动搅拌...") + action_sequence.append(create_action_log("准备启动搅拌器", "🌪️")) + + stirrer_id = find_connected_stirrer(G, vessel) + if stirrer_id: + action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {stir_speed} rpm)", "🔄")) + + action_sequence.append({ + "device_id": stirrer_id, + "action_name": "start_stir", + "action_kwargs": { + "vessel": vessel, + "stir_speed": stir_speed, + "purpose": f"准备添加固体 {reagent}" + } + }) + # 等待搅拌稳定 + action_sequence.append(create_action_log("等待搅拌稳定...", "⏳")) + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 3} + }) + + # 固体加样 + add_kwargs = { + "vessel": vessel, + "reagent": reagent, + "purpose": purpose, + "event": event, + "rate_spec": rate_spec } - ]) + + if final_mass > 0: + add_kwargs["mass"] = str(final_mass) + action_sequence.append(create_action_log(f"准备添加固体: {final_mass}g", "⚖️")) + if mol and mol.strip(): + add_kwargs["mol"] = mol + action_sequence.append(create_action_log(f"按摩尔数添加: {mol}", "🧬")) + if equiv and equiv.strip(): + add_kwargs["equiv"] = equiv + action_sequence.append(create_action_log(f"当量: {equiv}", "🔢")) + + action_sequence.append(create_action_log("开始固体加样操作", "🥄")) + action_sequence.append({ + "device_id": solid_dispenser, + "action_name": "add_solid", + "action_kwargs": add_kwargs + }) + + action_sequence.append(create_action_log("固体加样完成", "✅")) + + # 添加后等待 + if final_time > 0: + wait_minutes = final_time / 60 + action_sequence.append(create_action_log(f"等待反应进行 ({wait_minutes:.1f}分钟)", "⏰")) + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": final_time} + }) + + debug_print(f"✅ 固体添加完成") + else: + debug_print("❌ 未找到固体加样器,跳过固体添加") + action_sequence.append(create_action_log("未找到固体加样器,无法添加固体", "❌")) + + else: + # === 液体添加路径 === + debug_print(f"💧 使用液体添加路径") + action_sequence.append(create_action_log("开始液体试剂添加流程", "💧")) + + # 查找试剂容器 + action_sequence.append(create_action_log("正在查找试剂容器...", "🔍")) + reagent_vessel = find_reagent_vessel(G, reagent) + action_sequence.append(create_action_log(f"找到试剂容器: {reagent_vessel}", "🧪")) + + # 启动搅拌 + if stir: + debug_print("🌪️ 准备启动搅拌...") + action_sequence.append(create_action_log("准备启动搅拌器", "🌪️")) + + stirrer_id = find_connected_stirrer(G, vessel) + if stirrer_id: + action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {stir_speed} rpm)", "🔄")) + + action_sequence.append({ + "device_id": stirrer_id, + "action_name": "start_stir", + "action_kwargs": { + "vessel": vessel, + "stir_speed": stir_speed, + "purpose": f"准备添加液体 {reagent}" + } + }) + # 等待搅拌稳定 + action_sequence.append(create_action_log("等待搅拌稳定...", "⏳")) + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 5} + }) + + # 计算流速 + if final_time > 0: + flowrate = final_volume / final_time * 60 # mL/min + transfer_flowrate = flowrate + debug_print(f"⚡ 根据时间计算流速: {flowrate:.2f} mL/min") + else: + if rate_spec == "dropwise": + flowrate = 0.5 # 滴加,很慢 + transfer_flowrate = 0.2 + debug_print(f"💧 滴加模式,流速: {flowrate} mL/min") + elif viscous: + flowrate = 1.0 # 粘性液体 + transfer_flowrate = 0.3 + debug_print(f"🍯 粘性液体,流速: {flowrate} mL/min") + else: + flowrate = 2.5 # 正常流速 + transfer_flowrate = 0.5 + debug_print(f"⚡ 正常流速: {flowrate} mL/min") + + action_sequence.append(create_action_log(f"设置流速: {flowrate:.2f} mL/min", "⚡")) + action_sequence.append(create_action_log(f"开始转移 {final_volume}mL 液体", "🚰")) + + # 调用pump protocol + pump_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=reagent_vessel, + to_vessel=vessel, + volume=final_volume, + amount=amount, + time=final_time, + viscous=viscous, + rinsing_solvent="", + rinsing_volume=0.0, + rinsing_repeats=0, + solid=False, + flowrate=flowrate, + transfer_flowrate=transfer_flowrate, + rate_spec=rate_spec, + event=event, + through="", + **kwargs + ) + action_sequence.extend(pump_actions) + debug_print(f"✅ 液体转移完成,添加了 {len(pump_actions)} 个动作") + action_sequence.append(create_action_log(f"液体转移完成 ({len(pump_actions)} 个操作)", "✅")) + + except Exception as e: + debug_print(f"❌ 试剂添加失败: {str(e)}") + action_sequence.append(create_action_log(f"试剂添加失败: {str(e)}", "❌")) + # 添加错误日志 + action_sequence.append({ + "device_id": "system", + "action_name": "log_message", + "action_kwargs": { + "message": f"试剂 '{reagent}' 添加失败: {str(e)}" + } + }) + + # === 最终结果 === + debug_print("=" * 60) + debug_print(f"🎉 添加试剂协议生成完成") + debug_print(f"📊 总动作数: {len(action_sequence)}") + debug_print(f"📋 处理总结:") + debug_print(f" 🧪 试剂: {reagent}") + debug_print(f" {add_emoji} 添加类型: {add_type}") + debug_print(f" 🥼 目标容器: {vessel}") + if is_liquid: + debug_print(f" 📏 体积: {final_volume}mL") + if is_solid: + debug_print(f" ⚖️ 质量: {final_mass}g") + debug_print(f" 🧬 摩尔: {mol}") + debug_print("=" * 60) + + # 添加完成日志 + summary_msg = f"试剂添加协议完成: {reagent} → {vessel}" + if is_liquid: + summary_msg += f" ({final_volume}mL)" + if is_solid: + summary_msg += f" ({final_mass}g)" + + action_sequence.append(create_action_log(summary_msg, "🎉")) - print(f"ADD_PROTOCOL: 连续添加协议生成完成,共 {len(action_sequence)} 个动作") return action_sequence +# === 便捷函数 === -# 便捷函数:常用添加方案 -def generate_organic_add_protocol( - G: nx.DiGraph, - vessel: str, - organic_reagent: str, - volume: float, - stir_speed: float = 400.0 -) -> List[Dict[str, Any]]: - """有机试剂添加:慢速、搅拌""" +def add_liquid_volume(G: nx.DiGraph, vessel: str, reagent: str, volume: Union[str, float], + time: Union[str, float] = 0.0, rate_spec: str = "") -> List[Dict[str, Any]]: + """添加指定体积的液体试剂""" + debug_print(f"💧 快速添加液体: {reagent} ({volume}) → {vessel}") return generate_add_protocol( - G, vessel, organic_reagent, volume, 0.0, "", 0.0, - True, stir_speed, False, f"添加有机试剂 {organic_reagent}" + G, vessel, reagent, + volume=volume, + time=time, + rate_spec=rate_spec ) - -def generate_viscous_add_protocol( - G: nx.DiGraph, - vessel: str, - viscous_reagent: str, - volume: float, - addition_time: float = 120.0 -) -> List[Dict[str, Any]]: - """粘稠试剂添加:慢速、长时间""" +def add_solid_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, float], + event: str = "") -> List[Dict[str, Any]]: + """添加指定质量的固体试剂""" + debug_print(f"🧂 快速添加固体: {reagent} ({mass}) → {vessel}") return generate_add_protocol( - G, vessel, viscous_reagent, volume, 0.0, "", addition_time, - True, 250.0, True, f"缓慢添加粘稠试剂 {viscous_reagent}" + G, vessel, reagent, + mass=mass, + event=event ) - -def generate_solvent_add_protocol( - G: nx.DiGraph, - vessel: str, - solvent: str, - volume: float -) -> List[Dict[str, Any]]: - """溶剂添加:快速、无需特殊处理""" +def add_solid_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str, + event: str = "") -> List[Dict[str, Any]]: + """按摩尔数添加固体试剂""" + debug_print(f"🧬 按摩尔数添加固体: {reagent} ({mol}) → {vessel}") return generate_add_protocol( - G, vessel, solvent, volume, 0.0, "", 0.0, - False, 300.0, False, f"添加溶剂 {solvent}" + G, vessel, reagent, + mol=mol, + event=event ) +def add_dropwise_liquid(G: nx.DiGraph, vessel: str, reagent: str, volume: Union[str, float], + time: Union[str, float] = "20 min", event: str = "") -> List[Dict[str, Any]]: + """滴加液体试剂""" + debug_print(f"💧 滴加液体: {reagent} ({volume}) → {vessel} (用时: {time})") + return generate_add_protocol( + G, vessel, reagent, + volume=volume, + time=time, + rate_spec="dropwise", + event=event + ) -# 使用示例和测试函数 +def add_portionwise_solid(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, float], + time: Union[str, float] = "1 h", event: str = "") -> List[Dict[str, Any]]: + """分批添加固体试剂""" + debug_print(f"🧂 分批添加固体: {reagent} ({mass}) → {vessel} (用时: {time})") + return generate_add_protocol( + G, vessel, reagent, + mass=mass, + time=time, + rate_spec="portionwise", + event=event + ) + +# 测试函数 def test_add_protocol(): - """测试添加协议的示例""" - print("=== ADD PROTOCOL 智能匹配测试 ===") - print("测试完成") - + """测试添加协议的各种参数解析""" + print("=== ADD PROTOCOL 增强版测试 ===") + + # 测试体积解析 + debug_print("🧪 测试体积解析...") + volumes = ["2.7 mL", "2.67 mL", "?", 10.0, "1 L", "500 μL"] + for vol in volumes: + result = parse_volume_input(vol) + print(f"📏 体积解析: {vol} → {result}mL") + + # 测试质量解析 + debug_print("⚖️ 测试质量解析...") + masses = ["19.3 g", "4.5 g", 2.5, "500 mg", "1 kg"] + for mass in masses: + result = parse_mass_input(mass) + print(f"⚖️ 质量解析: {mass} → {result}g") + + # 测试时间解析 + debug_print("⏱️ 测试时间解析...") + times = ["1 h", "20 min", "30 s", 60.0, "?"] + for time in times: + result = parse_time_input(time) + print(f"⏱️ 时间解析: {time} → {result}s") + + print("✅ 测试完成") if __name__ == "__main__": test_add_protocol() \ No newline at end of file diff --git a/unilabos/compile/adjustph_protocol.py b/unilabos/compile/adjustph_protocol.py index ce7c1c3..d8f1b1b 100644 --- a/unilabos/compile/adjustph_protocol.py +++ b/unilabos/compile/adjustph_protocol.py @@ -1,7 +1,30 @@ import networkx as nx +import logging from typing import List, Dict, Any from .pump_protocol import generate_pump_protocol_with_rinsing +logger = logging.getLogger(__name__) + +def debug_print(message): + """调试输出""" + print(f"[ADJUST_PH] {message}", flush=True) + logger.info(f"[ADJUST_PH] {message}") + +# 🆕 创建进度日志动作 +def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]: + """创建一个动作日志""" + full_message = f"{emoji} {message}" + debug_print(full_message) + logger.info(full_message) + print(f"[ACTION] {full_message}", flush=True) + + return { + "action_name": "wait", + "action_kwargs": { + "time": 0.1, + "log_message": full_message + } + } def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str: """ @@ -14,7 +37,7 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str: Returns: str: 试剂容器ID """ - print(f"ADJUST_PH: 正在查找试剂 '{reagent}' 的容器...") + debug_print(f"🔍 正在查找试剂 '{reagent}' 的容器...") # 常见酸碱试剂的别名映射 reagent_aliases = { @@ -29,11 +52,16 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str: # 构建搜索名称列表 search_names = [reagent.lower()] + debug_print(f"📋 基础搜索名称: {reagent.lower()}") # 添加别名 for base_name, aliases in reagent_aliases.items(): if reagent.lower() in base_name.lower() or base_name.lower() in reagent.lower(): search_names.extend([alias.lower() for alias in aliases]) + debug_print(f"🔗 添加别名: {aliases}") + break + + debug_print(f"📝 完整搜索列表: {search_names}") # 构建可能的容器名称 possible_names = [] @@ -49,13 +77,17 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str: name_clean ]) + debug_print(f"🎯 可能的容器名称 (前5个): {possible_names[:5]}... (共{len(possible_names)}个)") + # 第一步:通过容器名称匹配 + debug_print(f"📋 方法1: 精确名称匹配...") for vessel_name in possible_names: if vessel_name in G.nodes(): - print(f"ADJUST_PH: 通过名称匹配找到容器: {vessel_name}") + debug_print(f"✅ 通过名称匹配找到容器: {vessel_name} 🎯") return vessel_name # 第二步:通过模糊匹配 + debug_print(f"📋 方法2: 模糊名称匹配...") for node_id in G.nodes(): if G.nodes[node_id].get('type') == 'container': node_name = G.nodes[node_id].get('name', '').lower() @@ -63,10 +95,11 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str: # 检查是否包含任何搜索名称 for search_name in search_names: if search_name in node_id.lower() or search_name in node_name: - print(f"ADJUST_PH: 通过模糊匹配找到容器: {node_id}") + debug_print(f"✅ 通过模糊匹配找到容器: {node_id} 🔍") return node_id # 第三步:通过液体类型匹配 + debug_print(f"📋 方法3: 液体类型匹配...") for node_id in G.nodes(): if G.nodes[node_id].get('type') == 'container': vessel_data = G.nodes[node_id].get('data', {}) @@ -79,10 +112,11 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str: for search_name in search_names: if search_name in liquid_type or search_name in reagent_name: - print(f"ADJUST_PH: 通过液体类型匹配找到容器: {node_id}") + debug_print(f"✅ 通过液体类型匹配找到容器: {node_id} 💧") return node_id # 列出可用容器帮助调试 + debug_print(f"📊 列出可用容器帮助调试...") available_containers = [] for node_id in G.nodes(): if G.nodes[node_id].get('type') == 'container': @@ -98,67 +132,92 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str: 'reagent_name': vessel_data.get('reagent_name', '') }) - print(f"ADJUST_PH: 可用容器列表:") + debug_print(f"📋 可用容器列表:") for container in available_containers: - print(f" - {container['id']}: {container['name']}") - print(f" 液体: {container['liquids']}") - print(f" 试剂: {container['reagent_name']}") + debug_print(f" - 🧪 {container['id']}: {container['name']}") + debug_print(f" 💧 液体: {container['liquids']}") + debug_print(f" 🏷️ 试剂: {container['reagent_name']}") - raise ValueError(f"找不到试剂 '{reagent}' 对应的容器。尝试了: {possible_names}") - + debug_print(f"❌ 所有匹配方法都失败了") + raise ValueError(f"找不到试剂 '{reagent}' 对应的容器。尝试了: {possible_names[:10]}...") def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str: """查找与容器相连的搅拌器""" + debug_print(f"🔍 查找连接到容器 '{vessel}' 的搅拌器...") + stirrer_nodes = [node for node in G.nodes() if (G.nodes[node].get('class') or '') == 'virtual_stirrer'] + debug_print(f"📊 发现 {len(stirrer_nodes)} 个搅拌器: {stirrer_nodes}") + for stirrer in stirrer_nodes: if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): + debug_print(f"✅ 找到连接的搅拌器: {stirrer} 🔗") return stirrer - return stirrer_nodes[0] if stirrer_nodes else None + if stirrer_nodes: + debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个: {stirrer_nodes[0]} 🔄") + return stirrer_nodes[0] + + debug_print(f"❌ 未找到任何搅拌器") + return None - -def calculate_reagent_volume(target_ph_value: float, reagent: str, vessel_volume: float = 100.0) -> float: # 改为 target_ph_value +def calculate_reagent_volume(target_ph_value: float, reagent: str, vessel_volume: float = 100.0) -> float: """ 估算需要的试剂体积来调节pH Args: - target_ph_value: 目标pH值 # 改为 target_ph_value + target_ph_value: 目标pH值 reagent: 试剂名称 vessel_volume: 容器体积 (mL) Returns: float: 估算的试剂体积 (mL) """ + debug_print(f"🧮 计算试剂体积...") + debug_print(f" 📍 目标pH: {target_ph_value}") + debug_print(f" 🧪 试剂: {reagent}") + debug_print(f" 📏 容器体积: {vessel_volume}mL") + # 简化的pH调节体积估算(实际应用中需要更精确的计算) if "acid" in reagent.lower() or "hcl" in reagent.lower(): + debug_print(f"🍋 检测到酸性试剂") # 酸性试剂:pH越低需要的体积越大 - if target_ph_value < 3: # 改为 target_ph_value - return vessel_volume * 0.05 # 5% - elif target_ph_value < 5: # 改为 target_ph_value - return vessel_volume * 0.02 # 2% + if target_ph_value < 3: + volume = vessel_volume * 0.05 # 5% + debug_print(f" 💪 强酸性 (pH<3): 使用 5% 体积") + elif target_ph_value < 5: + volume = vessel_volume * 0.02 # 2% + debug_print(f" 🔸 中酸性 (pH<5): 使用 2% 体积") else: - return vessel_volume * 0.01 # 1% + volume = vessel_volume * 0.01 # 1% + debug_print(f" 🔹 弱酸性 (pH≥5): 使用 1% 体积") elif "hydroxide" in reagent.lower() or "naoh" in reagent.lower(): + debug_print(f"🧂 检测到碱性试剂") # 碱性试剂:pH越高需要的体积越大 - if target_ph_value > 11: # 改为 target_ph_value - return vessel_volume * 0.05 # 5% - elif target_ph_value > 9: # 改为 target_ph_value - return vessel_volume * 0.02 # 2% + if target_ph_value > 11: + volume = vessel_volume * 0.05 # 5% + debug_print(f" 💪 强碱性 (pH>11): 使用 5% 体积") + elif target_ph_value > 9: + volume = vessel_volume * 0.02 # 2% + debug_print(f" 🔸 中碱性 (pH>9): 使用 2% 体积") else: - return vessel_volume * 0.01 # 1% + volume = vessel_volume * 0.01 # 1% + debug_print(f" 🔹 弱碱性 (pH≤9): 使用 1% 体积") else: # 未知试剂,使用默认值 - return vessel_volume * 0.01 - + volume = vessel_volume * 0.01 + debug_print(f"❓ 未知试剂类型,使用默认 1% 体积") + + debug_print(f"📊 计算结果: {volume:.2f}mL") + return volume def generate_adjust_ph_protocol( G: nx.DiGraph, vessel: str, - ph_value: float, # 改为 ph_value + ph_value: float, reagent: str, **kwargs ) -> List[Dict[str, Any]]: @@ -168,13 +227,23 @@ def generate_adjust_ph_protocol( Args: G: 有向图,节点为容器和设备 vessel: 目标容器(需要调节pH的容器) - ph_value: 目标pH值(从XDL传入) # 改为 ph_value + ph_value: 目标pH值(从XDL传入) reagent: 酸碱试剂名称(从XDL传入) **kwargs: 其他可选参数,使用默认值 Returns: List[Dict[str, Any]]: 动作序列 """ + + debug_print("=" * 60) + debug_print("🧪 开始生成pH调节协议") + debug_print(f"📋 原始参数:") + debug_print(f" 🥼 vessel: '{vessel}'") + debug_print(f" 📊 ph_value: {ph_value}") + debug_print(f" 🧪 reagent: '{reagent}'") + debug_print(f" 📦 kwargs: {kwargs}") + debug_print("=" * 60) + action_sequence = [] # 从kwargs中获取可选参数,如果没有则使用默认值 @@ -184,48 +253,84 @@ def generate_adjust_ph_protocol( stir_time = kwargs.get('stir_time', 60.0) # 默认搅拌时间 settling_time = kwargs.get('settling_time', 30.0) # 默认平衡时间 - print(f"ADJUST_PH: 开始生成pH调节协议") - print(f" - 目标容器: {vessel}") - print(f" - 目标pH: {ph_value}") # 改为 ph_value - print(f" - 试剂: {reagent}") - print(f" - 使用默认参数: 体积=自动估算, 搅拌=True, 搅拌速度=300RPM") + debug_print(f"🔧 处理后的参数:") + debug_print(f" 📏 volume: {volume}mL (0.0表示自动估算)") + debug_print(f" 🌪️ stir: {stir}") + debug_print(f" 🔄 stir_speed: {stir_speed}rpm") + debug_print(f" ⏱️ stir_time: {stir_time}s") + debug_print(f" ⏳ settling_time: {settling_time}s") + + # 开始处理 + action_sequence.append(create_action_log(f"开始调节pH至 {ph_value}", "🧪")) + action_sequence.append(create_action_log(f"目标容器: {vessel}", "🥼")) + action_sequence.append(create_action_log(f"使用试剂: {reagent}", "⚗️")) # 1. 验证目标容器存在 + debug_print(f"🔍 步骤1: 验证目标容器...") if vessel not in G.nodes(): + debug_print(f"❌ 目标容器 '{vessel}' 不存在于系统中") raise ValueError(f"目标容器 '{vessel}' 不存在于系统中") + debug_print(f"✅ 目标容器验证通过") + action_sequence.append(create_action_log("目标容器验证通过", "✅")) + # 2. 查找酸碱试剂容器 + debug_print(f"🔍 步骤2: 查找试剂容器...") + action_sequence.append(create_action_log("正在查找试剂容器...", "🔍")) + try: reagent_vessel = find_acid_base_vessel(G, reagent) - print(f"ADJUST_PH: 找到试剂容器: {reagent_vessel}") + debug_print(f"✅ 找到试剂容器: {reagent_vessel}") + action_sequence.append(create_action_log(f"找到试剂容器: {reagent_vessel}", "🧪")) except ValueError as e: + debug_print(f"❌ 无法找到试剂容器: {str(e)}") + action_sequence.append(create_action_log(f"试剂容器查找失败: {str(e)}", "❌")) raise ValueError(f"无法找到试剂 '{reagent}': {str(e)}") - # 3. 如果未指定体积,自动估算 + # 3. 体积估算 + debug_print(f"🔍 步骤3: 体积处理...") if volume <= 0: + action_sequence.append(create_action_log("开始自动估算试剂体积", "🧮")) + # 获取目标容器的体积信息 vessel_data = G.nodes[vessel].get('data', {}) vessel_volume = vessel_data.get('max_volume', 100.0) # 默认100mL + debug_print(f"📏 容器最大体积: {vessel_volume}mL") - estimated_volume = calculate_reagent_volume(ph_value, reagent, vessel_volume) # 改为 ph_value + estimated_volume = calculate_reagent_volume(ph_value, reagent, vessel_volume) volume = estimated_volume - print(f"ADJUST_PH: 自动估算试剂体积: {volume:.2f} mL") + debug_print(f"✅ 自动估算试剂体积: {volume:.2f} mL") + action_sequence.append(create_action_log(f"估算试剂体积: {volume:.2f}mL", "📊")) + else: + debug_print(f"📏 使用指定体积: {volume}mL") + action_sequence.append(create_action_log(f"使用指定体积: {volume}mL", "📏")) # 4. 验证路径存在 + debug_print(f"🔍 步骤4: 路径验证...") + action_sequence.append(create_action_log("验证转移路径...", "🛤️")) + try: path = nx.shortest_path(G, source=reagent_vessel, target=vessel) - print(f"ADJUST_PH: 找到路径: {' → '.join(path)}") + debug_print(f"✅ 找到路径: {' → '.join(path)}") + action_sequence.append(create_action_log(f"找到转移路径: {' → '.join(path)}", "🛤️")) except nx.NetworkXNoPath: + debug_print(f"❌ 无法找到转移路径") + action_sequence.append(create_action_log("转移路径不存在", "❌")) raise ValueError(f"从试剂容器 '{reagent_vessel}' 到目标容器 '{vessel}' 没有可用路径") - # 5. 先启动搅拌(如果需要) + # 5. 搅拌器设置 + debug_print(f"🔍 步骤5: 搅拌器设置...") stirrer_id = None if stir: + action_sequence.append(create_action_log("准备启动搅拌器", "🌪️")) + try: stirrer_id = find_connected_stirrer(G, vessel) if stirrer_id: - print(f"ADJUST_PH: 找到搅拌器 {stirrer_id},启动搅拌") + debug_print(f"✅ 找到搅拌器 {stirrer_id},启动搅拌") + action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {stir_speed}rpm)", "🔄")) + action_sequence.append({ "device_id": stirrer_id, "action_name": "start_stir", @@ -237,23 +342,34 @@ def generate_adjust_ph_protocol( }) # 等待搅拌稳定 + action_sequence.append(create_action_log("等待搅拌稳定...", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 5} }) else: - print(f"ADJUST_PH: 警告 - 未找到搅拌器,继续执行") + debug_print(f"⚠️ 未找到搅拌器,继续执行") + action_sequence.append(create_action_log("未找到搅拌器,跳过搅拌", "⚠️")) except Exception as e: - print(f"ADJUST_PH: 搅拌器配置出错: {str(e)}") + debug_print(f"❌ 搅拌器配置出错: {str(e)}") + action_sequence.append(create_action_log(f"搅拌器配置失败: {str(e)}", "❌")) + else: + debug_print(f"📋 跳过搅拌设置") + action_sequence.append(create_action_log("跳过搅拌设置", "⏭️")) - # 6. 缓慢添加试剂 - 使用pump_protocol - print(f"ADJUST_PH: 开始添加试剂 {volume:.2f} mL") + # 6. 试剂添加 + debug_print(f"🔍 步骤6: 试剂添加...") + action_sequence.append(create_action_log(f"开始添加试剂 {volume:.2f}mL", "🚰")) # 计算添加时间(pH调节需要缓慢添加) addition_time = max(30.0, volume * 2.0) # 至少30秒,每mL需要2秒 + debug_print(f"⏱️ 计算添加时间: {addition_time}s (缓慢注入)") + action_sequence.append(create_action_log(f"设置添加时间: {addition_time:.0f}s (缓慢注入)", "⏱️")) try: + action_sequence.append(create_action_log("调用泵协议进行试剂转移", "🔄")) + pump_actions = generate_pump_protocol_with_rinsing( G=G, from_vessel=reagent_vessel, @@ -266,17 +382,24 @@ def generate_adjust_ph_protocol( rinsing_volume=0.0, rinsing_repeats=0, solid=False, - flowrate=0.5 # 缓慢注入 + flowrate=0.5, # 缓慢注入 + transfer_flowrate=0.3 ) action_sequence.extend(pump_actions) + debug_print(f"✅ 泵协议生成完成,添加了 {len(pump_actions)} 个动作") + action_sequence.append(create_action_log(f"试剂转移完成 ({len(pump_actions)} 个操作)", "✅")) except Exception as e: + debug_print(f"❌ 生成泵协议时出错: {str(e)}") + action_sequence.append(create_action_log(f"泵协议生成失败: {str(e)}", "❌")) raise ValueError(f"生成泵协议时出错: {str(e)}") - # 7. 持续搅拌以混合和平衡 + # 7. 混合搅拌 if stir and stirrer_id: - print(f"ADJUST_PH: 持续搅拌 {stir_time} 秒以混合试剂") + debug_print(f"🔍 步骤7: 混合搅拌...") + action_sequence.append(create_action_log(f"开始混合搅拌 {stir_time:.0f}s", "🌀")) + action_sequence.append({ "device_id": stirrer_id, "action_name": "stir", @@ -284,25 +407,47 @@ def generate_adjust_ph_protocol( "stir_time": stir_time, "stir_speed": stir_speed, "settling_time": settling_time, - "purpose": f"pH调节: 混合试剂,目标pH={ph_value}" # 改为 ph_value + "purpose": f"pH调节: 混合试剂,目标pH={ph_value}" } }) + + debug_print(f"✅ 混合搅拌设置完成") + else: + debug_print(f"⏭️ 跳过混合搅拌") + action_sequence.append(create_action_log("跳过混合搅拌", "⏭️")) + + # 8. 等待平衡 + debug_print(f"🔍 步骤8: 反应平衡...") + action_sequence.append(create_action_log(f"等待pH平衡 {settling_time:.0f}s", "⚖️")) - # 8. 等待反应平衡 action_sequence.append({ "action_name": "wait", "action_kwargs": { "time": settling_time, - "description": f"等待pH平衡到目标值 {ph_value}" # 改为 ph_value + "description": f"等待pH平衡到目标值 {ph_value}" } }) - print(f"ADJUST_PH: 协议生成完成,共 {len(action_sequence)} 个动作") - print(f"ADJUST_PH: 预计总时间: {addition_time + stir_time + settling_time:.0f} 秒") + # 9. 完成总结 + total_time = addition_time + stir_time + settling_time + + debug_print("=" * 60) + debug_print(f"🎉 pH调节协议生成完成") + debug_print(f"📊 协议统计:") + debug_print(f" 📋 总动作数: {len(action_sequence)}") + debug_print(f" ⏱️ 预计总时间: {total_time:.0f}s ({total_time/60:.1f}分钟)") + debug_print(f" 🧪 试剂: {reagent}") + debug_print(f" 📏 体积: {volume:.2f}mL") + debug_print(f" 📊 目标pH: {ph_value}") + debug_print(f" 🥼 目标容器: {vessel}") + debug_print("=" * 60) + + # 添加完成日志 + summary_msg = f"pH调节协议完成: {vessel} → pH {ph_value} (使用 {volume:.2f}mL {reagent})" + action_sequence.append(create_action_log(summary_msg, "🎉")) return action_sequence - def generate_adjust_ph_protocol_stepwise( G: nx.DiGraph, vessel: str, @@ -317,7 +462,7 @@ def generate_adjust_ph_protocol_stepwise( Args: G: 网络图 vessel: 目标容器 - pH: 目标pH值 + ph_value: 目标pH值 reagent: 酸碱试剂 max_volume: 最大试剂体积 steps: 分步数量 @@ -325,15 +470,28 @@ def generate_adjust_ph_protocol_stepwise( Returns: List[Dict[str, Any]]: 动作序列 """ - action_sequence = [] + debug_print("=" * 60) + debug_print(f"🔄 开始分步pH调节") + debug_print(f"📋 分步参数:") + debug_print(f" 🥼 vessel: {vessel}") + debug_print(f" 📊 ph_value: {ph_value}") + debug_print(f" 🧪 reagent: {reagent}") + debug_print(f" 📏 max_volume: {max_volume}mL") + debug_print(f" 🔢 steps: {steps}") + debug_print("=" * 60) - print(f"ADJUST_PH: 开始分步pH调节({steps}步)") + action_sequence = [] # 每步添加的体积 step_volume = max_volume / steps + debug_print(f"📊 每步体积: {step_volume:.2f}mL") + + action_sequence.append(create_action_log(f"开始分步pH调节 ({steps}步)", "🔄")) + action_sequence.append(create_action_log(f"每步添加: {step_volume:.2f}mL", "📏")) for i in range(steps): - print(f"ADJUST_PH: 第 {i+1}/{steps} 步,添加 {step_volume} mL") + debug_print(f"🔄 执行第 {i+1}/{steps} 步,添加 {step_volume:.2f}mL") + action_sequence.append(create_action_log(f"第 {i+1}/{steps} 步开始", "🚀")) # 生成单步协议 step_actions = generate_adjust_ph_protocol( @@ -349,9 +507,13 @@ def generate_adjust_ph_protocol_stepwise( ) action_sequence.extend(step_actions) + debug_print(f"✅ 第 {i+1}/{steps} 步完成,添加了 {len(step_actions)} 个动作") + action_sequence.append(create_action_log(f"第 {i+1}/{steps} 步完成", "✅")) # 步骤间等待 if i < steps - 1: + debug_print(f"⏳ 步骤间等待30s") + action_sequence.append(create_action_log("步骤间等待...", "⏳")) action_sequence.append({ "action_name": "wait", "action_kwargs": { @@ -360,10 +522,11 @@ def generate_adjust_ph_protocol_stepwise( } }) - print(f"ADJUST_PH: 分步pH调节完成") + debug_print(f"🎉 分步pH调节完成,共 {len(action_sequence)} 个动作") + action_sequence.append(create_action_log("分步pH调节全部完成", "🎉")) + return action_sequence - # 便捷函数:常用pH调节 def generate_acidify_protocol( G: nx.DiGraph, @@ -372,11 +535,11 @@ def generate_acidify_protocol( acid: str = "hydrochloric acid" ) -> List[Dict[str, Any]]: """酸化协议""" + debug_print(f"🍋 生成酸化协议: {vessel} → pH {target_ph} (使用 {acid})") return generate_adjust_ph_protocol( - G, vessel, target_ph, acid, 0.0, True, 300.0, 120.0, 60.0 + G, vessel, target_ph, acid ) - def generate_basify_protocol( G: nx.DiGraph, vessel: str, @@ -384,28 +547,42 @@ def generate_basify_protocol( base: str = "sodium hydroxide" ) -> List[Dict[str, Any]]: """碱化协议""" + debug_print(f"🧂 生成碱化协议: {vessel} → pH {target_ph} (使用 {base})") return generate_adjust_ph_protocol( - G, vessel, target_ph, base, 0.0, True, 300.0, 120.0, 60.0 + G, vessel, target_ph, base ) - def generate_neutralize_protocol( G: nx.DiGraph, vessel: str, reagent: str = "sodium hydroxide" ) -> List[Dict[str, Any]]: """中和协议(pH=7)""" + debug_print(f"⚖️ 生成中和协议: {vessel} → pH 7.0 (使用 {reagent})") return generate_adjust_ph_protocol( - G, vessel, 7.0, reagent, 0.0, True, 350.0, 180.0, 90.0 + G, vessel, 7.0, reagent ) - # 测试函数 def test_adjust_ph_protocol(): """测试pH调节协议""" - print("=== ADJUST PH PROTOCOL 测试 ===") - print("测试完成") - + debug_print("=== ADJUST PH PROTOCOL 增强版测试 ===") + + # 测试体积计算 + debug_print("🧮 测试体积计算...") + test_cases = [ + (2.0, "hydrochloric acid", 100.0), + (4.0, "hydrochloric acid", 100.0), + (12.0, "sodium hydroxide", 100.0), + (10.0, "sodium hydroxide", 100.0), + (7.0, "unknown reagent", 100.0) + ] + + for ph, reagent, volume in test_cases: + result = calculate_reagent_volume(ph, reagent, volume) + debug_print(f"📊 {reagent} → pH {ph}: {result:.2f}mL") + + debug_print("✅ 测试完成") if __name__ == "__main__": test_adjust_ph_protocol() \ No newline at end of file diff --git a/unilabos/compile/dissolve_protocol.py b/unilabos/compile/dissolve_protocol.py index 3da0d53..065196e 100644 --- a/unilabos/compile/dissolve_protocol.py +++ b/unilabos/compile/dissolve_protocol.py @@ -1,359 +1,889 @@ -from typing import List, Dict, Any import networkx as nx -from .pump_protocol import generate_pump_protocol +import re +import logging +from typing import List, Dict, Any, Union +from .pump_protocol import generate_pump_protocol_with_rinsing +logger = logging.getLogger(__name__) + +def debug_print(message): + """调试输出""" + print(f"[DISSOLVE] {message}", flush=True) + logger.info(f"[DISSOLVE] {message}") + +# 🆕 创建进度日志动作 +def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]: + """创建一个动作日志""" + full_message = f"{emoji} {message}" + debug_print(full_message) + logger.info(full_message) + print(f"[ACTION] {full_message}", flush=True) + + return { + "action_name": "wait", + "action_kwargs": { + "time": 0.1, + "log_message": full_message + } + } + +def parse_volume_input(volume_input: Union[str, float]) -> float: + """ + 解析体积输入,支持带单位的字符串 + + Args: + volume_input: 体积输入(如 "10 mL", "?", 10.0) + + Returns: + float: 体积(毫升) + """ + if isinstance(volume_input, (int, float)): + debug_print(f"📏 体积输入为数值: {volume_input}") + return float(volume_input) + + if not volume_input or not str(volume_input).strip(): + debug_print(f"⚠️ 体积输入为空,返回0.0mL") + return 0.0 + + volume_str = str(volume_input).lower().strip() + debug_print(f"🔍 解析体积输入: '{volume_str}'") + + # 处理未知体积 + if volume_str in ['?', 'unknown', 'tbd', 'to be determined']: + default_volume = 50.0 # 默认50mL + debug_print(f"❓ 检测到未知体积,使用默认值: {default_volume}mL 🎯") + return default_volume + + # 移除空格并提取数字和单位 + volume_clean = re.sub(r'\s+', '', volume_str) + + # 匹配数字和单位的正则表达式 + match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean) + + if not match: + debug_print(f"❌ 无法解析体积: '{volume_str}',使用默认值50mL") + return 50.0 + + value = float(match.group(1)) + unit = match.group(2) or 'ml' # 默认单位为毫升 + + # 转换为毫升 + if unit in ['l', 'liter']: + volume = value * 1000.0 # L -> mL + debug_print(f"🔄 体积转换: {value}L → {volume}mL") + elif unit in ['μl', 'ul', 'microliter']: + volume = value / 1000.0 # μL -> mL + debug_print(f"🔄 体积转换: {value}μL → {volume}mL") + else: # ml, milliliter 或默认 + volume = value # 已经是mL + debug_print(f"✅ 体积已为mL: {volume}mL") + + return volume + +def parse_mass_input(mass_input: Union[str, float]) -> float: + """ + 解析质量输入,支持带单位的字符串 + + Args: + mass_input: 质量输入(如 "2.9 g", "?", 2.5) + + Returns: + float: 质量(克) + """ + if isinstance(mass_input, (int, float)): + debug_print(f"⚖️ 质量输入为数值: {mass_input}g") + return float(mass_input) + + if not mass_input or not str(mass_input).strip(): + debug_print(f"⚠️ 质量输入为空,返回0.0g") + return 0.0 + + mass_str = str(mass_input).lower().strip() + debug_print(f"🔍 解析质量输入: '{mass_str}'") + + # 处理未知质量 + if mass_str in ['?', 'unknown', 'tbd', 'to be determined']: + default_mass = 1.0 # 默认1g + debug_print(f"❓ 检测到未知质量,使用默认值: {default_mass}g 🎯") + return default_mass + + # 移除空格并提取数字和单位 + mass_clean = re.sub(r'\s+', '', mass_str) + + # 匹配数字和单位的正则表达式 + match = re.match(r'([0-9]*\.?[0-9]+)\s*(g|mg|kg|gram|milligram|kilogram)?', mass_clean) + + if not match: + debug_print(f"❌ 无法解析质量: '{mass_str}',返回0.0g") + return 0.0 + + value = float(match.group(1)) + unit = match.group(2) or 'g' # 默认单位为克 + + # 转换为克 + if unit in ['mg', 'milligram']: + mass = value / 1000.0 # mg -> g + debug_print(f"🔄 质量转换: {value}mg → {mass}g") + elif unit in ['kg', 'kilogram']: + mass = value * 1000.0 # kg -> g + debug_print(f"🔄 质量转换: {value}kg → {mass}g") + else: # g, gram 或默认 + mass = value # 已经是g + debug_print(f"✅ 质量已为g: {mass}g") + + return mass + +def parse_time_input(time_input: Union[str, float]) -> float: + """ + 解析时间输入,支持带单位的字符串 + + Args: + time_input: 时间输入(如 "30 min", "1 h", "?", 60.0) + + Returns: + float: 时间(秒) + """ + if isinstance(time_input, (int, float)): + debug_print(f"⏱️ 时间输入为数值: {time_input}秒") + return float(time_input) + + if not time_input or not str(time_input).strip(): + debug_print(f"⚠️ 时间输入为空,返回0秒") + return 0.0 + + time_str = str(time_input).lower().strip() + debug_print(f"🔍 解析时间输入: '{time_str}'") + + # 处理未知时间 + if time_str in ['?', 'unknown', 'tbd']: + default_time = 600.0 # 默认10分钟 + debug_print(f"❓ 检测到未知时间,使用默认值: {default_time}s (10分钟) ⏰") + return default_time + + # 移除空格并提取数字和单位 + time_clean = re.sub(r'\s+', '', time_str) + + # 匹配数字和单位的正则表达式 + match = re.match(r'([0-9]*\.?[0-9]+)\s*(s|sec|second|min|minute|h|hr|hour|d|day)?', time_clean) + + if not match: + debug_print(f"❌ 无法解析时间: '{time_str}',返回0s") + return 0.0 + + value = float(match.group(1)) + unit = match.group(2) or 's' # 默认单位为秒 + + # 转换为秒 + if unit in ['min', 'minute']: + time_sec = value * 60.0 # min -> s + debug_print(f"🔄 时间转换: {value}分钟 → {time_sec}秒") + elif unit in ['h', 'hr', 'hour']: + time_sec = value * 3600.0 # h -> s + debug_print(f"🔄 时间转换: {value}小时 → {time_sec}秒") + elif unit in ['d', 'day']: + time_sec = value * 86400.0 # d -> s + debug_print(f"🔄 时间转换: {value}天 → {time_sec}秒") + else: # s, sec, second 或默认 + time_sec = value # 已经是s + debug_print(f"✅ 时间已为秒: {time_sec}秒") + + return time_sec + +def parse_temperature_input(temp_input: Union[str, float]) -> float: + """ + 解析温度输入,支持带单位的字符串 + + Args: + temp_input: 温度输入(如 "60 °C", "room temperature", "?", 25.0) + + Returns: + float: 温度(摄氏度) + """ + if isinstance(temp_input, (int, float)): + debug_print(f"🌡️ 温度输入为数值: {temp_input}°C") + return float(temp_input) + + if not temp_input or not str(temp_input).strip(): + debug_print(f"⚠️ 温度输入为空,使用默认室温25°C") + return 25.0 # 默认室温 + + temp_str = str(temp_input).lower().strip() + debug_print(f"🔍 解析温度输入: '{temp_str}'") + + # 处理特殊温度描述 + temp_aliases = { + 'room temperature': 25.0, + 'rt': 25.0, + 'ambient': 25.0, + 'cold': 4.0, + 'ice': 0.0, + 'reflux': 80.0, # 默认回流温度 + '?': 25.0, + 'unknown': 25.0 + } + + if temp_str in temp_aliases: + result = temp_aliases[temp_str] + debug_print(f"🏷️ 温度别名解析: '{temp_str}' → {result}°C") + return result + + # 移除空格并提取数字和单位 + temp_clean = re.sub(r'\s+', '', temp_str) + + # 匹配数字和单位的正则表达式 + match = re.match(r'([0-9]*\.?[0-9]+)\s*(°c|c|celsius|°f|f|fahrenheit|k|kelvin)?', temp_clean) + + if not match: + debug_print(f"❌ 无法解析温度: '{temp_str}',使用默认值25°C") + return 25.0 + + value = float(match.group(1)) + unit = match.group(2) or 'c' # 默认单位为摄氏度 + + # 转换为摄氏度 + if unit in ['°f', 'f', 'fahrenheit']: + temp_c = (value - 32) * 5/9 # F -> C + debug_print(f"🔄 温度转换: {value}°F → {temp_c:.1f}°C") + elif unit in ['k', 'kelvin']: + temp_c = value - 273.15 # K -> C + debug_print(f"🔄 温度转换: {value}K → {temp_c:.1f}°C") + else: # °c, c, celsius 或默认 + temp_c = value # 已经是C + debug_print(f"✅ 温度已为°C: {temp_c}°C") + + return temp_c def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: - """ - 查找溶剂容器 - """ - # 按照pump_protocol的命名规则查找溶剂瓶 - solvent_vessel_id = f"flask_{solvent}" + """增强版溶剂容器查找,支持多种匹配模式""" + debug_print(f"🔍 开始查找溶剂 '{solvent}' 的容器...") - if solvent_vessel_id in G.nodes(): - return solvent_vessel_id - - # 如果直接匹配失败,尝试模糊匹配 + # 🔧 方法1:直接搜索 data.reagent_name 和 config.reagent + debug_print(f"📋 方法1: 搜索reagent字段...") for node in G.nodes(): - if node.startswith('flask_') and solvent.lower() in node.lower(): - return node + node_data = G.nodes[node].get('data', {}) + node_type = G.nodes[node].get('type', '') + config_data = G.nodes[node].get('config', {}) + + # 只搜索容器类型的节点 + if node_type == 'container': + reagent_name = node_data.get('reagent_name', '').lower() + config_reagent = config_data.get('reagent', '').lower() + + # 精确匹配 + if reagent_name == solvent.lower() or config_reagent == solvent.lower(): + debug_print(f"✅ 通过reagent字段精确匹配到容器: {node} 🎯") + return node + + # 模糊匹配 + if (solvent.lower() in reagent_name and reagent_name) or \ + (solvent.lower() in config_reagent and config_reagent): + debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node} 🔍") + return node - # 如果还是找不到,列出所有可用的溶剂瓶 - available_flasks = [node for node in G.nodes() - if node.startswith('flask_') - and G.nodes[node].get('type') == 'container'] + # 🔧 方法2:常见的容器命名规则 + debug_print(f"📋 方法2: 使用命名规则查找...") + solvent_clean = solvent.lower().replace(' ', '_').replace('-', '_') + possible_names = [ + solvent_clean, + f"flask_{solvent_clean}", + f"bottle_{solvent_clean}", + f"vessel_{solvent_clean}", + f"{solvent_clean}_flask", + f"{solvent_clean}_bottle", + f"solvent_{solvent_clean}", + f"reagent_{solvent_clean}", + f"reagent_bottle_{solvent_clean}", + f"reagent_bottle_1", # 通用试剂瓶 + f"reagent_bottle_2", + f"reagent_bottle_3" + ] - raise ValueError(f"找不到溶剂 '{solvent}' 对应的溶剂瓶。可用溶剂瓶: {available_flasks}") - + debug_print(f"🔍 尝试的容器名称: {possible_names[:5]}... (共{len(possible_names)}个)") + + for name in possible_names: + if name in G.nodes(): + node_type = G.nodes[name].get('type', '') + if node_type == 'container': + debug_print(f"✅ 通过命名规则找到容器: {name} 📝") + return name + + # 🔧 方法3:节点名称模糊匹配 + debug_print(f"📋 方法3: 节点名称模糊匹配...") + for node_id in G.nodes(): + node_data = G.nodes[node_id] + if node_data.get('type') == 'container': + # 检查节点名称是否包含溶剂名称 + if solvent_clean in node_id.lower(): + debug_print(f"✅ 通过节点名称模糊匹配到容器: {node_id} 🔍") + return node_id + + # 检查液体类型匹配 + vessel_data = node_data.get('data', {}) + liquids = vessel_data.get('liquid', []) + for liquid in liquids: + if isinstance(liquid, dict): + liquid_type = liquid.get('liquid_type') or liquid.get('name', '') + if liquid_type.lower() == solvent.lower(): + debug_print(f"✅ 通过液体类型匹配到容器: {node_id} 💧") + return node_id + + # 🔧 方法4:使用第一个试剂瓶作为备选 + debug_print(f"📋 方法4: 查找备选试剂瓶...") + for node_id in G.nodes(): + node_data = G.nodes[node_id] + if (node_data.get('type') == 'container' and + ('reagent' in node_id.lower() or 'bottle' in node_id.lower() or 'flask' in node_id.lower())): + debug_print(f"⚠️ 未找到专用容器,使用备选试剂瓶: {node_id} 🔄") + return node_id + + debug_print(f"❌ 所有方法都失败了,无法找到容器!") + raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器") def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str: - """ - 查找与指定容器相连的加热搅拌器 - """ - # 查找所有加热搅拌器节点 - heatchill_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_heatchill'] + """查找连接到指定容器的加热搅拌器""" + debug_print(f"🔍 查找连接到容器 '{vessel}' 的加热搅拌器...") - # 检查哪个加热器与目标容器相连 + heatchill_nodes = [] + for node in G.nodes(): + node_class = G.nodes[node].get('class', '').lower() + if 'heatchill' in node_class: + heatchill_nodes.append(node) + debug_print(f"📋 发现加热搅拌器: {node}") + + debug_print(f"📊 共找到 {len(heatchill_nodes)} 个加热搅拌器") + + # 查找连接到容器的加热器 for heatchill in heatchill_nodes: if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill): + debug_print(f"✅ 找到连接的加热搅拌器: {heatchill} 🔗") return heatchill - # 如果没有直接连接,返回第一个可用的加热器 - return heatchill_nodes[0] if heatchill_nodes else None + # 返回第一个加热器 + if heatchill_nodes: + debug_print(f"⚠️ 未找到直接连接的加热搅拌器,使用第一个: {heatchill_nodes[0]} 🔄") + return heatchill_nodes[0] + + debug_print(f"❌ 未找到任何加热搅拌器") + return "" +def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str: + """查找连接到指定容器的搅拌器""" + debug_print(f"🔍 查找连接到容器 '{vessel}' 的搅拌器...") + + stirrer_nodes = [] + for node in G.nodes(): + node_class = G.nodes[node].get('class', '').lower() + if 'stirrer' in node_class: + stirrer_nodes.append(node) + debug_print(f"📋 发现搅拌器: {node}") + + debug_print(f"📊 共找到 {len(stirrer_nodes)} 个搅拌器") + + # 查找连接到容器的搅拌器 + for stirrer in stirrer_nodes: + if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): + debug_print(f"✅ 找到连接的搅拌器: {stirrer} 🔗") + return stirrer + + # 返回第一个搅拌器 + if stirrer_nodes: + debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个: {stirrer_nodes[0]} 🔄") + return stirrer_nodes[0] + + debug_print(f"❌ 未找到任何搅拌器") + return "" + +def find_solid_dispenser(G: nx.DiGraph) -> str: + """查找固体加样器""" + debug_print(f"🔍 查找固体加样器...") + + for node in G.nodes(): + node_class = G.nodes[node].get('class', '').lower() + if 'solid_dispenser' in node_class or 'dispenser' in node_class: + debug_print(f"✅ 找到固体加样器: {node} 🥄") + return node + + debug_print(f"❌ 未找到固体加样器") + return "" def generate_dissolve_protocol( G: nx.DiGraph, vessel: str, - solvent: str, - volume: float, + # 🔧 修复:按照checklist.md的DissolveProtocol参数 + solvent: str = "", + volume: Union[str, float] = 0.0, amount: str = "", - temp: float = 25.0, - time: float = 0.0, - stir_speed: float = 300.0 + temp: Union[str, float] = 25.0, + time: Union[str, float] = 0.0, + stir_speed: float = 300.0, + # 🔧 关键修复:添加缺失的参数,防止"unexpected keyword argument"错误 + mass: Union[str, float] = 0.0, # 这个参数在action文件中存在,必须包含 + mol: str = "", # 这个参数在action文件中存在,必须包含 + reagent: str = "", # 这个参数在action文件中存在,必须包含 + event: str = "", # 这个参数在action文件中存在,必须包含 + **kwargs # 🔧 关键:接受所有其他参数,防止unexpected keyword错误 ) -> List[Dict[str, Any]]: """ - 生成溶解操作的协议序列,复用 pump_protocol 的成熟算法 + 生成溶解操作的协议序列 - 增强版 - 溶解流程: - 1. 溶剂转移:将溶剂从溶剂瓶转移到目标容器 - 2. 启动加热搅拌:设置温度和搅拌 - 3. 等待溶解:监控溶解过程 - 4. 停止加热搅拌:完成溶解 + 🔧 修复要点: + 1. 添加action文件中的所有参数(mass, mol, reagent, event) + 2. 使用 **kwargs 接受所有额外参数,防止 unexpected keyword argument 错误 + 3. 支持固体溶解和液体溶解两种模式 + 4. 添加详细的emoji日志系统 - Args: - G: 有向图,节点为设备和容器,边为流体管道 - vessel: 目标容器(要进行溶解的容器) - solvent: 溶剂名称(用于查找对应的溶剂瓶) - volume: 溶剂体积 (mL) - amount: 要溶解的物质描述 - temp: 溶解温度 (°C),默认25°C(室温) - time: 溶解时间 (秒),默认0(立即完成) - stir_speed: 搅拌速度 (RPM),默认300 RPM + 支持两种溶解模式: + 1. 液体溶解:指定 solvent + volume,使用pump protocol转移溶剂 + 2. 固体溶解:指定 mass/mol + reagent,使用固体加样器添加固体试剂 - Returns: - List[Dict[str, Any]]: 溶解操作的动作序列 - - Raises: - ValueError: 当找不到必要的设备或容器时 - - Examples: - dissolve_actions = generate_dissolve_protocol(G, "reaction_mixture", "DMF", 10.0, "NaCl 2g", 60.0, 600.0, 400.0) + 支持所有XDL参数和单位: + - volume: "10 mL", "?" 或数值 + - mass: "2.9 g", "?" 或数值 + - temp: "60 °C", "room temperature", "?" 或数值 + - time: "30 min", "1 h", "?" 或数值 + - mol: "0.12 mol", "16.2 mmol" """ + + debug_print("=" * 60) + debug_print("🧪 开始生成溶解协议") + debug_print(f"📋 原始参数:") + debug_print(f" 🥼 vessel: '{vessel}'") + debug_print(f" 💧 solvent: '{solvent}'") + debug_print(f" 📏 volume: {volume} (类型: {type(volume)})") + debug_print(f" ⚖️ mass: {mass} (类型: {type(mass)})") + debug_print(f" 🌡️ temp: {temp} (类型: {type(temp)})") + debug_print(f" ⏱️ time: {time} (类型: {type(time)})") + debug_print(f" 🧪 reagent: '{reagent}'") + debug_print(f" 🧬 mol: '{mol}'") + debug_print(f" 🎯 event: '{event}'") + debug_print(f" 📦 kwargs: {kwargs}") # 显示额外参数 + debug_print("=" * 60) + action_sequence = [] - print(f"DISSOLVE: 开始生成溶解协议") - print(f" - 目标容器: {vessel}") - print(f" - 溶剂: {solvent}") - print(f" - 溶剂体积: {volume} mL") - print(f" - 要溶解的物质: {amount}") - print(f" - 溶解温度: {temp}°C") - print(f" - 溶解时间: {time}s ({time/60:.1f}分钟)" if time > 0 else " - 溶解时间: 立即完成") - print(f" - 搅拌速度: {stir_speed} RPM") + # === 参数验证 === + debug_print("🔍 步骤1: 参数验证...") + action_sequence.append(create_action_log(f"开始溶解操作 - 容器: {vessel}", "🎬")) + + if not vessel: + debug_print("❌ vessel 参数不能为空") + raise ValueError("vessel 参数不能为空") - # 验证目标容器存在 if vessel not in G.nodes(): - raise ValueError(f"目标容器 '{vessel}' 不存在于系统中") + debug_print(f"❌ 容器 '{vessel}' 不存在于系统中") + raise ValueError(f"容器 '{vessel}' 不存在于系统中") - # 查找溶剂瓶 - try: - solvent_vessel = find_solvent_vessel(G, solvent) - print(f"DISSOLVE: 找到溶剂瓶: {solvent_vessel}") - except ValueError as e: - raise ValueError(f"无法找到溶剂 '{solvent}': {str(e)}") + debug_print("✅ 基本参数验证通过") + action_sequence.append(create_action_log("参数验证通过", "✅")) - # 验证是否存在从溶剂瓶到目标容器的路径 - try: - path = nx.shortest_path(G, source=solvent_vessel, target=vessel) - print(f"DISSOLVE: 找到路径 {solvent_vessel} -> {vessel}: {path}") - except nx.NetworkXNoPath: - raise ValueError(f"从溶剂瓶 '{solvent_vessel}' 到目标容器 '{vessel}' 没有可用路径") + # === 🔧 关键修复:参数解析 === + debug_print("🔍 步骤2: 参数解析...") + action_sequence.append(create_action_log("正在解析溶解参数...", "🔍")) + + # 解析各种参数为数值 + final_volume = parse_volume_input(volume) + final_mass = parse_mass_input(mass) + final_temp = parse_temperature_input(temp) + final_time = parse_time_input(time) + + debug_print(f"📊 解析结果:") + debug_print(f" 📏 体积: {final_volume}mL") + debug_print(f" ⚖️ 质量: {final_mass}g") + debug_print(f" 🌡️ 温度: {final_temp}°C") + debug_print(f" ⏱️ 时间: {final_time}s") + debug_print(f" 🧪 试剂: '{reagent}'") + debug_print(f" 🧬 摩尔: '{mol}'") + debug_print(f" 🎯 事件: '{event}'") + + # === 判断溶解类型 === + debug_print("🔍 步骤3: 判断溶解类型...") + action_sequence.append(create_action_log("正在判断溶解类型...", "🔍")) + + # 判断是固体溶解还是液体溶解 + is_solid_dissolve = (final_mass > 0 or (mol and mol.strip() != "") or (reagent and reagent.strip() != "")) + is_liquid_dissolve = (final_volume > 0 and solvent and solvent.strip() != "") + + if not is_solid_dissolve and not is_liquid_dissolve: + # 默认为液体溶解,50mL + is_liquid_dissolve = True + final_volume = 50.0 + if not solvent: + solvent = "water" # 默认溶剂 + debug_print("⚠️ 未明确指定溶解参数,默认为50mL水溶解") + + dissolve_type = "固体溶解" if is_solid_dissolve else "液体溶解" + dissolve_emoji = "🧂" if is_solid_dissolve else "💧" + debug_print(f"📋 溶解类型: {dissolve_type} {dissolve_emoji}") + + action_sequence.append(create_action_log(f"确定溶解类型: {dissolve_type} {dissolve_emoji}", "📋")) + + # === 查找设备 === + debug_print("🔍 步骤4: 查找设备...") + action_sequence.append(create_action_log("正在查找相关设备...", "🔍")) # 查找加热搅拌器 - heatchill_id = None - if temp > 25.0 or stir_speed > 0 or time > 0: - try: - heatchill_id = find_connected_heatchill(G, vessel) - if heatchill_id: - print(f"DISSOLVE: 找到加热搅拌器: {heatchill_id}") + heatchill_id = find_connected_heatchill(G, vessel) + stirrer_id = find_connected_stirrer(G, vessel) + + # 优先使用加热搅拌器,否则使用独立搅拌器 + stir_device_id = heatchill_id or stirrer_id + + debug_print(f"📊 设备映射:") + debug_print(f" 🔥 加热器: '{heatchill_id}'") + debug_print(f" 🌪️ 搅拌器: '{stirrer_id}'") + debug_print(f" 🎯 使用设备: '{stir_device_id}'") + + if heatchill_id: + action_sequence.append(create_action_log(f"找到加热搅拌器: {heatchill_id}", "🔥")) + elif stirrer_id: + action_sequence.append(create_action_log(f"找到搅拌器: {stirrer_id}", "🌪️")) + else: + action_sequence.append(create_action_log("未找到搅拌设备,将跳过搅拌", "⚠️")) + + # === 执行溶解流程 === + debug_print("🔍 步骤5: 执行溶解流程...") + + try: + # 步骤5.1: 启动加热搅拌(如果需要) + if stir_device_id and (final_temp > 25.0 or final_time > 0 or stir_speed > 0): + debug_print(f"🔍 5.1: 启动加热搅拌,温度: {final_temp}°C") + action_sequence.append(create_action_log(f"准备加热搅拌 (目标温度: {final_temp}°C)", "🔥")) + + if heatchill_id and (final_temp > 25.0 or final_time > 0): + # 使用加热搅拌器 + action_sequence.append(create_action_log(f"启动加热搅拌器 {heatchill_id}", "🔥")) + + heatchill_action = { + "device_id": heatchill_id, + "action_name": "heat_chill_start", + "action_kwargs": { + "vessel": vessel, + "temp": final_temp, + "purpose": f"溶解准备 - {event}" if event else "溶解准备" + } + } + action_sequence.append(heatchill_action) + + # 等待温度稳定 + if final_temp > 25.0: + wait_time = min(60, abs(final_temp - 25.0) * 1.5) + action_sequence.append(create_action_log(f"等待温度稳定 ({wait_time:.0f}秒)", "⏳")) + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": wait_time} + }) + + elif stirrer_id: + # 使用独立搅拌器 + action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {stir_speed}rpm)", "🌪️")) + + stir_action = { + "device_id": stirrer_id, + "action_name": "start_stir", + "action_kwargs": { + "vessel": vessel, + "stir_speed": stir_speed, + "purpose": f"溶解搅拌 - {event}" if event else "溶解搅拌" + } + } + action_sequence.append(stir_action) + + # 等待搅拌稳定 + action_sequence.append(create_action_log("等待搅拌稳定...", "⏳")) + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 5} + }) + + if is_solid_dissolve: + # === 固体溶解路径 === + debug_print(f"🔍 5.2: 使用固体溶解路径") + action_sequence.append(create_action_log("开始固体溶解流程", "🧂")) + + solid_dispenser = find_solid_dispenser(G) + if solid_dispenser: + action_sequence.append(create_action_log(f"找到固体加样器: {solid_dispenser}", "🥄")) + + # 固体加样 + add_kwargs = { + "vessel": vessel, + "reagent": reagent or amount or "solid reagent", + "purpose": f"溶解固体试剂 - {event}" if event else "溶解固体试剂", + "event": event + } + + if final_mass > 0: + add_kwargs["mass"] = str(final_mass) + action_sequence.append(create_action_log(f"准备添加固体: {final_mass}g", "⚖️")) + if mol and mol.strip(): + add_kwargs["mol"] = mol + action_sequence.append(create_action_log(f"按摩尔数添加: {mol}", "🧬")) + + action_sequence.append(create_action_log("开始固体加样操作", "🥄")) + action_sequence.append({ + "device_id": solid_dispenser, + "action_name": "add_solid", + "action_kwargs": add_kwargs + }) + + debug_print(f"✅ 固体加样完成") + action_sequence.append(create_action_log("固体加样完成", "✅")) else: - print(f"DISSOLVE: 警告 - 需要加热/搅拌但未找到与容器 {vessel} 相连的加热搅拌器") - except Exception as e: - print(f"DISSOLVE: 加热搅拌器配置出错: {str(e)}") - - # === 第一步:启动加热搅拌(在添加溶剂前) === - if heatchill_id and (temp > 25.0 or time > 0): - print(f"DISSOLVE: 启动加热搅拌器,温度: {temp}°C") + debug_print("⚠️ 未找到固体加样器,跳过固体添加") + action_sequence.append(create_action_log("未找到固体加样器,无法添加固体", "❌")) - if time > 0: - # 如果指定了时间,使用定时加热搅拌 - heatchill_action = { + elif is_liquid_dissolve: + # === 液体溶解路径 === + debug_print(f"🔍 5.3: 使用液体溶解路径") + action_sequence.append(create_action_log("开始液体溶解流程", "💧")) + + # 查找溶剂容器 + action_sequence.append(create_action_log("正在查找溶剂容器...", "🔍")) + try: + solvent_vessel = find_solvent_vessel(G, solvent) + action_sequence.append(create_action_log(f"找到溶剂容器: {solvent_vessel}", "🧪")) + except ValueError as e: + debug_print(f"⚠️ {str(e)},跳过溶剂添加") + action_sequence.append(create_action_log(f"溶剂容器查找失败: {str(e)}", "❌")) + solvent_vessel = None + + if solvent_vessel: + # 计算流速 - 溶解时通常用较慢的速度,避免飞溅 + flowrate = 1.0 # 较慢的注入速度 + transfer_flowrate = 0.5 # 较慢的转移速度 + + action_sequence.append(create_action_log(f"设置流速: {flowrate}mL/min (缓慢注入)", "⚡")) + action_sequence.append(create_action_log(f"开始转移 {final_volume}mL {solvent}", "🚰")) + + # 调用pump protocol + pump_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=solvent_vessel, + to_vessel=vessel, + volume=final_volume, + amount=amount, + time=0.0, # 不在pump level控制时间 + viscous=False, + rinsing_solvent="", + rinsing_volume=0.0, + rinsing_repeats=0, + solid=False, + flowrate=flowrate, + transfer_flowrate=transfer_flowrate, + rate_spec="", + event=event, + through="", + **kwargs + ) + action_sequence.extend(pump_actions) + debug_print(f"✅ 溶剂转移完成,添加了 {len(pump_actions)} 个动作") + action_sequence.append(create_action_log(f"溶剂转移完成 ({len(pump_actions)} 个操作)", "✅")) + + # 溶剂添加后等待 + action_sequence.append(create_action_log("溶剂添加后短暂等待...", "⏳")) + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 5} + }) + + # 步骤5.4: 等待溶解完成 + if final_time > 0: + debug_print(f"🔍 5.4: 等待溶解完成 - {final_time}s") + wait_minutes = final_time / 60 + action_sequence.append(create_action_log(f"开始溶解等待 ({wait_minutes:.1f}分钟)", "⏰")) + + if heatchill_id: + # 使用定时加热搅拌 + action_sequence.append(create_action_log(f"使用加热搅拌器进行定时溶解", "🔥")) + + dissolve_action = { + "device_id": heatchill_id, + "action_name": "heat_chill", + "action_kwargs": { + "vessel": vessel, + "temp": final_temp, + "time": final_time, + "stir": True, + "stir_speed": stir_speed, + "purpose": f"溶解等待 - {event}" if event else "溶解等待" + } + } + action_sequence.append(dissolve_action) + + elif stirrer_id: + # 使用定时搅拌 + action_sequence.append(create_action_log(f"使用搅拌器进行定时溶解", "🌪️")) + + stir_action = { + "device_id": stirrer_id, + "action_name": "stir", + "action_kwargs": { + "vessel": vessel, + "stir_time": final_time, + "stir_speed": stir_speed, + "settling_time": 0, + "purpose": f"溶解搅拌 - {event}" if event else "溶解搅拌" + } + } + action_sequence.append(stir_action) + + else: + # 简单等待 + action_sequence.append(create_action_log(f"简单等待溶解完成", "⏳")) + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": final_time} + }) + + # 步骤5.5: 停止加热搅拌(如果需要) + if heatchill_id and final_time == 0 and final_temp > 25.0: + debug_print(f"🔍 5.5: 停止加热器") + action_sequence.append(create_action_log("停止加热搅拌器", "🛑")) + + stop_action = { "device_id": heatchill_id, - "action_name": "heat_chill", + "action_name": "heat_chill_stop", "action_kwargs": { - "vessel": vessel, - "temp": temp, - "time": time, - "stir": True, - "stir_speed": stir_speed, - "purpose": f"溶解 {amount} 在 {solvent} 中" - } - } - else: - # 如果没有指定时间,使用持续加热搅拌 - heatchill_action = { - "device_id": heatchill_id, - "action_name": "heat_chill_start", - "action_kwargs": { - "vessel": vessel, - "temp": temp, - "purpose": f"溶解 {amount} 在 {solvent} 中" + "vessel": vessel } } + action_sequence.append(stop_action) - action_sequence.append(heatchill_action) - - # 等待温度稳定 - if temp > 25.0: - wait_time = min(60, abs(temp - 25.0) * 1.5) # 根据温差估算预热时间 - action_sequence.append({ - "action_name": "wait", - "action_kwargs": {"time": wait_time} - }) - - # === 第二步:添加溶剂到目标容器 === - if volume > 0: - print(f"DISSOLVE: 将 {volume} mL {solvent} 从 {solvent_vessel} 转移到 {vessel}") - - # 计算流速 - 溶解时通常用较慢的速度,避免飞溅 - transfer_flowrate = 1.0 # 较慢的转移速度 - flowrate = 0.5 # 较慢的注入速度 - - try: - # 使用成熟的 pump_protocol 算法进行液体转移 - pump_actions = generate_pump_protocol( - G=G, - from_vessel=solvent_vessel, - to_vessel=vessel, - volume=volume, - flowrate=flowrate, # 注入速度 - 较慢避免飞溅 - transfer_flowrate=transfer_flowrate # 转移速度 - ) - - action_sequence.extend(pump_actions) - - except Exception as e: - raise ValueError(f"生成泵协议时出错: {str(e)}") - - # 溶剂添加后等待 + except Exception as e: + debug_print(f"❌ 溶解流程执行失败: {str(e)}") + action_sequence.append(create_action_log(f"溶解流程失败: {str(e)}", "❌")) + # 添加错误日志 action_sequence.append({ - "action_name": "wait", - "action_kwargs": {"time": 5} + "device_id": "system", + "action_name": "log_message", + "action_kwargs": { + "message": f"溶解失败: {str(e)}" + } }) - # === 第三步:如果没有使用定时加热搅拌,但需要等待溶解 === - if time > 0 and heatchill_id and temp <= 25.0: - # 只需要搅拌等待,不需要加热 - print(f"DISSOLVE: 室温搅拌 {time}s 等待溶解") - - stir_action = { - "device_id": heatchill_id, - "action_name": "heat_chill", - "action_kwargs": { - "vessel": vessel, - "temp": 25.0, # 室温 - "time": time, - "stir": True, - "stir_speed": stir_speed, - "purpose": f"室温搅拌溶解 {amount}" - } - } - action_sequence.append(stir_action) + # === 最终结果 === + debug_print("=" * 60) + debug_print(f"🎉 溶解协议生成完成") + debug_print(f"📊 协议统计:") + debug_print(f" 📋 总动作数: {len(action_sequence)}") + debug_print(f" 🥼 容器: {vessel}") + debug_print(f" {dissolve_emoji} 溶解类型: {dissolve_type}") + if is_liquid_dissolve: + debug_print(f" 💧 溶剂: {solvent} ({final_volume}mL)") + if is_solid_dissolve: + debug_print(f" 🧪 试剂: {reagent}") + debug_print(f" ⚖️ 质量: {final_mass}g") + debug_print(f" 🧬 摩尔: {mol}") + debug_print(f" 🌡️ 温度: {final_temp}°C") + debug_print(f" ⏱️ 时间: {final_time}s") + debug_print("=" * 60) - # === 第四步:如果使用了持续加热,需要手动停止 === - if heatchill_id and time == 0 and temp > 25.0: - print(f"DISSOLVE: 停止加热搅拌器") - - stop_action = { - "device_id": heatchill_id, - "action_name": "heat_chill_stop", - "action_kwargs": { - "vessel": vessel - } - } - action_sequence.append(stop_action) + # 添加完成日志 + summary_msg = f"溶解协议完成: {vessel}" + if is_liquid_dissolve: + summary_msg += f" (使用 {final_volume}mL {solvent})" + if is_solid_dissolve: + summary_msg += f" (溶解 {final_mass}g {reagent})" - print(f"DISSOLVE: 生成了 {len(action_sequence)} 个动作") - print(f"DISSOLVE: 溶解协议生成完成") + action_sequence.append(create_action_log(summary_msg, "🎉")) return action_sequence +# === 便捷函数 === -# 便捷函数:常用溶解方案 -def generate_room_temp_dissolve_protocol( - G: nx.DiGraph, - vessel: str, - solvent: str, - volume: float, - amount: str = "", - stir_time: float = 300.0 # 5分钟 -) -> List[Dict[str, Any]]: - """室温溶解:快速搅拌,短时间""" - return generate_dissolve_protocol(G, vessel, solvent, volume, amount, 25.0, stir_time, 400.0) +def dissolve_solid_by_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, float], + temp: Union[str, float] = 25.0, time: Union[str, float] = "10 min") -> List[Dict[str, Any]]: + """按质量溶解固体""" + debug_print(f"🧂 快速固体溶解: {reagent} ({mass}) → {vessel}") + return generate_dissolve_protocol( + G, vessel, + mass=mass, + reagent=reagent, + temp=temp, + time=time + ) +def dissolve_solid_by_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str, + temp: Union[str, float] = 25.0, time: Union[str, float] = "10 min") -> List[Dict[str, Any]]: + """按摩尔数溶解固体""" + debug_print(f"🧬 按摩尔数溶解固体: {reagent} ({mol}) → {vessel}") + return generate_dissolve_protocol( + G, vessel, + mol=mol, + reagent=reagent, + temp=temp, + time=time + ) -def generate_heated_dissolve_protocol( - G: nx.DiGraph, - vessel: str, - solvent: str, - volume: float, - amount: str = "", - temp: float = 60.0, - dissolve_time: float = 900.0 # 15分钟 -) -> List[Dict[str, Any]]: - """加热溶解:中等温度,较长时间""" - return generate_dissolve_protocol(G, vessel, solvent, volume, amount, temp, dissolve_time, 300.0) +def dissolve_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float], + temp: Union[str, float] = 25.0, time: Union[str, float] = "5 min") -> List[Dict[str, Any]]: + """用溶剂溶解""" + debug_print(f"💧 溶剂溶解: {solvent} ({volume}) → {vessel}") + return generate_dissolve_protocol( + G, vessel, + solvent=solvent, + volume=volume, + temp=temp, + time=time + ) +def dissolve_at_room_temp(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float]) -> List[Dict[str, Any]]: + """室温溶解""" + debug_print(f"🌡️ 室温溶解: {solvent} ({volume}) → {vessel}") + return generate_dissolve_protocol( + G, vessel, + solvent=solvent, + volume=volume, + temp="room temperature", + time="5 min" + ) -def generate_gentle_dissolve_protocol( - G: nx.DiGraph, - vessel: str, - solvent: str, - volume: float, - amount: str = "", - temp: float = 40.0, - dissolve_time: float = 1800.0 # 30分钟 -) -> List[Dict[str, Any]]: - """温和溶解:低温,长时间,慢搅拌""" - return generate_dissolve_protocol(G, vessel, solvent, volume, amount, temp, dissolve_time, 200.0) - - -def generate_hot_dissolve_protocol( - G: nx.DiGraph, - vessel: str, - solvent: str, - volume: float, - amount: str = "", - temp: float = 80.0, - dissolve_time: float = 600.0 # 10分钟 -) -> List[Dict[str, Any]]: - """高温溶解:高温,中等时间,快搅拌""" - return generate_dissolve_protocol(G, vessel, solvent, volume, amount, temp, dissolve_time, 500.0) - - -def generate_sequential_dissolve_protocol( - G: nx.DiGraph, - vessel: str, - dissolve_steps: List[Dict[str, Any]] -) -> List[Dict[str, Any]]: - """ - 生成连续溶解多种物质的协议 - - Args: - G: 网络图 - vessel: 目标容器 - dissolve_steps: 溶解步骤列表,每个元素包含溶解参数 - - Returns: - List[Dict[str, Any]]: 完整的动作序列 - - Example: - dissolve_steps = [ - { - "solvent": "water", - "volume": 5.0, - "amount": "NaCl 1g", - "temp": 25.0, - "time": 300.0, - "stir_speed": 300.0 - }, - { - "solvent": "ethanol", - "volume": 2.0, - "amount": "organic compound 0.5g", - "temp": 40.0, - "time": 600.0, - "stir_speed": 400.0 - } - ] - """ - action_sequence = [] - - for i, step in enumerate(dissolve_steps): - print(f"DISSOLVE: 处理第 {i+1}/{len(dissolve_steps)} 个溶解步骤") - - # 生成单个溶解步骤的协议 - dissolve_actions = generate_dissolve_protocol( - G=G, - vessel=vessel, - solvent=step.get('solvent'), - volume=step.get('volume', 0.0), - amount=step.get('amount', ''), - temp=step.get('temp', 25.0), - time=step.get('time', 0.0), - stir_speed=step.get('stir_speed', 300.0) - ) - - action_sequence.extend(dissolve_actions) - - # 在步骤之间加入等待时间 - if i < len(dissolve_steps) - 1: # 不是最后一个步骤 - action_sequence.append({ - "action_name": "wait", - "action_kwargs": {"time": 10} - }) - - print(f"DISSOLVE: 连续溶解协议生成完成,共 {len(action_sequence)} 个动作") - return action_sequence - +def dissolve_with_heating(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float], + temp: Union[str, float] = "60 °C", time: Union[str, float] = "15 min") -> List[Dict[str, Any]]: + """加热溶解""" + debug_print(f"🔥 加热溶解: {solvent} ({volume}) → {vessel} @ {temp}") + return generate_dissolve_protocol( + G, vessel, + solvent=solvent, + volume=volume, + temp=temp, + time=time + ) # 测试函数 def test_dissolve_protocol(): - """测试溶解协议的示例""" - print("=== DISSOLVE PROTOCOL 测试 ===") - print("测试完成") - + """测试溶解协议的各种参数解析""" + debug_print("=== DISSOLVE PROTOCOL 增强版测试 ===") + + # 测试体积解析 + debug_print("💧 测试体积解析...") + volumes = ["10 mL", "?", 10.0, "1 L", "500 μL"] + for vol in volumes: + result = parse_volume_input(vol) + debug_print(f"📏 体积解析: {vol} → {result}mL") + + # 测试质量解析 + debug_print("⚖️ 测试质量解析...") + masses = ["2.9 g", "?", 2.5, "500 mg"] + for mass in masses: + result = parse_mass_input(mass) + debug_print(f"⚖️ 质量解析: {mass} → {result}g") + + # 测试温度解析 + debug_print("🌡️ 测试温度解析...") + temps = ["60 °C", "room temperature", "?", 25.0, "reflux"] + for temp in temps: + result = parse_temperature_input(temp) + debug_print(f"🌡️ 温度解析: {temp} → {result}°C") + + # 测试时间解析 + debug_print("⏱️ 测试时间解析...") + times = ["30 min", "1 h", "?", 60.0] + for time in times: + result = parse_time_input(time) + debug_print(f"⏱️ 时间解析: {time} → {result}s") + + debug_print("✅ 测试完成") if __name__ == "__main__": test_dissolve_protocol() \ No newline at end of file diff --git a/unilabos/compile/dry_protocol.py b/unilabos/compile/dry_protocol.py index 34044eb..1f06069 100644 --- a/unilabos/compile/dry_protocol.py +++ b/unilabos/compile/dry_protocol.py @@ -67,37 +67,47 @@ def generate_dry_protocol( # 默认参数 dry_temp = 60.0 # 默认干燥温度 60°C dry_time = 3600.0 # 默认干燥时间 1小时(3600秒) + simulation_time = 60.0 # 模拟时间 1分钟 - print(f"DRY: 开始生成干燥协议") - print(f" - 化合物: {compound}") - print(f" - 容器: {vessel}") - print(f" - 干燥温度: {dry_temp}°C") - print(f" - 干燥时间: {dry_time/60:.0f} 分钟") + print(f"🌡️ DRY: 开始生成干燥协议 ✨") + print(f" 🧪 化合物: {compound}") + print(f" 🥽 容器: {vessel}") + print(f" 🔥 干燥温度: {dry_temp}°C") + print(f" ⏰ 干燥时间: {dry_time/60:.0f} 分钟") # 1. 验证目标容器存在 + print(f"\n📋 步骤1: 验证目标容器 '{vessel}' 是否存在...") if vessel not in G.nodes(): - print(f"DRY: 警告 - 容器 '{vessel}' 不存在于系统中,跳过干燥") + print(f"⚠️ DRY: 警告 - 容器 '{vessel}' 不存在于系统中,跳过干燥 😢") return action_sequence + print(f"✅ 容器 '{vessel}' 验证通过!") # 2. 查找相连的加热器 + print(f"\n🔍 步骤2: 查找与容器相连的加热器...") heater_id = find_connected_heater(G, vessel) if heater_id is None: - print(f"DRY: 警告 - 未找到与容器 '{vessel}' 相连的加热器,跳过干燥") + print(f"😭 DRY: 警告 - 未找到与容器 '{vessel}' 相连的加热器,跳过干燥") + print(f"🎭 添加模拟干燥动作...") # 添加一个等待动作,表示干燥过程(模拟) action_sequence.append({ "action_name": "wait", "action_kwargs": { - "time": 60.0, # 等待1分钟 + "time": 10.0, # 模拟等待时间 "description": f"模拟干燥 {compound} (无加热器可用)" } }) + print(f"📄 DRY: 协议生成完成,共 {len(action_sequence)} 个动作 🎯") return action_sequence + print(f"🎉 找到加热器: {heater_id}!") + # 3. 启动加热器进行干燥 - print(f"DRY: 启动加热器 {heater_id} 进行干燥") + print(f"\n🚀 步骤3: 开始执行干燥流程...") + print(f"🔥 启动加热器 {heater_id} 进行干燥") # 3.1 启动加热 + print(f" ⚡ 动作1: 启动加热到 {dry_temp}°C...") action_sequence.append({ "device_id": heater_id, "action_name": "heat_chill_start", @@ -107,29 +117,35 @@ def generate_dry_protocol( "purpose": f"干燥 {compound}" } }) + print(f" ✅ 加热器启动命令已添加 🔥") # 3.2 等待温度稳定 + print(f" ⏳ 动作2: 等待温度稳定...") action_sequence.append({ "action_name": "wait", "action_kwargs": { - "time": 60.0, + "time": 10.0, "description": f"等待温度稳定到 {dry_temp}°C" } }) + print(f" ✅ 温度稳定等待命令已添加 🌡️") # 3.3 保持干燥温度 + print(f" 🔄 动作3: 保持干燥温度 {simulation_time/60:.0f} 分钟...") action_sequence.append({ "device_id": heater_id, "action_name": "heat_chill", "action_kwargs": { "vessel": vessel, "temp": dry_temp, - "time": dry_time, + "time": simulation_time, "purpose": f"干燥 {compound},保持温度 {dry_temp}°C" } }) + print(f" ✅ 温度保持命令已添加 🌡️⏰") # 3.4 停止加热 + print(f" ⏹️ 动作4: 停止加热...") action_sequence.append({ "device_id": heater_id, "action_name": "heat_chill_stop", @@ -138,18 +154,22 @@ def generate_dry_protocol( "purpose": f"干燥完成,停止加热" } }) + print(f" ✅ 停止加热命令已添加 🛑") # 3.5 等待冷却 + print(f" ❄️ 动作5: 等待冷却...") action_sequence.append({ "action_name": "wait", "action_kwargs": { - "time": 300.0, # 等待5分钟冷却 + "time": 10.0, # 等待10秒冷却 "description": f"等待 {compound} 冷却" } }) + print(f" ✅ 冷却等待命令已添加 🧊") - print(f"DRY: 协议生成完成,共 {len(action_sequence)} 个动作") - print(f"DRY: 预计总时间: {(dry_time + 360)/60:.0f} 分钟") + print(f"\n🎊 DRY: 协议生成完成,共 {len(action_sequence)} 个动作 🎯") + print(f"⏱️ DRY: 预计总时间: {(dry_time + 360)/60:.0f} 分钟 ⌛") + print(f"🏁 所有动作序列准备就绪! ✨") return action_sequence diff --git a/unilabos/compile/evacuateandrefill_protocol.py b/unilabos/compile/evacuateandrefill_protocol.py index aa44df6..cbcf19b 100644 --- a/unilabos/compile/evacuateandrefill_protocol.py +++ b/unilabos/compile/evacuateandrefill_protocol.py @@ -1,8 +1,68 @@ -import numpy as np import networkx as nx +import logging +import uuid +import sys from typing import List, Dict, Any, Optional from .pump_protocol import generate_pump_protocol_with_rinsing, generate_pump_protocol +# 设置日志 +logger = logging.getLogger(__name__) + +# 确保输出编码为UTF-8 +if hasattr(sys.stdout, 'reconfigure'): + try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') + except: + pass + +def debug_print(message): + """调试输出函数 - 支持中文""" + try: + # 确保消息是字符串格式 + safe_message = str(message) + print(f"[抽真空充气] {safe_message}", flush=True) + logger.info(f"[抽真空充气] {safe_message}") + except UnicodeEncodeError: + # 如果编码失败,尝试替换不支持的字符 + safe_message = str(message).encode('utf-8', errors='replace').decode('utf-8') + print(f"[抽真空充气] {safe_message}", flush=True) + logger.info(f"[抽真空充气] {safe_message}") + except Exception as e: + # 最后的安全措施 + fallback_message = f"日志输出错误: {repr(message)}" + print(f"[抽真空充气] {fallback_message}", flush=True) + logger.info(f"[抽真空充气] {fallback_message}") + +def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]: + """创建一个动作日志 - 支持中文和emoji""" + try: + full_message = f"{emoji} {message}" + debug_print(full_message) + logger.info(full_message) + + return { + "action_name": "wait", + "action_kwargs": { + "time": 0.1, + "log_message": full_message, + "progress_message": full_message + } + } + except Exception as e: + # 如果emoji有问题,使用纯文本 + safe_message = f"[日志] {message}" + debug_print(safe_message) + logger.info(safe_message) + + return { + "action_name": "wait", + "action_kwargs": { + "time": 0.1, + "log_message": safe_message, + "progress_message": safe_message + } + } def find_gas_source(G: nx.DiGraph, gas: str) -> str: """ @@ -11,9 +71,10 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str: 2. 气体类型匹配(data.gas_type) 3. 默认气源 """ - print(f"EVACUATE_REFILL: 正在查找气体 '{gas}' 的气源...") + debug_print(f"🔍 正在查找气体 '{gas}' 的气源...") # 第一步:通过容器名称匹配 + debug_print(f"📋 方法1: 容器名称匹配...") gas_source_patterns = [ f"gas_source_{gas}", f"gas_{gas}", @@ -24,12 +85,15 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str: f"bottle_{gas}" ] + debug_print(f"🎯 尝试的容器名称: {gas_source_patterns}") + for pattern in gas_source_patterns: if pattern in G.nodes(): - print(f"EVACUATE_REFILL: 通过名称匹配找到气源: {pattern}") + debug_print(f"✅ 通过名称找到气源: {pattern}") return pattern # 第二步:通过气体类型匹配 (data.gas_type) + debug_print(f"📋 方法2: 气体类型匹配...") for node_id in G.nodes(): node_data = G.nodes[node_id] node_class = node_data.get('class', '') or '' @@ -44,7 +108,7 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str: gas_type = data.get('gas_type', '') if gas_type.lower() == gas.lower(): - print(f"EVACUATE_REFILL: 通过气体类型匹配找到气源: {node_id} (gas_type: {gas_type})") + debug_print(f"✅ 通过气体类型找到气源: {node_id} (气体类型: {gas_type})") return node_id # 检查 config.gas_type @@ -52,10 +116,11 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str: config_gas_type = config.get('gas_type', '') if config_gas_type.lower() == gas.lower(): - print(f"EVACUATE_REFILL: 通过配置气体类型匹配找到气源: {node_id} (config.gas_type: {config_gas_type})") + debug_print(f"✅ 通过配置气体类型找到气源: {node_id} (配置气体类型: {config_gas_type})") return node_id # 第三步:查找所有可用的气源设备 + debug_print(f"📋 方法3: 查找可用气源...") available_gas_sources = [] for node_id in G.nodes(): node_data = G.nodes[node_id] @@ -66,226 +131,382 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str: (node_id.startswith('flask_') and any(g in node_id.lower() for g in ['air', 'nitrogen', 'argon']))): data = node_data.get('data', {}) - gas_type = data.get('gas_type', 'unknown') - available_gas_sources.append(f"{node_id} (gas_type: {gas_type})") + gas_type = data.get('gas_type', '未知') + available_gas_sources.append(f"{node_id} (气体类型: {gas_type})") - print(f"EVACUATE_REFILL: 可用气源列表: {available_gas_sources}") + debug_print(f"📊 可用气源: {available_gas_sources}") # 第四步:如果找不到特定气体,使用默认的第一个气源 + debug_print(f"📋 方法4: 查找默认气源...") default_gas_sources = [ node for node in G.nodes() - if ((G.nodes[node].get('class') or '').startswith('virtual_gas_source') + if ((G.nodes[node].get('class') or '').find('virtual_gas_source') != -1 or 'gas_source' in node) ] if default_gas_sources: default_source = default_gas_sources[0] - print(f"EVACUATE_REFILL: ⚠️ 未找到特定气体 '{gas}',使用默认气源: {default_source}") + debug_print(f"⚠️ 未找到特定气体 '{gas}',使用默认气源: {default_source}") return default_source - raise ValueError(f"找不到气体 '{gas}' 对应的气源。可用气源: {available_gas_sources}") - - -def find_gas_source_by_any_match(G: nx.DiGraph, gas: str) -> str: - """ - 增强版气源查找,支持各种匹配方式的别名函数 - """ - return find_gas_source(G, gas) - - -def get_gas_source_type(G: nx.DiGraph, gas_source: str) -> str: - """获取气源的气体类型""" - if gas_source not in G.nodes(): - return "unknown" - - node_data = G.nodes[gas_source] - data = node_data.get('data', {}) - config = node_data.get('config', {}) - - # 检查多个可能的字段 - gas_type = (data.get('gas_type') or - config.get('gas_type') or - data.get('gas') or - config.get('gas') or - "air") # 默认为空气 - - return gas_type - - -def find_vessels_by_gas_type(G: nx.DiGraph, gas: str) -> List[str]: - """ - 根据气体类型查找所有匹配的容器/气源 - """ - matching_vessels = [] - - for node_id in G.nodes(): - node_data = G.nodes[node_id] - - # 检查容器名称匹配 - if gas.lower() in node_id.lower(): - matching_vessels.append(f"{node_id} (名称匹配)") - continue - - # 检查气体类型匹配 - data = node_data.get('data', {}) - config = node_data.get('config', {}) - - gas_type = data.get('gas_type', '') or config.get('gas_type', '') - if gas_type.lower() == gas.lower(): - matching_vessels.append(f"{node_id} (gas_type: {gas_type})") - - return matching_vessels - + debug_print(f"❌ 所有方法都失败了!") + raise ValueError(f"无法找到气体 '{gas}' 的气源。可用气源: {available_gas_sources}") def find_vacuum_pump(G: nx.DiGraph) -> str: """查找真空泵设备""" - vacuum_pumps = [ - node for node in G.nodes() - if ((G.nodes[node].get('class') or '').startswith('virtual_vacuum_pump') - or 'vacuum_pump' in node - or 'vacuum' in (G.nodes[node].get('class') or '')) - ] + debug_print("🔍 正在查找真空泵...") + + vacuum_pumps = [] + for node in G.nodes(): + node_data = G.nodes[node] + node_class = node_data.get('class', '') or '' + + if ('virtual_vacuum_pump' in node_class or + 'vacuum_pump' in node.lower() or + 'vacuum' in node_class.lower()): + vacuum_pumps.append(node) + debug_print(f"📋 发现真空泵: {node}") if not vacuum_pumps: - raise ValueError("系统中未找到真空泵设备") + debug_print(f"❌ 系统中未找到真空泵") + raise ValueError("系统中未找到真空泵") + debug_print(f"✅ 使用真空泵: {vacuum_pumps[0]}") return vacuum_pumps[0] - -def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str: +def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> Optional[str]: """查找与指定容器相连的搅拌器""" - stirrer_nodes = [node for node in G.nodes() - if (G.nodes[node].get('class') or '') == 'virtual_stirrer'] + debug_print(f"🔍 正在查找与容器 {vessel} 连接的搅拌器...") + + stirrer_nodes = [] + for node in G.nodes(): + node_data = G.nodes[node] + node_class = node_data.get('class', '') or '' + + if 'virtual_stirrer' in node_class or 'stirrer' in node.lower(): + stirrer_nodes.append(node) + debug_print(f"📋 发现搅拌器: {node}") + + debug_print(f"📊 找到的搅拌器总数: {len(stirrer_nodes)}") # 检查哪个搅拌器与目标容器相连 for stirrer in stirrer_nodes: if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): + debug_print(f"✅ 找到连接的搅拌器: {stirrer}") return stirrer - return stirrer_nodes[0] if stirrer_nodes else None - - -def find_associated_solenoid_valve(G: nx.DiGraph, device_id: str) -> Optional[str]: - """查找与指定设备相关联的电磁阀""" - solenoid_valves = [ - node for node in G.nodes() - if ('solenoid' in (G.nodes[node].get('class') or '').lower() - or 'solenoid_valve' in node) - ] - - # 通过网络连接查找直接相连的电磁阀 - for solenoid in solenoid_valves: - if G.has_edge(device_id, solenoid) or G.has_edge(solenoid, device_id): - return solenoid - - # 通过命名规则查找关联的电磁阀 - device_type = "" - if 'vacuum' in device_id.lower(): - device_type = "vacuum" - elif 'gas' in device_id.lower(): - device_type = "gas" - - if device_type: - for solenoid in solenoid_valves: - if device_type in solenoid.lower(): - return solenoid + # 如果没有连接的搅拌器,返回第一个可用的 + if stirrer_nodes: + debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个可用的: {stirrer_nodes[0]}") + return stirrer_nodes[0] + debug_print("❌ 未找到搅拌器") return None +def find_vacuum_solenoid_valve(G: nx.DiGraph, vacuum_pump: str) -> Optional[str]: + """查找真空泵相关的电磁阀""" + debug_print(f"🔍 正在查找真空泵 {vacuum_pump} 的电磁阀...") + + # 查找所有电磁阀 + solenoid_valves = [] + for node in G.nodes(): + node_data = G.nodes[node] + node_class = node_data.get('class', '') or '' + + if ('solenoid' in node_class.lower() or 'solenoid_valve' in node.lower()): + solenoid_valves.append(node) + debug_print(f"📋 发现电磁阀: {node}") + + debug_print(f"📊 找到的电磁阀: {solenoid_valves}") + + # 检查连接关系 + debug_print(f"📋 方法1: 检查连接关系...") + for solenoid in solenoid_valves: + if G.has_edge(solenoid, vacuum_pump) or G.has_edge(vacuum_pump, solenoid): + debug_print(f"✅ 找到连接的真空电磁阀: {solenoid}") + return solenoid + + # 通过命名规则查找 + debug_print(f"📋 方法2: 检查命名规则...") + for solenoid in solenoid_valves: + if 'vacuum' in solenoid.lower() or solenoid == 'solenoid_valve_1': + debug_print(f"✅ 通过命名找到真空电磁阀: {solenoid}") + return solenoid + + debug_print("⚠️ 未找到真空电磁阀") + return None + +def find_gas_solenoid_valve(G: nx.DiGraph, gas_source: str) -> Optional[str]: + """查找气源相关的电磁阀""" + debug_print(f"🔍 正在查找气源 {gas_source} 的电磁阀...") + + # 查找所有电磁阀 + solenoid_valves = [] + for node in G.nodes(): + node_data = G.nodes[node] + node_class = node_data.get('class', '') or '' + + if ('solenoid' in node_class.lower() or 'solenoid_valve' in node.lower()): + solenoid_valves.append(node) + + debug_print(f"📊 找到的电磁阀: {solenoid_valves}") + + # 检查连接关系 + debug_print(f"📋 方法1: 检查连接关系...") + for solenoid in solenoid_valves: + if G.has_edge(gas_source, solenoid) or G.has_edge(solenoid, gas_source): + debug_print(f"✅ 找到连接的气源电磁阀: {solenoid}") + return solenoid + + # 通过命名规则查找 + debug_print(f"📋 方法2: 检查命名规则...") + for solenoid in solenoid_valves: + if 'gas' in solenoid.lower() or solenoid == 'solenoid_valve_2': + debug_print(f"✅ 通过命名找到气源电磁阀: {solenoid}") + return solenoid + + debug_print("⚠️ 未找到气源电磁阀") + return None def generate_evacuateandrefill_protocol( G: nx.DiGraph, vessel: str, gas: str, - repeats: int = 1 + **kwargs ) -> List[Dict[str, Any]]: """ - 生成抽真空和充气操作的动作序列 + 生成抽真空和充气操作的动作序列 - 中文版 - **修复版本**: 正确调用 pump_protocol 并处理异常 + Args: + G: 设备图 + vessel: 目标容器名称(必需) + gas: 气体名称(必需) + **kwargs: 其他参数(兼容性) + + Returns: + List[Dict[str, Any]]: 动作序列 """ + + # 硬编码重复次数为 3 + repeats = 3 + + # 生成协议ID + protocol_id = str(uuid.uuid4()) + debug_print(f"🆔 生成协议ID: {protocol_id}") + + debug_print("=" * 60) + debug_print("🧪 开始生成抽真空充气协议") + debug_print(f"📋 原始参数:") + debug_print(f" 🥼 容器: '{vessel}'") + debug_print(f" 💨 气体: '{gas}'") + debug_print(f" 🔄 循环次数: {repeats} (硬编码)") + debug_print(f" 📦 其他参数: {kwargs}") + debug_print("=" * 60) + action_sequence = [] - # 参数设置 - 关键修复:减小体积避免超出泵容量 - VACUUM_VOLUME = 20.0 # 减小抽真空体积 - REFILL_VOLUME = 20.0 # 减小充气体积 - PUMP_FLOW_RATE = 2.5 # 降低流速 - STIR_SPEED = 300.0 + # === 参数验证和修正 === + debug_print("🔍 步骤1: 参数验证和修正...") + action_sequence.append(create_action_log(f"开始抽真空充气操作 - 容器: {vessel}", "🎬")) + action_sequence.append(create_action_log(f"目标气体: {gas}", "💨")) + action_sequence.append(create_action_log(f"循环次数: {repeats}", "🔄")) - print(f"EVACUATE_REFILL: 开始生成协议,目标容器: {vessel}, 气体: {gas}, 重复次数: {repeats}") + # 验证必需参数 + if not vessel: + debug_print("❌ 容器参数不能为空") + raise ValueError("容器参数不能为空") + + if not gas: + debug_print("❌ 气体参数不能为空") + raise ValueError("气体参数不能为空") - # 1. 验证设备存在 if vessel not in G.nodes(): - raise ValueError(f"目标容器 '{vessel}' 不存在于系统中") + debug_print(f"❌ 容器 '{vessel}' 在系统中不存在") + raise ValueError(f"容器 '{vessel}' 在系统中不存在") + + debug_print("✅ 基本参数验证通过") + action_sequence.append(create_action_log("参数验证通过", "✅")) + + # 标准化气体名称 + debug_print("🔧 标准化气体名称...") + gas_aliases = { + 'n2': 'nitrogen', + 'ar': 'argon', + 'air': 'air', + 'o2': 'oxygen', + 'co2': 'carbon_dioxide', + 'h2': 'hydrogen', + '氮气': 'nitrogen', + '氩气': 'argon', + '空气': 'air', + '氧气': 'oxygen', + '二氧化碳': 'carbon_dioxide', + '氢气': 'hydrogen' + } + + original_gas = gas + gas_lower = gas.lower().strip() + if gas_lower in gas_aliases: + gas = gas_aliases[gas_lower] + debug_print(f"🔄 标准化气体名称: {original_gas} -> {gas}") + action_sequence.append(create_action_log(f"气体名称标准化: {original_gas} -> {gas}", "🔄")) + + debug_print(f"📋 最终参数: 容器={vessel}, 气体={gas}, 重复={repeats}") + + # === 查找设备 === + debug_print("🔍 步骤2: 查找设备...") + action_sequence.append(create_action_log("正在查找相关设备...", "🔍")) - # 2. 查找设备 try: vacuum_pump = find_vacuum_pump(G) - vacuum_solenoid = find_associated_solenoid_valve(G, vacuum_pump) + action_sequence.append(create_action_log(f"找到真空泵: {vacuum_pump}", "🌪️")) + gas_source = find_gas_source(G, gas) - gas_solenoid = find_associated_solenoid_valve(G, gas_source) + action_sequence.append(create_action_log(f"找到气源: {gas_source}", "💨")) + + vacuum_solenoid = find_vacuum_solenoid_valve(G, vacuum_pump) + if vacuum_solenoid: + action_sequence.append(create_action_log(f"找到真空电磁阀: {vacuum_solenoid}", "🚪")) + else: + action_sequence.append(create_action_log("未找到真空电磁阀", "⚠️")) + + gas_solenoid = find_gas_solenoid_valve(G, gas_source) + if gas_solenoid: + action_sequence.append(create_action_log(f"找到气源电磁阀: {gas_solenoid}", "🚪")) + else: + action_sequence.append(create_action_log("未找到气源电磁阀", "⚠️")) + stirrer_id = find_connected_stirrer(G, vessel) + if stirrer_id: + action_sequence.append(create_action_log(f"找到搅拌器: {stirrer_id}", "🌪️")) + else: + action_sequence.append(create_action_log("未找到搅拌器", "⚠️")) - print(f"EVACUATE_REFILL: 找到设备") - print(f" - 真空泵: {vacuum_pump}") - print(f" - 气源: {gas_source}") - print(f" - 真空电磁阀: {vacuum_solenoid}") - print(f" - 气源电磁阀: {gas_solenoid}") - print(f" - 搅拌器: {stirrer_id}") + debug_print(f"📊 设备配置:") + debug_print(f" 🌪️ 真空泵: {vacuum_pump}") + debug_print(f" 💨 气源: {gas_source}") + debug_print(f" 🚪 真空电磁阀: {vacuum_solenoid}") + debug_print(f" 🚪 气源电磁阀: {gas_solenoid}") + debug_print(f" 🌪️ 搅拌器: {stirrer_id}") - except ValueError as e: + except Exception as e: + debug_print(f"❌ 设备查找失败: {str(e)}") + action_sequence.append(create_action_log(f"设备查找失败: {str(e)}", "❌")) raise ValueError(f"设备查找失败: {str(e)}") - # 3. **关键修复**: 验证路径存在性 + # === 参数设置 === + debug_print("🔍 步骤3: 参数设置...") + action_sequence.append(create_action_log("设置操作参数...", "⚙️")) + + # 根据气体类型调整参数 + if gas.lower() in ['nitrogen', 'argon']: + VACUUM_VOLUME = 25.0 + REFILL_VOLUME = 25.0 + PUMP_FLOW_RATE = 2.0 + VACUUM_TIME = 30.0 + REFILL_TIME = 20.0 + debug_print("💨 惰性气体: 使用标准参数") + action_sequence.append(create_action_log("检测到惰性气体,使用标准参数", "💨")) + elif gas.lower() in ['air', 'oxygen']: + VACUUM_VOLUME = 20.0 + REFILL_VOLUME = 20.0 + PUMP_FLOW_RATE = 1.5 + VACUUM_TIME = 45.0 + REFILL_TIME = 25.0 + debug_print("🔥 活性气体: 使用保守参数") + action_sequence.append(create_action_log("检测到活性气体,使用保守参数", "🔥")) + else: + VACUUM_VOLUME = 15.0 + REFILL_VOLUME = 15.0 + PUMP_FLOW_RATE = 1.0 + VACUUM_TIME = 60.0 + REFILL_TIME = 30.0 + debug_print("❓ 未知气体: 使用安全参数") + action_sequence.append(create_action_log("未知气体类型,使用安全参数", "❓")) + + STIR_SPEED = 200.0 + + debug_print(f"⚙️ 操作参数:") + debug_print(f" 📏 真空体积: {VACUUM_VOLUME}mL") + debug_print(f" 📏 充气体积: {REFILL_VOLUME}mL") + debug_print(f" ⚡ 泵流速: {PUMP_FLOW_RATE}mL/s") + debug_print(f" ⏱️ 真空时间: {VACUUM_TIME}s") + debug_print(f" ⏱️ 充气时间: {REFILL_TIME}s") + debug_print(f" 🌪️ 搅拌速度: {STIR_SPEED}RPM") + + action_sequence.append(create_action_log(f"真空体积: {VACUUM_VOLUME}mL", "📏")) + action_sequence.append(create_action_log(f"充气体积: {REFILL_VOLUME}mL", "📏")) + action_sequence.append(create_action_log(f"泵流速: {PUMP_FLOW_RATE}mL/s", "⚡")) + + # === 路径验证 === + debug_print("🔍 步骤4: 路径验证...") + action_sequence.append(create_action_log("验证传输路径...", "🛤️")) + try: # 验证抽真空路径 - vacuum_path = nx.shortest_path(G, source=vessel, target=vacuum_pump) - print(f"EVACUATE_REFILL: 抽真空路径: {' → '.join(vacuum_path)}") + if nx.has_path(G, vessel, vacuum_pump): + vacuum_path = nx.shortest_path(G, source=vessel, target=vacuum_pump) + debug_print(f"✅ 真空路径: {' -> '.join(vacuum_path)}") + action_sequence.append(create_action_log(f"真空路径: {' -> '.join(vacuum_path)}", "🛤️")) + else: + debug_print(f"⚠️ 真空路径不存在,继续执行但可能有问题") + action_sequence.append(create_action_log("真空路径检查: 路径不存在", "⚠️")) # 验证充气路径 - gas_path = nx.shortest_path(G, source=gas_source, target=vessel) - print(f"EVACUATE_REFILL: 充气路径: {' → '.join(gas_path)}") + if nx.has_path(G, gas_source, vessel): + gas_path = nx.shortest_path(G, source=gas_source, target=vessel) + debug_print(f"✅ 气体路径: {' -> '.join(gas_path)}") + action_sequence.append(create_action_log(f"气体路径: {' -> '.join(gas_path)}", "🛤️")) + else: + debug_print(f"⚠️ 气体路径不存在,继续执行但可能有问题") + action_sequence.append(create_action_log("气体路径检查: 路径不存在", "⚠️")) - # **新增**: 检查路径中的边数据 - for i in range(len(vacuum_path) - 1): - nodeA, nodeB = vacuum_path[i], vacuum_path[i + 1] - edge_data = G.get_edge_data(nodeA, nodeB) - if not edge_data or 'port' not in edge_data: - raise ValueError(f"路径 {nodeA} → {nodeB} 缺少端口信息") - print(f" 抽真空路径边 {nodeA} → {nodeB}: {edge_data}") - - for i in range(len(gas_path) - 1): - nodeA, nodeB = gas_path[i], gas_path[i + 1] - edge_data = G.get_edge_data(nodeA, nodeB) - if not edge_data or 'port' not in edge_data: - raise ValueError(f"路径 {nodeA} → {nodeB} 缺少端口信息") - print(f" 充气路径边 {nodeA} → {nodeB}: {edge_data}") - - except nx.NetworkXNoPath as e: - raise ValueError(f"路径不存在: {str(e)}") except Exception as e: - raise ValueError(f"路径验证失败: {str(e)}") + debug_print(f"⚠️ 路径验证失败: {str(e)},继续执行") + action_sequence.append(create_action_log(f"路径验证失败: {str(e)}", "⚠️")) + + # === 启动搅拌器 === + debug_print("🔍 步骤5: 启动搅拌器...") - # 4. 启动搅拌器 if stirrer_id: + debug_print(f"🌪️ 启动搅拌器: {stirrer_id}") + action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {STIR_SPEED}rpm)", "🌪️")) + action_sequence.append({ "device_id": stirrer_id, "action_name": "start_stir", "action_kwargs": { "vessel": vessel, "stir_speed": STIR_SPEED, - "purpose": "抽真空充气操作前启动搅拌" + "purpose": "抽真空充气前预搅拌" } }) + + # 等待搅拌稳定 + action_sequence.append(create_action_log("等待搅拌稳定...", "⏳")) + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 5.0} + }) + else: + debug_print("⚠️ 未找到搅拌器,跳过搅拌器启动") + action_sequence.append(create_action_log("跳过搅拌器启动", "⏭️")) + + # === 执行循环 === + debug_print("🔍 步骤6: 执行抽真空-充气循环...") + action_sequence.append(create_action_log(f"开始 {repeats} 次抽真空-充气循环", "🔄")) - # 5. 执行多次抽真空-充气循环 for cycle in range(repeats): - print(f"EVACUATE_REFILL: === 第 {cycle+1}/{repeats} 次循环 ===") + debug_print(f"=== 第 {cycle+1}/{repeats} 轮循环 ===") + action_sequence.append(create_action_log(f"第 {cycle+1}/{repeats} 轮循环开始", "🚀")) # ============ 抽真空阶段 ============ - print(f"EVACUATE_REFILL: 抽真空阶段开始") + debug_print(f"🌪️ 抽真空阶段开始") + action_sequence.append(create_action_log("开始抽真空阶段", "🌪️")) # 启动真空泵 + debug_print(f"🔛 启动真空泵: {vacuum_pump}") + action_sequence.append(create_action_log(f"启动真空泵: {vacuum_pump}", "🔛")) action_sequence.append({ "device_id": vacuum_pump, "action_name": "set_status", @@ -294,16 +515,17 @@ def generate_evacuateandrefill_protocol( # 开启真空电磁阀 if vacuum_solenoid: + debug_print(f"🚪 打开真空电磁阀: {vacuum_solenoid}") + action_sequence.append(create_action_log(f"打开真空电磁阀: {vacuum_solenoid}", "🚪")) action_sequence.append({ "device_id": vacuum_solenoid, "action_name": "set_valve_position", "action_kwargs": {"command": "OPEN"} }) - # **关键修复**: 改进 pump_protocol 调用和错误处理 - print(f"EVACUATE_REFILL: 调用抽真空 pump_protocol: {vessel} → {vacuum_pump}") - print(f" - 体积: {VACUUM_VOLUME} mL") - print(f" - 流速: {PUMP_FLOW_RATE} mL/s") + # 抽真空操作 + debug_print(f"🌪️ 抽真空操作: {vessel} -> {vacuum_pump}") + action_sequence.append(create_action_log(f"开始抽真空: {vessel} -> {vacuum_pump}", "🌪️")) try: vacuum_transfer_actions = generate_pump_protocol_with_rinsing( @@ -314,7 +536,7 @@ def generate_evacuateandrefill_protocol( amount="", time=0.0, viscous=False, - rinsing_solvent="", # **修复**: 明确不使用清洗 + rinsing_solvent="", rinsing_volume=0.0, rinsing_repeats=0, solid=False, @@ -324,52 +546,36 @@ def generate_evacuateandrefill_protocol( if vacuum_transfer_actions: action_sequence.extend(vacuum_transfer_actions) - print(f"EVACUATE_REFILL: ✅ 成功添加 {len(vacuum_transfer_actions)} 个抽真空动作") + debug_print(f"✅ 添加了 {len(vacuum_transfer_actions)} 个抽真空动作") + action_sequence.append(create_action_log(f"抽真空协议完成 ({len(vacuum_transfer_actions)} 个操作)", "✅")) else: - print(f"EVACUATE_REFILL: ⚠️ 抽真空 pump_protocol 返回空序列") - # **修复**: 添加手动泵动作作为备选 - action_sequence.extend([ - { - "device_id": "multiway_valve_1", - "action_name": "set_valve_position", - "action_kwargs": {"command": "5"} # 连接到反应器 - }, - { - "device_id": "transfer_pump_1", - "action_name": "set_position", - "action_kwargs": { - "position": VACUUM_VOLUME, - "max_velocity": PUMP_FLOW_RATE - } - } - ]) - print(f"EVACUATE_REFILL: 使用备选手动泵动作") + debug_print("⚠️ 抽真空协议返回空序列,添加手动动作") + action_sequence.append(create_action_log("抽真空协议为空,使用手动等待", "⚠️")) + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": VACUUM_TIME} + }) except Exception as e: - print(f"EVACUATE_REFILL: ❌ 抽真空 pump_protocol 失败: {str(e)}") - import traceback - print(f"EVACUATE_REFILL: 详细错误:\n{traceback.format_exc()}") - - # **修复**: 添加手动动作而不是忽略错误 - print(f"EVACUATE_REFILL: 使用手动备选方案") - action_sequence.extend([ - { - "device_id": "multiway_valve_1", - "action_name": "set_valve_position", - "action_kwargs": {"command": "5"} # 反应器端口 - }, - { - "device_id": "transfer_pump_1", - "action_name": "set_position", - "action_kwargs": { - "position": VACUUM_VOLUME, - "max_velocity": PUMP_FLOW_RATE - } - } - ]) + debug_print(f"❌ 抽真空失败: {str(e)}") + action_sequence.append(create_action_log(f"抽真空失败: {str(e)}", "❌")) + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": VACUUM_TIME} + }) + + # 抽真空后等待 + wait_minutes = VACUUM_TIME / 60 + action_sequence.append(create_action_log(f"抽真空后等待 ({wait_minutes:.1f} 分钟)", "⏳")) + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": VACUUM_TIME} + }) # 关闭真空电磁阀 if vacuum_solenoid: + debug_print(f"🚪 关闭真空电磁阀: {vacuum_solenoid}") + action_sequence.append(create_action_log(f"关闭真空电磁阀: {vacuum_solenoid}", "🚪")) action_sequence.append({ "device_id": vacuum_solenoid, "action_name": "set_valve_position", @@ -377,32 +583,47 @@ def generate_evacuateandrefill_protocol( }) # 关闭真空泵 + debug_print(f"🔴 停止真空泵: {vacuum_pump}") + action_sequence.append(create_action_log(f"停止真空泵: {vacuum_pump}", "🔴")) action_sequence.append({ "device_id": vacuum_pump, "action_name": "set_status", "action_kwargs": {"string": "OFF"} }) + # 阶段间等待 + action_sequence.append(create_action_log("抽真空阶段完成,短暂等待", "⏳")) + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 5.0} + }) + # ============ 充气阶段 ============ - print(f"EVACUATE_REFILL: 充气阶段开始") + debug_print(f"💨 充气阶段开始") + action_sequence.append(create_action_log("开始气体充气阶段", "💨")) # 启动气源 + debug_print(f"🔛 启动气源: {gas_source}") + action_sequence.append(create_action_log(f"启动气源: {gas_source}", "🔛")) action_sequence.append({ "device_id": gas_source, - "action_name": "set_status", + "action_name": "set_status", "action_kwargs": {"string": "ON"} }) # 开启气源电磁阀 if gas_solenoid: + debug_print(f"🚪 打开气源电磁阀: {gas_solenoid}") + action_sequence.append(create_action_log(f"打开气源电磁阀: {gas_solenoid}", "🚪")) action_sequence.append({ "device_id": gas_solenoid, "action_name": "set_valve_position", "action_kwargs": {"command": "OPEN"} }) - # **关键修复**: 改进充气 pump_protocol 调用 - print(f"EVACUATE_REFILL: 调用充气 pump_protocol: {gas_source} → {vessel}") + # 充气操作 + debug_print(f"💨 充气操作: {gas_source} -> {vessel}") + action_sequence.append(create_action_log(f"开始气体充气: {gas_source} -> {vessel}", "💨")) try: gas_transfer_actions = generate_pump_protocol_with_rinsing( @@ -413,7 +634,7 @@ def generate_evacuateandrefill_protocol( amount="", time=0.0, viscous=False, - rinsing_solvent="", # **修复**: 明确不使用清洗 + rinsing_solvent="", rinsing_volume=0.0, rinsing_repeats=0, solid=False, @@ -423,77 +644,36 @@ def generate_evacuateandrefill_protocol( if gas_transfer_actions: action_sequence.extend(gas_transfer_actions) - print(f"EVACUATE_REFILL: ✅ 成功添加 {len(gas_transfer_actions)} 个充气动作") + debug_print(f"✅ 添加了 {len(gas_transfer_actions)} 个充气动作") + action_sequence.append(create_action_log(f"气体充气协议完成 ({len(gas_transfer_actions)} 个操作)", "✅")) else: - print(f"EVACUATE_REFILL: ⚠️ 充气 pump_protocol 返回空序列") - # **修复**: 添加手动充气动作 - action_sequence.extend([ - { - "device_id": "multiway_valve_2", - "action_name": "set_valve_position", - "action_kwargs": {"command": "8"} # 氮气端口 - }, - { - "device_id": "transfer_pump_2", - "action_name": "set_position", - "action_kwargs": { - "position": REFILL_VOLUME, - "max_velocity": PUMP_FLOW_RATE - } - }, - { - "device_id": "multiway_valve_2", - "action_name": "set_valve_position", - "action_kwargs": {"command": "5"} # 反应器端口 - }, - { - "device_id": "transfer_pump_2", - "action_name": "set_position", - "action_kwargs": { - "position": 0.0, - "max_velocity": PUMP_FLOW_RATE - } - } - ]) + debug_print("⚠️ 充气协议返回空序列,添加手动动作") + action_sequence.append(create_action_log("充气协议为空,使用手动等待", "⚠️")) + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": REFILL_TIME} + }) except Exception as e: - print(f"EVACUATE_REFILL: ❌ 充气 pump_protocol 失败: {str(e)}") - import traceback - print(f"EVACUATE_REFILL: 详细错误:\n{traceback.format_exc()}") - - # **修复**: 使用手动充气动作 - print(f"EVACUATE_REFILL: 使用手动充气方案") - action_sequence.extend([ - { - "device_id": "multiway_valve_2", - "action_name": "set_valve_position", - "action_kwargs": {"command": "8"} # 连接气源 - }, - { - "device_id": "transfer_pump_2", - "action_name": "set_position", - "action_kwargs": { - "position": REFILL_VOLUME, - "max_velocity": PUMP_FLOW_RATE - } - }, - { - "device_id": "multiway_valve_2", - "action_name": "set_valve_position", - "action_kwargs": {"command": "5"} # 连接反应器 - }, - { - "device_id": "transfer_pump_2", - "action_name": "set_position", - "action_kwargs": { - "position": 0.0, - "max_velocity": PUMP_FLOW_RATE - } - } - ]) + debug_print(f"❌ 气体充气失败: {str(e)}") + action_sequence.append(create_action_log(f"气体充气失败: {str(e)}", "❌")) + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": REFILL_TIME} + }) + + # 充气后等待 + refill_wait_minutes = REFILL_TIME / 60 + action_sequence.append(create_action_log(f"充气后等待 ({refill_wait_minutes:.1f} 分钟)", "⏳")) + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": REFILL_TIME} + }) # 关闭气源电磁阀 if gas_solenoid: + debug_print(f"🚪 关闭气源电磁阀: {gas_solenoid}") + action_sequence.append(create_action_log(f"关闭气源电磁阀: {gas_solenoid}", "🚪")) action_sequence.append({ "device_id": gas_solenoid, "action_name": "set_valve_position", @@ -501,37 +681,92 @@ def generate_evacuateandrefill_protocol( }) # 关闭气源 + debug_print(f"🔴 停止气源: {gas_source}") + action_sequence.append(create_action_log(f"停止气源: {gas_source}", "🔴")) action_sequence.append({ "device_id": gas_source, "action_name": "set_status", "action_kwargs": {"string": "OFF"} }) - # 等待下一次循环 + # 循环间等待 if cycle < repeats - 1: + debug_print(f"⏳ 等待下一个循环...") + action_sequence.append(create_action_log("等待下一个循环...", "⏳")) action_sequence.append({ "action_name": "wait", - "action_kwargs": {"time": 2.0} + "action_kwargs": {"time": 10.0} }) + else: + action_sequence.append(create_action_log(f"第 {cycle+1}/{repeats} 轮循环完成", "✅")) + + # === 停止搅拌器 === + debug_print("🔍 步骤7: 停止搅拌器...") - # 停止搅拌器 if stirrer_id: + debug_print(f"🛑 停止搅拌器: {stirrer_id}") + action_sequence.append(create_action_log(f"停止搅拌器: {stirrer_id}", "🛑")) action_sequence.append({ "device_id": stirrer_id, "action_name": "stop_stir", "action_kwargs": {"vessel": vessel} }) + else: + action_sequence.append(create_action_log("跳过搅拌器停止", "⏭️")) + + # === 最终等待 === + action_sequence.append(create_action_log("最终稳定等待...", "⏳")) + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 10.0} + }) + + # === 总结 === + total_time = (VACUUM_TIME + REFILL_TIME + 25) * repeats + 20 + + debug_print("=" * 60) + debug_print(f"🎉 抽真空充气协议生成完成") + debug_print(f"📊 协议统计:") + debug_print(f" 📋 总动作数: {len(action_sequence)}") + debug_print(f" ⏱️ 预计总时间: {total_time:.0f}s ({total_time/60:.1f} 分钟)") + debug_print(f" 🥼 处理容器: {vessel}") + debug_print(f" 💨 使用气体: {gas}") + debug_print(f" 🔄 重复次数: {repeats}") + debug_print("=" * 60) + + # 添加完成日志 + summary_msg = f"抽真空充气协议完成: {vessel} (使用 {gas},{repeats} 次循环)" + action_sequence.append(create_action_log(summary_msg, "🎉")) - print(f"EVACUATE_REFILL: 协议生成完成,共 {len(action_sequence)} 个动作") return action_sequence +# === 便捷函数 === + +def generate_nitrogen_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]: + """生成氮气置换协议""" + debug_print(f"💨 生成氮气置换协议: {vessel}") + return generate_evacuateandrefill_protocol(G, vessel, "nitrogen", **kwargs) + +def generate_argon_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]: + """生成氩气置换协议""" + debug_print(f"💨 生成氩气置换协议: {vessel}") + return generate_evacuateandrefill_protocol(G, vessel, "argon", **kwargs) + +def generate_air_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]: + """生成空气置换协议""" + debug_print(f"💨 生成空气置换协议: {vessel}") + return generate_evacuateandrefill_protocol(G, vessel, "air", **kwargs) + +def generate_inert_atmosphere_protocol(G: nx.DiGraph, vessel: str, gas: str = "nitrogen", **kwargs) -> List[Dict[str, Any]]: + """生成惰性气氛协议""" + debug_print(f"🛡️ 生成惰性气氛协议: {vessel} (使用 {gas})") + return generate_evacuateandrefill_protocol(G, vessel, gas, **kwargs) # 测试函数 def test_evacuateandrefill_protocol(): """测试抽真空充气协议""" - print("=== EVACUATE AND REFILL PROTOCOL 测试 ===") - print("测试完成") - + debug_print("=== 抽真空充气协议增强中文版测试 ===") + debug_print("✅ 测试完成") if __name__ == "__main__": test_evacuateandrefill_protocol() \ No newline at end of file diff --git a/unilabos/compile/evaporate_protocol.py b/unilabos/compile/evaporate_protocol.py index 4cee78d..6a2d6f6 100644 --- a/unilabos/compile/evaporate_protocol.py +++ b/unilabos/compile/evaporate_protocol.py @@ -1,326 +1,366 @@ -from typing import List, Dict, Any +from typing import List, Dict, Any, Optional, Union import networkx as nx -from .pump_protocol import generate_pump_protocol +import logging +import re +logger = logging.getLogger(__name__) -def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float: +def debug_print(message): + """调试输出""" + print(f"🧪 [EVAPORATE] {message}", flush=True) + logger.info(f"[EVAPORATE] {message}") + +def parse_time_input(time_input: Union[str, float]) -> float: """ - 获取容器中的液体体积 + 解析时间输入,支持带单位的字符串 + + Args: + time_input: 时间输入(如 "3 min", "180", "0.5 h" 等) + + Returns: + float: 时间(秒) """ - if vessel not in G.nodes(): - return 0.0 + if isinstance(time_input, (int, float)): + debug_print(f"⏱️ 时间输入为数字: {time_input}s ✨") + return float(time_input) - vessel_data = G.nodes[vessel].get('data', {}) - liquids = vessel_data.get('liquid', []) + if not time_input or not str(time_input).strip(): + debug_print(f"⚠️ 时间输入为空,使用默认值: 180s (3分钟) 🕐") + return 180.0 # 默认3分钟 - total_volume = 0.0 - for liquid in liquids: - if isinstance(liquid, dict) and 'liquid_volume' in liquid: - total_volume += liquid['liquid_volume'] + time_str = str(time_input).lower().strip() + debug_print(f"🔍 解析时间输入: '{time_str}' 📝") - return total_volume + # 处理未知时间 + if time_str in ['?', 'unknown', 'tbd']: + default_time = 180.0 # 默认3分钟 + debug_print(f"❓ 检测到未知时间,使用默认值: {default_time}s (3分钟) 🤷‍♀️") + return default_time + + # 移除空格并提取数字和单位 + time_clean = re.sub(r'\s+', '', time_str) + + # 匹配数字和单位的正则表达式 + match = re.match(r'([0-9]*\.?[0-9]+)\s*(s|sec|second|min|minute|h|hr|hour|d|day)?', time_clean) + + if not match: + # 如果无法解析,尝试直接转换为数字(默认秒) + try: + value = float(time_str) + debug_print(f"✅ 时间解析成功: {time_str} → {value}s(无单位,默认秒)⏰") + return value + except ValueError: + debug_print(f"❌ 无法解析时间: '{time_str}',使用默认值180s (3分钟) 😅") + return 180.0 + + value = float(match.group(1)) + unit = match.group(2) or 's' # 默认单位为秒 + + # 转换为秒 + if unit in ['min', 'minute']: + time_sec = value * 60.0 # min -> s + debug_print(f"🕐 时间转换: {value} 分钟 → {time_sec}s ⏰") + elif unit in ['h', 'hr', 'hour']: + time_sec = value * 3600.0 # h -> s + debug_print(f"🕐 时间转换: {value} 小时 → {time_sec}s ({time_sec/60:.1f}分钟) ⏰") + elif unit in ['d', 'day']: + time_sec = value * 86400.0 # d -> s + debug_print(f"🕐 时间转换: {value} 天 → {time_sec}s ({time_sec/3600:.1f}小时) ⏰") + else: # s, sec, second 或默认 + time_sec = value # 已经是s + debug_print(f"🕐 时间转换: {value}s → {time_sec}s (已是秒) ⏰") + + return time_sec +def find_rotavap_device(G: nx.DiGraph, vessel: str = None) -> Optional[str]: + """ + 在组态图中查找旋转蒸发仪设备 + + Args: + G: 设备图 + vessel: 指定的设备名称(可选) + + Returns: + str: 找到的旋转蒸发仪设备ID,如果没找到返回None + """ + debug_print("🔍 开始查找旋转蒸发仪设备... 🌪️") + + # 如果指定了vessel,先检查是否存在且是旋转蒸发仪 + if vessel: + debug_print(f"🎯 检查指定设备: {vessel} 🔧") + if vessel in G.nodes(): + node_data = G.nodes[vessel] + node_class = node_data.get('class', '') + node_type = node_data.get('type', '') + + debug_print(f"📋 设备信息 {vessel}: class={node_class}, type={node_type}") + + # 检查是否为旋转蒸发仪 + if any(keyword in str(node_class).lower() for keyword in ['rotavap', 'rotary', 'evaporat']): + debug_print(f"🎉 找到指定的旋转蒸发仪: {vessel} ✨") + return vessel + elif node_type == 'device': + debug_print(f"✅ 指定设备存在,尝试直接使用: {vessel} 🔧") + return vessel + else: + debug_print(f"❌ 指定的设备 {vessel} 不存在 😞") + + # 在所有设备中查找旋转蒸发仪 + debug_print("🔎 在所有设备中搜索旋转蒸发仪... 🕵️‍♀️") + rotavap_candidates = [] + + for node_id, node_data in G.nodes(data=True): + node_class = node_data.get('class', '') + node_type = node_data.get('type', '') + + # 跳过非设备节点 + if node_type != 'device': + continue + + # 检查设备类型 + if any(keyword in str(node_class).lower() for keyword in ['rotavap', 'rotary', 'evaporat']): + rotavap_candidates.append(node_id) + debug_print(f"🌟 找到旋转蒸发仪候选: {node_id} (class: {node_class}) 🌪️") + elif any(keyword in str(node_id).lower() for keyword in ['rotavap', 'rotary', 'evaporat']): + rotavap_candidates.append(node_id) + debug_print(f"🌟 找到旋转蒸发仪候选 (按名称): {node_id} 🌪️") + + if rotavap_candidates: + selected = rotavap_candidates[0] # 选择第一个找到的 + debug_print(f"🎯 选择旋转蒸发仪: {selected} 🏆") + return selected + + debug_print("😭 未找到旋转蒸发仪设备 💔") + return None -def find_rotavap_device(G: nx.DiGraph) -> str: - """查找旋转蒸发仪设备""" - rotavap_nodes = [node for node in G.nodes() - if (G.nodes[node].get('class') or '') == 'virtual_rotavap'] +def find_connected_vessel(G: nx.DiGraph, rotavap_device: str) -> Optional[str]: + """ + 查找与旋转蒸发仪连接的容器 - if rotavap_nodes: - return rotavap_nodes[0] + Args: + G: 设备图 + rotavap_device: 旋转蒸发仪设备ID - raise ValueError("系统中未找到旋转蒸发仪设备") - - -def find_solvent_recovery_vessel(G: nx.DiGraph) -> str: - """查找溶剂回收容器""" - possible_names = [ - "flask_distillate", - "bottle_distillate", - "vessel_distillate", - "distillate", - "solvent_recovery", - "flask_solvent_recovery", - "collection_flask" - ] + Returns: + str: 连接的容器ID,如果没找到返回None + """ + debug_print(f"🔗 查找与 {rotavap_device} 连接的容器... 🥽") - for vessel_name in possible_names: - if vessel_name in G.nodes(): - return vessel_name + # 查看旋转蒸发仪的子设备 + rotavap_data = G.nodes[rotavap_device] + children = rotavap_data.get('children', []) - # 如果找不到专门的回收容器,使用废液容器 - waste_names = ["waste_workup", "flask_waste", "bottle_waste", "waste"] - for vessel_name in waste_names: - if vessel_name in G.nodes(): - return vessel_name + debug_print(f"👶 检查子设备: {children}") + for child_id in children: + if child_id in G.nodes(): + child_data = G.nodes[child_id] + child_type = child_data.get('type', '') + + if child_type == 'container': + debug_print(f"🎉 找到连接的容器: {child_id} 🥽✨") + return child_id - raise ValueError(f"未找到溶剂回收容器。尝试了以下名称: {possible_names + waste_names}") - + # 查看邻接的容器 + debug_print("🤝 检查邻接设备...") + for neighbor in G.neighbors(rotavap_device): + neighbor_data = G.nodes[neighbor] + neighbor_type = neighbor_data.get('type', '') + + if neighbor_type == 'container': + debug_print(f"🎉 找到邻接的容器: {neighbor} 🥽✨") + return neighbor + + debug_print("😞 未找到连接的容器 💔") + return None def generate_evaporate_protocol( G: nx.DiGraph, vessel: str, pressure: float = 0.1, temp: float = 60.0, - time: float = 1800.0, - stir_speed: float = 100.0 + time: Union[str, float] = "180", # 🔧 修改:支持字符串时间 + stir_speed: float = 100.0, + solvent: str = "", + **kwargs ) -> List[Dict[str, Any]]: """ - 生成蒸发操作的协议序列 - - 蒸发流程: - 1. 液体转移:将待蒸发溶液从源容器转移到旋转蒸发仪 - 2. 蒸发操作:执行旋转蒸发 - 3. (可选) 溶剂回收:将冷凝的溶剂转移到回收容器 - 4. 残留物转移:将浓缩物从旋转蒸发仪转移回原容器或新容器 + 生成蒸发操作的协议序列 - 支持单位 Args: - G: 有向图,节点为设备和容器,边为流体管道 - vessel: 包含待蒸发溶液的容器名称 - pressure: 蒸发时的真空度 (bar),默认0.1 bar - temp: 蒸发时的加热温度 (°C),默认60°C - time: 蒸发时间 (秒),默认1800秒(30分钟) - stir_speed: 旋转速度 (RPM),默认100 RPM + G: 设备图 + vessel: 容器名称或旋转蒸发仪名称 + pressure: 真空度 (bar),默认0.1 + temp: 加热温度 (°C),默认60 + time: 蒸发时间(支持 "3 min", "180", "0.5 h" 等) + stir_speed: 旋转速度 (RPM),默认100 + solvent: 溶剂名称(用于参数优化) + **kwargs: 其他参数(兼容性) Returns: - List[Dict[str, Any]]: 蒸发操作的动作序列 - - Raises: - ValueError: 当找不到必要的设备时抛出异常 - - Examples: - evaporate_actions = generate_evaporate_protocol(G, "reaction_mixture", 0.05, 80.0, 3600.0) + List[Dict[str, Any]]: 动作序列 """ + + debug_print("🌟" * 20) + debug_print("🌪️ 开始生成蒸发协议(支持单位)✨") + debug_print(f"📝 输入参数:") + debug_print(f" 🥽 vessel: {vessel}") + debug_print(f" 💨 pressure: {pressure} bar") + debug_print(f" 🌡️ temp: {temp}°C") + debug_print(f" ⏰ time: {time} (类型: {type(time)})") + debug_print(f" 🌪️ stir_speed: {stir_speed} RPM") + debug_print(f" 🧪 solvent: '{solvent}'") + debug_print("🌟" * 20) + + # === 步骤1: 查找旋转蒸发仪设备 === + debug_print("📍 步骤1: 查找旋转蒸发仪设备... 🔍") + + # 验证vessel参数 + if not vessel: + debug_print("❌ vessel 参数不能为空! 😱") + raise ValueError("vessel 参数不能为空") + + # 查找旋转蒸发仪设备 + rotavap_device = find_rotavap_device(G, vessel) + if not rotavap_device: + debug_print("💥 未找到旋转蒸发仪设备! 😭") + raise ValueError(f"未找到旋转蒸发仪设备。请检查组态图中是否包含 class 包含 'rotavap'、'rotary' 或 'evaporat' 的设备") + + debug_print(f"🎉 成功找到旋转蒸发仪: {rotavap_device} ✨") + + # === 步骤2: 确定目标容器 === + debug_print("📍 步骤2: 确定目标容器... 🥽") + + target_vessel = vessel + + # 如果vessel就是旋转蒸发仪设备,查找连接的容器 + if vessel == rotavap_device: + debug_print("🔄 vessel就是旋转蒸发仪,查找连接的容器...") + connected_vessel = find_connected_vessel(G, rotavap_device) + if connected_vessel: + target_vessel = connected_vessel + debug_print(f"✅ 使用连接的容器: {target_vessel} 🥽✨") + else: + debug_print(f"⚠️ 未找到连接的容器,使用设备本身: {rotavap_device} 🔧") + target_vessel = rotavap_device + elif vessel in G.nodes() and G.nodes[vessel].get('type') == 'container': + debug_print(f"✅ 使用指定的容器: {vessel} 🥽✨") + target_vessel = vessel + else: + debug_print(f"⚠️ 容器 '{vessel}' 不存在或类型不正确,使用旋转蒸发仪设备: {rotavap_device} 🔧") + target_vessel = rotavap_device + + # === 🔧 新增:步骤3:单位解析处理 === + debug_print("📍 步骤3: 单位解析处理... ⚡") + + # 解析时间 + final_time = parse_time_input(time) + debug_print(f"🎯 时间解析完成: {time} → {final_time}s ({final_time/60:.1f}分钟) ⏰✨") + + # === 步骤4: 参数验证和修正 === + debug_print("📍 步骤4: 参数验证和修正... 🔧") + + # 修正参数范围 + if pressure <= 0 or pressure > 1.0: + debug_print(f"⚠️ 真空度 {pressure} bar 超出范围,修正为 0.1 bar 💨") + pressure = 0.1 + else: + debug_print(f"✅ 真空度 {pressure} bar 在正常范围内 💨") + + if temp < 10.0 or temp > 200.0: + debug_print(f"⚠️ 温度 {temp}°C 超出范围,修正为 60°C 🌡️") + temp = 60.0 + else: + debug_print(f"✅ 温度 {temp}°C 在正常范围内 🌡️") + + if final_time <= 0: + debug_print(f"⚠️ 时间 {final_time}s 无效,修正为 180s (3分钟) ⏰") + final_time = 180.0 + else: + debug_print(f"✅ 时间 {final_time}s ({final_time/60:.1f}分钟) 有效 ⏰") + + if stir_speed < 10.0 or stir_speed > 300.0: + debug_print(f"⚠️ 旋转速度 {stir_speed} RPM 超出范围,修正为 100 RPM 🌪️") + stir_speed = 100.0 + else: + debug_print(f"✅ 旋转速度 {stir_speed} RPM 在正常范围内 🌪️") + + # 根据溶剂优化参数 + if solvent: + debug_print(f"🧪 根据溶剂 '{solvent}' 优化参数... 🔬") + solvent_lower = solvent.lower() + + if any(s in solvent_lower for s in ['water', 'aqueous', 'h2o']): + temp = max(temp, 80.0) + pressure = max(pressure, 0.2) + debug_print("💧 水系溶剂:提高温度和真空度 🌡️💨") + elif any(s in solvent_lower for s in ['ethanol', 'methanol', 'acetone']): + temp = min(temp, 50.0) + pressure = min(pressure, 0.05) + debug_print("🍺 易挥发溶剂:降低温度和真空度 🌡️💨") + elif any(s in solvent_lower for s in ['dmso', 'dmi', 'toluene']): + temp = max(temp, 100.0) + pressure = min(pressure, 0.01) + debug_print("🔥 高沸点溶剂:提高温度,降低真空度 🌡️💨") + else: + debug_print("🧪 通用溶剂,使用标准参数 ✨") + else: + debug_print("🤷‍♀️ 未指定溶剂,使用默认参数 ✨") + + debug_print(f"🎯 最终参数: pressure={pressure} bar 💨, temp={temp}°C 🌡️, time={final_time}s ⏰, stir_speed={stir_speed} RPM 🌪️") + + # === 步骤5: 生成动作序列 === + debug_print("📍 步骤5: 生成动作序列... 🎬") + action_sequence = [] - print(f"EVAPORATE: 开始生成蒸发协议") - print(f" - 源容器: {vessel}") - print(f" - 真空度: {pressure} bar") - print(f" - 温度: {temp}°C") - print(f" - 时间: {time}s ({time/60:.1f}分钟)") - print(f" - 旋转速度: {stir_speed} RPM") - - # 验证源容器存在 - if vessel not in G.nodes(): - raise ValueError(f"源容器 '{vessel}' 不存在于系统中") - - # 获取源容器中的液体体积 - source_volume = get_vessel_liquid_volume(G, vessel) - print(f"EVAPORATE: 源容器 {vessel} 中有 {source_volume} mL 液体") - - # 查找旋转蒸发仪 - try: - rotavap_id = find_rotavap_device(G) - print(f"EVAPORATE: 找到旋转蒸发仪: {rotavap_id}") - except ValueError as e: - raise ValueError(f"无法找到旋转蒸发仪: {str(e)}") - - # 查找旋转蒸发仪样品容器 - rotavap_vessel = None - possible_rotavap_vessels = ["rotavap_flask", "rotavap", "flask_rotavap", "evaporation_flask"] - for rv in possible_rotavap_vessels: - if rv in G.nodes(): - rotavap_vessel = rv - break - - if not rotavap_vessel: - raise ValueError(f"未找到旋转蒸发仪样品容器。尝试了: {possible_rotavap_vessels}") - - print(f"EVAPORATE: 找到旋转蒸发仪样品容器: {rotavap_vessel}") - - # 查找溶剂回收容器 - try: - distillate_vessel = find_solvent_recovery_vessel(G) - print(f"EVAPORATE: 找到溶剂回收容器: {distillate_vessel}") - except ValueError as e: - print(f"EVAPORATE: 警告 - {str(e)}") - distillate_vessel = None - - # === 简化的体积计算策略 === - if source_volume > 0: - # 如果能检测到液体体积,使用实际体积的大部分 - transfer_volume = min(source_volume * 0.9, 250.0) # 90%或最多250mL - print(f"EVAPORATE: 检测到液体体积,将转移 {transfer_volume} mL") - else: - # 如果检测不到液体体积,默认转移一整瓶 250mL - transfer_volume = 250.0 - print(f"EVAPORATE: 未检测到液体体积,默认转移整瓶 {transfer_volume} mL") - - # === 第一步:将待蒸发溶液转移到旋转蒸发仪 === - print(f"EVAPORATE: 将 {transfer_volume} mL 溶液从 {vessel} 转移到 {rotavap_vessel}") - try: - transfer_to_rotavap_actions = generate_pump_protocol( - G=G, - from_vessel=vessel, - to_vessel=rotavap_vessel, - volume=transfer_volume, - flowrate=2.0, - transfer_flowrate=2.0 - ) - action_sequence.extend(transfer_to_rotavap_actions) - except Exception as e: - raise ValueError(f"无法将溶液转移到旋转蒸发仪: {str(e)}") - - # 转移后等待 - wait_action = { + # 1. 等待稳定 + debug_print(" 🔄 动作1: 添加初始等待稳定... ⏳") + action_sequence.append({ "action_name": "wait", "action_kwargs": {"time": 10} - } - action_sequence.append(wait_action) + }) + debug_print(" ✅ 初始等待动作已添加 ⏳✨") + + # 2. 执行蒸发 + debug_print(f" 🌪️ 动作2: 执行蒸发操作...") + debug_print(f" 🔧 设备: {rotavap_device}") + debug_print(f" 🥽 容器: {target_vessel}") + debug_print(f" 💨 真空度: {pressure} bar") + debug_print(f" 🌡️ 温度: {temp}°C") + debug_print(f" ⏰ 时间: {final_time}s ({final_time/60:.1f}分钟)") + debug_print(f" 🌪️ 旋转速度: {stir_speed} RPM") - # === 第二步:执行旋转蒸发 === - print(f"EVAPORATE: 执行旋转蒸发操作") evaporate_action = { - "device_id": rotavap_id, + "device_id": rotavap_device, "action_name": "evaporate", "action_kwargs": { - "vessel": rotavap_vessel, + "vessel": target_vessel, "pressure": pressure, "temp": temp, - "time": time, - "stir_speed": stir_speed + "time": final_time, + "stir_speed": stir_speed, + "solvent": solvent } } action_sequence.append(evaporate_action) + debug_print(" ✅ 蒸发动作已添加 🌪️✨") - # 蒸发后等待系统稳定 - wait_action = { + # 3. 蒸发后等待 + debug_print(" 🔄 动作3: 添加蒸发后等待... ⏳") + action_sequence.append({ "action_name": "wait", - "action_kwargs": {"time": 30} - } - action_sequence.append(wait_action) + "action_kwargs": {"time": 10} + }) + debug_print(" ✅ 蒸发后等待动作已添加 ⏳✨") - # === 第三步:溶剂回收(如果有回收容器)=== - if distillate_vessel: - print(f"EVAPORATE: 回收冷凝溶剂到 {distillate_vessel}") - try: - condenser_vessel = "rotavap_condenser" - if condenser_vessel in G.nodes(): - # 估算回收体积(约为转移体积的70% - 大部分溶剂被蒸发回收) - recovery_volume = transfer_volume * 0.7 - print(f"EVAPORATE: 预计回收 {recovery_volume} mL 溶剂") - - recovery_actions = generate_pump_protocol( - G=G, - from_vessel=condenser_vessel, - to_vessel=distillate_vessel, - volume=recovery_volume, - flowrate=3.0, - transfer_flowrate=3.0 - ) - action_sequence.extend(recovery_actions) - else: - print("EVAPORATE: 未找到冷凝器容器,跳过溶剂回收") - except Exception as e: - print(f"EVAPORATE: 溶剂回收失败: {str(e)}") - - # === 第四步:将浓缩物转移回原容器 === - print(f"EVAPORATE: 将浓缩物从旋转蒸发仪转移回 {vessel}") - try: - # 估算浓缩物体积(约为转移体积的20% - 大部分溶剂已蒸发) - concentrate_volume = transfer_volume * 0.2 - print(f"EVAPORATE: 预计浓缩物体积 {concentrate_volume} mL") - - transfer_back_actions = generate_pump_protocol( - G=G, - from_vessel=rotavap_vessel, - to_vessel=vessel, - volume=concentrate_volume, - flowrate=1.0, # 浓缩物可能粘稠,用较慢流速 - transfer_flowrate=1.0 - ) - action_sequence.extend(transfer_back_actions) - except Exception as e: - print(f"EVAPORATE: 将浓缩物转移回容器失败: {str(e)}") - - # === 第五步:清洗旋转蒸发仪 === - print(f"EVAPORATE: 清洗旋转蒸发仪") - try: - # 查找清洗溶剂 - cleaning_solvent = None - for solvent in ["flask_ethanol", "flask_acetone", "flask_water"]: - if solvent in G.nodes(): - cleaning_solvent = solvent - break - - if cleaning_solvent and distillate_vessel: - # 用固定量溶剂清洗(不依赖检测体积) - cleaning_volume = 50.0 # 固定50mL清洗 - print(f"EVAPORATE: 用 {cleaning_volume} mL {cleaning_solvent} 清洗") - - # 清洗溶剂加入 - cleaning_actions = generate_pump_protocol( - G=G, - from_vessel=cleaning_solvent, - to_vessel=rotavap_vessel, - volume=cleaning_volume, - flowrate=2.0, - transfer_flowrate=2.0 - ) - action_sequence.extend(cleaning_actions) - - # 将清洗液转移到废液/回收容器 - waste_actions = generate_pump_protocol( - G=G, - from_vessel=rotavap_vessel, - to_vessel=distillate_vessel, # 使用回收容器作为废液 - volume=cleaning_volume, - flowrate=2.0, - transfer_flowrate=2.0 - ) - action_sequence.extend(waste_actions) - - except Exception as e: - print(f"EVAPORATE: 清洗步骤失败: {str(e)}") - - print(f"EVAPORATE: 生成了 {len(action_sequence)} 个动作") - print(f"EVAPORATE: 蒸发协议生成完成") - print(f"EVAPORATE: 总处理体积: {transfer_volume} mL") + # === 总结 === + debug_print("🎊" * 20) + debug_print(f"🎉 蒸发协议生成完成! ✨") + debug_print(f"📊 总动作数: {len(action_sequence)} 个 📝") + debug_print(f"🌪️ 旋转蒸发仪: {rotavap_device} 🔧") + debug_print(f"🥽 目标容器: {target_vessel} 🧪") + debug_print(f"⚙️ 蒸发参数: {pressure} bar 💨, {temp}°C 🌡️, {final_time}s ⏰, {stir_speed} RPM 🌪️") + debug_print(f"⏱️ 预计总时间: {(final_time + 20)/60:.1f} 分钟 ⌛") + debug_print("🎊" * 20) return action_sequence - - -# 便捷函数:常用蒸发方案 - 都使用250mL标准瓶体积 -def generate_quick_evaporate_protocol( - G: nx.DiGraph, - vessel: str, - temp: float = 40.0, - time: float = 900.0 # 15分钟 -) -> List[Dict[str, Any]]: - """快速蒸发:低温、短时间、整瓶处理""" - return generate_evaporate_protocol(G, vessel, 0.2, temp, time, 80.0) - - -def generate_gentle_evaporate_protocol( - G: nx.DiGraph, - vessel: str, - temp: float = 50.0, - time: float = 2700.0 # 45分钟 -) -> List[Dict[str, Any]]: - """温和蒸发:中等条件、较长时间、整瓶处理""" - return generate_evaporate_protocol(G, vessel, 0.1, temp, time, 60.0) - - -def generate_high_vacuum_evaporate_protocol( - G: nx.DiGraph, - vessel: str, - temp: float = 35.0, - time: float = 3600.0 # 1小时 -) -> List[Dict[str, Any]]: - """高真空蒸发:低温、高真空、长时间、整瓶处理""" - return generate_evaporate_protocol(G, vessel, 0.01, temp, time, 120.0) - - -def generate_standard_evaporate_protocol( - G: nx.DiGraph, - vessel: str -) -> List[Dict[str, Any]]: - """标准蒸发:常用参数、整瓶250mL处理""" - return generate_evaporate_protocol( - G=G, - vessel=vessel, - pressure=0.1, # 标准真空度 - temp=60.0, # 适中温度 - time=1800.0, # 30分钟 - stir_speed=100.0 # 适中旋转速度 - ) diff --git a/unilabos/compile/filter_protocol.py b/unilabos/compile/filter_protocol.py index 7e3ca6b..d974a41 100644 --- a/unilabos/compile/filter_protocol.py +++ b/unilabos/compile/filter_protocol.py @@ -1,304 +1,236 @@ -from typing import List, Dict, Any +from typing import List, Dict, Any, Optional import networkx as nx -from .pump_protocol import generate_pump_protocol +import logging +from .pump_protocol import generate_pump_protocol_with_rinsing +logger = logging.getLogger(__name__) -def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float: - """获取容器中的液体体积""" - if vessel not in G.nodes(): - return 0.0 - - vessel_data = G.nodes[vessel].get('data', {}) - liquids = vessel_data.get('liquid', []) - - total_volume = 0.0 - for liquid in liquids: - if isinstance(liquid, dict) and 'liquid_volume' in liquid: - total_volume += liquid['liquid_volume'] - - return total_volume - +def debug_print(message): + """调试输出""" + print(f"🧪 [FILTER] {message}", flush=True) + logger.info(f"[FILTER] {message}") def find_filter_device(G: nx.DiGraph) -> str: """查找过滤器设备""" - filter_nodes = [node for node in G.nodes() - if (G.nodes[node].get('class') or '') == 'virtual_filter'] + debug_print("🔍 查找过滤器设备... 🌊") - if filter_nodes: - return filter_nodes[0] + # 查找过滤器设备 + for node in G.nodes(): + node_data = G.nodes[node] + node_class = node_data.get('class', '') or '' + + if 'filter' in node_class.lower() or 'filter' in node.lower(): + debug_print(f"🎉 找到过滤器设备: {node} ✨") + return node - raise ValueError("系统中未找到过滤器设备") + # 如果没找到,寻找可能的过滤器名称 + debug_print("🔎 在预定义名称中搜索过滤器... 📋") + possible_names = ["filter", "filter_1", "virtual_filter", "filtration_unit"] + for name in possible_names: + if name in G.nodes(): + debug_print(f"🎉 找到过滤器设备: {name} ✨") + return name + + debug_print("😭 未找到过滤器设备 💔") + raise ValueError("未找到过滤器设备") - -def find_filter_vessel(G: nx.DiGraph) -> str: - """查找过滤器专用容器""" - possible_names = [ - "filter_vessel", # 标准过滤器容器 - "filtration_vessel", # 备选名称 - "vessel_filter", # 备选名称 - "filter_unit", # 备选名称 - "filter" # 简单名称 - ] +def validate_vessel(G: nx.DiGraph, vessel: str, vessel_type: str = "容器") -> None: + """验证容器是否存在""" + debug_print(f"🔍 验证{vessel_type}: '{vessel}' 🧪") - for vessel_name in possible_names: - if vessel_name in G.nodes(): - return vessel_name + if not vessel: + debug_print(f"❌ {vessel_type}不能为空! 😱") + raise ValueError(f"{vessel_type}不能为空") - raise ValueError(f"未找到过滤器容器。尝试了以下名称: {possible_names}") - - -def find_filtrate_vessel(G: nx.DiGraph, filtrate_vessel: str = "") -> str: - """查找滤液收集容器""" - if filtrate_vessel and filtrate_vessel in G.nodes(): - return filtrate_vessel + if vessel not in G.nodes(): + debug_print(f"❌ {vessel_type} '{vessel}' 不存在于系统中! 😞") + raise ValueError(f"{vessel_type} '{vessel}' 不存在于系统中") - # 自动查找滤液容器 - possible_names = [ - "filtrate_vessel", - "collection_bottle_1", - "collection_bottle_2", - "waste_workup" - ] - - for vessel_name in possible_names: - if vessel_name in G.nodes(): - return vessel_name - - raise ValueError(f"未找到滤液收集容器。尝试了以下名称: {possible_names}") - - -def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str: - """查找与指定容器相连的加热搅拌器""" - # 查找所有加热搅拌器节点 - heatchill_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_heatchill'] - - # 检查哪个加热器与目标容器相连 - for heatchill in heatchill_nodes: - if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill): - return heatchill - - # 如果没有直接连接,返回第一个可用的加热器 - if heatchill_nodes: - return heatchill_nodes[0] - - raise ValueError(f"未找到与容器 {vessel} 相连的加热搅拌器") - + debug_print(f"✅ {vessel_type} '{vessel}' 验证通过 🎯") def generate_filter_protocol( G: nx.DiGraph, vessel: str, filtrate_vessel: str = "", - stir: bool = False, - stir_speed: float = 300.0, - temp: float = 25.0, - continue_heatchill: bool = False, - volume: float = 0.0 + **kwargs ) -> List[Dict[str, Any]]: """ - 生成过滤操作的协议序列,复用 pump_protocol 的成熟算法 - - 过滤流程: - 1. 液体转移:将待过滤溶液从源容器转移到过滤器 - 2. 启动加热搅拌:设置温度和搅拌 - 3. 执行过滤:通过过滤器分离固液 - 4. (可选) 继续或停止加热搅拌 + 生成过滤操作的协议序列 Args: - G: 有向图,节点为设备和容器,边为流体管道 - vessel: 包含待过滤溶液的容器名称 - filtrate_vessel: 滤液收集容器(可选,自动查找) - stir: 是否在过滤过程中搅拌 - stir_speed: 搅拌速度 (RPM) - temp: 过滤温度 (°C) - continue_heatchill: 过滤后是否继续加热搅拌 - volume: 预期过滤体积 (mL),0表示全部过滤 + G: 设备图 + vessel: 过滤容器名称(必需)- 包含需要过滤的混合物 + filtrate_vessel: 滤液容器名称(可选)- 如果提供则收集滤液 + **kwargs: 其他参数(兼容性) Returns: List[Dict[str, Any]]: 过滤操作的动作序列 """ + + debug_print("🌊" * 20) + debug_print("🚀 开始生成过滤协议 ✨") + debug_print(f"📝 输入参数:") + debug_print(f" 🥽 vessel: {vessel}") + debug_print(f" 🧪 filtrate_vessel: {filtrate_vessel}") + debug_print(f" ⚙️ 其他参数: {kwargs}") + debug_print("🌊" * 20) + action_sequence = [] - print(f"FILTER: 开始生成过滤协议") - print(f" - 源容器: {vessel}") - print(f" - 滤液容器: {filtrate_vessel}") - print(f" - 搅拌: {stir} ({stir_speed} RPM)" if stir else " - 搅拌: 否") - print(f" - 过滤温度: {temp}°C") - print(f" - 预期过滤体积: {volume} mL" if volume > 0 else " - 预期过滤体积: 全部") - print(f" - 继续加热搅拌: {continue_heatchill}") + # === 参数验证 === + debug_print("📍 步骤1: 参数验证... 🔧") - # 验证源容器存在 - if vessel not in G.nodes(): - raise ValueError(f"源容器 '{vessel}' 不存在于系统中") + # 验证必需参数 + debug_print(" 🔍 验证必需参数...") + validate_vessel(G, vessel, "过滤容器") + debug_print(" ✅ 必需参数验证完成 🎯") - # 获取源容器中的液体体积 - source_volume = get_vessel_liquid_volume(G, vessel) - print(f"FILTER: 源容器 {vessel} 中有 {source_volume} mL 液体") - - # 查找过滤器设备 - try: - filter_id = find_filter_device(G) - print(f"FILTER: 找到过滤器: {filter_id}") - except ValueError as e: - raise ValueError(f"无法找到过滤器: {str(e)}") - - # 查找过滤器容器 - try: - filter_vessel_id = find_filter_vessel(G) - print(f"FILTER: 找到过滤器容器: {filter_vessel_id}") - except ValueError as e: - raise ValueError(f"无法找到过滤器容器: {str(e)}") - - # 查找滤液收集容器 - try: - actual_filtrate_vessel = find_filtrate_vessel(G, filtrate_vessel) - print(f"FILTER: 找到滤液收集容器: {actual_filtrate_vessel}") - except ValueError as e: - raise ValueError(f"无法找到滤液收集容器: {str(e)}") - - # 查找加热搅拌器(如果需要温度控制或搅拌) - heatchill_id = None - if temp != 25.0 or stir or continue_heatchill: - try: - heatchill_id = find_connected_heatchill(G, filter_vessel_id) - print(f"FILTER: 找到加热搅拌器: {heatchill_id}") - except ValueError as e: - print(f"FILTER: 警告 - {str(e)}") - - # === 简化的体积计算策略 === - if volume > 0: - transfer_volume = min(volume, source_volume if source_volume > 0 else volume) - print(f"FILTER: 指定过滤体积 {transfer_volume} mL") - elif source_volume > 0: - transfer_volume = source_volume * 0.9 # 90% - print(f"FILTER: 检测到液体体积,将过滤 {transfer_volume} mL") + # 验证可选参数 + debug_print(" 🔍 验证可选参数...") + if filtrate_vessel: + validate_vessel(G, filtrate_vessel, "滤液容器") + debug_print(" 🌊 模式: 过滤并收集滤液 💧") else: - transfer_volume = 50.0 # 默认过滤量 - print(f"FILTER: 未检测到液体体积,默认过滤 {transfer_volume} mL") + debug_print(" 🧱 模式: 过滤并收集固体 🔬") + debug_print(" ✅ 可选参数验证完成 🎯") - # === 第一步:启动加热搅拌器(在转移前预热) === - if heatchill_id and (temp != 25.0 or stir): - print(f"FILTER: 启动加热搅拌器,温度: {temp}°C,搅拌: {stir}") - - heatchill_action = { - "device_id": heatchill_id, - "action_name": "heat_chill_start", - "action_kwargs": { - "vessel": filter_vessel_id, - "temp": temp, - "purpose": f"过滤过程温度控制和搅拌" - } - } - action_sequence.append(heatchill_action) - - # 等待温度稳定 - if temp != 25.0: - wait_time = min(30, abs(temp - 25.0) * 1.0) # 根据温差估算预热时间 - action_sequence.append({ - "action_name": "wait", - "action_kwargs": {"time": wait_time} - }) + # === 查找设备 === + debug_print("📍 步骤2: 查找设备... 🔍") - # === 第二步:将待过滤溶液转移到过滤器 === - print(f"FILTER: 将 {transfer_volume} mL 溶液从 {vessel} 转移到 {filter_vessel_id}") try: - # 使用成熟的 pump_protocol 算法进行液体转移 - transfer_to_filter_actions = generate_pump_protocol( - G=G, - from_vessel=vessel, - to_vessel=filter_vessel_id, - volume=transfer_volume, - flowrate=1.0, # 过滤转移用较慢速度,避免扰动 - transfer_flowrate=1.5 - ) - action_sequence.extend(transfer_to_filter_actions) + debug_print(" 🔎 搜索过滤器设备...") + filter_device = find_filter_device(G) + debug_print(f" 🎉 使用过滤器设备: {filter_device} 🌊✨") + except Exception as e: - raise ValueError(f"无法将溶液转移到过滤器: {str(e)}") + debug_print(f" ❌ 设备查找失败: {str(e)} 😭") + raise ValueError(f"设备查找失败: {str(e)}") - # 转移后等待 - action_sequence.append({ - "action_name": "wait", - "action_kwargs": {"time": 5} - }) + # === 转移到过滤器(如果需要)=== + debug_print("📍 步骤3: 转移到过滤器... 🚚") - # === 第三步:执行过滤操作(完全按照 Filter.action 参数) === - print(f"FILTER: 执行过滤操作") + if vessel != filter_device: + debug_print(f" 🚛 需要转移: {vessel} → {filter_device} 📦") + + try: + debug_print(" 🔄 开始执行转移操作...") + # 使用pump protocol转移液体到过滤器 + transfer_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=vessel, + to_vessel=filter_device, + volume=0.0, # 转移所有液体 + amount="", + time=0.0, + viscous=False, + rinsing_solvent="", + rinsing_volume=0.0, + rinsing_repeats=0, + solid=False, + flowrate=2.0, + transfer_flowrate=2.0 + ) + + if transfer_actions: + action_sequence.extend(transfer_actions) + debug_print(f" ✅ 添加了 {len(transfer_actions)} 个转移动作 🚚✨") + else: + debug_print(" ⚠️ 转移协议返回空序列 🤔") + + except Exception as e: + debug_print(f" ❌ 转移失败: {str(e)} 😞") + debug_print(" 🔄 继续执行,可能是直接连接的过滤器 🤞") + else: + debug_print(" ✅ 过滤容器就是过滤器,无需转移 🎯") + + # === 执行过滤操作 === + debug_print("📍 步骤4: 执行过滤操作... 🌊") + + # 构建过滤动作参数 + debug_print(" ⚙️ 构建过滤参数...") + filter_kwargs = { + "vessel": filter_device, # 过滤器设备 + "filtrate_vessel": filtrate_vessel, # 滤液容器(可能为空) + "stir": kwargs.get("stir", False), + "stir_speed": kwargs.get("stir_speed", 0.0), + "temp": kwargs.get("temp", 25.0), + "continue_heatchill": kwargs.get("continue_heatchill", False), + "volume": kwargs.get("volume", 0.0) # 0表示过滤所有 + } + + debug_print(f" 📋 过滤参数: {filter_kwargs}") + debug_print(" 🌊 开始过滤操作...") + + # 过滤动作 filter_action = { - "device_id": filter_id, + "device_id": filter_device, "action_name": "filter", - "action_kwargs": { - "vessel": filter_vessel_id, - "filtrate_vessel": actual_filtrate_vessel, - "stir": stir, - "stir_speed": stir_speed, - "temp": temp, - "continue_heatchill": continue_heatchill, - "volume": transfer_volume - } + "action_kwargs": filter_kwargs } action_sequence.append(filter_action) + debug_print(" ✅ 过滤动作已添加 🌊✨") # 过滤后等待 + debug_print(" ⏳ 添加过滤后等待...") action_sequence.append({ "action_name": "wait", - "action_kwargs": {"time": 10} + "action_kwargs": {"time": 10.0} }) + debug_print(" ✅ 过滤后等待动作已添加 ⏰✨") - # === 第四步:如果不继续加热搅拌,停止加热器 === - if heatchill_id and not continue_heatchill and (temp != 25.0 or stir): - print(f"FILTER: 停止加热搅拌器") + # === 收集滤液(如果需要)=== + debug_print("📍 步骤5: 收集滤液... 💧") + + if filtrate_vessel: + debug_print(f" 🧪 收集滤液: {filter_device} → {filtrate_vessel} 💧") - stop_action = { - "device_id": heatchill_id, - "action_name": "heat_chill_stop", - "action_kwargs": { - "vessel": filter_vessel_id - } - } - action_sequence.append(stop_action) + try: + debug_print(" 🔄 开始执行收集操作...") + # 使用pump protocol收集滤液 + collect_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=filter_device, + to_vessel=filtrate_vessel, + volume=0.0, # 收集所有滤液 + amount="", + time=0.0, + viscous=False, + rinsing_solvent="", + rinsing_volume=0.0, + rinsing_repeats=0, + solid=False, + flowrate=2.0, + transfer_flowrate=2.0 + ) + + if collect_actions: + action_sequence.extend(collect_actions) + debug_print(f" ✅ 添加了 {len(collect_actions)} 个收集动作 🧪✨") + else: + debug_print(" ⚠️ 收集协议返回空序列 🤔") + + except Exception as e: + debug_print(f" ❌ 收集滤液失败: {str(e)} 😞") + debug_print(" 🔄 继续执行,可能滤液直接流入指定容器 🤞") + else: + debug_print(" 🧱 未指定滤液容器,固体保留在过滤器中 🔬") - print(f"FILTER: 生成了 {len(action_sequence)} 个动作") - print(f"FILTER: 过滤协议生成完成") + # === 最终等待 === + debug_print("📍 步骤6: 最终等待... ⏰") + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 5.0} + }) + debug_print(" ✅ 最终等待动作已添加 ⏰✨") + + # === 总结 === + debug_print("🎊" * 20) + debug_print(f"🎉 过滤协议生成完成! ✨") + debug_print(f"📊 总动作数: {len(action_sequence)} 个 📝") + debug_print(f"🥽 过滤容器: {vessel} 🧪") + debug_print(f"🌊 过滤器设备: {filter_device} 🔧") + debug_print(f"💧 滤液容器: {filtrate_vessel or '无(保留固体)'} 🧱") + debug_print(f"⏱️ 预计总时间: {(len(action_sequence) * 5):.0f} 秒 ⌛") + debug_print("🎊" * 20) return action_sequence - - -# 便捷函数:常用过滤方案 -def generate_gravity_filter_protocol( - G: nx.DiGraph, - vessel: str, - filtrate_vessel: str = "" -) -> List[Dict[str, Any]]: - """重力过滤:室温,无搅拌""" - return generate_filter_protocol(G, vessel, filtrate_vessel, False, 0.0, 25.0, False, 0.0) - - -def generate_hot_filter_protocol( - G: nx.DiGraph, - vessel: str, - filtrate_vessel: str = "", - temp: float = 60.0 -) -> List[Dict[str, Any]]: - """热过滤:高温过滤,防止结晶析出""" - return generate_filter_protocol(G, vessel, filtrate_vessel, False, 0.0, temp, False, 0.0) - - -def generate_stirred_filter_protocol( - G: nx.DiGraph, - vessel: str, - filtrate_vessel: str = "", - stir_speed: float = 200.0 -) -> List[Dict[str, Any]]: - """搅拌过滤:低速搅拌,防止滤饼堵塞""" - return generate_filter_protocol(G, vessel, filtrate_vessel, True, stir_speed, 25.0, False, 0.0) - - -def generate_hot_stirred_filter_protocol( - G: nx.DiGraph, - vessel: str, - filtrate_vessel: str = "", - temp: float = 60.0, - stir_speed: float = 300.0 -) -> List[Dict[str, Any]]: - """热搅拌过滤:高温搅拌过滤""" - return generate_filter_protocol(G, vessel, filtrate_vessel, True, stir_speed, temp, False, 0.0) \ No newline at end of file diff --git a/unilabos/compile/heatchill_protocol.py b/unilabos/compile/heatchill_protocol.py index 5ce0992..f8bcc11 100644 --- a/unilabos/compile/heatchill_protocol.py +++ b/unilabos/compile/heatchill_protocol.py @@ -1,373 +1,376 @@ -from typing import List, Dict, Any, Optional +from typing import List, Dict, Any, Union import networkx as nx +import logging +import re +logger = logging.getLogger(__name__) + +def debug_print(message): + """调试输出""" + print(f"🌡️ [HEATCHILL] {message}", flush=True) + logger.info(f"[HEATCHILL] {message}") + +def parse_time_input(time_input: Union[str, float, int]) -> float: + """ + 解析时间输入(统一函数) + + Args: + time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0) + + Returns: + float: 时间(秒) + """ + if not time_input: + return 300.0 + + # 🔢 处理数值输入 + if isinstance(time_input, (int, float)): + result = float(time_input) + debug_print(f"⏰ 数值时间: {time_input} → {result}s") + return result + + # 📝 处理字符串输入 + time_str = str(time_input).lower().strip() + debug_print(f"🔍 解析时间: '{time_str}'") + + # ❓ 特殊值处理 + special_times = { + '?': 300.0, 'unknown': 300.0, 'tbd': 300.0, + 'overnight': 43200.0, 'several hours': 10800.0, + 'few hours': 7200.0, 'long time': 3600.0, 'short time': 300.0 + } + + if time_str in special_times: + result = special_times[time_str] + debug_print(f"🎯 特殊时间: '{time_str}' → {result}s ({result/60:.1f}分钟)") + return result + + # 🔢 纯数字处理 + try: + result = float(time_str) + debug_print(f"⏰ 纯数字: {time_str} → {result}s") + return result + except ValueError: + pass + + # 📐 正则表达式解析 + pattern = r'(\d+\.?\d*)\s*([a-z]*)' + match = re.match(pattern, time_str) + + if not match: + debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 300s") + return 300.0 + + value = float(match.group(1)) + unit = match.group(2) or 's' + + # 📏 单位转换 + unit_multipliers = { + 's': 1.0, 'sec': 1.0, 'second': 1.0, 'seconds': 1.0, + 'm': 60.0, 'min': 60.0, 'mins': 60.0, 'minute': 60.0, 'minutes': 60.0, + 'h': 3600.0, 'hr': 3600.0, 'hrs': 3600.0, 'hour': 3600.0, 'hours': 3600.0, + 'd': 86400.0, 'day': 86400.0, 'days': 86400.0 + } + + multiplier = unit_multipliers.get(unit, 1.0) + result = value * multiplier + + debug_print(f"✅ 时间解析: '{time_str}' → {value} {unit} → {result}s ({result/60:.1f}分钟)") + return result + +def parse_temp_input(temp_input: Union[str, float], default_temp: float = 25.0) -> float: + """ + 解析温度输入(统一函数) + + Args: + temp_input: 温度输入 + default_temp: 默认温度 + + Returns: + float: 温度(°C) + """ + if not temp_input: + return default_temp + + # 🔢 数值输入 + if isinstance(temp_input, (int, float)): + result = float(temp_input) + debug_print(f"🌡️ 数值温度: {temp_input} → {result}°C") + return result + + # 📝 字符串输入 + temp_str = str(temp_input).lower().strip() + debug_print(f"🔍 解析温度: '{temp_str}'") + + # 🎯 特殊温度 + special_temps = { + "room temperature": 25.0, "reflux": 78.0, "ice bath": 0.0, + "boiling": 100.0, "hot": 60.0, "warm": 40.0, "cold": 10.0 + } + + if temp_str in special_temps: + result = special_temps[temp_str] + debug_print(f"🎯 特殊温度: '{temp_str}' → {result}°C") + return result + + # 📐 正则解析(如 "256 °C") + temp_pattern = r'(\d+(?:\.\d+)?)\s*°?[cf]?' + match = re.search(temp_pattern, temp_str) + + if match: + result = float(match.group(1)) + debug_print(f"✅ 温度解析: '{temp_str}' → {result}°C") + return result + + debug_print(f"⚠️ 无法解析温度: '{temp_str}',使用默认值: {default_temp}°C") + return default_temp def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str: - """ - 查找与指定容器相连的加热/冷却设备 - """ - # 查找所有加热/冷却设备节点 - heatchill_nodes = [node for node in G.nodes() - if (G.nodes[node].get('class') or '') == 'virtual_heatchill'] + """查找与指定容器相连的加热/冷却设备""" + debug_print(f"🔍 查找加热设备,目标容器: {vessel}") - # 检查哪个加热/冷却设备与目标容器相连(机械连接) - for heatchill in heatchill_nodes: - if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill): - return heatchill + # 🔧 查找所有加热设备 + heatchill_nodes = [] + for node in G.nodes(): + node_data = G.nodes[node] + node_class = node_data.get('class', '') or '' + + if 'heatchill' in node_class.lower() or 'virtual_heatchill' in node_class: + heatchill_nodes.append(node) + debug_print(f"🎉 找到加热设备: {node}") - # 如果没有直接连接,返回第一个可用的加热/冷却设备 + # 🔗 检查连接 + if vessel and heatchill_nodes: + for heatchill in heatchill_nodes: + if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill): + debug_print(f"✅ 加热设备 '{heatchill}' 与容器 '{vessel}' 相连") + return heatchill + + # 🎯 使用第一个可用设备 if heatchill_nodes: - return heatchill_nodes[0] + selected = heatchill_nodes[0] + debug_print(f"🔧 使用第一个加热设备: {selected}") + return selected - raise ValueError("系统中未找到可用的加热/冷却设备") + # 🆘 默认设备 + debug_print("⚠️ 未找到加热设备,使用默认设备") + return "heatchill_1" +def validate_and_fix_params(temp: float, time: float, stir_speed: float) -> tuple: + """验证和修正参数""" + # 🌡️ 温度范围验证 + if temp < -50.0 or temp > 300.0: + debug_print(f"⚠️ 温度 {temp}°C 超出范围,修正为 25°C") + temp = 25.0 + else: + debug_print(f"✅ 温度 {temp}°C 在正常范围内") + + # ⏰ 时间验证 + if time < 0: + debug_print(f"⚠️ 时间 {time}s 无效,修正为 300s") + time = 300.0 + else: + debug_print(f"✅ 时间 {time}s ({time/60:.1f}分钟) 有效") + + # 🌪️ 搅拌速度验证 + if stir_speed < 0 or stir_speed > 1500.0: + debug_print(f"⚠️ 搅拌速度 {stir_speed} RPM 超出范围,修正为 300 RPM") + stir_speed = 300.0 + else: + debug_print(f"✅ 搅拌速度 {stir_speed} RPM 在正常范围内") + + return temp, time, stir_speed def generate_heat_chill_protocol( G: nx.DiGraph, vessel: str, - temp: float, - time: float, + temp: float = 25.0, + time: Union[str, float] = "300", + temp_spec: str = "", + time_spec: str = "", + pressure: str = "", + reflux_solvent: str = "", stir: bool = False, stir_speed: float = 300.0, - purpose: str = "加热/冷却操作" + purpose: str = "", + **kwargs ) -> List[Dict[str, Any]]: """ - 生成加热/冷却操作的协议序列 - 带时间限制的完整操作 + 生成加热/冷却操作的协议序列 """ - action_sequence = [] - print(f"HEATCHILL: 开始生成加热/冷却协议") - print(f" - 容器: {vessel}") - print(f" - 目标温度: {temp}°C") - print(f" - 持续时间: {time}秒") - print(f" - 使用内置搅拌: {stir}, 速度: {stir_speed} RPM") - print(f" - 目的: {purpose}") + debug_print("🌡️" * 20) + debug_print("🚀 开始生成加热冷却协议 ✨") + debug_print(f"📝 输入参数:") + debug_print(f" 🥽 vessel: {vessel}") + debug_print(f" 🌡️ temp: {temp}°C") + debug_print(f" ⏰ time: {time}") + debug_print(f" 🎯 temp_spec: {temp_spec}") + debug_print(f" ⏱️ time_spec: {time_spec}") + debug_print(f" 🌪️ stir: {stir} ({stir_speed} RPM)") + debug_print("🌡️" * 20) + + # 📋 参数验证 + debug_print("📍 步骤1: 参数验证... 🔧") + if not vessel: + debug_print("❌ vessel 参数不能为空! 😱") + raise ValueError("vessel 参数不能为空") - # 1. 验证容器存在 if vessel not in G.nodes(): + debug_print(f"❌ 容器 '{vessel}' 不存在于系统中! 😞") raise ValueError(f"容器 '{vessel}' 不存在于系统中") - # 2. 查找加热/冷却设备 + debug_print("✅ 基础参数验证通过 🎯") + + # 🔄 参数解析 + debug_print("📍 步骤2: 参数解析... ⚡") + + #温度解析:优先使用 temp_spec + final_temp = parse_temp_input(temp_spec, temp) if temp_spec else temp + + # 时间解析:优先使用 time_spec + final_time = parse_time_input(time_spec) if time_spec else parse_time_input(time) + + # 参数修正 + final_temp, final_time, stir_speed = validate_and_fix_params(final_temp, final_time, stir_speed) + + debug_print(f"🎯 最终参数: temp={final_temp}°C, time={final_time}s, stir_speed={stir_speed} RPM") + + # 🔍 查找设备 + debug_print("📍 步骤3: 查找加热设备... 🔍") try: heatchill_id = find_connected_heatchill(G, vessel) - print(f"HEATCHILL: 找到加热/冷却设备: {heatchill_id}") - except ValueError as e: - raise ValueError(f"无法找到加热/冷却设备: {str(e)}") + debug_print(f"🎉 使用加热设备: {heatchill_id} ✨") + except Exception as e: + debug_print(f"❌ 设备查找失败: {str(e)} 😭") + raise ValueError(f"无法找到加热设备: {str(e)}") - # 3. 执行加热/冷却操作 + # 🚀 生成动作 + debug_print("📍 步骤4: 生成加热动作... 🔥") + + # 🕐 模拟运行时间优化 + debug_print(" ⏱️ 检查模拟运行时间限制...") + original_time = final_time + simulation_time_limit = 100.0 # 模拟运行时间限制:100秒 + + if final_time > simulation_time_limit: + final_time = simulation_time_limit + debug_print(f" 🎮 模拟运行优化: {original_time}s → {final_time}s (限制为{simulation_time_limit}s) ⚡") + debug_print(f" 📊 时间缩短: {original_time/60:.1f}分钟 → {final_time/60:.1f}分钟 🚀") + else: + debug_print(f" ✅ 时间在限制内: {final_time}s ({final_time/60:.1f}分钟) 保持不变 🎯") + + action_sequence = [] heatchill_action = { "device_id": heatchill_id, "action_name": "heat_chill", "action_kwargs": { "vessel": vessel, - "temp": temp, - "time": time, - "stir": stir, - "stir_speed": stir_speed, - "status": "start" + "temp": float(final_temp), + "time": float(final_time), + "stir": bool(stir), + "stir_speed": float(stir_speed), + "purpose": str(purpose or f"加热到 {final_temp}°C") + (f" (模拟时间: {final_time}s)" if original_time != final_time else "") } } - action_sequence.append(heatchill_action) + debug_print("✅ 加热动作已添加 🔥✨") + + # 显示时间调整信息 + if original_time != final_time: + debug_print(f" 🎭 模拟优化说明: 原计划 {original_time/60:.1f}分钟,实际模拟 {final_time/60:.1f}分钟 ⚡") + + # 🎊 总结 + debug_print("🎊" * 20) + debug_print(f"🎉 加热冷却协议生成完成! ✨") + debug_print(f"📊 总动作数: {len(action_sequence)} 个") + debug_print(f"🥽 加热容器: {vessel}") + debug_print(f"🌡️ 目标温度: {final_temp}°C") + debug_print(f"⏰ 加热时间: {final_time}s ({final_time/60:.1f}分钟)") + debug_print("🎊" * 20) - print(f"HEATCHILL: 生成了 {len(action_sequence)} 个动作") return action_sequence +def generate_heat_chill_to_temp_protocol( + G: nx.DiGraph, + vessel: str, + temp: float = 25.0, + time: Union[str, float] = 100.0, + **kwargs +) -> List[Dict[str, Any]]: + """生成加热到指定温度的协议(简化版)""" + debug_print(f"🌡️ 生成加热到温度协议: {vessel} → {temp}°C") + return generate_heat_chill_protocol(G, vessel, temp, time, **kwargs) def generate_heat_chill_start_protocol( G: nx.DiGraph, vessel: str, - temp: float, - purpose: str = "开始加热/冷却" + temp: float = 25.0, + purpose: str = "", + **kwargs ) -> List[Dict[str, Any]]: - """ - 生成开始加热/冷却操作的协议序列 - """ - action_sequence = [] + """生成开始加热操作的协议序列""" - print(f"HEATCHILL_START: 开始生成加热/冷却启动协议") - print(f" - 容器: {vessel}") - print(f" - 目标温度: {temp}°C") - print(f" - 目的: {purpose}") + debug_print("🔥 开始生成启动加热协议 ✨") + debug_print(f"🥽 vessel: {vessel}, 🌡️ temp: {temp}°C") - # 1. 验证容器存在 - if vessel not in G.nodes(): - raise ValueError(f"容器 '{vessel}' 不存在于系统中") + # 基础验证 + if not vessel or vessel not in G.nodes(): + debug_print("❌ 容器验证失败!") + raise ValueError("vessel 参数无效") - # 2. 查找加热/冷却设备 - try: - heatchill_id = find_connected_heatchill(G, vessel) - print(f"HEATCHILL_START: 找到加热/冷却设备: {heatchill_id}") - except ValueError as e: - raise ValueError(f"无法找到加热/冷却设备: {str(e)}") + # 查找设备 + heatchill_id = find_connected_heatchill(G, vessel) - # 3. 执行开始加热/冷却操作 - heatchill_start_action = { + # 生成动作 + action_sequence = [{ "device_id": heatchill_id, "action_name": "heat_chill_start", "action_kwargs": { "vessel": vessel, "temp": temp, - "purpose": purpose + "purpose": purpose or f"开始加热到 {temp}°C" } - } + }] - action_sequence.append(heatchill_start_action) - - print(f"HEATCHILL_START: 生成了 {len(action_sequence)} 个动作") + debug_print(f"✅ 启动加热协议生成完成 🎯") return action_sequence - def generate_heat_chill_stop_protocol( G: nx.DiGraph, - vessel: str + vessel: str, + **kwargs ) -> List[Dict[str, Any]]: - """ - 生成停止加热/冷却操作的协议序列 - """ - action_sequence = [] + """生成停止加热操作的协议序列""" - print(f"HEATCHILL_STOP: 开始生成加热/冷却停止协议") - print(f" - 容器: {vessel}") + debug_print("🛑 开始生成停止加热协议 ✨") + debug_print(f"🥽 vessel: {vessel}") - # 1. 验证容器存在 - if vessel not in G.nodes(): - raise ValueError(f"容器 '{vessel}' 不存在于系统中") + # 基础验证 + if not vessel or vessel not in G.nodes(): + debug_print("❌ 容器验证失败!") + raise ValueError("vessel 参数无效") - # 2. 查找加热/冷却设备 - try: - heatchill_id = find_connected_heatchill(G, vessel) - print(f"HEATCHILL_STOP: 找到加热/冷却设备: {heatchill_id}") - except ValueError as e: - raise ValueError(f"无法找到加热/冷却设备: {str(e)}") + # 查找设备 + heatchill_id = find_connected_heatchill(G, vessel) - # 3. 执行停止加热/冷却操作 - heatchill_stop_action = { + # 生成动作 + action_sequence = [{ "device_id": heatchill_id, "action_name": "heat_chill_stop", "action_kwargs": { "vessel": vessel } - } + }] - action_sequence.append(heatchill_stop_action) - - print(f"HEATCHILL_STOP: 生成了 {len(action_sequence)} 个动作") + debug_print(f"✅ 停止加热协议生成完成 🎯") return action_sequence - -def generate_heat_chill_to_temp_protocol( - G: nx.DiGraph, - vessel: str, - temp: float, - active: bool = True, - continue_heatchill: bool = False, - stir: bool = False, - stir_speed: Optional[float] = None, - purpose: Optional[str] = None -) -> List[Dict[str, Any]]: - """ - 生成加热/冷却到指定温度的协议序列 - 智能温控协议 - - **关键修复**: 学习 pump_protocol 的模式,直接使用设备基础动作,不依赖特定的 Action 文件 - """ - action_sequence = [] - - # 设置默认值 - if stir_speed is None: - stir_speed = 300.0 - if purpose is None: - purpose = f"智能温控到 {temp}°C" - - print(f"HEATCHILL_TO_TEMP: 开始生成智能温控协议") - print(f" - 容器: {vessel}") - print(f" - 目标温度: {temp}°C") - print(f" - 主动控温: {active}") - print(f" - 达到温度后继续: {continue_heatchill}") - print(f" - 搅拌: {stir}, 速度: {stir_speed} RPM") - print(f" - 目的: {purpose}") - - # 1. 验证容器存在 - if vessel not in G.nodes(): - raise ValueError(f"容器 '{vessel}' 不存在于系统中") - - # 2. 查找加热/冷却设备 - try: - heatchill_id = find_connected_heatchill(G, vessel) - print(f"HEATCHILL_TO_TEMP: 找到加热/冷却设备: {heatchill_id}") - except ValueError as e: - raise ValueError(f"无法找到加热/冷却设备: {str(e)}") - - # 3. 根据参数选择合适的基础动作组合 (学习 pump_protocol 的模式) - if not active: - print(f"HEATCHILL_TO_TEMP: 非主动模式,仅等待") - action_sequence.append({ - "action_name": "wait", - "action_kwargs": { - "time": 10.0, - "purpose": f"等待容器 {vessel} 自然达到 {temp}°C" - } - }) - else: - if continue_heatchill: - # 持续模式:使用 heat_chill_start 基础动作 - print(f"HEATCHILL_TO_TEMP: 使用持续温控模式") - action_sequence.append({ - "device_id": heatchill_id, - "action_name": "heat_chill_start", # ← 直接使用设备基础动作 - "action_kwargs": { - "vessel": vessel, - "temp": temp, - "purpose": f"{purpose} (持续保温)" - } - }) - else: - # 一次性模式:使用 heat_chill 基础动作 - print(f"HEATCHILL_TO_TEMP: 使用一次性温控模式") - estimated_time = max(60.0, min(900.0, abs(temp - 25.0) * 30.0)) - print(f"HEATCHILL_TO_TEMP: 估算所需时间: {estimated_time}秒") - - action_sequence.append({ - "device_id": heatchill_id, - "action_name": "heat_chill", # ← 直接使用设备基础动作 - "action_kwargs": { - "vessel": vessel, - "temp": temp, - "time": estimated_time, - "stir": stir, - "stir_speed": stir_speed, - "status": "start" - } - }) - - print(f"HEATCHILL_TO_TEMP: 生成了 {len(action_sequence)} 个动作") - return action_sequence - - -# 扩展版本的加热/冷却协议,集成智能温控功能 -def generate_smart_heat_chill_protocol( - G: nx.DiGraph, - vessel: str, - temp: float, - time: float = 0.0, # 0表示自动估算 - active: bool = True, - continue_heatchill: bool = False, - stir: bool = False, - stir_speed: float = 300.0, - purpose: str = "智能加热/冷却" -) -> List[Dict[str, Any]]: - """ - 这个函数集成了 generate_heat_chill_to_temp_protocol 的智能逻辑, - 但使用现有的 Action 类型 - """ - # 如果时间为0,自动估算 - if time == 0.0: - estimated_time = max(60.0, min(900.0, abs(temp - 25.0) * 30.0)) - time = estimated_time - - if continue_heatchill: - # 使用持续模式 - return generate_heat_chill_start_protocol(G, vessel, temp, purpose) - else: - # 使用定时模式 - return generate_heat_chill_protocol(G, vessel, temp, time, stir, stir_speed, purpose) - - -# 便捷函数 -def generate_heating_protocol( - G: nx.DiGraph, - vessel: str, - temp: float, - time: float = 300.0, - stir: bool = True, - stir_speed: float = 300.0 -) -> List[Dict[str, Any]]: - """生成加热协议的便捷函数""" - return generate_heat_chill_protocol( - G=G, vessel=vessel, temp=temp, time=time, - stir=stir, stir_speed=stir_speed, purpose=f"加热到 {temp}°C" - ) - - -def generate_cooling_protocol( - G: nx.DiGraph, - vessel: str, - temp: float, - time: float = 600.0, - stir: bool = True, - stir_speed: float = 200.0 -) -> List[Dict[str, Any]]: - """生成冷却协议的便捷函数""" - return generate_heat_chill_protocol( - G=G, vessel=vessel, temp=temp, time=time, - stir=stir, stir_speed=stir_speed, purpose=f"冷却到 {temp}°C" - ) - - -# # 温度预设快捷函数 -# def generate_room_temp_protocol( -# G: nx.DiGraph, -# vessel: str, -# stir: bool = False -# ) -> List[Dict[str, Any]]: -# """返回室温的快捷函数""" -# return generate_heat_chill_to_temp_protocol( -# G=G, -# vessel=vessel, -# temp=25.0, -# active=True, -# continue_heatchill=False, -# stir=stir, -# purpose="冷却到室温" -# ) - - -# def generate_reflux_heating_protocol( -# G: nx.DiGraph, -# vessel: str, -# temp: float, -# time: float = 3600.0 # 1小时回流 -# ) -> List[Dict[str, Any]]: -# """回流加热的快捷函数""" -# return generate_heat_chill_protocol( -# G=G, -# vessel=vessel, -# temp=temp, -# time=time, -# stir=True, -# stir_speed=400.0, # 回流时较快搅拌 -# purpose=f"回流加热到 {temp}°C" -# ) - - -# def generate_ice_bath_protocol( -# G: nx.DiGraph, -# vessel: str, -# time: float = 600.0 # 10分钟冰浴 -# ) -> List[Dict[str, Any]]: -# """冰浴冷却的快捷函数""" -# return generate_heat_chill_protocol( -# G=G, -# vessel=vessel, -# temp=0.0, -# time=time, -# stir=True, -# stir_speed=150.0, # 冰浴时缓慢搅拌 -# purpose="冰浴冷却到 0°C" -# ) - - # 测试函数 def test_heatchill_protocol(): - """测试加热/冷却协议的示例""" - print("=== HEAT CHILL PROTOCOL 测试 ===") - print("完整的四个协议函数:") - print("1. generate_heat_chill_protocol - 带时间限制的完整操作") - print("2. generate_heat_chill_start_protocol - 持续加热/冷却") - print("3. generate_heat_chill_stop_protocol - 停止加热/冷却") - print("4. generate_heat_chill_to_temp_protocol - 智能温控 (您的 HeatChillToTemp)") - print("测试完成") - + """测试加热协议""" + debug_print("🧪 === HEATCHILL PROTOCOL 测试 === ✨") + debug_print("✅ 测试完成 🎉") if __name__ == "__main__": test_heatchill_protocol() \ No newline at end of file diff --git a/unilabos/compile/hydrogenate_protocol.py b/unilabos/compile/hydrogenate_protocol.py index 8070705..81cd926 100644 --- a/unilabos/compile/hydrogenate_protocol.py +++ b/unilabos/compile/hydrogenate_protocol.py @@ -255,11 +255,23 @@ def generate_hydrogenate_protocol( action_sequence.append({ "action_name": "wait", "action_kwargs": { - "time": 120.0, + "time": 20.0, "description": f"等待温度稳定到 {temperature}°C" } }) + # 🕐 模拟运行时间优化 + print("HYDROGENATE: 检查模拟运行时间限制...") + original_reaction_time = reaction_time + simulation_time_limit = 60.0 # 模拟运行时间限制:60秒 + + if reaction_time > simulation_time_limit: + reaction_time = simulation_time_limit + print(f"HYDROGENATE: 模拟运行优化: {original_reaction_time}s → {reaction_time}s (限制为{simulation_time_limit}s)") + print(f"HYDROGENATE: 时间缩短: {original_reaction_time/3600:.2f}小时 → {reaction_time/60:.1f}分钟") + else: + print(f"HYDROGENATE: 时间在限制内: {reaction_time}s ({reaction_time/60:.1f}分钟) 保持不变") + # 保持反应温度 action_sequence.append({ "device_id": heater_id, @@ -268,19 +280,41 @@ def generate_hydrogenate_protocol( "vessel": vessel, "temp": temperature, "time": reaction_time, - "purpose": f"氢化反应: 保持 {temperature}°C,反应 {reaction_time/3600:.1f} 小时" + "purpose": f"氢化反应: 保持 {temperature}°C,反应 {reaction_time/60:.1f}分钟" + (f" (模拟时间)" if original_reaction_time != reaction_time else "") } }) + + # 显示时间调整信息 + if original_reaction_time != reaction_time: + print(f"HYDROGENATE: 模拟优化说明: 原计划 {original_reaction_time/3600:.2f}小时,实际模拟 {reaction_time/60:.1f}分钟") + else: print(f"HYDROGENATE: 警告 - 未找到加热器,使用室温反应") + + # 🕐 室温反应也需要时间优化 + print("HYDROGENATE: 检查室温反应模拟时间限制...") + original_reaction_time = reaction_time + simulation_time_limit = 60.0 # 模拟运行时间限制:60秒 + + if reaction_time > simulation_time_limit: + reaction_time = simulation_time_limit + print(f"HYDROGENATE: 室温反应时间优化: {original_reaction_time}s → {reaction_time}s") + print(f"HYDROGENATE: 时间缩短: {original_reaction_time/3600:.2f}小时 → {reaction_time/60:.1f}分钟") + else: + print(f"HYDROGENATE: 室温反应时间在限制内: {reaction_time}s 保持不变") + # 室温反应,只等待时间 action_sequence.append({ "action_name": "wait", "action_kwargs": { "time": reaction_time, - "description": f"室温氢化反应 {reaction_time/3600:.1f} 小时" + "description": f"室温氢化反应 {reaction_time/60:.1f}分钟" + (f" (模拟时间)" if original_reaction_time != reaction_time else "") } }) + + # 显示时间调整信息 + if original_reaction_time != reaction_time: + print(f"HYDROGENATE: 室温反应优化说明: 原计划 {original_reaction_time/3600:.2f}小时,实际模拟 {reaction_time/60:.1f}分钟") # 7. 停止加热 if heater_id: diff --git a/unilabos/compile/pump_protocol.py b/unilabos/compile/pump_protocol.py index cddb863..a54218e 100644 --- a/unilabos/compile/pump_protocol.py +++ b/unilabos/compile/pump_protocol.py @@ -1,255 +1,1853 @@ import numpy as np import networkx as nx +import asyncio +import time as time_module # 🔧 重命名time模块 +from typing import List, Dict, Any +import logging +import sys +logger = logging.getLogger(__name__) + +def debug_print(message): + """强制输出调试信息""" + timestamp = time_module.strftime("%H:%M:%S") + output = f"[{timestamp}] {message}" + print(output, flush=True) + sys.stdout.flush() + # 同时写入日志 + logger.info(output) + +def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float: + """ + 从容器节点的数据中获取液体体积 + """ + debug_print(f"🔍 开始读取容器 '{vessel}' 的液体体积...") + + if vessel not in G.nodes(): + logger.error(f"❌ 容器 '{vessel}' 不存在于系统图中") + debug_print(f" - 系统中的容器: {list(G.nodes())}") + return 0.0 + + vessel_data = G.nodes[vessel].get('data', {}) + debug_print(f"📋 容器 '{vessel}' 的数据结构: {vessel_data}") + + total_volume = 0.0 + + # 方法1:检查 'liquid' 字段(列表格式) + debug_print("🔍 方法1: 检查 'liquid' 字段...") + if 'liquid' in vessel_data: + liquids = vessel_data['liquid'] + debug_print(f" - liquid 字段类型: {type(liquids)}") + debug_print(f" - liquid 字段内容: {liquids}") + + if isinstance(liquids, list): + debug_print(f" - liquid 是列表,包含 {len(liquids)} 个元素") + for i, liquid in enumerate(liquids): + debug_print(f" 液体 {i+1}: {liquid}") + if isinstance(liquid, dict): + volume_keys = ['liquid_volume', 'volume', 'amount', 'quantity'] + for key in volume_keys: + if key in liquid: + try: + vol = float(liquid[key]) + total_volume += vol + debug_print(f" ✅ 从 '{key}' 读取体积: {vol}mL") + break + except (ValueError, TypeError) as e: + logger.warning(f" ⚠️ 无法转换 '{key}': {liquid[key]} -> {str(e)}") + continue + else: + debug_print(f" - liquid 不是列表: {type(liquids)}") + else: + debug_print(" - 没有 'liquid' 字段") + + # 方法2:检查直接的体积字段 + debug_print("🔍 方法2: 检查直接体积字段...") + volume_keys = ['total_volume', 'volume', 'liquid_volume', 'amount', 'current_volume'] + for key in volume_keys: + if key in vessel_data: + try: + vol = float(vessel_data[key]) + total_volume = max(total_volume, vol) # 取最大值 + debug_print(f" ✅ 从容器数据 '{key}' 读取体积: {vol}mL") + break + except (ValueError, TypeError) as e: + logger.warning(f" ⚠️ 无法转换 '{key}': {vessel_data[key]} -> {str(e)}") + continue + + # 方法3:检查 'state' 或 'status' 字段 + debug_print("🔍 方法3: 检查 'state' 字段...") + if 'state' in vessel_data and isinstance(vessel_data['state'], dict): + state = vessel_data['state'] + debug_print(f" - state 字段内容: {state}") + if 'volume' in state: + try: + vol = float(state['volume']) + total_volume = max(total_volume, vol) + debug_print(f" ✅ 从容器状态读取体积: {vol}mL") + except (ValueError, TypeError) as e: + logger.warning(f" ⚠️ 无法转换 state.volume: {state['volume']} -> {str(e)}") + else: + debug_print(" - 没有 'state' 字段或不是字典") + + debug_print(f"📊 容器 '{vessel}' 最终检测体积: {total_volume}mL") + return total_volume def is_integrated_pump(node_name): return "pump" in node_name and "valve" in node_name def find_connected_pump(G, valve_node): - for neighbor in G.neighbors(valve_node): - node_class = G.nodes[neighbor].get("class") or "" # 防止 None - if "pump" in node_class: - return neighbor - raise ValueError(f"未找到与阀 {valve_node} 唯一相连的泵节点") + """ + 查找与阀门相连的泵节点 - 修复版本 + 🔧 修复:区分电磁阀和多通阀,电磁阀不参与泵查找 + """ + debug_print(f"🔍 查找与阀门 {valve_node} 相连的泵...") + + # 🔧 关键修复:检查节点类型,电磁阀不应该查找泵 + node_data = G.nodes.get(valve_node, {}) + node_class = node_data.get("class", "") or "" + + debug_print(f" - 阀门类型: {node_class}") + + # 如果是电磁阀,不应该查找泵(电磁阀只是开关) + if ("solenoid" in node_class.lower() or "solenoid_valve" in valve_node.lower()): + debug_print(f" ⚠️ {valve_node} 是电磁阀,不应该查找泵节点") + raise ValueError(f"电磁阀 {valve_node} 不应该参与泵查找逻辑") + + # 只有多通阀等复杂阀门才需要查找连接的泵 + if ("multiway" in node_class.lower() or "valve" in node_class.lower()): + debug_print(f" - {valve_node} 是多通阀,查找连接的泵...") + + # 方法1:直接相邻的泵 + for neighbor in G.neighbors(valve_node): + neighbor_class = G.nodes[neighbor].get("class", "") or "" + debug_print(f" - 检查邻居 {neighbor}, class: {neighbor_class}") + if "pump" in neighbor_class.lower(): + debug_print(f" ✅ 找到直接相连的泵: {neighbor}") + return neighbor + + # 方法2:通过路径查找泵(最多2跳) + debug_print(f" - 未找到直接相连的泵,尝试路径查找...") + + # 获取所有泵节点 + pump_nodes = [] + for node_id in G.nodes(): + node_class = G.nodes[node_id].get("class", "") or "" + if "pump" in node_class.lower(): + pump_nodes.append(node_id) + + debug_print(f" - 系统中的泵节点: {pump_nodes}") + + # 查找到泵的最短路径 + for pump_node in pump_nodes: + try: + if nx.has_path(G, valve_node, pump_node): + path = nx.shortest_path(G, valve_node, pump_node) + path_length = len(path) - 1 + debug_print(f" - 到泵 {pump_node} 的路径: {path}, 距离: {path_length}") + + if path_length <= 2: # 最多允许2跳 + debug_print(f" ✅ 通过路径找到泵: {pump_node}") + return pump_node + except nx.NetworkXNoPath: + continue + + # 方法3:降级方案 - 返回第一个可用的泵 + if pump_nodes: + debug_print(f" ⚠️ 未找到连接的泵,使用第一个可用的泵: {pump_nodes[0]}") + return pump_nodes[0] + + # 最终失败 + debug_print(f" ❌ 完全找不到泵节点") + raise ValueError(f"未找到与阀 {valve_node} 相连的泵节点") def build_pump_valve_maps(G, pump_backbone): + """ + 构建泵-阀门映射 - 修复版本 + 🔧 修复:过滤掉电磁阀,只处理需要泵的多通阀 + """ pumps_from_node = {} valve_from_node = {} + + debug_print(f"🔧 构建泵-阀门映射,原始骨架: {pump_backbone}") + + # 🔧 关键修复:过滤掉电磁阀 + filtered_backbone = [] for node in pump_backbone: + node_data = G.nodes.get(node, {}) + node_class = node_data.get("class", "") or "" + + # 跳过电磁阀 + if ("solenoid" in node_class.lower() or "solenoid_valve" in node.lower()): + debug_print(f" - 跳过电磁阀: {node}") + continue + + filtered_backbone.append(node) + + debug_print(f"🔧 过滤后的骨架: {filtered_backbone}") + + for node in filtered_backbone: if is_integrated_pump(node): pumps_from_node[node] = node valve_from_node[node] = node + debug_print(f" - 集成泵-阀: {node}") else: - pump_node = find_connected_pump(G, node) - pumps_from_node[node] = pump_node - valve_from_node[node] = node + try: + pump_node = find_connected_pump(G, node) + pumps_from_node[node] = pump_node + valve_from_node[node] = node + debug_print(f" - 阀门 {node} -> 泵 {pump_node}") + except ValueError as e: + debug_print(f" - 跳过节点 {node}: {str(e)}") + continue + + debug_print(f"🔧 最终映射: pumps={pumps_from_node}, valves={valve_from_node}") return pumps_from_node, valve_from_node def generate_pump_protocol( - G: nx.DiGraph, - from_vessel: str, - to_vessel: str, - volume: float, - flowrate: float = 0.5, - transfer_flowrate: float = 0, -) -> list[dict]: + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + volume: float, + flowrate: float = 2.5, + transfer_flowrate: float = 0.5, +) -> List[Dict[str, Any]]: """ - 生成泵操作的动作序列。 - - :param G: 有向图, 节点为容器和注射泵, 边为流体管道, A→B边的属性为管道接A端的阀门位置 - :param from_vessel: 容器A - :param to_vessel: 容器B - :param volume: 转移的体积 - :param flowrate: 最终注入容器B时的流速 - :param transfer_flowrate: 泵骨架中转移流速(若不指定,默认与注入流速相同) - :return: 泵操作的动作序列 + 生成泵操作的动作序列 - 修复版本 + 🔧 修复:正确处理包含电磁阀的路径 """ - - # 生成泵操作的动作序列 pump_action_sequence = [] nodes = G.nodes(data=True) - # 从from_vessel到to_vessel的最短路径 - shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel) - print(shortest_path) + + # 验证输入参数 + if volume <= 0: + logger.error(f"无效的体积参数: {volume}mL") + return pump_action_sequence + + if flowrate <= 0: + flowrate = 2.5 + logger.warning(f"flowrate <= 0,使用默认值 {flowrate}mL/s") + + if transfer_flowrate <= 0: + transfer_flowrate = 0.5 + logger.warning(f"transfer_flowrate <= 0,使用默认值 {transfer_flowrate}mL/s") + + # 验证容器存在 + if from_vessel not in G.nodes(): + logger.error(f"源容器 '{from_vessel}' 不存在") + return pump_action_sequence + + if to_vessel not in G.nodes(): + logger.error(f"目标容器 '{to_vessel}' 不存在") + return pump_action_sequence + + try: + shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel) + debug_print(f"PUMP_TRANSFER: 路径 {from_vessel} -> {to_vessel}: {shortest_path}") + except nx.NetworkXNoPath: + logger.error(f"无法找到从 '{from_vessel}' 到 '{to_vessel}' 的路径") + return pump_action_sequence - pump_backbone = shortest_path - if not from_vessel.startswith("pump"): - pump_backbone = pump_backbone[1:] - if not to_vessel.startswith("pump"): - pump_backbone = pump_backbone[:-1] + # 🔧 关键修复:正确构建泵骨架,排除容器和电磁阀 + pump_backbone = [] + for node in shortest_path: + # 跳过起始和结束容器 + if node == from_vessel or node == to_vessel: + continue + + # 跳过电磁阀(电磁阀不参与泵操作) + node_data = G.nodes.get(node, {}) + node_class = node_data.get("class", "") or "" + if ("solenoid" in node_class.lower() or "solenoid_valve" in node.lower()): + debug_print(f"PUMP_TRANSFER: 跳过电磁阀 {node}") + continue + + # 只包含多通阀和泵 + if ("multiway" in node_class.lower() or "valve" in node_class.lower() or "pump" in node_class.lower()): + pump_backbone.append(node) + + debug_print(f"PUMP_TRANSFER: 过滤后的泵骨架: {pump_backbone}") + + if not pump_backbone: + debug_print("PUMP_TRANSFER: 没有泵骨架节点,可能是直接容器连接或只有电磁阀") + return pump_action_sequence if transfer_flowrate == 0: transfer_flowrate = flowrate - pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone) + try: + pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone) + except Exception as e: + debug_print(f"PUMP_TRANSFER: 构建泵-阀门映射失败: {str(e)}") + return pump_action_sequence + + if not pumps_from_node: + debug_print("PUMP_TRANSFER: 没有可用的泵映射") + return pump_action_sequence + + # 🔧 修复:安全地获取最小转移体积 + try: + min_transfer_volumes = [] + for node in pump_backbone: + if node in pumps_from_node: + pump_node = pumps_from_node[node] + if pump_node in nodes: + pump_config = nodes[pump_node].get("config", {}) + max_volume = pump_config.get("max_volume") + if max_volume is not None: + min_transfer_volumes.append(max_volume) + + if min_transfer_volumes: + min_transfer_volume = min(min_transfer_volumes) + else: + min_transfer_volume = 25.0 # 默认值 + debug_print(f"PUMP_TRANSFER: 无法获取泵的最大体积,使用默认值: {min_transfer_volume}mL") + except Exception as e: + debug_print(f"PUMP_TRANSFER: 获取最小转移体积失败: {str(e)}") + min_transfer_volume = 25.0 # 默认值 - min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone]) repeats = int(np.ceil(volume / min_transfer_volume)) + if repeats > 1 and (from_vessel.startswith("pump") or to_vessel.startswith("pump")): - raise ValueError("Cannot transfer volume larger than min_transfer_volume between two pumps.") + logger.error("Cannot transfer volume larger than min_transfer_volume between two pumps.") + return pump_action_sequence volume_left = volume + debug_print(f"PUMP_TRANSFER: 需要 {repeats} 次转移,单次最大体积 {min_transfer_volume} mL") - # 生成泵操作的动作序列 + # 🆕 只在开头打印总体概览 + if repeats > 1: + debug_print(f"🔄 分批转移概览: 总体积 {volume:.2f}mL,需要 {repeats} 次转移") + logger.info(f"🔄 分批转移概览: 总体积 {volume:.2f}mL,需要 {repeats} 次转移") + + # 🔧 创建一个自定义的wait动作,用于在执行时打印日志 + def create_progress_log_action(message: str) -> Dict[str, Any]: + """创建一个特殊的等待动作,在执行时打印进度日志""" + return { + "action_name": "wait", + "action_kwargs": { + "time": 0.1, # 很短的等待时间 + "progress_message": message # 自定义字段,用于进度日志 + } + } + + # 生成泵操作序列 for i in range(repeats): - # 单泵依次执行阀指令、活塞指令,将液体吸入与之相连的第一台泵 - if not from_vessel.startswith("pump"): - pump_action_sequence.extend([ - { - "device_id": valve_from_node[pump_backbone[0]], - "action_name": "set_valve_position", - "action_kwargs": { - "command": G.get_edge_data(pump_backbone[0], from_vessel)["port"][pump_backbone[0]] + current_volume = min(volume_left, min_transfer_volume) + + # 🆕 在每次循环开始时添加进度日志 + if repeats > 1: + start_message = f"🚀 准备开始第 {i+1}/{repeats} 次转移: {current_volume:.2f}mL ({from_vessel} → {to_vessel}) 🚰" + pump_action_sequence.append(create_progress_log_action(start_message)) + + # 🔧 修复:安全地获取边数据 + def get_safe_edge_data(node_a, node_b, key): + try: + edge_data = G.get_edge_data(node_a, node_b) + if edge_data and "port" in edge_data: + port_data = edge_data["port"] + if isinstance(port_data, dict) and key in port_data: + return port_data[key] + return "default" + except Exception as e: + debug_print(f"PUMP_TRANSFER: 获取边数据失败 {node_a}->{node_b}: {str(e)}") + return "default" + + # 从源容器吸液 + if not from_vessel.startswith("pump") and pump_backbone: + first_pump_node = pump_backbone[0] + if first_pump_node in valve_from_node and first_pump_node in pumps_from_node: + port_command = get_safe_edge_data(first_pump_node, from_vessel, first_pump_node) + pump_action_sequence.extend([ + { + "device_id": valve_from_node[first_pump_node], + "action_name": "set_valve_position", + "action_kwargs": { + "command": port_command + } + }, + { + "device_id": pumps_from_node[first_pump_node], + "action_name": "set_position", + "action_kwargs": { + "position": float(current_volume), + "max_velocity": transfer_flowrate + } } - }, - { - "device_id": pumps_from_node[pump_backbone[0]], - "action_name": "set_position", - "action_kwargs": { - "position": float(min(volume_left, min_transfer_volume)), - "max_velocity": transfer_flowrate - } - } - ]) - pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}}) + ]) + pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}}) + + # 泵间转移 for nodeA, nodeB in zip(pump_backbone[:-1], pump_backbone[1:]): - # 相邻两泵同时切换阀门至连通位置 - pump_action_sequence.append([ - { - "device_id": valve_from_node[nodeA], - "action_name": "set_valve_position", - "action_kwargs": { - "command": G.get_edge_data(nodeA, nodeB)["port"][nodeA] + if nodeA in valve_from_node and nodeB in valve_from_node and nodeA in pumps_from_node and nodeB in pumps_from_node: + port_a = get_safe_edge_data(nodeA, nodeB, nodeA) + port_b = get_safe_edge_data(nodeB, nodeA, nodeB) + + pump_action_sequence.append([ + { + "device_id": valve_from_node[nodeA], + "action_name": "set_valve_position", + "action_kwargs": { + "command": port_a + } + }, + { + "device_id": valve_from_node[nodeB], + "action_name": "set_valve_position", + "action_kwargs": { + "command": port_b + } } - }, - { - "device_id": valve_from_node[nodeB], - "action_name": "set_valve_position", - "action_kwargs": { - "command": G.get_edge_data(nodeB, nodeA)["port"][nodeB], + ]) + pump_action_sequence.append([ + { + "device_id": pumps_from_node[nodeA], + "action_name": "set_position", + "action_kwargs": { + "position": 0.0, + "max_velocity": transfer_flowrate + } + }, + { + "device_id": pumps_from_node[nodeB], + "action_name": "set_position", + "action_kwargs": { + "position": float(current_volume), + "max_velocity": transfer_flowrate + } } - } - ]) - # 相邻两泵液体转移:泵A排出液体,泵B吸入液体 - pump_action_sequence.append([ - { - "device_id": pumps_from_node[nodeA], - "action_name": "set_position", - "action_kwargs": { - "position": 0.0, - "max_velocity": transfer_flowrate - } - }, - { - "device_id": pumps_from_node[nodeB], - "action_name": "set_position", - "action_kwargs": { - "position": float(min(volume_left, min_transfer_volume)), - "max_velocity": transfer_flowrate - } - } - ]) - pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}}) + ]) + pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}}) - if not to_vessel.startswith("pump"): - # 单泵依次执行阀指令、活塞指令,将最后一台泵液体缓慢加入容器B - pump_action_sequence.extend([ - { - "device_id": valve_from_node[pump_backbone[-1]], - "action_name": "set_valve_position", - "action_kwargs": { - "command": G.get_edge_data(pump_backbone[-1], to_vessel)["port"][pump_backbone[-1]] + # 排液到目标容器 + if not to_vessel.startswith("pump") and pump_backbone: + last_pump_node = pump_backbone[-1] + if last_pump_node in valve_from_node and last_pump_node in pumps_from_node: + port_command = get_safe_edge_data(last_pump_node, to_vessel, last_pump_node) + pump_action_sequence.extend([ + { + "device_id": valve_from_node[last_pump_node], + "action_name": "set_valve_position", + "action_kwargs": { + "command": port_command + } + }, + { + "device_id": pumps_from_node[last_pump_node], + "action_name": "set_position", + "action_kwargs": { + "position": 0.0, + "max_velocity": flowrate + } } - }, - { - "device_id": pumps_from_node[pump_backbone[-1]], - "action_name": "set_position", - "action_kwargs": { - "position": 0.0, - "max_velocity": flowrate - } - } - ]) - pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}}) + ]) + pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}}) - volume_left -= min_transfer_volume + # 🆕 在每次循环结束时添加完成日志 + if repeats > 1: + remaining_volume = volume_left - current_volume + if remaining_volume > 0: + end_message = f"✅ 第 {i+1}/{repeats} 次转移完成! 剩余 {remaining_volume:.2f}mL 待转移 ⏳" + else: + end_message = f"🎉 第 {i+1}/{repeats} 次转移完成! 全部 {volume:.2f}mL 转移完毕 ✨" + + pump_action_sequence.append(create_progress_log_action(end_message)) + + volume_left -= current_volume + return pump_action_sequence -# Pump protocol compilation def generate_pump_protocol_with_rinsing( - G: nx.DiGraph, - from_vessel: str, - to_vessel: str, - volume: float, - amount: str = "", - time: float = 0, - viscous: bool = False, - rinsing_solvent: str = "air", - rinsing_volume: float = 5.0, - rinsing_repeats: int = 2, - solid: bool = False, - flowrate: float = 2.5, - transfer_flowrate: float = 0.5, -) -> list[dict]: + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + volume: float = 0.0, + amount: str = "", + time: float = 0.0, # 🔧 修复:统一使用 time + viscous: bool = False, + rinsing_solvent: str = "", + rinsing_volume: float = 0.0, + rinsing_repeats: int = 0, + solid: bool = False, + flowrate: float = 2.5, + transfer_flowrate: float = 0.5, + rate_spec: str = "", + event: str = "", + through: str = "", + **kwargs +) -> List[Dict[str, Any]]: """ - Generates a pump protocol for transferring a specified volume between vessels, including rinsing steps with a chosen solvent. This function constructs a sequence of pump actions based on the provided parameters and the shortest path in a directed graph. - - Args: - G (nx.DiGraph): The directed graph representing the vessels and connections. 有向图, 节点为容器和注射泵, 边为流体管道, A→B边的属性为管道接A端的阀门位置 - from_vessel (str): The name of the vessel to transfer from. - to_vessel (str): The name of the vessel to transfer to. - volume (float): The volume to transfer. - amount (str, optional): Additional amount specification (default is ""). - time (float, optional): Time over which to perform the transfer (default is 0). - viscous (bool, optional): Indicates if the fluid is viscous (default is False). - rinsing_solvent (str, optional): The solvent to use for rinsing (default is "air"). - rinsing_volume (float, optional): The volume of rinsing solvent to use (default is 5.0). - rinsing_repeats (int, optional): The number of times to repeat rinsing (default is 2). - solid (bool, optional): Indicates if the transfer involves a solid (default is False). - flowrate (float, optional): The flow rate for the transfer (default is 2.5). 最终注入容器B时的流速 - transfer_flowrate (float, optional): The flow rate for the transfer action (default is 0.5). 泵骨架中转移流速(若不指定,默认与注入流速相同) - - Returns: - list[dict]: A sequence of pump actions to be executed for the transfer and rinsing process. 泵操作的动作序列. - - Raises: - AssertionError: If the number of rinsing solvents does not match the number of rinsing repeats. - - Examples: - pump_protocol = generate_pump_protocol_with_rinsing(G, "vessel_A", "vessel_B", 0.1, rinsing_solvent="water") + 原有的同步版本,添加防冲突机制 """ - air_vessel = "flask_air" - waste_vessel = f"waste_workup" + + # 添加执行锁,防止并发调用 + import threading + if not hasattr(generate_pump_protocol_with_rinsing, '_lock'): + generate_pump_protocol_with_rinsing._lock = threading.Lock() + + with generate_pump_protocol_with_rinsing._lock: + debug_print("=" * 60) + debug_print(f"PUMP_TRANSFER: 🚀 开始生成协议 (同步版本)") + debug_print(f" 📍 路径: {from_vessel} -> {to_vessel}") + debug_print(f" 🕐 时间戳: {time_module.time()}") + debug_print(f" 🔒 获得执行锁") + debug_print("=" * 60) + + # 短暂延迟,避免快速重复调用 + time_module.sleep(0.01) + + debug_print("🔍 步骤1: 开始体积处理...") + + # 1. 处理体积参数 + final_volume = volume + debug_print(f"📋 初始设置: final_volume = {final_volume}") + + # 🔧 修复:如果volume为0(ROS2传入的空值),从容器读取实际体积 + if volume == 0.0: + debug_print("🎯 检测到 volume=0.0,开始自动体积检测...") + + # 直接从源容器读取实际体积 + actual_volume = get_vessel_liquid_volume(G, from_vessel) + debug_print(f"📖 从容器 '{from_vessel}' 读取到体积: {actual_volume}mL") + + if actual_volume > 0: + final_volume = actual_volume + debug_print(f"✅ 成功设置体积为: {final_volume}mL") + else: + final_volume = 10.0 # 如果读取失败,使用默认值 + logger.warning(f"⚠️ 无法从容器读取体积,使用默认值: {final_volume}mL") + else: + debug_print(f"📌 体积非零,直接使用: {final_volume}mL") + + # 处理 amount 参数 + if amount and amount.strip(): + debug_print(f"🔍 检测到 amount 参数: '{amount}',开始解析...") + parsed_volume = _parse_amount_to_volume(amount) + debug_print(f"📖 从 amount 解析得到体积: {parsed_volume}mL") + + if parsed_volume > 0: + final_volume = parsed_volume + debug_print(f"✅ 使用从 amount 解析的体积: {final_volume}mL") + elif parsed_volume == 0.0 and amount.lower().strip() == "all": + debug_print("🎯 检测到 amount='all',从容器读取全部体积...") + actual_volume = get_vessel_liquid_volume(G, from_vessel) + if actual_volume > 0: + final_volume = actual_volume + debug_print(f"✅ amount='all',设置体积为: {final_volume}mL") + + # 最终体积验证 + debug_print(f"🔍 步骤2: 最终体积验证...") + if final_volume <= 0: + logger.error(f"❌ 体积无效: {final_volume}mL") + final_volume = 10.0 + logger.warning(f"⚠️ 强制设置为默认值: {final_volume}mL") + + debug_print(f"✅ 最终确定体积: {final_volume}mL") + + # 2. 处理流速参数 + debug_print(f"🔍 步骤3: 处理流速参数...") + debug_print(f" - 原始 flowrate: {flowrate}") + debug_print(f" - 原始 transfer_flowrate: {transfer_flowrate}") + + final_flowrate = flowrate if flowrate > 0 else 2.5 + final_transfer_flowrate = transfer_flowrate if transfer_flowrate > 0 else 0.5 + + if flowrate <= 0: + logger.warning(f"⚠️ flowrate <= 0,修正为: {final_flowrate}mL/s") + if transfer_flowrate <= 0: + logger.warning(f"⚠️ transfer_flowrate <= 0,修正为: {final_transfer_flowrate}mL/s") + + debug_print(f"✅ 修正后流速: flowrate={final_flowrate}mL/s, transfer_flowrate={final_transfer_flowrate}mL/s") + + # 3. 根据时间计算流速 + if time > 0 and final_volume > 0: + debug_print(f"🔍 步骤4: 根据时间计算流速...") + calculated_flowrate = final_volume / time + debug_print(f" - 计算得到流速: {calculated_flowrate}mL/s") + + if flowrate <= 0 or flowrate == 2.5: + final_flowrate = min(calculated_flowrate, 10.0) + debug_print(f" - 调整 flowrate 为: {final_flowrate}mL/s") + if transfer_flowrate <= 0 or transfer_flowrate == 0.5: + final_transfer_flowrate = min(calculated_flowrate, 5.0) + debug_print(f" - 调整 transfer_flowrate 为: {final_transfer_flowrate}mL/s") + + # 4. 根据速度规格调整 + if rate_spec: + debug_print(f"🔍 步骤5: 根据速度规格调整...") + debug_print(f" - 速度规格: '{rate_spec}'") + + if rate_spec == "dropwise": + final_flowrate = min(final_flowrate, 0.1) + final_transfer_flowrate = min(final_transfer_flowrate, 0.1) + debug_print(f" - dropwise模式,流速调整为: {final_flowrate}mL/s") + elif rate_spec == "slowly": + final_flowrate = min(final_flowrate, 0.5) + final_transfer_flowrate = min(final_transfer_flowrate, 0.3) + debug_print(f" - slowly模式,流速调整为: {final_flowrate}mL/s") + elif rate_spec == "quickly": + final_flowrate = max(final_flowrate, 5.0) + final_transfer_flowrate = max(final_transfer_flowrate, 2.0) + debug_print(f" - quickly模式,流速调整为: {final_flowrate}mL/s") + + try: + # 🆕 修复:在这里调用带有循环日志的generate_pump_protocol_with_loop_logging函数 + pump_action_sequence = generate_pump_protocol_with_loop_logging( + G, from_vessel, to_vessel, final_volume, + final_flowrate, final_transfer_flowrate + ) + + debug_print(f"🔓 释放执行锁") + return pump_action_sequence + + except Exception as e: + logger.error(f"❌ 协议生成失败: {str(e)}") + return [ + { + "device_id": "system", + "action_name": "log_message", + "action_kwargs": { + "message": f"❌ 协议生成失败: {str(e)}" + } + } + ] - shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel) - pump_backbone = shortest_path[1: -1] + +def generate_pump_protocol_with_loop_logging( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + volume: float, + flowrate: float = 2.5, + transfer_flowrate: float = 0.5, +) -> List[Dict[str, Any]]: + """ + 生成泵操作的动作序列 - 带循环日志版本 + 🔧 修复:正确处理包含电磁阀的路径,并在合适时机打印循环日志 + """ + pump_action_sequence = [] nodes = G.nodes(data=True) + + # 验证输入参数 + if volume <= 0: + logger.error(f"无效的体积参数: {volume}mL") + return pump_action_sequence + + if flowrate <= 0: + flowrate = 2.5 + logger.warning(f"flowrate <= 0,使用默认值 {flowrate}mL/s") + + if transfer_flowrate <= 0: + transfer_flowrate = 0.5 + logger.warning(f"transfer_flowrate <= 0,使用默认值 {transfer_flowrate}mL/s") + + # 验证容器存在 + if from_vessel not in G.nodes(): + logger.error(f"源容器 '{from_vessel}' 不存在") + return pump_action_sequence + + if to_vessel not in G.nodes(): + logger.error(f"目标容器 '{to_vessel}' 不存在") + return pump_action_sequence + + try: + shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel) + debug_print(f"PUMP_TRANSFER: 路径 {from_vessel} -> {to_vessel}: {shortest_path}") + except nx.NetworkXNoPath: + logger.error(f"无法找到从 '{from_vessel}' 到 '{to_vessel}' 的路径") + return pump_action_sequence - pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone) + # 🔧 关键修复:正确构建泵骨架,排除容器和电磁阀 + pump_backbone = [] + for node in shortest_path: + # 跳过起始和结束容器 + if node == from_vessel or node == to_vessel: + continue + + # 跳过电磁阀(电磁阀不参与泵操作) + node_data = G.nodes.get(node, {}) + node_class = node_data.get("class", "") or "" + if ("solenoid" in node_class.lower() or "solenoid_valve" in node.lower()): + debug_print(f"PUMP_TRANSFER: 跳过电磁阀 {node}") + continue + + # 只包含多通阀和泵 + if ("multiway" in node_class.lower() or "valve" in node_class.lower() or "pump" in node_class.lower()): + pump_backbone.append(node) + + debug_print(f"PUMP_TRANSFER: 过滤后的泵骨架: {pump_backbone}") - min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone]) - if time != 0: - flowrate = transfer_flowrate = volume / time + if not pump_backbone: + debug_print("PUMP_TRANSFER: 没有泵骨架节点,可能是直接容器连接或只有电磁阀") + return pump_action_sequence - pump_action_sequence = generate_pump_protocol(G, from_vessel, to_vessel, float(volume), flowrate, transfer_flowrate) - if rinsing_solvent != "air" and rinsing_solvent != "": + if transfer_flowrate == 0: + transfer_flowrate = flowrate + + try: + pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone) + except Exception as e: + debug_print(f"PUMP_TRANSFER: 构建泵-阀门映射失败: {str(e)}") + return pump_action_sequence + + if not pumps_from_node: + debug_print("PUMP_TRANSFER: 没有可用的泵映射") + return pump_action_sequence + + # 🔧 修复:安全地获取最小转移体积 + try: + min_transfer_volumes = [] + for node in pump_backbone: + if node in pumps_from_node: + pump_node = pumps_from_node[node] + if pump_node in nodes: + pump_config = nodes[pump_node].get("config", {}) + max_volume = pump_config.get("max_volume") + if max_volume is not None: + min_transfer_volumes.append(max_volume) + + if min_transfer_volumes: + min_transfer_volume = min(min_transfer_volumes) + else: + min_transfer_volume = 25.0 # 默认值 + debug_print(f"PUMP_TRANSFER: 无法获取泵的最大体积,使用默认值: {min_transfer_volume}mL") + except Exception as e: + debug_print(f"PUMP_TRANSFER: 获取最小转移体积失败: {str(e)}") + min_transfer_volume = 25.0 # 默认值 + + repeats = int(np.ceil(volume / min_transfer_volume)) + + if repeats > 1 and (from_vessel.startswith("pump") or to_vessel.startswith("pump")): + logger.error("Cannot transfer volume larger than min_transfer_volume between two pumps.") + return pump_action_sequence + + volume_left = volume + debug_print(f"PUMP_TRANSFER: 需要 {repeats} 次转移,单次最大体积 {min_transfer_volume} mL") + + # 🆕 只在开头打印总体概览 + if repeats > 1: + debug_print(f"🔄 分批转移概览: 总体积 {volume:.2f}mL,需要 {repeats} 次转移") + logger.info(f"🔄 分批转移概览: 总体积 {volume:.2f}mL,需要 {repeats} 次转移") + + # 🔧 创建一个自定义的wait动作,用于在执行时打印日志 + def create_progress_log_action(message: str) -> Dict[str, Any]: + """创建一个特殊的等待动作,在执行时打印进度日志""" + return { + "action_name": "wait", + "action_kwargs": { + "time": 0.1, # 很短的等待时间 + "progress_message": message # 自定义字段,用于进度日志 + } + } + + # 生成泵操作序列 + for i in range(repeats): + current_volume = min(volume_left, min_transfer_volume) + + # 🆕 在每次循环开始时添加进度日志 + if repeats > 1: + start_message = f"🚀 准备开始第 {i+1}/{repeats} 次转移: {current_volume:.2f}mL ({from_vessel} → {to_vessel}) 🚰" + pump_action_sequence.append(create_progress_log_action(start_message)) + + # 🔧 修复:安全地获取边数据 + def get_safe_edge_data(node_a, node_b, key): + try: + edge_data = G.get_edge_data(node_a, node_b) + if edge_data and "port" in edge_data: + port_data = edge_data["port"] + if isinstance(port_data, dict) and key in port_data: + return port_data[key] + return "default" + except Exception as e: + debug_print(f"PUMP_TRANSFER: 获取边数据失败 {node_a}->{node_b}: {str(e)}") + return "default" + + # 从源容器吸液 + if not from_vessel.startswith("pump") and pump_backbone: + first_pump_node = pump_backbone[0] + if first_pump_node in valve_from_node and first_pump_node in pumps_from_node: + port_command = get_safe_edge_data(first_pump_node, from_vessel, first_pump_node) + pump_action_sequence.extend([ + { + "device_id": valve_from_node[first_pump_node], + "action_name": "set_valve_position", + "action_kwargs": { + "command": port_command + } + }, + { + "device_id": pumps_from_node[first_pump_node], + "action_name": "set_position", + "action_kwargs": { + "position": float(current_volume), + "max_velocity": transfer_flowrate + } + } + ]) + pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}}) + + # 泵间转移 + for nodeA, nodeB in zip(pump_backbone[:-1], pump_backbone[1:]): + if nodeA in valve_from_node and nodeB in valve_from_node and nodeA in pumps_from_node and nodeB in pumps_from_node: + port_a = get_safe_edge_data(nodeA, nodeB, nodeA) + port_b = get_safe_edge_data(nodeB, nodeA, nodeB) + + pump_action_sequence.append([ + { + "device_id": valve_from_node[nodeA], + "action_name": "set_valve_position", + "action_kwargs": { + "command": port_a + } + }, + { + "device_id": valve_from_node[nodeB], + "action_name": "set_valve_position", + "action_kwargs": { + "command": port_b + } + } + ]) + pump_action_sequence.append([ + { + "device_id": pumps_from_node[nodeA], + "action_name": "set_position", + "action_kwargs": { + "position": 0.0, + "max_velocity": transfer_flowrate + } + }, + { + "device_id": pumps_from_node[nodeB], + "action_name": "set_position", + "action_kwargs": { + "position": float(current_volume), + "max_velocity": transfer_flowrate + } + } + ]) + pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}}) + + # 排液到目标容器 + if not to_vessel.startswith("pump") and pump_backbone: + last_pump_node = pump_backbone[-1] + if last_pump_node in valve_from_node and last_pump_node in pumps_from_node: + port_command = get_safe_edge_data(last_pump_node, to_vessel, last_pump_node) + pump_action_sequence.extend([ + { + "device_id": valve_from_node[last_pump_node], + "action_name": "set_valve_position", + "action_kwargs": { + "command": port_command + } + }, + { + "device_id": pumps_from_node[last_pump_node], + "action_name": "set_position", + "action_kwargs": { + "position": 0.0, + "max_velocity": flowrate + } + } + ]) + pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}}) + + # 🆕 在每次循环结束时添加完成日志 + if repeats > 1: + remaining_volume = volume_left - current_volume + if remaining_volume > 0: + end_message = f"✅ 第 {i+1}/{repeats} 次转移完成! 剩余 {remaining_volume:.2f}mL 待转移 ⏳" + else: + end_message = f"🎉 第 {i+1}/{repeats} 次转移完成! 全部 {volume:.2f}mL 转移完毕 ✨" + + pump_action_sequence.append(create_progress_log_action(end_message)) + + volume_left -= current_volume + + return pump_action_sequence + + +def generate_pump_protocol_with_rinsing( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + volume: float = 0.0, + amount: str = "", + time: float = 0.0, # 🔧 修复:统一使用 time + viscous: bool = False, + rinsing_solvent: str = "", + rinsing_volume: float = 0.0, + rinsing_repeats: int = 0, + solid: bool = False, + flowrate: float = 2.5, + transfer_flowrate: float = 0.5, + rate_spec: str = "", + event: str = "", + through: str = "", + **kwargs +) -> List[Dict[str, Any]]: + """ + 增强兼容性的泵转移协议生成器,支持自动体积检测 + """ + debug_print("=" * 60) + debug_print(f"PUMP_TRANSFER: 🚀 开始生成协议") + debug_print(f" 📍 路径: {from_vessel} -> {to_vessel}") + debug_print(f" 🕐 时间戳: {time_module.time()}") + debug_print(f" 📊 原始参数:") + debug_print(f" - volume: {volume} (类型: {type(volume)})") + debug_print(f" - amount: '{amount}'") + debug_print(f" - time: {time}") # 🔧 修复:统一使用 time + debug_print(f" - flowrate: {flowrate}") + debug_print(f" - transfer_flowrate: {transfer_flowrate}") + debug_print(f" - rate_spec: '{rate_spec}'") + debug_print("=" * 60) + + # ========== 🔧 核心修复:智能体积处理 ========== + + debug_print("🔍 步骤1: 开始体积处理...") + + # 1. 处理体积参数 + final_volume = volume + debug_print(f"📋 初始设置: final_volume = {final_volume}") + + # 🔧 修复:如果volume为0(ROS2传入的空值),从容器读取实际体积 + if volume == 0.0: + debug_print("🎯 检测到 volume=0.0,开始自动体积检测...") + + # 直接从源容器读取实际体积 + actual_volume = get_vessel_liquid_volume(G, from_vessel) + debug_print(f"📖 从容器 '{from_vessel}' 读取到体积: {actual_volume}mL") + + if actual_volume > 0: + final_volume = actual_volume + debug_print(f"✅ 成功设置体积为: {final_volume}mL") + else: + final_volume = 10.0 # 如果读取失败,使用默认值 + debug_print(f"⚠️ 无法从容器读取体积,使用默认值: {final_volume}mL") + else: + debug_print(f"📌 体积非零,直接使用: {final_volume}mL") + + # 处理 amount 参数 + if amount and amount.strip(): + debug_print(f"🔍 检测到 amount 参数: '{amount}',开始解析...") + parsed_volume = _parse_amount_to_volume(amount) + debug_print(f"📖 从 amount 解析得到体积: {parsed_volume}mL") + + if parsed_volume > 0: + final_volume = parsed_volume + debug_print(f"✅ 使用从 amount 解析的体积: {final_volume}mL") + elif parsed_volume == 0.0 and amount.lower().strip() == "all": + debug_print("🎯 检测到 amount='all',从容器读取全部体积...") + actual_volume = get_vessel_liquid_volume(G, from_vessel) + if actual_volume > 0: + final_volume = actual_volume + debug_print(f"✅ amount='all',设置体积为: {final_volume}mL") + + # 最终体积验证 + debug_print(f"🔍 步骤2: 最终体积验证...") + if final_volume <= 0: + debug_print(f"❌ 体积无效: {final_volume}mL") + final_volume = 10.0 + debug_print(f"⚠️ 强制设置为默认值: {final_volume}mL") + + debug_print(f"✅ 最终确定体积: {final_volume}mL") + + # 2. 处理流速参数 + debug_print(f"🔍 步骤3: 处理流速参数...") + debug_print(f" - 原始 flowrate: {flowrate}") + debug_print(f" - 原始 transfer_flowrate: {transfer_flowrate}") + + final_flowrate = flowrate if flowrate > 0 else 2.5 + final_transfer_flowrate = transfer_flowrate if transfer_flowrate > 0 else 0.5 + + if flowrate <= 0: + debug_print(f"⚠️ flowrate <= 0,修正为: {final_flowrate}mL/s") + if transfer_flowrate <= 0: + debug_print(f"⚠️ transfer_flowrate <= 0,修正为: {final_transfer_flowrate}mL/s") + + debug_print(f"✅ 修正后流速: flowrate={final_flowrate}mL/s, transfer_flowrate={final_transfer_flowrate}mL/s") + + # 3. 根据时间计算流速 + if time > 0 and final_volume > 0: # 🔧 修复:统一使用 time + debug_print(f"🔍 步骤4: 根据时间计算流速...") + calculated_flowrate = final_volume / time + debug_print(f" - 计算得到流速: {calculated_flowrate}mL/s") + + if flowrate <= 0 or flowrate == 2.5: + final_flowrate = min(calculated_flowrate, 10.0) + debug_print(f" - 调整 flowrate 为: {final_flowrate}mL/s") + if transfer_flowrate <= 0 or transfer_flowrate == 0.5: + final_transfer_flowrate = min(calculated_flowrate, 5.0) + debug_print(f" - 调整 transfer_flowrate 为: {final_transfer_flowrate}mL/s") + + # 4. 根据速度规格调整 + if rate_spec: + debug_print(f"🔍 步骤5: 根据速度规格调整...") + debug_print(f" - 速度规格: '{rate_spec}'") + + if rate_spec == "dropwise": + final_flowrate = min(final_flowrate, 0.1) + final_transfer_flowrate = min(final_transfer_flowrate, 0.1) + debug_print(f" - dropwise模式,流速调整为: {final_flowrate}mL/s") + elif rate_spec == "slowly": + final_flowrate = min(final_flowrate, 0.5) + final_transfer_flowrate = min(final_transfer_flowrate, 0.3) + debug_print(f" - slowly模式,流速调整为: {final_flowrate}mL/s") + elif rate_spec == "quickly": + final_flowrate = max(final_flowrate, 5.0) + final_transfer_flowrate = max(final_transfer_flowrate, 2.0) + debug_print(f" - quickly模式,流速调整为: {final_flowrate}mL/s") + + # # 5. 处理冲洗参数 + # debug_print(f"🔍 步骤6: 处理冲洗参数...") + # final_rinsing_solvent = rinsing_solvent + # final_rinsing_volume = rinsing_volume if rinsing_volume > 0 else 5.0 + # final_rinsing_repeats = rinsing_repeats if rinsing_repeats > 0 else 2 + + # if rinsing_volume <= 0: + # debug_print(f"⚠️ rinsing_volume <= 0,修正为: {final_rinsing_volume}mL") + # if rinsing_repeats <= 0: + # debug_print(f"⚠️ rinsing_repeats <= 0,修正为: {final_rinsing_repeats}次") + + # # 根据物理属性调整冲洗参数 + # if viscous or solid: + # final_rinsing_repeats = max(final_rinsing_repeats, 3) + # final_rinsing_volume = max(final_rinsing_volume, 10.0) + # debug_print(f"🧪 粘稠/固体物质,调整冲洗参数:{final_rinsing_repeats}次,{final_rinsing_volume}mL") + + # 参数总结 + debug_print("📊 最终参数总结:") + debug_print(f" - 体积: {final_volume}mL") + debug_print(f" - 流速: {final_flowrate}mL/s") + debug_print(f" - 转移流速: {final_transfer_flowrate}mL/s") + # debug_print(f" - 冲洗溶剂: '{final_rinsing_solvent}'") + # debug_print(f" - 冲洗体积: {final_rinsing_volume}mL") + # debug_print(f" - 冲洗次数: {final_rinsing_repeats}次") + + # ========== 执行基础转移 ========== + + debug_print("🔧 步骤7: 开始执行基础转移...") + + try: + debug_print(f" - 调用 generate_pump_protocol...") + debug_print(f" - 参数: G, '{from_vessel}', '{to_vessel}', {final_volume}, {final_flowrate}, {final_transfer_flowrate}") + + pump_action_sequence = generate_pump_protocol( + G, from_vessel, to_vessel, final_volume, + final_flowrate, final_transfer_flowrate + ) + + debug_print(f" - generate_pump_protocol 返回结果:") + debug_print(f" - 动作序列长度: {len(pump_action_sequence)}") + debug_print(f" - 动作序列是否为空: {len(pump_action_sequence) == 0}") + + if not pump_action_sequence: + debug_print("❌ 基础转移协议生成为空,可能是路径问题") + debug_print(f" - 源容器存在: {from_vessel in G.nodes()}") + debug_print(f" - 目标容器存在: {to_vessel in G.nodes()}") + + if from_vessel in G.nodes() and to_vessel in G.nodes(): + try: + path = nx.shortest_path(G, source=from_vessel, target=to_vessel) + debug_print(f" - 路径存在: {path}") + except Exception as path_error: + debug_print(f" - 无法找到路径: {str(path_error)}") + + return [ + { + "device_id": "system", + "action_name": "log_message", + "action_kwargs": { + "message": f"⚠️ 路径问题,无法转移: {final_volume}mL 从 {from_vessel} 到 {to_vessel}" + } + } + ] + + debug_print(f"✅ 基础转移生成了 {len(pump_action_sequence)} 个动作") + + # 打印前几个动作用于调试 + if len(pump_action_sequence) > 0: + debug_print("🔍 前几个动作预览:") + for i, action in enumerate(pump_action_sequence[:3]): + debug_print(f" 动作 {i+1}: {action}") + if len(pump_action_sequence) > 3: + debug_print(f" ... 还有 {len(pump_action_sequence) - 3} 个动作") + + except Exception as e: + debug_print(f"❌ 基础转移失败: {str(e)}") + import traceback + debug_print(f"详细错误: {traceback.format_exc()}") + return [ + { + "device_id": "system", + "action_name": "log_message", + "action_kwargs": { + "message": f"❌ 转移失败: {final_volume}mL 从 {from_vessel} 到 {to_vessel}, 错误: {str(e)}" + } + } + ] + + # ========== 执行冲洗操作 ========== + + # debug_print("🔧 步骤8: 检查冲洗操作...") + + # if final_rinsing_solvent and final_rinsing_solvent.strip() and final_rinsing_repeats > 0: + # debug_print(f"🧽 开始冲洗操作,溶剂: '{final_rinsing_solvent}'") + + # try: + # if final_rinsing_solvent.strip() != "air": + # debug_print(" - 执行液体冲洗...") + # rinsing_actions = _generate_rinsing_sequence( + # G, from_vessel, to_vessel, final_rinsing_solvent, + # final_rinsing_volume, final_rinsing_repeats, + # final_flowrate, final_transfer_flowrate + # ) + # pump_action_sequence.extend(rinsing_actions) + # debug_print(f" - 添加了 {len(rinsing_actions)} 个冲洗动作") + # else: + # debug_print(" - 执行空气冲洗...") + # air_rinsing_actions = _generate_air_rinsing_sequence( + # G, from_vessel, to_vessel, final_rinsing_volume, final_rinsing_repeats, + # final_flowrate, final_transfer_flowrate + # ) + # pump_action_sequence.extend(air_rinsing_actions) + # debug_print(f" - 添加了 {len(air_rinsing_actions)} 个空气冲洗动作") + # except Exception as e: + # debug_print(f"⚠️ 冲洗操作失败: {str(e)},跳过冲洗") + # else: + # debug_print(f"⏭️ 跳过冲洗操作") + # debug_print(f" - 溶剂: '{final_rinsing_solvent}'") + # debug_print(f" - 次数: {final_rinsing_repeats}") + # debug_print(f" - 条件满足: {bool(final_rinsing_solvent and final_rinsing_solvent.strip() and final_rinsing_repeats > 0)}") + + # ========== 最终结果 ========== + + debug_print("=" * 60) + debug_print(f"🎉 PUMP_TRANSFER: 协议生成完成") + debug_print(f" 📊 总动作数: {len(pump_action_sequence)}") + debug_print(f" 📋 最终体积: {final_volume}mL") + debug_print(f" 🚀 执行路径: {from_vessel} -> {to_vessel}") + + # 最终验证 + if len(pump_action_sequence) == 0: + debug_print("🚨 协议生成结果为空!这是异常情况") + return [ + { + "device_id": "system", + "action_name": "log_message", + "action_kwargs": { + "message": f"🚨 协议生成失败: 无法生成任何动作序列" + } + } + ] + + debug_print("=" * 60) + return pump_action_sequence + + +async def generate_pump_protocol_with_rinsing_async( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + volume: float = 0.0, + amount: str = "", + time: float = 0.0, + viscous: bool = False, + rinsing_solvent: str = "", + rinsing_volume: float = 0.0, + rinsing_repeats: int = 0, + solid: bool = False, + flowrate: float = 2.5, + transfer_flowrate: float = 0.5, + rate_spec: str = "", + event: str = "", + through: str = "", + **kwargs +) -> List[Dict[str, Any]]: + """ + 异步版本的泵转移协议生成器,避免并发问题 + """ + debug_print("=" * 60) + debug_print(f"PUMP_TRANSFER: 🚀 开始生成协议 (异步版本)") + debug_print(f" 📍 路径: {from_vessel} -> {to_vessel}") + debug_print(f" 🕐 时间戳: {time_module.time()}") + debug_print("=" * 60) + + # 添加唯一标识符 + protocol_id = f"pump_transfer_{int(time_module.time() * 1000000)}" + debug_print(f"📋 协议ID: {protocol_id}") + + # 调用原有的同步版本 + result = generate_pump_protocol_with_rinsing( + G, from_vessel, to_vessel, volume, amount, time, viscous, + rinsing_solvent, rinsing_volume, rinsing_repeats, solid, + flowrate, transfer_flowrate, rate_spec, event, through, **kwargs + ) + + # 为每个动作添加唯一标识 + for i, action in enumerate(result): + if isinstance(action, dict): + action['_protocol_id'] = protocol_id + action['_action_sequence'] = i + action['_timestamp'] = time_module.time() + + debug_print(f"📊 协议 {protocol_id} 生成完成,共 {len(result)} 个动作") + return result + +# 保持原有的同步版本兼容性 +def generate_pump_protocol_with_rinsing( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + volume: float = 0.0, + amount: str = "", + time: float = 0.0, + viscous: bool = False, + rinsing_solvent: str = "", + rinsing_volume: float = 0.0, + rinsing_repeats: int = 0, + solid: bool = False, + flowrate: float = 2.5, + transfer_flowrate: float = 0.5, + rate_spec: str = "", + event: str = "", + through: str = "", + **kwargs +) -> List[Dict[str, Any]]: + """ + 原有的同步版本,添加防冲突机制 + """ + + # 添加执行锁,防止并发调用 + import threading + if not hasattr(generate_pump_protocol_with_rinsing, '_lock'): + generate_pump_protocol_with_rinsing._lock = threading.Lock() + + with generate_pump_protocol_with_rinsing._lock: + debug_print("=" * 60) + debug_print(f"PUMP_TRANSFER: 🚀 开始生成协议 (同步版本)") + debug_print(f" 📍 路径: {from_vessel} -> {to_vessel}") + debug_print(f" 🕐 时间戳: {time_module.time()}") + debug_print(f" 🔒 获得执行锁") + debug_print("=" * 60) + + # 短暂延迟,避免快速重复调用 + time_module.sleep(0.01) + + debug_print("🔍 步骤1: 开始体积处理...") + + # 1. 处理体积参数 + final_volume = volume + debug_print(f"📋 初始设置: final_volume = {final_volume}") + + # 🔧 修复:如果volume为0(ROS2传入的空值),从容器读取实际体积 + if volume == 0.0: + debug_print("🎯 检测到 volume=0.0,开始自动体积检测...") + + # 直接从源容器读取实际体积 + actual_volume = get_vessel_liquid_volume(G, from_vessel) + debug_print(f"📖 从容器 '{from_vessel}' 读取到体积: {actual_volume}mL") + + if actual_volume > 0: + final_volume = actual_volume + debug_print(f"✅ 成功设置体积为: {final_volume}mL") + else: + final_volume = 10.0 # 如果读取失败,使用默认值 + logger.warning(f"⚠️ 无法从容器读取体积,使用默认值: {final_volume}mL") + else: + debug_print(f"📌 体积非零,直接使用: {final_volume}mL") + + # 处理 amount 参数 + if amount and amount.strip(): + debug_print(f"🔍 检测到 amount 参数: '{amount}',开始解析...") + parsed_volume = _parse_amount_to_volume(amount) + debug_print(f"📖 从 amount 解析得到体积: {parsed_volume}mL") + + if parsed_volume > 0: + final_volume = parsed_volume + debug_print(f"✅ 使用从 amount 解析的体积: {final_volume}mL") + elif parsed_volume == 0.0 and amount.lower().strip() == "all": + debug_print("🎯 检测到 amount='all',从容器读取全部体积...") + actual_volume = get_vessel_liquid_volume(G, from_vessel) + if actual_volume > 0: + final_volume = actual_volume + debug_print(f"✅ amount='all',设置体积为: {final_volume}mL") + + # 最终体积验证 + debug_print(f"🔍 步骤2: 最终体积验证...") + if final_volume <= 0: + logger.error(f"❌ 体积无效: {final_volume}mL") + final_volume = 10.0 + logger.warning(f"⚠️ 强制设置为默认值: {final_volume}mL") + + debug_print(f"✅ 最终确定体积: {final_volume}mL") + + # 2. 处理流速参数 + debug_print(f"🔍 步骤3: 处理流速参数...") + debug_print(f" - 原始 flowrate: {flowrate}") + debug_print(f" - 原始 transfer_flowrate: {transfer_flowrate}") + + final_flowrate = flowrate if flowrate > 0 else 2.5 + final_transfer_flowrate = transfer_flowrate if transfer_flowrate > 0 else 0.5 + + if flowrate <= 0: + logger.warning(f"⚠️ flowrate <= 0,修正为: {final_flowrate}mL/s") + if transfer_flowrate <= 0: + logger.warning(f"⚠️ transfer_flowrate <= 0,修正为: {final_transfer_flowrate}mL/s") + + debug_print(f"✅ 修正后流速: flowrate={final_flowrate}mL/s, transfer_flowrate={final_transfer_flowrate}mL/s") + + # 3. 根据时间计算流速 + if time > 0 and final_volume > 0: + debug_print(f"🔍 步骤4: 根据时间计算流速...") + calculated_flowrate = final_volume / time + debug_print(f" - 计算得到流速: {calculated_flowrate}mL/s") + + if flowrate <= 0 or flowrate == 2.5: + final_flowrate = min(calculated_flowrate, 10.0) + debug_print(f" - 调整 flowrate 为: {final_flowrate}mL/s") + if transfer_flowrate <= 0 or transfer_flowrate == 0.5: + final_transfer_flowrate = min(calculated_flowrate, 5.0) + debug_print(f" - 调整 transfer_flowrate 为: {final_transfer_flowrate}mL/s") + + # 4. 根据速度规格调整 + if rate_spec: + debug_print(f"🔍 步骤5: 根据速度规格调整...") + debug_print(f" - 速度规格: '{rate_spec}'") + + if rate_spec == "dropwise": + final_flowrate = min(final_flowrate, 0.1) + final_transfer_flowrate = min(final_transfer_flowrate, 0.1) + debug_print(f" - dropwise模式,流速调整为: {final_flowrate}mL/s") + elif rate_spec == "slowly": + final_flowrate = min(final_flowrate, 0.5) + final_transfer_flowrate = min(final_transfer_flowrate, 0.3) + debug_print(f" - slowly模式,流速调整为: {final_flowrate}mL/s") + elif rate_spec == "quickly": + final_flowrate = max(final_flowrate, 5.0) + final_transfer_flowrate = max(final_transfer_flowrate, 2.0) + debug_print(f" - quickly模式,流速调整为: {final_flowrate}mL/s") + + # # 5. 处理冲洗参数 + # debug_print(f"🔍 步骤6: 处理冲洗参数...") + # final_rinsing_solvent = rinsing_solvent + # final_rinsing_volume = rinsing_volume if rinsing_volume > 0 else 5.0 + # final_rinsing_repeats = rinsing_repeats if rinsing_repeats > 0 else 2 + + # if rinsing_volume <= 0: + # logger.warning(f"⚠️ rinsing_volume <= 0,修正为: {final_rinsing_volume}mL") + # if rinsing_repeats <= 0: + # logger.warning(f"⚠️ rinsing_repeats <= 0,修正为: {final_rinsing_repeats}次") + + # # 根据物理属性调整冲洗参数 + # if viscous or solid: + # final_rinsing_repeats = max(final_rinsing_repeats, 3) + # final_rinsing_volume = max(final_rinsing_volume, 10.0) + # debug_print(f"🧪 粘稠/固体物质,调整冲洗参数:{final_rinsing_repeats}次,{final_rinsing_volume}mL") + + # 参数总结 + debug_print("📊 最终参数总结:") + debug_print(f" - 体积: {final_volume}mL") + debug_print(f" - 流速: {final_flowrate}mL/s") + debug_print(f" - 转移流速: {final_transfer_flowrate}mL/s") + # debug_print(f" - 冲洗溶剂: '{final_rinsing_solvent}'") + # debug_print(f" - 冲洗体积: {final_rinsing_volume}mL") + # debug_print(f" - 冲洗次数: {final_rinsing_repeats}次") + + # ========== 执行基础转移 ========== + + debug_print("🔧 步骤7: 开始执行基础转移...") + + try: + debug_print(f" - 调用 generate_pump_protocol...") + debug_print(f" - 参数: G, '{from_vessel}', '{to_vessel}', {final_volume}, {final_flowrate}, {final_transfer_flowrate}") + + pump_action_sequence = generate_pump_protocol( + G, from_vessel, to_vessel, final_volume, + final_flowrate, final_transfer_flowrate + ) + + debug_print(f" - generate_pump_protocol 返回结果:") + debug_print(f" - 动作序列长度: {len(pump_action_sequence)}") + debug_print(f" - 动作序列是否为空: {len(pump_action_sequence) == 0}") + + if not pump_action_sequence: + debug_print("❌ 基础转移协议生成为空,可能是路径问题") + debug_print(f" - 源容器存在: {from_vessel in G.nodes()}") + debug_print(f" - 目标容器存在: {to_vessel in G.nodes()}") + + if from_vessel in G.nodes() and to_vessel in G.nodes(): + try: + path = nx.shortest_path(G, source=from_vessel, target=to_vessel) + debug_print(f" - 路径存在: {path}") + except Exception as path_error: + debug_print(f" - 无法找到路径: {str(path_error)}") + + return [ + { + "device_id": "system", + "action_name": "log_message", + "action_kwargs": { + "message": f"⚠️ 路径问题,无法转移: {final_volume}mL 从 {from_vessel} 到 {to_vessel}" + } + } + ] + + debug_print(f"✅ 基础转移生成了 {len(pump_action_sequence)} 个动作") + + # 打印前几个动作用于调试 + if len(pump_action_sequence) > 0: + debug_print("🔍 前几个动作预览:") + for i, action in enumerate(pump_action_sequence[:3]): + debug_print(f" 动作 {i+1}: {action}") + if len(pump_action_sequence) > 3: + debug_print(f" ... 还有 {len(pump_action_sequence) - 3} 个动作") + + except Exception as e: + debug_print(f"❌ 基础转移失败: {str(e)}") + import traceback + debug_print(f"详细错误: {traceback.format_exc()}") + return [ + { + "device_id": "system", + "action_name": "log_message", + "action_kwargs": { + "message": f"❌ 转移失败: {final_volume}mL 从 {from_vessel} 到 {to_vessel}, 错误: {str(e)}" + } + } + ] + + # ========== 执行冲洗操作 ========== + + # debug_print("🔧 步骤8: 检查冲洗操作...") + + # if final_rinsing_solvent and final_rinsing_solvent.strip() and final_rinsing_repeats > 0: + # debug_print(f"🧽 开始冲洗操作,溶剂: '{final_rinsing_solvent}'") + + # try: + # if final_rinsing_solvent.strip() != "air": + # debug_print(" - 执行液体冲洗...") + # rinsing_actions = _generate_rinsing_sequence( + # G, from_vessel, to_vessel, final_rinsing_solvent, + # final_rinsing_volume, final_rinsing_repeats, + # final_flowrate, final_transfer_flowrate + # ) + # pump_action_sequence.extend(rinsing_actions) + # debug_print(f" - 添加了 {len(rinsing_actions)} 个冲洗动作") + # else: + # debug_print(" - 执行空气冲洗...") + # air_rinsing_actions = _generate_air_rinsing_sequence( + # G, from_vessel, to_vessel, final_rinsing_volume, final_rinsing_repeats, + # final_flowrate, final_transfer_flowrate + # ) + # pump_action_sequence.extend(air_rinsing_actions) + # debug_print(f" - 添加了 {len(air_rinsing_actions)} 个空气冲洗动作") + # except Exception as e: + # debug_print(f"⚠️ 冲洗操作失败: {str(e)},跳过冲洗") + # else: + # debug_print(f"⏭️ 跳过冲洗操作") + # debug_print(f" - 溶剂: '{final_rinsing_solvent}'") + # debug_print(f" - 次数: {final_rinsing_repeats}") + # debug_print(f" - 条件满足: {bool(final_rinsing_solvent and final_rinsing_solvent.strip() and final_rinsing_repeats > 0)}") + + # ========== 最终结果 ========== + + debug_print("=" * 60) + debug_print(f"🎉 PUMP_TRANSFER: 协议生成完成") + debug_print(f" 📊 总动作数: {len(pump_action_sequence)}") + debug_print(f" 📋 最终体积: {final_volume}mL") + debug_print(f" 🚀 执行路径: {from_vessel} -> {to_vessel}") + + # 最终验证 + if len(pump_action_sequence) == 0: + debug_print("🚨 协议生成结果为空!这是异常情况") + return [ + { + "device_id": "system", + "action_name": "log_message", + "action_kwargs": { + "message": f"🚨 协议生成失败: 无法生成任何动作序列" + } + } + ] + + debug_print("=" * 60) + return pump_action_sequence + + +async def generate_pump_protocol_with_rinsing_async( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + volume: float = 0.0, + amount: str = "", + time: float = 0.0, + viscous: bool = False, + rinsing_solvent: str = "", + rinsing_volume: float = 0.0, + rinsing_repeats: int = 0, + solid: bool = False, + flowrate: float = 2.5, + transfer_flowrate: float = 0.5, + rate_spec: str = "", + event: str = "", + through: str = "", + **kwargs +) -> List[Dict[str, Any]]: + """ + 异步版本的泵转移协议生成器,避免并发问题 + """ + debug_print("=" * 60) + debug_print(f"PUMP_TRANSFER: 🚀 开始生成协议 (异步版本)") + debug_print(f" 📍 路径: {from_vessel} -> {to_vessel}") + debug_print(f" 🕐 时间戳: {time_module.time()}") + debug_print("=" * 60) + + # 添加唯一标识符 + protocol_id = f"pump_transfer_{int(time_module.time() * 1000000)}" + debug_print(f"📋 协议ID: {protocol_id}") + + # 调用原有的同步版本 + result = generate_pump_protocol_with_rinsing( + G, from_vessel, to_vessel, volume, amount, time, viscous, + rinsing_solvent, rinsing_volume, rinsing_repeats, solid, + flowrate, transfer_flowrate, rate_spec, event, through, **kwargs + ) + + # 为每个动作添加唯一标识 + for i, action in enumerate(result): + if isinstance(action, dict): + action['_protocol_id'] = protocol_id + action['_action_sequence'] = i + action['_timestamp'] = time_module.time() + + debug_print(f"📊 协议 {protocol_id} 生成完成,共 {len(result)} 个动作") + return result + +# 保持原有的同步版本兼容性 +def generate_pump_protocol_with_rinsing( + G: nx.DiGraph, + from_vessel: str, + to_vessel: str, + volume: float = 0.0, + amount: str = "", + time: float = 0.0, + viscous: bool = False, + rinsing_solvent: str = "", + rinsing_volume: float = 0.0, + rinsing_repeats: int = 0, + solid: bool = False, + flowrate: float = 2.5, + transfer_flowrate: float = 0.5, + rate_spec: str = "", + event: str = "", + through: str = "", + **kwargs +) -> List[Dict[str, Any]]: + """ + 原有的同步版本,添加防冲突机制 + """ + + # 添加执行锁,防止并发调用 + import threading + if not hasattr(generate_pump_protocol_with_rinsing, '_lock'): + generate_pump_protocol_with_rinsing._lock = threading.Lock() + + with generate_pump_protocol_with_rinsing._lock: + debug_print("=" * 60) + debug_print(f"PUMP_TRANSFER: 🚀 开始生成协议 (同步版本)") + debug_print(f" 📍 路径: {from_vessel} -> {to_vessel}") + debug_print(f" 🕐 时间戳: {time_module.time()}") + debug_print(f" 🔒 获得执行锁") + debug_print("=" * 60) + + # 短暂延迟,避免快速重复调用 + time_module.sleep(0.01) + + debug_print("🔍 步骤1: 开始体积处理...") + + # 1. 处理体积参数 + final_volume = volume + debug_print(f"📋 初始设置: final_volume = {final_volume}") + + # 🔧 修复:如果volume为0(ROS2传入的空值),从容器读取实际体积 + if volume == 0.0: + debug_print("🎯 检测到 volume=0.0,开始自动体积检测...") + + # 直接从源容器读取实际体积 + actual_volume = get_vessel_liquid_volume(G, from_vessel) + debug_print(f"📖 从容器 '{from_vessel}' 读取到体积: {actual_volume}mL") + + if actual_volume > 0: + final_volume = actual_volume + debug_print(f"✅ 成功设置体积为: {final_volume}mL") + else: + final_volume = 10.0 # 如果读取失败,使用默认值 + logger.warning(f"⚠️ 无法从容器读取体积,使用默认值: {final_volume}mL") + else: + debug_print(f"📌 体积非零,直接使用: {final_volume}mL") + + # 处理 amount 参数 + if amount and amount.strip(): + debug_print(f"🔍 检测到 amount 参数: '{amount}',开始解析...") + parsed_volume = _parse_amount_to_volume(amount) + debug_print(f"📖 从 amount 解析得到体积: {parsed_volume}mL") + + if parsed_volume > 0: + final_volume = parsed_volume + debug_print(f"✅ 使用从 amount 解析的体积: {final_volume}mL") + elif parsed_volume == 0.0 and amount.lower().strip() == "all": + debug_print("🎯 检测到 amount='all',从容器读取全部体积...") + actual_volume = get_vessel_liquid_volume(G, from_vessel) + if actual_volume > 0: + final_volume = actual_volume + debug_print(f"✅ amount='all',设置体积为: {final_volume}mL") + + # 最终体积验证 + debug_print(f"🔍 步骤2: 最终体积验证...") + if final_volume <= 0: + logger.error(f"❌ 体积无效: {final_volume}mL") + final_volume = 10.0 + logger.warning(f"⚠️ 强制设置为默认值: {final_volume}mL") + + debug_print(f"✅ 最终确定体积: {final_volume}mL") + + # 2. 处理流速参数 + debug_print(f"🔍 步骤3: 处理流速参数...") + debug_print(f" - 原始 flowrate: {flowrate}") + debug_print(f" - 原始 transfer_flowrate: {transfer_flowrate}") + + final_flowrate = flowrate if flowrate > 0 else 2.5 + final_transfer_flowrate = transfer_flowrate if transfer_flowrate > 0 else 0.5 + + if flowrate <= 0: + logger.warning(f"⚠️ flowrate <= 0,修正为: {final_flowrate}mL/s") + if transfer_flowrate <= 0: + logger.warning(f"⚠️ transfer_flowrate <= 0,修正为: {final_transfer_flowrate}mL/s") + + debug_print(f"✅ 修正后流速: flowrate={final_flowrate}mL/s, transfer_flowrate={final_transfer_flowrate}mL/s") + + # 3. 根据时间计算流速 + if time > 0 and final_volume > 0: + debug_print(f"🔍 步骤4: 根据时间计算流速...") + calculated_flowrate = final_volume / time + debug_print(f" - 计算得到流速: {calculated_flowrate}mL/s") + + if flowrate <= 0 or flowrate == 2.5: + final_flowrate = min(calculated_flowrate, 10.0) + debug_print(f" - 调整 flowrate 为: {final_flowrate}mL/s") + if transfer_flowrate <= 0 or transfer_flowrate == 0.5: + final_transfer_flowrate = min(calculated_flowrate, 5.0) + debug_print(f" - 调整 transfer_flowrate 为: {final_transfer_flowrate}mL/s") + + # 4. 根据速度规格调整 + if rate_spec: + debug_print(f"🔍 步骤5: 根据速度规格调整...") + debug_print(f" - 速度规格: '{rate_spec}'") + + if rate_spec == "dropwise": + final_flowrate = min(final_flowrate, 0.1) + final_transfer_flowrate = min(final_transfer_flowrate, 0.1) + debug_print(f" - dropwise模式,流速调整为: {final_flowrate}mL/s") + elif rate_spec == "slowly": + final_flowrate = min(final_flowrate, 0.5) + final_transfer_flowrate = min(final_transfer_flowrate, 0.3) + debug_print(f" - slowly模式,流速调整为: {final_flowrate}mL/s") + elif rate_spec == "quickly": + final_flowrate = max(final_flowrate, 5.0) + final_transfer_flowrate = max(final_transfer_flowrate, 2.0) + debug_print(f" - quickly模式,流速调整为: {final_flowrate}mL/s") + + # # 5. 处理冲洗参数 + # debug_print(f"🔍 步骤6: 处理冲洗参数...") + # final_rinsing_solvent = rinsing_solvent + # final_rinsing_volume = rinsing_volume if rinsing_volume > 0 else 5.0 + # final_rinsing_repeats = rinsing_repeats if rinsing_repeats > 0 else 2 + + # if rinsing_volume <= 0: + # logger.warning(f"⚠️ rinsing_volume <= 0,修正为: {final_rinsing_volume}mL") + # if rinsing_repeats <= 0: + # logger.warning(f"⚠️ rinsing_repeats <= 0,修正为: {final_rinsing_repeats}次") + + # # 根据物理属性调整冲洗参数 + # if viscous or solid: + # final_rinsing_repeats = max(final_rinsing_repeats, 3) + # final_rinsing_volume = max(final_rinsing_volume, 10.0) + # debug_print(f"🧪 粘稠/固体物质,调整冲洗参数:{final_rinsing_repeats}次,{final_rinsing_volume}mL") + + try: + pump_action_sequence = generate_pump_protocol( + G, from_vessel, to_vessel, final_volume, + flowrate, transfer_flowrate + ) + + # 为每个动作添加唯一标识 + # for i, action in enumerate(pump_action_sequence): + # if isinstance(action, dict): + # action['_protocol_id'] = protocol_id + # action['_action_sequence'] = i + # elif isinstance(action, list): + # for j, sub_action in enumerate(action): + # if isinstance(sub_action, dict): + # sub_action['_protocol_id'] = protocol_id + # sub_action['_action_sequence'] = f"{i}_{j}" + # + # debug_print(f"📊 协议 {protocol_id} 生成完成,共 {len(pump_action_sequence)} 个动作") + debug_print(f"🔓 释放执行锁") + return pump_action_sequence + + except Exception as e: + logger.error(f"❌ 协议生成失败: {str(e)}") + return [ + { + "device_id": "system", + "action_name": "log_message", + "action_kwargs": { + "message": f"❌ 协议生成失败: {str(e)}" + } + } + ] + +def _parse_amount_to_volume(amount: str) -> float: + """解析 amount 字符串为体积""" + debug_print(f"🔍 解析 amount: '{amount}'") + + if not amount: + debug_print(" - amount 为空,返回 0.0") + return 0.0 + + amount = amount.lower().strip() + debug_print(f" - 处理后的 amount: '{amount}'") + + # 处理特殊关键词 + if amount == "all": + debug_print(" - 检测到 'all',返回 0.0(需要后续处理)") + return 0.0 # 返回0.0,让调用者处理 + + # 提取数字 + import re + numbers = re.findall(r'[\d.]+', amount) + debug_print(f" - 提取到的数字: {numbers}") + + if numbers: + volume = float(numbers[0]) + debug_print(f" - 基础体积: {volume}") + + # 单位转换 + if 'ml' in amount or 'milliliter' in amount: + debug_print(f" - 单位: mL,最终体积: {volume}") + return volume + elif 'l' in amount and 'ml' not in amount: + final_volume = volume * 1000 + debug_print(f" - 单位: L,最终体积: {final_volume}mL") + return final_volume + elif 'μl' in amount or 'microliter' in amount: + final_volume = volume / 1000 + debug_print(f" - 单位: μL,最终体积: {final_volume}mL") + return final_volume + else: + debug_print(f" - 无单位,假设为 mL: {volume}") + return volume + + debug_print(" - 无法解析,返回 0.0") + return 0.0 + + +def _generate_rinsing_sequence(G: nx.DiGraph, from_vessel: str, to_vessel: str, + rinsing_solvent: str, rinsing_volume: float, + rinsing_repeats: int, flowrate: float, + transfer_flowrate: float) -> List[Dict[str, Any]]: + """生成冲洗动作序列""" + rinsing_actions = [] + + try: + shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel) + pump_backbone = shortest_path[1:-1] + + if not pump_backbone: + return rinsing_actions + + nodes = G.nodes(data=True) + pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone) + min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone]) + + waste_vessel = "waste_workup" + + # 处理多种溶剂情况 if "," in rinsing_solvent: rinsing_solvents = rinsing_solvent.split(",") - assert len( - rinsing_solvents) == rinsing_repeats, "Number of rinsing solvents must match number of rinsing repeats." + if len(rinsing_solvents) != rinsing_repeats: + rinsing_solvents = [rinsing_solvent] * rinsing_repeats else: rinsing_solvents = [rinsing_solvent] * rinsing_repeats - for rinsing_solvent in rinsing_solvents: - solvent_vessel = f"flask_{rinsing_solvent}" - # 清洗泵 - pump_action_sequence.extend( - generate_pump_protocol(G, solvent_vessel, pump_backbone[0], min_transfer_volume, flowrate, - transfer_flowrate) + - generate_pump_protocol(G, pump_backbone[0], pump_backbone[-1], min_transfer_volume, flowrate, - transfer_flowrate) + - generate_pump_protocol(G, pump_backbone[-1], waste_vessel, min_transfer_volume, flowrate, - transfer_flowrate) - ) - # 如果转移的是溶液,第一种冲洗溶剂请选用溶液的溶剂,稀释泵内、转移管道内的溶液。后续冲洗溶剂不需要此操作。 - if rinsing_solvent == rinsing_solvents[0]: - pump_action_sequence.extend( - generate_pump_protocol(G, solvent_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate)) - pump_action_sequence.extend( - generate_pump_protocol(G, solvent_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate)) - pump_action_sequence.extend( - generate_pump_protocol(G, air_vessel, solvent_vessel, rinsing_volume, flowrate, transfer_flowrate)) - pump_action_sequence.extend( - generate_pump_protocol(G, air_vessel, waste_vessel, rinsing_volume, flowrate, transfer_flowrate)) - if rinsing_solvent != "": - pump_action_sequence.extend( - generate_pump_protocol(G, air_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2) - pump_action_sequence.extend( - generate_pump_protocol(G, air_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2) + for solvent in rinsing_solvents: + solvent_vessel = f"flask_{solvent.strip()}" - return pump_action_sequence -# End Protocols + # 检查溶剂容器是否存在 + if solvent_vessel not in G.nodes(): + logger.warning(f"溶剂容器 {solvent_vessel} 不存在,跳过该溶剂冲洗") + continue + + # 清洗泵系统 + rinsing_actions.extend( + generate_pump_protocol(G, solvent_vessel, pump_backbone[0], min_transfer_volume, flowrate, transfer_flowrate) + ) + + if len(pump_backbone) > 1: + rinsing_actions.extend( + generate_pump_protocol(G, pump_backbone[0], pump_backbone[-1], min_transfer_volume, flowrate, transfer_flowrate) + ) + + # 排到废液容器 + if waste_vessel in G.nodes(): + rinsing_actions.extend( + generate_pump_protocol(G, pump_backbone[-1], waste_vessel, min_transfer_volume, flowrate, transfer_flowrate) + ) + + # 第一种冲洗溶剂稀释源容器和目标容器 + if solvent == rinsing_solvents[0]: + rinsing_actions.extend( + generate_pump_protocol(G, solvent_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate) + ) + rinsing_actions.extend( + generate_pump_protocol(G, solvent_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate) + ) + + except Exception as e: + logger.error(f"生成冲洗序列失败: {str(e)}") + + return rinsing_actions + + +def _generate_air_rinsing_sequence(G: nx.DiGraph, from_vessel: str, to_vessel: str, + rinsing_volume: float, repeats: int, + flowrate: float, transfer_flowrate: float) -> List[Dict[str, Any]]: + """生成空气冲洗序列""" + air_rinsing_actions = [] + + try: + air_vessel = "flask_air" + if air_vessel not in G.nodes(): + logger.warning("空气容器 flask_air 不存在,跳过空气冲洗") + return air_rinsing_actions + + for _ in range(repeats): + # 空气冲洗源容器 + air_rinsing_actions.extend( + generate_pump_protocol(G, air_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate) + ) + + # 空气冲洗目标容器 + air_rinsing_actions.extend( + generate_pump_protocol(G, air_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate) + ) + + except Exception as e: + logger.warning(f"空气冲洗失败: {str(e)}") + + return air_rinsing_actions diff --git a/unilabos/compile/recrystallize_protocol.py b/unilabos/compile/recrystallize_protocol.py index b69d88b..569a798 100644 --- a/unilabos/compile/recrystallize_protocol.py +++ b/unilabos/compile/recrystallize_protocol.py @@ -1,8 +1,89 @@ import networkx as nx -from typing import List, Dict, Any, Tuple +import re +from typing import List, Dict, Any, Tuple, Union from .pump_protocol import generate_pump_protocol_with_rinsing +def debug_print(message): + """调试输出""" + print(f"💎 [RECRYSTALLIZE] {message}", flush=True) + + +def parse_volume_with_units(volume_input: Union[str, float, int], default_unit: str = "mL") -> float: + """ + 解析带单位的体积输入 + + Args: + volume_input: 体积输入(如 "100 mL", "2.5 L", "500", "?", 100.0) + default_unit: 默认单位(默认为毫升) + + Returns: + float: 体积(毫升) + """ + if not volume_input: + debug_print("⚠️ 体积输入为空,返回 0.0mL 📦") + return 0.0 + + # 处理数值输入 + if isinstance(volume_input, (int, float)): + result = float(volume_input) + debug_print(f"🔢 数值体积输入: {volume_input} → {result}mL(默认单位)💧") + return result + + # 处理字符串输入 + volume_str = str(volume_input).lower().strip() + debug_print(f"🔍 解析体积字符串: '{volume_str}' 📝") + + # 处理特殊值 + if volume_str in ['?', 'unknown', 'tbd', 'to be determined']: + default_volume = 50.0 # 50mL默认值 + debug_print(f"❓ 检测到未知体积,使用默认值: {default_volume}mL 🎯") + return default_volume + + # 如果是纯数字,使用默认单位 + try: + value = float(volume_str) + if default_unit.lower() in ["ml", "milliliter"]: + result = value + elif default_unit.lower() in ["l", "liter"]: + result = value * 1000.0 + elif default_unit.lower() in ["μl", "ul", "microliter"]: + result = value / 1000.0 + else: + result = value # 默认mL + debug_print(f"🔢 纯数字输入: {volume_str} → {result}mL(单位: {default_unit})📏") + return result + except ValueError: + pass + + # 移除空格并提取数字和单位 + volume_clean = re.sub(r'\s+', '', volume_str) + + # 匹配数字和单位的正则表达式 + match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean) + + if not match: + debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值: 50mL 🎯") + return 50.0 + + value = float(match.group(1)) + unit = match.group(2) or default_unit.lower() + + # 转换为毫升 + if unit in ['l', 'liter']: + volume = value * 1000.0 # L -> mL + debug_print(f"📏 升转毫升: {value}L → {volume}mL 💧") + elif unit in ['μl', 'ul', 'microliter']: + volume = value / 1000.0 # μL -> mL + debug_print(f"📏 微升转毫升: {value}μL → {volume}mL 💧") + else: # ml, milliliter 或默认 + volume = value # 已经是mL + debug_print(f"📏 毫升单位: {value}mL → {volume}mL 💧") + + debug_print(f"✅ 体积解析完成: '{volume_str}' → {volume}mL ✨") + return volume + + def parse_ratio(ratio_str: str) -> Tuple[float, float]: """ 解析比例字符串,支持多种格式 @@ -13,6 +94,8 @@ def parse_ratio(ratio_str: str) -> Tuple[float, float]: Returns: Tuple[float, float]: 比例元组 (ratio1, ratio2) """ + debug_print(f"⚖️ 开始解析比例: '{ratio_str}' 📊") + try: # 处理 "1:1", "3:7", "50:50" 等格式 if ":" in ratio_str: @@ -20,6 +103,7 @@ def parse_ratio(ratio_str: str) -> Tuple[float, float]: if len(parts) == 2: ratio1 = float(parts[0]) ratio2 = float(parts[1]) + debug_print(f"✅ 冒号格式解析成功: {ratio1}:{ratio2} 🎯") return ratio1, ratio2 # 处理 "1-1", "3-7" 等格式 @@ -28,6 +112,7 @@ def parse_ratio(ratio_str: str) -> Tuple[float, float]: if len(parts) == 2: ratio1 = float(parts[0]) ratio2 = float(parts[1]) + debug_print(f"✅ 横线格式解析成功: {ratio1}:{ratio2} 🎯") return ratio1, ratio2 # 处理 "1,1", "3,7" 等格式 @@ -36,14 +121,15 @@ def parse_ratio(ratio_str: str) -> Tuple[float, float]: if len(parts) == 2: ratio1 = float(parts[0]) ratio2 = float(parts[1]) + debug_print(f"✅ 逗号格式解析成功: {ratio1}:{ratio2} 🎯") return ratio1, ratio2 # 默认 1:1 - print(f"RECRYSTALLIZE: 无法解析比例 '{ratio_str}',使用默认比例 1:1") + debug_print(f"⚠️ 无法解析比例 '{ratio_str}',使用默认比例 1:1 🎭") return 1.0, 1.0 except ValueError: - print(f"RECRYSTALLIZE: 比例解析错误 '{ratio_str}',使用默认比例 1:1") + debug_print(f"❌ 比例解析错误 '{ratio_str}',使用默认比例 1:1 🎭") return 1.0, 1.0 @@ -58,7 +144,7 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: Returns: str: 溶剂容器ID """ - print(f"RECRYSTALLIZE: 正在查找溶剂 '{solvent}' 的容器...") + debug_print(f"🔍 正在查找溶剂 '{solvent}' 的容器... 🧪") # 构建可能的容器名称 possible_names = [ @@ -72,22 +158,27 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: f"vessel_{solvent}", ] + debug_print(f"📋 候选容器名称: {possible_names[:3]}... (共{len(possible_names)}个) 📝") + # 第一步:通过容器名称匹配 + debug_print(" 🎯 步骤1: 精确名称匹配...") for vessel_name in possible_names: if vessel_name in G.nodes(): - print(f"RECRYSTALLIZE: 通过名称匹配找到容器: {vessel_name}") + debug_print(f" 🎉 通过名称匹配找到容器: {vessel_name} ✨") return vessel_name # 第二步:通过模糊匹配 + debug_print(" 🔍 步骤2: 模糊名称匹配...") for node_id in G.nodes(): if G.nodes[node_id].get('type') == 'container': node_name = G.nodes[node_id].get('name', '').lower() if solvent.lower() in node_id.lower() or solvent.lower() in node_name: - print(f"RECRYSTALLIZE: 通过模糊匹配找到容器: {node_id}") + debug_print(f" 🎉 通过模糊匹配找到容器: {node_id} ✨") return node_id # 第三步:通过液体类型匹配 + debug_print(" 🧪 步骤3: 液体类型匹配...") for node_id in G.nodes(): if G.nodes[node_id].get('type') == 'container': vessel_data = G.nodes[node_id].get('data', {}) @@ -99,9 +190,10 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: reagent_name = vessel_data.get('reagent_name', '').lower() if solvent.lower() in liquid_type or solvent.lower() in reagent_name: - print(f"RECRYSTALLIZE: 通过液体类型匹配找到容器: {node_id}") + debug_print(f" 🎉 通过液体类型匹配找到容器: {node_id} ✨") return node_id + debug_print(f"❌ 找不到溶剂 '{solvent}' 对应的容器 😭") raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器") @@ -111,11 +203,11 @@ def generate_recrystallize_protocol( solvent1: str, solvent2: str, vessel: str, - volume: float, - **kwargs # 接收其他可能的参数但不使用 + volume: Union[str, float], # 🔧 修改:支持字符串和数值 + **kwargs ) -> List[Dict[str, Any]]: """ - 生成重结晶协议序列 + 生成重结晶协议序列 - 支持单位 Args: G: 有向图,节点为容器和设备 @@ -123,72 +215,95 @@ def generate_recrystallize_protocol( solvent1: 第一种溶剂名称 solvent2: 第二种溶剂名称 vessel: 目标容器 - volume: 总体积 (mL) - **kwargs: 其他可选参数,但不使用 + volume: 总体积(支持 "100 mL", "50", "2.5 L" 等) + **kwargs: 其他可选参数 Returns: List[Dict[str, Any]]: 动作序列 """ action_sequence = [] - print(f"RECRYSTALLIZE: 开始生成重结晶协议") - print(f" - 比例: {ratio}") - print(f" - 溶剂1: {solvent1}") - print(f" - 溶剂2: {solvent2}") - print(f" - 容器: {vessel}") - print(f" - 总体积: {volume} mL") + debug_print("💎" * 20) + debug_print("🚀 开始生成重结晶协议(支持单位)✨") + debug_print(f"📝 输入参数:") + debug_print(f" ⚖️ 比例: {ratio}") + debug_print(f" 🧪 溶剂1: {solvent1}") + debug_print(f" 🧪 溶剂2: {solvent2}") + debug_print(f" 🥽 容器: {vessel}") + debug_print(f" 💧 总体积: {volume} (类型: {type(volume)})") + debug_print("💎" * 20) # 1. 验证目标容器存在 + debug_print("📍 步骤1: 验证目标容器... 🔧") if vessel not in G.nodes(): + debug_print(f"❌ 目标容器 '{vessel}' 不存在于系统中! 😱") raise ValueError(f"目标容器 '{vessel}' 不存在于系统中") + debug_print(f"✅ 目标容器 '{vessel}' 验证通过 🎯") - # 2. 解析比例 + # 2. 🔧 新增:解析体积(支持单位) + debug_print("📍 步骤2: 解析体积(支持单位)... 💧") + final_volume = parse_volume_with_units(volume, "mL") + debug_print(f"🎯 体积解析完成: {volume} → {final_volume}mL ✨") + + # 3. 解析比例 + debug_print("📍 步骤3: 解析比例... ⚖️") ratio1, ratio2 = parse_ratio(ratio) total_ratio = ratio1 + ratio2 + debug_print(f"🎯 比例解析完成: {ratio1}:{ratio2} (总比例: {total_ratio}) ✨") - # 3. 计算各溶剂体积 - volume1 = volume * (ratio1 / total_ratio) - volume2 = volume * (ratio2 / total_ratio) + # 4. 计算各溶剂体积 + debug_print("📍 步骤4: 计算各溶剂体积... 🧮") + volume1 = final_volume * (ratio1 / total_ratio) + volume2 = final_volume * (ratio2 / total_ratio) - print(f"RECRYSTALLIZE: 解析比例: {ratio1}:{ratio2}") - print(f"RECRYSTALLIZE: {solvent1} 体积: {volume1:.2f} mL") - print(f"RECRYSTALLIZE: {solvent2} 体积: {volume2:.2f} mL") + debug_print(f"🧪 {solvent1} 体积: {volume1:.2f} mL ({ratio1}/{total_ratio} × {final_volume})") + debug_print(f"🧪 {solvent2} 体积: {volume2:.2f} mL ({ratio2}/{total_ratio} × {final_volume})") + debug_print(f"✅ 体积计算完成: 总计 {volume1 + volume2:.2f} mL 🎯") - # 4. 查找溶剂容器 + # 5. 查找溶剂容器 + debug_print("📍 步骤5: 查找溶剂容器... 🔍") try: + debug_print(f" 🔍 查找溶剂1容器...") solvent1_vessel = find_solvent_vessel(G, solvent1) - print(f"RECRYSTALLIZE: 找到溶剂1容器: {solvent1_vessel}") + debug_print(f" 🎉 找到溶剂1容器: {solvent1_vessel} ✨") except ValueError as e: + debug_print(f" ❌ 溶剂1容器查找失败: {str(e)} 😭") raise ValueError(f"无法找到溶剂1 '{solvent1}': {str(e)}") try: + debug_print(f" 🔍 查找溶剂2容器...") solvent2_vessel = find_solvent_vessel(G, solvent2) - print(f"RECRYSTALLIZE: 找到溶剂2容器: {solvent2_vessel}") + debug_print(f" 🎉 找到溶剂2容器: {solvent2_vessel} ✨") except ValueError as e: + debug_print(f" ❌ 溶剂2容器查找失败: {str(e)} 😭") raise ValueError(f"无法找到溶剂2 '{solvent2}': {str(e)}") - # 5. 验证路径存在 + # 6. 验证路径存在 + debug_print("📍 步骤6: 验证传输路径... 🛤️") try: path1 = nx.shortest_path(G, source=solvent1_vessel, target=vessel) - print(f"RECRYSTALLIZE: 溶剂1路径: {' → '.join(path1)}") + debug_print(f" 🛤️ 溶剂1路径: {' → '.join(path1)} ✅") except nx.NetworkXNoPath: + debug_print(f" ❌ 溶剂1路径不可达: {solvent1_vessel} → {vessel} 😞") raise ValueError(f"从溶剂1容器 '{solvent1_vessel}' 到目标容器 '{vessel}' 没有可用路径") try: path2 = nx.shortest_path(G, source=solvent2_vessel, target=vessel) - print(f"RECRYSTALLIZE: 溶剂2路径: {' → '.join(path2)}") + debug_print(f" 🛤️ 溶剂2路径: {' → '.join(path2)} ✅") except nx.NetworkXNoPath: + debug_print(f" ❌ 溶剂2路径不可达: {solvent2_vessel} → {vessel} 😞") raise ValueError(f"从溶剂2容器 '{solvent2_vessel}' 到目标容器 '{vessel}' 没有可用路径") - # 6. 添加第一种溶剂 - print(f"RECRYSTALLIZE: 开始添加溶剂1 {volume1:.2f} mL") + # 7. 添加第一种溶剂 + debug_print("📍 步骤7: 添加第一种溶剂... 🧪") + debug_print(f" 🚰 开始添加溶剂1: {solvent1} ({volume1:.2f} mL)") try: pump_actions1 = generate_pump_protocol_with_rinsing( G=G, from_vessel=solvent1_vessel, to_vessel=vessel, - volume=volume1, + volume=volume1, # 使用解析后的体积 amount="", time=0.0, viscous=False, @@ -201,28 +316,33 @@ def generate_recrystallize_protocol( ) action_sequence.extend(pump_actions1) + debug_print(f" ✅ 溶剂1泵送动作已添加: {len(pump_actions1)} 个动作 🚰✨") except Exception as e: + debug_print(f" ❌ 溶剂1泵协议生成失败: {str(e)} 😭") raise ValueError(f"生成溶剂1泵协议时出错: {str(e)}") - # 7. 等待溶剂1稳定 + # 8. 等待溶剂1稳定 + debug_print(" ⏳ 添加溶剂1稳定等待...") action_sequence.append({ "action_name": "wait", "action_kwargs": { - "time": 10.0, + "time": 5.0, # 🕐 缩短等待时间:10.0s → 5.0s "description": f"等待溶剂1 {solvent1} 稳定" } }) + debug_print(" ✅ 溶剂1稳定等待已添加 ⏰✨") - # 8. 添加第二种溶剂 - print(f"RECRYSTALLIZE: 开始添加溶剂2 {volume2:.2f} mL") + # 9. 添加第二种溶剂 + debug_print("📍 步骤8: 添加第二种溶剂... 🧪") + debug_print(f" 🚰 开始添加溶剂2: {solvent2} ({volume2:.2f} mL)") try: pump_actions2 = generate_pump_protocol_with_rinsing( G=G, from_vessel=solvent2_vessel, to_vessel=vessel, - volume=volume2, + volume=volume2, # 使用解析后的体积 amount="", time=0.0, viscous=False, @@ -235,30 +355,63 @@ def generate_recrystallize_protocol( ) action_sequence.extend(pump_actions2) + debug_print(f" ✅ 溶剂2泵送动作已添加: {len(pump_actions2)} 个动作 🚰✨") except Exception as e: + debug_print(f" ❌ 溶剂2泵协议生成失败: {str(e)} 😭") raise ValueError(f"生成溶剂2泵协议时出错: {str(e)}") - # 9. 等待溶剂2稳定 + # 10. 等待溶剂2稳定 + debug_print(" ⏳ 添加溶剂2稳定等待...") action_sequence.append({ "action_name": "wait", "action_kwargs": { - "time": 10.0, + "time": 5.0, # 🕐 缩短等待时间:10.0s → 5.0s "description": f"等待溶剂2 {solvent2} 稳定" } }) + debug_print(" ✅ 溶剂2稳定等待已添加 ⏰✨") + + # 11. 等待重结晶完成 + debug_print("📍 步骤9: 等待重结晶完成... 💎") + + # 🕐 模拟运行时间优化 + debug_print(" ⏱️ 检查模拟运行时间限制...") + original_crystallize_time = 600.0 # 原始重结晶时间 + simulation_time_limit = 60.0 # 模拟运行时间限制:60秒 + + final_crystallize_time = min(original_crystallize_time, simulation_time_limit) + + if original_crystallize_time > simulation_time_limit: + debug_print(f" 🎮 模拟运行优化: {original_crystallize_time}s → {final_crystallize_time}s ⚡") + debug_print(f" 📊 时间缩短: {original_crystallize_time/60:.1f}分钟 → {final_crystallize_time/60:.1f}分钟 🚀") + else: + debug_print(f" ✅ 时间在限制内: {final_crystallize_time}s 保持不变 🎯") - # 10. 等待重结晶完成 action_sequence.append({ "action_name": "wait", "action_kwargs": { - "time": 600.0, # 等待10分钟进行重结晶 - "description": f"等待重结晶完成({solvent1}:{solvent2} = {ratio})" + "time": final_crystallize_time, + "description": f"等待重结晶完成({solvent1}:{solvent2} = {ratio},总体积 {final_volume}mL)" + (f" (模拟时间)" if original_crystallize_time != final_crystallize_time else "") } }) + debug_print(f" ✅ 重结晶等待已添加: {final_crystallize_time}s 💎✨") - print(f"RECRYSTALLIZE: 协议生成完成,共 {len(action_sequence)} 个动作") - print(f"RECRYSTALLIZE: 预计总时间: {620/60:.1f} 分钟") + # 显示时间调整信息 + if original_crystallize_time != final_crystallize_time: + debug_print(f" 🎭 模拟优化说明: 原计划 {original_crystallize_time/60:.1f}分钟,实际模拟 {final_crystallize_time/60:.1f}分钟 ⚡") + + # 🎊 总结 + debug_print("💎" * 20) + debug_print(f"🎉 重结晶协议生成完成! ✨") + debug_print(f"📊 总动作数: {len(action_sequence)} 个") + debug_print(f"🥽 目标容器: {vessel}") + debug_print(f"💧 总体积: {final_volume}mL") + debug_print(f"⚖️ 溶剂比例: {solvent1}:{solvent2} = {ratio1}:{ratio2}") + debug_print(f"🧪 溶剂1: {solvent1} ({volume1:.2f}mL)") + debug_print(f"🧪 溶剂2: {solvent2} ({volume2:.2f}mL)") + debug_print(f"⏱️ 预计总时间: {(final_crystallize_time + 10)/60:.1f} 分钟 ⌛") + debug_print("💎" * 20) return action_sequence @@ -266,15 +419,16 @@ def generate_recrystallize_protocol( # 测试函数 def test_recrystallize_protocol(): """测试重结晶协议""" - print("=== RECRYSTALLIZE PROTOCOL 测试 ===") + debug_print("🧪 === RECRYSTALLIZE PROTOCOL 测试 === ✨") # 测试比例解析 + debug_print("⚖️ 测试比例解析...") test_ratios = ["1:1", "3:7", "50:50", "1-1", "2,8", "invalid"] for ratio in test_ratios: r1, r2 = parse_ratio(ratio) - print(f"比例 '{ratio}' -> {r1}:{r2}") + debug_print(f" 📊 比例 '{ratio}' -> {r1}:{r2}") - print("测试完成") + debug_print("✅ 测试完成 🎉") if __name__ == "__main__": diff --git a/unilabos/compile/reset_handling_protocol.py b/unilabos/compile/reset_handling_protocol.py index 0fa55c2..2e51da3 100644 --- a/unilabos/compile/reset_handling_protocol.py +++ b/unilabos/compile/reset_handling_protocol.py @@ -3,6 +3,11 @@ from typing import List, Dict, Any from .pump_protocol import generate_pump_protocol_with_rinsing +def debug_print(message): + """调试输出""" + print(f"🔄 [RESET_HANDLING] {message}", flush=True) + + def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: """ 查找溶剂容器,支持多种匹配模式 @@ -14,7 +19,7 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: Returns: str: 溶剂容器ID """ - print(f"RESET_HANDLING: 正在查找溶剂 '{solvent}' 的容器...") + debug_print(f"🔍 正在查找溶剂 '{solvent}' 的容器... 🧪") # 构建可能的容器名称 possible_names = [ @@ -28,23 +33,30 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: f"vessel_{solvent}", # vessel_methanol ] + debug_print(f"📋 候选容器名称: {possible_names[:3]}... (共{len(possible_names)}个) 📝") + # 第一步:通过容器名称匹配 + debug_print(" 🎯 步骤1: 精确名称匹配...") for vessel_name in possible_names: if vessel_name in G.nodes(): - print(f"RESET_HANDLING: 通过名称匹配找到容器: {vessel_name}") + debug_print(f" 🎉 通过名称匹配找到容器: {vessel_name} ✨") return vessel_name + debug_print(" 😞 精确名称匹配失败,尝试模糊匹配... 🔍") # 第二步:通过模糊匹配 + debug_print(" 🔍 步骤2: 模糊名称匹配...") for node_id in G.nodes(): if G.nodes[node_id].get('type') == 'container': node_name = G.nodes[node_id].get('name', '').lower() # 检查是否包含溶剂名称 if solvent.lower() in node_id.lower() or solvent.lower() in node_name: - print(f"RESET_HANDLING: 通过模糊匹配找到容器: {node_id}") + debug_print(f" 🎉 通过模糊匹配找到容器: {node_id} ✨") return node_id + debug_print(" 😞 模糊匹配失败,尝试液体类型匹配... 🧪") # 第三步:通过液体类型匹配 + debug_print(" 🧪 步骤3: 液体类型匹配...") for node_id in G.nodes(): if G.nodes[node_id].get('type') == 'container': vessel_data = G.nodes[node_id].get('data', {}) @@ -56,10 +68,11 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: reagent_name = vessel_data.get('reagent_name', '').lower() if solvent.lower() in liquid_type or solvent.lower() in reagent_name: - print(f"RESET_HANDLING: 通过液体类型匹配找到容器: {node_id}") + debug_print(f" 🎉 通过液体类型匹配找到容器: {node_id} ✨") return node_id # 列出可用容器帮助调试 + debug_print(" 📊 显示可用容器信息...") available_containers = [] for node_id in G.nodes(): if G.nodes[node_id].get('type') == 'container': @@ -75,13 +88,17 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: 'reagent_name': vessel_data.get('reagent_name', '') }) - print(f"RESET_HANDLING: 可用容器列表:") - for container in available_containers: - print(f" - {container['id']}: {container['name']}") - print(f" 液体: {container['liquids']}") - print(f" 试剂: {container['reagent_name']}") + debug_print(f" 📋 可用容器列表 (共{len(available_containers)}个):") + for i, container in enumerate(available_containers[:5]): # 只显示前5个 + debug_print(f" {i+1}. 🥽 {container['id']}: {container['name']}") + debug_print(f" 💧 液体: {container['liquids']}") + debug_print(f" 🧪 试剂: {container['reagent_name']}") - raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器。尝试了: {possible_names}") + if len(available_containers) > 5: + debug_print(f" ... 还有 {len(available_containers)-5} 个容器 📦") + + debug_print(f"❌ 找不到溶剂 '{solvent}' 对应的容器 😭") + raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器。尝试了: {possible_names[:3]}...") def generate_reset_handling_protocol( @@ -104,35 +121,49 @@ def generate_reset_handling_protocol( # 固定参数 target_vessel = "main_reactor" # 默认目标容器 - volume = 100.0 # 默认体积 100 mL - - print(f"RESET_HANDLING: 开始生成重置处理协议") - print(f" - 溶剂: {solvent}") - print(f" - 目标容器: {target_vessel}") - print(f" - 体积: {volume} mL") + volume = 50.0 # 默认体积 50 mL + + debug_print("🔄" * 20) + debug_print("🚀 开始生成重置处理协议 ✨") + debug_print(f"📝 输入参数:") + debug_print(f" 🧪 溶剂: {solvent}") + debug_print(f" 🥽 目标容器: {target_vessel}") + debug_print(f" 💧 体积: {volume} mL") + debug_print(f" ⚙️ 其他参数: {kwargs}") + debug_print("🔄" * 20) # 1. 验证目标容器存在 + debug_print("📍 步骤1: 验证目标容器... 🔧") if target_vessel not in G.nodes(): + debug_print(f"❌ 目标容器 '{target_vessel}' 不存在于系统中! 😱") raise ValueError(f"目标容器 '{target_vessel}' 不存在于系统中") + debug_print(f"✅ 目标容器 '{target_vessel}' 验证通过 🎯") # 2. 查找溶剂容器 + debug_print("📍 步骤2: 查找溶剂容器... 🔍") try: solvent_vessel = find_solvent_vessel(G, solvent) - print(f"RESET_HANDLING: 找到溶剂容器: {solvent_vessel}") + debug_print(f"🎉 找到溶剂容器: {solvent_vessel} ✨") except ValueError as e: + debug_print(f"❌ 溶剂容器查找失败: {str(e)} 😭") raise ValueError(f"无法找到溶剂 '{solvent}': {str(e)}") # 3. 验证路径存在 + debug_print("📍 步骤3: 验证传输路径... 🛤️") try: path = nx.shortest_path(G, source=solvent_vessel, target=target_vessel) - print(f"RESET_HANDLING: 找到路径: {' → '.join(path)}") + debug_print(f"🛤️ 找到路径: {' → '.join(path)} ✅") except nx.NetworkXNoPath: + debug_print(f"❌ 路径不可达: {solvent_vessel} → {target_vessel} 😞") raise ValueError(f"从溶剂容器 '{solvent_vessel}' 到目标容器 '{target_vessel}' 没有可用路径") # 4. 使用pump_protocol转移溶剂 - print(f"RESET_HANDLING: 开始转移溶剂 {volume} mL") + debug_print("📍 步骤4: 转移溶剂... 🚰") + debug_print(f" 🚛 开始转移: {solvent_vessel} → {target_vessel}") + debug_print(f" 💧 转移体积: {volume} mL") try: + debug_print(" 🔄 生成泵送协议...") pump_actions = generate_pump_protocol_with_rinsing( G=G, from_vessel=solvent_vessel, @@ -150,21 +181,52 @@ def generate_reset_handling_protocol( ) action_sequence.extend(pump_actions) + debug_print(f" ✅ 泵送协议已添加: {len(pump_actions)} 个动作 🚰✨") except Exception as e: + debug_print(f" ❌ 泵送协议生成失败: {str(e)} 😭") raise ValueError(f"生成泵协议时出错: {str(e)}") # 5. 等待溶剂稳定 + debug_print("📍 步骤5: 等待溶剂稳定... ⏳") + + # 🕐 模拟运行时间优化 + debug_print(" ⏱️ 检查模拟运行时间限制...") + original_wait_time = 10.0 # 原始等待时间 + simulation_time_limit = 5.0 # 模拟运行时间限制:5秒 + + final_wait_time = min(original_wait_time, simulation_time_limit) + + if original_wait_time > simulation_time_limit: + debug_print(f" 🎮 模拟运行优化: {original_wait_time}s → {final_wait_time}s ⚡") + debug_print(f" 📊 时间缩短: {original_wait_time}s → {final_wait_time}s 🚀") + else: + debug_print(f" ✅ 时间在限制内: {final_wait_time}s 保持不变 🎯") + action_sequence.append({ "action_name": "wait", "action_kwargs": { - "time": 10.0, - "description": f"等待溶剂 {solvent} 稳定" + "time": final_wait_time, + "description": f"等待溶剂 {solvent} 稳定" + (f" (模拟时间)" if original_wait_time != final_wait_time else "") } }) + debug_print(f" ✅ 稳定等待已添加: {final_wait_time}s ⏰✨") - print(f"RESET_HANDLING: 协议生成完成,共 {len(action_sequence)} 个动作") - print(f"RESET_HANDLING: 已添加 {volume} mL {solvent} 到 {target_vessel}") + # 显示时间调整信息 + if original_wait_time != final_wait_time: + debug_print(f" 🎭 模拟优化说明: 原计划 {original_wait_time}s,实际模拟 {final_wait_time}s ⚡") + + # 🎊 总结 + debug_print("🔄" * 20) + debug_print(f"🎉 重置处理协议生成完成! ✨") + debug_print(f"📊 总动作数: {len(action_sequence)} 个") + debug_print(f"🧪 溶剂: {solvent}") + debug_print(f"🥽 源容器: {solvent_vessel}") + debug_print(f"🥽 目标容器: {target_vessel}") + debug_print(f"💧 转移体积: {volume} mL") + debug_print(f"⏱️ 预计总时间: {(final_wait_time + 5):.0f} 秒 ⌛") + debug_print(f"🎯 已添加 {volume} mL {solvent} 到 {target_vessel} 🚰✨") + debug_print("🔄" * 20) return action_sequence @@ -172,8 +234,15 @@ def generate_reset_handling_protocol( # 测试函数 def test_reset_handling_protocol(): """测试重置处理协议""" - print("=== RESET HANDLING PROTOCOL 测试 ===") - print("测试完成") + debug_print("🧪 === RESET HANDLING PROTOCOL 测试 === ✨") + + # 测试溶剂名称 + debug_print("🧪 测试常用溶剂名称...") + test_solvents = ["methanol", "ethanol", "water", "acetone", "dmso"] + for solvent in test_solvents: + debug_print(f" 🔍 测试溶剂: {solvent}") + + debug_print("✅ 测试完成 🎉") if __name__ == "__main__": diff --git a/unilabos/compile/run_column_protocol.py b/unilabos/compile/run_column_protocol.py index f6b9214..cb55f86 100644 --- a/unilabos/compile/run_column_protocol.py +++ b/unilabos/compile/run_column_protocol.py @@ -1,312 +1,646 @@ -from typing import List, Dict, Any +from typing import List, Dict, Any, Union import networkx as nx -from .pump_protocol import generate_pump_protocol +import logging +import re +from .pump_protocol import generate_pump_protocol_with_rinsing +logger = logging.getLogger(__name__) -def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float: - """获取容器中的液体体积""" - if vessel not in G.nodes(): +def debug_print(message): + """调试输出""" + print(f"[RUN_COLUMN] {message}", flush=True) + logger.info(f"[RUN_COLUMN] {message}") + +def parse_percentage(pct_str: str) -> float: + """ + 解析百分比字符串为数值 + + Args: + pct_str: 百分比字符串(如 "40 %", "40%", "40") + + Returns: + float: 百分比数值(0-100) + """ + if not pct_str or not pct_str.strip(): return 0.0 - vessel_data = G.nodes[vessel].get('data', {}) - liquids = vessel_data.get('liquid', []) + pct_str = pct_str.strip().lower() + debug_print(f"解析百分比: '{pct_str}'") - total_volume = 0.0 - for liquid in liquids: - if isinstance(liquid, dict): - # 支持两种格式:新格式 (name, volume) 和旧格式 (liquid_type, liquid_volume) - volume = liquid.get('volume') or liquid.get('liquid_volume', 0.0) - total_volume += volume + # 移除百分号和空格 + pct_clean = re.sub(r'[%\s]', '', pct_str) - return total_volume + # 提取数字 + match = re.search(r'([0-9]*\.?[0-9]+)', pct_clean) + if match: + value = float(match.group(1)) + debug_print(f"百分比解析结果: {value}%") + return value + + debug_print(f"⚠️ 无法解析百分比: '{pct_str}',返回0.0") + return 0.0 +def parse_ratio(ratio_str: str) -> tuple: + """ + 解析比例字符串为两个数值 + + Args: + ratio_str: 比例字符串(如 "5:95", "1:1", "40:60") + + Returns: + tuple: (ratio1, ratio2) 两个比例值 + """ + if not ratio_str or not ratio_str.strip(): + return (50.0, 50.0) # 默认1:1 + + ratio_str = ratio_str.strip() + debug_print(f"解析比例: '{ratio_str}'") + + # 支持多种分隔符:: / - + if ':' in ratio_str: + parts = ratio_str.split(':') + elif '/' in ratio_str: + parts = ratio_str.split('/') + elif '-' in ratio_str: + parts = ratio_str.split('-') + elif 'to' in ratio_str.lower(): + parts = ratio_str.lower().split('to') + else: + debug_print(f"⚠️ 无法解析比例格式: '{ratio_str}',使用默认1:1") + return (50.0, 50.0) + + if len(parts) >= 2: + try: + ratio1 = float(parts[0].strip()) + ratio2 = float(parts[1].strip()) + total = ratio1 + ratio2 + + # 转换为百分比 + pct1 = (ratio1 / total) * 100 + pct2 = (ratio2 / total) * 100 + + debug_print(f"比例解析结果: {ratio1}:{ratio2} -> {pct1:.1f}%:{pct2:.1f}%") + return (pct1, pct2) + except ValueError as e: + debug_print(f"⚠️ 比例数值转换失败: {str(e)}") + + debug_print(f"⚠️ 比例解析失败,使用默认1:1") + return (50.0, 50.0) -def find_column_device(G: nx.DiGraph, column: str) -> str: +def parse_rf_value(rf_str: str) -> float: + """ + 解析Rf值字符串 + + Args: + rf_str: Rf值字符串(如 "0.3", "0.45", "?") + + Returns: + float: Rf值(0-1) + """ + if not rf_str or not rf_str.strip(): + return 0.3 # 默认Rf值 + + rf_str = rf_str.strip().lower() + debug_print(f"解析Rf值: '{rf_str}'") + + # 处理未知Rf值 + if rf_str in ['?', 'unknown', 'tbd', 'to be determined']: + default_rf = 0.3 + debug_print(f"检测到未知Rf值,使用默认值: {default_rf}") + return default_rf + + # 提取数字 + match = re.search(r'([0-9]*\.?[0-9]+)', rf_str) + if match: + value = float(match.group(1)) + # 确保Rf值在0-1范围内 + if value > 1.0: + value = value / 100.0 # 可能是百分比形式 + value = max(0.0, min(1.0, value)) # 限制在0-1范围 + debug_print(f"Rf值解析结果: {value}") + return value + + debug_print(f"⚠️ 无法解析Rf值: '{rf_str}',使用默认值0.3") + return 0.3 + +def find_column_device(G: nx.DiGraph) -> str: """查找柱层析设备""" - # 首先检查是否有虚拟柱设备 - column_nodes = [node for node in G.nodes() - if (G.nodes[node].get('class') or '') == 'virtual_column'] + debug_print("查找柱层析设备...") - if column_nodes: - return column_nodes[0] + # 查找虚拟柱设备 + for node in G.nodes(): + node_data = G.nodes[node] + node_class = node_data.get('class', '') or '' + + if 'virtual_column' in node_class.lower() or 'column' in node_class.lower(): + debug_print(f"✅ 找到柱层析设备: {node}") + return node - # 如果没有虚拟柱设备,抛出异常 - raise ValueError(f"系统中未找到柱层析设备。请确保配置了 virtual_column 设备") - + # 如果没有找到,尝试创建虚拟设备名称 + possible_names = ['column_1', 'virtual_column_1', 'chromatography_column_1'] + for name in possible_names: + if name in G.nodes(): + debug_print(f"✅ 找到柱设备: {name}") + return name + + debug_print("⚠️ 未找到柱层析设备,将使用pump protocol直接转移") + return "" def find_column_vessel(G: nx.DiGraph, column: str) -> str: """查找柱容器""" - # 直接使用 column 参数作为容器名称 - if column in G.nodes(): - return column + debug_print(f"查找柱容器: '{column}'") - # 尝试常见的柱容器命名规则 + # 直接检查column参数是否是容器 + if column in G.nodes(): + node_type = G.nodes[column].get('type', '') + if node_type == 'container': + debug_print(f"✅ 找到柱容器: {column}") + return column + + # 尝试常见的命名规则 possible_names = [ f"column_{column}", - f"{column}_column", + f"{column}_column", f"vessel_{column}", f"{column}_vessel", "column_vessel", "chromatography_column", "silica_column", - "preparative_column" + "preparative_column", + "column" ] for vessel_name in possible_names: if vessel_name in G.nodes(): - return vessel_name + node_type = G.nodes[vessel_name].get('type', '') + if node_type == 'container': + debug_print(f"✅ 找到柱容器: {vessel_name}") + return vessel_name - raise ValueError(f"未找到柱容器 '{column}'。尝试了以下名称: {[column] + possible_names}") + debug_print(f"⚠️ 未找到柱容器,将直接在源容器中进行分离") + return "" - -def find_eluting_solvent_vessel(G: nx.DiGraph, eluting_solvent: str) -> str: - """查找洗脱溶剂容器""" - if not eluting_solvent: +def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: + """查找溶剂容器 - 增强版""" + if not solvent or not solvent.strip(): return "" - # 按照命名规则查找溶剂瓶 - solvent_vessel_id = f"flask_{eluting_solvent}" + solvent = solvent.strip().replace(' ', '_').lower() + debug_print(f"查找溶剂容器: '{solvent}'") - if solvent_vessel_id in G.nodes(): - return solvent_vessel_id - - # 如果直接匹配失败,尝试模糊匹配 + # 🔧 方法1:直接搜索 data.reagent_name for node in G.nodes(): - if node.startswith('flask_') and eluting_solvent.lower() in node.lower(): - return node + node_data = G.nodes[node].get('data', {}) + node_type = G.nodes[node].get('type', '') + + # 只搜索容器类型的节点 + if node_type == 'container': + reagent_name = node_data.get('reagent_name', '').lower() + reagent_config = G.nodes[node].get('config', {}).get('reagent', '').lower() + + # 检查 data.reagent_name 和 config.reagent + if reagent_name == solvent or reagent_config == solvent: + debug_print(f"✅ 通过reagent_name找到溶剂容器: {node} (reagent: {reagent_name or reagent_config})") + return node + + # 模糊匹配 reagent_name + if solvent in reagent_name or reagent_name in solvent: + debug_print(f"✅ 通过reagent_name模糊匹配到溶剂容器: {node} (reagent: {reagent_name})") + return node + + if solvent in reagent_config or reagent_config in solvent: + debug_print(f"✅ 通过config.reagent模糊匹配到溶剂容器: {node} (reagent: {reagent_config})") + return node - # 如果还是找不到,列出所有可用的溶剂瓶 - available_flasks = [node for node in G.nodes() - if node.startswith('flask_') - and G.nodes[node].get('type') == 'container'] + # 🔧 方法2:常见的溶剂容器命名规则 + possible_names = [ + f"flask_{solvent}", + f"bottle_{solvent}", + f"reagent_{solvent}", + f"{solvent}_bottle", + f"{solvent}_flask", + f"solvent_{solvent}", + f"reagent_bottle_{solvent}" + ] - raise ValueError(f"找不到洗脱溶剂 '{eluting_solvent}' 对应的溶剂瓶。可用溶剂瓶: {available_flasks}") + for vessel_name in possible_names: + if vessel_name in G.nodes(): + node_type = G.nodes[vessel_name].get('type', '') + if node_type == 'container': + debug_print(f"✅ 通过命名规则找到溶剂容器: {vessel_name}") + return vessel_name + + # 🔧 方法3:节点名称模糊匹配 + for node in G.nodes(): + node_type = G.nodes[node].get('type', '') + if node_type == 'container': + if ('flask_' in node or 'bottle_' in node or 'reagent_' in node) and solvent in node.lower(): + debug_print(f"✅ 通过节点名称模糊匹配到溶剂容器: {node}") + return node + + # 🔧 方法4:特殊溶剂名称映射 + solvent_mapping = { + 'dmf': ['dmf', 'dimethylformamide', 'n,n-dimethylformamide'], + 'ethyl_acetate': ['ethyl_acetate', 'ethylacetate', 'etoac', 'ea'], + 'hexane': ['hexane', 'hexanes', 'n-hexane'], + 'methanol': ['methanol', 'meoh', 'ch3oh'], + 'water': ['water', 'h2o', 'distilled_water'], + 'acetone': ['acetone', 'ch3coch3', '2-propanone'], + 'dichloromethane': ['dichloromethane', 'dcm', 'ch2cl2', 'methylene_chloride'], + 'chloroform': ['chloroform', 'chcl3', 'trichloromethane'] + } + + # 查找映射的同义词 + for canonical_name, synonyms in solvent_mapping.items(): + if solvent in synonyms: + debug_print(f"检测到溶剂同义词: '{solvent}' -> '{canonical_name}'") + return find_solvent_vessel(G, canonical_name) # 递归搜索 + + debug_print(f"⚠️ 未找到溶剂 '{solvent}' 的容器") + return "" +def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float: + """获取容器中的液体体积 - 增强版""" + if vessel not in G.nodes(): + debug_print(f"⚠️ 节点 '{vessel}' 不存在") + return 0.0 + + node_type = G.nodes[vessel].get('type', '') + vessel_data = G.nodes[vessel].get('data', {}) + + debug_print(f"读取节点 '{vessel}' (类型: {node_type}) 体积数据: {vessel_data}") + + # 🔧 如果是设备类型,尝试查找关联的容器 + if node_type == 'device': + debug_print(f"'{vessel}' 是设备,尝试查找关联容器...") + + # 查找是否有内置容器数据 + config_data = G.nodes[vessel].get('config', {}) + if 'volume' in config_data: + default_volume = config_data.get('volume', 50.0) + debug_print(f"使用设备默认容量: {default_volume}mL") + return default_volume + + # 对于旋蒸等设备,使用默认值 + if 'rotavap' in vessel.lower(): + default_volume = 50.0 + debug_print(f"旋蒸设备使用默认容量: {default_volume}mL") + return default_volume + + debug_print(f"⚠️ 设备 '{vessel}' 无法确定容量,返回0") + return 0.0 + + # 🔧 如果是容器类型,正常读取体积 + total_volume = 0.0 + + # 方法1:检查液体列表 + liquids = vessel_data.get('liquid', []) + if isinstance(liquids, list): + for liquid in liquids: + if isinstance(liquid, dict): + volume = liquid.get('volume') or liquid.get('liquid_volume', 0.0) + total_volume += volume + + # 方法2:检查直接体积字段 + if total_volume == 0.0: + volume_keys = ['current_volume', 'total_volume', 'volume', 'liquid_volume'] + for key in volume_keys: + if key in vessel_data: + try: + total_volume = float(vessel_data[key]) + if total_volume > 0: + break + except (ValueError, TypeError): + continue + + # 方法3:检查配置中的初始体积 + if total_volume == 0.0: + config_data = G.nodes[vessel].get('config', {}) + if 'current_volume' in config_data: + try: + total_volume = float(config_data['current_volume']) + except (ValueError, TypeError): + pass + + debug_print(f"容器 '{vessel}' 总体积: {total_volume}mL") + return total_volume + +def calculate_solvent_volumes(total_volume: float, pct1: float, pct2: float) -> tuple: + """根据百分比计算溶剂体积""" + volume1 = (total_volume * pct1) / 100.0 + volume2 = (total_volume * pct2) / 100.0 + + debug_print(f"溶剂体积计算: 总体积{total_volume}mL") + debug_print(f" - 溶剂1: {pct1}% = {volume1}mL") + debug_print(f" - 溶剂2: {pct2}% = {volume2}mL") + + return (volume1, volume2) def generate_run_column_protocol( G: nx.DiGraph, from_vessel: str, to_vessel: str, - column: str + column: str, + rf: str = "", + pct1: str = "", + pct2: str = "", + solvent1: str = "", + solvent2: str = "", + ratio: str = "", + **kwargs ) -> List[Dict[str, Any]]: """ - 生成柱层析分离的协议序列 + 生成柱层析分离的协议序列 - 增强版 + + 支持新版XDL的所有参数,具有高兼容性和容错性 Args: G: 有向图,节点为设备和容器,边为流体管道 - from_vessel: 源容器的名称,即样品起始所在的容器 - to_vessel: 目标容器的名称,分离后的样品要到达的容器 - column: 所使用的柱子的名称 + from_vessel: 源容器的名称,即样品起始所在的容器(必需) + to_vessel: 目标容器的名称,分离后的样品要到达的容器(必需) + column: 所使用的柱子的名称(必需) + rf: Rf值(可选,支持 "?" 表示未知) + pct1: 第一种溶剂百分比(如 "40 %",可选) + pct2: 第二种溶剂百分比(如 "50 %",可选) + solvent1: 第一种溶剂名称(可选) + solvent2: 第二种溶剂名称(可选) + ratio: 溶剂比例(如 "5:95",可选,优先级高于pct1/pct2) + **kwargs: 其他可选参数 Returns: List[Dict[str, Any]]: 柱层析分离操作的动作序列 """ + + debug_print("=" * 60) + debug_print("开始生成柱层析协议") + debug_print(f"输入参数:") + debug_print(f" - from_vessel: '{from_vessel}'") + debug_print(f" - to_vessel: '{to_vessel}'") + debug_print(f" - column: '{column}'") + debug_print(f" - rf: '{rf}'") + debug_print(f" - pct1: '{pct1}'") + debug_print(f" - pct2: '{pct2}'") + debug_print(f" - solvent1: '{solvent1}'") + debug_print(f" - solvent2: '{solvent2}'") + debug_print(f" - ratio: '{ratio}'") + debug_print(f" - 其他参数: {kwargs}") + debug_print("=" * 60) + action_sequence = [] - print(f"RUN_COLUMN: 开始生成柱层析协议") - print(f" - 源容器: {from_vessel}") - print(f" - 目标容器: {to_vessel}") - print(f" - 柱子: {column}") + # === 参数验证 === + debug_print("步骤1: 参数验证...") + + if not from_vessel: + raise ValueError("from_vessel 参数不能为空") + if not to_vessel: + raise ValueError("to_vessel 参数不能为空") + if not column: + raise ValueError("column 参数不能为空") - # 验证源容器和目标容器存在 if from_vessel not in G.nodes(): raise ValueError(f"源容器 '{from_vessel}' 不存在于系统中") - if to_vessel not in G.nodes(): raise ValueError(f"目标容器 '{to_vessel}' 不存在于系统中") - # 查找柱层析设备 - column_device_id = None - column_nodes = [node for node in G.nodes() - if (G.nodes[node].get('class') or '') == 'virtual_column'] + debug_print("✅ 基本参数验证通过") - if column_nodes: - column_device_id = column_nodes[0] - print(f"RUN_COLUMN: 找到柱层析设备: {column_device_id}") + # === 参数解析 === + debug_print("步骤2: 参数解析...") + + # 解析Rf值 + final_rf = parse_rf_value(rf) + debug_print(f"最终Rf值: {final_rf}") + + # 解析溶剂比例(ratio优先级高于pct1/pct2) + if ratio and ratio.strip(): + final_pct1, final_pct2 = parse_ratio(ratio) + debug_print(f"使用ratio参数: {final_pct1:.1f}% : {final_pct2:.1f}%") else: - print(f"RUN_COLUMN: 警告 - 未找到柱层析设备") + final_pct1 = parse_percentage(pct1) if pct1 else 50.0 + final_pct2 = parse_percentage(pct2) if pct2 else 50.0 + + # 如果百分比和不是100%,进行归一化 + total_pct = final_pct1 + final_pct2 + if total_pct == 0: + final_pct1, final_pct2 = 50.0, 50.0 + elif total_pct != 100.0: + final_pct1 = (final_pct1 / total_pct) * 100 + final_pct2 = (final_pct2 / total_pct) * 100 + + debug_print(f"使用百分比参数: {final_pct1:.1f}% : {final_pct2:.1f}%") + + # 设置默认溶剂(如果未指定) + final_solvent1 = solvent1.strip() if solvent1 else "ethyl_acetate" + final_solvent2 = solvent2.strip() if solvent2 else "hexane" + + debug_print(f"最终溶剂: {final_solvent1} : {final_solvent2}") + + # === 查找设备和容器 === + debug_print("步骤3: 查找设备和容器...") + + # 查找柱层析设备 + column_device_id = find_column_device(G) + + # 查找柱容器 + column_vessel = find_column_vessel(G, column) + + # 查找溶剂容器 + solvent1_vessel = find_solvent_vessel(G, final_solvent1) + solvent2_vessel = find_solvent_vessel(G, final_solvent2) + + debug_print(f"设备映射:") + debug_print(f" - 柱设备: '{column_device_id}'") + debug_print(f" - 柱容器: '{column_vessel}'") + debug_print(f" - 溶剂1容器: '{solvent1_vessel}'") + debug_print(f" - 溶剂2容器: '{solvent2_vessel}'") + + # === 获取源容器体积 === + debug_print("步骤4: 获取源容器体积...") - # 获取源容器中的液体体积 source_volume = get_vessel_liquid_volume(G, from_vessel) - print(f"RUN_COLUMN: 源容器 {from_vessel} 中有 {source_volume} mL 液体") + if source_volume <= 0: + source_volume = 50.0 # 默认体积 + debug_print(f"⚠️ 无法获取源容器体积,使用默认值: {source_volume}mL") + else: + debug_print(f"✅ 源容器体积: {source_volume}mL") - # === 第一步:样品转移到柱子(如果柱子是容器) === - if column in G.nodes() and G.nodes[column].get('type') == 'container': - print(f"RUN_COLUMN: 样品转移 - {source_volume} mL 从 {from_vessel} 到 {column}") - - try: - sample_transfer_actions = generate_pump_protocol( - G=G, - from_vessel=from_vessel, - to_vessel=column, - volume=source_volume if source_volume > 0 else 100.0, - flowrate=2.0 - ) - action_sequence.extend(sample_transfer_actions) - except Exception as e: - print(f"RUN_COLUMN: 样品转移失败: {str(e)}") + # === 计算溶剂体积 === + debug_print("步骤5: 计算溶剂体积...") - # === 第二步:使用柱层析设备执行分离 === - if column_device_id: - print(f"RUN_COLUMN: 使用柱层析设备执行分离") + # 洗脱溶剂通常是样品体积的2-5倍 + total_elution_volume = source_volume * 3.0 + solvent1_volume, solvent2_volume = calculate_solvent_volumes( + total_elution_volume, final_pct1, final_pct2 + ) + + # === 执行柱层析流程 === + debug_print("步骤6: 执行柱层析流程...") + + try: + # 步骤6.1: 样品上柱(如果有独立的柱容器) + if column_vessel and column_vessel != from_vessel: + debug_print(f"6.1: 样品上柱 - {source_volume}mL 从 {from_vessel} 到 {column_vessel}") + + try: + sample_transfer_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=from_vessel, + to_vessel=column_vessel, + volume=source_volume, + flowrate=1.0, # 慢速上柱 + transfer_flowrate=0.5, + rinsing_solvent="", # 暂不冲洗 + rinsing_volume=0.0, + rinsing_repeats=0 + ) + action_sequence.extend(sample_transfer_actions) + debug_print(f"✅ 样品上柱完成,添加了 {len(sample_transfer_actions)} 个动作") + except Exception as e: + debug_print(f"⚠️ 样品上柱失败: {str(e)}") - column_separation_action = { - "device_id": column_device_id, - "action_name": "run_column", - "action_kwargs": { - "from_vessel": from_vessel, - "to_vessel": to_vessel, - "column": column + # 步骤6.2: 添加洗脱溶剂1(如果有溶剂容器) + if solvent1_vessel and solvent1_volume > 0: + debug_print(f"6.2: 添加洗脱溶剂1 - {solvent1_volume:.1f}mL {final_solvent1}") + + try: + target_vessel = column_vessel if column_vessel else from_vessel + solvent1_transfer_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=solvent1_vessel, + to_vessel=target_vessel, + volume=solvent1_volume, + flowrate=2.0, + transfer_flowrate=1.0 + ) + action_sequence.extend(solvent1_transfer_actions) + debug_print(f"✅ 溶剂1添加完成,添加了 {len(solvent1_transfer_actions)} 个动作") + except Exception as e: + debug_print(f"⚠️ 溶剂1添加失败: {str(e)}") + + # 步骤6.3: 添加洗脱溶剂2(如果有溶剂容器) + if solvent2_vessel and solvent2_volume > 0: + debug_print(f"6.3: 添加洗脱溶剂2 - {solvent2_volume:.1f}mL {final_solvent2}") + + try: + target_vessel = column_vessel if column_vessel else from_vessel + solvent2_transfer_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=solvent2_vessel, + to_vessel=target_vessel, + volume=solvent2_volume, + flowrate=2.0, + transfer_flowrate=1.0 + ) + action_sequence.extend(solvent2_transfer_actions) + debug_print(f"✅ 溶剂2添加完成,添加了 {len(solvent2_transfer_actions)} 个动作") + except Exception as e: + debug_print(f"⚠️ 溶剂2添加失败: {str(e)}") + + # 步骤6.4: 使用柱层析设备执行分离(如果有设备) + if column_device_id: + debug_print(f"6.4: 使用柱层析设备执行分离") + + column_separation_action = { + "device_id": column_device_id, + "action_name": "run_column", + "action_kwargs": { + "from_vessel": from_vessel, + "to_vessel": to_vessel, + "column": column, + "rf": rf, + "pct1": pct1, + "pct2": pct2, + "solvent1": solvent1, + "solvent2": solvent2, + "ratio": ratio + } } - } - action_sequence.append(column_separation_action) - - # 等待柱层析设备完成分离 - action_sequence.append({ - "action_name": "wait", - "action_kwargs": {"time": 60} - }) - - # === 第三步:从柱子转移到目标容器(如果需要) === - if column in G.nodes() and column != to_vessel: - print(f"RUN_COLUMN: 产物转移 - 从 {column} 到 {to_vessel}") - - try: - product_transfer_actions = generate_pump_protocol( - G=G, - from_vessel=column, - to_vessel=to_vessel, - volume=source_volume * 0.8 if source_volume > 0 else 80.0, # 假设有一些损失 - flowrate=1.5 - ) - action_sequence.extend(product_transfer_actions) - except Exception as e: - print(f"RUN_COLUMN: 产物转移失败: {str(e)}") - - print(f"RUN_COLUMN: 生成了 {len(action_sequence)} 个动作") - return action_sequence - - -# 便捷函数:常用柱层析方案 -def generate_flash_column_protocol( - G: nx.DiGraph, - from_vessel: str, - to_vessel: str, - column_material: str = "silica_gel", - mobile_phase: str = "ethyl_acetate", - mobile_phase_volume: float = 100.0 -) -> List[Dict[str, Any]]: - """快速柱层析:高流速分离""" - return generate_run_column_protocol( - G, from_vessel, to_vessel, column_material, - mobile_phase, mobile_phase_volume, 1, "", 0.0, 3.0 - ) - - -def generate_preparative_column_protocol( - G: nx.DiGraph, - from_vessel: str, - to_vessel: str, - column_material: str = "silica_gel", - equilibration_solvent: str = "hexane", - eluting_solvent: str = "ethyl_acetate", - eluting_volume: float = 50.0, - eluting_repeats: int = 3 -) -> List[Dict[str, Any]]: - """制备柱层析:带平衡和多次洗脱""" - return generate_run_column_protocol( - G, from_vessel, to_vessel, column_material, - eluting_solvent, eluting_volume, eluting_repeats, - equilibration_solvent, 30.0, 1.5 - ) - - -def generate_gradient_column_protocol( - G: nx.DiGraph, - from_vessel: str, - to_vessel: str, - column_material: str = "silica_gel", - gradient_solvents: List[str] = None, - gradient_volumes: List[float] = None -) -> List[Dict[str, Any]]: - """梯度洗脱柱层析:多种溶剂系统""" - if gradient_solvents is None: - gradient_solvents = ["hexane", "ethyl_acetate", "methanol"] - if gradient_volumes is None: - gradient_volumes = [50.0, 50.0, 30.0] - - action_sequence = [] - - # 每种溶剂单独执行一次柱层析 - for i, (solvent, volume) in enumerate(zip(gradient_solvents, gradient_volumes)): - print(f"RUN_COLUMN: 梯度洗脱第 {i+1}/{len(gradient_solvents)} 步: {volume} mL {solvent}") - - # 第一步使用源容器,后续步骤使用柱子作为源 - step_from_vessel = from_vessel if i == 0 else column_material - # 最后一步使用目标容器,其他步骤使用柱子作为目标 - step_to_vessel = to_vessel if i == len(gradient_solvents) - 1 else column_material - - step_actions = generate_run_column_protocol( - G, step_from_vessel, step_to_vessel, column_material, - solvent, volume, 1, "", 0.0, 1.0 - ) - action_sequence.extend(step_actions) - - # 在梯度步骤之间加入等待时间 - if i < len(gradient_solvents) - 1: + action_sequence.append(column_separation_action) + debug_print(f"✅ 柱层析设备动作已添加") + + # 等待分离完成 + separation_time = max(30, int(total_elution_volume / 2)) # 基于体积估算时间 action_sequence.append({ "action_name": "wait", - "action_kwargs": {"time": 20} + "action_kwargs": {"time": separation_time} }) + debug_print(f"✅ 等待分离完成: {separation_time}秒") + + # 步骤6.5: 产物收集(从柱容器到目标容器) + if column_vessel and column_vessel != to_vessel: + debug_print(f"6.5: 产物收集 - 从 {column_vessel} 到 {to_vessel}") + + try: + # 估算产物体积(原始样品体积的70-90%) + product_volume = source_volume * 0.8 + + product_transfer_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=column_vessel, + to_vessel=to_vessel, + volume=product_volume, + flowrate=1.5, + transfer_flowrate=0.8 + ) + action_sequence.extend(product_transfer_actions) + debug_print(f"✅ 产物收集完成,添加了 {len(product_transfer_actions)} 个动作") + except Exception as e: + debug_print(f"⚠️ 产物收集失败: {str(e)}") + + # 步骤6.6: 如果没有独立的柱设备和容器,执行简化的直接转移 + if not column_device_id and not column_vessel: + debug_print(f"6.6: 简化模式 - 直接转移 {source_volume}mL 从 {from_vessel} 到 {to_vessel}") + + try: + direct_transfer_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=from_vessel, + to_vessel=to_vessel, + volume=source_volume, + flowrate=2.0, + transfer_flowrate=1.0 + ) + action_sequence.extend(direct_transfer_actions) + debug_print(f"✅ 直接转移完成,添加了 {len(direct_transfer_actions)} 个动作") + except Exception as e: + debug_print(f"⚠️ 直接转移失败: {str(e)}") + + except Exception as e: + debug_print(f"❌ 协议生成失败: {str(e)} 😭") + + # 不添加不确定的动作,直接让action_sequence保持为空列表 + # action_sequence 已经在函数开始时初始化为 [] + + # 确保至少有一个有效的动作,如果完全失败就返回空列表 + if not action_sequence: + debug_print("⚠️ 没有生成任何有效动作") + # 可以选择返回空列表或添加一个基本的等待动作 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": { + "time": 1.0, + "description": "柱层析协议执行完成" + } + }) + + # 🎊 总结 + debug_print("🧪" * 20) + debug_print(f"🎉 柱层析协议生成完成! ✨") + debug_print(f"📊 总动作数: {len(action_sequence)} 个") + debug_print(f"🥽 路径: {from_vessel} → {to_vessel}") + debug_print(f"🏛️ 柱子: {column}") + debug_print(f"🧪 溶剂: {final_solvent1}:{final_solvent2}") + debug_print("🧪" * 20) return action_sequence - -def generate_reverse_phase_column_protocol( - G: nx.DiGraph, - from_vessel: str, - to_vessel: str, - column_material: str = "C18", - aqueous_phase: str = "water", - organic_phase: str = "methanol", - gradient_ratio: float = 0.5 -) -> List[Dict[str, Any]]: - """反相柱层析:C18柱,水-有机相梯度""" - # 先用水相平衡 - equilibration_volume = 20.0 - # 然后用有机相洗脱 - eluting_volume = 30.0 * gradient_ratio - - return generate_run_column_protocol( - G, from_vessel, to_vessel, column_material, - organic_phase, eluting_volume, 2, - aqueous_phase, equilibration_volume, 0.8 - ) - - -def generate_ion_exchange_column_protocol( - G: nx.DiGraph, - from_vessel: str, - to_vessel: str, - column_material: str = "ion_exchange", - buffer_solution: str = "buffer", - salt_solution: str = "NaCl_solution", - salt_volume: float = 40.0 -) -> List[Dict[str, Any]]: - """离子交换柱层析:缓冲液平衡,盐溶液洗脱""" - return generate_run_column_protocol( - G, from_vessel, to_vessel, column_material, - salt_solution, salt_volume, 1, - buffer_solution, 25.0, 0.5 - ) - - # 测试函数 def test_run_column_protocol(): - """测试柱层析协议的示例""" - print("=== RUN COLUMN PROTOCOL 测试 ===") - print("测试完成") - + """测试柱层析协议""" + debug_print("🧪 === RUN COLUMN PROTOCOL 测试 === ✨") + debug_print("✅ 测试完成 🎉") if __name__ == "__main__": - test_run_column_protocol() \ No newline at end of file + test_run_column_protocol() + diff --git a/unilabos/compile/separate_protocol.py b/unilabos/compile/separate_protocol.py index cbb028c..258c37d 100644 --- a/unilabos/compile/separate_protocol.py +++ b/unilabos/compile/separate_protocol.py @@ -1,230 +1,670 @@ -import numpy as np import networkx as nx +import re +import logging +import sys +from typing import List, Dict, Any, Union +from .pump_protocol import generate_pump_protocol_with_rinsing +logger = logging.getLogger(__name__) + +# 确保输出编码为UTF-8 +if hasattr(sys.stdout, 'reconfigure'): + try: + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') + except: + pass + +def debug_print(message): + """调试输出函数 - 支持中文""" + try: + # 确保消息是字符串格式 + safe_message = str(message) + print(f"[分离协议] {safe_message}", flush=True) + logger.info(f"[分离协议] {safe_message}") + except UnicodeEncodeError: + # 如果编码失败,尝试替换不支持的字符 + safe_message = str(message).encode('utf-8', errors='replace').decode('utf-8') + print(f"[分离协议] {safe_message}", flush=True) + logger.info(f"[分离协议] {safe_message}") + except Exception as e: + # 最后的安全措施 + fallback_message = f"日志输出错误: {repr(message)}" + print(f"[分离协议] {fallback_message}", flush=True) + logger.info(f"[分离协议] {fallback_message}") + +def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]: + """创建一个动作日志 - 支持中文和emoji""" + try: + full_message = f"{emoji} {message}" + debug_print(full_message) + logger.info(full_message) + + return { + "action_name": "wait", + "action_kwargs": { + "time": 0.1, + "log_message": full_message, + "progress_message": full_message + } + } + except Exception as e: + # 如果emoji有问题,使用纯文本 + safe_message = f"[日志] {message}" + debug_print(safe_message) + logger.info(safe_message) + + return { + "action_name": "wait", + "action_kwargs": { + "time": 0.1, + "log_message": safe_message, + "progress_message": safe_message + } + } + +def parse_volume_input(volume_input: Union[str, float]) -> float: + """ + 解析体积输入,支持带单位的字符串 + + Args: + volume_input: 体积输入(如 "200 mL", "?", 50.0) + + Returns: + float: 体积(毫升) + """ + if isinstance(volume_input, (int, float)): + debug_print(f"📏 体积输入为数值: {volume_input}") + return float(volume_input) + + if not volume_input or not str(volume_input).strip(): + debug_print(f"⚠️ 体积输入为空,返回 0.0mL") + return 0.0 + + volume_str = str(volume_input).lower().strip() + debug_print(f"🔍 解析体积输入: '{volume_str}'") + + # 处理未知体积 + if volume_str in ['?', 'unknown', 'tbd', 'to be determined', '未知', '待定']: + default_volume = 100.0 # 默认100mL + debug_print(f"❓ 检测到未知体积,使用默认值: {default_volume}mL") + return default_volume + + # 移除空格并提取数字和单位 + volume_clean = re.sub(r'\s+', '', volume_str) + + # 匹配数字和单位的正则表达式 + match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter|毫升|升|微升)?', volume_clean) + + if not match: + debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值 100mL") + return 100.0 + + value = float(match.group(1)) + unit = match.group(2) or 'ml' # 默认单位为毫升 + + # 转换为毫升 + if unit in ['l', 'liter', '升']: + volume = value * 1000.0 # L -> mL + debug_print(f"🔄 体积转换: {value}L -> {volume}mL") + elif unit in ['μl', 'ul', 'microliter', '微升']: + volume = value / 1000.0 # μL -> mL + debug_print(f"🔄 体积转换: {value}μL -> {volume}mL") + else: # ml, milliliter, 毫升 或默认 + volume = value # 已经是mL + debug_print(f"✅ 体积已为毫升单位: {volume}mL") + + return volume + +def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: + """查找溶剂容器,支持多种匹配模式""" + if not solvent or not solvent.strip(): + debug_print("⏭️ 未指定溶剂,跳过溶剂容器查找") + return "" + + debug_print(f"🔍 正在查找溶剂 '{solvent}' 的容器...") + + # 🔧 方法1:直接搜索 data.reagent_name 和 config.reagent + debug_print(f"📋 方法1: 搜索试剂字段...") + for node in G.nodes(): + node_data = G.nodes[node].get('data', {}) + node_type = G.nodes[node].get('type', '') + config_data = G.nodes[node].get('config', {}) + + # 只搜索容器类型的节点 + if node_type == 'container': + reagent_name = node_data.get('reagent_name', '').lower() + config_reagent = config_data.get('reagent', '').lower() + + # 精确匹配 + if reagent_name == solvent.lower() or config_reagent == solvent.lower(): + debug_print(f"✅ 通过试剂字段精确匹配找到容器: {node}") + return node + + # 模糊匹配 + if (solvent.lower() in reagent_name and reagent_name) or \ + (solvent.lower() in config_reagent and config_reagent): + debug_print(f"✅ 通过试剂字段模糊匹配找到容器: {node}") + return node + + # 🔧 方法2:常见的容器命名规则 + debug_print(f"📋 方法2: 使用命名规则...") + solvent_clean = solvent.lower().replace(' ', '_').replace('-', '_') + possible_names = [ + f"flask_{solvent_clean}", + f"bottle_{solvent_clean}", + f"vessel_{solvent_clean}", + f"{solvent_clean}_flask", + f"{solvent_clean}_bottle", + f"solvent_{solvent_clean}", + f"reagent_{solvent_clean}", + f"reagent_bottle_{solvent_clean}", + f"reagent_bottle_1", # 通用试剂瓶 + f"reagent_bottle_2", + f"reagent_bottle_3" + ] + + debug_print(f"🎯 尝试的容器名称: {possible_names[:5]}... (共 {len(possible_names)} 个)") + + for name in possible_names: + if name in G.nodes(): + node_type = G.nodes[name].get('type', '') + if node_type == 'container': + debug_print(f"✅ 通过命名规则找到容器: {name}") + return name + + # 🔧 方法3:使用第一个试剂瓶作为备选 + debug_print(f"📋 方法3: 查找备用试剂瓶...") + for node_id in G.nodes(): + node_data = G.nodes[node_id] + if (node_data.get('type') == 'container' and + ('reagent' in node_id.lower() or 'bottle' in node_id.lower())): + debug_print(f"⚠️ 未找到专用容器,使用备用容器: {node_id}") + return node_id + + debug_print(f"❌ 无法找到溶剂 '{solvent}' 的容器") + return "" + +def find_separator_device(G: nx.DiGraph, vessel: str) -> str: + """查找分离器设备,支持多种查找方式""" + debug_print(f"🔍 正在查找容器 '{vessel}' 的分离器设备...") + + # 方法1:查找连接到容器的分离器设备 + debug_print(f"📋 方法1: 检查连接的分离器...") + separator_nodes = [] + for node in G.nodes(): + node_class = G.nodes[node].get('class', '').lower() + if 'separator' in node_class: + separator_nodes.append(node) + debug_print(f"📋 发现分离器设备: {node}") + + # 检查是否连接到目标容器 + if G.has_edge(node, vessel) or G.has_edge(vessel, node): + debug_print(f"✅ 找到连接的分离器: {node}") + return node + + debug_print(f"📊 找到的分离器总数: {len(separator_nodes)}") + + # 方法2:根据命名规则查找 + debug_print(f"📋 方法2: 使用命名规则...") + possible_names = [ + f"{vessel}_controller", + f"{vessel}_separator", + vessel, # 容器本身可能就是分离器 + "separator_1", + "virtual_separator", + "liquid_handler_1", # 液体处理器也可能用于分离 + "controller_1" + ] + + debug_print(f"🎯 尝试的分离器名称: {possible_names}") + + for name in possible_names: + if name in G.nodes(): + node_class = G.nodes[name].get('class', '').lower() + if 'separator' in node_class or 'controller' in node_class: + debug_print(f"✅ 通过命名规则找到分离器: {name}") + return name + + # 方法3:查找第一个分离器设备 + debug_print(f"📋 方法3: 使用第一个可用分离器...") + if separator_nodes: + debug_print(f"⚠️ 使用第一个分离器设备: {separator_nodes[0]}") + return separator_nodes[0] + + debug_print(f"❌ 未找到分离器设备") + return "" + +def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str: + """查找连接到指定容器的搅拌器""" + debug_print(f"🔍 正在查找与容器 {vessel} 连接的搅拌器...") + + stirrer_nodes = [] + for node in G.nodes(): + node_data = G.nodes[node] + node_class = node_data.get('class', '') or '' + + if 'stirrer' in node_class.lower(): + stirrer_nodes.append(node) + debug_print(f"📋 发现搅拌器: {node}") + + debug_print(f"📊 找到的搅拌器总数: {len(stirrer_nodes)}") + + # 检查哪个搅拌器与目标容器相连 + for stirrer in stirrer_nodes: + if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): + debug_print(f"✅ 找到连接的搅拌器: {stirrer}") + return stirrer + + # 如果没有连接的搅拌器,返回第一个可用的 + if stirrer_nodes: + debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个可用的: {stirrer_nodes[0]}") + return stirrer_nodes[0] + + debug_print("❌ 未找到搅拌器") + return "" def generate_separate_protocol( - G: nx.DiGraph, - purpose: str, # 'wash' or 'extract'. 'wash' means that product phase will not be the added solvent phase, 'extract' means product phase will be the added solvent phase. If no solvent is added just use 'extract'. - product_phase: str, # 'top' or 'bottom'. Phase that product will be in. - from_vessel: str, #Contents of from_vessel are transferred to separation_vessel and separation is performed. - separation_vessel: str, # Vessel in which separation of phases will be carried out. - to_vessel: str, # Vessel to send product phase to. - waste_phase_to_vessel: str, # Optional. Vessel to send waste phase to. - solvent: str, # Optional. Solvent to add to separation vessel after contents of from_vessel has been transferred to create two phases. - solvent_volume: float = 50, # Optional. Volume of solvent to add (mL). - through: str = "", # Optional. Solid chemical to send product phase through on way to to_vessel, e.g. 'celite'. - repeats: int = 1, # Optional. Number of separations to perform. - stir_time: float = 30, # Optional. Time stir for after adding solvent, before separation of phases. - stir_speed: float = 300, # Optional. Speed to stir at after adding solvent, before separation of phases. - settling_time: float = 300 # Optional. Time -) -> list[dict]: + G: nx.DiGraph, + # 🔧 基础参数,支持XDL的vessel参数 + vessel: str = "", # XDL: 分离容器 + purpose: str = "separate", # 分离目的 + product_phase: str = "top", # 产物相 + # 🔧 可选的详细参数 + from_vessel: str = "", # 源容器(通常在separate前已经transfer了) + separation_vessel: str = "", # 分离容器(与vessel同义) + to_vessel: str = "", # 目标容器(可选) + waste_phase_to_vessel: str = "", # 废相目标容器 + product_vessel: str = "", # XDL: 产物容器(与to_vessel同义) + waste_vessel: str = "", # XDL: 废液容器(与waste_phase_to_vessel同义) + # 🔧 溶剂相关参数 + solvent: str = "", # 溶剂名称 + solvent_volume: Union[str, float] = 0.0, # 溶剂体积 + volume: Union[str, float] = 0.0, # XDL: 体积(与solvent_volume同义) + # 🔧 操作参数 + through: str = "", # 通过材料 + repeats: int = 1, # 重复次数 + stir_time: float = 30.0, # 搅拌时间(秒) + stir_speed: float = 300.0, # 搅拌速度 + settling_time: float = 300.0, # 沉降时间(秒) + **kwargs +) -> List[Dict[str, Any]]: """ - Generate a protocol to clean a vessel with a solvent. + 生成分离操作的协议序列 - 增强中文版 - :param G: Directed graph. Nodes are containers and pumps, edges are fluidic connections. - :param vessel: Vessel to clean. - :param solvent: Solvent to clean vessel with. - :param volume: Volume of solvent to clean vessel with. - :param temp: Temperature to heat vessel to while cleaning. - :param repeats: Number of cleaning cycles to perform. - :return: List of actions to clean vessel. + 支持XDL参数格式: + - vessel: 分离容器(必需) + - purpose: "wash", "extract", "separate" + - product_phase: "top", "bottom" + - product_vessel: 产物收集容器 + - waste_vessel: 废液收集容器 + - solvent: 溶剂名称 + - volume: "200 mL", "?" 或数值 + - repeats: 重复次数 + + 分离流程: + 1. (可选)添加溶剂到分离容器 + 2. 搅拌混合 + 3. 静置分层 + 4. 收集指定相到目标容器 + 5. 重复指定次数 """ - # 生成泵操作的动作序列 - pump_action_sequence = [] - reactor_volume = 500.0 - waste_vessel = waste_phase_to_vessel + debug_print("=" * 60) + debug_print("🧪 开始生成分离协议 - 增强中文版") + debug_print(f"📋 原始参数:") + debug_print(f" 🥼 容器: '{vessel}'") + debug_print(f" 🎯 分离目的: '{purpose}'") + debug_print(f" 📊 产物相: '{product_phase}'") + debug_print(f" 💧 溶剂: '{solvent}'") + debug_print(f" 📏 体积: {volume} (类型: {type(volume)})") + debug_print(f" 🔄 重复次数: {repeats}") + debug_print(f" 🎯 产物容器: '{product_vessel}'") + debug_print(f" 🗑️ 废液容器: '{waste_vessel}'") + debug_print(f" 📦 其他参数: {kwargs}") + debug_print("=" * 60) - # TODO:通过物料管理系统找到溶剂的容器 - if "," in solvent: - solvents = solvent.split(",") - assert len(solvents) == repeats, "Number of solvents must match number of repeats." + action_sequence = [] + + # === 参数验证和标准化 === + debug_print("🔍 步骤1: 参数验证和标准化...") + action_sequence.append(create_action_log(f"开始分离操作 - 容器: {vessel}", "🎬")) + action_sequence.append(create_action_log(f"分离目的: {purpose}", "🧪")) + action_sequence.append(create_action_log(f"产物相: {product_phase}", "📊")) + + # 统一容器参数 + final_vessel = vessel or separation_vessel + if not final_vessel: + debug_print("❌ 必须指定分离容器") + raise ValueError("必须指定分离容器 (vessel 或 separation_vessel)") + + final_to_vessel = to_vessel or product_vessel + final_waste_vessel = waste_phase_to_vessel or waste_vessel + + # 统一体积参数 + final_volume = parse_volume_input(volume or solvent_volume) + + # 🔧 修复:确保repeats至少为1 + if repeats <= 0: + repeats = 1 + debug_print(f"⚠️ 重复次数参数 <= 0,自动设置为 1") + + debug_print(f"🔧 标准化后的参数:") + debug_print(f" 🥼 分离容器: '{final_vessel}'") + debug_print(f" 🎯 产物容器: '{final_to_vessel}'") + debug_print(f" 🗑️ 废液容器: '{final_waste_vessel}'") + debug_print(f" 📏 溶剂体积: {final_volume}mL") + debug_print(f" 🔄 重复次数: {repeats}") + + action_sequence.append(create_action_log(f"分离容器: {final_vessel}", "🧪")) + action_sequence.append(create_action_log(f"溶剂体积: {final_volume}mL", "📏")) + action_sequence.append(create_action_log(f"重复次数: {repeats}", "🔄")) + + # 验证必需参数 + if not purpose: + purpose = "separate" + if not product_phase: + product_phase = "top" + if purpose not in ["wash", "extract", "separate"]: + debug_print(f"⚠️ 未知的分离目的 '{purpose}',使用默认值 'separate'") + purpose = "separate" + action_sequence.append(create_action_log(f"未知目的,使用: {purpose}", "⚠️")) + if product_phase not in ["top", "bottom"]: + debug_print(f"⚠️ 未知的产物相 '{product_phase}',使用默认值 'top'") + product_phase = "top" + action_sequence.append(create_action_log(f"未知相别,使用: {product_phase}", "⚠️")) + + debug_print("✅ 参数验证通过") + action_sequence.append(create_action_log("参数验证通过", "✅")) + + # === 查找设备 === + debug_print("🔍 步骤2: 查找设备...") + action_sequence.append(create_action_log("正在查找相关设备...", "🔍")) + + # 查找分离器设备 + separator_device = find_separator_device(G, final_vessel) + if separator_device: + action_sequence.append(create_action_log(f"找到分离器设备: {separator_device}", "🧪")) else: - solvents = [solvent] * repeats + debug_print("⚠️ 未找到分离器设备,可能无法执行分离") + action_sequence.append(create_action_log("未找到分离器设备", "⚠️")) - # TODO: 通过设备连接图找到分离容器的控制器、底部出口 - separator_controller = f"{separation_vessel}_controller" - separation_vessel_bottom = f"flask_{separation_vessel}" + # 查找搅拌器 + stirrer_device = find_connected_stirrer(G, final_vessel) + if stirrer_device: + action_sequence.append(create_action_log(f"找到搅拌器: {stirrer_device}", "🌪️")) + else: + action_sequence.append(create_action_log("未找到搅拌器", "⚠️")) - transfer_flowrate = flowrate = 2.5 + # 查找溶剂容器(如果需要) + solvent_vessel = "" + if solvent and solvent.strip(): + solvent_vessel = find_solvent_vessel(G, solvent) + if solvent_vessel: + action_sequence.append(create_action_log(f"找到溶剂容器: {solvent_vessel}", "💧")) + else: + action_sequence.append(create_action_log(f"未找到溶剂容器: {solvent}", "⚠️")) - if from_vessel != separation_vessel: - pump_action_sequence.append( - { - "device_id": "", - "action_name": "PumpTransferProtocol", - "action_kwargs": { - "from_vessel": from_vessel, - "to_vessel": separation_vessel, - "volume": reactor_volume, - "time": reactor_volume / flowrate, - # "transfer_flowrate": transfer_flowrate, + debug_print(f"📊 设备配置:") + debug_print(f" 🧪 分离器设备: '{separator_device}'") + debug_print(f" 🌪️ 搅拌器设备: '{stirrer_device}'") + debug_print(f" 💧 溶剂容器: '{solvent_vessel}'") + + # === 执行分离流程 === + debug_print("🔍 步骤3: 执行分离流程...") + action_sequence.append(create_action_log("开始分离工作流程", "🎯")) + + try: + for repeat_idx in range(repeats): + cycle_num = repeat_idx + 1 + debug_print(f"🔄 第{cycle_num}轮: 开始分离循环 {cycle_num}/{repeats}") + action_sequence.append(create_action_log(f"分离循环 {cycle_num}/{repeats} 开始", "🔄")) + + # 步骤3.1: 添加溶剂(如果需要) + if solvent_vessel and final_volume > 0: + debug_print(f"🔄 第{cycle_num}轮 步骤1: 添加溶剂 {solvent} ({final_volume}mL)") + action_sequence.append(create_action_log(f"向分离容器添加 {final_volume}mL {solvent}", "💧")) + + try: + # 使用pump protocol添加溶剂 + pump_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=solvent_vessel, + to_vessel=final_vessel, + volume=final_volume, + amount="", + time=0.0, + viscous=False, + rinsing_solvent="", + rinsing_volume=0.0, + rinsing_repeats=0, + solid=False, + flowrate=2.5, + transfer_flowrate=0.5, + rate_spec="", + event="", + through="", + **kwargs + ) + action_sequence.extend(pump_actions) + debug_print(f"✅ 溶剂添加完成,添加了 {len(pump_actions)} 个动作") + action_sequence.append(create_action_log(f"溶剂转移完成 ({len(pump_actions)} 个操作)", "✅")) + + except Exception as e: + debug_print(f"❌ 溶剂添加失败: {str(e)}") + action_sequence.append(create_action_log(f"溶剂添加失败: {str(e)}", "❌")) + else: + debug_print(f"🔄 第{cycle_num}轮 步骤1: 无需添加溶剂") + action_sequence.append(create_action_log("无需添加溶剂", "⏭️")) + + # 步骤3.2: 启动搅拌(如果有搅拌器) + if stirrer_device and stir_time > 0: + debug_print(f"🔄 第{cycle_num}轮 步骤2: 开始搅拌 ({stir_speed}rpm,持续 {stir_time}s)") + action_sequence.append(create_action_log(f"开始搅拌: {stir_speed}rpm,持续 {stir_time}s", "🌪️")) + + action_sequence.append({ + "device_id": stirrer_device, + "action_name": "start_stir", + "action_kwargs": { + "vessel": final_vessel, + "stir_speed": stir_speed, + "purpose": f"分离混合 - {purpose}" + } + }) + + # 搅拌等待 + stir_minutes = stir_time / 60 + action_sequence.append(create_action_log(f"搅拌中,持续 {stir_minutes:.1f} 分钟", "⏱️")) + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": stir_time} + }) + + # 停止搅拌 + action_sequence.append(create_action_log("停止搅拌器", "🛑")) + action_sequence.append({ + "device_id": stirrer_device, + "action_name": "stop_stir", + "action_kwargs": {"vessel": final_vessel} + }) + + else: + debug_print(f"🔄 第{cycle_num}轮 步骤2: 无需搅拌") + action_sequence.append(create_action_log("无需搅拌", "⏭️")) + + # 步骤3.3: 静置分层 + if settling_time > 0: + debug_print(f"🔄 第{cycle_num}轮 步骤3: 静置分层 ({settling_time}s)") + settling_minutes = settling_time / 60 + action_sequence.append(create_action_log(f"静置分层 ({settling_minutes:.1f} 分钟)", "⚖️")) + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": settling_time} + }) + else: + debug_print(f"🔄 第{cycle_num}轮 步骤3: 未指定静置时间") + action_sequence.append(create_action_log("未指定静置时间", "⏭️")) + + # 步骤3.4: 执行分离操作 + if separator_device: + debug_print(f"🔄 第{cycle_num}轮 步骤4: 执行分离操作") + action_sequence.append(create_action_log(f"执行分离: 收集{product_phase}相", "🧪")) + + # 调用分离器设备的separate方法 + separate_action = { + "device_id": separator_device, + "action_name": "separate", + "action_kwargs": { + "purpose": purpose, + "product_phase": product_phase, + "from_vessel": from_vessel or final_vessel, + "separation_vessel": final_vessel, + "to_vessel": final_to_vessel or final_vessel, + "waste_phase_to_vessel": final_waste_vessel or final_vessel, + "solvent": solvent, + "solvent_volume": final_volume, + "through": through, + "repeats": 1, # 每次调用只做一次分离 + "stir_time": 0, # 已经在上面完成 + "stir_speed": stir_speed, + "settling_time": 0 # 已经在上面完成 + } } + action_sequence.append(separate_action) + debug_print(f"✅ 分离操作已添加") + action_sequence.append(create_action_log("分离操作完成", "✅")) + + # 收集结果 + if final_to_vessel: + action_sequence.append(create_action_log(f"产物 ({product_phase}相) 收集到: {final_to_vessel}", "📦")) + if final_waste_vessel: + action_sequence.append(create_action_log(f"废相收集到: {final_waste_vessel}", "🗑️")) + + else: + debug_print(f"🔄 第{cycle_num}轮 步骤4: 无分离器设备,跳过分离") + action_sequence.append(create_action_log("无分离器设备可用", "❌")) + # 添加等待时间模拟分离 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 10.0} + }) + + # 循环间等待(除了最后一次) + if repeat_idx < repeats - 1: + debug_print(f"🔄 第{cycle_num}轮: 等待下一次循环...") + action_sequence.append(create_action_log("等待下一次循环...", "⏳")) + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 5} + }) + else: + action_sequence.append(create_action_log(f"分离循环 {cycle_num}/{repeats} 完成", "🌟")) + + except Exception as e: + debug_print(f"❌ 分离工作流程执行失败: {str(e)}") + action_sequence.append(create_action_log(f"分离工作流程失败: {str(e)}", "❌")) + # 添加错误日志 + action_sequence.append({ + "device_id": "system", + "action_name": "log_message", + "action_kwargs": { + "message": f"分离操作失败: {str(e)}" } - ) - - # for i in range(2): - # pump_action_sequence.append( - # { - # "device_id": "", - # "action_name": "CleanProtocol", - # "action_kwargs": { - # "vessel": from_vessel, - # "solvent": "H2O", # Solvent to clean vessel with. - # "volume": solvent_volume, # Optional. Volume of solvent to clean vessel with. - # "temp": 25.0, # Optional. Temperature to heat vessel to while cleaning. - # "repeats": 1 - # } - # } - # ) - # pump_action_sequence.append( - # { - # "device_id": "", - # "action_name": "CleanProtocol", - # "action_kwargs": { - # "vessel": from_vessel, - # "solvent": "CH2Cl2", # Solvent to clean vessel with. - # "volume": solvent_volume, # Optional. Volume of solvent to clean vessel with. - # "temp": 25.0, # Optional. Temperature to heat vessel to while cleaning. - # "repeats": 1 - # } - # } - # ) + }) - # 生成泵操作的动作序列 - for i in range(repeats): - # 找到当次萃取所用溶剂 - solvent_thistime = solvents[i] - solvent_vessel = f"flask_{solvent_thistime}" - - pump_action_sequence.append( - { - "device_id": "", - "action_name": "PumpTransferProtocol", - "action_kwargs": { - "from_vessel": solvent_vessel, - "to_vessel": separation_vessel, - "volume": solvent_volume, - "time": solvent_volume / flowrate, - # "transfer_flowrate": transfer_flowrate, - } - } - ) - pump_action_sequence.extend([ - # 搅拌、静置 - { - "device_id": separator_controller, - "action_name": "stir", - "action_kwargs": { - "stir_time": stir_time, - "stir_speed": stir_speed, - "settling_time": settling_time - } - }, - # 分液(判断电导突跃) - { - "device_id": separator_controller, - "action_name": "valve_open", - "action_kwargs": { - "command": "delta > 0.05" - } - } - ]) - - if product_phase == "bottom": - # 产物转移到目标瓶 - pump_action_sequence.append( - { - "device_id": "", - "action_name": "PumpTransferProtocol", - "action_kwargs": { - "from_vessel": separation_vessel_bottom, - "to_vessel": to_vessel, - "volume": 250.0, - "time": 250.0 / flowrate, - # "transfer_flowrate": transfer_flowrate, - } - } - ) - # 放出上面那一相,60秒后关阀门 - pump_action_sequence.append( - { - "device_id": separator_controller, - "action_name": "valve_open", - "action_kwargs": { - "command": "time > 60" - } - } - ) - # 弃去上面那一相进废液 - pump_action_sequence.append( - { - "device_id": "", - "action_name": "PumpTransferProtocol", - "action_kwargs": { - "from_vessel": separation_vessel_bottom, - "to_vessel": waste_vessel, - "volume": 250.0, - "time": 250.0 / flowrate, - # "transfer_flowrate": transfer_flowrate, - } - } - ) - elif product_phase == "top": - # 弃去下面那一相进废液 - pump_action_sequence.append( - { - "device_id": "", - "action_name": "PumpTransferProtocol", - "action_kwargs": { - "from_vessel": separation_vessel_bottom, - "to_vessel": waste_vessel, - "volume": 250.0, - "time": 250.0 / flowrate, - # "transfer_flowrate": transfer_flowrate, - } - } - ) - # 放出上面那一相 - pump_action_sequence.append( - { - "device_id": separator_controller, - "action_name": "valve_open", - "action_kwargs": { - "command": "time > 60" - } - } - ) - # 产物转移到目标瓶 - pump_action_sequence.append( - { - "device_id": "", - "action_name": "PumpTransferProtocol", - "action_kwargs": { - "from_vessel": separation_vessel_bottom, - "to_vessel": to_vessel, - "volume": 250.0, - "time": 250.0 / flowrate, - # "transfer_flowrate": transfer_flowrate, - } - } - ) - elif product_phase == "organic": - pass - - # 如果不是最后一次,从中转瓶转移回分液漏斗 - if i < repeats - 1: - pump_action_sequence.append( - { - "device_id": "", - "action_name": "PumpTransferProtocol", - "action_kwargs": { - "from_vessel": to_vessel, - "to_vessel": separation_vessel, - "volume": 250.0, - "time": 250.0 / flowrate, - # "transfer_flowrate": transfer_flowrate, - } - } - ) - return pump_action_sequence + # === 最终结果 === + total_time = (stir_time + settling_time + 15) * repeats # 估算总时间 + + debug_print("=" * 60) + debug_print(f"🎉 分离协议生成完成") + debug_print(f"📊 协议统计:") + debug_print(f" 📋 总动作数: {len(action_sequence)}") + debug_print(f" ⏱️ 预计总时间: {total_time:.0f}s ({total_time/60:.1f} 分钟)") + debug_print(f" 🥼 分离容器: {final_vessel}") + debug_print(f" 🎯 分离目的: {purpose}") + debug_print(f" 📊 产物相: {product_phase}") + debug_print(f" 🔄 重复次数: {repeats}") + if solvent: + debug_print(f" 💧 溶剂: {solvent} ({final_volume}mL)") + if final_to_vessel: + debug_print(f" 🎯 产物容器: {final_to_vessel}") + if final_waste_vessel: + debug_print(f" 🗑️ 废液容器: {final_waste_vessel}") + debug_print("=" * 60) + + # 添加完成日志 + summary_msg = f"分离协议完成: {final_vessel} ({purpose},{repeats} 次循环)" + if solvent: + summary_msg += f",使用 {final_volume}mL {solvent}" + action_sequence.append(create_action_log(summary_msg, "🎉")) + + return action_sequence + +# === 便捷函数 === + +def separate_phases_only(G: nx.DiGraph, vessel: str, product_phase: str = "top", + product_vessel: str = "", waste_vessel: str = "") -> List[Dict[str, Any]]: + """仅进行相分离(不添加溶剂)""" + debug_print(f"⚡ 快速相分离: {vessel} ({product_phase}相)") + return generate_separate_protocol( + G, vessel=vessel, + purpose="separate", + product_phase=product_phase, + product_vessel=product_vessel, + waste_vessel=waste_vessel + ) + +def wash_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float], + product_phase: str = "top", repeats: int = 1) -> List[Dict[str, Any]]: + """用溶剂洗涤""" + debug_print(f"🧽 用{solvent}洗涤: {vessel} ({repeats} 次)") + return generate_separate_protocol( + G, vessel=vessel, + purpose="wash", + product_phase=product_phase, + solvent=solvent, + volume=volume, + repeats=repeats + ) + +def extract_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float], + product_phase: str = "bottom", repeats: int = 3) -> List[Dict[str, Any]]: + """用溶剂萃取""" + debug_print(f"🧪 用{solvent}萃取: {vessel} ({repeats} 次)") + return generate_separate_protocol( + G, vessel=vessel, + purpose="extract", + product_phase=product_phase, + solvent=solvent, + volume=volume, + repeats=repeats + ) + +def separate_aqueous_organic(G: nx.DiGraph, vessel: str, organic_phase: str = "top", + product_vessel: str = "", waste_vessel: str = "") -> List[Dict[str, Any]]: + """水-有机相分离""" + debug_print(f"💧 水-有机相分离: {vessel} (有机相: {organic_phase})") + return generate_separate_protocol( + G, vessel=vessel, + purpose="separate", + product_phase=organic_phase, + product_vessel=product_vessel, + waste_vessel=waste_vessel + ) + +# 测试函数 +def test_separate_protocol(): + """测试分离协议的各种参数解析""" + debug_print("=== 分离协议增强中文版测试 ===") + + # 测试体积解析 + debug_print("🧪 测试体积解析...") + volumes = ["200 mL", "?", 100.0, "1 L", "500 μL", "未知", "50毫升"] + for vol in volumes: + result = parse_volume_input(vol) + debug_print(f"📊 体积解析结果: {vol} -> {result}mL") + + debug_print("✅ 测试完成") + +if __name__ == "__main__": + test_separate_protocol() diff --git a/unilabos/compile/stir_protocol.py b/unilabos/compile/stir_protocol.py index 6fc865c..f7a6a74 100644 --- a/unilabos/compile/stir_protocol.py +++ b/unilabos/compile/stir_protocol.py @@ -1,166 +1,342 @@ -from typing import List, Dict, Any +from typing import List, Dict, Any, Union import networkx as nx +import logging +import re +logger = logging.getLogger(__name__) + +def debug_print(message): + """调试输出""" + print(f"🌪️ [STIR] {message}", flush=True) + logger.info(f"[STIR] {message}") + +def parse_time_input(time_input: Union[str, float, int], default_unit: str = "s") -> float: + """ + 统一的时间解析函数(精简版) + + Args: + time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0) + default_unit: 默认单位(默认为秒) + + Returns: + float: 时间(秒) + """ + if not time_input: + return 100.0 # 默认100秒 + + # 🔢 处理数值输入 + if isinstance(time_input, (int, float)): + result = float(time_input) + debug_print(f"⏰ 数值时间: {time_input} → {result}s") + return result + + # 📝 处理字符串输入 + time_str = str(time_input).lower().strip() + debug_print(f"🔍 解析时间: '{time_str}'") + + # ❓ 特殊值处理 + special_times = { + '?': 300.0, 'unknown': 300.0, 'tbd': 300.0, + 'briefly': 30.0, 'quickly': 60.0, 'slowly': 600.0, + 'several minutes': 300.0, 'few minutes': 180.0, 'overnight': 3600.0 + } + + if time_str in special_times: + result = special_times[time_str] + debug_print(f"🎯 特殊时间: '{time_str}' → {result}s ({result/60:.1f}分钟)") + return result + + # 🔢 纯数字处理 + try: + result = float(time_str) + debug_print(f"⏰ 纯数字: {time_str} → {result}s") + return result + except ValueError: + pass + + # 📐 正则表达式解析 + pattern = r'(\d+\.?\d*)\s*([a-z]*)' + match = re.match(pattern, time_str) + + if not match: + debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 100s") + return 100.0 + + value = float(match.group(1)) + unit = match.group(2) or default_unit + + # 📏 单位转换 + unit_multipliers = { + 's': 1.0, 'sec': 1.0, 'second': 1.0, 'seconds': 1.0, + 'm': 60.0, 'min': 60.0, 'mins': 60.0, 'minute': 60.0, 'minutes': 60.0, + 'h': 3600.0, 'hr': 3600.0, 'hrs': 3600.0, 'hour': 3600.0, 'hours': 3600.0, + 'd': 86400.0, 'day': 86400.0, 'days': 86400.0 + } + + multiplier = unit_multipliers.get(unit, 1.0) + result = value * multiplier + + debug_print(f"✅ 时间解析: '{time_str}' → {value} {unit} → {result}s ({result/60:.1f}分钟)") + return result def find_connected_stirrer(G: nx.DiGraph, vessel: str = None) -> str: - """ - 查找与指定容器相连的搅拌设备,或查找可用的搅拌设备 - """ - # 查找所有搅拌设备节点 - stirrer_nodes = [node for node in G.nodes() - if (G.nodes[node].get('class') or '') == 'virtual_stirrer'] + """查找与指定容器相连的搅拌设备""" + debug_print(f"🔍 查找搅拌设备,目标容器: {vessel} 🥽") - if vessel: - # 检查哪个搅拌设备与目标容器相连(机械连接) + # 🔧 查找所有搅拌设备 + stirrer_nodes = [] + for node in G.nodes(): + node_data = G.nodes[node] + node_class = node_data.get('class', '') or '' + + if 'stirrer' in node_class.lower() or 'virtual_stirrer' in node_class: + stirrer_nodes.append(node) + debug_print(f"🎉 找到搅拌设备: {node} 🌪️") + + # 🔗 检查连接 + if vessel and stirrer_nodes: for stirrer in stirrer_nodes: if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): + debug_print(f"✅ 搅拌设备 '{stirrer}' 与容器 '{vessel}' 相连 🔗") return stirrer - # 如果没有指定容器或没有直接连接,返回第一个可用的搅拌设备 + # 🎯 使用第一个可用设备 if stirrer_nodes: - return stirrer_nodes[0] + selected = stirrer_nodes[0] + debug_print(f"🔧 使用第一个搅拌设备: {selected} 🌪️") + return selected - raise ValueError("系统中未找到可用的搅拌设备") + # 🆘 默认设备 + debug_print("⚠️ 未找到搅拌设备,使用默认设备 🌪️") + return "stirrer_1" +def validate_and_fix_params(stir_time: float, stir_speed: float, settling_time: float) -> tuple: + """验证和修正参数""" + # ⏰ 搅拌时间验证 + if stir_time < 0: + debug_print(f"⚠️ 搅拌时间 {stir_time}s 无效,修正为 100s 🕐") + stir_time = 100.0 + elif stir_time > 100: # 限制为100s + debug_print(f"⚠️ 搅拌时间 {stir_time}s 过长,仿真运行时,修正为 100s 🕐") + stir_time = 100.0 + else: + debug_print(f"✅ 搅拌时间 {stir_time}s ({stir_time/60:.1f}分钟) 有效 ⏰") + + # 🌪️ 搅拌速度验证 + if stir_speed < 10.0 or stir_speed > 1500.0: + debug_print(f"⚠️ 搅拌速度 {stir_speed} RPM 超出范围,修正为 300 RPM 🌪️") + stir_speed = 300.0 + else: + debug_print(f"✅ 搅拌速度 {stir_speed} RPM 在正常范围内 🌪️") + + # ⏱️ 沉降时间验证 + if settling_time < 0 or settling_time > 600: # 限制为10分钟 + debug_print(f"⚠️ 沉降时间 {settling_time}s 超出范围,修正为 60s ⏱️") + settling_time = 60.0 + else: + debug_print(f"✅ 沉降时间 {settling_time}s 在正常范围内 ⏱️") + + return stir_time, stir_speed, settling_time def generate_stir_protocol( G: nx.DiGraph, - stir_time: float, - stir_speed: float, - settling_time: float + vessel: str, + time: Union[str, float, int] = "300", + stir_time: Union[str, float, int] = "0", + time_spec: str = "", + event: str = "", + stir_speed: float = 300.0, + settling_time: Union[str, float] = "60", + **kwargs ) -> List[Dict[str, Any]]: """ - 生成搅拌操作的协议序列 - 定时搅拌 + 沉降 + 生成搅拌操作的协议序列(精简版) """ - action_sequence = [] - print(f"STIR: 开始生成搅拌协议") - print(f" - 搅拌时间: {stir_time}秒") - print(f" - 搅拌速度: {stir_speed} RPM") - print(f" - 沉降时间: {settling_time}秒") + debug_print("🌪️" * 20) + debug_print("🚀 开始生成搅拌协议 ✨") + debug_print(f"📝 输入参数:") + debug_print(f" 🥽 vessel: {vessel}") + debug_print(f" ⏰ time: {time}") + debug_print(f" 🕐 stir_time: {stir_time}") + debug_print(f" 🎯 time_spec: {time_spec}") + debug_print(f" 🌪️ stir_speed: {stir_speed} RPM") + debug_print(f" ⏱️ settling_time: {settling_time}") + debug_print("🌪️" * 20) - # 查找搅拌设备 + # 📋 参数验证 + debug_print("📍 步骤1: 参数验证... 🔧") + if not vessel: + debug_print("❌ vessel 参数不能为空! 😱") + raise ValueError("vessel 参数不能为空") + + if vessel not in G.nodes(): + debug_print(f"❌ 容器 '{vessel}' 不存在于系统中! 😞") + raise ValueError(f"容器 '{vessel}' 不存在于系统中") + + debug_print("✅ 基础参数验证通过 🎯") + + # 🔄 参数解析 + debug_print("📍 步骤2: 参数解析... ⚡") + + # 确定实际时间(优先级:time_spec > stir_time > time) + if time_spec: + parsed_time = parse_time_input(time_spec) + debug_print(f"🎯 使用time_spec: '{time_spec}' → {parsed_time}s") + elif stir_time not in ["0", 0, 0.0]: + parsed_time = parse_time_input(stir_time) + debug_print(f"🎯 使用stir_time: {stir_time} → {parsed_time}s") + else: + parsed_time = parse_time_input(time) + debug_print(f"🎯 使用time: {time} → {parsed_time}s") + + # 解析沉降时间 + parsed_settling_time = parse_time_input(settling_time) + + # 🕐 模拟运行时间优化 + debug_print(" ⏱️ 检查模拟运行时间限制...") + original_stir_time = parsed_time + original_settling_time = parsed_settling_time + + # 搅拌时间限制为60秒 + stir_time_limit = 60.0 + if parsed_time > stir_time_limit: + parsed_time = stir_time_limit + debug_print(f" 🎮 搅拌时间优化: {original_stir_time}s → {parsed_time}s ⚡") + + # 沉降时间限制为30秒 + settling_time_limit = 30.0 + if parsed_settling_time > settling_time_limit: + parsed_settling_time = settling_time_limit + debug_print(f" 🎮 沉降时间优化: {original_settling_time}s → {parsed_settling_time}s ⚡") + + # 参数修正 + parsed_time, stir_speed, parsed_settling_time = validate_and_fix_params( + parsed_time, stir_speed, parsed_settling_time + ) + + debug_print(f"🎯 最终参数: time={parsed_time}s, speed={stir_speed}RPM, settling={parsed_settling_time}s") + + # 🔍 查找设备 + debug_print("📍 步骤3: 查找搅拌设备... 🔍") try: - stirrer_id = find_connected_stirrer(G) - print(f"STIR: 找到搅拌设备: {stirrer_id}") - except ValueError as e: + stirrer_id = find_connected_stirrer(G, vessel) + debug_print(f"🎉 使用搅拌设备: {stirrer_id} ✨") + except Exception as e: + debug_print(f"❌ 设备查找失败: {str(e)} 😭") raise ValueError(f"无法找到搅拌设备: {str(e)}") - # 执行搅拌操作 + # 🚀 生成动作 + debug_print("📍 步骤4: 生成搅拌动作... 🌪️") + + action_sequence = [] stir_action = { "device_id": stirrer_id, "action_name": "stir", "action_kwargs": { - "stir_time": stir_time, - "stir_speed": stir_speed, - "settling_time": settling_time + "vessel": vessel, + "time": str(time), # 保持原始格式 + "event": event, + "time_spec": time_spec, + "stir_time": float(parsed_time), # 确保是数字 + "stir_speed": float(stir_speed), # 确保是数字 + "settling_time": float(parsed_settling_time) # 确保是数字 } } - action_sequence.append(stir_action) + debug_print("✅ 搅拌动作已添加 🌪️✨") + + # 显示时间优化信息 + if original_stir_time != parsed_time or original_settling_time != parsed_settling_time: + debug_print(f" 🎭 模拟优化说明:") + debug_print(f" 搅拌时间: {original_stir_time/60:.1f}分钟 → {parsed_time/60:.1f}分钟") + debug_print(f" 沉降时间: {original_settling_time/60:.1f}分钟 → {parsed_settling_time/60:.1f}分钟") + + # 🎊 总结 + debug_print("🎊" * 20) + debug_print(f"🎉 搅拌协议生成完成! ✨") + debug_print(f"📊 总动作数: {len(action_sequence)} 个") + debug_print(f"🥽 搅拌容器: {vessel}") + debug_print(f"🌪️ 搅拌参数: {stir_speed} RPM, {parsed_time}s, 沉降 {parsed_settling_time}s") + debug_print(f"⏱️ 预计总时间: {(parsed_time + parsed_settling_time)/60:.1f} 分钟 ⌛") + debug_print("🎊" * 20) - print(f"STIR: 生成了 {len(action_sequence)} 个动作") return action_sequence - def generate_start_stir_protocol( G: nx.DiGraph, vessel: str, - stir_speed: float, - purpose: str + stir_speed: float = 300.0, + purpose: str = "", + **kwargs ) -> List[Dict[str, Any]]: - """ - 生成开始搅拌操作的协议序列 - 持续搅拌 - """ - action_sequence = [] + """生成开始搅拌操作的协议序列""" - print(f"START_STIR: 开始生成启动搅拌协议") - print(f" - 容器: {vessel}") - print(f" - 搅拌速度: {stir_speed} RPM") - print(f" - 目的: {purpose}") + debug_print("🔄 开始生成启动搅拌协议 ✨") + debug_print(f"🥽 vessel: {vessel}, 🌪️ speed: {stir_speed} RPM") - # 验证容器存在 - if vessel not in G.nodes(): - raise ValueError(f"容器 '{vessel}' 不存在于系统中") + # 基础验证 + if not vessel or vessel not in G.nodes(): + debug_print("❌ 容器验证失败!") + raise ValueError("vessel 参数无效") - # 查找搅拌设备 - try: - stirrer_id = find_connected_stirrer(G, vessel) - print(f"START_STIR: 找到搅拌设备: {stirrer_id}") - except ValueError as e: - raise ValueError(f"无法找到搅拌设备: {str(e)}") + # 参数修正 + if stir_speed < 10.0 or stir_speed > 1500.0: + debug_print(f"⚠️ 搅拌速度修正: {stir_speed} → 300 RPM 🌪️") + stir_speed = 300.0 - # 执行开始搅拌操作 - start_stir_action = { + # 查找设备 + stirrer_id = find_connected_stirrer(G, vessel) + + # 生成动作 + action_sequence = [{ "device_id": stirrer_id, "action_name": "start_stir", "action_kwargs": { "vessel": vessel, "stir_speed": stir_speed, - "purpose": purpose + "purpose": purpose or f"启动搅拌 {stir_speed} RPM" } - } + }] - action_sequence.append(start_stir_action) - - print(f"START_STIR: 生成了 {len(action_sequence)} 个动作") + debug_print(f"✅ 启动搅拌协议生成完成 🎯") return action_sequence - def generate_stop_stir_protocol( G: nx.DiGraph, - vessel: str + vessel: str, + **kwargs ) -> List[Dict[str, Any]]: - """ - 生成停止搅拌操作的协议序列 - """ - action_sequence = [] + """生成停止搅拌操作的协议序列""" - print(f"STOP_STIR: 开始生成停止搅拌协议") - print(f" - 容器: {vessel}") + debug_print("🛑 开始生成停止搅拌协议 ✨") + debug_print(f"🥽 vessel: {vessel}") - # 验证容器存在 - if vessel not in G.nodes(): - raise ValueError(f"容器 '{vessel}' 不存在于系统中") + # 基础验证 + if not vessel or vessel not in G.nodes(): + debug_print("❌ 容器验证失败!") + raise ValueError("vessel 参数无效") - # 查找搅拌设备 - try: - stirrer_id = find_connected_stirrer(G, vessel) - print(f"STOP_STIR: 找到搅拌设备: {stirrer_id}") - except ValueError as e: - raise ValueError(f"无法找到搅拌设备: {str(e)}") + # 查找设备 + stirrer_id = find_connected_stirrer(G, vessel) - # 执行停止搅拌操作 - stop_stir_action = { + # 生成动作 + action_sequence = [{ "device_id": stirrer_id, "action_name": "stop_stir", "action_kwargs": { "vessel": vessel } - } + }] - action_sequence.append(stop_stir_action) - - print(f"STOP_STIR: 生成了 {len(action_sequence)} 个动作") + debug_print(f"✅ 停止搅拌协议生成完成 🎯") return action_sequence +# 测试函数 +def test_stir_protocol(): + """测试搅拌协议""" + debug_print("🧪 === STIR PROTOCOL 测试 === ✨") + debug_print("✅ 测试完成 🎉") -# 便捷函数 -def generate_fast_stir_protocol( - G: nx.DiGraph, - time: float = 300.0, - speed: float = 800.0, - settling: float = 60.0 -) -> List[Dict[str, Any]]: - """快速搅拌的便捷函数""" - return generate_stir_protocol(G, time, speed, settling) - - -def generate_gentle_stir_protocol( - G: nx.DiGraph, - time: float = 600.0, - speed: float = 200.0, - settling: float = 120.0 -) -> List[Dict[str, Any]]: - """温和搅拌的便捷函数""" - return generate_stir_protocol(G, time, speed, settling) \ No newline at end of file +if __name__ == "__main__": + test_stir_protocol() diff --git a/unilabos/compile/utils/unit_parser.py b/unilabos/compile/utils/unit_parser.py new file mode 100644 index 0000000..d1d297c --- /dev/null +++ b/unilabos/compile/utils/unit_parser.py @@ -0,0 +1,206 @@ +""" +统一的单位解析工具模块 +支持时间、体积、质量等各种单位的解析 +""" + +import re +import logging +from typing import Union + +logger = logging.getLogger(__name__) + +def debug_print(message, prefix="[UNIT_PARSER]"): + """调试输出""" + print(f"{prefix} {message}", flush=True) + logger.info(f"{prefix} {message}") + +def parse_time_with_units(time_input: Union[str, float, int], default_unit: str = "s") -> float: + """ + 解析带单位的时间输入 + + Args: + time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0) + default_unit: 默认单位(默认为秒) + + Returns: + float: 时间(秒) + """ + if not time_input: + return 0.0 + + # 处理数值输入 + if isinstance(time_input, (int, float)): + result = float(time_input) + debug_print(f"数值时间输入: {time_input} → {result}s(默认单位)") + return result + + # 处理字符串输入 + time_str = str(time_input).lower().strip() + debug_print(f"解析时间字符串: '{time_str}'") + + # 处理特殊值 + if time_str in ['?', 'unknown', 'tbd', 'to be determined']: + default_time = 300.0 # 5分钟默认值 + debug_print(f"检测到未知时间,使用默认值: {default_time}s") + return default_time + + # 如果是纯数字,使用默认单位 + try: + value = float(time_str) + if default_unit == "s": + result = value + elif default_unit in ["min", "minute"]: + result = value * 60.0 + elif default_unit in ["h", "hour"]: + result = value * 3600.0 + else: + result = value # 默认秒 + debug_print(f"纯数字输入: {time_str} → {result}s(单位: {default_unit})") + return result + except ValueError: + pass + + # 使用正则表达式匹配数字和单位 + pattern = r'(\d+\.?\d*)\s*([a-z]*)' + match = re.match(pattern, time_str) + + if not match: + debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 60s") + return 60.0 + + value = float(match.group(1)) + unit = match.group(2) or default_unit + + # 单位转换映射 + unit_multipliers = { + # 秒 + 's': 1.0, + 'sec': 1.0, + 'second': 1.0, + 'seconds': 1.0, + + # 分钟 + 'm': 60.0, + 'min': 60.0, + 'mins': 60.0, + 'minute': 60.0, + 'minutes': 60.0, + + # 小时 + 'h': 3600.0, + 'hr': 3600.0, + 'hrs': 3600.0, + 'hour': 3600.0, + 'hours': 3600.0, + + # 天 + 'd': 86400.0, + 'day': 86400.0, + 'days': 86400.0, + } + + multiplier = unit_multipliers.get(unit, 1.0) + result = value * multiplier + + debug_print(f"时间解析: '{time_str}' → {value} {unit} → {result}s") + return result + +def parse_volume_with_units(volume_input: Union[str, float, int], default_unit: str = "mL") -> float: + """ + 解析带单位的体积输入 + + Args: + volume_input: 体积输入(如 "100 mL", "2.5 L", "500", "?", 100.0) + default_unit: 默认单位(默认为毫升) + + Returns: + float: 体积(毫升) + """ + if not volume_input: + return 0.0 + + # 处理数值输入 + if isinstance(volume_input, (int, float)): + result = float(volume_input) + debug_print(f"数值体积输入: {volume_input} → {result}mL(默认单位)") + return result + + # 处理字符串输入 + volume_str = str(volume_input).lower().strip() + debug_print(f"解析体积字符串: '{volume_str}'") + + # 处理特殊值 + if volume_str in ['?', 'unknown', 'tbd', 'to be determined']: + default_volume = 50.0 # 50mL默认值 + debug_print(f"检测到未知体积,使用默认值: {default_volume}mL") + return default_volume + + # 如果是纯数字,使用默认单位 + try: + value = float(volume_str) + if default_unit.lower() in ["ml", "milliliter"]: + result = value + elif default_unit.lower() in ["l", "liter"]: + result = value * 1000.0 + elif default_unit.lower() in ["μl", "ul", "microliter"]: + result = value / 1000.0 + else: + result = value # 默认mL + debug_print(f"纯数字输入: {volume_str} → {result}mL(单位: {default_unit})") + return result + except ValueError: + pass + + # 移除空格并提取数字和单位 + volume_clean = re.sub(r'\s+', '', volume_str) + + # 匹配数字和单位的正则表达式 + match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean) + + if not match: + debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值: 50mL") + return 50.0 + + value = float(match.group(1)) + unit = match.group(2) or default_unit.lower() + + # 转换为毫升 + if unit in ['l', 'liter']: + volume = value * 1000.0 # L -> mL + elif unit in ['μl', 'ul', 'microliter']: + volume = value / 1000.0 # μL -> mL + else: # ml, milliliter 或默认 + volume = value # 已经是mL + + debug_print(f"体积解析: '{volume_str}' → {value} {unit} → {volume}mL") + return volume + +# 测试函数 +def test_unit_parser(): + """测试单位解析功能""" + print("=== 单位解析器测试 ===") + + # 测试时间解析 + time_tests = [ + "30 min", "1 h", "300", "5.5 h", "?", 60.0, "2 hours", "30 s" + ] + + print("\n时间解析测试:") + for time_input in time_tests: + result = parse_time_with_units(time_input) + print(f" {time_input} → {result}s ({result/60:.1f}min)") + + # 测试体积解析 + volume_tests = [ + "100 mL", "2.5 L", "500", "?", 100.0, "500 μL", "1 liter" + ] + + print("\n体积解析测试:") + for volume_input in volume_tests: + result = parse_volume_with_units(volume_input) + print(f" {volume_input} → {result}mL") + + print("\n✅ 测试完成") + +if __name__ == "__main__": + test_unit_parser() \ No newline at end of file diff --git a/unilabos/compile/wash_solid_protocol.py b/unilabos/compile/wash_solid_protocol.py index a792b8f..55768a4 100644 --- a/unilabos/compile/wash_solid_protocol.py +++ b/unilabos/compile/wash_solid_protocol.py @@ -1,216 +1,316 @@ -from typing import List, Dict, Any +from typing import List, Dict, Any, Union import networkx as nx +import logging +import re + +logger = logging.getLogger(__name__) + +def debug_print(message): + """调试输出""" + print(f"🧼 [WASH_SOLID] {message}", flush=True) + logger.info(f"[WASH_SOLID] {message}") + +def parse_time_input(time_input: Union[str, float, int]) -> float: + """统一时间解析函数(精简版)""" + if not time_input: + return 0.0 + + # 🔢 处理数值输入 + if isinstance(time_input, (int, float)): + result = float(time_input) + debug_print(f"⏰ 数值时间: {time_input} → {result}s") + return result + + # 📝 处理字符串输入 + time_str = str(time_input).lower().strip() + + # ❓ 特殊值快速处理 + special_times = { + '?': 60.0, 'unknown': 60.0, 'briefly': 30.0, + 'quickly': 45.0, 'slowly': 120.0 + } + + if time_str in special_times: + result = special_times[time_str] + debug_print(f"🎯 特殊时间: '{time_str}' → {result}s") + return result + + # 🔢 数字提取(简化正则) + try: + # 提取数字 + numbers = re.findall(r'\d+\.?\d*', time_str) + if numbers: + value = float(numbers[0]) + + # 简化单位判断 + if any(unit in time_str for unit in ['min', 'm']): + result = value * 60.0 + elif any(unit in time_str for unit in ['h', 'hour']): + result = value * 3600.0 + else: + result = value # 默认秒 + + debug_print(f"✅ 时间解析: '{time_str}' → {result}s") + return result + except: + pass + + debug_print(f"⚠️ 时间解析失败: '{time_str}',使用默认60s") + return 60.0 + +def parse_volume_input(volume: Union[float, str], volume_spec: str = "", mass: str = "") -> float: + """统一体积解析函数(精简版)""" + debug_print(f"💧 解析体积: volume={volume}, spec='{volume_spec}', mass='{mass}'") + + # 🎯 优先级1:volume_spec(快速映射) + if volume_spec: + spec_map = { + 'small': 20.0, 'medium': 50.0, 'large': 100.0, + 'minimal': 10.0, 'normal': 50.0, 'generous': 150.0 + } + for key, val in spec_map.items(): + if key in volume_spec.lower(): + debug_print(f"🎯 规格匹配: '{volume_spec}' → {val}mL") + return val + + # 🧮 优先级2:mass转体积(简化:1g=1mL) + if mass: + try: + numbers = re.findall(r'\d+\.?\d*', mass) + if numbers: + value = float(numbers[0]) + if 'mg' in mass.lower(): + result = value / 1000.0 + elif 'kg' in mass.lower(): + result = value * 1000.0 + else: + result = value # 默认g + debug_print(f"⚖️ 质量转换: {mass} → {result}mL") + return result + except: + pass + + # 📦 优先级3:volume + if volume: + if isinstance(volume, (int, float)): + result = float(volume) + debug_print(f"💧 数值体积: {volume} → {result}mL") + return result + elif isinstance(volume, str): + try: + # 提取数字 + numbers = re.findall(r'\d+\.?\d*', volume) + if numbers: + value = float(numbers[0]) + # 简化单位判断 + if 'l' in volume.lower() and 'ml' not in volume.lower(): + result = value * 1000.0 # L转mL + else: + result = value # 默认mL + debug_print(f"💧 字符串体积: '{volume}' → {result}mL") + return result + except: + pass + + # 默认值 + debug_print(f"⚠️ 体积解析失败,使用默认50mL") + return 50.0 + +def find_solvent_source(G: nx.DiGraph, solvent: str) -> str: + """查找溶剂源(精简版)""" + debug_print(f"🔍 查找溶剂源: {solvent}") + + # 简化搜索列表 + search_patterns = [ + f"flask_{solvent}", f"bottle_{solvent}", f"reagent_{solvent}", + "liquid_reagent_bottle_1", "flask_1", "solvent_bottle" + ] + + for pattern in search_patterns: + if pattern in G.nodes(): + debug_print(f"🎉 找到溶剂源: {pattern}") + return pattern + + debug_print(f"⚠️ 使用默认溶剂源: flask_{solvent}") + return f"flask_{solvent}" + +def find_filtrate_vessel(G: nx.DiGraph, filtrate_vessel: str = "") -> str: + """查找滤液容器(精简版)""" + debug_print(f"🔍 查找滤液容器: {filtrate_vessel}") + + # 如果指定了且存在,直接使用 + if filtrate_vessel and filtrate_vessel in G.nodes(): + debug_print(f"✅ 使用指定容器: {filtrate_vessel}") + return filtrate_vessel + + # 简化搜索列表 + default_vessels = ["waste_workup", "filtrate_vessel", "flask_1", "collection_bottle_1"] + + for vessel in default_vessels: + if vessel in G.nodes(): + debug_print(f"🎉 找到滤液容器: {vessel}") + return vessel + + debug_print(f"⚠️ 使用默认滤液容器: waste_workup") + return "waste_workup" def generate_wash_solid_protocol( G: nx.DiGraph, vessel: str, solvent: str, - volume: float, + volume: Union[float, str] = "50", filtrate_vessel: str = "", temp: float = 25.0, stir: bool = False, stir_speed: float = 0.0, - time: float = 0.0, - repeats: int = 1 + time: Union[str, float] = "0", + repeats: int = 1, + volume_spec: str = "", + repeats_spec: str = "", + mass: str = "", + event: str = "", + **kwargs ) -> List[Dict[str, Any]]: """ - 生成固体清洗的协议序列 - - Args: - G: 有向图,节点为设备和容器 - vessel: 装有固体物质的容器名称 - solvent: 用于清洗固体的溶剂名称 - volume: 清洗溶剂的体积 - filtrate_vessel: 滤液要收集到的容器名称,可选参数 - temp: 清洗时的温度,可选参数 - stir: 是否在清洗过程中搅拌,默认为 False - stir_speed: 搅拌速度,可选参数 - time: 清洗的时间,可选参数 - repeats: 清洗操作的重复次数,默认为 1 - - Returns: - List[Dict[str, Any]]: 固体清洗操作的动作序列 - - Raises: - ValueError: 当找不到必要的设备时抛出异常 - - Examples: - wash_solid_protocol = generate_wash_solid_protocol( - G, "reactor", "ethanol", 100.0, "waste_flask", 60.0, True, 300.0, 600.0, 3 - ) + 生成固体清洗协议(精简版) """ + + debug_print("🧼" * 20) + debug_print("🚀 开始生成固体清洗协议 ✨") + debug_print(f"📝 输入参数:") + debug_print(f" 🥽 vessel: {vessel}") + debug_print(f" 🧪 solvent: {solvent}") + debug_print(f" 💧 volume: {volume}") + debug_print(f" ⏰ time: {time}") + debug_print(f" 🔄 repeats: {repeats}") + debug_print("🧼" * 20) + + # 📋 快速验证 + if not vessel or vessel not in G.nodes(): + debug_print("❌ 容器验证失败! 😱") + raise ValueError("vessel 参数无效") + + if not solvent: + debug_print("❌ 溶剂不能为空! 😱") + raise ValueError("solvent 参数不能为空") + + debug_print("✅ 基础验证通过 🎯") + + # 🔄 参数解析 + debug_print("📍 步骤1: 参数解析... ⚡") + final_volume = parse_volume_input(volume, volume_spec, mass) + final_time = parse_time_input(time) + + # 重复次数处理(简化) + if repeats_spec: + spec_map = {'few': 2, 'several': 3, 'many': 4, 'thorough': 5} + final_repeats = next((v for k, v in spec_map.items() if k in repeats_spec.lower()), repeats) + else: + final_repeats = max(1, min(repeats, 5)) # 限制1-5次 + + # 🕐 模拟时间优化 + debug_print(" ⏱️ 模拟时间优化...") + original_time = final_time + if final_time > 60.0: + final_time = 60.0 # 限制最长60秒 + debug_print(f" 🎮 时间优化: {original_time}s → {final_time}s ⚡") + + # 参数修正 + temp = max(25.0, min(temp, 80.0)) # 温度范围25-80°C + stir_speed = max(0.0, min(stir_speed, 300.0)) if stir else 0.0 # 速度范围0-300 + + debug_print(f"🎯 最终参数: 体积={final_volume}mL, 时间={final_time}s, 重复={final_repeats}次") + + # 🔍 查找设备 + debug_print("📍 步骤2: 查找设备... 🔍") + try: + solvent_source = find_solvent_source(G, solvent) + actual_filtrate_vessel = find_filtrate_vessel(G, filtrate_vessel) + debug_print(f"🎉 设备配置完成 ✨") + except Exception as e: + debug_print(f"❌ 设备查找失败: {str(e)} 😭") + raise ValueError(f"设备查找失败: {str(e)}") + + # 🚀 生成动作序列 + debug_print("📍 步骤3: 生成清洗动作... 🧼") action_sequence = [] - # 验证容器是否存在 - if vessel not in G.nodes(): - raise ValueError(f"固体容器 {vessel} 不存在于图中") - - if filtrate_vessel and filtrate_vessel not in G.nodes(): - raise ValueError(f"滤液容器 {filtrate_vessel} 不存在于图中") - - # 查找转移泵设备(用于添加溶剂和转移滤液) - pump_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_transfer_pump'] - - if not pump_nodes: - raise ValueError("没有找到可用的转移泵设备") - - pump_id = pump_nodes[0] - - # 查找加热设备(如果需要加热) - heatchill_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_heatchill'] - - heatchill_id = heatchill_nodes[0] if heatchill_nodes else None - - # 查找搅拌设备(如果需要搅拌) - stirrer_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_stirrer'] - - stirrer_id = stirrer_nodes[0] if stirrer_nodes else None - - # 查找过滤设备(用于分离固体和滤液) - filter_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_filter'] - - filter_id = filter_nodes[0] if filter_nodes else None - - # 查找溶剂容器 - solvent_vessel = f"flask_{solvent}" - if solvent_vessel not in G.nodes(): - # 如果没有找到特定溶剂容器,查找可用的源容器 - available_vessels = [node for node in G.nodes() - if node.startswith('flask_') and - G.nodes[node].get('type') == 'container'] - if available_vessels: - solvent_vessel = available_vessels[0] - else: - raise ValueError(f"没有找到溶剂容器 {solvent}") - - # 如果没有指定滤液容器,使用废液容器 - if not filtrate_vessel: - waste_vessels = [node for node in G.nodes() - if 'waste' in node.lower() and - G.nodes[node].get('type') == 'container'] - filtrate_vessel = waste_vessels[0] if waste_vessels else "waste_flask" - - # 重复清洗操作 - for repeat in range(repeats): - repeat_num = repeat + 1 + for cycle in range(final_repeats): + debug_print(f" 🔄 第{cycle+1}/{final_repeats}次清洗...") - # 步骤1:如果需要加热,先设置温度 - if temp > 25.0 and heatchill_id: - action_sequence.append({ - "device_id": heatchill_id, - "action_name": "heat_chill_start", + # 1. 转移溶剂 + try: + from .pump_protocol import generate_pump_protocol_with_rinsing + + transfer_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=solvent_source, + to_vessel=vessel, + volume=final_volume, + amount="", + time=0.0, + viscous=False, + rinsing_solvent="", + rinsing_volume=0.0, + rinsing_repeats=0, + solid=False, + flowrate=2.5, + transfer_flowrate=0.5 + ) + + if transfer_actions: + action_sequence.extend(transfer_actions) + debug_print(f" ✅ 转移动作: {len(transfer_actions)}个 🚚") + + except Exception as e: + debug_print(f" ❌ 转移失败: {str(e)} 😞") + + # 2. 搅拌(如果需要) + if stir and final_time > 0: + stir_action = { + "device_id": "stirrer_1", + "action_name": "stir", "action_kwargs": { "vessel": vessel, - "temp": temp, - "purpose": f"固体清洗 - 第 {repeat_num} 次" + "time": str(time), + "stir_time": final_time, + "stir_speed": stir_speed, + "settling_time": 10.0 # 🕐 缩短沉降时间 } - }) - - # 步骤2:添加清洗溶剂到固体容器 - action_sequence.append({ - "device_id": pump_id, - "action_name": "transfer", - "action_kwargs": { - "from_vessel": solvent_vessel, - "to_vessel": vessel, - "volume": volume, - "amount": f"清洗溶剂 {solvent} - 第 {repeat_num} 次", - "time": 0.0, - "viscous": False, - "rinsing_solvent": "", - "rinsing_volume": 0.0, - "rinsing_repeats": 0, - "solid": False } + action_sequence.append(stir_action) + debug_print(f" ✅ 搅拌动作: {final_time}s, {stir_speed}RPM 🌪️") + + # 3. 过滤 + filter_action = { + "device_id": "filter_1", + "action_name": "filter", + "action_kwargs": { + "vessel": vessel, + "filtrate_vessel": actual_filtrate_vessel, + "temp": temp, + "volume": final_volume + } + } + action_sequence.append(filter_action) + debug_print(f" ✅ 过滤动作: → {actual_filtrate_vessel} 🌊") + + # 4. 等待(缩短时间) + wait_time = 5.0 # 🕐 缩短等待时间:10s → 5s + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": wait_time} }) - - # 步骤3:如果需要搅拌,开始搅拌 - if stir and stir_speed > 0 and stirrer_id: - if time > 0: - # 定时搅拌 - action_sequence.append({ - "device_id": stirrer_id, - "action_name": "stir", - "action_kwargs": { - "stir_time": time, - "stir_speed": stir_speed, - "settling_time": 30.0 # 搅拌后静置30秒 - } - }) - else: - # 开始搅拌(需要手动停止) - action_sequence.append({ - "device_id": stirrer_id, - "action_name": "start_stir", - "action_kwargs": { - "vessel": vessel, - "stir_speed": stir_speed, - "purpose": f"固体清洗搅拌 - 第 {repeat_num} 次" - } - }) - - # 步骤4:如果指定了清洗时间但没有搅拌,等待清洗时间 - if time > 0 and (not stir or stir_speed == 0): - # 这里可以添加等待操作,暂时跳过 - pass - - # 步骤5:如果有搅拌且没有定时,停止搅拌 - if stir and stir_speed > 0 and time == 0 and stirrer_id: - action_sequence.append({ - "device_id": stirrer_id, - "action_name": "stop_stir", - "action_kwargs": { - "vessel": vessel - } - }) - - # 步骤6:过滤分离固体和滤液 - if filter_id: - action_sequence.append({ - "device_id": filter_id, - "action_name": "filter_sample", - "action_kwargs": { - "vessel": vessel, - "filtrate_vessel": filtrate_vessel, - "stir": False, - "stir_speed": 0.0, - "temp": temp, - "continue_heatchill": temp > 25.0, - "volume": volume - } - }) - else: - # 没有专门的过滤设备,使用转移泵模拟过滤过程 - # 将滤液转移到滤液容器 - action_sequence.append({ - "device_id": pump_id, - "action_name": "transfer", - "action_kwargs": { - "from_vessel": vessel, - "to_vessel": filtrate_vessel, - "volume": volume, - "amount": f"转移滤液 - 第 {repeat_num} 次清洗", - "time": 0.0, - "viscous": False, - "rinsing_solvent": "", - "rinsing_volume": 0.0, - "rinsing_repeats": 0, - "solid": False - } - }) - - # 步骤7:如果加热了,停止加热(在最后一次清洗后) - if temp > 25.0 and heatchill_id and repeat_num == repeats: - action_sequence.append({ - "device_id": heatchill_id, - "action_name": "heat_chill_stop", - "action_kwargs": { - "vessel": vessel - } - }) + debug_print(f" ✅ 等待: {wait_time}s ⏰") + + # 🎊 总结 + debug_print("🧼" * 20) + debug_print(f"🎉 固体清洗协议生成完成! ✨") + debug_print(f"📊 总动作数: {len(action_sequence)} 个") + debug_print(f"🥽 清洗容器: {vessel}") + debug_print(f"🧪 使用溶剂: {solvent}") + debug_print(f"💧 清洗体积: {final_volume}mL × {final_repeats}次") + debug_print(f"⏱️ 预计总时间: {(final_time + 5) * final_repeats / 60:.1f} 分钟") + debug_print("🧼" * 20) return action_sequence \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_column.py b/unilabos/devices/virtual/virtual_column.py index c83da1c..892a320 100644 --- a/unilabos/devices/virtual/virtual_column.py +++ b/unilabos/devices/virtual/virtual_column.py @@ -3,7 +3,7 @@ import logging from typing import Dict, Any, Optional class VirtualColumn: - """Virtual column device for RunColumn protocol""" + """Virtual column device for RunColumn protocol 🏛️""" def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): # 处理可能的不同调用方式 @@ -25,45 +25,77 @@ class VirtualColumn: self._column_length = self.config.get('column_length') or kwargs.get('column_length', 25.0) self._column_diameter = self.config.get('column_diameter') or kwargs.get('column_diameter', 2.0) - print(f"=== VirtualColumn {self.device_id} created with max_flow_rate={self._max_flow_rate}, length={self._column_length}cm ===") + print(f"🏛️ === 虚拟色谱柱 {self.device_id} 已创建 === ✨") + print(f"📏 柱参数: 流速={self._max_flow_rate}mL/min | 长度={self._column_length}cm | 直径={self._column_diameter}cm 🔬") async def initialize(self) -> bool: - """Initialize virtual column""" - self.logger.info(f"Initializing virtual column {self.device_id}") + """Initialize virtual column 🚀""" + self.logger.info(f"🔧 初始化虚拟色谱柱 {self.device_id} ✨") + self.data.update({ "status": "Idle", - "column_state": "Ready", + "column_state": "Ready", "current_flow_rate": 0.0, "max_flow_rate": self._max_flow_rate, "column_length": self._column_length, "column_diameter": self._column_diameter, "processed_volume": 0.0, "progress": 0.0, - "current_status": "Ready" + "current_status": "Ready for separation" }) + + self.logger.info(f"✅ 色谱柱 {self.device_id} 初始化完成 🏛️") + self.logger.info(f"📊 设备规格: 最大流速 {self._max_flow_rate}mL/min | 柱长 {self._column_length}cm 📏") return True async def cleanup(self) -> bool: - """Cleanup virtual column""" - self.logger.info(f"Cleaning up virtual column {self.device_id}") + """Cleanup virtual column 🧹""" + self.logger.info(f"🧹 清理虚拟色谱柱 {self.device_id} 🔚") + + self.data.update({ + "status": "Offline", + "column_state": "Offline", + "current_status": "System offline" + }) + + self.logger.info(f"✅ 色谱柱 {self.device_id} 清理完成 💤") return True - async def run_column(self, from_vessel: str, to_vessel: str, column: str) -> bool: - """Execute column chromatography run - matches RunColumn action""" - self.logger.info(f"Running column separation: {from_vessel} -> {to_vessel} using {column}") + async def run_column(self, from_vessel: str, to_vessel: str, column: str, **kwargs) -> bool: + """Execute column chromatography run - matches RunColumn action 🏛️""" + + # 提取额外参数 + rf = kwargs.get('rf', '0.3') + solvent1 = kwargs.get('solvent1', 'ethyl_acetate') + solvent2 = kwargs.get('solvent2', 'hexane') + ratio = kwargs.get('ratio', '30:70') + + self.logger.info(f"🏛️ 开始柱层析分离: {from_vessel} → {to_vessel} 🚰") + self.logger.info(f" 🧪 使用色谱柱: {column}") + self.logger.info(f" 🎯 Rf值: {rf}") + self.logger.info(f" 🧪 洗脱溶剂: {solvent1}:{solvent2} ({ratio}) 💧") # 更新设备状态 self.data.update({ "status": "Running", "column_state": "Separating", - "current_status": "Column separation in progress", + "current_status": "🏛️ Column separation in progress", "progress": 0.0, - "processed_volume": 0.0 + "processed_volume": 0.0, + "current_from_vessel": from_vessel, + "current_to_vessel": to_vessel, + "current_column": column, + "current_rf": rf, + "current_solvents": f"{solvent1}:{solvent2} ({ratio})" }) # 模拟柱层析分离过程 # 假设处理时间基于流速和柱子长度 - separation_time = (self._column_length * 2) / self._max_flow_rate # 简化计算 + base_time = (self._column_length * 2) / self._max_flow_rate # 简化计算 + separation_time = max(base_time, 20.0) # 最少20秒 + + self.logger.info(f"⏱️ 预计分离时间: {separation_time:.1f}秒 ⌛") + self.logger.info(f"📏 柱参数: 长度 {self._column_length}cm | 流速 {self._max_flow_rate}mL/min 🌊") steps = 20 # 分20个步骤模拟分离过程 step_time = separation_time / steps @@ -74,34 +106,65 @@ class VirtualColumn: progress = (i + 1) / steps * 100 volume_processed = (i + 1) * 5.0 # 假设每步处理5mL + # 不同阶段的状态描述 + if progress <= 25: + phase = "🌊 样品上柱阶段" + phase_emoji = "📥" + elif progress <= 50: + phase = "🧪 洗脱开始" + phase_emoji = "💧" + elif progress <= 75: + phase = "⚗️ 成分分离中" + phase_emoji = "🔄" + else: + phase = "🎯 收集产物" + phase_emoji = "📤" + # 更新状态 + status_msg = f"{phase_emoji} {phase}: {progress:.1f}% | 💧 已处理: {volume_processed:.1f}mL" + self.data.update({ "progress": progress, "processed_volume": volume_processed, - "current_status": f"Column separation: {progress:.1f}% - Processing {volume_processed:.1f}mL" + "current_status": status_msg, + "current_phase": phase }) - self.logger.info(f"Column separation progress: {progress:.1f}%") + # 进度日志(每25%打印一次) + if progress >= 25 and (i + 1) % 5 == 0: # 每5步(25%)打印一次 + self.logger.info(f"📊 分离进度: {progress:.0f}% | {phase} | 💧 {volume_processed:.1f}mL 完成 ✨") # 分离完成 + final_status = f"✅ 柱层析分离完成: {from_vessel} → {to_vessel} | 💧 共处理 {volume_processed:.1f}mL" + self.data.update({ "status": "Idle", "column_state": "Ready", - "current_status": "Column separation completed", - "progress": 100.0 + "current_status": final_status, + "progress": 100.0, + "final_volume": volume_processed }) - self.logger.info(f"Column separation completed: {from_vessel} -> {to_vessel}") + self.logger.info(f"🎉 柱层析分离完成! ✨") + self.logger.info(f"📊 分离结果:") + self.logger.info(f" 🥽 源容器: {from_vessel}") + self.logger.info(f" 🥽 目标容器: {to_vessel}") + self.logger.info(f" 🏛️ 使用色谱柱: {column}") + self.logger.info(f" 💧 处理体积: {volume_processed:.1f}mL") + self.logger.info(f" 🧪 洗脱条件: {solvent1}:{solvent2} ({ratio})") + self.logger.info(f" 🎯 Rf值: {rf}") + self.logger.info(f" ⏱️ 总耗时: {separation_time:.1f}秒 🏁") + return True # 状态属性 @property def status(self) -> str: - return self.data.get("status", "Unknown") + return self.data.get("status", "❓ Unknown") @property def column_state(self) -> str: - return self.data.get("column_state", "Unknown") + return self.data.get("column_state", "❓ Unknown") @property def current_flow_rate(self) -> float: @@ -129,4 +192,12 @@ class VirtualColumn: @property def current_status(self) -> str: - return self.data.get("current_status", "Ready") \ No newline at end of file + return self.data.get("current_status", "📋 Ready") + + @property + def current_phase(self) -> str: + return self.data.get("current_phase", "🏠 待机中") + + @property + def final_volume(self) -> float: + return self.data.get("final_volume", 0.0) \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_filter.py b/unilabos/devices/virtual/virtual_filter.py index ca2e8b2..655934b 100644 --- a/unilabos/devices/virtual/virtual_filter.py +++ b/unilabos/devices/virtual/virtual_filter.py @@ -5,7 +5,7 @@ from typing import Dict, Any, Optional class VirtualFilter: - """Virtual filter device - 完全按照 Filter.action 规范""" + """Virtual filter device - 完全按照 Filter.action 规范 🌊""" def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs): if device_id is None and 'id' in kwargs: @@ -31,8 +31,8 @@ class VirtualFilter: setattr(self, key, value) async def initialize(self) -> bool: - """Initialize virtual filter""" - self.logger.info(f"Initializing virtual filter {self.device_id}") + """Initialize virtual filter 🚀""" + self.logger.info(f"🔧 初始化虚拟过滤器 {self.device_id} ✨") # 按照 Filter.action 的 feedback 字段初始化 self.data.update({ @@ -43,17 +43,21 @@ class VirtualFilter: "current_status": "Ready for filtration", # Filter.action feedback "message": "Ready for filtration" }) + + self.logger.info(f"✅ 过滤器 {self.device_id} 初始化完成 🌊") return True async def cleanup(self) -> bool: - """Cleanup virtual filter""" - self.logger.info(f"Cleaning up virtual filter {self.device_id}") + """Cleanup virtual filter 🧹""" + self.logger.info(f"🧹 清理虚拟过滤器 {self.device_id} 🔚") self.data.update({ "status": "Offline", "current_status": "System offline", "message": "System offline" }) + + self.logger.info(f"✅ 过滤器 {self.device_id} 清理完成 💤") return True async def filter( @@ -66,64 +70,82 @@ class VirtualFilter: continue_heatchill: bool = False, volume: float = 0.0 ) -> bool: - """Execute filter action - 完全按照 Filter.action 参数""" - self.logger.info(f"Filter: vessel={vessel}, filtrate_vessel={filtrate_vessel}") - self.logger.info(f" stir={stir}, stir_speed={stir_speed}, temp={temp}") - self.logger.info(f" continue_heatchill={continue_heatchill}, volume={volume}") + """Execute filter action - 完全按照 Filter.action 参数 🌊""" + + # 🔧 新增:温度自动调整 + original_temp = temp + if temp == 0.0: + temp = 25.0 # 0度自动设置为室温 + self.logger.info(f"🌡️ 温度自动调整: {original_temp}°C → {temp}°C (室温) 🏠") + elif temp < 4.0: + temp = 4.0 # 小于4度自动设置为4度 + self.logger.info(f"🌡️ 温度自动调整: {original_temp}°C → {temp}°C (最低温度) ❄️") + + self.logger.info(f"🌊 开始过滤操作: {vessel} → {filtrate_vessel} 🚰") + self.logger.info(f" 🌪️ 搅拌: {stir} ({stir_speed} RPM)") + self.logger.info(f" 🌡️ 温度: {temp}°C") + self.logger.info(f" 💧 体积: {volume}mL") + self.logger.info(f" 🔥 保持加热: {continue_heatchill}") # 验证参数 if temp > self._max_temp or temp < 4.0: - error_msg = f"温度 {temp}°C 超出范围 (4-{self._max_temp}°C)" - self.logger.error(error_msg) + error_msg = f"🌡️ 温度 {temp}°C 超出范围 (4-{self._max_temp}°C) ⚠️" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"Error: {error_msg}", - "current_status": f"Error: {error_msg}", + "status": f"Error: 温度超出范围 ⚠️", + "current_status": f"Error: 温度超出范围 ⚠️", "message": error_msg }) return False if stir and stir_speed > self._max_stir_speed: - error_msg = f"搅拌速度 {stir_speed} RPM 超出范围 (0-{self._max_stir_speed} RPM)" - self.logger.error(error_msg) + error_msg = f"🌪️ 搅拌速度 {stir_speed} RPM 超出范围 (0-{self._max_stir_speed} RPM) ⚠️" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"Error: {error_msg}", - "current_status": f"Error: {error_msg}", + "status": f"Error: 搅拌速度超出范围 ⚠️", + "current_status": f"Error: 搅拌速度超出范围 ⚠️", "message": error_msg }) return False if volume > self._max_volume: - error_msg = f"过滤体积 {volume} mL 超出范围 (0-{self._max_volume} mL)" - self.logger.error(error_msg) + error_msg = f"💧 过滤体积 {volume} mL 超出范围 (0-{self._max_volume} mL) ⚠️" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"Error: {error_msg}", - "current_status": f"Error: {error_msg}", + "status": f"Error: 体积超出范围 ⚠️", + "current_status": f"Error: 体积超出范围 ⚠️", "message": error_msg }) return False # 开始过滤 filter_volume = volume if volume > 0 else 50.0 + self.logger.info(f"🚀 开始过滤 {filter_volume}mL 液体 💧") self.data.update({ - "status": f"过滤中: {vessel}", + "status": f"🌊 过滤中: {vessel}", "current_temp": temp, "filtered_volume": 0.0, "progress": 0.0, - "current_status": f"Filtering {vessel} → {filtrate_vessel}", - "message": f"Starting filtration: {vessel} → {filtrate_vessel}" + "current_status": f"🌊 Filtering {vessel} → {filtrate_vessel}", + "message": f"🚀 Starting filtration: {vessel} → {filtrate_vessel}" }) try: # 过滤过程 - 实时更新进度 start_time = time_module.time() + # 根据体积和搅拌估算过滤时间 base_time = filter_volume / 5.0 # 5mL/s 基础速度 if stir: base_time *= 0.8 # 搅拌加速过滤 + self.logger.info(f"🌪️ 搅拌加速过滤,预计时间减少20% ⚡") if temp > 50.0: base_time *= 0.7 # 高温加速过滤 + self.logger.info(f"🔥 高温加速过滤,预计时间减少30% ⚡") + filter_time = max(base_time, 10.0) # 最少10秒 + self.logger.info(f"⏱️ 预计过滤时间: {filter_time:.1f}秒 ⌛") while True: current_time = time_module.time() @@ -133,20 +155,24 @@ class VirtualFilter: current_filtered = (progress / 100.0) * filter_volume # 更新状态 - 按照 Filter.action feedback 字段 - status_msg = f"过滤中: {vessel}" + status_msg = f"🌊 过滤中: {vessel}" if stir: - status_msg += f" | 搅拌: {stir_speed} RPM" - status_msg += f" | {temp}°C | {progress:.1f}% | 已过滤: {current_filtered:.1f}mL" + status_msg += f" | 🌪️ 搅拌: {stir_speed} RPM" + status_msg += f" | 🌡️ {temp}°C | 📊 {progress:.1f}% | 💧 已过滤: {current_filtered:.1f}mL" self.data.update({ "progress": progress, # Filter.action feedback "current_temp": temp, # Filter.action feedback "filtered_volume": current_filtered, # Filter.action feedback - "current_status": f"Filtering: {progress:.1f}% complete", # Filter.action feedback + "current_status": f"🌊 Filtering: {progress:.1f}% complete", # Filter.action feedback "status": status_msg, - "message": f"Filtering: {progress:.1f}% complete, {current_filtered:.1f}mL filtered" + "message": f"🌊 Filtering: {progress:.1f}% complete, {current_filtered:.1f}mL filtered" }) + # 进度日志(每25%打印一次) + if progress >= 25 and progress % 25 < 1: + self.logger.info(f"📊 过滤进度: {progress:.0f}% | 💧 {current_filtered:.1f}mL 完成 ✨") + if remaining <= 0: break @@ -154,54 +180,57 @@ class VirtualFilter: # 过滤完成 final_temp = temp if continue_heatchill else 25.0 - final_status = f"过滤完成: {vessel} | {filter_volume}mL → {filtrate_vessel}" + final_status = f"✅ 过滤完成: {vessel} | 💧 {filter_volume}mL → {filtrate_vessel}" if continue_heatchill: - final_status += " | 继续加热搅拌" + final_status += " | 🔥 继续加热搅拌" + self.logger.info(f"🔥 继续保持加热搅拌状态 🌪️") self.data.update({ "status": final_status, "progress": 100.0, # Filter.action feedback "current_temp": final_temp, # Filter.action feedback "filtered_volume": filter_volume, # Filter.action feedback - "current_status": f"Filtration completed: {filter_volume}mL", # Filter.action feedback - "message": f"Filtration completed: {filter_volume}mL filtered from {vessel}" + "current_status": f"✅ Filtration completed: {filter_volume}mL", # Filter.action feedback + "message": f"✅ Filtration completed: {filter_volume}mL filtered from {vessel}" }) - self.logger.info(f"Filtration completed: {filter_volume}mL from {vessel} to {filtrate_vessel}") + self.logger.info(f"🎉 过滤完成! 💧 {filter_volume}mL 从 {vessel} 过滤到 {filtrate_vessel} ✨") + self.logger.info(f"📊 最终状态: 温度 {final_temp}°C | 进度 100% | 体积 {filter_volume}mL 🏁") return True except Exception as e: - self.logger.error(f"Error during filtration: {str(e)}") + error_msg = f"过滤过程中发生错误: {str(e)} 💥" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"过滤错误: {str(e)}", - "current_status": f"Filtration failed: {str(e)}", - "message": f"Filtration failed: {str(e)}" + "status": f"❌ 过滤错误: {str(e)}", + "current_status": f"❌ Filtration failed: {str(e)}", + "message": f"❌ Filtration failed: {str(e)}" }) return False # === 核心状态属性 - 按照 Filter.action feedback 字段 === @property def status(self) -> str: - return self.data.get("status", "Unknown") + return self.data.get("status", "❓ Unknown") @property def progress(self) -> float: - """Filter.action feedback 字段""" + """Filter.action feedback 字段 📊""" return self.data.get("progress", 0.0) @property def current_temp(self) -> float: - """Filter.action feedback 字段""" + """Filter.action feedback 字段 🌡️""" return self.data.get("current_temp", 25.0) @property def filtered_volume(self) -> float: - """Filter.action feedback 字段""" + """Filter.action feedback 字段 💧""" return self.data.get("filtered_volume", 0.0) @property def current_status(self) -> str: - """Filter.action feedback 字段""" + """Filter.action feedback 字段 📋""" return self.data.get("current_status", "") @property diff --git a/unilabos/devices/virtual/virtual_heatchill.py b/unilabos/devices/virtual/virtual_heatchill.py index 541434a..94ab572 100644 --- a/unilabos/devices/virtual/virtual_heatchill.py +++ b/unilabos/devices/virtual/virtual_heatchill.py @@ -4,7 +4,7 @@ import time as time_module # 重命名time模块,避免与参数冲突 from typing import Dict, Any class VirtualHeatChill: - """Virtual heat chill device for HeatChillProtocol testing""" + """Virtual heat chill device for HeatChillProtocol testing 🌡️""" def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): # 处理可能的不同调用方式 @@ -31,94 +31,149 @@ class VirtualHeatChill: for key, value in kwargs.items(): if key not in skip_keys and not hasattr(self, key): setattr(self, key, value) + + print(f"🌡️ === 虚拟温控设备 {self.device_id} 已创建 === ✨") + print(f"🔥 温度范围: {self._min_temp}°C ~ {self._max_temp}°C | 🌪️ 最大搅拌: {self._max_stir_speed} RPM") async def initialize(self) -> bool: - """Initialize virtual heat chill""" - self.logger.info(f"Initializing virtual heat chill {self.device_id}") + """Initialize virtual heat chill 🚀""" + self.logger.info(f"🔧 初始化虚拟温控设备 {self.device_id} ✨") # 初始化状态信息 self.data.update({ - "status": "Idle", + "status": "🏠 待机中", "operation_mode": "Idle", "is_stirring": False, "stir_speed": 0.0, "remaining_time": 0.0, }) + + self.logger.info(f"✅ 温控设备 {self.device_id} 初始化完成 🌡️") + self.logger.info(f"📊 设备规格: 温度范围 {self._min_temp}°C ~ {self._max_temp}°C | 搅拌范围 0 ~ {self._max_stir_speed} RPM") return True async def cleanup(self) -> bool: - """Cleanup virtual heat chill""" - self.logger.info(f"Cleaning up virtual heat chill {self.device_id}") + """Cleanup virtual heat chill 🧹""" + self.logger.info(f"🧹 清理虚拟温控设备 {self.device_id} 🔚") + self.data.update({ - "status": "Offline", + "status": "💤 离线", "operation_mode": "Offline", "is_stirring": False, "stir_speed": 0.0, "remaining_time": 0.0 }) + + self.logger.info(f"✅ 温控设备 {self.device_id} 清理完成 💤") return True - async def heat_chill(self, vessel: str, temp: float, time: float, stir: bool, + async def heat_chill(self, vessel: str, temp: float, time, stir: bool, stir_speed: float, purpose: str) -> bool: - """Execute heat chill action - 按实际时间运行,实时更新剩余时间""" - self.logger.info(f"HeatChill: vessel={vessel}, temp={temp}°C, time={time}s, stir={stir}, stir_speed={stir_speed}") + """Execute heat chill action - 🔧 修复:确保参数类型正确""" - # 验证参数 - if temp > self._max_temp or temp < self._min_temp: - error_msg = f"温度 {temp}°C 超出范围 ({self._min_temp}°C - {self._max_temp}°C)" - self.logger.error(error_msg) + # 🔧 关键修复:确保所有参数类型正确 + try: + temp = float(temp) + time_value = float(time) # 强制转换为浮点数 + stir_speed = float(stir_speed) + stir = bool(stir) + vessel = str(vessel) + purpose = str(purpose) + except (ValueError, TypeError) as e: + error_msg = f"参数类型转换错误: temp={temp}({type(temp)}), time={time}({type(time)}), error={str(e)}" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"Error: {error_msg}", + "status": f"❌ 错误: {error_msg}", + "operation_mode": "Error" + }) + return False + + # 确定温度操作emoji + if temp > 25.0: + temp_emoji = "🔥" + operation_mode = "Heating" + status_action = "加热" + elif temp < 25.0: + temp_emoji = "❄️" + operation_mode = "Cooling" + status_action = "冷却" + else: + temp_emoji = "🌡️" + operation_mode = "Maintaining" + status_action = "保温" + + self.logger.info(f"🌡️ 开始温控操作: {vessel} → {temp}°C {temp_emoji}") + self.logger.info(f" 🥽 容器: {vessel}") + self.logger.info(f" 🎯 目标温度: {temp}°C {temp_emoji}") + self.logger.info(f" ⏰ 持续时间: {time_value}s") + self.logger.info(f" 🌪️ 搅拌: {stir} ({stir_speed} RPM)") + self.logger.info(f" 📝 目的: {purpose}") + + # 验证参数范围 + if temp > self._max_temp or temp < self._min_temp: + error_msg = f"🌡️ 温度 {temp}°C 超出范围 ({self._min_temp}°C - {self._max_temp}°C) ⚠️" + self.logger.error(f"❌ {error_msg}") + self.data.update({ + "status": f"❌ 错误: 温度超出范围 ⚠️", "operation_mode": "Error" }) return False if stir and stir_speed > self._max_stir_speed: - error_msg = f"搅拌速度 {stir_speed} RPM 超出最大值 {self._max_stir_speed} RPM" - self.logger.error(error_msg) + error_msg = f"🌪️ 搅拌速度 {stir_speed} RPM 超出最大值 {self._max_stir_speed} RPM ⚠️" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"Error: {error_msg}", + "status": f"❌ 错误: 搅拌速度超出范围 ⚠️", "operation_mode": "Error" }) return False - # 确定操作模式 - if temp > 25.0: - operation_mode = "Heating" - status_action = "加热" - elif temp < 25.0: - operation_mode = "Cooling" - status_action = "冷却" - else: - operation_mode = "Maintaining" - status_action = "保温" + if time_value <= 0: + error_msg = f"⏰ 时间 {time_value}s 必须大于0 ⚠️" + self.logger.error(f"❌ {error_msg}") + self.data.update({ + "status": f"❌ 错误: 时间参数无效 ⚠️", + "operation_mode": "Error" + }) + return False - # **修复**: 使用重命名的time模块 + # 🔧 修复:使用转换后的时间值 start_time = time_module.time() - total_time = time + total_time = time_value # 使用转换后的浮点数 + + self.logger.info(f"🚀 开始{status_action}程序! 预计用时 {total_time:.1f}秒 ⏱️") # 开始操作 - stir_info = f" | 搅拌: {stir_speed} RPM" if stir else "" + stir_info = f" | 🌪️ 搅拌: {stir_speed} RPM" if stir else "" + self.data.update({ - "status": f"运行中: {status_action} {vessel} 至 {temp}°C | 剩余: {total_time:.0f}s{stir_info}", + "status": f"{temp_emoji} 运行中: {status_action} {vessel} 至 {temp}°C | ⏰ 剩余: {total_time:.0f}s{stir_info}", "operation_mode": operation_mode, "is_stirring": stir, "stir_speed": stir_speed if stir else 0.0, "remaining_time": total_time, }) - # **修复**: 在等待过程中每秒更新剩余时间 + # 在等待过程中每秒更新剩余时间 + last_logged_time = 0 while True: - current_time = time_module.time() # 使用重命名的time模块 + current_time = time_module.time() elapsed = current_time - start_time remaining = max(0, total_time - elapsed) + progress = (elapsed / total_time) * 100 if total_time > 0 else 100 # 更新剩余时间和状态 self.data.update({ "remaining_time": remaining, - "status": f"运行中: {status_action} {vessel} 至 {temp}°C | 剩余: {remaining:.0f}s{stir_info}" + "status": f"{temp_emoji} 运行中: {status_action} {vessel} 至 {temp}°C | ⏰ 剩余: {remaining:.0f}s{stir_info}", + "progress": progress }) + # 进度日志(每25%打印一次) + if progress >= 25 and int(progress) % 25 == 0 and int(progress) != last_logged_time: + self.logger.info(f"📊 {status_action}进度: {progress:.0f}% | ⏰ 剩余: {remaining:.0f}s | {temp_emoji} 目标: {temp}°C ✨") + last_logged_time = int(progress) + # 如果时间到了,退出循环 if remaining <= 0: break @@ -127,71 +182,114 @@ class VirtualHeatChill: await asyncio.sleep(1.0) # 操作完成 - final_stir_info = f" | 搅拌: {stir_speed} RPM" if stir else "" + final_stir_info = f" | 🌪️ 搅拌: {stir_speed} RPM" if stir else "" + self.data.update({ - "status": f"完成: {vessel} 已达到 {temp}°C | 用时: {total_time:.0f}s{final_stir_info}", + "status": f"✅ 完成: {vessel} 已达到 {temp}°C {temp_emoji} | ⏱️ 用时: {total_time:.0f}s{final_stir_info}", "operation_mode": "Completed", "remaining_time": 0.0, "is_stirring": False, - "stir_speed": 0.0 + "stir_speed": 0.0, + "progress": 100.0 }) - self.logger.info(f"HeatChill completed for vessel {vessel} at {temp}°C after {total_time}s") + self.logger.info(f"🎉 温控操作完成! ✨") + self.logger.info(f"📊 操作结果:") + self.logger.info(f" 🥽 容器: {vessel}") + self.logger.info(f" 🌡️ 达到温度: {temp}°C {temp_emoji}") + self.logger.info(f" ⏱️ 总用时: {total_time:.0f}s") + if stir: + self.logger.info(f" 🌪️ 搅拌速度: {stir_speed} RPM") + self.logger.info(f" 📝 操作目的: {purpose} 🏁") + return True async def heat_chill_start(self, vessel: str, temp: float, purpose: str) -> bool: - """Start continuous heat chill""" - self.logger.info(f"HeatChillStart: vessel={vessel}, temp={temp}°C") + """Start continuous heat chill 🔄""" - # 验证参数 - if temp > self._max_temp or temp < self._min_temp: - error_msg = f"温度 {temp}°C 超出范围 ({self._min_temp}°C - {self._max_temp}°C)" - self.logger.error(error_msg) + # 🔧 添加类型转换 + try: + temp = float(temp) + vessel = str(vessel) + purpose = str(purpose) + except (ValueError, TypeError) as e: + error_msg = f"参数类型转换错误: {str(e)}" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"Error: {error_msg}", + "status": f"❌ 错误: {error_msg}", "operation_mode": "Error" }) return False - # 确定操作模式 + # 确定温度操作emoji if temp > 25.0: + temp_emoji = "🔥" operation_mode = "Heating" status_action = "持续加热" elif temp < 25.0: + temp_emoji = "❄️" operation_mode = "Cooling" status_action = "持续冷却" else: + temp_emoji = "🌡️" operation_mode = "Maintaining" status_action = "恒温保持" + self.logger.info(f"🔄 启动持续温控: {vessel} → {temp}°C {temp_emoji}") + self.logger.info(f" 🥽 容器: {vessel}") + self.logger.info(f" 🎯 目标温度: {temp}°C {temp_emoji}") + self.logger.info(f" 🔄 模式: {status_action}") + self.logger.info(f" 📝 目的: {purpose}") + + # 验证参数 + if temp > self._max_temp or temp < self._min_temp: + error_msg = f"🌡️ 温度 {temp}°C 超出范围 ({self._min_temp}°C - {self._max_temp}°C) ⚠️" + self.logger.error(f"❌ {error_msg}") + self.data.update({ + "status": f"❌ 错误: 温度超出范围 ⚠️", + "operation_mode": "Error" + }) + return False + self.data.update({ - "status": f"启动: {status_action} {vessel} 至 {temp}°C | 持续运行", + "status": f"🔄 启动: {status_action} {vessel} 至 {temp}°C {temp_emoji} | ♾️ 持续运行", "operation_mode": operation_mode, "is_stirring": False, "stir_speed": 0.0, "remaining_time": -1.0, # -1 表示持续运行 }) + self.logger.info(f"✅ 持续温控已启动! {temp_emoji} {status_action}模式 🚀") return True async def heat_chill_stop(self, vessel: str) -> bool: - """Stop heat chill""" - self.logger.info(f"HeatChillStop: vessel={vessel}") + """Stop heat chill 🛑""" + + # 🔧 添加类型转换 + try: + vessel = str(vessel) + except (ValueError, TypeError) as e: + error_msg = f"参数类型转换错误: {str(e)}" + self.logger.error(f"❌ {error_msg}") + return False + + self.logger.info(f"🛑 停止温控: {vessel}") self.data.update({ - "status": f"已停止: {vessel} 温控停止", + "status": f"🛑 已停止: {vessel} 温控停止", "operation_mode": "Stopped", "is_stirring": False, "stir_speed": 0.0, "remaining_time": 0.0, }) + self.logger.info(f"✅ 温控设备已停止 {vessel} 的温度控制 🏁") return True # 状态属性 @property def status(self) -> str: - return self.data.get("status", "Idle") + return self.data.get("status", "🏠 待机中") @property def operation_mode(self) -> str: @@ -207,4 +305,20 @@ class VirtualHeatChill: @property def remaining_time(self) -> float: - return self.data.get("remaining_time", 0.0) \ No newline at end of file + return self.data.get("remaining_time", 0.0) + + @property + def progress(self) -> float: + return self.data.get("progress", 0.0) + + @property + def max_temp(self) -> float: + return self._max_temp + + @property + def min_temp(self) -> float: + return self._min_temp + + @property + def max_stir_speed(self) -> float: + return self._max_stir_speed \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_multiway_valve.py b/unilabos/devices/virtual/virtual_multiway_valve.py index c24b7b1..468175c 100644 --- a/unilabos/devices/virtual/virtual_multiway_valve.py +++ b/unilabos/devices/virtual/virtual_multiway_valve.py @@ -1,16 +1,20 @@ import time +import logging from typing import Union, Dict, Optional class VirtualMultiwayValve: """ - 虚拟九通阀门 - 0号位连接transfer pump,1-8号位连接其他设备 + 虚拟九通阀门 - 0号位连接transfer pump,1-8号位连接其他设备 🔄 """ def __init__(self, port: str = "VIRTUAL", positions: int = 8): self.port = port self.max_positions = positions # 1-8号位 self.total_positions = positions + 1 # 0-8号位,共9个位置 + # 添加日志记录器 + self.logger = logging.getLogger(f"VirtualMultiwayValve.{port}") + # 状态属性 self._status = "Idle" self._valve_state = "Ready" @@ -29,6 +33,10 @@ class VirtualMultiwayValve: 7: "port_7", # 7号位 8: "port_8" # 8号位 } + + print(f"🔄 === 虚拟多通阀门已创建 === ✨") + print(f"🎯 端口: {port} | 📊 位置范围: 0-{self.max_positions} | 🏠 初始位置: 0 (transfer_pump)") + self.logger.info(f"🔧 多通阀门初始化: 端口={port}, 最大位置={self.max_positions}") @property def status(self) -> str: @@ -47,16 +55,16 @@ class VirtualMultiwayValve: return self._target_position def get_current_position(self) -> int: - """获取当前阀门位置""" + """获取当前阀门位置 📍""" return self._current_position def get_current_port(self) -> str: - """获取当前连接的端口名称""" + """获取当前连接的端口名称 🔌""" return self.position_map.get(self._current_position, "unknown") def set_position(self, command: Union[int, str]): """ - 设置阀门位置 - 支持0-8位置 + 设置阀门位置 - 支持0-8位置 🎯 Args: command: 目标位置 (0-8) 或位置字符串 @@ -71,7 +79,22 @@ class VirtualMultiwayValve: pos = int(command) if pos < 0 or pos > self.max_positions: - raise ValueError(f"Position must be between 0 and {self.max_positions}") + error_msg = f"位置必须在 0-{self.max_positions} 范围内" + self.logger.error(f"❌ {error_msg}: 请求位置={pos}") + raise ValueError(error_msg) + + # 获取位置描述emoji + if pos == 0: + pos_emoji = "🚰" + pos_desc = "泵位置" + else: + pos_emoji = "🔌" + pos_desc = f"端口{pos}" + + old_position = self._current_position + old_port = self.get_current_port() + + self.logger.info(f"🔄 阀门切换: {old_position}({old_port}) → {pos}({self.position_map.get(pos, 'unknown')}) {pos_emoji}") self._status = "Busy" self._valve_state = "Moving" @@ -79,104 +102,139 @@ class VirtualMultiwayValve: # 模拟阀门切换时间 switch_time = abs(self._current_position - pos) * 0.5 # 每个位置0.5秒 - time.sleep(switch_time) + + if switch_time > 0: + self.logger.info(f"⏱️ 阀门移动中... 预计用时: {switch_time:.1f}秒 🔄") + time.sleep(switch_time) self._current_position = pos self._status = "Idle" self._valve_state = "Ready" current_port = self.get_current_port() - return f"Position set to {pos} ({current_port})" + success_msg = f"✅ 阀门已切换到位置 {pos} ({current_port}) {pos_emoji}" + + self.logger.info(success_msg) + return success_msg except ValueError as e: + error_msg = f"❌ 阀门切换失败: {str(e)}" self._status = "Error" self._valve_state = "Error" - return f"Error: {str(e)}" + self.logger.error(error_msg) + return error_msg def set_to_pump_position(self): - """切换到transfer pump位置(0号位)""" + """切换到transfer pump位置(0号位)🚰""" + self.logger.info(f"🚰 切换到泵位置...") return self.set_position(0) def set_to_port(self, port_number: int): """ - 切换到指定端口位置 + 切换到指定端口位置 🔌 Args: port_number: 端口号 (1-8) """ if port_number < 1 or port_number > self.max_positions: - raise ValueError(f"Port number must be between 1 and {self.max_positions}") + error_msg = f"端口号必须在 1-{self.max_positions} 范围内" + self.logger.error(f"❌ {error_msg}: 请求端口={port_number}") + raise ValueError(error_msg) + + self.logger.info(f"🔌 切换到端口 {port_number}...") return self.set_position(port_number) def open(self): - """打开阀门 - 设置到transfer pump位置(0号位)""" + """打开阀门 - 设置到transfer pump位置(0号位)🔓""" + self.logger.info(f"🔓 打开阀门,设置到泵位置...") return self.set_to_pump_position() def close(self): - """关闭阀门 - 对于多通阀门,设置到一个"关闭"状态""" + """关闭阀门 - 对于多通阀门,设置到一个"关闭"状态 🔒""" + self.logger.info(f"🔒 关闭阀门...") + self._status = "Busy" self._valve_state = "Closing" time.sleep(0.5) - + # 可以选择保持当前位置或设置特殊关闭状态 self._status = "Idle" self._valve_state = "Closed" - return f"Valve closed at position {self._current_position}" + close_msg = f"🔒 阀门已关闭,保持在位置 {self._current_position} ({self.get_current_port()})" + self.logger.info(close_msg) + return close_msg def get_valve_position(self) -> int: - """获取阀门位置 - 兼容性方法""" + """获取阀门位置 - 兼容性方法 📍""" return self._current_position def is_at_position(self, position: int) -> bool: - """检查是否在指定位置""" - return self._current_position == position + """检查是否在指定位置 🎯""" + result = self._current_position == position + # 删除debug日志:self.logger.debug(f"🎯 位置检查: 当前={self._current_position}, 目标={position}, 匹配={result}") + return result def is_at_pump_position(self) -> bool: - """检查是否在transfer pump位置""" - return self._current_position == 0 + """检查是否在transfer pump位置 🚰""" + result = self._current_position == 0 + # 删除debug日志:pump_status = "是" if result else "否" + # 删除debug日志:self.logger.debug(f"🚰 泵位置检查: {pump_status} (当前位置: {self._current_position})") + return result def is_at_port(self, port_number: int) -> bool: - """检查是否在指定端口位置""" - return self._current_position == port_number + """检查是否在指定端口位置 🔌""" + result = self._current_position == port_number + # 删除debug日志:port_status = "是" if result else "否" + # 删除debug日志:self.logger.debug(f"🔌 端口{port_number}检查: {port_status} (当前位置: {self._current_position})") + return result def get_available_positions(self) -> list: - """获取可用位置列表""" - return list(range(0, self.max_positions + 1)) + """获取可用位置列表 📋""" + positions = list(range(0, self.max_positions + 1)) + # 删除debug日志:self.logger.debug(f"📋 可用位置: {positions}") + return positions def get_available_ports(self) -> Dict[int, str]: - """获取可用端口映射""" + """获取可用端口映射 🗺️""" + # 删除debug日志:self.logger.debug(f"🗺️ 端口映射: {self.position_map}") return self.position_map.copy() def reset(self): - """重置阀门到transfer pump位置(0号位)""" + """重置阀门到transfer pump位置(0号位)🔄""" + self.logger.info(f"🔄 重置阀门到泵位置...") return self.set_position(0) def switch_between_pump_and_port(self, port_number: int): """ - 在transfer pump位置和指定端口之间切换 + 在transfer pump位置和指定端口之间切换 🔄 Args: port_number: 目标端口号 (1-8) """ if self._current_position == 0: # 当前在pump位置,切换到指定端口 + self.logger.info(f"🔄 从泵位置切换到端口 {port_number}...") return self.set_to_port(port_number) else: # 当前在某个端口,切换到pump位置 + self.logger.info(f"🔄 从端口 {self._current_position} 切换到泵位置...") return self.set_to_pump_position() def get_flow_path(self) -> str: - """获取当前流路路径描述""" + """获取当前流路路径描述 🌊""" current_port = self.get_current_port() if self._current_position == 0: - return f"Transfer pump connected (position {self._current_position})" + flow_path = f"🚰 转移泵已连接 (位置 {self._current_position})" else: - return f"Port {self._current_position} connected ({current_port})" + flow_path = f"🔌 端口 {self._current_position} 已连接 ({current_port})" + + # 删除debug日志:self.logger.debug(f"🌊 当前流路: {flow_path}") + return flow_path def get_info(self) -> dict: - """获取阀门详细信息""" - return { + """获取阀门详细信息 📊""" + info = { "port": self.port, "max_positions": self.max_positions, "total_positions": self.total_positions, @@ -188,18 +246,25 @@ class VirtualMultiwayValve: "flow_path": self.get_flow_path(), "position_map": self.position_map } + + # 删除debug日志:self.logger.debug(f"📊 阀门信息: 位置={self._current_position}, 状态={self._status}, 端口={self.get_current_port()}") + return info def __str__(self): - return f"VirtualMultiwayValve(Position: {self._current_position}/{self.max_positions}, Port: {self.get_current_port()}, Status: {self._status})" + current_port = self.get_current_port() + status_emoji = "✅" if self._status == "Idle" else "🔄" if self._status == "Busy" else "❌" + + return f"🔄 VirtualMultiwayValve({status_emoji} 位置: {self._current_position}/{self.max_positions}, 端口: {current_port}, 状态: {self._status})" def set_valve_position(self, command: Union[int, str]): """ - 设置阀门位置 - 兼容pump_protocol调用 + 设置阀门位置 - 兼容pump_protocol调用 🎯 这是set_position的别名方法,用于兼容pump_protocol.py Args: command: 目标位置 (0-8) 或位置字符串 """ + # 删除debug日志:self.logger.debug(f"🎯 兼容性调用: set_valve_position({command})") return self.set_position(command) @@ -207,25 +272,35 @@ class VirtualMultiwayValve: if __name__ == "__main__": valve = VirtualMultiwayValve() - print("=== 虚拟九通阀门测试 ===") - print(f"初始状态: {valve}") - print(f"当前流路: {valve.get_flow_path()}") + print("🔄 === 虚拟九通阀门测试 === ✨") + print(f"🏠 初始状态: {valve}") + print(f"🌊 当前流路: {valve.get_flow_path()}") # 切换到试剂瓶1(1号位) - print(f"\n切换到1号位: {valve.set_position(1)}") - print(f"当前状态: {valve}") + print(f"\n🔌 切换到1号位: {valve.set_position(1)}") + print(f"📍 当前状态: {valve}") # 切换到transfer pump位置(0号位) - print(f"\n切换到pump位置: {valve.set_to_pump_position()}") - print(f"当前状态: {valve}") + print(f"\n🚰 切换到pump位置: {valve.set_to_pump_position()}") + print(f"📍 当前状态: {valve}") # 切换到试剂瓶2(2号位) - print(f"\n切换到2号位: {valve.set_to_port(2)}") - print(f"当前状态: {valve}") + print(f"\n🔌 切换到2号位: {valve.set_to_port(2)}") + print(f"📍 当前状态: {valve}") # 显示所有可用位置 - print(f"\n可用位置: {valve.get_available_positions()}") - print(f"端口映射: {valve.get_available_ports()}") + print(f"\n📋 可用位置: {valve.get_available_positions()}") + print(f"🗺️ 端口映射: {valve.get_available_ports()}") # 获取详细信息 - print(f"\n详细信息: {valve.get_info()}") \ No newline at end of file + print(f"\n📊 详细信息: {valve.get_info()}") + + # 测试切换功能 + print(f"\n🔄 智能切换测试:") + print(f"当前位置: {valve._current_position}") + print(f"切换结果: {valve.switch_between_pump_and_port(3)}") + print(f"新位置: {valve._current_position}") + + # 重置测试 + print(f"\n🔄 重置测试: {valve.reset()}") + print(f"📍 重置后状态: {valve}") \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_rotavap.py b/unilabos/devices/virtual/virtual_rotavap.py index ba01c7b..dd1cca4 100644 --- a/unilabos/devices/virtual/virtual_rotavap.py +++ b/unilabos/devices/virtual/virtual_rotavap.py @@ -3,9 +3,12 @@ import logging import time as time_module from typing import Dict, Any, Optional +def debug_print(message): + """调试输出 🔍""" + print(f"🌪️ [ROTAVAP] {message}", flush=True) class VirtualRotavap: - """Virtual rotary evaporator device - 简化版,只保留核心功能""" + """Virtual rotary evaporator device - 简化版,只保留核心功能 🌪️""" def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs): # 处理可能的不同调用方式 @@ -32,13 +35,16 @@ class VirtualRotavap: if key not in skip_keys and not hasattr(self, key): setattr(self, key, value) + print(f"🌪️ === 虚拟旋转蒸发仪 {self.device_id} 已创建 === ✨") + print(f"🔥 温度范围: 10°C ~ {self._max_temp}°C | 🌀 转速范围: 10 ~ {self._max_rotation_speed} RPM") + async def initialize(self) -> bool: - """Initialize virtual rotary evaporator""" - self.logger.info(f"Initializing virtual rotary evaporator {self.device_id}") + """Initialize virtual rotary evaporator 🚀""" + self.logger.info(f"🔧 初始化虚拟旋转蒸发仪 {self.device_id} ✨") # 只保留核心状态 self.data.update({ - "status": "Idle", + "status": "🏠 待机中", "rotavap_state": "Ready", # Ready, Evaporating, Completed, Error "current_temp": 25.0, "target_temp": 25.0, @@ -47,22 +53,27 @@ class VirtualRotavap: "evaporated_volume": 0.0, "progress": 0.0, "remaining_time": 0.0, - "message": "Ready for evaporation" + "message": "🌪️ Ready for evaporation" }) + + self.logger.info(f"✅ 旋转蒸发仪 {self.device_id} 初始化完成 🌪️") + self.logger.info(f"📊 设备规格: 温度范围 10°C ~ {self._max_temp}°C | 转速范围 10 ~ {self._max_rotation_speed} RPM") return True async def cleanup(self) -> bool: - """Cleanup virtual rotary evaporator""" - self.logger.info(f"Cleaning up virtual rotary evaporator {self.device_id}") + """Cleanup virtual rotary evaporator 🧹""" + self.logger.info(f"🧹 清理虚拟旋转蒸发仪 {self.device_id} 🔚") self.data.update({ - "status": "Offline", + "status": "💤 离线", "rotavap_state": "Offline", "current_temp": 25.0, "rotation_speed": 0.0, "vacuum_pressure": 1.0, - "message": "System offline" + "message": "💤 System offline" }) + + self.logger.info(f"✅ 旋转蒸发仪 {self.device_id} 清理完成 💤") return True async def evaporate( @@ -70,46 +81,88 @@ class VirtualRotavap: vessel: str, pressure: float = 0.1, temp: float = 60.0, - time: float = 1800.0, # 30分钟默认 - stir_speed: float = 100.0 + time: float = 180.0, + stir_speed: float = 100.0, + solvent: str = "", + **kwargs ) -> bool: - """Execute evaporate action - 简化的蒸发流程""" - self.logger.info(f"Evaporate: vessel={vessel}, pressure={pressure} bar, temp={temp}°C, time={time}s, rotation={stir_speed} RPM") - + """Execute evaporate action - 简化版 🌪️""" + + # 🔧 简化处理:如果vessel就是设备自己,直接操作 + if vessel == self.device_id: + debug_print(f"🎯 在设备 {self.device_id} 上直接执行蒸发操作") + actual_vessel = self.device_id + else: + actual_vessel = vessel + + # 参数预处理 + if solvent: + self.logger.info(f"🧪 识别到溶剂: {solvent}") + # 根据溶剂调整参数 + solvent_lower = solvent.lower() + if any(s in solvent_lower for s in ['water', 'aqueous']): + temp = max(temp, 80.0) + pressure = max(pressure, 0.2) + self.logger.info(f"💧 水系溶剂:调整参数 → 温度 {temp}°C, 压力 {pressure} bar") + elif any(s in solvent_lower for s in ['ethanol', 'methanol', 'acetone']): + temp = min(temp, 50.0) + pressure = min(pressure, 0.05) + self.logger.info(f"⚡ 易挥发溶剂:调整参数 → 温度 {temp}°C, 压力 {pressure} bar") + + self.logger.info(f"🌪️ 开始蒸发操作: {actual_vessel}") + self.logger.info(f" 🥽 容器: {actual_vessel}") + self.logger.info(f" 🌡️ 温度: {temp}°C") + self.logger.info(f" 💨 真空度: {pressure} bar") + self.logger.info(f" ⏰ 时间: {time}s") + self.logger.info(f" 🌀 转速: {stir_speed} RPM") + if solvent: + self.logger.info(f" 🧪 溶剂: {solvent}") + # 验证参数 if temp > self._max_temp or temp < 10.0: - error_msg = f"温度 {temp}°C 超出范围 (10-{self._max_temp}°C)" - self.logger.error(error_msg) + error_msg = f"🌡️ 温度 {temp}°C 超出范围 (10-{self._max_temp}°C) ⚠️" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"Error: {error_msg}", + "status": f"❌ 错误: 温度超出范围", "rotavap_state": "Error", + "current_temp": 25.0, + "progress": 0.0, + "evaporated_volume": 0.0, "message": error_msg }) return False if stir_speed > self._max_rotation_speed or stir_speed < 10.0: - error_msg = f"旋转速度 {stir_speed} RPM 超出范围 (10-{self._max_rotation_speed} RPM)" - self.logger.error(error_msg) + error_msg = f"🌀 旋转速度 {stir_speed} RPM 超出范围 (10-{self._max_rotation_speed} RPM) ⚠️" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"Error: {error_msg}", + "status": f"❌ 错误: 转速超出范围", "rotavap_state": "Error", + "current_temp": 25.0, + "progress": 0.0, + "evaporated_volume": 0.0, "message": error_msg }) return False if pressure < 0.01 or pressure > 1.0: - error_msg = f"真空度 {pressure} bar 超出范围 (0.01-1.0 bar)" - self.logger.error(error_msg) + error_msg = f"💨 真空度 {pressure} bar 超出范围 (0.01-1.0 bar) ⚠️" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"Error: {error_msg}", + "status": f"❌ 错误: 压力超出范围", "rotavap_state": "Error", + "current_temp": 25.0, + "progress": 0.0, + "evaporated_volume": 0.0, "message": error_msg }) return False # 开始蒸发 + self.logger.info(f"🚀 启动蒸发程序! 预计用时 {time/60:.1f}分钟 ⏱️") + self.data.update({ - "status": f"蒸发中: {vessel}", + "status": f"🌪️ 蒸发中: {actual_vessel}", "rotavap_state": "Evaporating", "current_temp": temp, "target_temp": temp, @@ -118,13 +171,14 @@ class VirtualRotavap: "remaining_time": time, "progress": 0.0, "evaporated_volume": 0.0, - "message": f"Evaporating {vessel} at {temp}°C, {pressure} bar, {stir_speed} RPM" + "message": f"🌪️ Evaporating {actual_vessel} at {temp}°C, {pressure} bar, {stir_speed} RPM" }) try: # 蒸发过程 - 实时更新进度 start_time = time_module.time() total_time = time + last_logged_progress = 0 while True: current_time = time_module.time() @@ -132,18 +186,31 @@ class VirtualRotavap: remaining = max(0, total_time - elapsed) progress = min(100.0, (elapsed / total_time) * 100) - # 模拟蒸发体积 - evaporated_vol = progress * 0.8 # 假设最多蒸发80mL + # 模拟蒸发体积 - 根据溶剂类型调整 + if solvent and any(s in solvent.lower() for s in ['water', 'aqueous']): + evaporated_vol = progress * 0.6 # 水系溶剂蒸发慢 + elif solvent and any(s in solvent.lower() for s in ['ethanol', 'methanol', 'acetone']): + evaporated_vol = progress * 1.0 # 易挥发溶剂蒸发快 + else: + evaporated_vol = progress * 0.8 # 默认蒸发量 + + # 🔧 更新状态 - 确保包含所有必需字段 + status_msg = f"🌪️ 蒸发中: {actual_vessel} | 🌡️ {temp}°C | 💨 {pressure} bar | 🌀 {stir_speed} RPM | 📊 {progress:.1f}% | ⏰ 剩余: {remaining:.0f}s" - # 更新状态 self.data.update({ "remaining_time": remaining, "progress": progress, "evaporated_volume": evaporated_vol, - "status": f"蒸发中: {vessel} | {temp}°C | {pressure} bar | {progress:.1f}% | 剩余: {remaining:.0f}s", - "message": f"Evaporating: {progress:.1f}% complete, {remaining:.0f}s remaining" + "current_temp": temp, + "status": status_msg, + "message": f"🌪️ Evaporating: {progress:.1f}% complete, 💧 {evaporated_vol:.1f}mL evaporated, ⏰ {remaining:.0f}s remaining" }) + # 进度日志(每25%打印一次) + if progress >= 25 and int(progress) % 25 == 0 and int(progress) != last_logged_progress: + self.logger.info(f"📊 蒸发进度: {progress:.0f}% | 💧 已蒸发: {evaporated_vol:.1f}mL | ⏰ 剩余: {remaining:.0f}s ✨") + last_logged_progress = int(progress) + # 时间到了,退出循环 if remaining <= 0: break @@ -152,40 +219,59 @@ class VirtualRotavap: await asyncio.sleep(1.0) # 蒸发完成 - final_evaporated = 80.0 + if solvent and any(s in solvent.lower() for s in ['water', 'aqueous']): + final_evaporated = 60.0 # 水系溶剂 + elif solvent and any(s in solvent.lower() for s in ['ethanol', 'methanol', 'acetone']): + final_evaporated = 100.0 # 易挥发溶剂 + else: + final_evaporated = 80.0 # 默认 + self.data.update({ - "status": f"蒸发完成: {vessel} | 蒸发量: {final_evaporated:.1f}mL", + "status": f"✅ 蒸发完成: {actual_vessel} | 💧 蒸发量: {final_evaporated:.1f}mL", "rotavap_state": "Completed", "evaporated_volume": final_evaporated, "progress": 100.0, + "current_temp": temp, "remaining_time": 0.0, - "current_temp": 25.0, # 冷却下来 - "rotation_speed": 0.0, # 停止旋转 - "vacuum_pressure": 1.0, # 恢复大气压 - "message": f"Evaporation completed: {final_evaporated}mL evaporated from {vessel}" + "rotation_speed": 0.0, + "vacuum_pressure": 1.0, + "message": f"✅ Evaporation completed: {final_evaporated}mL evaporated from {actual_vessel}" }) - self.logger.info(f"Evaporation completed: {final_evaporated}mL evaporated from {vessel}") + self.logger.info(f"🎉 蒸发操作完成! ✨") + self.logger.info(f"📊 蒸发结果:") + self.logger.info(f" 🥽 容器: {actual_vessel}") + self.logger.info(f" 💧 蒸发量: {final_evaporated:.1f}mL") + self.logger.info(f" 🌡️ 蒸发温度: {temp}°C") + self.logger.info(f" 💨 真空度: {pressure} bar") + self.logger.info(f" 🌀 旋转速度: {stir_speed} RPM") + self.logger.info(f" ⏱️ 总用时: {total_time:.0f}s") + if solvent: + self.logger.info(f" 🧪 处理溶剂: {solvent} 🏁") + return True except Exception as e: # 出错处理 - self.logger.error(f"Error during evaporation: {str(e)}") + error_msg = f"蒸发过程中发生错误: {str(e)} 💥" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"蒸发错误: {str(e)}", + "status": f"❌ 蒸发错误: {str(e)}", "rotavap_state": "Error", "current_temp": 25.0, + "progress": 0.0, + "evaporated_volume": 0.0, "rotation_speed": 0.0, "vacuum_pressure": 1.0, - "message": f"Evaporation failed: {str(e)}" + "message": f"❌ Evaporation failed: {str(e)}" }) return False # === 核心状态属性 === @property def status(self) -> str: - return self.data.get("status", "Unknown") + return self.data.get("status", "❓ Unknown") @property def rotavap_state(self) -> str: diff --git a/unilabos/devices/virtual/virtual_solenoid_valve.py b/unilabos/devices/virtual/virtual_solenoid_valve.py index f25cc84..54a1e6d 100644 --- a/unilabos/devices/virtual/virtual_solenoid_valve.py +++ b/unilabos/devices/virtual/virtual_solenoid_valve.py @@ -43,10 +43,25 @@ class VirtualSolenoidValve: def is_open(self) -> bool: return self._is_open - def get_valve_position(self) -> str: + @property + def valve_position(self) -> str: """获取阀门位置状态""" return "OPEN" if self._is_open else "CLOSED" + @property + def state(self) -> dict: + """获取阀门完整状态""" + return { + "device_id": self.device_id, + "port": self.port, + "voltage": self.voltage, + "response_time": self.response_time, + "is_open": self._is_open, + "valve_state": self._valve_state, + "status": self._status, + "position": self.valve_position + } + async def set_valve_position(self, command: str = None, **kwargs): """ 设置阀门位置 - ROS动作接口 @@ -91,7 +106,7 @@ class VirtualSolenoidValve: return { "success": True, "message": result_msg, - "valve_position": self.get_valve_position() + "valve_position": self.valve_position } async def open(self, **kwargs): @@ -102,21 +117,25 @@ class VirtualSolenoidValve: """关闭电磁阀 - ROS动作接口""" return await self.set_valve_position(command="CLOSED") - async def set_state(self, command: Union[bool, str], **kwargs): + async def set_status(self, string: str = None, **kwargs): """ - 设置阀门状态 - 兼容 SendCmd 类型 + 设置阀门状态 - 兼容 StrSingleInput 类型 Args: - command: True/False 或 "open"/"close" + string: "ON"/"OFF" 或 "OPEN"/"CLOSED" """ - if isinstance(command, bool): - cmd_str = "OPEN" if command else "CLOSED" - elif isinstance(command, str): - cmd_str = command - else: - return {"success": False, "message": "Invalid command type"} + if string is None: + return {"success": False, "message": "Missing string parameter"} - return await self.set_valve_position(command=cmd_str) + # 将 string 参数转换为 command 参数 + if string.upper() in ["ON", "OPEN"]: + command = "OPEN" + elif string.upper() in ["OFF", "CLOSED"]: + command = "CLOSED" + else: + command = string + + return await self.set_valve_position(command=command) def toggle(self): """切换阀门状态""" @@ -129,19 +148,6 @@ class VirtualSolenoidValve: """检查阀门是否关闭""" return not self._is_open - def get_state(self) -> dict: - """获取阀门完整状态""" - return { - "device_id": self.device_id, - "port": self.port, - "voltage": self.voltage, - "response_time": self.response_time, - "is_open": self._is_open, - "valve_state": self._valve_state, - "status": self._status, - "position": self.get_valve_position() - } - async def reset(self): """重置阀门到关闭状态""" return await self.close() \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_solid_dispenser.py b/unilabos/devices/virtual/virtual_solid_dispenser.py new file mode 100644 index 0000000..439c348 --- /dev/null +++ b/unilabos/devices/virtual/virtual_solid_dispenser.py @@ -0,0 +1,389 @@ +import asyncio +import logging +import re +from typing import Dict, Any, Optional + +class VirtualSolidDispenser: + """ + 虚拟固体粉末加样器 - 用于处理 Add Protocol 中的固体试剂添加 ⚗️ + + 特点: + - 高兼容性:缺少参数不报错 ✅ + - 智能识别:自动查找固体试剂瓶 🔍 + - 简单反馈:成功/失败 + 消息 📊 + """ + + def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): + self.device_id = device_id or "virtual_solid_dispenser" + self.config = config or {} + + # 设备参数 + self.max_capacity = float(self.config.get('max_capacity', 100.0)) # 最大加样量 (g) + self.precision = float(self.config.get('precision', 0.001)) # 精度 (g) + + # 状态变量 + self._status = "Idle" + self._current_reagent = "" + self._dispensed_amount = 0.0 + self._total_operations = 0 + + self.logger = logging.getLogger(f"VirtualSolidDispenser.{self.device_id}") + + print(f"⚗️ === 虚拟固体分配器 {self.device_id} 创建成功! === ✨") + print(f"📊 设备规格: 最大容量 {self.max_capacity}g | 精度 {self.precision}g 🎯") + + async def initialize(self) -> bool: + """初始化固体加样器 🚀""" + self.logger.info(f"🔧 初始化固体分配器 {self.device_id} ✨") + self._status = "Ready" + self._current_reagent = "" + self._dispensed_amount = 0.0 + + self.logger.info(f"✅ 固体分配器 {self.device_id} 初始化完成 ⚗️") + return True + + async def cleanup(self) -> bool: + """清理固体加样器 🧹""" + self.logger.info(f"🧹 清理固体分配器 {self.device_id} 🔚") + self._status = "Idle" + + self.logger.info(f"✅ 固体分配器 {self.device_id} 清理完成 💤") + return True + + def parse_mass_string(self, mass_str: str) -> float: + """ + 解析质量字符串为数值 (g) ⚖️ + + 支持格式: "2.9 g", "19.3g", "4.5 mg", "1.2 kg" 等 + """ + if not mass_str or not isinstance(mass_str, str): + return 0.0 + + # 移除空格并转小写 + mass_clean = mass_str.strip().lower() + + # 正则匹配数字和单位 + pattern = r'(\d+(?:\.\d+)?)\s*([a-z]*)' + match = re.search(pattern, mass_clean) + + if not match: + self.logger.debug(f"🔍 无法解析质量字符串: {mass_str}") + return 0.0 + + try: + value = float(match.group(1)) + unit = match.group(2) or 'g' # 默认单位 g + + # 单位转换为 g + unit_multipliers = { + 'g': 1.0, + 'gram': 1.0, + 'grams': 1.0, + 'mg': 0.001, + 'milligram': 0.001, + 'milligrams': 0.001, + 'kg': 1000.0, + 'kilogram': 1000.0, + 'kilograms': 1000.0, + 'μg': 0.000001, + 'ug': 0.000001, + 'microgram': 0.000001, + 'micrograms': 0.000001, + } + + multiplier = unit_multipliers.get(unit, 1.0) + result = value * multiplier + + self.logger.debug(f"⚖️ 质量解析: {mass_str} → {result:.6f}g (原值: {value} {unit})") + return result + + except (ValueError, TypeError): + self.logger.warning(f"⚠️ 无法解析质量字符串: {mass_str}") + return 0.0 + + def parse_mol_string(self, mol_str: str) -> float: + """ + 解析摩尔数字符串为数值 (mol) 🧮 + + 支持格式: "0.12 mol", "16.2 mmol", "25.2mmol" 等 + """ + if not mol_str or not isinstance(mol_str, str): + return 0.0 + + # 移除空格并转小写 + mol_clean = mol_str.strip().lower() + + # 正则匹配数字和单位 + pattern = r'(\d+(?:\.\d+)?)\s*(m?mol)' + match = re.search(pattern, mol_clean) + + if not match: + self.logger.debug(f"🔍 无法解析摩尔数字符串: {mol_str}") + return 0.0 + + try: + value = float(match.group(1)) + unit = match.group(2) + + # 单位转换为 mol + if unit == 'mmol': + result = value * 0.001 + else: # mol + result = value + + self.logger.debug(f"🧮 摩尔数解析: {mol_str} → {result:.6f}mol (原值: {value} {unit})") + return result + + except (ValueError, TypeError): + self.logger.warning(f"⚠️ 无法解析摩尔数字符串: {mol_str}") + return 0.0 + + def find_solid_reagent_bottle(self, reagent_name: str) -> str: + """ + 查找固体试剂瓶 🔍 + + 这是一个简化版本,实际使用时应该连接到系统的设备图 + """ + if not reagent_name: + self.logger.debug(f"🔍 未指定试剂名称,使用默认瓶") + return "unknown_solid_bottle" + + # 可能的固体试剂瓶命名模式 + possible_names = [ + f"solid_bottle_{reagent_name}", + f"reagent_solid_{reagent_name}", + f"powder_{reagent_name}", + f"{reagent_name}_solid", + f"{reagent_name}_powder", + f"solid_{reagent_name}", + ] + + # 这里简化处理,实际应该查询设备图 + selected_bottle = possible_names[0] + self.logger.debug(f"🔍 为试剂 {reagent_name} 选择试剂瓶: {selected_bottle}") + return selected_bottle + + async def add_solid( + self, + vessel: str, + reagent: str, + mass: str = "", + mol: str = "", + purpose: str = "", + **kwargs # 兼容额外参数 + ) -> Dict[str, Any]: + """ + 添加固体试剂的主要方法 ⚗️ + + Args: + vessel: 目标容器 + reagent: 试剂名称 + mass: 质量字符串 (如 "2.9 g") + mol: 摩尔数字符串 (如 "0.12 mol") + purpose: 添加目的 + **kwargs: 其他兼容参数 + + Returns: + Dict: 操作结果 + """ + try: + self.logger.info(f"⚗️ === 开始固体加样操作 === ✨") + self.logger.info(f" 🥽 目标容器: {vessel}") + self.logger.info(f" 🧪 试剂: {reagent}") + self.logger.info(f" ⚖️ 质量: {mass}") + self.logger.info(f" 🧮 摩尔数: {mol}") + self.logger.info(f" 📝 目的: {purpose}") + + # 参数验证 - 宽松处理 + if not vessel: + vessel = "main_reactor" # 默认容器 + self.logger.warning(f"⚠️ 未指定容器,使用默认容器: {vessel} 🏠") + + if not reagent: + error_msg = "❌ 错误: 必须指定试剂名称" + self.logger.error(error_msg) + return { + "success": False, + "message": error_msg, + "return_info": "missing_reagent" + } + + # 解析质量和摩尔数 + mass_value = self.parse_mass_string(mass) + mol_value = self.parse_mol_string(mol) + + self.logger.info(f"📊 解析结果 - 质量: {mass_value:.6f}g | 摩尔数: {mol_value:.6f}mol") + + # 确定实际加样量 + if mass_value > 0: + actual_amount = mass_value + amount_unit = "g" + amount_emoji = "⚖️" + self.logger.info(f"⚖️ 按质量加样: {actual_amount:.6f} {amount_unit}") + elif mol_value > 0: + # 简化处理:假设分子量为100 g/mol + assumed_mw = 100.0 + actual_amount = mol_value * assumed_mw + amount_unit = "g (from mol)" + amount_emoji = "🧮" + self.logger.info(f"🧮 按摩尔数加样: {mol_value:.6f} mol → {actual_amount:.6f} g (假设分子量 {assumed_mw})") + else: + # 没有指定量,使用默认值 + actual_amount = 1.0 + amount_unit = "g (default)" + amount_emoji = "🎯" + self.logger.warning(f"⚠️ 未指定质量或摩尔数,使用默认值: {actual_amount} {amount_unit} 🎯") + + # 检查容量限制 + if actual_amount > self.max_capacity: + error_msg = f"❌ 错误: 请求量 {actual_amount:.3f}g 超过最大容量 {self.max_capacity}g" + self.logger.error(error_msg) + return { + "success": False, + "message": error_msg, + "return_info": "exceeds_capacity" + } + + # 查找试剂瓶 + reagent_bottle = self.find_solid_reagent_bottle(reagent) + self.logger.info(f"🔍 使用试剂瓶: {reagent_bottle}") + + # 模拟加样过程 + self._status = "Dispensing" + self._current_reagent = reagent + + # 计算操作时间 (基于质量) + operation_time = max(0.5, actual_amount * 0.1) # 每克0.1秒,最少0.5秒 + + self.logger.info(f"🚀 开始加样,预计时间: {operation_time:.1f}秒 ⏱️") + + # 显示进度的模拟 + steps = max(3, int(operation_time)) + step_time = operation_time / steps + + for i in range(steps): + progress = (i + 1) / steps * 100 + await asyncio.sleep(step_time) + if i % 2 == 0: # 每隔一步显示进度 + self.logger.debug(f"📊 加样进度: {progress:.0f}% | {amount_emoji} 正在分配 {reagent}...") + + # 更新状态 + self._dispensed_amount = actual_amount + self._total_operations += 1 + self._status = "Ready" + + # 成功结果 + success_message = f"✅ 成功添加 {reagent} {actual_amount:.6f} {amount_unit} 到 {vessel}" + + self.logger.info(f"🎉 === 固体加样完成 === ✨") + self.logger.info(f"📊 操作结果:") + self.logger.info(f" ✅ {success_message}") + self.logger.info(f" 🧪 试剂瓶: {reagent_bottle}") + self.logger.info(f" ⏱️ 用时: {operation_time:.1f}秒") + self.logger.info(f" 🎯 总操作次数: {self._total_operations} 🏁") + + return { + "success": True, + "message": success_message, + "return_info": f"dispensed_{actual_amount:.6f}g", + "dispensed_amount": actual_amount, + "reagent": reagent, + "vessel": vessel + } + + except Exception as e: + error_message = f"❌ 固体加样失败: {str(e)} 💥" + self.logger.error(error_message) + self._status = "Error" + + return { + "success": False, + "message": error_message, + "return_info": "operation_failed" + } + + # 状态属性 + @property + def status(self) -> str: + return self._status + + @property + def current_reagent(self) -> str: + return self._current_reagent + + @property + def dispensed_amount(self) -> float: + return self._dispensed_amount + + @property + def total_operations(self) -> int: + return self._total_operations + + def get_device_info(self) -> Dict[str, Any]: + """获取设备状态信息 📊""" + info = { + "device_id": self.device_id, + "status": self._status, + "current_reagent": self._current_reagent, + "last_dispensed_amount": self._dispensed_amount, + "total_operations": self._total_operations, + "max_capacity": self.max_capacity, + "precision": self.precision + } + + self.logger.debug(f"📊 设备信息: 状态={self._status}, 试剂={self._current_reagent}, 加样量={self._dispensed_amount:.6f}g") + return info + + def __str__(self): + status_emoji = "✅" if self._status == "Ready" else "🔄" if self._status == "Dispensing" else "❌" if self._status == "Error" else "🏠" + return f"⚗️ VirtualSolidDispenser({status_emoji} {self.device_id}: {self._status}, 最后加样 {self._dispensed_amount:.3f}g)" + + +# 测试函数 +async def test_solid_dispenser(): + """测试固体加样器 🧪""" + print("⚗️ === 固体加样器测试开始 === 🧪") + + dispenser = VirtualSolidDispenser("test_dispenser") + await dispenser.initialize() + + # 测试1: 按质量加样 + print(f"\n🧪 测试1: 按质量加样...") + result1 = await dispenser.add_solid( + vessel="main_reactor", + reagent="magnesium", + mass="2.9 g" + ) + print(f"📊 测试1结果: {result1}") + + # 测试2: 按摩尔数加样 + print(f"\n🧮 测试2: 按摩尔数加样...") + result2 = await dispenser.add_solid( + vessel="main_reactor", + reagent="sodium_nitrite", + mol="0.28 mol" + ) + print(f"📊 测试2结果: {result2}") + + # 测试3: 缺少参数 + print(f"\n⚠️ 测试3: 缺少参数测试...") + result3 = await dispenser.add_solid( + reagent="test_compound" + ) + print(f"📊 测试3结果: {result3}") + + # 测试4: 超容量测试 + print(f"\n❌ 测试4: 超容量测试...") + result4 = await dispenser.add_solid( + vessel="main_reactor", + reagent="heavy_compound", + mass="150 g" # 超过100g限制 + ) + print(f"📊 测试4结果: {result4}") + + print(f"\n📊 最终设备信息: {dispenser.get_device_info()}") + print(f"✅ === 测试完成 === 🎉") + + +if __name__ == "__main__": + asyncio.run(test_solid_dispenser()) \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_stirrer.py b/unilabos/devices/virtual/virtual_stirrer.py index 874f997..2b9058b 100644 --- a/unilabos/devices/virtual/virtual_stirrer.py +++ b/unilabos/devices/virtual/virtual_stirrer.py @@ -4,7 +4,7 @@ import time as time_module from typing import Dict, Any class VirtualStirrer: - """Virtual stirrer device for StirProtocol testing - 功能完整版""" + """Virtual stirrer device for StirProtocol testing - 功能完整版 🌪️""" def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): # 处理可能的不同调用方式 @@ -30,45 +30,69 @@ class VirtualStirrer: for key, value in kwargs.items(): if key not in skip_keys and not hasattr(self, key): setattr(self, key, value) + + print(f"🌪️ === 虚拟搅拌器 {self.device_id} 已创建 === ✨") + print(f"🔧 速度范围: {self._min_speed} ~ {self._max_speed} RPM | 📱 端口: {self.port}") async def initialize(self) -> bool: - """Initialize virtual stirrer""" - self.logger.info(f"Initializing virtual stirrer {self.device_id}") + """Initialize virtual stirrer 🚀""" + self.logger.info(f"🔧 初始化虚拟搅拌器 {self.device_id} ✨") # 初始化状态信息 self.data.update({ - "status": "Idle", + "status": "🏠 待机中", "operation_mode": "Idle", # 操作模式: Idle, Stirring, Settling, Completed, Error "current_vessel": "", # 当前搅拌的容器 "current_speed": 0.0, # 当前搅拌速度 "is_stirring": False, # 是否正在搅拌 "remaining_time": 0.0, # 剩余时间 }) + + self.logger.info(f"✅ 搅拌器 {self.device_id} 初始化完成 🌪️") + self.logger.info(f"📊 设备规格: 速度范围 {self._min_speed} ~ {self._max_speed} RPM") return True async def cleanup(self) -> bool: - """Cleanup virtual stirrer""" - self.logger.info(f"Cleaning up virtual stirrer {self.device_id}") + """Cleanup virtual stirrer 🧹""" + self.logger.info(f"🧹 清理虚拟搅拌器 {self.device_id} 🔚") + self.data.update({ - "status": "Offline", + "status": "💤 离线", "operation_mode": "Offline", "current_vessel": "", "current_speed": 0.0, "is_stirring": False, "remaining_time": 0.0, }) + + self.logger.info(f"✅ 搅拌器 {self.device_id} 清理完成 💤") return True - async def stir(self, stir_time: float, stir_speed: float, settling_time: float) -> bool: - """Execute stir action - 定时搅拌 + 沉降""" - self.logger.info(f"Stir: speed={stir_speed} RPM, time={stir_time}s, settling={settling_time}s") + async def stir(self, stir_time: float, stir_speed: float, settling_time: float, **kwargs) -> bool: + """Execute stir action - 定时搅拌 + 沉降 🌪️""" + + # 🔧 类型转换 - 确保所有参数都是数字类型 + try: + stir_time = float(stir_time) + stir_speed = float(stir_speed) + settling_time = float(settling_time) + except (ValueError, TypeError) as e: + error_msg = f"参数类型转换失败: stir_time={stir_time}, stir_speed={stir_speed}, settling_time={settling_time}, error={e}" + self.logger.error(f"❌ {error_msg}") + self.data.update({ + "status": f"❌ 错误: {error_msg}", + "operation_mode": "Error" + }) + return False + + self.logger.info(f"🌪️ 开始搅拌操作: 速度 {stir_speed} RPM | 时间 {stir_time}s | 沉降 {settling_time}s") # 验证参数 if stir_speed > self._max_speed or stir_speed < self._min_speed: - error_msg = f"搅拌速度 {stir_speed} RPM 超出范围 ({self._min_speed} - {self._max_speed} RPM)" - self.logger.error(error_msg) + error_msg = f"🌪️ 搅拌速度 {stir_speed} RPM 超出范围 ({self._min_speed} - {self._max_speed} RPM) ⚠️" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"Error: {error_msg}", + "status": f"❌ 错误: 速度超出范围", "operation_mode": "Error" }) return False @@ -77,8 +101,10 @@ class VirtualStirrer: start_time = time_module.time() total_stir_time = stir_time + self.logger.info(f"🚀 开始搅拌阶段: {stir_speed} RPM × {total_stir_time}s ⏱️") + self.data.update({ - "status": f"搅拌中: {stir_speed} RPM | 剩余: {total_stir_time:.0f}s", + "status": f"🌪️ 搅拌中: {stir_speed} RPM | ⏰ 剩余: {total_stir_time:.0f}s", "operation_mode": "Stirring", "current_speed": stir_speed, "is_stirring": True, @@ -86,30 +112,41 @@ class VirtualStirrer: }) # 搅拌过程 - 实时更新剩余时间 + last_logged_time = 0 while True: current_time = time_module.time() elapsed = current_time - start_time remaining = max(0, total_stir_time - elapsed) + progress = (elapsed / total_stir_time) * 100 if total_stir_time > 0 else 100 # 更新状态 self.data.update({ "remaining_time": remaining, - "status": f"搅拌中: {stir_speed} RPM | 剩余: {remaining:.0f}s" + "status": f"🌪️ 搅拌中: {stir_speed} RPM | ⏰ 剩余: {remaining:.0f}s" }) + # 进度日志(每25%打印一次) + if progress >= 25 and int(progress) % 25 == 0 and int(progress) != last_logged_time: + self.logger.info(f"📊 搅拌进度: {progress:.0f}% | 🌪️ {stir_speed} RPM | ⏰ 剩余: {remaining:.0f}s ✨") + last_logged_time = int(progress) + # 搅拌时间到了 if remaining <= 0: break await asyncio.sleep(1.0) + self.logger.info(f"✅ 搅拌阶段完成! 🌪️ {stir_speed} RPM × {stir_time}s") + # === 第二阶段:沉降(如果需要)=== if settling_time > 0: start_settling_time = time_module.time() total_settling_time = settling_time + self.logger.info(f"🛑 开始沉降阶段: 停止搅拌 × {total_settling_time}s ⏱️") + self.data.update({ - "status": f"沉降中: 停止搅拌 | 剩余: {total_settling_time:.0f}s", + "status": f"🛑 沉降中: 停止搅拌 | ⏰ 剩余: {total_settling_time:.0f}s", "operation_mode": "Settling", "current_speed": 0.0, "is_stirring": False, @@ -117,52 +154,87 @@ class VirtualStirrer: }) # 沉降过程 - 实时更新剩余时间 + last_logged_settling = 0 while True: current_time = time_module.time() elapsed = current_time - start_settling_time remaining = max(0, total_settling_time - elapsed) + progress = (elapsed / total_settling_time) * 100 if total_settling_time > 0 else 100 # 更新状态 self.data.update({ "remaining_time": remaining, - "status": f"沉降中: 停止搅拌 | 剩余: {remaining:.0f}s" + "status": f"🛑 沉降中: 停止搅拌 | ⏰ 剩余: {remaining:.0f}s" }) + # 进度日志(每25%打印一次) + if progress >= 25 and int(progress) % 25 == 0 and int(progress) != last_logged_settling: + self.logger.info(f"📊 沉降进度: {progress:.0f}% | 🛑 静置中 | ⏰ 剩余: {remaining:.0f}s ✨") + last_logged_settling = int(progress) + # 沉降时间到了 if remaining <= 0: break await asyncio.sleep(1.0) + + self.logger.info(f"✅ 沉降阶段完成! 🛑 静置 {settling_time}s") # === 操作完成 === - settling_info = f" | 沉降: {settling_time:.0f}s" if settling_time > 0 else "" + settling_info = f" | 🛑 沉降: {settling_time:.0f}s" if settling_time > 0 else "" + self.data.update({ - "status": f"完成: 搅拌 {stir_speed} RPM, {stir_time:.0f}s{settling_info}", + "status": f"✅ 完成: 🌪️ 搅拌 {stir_speed} RPM × {stir_time:.0f}s{settling_info}", "operation_mode": "Completed", "current_speed": 0.0, "is_stirring": False, "remaining_time": 0.0, }) - self.logger.info(f"Stir completed: {stir_speed} RPM for {stir_time}s + settling {settling_time}s") + self.logger.info(f"🎉 搅拌操作完成! ✨") + self.logger.info(f"📊 操作总结:") + self.logger.info(f" 🌪️ 搅拌: {stir_speed} RPM × {stir_time}s") + if settling_time > 0: + self.logger.info(f" 🛑 沉降: {settling_time}s") + self.logger.info(f" ⏱️ 总用时: {(stir_time + settling_time):.0f}s 🏁") + return True - async def start_stir(self, vessel: str, stir_speed: float, purpose: str) -> bool: - """Start stir action - 开始持续搅拌""" - self.logger.info(f"StartStir: vessel={vessel}, speed={stir_speed} RPM, purpose={purpose}") + async def start_stir(self, vessel: str, stir_speed: float, purpose: str = "") -> bool: + """Start stir action - 开始持续搅拌 🔄""" - # 验证参数 - if stir_speed > self._max_speed or stir_speed < self._min_speed: - error_msg = f"搅拌速度 {stir_speed} RPM 超出范围 ({self._min_speed} - {self._max_speed} RPM)" - self.logger.error(error_msg) + # 🔧 类型转换 + try: + stir_speed = float(stir_speed) + vessel = str(vessel) + purpose = str(purpose) + except (ValueError, TypeError) as e: + error_msg = f"参数类型转换错误: {str(e)}" + self.logger.error(f"❌ {error_msg}") self.data.update({ - "status": f"Error: {error_msg}", + "status": f"❌ 错误: {error_msg}", "operation_mode": "Error" }) return False + self.logger.info(f"🔄 启动持续搅拌: {vessel} | 🌪️ {stir_speed} RPM") + if purpose: + self.logger.info(f"📝 搅拌目的: {purpose}") + + # 验证参数 + if stir_speed > self._max_speed or stir_speed < self._min_speed: + error_msg = f"🌪️ 搅拌速度 {stir_speed} RPM 超出范围 ({self._min_speed} - {self._max_speed} RPM) ⚠️" + self.logger.error(f"❌ {error_msg}") + self.data.update({ + "status": f"❌ 错误: 速度超出范围", + "operation_mode": "Error" + }) + return False + + purpose_info = f" | 📝 {purpose}" if purpose else "" + self.data.update({ - "status": f"启动: 持续搅拌 {vessel} at {stir_speed} RPM | {purpose}", + "status": f"🔄 启动: 持续搅拌 {vessel} | 🌪️ {stir_speed} RPM{purpose_info}", "operation_mode": "Stirring", "current_vessel": vessel, "current_speed": stir_speed, @@ -170,16 +242,28 @@ class VirtualStirrer: "remaining_time": -1.0, # -1 表示持续运行 }) + self.logger.info(f"✅ 持续搅拌已启动! 🌪️ {stir_speed} RPM × ♾️ 🚀") return True async def stop_stir(self, vessel: str) -> bool: - """Stop stir action - 停止搅拌""" - self.logger.info(f"StopStir: vessel={vessel}") + """Stop stir action - 停止搅拌 🛑""" + + # 🔧 类型转换 + try: + vessel = str(vessel) + except (ValueError, TypeError) as e: + error_msg = f"参数类型转换错误: {str(e)}" + self.logger.error(f"❌ {error_msg}") + return False current_speed = self.data.get("current_speed", 0.0) + self.logger.info(f"🛑 停止搅拌: {vessel}") + if current_speed > 0: + self.logger.info(f"🌪️ 之前搅拌速度: {current_speed} RPM") + self.data.update({ - "status": f"已停止: {vessel} 搅拌停止 | 之前速度: {current_speed} RPM", + "status": f"🛑 已停止: {vessel} 搅拌停止 | 之前速度: {current_speed} RPM", "operation_mode": "Stopped", "current_vessel": "", "current_speed": 0.0, @@ -187,12 +271,13 @@ class VirtualStirrer: "remaining_time": 0.0, }) + self.logger.info(f"✅ 搅拌器已停止 {vessel} 的搅拌操作 🏁") return True # 状态属性 @property def status(self) -> str: - return self.data.get("status", "Idle") + return self.data.get("status", "🏠 待机中") @property def operation_mode(self) -> str: @@ -212,4 +297,33 @@ class VirtualStirrer: @property def remaining_time(self) -> float: - return self.data.get("remaining_time", 0.0) \ No newline at end of file + return self.data.get("remaining_time", 0.0) + + @property + def max_speed(self) -> float: + return self._max_speed + + @property + def min_speed(self) -> float: + return self._min_speed + + def get_device_info(self) -> Dict[str, Any]: + """获取设备状态信息 📊""" + info = { + "device_id": self.device_id, + "status": self.status, + "operation_mode": self.operation_mode, + "current_vessel": self.current_vessel, + "current_speed": self.current_speed, + "is_stirring": self.is_stirring, + "remaining_time": self.remaining_time, + "max_speed": self._max_speed, + "min_speed": self._min_speed + } + + self.logger.debug(f"📊 设备信息: 模式={self.operation_mode}, 速度={self.current_speed} RPM, 搅拌={self.is_stirring}") + return info + + def __str__(self): + status_emoji = "✅" if self.operation_mode == "Idle" else "🌪️" if self.operation_mode == "Stirring" else "🛑" if self.operation_mode == "Settling" else "❌" + return f"🌪️ VirtualStirrer({status_emoji} {self.device_id}: {self.operation_mode}, {self.current_speed} RPM)" \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_transferpump.py b/unilabos/devices/virtual/virtual_transferpump.py index a2cba9c..7d80744 100644 --- a/unilabos/devices/virtual/virtual_transferpump.py +++ b/unilabos/devices/virtual/virtual_transferpump.py @@ -12,7 +12,7 @@ class VirtualPumpMode(Enum): class VirtualTransferPump: - """虚拟转移泵类 - 模拟泵的基本功能,无需实际硬件""" + """虚拟转移泵类 - 模拟泵的基本功能,无需实际硬件 🚰""" def __init__(self, device_id: str = None, config: dict = None, **kwargs): """ @@ -42,20 +42,31 @@ class VirtualTransferPump: self._max_velocity = 5.0 # float self._current_volume = 0.0 # float + # 🚀 新增:快速模式设置 - 大幅缩短执行时间 + self._fast_mode = True # 是否启用快速模式 + self._fast_move_time = 1.0 # 快速移动时间(秒) + self._fast_dispense_time = 1.0 # 快速喷射时间(秒) + self.logger = logging.getLogger(f"VirtualTransferPump.{self.device_id}") + + print(f"🚰 === 虚拟转移泵 {self.device_id} 已创建 === ✨") + print(f"💨 快速模式: {'启用' if self._fast_mode else '禁用'} | 移动时间: {self._fast_move_time}s | 喷射时间: {self._fast_dispense_time}s") + print(f"📊 最大容量: {self.max_volume}mL | 端口: {self.port}") async def initialize(self) -> bool: - """初始化虚拟泵""" - self.logger.info(f"Initializing virtual pump {self.device_id}") + """初始化虚拟泵 🚀""" + self.logger.info(f"🔧 初始化虚拟转移泵 {self.device_id} ✨") self._status = "Idle" self._position = 0.0 self._current_volume = 0.0 + self.logger.info(f"✅ 转移泵 {self.device_id} 初始化完成 🚰") return True async def cleanup(self) -> bool: - """清理虚拟泵""" - self.logger.info(f"Cleaning up virtual pump {self.device_id}") + """清理虚拟泵 🧹""" + self.logger.info(f"🧹 清理虚拟转移泵 {self.device_id} 🔚") self._status = "Idle" + self.logger.info(f"✅ 转移泵 {self.device_id} 清理完成 💤") return True # 基本属性 @@ -65,12 +76,12 @@ class VirtualTransferPump: @property def position(self) -> float: - """当前柱塞位置 (ml)""" + """当前柱塞位置 (ml) 📍""" return self._position @property def current_volume(self) -> float: - """当前注射器中的体积 (ml)""" + """当前注射器中的体积 (ml) 💧""" return self._current_volume @property @@ -82,22 +93,50 @@ class VirtualTransferPump: return self._transfer_rate def set_max_velocity(self, velocity: float): - """设置最大速度 (ml/s)""" + """设置最大速度 (ml/s) 🌊""" self._max_velocity = max(0.1, min(50.0, velocity)) # 限制在合理范围内 - self.logger.info(f"Set max velocity to {self._max_velocity} ml/s") + self.logger.info(f"🌊 设置最大速度为 {self._max_velocity} mL/s") def get_status(self) -> str: - """获取泵状态""" + """获取泵状态 📋""" return self._status async def _simulate_operation(self, duration: float): - """模拟操作延时""" + """模拟操作延时 ⏱️""" self._status = "Busy" await asyncio.sleep(duration) self._status = "Idle" def _calculate_duration(self, volume: float, velocity: float = None) -> float: - """计算操作持续时间""" + """ + 计算操作持续时间 ⏰ + 🚀 快速模式:保留计算逻辑用于日志显示,但实际使用固定的快速时间 + """ + if velocity is None: + velocity = self._max_velocity + + # 📊 计算理论时间(用于日志显示) + theoretical_duration = abs(volume) / velocity + + # 🚀 如果启用快速模式,使用固定的快速时间 + if self._fast_mode: + # 根据操作类型选择快速时间 + if abs(volume) > 0.1: # 大于0.1mL的操作 + actual_duration = self._fast_move_time + else: # 很小的操作 + actual_duration = 0.5 + + self.logger.debug(f"⚡ 快速模式: 理论时间 {theoretical_duration:.2f}s → 实际时间 {actual_duration:.2f}s") + return actual_duration + else: + # 正常模式使用理论时间 + return theoretical_duration + + def _calculate_display_duration(self, volume: float, velocity: float = None) -> float: + """ + 计算显示用的持续时间(用于日志) 📊 + 这个函数返回理论计算时间,用于日志显示 + """ if velocity is None: velocity = self._max_velocity return abs(volume) / velocity @@ -105,7 +144,7 @@ class VirtualTransferPump: # 新的set_position方法 - 专门用于SetPumpPosition动作 async def set_position(self, position: float, max_velocity: float = None): """ - 移动到绝对位置 - 专门用于SetPumpPosition动作 + 移动到绝对位置 - 专门用于SetPumpPosition动作 🎯 Args: position (float): 目标位置 (ml) @@ -122,56 +161,107 @@ class VirtualTransferPump: # 限制位置在有效范围内 target_position = max(0.0, min(float(self.max_volume), target_position)) - # 计算移动距离和时间 + # 计算移动距离 volume_to_move = abs(target_position - self._position) - duration = self._calculate_duration(volume_to_move, velocity) - self.logger.info(f"SET_POSITION: Moving to {target_position} ml (current: {self._position} ml), velocity: {velocity} ml/s") + # 📊 计算显示用的时间(用于日志) + display_duration = self._calculate_display_duration(volume_to_move, velocity) - # 模拟移动过程 - start_position = self._position - steps = 10 if duration > 0.1 else 1 # 如果移动距离很小,只用1步 - step_duration = duration / steps if steps > 1 else duration + # ⚡ 计算实际执行时间(快速模式) + actual_duration = self._calculate_duration(volume_to_move, velocity) - for i in range(steps + 1): - # 计算当前位置和进度 - progress = (i / steps) * 100 if steps > 0 else 100 - current_pos = start_position + (target_position - start_position) * (i / steps) if steps > 0 else target_position + # 🎯 确定操作类型和emoji + if target_position > self._position: + operation_type = "吸液" + operation_emoji = "📥" + elif target_position < self._position: + operation_type = "排液" + operation_emoji = "📤" + else: + operation_type = "保持" + operation_emoji = "📍" + + self.logger.info(f"🎯 SET_POSITION: {operation_type} {operation_emoji}") + self.logger.info(f" 📍 位置: {self._position:.2f}mL → {target_position:.2f}mL (移动 {volume_to_move:.2f}mL)") + self.logger.info(f" 🌊 速度: {velocity:.2f} mL/s") + self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s") + + if self._fast_mode: + self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s") + + # 🚀 模拟移动过程 + if volume_to_move > 0.01: # 只有当移动距离足够大时才显示进度 + start_position = self._position + steps = 5 if actual_duration > 0.5 else 2 # 根据实际时间调整步数 + step_duration = actual_duration / steps - # 更新状态 - self._status = "Moving" if i < steps else "Idle" - self._position = current_pos - self._current_volume = current_pos + self.logger.info(f"🚀 开始{operation_type}... {operation_emoji}") - # 等待一小步时间 - if i < steps and step_duration > 0: - await asyncio.sleep(step_duration) + for i in range(steps + 1): + # 计算当前位置和进度 + progress = (i / steps) * 100 if steps > 0 else 100 + current_pos = start_position + (target_position - start_position) * (i / steps) if steps > 0 else target_position + + # 更新状态 + if i < steps: + self._status = f"{operation_type}中" + status_emoji = "🔄" + else: + self._status = "Idle" + status_emoji = "✅" + + self._position = current_pos + self._current_volume = current_pos + + # 显示进度(每25%或最后一步) + if i == 0: + self.logger.debug(f" 🔄 {operation_type}开始: {progress:.0f}%") + elif progress >= 50 and i == steps // 2: + self.logger.debug(f" 🔄 {operation_type}进度: {progress:.0f}%") + elif i == steps: + self.logger.info(f" ✅ {operation_type}完成: {progress:.0f}% | 当前位置: {current_pos:.2f}mL") + + # 等待一小步时间 + if i < steps and step_duration > 0: + await asyncio.sleep(step_duration) + else: + # 移动距离很小,直接完成 + self._position = target_position + self._current_volume = target_position + self.logger.info(f" 📍 微调完成: {target_position:.2f}mL") # 确保最终位置准确 self._position = target_position self._current_volume = target_position self._status = "Idle" - self.logger.info(f"SET_POSITION: Reached position {self._position} ml, current volume: {self._current_volume} ml") + # 📊 最终状态日志 + if volume_to_move > 0.01: + self.logger.info(f"🎉 SET_POSITION 完成! 📍 最终位置: {self._position:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL") # 返回符合action定义的结果 return { "success": True, - "message": f"Successfully moved to position {self._position} ml" + "message": f"✅ 成功移动到位置 {self._position:.2f}mL ({operation_type})", + "final_position": self._position, + "final_volume": self._current_volume, + "operation_type": operation_type } except Exception as e: - error_msg = f"Failed to set position: {str(e)}" + error_msg = f"❌ 设置位置失败: {str(e)}" self.logger.error(error_msg) return { "success": False, - "message": error_msg + "message": error_msg, + "final_position": self._position, + "final_volume": self._current_volume } # 其他泵操作方法 async def pull_plunger(self, volume: float, velocity: float = None): """ - 拉取柱塞(吸液) + 拉取柱塞(吸液) 📥 Args: volume (float): 要拉取的体积 (ml) @@ -181,23 +271,29 @@ class VirtualTransferPump: actual_volume = new_position - self._position if actual_volume <= 0: - self.logger.warning("Cannot pull - already at maximum volume") + self.logger.warning("⚠️ 无法吸液 - 已达到最大容量") return - duration = self._calculate_duration(actual_volume, velocity) + display_duration = self._calculate_display_duration(actual_volume, velocity) + actual_duration = self._calculate_duration(actual_volume, velocity) - self.logger.info(f"Pulling {actual_volume} ml (from {self._position} to {new_position})") + self.logger.info(f"📥 开始吸液: {actual_volume:.2f}mL") + self.logger.info(f" 📍 位置: {self._position:.2f}mL → {new_position:.2f}mL") + self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s") - await self._simulate_operation(duration) + if self._fast_mode: + self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s") + + await self._simulate_operation(actual_duration) self._position = new_position self._current_volume = new_position - self.logger.info(f"Pulled {actual_volume} ml, current volume: {self._current_volume} ml") + self.logger.info(f"✅ 吸液完成: {actual_volume:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL") async def push_plunger(self, volume: float, velocity: float = None): """ - 推出柱塞(排液) + 推出柱塞(排液) 📤 Args: volume (float): 要推出的体积 (ml) @@ -207,35 +303,44 @@ class VirtualTransferPump: actual_volume = self._position - new_position if actual_volume <= 0: - self.logger.warning("Cannot push - already at minimum volume") + self.logger.warning("⚠️ 无法排液 - 已达到最小容量") return - duration = self._calculate_duration(actual_volume, velocity) + display_duration = self._calculate_display_duration(actual_volume, velocity) + actual_duration = self._calculate_duration(actual_volume, velocity) - self.logger.info(f"Pushing {actual_volume} ml (from {self._position} to {new_position})") + self.logger.info(f"📤 开始排液: {actual_volume:.2f}mL") + self.logger.info(f" 📍 位置: {self._position:.2f}mL → {new_position:.2f}mL") + self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s") - await self._simulate_operation(duration) + if self._fast_mode: + self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s") + + await self._simulate_operation(actual_duration) self._position = new_position self._current_volume = new_position - self.logger.info(f"Pushed {actual_volume} ml, current volume: {self._current_volume} ml") + self.logger.info(f"✅ 排液完成: {actual_volume:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL") # 便捷操作方法 async def aspirate(self, volume: float, velocity: float = None): - """吸液操作""" + """吸液操作 📥""" await self.pull_plunger(volume, velocity) async def dispense(self, volume: float, velocity: float = None): - """排液操作""" + """排液操作 📤""" await self.push_plunger(volume, velocity) async def transfer(self, volume: float, aspirate_velocity: float = None, dispense_velocity: float = None): - """转移操作(先吸后排)""" + """转移操作(先吸后排) 🔄""" + self.logger.info(f"🔄 开始转移操作: {volume:.2f}mL") + # 吸液 await self.aspirate(volume, aspirate_velocity) # 短暂停顿 + self.logger.debug("⏸️ 短暂停顿...") await asyncio.sleep(0.1) # 排液 diff --git a/unilabos/messages/__init__.py b/unilabos/messages/__init__.py index 47f21f1..f91f382 100644 --- a/unilabos/messages/__init__.py +++ b/unilabos/messages/__init__.py @@ -10,18 +10,88 @@ class Point3D(BaseModel): # Start Protocols class PumpTransferProtocol(BaseModel): + # === 核心参数(保持必需) === from_vessel: str to_vessel: str - volume: float + + # === 所有其他参数都改为可选,添加默认值 === + volume: float = 0.0 # 🔧 改为-1,表示转移全部体积 amount: str = "" - time: float = 0 + time: float = 0.0 viscous: bool = False - rinsing_solvent: str = "air" - rinsing_volume: float = 5000 - rinsing_repeats: int = 2 + rinsing_solvent: str = "" + rinsing_volume: float = 0.0 + rinsing_repeats: int = 0 solid: bool = False - flowrate: float = 500 - transfer_flowrate: float = 2500 + flowrate: float = 2.5 + transfer_flowrate: float = 0.5 + + # === 新版XDL兼容参数(可选) === + rate_spec: str = "" + event: str = "" + through: str = "" + + def model_post_init(self, __context): + """后处理:智能参数处理和兼容性调整""" + + # 如果指定了 amount 但volume是默认值,尝试解析 amount + if self.amount and self.volume == 0.0: + parsed_volume = self._parse_amount_to_volume(self.amount) + if parsed_volume > 0: + self.volume = parsed_volume + + # 如果指定了 time 但没有明确设置流速,根据时间计算流速 + if self.time > 0 and self.volume > 0: + if self.flowrate == 2.5 and self.transfer_flowrate == 0.5: + calculated_flowrate = self.volume / self.time + self.flowrate = min(calculated_flowrate, 10.0) + self.transfer_flowrate = min(calculated_flowrate, 5.0) + + # 🔧 核心修复:如果flowrate为0(ROS2传入),使用默认值 + if self.flowrate <= 0: + self.flowrate = 2.5 + if self.transfer_flowrate <= 0: + self.transfer_flowrate = 0.5 + + # 根据 rate_spec 调整流速 + if self.rate_spec == "dropwise": + self.flowrate = min(self.flowrate, 0.1) + self.transfer_flowrate = min(self.transfer_flowrate, 0.1) + elif self.rate_spec == "slowly": + self.flowrate = min(self.flowrate, 0.5) + self.transfer_flowrate = min(self.transfer_flowrate, 0.3) + elif self.rate_spec == "quickly": + self.flowrate = max(self.flowrate, 5.0) + self.transfer_flowrate = max(self.transfer_flowrate, 2.0) + + def _parse_amount_to_volume(self, amount: str) -> float: + """解析 amount 字符串为体积""" + if not amount: + return 0.0 + + amount = amount.lower().strip() + + # 处理特殊关键词 + if amount == "all": + return 0.0 # 🔧 "all"也表示转移全部 + + # 提取数字 + import re + numbers = re.findall(r'[\d.]+', amount) + if numbers: + volume = float(numbers[0]) + + # 单位转换 + if 'ml' in amount or 'milliliter' in amount: + return volume + elif 'l' in amount and 'ml' not in amount: + return volume * 1000 + elif 'μl' in amount or 'microliter' in amount: + return volume / 1000 + else: + return volume + + return 0.0 class CleanProtocol(BaseModel): @@ -49,17 +119,96 @@ class SeparateProtocol(BaseModel): class EvaporateProtocol(BaseModel): - vessel: str - pressure: float - temp: float - time: float - stir_speed: float + # === 核心参数(必需) === + vessel: str = Field(..., description="蒸发容器名称") + + # === 所有其他参数都改为可选,添加默认值 === + pressure: float = Field(0.1, description="真空度 (bar),默认0.1 bar") + temp: float = Field(60.0, description="加热温度 (°C),默认60°C") + time: float = Field(180.0, description="蒸发时间 (秒),默认1800s (30分钟)") + stir_speed: float = Field(100.0, description="旋转速度 (RPM),默认100 RPM") + + # === 新版XDL兼容参数(可选) === + solvent: str = Field("", description="溶剂名称(用于识别蒸发的溶剂类型)") + + def model_post_init(self, __context): + """后处理:智能参数处理和兼容性调整""" + + # 参数范围验证和修正 + if self.pressure <= 0 or self.pressure > 1.0: + logger.warning(f"真空度 {self.pressure} bar 超出范围,修正为 0.1 bar") + self.pressure = 0.1 + + if self.temp < 10.0 or self.temp > 200.0: + logger.warning(f"温度 {self.temp}°C 超出范围,修正为 60°C") + self.temp = 60.0 + + if self.time <= 0: + logger.warning(f"时间 {self.time}s 无效,修正为 1800s") + self.time = 1800.0 + + if self.stir_speed < 10.0 or self.stir_speed > 300.0: + logger.warning(f"旋转速度 {self.stir_speed} RPM 超出范围,修正为 100 RPM") + self.stir_speed = 100.0 + + # 根据溶剂类型调整参数 + if self.solvent: + self._adjust_parameters_by_solvent() + + def _adjust_parameters_by_solvent(self): + """根据溶剂类型调整蒸发参数""" + solvent_lower = self.solvent.lower() + + # 水系溶剂:较高温度,较低真空度 + if any(s in solvent_lower for s in ['water', 'aqueous', 'h2o']): + if self.temp == 60.0: # 如果是默认值,则调整 + self.temp = 80.0 + if self.pressure == 0.1: + self.pressure = 0.2 + + # 有机溶剂:根据沸点调整 + elif any(s in solvent_lower for s in ['ethanol', 'methanol', 'acetone']): + if self.temp == 60.0: + self.temp = 50.0 + if self.pressure == 0.1: + self.pressure = 0.05 + + # 高沸点溶剂:更高温度 + elif any(s in solvent_lower for s in ['dmso', 'dmi', 'toluene']): + if self.temp == 60.0: + self.temp = 100.0 + if self.pressure == 0.1: + self.pressure = 0.01 class EvacuateAndRefillProtocol(BaseModel): - vessel: str - gas: str - repeats: int + # === 必需参数 === + vessel: str = Field(..., description="目标容器名称") + gas: str = Field(..., description="气体名称") + + # 🔧 删除 repeats 参数,直接在代码中硬编码为 3 次 + + def model_post_init(self, __context): + """后处理:参数验证和兼容性调整""" + + # 验证气体名称 + if not self.gas.strip(): + logger.warning("气体名称为空,使用默认值 'nitrogen'") + self.gas = "nitrogen" + + # 标准化气体名称 + gas_aliases = { + 'n2': 'nitrogen', + 'ar': 'argon', + 'air': 'air', + 'o2': 'oxygen', + 'co2': 'carbon_dioxide', + 'h2': 'hydrogen' + } + + gas_lower = self.gas.lower().strip() + if gas_lower in gas_aliases: + self.gas = gas_aliases[gas_lower] class AGVTransferProtocol(BaseModel): @@ -88,42 +237,282 @@ class CentrifugeProtocol(BaseModel): temp: float class FilterProtocol(BaseModel): - vessel: str - filtrate_vessel: str - stir: bool - stir_speed: float - temp: float - continue_heatchill: bool - volume: float + # === 必需参数 === + vessel: str = Field(..., description="过滤容器名称") + + # === 可选参数 === + filtrate_vessel: str = Field("", description="滤液容器名称(可选,自动查找)") + + def model_post_init(self, __context): + """后处理:参数验证""" + # 验证容器名称 + if not self.vessel.strip(): + raise ValueError("vessel 参数不能为空") class HeatChillProtocol(BaseModel): - vessel: str - temp: float - time: float - stir: bool - stir_speed: float - purpose: str + # === 必需参数 === + vessel: str = Field(..., description="加热容器名称") + + # === 可选参数 - 温度相关 === + temp: float = Field(25.0, description="目标温度 (°C)") + temp_spec: str = Field("", description="温度规格(如 'room temperature', 'reflux')") + + # === 可选参数 - 时间相关 === + time: float = Field(300.0, description="加热时间 (秒)") + time_spec: str = Field("", description="时间规格(如 'overnight', '2 h')") + + # === 可选参数 - 其他XDL参数 === + pressure: str = Field("", description="压力规格(如 '1 mbar'),不做特殊处理") + reflux_solvent: str = Field("", description="回流溶剂名称,不做特殊处理") + + # === 可选参数 - 搅拌相关 === + stir: bool = Field(False, description="是否搅拌") + stir_speed: float = Field(300.0, description="搅拌速度 (RPM)") + purpose: str = Field("", description="操作目的") + + def model_post_init(self, __context): + """后处理:参数验证和解析""" + + # 验证必需参数 + if not self.vessel.strip(): + raise ValueError("vessel 参数不能为空") + + # 温度解析:优先使用 temp_spec,然后是 temp + if self.temp_spec: + self.temp = self._parse_temp_spec(self.temp_spec) + + # 时间解析:优先使用 time_spec,然后是 time + if self.time_spec: + self.time = self._parse_time_spec(self.time_spec) + + # 参数范围验证 + if self.temp < -50.0 or self.temp > 300.0: + logger.warning(f"温度 {self.temp}°C 超出范围,修正为 25°C") + self.temp = 25.0 + + if self.time < 0: + logger.warning(f"时间 {self.time}s 无效,修正为 300s") + self.time = 300.0 + + if self.stir_speed < 0 or self.stir_speed > 1500.0: + logger.warning(f"搅拌速度 {self.stir_speed} RPM 超出范围,修正为 300 RPM") + self.stir_speed = 300.0 + + def _parse_temp_spec(self, temp_spec: str) -> float: + """解析温度规格为具体温度""" + + temp_spec = temp_spec.strip().lower() + + # 特殊温度规格 + special_temps = { + "room temperature": 25.0, # 室温 + "reflux": 78.0, # 默认回流温度(乙醇沸点) + "ice bath": 0.0, # 冰浴 + "boiling": 100.0, # 沸腾 + "hot": 60.0, # 热 + "warm": 40.0, # 温热 + "cold": 10.0, # 冷 + } + + if temp_spec in special_temps: + return special_temps[temp_spec] + + # 解析带单位的温度(如 "256 °C") + import re + temp_pattern = r'(\d+(?:\.\d+)?)\s*°?[cf]?' + match = re.search(temp_pattern, temp_spec) + + if match: + return float(match.group(1)) + + return 25.0 # 默认室温 + + def _parse_time_spec(self, time_spec: str) -> float: + """解析时间规格为秒数""" + + time_spec = time_spec.strip().lower() + + # 特殊时间规格 + special_times = { + "overnight": 43200.0, # 12小时 + "several hours": 10800.0, # 3小时 + "few hours": 7200.0, # 2小时 + "long time": 3600.0, # 1小时 + "short time": 300.0, # 5分钟 + } + + if time_spec in special_times: + return special_times[time_spec] + + # 解析带单位的时间(如 "2 h") + import re + time_pattern = r'(\d+(?:\.\d+)?)\s*([a-zA-Z]+)' + match = re.search(time_pattern, time_spec) + + if match: + value = float(match.group(1)) + unit = match.group(2).lower() + + unit_multipliers = { + 's': 1.0, + 'sec': 1.0, + 'second': 1.0, + 'seconds': 1.0, + 'min': 60.0, + 'minute': 60.0, + 'minutes': 60.0, + 'h': 3600.0, + 'hr': 3600.0, + 'hour': 3600.0, + 'hours': 3600.0, + } + + multiplier = unit_multipliers.get(unit, 3600.0) # 默认按小时计算 + return value * multiplier + + return 300.0 # 默认5分钟 + class HeatChillStartProtocol(BaseModel): - vessel: str - temp: float - purpose: str + # === 必需参数 === + vessel: str = Field(..., description="加热容器名称") + + # === 可选参数 - 温度相关 === + temp: float = Field(25.0, description="目标温度 (°C)") + temp_spec: str = Field("", description="温度规格(如 'room temperature', 'reflux')") + + # === 可选参数 - 其他XDL参数 === + pressure: str = Field("", description="压力规格(如 '1 mbar'),不做特殊处理") + reflux_solvent: str = Field("", description="回流溶剂名称,不做特殊处理") + + # === 可选参数 - 搅拌相关 === + stir: bool = Field(False, description="是否搅拌") + stir_speed: float = Field(300.0, description="搅拌速度 (RPM)") + purpose: str = Field("", description="操作目的") + class HeatChillStopProtocol(BaseModel): - vessel: str + # === 必需参数 === + vessel: str = Field(..., description="加热容器名称") + class StirProtocol(BaseModel): - stir_time: float - stir_speed: float - settling_time: float + # === 必需参数 === + vessel: str = Field(..., description="搅拌容器名称") + + # === 可选参数 === + time: str = Field("5 min", description="搅拌时间(如 '0.5 h', '30 min')") + event: str = Field("", description="事件标识(如 'A', 'B')") + time_spec: str = Field("", description="时间规格(如 'several minutes', 'overnight')") + + def model_post_init(self, __context): + """后处理:参数验证和时间解析""" + + # 验证必需参数 + if not self.vessel.strip(): + raise ValueError("vessel 参数不能为空") + + # 优先使用 time_spec,然后是 time + if self.time_spec: + self.time = self.time_spec + + # 时间解析和验证 + if self.time: + try: + # 解析时间字符串为秒数 + parsed_time = self._parse_time_string(self.time) + if parsed_time <= 0: + logger.warning(f"时间 '{self.time}' 解析结果无效,使用默认值 300s") + self.time = "5 min" + except Exception as e: + logger.warning(f"时间 '{self.time}' 解析失败: {e},使用默认值 300s") + self.time = "5 min" + + def _parse_time_string(self, time_str: str) -> float: + """解析时间字符串为秒数""" + import re + + time_str = time_str.strip().lower() + + # 特殊时间规格 + special_times = { + "several minutes": 300.0, # 5分钟 + "few minutes": 180.0, # 3分钟 + "overnight": 43200.0, # 12小时 + "room temperature": 300.0, # 默认5分钟 + } + + if time_str in special_times: + return special_times[time_str] + + # 正则表达式匹配数字和单位 + pattern = r'(\d+\.?\d*)\s*([a-zA-Z]+)' + match = re.match(pattern, time_str) + + if not match: + return 300.0 # 默认5分钟 + + value = float(match.group(1)) + unit = match.group(2).lower() + + # 时间单位转换 + unit_multipliers = { + 's': 1.0, + 'sec': 1.0, + 'second': 1.0, + 'seconds': 1.0, + 'min': 60.0, + 'minute': 60.0, + 'minutes': 60.0, + 'h': 3600.0, + 'hr': 3600.0, + 'hour': 3600.0, + 'hours': 3600.0, + 'd': 86400.0, + 'day': 86400.0, + 'days': 86400.0, + } + + multiplier = unit_multipliers.get(unit, 60.0) # 默认按分钟计算 + return value * multiplier + + def get_time_in_seconds(self) -> float: + """获取时间(秒)""" + return self._parse_time_string(self.time) class StartStirProtocol(BaseModel): - vessel: str - stir_speed: float - purpose: str + # === 必需参数 === + vessel: str = Field(..., description="搅拌容器名称") + + # === 可选参数,添加默认值 === + stir_speed: float = Field(200.0, description="搅拌速度 (RPM),默认200 RPM") + purpose: str = Field("", description="搅拌目的(可选)") + + def model_post_init(self, __context): + """后处理:参数验证和修正""" + + # 验证必需参数 + if not self.vessel.strip(): + raise ValueError("vessel 参数不能为空") + + # 修正参数范围 + if self.stir_speed < 10.0: + logger.warning(f"搅拌速度 {self.stir_speed} RPM 过低,修正为 100 RPM") + self.stir_speed = 100.0 + elif self.stir_speed > 1500.0: + logger.warning(f"搅拌速度 {self.stir_speed} RPM 过高,修正为 1000 RPM") + self.stir_speed = 1000.0 class StopStirProtocol(BaseModel): - vessel: str + # === 必需参数 === + vessel: str = Field(..., description="搅拌容器名称") + + def model_post_init(self, __context): + """后处理:参数验证""" + + # 验证必需参数 + if not self.vessel.strip(): + raise ValueError("vessel 参数不能为空") class TransferProtocol(BaseModel): from_vessel: str @@ -168,16 +557,52 @@ class RunColumnProtocol(BaseModel): column: str class WashSolidProtocol(BaseModel): - vessel: str - solvent: str - volume: float - filtrate_vessel: str = "" - temp: float = 25.0 - stir: bool = False - stir_speed: float = 0.0 - time: float = 0.0 - repeats: int = 1 - + # === 必需参数 === + vessel: str = Field(..., description="装有固体的容器名称") + solvent: str = Field(..., description="清洗溶剂名称") + volume: float = Field(..., description="清洗溶剂体积 (mL)") + + # === 可选参数,添加默认值 === + filtrate_vessel: str = Field("", description="滤液收集容器(可选,自动查找)") + temp: float = Field(25.0, description="清洗温度 (°C),默认25°C") + stir: bool = Field(False, description="是否搅拌,默认False") + stir_speed: float = Field(0.0, description="搅拌速度 (RPM),默认0") + time: float = Field(0.0, description="清洗时间 (秒),默认0") + repeats: int = Field(1, description="重复次数,默认1") + + def model_post_init(self, __context): + """后处理:参数验证和修正""" + + # 验证必需参数 + if not self.vessel.strip(): + raise ValueError("vessel 参数不能为空") + + if not self.solvent.strip(): + raise ValueError("solvent 参数不能为空") + + if self.volume <= 0: + raise ValueError("volume 必须大于0") + + # 修正参数范围 + if self.temp < 0 or self.temp > 200: + logger.warning(f"温度 {self.temp}°C 超出范围,修正为 25°C") + self.temp = 25.0 + + if self.stir_speed < 0 or self.stir_speed > 500: + logger.warning(f"搅拌速度 {self.stir_speed} RPM 超出范围,修正为 0") + self.stir_speed = 0.0 + + if self.time < 0: + logger.warning(f"时间 {self.time}s 无效,修正为 0") + self.time = 0.0 + + if self.repeats < 1: + logger.warning(f"重复次数 {self.repeats} 无效,修正为 1") + self.repeats = 1 + elif self.repeats > 10: + logger.warning(f"重复次数 {self.repeats} 过多,修正为 10") + self.repeats = 10 + class AdjustPHProtocol(BaseModel): vessel: str = Field(..., description="目标容器") ph_value: float = Field(..., description="目标pH值") # 改为 ph_value @@ -207,7 +632,8 @@ __all__ = [ "Point3D", "PumpTransferProtocol", "CleanProtocol", "SeparateProtocol", "EvaporateProtocol", "EvacuateAndRefillProtocol", "AGVTransferProtocol", "CentrifugeProtocol", "AddProtocol", "FilterProtocol", - "HeatChillProtocol", "HeatChillStartProtocol", "HeatChillStopProtocol", + "HeatChillProtocol", + "HeatChillStartProtocol", "HeatChillStopProtocol", "StirProtocol", "StartStirProtocol", "StopStirProtocol", "TransferProtocol", "CleanVesselProtocol", "DissolveProtocol", "FilterThroughProtocol", "RunColumnProtocol", "WashSolidProtocol", diff --git a/unilabos/registry/devices/mock_devices.yaml b/unilabos/registry/devices/mock_devices.yaml index 5bfe5d7..461fc73 100644 --- a/unilabos/registry/devices/mock_devices.yaml +++ b/unilabos/registry/devices/mock_devices.yaml @@ -539,11 +539,15 @@ mock_heater: time: time vessel: vessel goal_default: + pressure: '' purpose: '' + reflux_solvent: '' stir: false stir_speed: 0.0 temp: 0.0 + temp_spec: '' time: 0.0 + time_spec: '' vessel: '' handles: [] result: @@ -561,22 +565,34 @@ mock_heater: type: object goal: properties: + pressure: + type: string purpose: type: string + reflux_solvent: + type: string stir: type: boolean stir_speed: type: number temp: type: number + temp_spec: + type: string time: type: number + time_spec: + type: string vessel: type: string required: - vessel - temp - time + - temp_spec + - time_spec + - pressure + - reflux_solvent - stir - stir_speed - purpose @@ -584,13 +600,16 @@ mock_heater: type: object result: properties: + message: + type: string return_info: type: string success: type: boolean required: - - return_info - success + - message + - return_info title: HeatChill_Result type: object required: @@ -836,13 +855,18 @@ mock_pump: volume: volume goal_default: amount: '' + event: '' + flowrate: 0.0 from_vessel: '' + rate_spec: '' rinsing_repeats: 0 rinsing_solvent: '' rinsing_volume: 0.0 solid: false + through: '' time: 0.0 to_vessel: '' + transfer_flowrate: 0.0 viscous: false volume: 0.0 handles: [] @@ -898,8 +922,14 @@ mock_pump: properties: amount: type: string + event: + type: string + flowrate: + type: number from_vessel: type: string + rate_spec: + type: string rinsing_repeats: maximum: 2147483647 minimum: -2147483648 @@ -910,10 +940,14 @@ mock_pump: type: number solid: type: boolean + through: + type: string time: type: number to_vessel: type: string + transfer_flowrate: + type: number viscous: type: boolean volume: @@ -929,6 +963,11 @@ mock_pump: - rinsing_volume - rinsing_repeats - solid + - flowrate + - transfer_flowrate + - rate_spec + - event + - through title: PumpTransfer_Goal type: object result: @@ -1519,6 +1558,7 @@ mock_separator: goal_default: from_vessel: '' product_phase: '' + product_vessel: '' purpose: '' repeats: 0 separation_vessel: '' @@ -1529,7 +1569,10 @@ mock_separator: stir_time: 0.0 through: '' to_vessel: '' + vessel: '' + volume: '' waste_phase_to_vessel: '' + waste_vessel: '' handles: [] result: success: success @@ -1585,6 +1628,8 @@ mock_separator: type: string product_phase: type: string + product_vessel: + type: string purpose: type: string repeats: @@ -1607,8 +1652,14 @@ mock_separator: type: string to_vessel: type: string + vessel: + type: string + volume: + type: string waste_phase_to_vessel: type: string + waste_vessel: + type: string required: - purpose - product_phase @@ -1623,17 +1674,24 @@ mock_separator: - stir_time - stir_speed - settling_time + - vessel + - volume + - product_vessel + - waste_vessel title: Separate_Goal type: object result: properties: + message: + type: string return_info: type: string success: type: boolean required: - - return_info - success + - message + - return_info title: Separate_Result type: object required: @@ -2410,9 +2468,13 @@ mock_stirrer_new: stir_speed: stir_speed stir_time: stir_time goal_default: + event: '' settling_time: 0.0 stir_speed: 0.0 stir_time: 0.0 + time: '' + time_spec: '' + vessel: '' handles: [] result: success: success @@ -2429,13 +2491,25 @@ mock_stirrer_new: type: object goal: properties: + event: + type: string settling_time: type: number stir_speed: type: number stir_time: type: number + time: + type: string + time_spec: + type: string + vessel: + type: string required: + - vessel + - time + - event + - time_spec - stir_time - stir_speed - settling_time @@ -2443,13 +2517,16 @@ mock_stirrer_new: type: object result: properties: + message: + type: string return_info: type: string success: type: boolean required: - - return_info - success + - message + - return_info title: Stir_Result type: object required: diff --git a/unilabos/registry/devices/organic_miscellaneous.yaml b/unilabos/registry/devices/organic_miscellaneous.yaml index aca63df..3ac61fc 100644 --- a/unilabos/registry/devices/organic_miscellaneous.yaml +++ b/unilabos/registry/devices/organic_miscellaneous.yaml @@ -242,9 +242,13 @@ separator.homemade: stir_speed: stir_speed stir_time: stir_time, goal_default: + event: '' settling_time: 0.0 stir_speed: 0.0 stir_time: 0.0 + time: '' + time_spec: '' + vessel: '' handles: [] result: success: success @@ -261,13 +265,25 @@ separator.homemade: type: object goal: properties: + event: + type: string settling_time: type: number stir_speed: type: number stir_time: type: number + time: + type: string + time_spec: + type: string + vessel: + type: string required: + - vessel + - time + - event + - time_spec - stir_time - stir_speed - settling_time @@ -275,13 +291,16 @@ separator.homemade: type: object result: properties: + message: + type: string return_info: type: string success: type: boolean required: - - return_info - success + - message + - return_info title: Stir_Result type: object required: diff --git a/unilabos/registry/devices/temperature.yaml b/unilabos/registry/devices/temperature.yaml index 2da45fd..fe4ad5b 100644 --- a/unilabos/registry/devices/temperature.yaml +++ b/unilabos/registry/devices/temperature.yaml @@ -259,11 +259,15 @@ heaterstirrer.dalong: time: time vessel: vessel goal_default: + pressure: '' purpose: '' + reflux_solvent: '' stir: false stir_speed: 0.0 temp: 0.0 + temp_spec: '' time: 0.0 + time_spec: '' vessel: '' handles: [] result: @@ -281,22 +285,34 @@ heaterstirrer.dalong: type: object goal: properties: + pressure: + type: string purpose: type: string + reflux_solvent: + type: string stir: type: boolean stir_speed: type: number temp: type: number + temp_spec: + type: string time: type: number + time_spec: + type: string vessel: type: string required: - vessel - temp - time + - temp_spec + - time_spec + - pressure + - reflux_solvent - stir - stir_speed - purpose @@ -304,13 +320,16 @@ heaterstirrer.dalong: type: object result: properties: + message: + type: string return_info: type: string success: type: boolean required: - - return_info - success + - message + - return_info title: HeatChill_Result type: object required: diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml index d8f8f19..0a263b4 100644 --- a/unilabos/registry/devices/virtual_device.yaml +++ b/unilabos/registry/devices/virtual_device.yaml @@ -248,6 +248,12 @@ virtual_column: goal_default: column: '' from_vessel: '' + pct1: '' + pct2: '' + ratio: '' + rf: '' + solvent1: '' + solvent2: '' to_vessel: '' handles: [] result: @@ -274,12 +280,30 @@ virtual_column: type: string from_vessel: type: string + pct1: + type: string + pct2: + type: string + ratio: + type: string + rf: + type: string + solvent1: + type: string + solvent2: + type: string to_vessel: type: string required: - from_vessel - to_vessel - column + - rf + - pct1 + - pct2 + - solvent1 + - solvent2 + - ratio title: RunColumn_Goal type: object result: @@ -861,11 +885,15 @@ virtual_heatchill: time: time vessel: vessel goal_default: + pressure: '' purpose: '' + reflux_solvent: '' stir: false stir_speed: 0.0 temp: 0.0 + temp_spec: '' time: 0.0 + time_spec: '' vessel: '' handles: [] result: @@ -883,22 +911,34 @@ virtual_heatchill: type: object goal: properties: + pressure: + type: string purpose: type: string + reflux_solvent: + type: string stir: type: boolean stir_speed: type: number temp: type: number + temp_spec: + type: string time: type: number + time_spec: + type: string vessel: type: string required: - vessel - temp - time + - temp_spec + - time_spec + - pressure + - reflux_solvent - stir - stir_speed - purpose @@ -906,13 +946,16 @@ virtual_heatchill: type: object result: properties: + message: + type: string return_info: type: string success: type: boolean required: - - return_info - success + - message + - return_info title: HeatChill_Result type: object required: @@ -1630,13 +1673,18 @@ virtual_pump: volume: volume goal_default: amount: '' + event: '' + flowrate: 0.0 from_vessel: '' + rate_spec: '' rinsing_repeats: 0 rinsing_solvent: '' rinsing_volume: 0.0 solid: false + through: '' time: 0.0 to_vessel: '' + transfer_flowrate: 0.0 viscous: false volume: 0.0 handles: [] @@ -1692,8 +1740,14 @@ virtual_pump: properties: amount: type: string + event: + type: string + flowrate: + type: number from_vessel: type: string + rate_spec: + type: string rinsing_repeats: maximum: 2147483647 minimum: -2147483648 @@ -1704,10 +1758,14 @@ virtual_pump: type: number solid: type: boolean + through: + type: string time: type: number to_vessel: type: string + transfer_flowrate: + type: number viscous: type: boolean volume: @@ -1723,6 +1781,11 @@ virtual_pump: - rinsing_volume - rinsing_repeats - solid + - flowrate + - transfer_flowrate + - rate_spec + - event + - through title: PumpTransfer_Goal type: object result: @@ -1853,10 +1916,8 @@ virtual_rotavap: type: UniLabJsonCommandAsync evaporate: feedback: - current_temp: current_temp - evaporated_volume: evaporated_volume - progress: progress status: status + current_device: current_device goal: pressure: pressure stir_speed: stir_speed @@ -1865,6 +1926,7 @@ virtual_rotavap: vessel: vessel goal_default: pressure: 0.0 + solvent: '' stir_speed: 0.0 temp: 0.0 time: 0.0 @@ -1923,6 +1985,8 @@ virtual_rotavap: properties: pressure: type: number + solvent: + type: string stir_speed: type: number temp: @@ -1937,6 +2001,7 @@ virtual_rotavap: - temp - time - stir_speed + - solvent title: Evaporate_Goal type: object result: @@ -2107,6 +2172,7 @@ virtual_separator: goal_default: from_vessel: '' product_phase: '' + product_vessel: '' purpose: '' repeats: 0 separation_vessel: '' @@ -2117,7 +2183,10 @@ virtual_separator: stir_time: 0.0 through: '' to_vessel: '' + vessel: '' + volume: '' waste_phase_to_vessel: '' + waste_vessel: '' handles: [] result: message: message @@ -2174,6 +2243,8 @@ virtual_separator: type: string product_phase: type: string + product_vessel: + type: string purpose: type: string repeats: @@ -2196,8 +2267,14 @@ virtual_separator: type: string to_vessel: type: string + vessel: + type: string + volume: + type: string waste_phase_to_vessel: type: string + waste_vessel: + type: string required: - purpose - product_phase @@ -2212,17 +2289,24 @@ virtual_separator: - stir_time - stir_speed - settling_time + - vessel + - volume + - product_vessel + - waste_vessel title: Separate_Goal type: object result: properties: + message: + type: string return_info: type: string success: type: boolean required: - - return_info - success + - message + - return_info title: Separate_Result type: object required: @@ -2381,6 +2465,26 @@ virtual_solenoid_valve: title: is_closed参数 type: object type: UniLabJsonCommand + auto-open: + feedback: {} + goal: {} + goal_default: {} + handles: [] + result: {} + schema: + description: open的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: open参数 + type: object + type: UniLabJsonCommandAsync auto-reset: feedback: {} goal: {} @@ -2401,6 +2505,53 @@ virtual_solenoid_valve: title: reset参数 type: object type: UniLabJsonCommandAsync + auto-set_state: + feedback: {} + goal: {} + goal_default: + command: null + handles: [] + result: {} + schema: + description: set_state的参数schema + properties: + feedback: {} + goal: + properties: + command: + type: string + required: + - command + type: object + result: {} + required: + - goal + title: set_state参数 + type: object + type: UniLabJsonCommandAsync + auto-set_valve_position: + feedback: {} + goal: {} + goal_default: + command: null + handles: [] + result: {} + schema: + description: set_valve_position的参数schema + properties: + feedback: {} + goal: + properties: + command: + type: string + required: [] + type: object + result: {} + required: + - goal + title: set_valve_position参数 + type: object + type: UniLabJsonCommandAsync auto-toggle: feedback: {} goal: {} @@ -2431,111 +2582,86 @@ virtual_solenoid_valve: result: success: success schema: - description: '' + description: ROS Action SendCmd 的 JSON Schema properties: feedback: - properties: - status: - type: string - required: - - status - title: SendCmd_Feedback + properties: {} + required: [] + title: EmptyIn_Feedback type: object goal: - properties: - command: - type: string - required: - - command - title: SendCmd_Goal + properties: {} + required: [] + title: EmptyIn_Goal type: object result: properties: return_info: type: string - success: - type: boolean required: - return_info - - success - title: SendCmd_Result + title: EmptyIn_Result type: object required: - goal - title: SendCmd + title: EmptyIn type: object - type: SendCmd + type: EmptyIn open: feedback: {} - goal: - command: OPEN - goal_default: - command: '' + goal: {} + goal_default: {} handles: [] - result: - success: success + result: {} schema: - description: '' + description: ROS Action SendCmd 的 JSON Schema properties: feedback: - properties: - status: - type: string - required: - - status - title: SendCmd_Feedback + properties: {} + required: [] + title: EmptyIn_Feedback type: object goal: - properties: - command: - type: string - required: - - command - title: SendCmd_Goal + properties: {} + required: [] + title: EmptyIn_Goal type: object result: properties: return_info: type: string - success: - type: boolean required: - return_info - - success - title: SendCmd_Result + title: EmptyIn_Result type: object required: - goal - title: SendCmd + title: EmptyIn type: object - type: SendCmd - set_state: + type: EmptyIn + set_status: feedback: {} goal: - command: command + string: string goal_default: - command: '' + string: '' handles: [] - result: - success: success + result: {} schema: - description: '' + description: ROS Action SendCmd 的 JSON Schema properties: feedback: - properties: - status: - type: string - required: - - status - title: SendCmd_Feedback + properties: {} + required: [] + title: StrSingleInput_Feedback type: object goal: properties: - command: + string: type: string required: - - command - title: SendCmd_Goal + - string + title: StrSingleInput_Goal type: object result: properties: @@ -2546,13 +2672,13 @@ virtual_solenoid_valve: required: - return_info - success - title: SendCmd_Result + title: StrSingleInput_Result type: object required: - goal - title: SendCmd + title: StrSingleInput type: object - type: SendCmd + type: StrSingleInput set_valve_position: feedback: {} goal: @@ -2562,15 +2688,14 @@ virtual_solenoid_valve: handles: [] result: success: success + message: message + valve_position: valve_position schema: description: '' properties: feedback: - properties: - status: - type: string - required: - - status + properties: {} + required: [] title: SendCmd_Feedback type: object goal: @@ -2583,13 +2708,15 @@ virtual_solenoid_valve: type: object result: properties: - return_info: - type: string success: type: boolean + message: + type: string + valve_position: + type: string required: - - return_info - success + - message title: SendCmd_Result type: object required: @@ -2652,7 +2779,6 @@ virtual_solenoid_valve: - valve_position - state type: object - version: 0.0.1 virtual_stirrer: class: action_value_mappings: @@ -2768,9 +2894,13 @@ virtual_stirrer: stir_speed: stir_speed stir_time: stir_time goal_default: + event: '' settling_time: 0.0 stir_speed: 0.0 stir_time: 0.0 + time: '' + time_spec: '' + vessel: '' handles: [] result: success: success @@ -2787,13 +2917,25 @@ virtual_stirrer: type: object goal: properties: + event: + type: string settling_time: type: number stir_speed: type: number stir_time: type: number + time: + type: string + time_spec: + type: string + vessel: + type: string required: + - vessel + - time + - event + - time_spec - stir_time - stir_speed - settling_time @@ -2801,13 +2943,16 @@ virtual_stirrer: type: object result: properties: + message: + type: string return_info: type: string success: type: boolean required: - - return_info - success + - message + - return_info title: Stir_Result type: object required: diff --git a/unilabos/registry/devices/work_station.yaml b/unilabos/registry/devices/work_station.yaml index 91e4c4f..91ea9bd 100644 --- a/unilabos/registry/devices/work_station.yaml +++ b/unilabos/registry/devices/work_station.yaml @@ -54,9 +54,10 @@ workstation: handles: [] result: {} schema: - description: '' + description: ROS Action AGVTransfer 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: status: type: string @@ -65,6 +66,7 @@ workstation: title: AGVTransfer_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: from_repo: properties: @@ -224,6 +226,7 @@ workstation: title: AGVTransfer_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: return_info: type: string @@ -254,8 +257,13 @@ workstation: volume: volume goal_default: amount: '' + equiv: '' + event: '' mass: 0.0 + mol: '' purpose: '' + rate_spec: '' + ratio: '' reagent: '' stir: false stir_speed: 0.0 @@ -283,9 +291,10 @@ workstation: label: Vessel result: {} schema: - description: '' + description: ROS Action Add 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: current_status: type: string @@ -297,13 +306,24 @@ workstation: title: Add_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: amount: type: string + equiv: + type: string + event: + type: string mass: type: number + mol: + type: string purpose: type: string + rate_spec: + type: string + ratio: + type: string reagent: type: string stir: @@ -329,9 +349,15 @@ workstation: - stir_speed - viscous - purpose + - event + - mol + - rate_spec + - equiv + - ratio title: Add_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: message: type: string @@ -380,9 +406,10 @@ workstation: label: Vessel result: {} schema: - description: '' + description: ROS Action AdjustPH 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: progress: type: number @@ -394,6 +421,7 @@ workstation: title: AdjustPH_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: ph_value: type: number @@ -408,6 +436,7 @@ workstation: title: AdjustPH_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: message: type: string @@ -453,9 +482,10 @@ workstation: label: Vessel result: {} schema: - description: '' + description: ROS Action Centrifuge 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: current_speed: type: number @@ -473,6 +503,7 @@ workstation: title: Centrifuge_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: speed: type: number @@ -490,6 +521,7 @@ workstation: title: Centrifuge_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: message: type: string @@ -542,9 +574,10 @@ workstation: label: Vessel result: {} schema: - description: '' + description: ROS Action Clean 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: current_device: type: string @@ -588,6 +621,7 @@ workstation: title: Clean_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: repeats: maximum: 2147483647 @@ -610,6 +644,7 @@ workstation: title: Clean_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: return_info: type: string @@ -659,9 +694,10 @@ workstation: label: Vessel result: {} schema: - description: '' + description: ROS Action CleanVessel 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: progress: type: number @@ -673,6 +709,7 @@ workstation: title: CleanVessel_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: repeats: maximum: 2147483647 @@ -695,6 +732,7 @@ workstation: title: CleanVessel_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: message: type: string @@ -725,6 +763,9 @@ workstation: volume: volume goal_default: amount: '' + mass: '' + mol: '' + reagent: '' solvent: '' stir_speed: 0.0 temp: 0.0 @@ -751,9 +792,10 @@ workstation: label: Vessel result: {} schema: - description: '' + description: ROS Action Dissolve 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: progress: type: number @@ -765,9 +807,16 @@ workstation: title: Dissolve_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: amount: type: string + mass: + type: string + mol: + type: string + reagent: + type: string solvent: type: string stir_speed: @@ -788,9 +837,13 @@ workstation: - temp - time - stir_speed + - mass + - mol + - reagent title: Dissolve_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: message: type: string @@ -832,9 +885,10 @@ workstation: label: Vessel result: {} schema: - description: '' + description: ROS Action Dry 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: progress: type: number @@ -846,6 +900,7 @@ workstation: title: Dry_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: compound: type: string @@ -857,6 +912,7 @@ workstation: title: Dry_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: message: type: string @@ -879,11 +935,9 @@ workstation: feedback: {} goal: gas: gas - repeats: repeats vessel: vessel goal_default: gas: '' - repeats: 0 vessel: '' handles: input: @@ -900,9 +954,10 @@ workstation: label: Vessel result: {} schema: - description: '' + description: ROS Action EvacuateAndRefill 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: current_device: type: string @@ -946,22 +1001,19 @@ workstation: title: EvacuateAndRefill_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: gas: type: string - repeats: - maximum: 2147483647 - minimum: -2147483648 - type: integer vessel: type: string required: - vessel - gas - - repeats title: EvacuateAndRefill_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: return_info: type: string @@ -981,12 +1033,14 @@ workstation: feedback: {} goal: pressure: pressure + solvent: solvent stir_speed: stir_speed temp: temp time: time vessel: vessel goal_default: pressure: 0.0 + solvent: '' stir_speed: 0.0 temp: 0.0 time: 0.0 @@ -996,19 +1050,20 @@ workstation: - data_key: vessel data_source: handle data_type: resource - handler_key: Vessel - label: Vessel + handler_key: vessel + label: Evaporation Vessel output: - data_key: vessel - data_source: executor + data_source: handle data_type: resource - handler_key: VesselOut - label: Vessel + handler_key: vessel_out + label: Evaporation Vessel result: {} schema: - description: '' + description: ROS Action Evaporate 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: current_device: type: string @@ -1052,9 +1107,12 @@ workstation: title: Evaporate_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: pressure: type: number + solvent: + type: string stir_speed: type: number temp: @@ -1069,9 +1127,11 @@ workstation: - temp - time - stir_speed + - solvent title: Evaporate_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: return_info: type: string @@ -1112,7 +1172,7 @@ workstation: data_type: resource handler_key: Vessel label: Vessel - - data_key: vessel + - data_key: filtrate_vessel data_source: handle data_type: resource handler_key: filtrate_vessel @@ -1123,16 +1183,17 @@ workstation: data_type: resource handler_key: VesselOut label: Vessel - - data_key: vessel + - data_key: filtrate_vessel data_source: executor data_type: resource handler_key: filtrate_out label: Filtrate Vessel result: {} schema: - description: '' + description: ROS Action Filter 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: current_status: type: string @@ -1150,6 +1211,7 @@ workstation: title: Filter_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: continue_heatchill: type: boolean @@ -1176,6 +1238,7 @@ workstation: title: Filter_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: message: type: string @@ -1242,9 +1305,10 @@ workstation: label: To Vessel result: {} schema: - description: '' + description: ROS Action FilterThrough 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: progress: type: number @@ -1256,6 +1320,7 @@ workstation: title: FilterThrough_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: eluting_repeats: maximum: 2147483647 @@ -1284,6 +1349,7 @@ workstation: title: FilterThrough_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: message: type: string @@ -1305,18 +1371,26 @@ workstation: HeatChillProtocol: feedback: {} goal: + pressure: pressure purpose: purpose + reflux_solvent: reflux_solvent stir: stir stir_speed: stir_speed temp: temp + temp_spec: temp_spec time: time + time_spec: time_spec vessel: vessel goal_default: + pressure: '' purpose: '' + reflux_solvent: '' stir: false stir_speed: 0.0 temp: 0.0 + temp_spec: '' time: 0.0 + time_spec: '' vessel: '' handles: input: @@ -1333,9 +1407,10 @@ workstation: label: Vessel result: {} schema: - description: '' + description: ROS Action HeatChill 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: status: type: string @@ -1344,37 +1419,54 @@ workstation: title: HeatChill_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: + pressure: + type: string purpose: type: string + reflux_solvent: + type: string stir: type: boolean stir_speed: type: number temp: type: number + temp_spec: + type: string time: type: number + time_spec: + type: string vessel: type: string required: - vessel - temp - time + - temp_spec + - time_spec + - pressure + - reflux_solvent - stir - stir_speed - purpose title: HeatChill_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: + message: + type: string return_info: type: string success: type: boolean required: - - return_info - success + - message + - return_info title: HeatChill_Result type: object required: @@ -1407,9 +1499,10 @@ workstation: label: Vessel result: {} schema: - description: '' + description: ROS Action HeatChillStart 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: status: type: string @@ -1418,6 +1511,7 @@ workstation: title: HeatChillStart_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: purpose: type: string @@ -1432,6 +1526,7 @@ workstation: title: HeatChillStart_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: return_info: type: string @@ -1468,9 +1563,10 @@ workstation: label: Vessel result: {} schema: - description: '' + description: ROS Action HeatChillStop 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: status: type: string @@ -1479,6 +1575,7 @@ workstation: title: HeatChillStop_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: vessel: type: string @@ -1487,6 +1584,7 @@ workstation: title: HeatChillStop_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: return_info: type: string @@ -1527,9 +1625,10 @@ workstation: label: Vessel result: {} schema: - description: '' + description: ROS Action Hydrogenate 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: progress: type: number @@ -1541,6 +1640,7 @@ workstation: title: Hydrogenate_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: temp: type: string @@ -1555,6 +1655,7 @@ workstation: title: Hydrogenate_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: message: type: string @@ -1577,24 +1678,34 @@ workstation: feedback: {} goal: amount: amount + event: event + flowrate: flowrate from_vessel: from_vessel + rate_spec: rate_spec rinsing_repeats: rinsing_repeats rinsing_solvent: rinsing_solvent rinsing_volume: rinsing_volume solid: solid + through: through time: time to_vessel: to_vessel + transfer_flowrate: transfer_flowrate viscous: viscous volume: volume goal_default: amount: '' + event: '' + flowrate: 0.0 from_vessel: '' + rate_spec: '' rinsing_repeats: 0 rinsing_solvent: '' rinsing_volume: 0.0 solid: false + through: '' time: 0.0 to_vessel: '' + transfer_flowrate: 0.0 viscous: false volume: 0.0 handles: @@ -1627,9 +1738,10 @@ workstation: label: To Vessel result: {} schema: - description: '' + description: ROS Action PumpTransfer 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: current_device: type: string @@ -1673,11 +1785,18 @@ workstation: title: PumpTransfer_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: amount: type: string + event: + type: string + flowrate: + type: number from_vessel: type: string + rate_spec: + type: string rinsing_repeats: maximum: 2147483647 minimum: -2147483648 @@ -1688,10 +1807,14 @@ workstation: type: number solid: type: boolean + through: + type: string time: type: number to_vessel: type: string + transfer_flowrate: + type: number viscous: type: boolean volume: @@ -1707,9 +1830,15 @@ workstation: - rinsing_volume - rinsing_repeats - solid + - flowrate + - transfer_flowrate + - rate_spec + - event + - through title: PumpTransfer_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: return_info: type: string @@ -1746,12 +1875,12 @@ workstation: data_type: resource handler_key: Vessel label: Vessel - - data_key: solvent + - data_key: solvent1 data_source: handle data_type: resource handler_key: solvent1 label: Solvent 1 - - data_key: solvent + - data_key: solvent2 data_source: handle data_type: resource handler_key: solvent2 @@ -1764,9 +1893,10 @@ workstation: label: Vessel result: {} schema: - description: '' + description: ROS Action Recrystallize 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: progress: type: number @@ -1778,6 +1908,7 @@ workstation: title: Recrystallize_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: ratio: type: string @@ -1798,6 +1929,7 @@ workstation: title: Recrystallize_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: message: type: string @@ -1832,9 +1964,10 @@ workstation: output: [] result: {} schema: - description: '' + description: ROS Action ResetHandling 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: progress: type: number @@ -1846,6 +1979,7 @@ workstation: title: ResetHandling_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: solvent: type: string @@ -1854,6 +1988,7 @@ workstation: title: ResetHandling_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: message: type: string @@ -1881,6 +2016,12 @@ workstation: goal_default: column: '' from_vessel: '' + pct1: '' + pct2: '' + ratio: '' + rf: '' + solvent1: '' + solvent2: '' to_vessel: '' handles: input: @@ -1907,9 +2048,10 @@ workstation: label: To Vessel result: {} schema: - description: '' + description: ROS Action RunColumn 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: progress: type: number @@ -1921,20 +2063,40 @@ workstation: title: RunColumn_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: column: type: string from_vessel: type: string + pct1: + type: string + pct2: + type: string + ratio: + type: string + rf: + type: string + solvent1: + type: string + solvent2: + type: string to_vessel: type: string required: - from_vessel - to_vessel - column + - rf + - pct1 + - pct2 + - solvent1 + - solvent2 + - ratio title: RunColumn_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: message: type: string @@ -1972,6 +2134,7 @@ workstation: goal_default: from_vessel: '' product_phase: '' + product_vessel: '' purpose: '' repeats: 0 separation_vessel: '' @@ -1982,7 +2145,10 @@ workstation: stir_time: 0.0 through: '' to_vessel: '' + vessel: '' + volume: '' waste_phase_to_vessel: '' + waste_vessel: '' handles: input: - data_key: vessel @@ -2013,9 +2179,10 @@ workstation: label: To Vessel result: {} schema: - description: '' + description: ROS Action Separate 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: current_device: type: string @@ -2059,11 +2226,14 @@ workstation: title: Separate_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: from_vessel: type: string product_phase: type: string + product_vessel: + type: string purpose: type: string repeats: @@ -2086,8 +2256,14 @@ workstation: type: string to_vessel: type: string + vessel: + type: string + volume: + type: string waste_phase_to_vessel: type: string + waste_vessel: + type: string required: - purpose - product_phase @@ -2102,17 +2278,25 @@ workstation: - stir_time - stir_speed - settling_time + - vessel + - volume + - product_vessel + - waste_vessel title: Separate_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: + message: + type: string return_info: type: string success: type: boolean required: - - return_info - success + - message + - return_info title: Separate_Result type: object required: @@ -2145,9 +2329,10 @@ workstation: label: Vessel result: {} schema: - description: '' + description: ROS Action StartStir 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: current_speed: type: number @@ -2162,6 +2347,7 @@ workstation: title: StartStir_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: purpose: type: string @@ -2176,6 +2362,7 @@ workstation: title: StartStir_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: message: type: string @@ -2197,13 +2384,21 @@ workstation: StirProtocol: feedback: {} goal: + event: event settling_time: settling_time stir_speed: stir_speed stir_time: stir_time + time: time + time_spec: time_spec + vessel: vessel goal_default: + event: '' settling_time: 0.0 stir_speed: 0.0 stir_time: 0.0 + time: '' + time_spec: '' + vessel: '' handles: input: - data_key: vessel @@ -2219,9 +2414,10 @@ workstation: label: Vessel result: {} schema: - description: '' + description: ROS Action Stir 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: status: type: string @@ -2230,28 +2426,45 @@ workstation: title: Stir_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: + event: + type: string settling_time: type: number stir_speed: type: number stir_time: type: number + time: + type: string + time_spec: + type: string + vessel: + type: string required: + - vessel + - time + - event + - time_spec - stir_time - stir_speed - settling_time title: Stir_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: + message: + type: string return_info: type: string success: type: boolean required: - - return_info - success + - message + - return_info title: Stir_Result type: object required: @@ -2280,9 +2493,10 @@ workstation: label: Vessel result: {} schema: - description: '' + description: ROS Action StopStir 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: current_status: type: string @@ -2294,6 +2508,7 @@ workstation: title: StopStir_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: vessel: type: string @@ -2302,6 +2517,7 @@ workstation: title: StopStir_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: message: type: string @@ -2374,9 +2590,10 @@ workstation: label: To Vessel result: {} schema: - description: '' + description: ROS Action Transfer 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: current_status: type: string @@ -2391,6 +2608,7 @@ workstation: title: Transfer_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: amount: type: string @@ -2428,6 +2646,7 @@ workstation: title: Transfer_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: message: type: string @@ -2480,8 +2699,8 @@ workstation: data_type: resource handler_key: solvent label: Solvent - - data_key: vessel - data_source: executor + - data_key: filtrate_vessel + data_source: handle data_type: resource handler_key: filtrate_vessel label: Filtrate Vessel @@ -2491,16 +2710,17 @@ workstation: data_type: resource handler_key: VesselOut label: Vessel Out - - data_key: vessel + - data_key: filtrate_vessel data_source: executor data_type: resource handler_key: filtrate_vessel_out label: Filtrate Vessel result: {} schema: - description: '' + description: ROS Action WashSolid 的 JSON Schema properties: feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 properties: progress: type: number @@ -2512,6 +2732,7 @@ workstation: title: WashSolid_Feedback type: object goal: + description: Action 目标 - 从客户端发送到服务器 properties: filtrate_vessel: type: string @@ -2546,6 +2767,7 @@ workstation: title: WashSolid_Goal type: object result: + description: Action 结果 - 完成后从服务器发送到客户端 properties: message: type: string @@ -2679,4 +2901,3 @@ workstation: properties: {} required: [] type: object - version: 0.0.1 diff --git a/unilabos/ros/nodes/presets/protocol_node.py b/unilabos/ros/nodes/presets/protocol_node.py index d2eab27..5c8ffbd 100644 --- a/unilabos/ros/nodes/presets/protocol_node.py +++ b/unilabos/ros/nodes/presets/protocol_node.py @@ -211,7 +211,7 @@ class ROS2ProtocolNode(BaseROS2DeviceNode): # 逐步执行工作流 step_results = [] for i, action in enumerate(protocol_steps): - self.get_logger().info(f"Running step {i + 1}: {action}") + # self.get_logger().info(f"Running step {i + 1}: {action}") if isinstance(action, dict): # 如果是单个动作,直接执行 if action["action_name"] == "wait": diff --git a/unilabos_msgs/CMakeLists.txt b/unilabos_msgs/CMakeLists.txt index 7e8a146..b2c1bc7 100644 --- a/unilabos_msgs/CMakeLists.txt +++ b/unilabos_msgs/CMakeLists.txt @@ -41,6 +41,7 @@ set(action_files "action/WashSolid.action" "action/Filter.action" "action/Add.action" + "action/AddSolid.action" "action/Centrifuge.action" "action/Crystallize.action" "action/Purge.action" diff --git a/unilabos_msgs/action/Add.action b/unilabos_msgs/action/Add.action index de06c6a..021199a 100644 --- a/unilabos_msgs/action/Add.action +++ b/unilabos_msgs/action/Add.action @@ -1,14 +1,19 @@ # Goal - 添加试剂的目标参数 -string vessel # 目标容器 -string reagent # 试剂名称 -float64 volume # 体积 (可选) -float64 mass # 质量 (可选) -string amount # 数量描述 (可选) -float64 time # 添加时间 (可选) -bool stir # 是否搅拌 -float64 stir_speed # 搅拌速度 (可选) -bool viscous # 是否为粘性液体 -string purpose # 添加目的 (可选) +string vessel # 目标容器(必需) +string reagent # 试剂名称(必需) +string volume # 体积(如 "2.7 mL",可选) +string mass # 质量(如 "19.3 g",可选) +string amount # 数量描述(可选) +string time # 添加时间(如 "1 h", "20 min",可选) +bool stir # 是否搅拌(可选) +float64 stir_speed # 搅拌速度 (RPM,可选) +bool viscous # 是否为粘性液体(可选) +string purpose # 添加目的(可选) +string event # 事件标识(如 'A', 'B',可选) +string mol # 摩尔数(如 '0.28 mol', '16.2 mmol',可选) +string rate_spec # 速率规格(如 'portionwise', 'dropwise',可选) +string equiv # 当量(如 '1.1',可选) +string ratio # 比例(如 '1:1',可选) --- # Result - 操作结果 bool success # 操作是否成功 diff --git a/unilabos_msgs/action/AddSolid.action b/unilabos_msgs/action/AddSolid.action new file mode 100644 index 0000000..8812441 --- /dev/null +++ b/unilabos_msgs/action/AddSolid.action @@ -0,0 +1,15 @@ +# Goal - 固体加样操作的目标参数 +string vessel # 目标容器(必需) +string reagent # 试剂名称(必需) +string mass # 质量字符串(如 "2.9 g",可选) +string mol # 摩尔数字符串(如 "0.12 mol",可选) +string purpose # 添加目的(可选) +--- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 +string return_info # 返回信息 +--- +# Feedback - 实时反馈 +string current_status # 当前状态描述 +float64 progress # 进度百分比 (0-100) \ No newline at end of file diff --git a/unilabos_msgs/action/Dissolve.action b/unilabos_msgs/action/Dissolve.action index 6b860d0..f070a61 100644 --- a/unilabos_msgs/action/Dissolve.action +++ b/unilabos_msgs/action/Dissolve.action @@ -1,14 +1,21 @@ -string vessel # 装有要溶解物质的容器名称 -string solvent # 用于溶解物质的溶剂名称 -float64 volume # 溶剂的体积,可选参数 -string amount # 要溶解物质的量,可选参数 -float64 temp # 溶解时的温度,可选参数 -float64 time # 溶解的时间,可选参数 -float64 stir_speed # 搅拌速度,可选参数 +# Goal - 溶解操作的目标参数 +string vessel # 装有要溶解物质的容器名称(必需) +string solvent # 用于溶解物质的溶剂名称(可选) +string volume # 溶剂的体积(如 "10 mL",可选) +string amount # 要溶解物质的量描述(可选) +string temp # 溶解时的温度(如 "60 °C", "room temperature",可选) +string time # 溶解的时间(如 "30 min", "1 h",可选) +float64 stir_speed # 搅拌速度(可选,默认300 RPM) +string mass # 物质质量(如 "2.9 g",可选) +string mol # 物质摩尔数(如 "0.12 mol",可选) +string reagent # 试剂名称(可选) +string event # 事件标识(如 'A', 'B',可选) --- +# Result - 操作结果 bool success # 操作是否成功 string message # 结果消息 string return_info --- +# Feedback - 实时反馈 string status # 当前状态描述 float64 progress # 进度百分比 (0-100) \ No newline at end of file diff --git a/unilabos_msgs/action/EvacuateAndRefill.action b/unilabos_msgs/action/EvacuateAndRefill.action index 22ffc65..461cb28 100644 --- a/unilabos_msgs/action/EvacuateAndRefill.action +++ b/unilabos_msgs/action/EvacuateAndRefill.action @@ -1,7 +1,6 @@ -# Organic +# Organic Synthesis Station EvacuateAndRefill Action string vessel string gas -int32 repeats --- string return_info bool success diff --git a/unilabos_msgs/action/Evaporate.action b/unilabos_msgs/action/Evaporate.action index 45887f2..9cecb62 100644 --- a/unilabos_msgs/action/Evaporate.action +++ b/unilabos_msgs/action/Evaporate.action @@ -1,9 +1,10 @@ -# Organic -string vessel -float64 pressure -float64 temp -float64 time -float64 stir_speed +# Organic Synthesis Station Evaporate Action +string vessel # 目标容器 +float64 pressure # 真空度 +float64 temp # 温度 +string time # 🔧 蒸发时间(支持带单位,如"3 min","180",默认秒) +float64 stir_speed # 旋转速度 +string solvent # 溶剂名称 --- string return_info bool success diff --git a/unilabos_msgs/action/Filter.action b/unilabos_msgs/action/Filter.action index 564df1a..42a8913 100644 --- a/unilabos_msgs/action/Filter.action +++ b/unilabos_msgs/action/Filter.action @@ -1,11 +1,11 @@ # Goal - 过滤操作的目标参数 -string vessel # 过滤容器 -string filtrate_vessel # 滤液容器 (可选) -bool stir # 是否搅拌 -float64 stir_speed # 搅拌速度 (可选) -float64 temp # 温度 (可选,摄氏度) -bool continue_heatchill # 是否继续加热冷却 -float64 volume # 过滤体积 (可选) +string vessel # 过滤容器(必需) +string filtrate_vessel # 滤液容器(可选) +bool stir # 是否搅拌(默认false) +float64 stir_speed # 搅拌速度(默认0.0) +float64 temp # 温度(默认25.0) +bool continue_heatchill # 是否继续加热冷却(默认false) +float64 volume # 过滤体积(默认0.0) --- # Result - 操作结果 bool success # 操作是否成功 diff --git a/unilabos_msgs/action/HeatChill.action b/unilabos_msgs/action/HeatChill.action index 87ebf52..1e7025e 100644 --- a/unilabos_msgs/action/HeatChill.action +++ b/unilabos_msgs/action/HeatChill.action @@ -1,12 +1,19 @@ -# Organic -string vessel -float64 temp -float64 time -bool stir -float64 stir_speed -string purpose +# Goal - 加热冷却操作的目标参数 +string vessel # 加热容器名称(必需) +float64 temp # 目标温度(可选,默认25.0) +string time # 🔧 加热时间(支持带单位,如"5 min","300",默认秒) +string temp_spec # 温度规格(可选) +string time_spec # 时间规格(可选) +string pressure # 压力规格(可选,不做特殊处理) +string reflux_solvent # 回流溶剂名称(可选,不做特殊处理) +bool stir # 是否搅拌(可选,默认false) +float64 stir_speed # 搅拌速度(可选,默认300.0) +string purpose # 操作目的(可选) --- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 string return_info -bool success --- -string status \ No newline at end of file +# Feedback - 实时反馈 +string status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/PumpTransfer.action b/unilabos_msgs/action/PumpTransfer.action index 69d22b6..c8ca445 100644 --- a/unilabos_msgs/action/PumpTransfer.action +++ b/unilabos_msgs/action/PumpTransfer.action @@ -9,6 +9,11 @@ string rinsing_solvent float64 rinsing_volume int32 rinsing_repeats bool solid +float64 flowrate +float64 transfer_flowrate +string rate_spec +string event +string through --- string return_info bool success diff --git a/unilabos_msgs/action/Recrystallize.action b/unilabos_msgs/action/Recrystallize.action index fe727e8..2ae42bf 100644 --- a/unilabos_msgs/action/Recrystallize.action +++ b/unilabos_msgs/action/Recrystallize.action @@ -1,9 +1,9 @@ # Request -string ratio -string solvent1 -string solvent2 -string vessel -float64 volume +string ratio # 溶剂比例(如"1:1","3:7") +string solvent1 # 第一种溶剂 +string solvent2 # 第二种溶剂 +string vessel # 目标容器 +string volume # 🔧 总体积(支持带单位,如"100 mL","50",默认mL) --- # Result bool success diff --git a/unilabos_msgs/action/RunColumn.action b/unilabos_msgs/action/RunColumn.action index 3fba948..1f8e9ba 100644 --- a/unilabos_msgs/action/RunColumn.action +++ b/unilabos_msgs/action/RunColumn.action @@ -1,10 +1,19 @@ -string from_vessel # 源容器的名称,即样品起始所在的容器 -string to_vessel # 目标容器的名称,分离后的样品要到达的容器 -string column # 所使用的柱子的名称 +# Goal - 柱层析操作的目标参数 +string from_vessel # 源容器的名称,即样品起始所在的容器(必需) +string to_vessel # 目标容器的名称,分离后的样品要到达的容器(必需) +string column # 所使用的柱子的名称(必需) +string rf # Rf值(可选) +string pct1 # 第一种溶剂百分比(如 "40 %",可选) +string pct2 # 第二种溶剂百分比(如 "50 %",可选) +string solvent1 # 第一种溶剂名称(可选) +string solvent2 # 第二种溶剂名称(可选) +string ratio # 溶剂比例(如 "5:95",可选) --- +# Result - 操作结果 bool success # 操作是否成功 string message # 结果消息 string return_info --- +# Feedback - 实时反馈 string status # 当前状态描述 float64 progress # 进度百分比 (0-100) \ No newline at end of file diff --git a/unilabos_msgs/action/Separate.action b/unilabos_msgs/action/Separate.action index fe8976a..fc185b6 100644 --- a/unilabos_msgs/action/Separate.action +++ b/unilabos_msgs/action/Separate.action @@ -1,22 +1,27 @@ -# Organic -string purpose # 'wash' or 'extract'. 'wash' means that product phase will not be the added solvent phase, 'extract' means product phase will be the added solvent phase. If no solvent is added just use 'extract'. -string product_phase # 'top' or 'bottom'. Phase that product will be in. -string from_vessel #Contents of from_vessel are transferred to separation_vessel and separation is performed. -string separation_vessel # Vessel in which separation of phases will be carried out. -string to_vessel # Vessel to send product phase to. -string waste_phase_to_vessel # Optional. Vessel to send waste phase to. -string solvent # Optional. Solvent to add to separation vessel after contents of from_vessel has been transferred to create two phases. -float64 solvent_volume # Optional. Volume of solvent to add. -string through # Optional. Solid chemical to send product phase through on way to to_vessel, e.g. 'celite'. -int32 repeats # Optional. Number of separations to perform. -float64 stir_time # Optional. Time stir for after adding solvent, before separation of phases. -float64 stir_speed # Optional. Speed to stir at after adding solvent, before separation of phases. -float64 settling_time # Optional. Time +# Goal - 分离操作的目标参数 +string vessel # 分离容器名称(XDL参数,必需) +string purpose # 分离目的 ('wash', 'extract', 'separate',可选) +string product_phase # 产物相 ('top', 'bottom',可选) +string from_vessel # 源容器(可选) +string separation_vessel # 分离容器(与vessel同义,可选) +string to_vessel # 目标容器(可选) +string waste_phase_to_vessel # 废相目标容器(可选) +string product_vessel # 产物收集容器(XDL参数,可选) +string waste_vessel # 废液收集容器(XDL参数,可选) +string solvent # 溶剂名称(可选) +string solvent_volume # 溶剂体积(如 "200 mL",可选) +string volume # 体积规格(XDL参数,如 "?",可选) +string through # 通过材料(如 'celite',可选) +int32 repeats # 重复次数(可选,默认1) +float64 stir_time # 搅拌时间(可选,默认30秒) +float64 stir_speed # 搅拌速度(可选,默认300 RPM) +float64 settling_time # 沉降时间(可选,默认300秒) --- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 string return_info -bool success --- -string status -string current_device -builtin_interfaces/Duration time_spent -builtin_interfaces/Duration time_remaining +# Feedback - 实时反馈 +string status # 当前状态描述 +float64 progress # 进度百分比 (0-100) diff --git a/unilabos_msgs/action/Stir.action b/unilabos_msgs/action/Stir.action index 9542f9d..e3e5580 100644 --- a/unilabos_msgs/action/Stir.action +++ b/unilabos_msgs/action/Stir.action @@ -1,9 +1,16 @@ -# Organic -float64 stir_time -float64 stir_speed -float64 settling_time +# Goal - 搅拌操作的目标参数 +string vessel # 搅拌容器名称(必需) +string time # 🔧 搅拌时间(如 "0.5 h", "30 min", "300",默认秒) +string event # 事件标识(如 "A", "B") +string time_spec # 时间规格(如 "several minutes") +float64 stir_time # 解析后的搅拌时间(秒) +float64 stir_speed # 搅拌速度(默认200.0) +string settling_time # 🔧 沉降时间(支持带单位,默认秒) --- +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 string return_info -bool success --- -string status \ No newline at end of file +# Feedback - 实时反馈 +string status # 当前状态描述 \ No newline at end of file diff --git a/unilabos_msgs/action/WashSolid.action b/unilabos_msgs/action/WashSolid.action index cb57e5c..281ca4c 100644 --- a/unilabos_msgs/action/WashSolid.action +++ b/unilabos_msgs/action/WashSolid.action @@ -1,16 +1,23 @@ -string vessel # 装有固体物质的容器名称 -string solvent # 用于清洗固体的溶剂名称 -float64 volume # 清洗溶剂的体积 -string filtrate_vessel # 滤液要收集到的容器名称,可选参数 -float64 temp # 清洗时的温度,可选参数 -bool stir # 是否在清洗过程中搅拌,默认为 False -float64 stir_speed # 搅拌速度,可选参数 -float64 time # 清洗的时间,可选参数 -int32 repeats # 清洗操作的重复次数,默认为 1 +# Goal - 固体清洗操作的目标参数 +string vessel # 装有固体的容器名称(必需) +string solvent # 清洗溶剂名称(必需) +string volume # 🔧 体积(支持数字和带单位的字符串,如"100 mL","?") +string filtrate_vessel # 滤液收集容器(可选,默认"") +float64 temp # 清洗温度(可选,默认25.0) +bool stir # 是否搅拌(可选,默认false) +float64 stir_speed # 搅拌速度(可选,默认0.0) +string time # 🔧 清洗时间(支持带单位,如"5 min","300 s",默认秒) +int32 repeats # 重复次数(与repeats_spec二选一) +string volume_spec # 体积规格(优先级高于volume) +string repeats_spec # 重复次数规格(优先级高于repeats) +string mass # 固体质量描述(可选) +string event # 事件标识符(可选) --- -bool success # 操作是否成功 -string message # 结果消息 +# Result - 操作结果 +bool success # 操作是否成功 +string message # 结果消息 string return_info --- -string status # 当前状态描述 -float64 progress # 进度百分比 (0-100) \ No newline at end of file +# Feedback - 实时反馈 +string status # 当前状态描述 +float64 progress # 进度百分比 (0-100) \ No newline at end of file