From 771540b88cfcd3fbf5bace5ca9e730ddb7933c6e Mon Sep 17 00:00:00 2001 From: KCFeng425 <2100011801@stu.pku.edu.cn> Date: Thu, 19 Jun 2025 20:25:07 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E5=BE=88=E5=A4=9Apr?= =?UTF-8?q?otocol=EF=BC=8C=E4=BA=B2=E6=B5=8B=E8=83=BD=E8=B7=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../add_protocol_test_station.json | 563 +++++++++++++++ .../centrifuge_protocol_test_station.json | 438 ++++++++++++ .../clean_vessel_protocol_test_station.json | 426 +++++++++++ .../dual_valve_pump_test_station.json} | 0 .../evacuateandrefill_test_station.json | 557 +++++++++++++++ .../evaporate_protocol_test_station.json | 503 +++++++++++++ .../filter_protocol_test_station.json | 534 ++++++++++++++ .../heatchill_protocol_test_station.json | 671 ++++++++++++++++++ .../pumptransfer_test_station.json} | 0 .../simple_stir_heatchill_test_station.json | 141 ++++ .../comprehensive_protocol/checklist.md | 42 +- unilabos/compile/__init__.py | 8 +- unilabos/compile/add_protocol.py | 615 +++++++++------- unilabos/compile/centrifuge_protocol.py | 324 ++++++--- unilabos/compile/clean_vessel_protocol.py | 324 ++++++--- unilabos/compile/dissolve_protocol.py | 433 ++++++++--- .../compile/evacuateandrefill_protocol.py | 540 ++++++++++---- .../compile/evacuateandrefill_protocol_old.py | 143 ++++ unilabos/compile/evaporate_protocol.py | 375 ++++++++-- unilabos/compile/filter_protocol.py | 302 +++++++- unilabos/compile/heatchill_protocol.py | 352 +++++++-- unilabos/compile/stir_protocol.py | 165 +++-- .../devices/virtual/virtual_centrifuge.py | 257 ++++--- unilabos/devices/virtual/virtual_filter.py | 250 ++++--- unilabos/devices/virtual/virtual_heatchill.py | 165 ++++- unilabos/devices/virtual/virtual_rotavap.py | 220 +++--- .../devices/virtual/virtual_solenoid_valve.py | 138 ++-- unilabos/devices/virtual/virtual_stirrer.py | 185 ++++- unilabos/messages/__init__.py | 113 +-- unilabos/registry/devices/virtual_device.yaml | 121 ++-- 30 files changed, 7477 insertions(+), 1428 deletions(-) create mode 100644 test/experiments/Protocol_Test_Station/add_protocol_test_station.json create mode 100644 test/experiments/Protocol_Test_Station/centrifuge_protocol_test_station.json create mode 100644 test/experiments/Protocol_Test_Station/clean_vessel_protocol_test_station.json rename test/experiments/{mock_protocol/dual_valve_pump_station.json => Protocol_Test_Station/dual_valve_pump_test_station.json} (100%) create mode 100644 test/experiments/Protocol_Test_Station/evacuateandrefill_test_station.json create mode 100644 test/experiments/Protocol_Test_Station/evaporate_protocol_test_station.json create mode 100644 test/experiments/Protocol_Test_Station/filter_protocol_test_station.json create mode 100644 test/experiments/Protocol_Test_Station/heatchill_protocol_test_station.json rename test/experiments/{mock_protocol/pumptransferteststation.json => Protocol_Test_Station/pumptransfer_test_station.json} (100%) create mode 100644 test/experiments/Protocol_Test_Station/simple_stir_heatchill_test_station.json create mode 100644 unilabos/compile/evacuateandrefill_protocol_old.py diff --git a/test/experiments/Protocol_Test_Station/add_protocol_test_station.json b/test/experiments/Protocol_Test_Station/add_protocol_test_station.json new file mode 100644 index 0000000..df7f818 --- /dev/null +++ b/test/experiments/Protocol_Test_Station/add_protocol_test_station.json @@ -0,0 +1,563 @@ +{ + "nodes": [ + { + "id": "AddProtocolTestStation", + "name": "添加协议测试站", + "children": [ + "transfer_pump_1", + "transfer_pump_2", + "multiway_valve_1", + "multiway_valve_2", + "stirrer_1", + "stirrer_2", + "flask_DMF", + "flask_ethyl_acetate", + "flask_methanol", + "flask_acetone", + "flask_water", + "flask_air", + "main_reactor", + "secondary_reactor", + "waste_workup", + "collection_bottle_1", + "collection_bottle_2" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 500, + "y": 200, + "z": 0 + }, + "config": { + "protocol_type": ["PumpTransferProtocol", "AddProtocol"] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "转移泵1", + "children": [], + "parent": "AddProtocolTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 250, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP1", + "max_volume": 25.0, + "transfer_rate": 5.0 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "transfer_pump_2", + "name": "转移泵2", + "children": [], + "parent": "AddProtocolTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 750, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP2", + "max_volume": 25.0, + "transfer_rate": 5.0 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "multiway_valve_1", + "name": "试剂分配阀", + "children": [], + "parent": "AddProtocolTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 250, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE1", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "multiway_valve_2", + "name": "反应器分配阀", + "children": [], + "parent": "AddProtocolTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 750, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE2", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "stirrer_1", + "name": "主反应器搅拌器", + "children": [], + "parent": "AddProtocolTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 600, + "y": 450, + "z": 0 + }, + "config": { + "port": "VIRTUAL_STIRRER1", + "max_speed": 1500.0, + "default_speed": 300.0 + }, + "data": { + "speed": 0.0, + "status": "Stopped" + } + }, + { + "id": "stirrer_2", + "name": "副反应器搅拌器", + "children": [], + "parent": "AddProtocolTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 900, + "y": 450, + "z": 0 + }, + "config": { + "port": "VIRTUAL_STIRRER2", + "max_speed": 1500.0, + "default_speed": 300.0 + }, + "data": { + "speed": 0.0, + "status": "Stopped" + } + }, + { + "id": "flask_DMF", + "name": "DMF试剂瓶", + "children": [], + "parent": "AddProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 50, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "DMF", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_ethyl_acetate", + "name": "乙酸乙酯试剂瓶", + "children": [], + "parent": "AddProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 150, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "ethyl_acetate", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_methanol", + "name": "甲醇试剂瓶", + "children": [], + "parent": "AddProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "methanol", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_acetone", + "name": "丙酮试剂瓶", + "children": [], + "parent": "AddProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 350, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "acetone", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_water", + "name": "蒸馏水瓶", + "children": [], + "parent": "AddProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 450, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "water", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "AddProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 550, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "main_reactor", + "name": "主反应器", + "children": [], + "parent": "AddProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 600, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "secondary_reactor", + "name": "副反应器", + "children": [], + "parent": "AddProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 900, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "waste_workup", + "name": "废液处理瓶", + "children": [], + "parent": "AddProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 700, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_bottle_1", + "name": "收集瓶1", + "children": [], + "parent": "AddProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 800, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_bottle_2", + "name": "收集瓶2", + "children": [], + "parent": "AddProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 900, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "id": "link_pump1_valve1", + "source": "transfer_pump_1", + "target": "multiway_valve_1", + "type": "fluid", + "port": { + "transfer_pump_1": "transferpump", + "multiway_valve_1": "transferpump" + } + }, + { + "id": "link_pump2_valve2", + "source": "transfer_pump_2", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "transfer_pump_2": "transferpump", + "multiway_valve_2": "transferpump" + } + }, + { + "id": "link_valve1_valve2", + "source": "multiway_valve_1", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "multiway_valve_1": "8", + "multiway_valve_2": "1" + } + }, + { + "id": "link_valve1_DMF", + "source": "multiway_valve_1", + "target": "flask_DMF", + "type": "fluid", + "port": { + "multiway_valve_1": "1", + "flask_DMF": "outlet" + } + }, + { + "id": "link_valve1_ethyl_acetate", + "source": "multiway_valve_1", + "target": "flask_ethyl_acetate", + "type": "fluid", + "port": { + "multiway_valve_1": "2", + "flask_ethyl_acetate": "outlet" + } + }, + { + "id": "link_valve1_methanol", + "source": "multiway_valve_1", + "target": "flask_methanol", + "type": "fluid", + "port": { + "multiway_valve_1": "3", + "flask_methanol": "outlet" + } + }, + { + "id": "link_valve1_acetone", + "source": "multiway_valve_1", + "target": "flask_acetone", + "type": "fluid", + "port": { + "multiway_valve_1": "4", + "flask_acetone": "outlet" + } + }, + { + "id": "link_valve1_water", + "source": "multiway_valve_1", + "target": "flask_water", + "type": "fluid", + "port": { + "multiway_valve_1": "5", + "flask_water": "outlet" + } + }, + { + "id": "link_valve1_air", + "source": "multiway_valve_1", + "target": "flask_air", + "type": "fluid", + "port": { + "multiway_valve_1": "6", + "flask_air": "top" + } + }, + { + "id": "link_valve2_main_reactor", + "source": "multiway_valve_2", + "target": "main_reactor", + "type": "fluid", + "port": { + "multiway_valve_2": "2", + "main_reactor": "inlet" + } + }, + { + "id": "link_valve2_secondary_reactor", + "source": "multiway_valve_2", + "target": "secondary_reactor", + "type": "fluid", + "port": { + "multiway_valve_2": "3", + "secondary_reactor": "inlet" + } + }, + { + "id": "link_valve2_waste", + "source": "multiway_valve_2", + "target": "waste_workup", + "type": "fluid", + "port": { + "multiway_valve_2": "6", + "waste_workup": "inlet" + } + }, + { + "id": "link_valve2_collection1", + "source": "multiway_valve_2", + "target": "collection_bottle_1", + "type": "fluid", + "port": { + "multiway_valve_2": "7", + "collection_bottle_1": "inlet" + } + }, + { + "id": "link_valve2_collection2", + "source": "multiway_valve_2", + "target": "collection_bottle_2", + "type": "fluid", + "port": { + "multiway_valve_2": "8", + "collection_bottle_2": "inlet" + } + }, + { + "id": "link_stirrer1_main_reactor", + "source": "stirrer_1", + "target": "main_reactor", + "type": "mechanical", + "port": { + "stirrer_1": "stirrer_head", + "main_reactor": "stirrer_port" + } + }, + { + "id": "link_stirrer2_secondary_reactor", + "source": "stirrer_2", + "target": "secondary_reactor", + "type": "mechanical", + "port": { + "stirrer_2": "stirrer_head", + "secondary_reactor": "stirrer_port" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/Protocol_Test_Station/centrifuge_protocol_test_station.json b/test/experiments/Protocol_Test_Station/centrifuge_protocol_test_station.json new file mode 100644 index 0000000..affb6e8 --- /dev/null +++ b/test/experiments/Protocol_Test_Station/centrifuge_protocol_test_station.json @@ -0,0 +1,438 @@ +{ + "nodes": [ + { + "id": "CentrifugeProtocolTestStation", + "name": "离心协议测试站", + "children": [ + "transfer_pump_1", + "transfer_pump_2", + "multiway_valve_1", + "multiway_valve_2", + "centrifuge_1", + "reaction_mixture", + "centrifuge_tube", + "collection_bottle_1", + "flask_water", + "flask_ethanol", + "flask_acetone", + "flask_air", + "waste_workup" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 500, + "y": 200, + "z": 0 + }, + "config": { + "protocol_type": [ + "CentrifugeProtocol", + "PumpTransferProtocol" + ] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "主转移泵", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 200, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP1", + "max_volume": 25.0, + "transfer_rate": 2.0 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "transfer_pump_2", + "name": "副转移泵", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 400, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP2", + "max_volume": 25.0, + "transfer_rate": 2.0 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "multiway_valve_1", + "name": "溶剂分配阀", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 200, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE1", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "multiway_valve_2", + "name": "样品分配阀", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 400, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE2", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "centrifuge_1", + "name": "离心机", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "device", + "class": "virtual_centrifuge", + "position": { + "x": 600, + "y": 350, + "z": 0 + }, + "config": { + "port": "VIRTUAL_CENTRIFUGE1", + "max_speed": 15000.0, + "max_temp": 40.0, + "min_temp": 4.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "reaction_mixture", + "name": "反应混合物", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "cell_suspension", + "liquid_volume": 200.0 + } + ] + } + }, + { + "id": "centrifuge_tube", + "name": "离心管", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 600, + "y": 450, + "z": 0 + }, + "config": { + "max_volume": 15.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_bottle_1", + "name": "上清液收集瓶", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 700, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_water", + "name": "蒸馏水瓶", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 200, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "water", + "liquid_volume": 900.0 + } + ] + } + }, + { + "id": "flask_ethanol", + "name": "乙醇清洗瓶", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 300, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "ethanol", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_acetone", + "name": "丙酮清洗瓶", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "acetone", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "waste_workup", + "name": "废液瓶", + "children": [], + "parent": "CentrifugeProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 800, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "id": "link_pump1_valve1", + "source": "transfer_pump_1", + "target": "multiway_valve_1", + "type": "fluid", + "port": { + "transfer_pump_1": "transferpump", + "multiway_valve_1": "transferpump" + } + }, + { + "id": "link_pump2_valve2", + "source": "transfer_pump_2", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "transfer_pump_2": "transferpump", + "multiway_valve_2": "transferpump" + } + }, + { + "id": "link_valve1_air", + "source": "multiway_valve_1", + "target": "flask_air", + "type": "fluid", + "port": { + "multiway_valve_1": "1", + "flask_air": "top" + } + }, + { + "id": "link_valve1_water", + "source": "multiway_valve_1", + "target": "flask_water", + "type": "fluid", + "port": { + "multiway_valve_1": "2", + "flask_water": "outlet" + } + }, + { + "id": "link_valve1_ethanol", + "source": "multiway_valve_1", + "target": "flask_ethanol", + "type": "fluid", + "port": { + "multiway_valve_1": "3", + "flask_ethanol": "outlet" + } + }, + { + "id": "link_valve1_acetone", + "source": "multiway_valve_1", + "target": "flask_acetone", + "type": "fluid", + "port": { + "multiway_valve_1": "4", + "flask_acetone": "outlet" + } + }, + { + "id": "link_valve1_valve2", + "source": "multiway_valve_1", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "multiway_valve_1": "5", + "multiway_valve_2": "1" + } + }, + { + "id": "link_valve2_reaction_mixture", + "source": "multiway_valve_2", + "target": "reaction_mixture", + "type": "fluid", + "port": { + "multiway_valve_2": "2", + "reaction_mixture": "inlet" + } + }, + { + "id": "link_valve2_centrifuge_tube", + "source": "multiway_valve_2", + "target": "centrifuge_tube", + "type": "fluid", + "port": { + "multiway_valve_2": "3", + "centrifuge_tube": "inlet" + } + }, + { + "id": "link_valve2_collection", + "source": "multiway_valve_2", + "target": "collection_bottle_1", + "type": "fluid", + "port": { + "multiway_valve_2": "4", + "collection_bottle_1": "inlet" + } + }, + { + "id": "link_valve2_waste", + "source": "multiway_valve_2", + "target": "waste_workup", + "type": "fluid", + "port": { + "multiway_valve_2": "5", + "waste_workup": "inlet" + } + }, + { + "id": "link_centrifuge1_centrifuge_tube", + "source": "centrifuge_1", + "target": "centrifuge_tube", + "type": "transport", + "port": { + "centrifuge_1": "centrifuge", + "centrifuge_tube": "centrifuge_port" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/Protocol_Test_Station/clean_vessel_protocol_test_station.json b/test/experiments/Protocol_Test_Station/clean_vessel_protocol_test_station.json new file mode 100644 index 0000000..cdd9615 --- /dev/null +++ b/test/experiments/Protocol_Test_Station/clean_vessel_protocol_test_station.json @@ -0,0 +1,426 @@ +{ + "nodes": [ + { + "id": "CleanVesselProtocolTestStation", + "name": "容器清洗协议测试站", + "children": [ + "transfer_pump_1", + "transfer_pump_2", + "multiway_valve_1", + "multiway_valve_2", + "heatchill_1", + "flask_water", + "flask_acetone", + "flask_ethanol", + "flask_air", + "main_reactor", + "secondary_reactor", + "waste_workup" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 500, + "y": 200, + "z": 0 + }, + "config": { + "protocol_type": [ + "CleanVesselProtocol", + "PumpTransferProtocol", + "HeatChillProtocol", + "HeatChillStartProtocol", + "HeatChillStopProtocol" + ] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "主清洗泵", + "children": [], + "parent": "CleanVesselProtocolTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 250, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP1", + "max_volume": 25.0, + "transfer_rate": 2.5 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "transfer_pump_2", + "name": "副清洗泵", + "children": [], + "parent": "CleanVesselProtocolTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 450, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP2", + "max_volume": 25.0, + "transfer_rate": 2.5 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "multiway_valve_1", + "name": "溶剂分配阀", + "children": [], + "parent": "CleanVesselProtocolTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 250, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE1", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "multiway_valve_2", + "name": "容器分配阀", + "children": [], + "parent": "CleanVesselProtocolTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 450, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE2", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "heatchill_1", + "name": "加热清洗器", + "children": [], + "parent": "CleanVesselProtocolTestStation", + "type": "device", + "class": "virtual_heatchill", + "position": { + "x": 600, + "y": 350, + "z": 0 + }, + "config": { + "port": "VIRTUAL_HEATCHILL1", + "max_temp": 100.0, + "min_temp": 10.0, + "max_stir_speed": 500.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "flask_water", + "name": "蒸馏水瓶", + "children": [], + "parent": "CleanVesselProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 50, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "water", + "liquid_volume": 900.0 + } + ] + } + }, + { + "id": "flask_acetone", + "name": "丙酮清洗瓶", + "children": [], + "parent": "CleanVesselProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 150, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "acetone", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_ethanol", + "name": "乙醇清洗瓶", + "children": [], + "parent": "CleanVesselProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "ethanol", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "CleanVesselProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 350, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "main_reactor", + "name": "主反应器", + "children": [], + "parent": "CleanVesselProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 600, + "y": 450, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "residue", + "liquid_volume": 50.0 + } + ] + } + }, + { + "id": "secondary_reactor", + "name": "副反应器", + "children": [], + "parent": "CleanVesselProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 800, + "y": 450, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "organic_residue", + "liquid_volume": 30.0 + } + ] + } + }, + { + "id": "waste_workup", + "name": "清洗废液瓶", + "children": [], + "parent": "CleanVesselProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 700, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 3000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "id": "link_pump1_valve1", + "source": "transfer_pump_1", + "target": "multiway_valve_1", + "type": "fluid", + "port": { + "transfer_pump_1": "transferpump", + "multiway_valve_1": "transferpump" + } + }, + { + "id": "link_pump2_valve2", + "source": "transfer_pump_2", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "transfer_pump_2": "transferpump", + "multiway_valve_2": "transferpump" + } + }, + { + "id": "link_valve1_air", + "source": "multiway_valve_1", + "target": "flask_air", + "type": "fluid", + "port": { + "multiway_valve_1": "1", + "flask_air": "top" + } + }, + { + "id": "link_valve1_water", + "source": "multiway_valve_1", + "target": "flask_water", + "type": "fluid", + "port": { + "multiway_valve_1": "2", + "flask_water": "outlet" + } + }, + { + "id": "link_valve1_acetone", + "source": "multiway_valve_1", + "target": "flask_acetone", + "type": "fluid", + "port": { + "multiway_valve_1": "3", + "flask_acetone": "outlet" + } + }, + { + "id": "link_valve1_ethanol", + "source": "multiway_valve_1", + "target": "flask_ethanol", + "type": "fluid", + "port": { + "multiway_valve_1": "4", + "flask_ethanol": "outlet" + } + }, + { + "id": "link_valve1_valve2", + "source": "multiway_valve_1", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "multiway_valve_1": "5", + "multiway_valve_2": "1" + } + }, + { + "id": "link_valve2_main_reactor", + "source": "multiway_valve_2", + "target": "main_reactor", + "type": "fluid", + "port": { + "multiway_valve_2": "2", + "main_reactor": "inlet" + } + }, + { + "id": "link_valve2_secondary_reactor", + "source": "multiway_valve_2", + "target": "secondary_reactor", + "type": "fluid", + "port": { + "multiway_valve_2": "3", + "secondary_reactor": "inlet" + } + }, + { + "id": "link_valve2_waste", + "source": "multiway_valve_2", + "target": "waste_workup", + "type": "fluid", + "port": { + "multiway_valve_2": "4", + "waste_workup": "inlet" + } + }, + { + "id": "link_valve2_air_return", + "source": "multiway_valve_2", + "target": "flask_air", + "type": "fluid", + "port": { + "multiway_valve_2": "5", + "flask_air": "bottom" + } + }, + { + "id": "link_heatchill1_main_reactor", + "source": "heatchill_1", + "target": "main_reactor", + "type": "mechanical", + "port": { + "heatchill_1": "heatchill", + "main_reactor": "heating_jacket" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/mock_protocol/dual_valve_pump_station.json b/test/experiments/Protocol_Test_Station/dual_valve_pump_test_station.json similarity index 100% rename from test/experiments/mock_protocol/dual_valve_pump_station.json rename to test/experiments/Protocol_Test_Station/dual_valve_pump_test_station.json diff --git a/test/experiments/Protocol_Test_Station/evacuateandrefill_test_station.json b/test/experiments/Protocol_Test_Station/evacuateandrefill_test_station.json new file mode 100644 index 0000000..b4f89c2 --- /dev/null +++ b/test/experiments/Protocol_Test_Station/evacuateandrefill_test_station.json @@ -0,0 +1,557 @@ +{ + "nodes": [ + { + "id": "EvacuateRefillTestStation", + "name": "抽真空充气测试站", + "children": [ + "transfer_pump_1", + "transfer_pump_2", + "multiway_valve_1", + "multiway_valve_2", + "flask_DMF", + "flask_ethyl_acetate", + "flask_methanol", + "flask_air", + "vacuum_pump_1", + "gas_source_nitrogen", + "gas_source_air", + "solenoid_valve_vacuum", + "solenoid_valve_gas", + "main_reactor", + "stirrer_1", + "waste_workup", + "collection_bottle_1" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 500, + "y": 200, + "z": 0 + }, + "config": { + "protocol_type": ["PumpTransferProtocol", "EvacuateAndRefillProtocol"] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "转移泵1", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 300, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP1", + "max_volume": 25.0, + "transfer_rate": 5.0 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "transfer_pump_2", + "name": "转移泵2", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 700, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP2", + "max_volume": 25.0, + "transfer_rate": 5.0 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "multiway_valve_1", + "name": "第一个八通阀", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 300, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE1", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "multiway_valve_2", + "name": "第二个八通阀", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 700, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE2", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "vacuum_pump_1", + "name": "真空泵1", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "device", + "class": "virtual_vacuum_pump", + "position": { + "x": 150, + "y": 200, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VACUUM1", + "max_pressure": -0.9 + }, + "data": { + "status": "OFF", + "pressure": 0.0 + } + }, + { + "id": "gas_source_nitrogen", + "name": "氮气源", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "device", + "class": "virtual_gas_source", + "position": { + "x": 850, + "y": 200, + "z": 0 + }, + "config": { + "port": "VIRTUAL_GAS_N2", + "gas_type": "nitrogen", + "max_pressure": 5.0 + }, + "data": { + "status": "OFF", + "flow_rate": 0.0 + } + }, + { + "id": "gas_source_air", + "name": "空气源", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "device", + "class": "virtual_gas_source", + "position": { + "x": 950, + "y": 200, + "z": 0 + }, + "config": { + "port": "VIRTUAL_GAS_AIR", + "gas_type": "air", + "max_pressure": 3.0 + }, + "data": { + "status": "OFF", + "flow_rate": 0.0 + } + }, + { + "id": "solenoid_valve_vacuum", + "name": "真空电磁阀", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "device", + "class": "virtual_solenoid_valve", + "position": { + "x": 225, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_SOLENOID_VACUUM" + }, + "data": { + "valve_position": "CLOSED" + } + }, + { + "id": "solenoid_valve_gas", + "name": "气源电磁阀", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "device", + "class": "virtual_solenoid_valve", + "position": { + "x": 775, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_SOLENOID_GAS" + }, + "data": { + "valve_position": "CLOSED" + } + }, + { + "id": "flask_DMF", + "name": "DMF试剂瓶", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "DMF", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_ethyl_acetate", + "name": "乙酸乙酯试剂瓶", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "container", + "class": null, + "position": { + "x": 200, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "ethyl_acetate", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_methanol", + "name": "甲醇试剂瓶", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "container", + "class": null, + "position": { + "x": 300, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "methanol", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "main_reactor", + "name": "主反应器", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "container", + "class": null, + "position": { + "x": 600, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "stirrer_1", + "name": "搅拌器1", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 600, + "y": 450, + "z": 0 + }, + "config": { + "port": "VIRTUAL_STIRRER1", + "max_speed": 1500.0 + }, + "data": { + "speed": 0.0, + "status": "OFF" + } + }, + { + "id": "waste_workup", + "name": "废液处理瓶", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "container", + "class": null, + "position": { + "x": 700, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_bottle_1", + "name": "收集瓶1", + "children": [], + "parent": "EvacuateRefillTestStation", + "type": "container", + "class": null, + "position": { + "x": 800, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "id": "link_pump1_valve1", + "source": "transfer_pump_1", + "target": "multiway_valve_1", + "type": "fluid", + "port": { + "transfer_pump_1": "transferpump", + "multiway_valve_1": "transferpump" + } + }, + { + "id": "link_pump2_valve2", + "source": "transfer_pump_2", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "transfer_pump_2": "transferpump", + "multiway_valve_2": "transferpump" + } + }, + { + "id": "link_valve1_valve2", + "source": "multiway_valve_1", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "multiway_valve_1": "8", + "multiway_valve_2": "1" + } + }, + { + "id": "link_vacuum_solenoid", + "source": "vacuum_pump_1", + "target": "solenoid_valve_vacuum", + "type": "fluid", + "port": { + "vacuum_pump_1": "outlet", + "solenoid_valve_vacuum": "inlet" + } + }, + { + "id": "link_solenoid_vacuum_valve1", + "source": "solenoid_valve_vacuum", + "target": "multiway_valve_1", + "type": "fluid", + "port": { + "solenoid_valve_vacuum": "outlet", + "multiway_valve_1": "7" + } + }, + { + "id": "link_gas_solenoid", + "source": "gas_source_nitrogen", + "target": "solenoid_valve_gas", + "type": "fluid", + "port": { + "gas_source_nitrogen": "outlet", + "solenoid_valve_gas": "inlet" + } + }, + { + "id": "link_solenoid_gas_valve2", + "source": "solenoid_valve_gas", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "solenoid_valve_gas": "outlet", + "multiway_valve_2": "8" + } + }, + { + "id": "link_air_source_valve2", + "source": "gas_source_air", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "gas_source_air": "outlet", + "multiway_valve_2": "2" + } + }, + { + "id": "link_valve1_air", + "source": "multiway_valve_1", + "target": "flask_air", + "type": "fluid", + "port": { + "multiway_valve_1": "1", + "flask_air": "top" + } + }, + { + "id": "link_valve1_DMF", + "source": "multiway_valve_1", + "target": "flask_DMF", + "type": "fluid", + "port": { + "multiway_valve_1": "2", + "flask_DMF": "outlet" + } + }, + { + "id": "link_valve1_ethyl_acetate", + "source": "multiway_valve_1", + "target": "flask_ethyl_acetate", + "type": "fluid", + "port": { + "multiway_valve_1": "3", + "flask_ethyl_acetate": "outlet" + } + }, + { + "id": "link_valve1_methanol", + "source": "multiway_valve_1", + "target": "flask_methanol", + "type": "fluid", + "port": { + "multiway_valve_1": "4", + "flask_methanol": "outlet" + } + }, + { + "id": "link_valve2_reactor", + "source": "multiway_valve_2", + "target": "main_reactor", + "type": "fluid", + "port": { + "multiway_valve_2": "5", + "main_reactor": "inlet" + } + }, + { + "id": "link_valve2_waste", + "source": "multiway_valve_2", + "target": "waste_workup", + "type": "fluid", + "port": { + "multiway_valve_2": "6", + "waste_workup": "inlet" + } + }, + { + "id": "link_valve2_collection", + "source": "multiway_valve_2", + "target": "collection_bottle_1", + "type": "fluid", + "port": { + "multiway_valve_2": "7", + "collection_bottle_1": "inlet" + } + }, + { + "id": "link_stirrer_reactor", + "source": "stirrer_1", + "target": "main_reactor", + "type": "mechanical", + "port": { + "stirrer_1": "stirrer", + "main_reactor": "stirrer" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/Protocol_Test_Station/evaporate_protocol_test_station.json b/test/experiments/Protocol_Test_Station/evaporate_protocol_test_station.json new file mode 100644 index 0000000..d47bb4d --- /dev/null +++ b/test/experiments/Protocol_Test_Station/evaporate_protocol_test_station.json @@ -0,0 +1,503 @@ +{ + "nodes": [ + { + "id": "EvaporateProtocolTestStation", + "name": "蒸发协议测试站", + "children": [ + "transfer_pump_1", + "transfer_pump_2", + "multiway_valve_1", + "multiway_valve_2", + "rotavap_1", + "heatchill_1", + "reaction_mixture", + "rotavap_flask", + "rotavap_condenser", + "flask_distillate", + "flask_ethanol", + "flask_acetone", + "flask_water", + "flask_air", + "waste_workup" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 500, + "y": 200, + "z": 0 + }, + "config": { + "protocol_type": [ + "EvaporateProtocol", + "PumpTransferProtocol", + "HeatChillProtocol", + "HeatChillStartProtocol", + "HeatChillStopProtocol" + ] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "主转移泵", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 200, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP1", + "max_volume": 25.0, + "transfer_rate": 2.5 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "transfer_pump_2", + "name": "副转移泵", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 400, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP2", + "max_volume": 25.0, + "transfer_rate": 2.5 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "multiway_valve_1", + "name": "溶剂分配阀", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 200, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE1", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "multiway_valve_2", + "name": "容器分配阀", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 400, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE2", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "rotavap_1", + "name": "旋转蒸发仪", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "device", + "class": "virtual_rotavap", + "position": { + "x": 700, + "y": 350, + "z": 0 + }, + "config": { + "port": "VIRTUAL_ROTAVAP1", + "max_temp": 180.0, + "max_rotation_speed": 280.0 + }, + "data": { + "status": "Ready" + } + }, + { + "id": "heatchill_1", + "name": "预加热器", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "device", + "class": "virtual_heatchill", + "position": { + "x": 100, + "y": 550, + "z": 0 + }, + "config": { + "port": "VIRTUAL_HEATCHILL1", + "max_temp": 100.0, + "min_temp": 10.0, + "max_stir_speed": 500.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "reaction_mixture", + "name": "反应混合物", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 450, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "reaction_mixture", + "liquid_volume": 600.0 + } + ] + } + }, + { + "id": "rotavap_flask", + "name": "旋蒸样品瓶", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 700, + "y": 450, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "rotavap_condenser", + "name": "旋蒸冷凝器", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 800, + "y": 350, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_distillate", + "name": "溶剂回收瓶", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 800, + "y": 450, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_ethanol", + "name": "乙醇清洗瓶", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 50, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "ethanol", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_acetone", + "name": "丙酮清洗瓶", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 150, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "acetone", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_water", + "name": "蒸馏水瓶", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "water", + "liquid_volume": 900.0 + } + ] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 350, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "waste_workup", + "name": "废液瓶", + "children": [], + "parent": "EvaporateProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 600, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 3000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "id": "link_pump1_valve1", + "source": "transfer_pump_1", + "target": "multiway_valve_1", + "type": "fluid", + "port": { + "transfer_pump_1": "transferpump", + "multiway_valve_1": "transferpump" + } + }, + { + "id": "link_pump2_valve2", + "source": "transfer_pump_2", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "transfer_pump_2": "transferpump", + "multiway_valve_2": "transferpump" + } + }, + { + "id": "link_valve1_air", + "source": "multiway_valve_1", + "target": "flask_air", + "type": "fluid", + "port": { + "multiway_valve_1": "1", + "flask_air": "top" + } + }, + { + "id": "link_valve1_ethanol", + "source": "multiway_valve_1", + "target": "flask_ethanol", + "type": "fluid", + "port": { + "multiway_valve_1": "2", + "flask_ethanol": "outlet" + } + }, + { + "id": "link_valve1_acetone", + "source": "multiway_valve_1", + "target": "flask_acetone", + "type": "fluid", + "port": { + "multiway_valve_1": "3", + "flask_acetone": "outlet" + } + }, + { + "id": "link_valve1_water", + "source": "multiway_valve_1", + "target": "flask_water", + "type": "fluid", + "port": { + "multiway_valve_1": "4", + "flask_water": "outlet" + } + }, + { + "id": "link_valve1_valve2", + "source": "multiway_valve_1", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "multiway_valve_1": "5", + "multiway_valve_2": "1" + } + }, + { + "id": "link_valve2_reaction_mixture", + "source": "multiway_valve_2", + "target": "reaction_mixture", + "type": "fluid", + "port": { + "multiway_valve_2": "2", + "reaction_mixture": "inlet" + } + }, + { + "id": "link_valve2_rotavap_flask", + "source": "multiway_valve_2", + "target": "rotavap_flask", + "type": "fluid", + "port": { + "multiway_valve_2": "3", + "rotavap_flask": "inlet" + } + }, + { + "id": "link_valve2_rotavap_condenser", + "source": "multiway_valve_2", + "target": "rotavap_condenser", + "type": "fluid", + "port": { + "multiway_valve_2": "4", + "rotavap_condenser": "inlet" + } + }, + { + "id": "link_valve2_distillate", + "source": "multiway_valve_2", + "target": "flask_distillate", + "type": "fluid", + "port": { + "multiway_valve_2": "5", + "flask_distillate": "inlet" + } + }, + { + "id": "link_valve2_waste", + "source": "multiway_valve_2", + "target": "waste_workup", + "type": "fluid", + "port": { + "multiway_valve_2": "6", + "waste_workup": "inlet" + } + }, + { + "id": "link_rotavap1_rotavap_flask", + "source": "rotavap_1", + "target": "rotavap_flask", + "type": "fluid", + "port": { + "rotavap_1": "rotavap-sample", + "rotavap_flask": "rotavap_port" + } + }, + { + "id": "link_heatchill1_reaction_mixture", + "source": "heatchill_1", + "target": "reaction_mixture", + "type": "mechanical", + "port": { + "heatchill_1": "heatchill", + "reaction_mixture": "heating_jacket" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/Protocol_Test_Station/filter_protocol_test_station.json b/test/experiments/Protocol_Test_Station/filter_protocol_test_station.json new file mode 100644 index 0000000..9a25b76 --- /dev/null +++ b/test/experiments/Protocol_Test_Station/filter_protocol_test_station.json @@ -0,0 +1,534 @@ +{ + "nodes": [ + { + "id": "FilterProtocolTestStation", + "name": "过滤协议测试站", + "children": [ + "transfer_pump_1", + "transfer_pump_2", + "multiway_valve_1", + "multiway_valve_2", + "filter_1", + "heatchill_1", + "reaction_mixture", + "filter_vessel", + "filtrate_vessel", + "collection_bottle_1", + "collection_bottle_2", + "flask_water", + "flask_ethanol", + "flask_acetone", + "flask_air", + "waste_workup" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 500, + "y": 200, + "z": 0 + }, + "config": { + "protocol_type": [ + "FilterProtocol", + "PumpTransferProtocol", + "HeatChillProtocol", + "HeatChillStartProtocol", + "HeatChillStopProtocol" + ] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "主转移泵", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 200, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP1", + "max_volume": 25.0, + "transfer_rate": 2.0 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "transfer_pump_2", + "name": "副转移泵", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 400, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP2", + "max_volume": 25.0, + "transfer_rate": 2.0 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "multiway_valve_1", + "name": "溶剂分配阀", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 200, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE1", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "multiway_valve_2", + "name": "样品分配阀", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 400, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE2", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "filter_1", + "name": "过滤器", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "device", + "class": "virtual_filter", + "position": { + "x": 600, + "y": 350, + "z": 0 + }, + "config": { + "port": "VIRTUAL_FILTER1", + "max_temp": 100.0, + "max_stir_speed": 1000.0, + "max_volume": 500.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "heatchill_1", + "name": "加热搅拌器", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "device", + "class": "virtual_heatchill", + "position": { + "x": 600, + "y": 450, + "z": 0 + }, + "config": { + "port": "VIRTUAL_HEATCHILL1", + "max_temp": 100.0, + "min_temp": 4.0, + "max_stir_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "reaction_mixture", + "name": "反应混合物", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "cell_suspension", + "liquid_volume": 200.0 + } + ] + } + }, + { + "id": "filter_vessel", + "name": "过滤器容器", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 600, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "filtrate_vessel", + "name": "滤液收集容器", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 700, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 500.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_bottle_1", + "name": "收集瓶1", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 800, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_bottle_2", + "name": "收集瓶2", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 900, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "flask_water", + "name": "蒸馏水瓶", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 200, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "water", + "liquid_volume": 900.0 + } + ] + } + }, + { + "id": "flask_ethanol", + "name": "乙醇清洗瓶", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 300, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "ethanol", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_acetone", + "name": "丙酮清洗瓶", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 400, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "acetone", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 100, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "waste_workup", + "name": "废液瓶", + "children": [], + "parent": "FilterProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 800, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "id": "link_pump1_valve1", + "source": "transfer_pump_1", + "target": "multiway_valve_1", + "type": "fluid", + "port": { + "transfer_pump_1": "transferpump", + "multiway_valve_1": "transferpump" + } + }, + { + "id": "link_pump2_valve2", + "source": "transfer_pump_2", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "transfer_pump_2": "transferpump", + "multiway_valve_2": "transferpump" + } + }, + { + "id": "link_valve1_air", + "source": "multiway_valve_1", + "target": "flask_air", + "type": "fluid", + "port": { + "multiway_valve_1": "1", + "flask_air": "top" + } + }, + { + "id": "link_valve1_water", + "source": "multiway_valve_1", + "target": "flask_water", + "type": "fluid", + "port": { + "multiway_valve_1": "2", + "flask_water": "outlet" + } + }, + { + "id": "link_valve1_ethanol", + "source": "multiway_valve_1", + "target": "flask_ethanol", + "type": "fluid", + "port": { + "multiway_valve_1": "3", + "flask_ethanol": "outlet" + } + }, + { + "id": "link_valve1_acetone", + "source": "multiway_valve_1", + "target": "flask_acetone", + "type": "fluid", + "port": { + "multiway_valve_1": "4", + "flask_acetone": "outlet" + } + }, + { + "id": "link_valve1_valve2", + "source": "multiway_valve_1", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "multiway_valve_1": "5", + "multiway_valve_2": "1" + } + }, + { + "id": "link_valve2_reaction_mixture", + "source": "multiway_valve_2", + "target": "reaction_mixture", + "type": "fluid", + "port": { + "multiway_valve_2": "2", + "reaction_mixture": "inlet" + } + }, + { + "id": "link_valve2_filter_vessel", + "source": "multiway_valve_2", + "target": "filter_vessel", + "type": "fluid", + "port": { + "multiway_valve_2": "3", + "filter_vessel": "inlet" + } + }, + { + "id": "link_valve2_filtrate_vessel", + "source": "multiway_valve_2", + "target": "filtrate_vessel", + "type": "fluid", + "port": { + "multiway_valve_2": "4", + "filtrate_vessel": "inlet" + } + }, + { + "id": "link_valve2_collection1", + "source": "multiway_valve_2", + "target": "collection_bottle_1", + "type": "fluid", + "port": { + "multiway_valve_2": "5", + "collection_bottle_1": "inlet" + } + }, + { + "id": "link_valve2_collection2", + "source": "multiway_valve_2", + "target": "collection_bottle_2", + "type": "fluid", + "port": { + "multiway_valve_2": "6", + "collection_bottle_2": "inlet" + } + }, + { + "id": "link_valve2_waste", + "source": "multiway_valve_2", + "target": "waste_workup", + "type": "fluid", + "port": { + "multiway_valve_2": "7", + "waste_workup": "inlet" + } + }, + { + "id": "link_filter1_filter_vessel", + "source": "filter_1", + "target": "filter_vessel", + "type": "transport", + "port": { + "filter_1": "filter", + "filter_vessel": "filter_port" + } + }, + { + "id": "link_heatchill1_filter_vessel", + "source": "heatchill_1", + "target": "filter_vessel", + "type": "mechanical", + "port": { + "heatchill_1": "heatchill", + "filter_vessel": "heating_jacket" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/Protocol_Test_Station/heatchill_protocol_test_station.json b/test/experiments/Protocol_Test_Station/heatchill_protocol_test_station.json new file mode 100644 index 0000000..751f312 --- /dev/null +++ b/test/experiments/Protocol_Test_Station/heatchill_protocol_test_station.json @@ -0,0 +1,671 @@ +{ + "nodes": [ + { + "id": "HeatChillProtocolTestStation", + "name": "加热冷却协议测试站", + "children": [ + "transfer_pump_1", + "transfer_pump_2", + "multiway_valve_1", + "multiway_valve_2", + "stirrer_1", + "stirrer_2", + "heatchill_1", + "heatchill_2", + "flask_DMF", + "flask_ethyl_acetate", + "flask_methanol", + "flask_acetone", + "flask_water", + "flask_ethanol", + "flask_air", + "main_reactor", + "secondary_reactor", + "waste_workup", + "collection_bottle_1", + "collection_bottle_2" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 500, + "y": 200, + "z": 0 + }, + "config": { + "protocol_type": [ + "PumpTransferProtocol", + "AddProtocol", + "HeatChillProtocol", + "HeatChillStartProtocol", + "HeatChillStopProtocol", + "DissolveProtocol" + ] + }, + "data": {} + }, + { + "id": "transfer_pump_1", + "name": "转移泵1", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 250, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP1", + "max_volume": 25.0, + "transfer_rate": 5.0 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "transfer_pump_2", + "name": "转移泵2", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "device", + "class": "virtual_transfer_pump", + "position": { + "x": 750, + "y": 300, + "z": 0 + }, + "config": { + "port": "VIRTUAL_PUMP2", + "max_volume": 25.0, + "transfer_rate": 5.0 + }, + "data": { + "position": 0.0, + "status": "Idle" + } + }, + { + "id": "multiway_valve_1", + "name": "试剂分配阀", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 250, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE1", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "multiway_valve_2", + "name": "反应器分配阀", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "device", + "class": "virtual_multiway_valve", + "position": { + "x": 750, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_VALVE2", + "positions": 8 + }, + "data": { + "current_position": 1 + } + }, + { + "id": "stirrer_1", + "name": "主反应器搅拌器", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 600, + "y": 450, + "z": 0 + }, + "config": { + "port": "VIRTUAL_STIRRER1", + "max_speed": 1500.0, + "default_speed": 300.0 + }, + "data": { + "speed": 0.0, + "status": "Stopped" + } + }, + { + "id": "stirrer_2", + "name": "副反应器搅拌器", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 900, + "y": 450, + "z": 0 + }, + "config": { + "port": "VIRTUAL_STIRRER2", + "max_speed": 1500.0, + "default_speed": 300.0 + }, + "data": { + "speed": 0.0, + "status": "Stopped" + } + }, + { + "id": "heatchill_1", + "name": "主反应器加热冷却器", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "device", + "class": "virtual_heatchill", + "position": { + "x": 550, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_HEATCHILL1", + "max_temp": 200.0, + "min_temp": -80.0, + "max_stir_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "heatchill_2", + "name": "副反应器加热冷却器", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "device", + "class": "virtual_heatchill", + "position": { + "x": 850, + "y": 400, + "z": 0 + }, + "config": { + "port": "VIRTUAL_HEATCHILL2", + "max_temp": 200.0, + "min_temp": -80.0, + "max_stir_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "flask_DMF", + "name": "DMF试剂瓶", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 50, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "DMF", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_ethyl_acetate", + "name": "乙酸乙酯试剂瓶", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 150, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "ethyl_acetate", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_methanol", + "name": "甲醇试剂瓶", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 250, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "methanol", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_ethanol", + "name": "乙醇试剂瓶", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 650, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "ethanol", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_acetone", + "name": "丙酮试剂瓶", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 350, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "acetone", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_water", + "name": "蒸馏水瓶", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 450, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "water", + "liquid_volume": 800.0 + } + ] + } + }, + { + "id": "flask_air", + "name": "空气瓶", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 550, + "y": 550, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "main_reactor", + "name": "主反应器", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 600, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "secondary_reactor", + "name": "副反应器", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 900, + "y": 500, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "waste_workup", + "name": "废液处理瓶", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 700, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_bottle_1", + "name": "收集瓶1", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 800, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + }, + { + "id": "collection_bottle_2", + "name": "收集瓶2", + "children": [], + "parent": "HeatChillProtocolTestStation", + "type": "container", + "class": null, + "position": { + "x": 900, + "y": 600, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "id": "link_pump1_valve1", + "source": "transfer_pump_1", + "target": "multiway_valve_1", + "type": "fluid", + "port": { + "transfer_pump_1": "transferpump", + "multiway_valve_1": "transferpump" + } + }, + { + "id": "link_pump2_valve2", + "source": "transfer_pump_2", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "transfer_pump_2": "transferpump", + "multiway_valve_2": "transferpump" + } + }, + { + "id": "link_valve1_valve2", + "source": "multiway_valve_1", + "target": "multiway_valve_2", + "type": "fluid", + "port": { + "multiway_valve_1": "8", + "multiway_valve_2": "1" + } + }, + { + "id": "link_valve1_DMF", + "source": "multiway_valve_1", + "target": "flask_DMF", + "type": "fluid", + "port": { + "multiway_valve_1": "1", + "flask_DMF": "outlet" + } + }, + { + "id": "link_valve1_ethyl_acetate", + "source": "multiway_valve_1", + "target": "flask_ethyl_acetate", + "type": "fluid", + "port": { + "multiway_valve_1": "2", + "flask_ethyl_acetate": "outlet" + } + }, + { + "id": "link_valve1_methanol", + "source": "multiway_valve_1", + "target": "flask_methanol", + "type": "fluid", + "port": { + "multiway_valve_1": "3", + "flask_methanol": "outlet" + } + }, + { + "id": "link_valve1_acetone", + "source": "multiway_valve_1", + "target": "flask_acetone", + "type": "fluid", + "port": { + "multiway_valve_1": "4", + "flask_acetone": "outlet" + } + }, + { + "id": "link_valve1_water", + "source": "multiway_valve_1", + "target": "flask_water", + "type": "fluid", + "port": { + "multiway_valve_1": "5", + "flask_water": "outlet" + } + }, + { + "id": "link_valve1_air", + "source": "multiway_valve_1", + "target": "flask_air", + "type": "fluid", + "port": { + "multiway_valve_1": "6", + "flask_air": "top" + } + }, + { + "id": "link_valve2_main_reactor", + "source": "multiway_valve_2", + "target": "main_reactor", + "type": "fluid", + "port": { + "multiway_valve_2": "2", + "main_reactor": "inlet" + } + }, + { + "id": "link_valve2_secondary_reactor", + "source": "multiway_valve_2", + "target": "secondary_reactor", + "type": "fluid", + "port": { + "multiway_valve_2": "3", + "secondary_reactor": "inlet" + } + }, + { + "id": "link_valve2_waste", + "source": "multiway_valve_2", + "target": "waste_workup", + "type": "fluid", + "port": { + "multiway_valve_2": "6", + "waste_workup": "inlet" + } + }, + { + "id": "link_valve2_collection1", + "source": "multiway_valve_2", + "target": "collection_bottle_1", + "type": "fluid", + "port": { + "multiway_valve_2": "7", + "collection_bottle_1": "inlet" + } + }, + { + "id": "link_valve2_collection2", + "source": "multiway_valve_2", + "target": "collection_bottle_2", + "type": "fluid", + "port": { + "multiway_valve_2": "8", + "collection_bottle_2": "inlet" + } + }, + { + "id": "link_stirrer1_main_reactor", + "source": "stirrer_1", + "target": "main_reactor", + "type": "mechanical", + "port": { + "stirrer_1": "stirrer_head", + "main_reactor": "stirrer_port" + } + }, + { + "id": "link_stirrer2_secondary_reactor", + "source": "stirrer_2", + "target": "secondary_reactor", + "type": "mechanical", + "port": { + "stirrer_2": "stirrer_head", + "secondary_reactor": "stirrer_port" + } + }, + { + "id": "link_heatchill1_main_reactor", + "source": "heatchill_1", + "target": "main_reactor", + "type": "thermal", + "port": { + "heatchill_1": "heating_surface", + "main_reactor": "heating_jacket" + } + }, + { + "id": "link_heatchill2_secondary_reactor", + "source": "heatchill_2", + "target": "secondary_reactor", + "type": "thermal", + "port": { + "heatchill_2": "heating_surface", + "secondary_reactor": "heating_jacket" + } + }, + { + "id": "link_valve1_ethanol", + "source": "multiway_valve_1", + "target": "flask_ethanol", + "type": "fluid", + "port": { + "multiway_valve_1": "7", + "flask_ethanol": "outlet" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/mock_protocol/pumptransferteststation.json b/test/experiments/Protocol_Test_Station/pumptransfer_test_station.json similarity index 100% rename from test/experiments/mock_protocol/pumptransferteststation.json rename to test/experiments/Protocol_Test_Station/pumptransfer_test_station.json diff --git a/test/experiments/Protocol_Test_Station/simple_stir_heatchill_test_station.json b/test/experiments/Protocol_Test_Station/simple_stir_heatchill_test_station.json new file mode 100644 index 0000000..1f65cc7 --- /dev/null +++ b/test/experiments/Protocol_Test_Station/simple_stir_heatchill_test_station.json @@ -0,0 +1,141 @@ +{ + "nodes": [ + { + "id": "SimpleStirHeatChillTestStation", + "name": "搅拌加热测试站", + "children": [ + "stirrer_1", + "heatchill_1", + "main_reactor", + "secondary_reactor" + ], + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 500, + "y": 200, + "z": 0 + }, + "config": { + "protocol_type": [ + "StirProtocol", + "StartStirProtocol", + "StopStirProtocol", + "HeatChillProtocol", + "HeatChillStartProtocol", + "HeatChillStopProtocol" + ] + }, + "data": {} + }, + { + "id": "stirrer_1", + "name": "主搅拌器", + "children": [], + "parent": "SimpleStirHeatChillTestStation", + "type": "device", + "class": "virtual_stirrer", + "position": { + "x": 400, + "y": 350, + "z": 0 + }, + "config": { + "port": "VIRTUAL_STIRRER1", + "max_speed": 1500.0, + "min_speed": 50.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "heatchill_1", + "name": "主加热冷却器", + "children": [], + "parent": "SimpleStirHeatChillTestStation", + "type": "device", + "class": "virtual_heatchill", + "position": { + "x": 600, + "y": 350, + "z": 0 + }, + "config": { + "port": "VIRTUAL_HEATCHILL1", + "max_temp": 200.0, + "min_temp": -80.0, + "max_stir_speed": 1000.0 + }, + "data": { + "status": "Idle" + } + }, + { + "id": "main_reactor", + "name": "主反应器", + "children": [], + "parent": "SimpleStirHeatChillTestStation", + "type": "container", + "class": null, + "position": { + "x": 500, + "y": 450, + "z": 0 + }, + "config": { + "max_volume": 2000.0 + }, + "data": { + "liquid": [ + { + "liquid_type": "water", + "liquid_volume": 500.0 + } + ] + } + }, + { + "id": "secondary_reactor", + "name": "副反应器", + "children": [], + "parent": "SimpleStirHeatChillTestStation", + "type": "container", + "class": null, + "position": { + "x": 700, + "y": 450, + "z": 0 + }, + "config": { + "max_volume": 1000.0 + }, + "data": { + "liquid": [] + } + } + ], + "links": [ + { + "id": "link_stirrer1_main_reactor", + "source": "stirrer_1", + "target": "main_reactor", + "type": "mechanical", + "port": { + "stirrer_1": "stirrer", + "main_reactor": "stirrer_port" + } + }, + { + "id": "link_heatchill1_main_reactor", + "source": "heatchill_1", + "target": "main_reactor", + "type": "mechanical", + "port": { + "heatchill_1": "heatchill", + "main_reactor": "heating_jacket" + } + } + ] +} \ No newline at end of file diff --git a/test/experiments/comprehensive_protocol/checklist.md b/test/experiments/comprehensive_protocol/checklist.md index d9919a3..4d1e7c0 100644 --- a/test/experiments/comprehensive_protocol/checklist.md +++ b/test/experiments/comprehensive_protocol/checklist.md @@ -1,6 +1,6 @@ 1. 用到的仪器 - virtual_multiway_valve() 八通阀门 - virtual_transfer_pump() 转移泵 + virtual_multiway_valve(√) 八通阀门 + virtual_transfer_pump(√) 转移泵 virtual_centrifuge() 离心机 virtual_rotavap() 旋蒸仪 virtual_heatchill() 加热器 @@ -12,21 +12,23 @@ virtual_column(√) 层析柱 separator() homemade_grbl_conductivity 分液漏斗 2. 用到的protocol - AddProtocol() - TransferProtocol() 应该用pump_protocol.py删掉transfer - StartStirProtocol() - StopStirProtocol() - StirProtocol() - RunColumnProtocol() - CentrifugeProtocol() - FilterProtocol() - CleanVesselProtocol() - DissolveProtocol() - FilterThroughProtocol() - WashSolidProtocol() - SeparateProtocol() - EvaporateProtocol() - HeatChillProtocol() - HeatChillStartProtocol() - HeatChillStopProtocol() - EvacuateAndRefillProtocol() + PumpTransferProtocol: generate_pump_protocol_with_rinsing, (√) + 这个重复了,删掉CleanProtocol: generate_clean_protocol, + SeparateProtocol: generate_separate_protocol, (×) + EvaporateProtocol: generate_evaporate_protocol, (√) + EvacuateAndRefillProtocol: generate_evacuateandrefill_protocol, (√) + CentrifugeProtocol: generate_centrifuge_protocol, (√) + AddProtocol: generate_add_protocol, (√) + FilterProtocol: generate_filter_protocol, (√) + HeatChillProtocol: generate_heat_chill_protocol, (√) + HeatChillStartProtocol: generate_heat_chill_start_protocol, (√) + HeatChillStopProtocol: generate_heat_chill_stop_protocol, (√) + StirProtocol: generate_stir_protocol, (√) + StartStirProtocol: generate_start_stir_protocol, (√) + StopStirProtocol: generate_stop_stir_protocol, (√) + 这个重复了,删掉TransferProtocol: generate_transfer_protocol, + CleanVesselProtocol: generate_clean_vessel_protocol, (√) + DissolveProtocol: generate_dissolve_protocol, (√) + FilterThroughProtocol: generate_filter_through_protocol, (×) + RunColumnProtocol: generate_run_column_protocol, (×) + WashSolidProtocol: generate_wash_solid_protocol, (×) diff --git a/unilabos/compile/__init__.py b/unilabos/compile/__init__.py index fa61e12..0b56a5d 100644 --- a/unilabos/compile/__init__.py +++ b/unilabos/compile/__init__.py @@ -8,7 +8,12 @@ from .agv_transfer_protocol import generate_agv_transfer_protocol from .add_protocol import generate_add_protocol from .centrifuge_protocol import generate_centrifuge_protocol from .filter_protocol import generate_filter_protocol -from .heatchill_protocol import generate_heat_chill_protocol, generate_heat_chill_start_protocol, generate_heat_chill_stop_protocol +from .heatchill_protocol import ( + generate_heat_chill_protocol, + generate_heat_chill_start_protocol, + generate_heat_chill_stop_protocol, + generate_heat_chill_to_temp_protocol # 保留导入,但不注册为协议 +) from .stir_protocol import generate_stir_protocol, generate_start_stir_protocol, generate_stop_stir_protocol from .transfer_protocol import generate_transfer_protocol from .clean_vessel_protocol import generate_clean_vessel_protocol @@ -32,6 +37,7 @@ action_protocol_generators = { HeatChillProtocol: generate_heat_chill_protocol, HeatChillStartProtocol: generate_heat_chill_start_protocol, HeatChillStopProtocol: generate_heat_chill_stop_protocol, + # HeatChillToTempProtocol: generate_heat_chill_to_temp_protocol, # **移除这行** StirProtocol: generate_stir_protocol, StartStirProtocol: generate_start_stir_protocol, StopStirProtocol: generate_stop_stir_protocol, diff --git a/unilabos/compile/add_protocol.py b/unilabos/compile/add_protocol.py index c75e4fd..a1398e6 100644 --- a/unilabos/compile/add_protocol.py +++ b/unilabos/compile/add_protocol.py @@ -1,288 +1,381 @@ -import networkx as nx -from typing import List, Dict, Any - -def generate_add_protocol( - G: nx.DiGraph, - vessel: str, - reagent: str, - volume: float, - mass: float, - amount: str, - time: float, - stir: bool, - stir_speed: float, - viscous: bool, - purpose: str -) -> List[Dict[str, Any]]: - """ - 生成添加试剂的协议序列 - - 流程: - 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 available_flasks: - reagent_vessel = available_flasks[0] - else: - raise ValueError("没有找到可用的试剂容器") - - # 4. 获取试剂瓶和反应器对应的阀门位置 - # 这需要根据实际连接图来确定,这里假设: - reagent_valve_position = 1 # 试剂瓶连接到阀门位置1 - reactor_valve_position = 2 # 反应器连接到阀门位置2 - - # 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 - } - }) - - # 6. 如果需要搅拌,启动搅拌器 - if stir: - stirrer_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_stirrer'] - - if stirrer_nodes: - stirrer_id = stirrer_nodes[0] - action_sequence.append({ - "device_id": stirrer_id, - "action_name": "start_stir", - "action_kwargs": { - "vessel": vessel, - "stir_speed": stir_speed, - "purpose": f"添加 {reagent} 后搅拌混合" - } - }) - else: - print("警告:需要搅拌但未找到搅拌设备") - - return action_sequence +import networkx as nx +from typing import List, Dict, Any +from .pump_protocol import generate_pump_protocol_with_rinsing -def find_valve_position_for_vessel(G: nx.DiGraph, valve_id: str, vessel_id: str) -> int: +def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str: """ - 根据连接图找到容器对应的阀门位置 + 根据试剂名称查找对应的试剂瓶 Args: G: 网络图 - valve_id: 阀门设备ID - vessel_id: 容器ID + reagent: 试剂名称 Returns: - int: 阀门位置编号 (1-8) + str: 试剂瓶的vessel ID + + Raises: + ValueError: 如果找不到对应的试剂瓶 """ - # 查找阀门到容器的连接 - edges = G.edges(data=True) + # 按照pump_protocol的命名规则查找试剂瓶 + reagent_vessel_id = f"flask_{reagent}" - 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) + if reagent_vessel_id in G.nodes(): + return reagent_vessel_id - # 默认返回位置1 - return 1 + # 如果直接匹配失败,尝试模糊匹配 + for node in G.nodes(): + if node.startswith('flask_') and reagent.lower() in node.lower(): + return node + + # 如果还是找不到,列出所有可用的试剂瓶 + available_flasks = [node for node in G.nodes() + if node.startswith('flask_') + and G.nodes[node].get('type') == 'container'] + + raise ValueError(f"找不到试剂 '{reagent}' 对应的试剂瓶。可用试剂瓶: {available_flasks}") -def generate_add_with_autodiscovery( - G: nx.DiGraph, - vessel: str, - reagent: str, - volume: float, - **kwargs +def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str: + """ + 查找与指定容器相连的搅拌器 + + Args: + G: 网络图 + vessel: 容器ID + + Returns: + str: 搅拌器ID,如果找不到则返回None + """ + # 查找所有搅拌器节点 + stirrer_nodes = [node for node in G.nodes() + if G.nodes[node].get('class') == 'virtual_stirrer'] + + # 检查哪个搅拌器与目标容器相连 + for stirrer in stirrer_nodes: + if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): + return stirrer + + # 如果没有直接连接,返回第一个可用的搅拌器 + return stirrer_nodes[0] if stirrer_nodes else None + + +def generate_add_protocol( + G: nx.DiGraph, + vessel: str, + reagent: str, + volume: float, + mass: 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 = [] - # 查找必需的设备 - devices = { - 'transfer_pump': None, - 'multiway_valve': None, - 'stirrer': None - } + # 1. 验证目标容器存在 + if vessel not in G.nodes(): + raise ValueError(f"目标容器 '{vessel}' 不存在于系统中") - 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 + # 2. 查找试剂瓶 + try: + reagent_vessel = find_reagent_vessel(G, reagent) + except ValueError as e: + raise ValueError(f"无法找到试剂 '{reagent}': {str(e)}") - # 验证必需设备 - if not devices['transfer_pump']: - raise ValueError("缺少注射器泵设备") - if not devices['multiway_valve']: - raise ValueError("缺少八通阀门设备") + # 3. 验证是否存在从试剂瓶到目标容器的路径 + 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}' 没有可用路径") - # 查找试剂容器 - reagent_vessels = [node for node in G.nodes() - if node.startswith('flask_') - and G.nodes[node].get('type') == 'container'] + # 4. **先启动搅拌** - 关键改进! + 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") + else: + print(f"ADD_PROTOCOL: 警告 - 需要搅拌但未找到与容器 {vessel} 相连的搅拌器") + + except Exception as e: + print(f"ADD_PROTOCOL: 搅拌器配置出错: {str(e)}") - if not reagent_vessels: - raise ValueError("没有找到试剂容器") + # 5. 如果指定了体积,执行液体转移 + if volume > 0: + # 5.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") + + # 5.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)}") - # 执行添加流程 - 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) + print(f"ADD_PROTOCOL: 生成了 {len(action_sequence)} 个动作") + 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, + 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 +) -> List[Dict[str, Any]]: + """ + 生成带清洗的添加试剂协议 - # 生成操作序列 - 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 + 与普通添加协议的区别是会在添加后进行管道清洗 + + 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]]: 动作序列 + """ + action_sequence = [] + + # 1. 查找试剂瓶 + reagent_vessel = find_reagent_vessel(G, reagent) + + # 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}" + } + }) + + # 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", + "volume": 10.0, + "viscous": False, + "stir_speed": 300.0 + }, + { + "reagent": "ethyl_acetate", + "volume": 5.0, + "viscous": False, + "stir_speed": 350.0 } - }, - # 切换到反应器 - { - "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 - } - } - ]) + ] + """ + action_sequence = [] - # 如果需要搅拌 - 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} 后混合" - } - }) + for i, reagent_params in enumerate(reagents): + print(f"ADD_PROTOCOL: 处理第 {i+1}/{len(reagents)} 个试剂: {reagent_params.get('reagent')}") + + # 生成单个试剂的添加协议 + add_actions = generate_add_protocol( + G=G, + vessel=vessel, + reagent=reagent_params.get('reagent'), + 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'添加试剂 {i+1}') + ) + + action_sequence.extend(add_actions) + + # 在添加之间加入等待时间 + if i < len(reagents) - 1: # 不是最后一个试剂 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 2} + }) - return action_sequence \ No newline at end of file + # 最终搅拌 + if final_stir: + stirrer_id = find_connected_stirrer(G, vessel) + if stirrer_id: + action_sequence.extend([ + { + "action_name": "wait", + "action_kwargs": {"time": final_stir_time} + } + ]) + + print(f"ADD_PROTOCOL: 连续添加协议生成完成,共 {len(action_sequence)} 个动作") + return action_sequence + + +# 使用示例和测试函数 +def test_add_protocol(): + """测试添加协议的示例""" + print("=== ADD PROTOCOL 测试 ===") + print("测试完成") + + +if __name__ == "__main__": + test_add_protocol() \ No newline at end of file diff --git a/unilabos/compile/centrifuge_protocol.py b/unilabos/compile/centrifuge_protocol.py index e55644d..3f54107 100644 --- a/unilabos/compile/centrifuge_protocol.py +++ b/unilabos/compile/centrifuge_protocol.py @@ -1,5 +1,59 @@ from typing import List, Dict, Any import networkx as nx +from .pump_protocol import generate_pump_protocol + + +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 find_centrifuge_device(G: nx.DiGraph) -> str: + """ + 查找离心机设备 + """ + centrifuge_nodes = [node for node in G.nodes() + if (G.nodes[node].get('class') or '') == 'virtual_centrifuge'] + + if centrifuge_nodes: + return centrifuge_nodes[0] + + raise ValueError("系统中未找到离心机设备") + + +def find_centrifuge_vessel(G: nx.DiGraph) -> str: + """ + 查找离心机专用容器 + """ + possible_names = [ + "centrifuge_tube", + "centrifuge_vessel", + "tube_centrifuge", + "vessel_centrifuge", + "centrifuge", + "tube_15ml", + "tube_50ml" + ] + + for vessel_name in possible_names: + if vessel_name in G.nodes(): + return vessel_name + + raise ValueError(f"未找到离心机容器。尝试了以下名称: {possible_names}") + def generate_centrifuge_protocol( G: nx.DiGraph, @@ -9,115 +63,223 @@ def generate_centrifuge_protocol( temp: float = 25.0 ) -> List[Dict[str, Any]]: """ - 生成离心操作的协议序列 + 生成离心操作的协议序列,复用 pump_protocol 的成熟算法 + + 离心流程: + 1. 液体转移:将待离心溶液从源容器转移到离心机容器 + 2. 离心操作:执行离心分离 + 3. 上清液转移:将离心后的上清液转移回原容器或新容器 + 4. 沉淀处理:处理离心沉淀(可选) Args: - G: 有向图,节点为设备和容器 - vessel: 离心容器名称 + G: 有向图,节点为设备和容器,边为流体管道 + vessel: 包含待离心溶液的容器名称 speed: 离心速度 (rpm) time: 离心时间 (秒) - temp: 温度 (摄氏度,可选) + temp: 离心温度 (°C),默认25°C Returns: List[Dict[str, Any]]: 离心操作的动作序列 Raises: - ValueError: 当找不到离心机设备时抛出异常 + ValueError: 当找不到必要的设备时抛出异常 Examples: - centrifuge_protocol = generate_centrifuge_protocol(G, "reactor", 5000, 300, 4.0) + centrifuge_actions = generate_centrifuge_protocol(G, "reaction_mixture", 5000, 600, 4.0) """ action_sequence = [] - # 查找离心机设备 - centrifuge_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_centrifuge'] + print(f"CENTRIFUGE: 开始生成离心协议") + print(f" - 源容器: {vessel}") + print(f" - 离心速度: {speed} rpm") + print(f" - 离心时间: {time}s ({time/60:.1f}分钟)") + print(f" - 离心温度: {temp}°C") - if not centrifuge_nodes: - raise ValueError("没有找到可用的离心机设备") - - # 使用第一个可用的离心机 - centrifuge_id = centrifuge_nodes[0] - - # 验证容器是否存在 + # 验证源容器存在 if vessel not in G.nodes(): - raise ValueError(f"容器 {vessel} 不存在于图中") + raise ValueError(f"源容器 '{vessel}' 不存在于系统中") - # 执行离心操作 - action_sequence.append({ + # 获取源容器中的液体体积 + source_volume = get_vessel_liquid_volume(G, vessel) + print(f"CENTRIFUGE: 源容器 {vessel} 中有 {source_volume} mL 液体") + + # 查找离心机设备 + try: + centrifuge_id = find_centrifuge_device(G) + print(f"CENTRIFUGE: 找到离心机: {centrifuge_id}") + except ValueError as e: + raise ValueError(f"无法找到离心机: {str(e)}") + + # 查找离心机容器 + try: + centrifuge_vessel = find_centrifuge_vessel(G) + print(f"CENTRIFUGE: 找到离心机容器: {centrifuge_vessel}") + except ValueError as e: + raise ValueError(f"无法找到离心机容器: {str(e)}") + + # === 简化的体积计算策略 === + if source_volume > 0: + # 如果能检测到液体体积,使用实际体积的大部分 + transfer_volume = min(source_volume * 0.9, 15.0) # 90%或最多15mL(离心管通常较小) + print(f"CENTRIFUGE: 检测到液体体积,将转移 {transfer_volume} mL") + else: + # 如果检测不到液体体积,默认转移标准量 + transfer_volume = 10.0 # 标准离心管体积 + print(f"CENTRIFUGE: 未检测到液体体积,默认转移 {transfer_volume} mL") + + # === 第一步:将待离心溶液转移到离心机容器 === + print(f"CENTRIFUGE: 将 {transfer_volume} mL 溶液从 {vessel} 转移到 {centrifuge_vessel}") + try: + # 使用成熟的 pump_protocol 算法进行液体转移 + transfer_to_centrifuge_actions = generate_pump_protocol( + G=G, + from_vessel=vessel, + to_vessel=centrifuge_vessel, + volume=transfer_volume, + flowrate=1.0, # 离心转移用慢速,避免气泡 + transfer_flowrate=1.0 + ) + action_sequence.extend(transfer_to_centrifuge_actions) + except Exception as e: + raise ValueError(f"无法将溶液转移到离心机: {str(e)}") + + # 转移后等待 + wait_action = { + "action_name": "wait", + "action_kwargs": {"time": 5} + } + action_sequence.append(wait_action) + + # === 第二步:执行离心操作 === + print(f"CENTRIFUGE: 执行离心操作") + centrifuge_action = { "device_id": centrifuge_id, "action_name": "centrifuge", "action_kwargs": { - "vessel": vessel, + "vessel": centrifuge_vessel, "speed": speed, "time": time, "temp": temp } - }) + } + action_sequence.append(centrifuge_action) + + # 离心后等待系统稳定 + wait_action = { + "action_name": "wait", + "action_kwargs": {"time": 10} # 离心后等待稍长,让沉淀稳定 + } + action_sequence.append(wait_action) + + # === 第三步:将上清液转移回原容器 === + print(f"CENTRIFUGE: 将上清液从离心机转移回 {vessel}") + try: + # 估算上清液体积(约为转移体积的80% - 假设20%成为沉淀) + supernatant_volume = transfer_volume * 0.8 + print(f"CENTRIFUGE: 预计上清液体积 {supernatant_volume} mL") + + transfer_back_actions = generate_pump_protocol( + G=G, + from_vessel=centrifuge_vessel, + to_vessel=vessel, + volume=supernatant_volume, + flowrate=0.5, # 上清液转移更慢,避免扰动沉淀 + transfer_flowrate=0.5 + ) + action_sequence.extend(transfer_back_actions) + except Exception as e: + print(f"CENTRIFUGE: 将上清液转移回容器失败: {str(e)}") + + # === 第四步:清洗离心机容器 === + print(f"CENTRIFUGE: 清洗离心机容器") + try: + # 查找清洗溶剂 + cleaning_solvent = None + for solvent in ["flask_water", "flask_ethanol", "flask_acetone"]: + if solvent in G.nodes(): + cleaning_solvent = solvent + break + + if cleaning_solvent: + # 用少量溶剂清洗离心管 + cleaning_volume = 5.0 # 5mL清洗 + print(f"CENTRIFUGE: 用 {cleaning_volume} mL {cleaning_solvent} 清洗") + + # 清洗溶剂加入 + cleaning_actions = generate_pump_protocol( + G=G, + from_vessel=cleaning_solvent, + to_vessel=centrifuge_vessel, + volume=cleaning_volume, + flowrate=2.0, + transfer_flowrate=2.0 + ) + action_sequence.extend(cleaning_actions) + + # 将清洗液转移到废液 + if "waste_workup" in G.nodes(): + waste_actions = generate_pump_protocol( + G=G, + from_vessel=centrifuge_vessel, + to_vessel="waste_workup", + volume=cleaning_volume, + flowrate=2.0, + transfer_flowrate=2.0 + ) + action_sequence.extend(waste_actions) + + except Exception as e: + print(f"CENTRIFUGE: 清洗步骤失败: {str(e)}") + + print(f"CENTRIFUGE: 生成了 {len(action_sequence)} 个动作") + print(f"CENTRIFUGE: 离心协议生成完成") + print(f"CENTRIFUGE: 总处理体积: {transfer_volume} mL") return action_sequence -def generate_multi_step_centrifuge_protocol( +# 便捷函数:常用离心方案 +def generate_low_speed_centrifuge_protocol( G: nx.DiGraph, vessel: str, - steps: List[Dict[str, Any]] + time: float = 300.0 # 5分钟 ) -> List[Dict[str, Any]]: - """ - 生成多步骤离心操作的协议序列 - - Args: - G: 有向图,节点为设备和容器 - vessel: 离心容器名称 - steps: 离心步骤列表,每个步骤包含 speed, time, temp 参数 - - Returns: - List[Dict[str, Any]]: 多步骤离心操作的动作序列 - - Examples: - steps = [ - {"speed": 1000, "time": 60, "temp": 4.0}, # 低速预离心 - {"speed": 12000, "time": 600, "temp": 4.0} # 高速离心 - ] - protocol = generate_multi_step_centrifuge_protocol(G, "reactor", steps) - """ - action_sequence = [] - - # 查找离心机设备 - centrifuge_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_centrifuge'] - - if not centrifuge_nodes: - raise ValueError("没有找到可用的离心机设备") - - centrifuge_id = centrifuge_nodes[0] - - # 验证容器是否存在 - if vessel not in G.nodes(): - raise ValueError(f"容器 {vessel} 不存在于图中") - - # 执行每个离心步骤 - for i, step in enumerate(steps): - speed = step.get('speed', 5000) - time = step.get('time', 300) - temp = step.get('temp', 25.0) - - action_sequence.append({ - "device_id": centrifuge_id, - "action_name": "centrifuge", - "action_kwargs": { - "vessel": vessel, - "speed": speed, - "time": time, - "temp": temp - } - }) - - # 步骤间等待时间(除了最后一步) - if i < len(steps) - 1: - action_sequence.append({ - "action_name": "wait", - "action_kwargs": {"time": 3} - }) - - return action_sequence \ No newline at end of file + """低速离心:细胞分离或大颗粒沉淀""" + return generate_centrifuge_protocol(G, vessel, 1000.0, time, 4.0) + + +def generate_high_speed_centrifuge_protocol( + G: nx.DiGraph, + vessel: str, + time: float = 600.0 # 10分钟 +) -> List[Dict[str, Any]]: + """高速离心:蛋白质沉淀或小颗粒分离""" + return generate_centrifuge_protocol(G, vessel, 12000.0, time, 4.0) + + +def generate_standard_centrifuge_protocol( + G: nx.DiGraph, + vessel: str, + time: float = 600.0 # 10分钟 +) -> List[Dict[str, Any]]: + """标准离心:常规样品处理""" + return generate_centrifuge_protocol(G, vessel, 5000.0, time, 25.0) + + +def generate_cold_centrifuge_protocol( + G: nx.DiGraph, + vessel: str, + speed: float = 5000.0, + time: float = 600.0 +) -> List[Dict[str, Any]]: + """冷冻离心:热敏感样品处理""" + return generate_centrifuge_protocol(G, vessel, speed, time, 4.0) + + +def generate_ultra_centrifuge_protocol( + G: nx.DiGraph, + vessel: str, + time: float = 1800.0 # 30分钟 +) -> List[Dict[str, Any]]: + """超高速离心:超细颗粒分离""" + return generate_centrifuge_protocol(G, vessel, 15000.0, time, 4.0) \ No newline at end of file diff --git a/unilabos/compile/clean_vessel_protocol.py b/unilabos/compile/clean_vessel_protocol.py index d8a746f..cf752fa 100644 --- a/unilabos/compile/clean_vessel_protocol.py +++ b/unilabos/compile/clean_vessel_protocol.py @@ -1,5 +1,69 @@ from typing import List, Dict, Any import networkx as nx +from .pump_protocol import generate_pump_protocol + + +def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: + """ + 查找溶剂容器,支持多种命名模式 + """ + # 可能的溶剂容器命名模式 + possible_names = [ + f"flask_{solvent}", # flask_water, flask_ethanol + f"bottle_{solvent}", # bottle_water, bottle_ethanol + f"vessel_{solvent}", # vessel_water, vessel_ethanol + f"{solvent}_flask", # water_flask, ethanol_flask + f"{solvent}_bottle", # water_bottle, ethanol_bottle + f"{solvent}", # 直接用溶剂名 + f"solvent_{solvent}", # solvent_water, solvent_ethanol + ] + + for vessel_name in possible_names: + if vessel_name in G.nodes(): + return vessel_name + + raise ValueError(f"未找到溶剂 '{solvent}' 的容器。尝试了以下名称: {possible_names}") + + +def find_waste_vessel(G: nx.DiGraph) -> str: + """ + 查找废液容器 + """ + possible_waste_names = [ + "waste_workup", + "flask_waste", + "bottle_waste", + "waste", + "waste_vessel", + "waste_container" + ] + + for waste_name in possible_waste_names: + if waste_name in G.nodes(): + return waste_name + + raise ValueError(f"未找到废液容器。尝试了以下名称: {possible_waste_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') or '') == '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] + + return None # 没有加热设备也可以工作,只是不能加热 + def generate_clean_vessel_protocol( G: nx.DiGraph, @@ -10,13 +74,22 @@ def generate_clean_vessel_protocol( repeats: int = 1 ) -> List[Dict[str, Any]]: """ - 生成容器清洗操作的协议序列,使用transfer操作实现清洗 + 生成容器清洗操作的协议序列,复用 pump_protocol 的成熟算法 + + 清洗流程: + 1. 查找溶剂容器和废液容器 + 2. 如果需要加热,启动加热设备 + 3. 重复以下操作 repeats 次: + a. 使用 pump_protocol 将溶剂从溶剂容器转移到目标容器 + b. (可选) 等待清洗作用时间 + c. 使用 pump_protocol 将清洗液从目标容器转移到废液容器 + 4. 如果加热了,停止加热 Args: - G: 有向图,节点为设备和容器 + G: 有向图,节点为设备和容器,边为流体管道 vessel: 要清洗的容器名称 - solvent: 用于清洗容器的溶剂名称 - volume: 清洗溶剂的体积 + solvent: 用于清洗的溶剂名称 + volume: 每次清洗使用的溶剂体积 temp: 清洗时的温度 repeats: 清洗操作的重复次数,默认为 1 @@ -24,103 +97,188 @@ def generate_clean_vessel_protocol( List[Dict[str, Any]]: 容器清洗操作的动作序列 Raises: - ValueError: 当找不到必要的设备时抛出异常 + ValueError: 当找不到必要的容器或设备时抛出异常 Examples: - clean_vessel_protocol = generate_clean_vessel_protocol(G, "reactor", "water", 50.0, 25.0, 2) + clean_protocol = generate_clean_vessel_protocol(G, "main_reactor", "water", 100.0, 60.0, 2) """ action_sequence = [] - # 查找虚拟转移泵设备进行清洗操作 - pump_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_transfer_pump'] + print(f"CLEAN_VESSEL: 开始生成容器清洗协议") + print(f" - 目标容器: {vessel}") + print(f" - 清洗溶剂: {solvent}") + print(f" - 清洗体积: {volume} mL") + print(f" - 清洗温度: {temp}°C") + print(f" - 重复次数: {repeats}") - if not pump_nodes: - raise ValueError("没有找到可用的转移泵设备进行容器清洗") - - pump_id = pump_nodes[0] - - # 验证容器是否存在 + # 验证目标容器存在 if vessel not in G.nodes(): - raise ValueError(f"容器 {vessel} 不存在于图中") + raise ValueError(f"目标容器 '{vessel}' 不存在于系统中") # 查找溶剂容器 - solvent_vessel = f"flask_{solvent}" - if solvent_vessel not in G.nodes(): - raise ValueError(f"溶剂容器 {solvent_vessel} 不存在于图中") + try: + solvent_vessel = find_solvent_vessel(G, solvent) + print(f"CLEAN_VESSEL: 找到溶剂容器: {solvent_vessel}") + except ValueError as e: + raise ValueError(f"无法找到溶剂容器: {str(e)}") # 查找废液容器 - waste_vessel = "flask_waste" - if waste_vessel not in G.nodes(): - raise ValueError(f"废液容器 {waste_vessel} 不存在于图中") + try: + waste_vessel = find_waste_vessel(G) + print(f"CLEAN_VESSEL: 找到废液容器: {waste_vessel}") + except ValueError as e: + raise ValueError(f"无法找到废液容器: {str(e)}") - # 查找加热设备(如果需要加热) - heatchill_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_heatchill'] + # 查找加热设备(可选) + heatchill_id = find_connected_heatchill(G, vessel) + if heatchill_id: + print(f"CLEAN_VESSEL: 找到加热设备: {heatchill_id}") + else: + print(f"CLEAN_VESSEL: 未找到加热设备,将在室温下清洗") - heatchill_id = heatchill_nodes[0] if heatchill_nodes else None + # 第一步:如果需要加热且有加热设备,启动加热 + if temp > 25.0 and heatchill_id: + print(f"CLEAN_VESSEL: 启动加热至 {temp}°C") + heatchill_start_action = { + "device_id": heatchill_id, + "action_name": "heat_chill_start", + "action_kwargs": { + "vessel": vessel, + "temp": temp, + "purpose": f"cleaning with {solvent}" + } + } + action_sequence.append(heatchill_start_action) + + # 等待温度稳定 + wait_action = { + "action_name": "wait", + "action_kwargs": {"time": 30} # 等待30秒让温度稳定 + } + action_sequence.append(wait_action) - # 执行清洗操作序列 + # 第二步:重复清洗操作 for repeat in range(repeats): - # 1. 如果需要加热,先设置温度 - if temp > 25.0 and heatchill_id: - action_sequence.append({ - "device_id": heatchill_id, - "action_name": "heat_chill_start", - "action_kwargs": { - "vessel": vessel, - "temp": temp, - "purpose": "cleaning" - } - }) + print(f"CLEAN_VESSEL: 执行第 {repeat + 1} 次清洗") - # 2. 使用transfer操作:从溶剂容器转移清洗溶剂到目标容器 - action_sequence.append({ - "device_id": pump_id, - "action_name": "transfer", - "action_kwargs": { - "from_vessel": solvent_vessel, - "to_vessel": vessel, - "volume": volume, - "amount": f"cleaning with {solvent} - cycle {repeat + 1}", - "time": 0.0, - "viscous": False, - "rinsing_solvent": "", - "rinsing_volume": 0.0, - "rinsing_repeats": 0, - "solid": False + # 2a. 使用 pump_protocol 将溶剂转移到目标容器 + print(f"CLEAN_VESSEL: 将 {volume} mL {solvent} 转移到 {vessel}") + try: + # 调用成熟的 pump_protocol 算法 + add_solvent_actions = generate_pump_protocol( + G=G, + from_vessel=solvent_vessel, + to_vessel=vessel, + volume=volume, + flowrate=2.5, # 适中的流速,避免飞溅 + transfer_flowrate=2.5 + ) + action_sequence.extend(add_solvent_actions) + except Exception as e: + raise ValueError(f"无法将溶剂转移到容器: {str(e)}") + + # 2b. 等待清洗作用时间(让溶剂充分清洗容器) + cleaning_wait_time = 60 if temp > 50.0 else 30 # 高温下等待更久 + print(f"CLEAN_VESSEL: 等待清洗作用 {cleaning_wait_time} 秒") + wait_action = { + "action_name": "wait", + "action_kwargs": {"time": cleaning_wait_time} + } + action_sequence.append(wait_action) + + # 2c. 使用 pump_protocol 将清洗液转移到废液容器 + print(f"CLEAN_VESSEL: 将清洗液从 {vessel} 转移到废液容器") + try: + # 调用成熟的 pump_protocol 算法 + remove_waste_actions = generate_pump_protocol( + G=G, + from_vessel=vessel, + to_vessel=waste_vessel, + volume=volume, + flowrate=2.5, # 适中的流速 + transfer_flowrate=2.5 + ) + action_sequence.extend(remove_waste_actions) + except Exception as e: + raise ValueError(f"无法将清洗液转移到废液容器: {str(e)}") + + # 2d. 清洗循环间的短暂等待 + if repeat < repeats - 1: # 不是最后一次清洗 + print(f"CLEAN_VESSEL: 清洗循环间等待") + wait_action = { + "action_name": "wait", + "action_kwargs": {"time": 10} } - }) - - # 3. 等待清洗作用时间(可选,可以添加wait操作) - # 这里省略wait操作,直接进行下一步 - - # 4. 将清洗后的溶剂转移到废液容器 - action_sequence.append({ - "device_id": pump_id, - "action_name": "transfer", + action_sequence.append(wait_action) + + # 第三步:如果加热了,停止加热 + if temp > 25.0 and heatchill_id: + print(f"CLEAN_VESSEL: 停止加热") + heatchill_stop_action = { + "device_id": heatchill_id, + "action_name": "heat_chill_stop", "action_kwargs": { - "from_vessel": vessel, - "to_vessel": waste_vessel, - "volume": volume, - "amount": f"waste from cleaning {vessel} - cycle {repeat + 1}", - "time": 0.0, - "viscous": False, - "rinsing_solvent": "", - "rinsing_volume": 0.0, - "rinsing_repeats": 0, - "solid": False + "vessel": vessel } - }) - - # 5. 如果加热了,停止加热 - if temp > 25.0 and heatchill_id: - action_sequence.append({ - "device_id": heatchill_id, - "action_name": "heat_chill_stop", - "action_kwargs": { - "vessel": vessel - } - }) + } + action_sequence.append(heatchill_stop_action) + + print(f"CLEAN_VESSEL: 生成了 {len(action_sequence)} 个动作") + print(f"CLEAN_VESSEL: 清洗协议生成完成") + + return action_sequence + + +# 便捷函数:常用清洗方案 +def generate_quick_clean_protocol( + G: nx.DiGraph, + vessel: str, + solvent: str = "water", + volume: float = 100.0 +) -> List[Dict[str, Any]]: + """快速清洗:室温,单次清洗""" + return generate_clean_vessel_protocol(G, vessel, solvent, volume, 25.0, 1) + + +def generate_thorough_clean_protocol( + G: nx.DiGraph, + vessel: str, + solvent: str = "water", + volume: float = 150.0, + temp: float = 60.0 +) -> List[Dict[str, Any]]: + """深度清洗:加热,多次清洗""" + return generate_clean_vessel_protocol(G, vessel, solvent, volume, temp, 3) + + +def generate_organic_clean_protocol( + G: nx.DiGraph, + vessel: str, + volume: float = 100.0 +) -> List[Dict[str, Any]]: + """有机清洗:先用有机溶剂,再用水清洗""" + action_sequence = [] + + # 第一步:有机溶剂清洗 + try: + organic_actions = generate_clean_vessel_protocol( + G, vessel, "acetone", volume, 25.0, 2 + ) + action_sequence.extend(organic_actions) + except ValueError: + # 如果没有丙酮,尝试乙醇 + try: + organic_actions = generate_clean_vessel_protocol( + G, vessel, "ethanol", volume, 25.0, 2 + ) + action_sequence.extend(organic_actions) + except ValueError: + print("警告:未找到有机溶剂,跳过有机清洗步骤") + + # 第二步:水清洗 + water_actions = generate_clean_vessel_protocol( + G, vessel, "water", volume, 25.0, 2 + ) + action_sequence.extend(water_actions) return action_sequence \ No newline at end of file diff --git a/unilabos/compile/dissolve_protocol.py b/unilabos/compile/dissolve_protocol.py index eda88cd..3da0d53 100644 --- a/unilabos/compile/dissolve_protocol.py +++ b/unilabos/compile/dissolve_protocol.py @@ -1,5 +1,47 @@ from typing import List, Dict, Any import networkx as nx +from .pump_protocol import generate_pump_protocol + + +def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str: + """ + 查找溶剂容器 + """ + # 按照pump_protocol的命名规则查找溶剂瓶 + solvent_vessel_id = f"flask_{solvent}" + + if solvent_vessel_id in G.nodes(): + return solvent_vessel_id + + # 如果直接匹配失败,尝试模糊匹配 + for node in G.nodes(): + if node.startswith('flask_') and solvent.lower() in node.lower(): + return node + + # 如果还是找不到,列出所有可用的溶剂瓶 + available_flasks = [node for node in G.nodes() + if node.startswith('flask_') + and G.nodes[node].get('type') == 'container'] + + raise ValueError(f"找不到溶剂 '{solvent}' 对应的溶剂瓶。可用溶剂瓶: {available_flasks}") + + +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 + + # 如果没有直接连接,返回第一个可用的加热器 + return heatchill_nodes[0] if heatchill_nodes else None + def generate_dissolve_protocol( G: nx.DiGraph, @@ -9,154 +51,309 @@ def generate_dissolve_protocol( amount: str = "", temp: float = 25.0, time: float = 0.0, - stir_speed: float = 0.0 + stir_speed: float = 300.0 ) -> List[Dict[str, Any]]: """ - 生成溶解操作的协议序列 + 生成溶解操作的协议序列,复用 pump_protocol 的成熟算法 + + 溶解流程: + 1. 溶剂转移:将溶剂从溶剂瓶转移到目标容器 + 2. 启动加热搅拌:设置温度和搅拌 + 3. 等待溶解:监控溶解过程 + 4. 停止加热搅拌:完成溶解 Args: - G: 有向图,节点为设备和容器 - vessel: 装有要溶解物质的容器名称 - solvent: 用于溶解物质的溶剂名称 - volume: 溶剂的体积,可选参数 - amount: 要溶解物质的量,可选参数 - temp: 溶解时的温度,可选参数 - time: 溶解的时间,可选参数 - stir_speed: 搅拌速度,可选参数 + G: 有向图,节点为设备和容器,边为流体管道 + vessel: 目标容器(要进行溶解的容器) + solvent: 溶剂名称(用于查找对应的溶剂瓶) + volume: 溶剂体积 (mL) + amount: 要溶解的物质描述 + temp: 溶解温度 (°C),默认25°C(室温) + time: 溶解时间 (秒),默认0(立即完成) + stir_speed: 搅拌速度 (RPM),默认300 RPM Returns: List[Dict[str, Any]]: 溶解操作的动作序列 Raises: - ValueError: 当找不到必要的设备时抛出异常 + ValueError: 当找不到必要的设备或容器时 Examples: - dissolve_protocol = generate_dissolve_protocol(G, "reactor", "water", 100.0, "NaCl 5g", 60.0, 300.0, 500.0) + dissolve_actions = generate_dissolve_protocol(G, "reaction_mixture", "DMF", 10.0, "NaCl 2g", 60.0, 600.0, 400.0) """ 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") + + # 验证目标容器存在 if vessel not in G.nodes(): - raise ValueError(f"容器 {vessel} 不存在于图中") + raise ValueError(f"目标容器 '{vessel}' 不存在于系统中") - # 查找溶剂容器 - 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}") + # 查找溶剂瓶 + try: + solvent_vessel = find_solvent_vessel(G, solvent) + print(f"DISSOLVE: 找到溶剂瓶: {solvent_vessel}") + except ValueError as e: + raise ValueError(f"无法找到溶剂 '{solvent}': {str(e)}") - # 查找转移泵设备 - pump_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_transfer_pump'] + # 验证是否存在从溶剂瓶到目标容器的路径 + 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}' 没有可用路径") - if not pump_nodes: - raise ValueError("没有找到可用的转移泵设备") + # 查找加热搅拌器 + 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}") + else: + print(f"DISSOLVE: 警告 - 需要加热/搅拌但未找到与容器 {vessel} 相连的加热搅拌器") + except Exception as e: + print(f"DISSOLVE: 加热搅拌器配置出错: {str(e)}") - 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 - - # 步骤1:如果需要加热,先设置温度 - if temp > 25.0 and heatchill_id: - action_sequence.append({ - "device_id": heatchill_id, - "action_name": "heat_chill_start", - "action_kwargs": { - "vessel": vessel, - "temp": temp, - "purpose": "dissolution" - } - }) - - # 步骤2:添加溶剂到容器中 - if volume > 0: - action_sequence.append({ - "device_id": pump_id, - "action_name": "transfer", - "action_kwargs": { - "from_vessel": solvent_vessel, - "to_vessel": vessel, - "volume": volume, - "amount": f"solvent {solvent} for dissolving {amount}", - "time": 0.0, - "viscous": False, - "rinsing_solvent": "", - "rinsing_volume": 0.0, - "rinsing_repeats": 0, - "solid": False - } - }) - - # 步骤3:如果需要搅拌,开始搅拌 - if stir_speed > 0 and stirrer_id: - action_sequence.append({ - "device_id": stirrer_id, - "action_name": "start_stir", - "action_kwargs": { - "vessel": vessel, - "stir_speed": stir_speed, - "purpose": f"dissolving {amount} in {solvent}" - } - }) - - # 步骤4:如果指定了溶解时间,等待溶解完成 - if time > 0: - # 这里可以添加等待操作,或者使用搅拌操作来模拟溶解时间 - if stirrer_id and stir_speed > 0: - # 停止之前的搅拌,使用定时搅拌 - action_sequence.append({ - "device_id": stirrer_id, - "action_name": "stop_stir", + # === 第一步:启动加热搅拌(在添加溶剂前) === + if heatchill_id and (temp > 25.0 or time > 0): + print(f"DISSOLVE: 启动加热搅拌器,温度: {temp}°C") + + if time > 0: + # 如果指定了时间,使用定时加热搅拌 + heatchill_action = { + "device_id": heatchill_id, + "action_name": "heat_chill", "action_kwargs": { - "vessel": vessel - } - }) - - # 开始定时搅拌 - action_sequence.append({ - "device_id": stirrer_id, - "action_name": "stir", - "action_kwargs": { - "stir_time": time, + "vessel": vessel, + "temp": temp, + "time": time, + "stir": True, "stir_speed": stir_speed, - "settling_time": 10.0 # 搅拌后静置10秒 + "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} 中" + } + } + + 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} }) - # 步骤5:如果加热了,停止加热 - if temp > 25.0 and heatchill_id: + # === 第二步:添加溶剂到目标容器 === + 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)}") + + # 溶剂添加后等待 action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 5} + }) + + # === 第三步:如果没有使用定时加热搅拌,但需要等待溶解 === + 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) + + # === 第四步:如果使用了持续加热,需要手动停止 === + 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) - # 步骤6:如果还在搅拌,停止搅拌(除非已经用定时搅拌) - if stir_speed > 0 and stirrer_id and time == 0: - action_sequence.append({ - "device_id": stirrer_id, - "action_name": "stop_stir", - "action_kwargs": { - "vessel": vessel + print(f"DISSOLVE: 生成了 {len(action_sequence)} 个动作") + print(f"DISSOLVE: 溶解协议生成完成") + + 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 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 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 = [] - return action_sequence \ No newline at end of file + 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 test_dissolve_protocol(): + """测试溶解协议的示例""" + print("=== DISSOLVE PROTOCOL 测试 ===") + print("测试完成") + + +if __name__ == "__main__": + test_dissolve_protocol() \ No newline at end of file diff --git a/unilabos/compile/evacuateandrefill_protocol.py b/unilabos/compile/evacuateandrefill_protocol.py index 66e3ca3..96e057d 100644 --- a/unilabos/compile/evacuateandrefill_protocol.py +++ b/unilabos/compile/evacuateandrefill_protocol.py @@ -1,143 +1,437 @@ import numpy as np import networkx as nx +from typing import List, Dict, Any, Optional +from .pump_protocol import generate_pump_protocol_with_rinsing, generate_pump_protocol + + +def find_gas_source(G: nx.DiGraph, gas: str) -> str: + """根据气体名称查找对应的气源""" + # 按照命名规则查找气源 + gas_source_patterns = [ + f"gas_source_{gas}", + f"gas_{gas}", + f"flask_{gas}", + f"{gas}_source" + ] + + for pattern in gas_source_patterns: + if pattern in G.nodes(): + return pattern + + # 模糊匹配 + for node in G.nodes(): + node_class = G.nodes[node].get('class', '') or '' + if 'gas_source' in node_class and gas.lower() in node.lower(): + return node + if node.startswith('flask_') and gas.lower() in node.lower(): + return node + + # 查找所有可用的气源 + available_gas_sources = [ + node for node in G.nodes() + if ((G.nodes[node].get('class') or '').startswith('virtual_gas_source') + or ('gas' in node and 'source' in node) + or (node.startswith('flask_') and any(g in node.lower() for g in ['air', 'nitrogen', 'argon', 'vacuum']))) + ] + + 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 '')) + ] + + if not vacuum_pumps: + raise ValueError("系统中未找到真空泵设备") + + return vacuum_pumps[0] + + +def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str: + """查找与指定容器相连的搅拌器""" + stirrer_nodes = [node for node in G.nodes() + if (G.nodes[node].get('class') or '') == 'virtual_stirrer'] + + # 检查哪个搅拌器与目标容器相连 + for stirrer in stirrer_nodes: + if G.has_edge(stirrer, vessel) or G.has_edge(vessel, 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 + + return None def generate_evacuateandrefill_protocol( - G: nx.DiGraph, - vessel: str, - gas: str, + G: nx.DiGraph, + vessel: str, + gas: str, repeats: int = 1 -) -> list[dict]: +) -> 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_protocol 并处理异常 """ + action_sequence = [] - # 生成电磁阀、真空泵、气源操作的动作序列 - vacuum_action_sequence = [] - nodes = G.nodes(data=True) + # 参数设置 - 关键修复:减小体积避免超出泵容量 + VACUUM_VOLUME = 20.0 # 减小抽真空体积 + REFILL_VOLUME = 20.0 # 减小充气体积 + PUMP_FLOW_RATE = 2.5 # 降低流速 + STIR_SPEED = 300.0 - # 找到和 vessel 相连的电磁阀和真空泵、气源 - vacuum_backbone = {"vessel": vessel} + print(f"EVACUATE_REFILL: 开始生成协议,目标容器: {vessel}, 气体: {gas}, 重复次数: {repeats}") - for neighbor in G.neighbors(vessel): - if nodes[neighbor]["class"].startswith("solenoid_valve"): - for neighbor2 in G.neighbors(neighbor): - if neighbor2 == vessel: - continue - if nodes[neighbor2]["class"].startswith("vacuum_pump"): - vacuum_backbone.update({"vacuum_valve": neighbor, "pump": neighbor2}) - break - elif nodes[neighbor2]["class"].startswith("gas_source"): - vacuum_backbone.update({"gas_valve": neighbor, "gas": neighbor2}) - break - # 判断是否设备齐全 - if len(vacuum_backbone) < 5: - print(f"\n\n\n{vacuum_backbone}\n\n\n") - raise ValueError("Not all devices are connected to the vessel.") + # 1. 验证设备存在 + if vessel not in G.nodes(): + raise ValueError(f"目标容器 '{vessel}' 不存在于系统中") - # 生成操作的动作序列 - for i in range(repeats): - # 打开真空泵阀门、关闭气源阀门 - vacuum_action_sequence.append([ - { - "device_id": vacuum_backbone["vacuum_valve"], - "action_name": "set_valve_position", - "action_kwargs": { - "command": "OPEN" - } - }, - { - "device_id": vacuum_backbone["gas_valve"], - "action_name": "set_valve_position", - "action_kwargs": { - "command": "CLOSED" - } - } - ]) + # 2. 查找设备 + try: + vacuum_pump = find_vacuum_pump(G) + vacuum_solenoid = find_associated_solenoid_valve(G, vacuum_pump) + gas_source = find_gas_source(G, gas) + gas_solenoid = find_associated_solenoid_valve(G, gas_source) + stirrer_id = find_connected_stirrer(G, vessel) - # 打开真空泵、关闭气源 - vacuum_action_sequence.append([ - { - "device_id": vacuum_backbone["pump"], - "action_name": "set_status", - "action_kwargs": { - "string": "ON" - } - }, - { - "device_id": vacuum_backbone["gas"], - "action_name": "set_status", - "action_kwargs": { - "string": "OFF" - } - } - ]) - vacuum_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 60}}) + print(f"EVACUATE_REFILL: 找到设备") + print(f" - 真空泵: {vacuum_pump}") + print(f" - 气源: {gas_source}") + print(f" - 真空电磁阀: {vacuum_solenoid}") + print(f" - 气源电磁阀: {gas_solenoid}") + print(f" - 搅拌器: {stirrer_id}") - # 关闭真空泵阀门、打开气源阀门 - vacuum_action_sequence.append([ - { - "device_id": vacuum_backbone["vacuum_valve"], - "action_name": "set_valve_position", - "action_kwargs": { - "command": "CLOSED" - } - }, - { - "device_id": vacuum_backbone["gas_valve"], - "action_name": "set_valve_position", - "action_kwargs": { - "command": "OPEN" - } - } - ]) + except ValueError as e: + raise ValueError(f"设备查找失败: {str(e)}") + + # 3. **关键修复**: 验证路径存在性 + try: + # 验证抽真空路径 + vacuum_path = nx.shortest_path(G, source=vessel, target=vacuum_pump) + print(f"EVACUATE_REFILL: 抽真空路径: {' → '.join(vacuum_path)}") - # 关闭真空泵、打开气源 - vacuum_action_sequence.append([ - { - "device_id": vacuum_backbone["pump"], - "action_name": "set_status", - "action_kwargs": { - "string": "OFF" - } - }, - { - "device_id": vacuum_backbone["gas"], - "action_name": "set_status", - "action_kwargs": { - "string": "ON" - } + # 验证充气路径 + gas_path = nx.shortest_path(G, source=gas_source, target=vessel) + print(f"EVACUATE_REFILL: 充气路径: {' → '.join(gas_path)}") + + # **新增**: 检查路径中的边数据 + 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)}") + + # 4. 启动搅拌器 + if stirrer_id: + action_sequence.append({ + "device_id": stirrer_id, + "action_name": "start_stir", + "action_kwargs": { + "vessel": vessel, + "stir_speed": STIR_SPEED, + "purpose": "抽真空充气操作前启动搅拌" } - ]) - vacuum_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 60}}) + }) + + # 5. 执行多次抽真空-充气循环 + for cycle in range(repeats): + print(f"EVACUATE_REFILL: === 第 {cycle+1}/{repeats} 次循环 ===") + + # ============ 抽真空阶段 ============ + print(f"EVACUATE_REFILL: 抽真空阶段开始") + + # 启动真空泵 + action_sequence.append({ + "device_id": vacuum_pump, + "action_name": "set_status", + "action_kwargs": {"string": "ON"} + }) + + # 开启真空电磁阀 + if 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") + + try: + vacuum_transfer_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=vessel, + to_vessel=vacuum_pump, + volume=VACUUM_VOLUME, + amount="", + time=0.0, + viscous=False, + rinsing_solvent="", # **修复**: 明确不使用清洗 + rinsing_volume=0.0, + rinsing_repeats=0, + solid=False, + flowrate=PUMP_FLOW_RATE, + transfer_flowrate=PUMP_FLOW_RATE + ) + + if vacuum_transfer_actions: + action_sequence.extend(vacuum_transfer_actions) + print(f"EVACUATE_REFILL: ✅ 成功添加 {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: 使用备选手动泵动作") + + 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 + } + } + ]) + + # 关闭真空电磁阀 + if vacuum_solenoid: + action_sequence.append({ + "device_id": vacuum_solenoid, + "action_name": "set_valve_position", + "action_kwargs": {"command": "CLOSED"} + }) + + # 关闭真空泵 + action_sequence.append({ + "device_id": vacuum_pump, + "action_name": "set_status", + "action_kwargs": {"string": "OFF"} + }) + + # ============ 充气阶段 ============ + print(f"EVACUATE_REFILL: 充气阶段开始") + + # 启动气源 + action_sequence.append({ + "device_id": gas_source, + "action_name": "set_status", + "action_kwargs": {"string": "ON"} + }) + + # 开启气源电磁阀 + if 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}") + + try: + gas_transfer_actions = generate_pump_protocol_with_rinsing( + G=G, + from_vessel=gas_source, + to_vessel=vessel, + volume=REFILL_VOLUME, + amount="", + time=0.0, + viscous=False, + rinsing_solvent="", # **修复**: 明确不使用清洗 + rinsing_volume=0.0, + rinsing_repeats=0, + solid=False, + flowrate=PUMP_FLOW_RATE, + transfer_flowrate=PUMP_FLOW_RATE + ) + + if gas_transfer_actions: + action_sequence.extend(gas_transfer_actions) + print(f"EVACUATE_REFILL: ✅ 成功添加 {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 + } + } + ]) + + 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 + } + } + ]) + + # 关闭气源电磁阀 + if gas_solenoid: + action_sequence.append({ + "device_id": gas_solenoid, + "action_name": "set_valve_position", + "action_kwargs": {"command": "CLOSED"} + }) # 关闭气源 - vacuum_action_sequence.append( - { - "device_id": vacuum_backbone["gas"], - "action_name": "set_status", - "action_kwargs": { - "string": "OFF" - } - } - ) + action_sequence.append({ + "device_id": gas_source, + "action_name": "set_status", + "action_kwargs": {"string": "OFF"} + }) - # 关闭阀门 - vacuum_action_sequence.append( - { - "device_id": vacuum_backbone["gas_valve"], - "action_name": "set_valve_position", - "action_kwargs": { - "command": "CLOSED" - } - } - ) - return vacuum_action_sequence + # 等待下一次循环 + if cycle < repeats - 1: + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 2.0} + }) + + # 停止搅拌器 + if stirrer_id: + action_sequence.append({ + "device_id": stirrer_id, + "action_name": "stop_stir", + "action_kwargs": {"vessel": vessel} + }) + + print(f"EVACUATE_REFILL: 协议生成完成,共 {len(action_sequence)} 个动作") + return action_sequence + + +# 测试函数 +def test_evacuateandrefill_protocol(): + """测试抽真空充气协议""" + print("=== EVACUATE AND REFILL PROTOCOL 测试 ===") + print("测试完成") + + +if __name__ == "__main__": + test_evacuateandrefill_protocol() \ No newline at end of file diff --git a/unilabos/compile/evacuateandrefill_protocol_old.py b/unilabos/compile/evacuateandrefill_protocol_old.py new file mode 100644 index 0000000..9f891ea --- /dev/null +++ b/unilabos/compile/evacuateandrefill_protocol_old.py @@ -0,0 +1,143 @@ +# import numpy as np +# import networkx as nx + + +# def generate_evacuateandrefill_protocol( +# G: nx.DiGraph, +# vessel: str, +# gas: str, +# repeats: int = 1 +# ) -> list[dict]: +# """ +# 生成泵操作的动作序列。 + +# :param G: 有向图, 节点为容器和注射泵, 边为流体管道, A→B边的属性为管道接A端的阀门位置 +# :param from_vessel: 容器A +# :param to_vessel: 容器B +# :param volume: 转移的体积 +# :param flowrate: 最终注入容器B时的流速 +# :param transfer_flowrate: 泵骨架中转移流速(若不指定,默认与注入流速相同) +# :return: 泵操作的动作序列 +# """ + +# # 生成电磁阀、真空泵、气源操作的动作序列 +# vacuum_action_sequence = [] +# nodes = G.nodes(data=True) + +# # 找到和 vessel 相连的电磁阀和真空泵、气源 +# vacuum_backbone = {"vessel": vessel} + +# for neighbor in G.neighbors(vessel): +# if nodes[neighbor]["class"].startswith("solenoid_valve"): +# for neighbor2 in G.neighbors(neighbor): +# if neighbor2 == vessel: +# continue +# if nodes[neighbor2]["class"].startswith("vacuum_pump"): +# vacuum_backbone.update({"vacuum_valve": neighbor, "pump": neighbor2}) +# break +# elif nodes[neighbor2]["class"].startswith("gas_source"): +# vacuum_backbone.update({"gas_valve": neighbor, "gas": neighbor2}) +# break +# # 判断是否设备齐全 +# if len(vacuum_backbone) < 5: +# print(f"\n\n\n{vacuum_backbone}\n\n\n") +# raise ValueError("Not all devices are connected to the vessel.") + +# # 生成操作的动作序列 +# for i in range(repeats): +# # 打开真空泵阀门、关闭气源阀门 +# vacuum_action_sequence.append([ +# { +# "device_id": vacuum_backbone["vacuum_valve"], +# "action_name": "set_valve_position", +# "action_kwargs": { +# "command": "OPEN" +# } +# }, +# { +# "device_id": vacuum_backbone["gas_valve"], +# "action_name": "set_valve_position", +# "action_kwargs": { +# "command": "CLOSED" +# } +# } +# ]) + +# # 打开真空泵、关闭气源 +# vacuum_action_sequence.append([ +# { +# "device_id": vacuum_backbone["pump"], +# "action_name": "set_status", +# "action_kwargs": { +# "string": "ON" +# } +# }, +# { +# "device_id": vacuum_backbone["gas"], +# "action_name": "set_status", +# "action_kwargs": { +# "string": "OFF" +# } +# } +# ]) +# vacuum_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 60}}) + +# # 关闭真空泵阀门、打开气源阀门 +# vacuum_action_sequence.append([ +# { +# "device_id": vacuum_backbone["vacuum_valve"], +# "action_name": "set_valve_position", +# "action_kwargs": { +# "command": "CLOSED" +# } +# }, +# { +# "device_id": vacuum_backbone["gas_valve"], +# "action_name": "set_valve_position", +# "action_kwargs": { +# "command": "OPEN" +# } +# } +# ]) + +# # 关闭真空泵、打开气源 +# vacuum_action_sequence.append([ +# { +# "device_id": vacuum_backbone["pump"], +# "action_name": "set_status", +# "action_kwargs": { +# "string": "OFF" +# } +# }, +# { +# "device_id": vacuum_backbone["gas"], +# "action_name": "set_status", +# "action_kwargs": { +# "string": "ON" +# } +# } +# ]) +# vacuum_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 60}}) + +# # 关闭气源 +# vacuum_action_sequence.append( +# { +# "device_id": vacuum_backbone["gas"], +# "action_name": "set_status", +# "action_kwargs": { +# "string": "OFF" +# } +# } +# ) + +# # 关闭阀门 +# vacuum_action_sequence.append( +# { +# "device_id": vacuum_backbone["gas_valve"], +# "action_name": "set_valve_position", +# "action_kwargs": { +# "command": "CLOSED" +# } +# } +# ) +# return vacuum_action_sequence diff --git a/unilabos/compile/evaporate_protocol.py b/unilabos/compile/evaporate_protocol.py index 15af5e1..4cee78d 100644 --- a/unilabos/compile/evaporate_protocol.py +++ b/unilabos/compile/evaporate_protocol.py @@ -1,81 +1,326 @@ -import numpy as np +from typing import List, Dict, Any import networkx as nx +from .pump_protocol import generate_pump_protocol + + +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 find_rotavap_device(G: nx.DiGraph) -> str: + """查找旋转蒸发仪设备""" + rotavap_nodes = [node for node in G.nodes() + if (G.nodes[node].get('class') or '') == 'virtual_rotavap'] + + if rotavap_nodes: + return rotavap_nodes[0] + + 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" + ] + + for vessel_name in possible_names: + if vessel_name in G.nodes(): + return vessel_name + + # 如果找不到专门的回收容器,使用废液容器 + waste_names = ["waste_workup", "flask_waste", "bottle_waste", "waste"] + for vessel_name in waste_names: + if vessel_name in G.nodes(): + return vessel_name + + raise ValueError(f"未找到溶剂回收容器。尝试了以下名称: {possible_names + waste_names}") def generate_evaporate_protocol( - G: nx.DiGraph, + G: nx.DiGraph, vessel: str, - pressure: float, - temp: float, - time: float, - stir_speed: float -) -> list[dict]: + pressure: float = 0.1, + temp: float = 60.0, + time: float = 1800.0, + stir_speed: float = 100.0 +) -> List[Dict[str, Any]]: """ - Generate a protocol to evaporate a solution from a vessel. + 生成蒸发操作的协议序列 - :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. + 蒸发流程: + 1. 液体转移:将待蒸发溶液从源容器转移到旋转蒸发仪 + 2. 蒸发操作:执行旋转蒸发 + 3. (可选) 溶剂回收:将冷凝的溶剂转移到回收容器 + 4. 残留物转移:将浓缩物从旋转蒸发仪转移回原容器或新容器 + + Args: + G: 有向图,节点为设备和容器,边为流体管道 + vessel: 包含待蒸发溶液的容器名称 + pressure: 蒸发时的真空度 (bar),默认0.1 bar + temp: 蒸发时的加热温度 (°C),默认60°C + time: 蒸发时间 (秒),默认1800秒(30分钟) + stir_speed: 旋转速度 (RPM),默认100 RPM + + Returns: + List[Dict[str, Any]]: 蒸发操作的动作序列 + + Raises: + ValueError: 当找不到必要的设备时抛出异常 + + Examples: + evaporate_actions = generate_evaporate_protocol(G, "reaction_mixture", 0.05, 80.0, 3600.0) """ + action_sequence = [] - # 生成泵操作的动作序列 - pump_action_sequence = [] - reactor_volume = 500.0 - transfer_flowrate = flowrate = 2.5 + 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") - # 开启冷凝器 - pump_action_sequence.append({ - "device_id": "rotavap_chiller", - "action_name": "set_temperature", - "action_kwargs": { - "command": "-40" - } - }) - # TODO: 通过温度反馈改为 HeatChillToTemp,而非等待固定时间 - pump_action_sequence.append({ + # 验证源容器存在 + 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 = { "action_name": "wait", - "action_kwargs": { - "time": 1800 - } - }) + "action_kwargs": {"time": 10} + } + action_sequence.append(wait_action) - # 开启旋蒸真空泵、旋转,在液体转移后运行time时间 - pump_action_sequence.append({ - "device_id": "rotavap_controller", - "action_name": "set_pump_time", + # === 第二步:执行旋转蒸发 === + print(f"EVAPORATE: 执行旋转蒸发操作") + evaporate_action = { + "device_id": rotavap_id, + "action_name": "evaporate", "action_kwargs": { - "command": str(time + reactor_volume / flowrate * 3) + "vessel": rotavap_vessel, + "pressure": pressure, + "temp": temp, + "time": time, + "stir_speed": stir_speed } - }) - pump_action_sequence.append({ - "device_id": "rotavap_controller", - "action_name": "set_pump_time", - "action_kwargs": { - "command": str(time + reactor_volume / flowrate * 3) - } - }) + } + action_sequence.append(evaporate_action) - # 液体转入旋转蒸发器 - pump_action_sequence.append({ - "device_id": "", - "action_name": "PumpTransferProtocol", - "action_kwargs": { - "from_vessel": vessel, - "to_vessel": "rotavap", - "volume": reactor_volume, - "time": reactor_volume / flowrate, - # "transfer_flowrate": transfer_flowrate, - } - }) - - pump_action_sequence.append({ + # 蒸发后等待系统稳定 + wait_action = { "action_name": "wait", - "action_kwargs": { - "time": time - } - }) - return pump_action_sequence + "action_kwargs": {"time": 30} + } + action_sequence.append(wait_action) + + # === 第三步:溶剂回收(如果有回收容器)=== + 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") + + 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 2847c5d..7e3ca6b 100644 --- a/unilabos/compile/filter_protocol.py +++ b/unilabos/compile/filter_protocol.py @@ -1,5 +1,89 @@ from typing import List, Dict, Any import networkx as nx +from .pump_protocol import generate_pump_protocol + + +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 find_filter_device(G: nx.DiGraph) -> str: + """查找过滤器设备""" + filter_nodes = [node for node in G.nodes() + if (G.nodes[node].get('class') or '') == 'virtual_filter'] + + if filter_nodes: + return filter_nodes[0] + + raise ValueError("系统中未找到过滤器设备") + + +def find_filter_vessel(G: nx.DiGraph) -> str: + """查找过滤器专用容器""" + possible_names = [ + "filter_vessel", # 标准过滤器容器 + "filtration_vessel", # 备选名称 + "vessel_filter", # 备选名称 + "filter_unit", # 备选名称 + "filter" # 简单名称 + ] + + for vessel_name in possible_names: + if vessel_name in G.nodes(): + return vessel_name + + 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 + + # 自动查找滤液容器 + 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} 相连的加热搅拌器") + def generate_filter_protocol( G: nx.DiGraph, @@ -12,59 +96,209 @@ def generate_filter_protocol( volume: float = 0.0 ) -> List[Dict[str, Any]]: """ - 生成过滤操作的协议序列 + 生成过滤操作的协议序列,复用 pump_protocol 的成熟算法 + + 过滤流程: + 1. 液体转移:将待过滤溶液从源容器转移到过滤器 + 2. 启动加热搅拌:设置温度和搅拌 + 3. 执行过滤:通过过滤器分离固液 + 4. (可选) 继续或停止加热搅拌 Args: - G: 有向图,节点为设备和容器 - vessel: 过滤容器 - filtrate_vessel: 滤液容器(可选) - stir: 是否搅拌 - stir_speed: 搅拌速度(可选) - temp: 温度(可选,摄氏度) - continue_heatchill: 是否继续加热冷却 - volume: 过滤体积(可选) + G: 有向图,节点为设备和容器,边为流体管道 + vessel: 包含待过滤溶液的容器名称 + filtrate_vessel: 滤液收集容器(可选,自动查找) + stir: 是否在过滤过程中搅拌 + stir_speed: 搅拌速度 (RPM) + temp: 过滤温度 (°C) + continue_heatchill: 过滤后是否继续加热搅拌 + volume: 预期过滤体积 (mL),0表示全部过滤 Returns: List[Dict[str, Any]]: 过滤操作的动作序列 - - Raises: - ValueError: 当找不到过滤设备时抛出异常 - - Examples: - filter_protocol = generate_filter_protocol(G, "reactor", "filtrate_vessel", stir=True, volume=100.0) """ action_sequence = [] - # 查找过滤设备 - filter_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_filter'] + 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}") - if not filter_nodes: - raise ValueError("没有找到可用的过滤设备") - - # 使用第一个可用的过滤器 - filter_id = filter_nodes[0] - - # 验证容器是否存在 + # 验证源容器存在 if vessel not in G.nodes(): - raise ValueError(f"过滤容器 {vessel} 不存在于图中") + raise ValueError(f"源容器 '{vessel}' 不存在于系统中") - if filtrate_vessel and filtrate_vessel not in G.nodes(): - raise ValueError(f"滤液容器 {filtrate_vessel} 不存在于图中") + # 获取源容器中的液体体积 + 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") + else: + transfer_volume = 50.0 # 默认过滤量 + print(f"FILTER: 未检测到液体体积,默认过滤 {transfer_volume} mL") + + # === 第一步:启动加热搅拌器(在转移前预热) === + 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} + }) + + # === 第二步:将待过滤溶液转移到过滤器 === + 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) + except Exception as e: + raise ValueError(f"无法将溶液转移到过滤器: {str(e)}") + + # 转移后等待 action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 5} + }) + + # === 第三步:执行过滤操作(完全按照 Filter.action 参数) === + print(f"FILTER: 执行过滤操作") + filter_action = { "device_id": filter_id, - "action_name": "filter_sample", + "action_name": "filter", "action_kwargs": { - "vessel": vessel, - "filtrate_vessel": filtrate_vessel, + "vessel": filter_vessel_id, + "filtrate_vessel": actual_filtrate_vessel, "stir": stir, "stir_speed": stir_speed, "temp": temp, "continue_heatchill": continue_heatchill, - "volume": volume + "volume": transfer_volume } + } + action_sequence.append(filter_action) + + # 过滤后等待 + action_sequence.append({ + "action_name": "wait", + "action_kwargs": {"time": 10} }) - return action_sequence \ No newline at end of file + # === 第四步:如果不继续加热搅拌,停止加热器 === + if heatchill_id and not continue_heatchill and (temp != 25.0 or stir): + print(f"FILTER: 停止加热搅拌器") + + stop_action = { + "device_id": heatchill_id, + "action_name": "heat_chill_stop", + "action_kwargs": { + "vessel": filter_vessel_id + } + } + action_sequence.append(stop_action) + + print(f"FILTER: 生成了 {len(action_sequence)} 个动作") + print(f"FILTER: 过滤协议生成完成") + + 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 ac8ca17..5ce0992 100644 --- a/unilabos/compile/heatchill_protocol.py +++ b/unilabos/compile/heatchill_protocol.py @@ -1,33 +1,61 @@ -from typing import List, Dict, Any +from typing import List, Dict, Any, Optional import networkx as nx + +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'] + + # 检查哪个加热/冷却设备与目标容器相连(机械连接) + 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("系统中未找到可用的加热/冷却设备") + + def generate_heat_chill_protocol( G: nx.DiGraph, vessel: str, temp: float, time: float, - stir: bool, - stir_speed: float, - purpose: str + stir: bool = False, + stir_speed: float = 300.0, + purpose: str = "加热/冷却操作" ) -> List[Dict[str, Any]]: """ - 生成加热/冷却操作的协议序列 - 严格按照 HeatChill.action + 生成加热/冷却操作的协议序列 - 带时间限制的完整操作 """ action_sequence = [] - # 查找加热/冷却设备 - heatchill_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_heatchill'] - - if not heatchill_nodes: - raise ValueError("没有找到可用的加热/冷却设备") - - heatchill_id = heatchill_nodes[0] + print(f"HEATCHILL: 开始生成加热/冷却协议") + print(f" - 容器: {vessel}") + print(f" - 目标温度: {temp}°C") + print(f" - 持续时间: {time}秒") + print(f" - 使用内置搅拌: {stir}, 速度: {stir_speed} RPM") + print(f" - 目的: {purpose}") + # 1. 验证容器存在 if vessel not in G.nodes(): - raise ValueError(f"容器 {vessel} 不存在于图中") + raise ValueError(f"容器 '{vessel}' 不存在于系统中") - action_sequence.append({ + # 2. 查找加热/冷却设备 + try: + heatchill_id = find_connected_heatchill(G, vessel) + print(f"HEATCHILL: 找到加热/冷却设备: {heatchill_id}") + except ValueError as e: + raise ValueError(f"无法找到加热/冷却设备: {str(e)}") + + # 3. 执行加热/冷却操作 + heatchill_action = { "device_id": heatchill_id, "action_name": "heat_chill", "action_kwargs": { @@ -36,10 +64,13 @@ def generate_heat_chill_protocol( "time": time, "stir": stir, "stir_speed": stir_speed, - "purpose": purpose + "status": "start" } - }) + } + action_sequence.append(heatchill_action) + + print(f"HEATCHILL: 生成了 {len(action_sequence)} 个动作") return action_sequence @@ -47,25 +78,31 @@ def generate_heat_chill_start_protocol( G: nx.DiGraph, vessel: str, temp: float, - purpose: str + purpose: str = "开始加热/冷却" ) -> List[Dict[str, Any]]: """ - 生成开始加热/冷却操作的协议序列 - 严格按照 HeatChillStart.action + 生成开始加热/冷却操作的协议序列 """ action_sequence = [] - heatchill_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_heatchill'] - - if not heatchill_nodes: - raise ValueError("没有找到可用的加热/冷却设备") - - heatchill_id = heatchill_nodes[0] + print(f"HEATCHILL_START: 开始生成加热/冷却启动协议") + print(f" - 容器: {vessel}") + print(f" - 目标温度: {temp}°C") + print(f" - 目的: {purpose}") + # 1. 验证容器存在 if vessel not in G.nodes(): - raise ValueError(f"容器 {vessel} 不存在于图中") + raise ValueError(f"容器 '{vessel}' 不存在于系统中") - action_sequence.append({ + # 2. 查找加热/冷却设备 + try: + heatchill_id = find_connected_heatchill(G, vessel) + print(f"HEATCHILL_START: 找到加热/冷却设备: {heatchill_id}") + except ValueError as e: + raise ValueError(f"无法找到加热/冷却设备: {str(e)}") + + # 3. 执行开始加热/冷却操作 + heatchill_start_action = { "device_id": heatchill_id, "action_name": "heat_chill_start", "action_kwargs": { @@ -73,8 +110,11 @@ def generate_heat_chill_start_protocol( "temp": temp, "purpose": purpose } - }) + } + action_sequence.append(heatchill_start_action) + + print(f"HEATCHILL_START: 生成了 {len(action_sequence)} 个动作") return action_sequence @@ -84,34 +124,250 @@ def generate_heat_chill_stop_protocol( ) -> List[Dict[str, Any]]: """ 生成停止加热/冷却操作的协议序列 - - Args: - G: 有向图,节点为设备和容器 - vessel: 容器名称 - - Returns: - List[Dict[str, Any]]: 停止加热/冷却操作的动作序列 """ action_sequence = [] - # 查找加热/冷却设备 - heatchill_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_heatchill'] - - if not heatchill_nodes: - raise ValueError("没有找到可用的加热/冷却设备") - - heatchill_id = heatchill_nodes[0] + print(f"HEATCHILL_STOP: 开始生成加热/冷却停止协议") + print(f" - 容器: {vessel}") + # 1. 验证容器存在 if vessel not in G.nodes(): - raise ValueError(f"容器 {vessel} 不存在于图中") + raise ValueError(f"容器 '{vessel}' 不存在于系统中") - action_sequence.append({ + # 2. 查找加热/冷却设备 + try: + heatchill_id = find_connected_heatchill(G, vessel) + print(f"HEATCHILL_STOP: 找到加热/冷却设备: {heatchill_id}") + except ValueError as e: + raise ValueError(f"无法找到加热/冷却设备: {str(e)}") + + # 3. 执行停止加热/冷却操作 + heatchill_stop_action = { "device_id": heatchill_id, "action_name": "heat_chill_stop", "action_kwargs": { "vessel": vessel } - }) + } - return action_sequence \ No newline at end of file + action_sequence.append(heatchill_stop_action) + + print(f"HEATCHILL_STOP: 生成了 {len(action_sequence)} 个动作") + 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("测试完成") + + +if __name__ == "__main__": + test_heatchill_protocol() \ No newline at end of file diff --git a/unilabos/compile/stir_protocol.py b/unilabos/compile/stir_protocol.py index 90a207c..6fc865c 100644 --- a/unilabos/compile/stir_protocol.py +++ b/unilabos/compile/stir_protocol.py @@ -1,6 +1,28 @@ from typing import List, Dict, Any import networkx as nx + +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'] + + if vessel: + # 检查哪个搅拌设备与目标容器相连(机械连接) + for stirrer in stirrer_nodes: + if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer): + return stirrer + + # 如果没有指定容器或没有直接连接,返回第一个可用的搅拌设备 + if stirrer_nodes: + return stirrer_nodes[0] + + raise ValueError("系统中未找到可用的搅拌设备") + + def generate_stir_protocol( G: nx.DiGraph, stir_time: float, @@ -8,37 +30,24 @@ def generate_stir_protocol( settling_time: float ) -> List[Dict[str, Any]]: """ - 生成搅拌操作的协议序列 - - Args: - G: 有向图,节点为设备和容器 - stir_time: 搅拌时间 (秒) - stir_speed: 搅拌速度 (rpm) - settling_time: 沉降时间 (秒) - - Returns: - List[Dict[str, Any]]: 搅拌操作的动作序列 - - Raises: - ValueError: 当找不到搅拌设备时抛出异常 - - Examples: - stir_protocol = generate_stir_protocol(G, 300.0, 500.0, 60.0) + 生成搅拌操作的协议序列 - 定时搅拌 + 沉降 """ action_sequence = [] + print(f"STIR: 开始生成搅拌协议") + print(f" - 搅拌时间: {stir_time}秒") + print(f" - 搅拌速度: {stir_speed} RPM") + print(f" - 沉降时间: {settling_time}秒") + # 查找搅拌设备 - stirrer_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_stirrer'] - - if not stirrer_nodes: - raise ValueError("没有找到可用的搅拌设备") - - # 使用第一个可用的搅拌器 - stirrer_id = stirrer_nodes[0] + try: + stirrer_id = find_connected_stirrer(G) + print(f"STIR: 找到搅拌设备: {stirrer_id}") + except ValueError as e: + raise ValueError(f"无法找到搅拌设备: {str(e)}") # 执行搅拌操作 - action_sequence.append({ + stir_action = { "device_id": stirrer_id, "action_name": "stir", "action_kwargs": { @@ -46,8 +55,11 @@ def generate_stir_protocol( "stir_speed": stir_speed, "settling_time": settling_time } - }) + } + action_sequence.append(stir_action) + + print(f"STIR: 生成了 {len(action_sequence)} 个动作") return action_sequence @@ -58,33 +70,28 @@ def generate_start_stir_protocol( purpose: str ) -> List[Dict[str, Any]]: """ - 生成开始搅拌操作的协议序列 - - Args: - G: 有向图,节点为设备和容器 - vessel: 搅拌容器 - stir_speed: 搅拌速度 (rpm) - purpose: 搅拌目的 - - Returns: - List[Dict[str, Any]]: 开始搅拌操作的动作序列 + 生成开始搅拌操作的协议序列 - 持续搅拌 """ action_sequence = [] - # 查找搅拌设备 - stirrer_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_stirrer'] + print(f"START_STIR: 开始生成启动搅拌协议") + print(f" - 容器: {vessel}") + print(f" - 搅拌速度: {stir_speed} RPM") + print(f" - 目的: {purpose}") - if not stirrer_nodes: - raise ValueError("没有找到可用的搅拌设备") - - stirrer_id = stirrer_nodes[0] - - # 验证容器是否存在 + # 验证容器存在 if vessel not in G.nodes(): - raise ValueError(f"容器 {vessel} 不存在于图中") + raise ValueError(f"容器 '{vessel}' 不存在于系统中") - action_sequence.append({ + # 查找搅拌设备 + try: + stirrer_id = find_connected_stirrer(G, vessel) + print(f"START_STIR: 找到搅拌设备: {stirrer_id}") + except ValueError as e: + raise ValueError(f"无法找到搅拌设备: {str(e)}") + + # 执行开始搅拌操作 + start_stir_action = { "device_id": stirrer_id, "action_name": "start_stir", "action_kwargs": { @@ -92,8 +99,11 @@ def generate_start_stir_protocol( "stir_speed": stir_speed, "purpose": purpose } - }) + } + action_sequence.append(start_stir_action) + + print(f"START_STIR: 生成了 {len(action_sequence)} 个动作") return action_sequence @@ -103,35 +113,54 @@ def generate_stop_stir_protocol( ) -> List[Dict[str, Any]]: """ 生成停止搅拌操作的协议序列 - - Args: - G: 有向图,节点为设备和容器 - vessel: 搅拌容器 - - Returns: - List[Dict[str, Any]]: 停止搅拌操作的动作序列 """ action_sequence = [] - # 查找搅拌设备 - stirrer_nodes = [node for node in G.nodes() - if G.nodes[node].get('class') == 'virtual_stirrer'] + print(f"STOP_STIR: 开始生成停止搅拌协议") + print(f" - 容器: {vessel}") - if not stirrer_nodes: - raise ValueError("没有找到可用的搅拌设备") - - stirrer_id = stirrer_nodes[0] - - # 验证容器是否存在 + # 验证容器存在 if vessel not in G.nodes(): - raise ValueError(f"容器 {vessel} 不存在于图中") + raise ValueError(f"容器 '{vessel}' 不存在于系统中") - action_sequence.append({ + # 查找搅拌设备 + try: + stirrer_id = find_connected_stirrer(G, vessel) + print(f"STOP_STIR: 找到搅拌设备: {stirrer_id}") + except ValueError as e: + raise ValueError(f"无法找到搅拌设备: {str(e)}") + + # 执行停止搅拌操作 + stop_stir_action = { "device_id": stirrer_id, "action_name": "stop_stir", "action_kwargs": { "vessel": vessel } - }) + } - return action_sequence \ No newline at end of file + action_sequence.append(stop_stir_action) + + print(f"STOP_STIR: 生成了 {len(action_sequence)} 个动作") + return action_sequence + + +# 便捷函数 +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 diff --git a/unilabos/devices/virtual/virtual_centrifuge.py b/unilabos/devices/virtual/virtual_centrifuge.py index 20366ab..79f9dce 100644 --- a/unilabos/devices/virtual/virtual_centrifuge.py +++ b/unilabos/devices/virtual/virtual_centrifuge.py @@ -1,158 +1,213 @@ import asyncio import logging -from typing import Dict, Any +import time as time_module +from typing import Dict, Any, Optional + class VirtualCentrifuge: - """Virtual centrifuge device for CentrifugeProtocol testing""" - - def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): + """Virtual centrifuge device - 简化版,只保留核心功能""" + + def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs): # 处理可能的不同调用方式 - if device_id is None and 'id' in kwargs: - device_id = kwargs.pop('id') - if config is None and 'config' in kwargs: - config = kwargs.pop('config') - + if device_id is None and "id" in kwargs: + device_id = kwargs.pop("id") + if config is None and "config" in kwargs: + config = kwargs.pop("config") + # 设置默认值 self.device_id = device_id or "unknown_centrifuge" self.config = config or {} - + self.logger = logging.getLogger(f"VirtualCentrifuge.{self.device_id}") self.data = {} - - # 添加调试信息 - print(f"=== VirtualCentrifuge {self.device_id} is being created! ===") - print(f"=== Config: {self.config} ===") - print(f"=== Kwargs: {kwargs} ===") - + # 从config或kwargs中获取配置参数 - self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL') - self._max_speed = self.config.get('max_speed') or kwargs.get('max_speed', 15000.0) - self._max_temp = self.config.get('max_temp') or kwargs.get('max_temp', 40.0) - self._min_temp = self.config.get('min_temp') or kwargs.get('min_temp', 4.0) - - # 处理其他kwargs参数,但跳过已知的配置参数 - skip_keys = {'port', 'max_speed', 'max_temp', 'min_temp'} + self.port = self.config.get("port") or kwargs.get("port", "VIRTUAL") + self._max_speed = self.config.get("max_speed") or kwargs.get("max_speed", 15000.0) + self._max_temp = self.config.get("max_temp") or kwargs.get("max_temp", 40.0) + self._min_temp = self.config.get("min_temp") or kwargs.get("min_temp", 4.0) + + # 处理其他kwargs参数 + skip_keys = {"port", "max_speed", "max_temp", "min_temp"} for key, value in kwargs.items(): if key not in skip_keys and not hasattr(self, key): setattr(self, key, value) - + async def initialize(self) -> bool: """Initialize virtual centrifuge""" - print(f"=== VirtualCentrifuge {self.device_id} initialize() called! ===") self.logger.info(f"Initializing virtual centrifuge {self.device_id}") + + # 只保留核心状态 self.data.update({ "status": "Idle", + "centrifuge_state": "Stopped", # Stopped, Running, Completed, Error "current_speed": 0.0, "target_speed": 0.0, "current_temp": 25.0, "target_temp": 25.0, - "max_speed": self._max_speed, - "max_temp": self._max_temp, - "min_temp": self._min_temp, - "centrifuge_state": "Stopped", "time_remaining": 0.0, "progress": 0.0, - "message": "" + "message": "Ready for centrifugation" }) return True - + async def cleanup(self) -> bool: """Cleanup virtual centrifuge""" self.logger.info(f"Cleaning up virtual centrifuge {self.device_id}") + + self.data.update({ + "status": "Offline", + "centrifuge_state": "Offline", + "current_speed": 0.0, + "current_temp": 25.0, + "message": "System offline" + }) return True - - async def centrifuge(self, vessel: str, speed: float, time: float, temp: float = 25.0) -> bool: - """Execute centrifuge action - matches Centrifuge action""" - self.logger.info(f"Centrifuge: vessel={vessel}, speed={speed} RPM, time={time}s, temp={temp}°C") - + + async def centrifuge( + self, + vessel: str, + speed: float, + time: float, + temp: float = 25.0 + ) -> bool: + """Execute centrifuge action - 简化的离心流程""" + self.logger.info(f"Centrifuge: vessel={vessel}, speed={speed} rpm, time={time}s, temp={temp}°C") + # 验证参数 - if speed > self._max_speed: - self.logger.error(f"Speed {speed} exceeds maximum {self._max_speed}") - self.data["message"] = f"速度 {speed} 超过最大值 {self._max_speed}" + if speed > self._max_speed or speed < 100.0: + error_msg = f"离心速度 {speed} rpm 超出范围 (100-{self._max_speed} rpm)" + self.logger.error(error_msg) + self.data.update({ + "status": f"Error: {error_msg}", + "centrifuge_state": "Error", + "message": error_msg + }) return False - + if temp > self._max_temp or temp < self._min_temp: - self.logger.error(f"Temperature {temp} outside range {self._min_temp}-{self._max_temp}") - self.data["message"] = f"温度 {temp} 超出范围 {self._min_temp}-{self._max_temp}" + error_msg = f"温度 {temp}°C 超出范围 ({self._min_temp}-{self._max_temp}°C)" + self.logger.error(error_msg) + self.data.update({ + "status": f"Error: {error_msg}", + "centrifuge_state": "Error", + "message": error_msg + }) return False - + # 开始离心 self.data.update({ - "status": "Running", - "centrifuge_state": "Centrifuging", - "target_speed": speed, + "status": f"离心中: {vessel}", + "centrifuge_state": "Running", "current_speed": speed, - "target_temp": temp, + "target_speed": speed, "current_temp": temp, + "target_temp": temp, "time_remaining": time, - "vessel": vessel, "progress": 0.0, - "message": f"离心中: {vessel} at {speed} RPM" + "message": f"Centrifuging {vessel} at {speed} rpm, {temp}°C" }) - - # 模拟离心过程 - simulation_time = min(time, 5.0) # 最多等待5秒用于测试 - await asyncio.sleep(simulation_time) - - # 离心完成 - self.data.update({ - "status": "Idle", - "centrifuge_state": "Stopped", - "current_speed": 0.0, - "target_speed": 0.0, - "time_remaining": 0.0, - "progress": 100.0, - "message": f"离心完成: {vessel}" - }) - - self.logger.info(f"Centrifuge completed for vessel {vessel}") - return True - - # 状态属性 + + try: + # 离心过程 - 实时更新进度 + start_time = time_module.time() + total_time = time + + while True: + current_time = time_module.time() + elapsed = current_time - start_time + remaining = max(0, total_time - elapsed) + progress = min(100.0, (elapsed / total_time) * 100) + + # 更新状态 + self.data.update({ + "time_remaining": remaining, + "progress": progress, + "status": f"离心中: {vessel} | {speed} rpm | {temp}°C | {progress:.1f}% | 剩余: {remaining:.0f}s", + "message": f"Centrifuging: {progress:.1f}% complete, {remaining:.0f}s remaining" + }) + + # 时间到了,退出循环 + if remaining <= 0: + break + + # 每秒更新一次 + await asyncio.sleep(1.0) + + # 离心完成 + self.data.update({ + "status": f"离心完成: {vessel} | {speed} rpm | {time}s", + "centrifuge_state": "Completed", + "progress": 100.0, + "time_remaining": 0.0, + "current_speed": 0.0, # 停止旋转 + "current_temp": 25.0, # 恢复室温 + "message": f"Centrifugation completed: {vessel} at {speed} rpm for {time}s" + }) + + self.logger.info(f"Centrifugation completed: {vessel} at {speed} rpm for {time}s") + return True + + except Exception as e: + # 出错处理 + self.logger.error(f"Error during centrifugation: {str(e)}") + + self.data.update({ + "status": f"离心错误: {str(e)}", + "centrifuge_state": "Error", + "current_speed": 0.0, + "current_temp": 25.0, + "progress": 0.0, + "time_remaining": 0.0, + "message": f"Centrifugation failed: {str(e)}" + }) + return False + + # === 核心状态属性 === @property def status(self) -> str: return self.data.get("status", "Unknown") - - @property - def current_speed(self) -> float: - return self.data.get("current_speed", 0.0) - - @property - def target_speed(self) -> float: - return self.data.get("target_speed", 0.0) - - @property - def current_temp(self) -> float: - return self.data.get("current_temp", 25.0) - - @property - def target_temp(self) -> float: - return self.data.get("target_temp", 25.0) - - @property - def max_speed(self) -> float: - return self.data.get("max_speed", self._max_speed) - - @property - def max_temp(self) -> float: - return self.data.get("max_temp", self._max_temp) - - @property - def min_temp(self) -> float: - return self.data.get("min_temp", self._min_temp) - + @property def centrifuge_state(self) -> str: return self.data.get("centrifuge_state", "Unknown") - + + @property + def current_speed(self) -> float: + return self.data.get("current_speed", 0.0) + + @property + def target_speed(self) -> float: + return self.data.get("target_speed", 0.0) + + @property + def current_temp(self) -> float: + return self.data.get("current_temp", 25.0) + + @property + def target_temp(self) -> float: + return self.data.get("target_temp", 25.0) + + @property + def max_speed(self) -> float: + return self._max_speed + + @property + def max_temp(self) -> float: + return self._max_temp + + @property + def min_temp(self) -> float: + return self._min_temp + @property def time_remaining(self) -> float: return self.data.get("time_remaining", 0.0) - + @property def progress(self) -> float: return self.data.get("progress", 0.0) - + @property def message(self) -> str: return self.data.get("message", "") \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_filter.py b/unilabos/devices/virtual/virtual_filter.py index 71a984a..ca2e8b2 100644 --- a/unilabos/devices/virtual/virtual_filter.py +++ b/unilabos/devices/virtual/virtual_filter.py @@ -1,151 +1,221 @@ import asyncio import logging -from typing import Dict, Any +import time as time_module +from typing import Dict, Any, Optional + class VirtualFilter: - """Virtual filter device for FilterProtocol testing""" + """Virtual filter device - 完全按照 Filter.action 规范""" - def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): - # 处理可能的不同调用方式 + def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs): if device_id is None and 'id' in kwargs: device_id = kwargs.pop('id') if config is None and 'config' in kwargs: config = kwargs.pop('config') - # 设置默认值 self.device_id = device_id or "unknown_filter" self.config = config or {} - self.logger = logging.getLogger(f"VirtualFilter.{self.device_id}") self.data = {} - # 添加调试信息 - print(f"=== VirtualFilter {self.device_id} is being created! ===") - print(f"=== Config: {self.config} ===") - print(f"=== Kwargs: {kwargs} ===") - # 从config或kwargs中获取配置参数 self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL') self._max_temp = self.config.get('max_temp') or kwargs.get('max_temp', 100.0) self._max_stir_speed = self.config.get('max_stir_speed') or kwargs.get('max_stir_speed', 1000.0) + self._max_volume = self.config.get('max_volume') or kwargs.get('max_volume', 500.0) - # 处理其他kwargs参数,但跳过已知的配置参数 - skip_keys = {'port', 'max_temp', 'max_stir_speed'} + # 处理其他kwargs参数 + skip_keys = {'port', 'max_temp', 'max_stir_speed', 'max_volume'} for key, value in kwargs.items(): if key not in skip_keys and not hasattr(self, key): setattr(self, key, value) async def initialize(self) -> bool: """Initialize virtual filter""" - print(f"=== VirtualFilter {self.device_id} initialize() called! ===") self.logger.info(f"Initializing virtual filter {self.device_id}") + + # 按照 Filter.action 的 feedback 字段初始化 self.data.update({ "status": "Idle", - "filter_state": "Ready", - "current_temp": 25.0, - "target_temp": 25.0, - "max_temp": self._max_temp, - "stir_speed": 0.0, - "max_stir_speed": self._max_stir_speed, - "filtered_volume": 0.0, - "progress": 0.0, - "message": "" + "progress": 0.0, # Filter.action feedback + "current_temp": 25.0, # Filter.action feedback + "filtered_volume": 0.0, # Filter.action feedback + "current_status": "Ready for filtration", # Filter.action feedback + "message": "Ready for filtration" }) return True async def cleanup(self) -> bool: """Cleanup virtual filter""" self.logger.info(f"Cleaning up virtual filter {self.device_id}") + + self.data.update({ + "status": "Offline", + "current_status": "System offline", + "message": "System offline" + }) return True - async def filter_sample(self, 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) -> bool: - """Execute filter action - matches Filter action""" - self.logger.info(f"Filter: vessel={vessel}, filtrate_vessel={filtrate_vessel}, stir={stir}, volume={volume}") + async def filter( + self, + 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 + ) -> 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}") # 验证参数 - if temp > self._max_temp: - self.logger.error(f"Temperature {temp} exceeds maximum {self._max_temp}") - self.data["message"] = f"温度 {temp} 超过最大值 {self._max_temp}" + if temp > self._max_temp or temp < 4.0: + error_msg = f"温度 {temp}°C 超出范围 (4-{self._max_temp}°C)" + self.logger.error(error_msg) + self.data.update({ + "status": f"Error: {error_msg}", + "current_status": f"Error: {error_msg}", + "message": error_msg + }) return False if stir and stir_speed > self._max_stir_speed: - self.logger.error(f"Stir speed {stir_speed} exceeds maximum {self._max_stir_speed}") - self.data["message"] = f"搅拌速度 {stir_speed} 超过最大值 {self._max_stir_speed}" + error_msg = f"搅拌速度 {stir_speed} RPM 超出范围 (0-{self._max_stir_speed} RPM)" + self.logger.error(error_msg) + self.data.update({ + "status": f"Error: {error_msg}", + "current_status": f"Error: {error_msg}", + "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) + self.data.update({ + "status": f"Error: {error_msg}", + "current_status": f"Error: {error_msg}", + "message": error_msg + }) return False # 开始过滤 + filter_volume = volume if volume > 0 else 50.0 + self.data.update({ - "status": "Running", - "filter_state": "Filtering", - "target_temp": temp, + "status": f"过滤中: {vessel}", "current_temp": temp, - "stir_speed": stir_speed if stir else 0.0, - "vessel": vessel, - "filtrate_vessel": filtrate_vessel, - "target_volume": volume, + "filtered_volume": 0.0, "progress": 0.0, - "message": f"过滤中: {vessel}" + "current_status": f"Filtering {vessel} → {filtrate_vessel}", + "message": f"Starting filtration: {vessel} → {filtrate_vessel}" }) - # 模拟过滤过程 - simulation_time = min(volume / 10.0 if volume > 0 else 5.0, 10.0) - await asyncio.sleep(simulation_time) - - # 过滤完成 - filtered_vol = volume if volume > 0 else 50.0 # 默认过滤量 - self.data.update({ - "status": "Idle", - "filter_state": "Ready", - "current_temp": 25.0 if not continue_heatchill else temp, - "target_temp": 25.0 if not continue_heatchill else temp, - "stir_speed": 0.0 if not stir else stir_speed, - "filtered_volume": filtered_vol, - "progress": 100.0, - "message": f"过滤完成: {filtered_vol}mL" - }) - - self.logger.info(f"Filter completed: {filtered_vol}mL from {vessel}") - return True + try: + # 过滤过程 - 实时更新进度 + start_time = time_module.time() + # 根据体积和搅拌估算过滤时间 + base_time = filter_volume / 5.0 # 5mL/s 基础速度 + if stir: + base_time *= 0.8 # 搅拌加速过滤 + if temp > 50.0: + base_time *= 0.7 # 高温加速过滤 + filter_time = max(base_time, 10.0) # 最少10秒 + + while True: + current_time = time_module.time() + elapsed = current_time - start_time + remaining = max(0, filter_time - elapsed) + progress = min(100.0, (elapsed / filter_time) * 100) + current_filtered = (progress / 100.0) * filter_volume + + # 更新状态 - 按照 Filter.action feedback 字段 + status_msg = f"过滤中: {vessel}" + if stir: + 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 + "status": status_msg, + "message": f"Filtering: {progress:.1f}% complete, {current_filtered:.1f}mL filtered" + }) + + if remaining <= 0: + break + + await asyncio.sleep(1.0) + + # 过滤完成 + final_temp = temp if continue_heatchill else 25.0 + final_status = f"过滤完成: {vessel} | {filter_volume}mL → {filtrate_vessel}" + if continue_heatchill: + final_status += " | 继续加热搅拌" + + 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}" + }) + + self.logger.info(f"Filtration completed: {filter_volume}mL from {vessel} to {filtrate_vessel}") + return True + + except Exception as e: + self.logger.error(f"Error during filtration: {str(e)}") + self.data.update({ + "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") - @property - def filter_state(self) -> str: - return self.data.get("filter_state", "Unknown") - - @property - def current_temp(self) -> float: - return self.data.get("current_temp", 25.0) - - @property - def target_temp(self) -> float: - return self.data.get("target_temp", 25.0) - - @property - def max_temp(self) -> float: - return self.data.get("max_temp", self._max_temp) - - @property - def stir_speed(self) -> float: - return self.data.get("stir_speed", 0.0) - - @property - def max_stir_speed(self) -> float: - return self.data.get("max_stir_speed", self._max_stir_speed) - - @property - def filtered_volume(self) -> float: - return self.data.get("filtered_volume", 0.0) - @property def progress(self) -> float: + """Filter.action feedback 字段""" return self.data.get("progress", 0.0) + @property + def current_temp(self) -> float: + """Filter.action feedback 字段""" + return self.data.get("current_temp", 25.0) + + @property + def filtered_volume(self) -> float: + """Filter.action feedback 字段""" + return self.data.get("filtered_volume", 0.0) + + @property + def current_status(self) -> str: + """Filter.action feedback 字段""" + return self.data.get("current_status", "") + @property def message(self) -> str: - return self.data.get("message", "") \ No newline at end of file + return self.data.get("message", "") + + @property + def max_temp(self) -> float: + return self._max_temp + + @property + def max_stir_speed(self) -> float: + return self._max_stir_speed + + @property + def max_volume(self) -> float: + return self._max_volume \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_heatchill.py b/unilabos/devices/virtual/virtual_heatchill.py index 98a03ce..541434a 100644 --- a/unilabos/devices/virtual/virtual_heatchill.py +++ b/unilabos/devices/virtual/virtual_heatchill.py @@ -1,5 +1,6 @@ import asyncio import logging +import time as time_module # 重命名time模块,避免与参数冲突 from typing import Dict, Any class VirtualHeatChill: @@ -19,18 +20,13 @@ class VirtualHeatChill: self.logger = logging.getLogger(f"VirtualHeatChill.{self.device_id}") self.data = {} - # 添加调试信息 - print(f"=== VirtualHeatChill {self.device_id} is being created! ===") - print(f"=== Config: {self.config} ===") - print(f"=== Kwargs: {kwargs} ===") - # 从config或kwargs中获取配置参数 self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL') self._max_temp = self.config.get('max_temp') or kwargs.get('max_temp', 200.0) self._min_temp = self.config.get('min_temp') or kwargs.get('min_temp', -80.0) self._max_stir_speed = self.config.get('max_stir_speed') or kwargs.get('max_stir_speed', 1000.0) - # 处理其他kwargs参数,但跳过已知的配置参数 + # 处理其他kwargs参数 skip_keys = {'port', 'max_temp', 'min_temp', 'max_stir_speed'} for key, value in kwargs.items(): if key not in skip_keys and not hasattr(self, key): @@ -38,70 +34,177 @@ class VirtualHeatChill: async def initialize(self) -> bool: """Initialize virtual heat chill""" - print(f"=== VirtualHeatChill {self.device_id} initialize() called! ===") self.logger.info(f"Initializing virtual heat chill {self.device_id}") + + # 初始化状态信息 self.data.update({ - "status": "Idle" + "status": "Idle", + "operation_mode": "Idle", + "is_stirring": False, + "stir_speed": 0.0, + "remaining_time": 0.0, }) return True async def cleanup(self) -> bool: """Cleanup virtual heat chill""" self.logger.info(f"Cleaning up virtual heat chill {self.device_id}") + self.data.update({ + "status": "Offline", + "operation_mode": "Offline", + "is_stirring": False, + "stir_speed": 0.0, + "remaining_time": 0.0 + }) return True async def heat_chill(self, vessel: str, temp: float, time: float, stir: bool, stir_speed: float, purpose: str) -> bool: - """Execute heat chill action - matches HeatChill action exactly""" - self.logger.info(f"HeatChill: vessel={vessel}, temp={temp}°C, time={time}s, stir={stir}, stir_speed={stir_speed}, purpose={purpose}") + """Execute heat chill action - 按实际时间运行,实时更新剩余时间""" + self.logger.info(f"HeatChill: vessel={vessel}, temp={temp}°C, time={time}s, stir={stir}, stir_speed={stir_speed}") # 验证参数 if temp > self._max_temp or temp < self._min_temp: - self.logger.error(f"Temperature {temp} outside range {self._min_temp}-{self._max_temp}") - self.data["status"] = f"温度 {temp} 超出范围" + error_msg = f"温度 {temp}°C 超出范围 ({self._min_temp}°C - {self._max_temp}°C)" + self.logger.error(error_msg) + self.data.update({ + "status": f"Error: {error_msg}", + "operation_mode": "Error" + }) return False if stir and stir_speed > self._max_stir_speed: - self.logger.error(f"Stir speed {stir_speed} exceeds maximum {self._max_stir_speed}") - self.data["status"] = f"搅拌速度 {stir_speed} 超出范围" + error_msg = f"搅拌速度 {stir_speed} RPM 超出最大值 {self._max_stir_speed} RPM" + self.logger.error(error_msg) + self.data.update({ + "status": f"Error: {error_msg}", + "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 = "保温" + + # **修复**: 使用重命名的time模块 + start_time = time_module.time() + total_time = time + + # 开始操作 + stir_info = f" | 搅拌: {stir_speed} RPM" if stir else "" self.data.update({ - "status": f"加热/冷却中: {vessel} 至 {temp}°C" + "status": f"运行中: {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, }) - # 模拟加热/冷却时间 - simulation_time = min(time, 10.0) # 最多等待10秒用于测试 - await asyncio.sleep(simulation_time) + # **修复**: 在等待过程中每秒更新剩余时间 + while True: + current_time = time_module.time() # 使用重命名的time模块 + elapsed = current_time - start_time + remaining = max(0, total_time - elapsed) + + # 更新剩余时间和状态 + self.data.update({ + "remaining_time": remaining, + "status": f"运行中: {status_action} {vessel} 至 {temp}°C | 剩余: {remaining:.0f}s{stir_info}" + }) + + # 如果时间到了,退出循环 + if remaining <= 0: + break + + # 等待1秒后再次检查 + await asyncio.sleep(1.0) - # 加热/冷却完成 - self.data["status"] = f"完成: {vessel} 已达到 {temp}°C" + # 操作完成 + 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}", + "operation_mode": "Completed", + "remaining_time": 0.0, + "is_stirring": False, + "stir_speed": 0.0 + }) - self.logger.info(f"HeatChill completed for vessel {vessel} at {temp}°C") + self.logger.info(f"HeatChill completed for vessel {vessel} at {temp}°C after {total_time}s") return True async def heat_chill_start(self, vessel: str, temp: float, purpose: str) -> bool: - """Start heat chill - matches HeatChillStart action exactly""" - self.logger.info(f"HeatChillStart: vessel={vessel}, temp={temp}°C, purpose={purpose}") + """Start continuous heat chill""" + self.logger.info(f"HeatChillStart: vessel={vessel}, temp={temp}°C") # 验证参数 if temp > self._max_temp or temp < self._min_temp: - self.logger.error(f"Temperature {temp} outside range {self._min_temp}-{self._max_temp}") - self.data["status"] = f"温度 {temp} 超出范围" + error_msg = f"温度 {temp}°C 超出范围 ({self._min_temp}°C - {self._max_temp}°C)" + self.logger.error(error_msg) + self.data.update({ + "status": f"Error: {error_msg}", + "operation_mode": "Error" + }) return False - self.data["status"] = f"开始加热/冷却: {vessel} 至 {temp}°C" + # 确定操作模式 + if temp > 25.0: + operation_mode = "Heating" + status_action = "持续加热" + elif temp < 25.0: + operation_mode = "Cooling" + status_action = "持续冷却" + else: + operation_mode = "Maintaining" + status_action = "恒温保持" + + self.data.update({ + "status": f"启动: {status_action} {vessel} 至 {temp}°C | 持续运行", + "operation_mode": operation_mode, + "is_stirring": False, + "stir_speed": 0.0, + "remaining_time": -1.0, # -1 表示持续运行 + }) + return True async def heat_chill_stop(self, vessel: str) -> bool: - """Stop heat chill - matches HeatChillStop action exactly""" + """Stop heat chill""" self.logger.info(f"HeatChillStop: vessel={vessel}") - self.data["status"] = f"停止加热/冷却: {vessel}" + self.data.update({ + "status": f"已停止: {vessel} 温控停止", + "operation_mode": "Stopped", + "is_stirring": False, + "stir_speed": 0.0, + "remaining_time": 0.0, + }) + return True - # 状态属性 - 只保留 action 中定义的 feedback + # 状态属性 @property def status(self) -> str: - return self.data.get("status", "Idle") \ No newline at end of file + return self.data.get("status", "Idle") + + @property + def operation_mode(self) -> str: + return self.data.get("operation_mode", "Idle") + + @property + def is_stirring(self) -> bool: + return self.data.get("is_stirring", False) + + @property + def stir_speed(self) -> float: + return self.data.get("stir_speed", 0.0) + + @property + def remaining_time(self) -> float: + return self.data.get("remaining_time", 0.0) \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_rotavap.py b/unilabos/devices/virtual/virtual_rotavap.py index 01f1ae0..ba01c7b 100644 --- a/unilabos/devices/virtual/virtual_rotavap.py +++ b/unilabos/devices/virtual/virtual_rotavap.py @@ -1,10 +1,11 @@ import asyncio import logging +import time as time_module from typing import Dict, Any, Optional class VirtualRotavap: - """Virtual rotary evaporator device for EvaporateProtocol testing""" + """Virtual rotary evaporator device - 简化版,只保留核心功能""" def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs): # 处理可能的不同调用方式 @@ -20,17 +21,12 @@ class VirtualRotavap: self.logger = logging.getLogger(f"VirtualRotavap.{self.device_id}") self.data = {} - # 添加调试信息 - print(f"=== VirtualRotavap {self.device_id} is being created! ===") - print(f"=== Config: {self.config} ===") - print(f"=== Kwargs: {kwargs} ===") - # 从config或kwargs中获取配置参数 self.port = self.config.get("port") or kwargs.get("port", "VIRTUAL") self._max_temp = self.config.get("max_temp") or kwargs.get("max_temp", 180.0) self._max_rotation_speed = self.config.get("max_rotation_speed") or kwargs.get("max_rotation_speed", 280.0) - # 处理其他kwargs参数,但跳过已知的配置参数 + # 处理其他kwargs参数 skip_keys = {"port", "max_temp", "max_rotation_speed"} for key, value in kwargs.items(): if key not in skip_keys and not hasattr(self, key): @@ -38,95 +34,155 @@ class VirtualRotavap: async def initialize(self) -> bool: """Initialize virtual rotary evaporator""" - print(f"=== VirtualRotavap {self.device_id} initialize() called! ===") self.logger.info(f"Initializing virtual rotary evaporator {self.device_id}") - self.data.update( - { - "status": "Idle", - "rotavap_state": "Ready", - "current_temp": 25.0, - "target_temp": 25.0, - "max_temp": self._max_temp, - "rotation_speed": 0.0, - "max_rotation_speed": self._max_rotation_speed, - "vacuum_pressure": 1.0, # atmospheric pressure - "evaporated_volume": 0.0, - "progress": 0.0, - "message": "", - } - ) + + # 只保留核心状态 + self.data.update({ + "status": "Idle", + "rotavap_state": "Ready", # Ready, Evaporating, Completed, Error + "current_temp": 25.0, + "target_temp": 25.0, + "rotation_speed": 0.0, + "vacuum_pressure": 1.0, # 大气压 + "evaporated_volume": 0.0, + "progress": 0.0, + "remaining_time": 0.0, + "message": "Ready for evaporation" + }) return True async def cleanup(self) -> bool: """Cleanup virtual rotary evaporator""" self.logger.info(f"Cleaning up virtual rotary evaporator {self.device_id}") + + self.data.update({ + "status": "Offline", + "rotavap_state": "Offline", + "current_temp": 25.0, + "rotation_speed": 0.0, + "vacuum_pressure": 1.0, + "message": "System offline" + }) return True async def evaporate( - self, vessel: str, pressure: float = 0.5, temp: float = 60.0, time: float = 300.0, stir_speed: float = 100.0 + self, + vessel: str, + pressure: float = 0.1, + temp: float = 60.0, + time: float = 1800.0, # 30分钟默认 + stir_speed: float = 100.0 ) -> bool: - """Execute evaporate action - matches Evaporate action""" - self.logger.info(f"Evaporate: vessel={vessel}, pressure={pressure}, temp={temp}, time={time}") + """Execute evaporate action - 简化的蒸发流程""" + self.logger.info(f"Evaporate: vessel={vessel}, pressure={pressure} bar, temp={temp}°C, time={time}s, rotation={stir_speed} RPM") # 验证参数 - if temp > self._max_temp: - self.logger.error(f"Temperature {temp} exceeds maximum {self._max_temp}") - self.data["message"] = f"温度 {temp} 超过最大值 {self._max_temp}" + if temp > self._max_temp or temp < 10.0: + error_msg = f"温度 {temp}°C 超出范围 (10-{self._max_temp}°C)" + self.logger.error(error_msg) + self.data.update({ + "status": f"Error: {error_msg}", + "rotavap_state": "Error", + "message": error_msg + }) return False - if stir_speed > self._max_rotation_speed: - self.logger.error(f"Rotation speed {stir_speed} exceeds maximum {self._max_rotation_speed}") - self.data["message"] = f"旋转速度 {stir_speed} 超过最大值 {self._max_rotation_speed}" + 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) + self.data.update({ + "status": f"Error: {error_msg}", + "rotavap_state": "Error", + "message": error_msg + }) return False if pressure < 0.01 or pressure > 1.0: - self.logger.error(f"Pressure {pressure} bar is out of valid range (0.01-1.0)") - self.data["message"] = f"真空度 {pressure} bar 超出有效范围 (0.01-1.0)" + error_msg = f"真空度 {pressure} bar 超出范围 (0.01-1.0 bar)" + self.logger.error(error_msg) + self.data.update({ + "status": f"Error: {error_msg}", + "rotavap_state": "Error", + "message": error_msg + }) return False # 开始蒸发 - self.data.update( - { - "status": "Running", - "rotavap_state": "Evaporating", - "target_temp": temp, - "current_temp": temp, - "rotation_speed": stir_speed, - "vacuum_pressure": pressure, - "vessel": vessel, - "target_time": time, - "progress": 0.0, - "message": f"正在蒸发: {vessel}", - } - ) + self.data.update({ + "status": f"蒸发中: {vessel}", + "rotavap_state": "Evaporating", + "current_temp": temp, + "target_temp": temp, + "rotation_speed": stir_speed, + "vacuum_pressure": pressure, + "remaining_time": time, + "progress": 0.0, + "evaporated_volume": 0.0, + "message": f"Evaporating {vessel} at {temp}°C, {pressure} bar, {stir_speed} RPM" + }) - # 模拟蒸发过程 - simulation_time = min(time / 60.0, 10.0) # 最多模拟10秒 - for progress in range(0, 101, 10): - await asyncio.sleep(simulation_time / 10) - self.data["progress"] = progress - self.data["evaporated_volume"] = progress * 0.5 # 假设最多蒸发50mL + try: + # 蒸发过程 - 实时更新进度 + start_time = time_module.time() + total_time = time + + while True: + current_time = time_module.time() + elapsed = current_time - start_time + remaining = max(0, total_time - elapsed) + progress = min(100.0, (elapsed / total_time) * 100) + + # 模拟蒸发体积 + evaporated_vol = progress * 0.8 # 假设最多蒸发80mL + + # 更新状态 + 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" + }) + + # 时间到了,退出循环 + if remaining <= 0: + break + + # 每秒更新一次 + await asyncio.sleep(1.0) + + # 蒸发完成 + final_evaporated = 80.0 + self.data.update({ + "status": f"蒸发完成: {vessel} | 蒸发量: {final_evaporated:.1f}mL", + "rotavap_state": "Completed", + "evaporated_volume": final_evaporated, + "progress": 100.0, + "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}" + }) - # 蒸发完成 - evaporated_vol = 50.0 # 假设蒸发了50mL - self.data.update( - { - "status": "Idle", - "rotavap_state": "Ready", + self.logger.info(f"Evaporation completed: {final_evaporated}mL evaporated from {vessel}") + return True + + except Exception as e: + # 出错处理 + self.logger.error(f"Error during evaporation: {str(e)}") + + self.data.update({ + "status": f"蒸发错误: {str(e)}", + "rotavap_state": "Error", "current_temp": 25.0, - "target_temp": 25.0, "rotation_speed": 0.0, "vacuum_pressure": 1.0, - "evaporated_volume": evaporated_vol, - "progress": 100.0, - "message": f"蒸发完成: {evaporated_vol}mL", - } - ) + "message": f"Evaporation failed: {str(e)}" + }) + return False - self.logger.info(f"Evaporation completed: {evaporated_vol}mL from {vessel}") - return True - - # 状态属性 + # === 核心状态属性 === @property def status(self) -> str: return self.data.get("status", "Unknown") @@ -139,22 +195,10 @@ class VirtualRotavap: def current_temp(self) -> float: return self.data.get("current_temp", 25.0) - @property - def target_temp(self) -> float: - return self.data.get("target_temp", 25.0) - - @property - def max_temp(self) -> float: - return self.data.get("max_temp", self._max_temp) - @property def rotation_speed(self) -> float: return self.data.get("rotation_speed", 0.0) - @property - def max_rotation_speed(self) -> float: - return self.data.get("max_rotation_speed", self._max_rotation_speed) - @property def vacuum_pressure(self) -> float: return self.data.get("vacuum_pressure", 1.0) @@ -170,3 +214,15 @@ class VirtualRotavap: @property def message(self) -> str: return self.data.get("message", "") + + @property + def max_temp(self) -> float: + return self._max_temp + + @property + def max_rotation_speed(self) -> float: + return self._max_rotation_speed + + @property + def remaining_time(self) -> float: + return self.data.get("remaining_time", 0.0) diff --git a/unilabos/devices/virtual/virtual_solenoid_valve.py b/unilabos/devices/virtual/virtual_solenoid_valve.py index fb3b702..f25cc84 100644 --- a/unilabos/devices/virtual/virtual_solenoid_valve.py +++ b/unilabos/devices/virtual/virtual_solenoid_valve.py @@ -1,4 +1,5 @@ import time +import asyncio from typing import Union @@ -6,16 +7,30 @@ class VirtualSolenoidValve: """ 虚拟电磁阀门 - 简单的开关型阀门,只有开启和关闭两个状态 """ - def __init__(self, port: str = "VIRTUAL", voltage: float = 12.0, response_time: float = 0.1): - self.port = port - self.voltage = voltage - self.response_time = response_time + def __init__(self, device_id: str = None, config: dict = None, **kwargs): + # 从配置中获取参数,提供默认值 + if config is None: + config = {} + + self.device_id = device_id + self.port = config.get("port", "VIRTUAL") + self.voltage = config.get("voltage", 12.0) + self.response_time = config.get("response_time", 0.1) # 状态属性 self._status = "Idle" self._valve_state = "Closed" # "Open" or "Closed" self._is_open = False + async def initialize(self) -> bool: + """初始化设备""" + self._status = "Idle" + return True + + async def cleanup(self) -> bool: + """清理资源""" + return True + @property def status(self) -> str: return self._status @@ -32,55 +47,62 @@ class VirtualSolenoidValve: """获取阀门位置状态""" return "OPEN" if self._is_open else "CLOSED" - def set_valve_position(self, position: Union[str, bool]): + async def set_valve_position(self, command: str = None, **kwargs): """ - 设置阀门位置 + 设置阀门位置 - ROS动作接口 Args: - position: "OPEN"/"CLOSED" 或 True/False + command: "OPEN"/"CLOSED" 或其他控制命令 """ + if command is None: + return {"success": False, "message": "Missing command parameter"} + + print(f"SOLENOID_VALVE: {self.device_id} 接收到命令: {command}") + self._status = "Busy" # 模拟阀门响应时间 - time.sleep(self.response_time) + await asyncio.sleep(self.response_time) - if isinstance(position, str): - target_open = position.upper() == "OPEN" - elif isinstance(position, bool): - target_open = position + # 处理不同的命令格式 + if isinstance(command, str): + cmd_upper = command.upper() + if cmd_upper in ["OPEN", "ON", "TRUE", "1"]: + self._is_open = True + self._valve_state = "Open" + result_msg = f"Valve {self.device_id} opened" + elif cmd_upper in ["CLOSED", "CLOSE", "OFF", "FALSE", "0"]: + self._is_open = False + self._valve_state = "Closed" + result_msg = f"Valve {self.device_id} closed" + else: + # 可能是端口名称,处理路径设置 + # 对于简单电磁阀,任何非关闭命令都视为开启 + self._is_open = True + self._valve_state = "Open" + result_msg = f"Valve {self.device_id} set to position: {command}" else: self._status = "Error" - return "Error: Invalid position" + return {"success": False, "message": "Invalid command type"} - self._is_open = target_open - self._valve_state = "Open" if target_open else "Closed" self._status = "Idle" + print(f"SOLENOID_VALVE: {result_msg}") - return f"Valve {'opened' if target_open else 'closed'}" + return { + "success": True, + "message": result_msg, + "valve_position": self.get_valve_position() + } - def open(self): - """打开电磁阀""" - self._status = "Busy" - time.sleep(self.response_time) - - self._is_open = True - self._valve_state = "Open" - self._status = "Idle" - - return "Valve opened" + async def open(self, **kwargs): + """打开电磁阀 - ROS动作接口""" + return await self.set_valve_position(command="OPEN") - def close(self): - """关闭电磁阀""" - self._status = "Busy" - time.sleep(self.response_time) - - self._is_open = False - self._valve_state = "Closed" - self._status = "Idle" - - return "Valve closed" + async def close(self, **kwargs): + """关闭电磁阀 - ROS动作接口""" + return await self.set_valve_position(command="CLOSED") - def set_state(self, command: Union[bool, str]): + async def set_state(self, command: Union[bool, str], **kwargs): """ 设置阀门状态 - 兼容 SendCmd 类型 @@ -88,18 +110,13 @@ class VirtualSolenoidValve: command: True/False 或 "open"/"close" """ if isinstance(command, bool): - return self.open() if command else self.close() + cmd_str = "OPEN" if command else "CLOSED" elif isinstance(command, str): - if command.lower() in ["open", "on", "true", "1"]: - return self.open() - elif command.lower() in ["close", "closed", "off", "false", "0"]: - return self.close() - else: - self._status = "Error" - return "Error: Invalid command" + cmd_str = command else: - self._status = "Error" - return "Error: Invalid command type" + return {"success": False, "message": "Invalid command type"} + + return await self.set_valve_position(command=cmd_str) def toggle(self): """切换阀门状态""" @@ -115,6 +132,7 @@ class VirtualSolenoidValve: def get_state(self) -> dict: """获取阀门完整状态""" return { + "device_id": self.device_id, "port": self.port, "voltage": self.voltage, "response_time": self.response_time, @@ -124,28 +142,6 @@ class VirtualSolenoidValve: "position": self.get_valve_position() } - def reset(self): + async def reset(self): """重置阀门到关闭状态""" - return self.close() - - def test_cycle(self, cycles: int = 3, delay: float = 1.0): - """ - 测试阀门开关循环 - - Args: - cycles: 循环次数 - delay: 每次开关间隔时间(秒) - """ - results = [] - for i in range(cycles): - # 打开 - result_open = self.open() - results.append(f"Cycle {i+1} - Open: {result_open}") - time.sleep(delay) - - # 关闭 - result_close = self.close() - results.append(f"Cycle {i+1} - Close: {result_close}") - time.sleep(delay) - - return results \ No newline at end of file + return await self.close() \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_stirrer.py b/unilabos/devices/virtual/virtual_stirrer.py index b1a4098..874f997 100644 --- a/unilabos/devices/virtual/virtual_stirrer.py +++ b/unilabos/devices/virtual/virtual_stirrer.py @@ -1,9 +1,10 @@ import asyncio import logging +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): # 处理可能的不同调用方式 @@ -19,86 +20,196 @@ class VirtualStirrer: self.logger = logging.getLogger(f"VirtualStirrer.{self.device_id}") self.data = {} - # 添加调试信息 - print(f"=== VirtualStirrer {self.device_id} is being created! ===") - print(f"=== Config: {self.config} ===") - print(f"=== Kwargs: {kwargs} ===") - # 从config或kwargs中获取配置参数 self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL') - self._max_temp = self.config.get('max_temp') or kwargs.get('max_temp', 100.0) - self._max_speed = self.config.get('max_speed') or kwargs.get('max_speed', 1000.0) + self._max_speed = self.config.get('max_speed') or kwargs.get('max_speed', 1500.0) + self._min_speed = self.config.get('min_speed') or kwargs.get('min_speed', 50.0) - # 处理其他kwargs参数,但跳过已知的配置参数 - skip_keys = {'port', 'max_temp', 'max_speed'} + # 处理其他kwargs参数 + skip_keys = {'port', 'max_speed', 'min_speed'} for key, value in kwargs.items(): if key not in skip_keys and not hasattr(self, key): setattr(self, key, value) async def initialize(self) -> bool: """Initialize virtual stirrer""" - print(f"=== VirtualStirrer {self.device_id} initialize() called! ===") self.logger.info(f"Initializing virtual stirrer {self.device_id}") + + # 初始化状态信息 self.data.update({ - "status": "Idle" + "status": "Idle", + "operation_mode": "Idle", # 操作模式: Idle, Stirring, Settling, Completed, Error + "current_vessel": "", # 当前搅拌的容器 + "current_speed": 0.0, # 当前搅拌速度 + "is_stirring": False, # 是否正在搅拌 + "remaining_time": 0.0, # 剩余时间 }) return True async def cleanup(self) -> bool: """Cleanup virtual stirrer""" self.logger.info(f"Cleaning up virtual stirrer {self.device_id}") + self.data.update({ + "status": "Offline", + "operation_mode": "Offline", + "current_vessel": "", + "current_speed": 0.0, + "is_stirring": False, + "remaining_time": 0.0, + }) return True async def stir(self, stir_time: float, stir_speed: float, settling_time: float) -> bool: - """Execute stir action - matches Stir action exactly""" + """Execute stir action - 定时搅拌 + 沉降""" self.logger.info(f"Stir: speed={stir_speed} RPM, time={stir_time}s, settling={settling_time}s") # 验证参数 - if stir_speed > self._max_speed: - self.logger.error(f"Stir speed {stir_speed} exceeds maximum {self._max_speed}") - self.data["status"] = f"搅拌速度 {stir_speed} 超出范围" + 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) + self.data.update({ + "status": f"Error: {error_msg}", + "operation_mode": "Error" + }) return False - # 开始搅拌 - self.data["status"] = f"搅拌中: {stir_speed} RPM, {stir_time}s" + # === 第一阶段:搅拌 === + start_time = time_module.time() + total_stir_time = stir_time - # 模拟搅拌时间 - simulation_time = min(stir_time, 10.0) # 最多等待10秒用于测试 - await asyncio.sleep(simulation_time) + self.data.update({ + "status": f"搅拌中: {stir_speed} RPM | 剩余: {total_stir_time:.0f}s", + "operation_mode": "Stirring", + "current_speed": stir_speed, + "is_stirring": True, + "remaining_time": total_stir_time, + }) - # 搅拌完成,开始沉降 + # 搅拌过程 - 实时更新剩余时间 + while True: + current_time = time_module.time() + elapsed = current_time - start_time + remaining = max(0, total_stir_time - elapsed) + + # 更新状态 + self.data.update({ + "remaining_time": remaining, + "status": f"搅拌中: {stir_speed} RPM | 剩余: {remaining:.0f}s" + }) + + # 搅拌时间到了 + if remaining <= 0: + break + + await asyncio.sleep(1.0) + + # === 第二阶段:沉降(如果需要)=== if settling_time > 0: - self.data["status"] = f"沉降中: {settling_time}s" - settling_simulation = min(settling_time, 5.0) # 最多等待5秒 - await asyncio.sleep(settling_simulation) + start_settling_time = time_module.time() + total_settling_time = settling_time + + self.data.update({ + "status": f"沉降中: 停止搅拌 | 剩余: {total_settling_time:.0f}s", + "operation_mode": "Settling", + "current_speed": 0.0, + "is_stirring": False, + "remaining_time": total_settling_time, + }) + + # 沉降过程 - 实时更新剩余时间 + while True: + current_time = time_module.time() + elapsed = current_time - start_settling_time + remaining = max(0, total_settling_time - elapsed) + + # 更新状态 + self.data.update({ + "remaining_time": remaining, + "status": f"沉降中: 停止搅拌 | 剩余: {remaining:.0f}s" + }) + + # 沉降时间到了 + if remaining <= 0: + break + + await asyncio.sleep(1.0) - # 操作完成 - self.data["status"] = "搅拌完成" + # === 操作完成 === + 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}", + "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") + self.logger.info(f"Stir completed: {stir_speed} RPM for {stir_time}s + settling {settling_time}s") return True async def start_stir(self, vessel: str, stir_speed: float, purpose: str) -> bool: - """Start stir action - matches StartStir action exactly""" + """Start stir action - 开始持续搅拌""" self.logger.info(f"StartStir: vessel={vessel}, speed={stir_speed} RPM, purpose={purpose}") # 验证参数 - if stir_speed > self._max_speed: - self.logger.error(f"Stir speed {stir_speed} exceeds maximum {self._max_speed}") - self.data["status"] = f"搅拌速度 {stir_speed} 超出范围" + 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) + self.data.update({ + "status": f"Error: {error_msg}", + "operation_mode": "Error" + }) return False - self.data["status"] = f"开始搅拌: {vessel} at {stir_speed} RPM" + self.data.update({ + "status": f"启动: 持续搅拌 {vessel} at {stir_speed} RPM | {purpose}", + "operation_mode": "Stirring", + "current_vessel": vessel, + "current_speed": stir_speed, + "is_stirring": True, + "remaining_time": -1.0, # -1 表示持续运行 + }) + return True async def stop_stir(self, vessel: str) -> bool: - """Stop stir action - matches StopStir action exactly""" + """Stop stir action - 停止搅拌""" self.logger.info(f"StopStir: vessel={vessel}") - self.data["status"] = f"停止搅拌: {vessel}" + current_speed = self.data.get("current_speed", 0.0) + + self.data.update({ + "status": f"已停止: {vessel} 搅拌停止 | 之前速度: {current_speed} RPM", + "operation_mode": "Stopped", + "current_vessel": "", + "current_speed": 0.0, + "is_stirring": False, + "remaining_time": 0.0, + }) + return True - # 状态属性 - 只保留 action 中定义的 feedback + # 状态属性 @property def status(self) -> str: - return self.data.get("status", "Idle") \ No newline at end of file + return self.data.get("status", "Idle") + + @property + def operation_mode(self) -> str: + return self.data.get("operation_mode", "Idle") + + @property + def current_vessel(self) -> str: + return self.data.get("current_vessel", "") + + @property + def current_speed(self) -> float: + return self.data.get("current_speed", 0.0) + + @property + def is_stirring(self) -> bool: + return self.data.get("is_stirring", False) + + @property + def remaining_time(self) -> float: + return self.data.get("remaining_time", 0.0) \ No newline at end of file diff --git a/unilabos/messages/__init__.py b/unilabos/messages/__init__.py index 883b9ad..16947e6 100644 --- a/unilabos/messages/__init__.py +++ b/unilabos/messages/__init__.py @@ -33,19 +33,19 @@ class CleanProtocol(BaseModel): class SeparateProtocol(BaseModel): - 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 # Optional. Volume of solvent to add. - through: str # Optional. Solid chemical to send product phase through on way to to_vessel, e.g. 'celite'. - repeats: int # Optional. Number of separations to perform. - stir_time: float # Optional. Time stir for after adding solvent, before separation of phases. - stir_speed: float # Optional. Speed to stir at after adding solvent, before separation of phases. - settling_time: float # Optional. Time + purpose: str + product_phase: str + from_vessel: str + separation_vessel: str + to_vessel: str + waste_phase_to_vessel: str + solvent: str + solvent_volume: float + through: str + repeats: int + stir_time: float + stir_speed: float + settling_time: float class EvaporateProtocol(BaseModel): @@ -67,6 +67,7 @@ class AGVTransferProtocol(BaseModel): to_repo: dict from_repo_position: str to_repo_position: str + #=============新添加的新的协议================ class AddProtocol(BaseModel): vessel: str @@ -84,16 +85,16 @@ class CentrifugeProtocol(BaseModel): vessel: str speed: float time: float - temp: float # 移除默认值 + temp: float class FilterProtocol(BaseModel): vessel: str - filtrate_vessel: str # 移除默认值 - stir: bool # 移除默认值 - stir_speed: float # 移除默认值 - temp: float # 移除默认值 - continue_heatchill: bool # 移除默认值 - volume: float # 移除默认值 + filtrate_vessel: str + stir: bool + stir_speed: float + temp: float + continue_heatchill: bool + volume: float class HeatChillProtocol(BaseModel): vessel: str @@ -137,45 +138,53 @@ class TransferProtocol(BaseModel): solid: bool = False class CleanVesselProtocol(BaseModel): - vessel: str # 要清洗的容器名称 - solvent: str # 用于清洗容器的溶剂名称 - volume: float # 清洗溶剂的体积,可选参数 - temp: float # 清洗时的温度,可选参数 - repeats: int = 1 # 清洗操作的重复次数,默认为 1 + vessel: str + solvent: str + volume: float + temp: float + repeats: int = 1 class DissolveProtocol(BaseModel): - vessel: str # 装有要溶解物质的容器名称 - solvent: str # 用于溶解物质的溶剂名称 - volume: float # 溶剂的体积,可选参数 - amount: str = "" # 要溶解物质的量,可选参数 - temp: float = 25.0 # 溶解时的温度,可选参数 - time: float = 0.0 # 溶解的时间,可选参数 - stir_speed: float = 0.0 # 搅拌速度,可选参数 + vessel: str + solvent: str + volume: float + amount: str = "" + temp: float = 25.0 + time: float = 0.0 + stir_speed: float = 0.0 class FilterThroughProtocol(BaseModel): - from_vessel: str # 源容器的名称,即物质起始所在的容器 - to_vessel: str # 目标容器的名称,物质过滤后要到达的容器 - filter_through: str # 过滤时所通过的介质,如滤纸、柱子等 - eluting_solvent: str = "" # 洗脱溶剂的名称,可选参数 - eluting_volume: float = 0.0 # 洗脱溶剂的体积,可选参数 - eluting_repeats: int = 0 # 洗脱操作的重复次数,默认为 0 - residence_time: float = 0.0 # 物质在过滤介质中的停留时间,可选参数 + from_vessel: str + to_vessel: str + filter_through: str + eluting_solvent: str = "" + eluting_volume: float = 0.0 + eluting_repeats: int = 0 + residence_time: float = 0.0 class RunColumnProtocol(BaseModel): - from_vessel: str # 源容器的名称,即样品起始所在的容器 - to_vessel: str # 目标容器的名称,分离后的样品要到达的容器 - column: str # 所使用的柱子的名称 + from_vessel: str + to_vessel: str + column: str class WashSolidProtocol(BaseModel): - vessel: str # 装有固体物质的容器名称 - solvent: str # 用于清洗固体的溶剂名称 - volume: float # 清洗溶剂的体积 - filtrate_vessel: str = "" # 滤液要收集到的容器名称,可选参数 - temp: float = 25.0 # 清洗时的温度,可选参数 - stir: bool = False # 是否在清洗过程中搅拌,默认为 False - stir_speed: float = 0.0 # 搅拌速度,可选参数 - time: float = 0.0 # 清洗的时间,可选参数 - repeats: int = 1 # 清洗操作的重复次数,默认为 1 + 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 -__all__ = ["Point3D", "PumpTransferProtocol", "CleanProtocol", "SeparateProtocol", "EvaporateProtocol", "EvacuateAndRefillProtocol", "AGVTransferProtocol", "CentrifugeProtocol", "AddProtocol", "FilterProtocol", "HeatChillProtocol", "HeatChillStartProtocol", "HeatChillStopProtocol", "StirProtocol", "StartStirProtocol", "StopStirProtocol", "TransferProtocol", "CleanVesselProtocol", "DissolveProtocol", "FilterThroughProtocol", "RunColumnProtocol", "WashSolidProtocol"] +__all__ = [ + "Point3D", "PumpTransferProtocol", "CleanProtocol", "SeparateProtocol", + "EvaporateProtocol", "EvacuateAndRefillProtocol", "AGVTransferProtocol", + "CentrifugeProtocol", "AddProtocol", "FilterProtocol", + "HeatChillProtocol", "HeatChillStartProtocol", "HeatChillStopProtocol", + "StirProtocol", "StartStirProtocol", "StopStirProtocol", + "TransferProtocol", "CleanVesselProtocol", "DissolveProtocol", + "FilterThroughProtocol", "RunColumnProtocol", "WashSolidProtocol" +] # End Protocols diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml index d3b911b..aaf840a 100644 --- a/unilabos/registry/devices/virtual_device.yaml +++ b/unilabos/registry/devices/virtual_device.yaml @@ -133,6 +133,11 @@ virtual_stirrer: type: python status_types: status: String + operation_mode: String # 操作模式 + current_vessel: String # 当前容器 + current_speed: Float64 # 当前搅拌速度 + is_stirring: Bool # 是否搅拌 + remaining_time: Float64 # 剩余时间 action_value_mappings: stir: type: Stir @@ -162,28 +167,28 @@ virtual_stirrer: status: status result: success: success - # 虚拟搅拌器节点配置 - 机械连接设备,单一双向连接点 + # 虚拟搅拌器节点配置 - 机械连接设备,双向连接点用于搅拌容器 handles: - handler_key: stirrer label: stirrer data_type: mechanical side: NORTH - io_type: source + io_type: undirected data_source: handle data_key: vessel - description: "搅拌器的机械连接口,直接与反应容器连接提供搅拌功能" + description: "搅拌器的机械连接口,容器通过机械连接进行搅拌" schema: type: object properties: port: type: string default: "VIRTUAL" - max_temp: - type: number - default: 100.0 max_speed: type: number - default: 1000.0 + default: 1500.0 + min_speed: + type: number + default: 50.0 additionalProperties: false virtual_multiway_valve: @@ -308,17 +313,24 @@ virtual_solenoid_valve: valve_state: String # "open" or "closed" is_open: Bool action_value_mappings: + set_valve_position: + type: SendCmd + goal: + command: command # 确保参数名匹配 + feedback: {} + result: + success: success open: type: SendCmd goal: - command: "open" + command: "OPEN" feedback: {} result: success: success close: type: SendCmd goal: - command: "close" + command: "CLOSED" feedback: {} result: success: success @@ -331,22 +343,22 @@ virtual_solenoid_valve: success: success # 电磁阀门节点配置 - 双向流通的开关型阀门,流动方向由泵决定 handles: - - handler_key: in - label: in + - handler_key: inlet + label: inlet data_type: fluid side: NORTH io_type: target data_source: handle data_key: fluid_port_in - description: "电磁阀的双向流体口,开启时允许流体双向通过,关闭时完全阻断" - - handler_key: out - label: out + description: "电磁阀的进液口" + - handler_key: outlet + label: outlet data_type: fluid side: SOUTH io_type: source data_source: handle data_key: fluid_port_out - description: "电磁阀的双向流体口,开启时允许流体双向通过,关闭时完全阻断" + description: "电磁阀的出液口" schema: type: object properties: @@ -377,6 +389,8 @@ virtual_centrifuge: min_temp: Float64 centrifuge_state: String time_remaining: Float64 + progress: Float64 # 添加这个状态 + message: String # 添加这个状态 action_value_mappings: centrifuge: type: Centrifuge @@ -422,23 +436,22 @@ virtual_centrifuge: virtual_filter: description: Virtual Filter for FilterProtocol Testing - icon: Filter.webp + #icon: Filter.webp暂时还没有 class: module: unilabos.devices.virtual.virtual_filter:VirtualFilter type: python status_types: status: String - filter_state: String - current_temp: Float64 - target_temp: Float64 - max_temp: Float64 - stir_speed: Float64 - max_stir_speed: Float64 - filtered_volume: Float64 progress: Float64 + current_temp: Float64 + filtered_volume: Float64 + current_status: String message: String + max_temp: Float64 + max_stir_speed: Float64 + max_volume: Float64 action_value_mappings: - filter_sample: + filter: type: Filter goal: vessel: vessel @@ -452,36 +465,21 @@ virtual_filter: progress: progress current_temp: current_temp filtered_volume: filtered_volume - current_status: status + current_status: current_status result: success: success message: message - # 虚拟过滤器节点配置 - 分离设备,1个输入(原始样品),2个输出(滤液和滤渣) + return_info: message + # 过滤器节点配置 - 固液分离设备 handles: - - handler_key: filterin - label: filterin - data_type: fluid + - handler_key: filter + label: filter + data_type: transport side: NORTH - io_type: target + io_type: source data_source: handle data_key: vessel - description: "需要过滤的原始样品容器" - - handler_key: filtrate_out - label: filtrate_out - data_type: fluid - side: SOUTH - io_type: source - data_source: executor - data_key: filtrate_vessel - description: "过滤后的滤液容器" - - handler_key: filter-residue-out - label: Residue - data_type: resource - side: WEST - io_type: source - data_source: executor - data_key: residue_vessel - description: "过滤后的滤渣(固体残留物)" + description: "需要过滤的样品容器" schema: type: object properties: @@ -494,6 +492,9 @@ virtual_filter: max_stir_speed: type: number default: 1000.0 + max_volume: + type: number + default: 500.0 additionalProperties: false virtual_heatchill: @@ -504,6 +505,10 @@ virtual_heatchill: type: python status_types: status: String + operation_mode: String # 保留:操作模式 + is_stirring: Bool # 保留:是否搅拌 + stir_speed: Float64 # 保留:搅拌速度 + # remaining_time: Float64 # 保留:剩余时间 action_value_mappings: heat_chill: type: HeatChill @@ -536,7 +541,7 @@ virtual_heatchill: status: status result: success: success - # 虚拟加热/冷却器节点配置 - 温控设备,单一双向连接点用于放置容器 + # 虚拟加热/冷却器节点配置 handles: - handler_key: heatchill label: heatchill @@ -709,14 +714,14 @@ virtual_rotavap: status: String rotavap_state: String current_temp: Float64 - target_temp: Float64 - max_temp: Float64 rotation_speed: Float64 - max_rotation_speed: Float64 vacuum_pressure: Float64 evaporated_volume: Float64 progress: Float64 + remaining_time: Float64 message: String + max_temp: Float64 + max_rotation_speed: Float64 action_value_mappings: evaporate: type: Evaporate @@ -730,11 +735,11 @@ virtual_rotavap: progress: progress current_temp: current_temp evaporated_volume: evaporated_volume - current_status: status + status: status result: success: success message: message - # 虚拟旋转蒸发仪节点配置 - 1个双向口(样品进出),1个单向输出口(冷凝溶剂) + # 虚拟旋转蒸发仪节点配置 - 1个样品口 handles: - handler_key: rotavap-sample label: rotavap-sample @@ -743,15 +748,7 @@ virtual_rotavap: io_type: target data_source: handle data_key: vessel - description: "样品的双向连接口,可放入需要蒸发的样品,蒸发完成后取出浓缩物" - - handler_key: rotavap-distillate-outlet - label: Distillate Outlet - data_type: fluid - side: WEST - io_type: source - data_source: executor - data_key: distillate_vessel - description: "冷凝回收的溶剂单向输出口,连接收集瓶" + description: "样品连接口,放入需要蒸发的样品" schema: type: object properties: