Add Virtual Device, Action, YAML, Protocol for Organic Syn

This commit is contained in:
KCFeng425
2025-06-12 12:43:50 +08:00
parent fe492bbca9
commit f2b3e0743e
46 changed files with 6839 additions and 109 deletions

View File

@@ -0,0 +1,250 @@
{
"nodes": [
{
"id": "AddTestStation",
"name": "添加试剂测试工作站",
"children": [
"pump_add",
"flask_1",
"flask_2",
"flask_3",
"flask_4",
"reactor",
"stirrer",
"flask_air"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"protocol_type": ["AddProtocol", "PumpTransferProtocol", "CleanProtocol"]
},
"data": {}
},
{
"id": "pump_add",
"name": "pump_add",
"children": [],
"parent": "AddTestStation",
"type": "device",
"class": "virtual_pump",
"position": {
"x": 520.6111111111111,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_volume": 25.0
},
"data": {
"status": "Idle"
}
},
{
"id": "stirrer",
"name": "stirrer",
"children": [],
"parent": "AddTestStation",
"type": "device",
"class": "virtual_stirrer",
"position": {
"x": 698.1111111111111,
"y": 478,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_temp": 100.0,
"max_speed": 1000.0
},
"data": {
"status": "Idle"
}
},
{
"id": "flask_1",
"name": "通用试剂瓶1",
"children": [],
"parent": "AddTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_2",
"name": "通用试剂瓶2",
"children": [],
"parent": "AddTestStation",
"type": "container",
"class": null,
"position": {
"x": 250,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_3",
"name": "通用试剂瓶3",
"children": [],
"parent": "AddTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_4",
"name": "通用试剂瓶4",
"children": [],
"parent": "AddTestStation",
"type": "container",
"class": null,
"position": {
"x": 550,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "reactor",
"name": "reactor",
"children": [],
"parent": "AddTestStation",
"type": "container",
"class": null,
"position": {
"x": 698.1111111111111,
"y": 428,
"z": 0
},
"config": {
"max_volume": 5000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_air",
"name": "flask_air",
"children": [],
"parent": "AddTestStation",
"type": "container",
"class": null,
"position": {
"x": 800,
"y": 300,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"source": "stirrer",
"target": "reactor",
"type": "physical",
"port": {
"stirrer": "top",
"reactor": "bottom"
}
},
{
"source": "pump_add",
"target": "flask_1",
"type": "physical",
"port": {
"pump_add": "outlet",
"flask_1": "top"
}
},
{
"source": "pump_add",
"target": "flask_2",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_2": "top"
}
},
{
"source": "pump_add",
"target": "flask_3",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_3": "top"
}
},
{
"source": "pump_add",
"target": "flask_4",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_4": "top"
}
},
{
"source": "pump_add",
"target": "reactor",
"type": "physical",
"port": {
"pump_add": "outlet",
"reactor": "top"
}
},
{
"source": "pump_add",
"target": "flask_air",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_air": "top"
}
}
]
}

View File

@@ -0,0 +1,271 @@
{
"nodes": [
{
"id": "CentrifugeTestStation",
"name": "离心机测试工作站",
"children": [
"pump_add",
"flask_1",
"flask_2",
"flask_3",
"reactor",
"stirrer",
"centrifuge_1",
"flask_air"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"protocol_type": ["AddProtocol", "PumpTransferProtocol", "CleanProtocol", "CentrifugeProtocol"]
},
"data": {}
},
{
"id": "pump_add",
"name": "pump_add",
"children": [],
"parent": "CentrifugeTestStation",
"type": "device",
"class": "virtual_pump",
"position": {
"x": 520.6111111111111,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_volume": 25.0
},
"data": {
"status": "Idle"
}
},
{
"id": "stirrer",
"name": "stirrer",
"children": [],
"parent": "CentrifugeTestStation",
"type": "device",
"class": "virtual_stirrer",
"position": {
"x": 650.1111111111111,
"y": 478,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_temp": 100.0,
"max_speed": 1000.0
},
"data": {
"status": "Idle"
}
},
{
"id": "centrifuge_1",
"name": "离心机",
"children": [],
"parent": "CentrifugeTestStation",
"type": "device",
"class": "virtual_centrifuge",
"position": {
"x": 800,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_speed": 15000.0,
"max_temp": 40.0,
"min_temp": 4.0
},
"data": {
"status": "Idle"
}
},
{
"id": "flask_1",
"name": "样品瓶1",
"children": [],
"parent": "CentrifugeTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1500.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_2",
"name": "样品瓶2",
"children": [],
"parent": "CentrifugeTestStation",
"type": "container",
"class": null,
"position": {
"x": 250,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1500.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_3",
"name": "缓冲液瓶",
"children": [],
"parent": "CentrifugeTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "reactor",
"name": "反应器",
"children": [],
"parent": "CentrifugeTestStation",
"type": "container",
"class": null,
"position": {
"x": 698.1111111111111,
"y": 428,
"z": 0
},
"config": {
"max_volume": 5000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_air",
"name": "空气瓶",
"children": [],
"parent": "CentrifugeTestStation",
"type": "container",
"class": null,
"position": {
"x": 950,
"y": 300,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"source": "stirrer",
"target": "reactor",
"type": "physical",
"port": {
"stirrer": "top",
"reactor": "bottom"
}
},
{
"source": "pump_add",
"target": "flask_1",
"type": "physical",
"port": {
"pump_add": "outlet",
"flask_1": "top"
}
},
{
"source": "pump_add",
"target": "flask_2",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_2": "top"
}
},
{
"source": "pump_add",
"target": "flask_3",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_3": "top"
}
},
{
"source": "pump_add",
"target": "reactor",
"type": "physical",
"port": {
"pump_add": "outlet",
"reactor": "top"
}
},
{
"source": "pump_add",
"target": "flask_air",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_air": "top"
}
},
{
"source": "centrifuge_1",
"target": "reactor",
"type": "logical",
"port": {
"centrifuge_1": "chamber",
"reactor": "vessel"
}
},
{
"source": "centrifuge_1",
"target": "flask_1",
"type": "logical",
"port": {
"centrifuge_1": "chamber",
"flask_1": "vessel"
}
},
{
"source": "centrifuge_1",
"target": "flask_2",
"type": "logical",
"port": {
"centrifuge_1": "chamber",
"flask_2": "vessel"
}
}
]
}

View File

@@ -0,0 +1,362 @@
{
"nodes": [
{
"id": "CleanVesselTestStation",
"name": "容器清洗测试工作站",
"children": [
"transfer_pump_cleaner",
"heatchill_1",
"flask_water",
"flask_ethanol",
"flask_acetone",
"flask_waste",
"reactor",
"flask_buffer",
"flask_sample",
"flask_air"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"protocol_type": ["CleanVesselProtocol", "TransferProtocol", "AddProtocol"]
},
"data": {}
},
{
"id": "transfer_pump_cleaner",
"name": "清洗转移泵",
"children": [],
"parent": "CleanVesselTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 520.6111111111111,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_volume": 50.0,
"transfer_rate": 10.0
},
"data": {
"status": "Idle",
"current_volume": 0.0,
"max_volume": 50.0,
"transfer_rate": 10.0,
"from_vessel": "",
"to_vessel": "",
"progress": 0.0,
"transferred_volume": 0.0,
"current_status": "Ready"
}
},
{
"id": "heatchill_1",
"name": "加热冷却器",
"children": [],
"parent": "CleanVesselTestStation",
"type": "device",
"class": "virtual_heatchill",
"position": {
"x": 650.1111111111111,
"y": 478,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_temp": 150.0,
"min_temp": -20.0
},
"data": {
"status": "Idle",
"current_temp": 25.0,
"target_temp": 25.0,
"vessel": "",
"purpose": "",
"progress": 0.0,
"current_status": "Ready"
}
},
{
"id": "flask_water",
"name": "水溶剂瓶",
"children": [],
"parent": "CleanVesselTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "water",
"volume": 1500.0,
"concentration": 100.0
}
]
}
},
{
"id": "flask_ethanol",
"name": "乙醇溶剂瓶",
"children": [],
"parent": "CleanVesselTestStation",
"type": "container",
"class": null,
"position": {
"x": 250,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "ethanol",
"volume": 1500.0,
"concentration": 99.5
}
]
}
},
{
"id": "flask_acetone",
"name": "丙酮溶剂瓶",
"children": [],
"parent": "CleanVesselTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "acetone",
"volume": 1800.0,
"concentration": 99.9
}
]
}
},
{
"id": "flask_waste",
"name": "废液瓶",
"children": [],
"parent": "CleanVesselTestStation",
"type": "container",
"class": null,
"position": {
"x": 550,
"y": 428,
"z": 0
},
"config": {
"max_volume": 5000.0
},
"data": {
"liquid": []
}
},
{
"id": "reactor",
"name": "反应器",
"children": [],
"parent": "CleanVesselTestStation",
"type": "container",
"class": null,
"position": {
"x": 698.1111111111111,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "residue",
"volume": 50.0,
"concentration": 100.0
}
]
}
},
{
"id": "flask_buffer",
"name": "缓冲液瓶",
"children": [],
"parent": "CleanVesselTestStation",
"type": "container",
"class": null,
"position": {
"x": 850,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "buffer",
"volume": 1000.0,
"concentration": 10.0
}
]
}
},
{
"id": "flask_sample",
"name": "样品瓶",
"children": [],
"parent": "CleanVesselTestStation",
"type": "container",
"class": null,
"position": {
"x": 1000,
"y": 428,
"z": 0
},
"config": {
"max_volume": 500.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_air",
"name": "空气瓶",
"children": [],
"parent": "CleanVesselTestStation",
"type": "container",
"class": null,
"position": {
"x": 950,
"y": 300,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"source": "transfer_pump_cleaner",
"target": "flask_water",
"type": "physical",
"port": {
"transfer_pump_cleaner": "1",
"flask_water": "top"
}
},
{
"source": "transfer_pump_cleaner",
"target": "flask_ethanol",
"type": "physical",
"port": {
"transfer_pump_cleaner": "2",
"flask_ethanol": "top"
}
},
{
"source": "transfer_pump_cleaner",
"target": "flask_acetone",
"type": "physical",
"port": {
"transfer_pump_cleaner": "3",
"flask_acetone": "top"
}
},
{
"source": "transfer_pump_cleaner",
"target": "flask_waste",
"type": "physical",
"port": {
"transfer_pump_cleaner": "4",
"flask_waste": "top"
}
},
{
"source": "transfer_pump_cleaner",
"target": "reactor",
"type": "physical",
"port": {
"transfer_pump_cleaner": "5",
"reactor": "top"
}
},
{
"source": "transfer_pump_cleaner",
"target": "flask_buffer",
"type": "physical",
"port": {
"transfer_pump_cleaner": "6",
"flask_buffer": "top"
}
},
{
"source": "transfer_pump_cleaner",
"target": "flask_sample",
"type": "physical",
"port": {
"transfer_pump_cleaner": "7",
"flask_sample": "top"
}
},
{
"source": "transfer_pump_cleaner",
"target": "flask_air",
"type": "physical",
"port": {
"transfer_pump_cleaner": "8",
"flask_air": "top"
}
},
{
"source": "heatchill_1",
"target": "reactor",
"type": "physical",
"port": {
"heatchill_1": "heating_element",
"reactor": "bottom"
}
},
{
"source": "heatchill_1",
"target": "flask_sample",
"type": "physical",
"port": {
"heatchill_1": "heating_element",
"flask_sample": "bottom"
}
}
]
}

View File

