规范化xml内容,并在xml中添加仿真设置,规范化版本号等内容,补充xml规范md文件
This commit is contained in:
1 parent
c52266ba11
commit
104d41d294
6 files changed
+254
-2
No files matched your search
@@ -1,2 +1,9 @@
|
|||||||
# SystemSimulationApp
|
# SystemSimulationApp
|
||||||
|
|
||||||
|
ReactFlow 系统建模与 PythonModels 仿真应用。
|
||||||
|
|
||||||
|
## 文档
|
||||||
|
|
||||||
|
- [System XML v1 协议](docs/system-xml-v1.md)
|
||||||
|
- [System XML v1 XSD](schemas/system-simulation-v1.xsd)
|
||||||
|
|
||||||
+21
-1
@@ -14,6 +14,8 @@ from pydantic import BaseModel, Field
|
|||||||
app = FastAPI(title="System Simulation ReactFlow App")
|
app = FastAPI(title="System Simulation ReactFlow App")
|
||||||
FRONTEND_DIST_DIR = Path(__file__).resolve().parent.parent / "frontend" / "dist"
|
FRONTEND_DIST_DIR = Path(__file__).resolve().parent.parent / "frontend" / "dist"
|
||||||
PROJECT_STORAGE_DIR = Path(__file__).parent / "data" / "reactflow-projects"
|
PROJECT_STORAGE_DIR = Path(__file__).parent / "data" / "reactflow-projects"
|
||||||
|
SYSTEM_XML_SCHEMA_VERSION = "1"
|
||||||
|
SYSTEM_XML_UNIT_SYSTEM = "SI"
|
||||||
|
|
||||||
|
|
||||||
class ReactFlowPosition(BaseModel):
|
class ReactFlowPosition(BaseModel):
|
||||||
@@ -190,7 +192,25 @@ def simulate_reactflow_testmodel(payload: ReactFlowProjectPayload) -> dict[str,
|
|||||||
|
|
||||||
|
|
||||||
def build_reactflow_system_xml(project: ReactFlowProjectPayload) -> bytes:
|
def build_reactflow_system_xml(project: ReactFlowProjectPayload) -> bytes:
|
||||||
system = ET.Element("System", {"name": project.name})
|
system = ET.Element(
|
||||||
|
"System",
|
||||||
|
{
|
||||||
|
"name": project.name,
|
||||||
|
"schemaVersion": SYSTEM_XML_SCHEMA_VERSION,
|
||||||
|
"unitSystem": SYSTEM_XML_UNIT_SYSTEM,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ET.SubElement(
|
||||||
|
system,
|
||||||
|
"Simulation",
|
||||||
|
{
|
||||||
|
"tStart": str(project.simulation.t_start),
|
||||||
|
"tStop": str(project.simulation.t_stop),
|
||||||
|
"step": str(project.simulation.step),
|
||||||
|
"maxStep": str(project.simulation.max_step),
|
||||||
|
"method": project.simulation.method,
|
||||||
|
},
|
||||||
|
)
|
||||||
components_node = ET.SubElement(system, "Components")
|
components_node = ET.SubElement(system, "Components")
|
||||||
connections_node = ET.SubElement(system, "Connections")
|
connections_node = ET.SubElement(system, "Connections")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# System XML v1 协议
|
||||||
|
|
||||||
|
System XML 是 ReactFlow 前端与 PythonModels 仿真层之间的稳定交换格式。
|
||||||
|
|
||||||
|
## 基本约定
|
||||||
|
|
||||||
|
- 根元素固定为 `System`,`schemaVersion` 固定为 `1`。
|
||||||
|
- `unitSystem` 固定为 `SI`。组件参数必须保存为 SI 基准值,界面显示单位不进入数值换算语义。
|
||||||
|
- 子元素顺序固定为 `Simulation`、`Components`、`Connections`。
|
||||||
|
- `Component.id` 是稳定的仿真实例 ID;`name` 是用户可编辑的显示名称。
|
||||||
|
- `Component.type` 是 PythonModels 模型类型;`componentType` 是前端组件类型。
|
||||||
|
- `Connection.source/target` 只标识图形拓扑端点,不代表仿真中的实际流动方向。正流和倒流由求解器决定。
|
||||||
|
- v1 文档不携带结果数据;仿真结果通过运行接口返回。
|
||||||
|
|
||||||
|
## 完整示例
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<System name="transfer-system" schemaVersion="1" unitSystem="SI">
|
||||||
|
<Simulation tStart="0" tStop="2" step="0.1" maxStep="0.005" method="BDF"/>
|
||||||
|
<Components>
|
||||||
|
<Component id="cylinder_1" name="cylinder_1" type="cylinder" componentType="cylinder" x="90" y="180">
|
||||||
|
<Port name="port_b"/>
|
||||||
|
<Parameter name="volume" value="0.01"/>
|
||||||
|
<Parameter name="p0" value="35000000"/>
|
||||||
|
<Parameter name="T0" value="300"/>
|
||||||
|
</Component>
|
||||||
|
<Component id="tank_1" name="tank_1" type="tank" componentType="tank" x="420" y="180">
|
||||||
|
<Port name="port_a"/>
|
||||||
|
<Parameter name="volume" value="0.1"/>
|
||||||
|
<Parameter name="p0" value="100000"/>
|
||||||
|
<Parameter name="T0" value="300"/>
|
||||||
|
</Component>
|
||||||
|
</Components>
|
||||||
|
<Connections>
|
||||||
|
<Connection id="edge-1" source="cylinder_1" sourcePort="port_b" target="tank_1" targetPort="port_a"/>
|
||||||
|
</Connections>
|
||||||
|
</System>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Simulation
|
||||||
|
|
||||||
|
| 属性 | 类型 | 含义 | 约束 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `tStart` | double | 仿真起始时间,单位 s | 有限数 |
|
||||||
|
| `tStop` | double | 仿真结束时间,单位 s | 必须大于 `tStart` |
|
||||||
|
| `step` | double | 结果采样间隔,单位 s | 必须大于 0 |
|
||||||
|
| `maxStep` | double | 求解器最大积分步长,单位 s | 必须大于 0 |
|
||||||
|
| `method` | string | 求解器名称,例如 `BDF` | 非空 |
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
每个 `Component` 必须包含唯一 `id`、显示名称、模型类型、前端类型和画布坐标。组件下可以包含多个 `Port` 和 `Parameter`。
|
||||||
|
|
||||||
|
参数的 `value` 必须是可转换为有限浮点数的 SI 值。具体组件支持的端口、参数和取值范围由后续组件注册表协议约束。
|
||||||
|
|
||||||
|
## Connections
|
||||||
|
|
||||||
|
每个 `Connection` 必须包含唯一 `id` 和两个完整端点:
|
||||||
|
|
||||||
|
- `source` + `sourcePort`
|
||||||
|
- `target` + `targetPort`
|
||||||
|
|
||||||
|
端点中的组件 ID 必须存在,端口名称必须属于对应组件。v1 前端仍使用 source/target 组织连线,但仿真层必须将它们视为连接的两个端点,不得直接解释为固定物理流向。
|
||||||
|
|
||||||
|
## 版本兼容
|
||||||
|
|
||||||
|
- 解析器遇到缺少 `schemaVersion` 的旧 XML 时,应明确报告“旧协议”,不能静默当作 v1。
|
||||||
|
- v1 新增可选字段时必须保持旧解析器可忽略;删除字段或改变字段语义时必须升级主版本。
|
||||||
|
- XSD 只验证文档结构和基础数据类型。跨字段约束、组件端口规则和拓扑可求解性由仿真校验层负责。
|
||||||
@@ -1814,10 +1814,18 @@ function buildSystemXml(project: ReactFlowProjectPayload) {
|
|||||||
const xmlDocument = document.implementation.createDocument("", "System", null);
|
const xmlDocument = document.implementation.createDocument("", "System", null);
|
||||||
const system = xmlDocument.documentElement;
|
const system = xmlDocument.documentElement;
|
||||||
system.setAttribute("name", project.name);
|
system.setAttribute("name", project.name);
|
||||||
|
system.setAttribute("schemaVersion", "1");
|
||||||
|
system.setAttribute("unitSystem", "SI");
|
||||||
|
|
||||||
|
const simulation = xmlDocument.createElement("Simulation");
|
||||||
|
simulation.setAttribute("tStart", String(project.simulation.t_start));
|
||||||
|
simulation.setAttribute("tStop", String(project.simulation.t_stop));
|
||||||
|
simulation.setAttribute("step", String(project.simulation.step));
|
||||||
|
simulation.setAttribute("maxStep", String(project.simulation.max_step));
|
||||||
|
simulation.setAttribute("method", project.simulation.method);
|
||||||
const components = xmlDocument.createElement("Components");
|
const components = xmlDocument.createElement("Components");
|
||||||
const connections = xmlDocument.createElement("Connections");
|
const connections = xmlDocument.createElement("Connections");
|
||||||
system.append(components, connections);
|
system.append(simulation, components, connections);
|
||||||
|
|
||||||
project.nodes.forEach((node) => {
|
project.nodes.forEach((node) => {
|
||||||
const component = xmlDocument.createElement("Component");
|
const component = xmlDocument.createElement("Component");
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
|
||||||
|
<xs:simpleType name="NonEmptyString">
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:minLength value="1"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
|
||||||
|
<xs:simpleType name="PositiveDouble">
|
||||||
|
<xs:restriction base="xs:double">
|
||||||
|
<xs:minExclusive value="0"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
|
||||||
|
<xs:element name="System">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="Simulation">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:attribute name="tStart" type="xs:double" use="required"/>
|
||||||
|
<xs:attribute name="tStop" type="xs:double" use="required"/>
|
||||||
|
<xs:attribute name="step" type="PositiveDouble" use="required"/>
|
||||||
|
<xs:attribute name="maxStep" type="PositiveDouble" use="required"/>
|
||||||
|
<xs:attribute name="method" type="NonEmptyString" use="required"/>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
|
||||||
|
<xs:element name="Components">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="Component" minOccurs="0" maxOccurs="unbounded">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="Port" minOccurs="0" maxOccurs="unbounded">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:attribute name="name" type="NonEmptyString" use="required"/>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="Parameter" minOccurs="0" maxOccurs="unbounded">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:attribute name="name" type="NonEmptyString" use="required"/>
|
||||||
|
<xs:attribute name="value" type="xs:double" use="required"/>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
<xs:attribute name="id" type="NonEmptyString" use="required"/>
|
||||||
|
<xs:attribute name="name" type="NonEmptyString" use="required"/>
|
||||||
|
<xs:attribute name="type" type="NonEmptyString" use="required"/>
|
||||||
|
<xs:attribute name="componentType" type="NonEmptyString" use="required"/>
|
||||||
|
<xs:attribute name="x" type="xs:double" use="required"/>
|
||||||
|
<xs:attribute name="y" type="xs:double" use="required"/>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
|
||||||
|
<xs:element name="Connections">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="Connection" minOccurs="0" maxOccurs="unbounded">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:attribute name="id" type="NonEmptyString" use="required"/>
|
||||||
|
<xs:attribute name="source" type="NonEmptyString" use="required"/>
|
||||||
|
<xs:attribute name="sourcePort" type="NonEmptyString" use="required"/>
|
||||||
|
<xs:attribute name="target" type="NonEmptyString" use="required"/>
|
||||||
|
<xs:attribute name="targetPort" type="NonEmptyString" use="required"/>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
|
||||||
|
<xs:attribute name="name" type="NonEmptyString" use="required"/>
|
||||||
|
<xs:attribute name="schemaVersion" use="required" fixed="1"/>
|
||||||
|
<xs:attribute name="unitSystem" use="required" fixed="SI"/>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:schema>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from xml.etree import ElementTree as ET
|
||||||
|
|
||||||
|
from app.main import ReactFlowProjectPayload, build_reactflow_system_xml
|
||||||
|
|
||||||
|
|
||||||
|
class SystemXmlProtocolTests(unittest.TestCase):
|
||||||
|
def test_v1_xml_contains_simulation_and_stable_section_order(self) -> None:
|
||||||
|
project = ReactFlowProjectPayload(
|
||||||
|
name="protocol-test",
|
||||||
|
nodes=[
|
||||||
|
{
|
||||||
|
"id": "cylinder_1",
|
||||||
|
"type": "simulationComponent",
|
||||||
|
"position": {"x": 12.5, "y": 24.0},
|
||||||
|
"data": {
|
||||||
|
"label": "source-cylinder",
|
||||||
|
"componentType": "cylinder",
|
||||||
|
"modelType": "cylinder",
|
||||||
|
"ports": ["port_b"],
|
||||||
|
"parameters": {"volume": 0.01, "p0": 35000000, "T0": 300},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
edges=[],
|
||||||
|
simulation={
|
||||||
|
"t_start": 1.0,
|
||||||
|
"t_stop": 5.0,
|
||||||
|
"step": 0.2,
|
||||||
|
"max_step": 0.01,
|
||||||
|
"method": "BDF",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
root = ET.fromstring(build_reactflow_system_xml(project))
|
||||||
|
|
||||||
|
self.assertEqual(root.tag, "System")
|
||||||
|
self.assertEqual(root.attrib["name"], "protocol-test")
|
||||||
|
self.assertEqual(root.attrib["schemaVersion"], "1")
|
||||||
|
self.assertEqual(root.attrib["unitSystem"], "SI")
|
||||||
|
self.assertEqual([child.tag for child in root], ["Simulation", "Components", "Connections"])
|
||||||
|
|
||||||
|
simulation = root.find("Simulation")
|
||||||
|
self.assertIsNotNone(simulation)
|
||||||
|
assert simulation is not None
|
||||||
|
self.assertEqual(
|
||||||
|
simulation.attrib,
|
||||||
|
{
|
||||||
|
"tStart": "1.0",
|
||||||
|
"tStop": "5.0",
|
||||||
|
"step": "0.2",
|
||||||
|
"maxStep": "0.01",
|
||||||
|
"method": "BDF",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
component = root.find("./Components/Component")
|
||||||
|
self.assertIsNotNone(component)
|
||||||
|
assert component is not None
|
||||||
|
self.assertEqual(component.attrib["id"], "cylinder_1")
|
||||||
|
self.assertEqual(component.find("./Parameter[@name='p0']").attrib["value"], "35000000")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in new issue
Block a user