543 lines
39 KiB
Markdown
543 lines
39 KiB
Markdown
# SystemSimulationApp 后端求解逻辑与效率优化调研(通俗版)
|
||
|
||
> 调研基线:2026-08-12(System XML v3 迁移后),依据当前仓库代码、配置、说明文档与测试。
|
||
> 本文所称“主求解路径”是当前前端实际调用的 System XML 流式接口;固定 TestModel 和 Test MQL 接口另行说明。文中没有把静态代码分析冒充 CPU、内存实测。
|
||
|
||
## 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. **[已实现] 每次 RHS 的完整闭合至少调用 2 次、存在外部容积传播时最多调用 3 次压力流量求解。** 积分结束后,每个输出采样点又执行一次完整闭合并提取全部公开结果。这是当前最明确的单任务重复工作来源。
|
||
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 节所说的“当前瞬间对账”。以高压气缸—节流孔—管路—低压储罐为例,求解器需要同时保证:
|
||
|
||
- 接头压力相容;
|
||
- 从一个组件流出的质量等于进入另一个组件的质量;
|
||
- 节流孔和管路自己的压差—流量关系成立;
|
||
- 气体携带的能量按实际流向交给下游;
|
||
- 若还有机械活塞或控制信号,它们在同一时刻也要一致。
|
||
|
||
代码目前不是一次对完,而是按固定顺序做若干轮专项检查。因此一个“计算变化率”的请求会调用 **2 次压力/流量闭合**;涉及外部容积传播时会调用 **3 次**。这也是后文首要优化方向。
|
||
|
||
`GenericFluidSystem._close_current_state()` 的实际顺序见 `app/simulation/systems/generic.py:258-290`:
|
||
|
||
| 顺序 | 操作 | 目的 |
|
||
| ---: | --- | --- |
|
||
| 1 | `SignalResolver.solve(time)` | 更新时间信号源并从 output 传播到 input |
|
||
| 2 | 刷新动态组件热力端口 | 由当前 `m/U/V` 恢复压力、温度、焓等 |
|
||
| 3 | 第一次 `PressureFlowSolver.solve()` | 闭合当前压力、流量、机械端口代数关系 |
|
||
| 4 | `PneumaticVolumeResolver.solve()` | 沿气动连接传播外部 `volume/volume_flow` |
|
||
| 5 | 若发生容积传播,再刷新热力状态并第二次求压力/流量 | 让变容边界进入气室状态关系 |
|
||
| 6 | `StreamResolver.solve()` | 按实际流向迭代传播/混合 `h_outflow` |
|
||
| 7 | 无条件再次求压力/流量 | 让依赖 stream/温度的构成关系重新闭合 |
|
||
| 8 | 更新机械约束加速度 | 为机械状态导数准备 `a` |
|
||
|
||
随后 `rhs()` 才收集各动态组件的导数(`app/simulation/systems/generic.py:298-301`)。
|
||
|
||
因此:
|
||
|
||
- 无外部容积传播时,每次闭合固定有 **2 次**压力流量求解;
|
||
- 有外部容积传播时固定有 **3 次**;
|
||
- stream 默认相对容差 `1e-9`、最多 100 次迭代,每轮复制端口焓并扫描组件/端口(`app/simulation/solvers/stream.py:29-119`)。
|
||
|
||
**[发现]** 这套固定顺序没有按模型实际能力裁剪。例如没有信号、没有外部容积源或 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-5` | 外层 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 最大迭代数;
|
||
- 停止状态及部分错误上下文。
|
||
|
||
**[发现]** `_close_current_state()` 中局部变量 `algebraic` 会被后续 pass 覆盖;最大残差/评估统计只采集每次闭合最后一次压力求解,而 `solveCount` 才累计了全部调用。现有响应还没有外层积分 `nfev/njev/nlu`、接受/拒绝步、重启次数、分阶段墙钟时间、热物性调用数、峰值 RSS、队列深度和结果字节数。
|
||
|
||
因此本文可静态定位重复工作,但不能用现有诊断精确量化每个热点的时间占比。
|
||
|
||
## 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~3 次压力流量求解 | `generic.py:258-290` | 每个 RHS、初始化、每个结果采样点 | [已实现] 重复 pass;真实耗时待测 |
|
||
| stream 每轮复制/扫描并重复刷新 | `stream.py:29-119` | 每个闭合,最多 100 轮 | [已实现];网络越大越明显 |
|
||
| 非线性回退用全局有限差分 least-squares | `algebraic.py:927-947` | 快速路径失效时 | [已实现];大非线性网络潜在陡增 |
|
||
| 外层刚性积分器看不到显式稀疏 Jacobian | `solver.py:528-1009` | BDF/Radau 的每步/Newton | [已实现] |
|
||
| 热物性重复反算 | `mediums.py:199-266` 及各动态组件 refresh | 每个 RHS/闭合 pass | [推断] 需调用计数确认 |
|
||
| 每采样点完整后处理闭合 | `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. 优化建议排序
|
||
|
||
以下按**预期综合收益**排序;同档位优先低风险、低难度项。收益是基于调用频率与复杂度的代码推断,不是基准测试结果。“单算例”指一个模型的墙钟时间,“吞吐”指多任务服务能力。
|
||
|
||
### 13.1 先看人话版
|
||
|
||
在改算法前,应先给各阶段计时和计数;这本身不直接加速,但能防止优化错地方。之后可按下面顺序理解主要方案:
|
||
|
||
| 顺序 | 人话方案 | 为什么可能更快 | 主要风险 |
|
||
| ---: | --- | --- | --- |
|
||
| 1 | 少做重复“瞬时对账” | 当前每次变化率计算固定做 2~3 次压力/流量闭合,调用频率最高 | 少做一轮可能漏掉真实耦合,必须按组件依赖裁剪 |
|
||
| 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. 记录每个闭合 pass 的调用数、墙钟时间、代数 `nfev` 与是否命中快路径;
|
||
2. 记录热物性调用数/迭代数、stream 迭代数;
|
||
3. 导出外层 `nfev/njev/nlu`、接受/拒绝步、事件与重启次数;
|
||
4. 记录状态/结果数组字节数、最终 JSON 字节、任务队列深度和进程 RSS;
|
||
5. 用小、中、大三类基准网络定位排名 1~5 的真实占比;
|
||
6. 先实施第 1、4、8、9 项的可回滚改造,再决定第 2、3、5 项的深度;
|
||
7. 服务并发需求明确后并行推进第 6、7 项。
|
||
|
||
每项算法改动都应继续验证质量/能量守恒、正反流、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 基准值。
|
||
- 采样上限、超时、心跳和任务名义保留时长。
|
||
- 组件参数在单次运行中静态;时间变化通过信号源等模型表达。
|
||
|
||
### 推断及必须实测
|
||
|
||
- 哪一类闭合 pass、热物性或 Jacobian 估计占主要墙钟时间。
|
||
- 单个任务实际占用几个核心、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()` |
|
||
| 前端流式协议 | `frontend/src/App.tsx` | `streamSystemSimulation()`、取消/轮询 |
|
||
| 启动方式 | `start-backend.bat:17-21` | Uvicorn 单 worker 命令 |
|
||
| 主路径回归测试 | `tests/test_generic_system_xml_simulation.py`、`tests/test_core_solver.py` | 通用仿真、事件、取消 |
|