@@ -0,0 +1,343 @@
{
"nodes": [
{
"id": "DissolveTestStation",
"name": "溶解测试工作站",
"children": [
"transfer_pump_1",
"heatchill_1",
"stirrer_1",
"flask_water",
"flask_ethanol",
"flask_dmso",
"reactor",
"flask_sample",
"flask_buffer"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"protocol_type": ["DissolveProtocol", "TransferProtocol", "HeatChillProtocol", "StirProtocol"]
},
"data": {}
},
{
"id": "transfer_pump_1",
"name": "转移泵",
"children": [],
"parent": "DissolveTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 520.6111111111111,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_volume": 50.0,
"transfer_rate": 10.0
},
"data": {
"status": "Idle",
"current_volume": 0.0,
"max_volume": 50.0,
"transfer_rate": 10.0,
"from_vessel": "",
"to_vessel": "",
"progress": 0.0,
"transferred_volume": 0.0,
"current_status": "Ready"
}
},
{
"id": "heatchill_1",
"name": "加热冷却器",
"children": [],
"parent": "DissolveTestStation",
"type": "device",
"class": "virtual_heatchill",
"position": {
"x": 650.1111111111111,
"y": 478,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_temp": 150.0,
"min_temp": -20.0
},
"data": {
"status": "Idle",
"current_temp": 25.0,
"target_temp": 25.0,
"vessel": "",
"purpose": "",
"progress": 0.0,
"current_status": "Ready"
}
},
{
"id": "stirrer_1",
"name": "搅拌器",
"children": [],
"parent": "DissolveTestStation",
"type": "device",
"class": "virtual_stirrer",
"position": {
"x": 750.1111111111111,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_speed": 1000.0
},
"data": {
"status": "Idle"
}
},
{
"id": "flask_water",
"name": "水溶剂瓶",
"children": [],
"parent": "DissolveTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "water",
"volume": 1500.0,
"concentration": 100.0
}
]
}
},
{
"id": "flask_ethanol",
"name": "乙醇溶剂瓶",
"children": [],
"parent": "DissolveTestStation",
"type": "container",
"class": null,
"position": {
"x": 250,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "ethanol",
"volume": 1500.0,
"concentration": 99.5
}
]
}
},
{
"id": "flask_dmso",
"name": "DMSO溶剂瓶",
"children": [],
"parent": "DissolveTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "dmso",
"volume": 800.0,
"concentration": 99.9
}
]
}
},
{
"id": "reactor",
"name": "反应器",
"children": [],
"parent": "DissolveTestStation",
"type": "container",
"class": null,
"position": {
"x": 698.1111111111111,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "solid_sample",
"volume": 10.0,
"concentration": 100.0
}
]
}
},
{
"id": "flask_sample",
"name": "样品瓶",
"children": [],
"parent": "DissolveTestStation",
"type": "container",
"class": null,
"position": {
"x": 1000,
"y": 428,
"z": 0
},
"config": {
"max_volume": 500.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_buffer",
"name": "缓冲液瓶",
"children": [],
"parent": "DissolveTestStation",
"type": "container",
"class": null,
"position": {
"x": 850,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "buffer",
"volume": 1000.0,
"concentration": 10.0
}
]
}
}
],
"links": [
{
"source": "transfer_pump_1",
"target": "flask_water",
"type": "physical",
"port": {
"transfer_pump_1": "1",
"flask_water": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_ethanol",
"type": "physical",
"port": {
"transfer_pump_1": "2",
"flask_ethanol": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_dmso",
"type": "physical",
"port": {
"transfer_pump_1": "3",
"flask_dmso": "top"
}
},
{
"source": "transfer_pump_1",
"target": "reactor",
"type": "physical",
"port": {
"transfer_pump_1": "4",
"reactor": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_sample",
"type": "physical",
"port": {
"transfer_pump_1": "5",
"flask_sample": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_buffer",
"type": "physical",
"port": {
"transfer_pump_1": "6",
"flask_buffer": "top"
}
},
{
"source": "heatchill_1",
"target": "reactor",
"type": "physical",
"port": {
"heatchill_1": "heating_element",
"reactor": "bottom"
}
},
{
"source": "heatchill_1",
"target": "flask_sample",
"type": "physical",
"port": {
"heatchill_1": "heating_element",
"flask_sample": "bottom"
}
},
{
"source": "stirrer_1",
"target": "reactor",
"type": "physical",
"port": {
"stirrer_1": "stir_rod",
"reactor": "center"
}
},
{
"source": "stirrer_1",
"target": "flask_sample",
"type": "physical",
"port": {
"stirrer_1": "stir_rod",
"flask_sample": "center"
}
}
]
}

View File

@@ -0,0 +1,270 @@
{
"nodes": [
{
"id": "FilterTestStation",
"name": "过滤器测试工作站",
"children": [
"pump_add",
"flask_sample",
"flask_filtrate",
"flask_buffer",
"reactor",
"stirrer",
"filter_1",
"flask_air"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"protocol_type": ["AddProtocol", "PumpTransferProtocol", "CleanProtocol", "FilterProtocol"]
},
"data": {}
},
{
"id": "pump_add",
"name": "pump_add",
"children": [],
"parent": "FilterTestStation",
"type": "device",
"class": "virtual_pump",
"position": {
"x": 520.6111111111111,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_volume": 25.0
},
"data": {
"status": "Idle"
}
},
{
"id": "stirrer",
"name": "stirrer",
"children": [],
"parent": "FilterTestStation",
"type": "device",
"class": "virtual_stirrer",
"position": {
"x": 650.1111111111111,
"y": 478,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_temp": 100.0,
"max_speed": 1000.0
},
"data": {
"status": "Idle"
}
},
{
"id": "filter_1",
"name": "过滤器",
"children": [],
"parent": "FilterTestStation",
"type": "device",
"class": "virtual_filter",
"position": {
"x": 800,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_temp": 100.0,
"max_stir_speed": 1000.0
},
"data": {
"status": "Idle"
}
},
{
"id": "flask_sample",
"name": "样品瓶",
"children": [],
"parent": "FilterTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_filtrate",
"name": "滤液瓶",
"children": [],
"parent": "FilterTestStation",
"type": "container",
"class": null,
"position": {
"x": 250,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_buffer",
"name": "缓冲液瓶",
"children": [],
"parent": "FilterTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "reactor",
"name": "反应器",
"children": [],
"parent": "FilterTestStation",
"type": "container",
"class": null,
"position": {
"x": 698.1111111111111,
"y": 428,
"z": 0
},
"config": {
"max_volume": 5000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_air",
"name": "空气瓶",
"children": [],
"parent": "FilterTestStation",
"type": "container",
"class": null,
"position": {
"x": 950,
"y": 300,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"source": "stirrer",
"target": "reactor",
"type": "physical",
"port": {
"stirrer": "top",
"reactor": "bottom"
}
},
{
"source": "pump_add",
"target": "flask_sample",
"type": "physical",
"port": {
"pump_add": "outlet",
"flask_sample": "top"
}
},
{
"source": "pump_add",
"target": "flask_filtrate",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_filtrate": "top"
}
},
{
"source": "pump_add",
"target": "flask_buffer",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_buffer": "top"
}
},
{
"source": "pump_add",
"target": "reactor",
"type": "physical",
"port": {
"pump_add": "outlet",
"reactor": "top"
}
},
{
"source": "pump_add",
"target": "flask_air",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_air": "top"
}
},
{
"source": "filter_1",
"target": "reactor",
"type": "logical",
"port": {
"filter_1": "input",
"reactor": "vessel"
}
},
{
"source": "filter_1",
"target": "flask_sample",
"type": "logical",
"port": {
"filter_1": "input",
"flask_sample": "vessel"
}
},
{
"source": "filter_1",
"target": "flask_filtrate",
"type": "logical",
"port": {
"filter_1": "output",
"flask_filtrate": "vessel"
}
}
]
}

View File

@@ -0,0 +1,388 @@
{
"nodes": [
{
"id": "FilterThroughTestStation",
"name": "过滤通过测试工作站",
"children": [
"transfer_pump_1",
"filter_1",
"flask_ethanol",
"flask_water",
"flask_methanol",
"reactor",
"collection_flask",
"waste_flask",
"flask_sample",
"flask_celite",
"flask_silica"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"protocol_type": ["FilterThroughProtocol", "TransferProtocol", "FilterProtocol"]
},
"data": {}
},
{
"id": "transfer_pump_1",
"name": "转移泵",
"children": [],
"parent": "FilterThroughTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 520.6111111111111,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_volume": 50.0,
"transfer_rate": 10.0
},
"data": {
"status": "Idle",
"current_volume": 0.0,
"max_volume": 50.0,
"transfer_rate": 10.0,
"from_vessel": "",
"to_vessel": "",
"progress": 0.0,
"transferred_volume": 0.0,
"current_status": "Ready"
}
},
{
"id": "filter_1",
"name": "过滤器",
"children": [],
"parent": "FilterThroughTestStation",
"type": "device",
"class": "virtual_filter",
"position": {
"x": 650.1111111111111,
"y": 478,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_temp": 100.0,
"max_stir_speed": 1000.0
},
"data": {
"status": "Idle",
"filter_state": "Ready",
"current_temp": 25.0,
"target_temp": 25.0,
"max_temp": 100.0,
"stir_speed": 0.0,
"max_stir_speed": 1000.0,
"filtered_volume": 0.0,
"progress": 0.0,
"message": ""
}
},
{
"id": "flask_ethanol",
"name": "乙醇溶剂瓶",
"children": [],
"parent": "FilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "ethanol",
"volume": 1500.0,
"concentration": 99.5
}
]
}
},
{
"id": "flask_water",
"name": "水溶剂瓶",
"children": [],
"parent": "FilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 250,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "water",
"volume": 1800.0,
"concentration": 100.0
}
]
}
},
{
"id": "flask_methanol",
"name": "甲醇溶剂瓶",
"children": [],
"parent": "FilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "methanol",
"volume": 800.0,
"concentration": 99.9
}
]
}
},
{
"id": "reactor",
"name": "反应器",
"children": [],
"parent": "FilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 698.1111111111111,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "crude_product",
"volume": 200.0,
"concentration": 80.0
}
]
}
},
{
"id": "collection_flask",
"name": "收集瓶",
"children": [],
"parent": "FilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 850,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "waste_flask",
"name": "废液瓶",
"children": [],
"parent": "FilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 1000,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_sample",
"name": "样品瓶",
"children": [],
"parent": "FilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 550,
"y": 300,
"z": 0
},
"config": {
"max_volume": 500.0
},
"data": {
"liquid": [
{
"name": "sample_mixture",
"volume": 100.0,
"concentration": 50.0
}
]
}
},
{
"id": "flask_celite",
"name": "硅藻土容器",
"children": [],
"parent": "FilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 150,
"y": 300,
"z": 0
},
"config": {
"max_volume": 500.0
},
"data": {
"liquid": [
{
"name": "celite",
"volume": 50.0,
"concentration": 100.0
}
]
}
},
{
"id": "flask_silica",
"name": "硅胶容器",
"children": [],
"parent": "FilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 300,
"y": 300,
"z": 0
},
"config": {
"max_volume": 500.0
},
"data": {
"liquid": [
{
"name": "silica",
"volume": 30.0,
"concentration": 100.0
}
]
}
}
],
"links": [
{
"source": "transfer_pump_1",
"target": "flask_ethanol",
"type": "physical",
"port": {
"transfer_pump_1": "1",
"flask_ethanol": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_water",
"type": "physical",
"port": {
"transfer_pump_1": "2",
"flask_water": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_methanol",
"type": "physical",
"port": {
"transfer_pump_1": "3",
"flask_methanol": "top"
}
},
{
"source": "transfer_pump_1",
"target": "reactor",
"type": "physical",
"port": {
"transfer_pump_1": "4",
"reactor": "top"
}
},
{
"source": "transfer_pump_1",
"target": "collection_flask",
"type": "physical",
"port": {
"transfer_pump_1": "5",
"collection_flask": "top"
}
},
{
"source": "transfer_pump_1",
"target": "waste_flask",
"type": "physical",
"port": {
"transfer_pump_1": "6",
"waste_flask": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_sample",
"type": "physical",
"port": {
"transfer_pump_1": "7",
"flask_sample": "top"
}
},
{
"source": "filter_1",
"target": "collection_flask",
"type": "physical",
"port": {
"filter_1": "filter_element",
"collection_flask": "top"
}
},
{
"source": "filter_1",
"target": "reactor",
"type": "physical",
"port": {
"filter_1": "filter_element",
"reactor": "top"
}
}
]
}

View File

@@ -0,0 +1,262 @@
{
"nodes": [
{
"id": "HeatChillTestStation",
"name": "加热冷却测试工作站",
"children": [
"pump_add",
"flask_sample",
"flask_buffer1",
"flask_buffer2",
"reactor",
"stirrer",
"heatchill_1",
"flask_air"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"protocol_type": ["AddProtocol", "PumpTransferProtocol", "CleanProtocol", "HeatChillProtocol", "HeatChillStartProtocol", "HeatChillStopProtocol"]
},
"data": {}
},
{
"id": "pump_add",
"name": "pump_add",
"children": [],
"parent": "HeatChillTestStation",
"type": "device",
"class": "virtual_pump",
"position": {
"x": 520.6111111111111,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_volume": 25.0
},
"data": {
"status": "Idle"
}
},
{
"id": "stirrer",
"name": "stirrer",
"children": [],
"parent": "HeatChillTestStation",
"type": "device",
"class": "virtual_stirrer",
"position": {
"x": 650.1111111111111,
"y": 478,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_temp": 100.0,
"max_speed": 1000.0
},
"data": {
"status": "Idle"
}
},
{
"id": "heatchill_1",
"name": "加热冷却器",
"children": [],
"parent": "HeatChillTestStation",
"type": "device",
"class": "virtual_heatchill",
"position": {
"x": 800,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_temp": 200.0,
"min_temp": -80.0,
"max_stir_speed": 1000.0
},
"data": {
"status": "Idle"
}
},
{
"id": "flask_sample",
"name": "样品瓶",
"children": [],
"parent": "HeatChillTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_buffer1",
"name": "缓冲液瓶1",
"children": [],
"parent": "HeatChillTestStation",
"type": "container",
"class": null,
"position": {
"x": 250,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_buffer2",
"name": "缓冲液瓶2",
"children": [],
"parent": "HeatChillTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "reactor",
"name": "反应器",
"children": [],
"parent": "HeatChillTestStation",
"type": "container",
"class": null,
"position": {
"x": 698.1111111111111,
"y": 428,
"z": 0
},
"config": {
"max_volume": 5000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_air",
"name": "空气瓶",
"children": [],
"parent": "HeatChillTestStation",
"type": "container",
"class": null,
"position": {
"x": 950,
"y": 300,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"source": "stirrer",
"target": "reactor",
"type": "physical",
"port": {
"stirrer": "top",
"reactor": "bottom"
}
},
{
"source": "pump_add",
"target": "flask_sample",
"type": "physical",
"port": {
"pump_add": "outlet",
"flask_sample": "top"
}
},
{
"source": "pump_add",
"target": "flask_buffer1",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_buffer1": "top"
}
},
{
"source": "pump_add",
"target": "flask_buffer2",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_buffer2": "top"
}
},
{
"source": "pump_add",
"target": "reactor",
"type": "physical",
"port": {
"pump_add": "outlet",
"reactor": "top"
}
},
{
"source": "pump_add",
"target": "flask_air",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_air": "top"
}
},
{
"source": "heatchill_1",
"target": "reactor",
"type": "logical",
"port": {
"heatchill_1": "heating_element",
"reactor": "vessel"
}
},
{
"source": "heatchill_1",
"target": "flask_sample",
"type": "logical",
"port": {
"heatchill_1": "heating_element",
"flask_sample": "vessel"
}
}
]
}

View File

@@ -0,0 +1,412 @@
{
"nodes": [
{
"id": "RunColumnTestStation",
"name": "柱层析测试工作站",
"children": [
"transfer_pump_1",
"column_1",
"flask_sample",
"flask_hexane",
"flask_ethyl_acetate",
"flask_methanol",
"collection_flask_1",
"collection_flask_2",
"collection_flask_3",
"waste_flask",
"reactor"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"protocol_type": ["RunColumnProtocol", "TransferProtocol"]
},
"data": {}
},
{
"id": "transfer_pump_1",
"name": "转移泵",
"children": [],
"parent": "RunColumnTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 520.6111111111111,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_volume": 50.0,
"transfer_rate": 10.0
},
"data": {
"status": "Idle",
"current_volume": 0.0,
"max_volume": 50.0,
"transfer_rate": 10.0,
"from_vessel": "",
"to_vessel": "",
"progress": 0.0,
"transferred_volume": 0.0,
"current_status": "Ready"
}
},
{
"id": "column_1",
"name": "柱层析设备",
"children": [],
"parent": "RunColumnTestStation",
"type": "device",
"class": "virtual_column",
"position": {
"x": 650.1111111111111,
"y": 478,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_flow_rate": 5.0,
"column_length": 30.0,
"column_diameter": 2.5
},
"data": {
"status": "Idle",
"column_state": "Ready",
"current_flow_rate": 0.0,
"max_flow_rate": 5.0,
"column_length": 30.0,
"column_diameter": 2.5,
"processed_volume": 0.0,
"progress": 0.0,
"current_status": "Ready"
}
},
{
"id": "flask_sample",
"name": "样品瓶",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 428,
"z": 0
},
"config": {
"max_volume": 500.0
},
"data": {
"liquid": [
{
"name": "crude_mixture",
"volume": 200.0,
"concentration": 70.0
}
]
}
},
{
"id": "flask_hexane",
"name": "正己烷洗脱剂",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 250,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "hexane",
"volume": 1500.0,
"concentration": 99.8
}
]
}
},
{
"id": "flask_ethyl_acetate",
"name": "乙酸乙酯洗脱剂",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "ethyl_acetate",
"volume": 1500.0,
"concentration": 99.5
}
]
}
},
{
"id": "flask_methanol",
"name": "甲醇洗脱剂",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 550,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "methanol",
"volume": 800.0,
"concentration": 99.9
}
]
}
},
{
"id": "collection_flask_1",
"name": "收集瓶1",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 750,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "collection_flask_2",
"name": "收集瓶2",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 900,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "collection_flask_3",
"name": "收集瓶3",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 1050,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "waste_flask",
"name": "废液瓶",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 1200,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "reactor",
"name": "反应器",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 698.1111111111111,
"y": 300,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "reaction_mixture",
"volume": 300.0,
"concentration": 85.0
}
]
}
}
],
"links": [
{
"source": "transfer_pump_1",
"target": "flask_sample",
"type": "physical",
"port": {
"transfer_pump_1": "1",
"flask_sample": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_hexane",
"type": "physical",
"port": {
"transfer_pump_1": "2",
"flask_hexane": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_ethyl_acetate",
"type": "physical",
"port": {
"transfer_pump_1": "3",
"flask_ethyl_acetate": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_methanol",
"type": "physical",
"port": {
"transfer_pump_1": "4",
"flask_methanol": "top"
}
},
{
"source": "transfer_pump_1",
"target": "column_1",
"type": "physical",
"port": {
"transfer_pump_1": "5",
"column_1": "inlet"
}
},
{
"source": "transfer_pump_1",
"target": "collection_flask_1",
"type": "physical",
"port": {
"transfer_pump_1": "6",
"collection_flask_1": "top"
}
},
{
"source": "transfer_pump_1",
"target": "collection_flask_2",
"type": "physical",
"port": {
"transfer_pump_1": "7",
"collection_flask_2": "top"
}
},
{
"source": "transfer_pump_1",
"target": "collection_flask_3",
"type": "physical",
"port": {
"transfer_pump_1": "8",
"collection_flask_3": "top"
}
},
{
"source": "transfer_pump_1",
"target": "waste_flask",
"type": "physical",
"port": {
"transfer_pump_1": "9",
"waste_flask": "top"
}
},
{
"source": "transfer_pump_1",
"target": "reactor",
"type": "physical",
"port": {
"transfer_pump_1": "10",
"reactor": "top"
}
},
{
"source": "column_1",
"target": "collection_flask_1",
"type": "physical",
"port": {
"column_1": "outlet",
"collection_flask_1": "top"
}
},
{
"source": "column_1",
"target": "collection_flask_2",
"type": "physical",
"port": {
"column_1": "outlet",
"collection_flask_2": "top"
}
},
{
"source": "column_1",
"target": "collection_flask_3",
"type": "physical",
"port": {
"column_1": "outlet",
"collection_flask_3": "top"
}
}
]
}

View File

@@ -0,0 +1,250 @@
{
"nodes": [
{
"id": "StirTestStation",
"name": "搅拌测试工作站",
"children": [
"pump_add",
"flask_sample",
"flask_buffer1",
"flask_buffer2",
"reactor",
"stirrer",
"flask_waste",
"flask_air"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"protocol_type": ["AddProtocol", "PumpTransferProtocol", "CleanProtocol", "StirProtocol", "StartStirProtocol", "StopStirProtocol"]
},
"data": {}
},
{
"id": "pump_add",
"name": "添加泵",
"children": [],
"parent": "StirTestStation",
"type": "device",
"class": "virtual_pump",
"position": {
"x": 520.6111111111111,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_volume": 25.0
},
"data": {
"status": "Idle"
}
},
{
"id": "stirrer",
"name": "搅拌器",
"children": [],
"parent": "StirTestStation",
"type": "device",
"class": "virtual_stirrer",
"position": {
"x": 650.1111111111111,
"y": 478,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_temp": 100.0,
"max_speed": 1000.0
},
"data": {
"status": "Idle"
}
},
{
"id": "flask_sample",
"name": "样品瓶",
"children": [],
"parent": "StirTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_buffer1",
"name": "缓冲液瓶1",
"children": [],
"parent": "StirTestStation",
"type": "container",
"class": null,
"position": {
"x": 250,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_buffer2",
"name": "缓冲液瓶2",
"children": [],
"parent": "StirTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "reactor",
"name": "反应器",
"children": [],
"parent": "StirTestStation",
"type": "container",
"class": null,
"position": {
"x": 698.1111111111111,
"y": 428,
"z": 0
},
"config": {
"max_volume": 5000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_waste",
"name": "废液瓶",
"children": [],
"parent": "StirTestStation",
"type": "container",
"class": null,
"position": {
"x": 850,
"y": 428,
"z": 0
},
"config": {
"max_volume": 3000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_air",
"name": "空气瓶",
"children": [],
"parent": "StirTestStation",
"type": "container",
"class": null,
"position": {
"x": 950,
"y": 300,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"source": "stirrer",
"target": "reactor",
"type": "physical",
"port": {
"stirrer": "top",
"reactor": "bottom"
}
},
{
"source": "pump_add",
"target": "flask_sample",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_sample": "top"
}
},
{
"source": "pump_add",
"target": "flask_buffer1",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_buffer1": "top"
}
},
{
"source": "pump_add",
"target": "flask_buffer2",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_buffer2": "top"
}
},
{
"source": "pump_add",
"target": "reactor",
"type": "physical",
"port": {
"pump_add": "outlet",
"reactor": "top"
}
},
{
"source": "pump_add",
"target": "flask_waste",
"type": "physical",
"port": {
"pump_add": "outlet",
"flask_waste": "top"
}
},
{
"source": "pump_add",
"target": "flask_air",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_air": "top"
}
}
]
}

View File

@@ -0,0 +1,249 @@
{
"nodes": [
{
"id": "TransferTestStation",
"name": "液体转移测试工作站",
"children": [
"transfer_pump",
"flask_source1",
"flask_source2",
"flask_target1",
"flask_target2",
"reactor",
"flask_waste",
"flask_rinsing"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"protocol_type": ["TransferProtocol"]
},
"data": {}
},
{
"id": "transfer_pump",
"name": "转移泵",
"children": [],
"parent": "TransferTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 520.6111111111111,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_volume": 50.0,
"transfer_rate": 5.0
},
"data": {
"status": "Idle"
}
},
{
"id": "flask_source1",
"name": "源容器1",
"children": [],
"parent": "TransferTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_source2",
"name": "源容器2",
"children": [],
"parent": "TransferTestStation",
"type": "container",
"class": null,
"position": {
"x": 250,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_target1",
"name": "目标容器1",
"children": [],
"parent": "TransferTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_target2",
"name": "目标容器2",
"children": [],
"parent": "TransferTestStation",
"type": "container",
"class": null,
"position": {
"x": 550,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "reactor",
"name": "反应器",
"children": [],
"parent": "TransferTestStation",
"type": "container",
"class": null,
"position": {
"x": 698.1111111111111,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_waste",
"name": "废液瓶",
"children": [],
"parent": "TransferTestStation",
"type": "container",
"class": null,
"position": {
"x": 850,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_rinsing",
"name": "冲洗液瓶",
"children": [],
"parent": "TransferTestStation",
"type": "container",
"class": null,
"position": {
"x": 950,
"y": 300,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"source": "transfer_pump",
"target": "flask_source1",
"type": "physical",
"port": {
"transfer_pump": "inlet",
"flask_source1": "top"
}
},
{
"source": "transfer_pump",
"target": "flask_source2",
"type": "physical",
"port": {
"transfer_pump": "inlet",
"flask_source2": "top"
}
},
{
"source": "transfer_pump",
"target": "flask_target1",
"type": "physical",
"port": {
"transfer_pump": "outlet",
"flask_target1": "top"
}
},
{
"source": "transfer_pump",
"target": "flask_target2",
"type": "physical",
"port": {
"transfer_pump": "outlet",
"flask_target2": "top"
}
},
{
"source": "transfer_pump",
"target": "reactor",
"type": "physical",
"port": {
"transfer_pump": "outlet",
"reactor": "top"
}
},
{
"source": "transfer_pump",
"target": "flask_waste",
"type": "physical",
"port": {
"transfer_pump": "outlet",
"flask_waste": "top"
}
},
{
"source": "transfer_pump",
"target": "flask_rinsing",
"type": "physical",
"port": {
"transfer_pump": "inlet",
"flask_rinsing": "top"
}
}
]
}

View File

@@ -0,0 +1,494 @@
{
"nodes": [
{
"id": "WashSolidTestStation",
"name": "固体清洗测试工作站",
"children": [
"transfer_pump_1",
"heatchill_1",
"stirrer_1",
"filter_1",
"flask_ethanol",
"flask_water",
"flask_acetone",
"flask_methanol",
"reactor",
"collection_flask",
"waste_flask",
"flask_sample",
"filtrate_flask"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"protocol_type": ["WashSolidProtocol", "TransferProtocol", "FilterProtocol", "HeatChillProtocol", "StirProtocol"]
},
"data": {}
},
{
"id": "transfer_pump_1",
"name": "转移泵",
"children": [],
"parent": "WashSolidTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 520.6111111111111,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_volume": 50.0,
"transfer_rate": 10.0
},
"data": {
"status": "Idle",
"current_volume": 0.0,
"max_volume": 50.0,
"transfer_rate": 10.0,
"from_vessel": "",
"to_vessel": "",
"progress": 0.0,
"transferred_volume": 0.0,
"current_status": "Ready"
}
},
{
"id": "heatchill_1",
"name": "加热冷却器",
"children": [],
"parent": "WashSolidTestStation",
"type": "device",
"class": "virtual_heatchill",
"position": {
"x": 650.1111111111111,
"y": 478,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_temp": 150.0,
"min_temp": -20.0
},
"data": {
"status": "Idle",
"current_temp": 25.0,
"target_temp": 25.0,
"vessel": "",
"purpose": "",
"progress": 0.0,
"current_status": "Ready"
}
},
{
"id": "stirrer_1",
"name": "搅拌器",
"children": [],
"parent": "WashSolidTestStation",
"type": "device",
"class": "virtual_stirrer",
"position": {
"x": 750.1111111111111,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_speed": 1000.0
},
"data": {
"status": "Idle"
}
},
{
"id": "filter_1",
"name": "过滤器",
"children": [],
"parent": "WashSolidTestStation",
"type": "device",
"class": "virtual_filter",
"position": {
"x": 850.1111111111111,
"y": 478,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_temp": 100.0,
"max_stir_speed": 1000.0
},
"data": {
"status": "Idle",
"filter_state": "Ready",
"current_temp": 25.0,
"target_temp": 25.0,
"max_temp": 100.0,
"stir_speed": 0.0,
"max_stir_speed": 1000.0,
"filtered_volume": 0.0,
"progress": 0.0,
"message": ""
}
},
{
"id": "flask_ethanol",
"name": "乙醇清洗剂",
"children": [],
"parent": "WashSolidTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "ethanol",
"volume": 1500.0,
"concentration": 99.5
}
]
}
},
{
"id": "flask_water",
"name": "水清洗剂",
"children": [],
"parent": "WashSolidTestStation",
"type": "container",
"class": null,
"position": {
"x": 250,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "water",
"volume": 1800.0,
"concentration": 100.0
}
]
}
},
{
"id": "flask_acetone",
"name": "丙酮清洗剂",
"children": [],
"parent": "WashSolidTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "acetone",
"volume": 800.0,
"concentration": 99.8
}
]
}
},
{
"id": "flask_methanol",
"name": "甲醇清洗剂",
"children": [],
"parent": "WashSolidTestStation",
"type": "container",
"class": null,
"position": {
"x": 550,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "methanol",
"volume": 800.0,
"concentration": 99.9
}
]
}
},
{
"id": "reactor",
"name": "反应器",
"children": [],
"parent": "WashSolidTestStation",
"type": "container",
"class": null,
"position": {
"x": 698.1111111111111,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "solid_product",
"volume": 50.0,
"concentration": 100.0
}
]
}
},
{
"id": "collection_flask",
"name": "收集瓶",
"children": [],
"parent": "WashSolidTestStation",
"type": "container",
"class": null,
"position": {
"x": 850,
"y": 300,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "waste_flask",
"name": "废液瓶",
"children": [],
"parent": "WashSolidTestStation",
"type": "container",
"class": null,
"position": {
"x": 1000,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_sample",
"name": "样品瓶",
"children": [],
"parent": "WashSolidTestStation",
"type": "container",
"class": null,
"position": {
"x": 1150,
"y": 300,
"z": 0
},
"config": {
"max_volume": 500.0
},
"data": {
"liquid": [
{
"name": "crude_solid",
"volume": 30.0,
"concentration": 80.0
}
]
}
},
{
"id": "filtrate_flask",
"name": "滤液收集瓶",
"children": [],
"parent": "WashSolidTestStation",
"type": "container",
"class": null,
"position": {
"x": 1000,
"y": 300,
"z": 0
},
"config": {
"max_volume": 1500.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"source": "transfer_pump_1",
"target": "flask_ethanol",
"type": "physical",
"port": {
"transfer_pump_1": "1",
"flask_ethanol": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_water",
"type": "physical",
"port": {
"transfer_pump_1": "2",
"flask_water": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_acetone",
"type": "physical",
"port": {
"transfer_pump_1": "3",
"flask_acetone": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_methanol",
"type": "physical",
"port": {
"transfer_pump_1": "4",
"flask_methanol": "top"
}
},
{
"source": "transfer_pump_1",
"target": "reactor",
"type": "physical",
"port": {
"transfer_pump_1": "5",
"reactor": "top"
}
},
{
"source": "transfer_pump_1",
"target": "collection_flask",
"type": "physical",
"port": {
"transfer_pump_1": "6",
"collection_flask": "top"
}
},
{
"source": "transfer_pump_1",
"target": "waste_flask",
"type": "physical",
"port": {
"transfer_pump_1": "7",
"waste_flask": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_sample",
"type": "physical",
"port": {
"transfer_pump_1": "8",
"flask_sample": "top"
}
},
{
"source": "transfer_pump_1",
"target": "filtrate_flask",
"type": "physical",
"port": {
"transfer_pump_1": "9",
"filtrate_flask": "top"
}
},
{
"source": "heatchill_1",
"target": "reactor",
"type": "physical",
"port": {
"heatchill_1": "heating_element",
"reactor": "bottom"
}
},
{
"source": "heatchill_1",
"target": "flask_sample",
"type": "physical",
"port": {
"heatchill_1": "heating_element",
"flask_sample": "bottom"
}
},
{
"source": "stirrer_1",
"target": "reactor",
"type": "physical",
"port": {
"stirrer_1": "stir_rod",
"reactor": "center"
}
},
{
"source": "stirrer_1",
"target": "flask_sample",
"type": "physical",
"port": {
"stirrer_1": "stir_rod",
"flask_sample": "center"
}
},
{
"source": "filter_1",
"target": "reactor",
"type": "physical",
"port": {
"filter_1": "filter_element",
"reactor": "top"
}
},
{
"source": "filter_1",
"target": "flask_sample",
"type": "physical",
"port": {
"filter_1": "filter_element",
"flask_sample": "top"
}
},
{
"source": "filter_1",
"target": "filtrate_flask",
"type": "physical",
"port": {
"filter_1": "filter_element",
"filtrate_flask": "top"
}
}
]
}

View File

@@ -5,6 +5,17 @@ from .separate_protocol import generate_separate_protocol
from .evaporate_protocol import generate_evaporate_protocol from .evaporate_protocol import generate_evaporate_protocol
from .evacuateandrefill_protocol import generate_evacuateandrefill_protocol from .evacuateandrefill_protocol import generate_evacuateandrefill_protocol
from .agv_transfer_protocol import generate_agv_transfer_protocol 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 .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
from .dissolve_protocol import generate_dissolve_protocol
from .filter_through_protocol import generate_filter_through_protocol
from .run_column_protocol import generate_run_column_protocol
from .wash_solid_protocol import generate_wash_solid_protocol
# Define a dictionary of protocol generators. # Define a dictionary of protocol generators.
@@ -15,5 +26,19 @@ action_protocol_generators = {
EvaporateProtocol: generate_evaporate_protocol, EvaporateProtocol: generate_evaporate_protocol,
EvacuateAndRefillProtocol: generate_evacuateandrefill_protocol, EvacuateAndRefillProtocol: generate_evacuateandrefill_protocol,
AGVTransferProtocol: generate_agv_transfer_protocol, AGVTransferProtocol: generate_agv_transfer_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,
} }
# End Protocols

View File

@@ -0,0 +1,74 @@
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]]:
"""
生成添加试剂的协议序列 - 严格按照 Add.action
"""
action_sequence = []
# 如果指定了体积,执行液体转移
if volume > 0:
# 查找可用的试剂瓶
available_flasks = [node for node in G.nodes()
if node.startswith('flask_')
and G.nodes[node].get('type') == 'container']
if not available_flasks:
raise ValueError("没有找到可用的试剂容器")
reagent_vessel = available_flasks[0]
# 查找泵设备
pump_nodes = [node for node in G.nodes()
if G.nodes[node].get('class') == 'virtual_pump']
if pump_nodes:
pump_id = pump_nodes[0]
action_sequence.append({
"device_id": pump_id,
"action_name": "transfer",
"action_kwargs": {
"from_vessel": reagent_vessel,
"to_vessel": vessel,
"volume": volume,
"amount": amount,
"time": time,
"viscous": viscous,
"rinsing_solvent": "",
"rinsing_volume": 0.0,
"rinsing_repeats": 0,
"solid": False
}
})
# 如果需要搅拌,使用 StartStir 而不是 Stir
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", # 使用 start_stir 而不是 stir
"action_kwargs": {
"vessel": vessel,
"stir_speed": stir_speed,
"purpose": f"添加 {reagent} 后搅拌"
}
})
return action_sequence

