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

39 KiB
Raw Blame History

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 有一条完整回归气路:

高压气缸                         低压储气罐
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。它们可以这样理解:

结果快照:  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. 主求解调用链

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. 求解时前后端交流

主路径可以压缩为下图:

浏览器 ──一次 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 对象常数项,主峰值可写为:

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 通用仿真、事件、取消