2. 事件检测的数学基础
在微分方程数值仿真中,连续状态并不会自己“告诉”求解器何时发生了离散事件。事件检测的本质,是把一个物理或逻辑上的“事件发生”翻译成一个标量函数的零点问题:构造事件函数 \(g(t, x)\),让它在事件发生时刻恰好等于零,在事件前后取不同符号,再通过数值手段定位这个零点。本节围绕这一主线,依次说明事件函数的构造、穿越条件的判定、零点检测的实现、符号变化带来的信息,以及根隔离如何保证检测不遗漏、不误判。
一、事件函数:把“发生了什么”写成标量方程
事件函数是一个从状态空间映射到实数的连续(或分段连续)函数 \(g: \mathbb{R} \times \mathbb{R}^n \to \mathbb{R}\)。约定:当且仅当 \(g(t, x(t)) = 0\) 时,事件发生。这个约定看似简单,却决定了后续所有检测逻辑的形式。常见的事件函数有三类构造方式:
- 阈值型:物理量越过给定门限,如小球高度 \(h\) 触地,取 \(g = h\),零点即 \(h=0\);又如温度达到报警值 \(T_{\text{alarm}}\),取 \(g = T - T_{\text{alarm}}\)。
- 差值型:两个量相等时触发,如两物体追及,取 \(g = x_1 - x_2\);又如电池荷电状态与下限之差 \(g = \text{SOC} - \text{SOC}_{\min}\)。
- 复合型:由多个条件组合,如“速度为正且位置越过边界”,可写成 \(g = (x - x_b)\cdot \mathbb{1}[v>0]\),但要注意这种带指示函数的写法会引入不连续,需谨慎处理。
构造事件函数有一条重要原则:符号必须能稳定区分“事件前”与“事件后”。以小球落地为例,若取 \(g = h\),则落地前 \(g>0\)、落地后(若允许穿透)\(g<0\),符号变化清晰;但若取 \(g = h^2\),则落地前后都为正,零点处只是“触碰”而不穿越,检测逻辑必须单独处理这种“切触”情形。因此,事件函数不仅要零点正确,还要在零点邻域内具有可判读的符号行为。
二、穿越条件与符号变化
数值求解器只能在离散的时间步上采样 \(g\) 的值。设第 \(n\) 步与第 \(n+1\) 步的事件函数值为 \(g_n = g(t_n, x_n)\)、\(g_{n+1} = g(t_{n+1}, x_{n+1})\)。最常用的穿越判据是:
若 g_n * g_{n+1} < 0,则区间 [t_n, t_{n+1}] 内存在一次穿越型零点;
若 g_n == 0,则 t_n 本身即为事件时刻;
若 g_n * g_{n+1} > 0,则本步内无符号变化,默认无事件。
乘积为负意味着符号改变,这是“穿越”的代数标志。但符号变化并不等于一定发生了物理事件,也不等于只发生了一次事件。前者涉及“伪穿越”,后者涉及“一步多穿”。此外,若 \(g_n\) 与 \(g_{n+1}\) 同号而中间曾短暂变号又变回(例如高频振荡),离散采样会完全漏检——这正是需要根隔离与步长控制配合的原因。
穿越还有方向性。定义穿越方向为 \(g\) 从正到负(下降穿越)或从负到正(上升穿越)。很多工程事件只关心单一方向,例如“液位低于下限”只关心下降穿越,回程不应重复触发。实现上通常给事件附加一个方向参数 \(\text{direction} \in \{+1, -1, 0\}\),其中 \(0\) 表示双向都触发。方向判定可直接由 \(g_n\) 与 \(g_{n+1}\) 的符号关系得到,无需额外计算。
三、零点检测:从符号变化到精确定位
检测到符号变化只说明“区间内有根”,要得到事件时刻还需求根。由于 \(g\) 通常只能通过求解器插值或重新积分获得,解析根一般不可得,常用做法有三类:
- 线性插值(割线法):\(t^* \approx t_n - g_n \dfrac{t_{n+1}-t_n}{g_{n+1}-g_n}\)。实现最简单,一步到位,但当 \(g\) 在区间内弯曲明显时误差较大。
- Brent 法:在保持符号夹逼的前提下结合二分与反二次插值,收敛稳定且超线性,是多数事件检测器的默认选择。
- 密集输出重采样:利用求解器的稠密输出(dense output)在区间内多次采样 \(g\),先做粗扫描再做局部求根,适合 \(g\) 变化剧烈的情形。
无论哪种方法,都必须维持“夹逼区间端点异号”这一不变量,否则求根会发散或收敛到错误的根。定位精度通常由绝对容差与相对容差共同控制,例如取 \(\text{rtol}=10^{-6}\)、\(\text{atol}=10^{-9}\),并在达到容差或最大迭代次数时终止。
四、根隔离:保证不漏检、不误判
根隔离指在求根之前,先把可能含根的时间区间分割出来,使每个子区间至多含一个根,从而把“找所有根”化为“逐区间找单根”。它要解决两个问题:漏检(一步内多次穿越)与误判(把切触或数值噪声当成穿越)。
常用手段包括:限制最大步长,使单步内 \(g\) 的变化幅度受控;对 \(g\) 做单调性检查,若区间内 \(g'\) 不变号则至多一根;对高频或强非线性事件函数,改用固定小步长的扫描式检测,牺牲效率换取完备性。对于切触型零点(\(g\) 不变号),乘积判据失效,需要额外检测 \(|g|\) 的局部极小是否低于容差,或直接对 \(g'\) 做零点检测。
下面这张图把本节的核心逻辑串成一条检测链:从状态采样出发,经事件函数求值、符号变化判定、方向过滤,到区间求根与根隔离校验,最终输出事件时刻与切换请求。
五、一个可复现的数值示例
设一维运动 \(x(t)\) 满足 \(\dot{x} = -2x + 1\),初值 \(x(0)=3\),事件定义为“\(x\) 下降到 0.5”。取事件函数 \(g = x - 0.5\)。解析解为 \(x(t) = 0.5 + 2.5e^{-2t}\),令 \(g=0\) 得 \(t^* = -\tfrac{1}{2}\ln(0.2) \approx 0.8047\)。若用固定步长 \(h=0.1\) 的显式欧拉法推进,采样得到 \(g_8 = x(0.8)-0.5 \approx 0.0036 > 0\),\(g_9 = x(0.9)-0.5 \approx -0.0864 < 0\),乘积为负,判定区间 \([0.8, 0.9]\) 内有穿越。用割线插值:
t* ≈ 0.8 - 0.0036 * (0.9 - 0.8) / (-0.0864 - 0.0036)
≈ 0.8 + 0.0040 = 0.8040
与解析值 0.8047 相比误差约 \(7\times10^{-4}\),主要来自欧拉法本身的状态误差而非求根误差。若改用四阶 Runge–Kutta 并配合 Brent 求根,同一容差下事件时刻误差可降到 \(10^{-7}\) 量级。这个对比说明:事件检测精度 = 状态积分精度 + 求根精度,两者不可偏废。示例数值为教学演示设定,非实测数据。
六、实现要点与常见陷阱
把上述数学落到代码时,有几个细节直接决定检测是否可靠。第一,事件函数应在同一状态与同一时刻上求值,避免用上一步的缓存值参与符号判定,否则会引入一步滞后。第二,触发事件后若方程右端项发生切换,必须从事件时刻 \(t^*\) 重新起步积分,而不是从 \(t_{n+1}\) 继续,否则会“跳过”切换后的正确轨迹。第三,多个事件函数同时变号时,应按最早发生者优先处理,并在处理完一个事件后重新评估其余事件,防止同一时刻的耦合事件被错误排序。第四,对切触型零点与高频振荡,乘积判据天然不完备,需辅以 \(|g|\) 极小检测或缩小步长扫描。
综上,事件检测的数学骨架可以概括为:用事件函数把事件写成零点方程,用符号变化识别穿越区间,用方向参数筛选有效事件,用根隔离保证区间内单根,用求根算法精确定位事件时刻。这五步环环相扣,任何一步的疏忽都会以“漏触发”“误触发”或“触发时刻偏移”的形式出现在仿真结果中。