View File

@@ -0,0 +1,123 @@
from typing import List, Dict, Any
import networkx as nx
def generate_centrifuge_protocol(
G: nx.DiGraph,
vessel: str,
speed: float,
time: float,
temp: float = 25.0
) -> List[Dict[str, Any]]:
"""
生成离心操作的协议序列
Args:
G: 有向图,节点为设备和容器
vessel: 离心容器名称
speed: 离心速度 (rpm)
time: 离心时间 (秒)
temp: 温度 (摄氏度,可选)
Returns:
List[Dict[str, Any]]: 离心操作的动作序列
Raises:
ValueError: 当找不到离心机设备时抛出异常
Examples:
centrifuge_protocol = generate_centrifuge_protocol(G, "reactor", 5000, 300, 4.0)
"""
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} 不存在于图中")
# 执行离心操作
action_sequence.append({
"device_id": centrifuge_id,
"action_name": "centrifuge",
"action_kwargs": {
"vessel": vessel,
"speed": speed,
"time": time,
"temp": temp
}
})
return action_sequence
def generate_multi_step_centrifuge_protocol(
G: nx.DiGraph,
vessel: str,
steps: List[Dict[str, Any]]
) -> 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

View File

@@ -0,0 +1,126 @@
from typing import List, Dict, Any
import networkx as nx
def generate_clean_vessel_protocol(
G: nx.DiGraph,
vessel: str,
solvent: str,
volume: float,
temp: float,
repeats: int = 1
) -> List[Dict[str, Any]]:
"""
生成容器清洗操作的协议序列使用transfer操作实现清洗
Args:
G: 有向图,节点为设备和容器
vessel: 要清洗的容器名称
solvent: 用于清洗容器的溶剂名称
volume: 清洗溶剂的体积
temp: 清洗时的温度
repeats: 清洗操作的重复次数,默认为 1
Returns:
List[Dict[str, Any]]: 容器清洗操作的动作序列
Raises:
ValueError: 当找不到必要的设备时抛出异常
Examples:
clean_vessel_protocol = generate_clean_vessel_protocol(G, "reactor", "water", 50.0, 25.0, 2)
"""
action_sequence = []
# 查找虚拟转移泵设备进行清洗操作
pump_nodes = [node for node in G.nodes()
if G.nodes[node].get('class') == 'virtual_transfer_pump']
if not pump_nodes:
raise ValueError("没有找到可用的转移泵设备进行容器清洗")
pump_id = pump_nodes[0]
# 验证容器是否存在
if vessel not in G.nodes():
raise ValueError(f"容器 {vessel} 不存在于图中")
# 查找溶剂容器
solvent_vessel = f"flask_{solvent}"
if solvent_vessel not in G.nodes():
raise ValueError(f"溶剂容器 {solvent_vessel} 不存在于图中")
# 查找废液容器
waste_vessel = "flask_waste"
if waste_vessel not in G.nodes():
raise ValueError(f"废液容器 {waste_vessel} 不存在于图中")
# 查找加热设备(如果需要加热)
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
# 执行清洗操作序列
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"
}
})
# 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
}
})
# 3. 等待清洗作用时间可选可以添加wait操作
# 这里省略wait操作直接进行下一步
# 4. 将清洗后的溶剂转移到废液容器
action_sequence.append({
"device_id": pump_id,
"action_name": "transfer",
"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
}
})
# 5. 如果加热了,停止加热
if temp > 25.0 and heatchill_id:
action_sequence.append({
"device_id": heatchill_id,
"action_name": "heat_chill_stop",
"action_kwargs": {
"vessel": vessel
}
})
return action_sequence

