← 全部笔记

Note 02 数值

95 维状态、10 毫秒一步:
为什么我放弃了 RK4

7 MIN READ 阮家忆 数值 / 实时系统

生理引擎的状态是一个向量:心血管、呼吸、气道力学、肾、肝、内分泌、神经、免疫、胃肠、体温、电解质。写 RK4 的时候它是 95 维,现在是 100 维,结构没变。主循环固定 10 毫秒一步,也就是 100 Hz——现实过一秒,模型走一百步。

一开始我给全部状态上了四阶龙格库塔。理由很正当:通用、精度高、教科书都这么写。它在静息基线上跑得很好,好到我以为这件事已经解决了。

问题不在精度,在刚性

这 95 维里,绝大多数状态并不是自由的物理量,而是「向某个目标值一阶惯性趋近」的量。心率趋近目标心率,气道阻力趋近目标阻力,分泌率趋近由反馈算出来的分泌因子。目标值本身由别的方程给出,状态只负责跟上去。

这类方程的时间常数可以非常小。药物快速起效、病理突然触发的时候,目标值会在一步之内跳很远。显式 RK4 的稳定域是有限的,遇到小时间常数就开始振荡,再几步就发散。而发散不会礼貌地停在一个错误答案上,它先给你一个天文数字,然后变成 NaN。

更麻烦的是它时好时坏:静息时完全正常,只有在你连着推三个药、或者把严重度拉到最大演示给别人看的时候炸。这种 bug 不会让你怀疑积分器,只会让你怀疑自己。

换成指数逼近

后来我把「趋近型」状态全部改成半隐式的一阶惯性逼近,直接用解析解推进:

v ← v∞ + (v − v∞) · e−Δt / τ

这个式子等价于用隐式欧拉解 dv/dt = (v∞ − v) / τ,无条件稳定:不管 τ 多小、Δt 多大,结果一定落在旧值与目标值之间,不会越界,不会振荡,更不会发散。τ 趋于零时它自动退化为「瞬间到达目标」,这在物理上是对的——一个无限快的响应本来就该这样。

代价是精度。它只有一阶,RK4 是四阶。但这里的精度损失落在了一个观测不到的地方:模型是目标驱动的,稳态由目标值决定,不由积分误差决定。换句话说,RK4 多算出来的那几位有效数字,描述的是一个我们本来就不打算模拟的瞬态。

方式每步成本稳定性在这里的表现
显式欧拉最低差小 τ 时振荡
显式 RK4四倍有限稳定域静息正常,用药或病理突变时发散
半隐式指数逼近一次指数运算无条件稳定全程有界,可预测

为什么还留着导数

导数函数没有删。它现在是模型文档的一部分:读一遍导数,就知道每个状态受什么影响、被哪些病理和药物推动。它也是扩展接口——真要加一个非趋近型的量,比如某种需要守恒的积分,从那里接出去比在指数逼近里硬塞要干净。

但主循环不再调用它。实时系统里,一条不执行的路径如果还被当成主路径,它迟早会被改坏而没人发现——这正是上一篇说的静默失败:代码还在,语义已经没了。

在实时系统里,选积分器不是在选精度,是在选一种失败模式。 数值笔记 / 2026

步长与快进是两件事

10 毫秒是实时播放的步长,不是唯一的步长。要「过一天」的时候,86400 秒除以 10 毫秒是八百多万步,直接跑不现实。但快进不能简单把步长乘大:指数逼近虽然稳定,当 τ 小于步长时它会把「趋近」抹成「瞬间到达」,读数看着正常,动态全丢了。

所以快进有单独的标定脚本:用不同步长跑到同一个终点,比对关键读数,确定每个尺度下最多能放大到多少。这是一次性的工作,但它决定了快进按钮到底是在加速,还是在撒谎。

还是加了一张网

即使换了无条件稳定的积分器,我还是加了一道网:每一步扫描全部状态量,非有限值就回退到基线并计数。医学模拟器不能输出 NaN——不是因为它不好看,而是因为界面上一条 NaN 曲线和一条正常曲线看起来一样安静,而所有基于它的判定都会静默失效。

这张网从来没触发过。留着它的理由和买保险一样:它存在的价值不是被用到,而是让「用不到」这件事可以被相信。

所以结论是什么

如果状态是自由演化的、时间尺度彼此接近、而且允许离线慢慢算,RK4 是更好的选择,这一点没有争议。但如果状态大多是「趋近目标」、时间常数跨几个数量级、而且必须每一帧都给出有界的数,那么一个无条件稳定的一阶方法反而是正确的那一个。

复杂度不是先进程度。匹配才是。

Contact 交流

如果你也在做类似的东西,
欢迎写信。

806117943@qq.com ↗

物理、界面、数值方法,或者任何一个还没做完的模拟。