From b875f86bbb0553bbedebf2ac9563fccf2b1ad017 Mon Sep 17 00:00:00 2001 From: KCFeng425 <2100011801@stu.pku.edu.cn> Date: Sat, 14 Jun 2025 18:53:43 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86add=20protocol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mock_protocol/addteststation.json | 352 ++++++++++++------ unilabos/compile/add_protocol.py | 274 ++++++++++++-- 2 files changed, 483 insertions(+), 143 deletions(-) diff --git a/test/experiments/mock_protocol/addteststation.json b/test/experiments/mock_protocol/addteststation.json index dd16a1f..6e182a8 100644 --- a/test/experiments/mock_protocol/addteststation.json +++ b/test/experiments/mock_protocol/addteststation.json @@ -4,58 +4,83 @@ "id": "AddTestStation", "name": "添加试剂测试工作站", "children": [ - "pump_add", - "flask_1", - "flask_2", - "flask_3", - "flask_4", - "reactor", + "transfer_pump", + "multiway_valve", "stirrer", - "flask_air" + "flask_reagent1", + "flask_reagent2", + "flask_reagent3", + "flask_reagent4", + "reactor", + "flask_waste", + "flask_rinsing", + "flask_buffer" ], "parent": null, "type": "device", "class": "workstation", "position": { - "x": 620.6111111111111, + "x": 620, "y": 171, "z": 0 }, "config": { - "protocol_type": ["AddProtocol", "PumpTransferProtocol", "CleanProtocol"] + "protocol_type": ["AddProtocol", "TransferProtocol", "StirProtocol", "StartStirProtocol", "StopStirProtocol"] }, "data": {} }, { - "id": "pump_add", - "name": "pump_add", + "id": "transfer_pump", + "name": "注射器泵", "children": [], "parent": "AddTestStation", "type": "device", - "class": "virtual_pump", + "class": "virtual_transfer_pump", "position": { - "x": 520.6111111111111, + "x": 520, "y": 300, "z": 0 }, "config": { "port": "VIRTUAL", - "max_volume": 25.0 + "max_volume": 50.0, + "transfer_rate": 5.0 }, "data": { "status": "Idle" } }, + { + "id": "multiway_valve", + "name": "八通阀门", + "children": [], + "parent": "AddTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 420, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL", + "positions": 8 + }, + "data": { + "status": "Idle", + "current_position": 1 + } + }, { "id": "stirrer", - "name": "stirrer", + "name": "搅拌器", "children": [], "parent": "AddTestStation", "type": "device", "class": "virtual_stirrer", "position": { - "x": 698.1111111111111, - "y": 478, + "x": 720, + "y": 450, "z": 0 }, "config": { @@ -68,110 +93,115 @@ } }, { - "id": "flask_1", - "name": "通用试剂瓶1", + "id": "flask_reagent1", + "name": "试剂瓶1 (甲醇)", "children": [], "parent": "AddTestStation", "type": "container", "class": null, "position": { "x": 100, - "y": 428, + "y": 400, "z": 0 }, "config": { - "max_volume": 2000.0 + "max_volume": 1000.0 }, "data": { - "liquid": [] + "liquid": [ + { + "name": "甲醇", + "volume": 800.0, + "concentration": "99.9%" + } + ] } }, { - "id": "flask_2", - "name": "通用试剂瓶2", + "id": "flask_reagent2", + "name": "试剂瓶2 (乙醇)", "children": [], "parent": "AddTestStation", "type": "container", "class": null, "position": { - "x": 250, - "y": 428, + "x": 180, + "y": 400, "z": 0 }, "config": { - "max_volume": 2000.0 + "max_volume": 1000.0 }, "data": { - "liquid": [] + "liquid": [ + { + "name": "乙醇", + "volume": 750.0, + "concentration": "95%" + } + ] } }, { - "id": "flask_3", - "name": "通用试剂瓶3", + "id": "flask_reagent3", + "name": "试剂瓶3 (丙酮)", "children": [], "parent": "AddTestStation", "type": "container", "class": null, "position": { - "x": 400, - "y": 428, + "x": 260, + "y": 400, "z": 0 }, "config": { - "max_volume": 2000.0 + "max_volume": 1000.0 }, "data": { - "liquid": [] + "liquid": [ + { + "name": "丙酮", + "volume": 900.0, + "concentration": "99.5%" + } + ] } }, { - "id": "flask_4", - "name": "通用试剂瓶4", + "id": "flask_reagent4", + "name": "试剂瓶4 (二氯甲烷)", "children": [], "parent": "AddTestStation", "type": "container", "class": null, "position": { - "x": 550, - "y": 428, + "x": 340, + "y": 400, "z": 0 }, "config": { - "max_volume": 2000.0 + "max_volume": 1000.0 }, "data": { - "liquid": [] + "liquid": [ + { + "name": "二氯甲烷", + "volume": 850.0, + "concentration": "99.8%" + } + ] } }, { "id": "reactor", - "name": "reactor", + "name": "反应器", "children": [], "parent": "AddTestStation", "type": "container", "class": null, "position": { - "x": 698.1111111111111, - "y": 428, - "z": 0 - }, - "config": { - "max_volume": 5000.0 - }, - "data": { - "liquid": [] - } - }, - { - "id": "flask_air", - "name": "flask_air", - "children": [], - "parent": "AddTestStation", - "type": "container", - "class": null, - "position": { - "x": 800, - "y": 300, + "x": 720, + "y": 400, "z": 0 }, "config": { @@ -180,70 +210,166 @@ "data": { "liquid": [] } + }, + { + "id": "flask_waste", + "name": "废液瓶", + "children": [], + "parent": "AddTestStation", + "type": "container", + "class": null, + "position": { + "x": 850, + "y": 400, + "z": 0 + }, + "config": { + "max_volume": 3000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_rinsing", + "name": "冲洗液瓶", + "children": [], + "parent": "AddTestStation", + "type": "container", + "class": null, + "position": { + "x": 950, + "y": 300, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "去离子水", + "volume": 800.0, + "concentration": "纯净" + } + ] + } + }, + { + "id": "flask_buffer", + "name": "缓冲液瓶", + "children": [], + "parent": "AddTestStation", + "type": "container", + "class": null, + "position": { + "x": 950, + "y": 400, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "name": "磷酸盐缓冲液", + "volume": 700.0, + "concentration": "0.1M, pH 7.4" + } + ] + } } ], "links": [ { - "source": "stirrer", + "source": "transfer_pump", + "target": "multiway_valve", + "type": "physical", + "port": { + "transfer_pump": "syringe-port", + "multiway_valve": "multiway-valve-inlet" + } + }, + { + "source": "multiway_valve", + "target": "flask_reagent1", + "type": "physical", + "port": { + "multiway_valve": "multiway-valve-port-1", + "flask_reagent1": "top" + } + }, + { + "source": "multiway_valve", + "target": "flask_reagent2", + "type": "physical", + "port": { + "multiway_valve": "multiway-valve-port-2", + "flask_reagent2": "top" + } + }, + { + "source": "multiway_valve", + "target": "flask_reagent3", + "type": "physical", + "port": { + "multiway_valve": "multiway-valve-port-3", + "flask_reagent3": "top" + } + }, + { + "source": "multiway_valve", + "target": "flask_reagent4", + "type": "physical", + "port": { + "multiway_valve": "multiway-valve-port-4", + "flask_reagent4": "top" + } + }, + { + "source": "multiway_valve", "target": "reactor", "type": "physical", "port": { - "stirrer": "top", - "reactor": "bottom" - } - }, - { - "source": "pump_add", - "target": "flask_1", - "type": "physical", - "port": { - "pump_add": "outlet", - "flask_1": "top" - } - }, - { - "source": "pump_add", - "target": "flask_2", - "type": "physical", - "port": { - "pump_add": "inlet", - "flask_2": "top" - } - }, - { - "source": "pump_add", - "target": "flask_3", - "type": "physical", - "port": { - "pump_add": "inlet", - "flask_3": "top" - } - }, - { - "source": "pump_add", - "target": "flask_4", - "type": "physical", - "port": { - "pump_add": "inlet", - "flask_4": "top" - } - }, - { - "source": "pump_add", - "target": "reactor", - "type": "physical", - "port": { - "pump_add": "outlet", + "multiway_valve": "multiway-valve-port-5", "reactor": "top" } }, { - "source": "pump_add", - "target": "flask_air", + "source": "multiway_valve", + "target": "flask_waste", "type": "physical", "port": { - "pump_add": "inlet", - "flask_air": "top" + "multiway_valve": "multiway-valve-port-6", + "flask_waste": "top" + } + }, + { + "source": "multiway_valve", + "target": "flask_rinsing", + "type": "physical", + "port": { + "multiway_valve": "multiway-valve-port-7", + "flask_rinsing": "top" + } + }, + { + "source": "multiway_valve", + "target": "flask_buffer", + "type": "physical", + "port": { + "multiway_valve": "multiway-valve-port-8", + "flask_buffer": "top" + } + }, + { + "source": "stirrer", + "target": "reactor", + "type": "physical", + "port": { + "stirrer": "stirrer-vessel", + "reactor": "bottom" } } ] diff --git a/unilabos/compile/add_protocol.py b/unilabos/compile/add_protocol.py index e2cdc3c..c75e4fd 100644 --- a/unilabos/compile/add_protocol.py +++ b/unilabos/compile/add_protocol.py @@ -15,46 +15,116 @@ def generate_add_protocol( purpose: str ) -> List[Dict[str, Any]]: """ - 生成添加试剂的协议序列 - 严格按照 Add.action + 生成添加试剂的协议序列 + + 流程: + 1. 找到包含目标试剂的试剂瓶 + 2. 配置八通阀门到试剂瓶位置 + 3. 使用注射器泵吸取试剂 + 4. 配置八通阀门到反应器位置 + 5. 使用注射器泵推送试剂到反应器 + 6. 如果需要,启动搅拌 """ action_sequence = [] - + + # 验证目标容器存在 + if vessel not in G.nodes(): + raise ValueError(f"目标容器 {vessel} 不存在") + # 如果指定了体积,执行液体转移 if volume > 0: - # 查找可用的试剂瓶 + # 1. 查找注射器泵 (transfer pump) + transfer_pump_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_transfer_pump'] + + if not transfer_pump_nodes: + raise ValueError("没有找到可用的注射器泵 (virtual_transfer_pump)") + + transfer_pump_id = transfer_pump_nodes[0] + + # 2. 查找八通阀门 + multiway_valve_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_multiway_valve'] + + if not multiway_valve_nodes: + raise ValueError("没有找到可用的八通阀门 (virtual_multiway_valve)") + + valve_id = multiway_valve_nodes[0] + + # 3. 查找包含指定试剂的试剂瓶 + reagent_vessel = None available_flasks = [node for node in G.nodes() if node.startswith('flask_') and G.nodes[node].get('type') == 'container'] - if not available_flasks: + # 简化:使用第一个可用的试剂瓶,实际应该根据试剂名称匹配 + if available_flasks: + reagent_vessel = available_flasks[0] + else: raise ValueError("没有找到可用的试剂容器") - - reagent_vessel = available_flasks[0] - # 查找泵设备 - pump_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_pump'] + # 4. 获取试剂瓶和反应器对应的阀门位置 + # 这需要根据实际连接图来确定,这里假设: + reagent_valve_position = 1 # 试剂瓶连接到阀门位置1 + reactor_valve_position = 2 # 反应器连接到阀门位置2 - if pump_nodes: - pump_id = pump_nodes[0] - action_sequence.append({ - "device_id": pump_id, - "action_name": "transfer", - "action_kwargs": { - "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 - } - }) + # 5. 执行添加操作序列 + + # 5.1 设置阀门到试剂瓶位置 + action_sequence.append({ + "device_id": valve_id, + "action_name": "set_position", + "action_kwargs": { + "position": reagent_valve_position + } + }) + + # 5.2 使用注射器泵从试剂瓶吸取液体 + action_sequence.append({ + "device_id": transfer_pump_id, + "action_name": "transfer", + "action_kwargs": { + "from_vessel": reagent_vessel, + "to_vessel": transfer_pump_id, # 吸入到注射器 + "volume": volume, + "amount": amount, + "time": time / 2, # 吸取时间为总时间的一半 + "viscous": viscous, + "rinsing_solvent": "", + "rinsing_volume": 0.0, + "rinsing_repeats": 0, + "solid": False + } + }) + + # 5.3 设置阀门到反应器位置 + action_sequence.append({ + "device_id": valve_id, + "action_name": "set_position", + "action_kwargs": { + "position": reactor_valve_position + } + }) + + # 5.4 使用注射器泵将液体推送到反应器 + action_sequence.append({ + "device_id": transfer_pump_id, + "action_name": "transfer", + "action_kwargs": { + "from_vessel": transfer_pump_id, # 从注射器推出 + "to_vessel": vessel, + "volume": volume, + "amount": amount, + "time": time / 2, # 推送时间为总时间的一半 + "viscous": viscous, + "rinsing_solvent": "", + "rinsing_volume": 0.0, + "rinsing_repeats": 0, + "solid": False + } + }) - # 如果需要搅拌,使用 StartStir 而不是 Stir + # 6. 如果需要搅拌,启动搅拌器 if stir: stirrer_nodes = [node for node in G.nodes() if G.nodes[node].get('class') == 'virtual_stirrer'] @@ -63,12 +133,156 @@ def generate_add_protocol( stirrer_id = stirrer_nodes[0] action_sequence.append({ "device_id": stirrer_id, - "action_name": "start_stir", # 使用 start_stir 而不是 stir + "action_name": "start_stir", "action_kwargs": { "vessel": vessel, "stir_speed": stir_speed, - "purpose": f"添加 {reagent} 后搅拌" + "purpose": f"添加 {reagent} 后搅拌混合" } }) + else: + print("警告:需要搅拌但未找到搅拌设备") + return action_sequence + + +def find_valve_position_for_vessel(G: nx.DiGraph, valve_id: str, vessel_id: str) -> int: + """ + 根据连接图找到容器对应的阀门位置 + + Args: + G: 网络图 + valve_id: 阀门设备ID + vessel_id: 容器ID + + Returns: + int: 阀门位置编号 (1-8) + """ + # 查找阀门到容器的连接 + edges = G.edges(data=True) + + for source, target, data in edges: + if source == valve_id and target == vessel_id: + # 从连接数据中提取端口信息 + port_info = data.get('port', {}) + valve_port = port_info.get(valve_id, '') + + # 解析端口名称获取位置编号 + if valve_port.startswith('multiway-valve-port-'): + position = valve_port.split('-')[-1] + return int(position) + + # 默认返回位置1 + return 1 + + +def generate_add_with_autodiscovery( + G: nx.DiGraph, + vessel: str, + reagent: str, + volume: float, + **kwargs +) -> List[Dict[str, Any]]: + """ + 智能添加协议生成器 - 自动发现设备连接关系 + """ + action_sequence = [] + + # 查找必需的设备 + devices = { + 'transfer_pump': None, + 'multiway_valve': None, + 'stirrer': None + } + + for node in G.nodes(): + node_class = G.nodes[node].get('class') + if node_class == 'virtual_transfer_pump': + devices['transfer_pump'] = node + elif node_class == 'virtual_multiway_valve': + devices['multiway_valve'] = node + elif node_class == 'virtual_stirrer': + devices['stirrer'] = node + + # 验证必需设备 + if not devices['transfer_pump']: + raise ValueError("缺少注射器泵设备") + if not devices['multiway_valve']: + raise ValueError("缺少八通阀门设备") + + # 查找试剂容器 + reagent_vessels = [node for node in G.nodes() + if node.startswith('flask_') + and G.nodes[node].get('type') == 'container'] + + if not reagent_vessels: + raise ValueError("没有找到试剂容器") + + # 执行添加流程 + reagent_vessel = reagent_vessels[0] + reagent_pos = find_valve_position_for_vessel(G, devices['multiway_valve'], reagent_vessel) + reactor_pos = find_valve_position_for_vessel(G, devices['multiway_valve'], vessel) + + # 生成操作序列 + action_sequence.extend([ + # 切换到试剂瓶 + { + "device_id": devices['multiway_valve'], + "action_name": "set_position", + "action_kwargs": {"position": reagent_pos} + }, + # 吸取试剂 + { + "device_id": devices['transfer_pump'], + "action_name": "transfer", + "action_kwargs": { + "from_vessel": reagent_vessel, + "to_vessel": devices['transfer_pump'], + "volume": volume, + "amount": kwargs.get('amount', ''), + "time": kwargs.get('time', 10.0) / 2, + "viscous": kwargs.get('viscous', False), + "rinsing_solvent": "", + "rinsing_volume": 0.0, + "rinsing_repeats": 0, + "solid": False + } + }, + # 切换到反应器 + { + "device_id": devices['multiway_valve'], + "action_name": "set_position", + "action_kwargs": {"position": reactor_pos} + }, + # 推送到反应器 + { + "device_id": devices['transfer_pump'], + "action_name": "transfer", + "action_kwargs": { + "from_vessel": devices['transfer_pump'], + "to_vessel": vessel, + "volume": volume, + "amount": kwargs.get('amount', ''), + "time": kwargs.get('time', 10.0) / 2, + "viscous": kwargs.get('viscous', False), + "rinsing_solvent": "", + "rinsing_volume": 0.0, + "rinsing_repeats": 0, + "solid": False + } + } + ]) + + # 如果需要搅拌 + if kwargs.get('stir', False) and devices['stirrer']: + action_sequence.append({ + "device_id": devices['stirrer'], + "action_name": "start_stir", + "action_kwargs": { + "vessel": vessel, + "stir_speed": kwargs.get('stir_speed', 300.0), + "purpose": f"添加 {reagent} 后混合" + } + }) + return action_sequence \ No newline at end of file