View File

@@ -0,0 +1,162 @@
from typing import List, Dict, Any
import networkx as nx
def generate_dissolve_protocol(
G: nx.DiGraph,
vessel: str,
solvent: str,
volume: float,
amount: str = "",
temp: float = 25.0,
time: float = 0.0,
stir_speed: float = 0.0
) -> List[Dict[str, Any]]:
"""
生成溶解操作的协议序列
Args:
G: 有向图,节点为设备和容器
vessel: 装有要溶解物质的容器名称
solvent: 用于溶解物质的溶剂名称
volume: 溶剂的体积,可选参数
amount: 要溶解物质的量,可选参数
temp: 溶解时的温度,可选参数
time: 溶解的时间,可选参数
stir_speed: 搅拌速度,可选参数
Returns:
List[Dict[str, Any]]: 溶解操作的动作序列
Raises:
ValueError: 当找不到必要的设备时抛出异常
Examples:
dissolve_protocol = generate_dissolve_protocol(G, "reactor", "water", 100.0, "NaCl 5g", 60.0, 300.0, 500.0)
"""
action_sequence = []
# 验证容器是否存在
if vessel not in G.nodes():
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}")
# 查找转移泵设备
pump_nodes = [node for node in G.nodes()
if G.nodes[node].get('class') == 'virtual_transfer_pump']
if not pump_nodes:
raise ValueError("没有找到可用的转移泵设备")
pump_id = pump_nodes[0]
# 查找加热设备(如果需要加热)
heatchill_nodes = [node for node in G.nodes()
if G.nodes[node].get('class') == 'virtual_heatchill']
heatchill_id = heatchill_nodes[0] if heatchill_nodes else None
# 查找搅拌设备(如果需要搅拌)
stirrer_nodes = [node for node in G.nodes()
if G.nodes[node].get('class') == 'virtual_stirrer']
stirrer_id = stirrer_nodes[0] if stirrer_nodes else None
# 步骤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",
"action_kwargs": {
"vessel": vessel
}
})
# 开始定时搅拌
action_sequence.append({
"device_id": stirrer_id,
"action_name": "stir",
"action_kwargs": {
"stir_time": time,
"stir_speed": stir_speed,
"settling_time": 10.0 # 搅拌后静置10秒
}
})
# 步骤5如果加热了停止加热
if temp > 25.0 and heatchill_id:
action_sequence.append({
"device_id": heatchill_id,
"action_name": "heat_chill_stop",
"action_kwargs": {
"vessel": vessel
}
})
# 步骤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
}
})
return action_sequence

View File

@@ -69,14 +69,14 @@ def generate_evacuateandrefill_protocol(
"device_id": vacuum_backbone["pump"], "device_id": vacuum_backbone["pump"],
"action_name": "set_status", "action_name": "set_status",
"action_kwargs": { "action_kwargs": {
"command": "ON" "string": "ON"
} }
}, },
{ {
"device_id": vacuum_backbone["gas"], "device_id": vacuum_backbone["gas"],
"action_name": "set_status", "action_name": "set_status",
"action_kwargs": { "action_kwargs": {
"command": "OFF" "string": "OFF"
} }
} }
]) ])
@@ -106,14 +106,14 @@ def generate_evacuateandrefill_protocol(
"device_id": vacuum_backbone["pump"], "device_id": vacuum_backbone["pump"],
"action_name": "set_status", "action_name": "set_status",
"action_kwargs": { "action_kwargs": {
"command": "OFF" "string": "OFF"
} }
}, },
{ {
"device_id": vacuum_backbone["gas"], "device_id": vacuum_backbone["gas"],
"action_name": "set_status", "action_name": "set_status",
"action_kwargs": { "action_kwargs": {
"command": "ON" "string": "ON"
} }
} }
]) ])
@@ -125,7 +125,7 @@ def generate_evacuateandrefill_protocol(
"device_id": vacuum_backbone["gas"], "device_id": vacuum_backbone["gas"],
"action_name": "set_status", "action_name": "set_status",
"action_kwargs": { "action_kwargs": {
"command": "OFF" "string": "OFF"
} }
} }
) )

View File

