Files
SystemSimulationApp/docs/后端求解逻辑与效率优化调研.md
T

543 lines
40 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-15(System XML v3 迁移后),依据当前仓库代码、配置、说明文档、测试与阶段埋点。
> 本文所称“主求解路径”是当前前端实际调用的 System XML 流式接口;固定 TestModel 和 Test MQL 接口另行说明。机器相关的实测结果单独见[仿真性能评估 2026-08-15](仿真性能评估-2026-08-15.md)。
## 0. 三分钟读懂
### 0.1 求解器到底在做什么
先不管 ODE、RHS、BDF 这些名字。把一次仿真想成制作一段工程动画:
1. **检查装配图。** 气管有没有漏接,控制线方向对不对,模型参数是否齐全。
2. **读取当前“存量”。** 例如气室里有多少气体和能量,质量块现在的位置和速度。
3. **让当前瞬间自洽。** 根据这些存量,把此刻的压力、流量和力反复对账,直到连接规则和组件方程同时满足。
4. **计算变化速度。** 得出“下一小段时间内,质量、能量、位置、速度将怎样变化”。
5. **内部小步前进。** 求解器自己决定每次走多小;变化剧烈时会缩短步长。
6. **按用户指定时刻留快照。** 内部可能算很多小步,但结果文件只在 XML v3 的 `sampleStep` 指定的时刻保存数值。
7. **把进度和结果交给前端。** 运行中发心跳/进度,结束时一次性发送完整曲线数据。
项目里的技术说法“**半显式 ODE + 每次变化率计算前做代数闭合**”,翻译成人话就是:**会积累的量用时间积分向前推;必须在当前瞬间成立的关系,每次都先对账求平衡。**
### 0.2 用仓库里的真实案例贯穿全文
`tests/test_generic_system_xml_simulation.py:86-145` 有一条完整回归气路:
```text
高压气缸 低压储气罐
0.01 m³、500 kPa 0.1 m³、100 kPa
──> 节流孔 ──> 1 m 管路 ──>
```
图中箭头只表示这个初始压差下**预计**的气流方向,不代表物理连线本身有 `source/target` 方向。回归测试检查了:
- 气缸压力下降;
- 储气罐压力上升;
- 总质量和总能量守恒;
- 把工程 JSON 中物理边的 `source/target` 对调,结果不变。
测试只运行 `0~0.01 s`,设置结果采样 `sample_step=0.005 s`、内部上限 `max_step=0.001 s`、`method=BDF`(`tests/test_generic_system_xml_simulation.py:86-145, 254-300, 354-372`)。在 System XML v3 中,前两个字段分别写成 `sampleStep` 和 `maxStep`。它们可以这样理解:
```text
结果快照: 0 s -------- 0.005 s -------- 0.01 s
内部计算: 0 s - 小步 - 小步 - 小步 - ... - 0.01 s
每个内部步最多 0.001 s,也可能更短或被重算
```
- `sampleStep`:相机隔多久保存一张结果快照;
- `max_step`:求解器一次内部前进最多能走多远;
- `BDF`:一种适合系统中“有的变化快、有的变化慢”的自适应算法。本文不需要展开它的公式。
另一个真实案例位于 `tests/test_amesim_pnvo001_signal_xml.py:87-204, 230-246`:高、低压氦气室之间有一个阀,阶跃信号在 `0.04 s` 从 0 跳到 1。求解器会像遇到“定时闹钟”一样,准确停到事件时刻,更新阀命令,再从该时刻继续积分。
### 0.3 常见术语翻译
| 技术词 | 先这样理解 | 本项目里的具体含义 |
| --- | --- | --- |
| 动态状态(state) | 会随时间积累的存量 | 气体质量/内能 `[m,U]`,或机械速度/位置 `[v,x]` |
| 代数量 | 当前瞬间的仪表读数 | 压力、流量、连接力等;由当前状态和约束求出 |
| 代数闭合(closure) | 把所有账对平 | 让组件方程、连接守恒和当前状态同时成立 |
| 变化率计算(RHS) | 算下一刻变化有多快 | 输入当前状态,输出 `dm/dt`、`dU/dt`、加速度等 |
| ODE 积分 | 根据变化率向时间前进 | SciPy 的 BDF、Radau、RK45 等 |
| DAE | 状态和瞬时约束一起交给专用求解器 | 当前主内核不是通用 DAE 求解器 |
| stream 焓 | 气体随流动携带的“能量标签” | `h_outflow` 按实际流向传播/混合,不是两端温度相等 |
| 非线性迭代(`least_squares`) | 直接算不出时反复试值 | 压力流量快速路径失败后的回退方案 |
| Jacobian | “改一个量会影响哪些方程”的灵敏度地图 | 可帮助 BDF/Radau 和非线性求解少做试算 |
| dense output | 两个内部步之间的插值尺 | 用来补采样点和定位机械事件 |
| NDJSON | 一行一个 JSON 消息 | 同一 HTTP 响应中依次发送心跳、进度、结果 |
| worker | 后台办事通道 | 当前脚本是一个 Uvicorn worker;流式任务另开求解线程 |
只想了解系统如何运行,可以读第 0、3、5、6、9、10、13 节;需要改求解器时,再阅读其余技术细节和第 15 节代码索引。
## 1. 结论先行
1. **[已实现] 当前主内核是“半显式 ODE + RHS 内代数闭合”。** 动态组件只把储能状态交给 ODE 积分器;每次计算导数前,系统先传播信号、刷新热力状态、求压力/流量代数网络、传播变容边界、迭代 stream 焓并更新机械加速度。它不是通用 DAE 求解器,也不等价于完整 Modelica `inStream/actualStream` 语义(`README.md:18-20`、`app/simulation/README.md:174-195`)。
2. **[已实现] 当前前端主链路是 System XML 流式仿真。** 浏览器生成 XML,经 `POST /api/system-xml/simulate-stream` 发送;后端以 NDJSON 返回心跳与进度,最后在一个 JSON 行中返回完整结果。不是 WebSocket 或标准 SSE。
3. **[已实现] XML v3 的 `sampleStep` 是输出采样间隔,不是固定积分步长。** 内部的 `max_step`(XML 为 `maxStep`)才是自适应积分步长上限;`BDF/Radau/LSODA/RK45/RK23/DOP853` 均受支持。流式运行因总是提供取消检查,会使用 SciPy 低层求解器逐个已接受步推进。
4. **[已实现] 每次完整闭合先调用 1 次压力流量求解,再执行最多 25 轮 `stream → pressure-flow` 固定点。** 因而每次闭合至少 2 次、理论上最多 26 次压力求解;本次代表算例平均为 2.00~2.30 次。积分结束后,每个输出采样点又执行一次完整闭合并提取结果。
5. **[已实现] 代数求解已有因果化快路径。** 压力流量求解器预编译相等组与显式流量计划,种子残差足够小时不调用非线性优化;否则对全局未知向量调用 SciPy `least_squares`,当前未提供解析 Jacobian 或 `jac_sparsity`。
6. **[已实现] 当前启动脚本是一个 Uvicorn worker。** 每个流式任务再创建一个无并发上限的 daemon 线程和无界队列;没有进程池、集中任务队列、CPU/内存配额或持久化作业系统。同步仿真端点还会在 `async def` 中直接执行 CPU 密集代码。
7. **[推断] 优化应分两条线:**
- 单算例速度:减少闭合 pass、代数未知量与残差装配,缓存热物性,改善外层积分尺度/Jacobian,减少后处理重算;
- 服务吞吐与资源稳定性:有界进程 worker、结果分块/按需返回、任务表主动清理和前端结果存储降副本。
## 2. 证据标签与范围
- **[已实现]**:当前可执行代码或测试直接体现。
- **[约定]**:配置、Schema、类型或仓库说明规定,但不一定被每条入口完整执行。
- **[推断]**:从调用结构推导的资源或性能判断,尚无仓库内基准数据。
- **[发现]**:实现间的不一致、诊断缺口或潜在效率风险。
本次覆盖四条后端执行路径:
| 路径 | 是否为当前 UI 主路径 | 数值行为 |
| --- | --- | --- |
| `POST /api/system-xml/simulate-stream` | 是 | XML 校验、拓扑编译、通用系统积分;NDJSON 进度与最终结果 |
| `POST /api/system-xml/simulate` | 否 | 同一通用求解函数;同步返回、无任务登记/流式进度 |
| `POST /api/reactflow/simulate-testmodel` | 否 | 固定 TestModel 专用闭包与产物生成,不按任意 ReactFlow 边拓扑求解 |
| `POST /api/reactflow/simulate-test-mql` | 否 | 当前只返回结构/采样摘要和全零状态数组,不执行 132 状态时域积分 |
## 3. 主求解调用链
```mermaid
flowchart TD
A["前端校验参数并生成 System XML"] --> B["POST simulate-stream"]
B --> C["安全解析 + XSD v3 + 语义校验"]
C --> D["XML 执行模型直接编译 SimulationNetwork"]
D --> E["准备检查与求解器构造"]
E --> F["一致初始代数闭合"]
F --> G["自适应 ODE 逐步积分"]
G --> H["每个 RHS:信号/热力/代数/stream/机械闭合"]
G --> I["事件定位、状态重置、求解器重启"]
G --> J["逐采样点完整后处理闭合"]
J --> K["完整结果作为末条 NDJSON 返回"]
```
### 3.1 前端形成输入
**[已实现]** 前端先检查模型、表达式和仿真设置,再由 `buildSystemXml()` 在浏览器内构造精简 v3 XML。参数表达式会先求值,并按选定显示单位转成 SI 基准数值;求解期间这些组件参数保持静态,不存在前端实时调参或联合仿真输入通道(`frontend/src/App.tsx` 中的参数解析与 `buildSystemXml()`;`app/simulation/core/base.py:65-95`)。
前端生成 `simulationId`,通过 `fetch` 发送 XML,请求头携带该 ID;响应类型要求为 `application/x-ndjson`(`frontend/src/App.tsx` 中的运行入口与 `streamSystemSimulation()`)。
### 3.2 XML 校验、解析和网络编译
主序列见 `app/main.py` 的 `run_system_xml_simulation()`:
1. `validate_system_xml_document()` 执行大小/安全解析、v3 XSD 和语义校验;
2. XML 解析为只含求解信息的规范化执行模型,不重建 ReactFlow 画布;
3. `compile_system_xml_network()` 创建 `SimulationNetwork`;
4. 构造 `GenericFluidSystem` 与 `SolveIVPConfig`;
5. 调用通用 `simulate()`;
6. 汇总验证、模型接口、数值结果和错误诊断。
XML 安全层限制 5 MiB、禁用 DTD/实体和网络访问(`app/system_xml.py` 的安全解析与 `validate_system_xml_document()`)。
XML 编译最终进入 `_compile_solver_network()` 的两阶段装配(`app/main.py`):
- v3 语义层先核对组件 `modelVersion`、完整参数和端点引用;编译层再消费零端口介质定义节点;
- 按气动连通区域解析介质引用;
- 实例化实际组件,并根据注册模型恢复端口类型、角色和变量合同;
- 最后建立网络连接。
**[已实现]** v3 不携带坐标、旋转、镜像或端口显示侧,布局不会进入求解。`AmesimForc` 的物理正反由模型参数 `direction=+1/-1` 决定;旋转/镜像图标不会改变力方程(`app/simulation/components/amesim/mechanical/translational.py`)。
### 3.3 求解前结构检查
创建通用系统前,`generic_simulation_preparation_issues()` 检查(`app/simulation/systems/generic.py:93-201`):
- 所有物理端口已经连接;
- 压力流量系统的未知量数与方程数相等;
- 网络至少存在一个动态储能组件;
- 每个相连物理岛具有动态储能锚点;
- 不允许多个理想储能元件无阻力直接耦合。
这些检查是当前可求解结构的边界,不表示任意声明了 `PortDefinition` 的网络都能被通用求解器处理。
## 4. 数学结构与状态组织
本节的核心区别只有一个:**有些量要“记住过去并随时间积累”,有些量只要在当前瞬间满足约束。**
在高压气缸向低压储罐放气的案例里,气室内的气体质量和能量属于前者;端口压力和通过节流孔的瞬时流量属于后者。求解器先保存前者,再用它们求出后者。下面的“动态状态”“代数未知量”只是这两组量的技术名称。
### 4.1 动态状态
**[已实现]** 动态组件把自身状态拼成全局 ODE 向量:
- 固定/变容气室及部分动态管路通常使用质量与内能 `[m, U]`;导数由质量流、焓流和换热构成。PNCH023 示例见 `app/simulation/components/amesim/storage/chambers.py:111-218`。
- PNL0003 具有两个容积单元,使用四维 `[m1, U1, m2, U2]`(`app/simulation/components/amesim/flow/pipes.py:1043-1078`)。
- MECMAS21 的机械状态是 `[v, x]`,导数是 `[a, v]`(`app/simulation/components/amesim/mechanical/translational.py:569-700`)。
`MechanicalStateReducer` 会按机械连接中的 `x/v` 等值关系将刚性相连的质量归组,每组只保留一套 `[v, x]` 状态,从而避免重复积分同一个运动自由度(`app/simulation/solvers/mechanical.py:225-427`)。
### 4.2 代数未知量与方程
网络从物理端口合同和组件残差构造代数系统(`app/simulation/systems/network.py:159-237`):
- 气动端口的主要代数变量是 `p` 和 `m_flow`;
- 机械端口包含 `x`、`v`、`f`,其中已归属动态状态的坐标会被状态/等值关系约束;
- 连接对 `effort/equal` 生成两端差值,对 `flow/sumToZero` 生成两端和值;
- 组件再提供状态约束、构成关系、质量/力守恒等残差。
stream 焓 `h_outflow` 不直接强制相等;标量信号和气动外部容积也不进入同一通用连接残差,而由专用 resolver 处理。
### 4.3 压力流量因果化与非线性回退
`PressureFlowSolver` 初始化时预编译 effort 等值组、显式流/力赋值计划和未知量布局(`app/simulation/solvers/algebraic.py:86-106, 637-729`)。每次 `solve()`:
1. 从上一解与当前动态状态播种端口未知量;
2. 传播状态拥有的压力、位移和速度;
3. 执行可显式求值的构成关系与守恒关系;
4. 处理单边接触约束;
5. 若缩放后残差不超过 `1e-7`,直接返回且 `evaluations=0`;
6. 否则调用 `scipy.optimize.least_squares`。
非线性回退当前参数为 `x_scale="jac"`、`ftol=xtol=gtol=1e-10`、`max_nfev=500`,没有传入解析 Jacobian 或 `jac_sparsity`(`app/simulation/solvers/algebraic.py:771-968`)。
**[推断]** 即使快速路径经常命中,固定的多次全网扫描、残差重建和热物性刷新仍会发生;一旦回退到有限差分 `least_squares`,成本会随全局未知量数量快速上升。
## 5. 每次导数计算的代数闭合
这一步就是第 0 节所说的“当前瞬间对账”。以高压气缸—节流孔—管路—低压储罐为例,求解器需要同时保证:
- 接头压力相容;
- 从一个组件流出的质量等于进入另一个组件的质量;
- 节流孔和管路自己的压差—流量关系成立;
- 气体携带的能量按实际流向交给下游;
- 若还有机械活塞或控制信号,它们在同一时刻也要一致。
代码目前不是一次对完,而是先建立初始压力解,再让 stream 焓和压力—流量相互迭代到同一个固定点。这样做是为了让当前 RHS 不依赖上一次调用留下的焓/流量历史,并保持有限差分 Jacobian 可重复。
`GenericFluidSystem._close_current_state()` 的实际顺序见 `app/simulation/systems/generic.py`:
| 顺序 | 操作 | 目的 |
| ---: | --- | --- |
| 1 | `SignalResolver.solve(time)` | 更新时间信号源并从 output 传播到 input |
| 2 | 传播机械 `x/v` 等值关系 | 把当前机械状态同步到刚性连接端口 |
| 3 | `PneumaticVolumeResolver.solve()` | 沿气动连接传播外部 `volume/volume_flow` |
| 4 | 刷新动态组件热力端口 | 由当前 `m/U/V` 和最新体积恢复压力、温度、焓 |
| 5 | 第一次 `PressureFlowSolver.solve()` | 建立本轮热流固定点的初始压力和流量 |
| 6 | 最多 25 轮 `StreamResolver.solve()` 后再求压力流量 | 让焓、温度引用和构成流量同时收敛;按流量变化判停 |
| 7 | 更新机械约束加速度 | 为机械状态导数准备 `a` |
随后 `rhs()` 才收集各动态组件的导数。
因此每次闭合至少有 **2 次**、最多有 **26 次**压力流量求解。外部容积传播会影响初始热力状态,但不再用“有没有容积传播”直接决定固定次数。stream 自身仍有相对容差 `1e-9` 和最多 100 次内部迭代;外层热流固定点最多 25 轮,两层上限不能混为一个数。
**[发现]** 这套顺序仍没有按模型实际能力完全裁剪。例如没有信号、没有外部容积源或 stream 不反向影响构成关系的网络,仍经过对应全网 pass。是否能安全删去某一 pass 必须由依赖关系、脏标记和回归测试决定,不能只凭某个算例结果不变。
## 6. 初始化、积分参数与推进方式
可以把初始化理解为“先摆好第 0 帧”,把积分理解为“根据每一帧的变化速度继续制作后续帧”。初始化并不会自动修改所有不合理的初始存量;它主要是在给定初始质量、能量、位置和速度后,求出与之匹配的端口压力、流量和力。
### 6.1 初始状态
`consistent_initial_state_vector()` 取得组件构造时形成的初值,应用该状态并执行一次完整代数闭合,然后原样返回状态向量(`app/simulation/systems/generic.py:292-296`)。
**[重要边界]** 这不是通用 DAE 一致初值求解:它不会联合调整微分状态及导数,只求“给定当前动态状态时”的端口代数变量。固定 TestModel 的专用压力投影/初始化逻辑不能外推为通用 XML 求解器能力。
### 6.2 参数来源及真实含义
初学者最需要先分清 XML v3 的 `sampleStep` 和 `maxStep`(进入 Python 后分别是 `sample_step` 和 `max_step`):
- `sampleStep` 只控制**结果多久记一条**;
- `max_step` 才限制**内部一次最多走多远**;
- 自适应求解器可以走得比 `max_step` 更短,也可能先试一步、发现误差过大后退回来重算。
所以,把 `sampleStep` 从 0.1 改成 0.01 通常会让输出曲线更密、结果占用更多内存,但它不等价于命令求解器固定每 0.01 秒算一步。
| 参数 | 当前来源/默认值 | 实际用途 |
| --- | --- | --- |
| `t_start / t_stop` | 前端与 Pydantic 默认 `0 / 2 s` | 积分区间 |
| `sampleStep`(内部 `sample_step`) | 默认 `0.1 s` | 仅生成输出 `t_eval` 采样网格;工程 JSON 仍暂名 `simulation.step` |
| `maxStep`(内部 `max_step`) | 默认 `0.005 s` | 自适应求解器内部已接受步的上限 |
| `method` | 默认 `BDF` | BDF、Radau、LSODA、RK45、RK23、DOP853 |
| `rtol` | 通用 XML 路径硬编码 `1e-6` | 外层 ODE 相对误差;用户不可配置 |
| `atol` | `SolveIVPConfig` 标量默认 `1e-8` | 同时用于不同量纲的全部状态;用户不可配置 |
| `first_step` | 默认 `None` | 交给 SciPy;用户不可配置 |
| 代数残差容差 | `1e-7` | 压力流量快速路径/接受标准 |
| 代数最大评估 | `500` | 单次 `least_squares` 上限 |
| stream 容差/迭代 | `1e-9 / 100` | 焓传播固定点 |
| 采样数上限 | `10001` | 限制输出样本,不限制 RHS 次数或事件数 |
前端/Pydantic 默认值见 `frontend/src/App.tsx` 的仿真默认配置和 `app/main.py:131-137`;通用路径构造 `SolveIVPConfig` 见 `app/main.py:684-701`;采样网格见 `app/simulation/systems/generic.py:204-225`。
**[已实现]** `sampleStep` 生成的采样网格会确保包含 `t_stop`,并在超过 10001 点时拒绝;它不会把 BDF 变成固定步算法。实际 RHS 次数由自适应误差控制、Jacobian 估计、拒绝步、事件重启和 `max_step` 共同决定。
### 6.3 逐步推进分派
`integrate_ode()` 位于 `app/simulation/solvers/solver.py:917-1009`:
- 有取消检查、信号断点或机械状态事件时,使用 SciPy 的低层 BDF/DOP853/LSODA/RK23/RK45/Radau 类逐步推进;
- 无上述需求时,可一次调用常规 `solve_ivp`;
- 当前流式接口总会传 `cancel_check`,所以总走逐步路径;
- 同步 XML 接口只有在没有断点/状态事件时才可能走一次性 `solve_ivp`。
逐步路径在每个已接受步上:
1. 检查取消;
2. 推进一步;
3. 构造 dense output,并插入跨越到的 `t_eval` 样本;
4. 检测状态事件;
5. 报告进度;
6. 必要时重建求解器。
Peng–Robinson 试探状态越界会抛 `RecoverableTrialStateError`;代码回到最后已接受状态,将 `max_step` 减半,最多重试 16 次(`app/simulation/solvers/solver.py:528-914`)。
仓库有固定 RK4 回退,但通用压力流量求解器本身依赖 SciPy;因此它不能被视为一般流体网络在无 SciPy 环境下的完整替代方案。
## 7. 事件、取消与停止
### 7.1 信号离散时刻
STEP0、UD00 等信号源提供离散事件时刻。积分器先推进到事件左侧的相邻浮点时刻,再在精确事件时间更新信号并重建求解器,连续动态状态保持不变(`app/simulation/solvers/signal.py:70-99`、`app/simulation/components/amesim/signals/sources.py:128-141`)。
### 7.2 机械端挡
机械状态事件在已接受步的 dense state 上检查端挡穿越,最多 60 次二分定位;命中后按塑性/恢复系数重置位置和速度并重启积分器。同一时刻最多允许 64 次链式状态重置(`app/simulation/solvers/mechanical.py:430-627`、`app/simulation/solvers/solver.py:92-166`)。
**[约定]** 这是为信号断点与机械端挡编写的专用事件框架,不是可由任意组件声明残差事件的通用高指数 DAE 框架。
### 7.3 协作取消
流式任务的取消端点只设置共享 `threading.Event`。积分器在已接受步、重试边界等检查点协作停止;若正在执行一次耗时的热物性、stream 或 `least_squares` 调用,取消不能立即抢占(`app/main.py:535-547`、`app/simulation/solvers/solver.py:917-1009`)。
停止后若至少已有两个有效采样点,通用系统可整理并返回部分结果;相关行为由 `tests/test_generic_system_xml_simulation.py:433-533` 覆盖。
## 8. 后处理与现有诊断
### 8.1 结果后处理
积分完成后,`GenericFluidSystem.simulate()` 重置机械约束模式,并对每个 `solution.t`:
1. 重新应用状态;
2. 再执行一次完整 `_close_current_state()`;
3. 提取所有组件级及端口级公开结果变量。
见 `app/simulation/systems/generic.py:397-474`。
**[推断]** 采样密集或结果变量多时,这会形成明显的第二计算阶段;此时内存中还保留积分状态矩阵,CPU 与内存峰值可能重叠。
### 8.2 当前返回的诊断
**[已实现]** 结果包含:
- 状态数、采样数;
- 压力流量 `solveCount`、最大残差、最大单次评估数;
- stream 最大迭代数;
- 停止状态及部分错误上下文。
**[已实现]** 积分诊断已经包含分段及汇总的 `nfev/njev/nlu`、已接受步、求解器启动、状态迁移和可恢复重试数。设置 `SIMULATIONAPP_PROFILE=standard|audit` 后,响应还会加入分阶段墙钟时间;audit 进一步记录物性调用、精确重复、缓存命中和逆解迭代。
**[发现]** `_close_current_state()` 中局部变量 `algebraic` 会被后续 pass 覆盖;最大残差/评估统计只采集每次闭合最后一次压力求解,而 `solveCount` 才累计了全部调用。仍缺少压力快路径命中率/累计 `nfev`、峰值 RSS、任务队列深度等服务级指标。结果字节和编码时间目前由离线基准工具测量,不进入常规 API 响应。
## 9. 求解时前后端交流
主路径可以压缩为下图:
```text
浏览器 ──一次 POST:完整 System XML──────────────> 后端
浏览器 <──同一长连接:心跳、进度、心跳、进度──── 后端求解线程
浏览器 <──最后一个消息:完整结果 JSON─────────── 后端
浏览器 ──需要时另发取消 POST───────────────────> 后端
```
这里的“流式”主要是**进度消息流式**,不是每算出一段曲线就立刻传一段曲线。最终数值序列仍在末尾一次性返回。
### 9.1 当前协议
| 阶段 | 通信 | 当前行为 |
| --- | --- | --- |
| 提交 | 一个 HTTP POST | 请求体为完整 XML;`X-Simulation-Id` 标识任务 |
| 运行 | 同一响应上的 NDJSON | 进度事件、错误事件、5 秒心跳 |
| 完成 | 同一 NDJSON 流最后一行 | 一次性携带完整 `SimulationResult` |
| 用户取消 | 另一个短 POST | 设置协作取消事件;原流继续等待终态 |
| 流断开/停滞恢复 | GET 状态 | 每 500 ms 轮询,最多 30 秒 |
后端路由见 `app/main.py:589-634, 773-906`,前端解析、取消和恢复见 `frontend/src/App.tsx` 中的 `streamSystemSimulation()` 及相邻任务控制函数。
**[已实现]** 后端每 5 秒无队列事件时直接发送 heartbeat。通用系统按进度至少变化 0.25% 才发送积分进度,通常至多约 400 条积分进度事件(`app/simulation/systems/generic.py:318-343`)。
前端规则:
- 30 秒没有收到任何字节:连接超时;
- 60 秒只收到心跳而没有真实积分进度:判定 stalled 并请求取消;
- 正常运行不是轮询,轮询仅用于异常恢复。
**[发现]** 合法但单个已接受步/闭合超过 60 秒时,前端可能误判停滞。后端结果事件的 `phase` 使用 `completed/stopped/stalled/failed`,前端事件类型却声明 `"complete"`;运行时当前没有按该字段做严格校验,所以契约漂移尚未直接报错(`app/main.py:825-840`、`frontend/src/App.tsx` 的流式事件类型)。
### 9.2 开发和部署连接数
**[已实现]** 开发态 Vite 将 `/api` 代理到 `127.0.0.1:8000`(`frontend/vite.config.ts:4-10`),所以一个流式仿真在开发态占用浏览器→Vite、Vite→FastAPI 两段长连接;若生产部署由 FastAPI/反向代理直接提供 API,则具体连接层数取决于部署。
仓库没有 WebSocket 路由、`EventSource` 或 `text/event-stream`;当前 NDJSON 只是普通 HTTP 分块响应。
## 10. CPU、线程、内存、网络和磁盘占用
先区分两个问题:
- **一个算例跑得快不快**:主要看每次变化率计算做了多少轮闭合、非线性试算和热物性计算;
- **多人同时运行稳不稳**:主要看并发任务是否有上限、是否能使用多个进程、每个结果在内存中保留多少份。
“每个任务开一个线程”不等于“每个任务独占一个 CPU 核”。Python 组件逻辑、SciPy 数值核和底层 BLAS 的实际并行程度取决于运行环境;仓库没有 CPU/RAM 实测数据,所以本节只给代码可证明的结构和数量级。
### 10.1 进程与线程
- `start-all.bat` 分别启动 Vite 与 FastAPI(`start-all.bat:19-22`)。
- `start-backend.bat` 的 Uvicorn 命令没有 `--workers`,当前脚本即单进程单 worker(`start-backend.bat:17-21`)。
- 每个流式仿真创建一个 daemon `threading.Thread` 和一个无界 `queue.Queue`;没有信号量、线程池或排队上限(`app/main.py:773-880`)。
- 全局任务字典只在读写元数据时持锁,不限制同时启动的求解数量。
- `POST /api/system-xml/simulate` 是 `async def`,但直接执行同步 CPU 求解;若调用该端点,会占用当前 Uvicorn 事件循环。
**[推断]** 单个求解主要是串行 Python 全网扫描加 SciPy 数值核,常会持续消耗一个核心;多任务线程不保证线性利用多核,还可能出现 GIL 竞争、SciPy/BLAS 原生线程过度订阅和内存峰值相叠。仓库没有固定 BLAS 线程数,具体 CPU 占用必须在目标部署环境实测。
### 10.2 内存数量级
不计 Python 对象常数项,主峰值可写为:
```text
O(组件 + 连接 + 代数结构)
+ O(采样数 × 动态状态数)
+ O(采样数 × 公开结果变量数)
```
当前采样上限是 10001。放大因素包括:
- 积分状态矩阵与后处理 `series` 在后处理阶段同时存在;
- 最终完整结果保存在全局任务记录中,又被编码为一个大型 NDJSON 行;
- 前端收到结果后执行 `structuredClone`,再 `JSON.stringify` 写入 `sessionStorage`(`frontend/src/App.tsx` 的结果快照与恢复逻辑);
- 图表会把数值数组映射为对象点数组,多个曲线窗口会产生更多前端副本;
- CSV 导出把完整结果再次上传,后端在 `StringIO` 中一次性构造完整 CSV(`frontend/src/SimulationResultsView.tsx:1041`、`app/main.py:299-377`)。
`SIMULATION_TASK_RETENTION_SECONDS=600`,但过期任务只在注册下一个任务时清理;没有新任务时,最后一批终态结果可能一直保留到进程退出(`app/main.py:491-520`)。
### 10.3 队列、网络和磁盘
- 任务队列是无界的,但进度被 0.25% 节流;正常单任务队列通常不大,客户端变慢或终态序列化时仍没有硬上限。
- 最终数值序列不分块,网络、后端 JSON 编码、前端字符串缓冲与 `JSON.parse` 会在完成时形成瞬时峰值。
- 通用 XML 求解本身不写仿真产物,结果主要驻留内存。
- 固定 TestModel 与 public Test MQL runner 会在 `app/data/simulation-runs` 下写时间戳产物;这不是主流式路径的磁盘行为。
## 11. 其他求解入口不能与主路径混同
### 11.1 固定 TestModel
`POST /api/reactflow/simulate-testmodel` 从所选类型/参数中提取固定数量的气瓶、贮箱、管/孔板来构造专用 `TestModelClosure`,不消费用户的任意节点边拓扑;它复用 `integrate_ode`,并写 CSV、SVG 和报告产物(`app/main.py:1363-1447`、`app/simulation/examples/testmodel/run.py`)。
它是回归/演示算例,不是通用 ReactFlow 网络求解器。
### 11.2 Test MQL
`POST /api/reactflow/simulate-test-mql` 当前忽略任意拓扑和主要积分配置;`TestMqlSystem.simulate()` 构造 132 状态的全零 `y`,只返回结构数量随时间的摘要(`app/main.py:1450-1481`、`app/simulation/examples/test_mql/system.py:6302-6318`)。
完整的 112 个气动状态与 20 个机械状态闭包存在于独立诊断/comparison 代码,但没有接入这个公开 API;仓库仍将其描述为校准阶段,不能宣称与 AMESim 全时域等价。
## 12. 当前明确的效率热点
| 热点 | 代码证据 | 影响范围 | 判断 |
| --- | --- | --- | --- |
| 每次闭合执行 2~26 次压力流量求解 | `generic.py` 的热流固定点 | 每个 RHS、初始化、每个结果采样点 | [实测] 代表气动算例平均 2.00~2.30 次;audit 包含时间占 71%~85% |
| stream 每轮复制/扫描并重复刷新 | `stream.py:29-119` | 每个闭合,最多 100 轮 | [已实现];网络越大越明显 |
| 非线性回退用全局有限差分 least-squares | `algebraic.py:927-947` | 快速路径失效时 | [已实现];大非线性网络潜在陡增 |
| 外层刚性积分器看不到显式稀疏 Jacobian | `solver.py:528-1009` | BDF/Radau 的每步/Newton | [已实现] |
| 热物性重复反算 | `mediums.py` 及各动态组件 refresh | 每个 RHS/闭合 pass | [实测] 闭合内精确重复率 82%~97%;PR 氦气缓存端到端收益约 8% |
| 每采样点完整后处理闭合 | `generic.py:397-474` | 输出点 × 全网 | [已实现] |
| 每个已接受步构造 dense output | `solver.py:528-914` | 流式逐步路径 | [已实现];无跨样本/事件时可能浪费 |
| 无界求解线程与任务结果驻留 | `main.py:491-520, 773-880` | 并发任务 | [已实现] 稳定性风险,不等于单算例变慢 |
| 完整结果单行 JSON 与前端多副本 | `main.py:825-840`、`App.tsx:8872-8958` | 大输出 | [已实现] 内存/网络热点 |
## 13. 优化建议排序
以下按**预期综合收益**排序;同档位优先低风险、低难度项。排序同时参考代码结构和 2026-08-15 的阶段/物性实测,但尚未覆盖大规模拓扑与多任务吞吐。“单算例”指一个模型的墙钟时间,“吞吐”指多任务服务能力。
### 13.1 先看人话版
在改算法前,应先给各阶段计时和计数;这本身不直接加速,但能防止优化错地方。之后可按下面顺序理解主要方案:
| 顺序 | 人话方案 | 为什么可能更快 | 主要风险 |
| ---: | --- | --- | --- |
| 1 | 少做重复“瞬时对账” | 当前每次闭合做初始压力求解和热流固定点,实测压力流量层最热 | 少做一轮可能漏掉真实耦合,必须按组件依赖和脏标记裁剪 |
| 2 | 先整理方程,再求解 | 合并重复未知量,把关联较弱的方程分组;大模型回退迭代时收益很高 | 连接、接触和跨域活塞会让分组出错 |
| 3 | 给不同状态使用合适的“尺子” | 质量、内能、位置、速度量级差异很大;合理缩放可减少无效内部步 | 容差改变会影响精度和事件时刻 |
| 4 | 相同输入不要重复查热物性 | 同一轮闭合中常以相同状态反算压力、温度等 | 缓存失效不严谨会产生错误结果 |
| 5 | 只计算、保存和传输需要的曲线 | 采样多、变量多时,可同时减少后处理、内存和网络开销 | 会改变默认结果合同,需要保留完整模式 |
| 6 | 给并发任务设固定“办理窗口” | 有界进程 worker 可防止无限建线程,并更好利用多核 | 对单个算例未必更快,跨进程取消和结果传递更复杂 |
下面的完整表把这些方向进一步拆成 12 项,并明确收益、风险和实施难度。
| 排名 | 建议 | 主要收益对象 | 预期收益 | 风险 | 实施难度 |
| ---: | --- | --- | --- | --- | --- |
| 1 | 将 `_close_current_state` 编译为按能力/依赖启用的执行计划:无信号不扫信号、无外部容积源不传播;仅在 stream 或体积确实使构成关系变脏时追加压力求解。保留可收敛的耦合迭代上限。 | 单算例 | 高;覆盖每个 RHS 和每个后处理点 | 中:错误裁剪会破坏耦合一致性 | 中 |
| 2 | 强化代数结构消元:合并 equality group 中重复未知量,按方程关联图分块,预编译残差/尺度;为非线性回退提供解析或稀疏 Jacobian/`jac_sparsity`。跨域活塞应按方程关联而非仅按物理域分块。 | 单算例、大网络 | 很高,尤其 least-squares 回退时 | 高:影响收敛与接触约束 | 高 |
| 3 | 为 BDF/Radau 提供状态缩放、分量级 `atol` 和 Jacobian 稀疏结构;允许有边界地配置 `rtol/atol`,依据物理时标选择 `max_step`,不要简单全局放宽容差。 | 单算例、刚性网络 | 中到高 | 中高:会改变误差轨迹/事件时刻 | 中高 |
| 4 | 在单次闭包内缓存热物性结果,并预计算 dynamic components、signal sources、stream components、端口引用和结果访问器;状态或体积变化时严格失效。 | 单算例 | 中到高,热力网络可能高 | 中:缓存失效错误会污染物理结果 | 中 |
| 5 | 改造结果选择和后处理:允许选择变量、采样/降采样;避免对不需要的变量和时间点执行完整闭合,必要时复用积分期间已接受的闭合快照。 | 单算例、内存 | 长仿真/多变量时高 | 中:结果合同与复用精度 | 中高 |
| 6 | 引入有界作业队列和固定大小的进程 worker;统一让同步端点也进入执行器,并设置最大并发、排队长度和结果尺寸。 | 吞吐、稳定性 | 高;单任务速度通常不变 | 中高:跨进程取消和序列化 | 高 |
| 7 | 进度与结果解耦:NDJSON 只发送进度和 `resultId`,结果按变量/时间块压缩下载或外部存储;前端改用 TypedArray/IndexedDB,图表先降采样。 | 内存、网络、UI | 大结果时高 | 中:需要版本化协议 | 中高 |
| 8 | 主动定时清理任务表,限制任务数/结果字节;成功交付后只保留摘要或引用。将无界进度队列改为“最新进度槽 + 不可丢终态槽”。 | 稳定性 | 中到高 | 低中 | 低中 |
| 9 | 只在当前步跨越下一采样点或需要状态事件检测时构造 dense output;记录并优化事件重启。大量周期 UD00 事件采用惰性调度。 | 单算例、事件密集模型 | 中 | 低到中 | 低到中 |
| 10 | CSV 在浏览器直接生成或按 `resultId` 服务端流式生成,避免全量 series 重新上传与 `StringIO` 全量复制。 | 内存、网络 | 中 | 低 | 低中 |
| 11 | 用共享 Schema/OpenAPI 生成前后端事件类型,修正 `complete/completed`;停滞依据服务端活动计数/已接受步时间戳并允许按模型调节。 | 可靠性、减少误杀重算 | 中 | 低 | 低中 |
| 12 | 长期评估支持稀疏残差/Jacobian 的 DAE 求解器,将外层 ODE 与内层代数 least-squares 统一成状态—代数系统。 | 复杂大模型 | 潜在很高 | 很高:架构与验证成本大 | 很高 |
### 13.2 推荐落地顺序
低侵入阶段/物性观测、积分计数和代表算例首轮基准已经落地,但不把“加指标”误列为直接加速。下一步建议:
1. 给压力流量层补快路径命中、累计非线性 `nfev` 和残差装配时间,继续拆解本次确认的首要热点;
2. 用同一套基准对执行计划裁剪、组件级精确物性复用做 `off` 模式 A/B;
3. 补 1/8/32 单元规模曲线、1/2/4 并发吞吐和峰值 RSS;
4. 记录状态/结果数组字节、任务队列深度;结果 JSON 字节可继续由基准工具测量;
5. 再决定代数分块/Jacobian、结果按需计算和进程 worker 的实施深度。
每项算法改动都应继续验证质量/能量守恒、正反流、stream 混合、机械端挡、信号断点、取消部分结果和 AMESim/TestModel 基线。相关测试证据包括 `tests/test_generic_system_xml_simulation.py:244-533`、`tests/test_core_solver.py:19-508`、`tests/test_amesim_mechanical_public_components.py`。
## 14. 已实现、约定与推断的边界汇总
### 已实现
- System XML 校验、拓扑编译和通用半显式 ODE/代数求解主链。
- 气动、机械和信号的专用闭合顺序。
- 压力流量显式因果化快路径与 `least_squares` 回退。
- 自适应积分、输出采样、信号断点、机械端挡、协作取消和部分结果。
- NDJSON 长响应、心跳、取消端点、异常恢复轮询和任务状态表。
- 单 Uvicorn worker、每任务 daemon 线程、完整终态结果驻留与浏览器多副本行为。
### 约定
- 内核定位为半显式 ODE/代数 MVP,而非任意 DAE。
- XML/组件运行参数使用 SI 基准值。
- 采样上限、超时、心跳和任务名义保留时长。
- 组件参数在单次运行中静态;时间变化通过信号源等模型表达。
### 已有初步实测、仍需扩大样本
- 压力流量闭合是当前代表气动短算例的首要热点;物性调用具有高精确重复率,现有 PR 缓存有可见端到端收益。
- 长氦气代表算例的积分阶段占约 90.5%,后处理约 5%。
- 上述结论仍需在更大拓扑、更多真实工程和固定硬件环境复测。
### 推断及必须继续实测
- 单个任务实际占用几个核心、SciPy/BLAS 原生线程数和多任务扩展曲线。
- 典型/最大工程的峰值 RSS、结果 JSON 大小、浏览器内存副本和 sessionStorage 成功率。
- 各优化的实际收益;表中排序应在观测数据出现后更新。
## 15. 关键文件与符号索引
| 主题 | 文件与位置 | 关键符号 |
| --- | --- | --- |
| API 主入口与任务流 | `app/main.py` | `run_system_xml_simulation()`、`simulation_event_stream()` |
| JSON/XML 网络编译 | `app/main.py` | `compile_reactflow_network()`、`compile_system_xml_network()`、`_compile_solver_network()` |
| XML v3 校验/解析 | `app/system_xml.py`、`schemas/system-simulation-v3.xsd` | `SystemXmlDocument`、`validate_system_xml_document()` |
| 通用系统准备与仿真 | `app/simulation/systems/generic.py:93-474` | `GenericFluidSystem`、`_close_current_state()` |
| 压力流量代数闭合 | `app/simulation/solvers/algebraic.py:86-968` | `PressureFlowSolver.solve()` |
| stream 焓 | `app/simulation/solvers/stream.py:29-119` | `StreamResolver.solve()` |
| 标量信号 | `app/simulation/solvers/signal.py:40-109` | `SignalResolver`、`signal_event_times()` |
| 气动外部容积 | `app/simulation/solvers/pneumatic_volume.py:21-93` | `PneumaticVolumeResolver` |
| 机械因果化与事件 | `app/simulation/solvers/mechanical.py:225-627` | `MechanicalStateReducer` |
| ODE 推进 | `app/simulation/solvers/solver.py:37-1009` | `SolveIVPConfig`、`integrate_ode()` |
| 可选性能埋点 | `app/simulation/performance.py` | `profile_run()`、`profile_phase()`、`profile_property()` |
| 可重复性能基准 | `app/simulation/benchmark_performance.py` | `python -m app.simulation.benchmark_performance` |
| 前端流式协议 | `frontend/src/App.tsx` | `streamSystemSimulation()`、取消/轮询 |
| 启动方式 | `start-backend.bat:17-21` | Uvicorn 单 worker 命令 |
| 主路径回归测试 | `tests/test_generic_system_xml_simulation.py`、`tests/test_core_solver.py` | 通用仿真、事件、取消 |