仿真平台与模型设计的第一性原理¶
本文从"为什么大多数工程系统没有解析解"这一根本性问题出发,分析仿真平台架构与模型设计的核心原则。目标在于建立一套仿真平台框架与模型设计决策的底层认知。
§ 0 问题的提出¶
如果你在大学中学过物理,或者做过任何动力学相关的课程设计,大概率见过这样的代码:
// "经典"的炮弹仿真
double x = 0, y = 0;
double vx = v0 * cos(theta);
double vy = v0 * sin(theta);
double dt = 0.01;
while (y >= 0) {
x += vx * dt;
y += vy * dt;
vy -= g * dt;
record(x, y);
}

这段代码跑起来,曲线很漂亮,交作业也够用。
这种感觉让很多人误以为:"仿真,不就是把公式写成代码吗?" 世界上的系统都可以被"一个公式"来描述。
但如果你的炮弹要考虑以下因素:
- 空气阻力随速度的平方变化(非线性)
- 随高度变化的空气密度
- 推进燃料燃烧导致的质量变化
- 风场的时变扰动
- 制导控制律的实时介入
这时候,x(t) = ? 这个问题没有答案——不是因为我们不够聪明,而是因为通常不存在可直接用于工程求值的闭式解析表达。
这就引出了数值积分的必要性。
数值积分的本质:是用"无数个现在"拼出"整个未来"
数值积分的核心思想,其实极其简单:
既然我不知道 \(t\) 时刻状态的公式,但我知道此刻的状态和此刻的变化速率——那我就用"变化速率 × 时间",算出下一时刻的状态。
用数学可表示为:
其中 \(f(...)\) 是状态变化率(导数),不需要知道 \(x(t)\) 的公式,只需要知道"此刻变化有多快",这一步一步往前推的过程,就是数值积分。
回头看最开始的那段炮弹代码,其实就在做这件事:
// 以下做的事,其实是数值积分
x += vx * dt; // 位置 += 速度 × 时间步
y += vy * dt;
vy -= g * dt; // 速度 += 加速度 × 时间步 (欧拉法)
炮弹这个例子,它确实有解析解,但你的代码是在"多此一举"地数值模拟一个可以直接算出答案的问题。但对于复杂系统,这条路就是唯一的路。
法则一
仿真的本质,不是"把公式翻译成代码",而是"把状态方程翻译成时间推进"。你的程序不是在算答案,而是在推答案。这两个字的差别,决定了仿真平台所有设计决策的出发点。
以上那段我们都编过的朴素代码真正的问题在哪里?¶
如果"逐步推进"这个思路本身没问题,那最开始那段炮弹代码到底有什么问题?
单独用来算炮弹,没什么问题。但如果你要做一个可复用的仿真平台,那段代码的结构就会带来一系列麻烦:
问题一:模型和积分算法耦合在一起
看这行代码:
这一行同时做了两件事:
- 物理描述:重力场使速度下降(这是"模型的事")
- 积分算法:用欧拉法推进状态(这是"平台的事")
两件事写在了一起,所以:如果你想换一个更精确的积分方法(比如 RK4),你得改模型代码;如果你想复用这段物理代码到另一个平台,你无法单独提取。模型和平台耦合在了一起。
那模型和平台的分离写法应是怎样?正确答案应是这样:
模型只返回:dvy = -g
平台来执行:vy += dvy * dt
模型和平台职责清晰,互不干扰。
问题二:什么都混在一起,平台无从调度
真实系统里,你有飞行器动力学(1000 Hz)、控制律(200 Hz)、传感器模型(100 Hz)、任务逻辑(10 Hz)……它们更新频率不同,谁先算谁后算,都是有讲究的。
如果所有东西都像那段代码一样写在一个大 while 循环里,平台根本无法管理调度频率、无法处理事件、无法做检查点恢复。
问题三:没有"状态"的概念
那段代码里的 x, y, vx, vy 只是一些局部变量。平台不知道哪些是"需要被积分推进的状态",哪些是"由当前状态直接算出来的代数量",哪些是"事件触发才改变的离散状态"。这种区分,在大型仿真里极其重要。
认识到上面的问题之后,一个高质量仿真架构的核心分层就变得非常自然:
模型负责回答"世界怎么变化";平台负责"用正确、稳定、可重复的方式把这种变化算出来"。
这句话听起来简单,但真正地理解了它,你就会对很多"为什么要这样设计"的问题就会豁然开朗。
§ 1 解析解的本质限制¶
在理论力学或常微分方程课程中,求解问题通常以"得到解析解"为终点。理想弹道运动是一个典型例子——在匀强重力场、无阻力、质量恒定的假设下,运动方程是线性的,状态可以显式地表达为时间的函数:
解析解的核心特征是:状态量对时间的映射是一个封闭的代数表达式,可以在任意时刻直接求值,无需历史信息。
这类解的存在,依赖于系统方程在某种意义上的"好性质"——线性、常系数、简单边界条件。一旦这些条件不成立,解析解一般不存在,不是因为数学工具不够强,而是因为描述系统的微分方程本身没有初等函数形式的积分。
考虑一个稍微现实一些的飞行器模型:空气阻力与速度平方成正比,密度随高度非线性变化,推力随燃料消耗而减小,控制律在连续时间上修正姿态角。系统的状态方程仍然形式上存在:
其中 \(x \in \mathbb{R}^n\) 为状态向量,\(u\) 为控制输入,\(t\) 为时间,\(p\) 为参数向量;\(f\) 通常是高度非线性的向量场。但方程右端 \(f\) 的非线性结构,使得 \(x(t)\) 无法被表达为 \(t\) 的初等函数。这不是工程近似或偷懒,而是方程的内在性质。在局部 Lipschitz 等条件下,Picard–Lindelöf 定理保证局部存在与唯一性,工程上仿真通常再借助数值方法在可计算区间内构造近似解,多数非线性耦合系统通常不能化为可直接求值的闭式表达。因此,数值积分不是"求不出解析解时的退而求其次",而是针对这类问题的正确工具。
注:即便对于有解析解的系统,数值积分仍然是必要的——因为实际仿真往往需要将多个子系统耦合,而耦合后的系统整体通常不再具有解析解,即便每个子系统单独具有。
§ 2 数值积分的数学本质¶
数值积分的基本思想来自于微分的定义。给定上述方程和初始条件 \(x(t_0) = x_0\),Euler 法给出最简单的时间离散化:
局部截断误差为 \(O(\Delta t^2)\),全局误差为 \(O(\Delta t)\),即一阶方法。
Euler 法的推导不需要对 \(f\) 有任何特殊假设,因此在原理上可以应用于任何满足 Lipschitz 条件的右端项。这是数值积分作为通用求解框架的根本来源。当然,Euler 法精度低,在步长较大时会积累显著误差,在刚性系统中往往不稳定。实用的积分器——Runge-Kutta 族、Adams 多步法、Dormand-Prince 自适应步长方法、BDF 族——本质上都是对 Euler 思想的精细化:通过在当前步内多次计算 \(f\) 的值,以更高阶的方式逼近积分。
理解这一点有一个重要的推论:积分器的选择是算法层面的决策,与系统动力学的描述无关。方程中的 \(f\) 描述了物理现实;选择用 Euler、RK4 还是自适应步长 Dormand-Prince 来推进这个方程,是数值分析的问题。这两件事的分离,是仿真架构设计的第一条公理。
§ 3 从数学分离到工程分离:模型与平台的边界¶
上一节的推论在数学上是显然的,但在工程实现中却经常被违反。初学者写出的仿真代码往往是这样的:
// 常见反模式——模型与积分耦合
double x = x0, v = v0;
while (t < T) {
double F = -k*x - c*v + external_force(t);
v += (F/m) * dt; // 物理方程和 Euler 步混在同一行
x += v * dt;
t += dt;
}
这段代码将"系统的动力学"(弹簧-阻尼方程)和"积分算法"(Euler 法)写在了同一个表达式里。表面上这是无害的简洁,实则带来了一系列结构性问题。
首先,若要将 Euler 法替换为 RK4(四阶龙格-库塔法),必须重构物理代码本身,因为 RK4 需要在当前步内以不同的状态值多次调用 \(f\)——而在上面的写法里,\(f\) 的计算散落在循环体中,没有办法独立提取。其次,积分步长 dt 直接进入了模型层,使模型失去了与调度机制的独立性。第三,这样的代码无法纳入多速率仿真框架,因为平台无从区分"这段代码描述了什么物理现象"和"这段代码在执行什么数值操作"。
正确的做法是强制执行以下边界:
模型的唯一职责是将当前状态和输入映射到状态导数——即实现 \(\dot{x} = f(x, u, t, p)\)。平台的职责是管理时间推进、调用积分器、协调调度、记录数据、处理事件。两者之间的接口是函数 \(f\) 的签名,除此之外没有任何耦合。
这一分离的价值不只是代码的整洁性。它使得同一个物理模型可以不经修改地在不同精度等级的积分器下运行;使得平台可以在模型完全不感知的情况下切换到自适应步长、开启并行积分、插入误差检测;使得模型函数可以被独立单元测试,不需要搭建完整的仿真环境。这些性质在大型仿真系统的开发和验证中至关重要。
法则二
模型和平台这种分离,和软件工程里"业务逻辑与框架分离"的思想完全一致。模型是"业务逻辑"——描述系统动力学;平台是"框架"——负责调度、执行、记录。好的框架让业务逻辑不需要关心执行细节。
面向显式状态方程模型的基础接口¶
一个满足上述原则的ODE(常微分方程)模型,其接口在 C++ 中大致如下:
class DynamicModel {
public:
// 核心接口:给定 (t, x, u),输出状态导数
virtual void compute_derivatives(
double t,
const StateVec& x,
const InputVec& u,
DerivVec& dx_dt // 输出:各状态量的变化率
) = 0;
// 输出方程:可观测量,不参与积分
virtual void compute_outputs(
double t,
const StateVec& x,
const InputVec& u,
OutputVec& y
) = 0;
// 以下为平台生命周期钩子,不含物理逻辑
virtual void initialize(const Params& p) = 0;
virtual void handle_event(const Event& e) = 0;
virtual void save_state(Checkpoint& cp) const = 0;
virtual void load_state(const Checkpoint& cp) = 0;
};
注意 compute_derivatives 的签名中没有 dt——这不是疏漏,而是原则。导数的计算不应知道步长;步长是积分器的事。
§ 4 状态量的分类:一个被低估的设计决策¶
在具体的模型实现中,有一个常被忽视但影响深远的分类问题:一个仿真系统中的变量,并非都以相同的方式随时间演化,因此不能以相同的方式处理。至少需要区分以下三类:
连续状态是微分方程的解分量,通过积分器推进。典型例子是位置、速度、姿态四元数、角速度。这类量的特征是:它们的演化是连续的,在时间上没有跳跃,因此积分器可以为它们估计局部误差并自适应步长。
离散状态由事件触发,不经过积分器,在事件发生时以跳跃方式改变。飞行阶段编码、制导模式标志、故障位、武器状态机——这类变量的演化逻辑是条件分支和状态转移,而非微分方程。将它们混入连续积分流程中,积分器会对它们应用毫无意义的插值和误差估计,结果既错误又浪费计算资源。
代数量由当前时刻的状态和输入直接计算,没有历史依赖,不需要存储推进。动压 \(q = \tfrac{1}{2}\rho v^2\)、攻角、侧滑角、观测矩阵——这类量在每个时间步重新计算即可,不应占用状态向量的维度。如果将代数量当作连续状态处理,不仅造成不必要的状态维度膨胀,还会引入积分误差累积,而这些量本来是无误差可言的。
对含约束系统,还需单独考虑代数状态/DAE(微分代数方程) 变量。
对这三类量的区分带来的工程含义是直接影响检查点存储、状态空间大小、调度策略和事件检测机制的设计。在 Simulink 的 S-Function、AFSIM 的模型接口、JSBSim 的状态管理中,都能看到对这一分类的显式或隐式处理。在自行构建仿真框架时,若不在早期做出这一区分,后期的重构成本通常是不可接受的。
§ 5 平台的核心责任¶
厘清了模型的职责边界之后,平台应当承担什么就变得清晰了。以下几项能力的重点不在于罗列功能,而在于说明每项能力背后的必要性。
多速率调度¶
真实的飞行仿真中,不同子系统的特征时间尺度相差数个量级:刚体动力学的特征频率可达千赫兹量级,控制律在几百赫兹,传感器模型在几十至一百赫兹,任务逻辑在个位数赫兹。如果平台以所有子系统中最高的频率统一推进,则低频模块被无意义地重复调用,计算开销以数量级增长;如果以最低频率推进,则高频动力学被严重欠采样,数值解完全失真。多速率调度的本质是为每个子系统维护各自的更新时钟,在主积分步内按需触发,并在速率边界处做正确的插值或零阶保持。这不是性能优化,而是保证数值正确性的基本要求。
混合系统的事件机制¶
大多数工程仿真系统是混合系统——连续动力学和离散事件的耦合。接触碰撞、相变、控制律切换、武器发射时序……这些事件不能简单地在 Euler 步的末尾检查条件然后修改状态,因为这样做会引入最多一个步长的事件时刻定位误差,在高频动力学中可能产生无法接受的误差积累。成熟的平台使用事件检测算法(如基于 Brent 方法的根求解)在积分步内以高精度定位状态穿越事件的精确时刻,在该时刻暂停积分、处理状态跳跃、然后以正确的后事件状态重启积分。这一机制的缺失,是导致仿真结果"看起来跑通了但数值不可信"的最常见原因之一。
数值稳定性与误差控制¶
工程系统中存在大量刚性问题——不同时间尺度的动态过程耦合在同一个状态方程中。刚体姿态动力学和推进剂晃动的耦合,结构弹性模态和刚体运动的耦合,这类系统用显式定步长积分器(如 RK4)求解时,为维持稳定性所需的步长往往远小于精度所需的步长,导致计算效率极低。平台应提供隐式积分器(如 BDF 族)作为选项,并在可能时支持自适应步长——在解变化平缓时放大步长,在解剧烈变化时缩小步长,以在精度和效率之间动态平衡。此外,守恒量监视(能量、动量、四元数模长)是检验数值解是否可信的重要手段,平台应内建支持,而不是把这个责任推给模型开发者。
可重复性基础设施¶
仿真结果的可重复性在工程验证中是一个必要条件,但在实现层面并不自明。随机扰动的种子管理、浮点运算的确定性、检查点恢复的一致性——这些都是平台层面的系统性工程,不能依靠约定俗成由模型开发者自行处理。一个没有严格可重复性保证的仿真平台,无法支持回归测试、算法对比和故障复现,从而在工程价值上大打折扣。
综合以上平台的职责,平台的主循环自然地大概长这样:
initialize();
while (sim_time < end_time) {
read_inputs(); // 读取外部输入
compute_derivatives(); // 调用各模型,得到导数
integrate_states(); // 平台自己决定用什么积分器
process_events(); // 处理离散事件
publish_outputs(); // 把结果发出去
record_data(); // 记录时间序列
sim_time += dt;
}
finalize();
模型看不到这个循环。平台可以在不改任何模型代码的情况下,把积分器从欧拉法换成 RK4,或者开启自适应步长——模型完全不感知这些变化。
平台七件必须做好的事¶
有了"模型与平台分离"的大原则,作为支撑复杂仿真系统的基础设施,下面梳理平台侧起码应该做好的几件事。
1. 统一时钟与时间推进机制
平台要有一个权威的 sim_time,所有模型的时间都由它派发。这听起来是废话,但很多初学者写的仿真里,每个模型自己维护时间,最后出现时序不一致的 bug,排查极其痛苦。
2. 积分器作为可替换模块
平台要内置多种积分器(Euler、RK2、RK4、自适应步长……),并且可以在不修改模型的情况下切换。这让你在开发阶段用快速的 Euler 法调参,在验证阶段换成高精度 RK4,而不需要动任何物理代码。
3. 多速率调度
飞控 1000 Hz、导航 200 Hz、任务逻辑 10 Hz——平台必须能同时管理这些不同频率的模块,在正确的时刻更新正确的模型,并处理模块间的插值和数据对齐。这是一个真实系统中最容易被低估的复杂度。
4. 事件机制
真实系统不是纯粹的连续动力学。飞行器触地、模式切换、控制律重构、通信中断——这些"事件"在积分循环中会突然改变系统行为。平台必须有事件检测和处理机制,否则你只能仿真"什么都不会发生的系统"。
5. 数值稳定性监控
"能跑起来"不等于"结果可信"。数值积分可能在你毫无察觉的情况下发散。一个负责任的平台会检测 NaN/Inf、监控守恒量、在步长过大时告警,而不是默默给出一个错误的答案。
6. 数据记录与可重复性
仿真结果必须可以重复。固定随机种子、全状态快照、checkpoint/restart——这些功能让你能复现一个罕见的故障场景,能做回归测试,能比较两套算法在完全相同初始条件下的差异。
7. 模型耦合机制
动力学、气动、推进、导航、传感器、控制器——这些子模型互相依赖,平台要提供清晰的连接方式(变量总线、消息机制、数据同步),让模型之间的依赖关系显式可见,而不是在代码里到处全局变量。
§ 6 仿真平台的分层结构¶
将上述讨论组织为一个架构视图,成熟的仿真平台通常可以形成以下分层结构,每一层有清晰的职责边界,层间通过定义良好的接口通信:
| 层次 | 核心职责 |
|---|---|
| 可视化 / 分析层 | 时间序列曲线、三维态势显示、后处理统计分析、结果对比报告。这一层消费数据,不产生数据,与仿真核心解耦。 |
| 数据 / 实验层 | 高速数据记录、时间序列存储、检查点管理、Monte Carlo 驱动、参数扫描框架。负责仿真过程的可观测性和可重复性。 |
| 耦合 / 总线层 | 模型间变量的映射与路由、消息总线、跨速率数据同步、时间对齐与插值。使多模型系统的连接关系显式可管理。 |
| 模型抽象层 | 统一的模型接口规范:状态声明、导数计算、输出方程、事件处理、状态序列化。是平台与具体物理模型之间的契约。 |
| 求解器 / 积分器层 | ODE/DAE 数值求解、固定与自适应步长控制、局部误差估计、事件检测与定位、刚性问题的隐式处理。 |
| Runtime / Executive 层 | 仿真时钟权威管理、多速率任务调度、生命周期管理(初始化、运行、终止)、实时与非实时模式切换。 |
这个分层的形成并非自顶向下的预先设计,而是大量工程系统在实践中反复碰壁、反复重构之后自然收敛的结果。Simulink、AFSIM、DIS/HLA 体系、OpenFlight 这些平台,在架构上的差异主要体现在各层的具体实现和层间接口的细节,而不是这个分层结构本身。理解这一点,阅读任何仿真平台的文档时,就能迅速识别出每个概念在这个框架中的位置。
§ 7 模型设计的若干原则¶
在平台提供了上述基础设施的前提下,模型开发者需要遵守对应的约束,才能使这套架构真正发挥作用。
最重要的约束是导数函数的纯粹性:compute_derivatives 应当是一个数学意义上近似纯的函数——给定 \((t, x, u, p)\),输出确定,无内部状态修改,无全局副作用。这一性质不是风格建议,而是功能需求。自适应积分器(如 Dormand-Prince)在每个推进步内会以不同的中间状态多次调用 \(f\);若 \(f\) 有副作用,这些重复调用会产生无法预期的状态污染。同理,并行积分要求多个线程同时调用 \(f\) 的不同实例;副作用的存在意味着必须引入锁,而锁往往消除了并行的性能收益。
其次是参数与状态的严格分离。模型的物理参数(质量、气动系数、控制增益、采样率)应通过外部配置传入,不应硬编码在实现中。这不仅是代码可维护性的问题,更是实验设计的需要:Monte Carlo 分析要求在不同参数实例上并发运行同一模型;参数扫描要求在参数空间上系统性地重用模型;这些都依赖参数在运行时可注入的能力。
第三是避免在模型内部实现时间管理。模型不应持有对仿真时钟的引用,不应自行累积时间步,不应对调用频率有任何假设。时间由平台作为参数传入 \(f\);模型对调度机制完全透明。违反这一原则的模型,在被移植到不同速率的调度器时,会出现时序行为的改变,而这种改变往往难以诊断。
关于模型验证:纯粹的导数函数有一个额外的好处——它可以被单独验证。给定已知解析解的测试用例,可以在不运行完整仿真的情况下,对 \(f\) 的输出与理论导数进行逐点比较,从而将模型验证与数值积分误差完全解耦。这在高可靠性仿真系统的 V&V 流程中是一个重要实践。
如何写一个"好的"仿真模型¶
这些原则不是为了好看,每一条都是上述的工程理由。
原则一:写成状态方程,不要写成流程脚本¶
区分"状态方程"和"流程脚本",是建模能力最重要的分水岭。
❌ 流程脚本风格:
把所有逻辑杂糅在一起
模型内部又算物理,又记日志,又更新全局状态,又检查边界条件,各种逻辑混杂,无法测试,无法复用。
✅ 状态方程风格
清晰分离关注点
参数 p → 状态 x → 输入 u → 导数 dx/dt → 输出 y。每个函数只做一件事,平台统一接管其余的一切。
原则二:模型函数尽量纯净(无副作用)¶
一个好的模型函数,输入确定了,输出就确定。它不在内部偷偷修改全局变量,不依赖调用顺序,不记录状态到文件里。
这不是洁癖——它带来的好处非常实际:
- 易测试:给定输入,验证输出,不需要搭建仿真环境
- 易并行:多个模型可以并发调用,没有竞争条件
- 易替换积分器:积分器可能需要多次调用导数函数(RK4 就需要 4 次),纯函数不会有副作用积累的问题
- 易做灵敏度分析:自动微分和优化算法依赖这一点
原则三:区分连续状态、离散状态和代数量¶
很多初学者把所有变量都混成一锅。但这三类变量的性质完全不同:
- 连续状态(如位置、速度、姿态):通过积分推进,每个时间步都平滑变化
- 离散状态(如飞行模式、故障标志):事件触发才改变,不走积分流程
- 代数量(如动压、攻角):由当前状态直接算出,没有"历史",不需要存储推进
不区分这三类,平台的调度器就无从下手——它不知道哪些变量要积分,哪些要事件触发,哪些在每步重新计算。
原则四:参数外部化,不要把场景写死¶
常见错误模式:
把质量 m = 500.0、阻力系数 Cd = 0.42、控制增益 Kp = 2.3 硬编码在模型类里。然后每次换工况,改代码,重新编译,版本管理一团乱麻。
模型的参数应该通过配置文件(JSON/YAML/XML)或参数数据库传入。这样同一份代码可以用于不同平台、不同工况,也可以做参数扫描和 Monte Carlo 分析。
结语¶
本文想阐明的核心观点是:仿真平台的架构设计,以及模型设计的规范,都不是凭经验积累的约定俗成,而是由数值积分的数学性质直接推导出来的。一旦接受"大多数工程系统不存在解析解,因此仿真的本质是时间推进而非公式代入"这个前提,"模型与积分器分离"、"状态量分类管理"、"平台统管调度与记录"这些原则就不再是需要死记的规则,而是必然的推论。
用一句话来概括:模型的职责是忠实描述系统在每一时刻的动力学;平台的职责是以数值上正确、稳定且可重复的方式将这种动力学在时间上积分出来。 这条边界的任何模糊,都会以结构性的代价在系统规模增长时显现出来。
理解了这一点,再看那些成熟仿真平台在接口设计上的种种"繁琐"——为什么模型要实现这么多钩子函数,为什么平台要区分这么多变量类型,为什么架构要分这么多层——你会发现这些不是过度工程化,而是对问题本质的准确响应。
这些有点抽象结论对于我们来说意味着什么?我们可以把它翻译成更具体的行动建议:
1)下次写仿真代码时,先问自己:这里的"模型"是什么?"平台"是什么?
哪些代码在描述物理现象,哪些代码在管理时间推进?尝试把它们写在不同的函数或类里,即使只是课程作业,也能建立这种分离意识。
2)学会写 compute_derivatives() 这类接口
练习把物理模型写成"给定 x 和 u,返回 dx/dt"的纯函数。这是高质量仿真代码最核心的能力,也是接入任何专业仿真框架的基础语言。
3)遇到"只能用数值积分"的问题时,不要觉得遗憾
这不是退而求其次。大多数有价值的仿真问题都没有解析解——数值积分不是无奈的选择,而是唯一的路径。理解这一点,你就不会再把复杂问题的数值求解当成"hack",而是一套正式的工程方法。
4)如果有机会使用成熟仿真框架,先读它的架构文档
AFSIM、FLAMES、EADSIM这些平台,以及 DIS/HLA 互操作体系的设计,都能在本文描述的框架里找到对应。理解它们为什么要有这些层,比会用几个 API 更重要。
5)数值结果不可信比程序崩溃更危险
仿真工程里有一个隐患:程序跑得好好的,曲线看起来很合理,但结果其实已经在慢慢发散。养成检查能量守恒、动量守恒等物理约束的习惯,这是验证数值积分结果是否可信的最基本手段。
理解了这些,你再看那些成熟仿真平台的设计,那些看起来"过度复杂"的接口和分层,就会觉得恰如其分——甚至可能觉得,它们是怎么被逼出来的,我现在完全能理解。
这,就是从第一性原理理解仿真平台与模型分离设计理念的感觉。