@@ -0,0 +1,70 @@
from typing import List, Dict, Any
import networkx as nx
def generate_filter_protocol(
G: nx.DiGraph,
vessel: str,
filtrate_vessel: str = "",
stir: bool = False,
stir_speed: float = 300.0,
temp: float = 25.0,
continue_heatchill: bool = False,
volume: float = 0.0
) -> List[Dict[str, Any]]:
"""
生成过滤操作的协议序列
Args:
G: 有向图,节点为设备和容器
vessel: 过滤容器
filtrate_vessel: 滤液容器(可选)
stir: 是否搅拌
stir_speed: 搅拌速度(可选)
temp: 温度(可选,摄氏度)
continue_heatchill: 是否继续加热冷却
volume: 过滤体积(可选)
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']
if not filter_nodes:
raise ValueError("没有找到可用的过滤设备")
# 使用第一个可用的过滤器
filter_id = filter_nodes[0]
# 验证容器是否存在
if vessel not in G.nodes():
raise ValueError(f"过滤容器 {vessel} 不存在于图中")
if filtrate_vessel and filtrate_vessel not in G.nodes():
raise ValueError(f"滤液容器 {filtrate_vessel} 不存在于图中")
# 执行过滤操作
action_sequence.append({
"device_id": filter_id,
"action_name": "filter_sample",
"action_kwargs": {
"vessel": vessel,
"filtrate_vessel": filtrate_vessel,
"stir": stir,
"stir_speed": stir_speed,
"temp": temp,
"continue_heatchill": continue_heatchill,
"volume": volume
}
})
return action_sequence

View File

@@ -0,0 +1,150 @@
from typing import List, Dict, Any
import networkx as nx
def generate_filter_through_protocol(
G: nx.DiGraph,
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
) -> List[Dict[str, Any]]:
"""
生成通过过滤介质过滤的协议序列
Args:
G: 有向图,节点为设备和容器
from_vessel: 源容器的名称,即物质起始所在的容器
to_vessel: 目标容器的名称,物质过滤后要到达的容器
filter_through: 过滤时所通过的介质,如滤纸、柱子等
eluting_solvent: 洗脱溶剂的名称,可选参数
eluting_volume: 洗脱溶剂的体积,可选参数
eluting_repeats: 洗脱操作的重复次数,默认为 0
residence_time: 物质在过滤介质中的停留时间,可选参数
Returns:
List[Dict[str, Any]]: 过滤操作的动作序列
Raises:
ValueError: 当找不到必要的设备时抛出异常
Examples:
filter_through_protocol = generate_filter_through_protocol(
G, "reactor", "collection_flask", "celite", "ethanol", 50.0, 2, 60.0
)
"""
action_sequence = []
# 验证容器是否存在
if from_vessel not in G.nodes():
raise ValueError(f"源容器 {from_vessel} 不存在于图中")
if to_vessel not in G.nodes():
raise ValueError(f"目标容器 {to_vessel} 不存在于图中")
# 查找转移泵设备(用于液体转移)
pump_nodes = [node for node in G.nodes()
if G.nodes[node].get('class') == 'virtual_transfer_pump']
if not pump_nodes:
raise ValueError("没有找到可用的转移泵设备")
pump_id = pump_nodes[0]
# 查找过滤设备(可选,如果有专门的过滤设备)
filter_nodes = [node for node in G.nodes()
if G.nodes[node].get('class') == 'virtual_filter']
filter_id = filter_nodes[0] if filter_nodes else None
# 查找洗脱溶剂容器(如果需要洗脱)
eluting_vessel = None
if eluting_solvent and eluting_volume > 0:
eluting_vessel = f"flask_{eluting_solvent}"
if eluting_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:
eluting_vessel = available_vessels[0]
else:
raise ValueError(f"没有找到洗脱溶剂容器 {eluting_solvent}")
# 步骤1将样品从源容器转移到过滤装置模拟通过过滤介质
# 这里我们将过滤过程分解为多个转移步骤来模拟通过介质的过程
# 首先转移样品(模拟样品通过过滤介质)
action_sequence.append({
"device_id": pump_id,
"action_name": "transfer",
"action_kwargs": {
"from_vessel": from_vessel,
"to_vessel": to_vessel,
"volume": 0.0, # 转移所有液体,体积由系统确定
"amount": f"通过 {filter_through} 过滤",
"time": residence_time if residence_time > 0 else 0.0,
"viscous": False,
"rinsing_solvent": "",
"rinsing_volume": 0.0,
"rinsing_repeats": 0,
"solid": True # 通过过滤介质可能涉及固体分离
}
})
# 步骤2如果有专门的过滤设备使用过滤设备处理
if filter_id:
action_sequence.append({
"device_id": filter_id,
"action_name": "filter_sample",
"action_kwargs": {
"vessel": to_vessel,
"filtrate_vessel": to_vessel,
"stir": False,
"stir_speed": 0.0,
"temp": 25.0,
"continue_heatchill": False,
"volume": 0.0
}
})
# 步骤3洗脱操作如果指定了洗脱溶剂和重复次数
if eluting_solvent and eluting_volume > 0 and eluting_repeats > 0 and eluting_vessel:
for repeat in range(eluting_repeats):
# 添加洗脱溶剂
action_sequence.append({
"device_id": pump_id,
"action_name": "transfer",
"action_kwargs": {
"from_vessel": eluting_vessel,
"to_vessel": to_vessel,
"volume": eluting_volume,
"amount": f"洗脱溶剂 {eluting_solvent} - 第 {repeat + 1}",
"time": 0.0,
"viscous": False,
"rinsing_solvent": "",
"rinsing_volume": 0.0,
"rinsing_repeats": 0,
"solid": False
}
})
# 如果有过滤设备,再次过滤洗脱液
if filter_id:
action_sequence.append({
"device_id": filter_id,
"action_name": "filter_sample",
"action_kwargs": {
"vessel": to_vessel,
"filtrate_vessel": to_vessel,
"stir": False,
"stir_speed": 0.0,
"temp": 25.0,
"continue_heatchill": False,
"volume": eluting_volume
}
})
return action_sequence

View File

@@ -0,0 +1,117 @@
from typing import List, Dict, Any
import networkx as nx
def generate_heat_chill_protocol(
G: nx.DiGraph,
vessel: str,
temp: float,
time: float,
stir: bool,
stir_speed: float,
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]
if vessel not in G.nodes():
raise ValueError(f"容器 {vessel} 不存在于图中")
action_sequence.append({
"device_id": heatchill_id,
"action_name": "heat_chill",
"action_kwargs": {
"vessel": vessel,
"temp": temp,
"time": time,
"stir": stir,
"stir_speed": stir_speed,
"purpose": purpose
}
})
return action_sequence
def generate_heat_chill_start_protocol(
G: nx.DiGraph,
vessel: str,
temp: float,
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]
if vessel not in G.nodes():
raise ValueError(f"容器 {vessel} 不存在于图中")
action_sequence.append({
"device_id": heatchill_id,
"action_name": "heat_chill_start",
"action_kwargs": {
"vessel": vessel,
"temp": temp,
"purpose": purpose
}
})
return action_sequence
def generate_heat_chill_stop_protocol(
G: nx.DiGraph,
vessel: str
) -> 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]
if vessel not in G.nodes():
raise ValueError(f"容器 {vessel} 不存在于图中")
action_sequence.append({
"device_id": heatchill_id,
"action_name": "heat_chill_stop",
"action_kwargs": {
"vessel": vessel
}
})
return action_sequence

View File

@@ -24,10 +24,27 @@ def generate_pump_protocol(
# 生成泵操作的动作序列 # 生成泵操作的动作序列
pump_action_sequence = [] pump_action_sequence = []
nodes = G.nodes(data=True)
# 从from_vessel到to_vessel的最短路径 # 检查节点是否存在
shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel) if from_vessel not in G.nodes:
print(shortest_path) print(f"Warning: Source vessel '{from_vessel}' not found in graph. Skipping.")
return []
if to_vessel not in G.nodes:
print(f"Warning: Target vessel '{to_vessel}' not found in graph. Skipping.")
return []
# 检查是否存在路径
try:
shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
except nx.NetworkXNoPath:
print(f"Warning: No path from '{from_vessel}' to '{to_vessel}'. Skipping.")
return []
except nx.NodeNotFound as e:
print(f"Warning: Node not found: {e}. Skipping.")
return []
print(f"Shortest path: {shortest_path}")
pump_backbone = shortest_path pump_backbone = shortest_path
if not from_vessel.startswith("pump"): if not from_vessel.startswith("pump"):
@@ -35,10 +52,34 @@ def generate_pump_protocol(
if not to_vessel.startswith("pump"): if not to_vessel.startswith("pump"):
pump_backbone = pump_backbone[:-1] pump_backbone = pump_backbone[:-1]
print(f"Pump backbone: {pump_backbone}")
# 修复检查pump_backbone是否为空
if not pump_backbone:
print(f"Warning: No pumps found in path from '{from_vessel}' to '{to_vessel}'. Skipping.")
return []
if transfer_flowrate == 0: if transfer_flowrate == 0:
transfer_flowrate = flowrate transfer_flowrate = flowrate
min_transfer_volume = min([nodes[pump]["max_volume"] for pump in pump_backbone]) # 修复:正确访问节点数据
pump_max_volumes = []
for pump in pump_backbone:
# 直接使用 G.nodes[pump] 来访问节点数据
pump_data = G.nodes[pump] if pump in G.nodes else {}
# 尝试多种可能的键名,并提供默认值
max_vol = pump_data.get('max_volume') or pump_data.get('max_vol') or pump_data.get('volume')
if max_vol is None:
# 如果是设备节点尝试从config中获取
config = pump_data.get('config', {})
max_vol = config.get('max_volume', 25.0)
pump_max_volumes.append(float(max_vol))
if pump_max_volumes:
min_transfer_volume = min(pump_max_volumes)
else:
min_transfer_volume = 25.0 # 默认值
repeats = int(np.ceil(volume / min_transfer_volume)) repeats = int(np.ceil(volume / min_transfer_volume))
if repeats > 1 and (from_vessel.startswith("pump") or to_vessel.startswith("pump")): if repeats > 1 and (from_vessel.startswith("pump") or to_vessel.startswith("pump")):
raise ValueError("Cannot transfer volume larger than min_transfer_volume between two pumps.") raise ValueError("Cannot transfer volume larger than min_transfer_volume between two pumps.")
@@ -48,84 +89,102 @@ def generate_pump_protocol(
# 生成泵操作的动作序列 # 生成泵操作的动作序列
for i in range(repeats): for i in range(repeats):
# 单泵依次执行阀指令、活塞指令,将液体吸入与之相连的第一台泵 # 单泵依次执行阀指令、活塞指令,将液体吸入与之相连的第一台泵
if not from_vessel.startswith("pump"): if not from_vessel.startswith("pump") and pump_backbone:
pump_action_sequence.extend([ # 修复:添加边缘数据检查
{ edge_data = G.get_edge_data(pump_backbone[0], from_vessel)
"device_id": pump_backbone[0], if edge_data and "port" in edge_data:
"action_name": "set_valve_position", pump_action_sequence.extend([
"action_kwargs": { {
"command": G.get_edge_data(pump_backbone[0], from_vessel)["port"][pump_backbone[0]] "device_id": pump_backbone[0],
"action_name": "set_valve_position",
"action_kwargs": {
"command": edge_data["port"][pump_backbone[0]]
}
},
{
"device_id": pump_backbone[0],
"action_name": "set_position",
"action_kwargs": {
"position": float(min(volume_left, min_transfer_volume)),
"max_velocity": transfer_flowrate
}
} }
}, ])
{ pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
"device_id": pump_backbone[0], else:
"action_name": "set_position", print(f"Warning: No edge data found between {pump_backbone[0]} and {from_vessel}")
"action_kwargs": {
"position": float(min(volume_left, min_transfer_volume)),
"max_velocity": transfer_flowrate
}
}
])
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
for pumpA, pumpB in zip(pump_backbone[:-1], pump_backbone[1:]):
# 相邻两泵同时切换阀门至连通位置
pump_action_sequence.append([
{
"device_id": pumpA,
"action_name": "set_valve_position",
"action_kwargs": {
"command": G.get_edge_data(pumpA, pumpB)["port"][pumpA]
}
},
{
"device_id": pumpB,
"action_name": "set_valve_position",
"action_kwargs": {
"command": G.get_edge_data(pumpB, pumpA)["port"][pumpB],
}
}
])
# 相邻两泵液体转移泵A排出液体泵B吸入液体
pump_action_sequence.append([
{
"device_id": pumpA,
"action_name": "set_position",
"action_kwargs": {
"position": 0.0,
"max_velocity": transfer_flowrate
}
},
{
"device_id": pumpB,
"action_name": "set_position",
"action_kwargs": {
"position": float(min(volume_left, min_transfer_volume)),
"max_velocity": transfer_flowrate
}
}
])
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
if not to_vessel.startswith("pump"): # 修复检查pump_backbone长度避免多泵操作时出错
if len(pump_backbone) > 1:
for pumpA, pumpB in zip(pump_backbone[:-1], pump_backbone[1:]):
# 相邻两泵同时切换阀门至连通位置
edge_AB = G.get_edge_data(pumpA, pumpB)
edge_BA = G.get_edge_data(pumpB, pumpA)
if edge_AB and "port" in edge_AB and edge_BA and "port" in edge_BA:
pump_action_sequence.append([
{
"device_id": pumpA,
"action_name": "set_valve_position",
"action_kwargs": {
"command": edge_AB["port"][pumpA]
}
},
{
"device_id": pumpB,
"action_name": "set_valve_position",
"action_kwargs": {
"command": edge_BA["port"][pumpB],
}
}
])
# 相邻两泵液体转移泵A排出液体泵B吸入液体
pump_action_sequence.append([
{
"device_id": pumpA,
"action_name": "set_position",
"action_kwargs": {
"position": 0.0,
"max_velocity": transfer_flowrate
}
},
{
"device_id": pumpB,
"action_name": "set_position",
"action_kwargs": {
"position": float(min(volume_left, min_transfer_volume)),
"max_velocity": transfer_flowrate
}
}
])
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
else:
print(f"Warning: No edge data found between {pumpA} and {pumpB}")
if not to_vessel.startswith("pump") and pump_backbone:
# 单泵依次执行阀指令、活塞指令将最后一台泵液体缓慢加入容器B # 单泵依次执行阀指令、活塞指令将最后一台泵液体缓慢加入容器B
pump_action_sequence.extend([ edge_data = G.get_edge_data(pump_backbone[-1], to_vessel)
{ if edge_data and "port" in edge_data:
"device_id": pump_backbone[-1], pump_action_sequence.extend([
"action_name": "set_valve_position", {
"action_kwargs": { "device_id": pump_backbone[-1],
"command": G.get_edge_data(pump_backbone[-1], to_vessel)["port"][pump_backbone[-1]] "action_name": "set_valve_position",
"action_kwargs": {
"command": edge_data["port"][pump_backbone[-1]]
}
},
{
"device_id": pump_backbone[-1],
"action_name": "set_position",
"action_kwargs": {
"position": 0.0,
"max_velocity": flowrate
}
} }
}, ])
{ pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
"device_id": pump_backbone[-1], else:
"action_name": "set_position", print(f"Warning: No edge data found between {pump_backbone[-1]} and {to_vessel}")
"action_kwargs": {
"position": 0.0,
"max_velocity": flowrate
}
}
])
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
volume_left -= min_transfer_volume volume_left -= min_transfer_volume
return pump_action_sequence return pump_action_sequence
@@ -174,18 +233,52 @@ def generate_pump_protocol_with_rinsing(
Examples: Examples:
pump_protocol = generate_pump_protocol_with_rinsing(G, "vessel_A", "vessel_B", 0.1, rinsing_solvent="water") pump_protocol = generate_pump_protocol_with_rinsing(G, "vessel_A", "vessel_B", 0.1, rinsing_solvent="water")
""" """
air_vessel = "flask_air" # 修复:使用实际存在的节点名称
waste_vessel = f"waste_workup" air_vessel = "flask_air" # 这个在你的配置中存在
shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel) # 寻找合适的废料容器,如果没有找到则使用空的容器作为替代
pump_backbone = shortest_path[1: -1] waste_vessel = None
nodes = G.nodes(data=True) available_vessels = [node for node in G.nodes if node.startswith("flask_") and node != air_vessel]
min_transfer_volume = float(min([nodes[pump]["max_volume"] for pump in pump_backbone])) if available_vessels:
# 使用第一个可用的容器作为废料容器
waste_vessel = available_vessels[0]
print(f"Using {waste_vessel} as waste vessel")
else:
waste_vessel = "flask_1" # 备用选择
# 修复:添加路径检查
try:
shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
pump_backbone = shortest_path[1: -1]
except (nx.NetworkXNoPath, nx.NodeNotFound) as e:
print(f"Warning: Cannot find path from {from_vessel} to {to_vessel}: {e}")
return []
# 修复:正确访问节点数据
pump_max_volumes = []
for pump in pump_backbone:
# 直接使用 G.nodes[pump] 来访问节点数据
pump_data = G.nodes[pump] if pump in G.nodes else {}
# 尝试多种可能的键名,并提供默认值
max_vol = pump_data.get('max_volume') or pump_data.get('max_vol') or pump_data.get('volume')
if max_vol is None:
# 如果是设备节点尝试从config中获取
config = pump_data.get('config', {})
max_vol = config.get('max_volume', 25.0)
pump_max_volumes.append(float(max_vol))
if pump_max_volumes:
min_transfer_volume = float(min(pump_max_volumes))
else:
min_transfer_volume = 25.0 # 默认值
if time != 0: if time != 0:
flowrate = transfer_flowrate = volume / time flowrate = transfer_flowrate = volume / time
pump_action_sequence = generate_pump_protocol(G, from_vessel, to_vessel, float(volume), flowrate, transfer_flowrate) pump_action_sequence = generate_pump_protocol(G, from_vessel, to_vessel, float(volume), flowrate, transfer_flowrate)
if rinsing_solvent != "air":
# 修复:只在需要清洗且相关节点存在时才执行清洗步骤
if rinsing_solvent != "air" and pump_backbone:
if "," in rinsing_solvent: if "," in rinsing_solvent:
rinsing_solvents = rinsing_solvent.split(",") rinsing_solvents = rinsing_solvent.split(",")
assert len(rinsing_solvents) == rinsing_repeats, "Number of rinsing solvents must match number of rinsing repeats." assert len(rinsing_solvents) == rinsing_repeats, "Number of rinsing solvents must match number of rinsing repeats."
@@ -194,20 +287,32 @@ def generate_pump_protocol_with_rinsing(
for rinsing_solvent in rinsing_solvents: for rinsing_solvent in rinsing_solvents:
solvent_vessel = f"flask_{rinsing_solvent}" solvent_vessel = f"flask_{rinsing_solvent}"
# 清洗泵
pump_action_sequence.extend( # 检查溶剂容器是否存在
generate_pump_protocol(G, solvent_vessel, pump_backbone[0], min_transfer_volume, flowrate, transfer_flowrate) + if solvent_vessel not in G.nodes:
generate_pump_protocol(G, pump_backbone[0], pump_backbone[-1], min_transfer_volume, flowrate, transfer_flowrate) + print(f"Warning: Solvent vessel '{solvent_vessel}' not found in graph. Skipping rinsing step.")
generate_pump_protocol(G, pump_backbone[-1], waste_vessel, min_transfer_volume, flowrate, transfer_flowrate) continue
)
# 如果转移的是溶液,第一种冲洗溶剂请选用溶液的溶剂,稀释泵内、转移管道内的溶液。后续冲洗溶剂不需要此操作。 # 清洗泵 - 只有当所有必需的节点都存在且pump_backbone不为空时才执行
if rinsing_solvent == rinsing_solvents[0]: if pump_backbone and len(pump_backbone) > 0 and waste_vessel in G.nodes:
pump_action_sequence.extend(generate_pump_protocol(G, solvent_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate)) pump_action_sequence.extend(
pump_action_sequence.extend(generate_pump_protocol(G, solvent_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate)) generate_pump_protocol(G, solvent_vessel, pump_backbone[0], min_transfer_volume, flowrate, transfer_flowrate) +
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, solvent_vessel, rinsing_volume, flowrate, transfer_flowrate)) generate_pump_protocol(G, pump_backbone[0], pump_backbone[-1], min_transfer_volume, flowrate, transfer_flowrate) +
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, waste_vessel, rinsing_volume, flowrate, transfer_flowrate)) generate_pump_protocol(G, pump_backbone[-1], waste_vessel, min_transfer_volume, flowrate, transfer_flowrate)
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2) )
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2)
# 如果转移的是溶液,第一种冲洗溶剂请选用溶液的溶剂,稀释泵内、转移管道内的溶液。后续冲洗溶剂不需要此操作。
if rinsing_solvent == rinsing_solvents[0]:
pump_action_sequence.extend(generate_pump_protocol(G, solvent_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate))
pump_action_sequence.extend(generate_pump_protocol(G, solvent_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate))
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, solvent_vessel, rinsing_volume, flowrate, transfer_flowrate))
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, waste_vessel, rinsing_volume, flowrate, transfer_flowrate))
# 最后的空气清洗 - 只有当节点存在时才执行
if air_vessel in G.nodes:
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2)
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2)
return pump_action_sequence return pump_action_sequence
# End Protocols # End Protocols

View File

@@ -0,0 +1,102 @@
from typing import List, Dict, Any
import networkx as nx
def generate_run_column_protocol(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
column: str
) -> List[Dict[str, Any]]:
"""
生成柱层析分离的协议序列
Args:
G: 有向图,节点为设备和容器
from_vessel: 源容器的名称,即样品起始所在的容器
to_vessel: 目标容器的名称,分离后的样品要到达的容器
column: 所使用的柱子的名称
Returns:
List[Dict[str, Any]]: 柱层析分离操作的动作序列
Raises:
ValueError: 当找不到必要的设备时抛出异常
Examples:
run_column_protocol = generate_run_column_protocol(G, "reactor", "collection_flask", "silica_column")
"""
action_sequence = []
# 验证容器是否存在
if from_vessel not in G.nodes():
raise ValueError(f"源容器 {from_vessel} 不存在于图中")
if to_vessel not in G.nodes():
raise ValueError(f"目标容器 {to_vessel} 不存在于图中")
# 查找转移泵设备(用于样品转移)
pump_nodes = [node for node in G.nodes()
if G.nodes[node].get('class') == 'virtual_transfer_pump']
if not pump_nodes:
raise ValueError("没有找到可用的转移泵设备")
pump_id = pump_nodes[0]
# 查找柱层析设备
column_nodes = [node for node in G.nodes()
if G.nodes[node].get('class') == 'virtual_column']
if not column_nodes:
raise ValueError("没有找到可用的柱层析设备")
column_id = column_nodes[0]
# 步骤1将样品从源容器转移到柱子上
action_sequence.append({
"device_id": pump_id,
"action_name": "transfer",
"action_kwargs": {
"from_vessel": from_vessel,
"to_vessel": column_id, # 将样品转移到柱子设备
"volume": 0.0, # 转移所有液体,体积由系统确定
"amount": f"样品上柱 - 使用 {column}",
"time": 0.0,
"viscous": False,
"rinsing_solvent": "",
"rinsing_volume": 0.0,
"rinsing_repeats": 0,
"solid": False
}
})
# 步骤2运行柱层析分离
action_sequence.append({
"device_id": column_id,
"action_name": "run_column",
"action_kwargs": {
"from_vessel": from_vessel,
"to_vessel": to_vessel,
"column": column
}
})
# 步骤3将分离后的产物从柱子转移到目标容器
action_sequence.append({
"device_id": pump_id,
"action_name": "transfer",
"action_kwargs": {
"from_vessel": column_id, # 从柱子设备转移
"to_vessel": to_vessel,
"volume": 0.0, # 转移所有液体,体积由系统确定
"amount": f"收集分离产物 - 来自 {column}",
"time": 0.0,
"viscous": False,
"rinsing_solvent": "",
"rinsing_volume": 0.0,
"rinsing_repeats": 0,
"solid": False
}
})
return action_sequence

View File

@@ -0,0 +1,137 @@
from typing import List, Dict, Any
import networkx as nx
def generate_stir_protocol(
G: nx.DiGraph,
stir_time: float,
stir_speed: float,
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 = []
# 查找搅拌设备
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]
# 执行搅拌操作
action_sequence.append({
"device_id": stirrer_id,
"action_name": "stir",
"action_kwargs": {
"stir_time": stir_time,
"stir_speed": stir_speed,
"settling_time": settling_time
}
})
return action_sequence
def generate_start_stir_protocol(
G: nx.DiGraph,
vessel: str,
stir_speed: float,
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']
if not stirrer_nodes:
raise ValueError("没有找到可用的搅拌设备")
stirrer_id = stirrer_nodes[0]
# 验证容器是否存在
if vessel not in G.nodes():
raise ValueError(f"容器 {vessel} 不存在于图中")
action_sequence.append({
"device_id": stirrer_id,
"action_name": "start_stir",
"action_kwargs": {
"vessel": vessel,
"stir_speed": stir_speed,
"purpose": purpose
}
})
return action_sequence
def generate_stop_stir_protocol(
G: nx.DiGraph,
vessel: str
) -> 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']
if not stirrer_nodes:
raise ValueError("没有找到可用的搅拌设备")
stirrer_id = stirrer_nodes[0]
# 验证容器是否存在
if vessel not in G.nodes():
raise ValueError(f"容器 {vessel} 不存在于图中")
action_sequence.append({
"device_id": stirrer_id,
"action_name": "stop_stir",
"action_kwargs": {
"vessel": vessel
}
})
return action_sequence

View File

@@ -0,0 +1,79 @@
from typing import List, Dict, Any
import networkx as nx
def generate_transfer_protocol(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
volume: float,
amount: str = "",
time: float = 0,
viscous: bool = False,
rinsing_solvent: str = "",
rinsing_volume: float = 0.0,
rinsing_repeats: int = 0,
solid: bool = False
) -> List[Dict[str, Any]]:
"""
生成液体转移操作的协议序列
Args:
G: 有向图,节点为设备和容器
from_vessel: 源容器
to_vessel: 目标容器
volume: 转移体积 (mL)
amount: 数量描述 (可选)
time: 转移时间 (秒,可选)
viscous: 是否为粘性液体
rinsing_solvent: 冲洗溶剂 (可选)
rinsing_volume: 冲洗体积 (mL可选)
rinsing_repeats: 冲洗重复次数
solid: 是否涉及固体
Returns:
List[Dict[str, Any]]: 转移操作的动作序列
Raises:
ValueError: 当找不到合适的转移设备时抛出异常
Examples:
transfer_protocol = generate_transfer_protocol(G, "flask_1", "reactor", 10.0)
"""
action_sequence = []
# 查找虚拟转移泵设备用于液体转移 - 修复:应该查找 virtual_transfer_pump
pump_nodes = [node for node in G.nodes()
if G.nodes[node].get('class') == 'virtual_transfer_pump']
if not pump_nodes:
raise ValueError("没有找到可用的转移泵设备进行液体转移")
# 使用第一个可用的泵
pump_id = pump_nodes[0]
# 验证容器是否存在
if from_vessel not in G.nodes():
raise ValueError(f"源容器 {from_vessel} 不存在于图中")
if to_vessel not in G.nodes():
raise ValueError(f"目标容器 {to_vessel} 不存在于图中")
# 执行液体转移操作 - 参数完全匹配Transfer.action
action_sequence.append({
"device_id": pump_id,
"action_name": "transfer",
"action_kwargs": {
"from_vessel": from_vessel,
"to_vessel": to_vessel,
"volume": volume,
"amount": amount,
"time": time,
"viscous": viscous,
"rinsing_solvent": rinsing_solvent,
"rinsing_volume": rinsing_volume,
"rinsing_repeats": rinsing_repeats,
"solid": solid
}
})
return action_sequence

View File

@@ -0,0 +1,216 @@
from typing import List, Dict, Any
import networkx as nx
def generate_wash_solid_protocol(
G: nx.DiGraph,
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
) -> List[Dict[str, Any]]:
"""
生成固体清洗的协议序列
Args:
G: 有向图,节点为设备和容器
vessel: 装有固体物质的容器名称
solvent: 用于清洗固体的溶剂名称
volume: 清洗溶剂的体积
filtrate_vessel: 滤液要收集到的容器名称,可选参数
temp: 清洗时的温度,可选参数
stir: 是否在清洗过程中搅拌,默认为 False
stir_speed: 搅拌速度,可选参数
time: 清洗的时间,可选参数
repeats: 清洗操作的重复次数,默认为 1
Returns:
List[Dict[str, Any]]: 固体清洗操作的动作序列
Raises:
ValueError: 当找不到必要的设备时抛出异常
Examples:
wash_solid_protocol = generate_wash_solid_protocol(
G, "reactor", "ethanol", 100.0, "waste_flask", 60.0, True, 300.0, 600.0, 3
)
"""
action_sequence = []
# 验证容器是否存在
if vessel not in G.nodes():
raise ValueError(f"固体容器 {vessel} 不存在于图中")
if filtrate_vessel and filtrate_vessel not in G.nodes():
raise ValueError(f"滤液容器 {filtrate_vessel} 不存在于图中")
# 查找转移泵设备(用于添加溶剂和转移滤液)
pump_nodes = [node for node in G.nodes()
if G.nodes[node].get('class') == 'virtual_transfer_pump']
if not pump_nodes:
raise ValueError("没有找到可用的转移泵设备")
pump_id = pump_nodes[0]
# 查找加热设备(如果需要加热)
heatchill_nodes = [node for node in G.nodes()
if G.nodes[node].get('class') == 'virtual_heatchill']
heatchill_id = heatchill_nodes[0] if heatchill_nodes else None
# 查找搅拌设备(如果需要搅拌)
stirrer_nodes = [node for node in G.nodes()
if G.nodes[node].get('class') == 'virtual_stirrer']
stirrer_id = stirrer_nodes[0] if stirrer_nodes else None
# 查找过滤设备(用于分离固体和滤液)
filter_nodes = [node for node in G.nodes()
if G.nodes[node].get('class') == 'virtual_filter']
filter_id = filter_nodes[0] if filter_nodes else None
# 查找溶剂容器
solvent_vessel = f"flask_{solvent}"
if solvent_vessel not in G.nodes():
# 如果没有找到特定溶剂容器,查找可用的源容器
available_vessels = [node for node in G.nodes()
if node.startswith('flask_') and
G.nodes[node].get('type') == 'container']
if available_vessels:
solvent_vessel = available_vessels[0]
else:
raise ValueError(f"没有找到溶剂容器 {solvent}")
# 如果没有指定滤液容器,使用废液容器
if not filtrate_vessel:
waste_vessels = [node for node in G.nodes()
if 'waste' in node.lower() and
G.nodes[node].get('type') == 'container']
filtrate_vessel = waste_vessels[0] if waste_vessels else "waste_flask"
# 重复清洗操作
for repeat in range(repeats):
repeat_num = repeat + 1
# 步骤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": f"固体清洗 - 第 {repeat_num}"
}
})
# 步骤2添加清洗溶剂到固体容器
action_sequence.append({
"device_id": pump_id,
"action_name": "transfer",
"action_kwargs": {
"from_vessel": solvent_vessel,
"to_vessel": vessel,
"volume": volume,
"amount": f"清洗溶剂 {solvent} - 第 {repeat_num}",
"time": 0.0,
"viscous": False,
"rinsing_solvent": "",
"rinsing_volume": 0.0,
"rinsing_repeats": 0,
"solid": False
}
})
# 步骤3如果需要搅拌开始搅拌
if stir and stir_speed > 0 and stirrer_id:
if time > 0:
# 定时搅拌
action_sequence.append({
"device_id": stirrer_id,
"action_name": "stir",
"action_kwargs": {
"stir_time": time,
"stir_speed": stir_speed,
"settling_time": 30.0 # 搅拌后静置30秒
}
})
else:
# 开始搅拌(需要手动停止)
action_sequence.append({
"device_id": stirrer_id,
"action_name": "start_stir",
"action_kwargs": {
"vessel": vessel,
"stir_speed": stir_speed,
"purpose": f"固体清洗搅拌 - 第 {repeat_num}"
}
})
# 步骤4如果指定了清洗时间但没有搅拌等待清洗时间
if time > 0 and (not stir or stir_speed == 0):
# 这里可以添加等待操作,暂时跳过
pass
# 步骤5如果有搅拌且没有定时停止搅拌
if stir and stir_speed > 0 and time == 0 and stirrer_id:
action_sequence.append({
"device_id": stirrer_id,
"action_name": "stop_stir",
"action_kwargs": {
"vessel": vessel
}
})
# 步骤6过滤分离固体和滤液
if filter_id:
action_sequence.append({
"device_id": filter_id,
"action_name": "filter_sample",
"action_kwargs": {
"vessel": vessel,
"filtrate_vessel": filtrate_vessel,
"stir": False,
"stir_speed": 0.0,
"temp": temp,
"continue_heatchill": temp > 25.0,
"volume": volume
}
})
else:
# 没有专门的过滤设备,使用转移泵模拟过滤过程
# 将滤液转移到滤液容器
action_sequence.append({
"device_id": pump_id,
"action_name": "transfer",
"action_kwargs": {
"from_vessel": vessel,
"to_vessel": filtrate_vessel,
"volume": volume,
"amount": f"转移滤液 - 第 {repeat_num} 次清洗",
"time": 0.0,
"viscous": False,
"rinsing_solvent": "",
"rinsing_volume": 0.0,
"rinsing_repeats": 0,
"solid": False
}
})
# 步骤7如果加热了停止加热在最后一次清洗后
if temp > 25.0 and heatchill_id and repeat_num == repeats:
action_sequence.append({
"device_id": heatchill_id,
"action_name": "heat_chill_stop",
"action_kwargs": {
"vessel": vessel
}
})
return action_sequence

View File

@@ -12,10 +12,10 @@ class VacuumPumpMock:
def get_status(self) -> str: def get_status(self) -> str:
return self._status return self._status
def set_status(self, position): def set_status(self, string):
time.sleep(5) time.sleep(5)
self._status = position self._status = string
time.sleep(5) time.sleep(5)
def open(self): def open(self):

View File

View File

@@ -0,0 +1,158 @@
import asyncio
import logging
from typing import Dict, Any
class VirtualCentrifuge:
"""Virtual centrifuge device for CentrifugeProtocol testing"""
def __init__(self, device_id: str = None, config: 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_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'}
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",
"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": ""
})
return True
async def cleanup(self) -> bool:
"""Cleanup virtual centrifuge"""
self.logger.info(f"Cleaning up virtual centrifuge {self.device_id}")
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")
# 验证参数
if speed > self._max_speed:
self.logger.error(f"Speed {speed} exceeds maximum {self._max_speed}")
self.data["message"] = f"速度 {speed} 超过最大值 {self._max_speed}"
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}"
return False
# 开始离心
self.data.update({
"status": "Running",
"centrifuge_state": "Centrifuging",
"target_speed": speed,
"current_speed": speed,
"target_temp": temp,
"current_temp": temp,
"time_remaining": time,
"vessel": vessel,
"progress": 0.0,
"message": f"离心中: {vessel} at {speed} RPM"
})
# 模拟离心过程
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
# 状态属性
@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 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", "")

View File

@@ -0,0 +1,132 @@
import asyncio
import logging
from typing import Dict, Any, Optional
class VirtualColumn:
"""Virtual column device for RunColumn protocol"""
def __init__(self, device_id: str = None, config: 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_column"
self.config = config or {}
self.logger = logging.getLogger(f"VirtualColumn.{self.device_id}")
self.data = {}
# 从config或kwargs中获取配置参数
self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL')
self._max_flow_rate = self.config.get('max_flow_rate') or kwargs.get('max_flow_rate', 10.0)
self._column_length = self.config.get('column_length') or kwargs.get('column_length', 25.0)
self._column_diameter = self.config.get('column_diameter') or kwargs.get('column_diameter', 2.0)
print(f"=== VirtualColumn {self.device_id} created with max_flow_rate={self._max_flow_rate}, length={self._column_length}cm ===")
async def initialize(self) -> bool:
"""Initialize virtual column"""
self.logger.info(f"Initializing virtual column {self.device_id}")
self.data.update({
"status": "Idle",
"column_state": "Ready",
"current_flow_rate": 0.0,
"max_flow_rate": self._max_flow_rate,
"column_length": self._column_length,
"column_diameter": self._column_diameter,
"processed_volume": 0.0,
"progress": 0.0,
"current_status": "Ready"
})
return True
async def cleanup(self) -> bool:
"""Cleanup virtual column"""
self.logger.info(f"Cleaning up virtual column {self.device_id}")
return True
async def run_column(self, from_vessel: str, to_vessel: str, column: str) -> bool:
"""Execute column chromatography run - matches RunColumn action"""
self.logger.info(f"Running column separation: {from_vessel} -> {to_vessel} using {column}")
# 更新设备状态
self.data.update({
"status": "Running",
"column_state": "Separating",
"current_status": "Column separation in progress",
"progress": 0.0,
"processed_volume": 0.0
})
# 模拟柱层析分离过程
# 假设处理时间基于流速和柱子长度
separation_time = (self._column_length * 2) / self._max_flow_rate # 简化计算
steps = 20 # 分20个步骤模拟分离过程
step_time = separation_time / steps
for i in range(steps):
await asyncio.sleep(step_time)
progress = (i + 1) / steps * 100
volume_processed = (i + 1) * 5.0 # 假设每步处理5mL
# 更新状态
self.data.update({
"progress": progress,
"processed_volume": volume_processed,
"current_status": f"Column separation: {progress:.1f}% - Processing {volume_processed:.1f}mL"
})
self.logger.info(f"Column separation progress: {progress:.1f}%")
# 分离完成
self.data.update({
"status": "Idle",
"column_state": "Ready",
"current_status": "Column separation completed",
"progress": 100.0
})
self.logger.info(f"Column separation completed: {from_vessel} -> {to_vessel}")
return True
# 状态属性
@property
def status(self) -> str:
return self.data.get("status", "Unknown")
@property
def column_state(self) -> str:
return self.data.get("column_state", "Unknown")
@property
def current_flow_rate(self) -> float:
return self.data.get("current_flow_rate", 0.0)
@property
def max_flow_rate(self) -> float:
return self.data.get("max_flow_rate", 0.0)
@property
def column_length(self) -> float:
return self.data.get("column_length", 0.0)
@property
def column_diameter(self) -> float:
return self.data.get("column_diameter", 0.0)
@property
def processed_volume(self) -> float:
return self.data.get("processed_volume", 0.0)
@property
def progress(self) -> float:
return self.data.get("progress", 0.0)
@property
def current_status(self) -> str:
return self.data.get("current_status", "Ready")

View File

@@ -0,0 +1,151 @@
import asyncio
import logging
from typing import Dict, Any
class VirtualFilter:
"""Virtual filter device for FilterProtocol testing"""
def __init__(self, device_id: str = None, config: 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)
# 处理其他kwargs参数但跳过已知的配置参数
skip_keys = {'port', 'max_temp', 'max_stir_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 filter"""
print(f"=== VirtualFilter {self.device_id} initialize() called! ===")
self.logger.info(f"Initializing virtual filter {self.device_id}")
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": ""
})
return True
async def cleanup(self) -> bool:
"""Cleanup virtual filter"""
self.logger.info(f"Cleaning up virtual filter {self.device_id}")
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}")
# 验证参数
if temp > self._max_temp:
self.logger.error(f"Temperature {temp} exceeds maximum {self._max_temp}")
self.data["message"] = f"温度 {temp} 超过最大值 {self._max_temp}"
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}"
return False
# 开始过滤
self.data.update({
"status": "Running",
"filter_state": "Filtering",
"target_temp": temp,
"current_temp": temp,
"stir_speed": stir_speed if stir else 0.0,
"vessel": vessel,
"filtrate_vessel": filtrate_vessel,
"target_volume": volume,
"progress": 0.0,
"message": f"过滤中: {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
# 状态属性
@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:
return self.data.get("progress", 0.0)
@property
def message(self) -> str:
return self.data.get("message", "")

View File

@@ -0,0 +1,107 @@
import asyncio
import logging
from typing import Dict, Any
class VirtualHeatChill:
"""Virtual heat chill device for HeatChillProtocol testing"""
def __init__(self, device_id: str = None, config: 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_heatchill"
self.config = config or {}
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参数但跳过已知的配置参数
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):
setattr(self, key, value)
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"
})
return True
async def cleanup(self) -> bool:
"""Cleanup virtual heat chill"""
self.logger.info(f"Cleaning up virtual heat chill {self.device_id}")
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}")
# 验证参数
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} 超出范围"
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} 超出范围"
return False
# 开始加热/冷却
self.data.update({
"status": f"加热/冷却中: {vessel}{temp}°C"
})
# 模拟加热/冷却时间
simulation_time = min(time, 10.0) # 最多等待10秒用于测试
await asyncio.sleep(simulation_time)
# 加热/冷却完成
self.data["status"] = f"完成: {vessel} 已达到 {temp}°C"
self.logger.info(f"HeatChill completed for vessel {vessel} at {temp}°C")
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}")
# 验证参数
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} 超出范围"
return False
self.data["status"] = f"开始加热/冷却: {vessel}{temp}°C"
return True
async def heat_chill_stop(self, vessel: str) -> bool:
"""Stop heat chill - matches HeatChillStop action exactly"""
self.logger.info(f"HeatChillStop: vessel={vessel}")
self.data["status"] = f"停止加热/冷却: {vessel}"
return True
# 状态属性 - 只保留 action 中定义的 feedback
@property
def status(self) -> str:
return self.data.get("status", "Idle")

