Files
SystemSimulationApp/docs/接口类型与表示方式总结.md
T

642 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SystemSimulationApp 接口类型与表示方式总结(通俗版)
> 调研基线:2026-08-12(System XML v3 接口基线)。本文依据当前仓库的代码、Schema、说明文档和测试编写。
> 这里的“接口”主要指组件上的端口(port/connector),不是只指 HTTP API。文末也单独列出了相关 HTTP API。
## 0. 三分钟读懂
先把整个系统想成一张“可以计算的工程图”:
- **组件**像气瓶、管路、阀门、质量块等设备;
- **端口**像设备上的接头或插座;
- **连接**像气管、机械连接杆或控制线;
- **编译**像正式计算前的接线检查:插头是否匹配、有没有漏接、方向是否正确;
- **求解**才是真正计算每个时刻的压力、流量、位移、速度等数值。
当前项目实际只有三类端口:
| 看到的类型 | 可以把它理解成 | 主要传递什么 |
| --- | --- | --- |
| `physical / pneumatic` | 气路接头 | 压力、质量流量、气体携带的能量 |
| `physical / mechanical` | 机械连接点 | 位移、速度、力 |
| `signal / signal` | 控制线 | 一个有方向的数值,例如阀门开度或目标力 |
最容易混淆的四种文件/数据,可以这样记:
| 数据 | 通俗比喻 | 它回答的问题 |
| --- | --- | --- |
| 组件目录 JSON | 产品说明书 | 某种组件天生有哪些端口、每个端口有哪些变量? |
| 工程 JSON | 画布存档 | 这张图上放了哪些组件、摆在哪里、怎样连? |
| System XML v3 | 交给后端的精简求解清单 | 只带组件、模型版本、SI 参数、连接和仿真设置,不负责保存画布 |
| 编译结果 JSON | 接线检查报告 | 后端恢复完整模型后,最终认出了哪些端口、连接和方程结构? |
贯穿全文的两个例子:
```text
案例 A:气路
储气容器/气室 A ── 节流孔或管路 ── 气室 B
气动端口 气动端口
案例 B:控制
阶跃信号源 step_1.out ──控制线──> 阀 valve_1.res
│
控制气路通断/开度
也可以是:step_1.out ──控制线──> 力源 force_1.res ──机械端口── 质量块
```
这两个示意图不是凭空编造的:仓库里已有对应的回归案例。`tests/test_generic_system_xml_simulation.py:86-145` 搭建了 `cylinder(500 kPa) → orifice → pipe → tank(100 kPa)` 气路;`tests/test_amesim_pnvo001_signal_xml.py:37-84` 搭建了 `step_1.out → valve_1.res`,同时让气缸、阀和气罐通过物理端口相连。
先记住六点就能继续阅读:
1. **物理端口必须同类相连。** 气动只能接气动,机械只能接机械,不能把“气管”插到“机械接头”上。
2. **信号线有方向。** 必须连接一个注册为 `output` 的端口和一个注册为 `input` 的端口;XML v3 不再另写 `source/target` 角色。
3. **物理线没有 source/target 的物理含义。** 画布虽然要写 `source/target`,后端会把它当作无方向的两个端点。
4. **流变量统一以“进入当前组件”为正。** 因此同一条气路两端的质量流量数值互为相反数。
5. **工程 JSON 和 XML 不重复保存完整变量表。** 后端依靠组件的 `modelType/type` 去注册表找回完整定义。
6. **`side/rotation/mirrored` 都只负责画面。** 力源是否反向由显式参数 `direction=+1/-1` 决定;转动或镜像图标不再改变方程。
只想看懂工程图,可以读第 0、2、3、5、6 节;需要开发或排查兼容问题时,再读第 1、4、7~11 节。
## 1. 本文中的标签和“谁说了算”
为了避免把“已经能运行”和“文档希望如此”混为一谈,本文使用四种标签:
- **[已实现]**:当前代码或测试直接体现的行为。
- **[约定]**:Schema、类型声明或说明文档规定的合同,但不一定所有入口都完整实现。
- **[推断]**:根据多处代码可以合理得到的判断,仓库没有直接承诺或实测数据。
- **[发现]**:代码层之间不一致、容易误解或存在兼容风险的地方。
如果不同层的说法不一致,优先相信更靠上的事实来源:
| 优先级 | 事实来源 | 关键文件/符号 | 通俗解释 |
| --- | --- | --- | --- |
| 1 | 端口核心定义 | `app/simulation/core/ports.py:7-207`:`PortVariableDefinition`、`PortDefinition`、`PortState` | 定义“插头标准”和运行时数值 |
| 2 | 具体组件模型类 | `MODEL_TYPE`、`PORTS`、`DISPLAY` | 声明某个产品实际装了哪些插头 |
| 3 | 模型注册器 | `app/simulation/registry.py:41-170, 416-490, 825-980` | 启动时核对产品声明,并生成目录 |
| 4 | 网络连接层 | `app/simulation/systems/network.py:83-150`:`SimulationNetwork.connect()` | 真正接线时做最终兼容检查 |
| 5 | JSON/XML | `ReactFlowPortDefinition`、System XML v3 XSD | JSON 搬运画布和端口显示快照;XML 只搬运可执行模型,端口合同由注册表恢复 |
**[约定]** `docs/README.md:28-29` 和组件建模规范都说明:组件模型类及受控库清单是后端事实来源。XML 或前端不能凭空创造一个模型没有声明的端口。
## 2. 常见术语翻译表
第一次阅读时,可以先把英文术语替换成右侧的日常说法。
| 术语 | 通俗说法 | 在本项目中的具体意思 |
| --- | --- | --- |
| component | 设备/元件 | 气室、节流孔、阀、质量块、信号源等 |
| port / connector | 接头/插座 | 组件可以与外界连接的位置 |
| interface contract | 接口说明书 | 端口名称、类型、变量、单位和连接规则的完整定义 |
| `kind` | 大类 | `physical` 物理连接,或 `signal` 控制信号 |
| `domain` | 专业类别 | 当前为 `pneumatic` 气动、`mechanical` 机械、`signal` 信号 |
| `nominalRole` | 名义用途 | 物理端口的入口/出口提示,或信号端口的输入/输出方向 |
| `effort` | 两端要相同的“势” | 气动压力 `p`;机械位移 `x`、速度 `v` |
| `flow` | 连接处要守恒的“流” | 气动质量流量 `m_flow`;机械力 `f` |
| `stream` | 随介质流动携带的性质 | 当前是气体流出比焓 `h_outflow` |
| `equal` | 两端相等 | 例如连接后 `p_A = p_B` |
| `sumToZero` | 两端相加为零 | 例如 `m_flow_A + m_flow_B = 0` |
| `streamMix` | 按实际流向传播/混合 | 不能简单令两端 `h_outflow` 相等 |
| `directed` | 按指定方向传值 | 例如阶跃源输出写入阀的信号输入 |
| registry / catalog | 型号登记表/产品目录 | 后端支持哪些模型,以及每种模型的完整定义 |
| compile | 接线检查和模型装配 | 根据 `modelType` 实例化组件并检查所有连接 |
| resolver | 专项计算器 | 分别处理信号、气体焓、移动容积等传播问题 |
| Schema / XSD | 格式规则 | 检查 JSON/XML 的字段和结构是否合规 |
| SI | 国际单位制 | Pa、kg/s、m、N 等;提交给求解器的值使用 SI 基准值 |
## 3. 用案例理解 physical 和 signal
### 3.1 案例 A:气室经节流孔连接
假设储气容器 A 的压力高于气室 B:
```text
tank_1.port_a ── orifice_1.port_a [节流孔] orifice_1.port_b ── chamber_1.port_1
```
仓库中的真实回归测试使用了一条更完整的链路:`cylinder(500 kPa) → orifice → pipe → tank(100 kPa)`(`tests/test_generic_system_xml_simulation.py:86-145`)。500 kPa 与 100 kPa 提供明显压差,便于检查压力和质量流量是否按预期推进。下面仍用 A、B 表示任意一对相连端口,规则与该测试相同。
这些都是 `physical / pneumatic` 端口。连接后,求解器关心三件主要事情:
1. 接头处的压力要相容;
2. 从一个组件流出的质量,必须流入另一个组件;
3. 气体携带的能量要按实际流向传递,发生汇合时还要混合。
这也是 `p`、`m_flow`、`h_outflow` 三个变量的来历:
| 变量 | 单位 | 人话解释 | 接线后的处理方式 |
| --- | --- | --- | --- |
| `p` | Pa | 接头处的绝对压力 | 两端相等:`p_A - p_B = 0` |
| `m_flow` | kg/s | 每秒有多少质量的气体流过 | 两端守恒:`m_flow_A + m_flow_B = 0` |
| `h_outflow` | J/kg | 如果气体从该组件流出,每公斤带走多少能量 | 按实际流向传播/混合,不直接令两端相等 |
| `volume` | m³ | 相邻移动机构提供的外部容积 | 内部辅助量,结果默认不展示 |
| `volume_flow` | m³/s | 上述外部容积每秒变化多少 | 内部辅助量,结果默认不展示 |
为什么两端的 `m_flow` 一正一负?项目统一规定“**进入当前组件为正**”。如果 0.01 kg/s 从 A 流进 B,那么从 A 的视角它在流出,约为 `-0.01`;从 B 的视角它在流入,约为 `+0.01`。这不是矛盾,只是观察对象不同。
`inlet`、`outlet`、`bidirectional` 是设计上的名义角色,不是止回阀。即使一个端口名义上叫 `outlet`,求解过程中仍可能出现反向流动。`PortState.actual_direction()` 使用约 `1e-12` 的死区判断 `in/out/stagnant`(`app/simulation/core/ports.py:218-231`)。
**[已实现]** `volume` 和 `volume_flow` 虽然使用 `signal/directed` 的变量规则,但它们仍装在气动物理端口里,不是画布上另一根信号线。`PneumaticVolumeResolver` 会沿现有气路传播它们(`app/simulation/solvers/pneumatic_volume.py:21-93`)。
### 3.2 案例 B:阶跃信号控制阀或力源
控制线与气管不同,它只把一个数值从发送方交给接收方:
```text
amesim_step0.out ───────────────> amesim_pnvo001.res
信号输出 output(发送方) 阀的信号输入 input(接收方)
```
当阶跃源在某个时刻从 0 跳到 1,`SignalResolver.solve()` 先更新信号源,再把输出值写到阀的 `res` 输入(`app/simulation/solvers/signal.py:40-68`)。阶跃发生的时刻还能作为积分断点,避免数值积分跨过突变点而不知情。
`tests/test_amesim_pnvo001_signal_xml.py:37-84` 正好演示了这个分工:`step_1.out → valve_1.res` 只传阀的控制命令,而 `cylinder → valve → tank` 的另外几条连接才传递气体的压力、质量流量和焓。控制线不会“变成气管”,阀组件负责在内部用命令改变气路行为。
同样的信号也能驱动力源:
```text
amesim_step0.out ──> amesim_forc.res [力源] amesim_forc.port_2 ── 机械网络
```
这里 `res` 是信号输入,`port_2` 是机械端口。信号与机械并没有直接相连,而是由 `amesim_forc` 组件内部方程把输入数值转换成力。
### 3.3 机械端口:把连接点当成同一个运动点
`physical / mechanical` 是一维平动机械连接,主要变量为:
| 变量 | 单位 | 人话解释 | 接线后的规则 |
| --- | --- | --- | --- |
| `x` | m | 连接点的位置 | 两端位移相等 |
| `v` | m/s | 连接点的速度 | 两端速度相等 |
| `f` | N | 组件在连接点承受的力 | 两端力相加为零 |
机械端口当前都标为 `bidirectional`。`MechanicalStateReducer` 会把刚性连接的一组惯性元件整理为一个共享的 `[v, x]` 状态坐标,避免同一运动被重复积分(`app/simulation/solvers/mechanical.py:225-248, 354-427`)。
### 3.4 跨域组件不是“不同插头直接相连”
当前有三种典型跨域组件:
- `amesim_forc`:信号输入 + 机械端口;
- `amesim_pnvo001`:信号输入 + 两个气动端口;
- `amesim_pnrp17`:一个气动端口 + 四个机械端口。
不同域之间的转换发生在组件内部方程中。网络层仍禁止把气动端口直接接到机械端口或信号端口。
## 4. 端口定义究竟包含什么
可以把 `PortDefinition` 看成端口铭牌。稳定定义在 `app/simulation/core/ports.py:38-47`:
| 字段 | 例子 | 通俗含义 |
| --- | --- | --- |
| `name` | `port_a`、`res` | 组件内部唯一的端口编号 |
| `kind` | `physical` / `signal` | 是物理接头还是控制线插座 |
| `domain` | `pneumatic` / `mechanical` / `signal` | 具体属于哪个专业类别 |
| `nominal_role` | `inlet`、`output` 等 | 名义用途;只有信号的 input/output 决定传播方向 |
| `positive_flow_direction` | `intoComponent` | 流和力的正号统一指向组件内部 |
| `variables` | `p`、`m_flow` 等 | 端口真正携带的变量、单位和连接规则 |
`side` 和 `order` 来自显示定义 `ComponentDisplaySpec.ports`,不是物理合同:
- `side`:端口图标画在节点左、右、上还是下;
- `order`:多个端口的显示顺序。
它们由 `ComponentModelSpec.as_catalog_dict()` 合并进目录响应(`app/simulation/registry.py:76-110`)。求解器不读取 `side`。
运行时的 `PortState` 是一只通用“数值盒子”,同时预留气动、机械和信号字段(`app/simulation/core/ports.py:194-207`)。不能因为盒子里有某个字段,就认定所有端口都支持该变量;真正要看的是 `PortDefinition.variables`。
## 5. JSON 和 XML 分别保存什么
本节继续用“储气容器连接节流孔”说明同一件事如何经过四层表示。
### 5.1 组件目录 JSON:产品说明书
`GET /api/components/catalog` 返回后端支持的全部型号(`app/main.py:283-287`、`app/simulation/registry.py:1002-1027`)。它受 `schemas/component-catalog-v1.schema.json` 约束,信息最完整。
下面是为了讲解而加了注释的 **JSONC**,不是可直接提交的严格 JSON:
```jsonc
{
"name": "port_a", // 端口编号
"kind": "physical", // 物理接头,不是控制线
"domain": "pneumatic", // 气动类别
"nominalRole": "bidirectional", // 设计上允许双向使用
"positiveFlowDirection": "intoComponent", // 正流量指向组件内部
"variables": [ // 完整变量表只在目录/编译结果中出现
{
"name": "p", // 压力
"role": "effort", // 连接后两端相等
"connectionRule": "equal",
"unit": "Pa",
"resultVisible": true
},
{
"name": "m_flow", // 质量流量
"role": "flow", // 连接后两端相加为零
"connectionRule": "sumToZero",
"unit": "kg/s",
"resultVisible": true
},
{
"name": "h_outflow", // 流出气体的比焓
"role": "stream", // 按流向传播/混合
"connectionRule": "streamMix",
"unit": "J/kg",
"resultVisible": true
}
],
"side": "left", // 只影响画面位置
"order": 10 // 只影响显示顺序
}
```
**[已实现]** 当前目录加载结果为 **27 个模型、61 个已声明端口**:38 个气动端口、19 个机械端口、4 个信号端口。两个介质定义模型没有端口,因此不计入 61 个端口。
### 5.2 ReactFlow 工程 JSON:画布存档
工程 JSON 保存“这次用了哪一个型号、端口快照和连线”,但不重复保存完整变量表。请求模型在 `app/main.py:86-155`;前端定义与生成逻辑见 `frontend/src/App.tsx` 中的 `PortDefinition`、`ReactFlowProjectPayload`、`buildProjectPayload()`。
下面仍是带说明的 JSONC:
为避免示例过长,这里只截取“储气容器接到节流孔”的局部画布;节流孔另一端尚未接出,所以它是**字段讲解片段**,不是可直接运行的完整工程。可运行的完整链路见 `tests/test_generic_system_xml_simulation.py:86-145`。
```jsonc
{
"projectSchemaVersion": 1, // 当前工程 JSON 的唯一格式版本
"name": "tank-orifice-demo",
"nodes": [
{
"id": "tank_1", // 本张图中的实例 ID
"type": "simulationComponent",
"position": {"x": 120, "y": 80}, // 画布位置
"data": {
"label": "储气容器 A",
"componentType": "tank",
"modelType": "tank", // 后端靠它回查完整模型定义
"modelVersion": "1.0.0", // 锁定保存时使用的模型合同
"ports": [
{
"name": "port_a",
"kind": "physical",
"domain": "pneumatic",
"nominalRole": "bidirectional",
"positiveFlowDirection": "intoComponent",
"side": "right"
}
],
"parameters": {} // 当前组件实例的参数值
}
},
{
"id": "orifice_1",
"type": "simulationComponent",
"position": {"x": 360, "y": 80},
"data": {
"label": "节流孔",
"componentType": "orifice",
"modelType": "orifice",
"modelVersion": "1.0.0",
"ports": [
{"name": "port_a", "kind": "physical", "domain": "pneumatic",
"nominalRole": "bidirectional", "positiveFlowDirection": "intoComponent", "side": "left"},
{"name": "port_b", "kind": "physical", "domain": "pneumatic",
"nominalRole": "bidirectional", "positiveFlowDirection": "intoComponent", "side": "right"}
],
"parameters": {}
}
}
],
"edges": [
{
"id": "edge-1",
"source": "tank_1", // ReactFlow 画线需要 source/target
"sourceHandle": "port_a",
"target": "orifice_1",
"targetHandle": "port_a", // 对物理线而言,不代表流动方向
"data": {"isContactEdge": false}
}
],
"simulation": {
"t_start": 0,
"t_stop": 2,
"step": 0.1,
"max_step": 0.005,
"method": "BDF"
}
}
```
后端会按 `modelType` 创建真实模型,并先要求节点 `modelVersion` 与注册版本完全一致,
再检查这里的端口名、类型、域、名义角色和正号是否与注册定义一致。
**[已实现] 前端工程和后端求解模型有意保持分工。**
- 目录 JSON 有完整 `variables`,前端只读取完成画布连接和即时提示所需的端口级字段;后端编译仍是物理合同的最终检查点。
- 工程 JSON 使用必填 `projectSchemaVersion: 1`,每个节点保存 `modelVersion`,端口必须是结构化对象,连接必须写明两端 Handle。结构版本不受支持时直接拒绝;节点模型版本缺失或不匹配时不得编译、导出 XML 或仿真。
- UI 继续通过浏览器 `localStorage` 和本地 JSON 文件保存工程。后端同时提供严格按工程 JSON v1 校验的工程列表、保存和读取接口;仿真主路径仍只向后端提交精简的 System XML v3。
### 5.3 System XML v3:交给后端的精简求解清单
XML v3 和工程 JSON 不再追求“保存同一份完整工程”。两者分工很明确:工程 JSON 保存怎样编辑和显示,XML v3 保存后端求解什么。当前结构由 `schemas/system-simulation-v3.xsd` 定义;完整规范见 `docs/system-xml-v3.md`。
#### 5.3.1 生成出来的 XML 是什么结构
```text
System 整个可执行模型
├─ Simulation 恰好 1 个:仿真时间和算法
├─ Components 组件清单
│ └─ Component * 0~多个组件
│ └─ Parameter * 组件的完整 SI 参数
└─ Connections 接线清单
└─ Connection * 0~多条连接
├─ Endpoint 每条连接恰好两个端点
└─ Endpoint
```
XML 外形是一棵树,模型仍是一张连接图。组件平铺在 `<Components>` 中,`<Connections>` 再通过“组件 `id` + 注册端口名”把它们接起来。顶层顺序固定为 `Simulation → Components → Connections`。
与 v2 相比,v3 主动删掉了 `Port` 快照和画布字段。各字段来源如下:
| XML v3 内容 | 来源 | 通俗解释 |
| --- | --- | --- |
| `System/@name` | `project.name` | 可选的模型名称 |
| `schemaVersion/unitSystem` | 生成器固定写入 | 当前协议固定为 v3,参数使用 SI |
| `Simulation` | 求值后的仿真设置 | 起止时间、结果采样间隔、内部最大步长和算法 |
| `Component/@id` | `node.id` | 连接实际引用的稳定实例编号 |
| `Component/@type` | `modelType` | 用哪个后端模型类创建实例 |
| `Component/@modelVersion` | 组件目录/注册表 | 锁定本文件采用的模型合同版本 |
| `Parameter` | 参数表达式求值并换算后的值 | 写出该模型的全部注册参数,只留最终 SI 数值 |
| `Endpoint` | ReactFlow 边两端的 handle | 用“组件 ID + 端口名”重新接线 |
实际生成过程可以概括为:
1. 收集当前节点、连线和仿真设置;
2. 把工程 JSON 的 `simulation.step` 映射成 XML 的 `sampleStep`;
3. 根据组件目录写入 `modelVersion`,并用注册默认值补齐全部参数;
4. 把参数表达式求值、换算为 SI,只写 `Component/Parameter`;
5. 每条边只写两个 `Endpoint`,不复制端口类型或方向角色。
XML v3 不保存显示名称、`symbol`、坐标、`side`、旋转、镜像、参数显示单位、科学计数法偏好、撤销历史、当前选择和仿真结果。因此它适合校验、交换和求解,但不能无损还原前端画布;要继续编辑,应保存工程 JSON。
#### 5.3.2 一个最小 XML 片段
下面用“阶跃信号 → 力源 → 零力端”同时展示信号和机械连接:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<System name="signal-force-demo" schemaVersion="3" unitSystem="SI">
<Simulation tStart="0" tStop="2"
sampleStep="0.1" maxStep="0.005" method="BDF"/>
<Components>
<Component id="step_1" type="amesim_step0" modelVersion="0.1.0">
<Parameter name="initial" value="0"/>
<Parameter name="final" value="20"/>
<Parameter name="time" value="0.04"/>
</Component>
<Component id="force_1" type="amesim_forc" modelVersion="0.2.0">
<!-- +1 为默认施力方向,-1 为反向 -->
<Parameter name="direction" value="1"/>
</Component>
<Component id="zero_1" type="amesim_f000" modelVersion="0.1.0"/>
</Components>
<Connections>
<Connection id="signal-1">
<Endpoint component="step_1" port="out"/>
<Endpoint component="force_1" port="res"/>
</Connection>
<Connection id="mechanical-1">
<Endpoint component="force_1" port="port_2"/>
<Endpoint component="zero_1" port="port_1"/>
</Connection>
</Connections>
</System>
```
这里没有写 `kind/domain/role`,也没有 `<Port>`。后端看到 `type="amesim_step0"` 后,会从注册表知道 `out` 是信号输出;看到 `amesim_forc.res` 后,会知道它是信号输入;同理还能恢复两个机械端口的变量合同。也就是说,端口名是“查说明书的索引”,不是由 XML 自己重新定义接口。
XML v3 的连接规则是:
- 物理和信号 `Connection` 都只写两个 `Endpoint`;
- 两端的 `kind/domain/nominalRole/variables` 都从当前注册模型恢复;
- 信号连接必须恰好包含一个注册输出端和一个注册输入端,但端点先后顺序不决定方向;一个输出可以扇出到多个输入,每个输入只能有一个驱动;
- 物理端点无序,流向由求解结果和统一正号约定决定;
- 参数必须完整、有限、使用 SI,并通过当前模型的范围或枚举校验。
**[已实现] 布局与物理方向已经分开。** `side/rotation/mirrored` 只保留在工程 JSON 中,XML v3 不包含它们。`amesim_forc` 从模型版本 `0.2.0` 起用显式 `direction=+1/-1` 控制力的正反:相同输入 20 N 时,`direction=+1` 要求机械端口平衡值为 `f=-20 N`,`direction=-1` 时为 `f=+20 N`。旋转或镜像图标不改变求解结果(`app/simulation/components/amesim/mechanical/translational.py`、`tests/test_amesim_mechanical_public_components.py`)。
### 5.4 编译结果 JSON:接线检查报告
`POST /api/reactflow/compile-model` 和 `POST /api/system-xml/compile-model` 最终调用 `SimulationNetwork.as_interface_dict()`(`app/simulation/systems/network.py:280-314`)。
它不是新的工程存档,而是告诉调用者:“后端实际装配出了什么”。下例也是带注释的结构示意:
```jsonc
{
"name": "tank-orifice-demo",
"components": [
{
"id": "tank_1",
"type": "tank",
"ports": [
{
"name": "port_a",
"kind": "physical",
"domain": "pneumatic",
"variables": [
// 工程 JSON 和 XML 都没保存的完整变量定义,在这里由注册表恢复
{"name": "p", "connectionRule": "equal", "unit": "Pa"},
{"name": "m_flow", "connectionRule": "sumToZero", "unit": "kg/s"},
{"name": "h_outflow", "connectionRule": "streamMix", "unit": "J/kg"}
]
}
]
}
],
"connections": [
{
"id": "edge-1",
"kind": "physical",
"domain": "pneumatic",
"endpoints": [
{"component": "tank_1", "port": "port_a"},
{"component": "orifice_1", "port": "port_a"}
] // 物理连接只有两个端点,不再保留 source/target 含义
}
],
"unconnectedPorts": [] // 若有漏接,会在这里或诊断中体现
}
```
### 5.5 四层字段对照
先说结论:v3 XML 只携带连接实际用到的端口名,端口大类和完整变量表都由后端注册表恢复。这样不会同时维护“模型类中的端口”和“XML 中的端口副本”。
| 模型含义 | 目录 JSON | 工程 JSON | System XML v3 | 编译结果 JSON |
| --- | --- | --- | --- | --- |
| 端口编号 | `name` | `name` | 只在 `Endpoint/@port` 出现 | `name` |
| 物理/信号 | `kind` | `kind` | 不保存,由 `type + port` 恢复 | `kind` |
| 气动/机械/信号 | `domain` | `domain` | 不保存,由注册表恢复 | `domain` |
| 名义角色 | `nominalRole` | `nominalRole` | 不保存,由注册表恢复 | `nominalRole` |
| 正号约定 | `positiveFlowDirection` | 同名字段 | 不保存,由注册表恢复 | 同名字段 |
| 完整变量和规则 | `variables[]` | 不保存 | 不保存 | `variables[]`,由注册表恢复 |
| 显示侧 | `side` | `side` | 不保存 | 不是求解合同 |
| 画布连线 | 不适用 | `source/.../targetHandle` | 两个 `Endpoint` | 规范化后的连接端点 |
| 模型合同版本 | `modelVersion` | 节点显式保存并与目录核对 | `Component/@modelVersion` 必填 | 按当前注册模型编译 |
## 6. 接线时后端具体检查什么
可以把 `SimulationNetwork.connect()` 当作最后一道“防止插错线”的检查(`app/simulation/systems/network.py:83-150`)。它会依次确认:
1. 组件和端口确实存在,且不是自己接自己;
2. 两端 `kind` 一样;
3. 两端 `domain` 一样;
4. 完整变量表一致;
5. 信号线是一端 input、一端 output;
6. 一个物理端口最多接一条线;需要分支时必须放入 `tee`、`amesim_pn3node2`、`amesim_p4node2` 等分支组件;
7. 连接 ID 和端点组合没有重复。
对于案例 A 的一条气动连接,网络直接形成:
```text
p_A - p_B = 0 # 接头两侧压力一致
m_flow_A + m_flow_B = 0 # 流出一侧的质量等于流入另一侧的质量
```
不会形成 `h_outflow_A = h_outflow_B`。焓要在压力、流量确定后,由 `StreamResolver` 根据真实流向传播或混合。物理连接端点顺序不影响结果,相关测试包括 `tests/test_component_interfaces.py:51-60`、`tests/test_system_xml_protocol.py:146-176` 和 `tests/test_generic_system_xml_simulation.py:354-372`。
XML v3 还会先经过:
- 安全解析和 5 MiB 大小限制(`app/system_xml.py:21, 243-280`);
- XSD 格式检查;
- 组件类型、`modelVersion`、参数完整性和值域检查;
- 端点引用检查,并从注册表恢复端口类型、角色、变量和正号约定;
- 端点占用、信号输入/输出配对、物理域和变量合同检查。
XML 语义检查会把未连接端口记为 warning;真正进入通用求解前,未连接的**物理端口**会成为 `PORT_UNCONNECTED` error(`app/simulation/systems/generic.py:93-125`)。未连接信号端口不会阻止求解。
## 7. 当前容易踩坑的跨层差异
这些问题不妨碍理解主流程,但开发或制作 XML 时必须注意。
### 7.1 信号多接的规则前后端不一致
**[发现]** 后端网络和 XML 只限制物理端口单连接,没有禁止多个信号源同时连接到一个 input。`SignalResolver.solve()` 会按连接顺序依次写入,因此最后一个值覆盖前面的值。
前端 `canConnectPorts()` 却对所有端口实行一对一:既阻止多个源写同一输入,也阻止一个输出连到多个输入(`frontend/src/App.tsx`)。所以:
- 在浏览器里通常画不出这种多接;
- 直接调用 XML/API 却可能构造出来;
- 最后覆盖不是稳定的“求和”或“仲裁”规则,不应依赖。
后续最好统一为“每个信号输入只能有一个驱动”,或者显式增加求和、选择、总线组件。
### 7.2 XML 不写端口类型,不等于后端不知道类型
**[已实现]** v3 有意删除 `Port`、连接的 `kind/domain` 和端点 `role`。这不是允许调用者“随便省略”,而是把端口合同收回到唯一事实来源:后端注册模型。语义检查会用 `Component/@type + Endpoint/@port` 查出 `kind/domain/nominalRole/variables`;端口不存在或两端不兼容仍会报错。手写 v3 时不要把 v2 的这些字段加回来,XSD 会拒绝它们。
### 7.3 JSON 和 XML 对缺省参数的处理不同
**[发现]** ReactFlow JSON 编译和 JSON→XML 导出会用注册默认值补齐缺失参数;System XML v3 语义检查会对缺少的注册参数报告 `PARAMETER_REQUIRED_MISSING`。因此“工程 JSON 可以省略默认参数”不等于“手写 XML 也可以省略”。正规导出器会替你写全,人工制作 v3 时必须列全。
### 7.4 当前 API 只接受 v3
仓库只保留当前的 v3 协议和 XSD。v1/v2 不属于受支持输入,旧版本号只出现在拒绝边界测试和“旧格式不受支持”的说明中。
**[已实现]** 当前 `validate_system_xml_document()` 固定加载 `schemas/system-simulation-v3.xsd`,不会按 `schemaVersion` 自动切换或迁移 v1/v2;新文件必须使用 v3。
### 7.5 XML v3 会锁定模型版本
**[已实现]** `Component/@modelVersion` 是 v3 必填属性,语义检查要求它与当前注册模型版本完全一致。例如 `amesim_forc` 当前是 `0.2.0`,旧版本号不会被静默当成新方程求解。工程 JSON v1 的每个节点也保存创建时的 `modelVersion`,编译和导出 XML 前必须先与当前目录核对。这种设计采取“发现不一致就拒绝”的策略,不表示系统已经提供自动模型迁移器。
### 7.6 不从旧格式推断当前行为
当前格式只看 `docs/system-xml-v3.md` 和 `schemas/system-simulation-v3.xsd`。旧格式中的 `Port`、布局字段、端点 `role`、`Simulation/@step` 和“旋转改变力方向”都不能继续套用到 v3。
## 8. 当前模型覆盖范围
不需要记住所有型号,只需知道它们仍归入前面三种插头标准。
| 用途 | 当前模型 |
| --- | --- |
| 实验气动 | `cylinder`、`tank`、`pipe`、`orifice`、`tee` |
| AMESim 气动 | `amesim_pnpl01`、`amesim_pnrp17`、`amesim_pnch023`、`amesim_pnch012`、`amesim_pnor001`、`amesim_pnvo001_fixed`、`amesim_pnvo001`、`amesim_pnl00r`、`amesim_pnl0001`、`amesim_pnl0002`、`amesim_pnl0003`、`amesim_pn3node2`、`amesim_p4node2` |
| AMESim 机械 | `amesim_f000`、`amesim_forc`、`amesim_mecmas21`、`amesim_lstp00a`、`amesim_lmechn1`、`amesim_pnrp17` |
| AMESim 信号 | `amesim_step0`、`amesim_ud00`、`amesim_forc`、`amesim_pnvo001` |
| 零端口介质定义 | `amesim_ideal_air_medium`、`amesim_helium_medium` |
两个介质模型虽然出现在组件目录中,却没有连接端口。它们在编译第一阶段登记 `gi=1..99` 的介质定义,之后不进入方程网络(`app/main.py:1206-1253`)。它们是配置节点,不是第四类接口。
## 9. 与接口表示相关的 HTTP API
这里的 API 可以理解为围绕上述四层数据提供的“入口按钮”。
| API | 人话解释 | 当前前端是否直接使用 |
| --- | --- | --- |
| `GET /api/components/catalog` | 获取后端产品说明书 | 是 |
| `GET /api/reactflow/projects` | 列出后端保存的工程 JSON v1 | 当前 UI 主路径不用 |
| `GET /api/reactflow/projects/{id}` | 读取并校验一个后端工程 | 当前 UI 主路径不用 |
| `POST /api/reactflow/projects/{id}` | 保存一个工程并保留单位/科学计数显示信息 | 当前 UI 主路径不用 |
| `POST /api/reactflow/system-xml` | 把工程 JSON 转成精简 XML v3 | UI 当前也能在浏览器内生成同一结构 |
| `POST /api/reactflow/compile-model` | 检查工程 JSON 并返回装配结果 | 可用于诊断 |
| `POST /api/system-xml/validate` | 只检查 XML 格式和语义 | 可用于诊断 |
| `POST /api/system-xml/parse` | 把 XML 变成规范化执行模型;不还原坐标、旋转等画布信息 | 可用于检查求解输入,不是无损工程导入 |
| `POST /api/system-xml/compile-model` | 检查 XML 并装配网络 | 求解前使用 |
| `POST /api/system-xml/simulate-stream` | 提交 XML 并持续接收进度/最终结果 | 当前 UI 的通用求解入口 |
| `POST /api/system-xml/simulate` | 同步返回完整结果 | 后端提供,UI 主路径不用 |
后端还提供 CSV 导出 API。当前 UI 的工程保存和读取主要发生在浏览器与用户选择的本地文件中,后端工程接口作为同一严格合同的可选持久化入口。
## 10. 已实现、约定和推断:最后再分一次边界
### [已实现]
- 当前实际有气动、机械、标量信号三类端口,模型加载快照为 27 个模型、61 个端口。
- 物理端口以进入组件为正,物理连接端点无序;信号方向由注册端口的 output/input 决定,XML 端点不写 source/target 角色。
- 注册器、JSON/XML 编译器、XML 语义层和网络层会分层检查接口。
- 气动压力/质量流量约束、焓传播、外部容积传播、机械连接约束和标量信号传播已有可执行代码。
- 工程 JSON 可导出 System XML v3;XML v3 可解析为执行模型并编译为带完整端口合同的网络。
- `amesim_forc` 用 `direction=+1/-1` 决定力方向;旋转/镜像只影响显示。
- MECMAS21 工程、目录、XML 和模型统一使用 AMESim 原生 `1/2` 选项编码,不再猜测或转换旧 `0/1` 值。
### [约定]
- 组件模型类的 `PORTS` 是权威端口合同;前端目录只是读取视图。
- XML 和执行参数使用 SI 基准值。
- 物理端口的 `nominalRole` 不限制实际流向。
- 新文件使用 System XML v3;每个组件显式写当前 `modelVersion`,仿真采样字段写 `sampleStep`。
### [推断/需要另行实测]
- 类型字段允许出现其他 `domain` 字符串,但新增液压、电气等域还需要变量定义、组件方程和专项求解器;只改字符串不能工作。
- 27 个模型和 61 个端口是本次加载快照。受控库清单改变后,数量也会改变。
- v1/v2 XML 明确不受支持;旧工程 JSON 也不会由当前前端自动猜测或迁移。
## 11. 关键文件与符号索引
读到具体疑问时,可从这里回到代码。前面的章节已经给出人话解释,本表用于精确定位。
| 主题 | 文件与位置 | 关键符号 |
| --- | --- | --- |
| 端口类型、变量、正号 | `app/simulation/core/ports.py:7-231` | `PortVariableDefinition`、`PortDefinition`、`PortState` |
| 组件事实来源 | `app/simulation/core/base.py:21-63` | `Component.PORTS`、`register_declared_port()` |
| 端口显示信息 | `app/simulation/core/catalog.py:27-71` | `PortDisplaySpec`、`ComponentDisplaySpec` |
| 注册发现与校验 | `app/simulation/registry.py:41-170, 416-490, 825-1027` | `ComponentModelSpec`、`_validate_port()`、`build_component_catalog()` |
| 网络接线与方程 | `app/simulation/systems/network.py:24-314` | `Connection`、`SimulationNetwork.connect()`、`connection_equation_residuals()` |
| 工程 JSON 与编译 | `app/main.py:86-155, 1181-1344` | `ReactFlowPortDefinition`、`compile_reactflow_network()` |
| JSON 转 XML | `app/main.py:947-1097` | `build_reactflow_system_xml()` |
| XML 解析和语义检查 | `app/system_xml.py` | `SystemXmlComponent`、`SystemXmlEndpoint`、`SystemXmlDocument`、`validate_system_xml_document()` |
| XML v3 当前格式 | `schemas/system-simulation-v3.xsd`、`docs/system-xml-v3.md` | `sampleStep`、`modelVersion`、`Parameter`、`Endpoint` |
| 目录 JSON 格式 | `schemas/component-catalog-v1.schema.json:52-160` | `$defs.portVariable`、`$defs.port` |
| 前端端口和工程类型 | `frontend/src/App.tsx` | `PortDefinition`、`ReactFlowProjectPayload` |
| 前端生成 XML | `frontend/src/App.tsx` | `buildSystemXml()`、`projectConnectionMetadata()` |
| 接口核心测试 | `tests/test_component_interfaces.py` | 变量规则、端点中立、单连接限制 |
| XML 协议测试 | `tests/test_system_xml_protocol.py`、`tests/test_system_xml_parser.py` | v3 表示和语义诊断 |
## 12. 一句话复盘
SystemSimulationApp 用三种端口把组件组成网络:气动和机械端口负责“守恒与相容”,信号端口负责“有方向地传一个数值”;目录 JSON 定义型号,工程 JSON 保存画布,System XML v3 保存精简求解清单,编译结果则证明后端最终理解并装配出了什么。