View File

@@ -0,0 +1,197 @@
import asyncio
import logging
from typing import Dict, Any, Optional
class VirtualPump:
"""Virtual pump device for transfer and cleaning operations"""
def __init__(self, device_id: str = None, config: 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_pump"
self.config = config or {}
self.logger = logging.getLogger(f"VirtualPump.{self.device_id}")
self.data = {}
# 从config或kwargs中获取配置参数
self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL')
self._max_volume = self.config.get('max_volume') or kwargs.get('max_volume', 50.0)
self._transfer_rate = self.config.get('transfer_rate') or kwargs.get('transfer_rate', 10.0)
print(f"=== VirtualPump {self.device_id} created with max_volume={self._max_volume}, transfer_rate={self._transfer_rate} ===")
async def initialize(self) -> bool:
"""Initialize virtual pump"""
self.logger.info(f"Initializing virtual pump {self.device_id}")
self.data.update({
"status": "Idle",
"valve_position": 0,
"current_volume": 0.0,
"max_volume": self._max_volume,
"transfer_rate": self._transfer_rate,
"from_vessel": "",
"to_vessel": "",
"progress": 0.0,
"transferred_volume": 0.0,
"current_status": "Ready"
})
return True
async def cleanup(self) -> bool:
"""Cleanup virtual pump"""
self.logger.info(f"Cleaning up virtual pump {self.device_id}")
return True
async def transfer(self, from_vessel: str, to_vessel: str, volume: float,
amount: str = "", time: float = 0.0, viscous: bool = False,
rinsing_solvent: str = "", rinsing_volume: float = 0.0,
rinsing_repeats: int = 0, solid: bool = False) -> bool:
"""Execute transfer operation"""
self.logger.info(f"Transferring {volume}mL from {from_vessel} to {to_vessel}")
# 计算转移时间
transfer_time = volume / self._transfer_rate if time == 0 else time
self.data.update({
"status": "Running",
"from_vessel": from_vessel,
"to_vessel": to_vessel,
"current_status": "Transferring",
"progress": 0.0,
"transferred_volume": 0.0
})
# 模拟转移过程
steps = 10
step_time = transfer_time / steps
step_volume = volume / steps
for i in range(steps):
await asyncio.sleep(step_time)
progress = (i + 1) / steps * 100
current_volume = step_volume * (i + 1)
self.data.update({
"progress": progress,
"transferred_volume": current_volume,
"current_status": f"Transferring: {progress:.1f}%"
})
self.logger.info(f"Transfer progress: {progress:.1f}%")
self.data.update({
"status": "Idle",
"current_status": "Transfer completed",
"progress": 100.0,
"transferred_volume": volume
})
return True
async def clean_vessel(self, vessel: str, solvent: str, volume: float,
temp: float, repeats: int = 1) -> bool:
"""Execute vessel cleaning operation - matches CleanVessel action"""
self.logger.info(f"Starting vessel cleaning: {vessel} with {solvent} ({volume}mL at {temp}°C, {repeats} repeats)")
# 更新设备状态
self.data.update({
"status": "Running",
"from_vessel": f"flask_{solvent}",
"to_vessel": vessel,
"current_status": "Cleaning in progress",
"progress": 0.0,
"transferred_volume": 0.0
})
# 计算清洗时间(基于体积和重复次数)
# 假设清洗速度为 transfer_rate 的一半(因为需要加载和排放)
cleaning_rate = self._transfer_rate / 2
cleaning_time_per_cycle = volume / cleaning_rate
total_cleaning_time = cleaning_time_per_cycle * repeats
# 模拟清洗过程
steps_per_repeat = 10 # 每次重复清洗分10个步骤
total_steps = steps_per_repeat * repeats
step_time = total_cleaning_time / total_steps
for repeat in range(repeats):
self.logger.info(f"Starting cleaning cycle {repeat + 1}/{repeats}")
for step in range(steps_per_repeat):
await asyncio.sleep(step_time)
# 计算当前进度
current_step = repeat * steps_per_repeat + step + 1
progress = (current_step / total_steps) * 100
# 计算已处理的体积
volume_processed = (current_step / total_steps) * volume * repeats
# 更新状态
self.data.update({
"progress": progress,
"transferred_volume": volume_processed,
"current_status": f"Cleaning cycle {repeat + 1}/{repeats} - Step {step + 1}/{steps_per_repeat} ({progress:.1f}%)"
})
self.logger.info(f"Cleaning progress: {progress:.1f}% (Cycle {repeat + 1}/{repeats})")
# 清洗完成
self.data.update({
"status": "Idle",
"current_status": "Cleaning completed successfully",
"progress": 100.0,
"transferred_volume": volume * repeats,
"from_vessel": "",
"to_vessel": ""
})
self.logger.info(f"Vessel cleaning completed: {vessel}")
return True
# 状态属性
@property
def status(self) -> str:
return self.data.get("status", "Unknown")
@property
def valve_position(self) -> int:
return self.data.get("valve_position", 0)
@property
def current_volume(self) -> float:
return self.data.get("current_volume", 0.0)
@property
def max_volume(self) -> float:
return self.data.get("max_volume", 0.0)
@property
def transfer_rate(self) -> float:
return self.data.get("transfer_rate", 0.0)
@property
def from_vessel(self) -> str:
return self.data.get("from_vessel", "")
@property
def to_vessel(self) -> str:
return self.data.get("to_vessel", "")
@property
def progress(self) -> float:
return self.data.get("progress", 0.0)
@property
def transferred_volume(self) -> float:
return self.data.get("transferred_volume", 0.0)
@property
def current_status(self) -> str:
return self.data.get("current_status", "Ready")

View File

@@ -0,0 +1,104 @@
import asyncio
import logging
from typing import Dict, Any
class VirtualStirrer:
"""Virtual stirrer device for StirProtocol testing"""
def __init__(self, device_id: str = None, config: 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_stirrer"
self.config = config or {}
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)
# 处理其他kwargs参数但跳过已知的配置参数
skip_keys = {'port', 'max_temp', 'max_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"
})
return True
async def cleanup(self) -> bool:
"""Cleanup virtual stirrer"""
self.logger.info(f"Cleaning up virtual stirrer {self.device_id}")
return True
async def stir(self, stir_time: float, stir_speed: float, settling_time: float) -> bool:
"""Execute stir action - matches Stir action exactly"""
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} 超出范围"
return False
# 开始搅拌
self.data["status"] = f"搅拌中: {stir_speed} RPM, {stir_time}s"
# 模拟搅拌时间
simulation_time = min(stir_time, 10.0) # 最多等待10秒用于测试
await asyncio.sleep(simulation_time)
# 搅拌完成,开始沉降
if settling_time > 0:
self.data["status"] = f"沉降中: {settling_time}s"
settling_simulation = min(settling_time, 5.0) # 最多等待5秒
await asyncio.sleep(settling_simulation)
# 操作完成
self.data["status"] = "搅拌完成"
self.logger.info(f"Stir completed: {stir_speed} RPM for {stir_time}s")
return True
async def start_stir(self, vessel: str, stir_speed: float, purpose: str) -> bool:
"""Start stir action - matches StartStir action exactly"""
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} 超出范围"
return False
self.data["status"] = f"开始搅拌: {vessel} at {stir_speed} RPM"
return True
async def stop_stir(self, vessel: str) -> bool:
"""Stop stir action - matches StopStir action exactly"""
self.logger.info(f"StopStir: vessel={vessel}")
self.data["status"] = f"停止搅拌: {vessel}"
return True
# 状态属性 - 只保留 action 中定义的 feedback
@property
def status(self) -> str:
return self.data.get("status", "Idle")

View File

@@ -0,0 +1,149 @@
import asyncio
import logging
from typing import Dict, Any, Optional
class VirtualTransferPump:
"""Virtual pump device specifically for Transfer protocol"""
def __init__(self, device_id: str = None, config: 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_transfer_pump"
self.config = config or {}
self.logger = logging.getLogger(f"VirtualTransferPump.{self.device_id}")
self.data = {}
# 添加调试信息
print(f"=== VirtualTransferPump {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_volume = self.config.get('max_volume') or kwargs.get('max_volume', 50.0)
self._transfer_rate = self.config.get('transfer_rate') or kwargs.get('transfer_rate', 5.0)
self._current_volume = 0.0
self.is_running = False
async def initialize(self) -> bool:
"""Initialize virtual transfer pump"""
print(f"=== VirtualTransferPump {self.device_id} initialize() called! ===")
self.logger.info(f"Initializing virtual transfer pump {self.device_id}")
self.data.update({
"status": "Idle",
"current_volume": 0.0,
"max_volume": self._max_volume,
"transfer_rate": self._transfer_rate,
"from_vessel": "",
"to_vessel": "",
"progress": 0.0,
"transferred_volume": 0.0,
"current_status": "Ready"
})
return True
async def cleanup(self) -> bool:
"""Cleanup virtual transfer pump"""
self.logger.info(f"Cleaning up virtual transfer pump {self.device_id}")
return True
async def transfer(self, from_vessel: str, to_vessel: str, volume: float,
amount: str = "", time: float = 0, viscous: bool = False,
rinsing_solvent: str = "", rinsing_volume: float = 0.0,
rinsing_repeats: int = 0, solid: bool = False) -> bool:
"""Execute liquid transfer - matches Transfer action"""
self.logger.info(f"Transfer: {volume}mL from {from_vessel} to {to_vessel}")
# 计算转移时间
if time > 0:
transfer_time = time
else:
# 如果是粘性液体,降低转移速率
rate = self._transfer_rate * 0.5 if viscous else self._transfer_rate
transfer_time = volume / rate
self.data.update({
"status": "Running",
"from_vessel": from_vessel,
"to_vessel": to_vessel,
"current_status": "Transferring",
"progress": 0.0,
"transferred_volume": 0.0
})
# 模拟转移过程
steps = 10
step_time = transfer_time / steps
step_volume = volume / steps
for i in range(steps):
await asyncio.sleep(step_time)
progress = (i + 1) / steps * 100
transferred = (i + 1) * step_volume
self.data.update({
"progress": progress,
"transferred_volume": transferred,
"current_status": f"Transferring {progress:.1f}%"
})
self.logger.info(f"Transfer progress: {progress:.1f}% ({transferred:.1f}/{volume}mL)")
# 如果需要冲洗
if rinsing_solvent and rinsing_volume > 0 and rinsing_repeats > 0:
self.data["current_status"] = "Rinsing"
for repeat in range(rinsing_repeats):
self.logger.info(f"Rinsing cycle {repeat + 1}/{rinsing_repeats} with {rinsing_solvent}")
await asyncio.sleep(1) # 模拟冲洗时间
self.data.update({
"status": "Idle",
"current_status": "Transfer completed",
"progress": 100.0,
"transferred_volume": volume
})
return True
# 添加所有在virtual_device.yaml中定义的状态属性
@property
def status(self) -> str:
return self.data.get("status", "Unknown")
@property
def current_volume(self) -> float:
return self.data.get("current_volume", 0.0)
@property
def max_volume(self) -> float:
return self.data.get("max_volume", self._max_volume)
@property
def transfer_rate(self) -> float:
return self.data.get("transfer_rate", self._transfer_rate)
@property
def from_vessel(self) -> str:
return self.data.get("from_vessel", "")
@property
def to_vessel(self) -> str:
return self.data.get("to_vessel", "")
@property
def progress(self) -> float:
return self.data.get("progress", 0.0)
@property
def transferred_volume(self) -> float:
return self.data.get("transferred_volume", 0.0)
@property
def current_status(self) -> str:
return self.data.get("current_status", "Ready")

View File

@@ -0,0 +1,105 @@
import asyncio
import logging
from typing import Dict, Any
class VirtualValve:
"""Virtual valve device for AddProtocol testing"""
def __init__(self, device_id: str = None, config: 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_valve"
self.config = config or {}
self.logger = logging.getLogger(f"VirtualValve.{self.device_id}")
self.data = {}
print(f"=== VirtualValve {self.device_id} is being created! ===")
print(f"=== Config: {self.config} ===")
print(f"=== Kwargs: {kwargs} ===")
# 处理所有配置参数包括port
self.port = self.config.get('port', 'VIRTUAL')
self.positions = self.config.get('positions', 6)
self.current_position = 0
# 忽略其他可能的kwargs参数
for key, value in kwargs.items():
if not hasattr(self, key):
setattr(self, key, value)
async def initialize(self) -> bool:
"""Initialize virtual valve"""
print(f"=== VirtualValve {self.device_id} initialize() called! ===")
self.logger.info(f"Initializing virtual valve {self.device_id}")
self.data.update({
"status": "Idle",
"valve_state": "Closed",
"current_position": 0,
"target_position": 0,
"max_positions": self.positions
})
return True
async def cleanup(self) -> bool:
"""Cleanup virtual valve"""
self.logger.info(f"Cleaning up virtual valve {self.device_id}")
return True
async def set_position(self, position: int) -> bool:
"""Set valve position - matches SendCmd action"""
if 0 <= position <= self.positions:
self.logger.info(f"Setting valve position to {position}")
self.data.update({
"target_position": position,
"current_position": position,
"valve_state": "Open" if position > 0 else "Closed"
})
return True
else:
self.logger.error(f"Invalid position {position}. Must be 0-{self.positions}")
return False
async def open(self) -> bool:
"""Open valve - matches EmptyIn action"""
self.logger.info("Opening valve")
self.data.update({
"valve_state": "Open",
"current_position": 1
})
return True
async def close(self) -> bool:
"""Close valve - matches EmptyIn action"""
self.logger.info("Closing valve")
self.data.update({
"valve_state": "Closed",
"current_position": 0
})
return True
# 状态属性
@property
def status(self) -> str:
return self.data.get("status", "Unknown")
@property
def valve_state(self) -> str:
return self.data.get("valve_state", "Unknown")
@property
def current_position(self) -> int:
return self.data.get("current_position", 0)
@property
def target_position(self) -> int:
return self.data.get("target_position", 0)
@property
def max_positions(self) -> int:
return self.data.get("max_positions", 6)

View File

@@ -67,7 +67,115 @@ class AGVTransferProtocol(BaseModel):
to_repo: dict to_repo: dict
from_repo_position: str from_repo_position: str
to_repo_position: str to_repo_position: str
#=============新添加的新的协议================
class AddProtocol(BaseModel):
vessel: str
reagent: str
volume: float
mass: float
amount: str
time: float
stir: bool
stir_speed: float
viscous: bool
purpose: str
class CentrifugeProtocol(BaseModel):
vessel: str
speed: float
time: float
temp: float # 移除默认值
__all__ = ["Point3D", "PumpTransferProtocol", "CleanProtocol", "SeparateProtocol", "EvaporateProtocol", "EvacuateAndRefillProtocol", "AGVTransferProtocol"] class FilterProtocol(BaseModel):
vessel: str
filtrate_vessel: str # 移除默认值
stir: bool # 移除默认值
stir_speed: float # 移除默认值
temp: float # 移除默认值
continue_heatchill: bool # 移除默认值
volume: float # 移除默认值
class HeatChillProtocol(BaseModel):
vessel: str
temp: float
time: float
stir: bool
stir_speed: float
purpose: str
class HeatChillStartProtocol(BaseModel):
vessel: str
temp: float
purpose: str
class HeatChillStopProtocol(BaseModel):
vessel: str
class StirProtocol(BaseModel):
stir_time: float
stir_speed: float
settling_time: float
class StartStirProtocol(BaseModel):
vessel: str
stir_speed: float
purpose: str
class StopStirProtocol(BaseModel):
vessel: str
class TransferProtocol(BaseModel):
from_vessel: str
to_vessel: str
volume: float
amount: str = ""
time: float = 0
viscous: bool = False
rinsing_solvent: str = ""
rinsing_volume: float = 0.0
rinsing_repeats: int = 0
solid: bool = False
class CleanVesselProtocol(BaseModel):
vessel: str # 要清洗的容器名称
solvent: str # 用于清洗容器的溶剂名称
volume: float # 清洗溶剂的体积,可选参数
temp: float # 清洗时的温度,可选参数
repeats: int = 1 # 清洗操作的重复次数,默认为 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 # 搅拌速度,可选参数
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 # 物质在过滤介质中的停留时间,可选参数
class RunColumnProtocol(BaseModel):
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
__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 # End Protocols

View File

@@ -28,3 +28,22 @@ gas_source.mock:
class: class:
module: unilabos.devices.pump_and_valve.vacuum_pump_mock:VacuumPumpMock module: unilabos.devices.pump_and_valve.vacuum_pump_mock:VacuumPumpMock
type: python type: python
status_types:
status: String
action_value_mappings:
open:
type: EmptyIn
goal: {}
feedback: {}
result: {}
close:
type: EmptyIn
goal: {}
feedback: {}
result: {}
set_status:
type: StrSingleInput
goal:
string: string
feedback: {}
result: {}

View File

@@ -0,0 +1,388 @@
virtual_pump:
description: Virtual Pump for PumpTransferProtocol Testing
class:
module: unilabos.devices.virtual.virtual_pump:VirtualPump
type: python
status_types:
status: String
position: Float64
valve_position: Int32 # 修复:使用 Int32 而不是 String
max_volume: Float64
current_volume: Float64
action_value_mappings:
transfer:
type: PumpTransfer
goal:
from_vessel: from_vessel
to_vessel: to_vessel
volume: volume
amount: amount
time: time
viscous: viscous
rinsing_solvent: rinsing_solvent
rinsing_volume: rinsing_volume
rinsing_repeats: rinsing_repeats
solid: solid
feedback:
status: status
result:
success: success
set_valve_position:
type: FloatSingleInput
goal:
Int32: Int32
feedback:
status: status
result:
success: success
schema:
type: object
properties:
port:
type: string
default: "VIRTUAL"
max_volume:
type: number
default: 25.0
additionalProperties: false
virtual_stirrer:
description: Virtual Stirrer for StirProtocol Testing
class:
module: unilabos.devices.virtual.virtual_stirrer:VirtualStirrer
type: python
status_types:
status: String
action_value_mappings:
stir:
type: Stir
goal:
stir_time: stir_time
stir_speed: stir_speed
settling_time: settling_time
feedback:
status: status
result:
success: success
start_stir:
type: StartStir
goal:
vessel: vessel
stir_speed: stir_speed
purpose: purpose
feedback:
status: status
result:
success: success
stop_stir:
type: StopStir
goal:
vessel: vessel
feedback:
status: status
result:
success: success
schema:
type: object
properties:
port:
type: string
default: "VIRTUAL"
max_temp:
type: number
default: 100.0
max_speed:
type: number
default: 1000.0
additionalProperties: false
virtual_valve:
description: Virtual Valve for AddProtocol Testing
class:
module: unilabos.devices.virtual.virtual_valve:VirtualValve
type: python
status_types:
status: String
valve_state: String
current_position: Int32
target_position: Int32
max_positions: Int32
action_value_mappings:
set_position:
type: SendCmd
goal:
command: position
feedback: {}
result:
success: success
open:
type: EmptyIn
goal: {}
feedback: {}
result:
success: success
close:
type: EmptyIn
goal: {}
feedback: {}
result:
success: success
schema:
type: object
properties:
port:
type: string
default: "VIRTUAL"
positions:
type: integer
default: 6
additionalProperties: false
virtual_centrifuge:
description: Virtual Centrifuge for CentrifugeProtocol Testing
class:
module: unilabos.devices.virtual.virtual_centrifuge:VirtualCentrifuge
type: python
status_types:
status: String
current_speed: Float64
target_speed: Float64
current_temp: Float64
target_temp: Float64
max_speed: Float64
max_temp: Float64
min_temp: Float64
centrifuge_state: String
time_remaining: Float64
action_value_mappings:
centrifuge:
type: Centrifuge
goal:
vessel: vessel
speed: speed
time: time
temp: temp
feedback:
progress: progress
current_speed: current_speed
current_temp: current_temp
current_status: status
result:
success: success
message: message
schema:
type: object
properties:
port:
type: string
default: "VIRTUAL"
max_speed:
type: number
default: 15000.0
max_temp:
type: number
default: 40.0
min_temp:
type: number
default: 4.0
additionalProperties: false
virtual_filter:
description: Virtual Filter for FilterProtocol Testing
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
message: String
action_value_mappings:
filter_sample:
type: Filter
goal:
vessel: vessel
filtrate_vessel: filtrate_vessel
stir: stir
stir_speed: stir_speed
temp: temp
continue_heatchill: continue_heatchill
volume: volume
feedback:
progress: progress
current_temp: current_temp
filtered_volume: filtered_volume
current_status: status
result:
success: success
message: message
schema:
type: object
properties:
port:
type: string
default: "VIRTUAL"
max_temp:
type: number
default: 100.0
max_stir_speed:
type: number
default: 1000.0
additionalProperties: false
virtual_heatchill:
description: Virtual HeatChill for HeatChillProtocol Testing
class:
module: unilabos.devices.virtual.virtual_heatchill:VirtualHeatChill
type: python
status_types:
status: String
action_value_mappings:
heat_chill:
type: HeatChill
goal:
vessel: vessel
temp: temp
time: time
stir: stir
stir_speed: stir_speed
purpose: purpose
feedback:
status: status
result:
success: success
heat_chill_start:
type: HeatChillStart
goal:
vessel: vessel
temp: temp
purpose: purpose
feedback:
status: status
result:
success: success
heat_chill_stop:
type: HeatChillStop
goal:
vessel: vessel
feedback:
status: status
result:
success: success
schema:
type: object
properties:
port:
type: string
default: "VIRTUAL"
max_temp:
type: number
default: 200.0
min_temp:
type: number
default: -80.0
max_stir_speed:
type: number
default: 1000.0
additionalProperties: false
virtual_transfer_pump:
description: Virtual Transfer Pump for TransferProtocol Testing
class:
module: unilabos.devices.virtual.virtual_transferpump:VirtualTransferPump
type: python
status_types:
status: String
current_volume: Float64
max_volume: Float64
transfer_rate: Float64
from_vessel: String
to_vessel: String
progress: Float64
transferred_volume: Float64
current_status: String
action_value_mappings:
transfer:
type: Transfer
goal:
from_vessel: from_vessel
to_vessel: to_vessel
volume: volume
amount: amount
time: time
viscous: viscous
rinsing_solvent: rinsing_solvent
rinsing_volume: rinsing_volume
rinsing_repeats: rinsing_repeats
solid: solid
feedback:
progress: progress
transferred_volume: transferred_volume
current_status: current_status
result:
success: success
message: message
schema:
type: object
properties:
port:
type: string
default: "VIRTUAL"
max_volume:
type: number
default: 50.0
transfer_rate:
type: number
default: 5.0
additionalProperties: false
virtual_column:
description: Virtual Column for RunColumn Protocol Testing
class:
module: unilabos.devices.virtual.virtual_column:VirtualColumn
type: python
status_types:
status: String
column_state: String
current_flow_rate: Float64
max_flow_rate: Float64
column_length: Float64
column_diameter: Float64
processed_volume: Float64
progress: Float64
current_status: String
action_value_mappings:
run_column:
type: RunColumn
goal:
from_vessel: from_vessel
to_vessel: to_vessel
column: column
feedback:
status: current_status
progress: progress
result:
success: success
message: message
schema:
type: object
properties:
port:
type: string
default: "VIRTUAL"
max_flow_rate:
type: number
default: 10.0
column_length:
type: number
default: 25.0
column_diameter:
type: number
default: 2.0
additionalProperties: false

View File

@@ -24,6 +24,12 @@ find_package(geometry_msgs REQUIRED)
set(action_files set(action_files
"action/SendCmd.action" "action/SendCmd.action"
"action/CleanVessel.action"
"action/Dissolve.action"
"action/FilterThrough.action"
"action/RunColumn.action"
"action/Wait.action"
"action/WashSolid.action"
"action/Stir.action" "action/Stir.action"
"action/HeatChill.action" "action/HeatChill.action"
"action/HeatChillStart.action" "action/HeatChillStart.action"

View File

@@ -0,0 +1,11 @@
string vessel # 要清洗的容器名称
string solvent # 用于清洗容器的溶剂名称
float64 volume # 清洗溶剂的体积,可选参数
float64 temp # 清洗时的温度,可选参数
int32 repeats # 清洗操作的重复次数,默认为 1
---
bool success # 操作是否成功
string message # 结果消息
---
string status # 当前状态描述
float64 progress # 进度百分比 (0-100)

View File

@@ -0,0 +1,13 @@
string vessel # 装有要溶解物质的容器名称
string solvent # 用于溶解物质的溶剂名称
float64 volume # 溶剂的体积,可选参数
string amount # 要溶解物质的量,可选参数
float64 temp # 溶解时的温度,可选参数
float64 time # 溶解的时间,可选参数
float64 stir_speed # 搅拌速度,可选参数
---
bool success # 操作是否成功
string message # 结果消息
---
string status # 当前状态描述
float64 progress # 进度百分比 (0-100)

View File

@@ -0,0 +1,13 @@
string from_vessel # 源容器的名称,即物质起始所在的容器
string to_vessel # 目标容器的名称,物质过滤后要到达的容器
string filter_through # 过滤时所通过的介质,如滤纸、柱子等
string eluting_solvent # 洗脱溶剂的名称,可选参数
float64 eluting_volume # 洗脱溶剂的体积,可选参数
int32 eluting_repeats # 洗脱操作的重复次数,默认为 0
float64 residence_time # 物质在过滤介质中的停留时间,可选参数
---
bool success # 操作是否成功
string message # 结果消息
---
string status # 当前状态描述
float64 progress # 进度百分比 (0-100)

View File

@@ -0,0 +1,9 @@
string from_vessel # 源容器的名称,即样品起始所在的容器
string to_vessel # 目标容器的名称,分离后的样品要到达的容器
string column # 所使用的柱子的名称
---
bool success # 操作是否成功
string message # 结果消息
---
string status # 当前状态描述
float64 progress # 进度百分比 (0-100)

View File

@@ -0,0 +1,8 @@
int32 time # 等待时间(秒)
---
bool success # 操作是否成功
string message # 结果消息
---
string status # 当前状态描述
float64 progress # 进度百分比 (0-100)
int32 remaining_time # 剩余等待时间(秒)

View File

@@ -0,0 +1,15 @@
string vessel # 装有固体物质的容器名称
string solvent # 用于清洗固体的溶剂名称
float64 volume # 清洗溶剂的体积
string filtrate_vessel # 滤液要收集到的容器名称,可选参数
float64 temp # 清洗时的温度,可选参数
bool stir # 是否在清洗过程中搅拌,默认为 False
float64 stir_speed # 搅拌速度,可选参数
float64 time # 清洗的时间,可选参数
int32 repeats # 清洗操作的重复次数,默认为 1
---
bool success # 操作是否成功
string message # 结果消息
---
string status # 当前状态描述
float64 progress # 进度百分比 (0-100)