迟到观测的 EKF 回退:512 项检查点与批量输入契约

作者:

在

结论:rednose EKF 处理迟到观测的方式是回退到不晚于该观测的检查点,再按时间顺序重放后续观测,而不是丢弃或就地覆盖。可回退的历史同时受时间阈值与 512 条检查点容量两个限制,所以不能把「0.8 秒」理解为绝对保证可回退的长度。另一个关键边界是返回值:迟到样本返回的 Estimate.xk/Pk 是「新观测刚处理完」的结果,返回前的重放可能继续推进对象内部状态,因此它不能当作当前最新状态。固定快照基线为 c8fb906,未生成滤波模型、未运行数值测试或实车验证。

关键组件与调用链

业务包装 KalmanFilter.predict_and_observe() 在 data 非空时升到二维;未传 R 时按 len(data) 复制观测噪声矩阵,然后把时间、类型、样本、R 交给 Cython。Cython 桥 EKF_sym_pyx.predict_and_update_batch() 把每条 z、R 及每组 extra_args 转给 C++;默认 extra_args=[[]],仅适配单条观测,过旧时把 C++ 空结果映射为 None。

组件 契约
KalmanFilter.predict_and_observe() data 非空时升到二维;未传 R 时按 len(data) 复制噪声矩阵;包装层未暴露 extra_args。
EKF_sym_pyx.predict_and_update_batch() 把每条 z、R 及每组 extra_args 转给 C++;默认仅一组空参数。
EKFSym::predict_and_update_batch(double, ...) 当前时间更早时检查历史窗口;能回退则先处理新观测,再依时间顺序重放移出的后续观测。
rewind() / checkpoint() 三个同步队列保存时间、状态 / 协方差、观测;逐出早于新观测的后续项,以最后一个 <= t 的检查点恢复;容量常量为 512。
数值核入口 三数组等长断言;每批预测一次,按样本顺序更新并记录前后状态;augment=True 目前被断言拒绝。

迟到观测的实际顺序

时间 t 小于当前 filter_time 才进入回退分支。若无检查点、t 早于最早检查点,或 t < 最新检查点时间 - max_rewind_age,函数直接返回空,不更新状态。两个比较都为严格小于,因此等于边界并不因该条件被拒绝;但仍须有足够早的检查点。

允许回退时,rewind() 移出时间 > t 的检查点和原观测,以最后一个 <= t 的检查点恢复 x、P、filter_time,随后运行新观测,再逐个重放被移出的观测。相同时间的旧检查点不被移出,新观测将排在原有同时间观测之后。每次重放都会重新建检查点;这是基于缓存观测的重新计算,不是恢复旧的最终数值。返回的 Estimate.xk/Pk 是新观测刚处理完的结果,返回前重放可能继续推进对象内部状态,应从 state() / covs() 读取重放后的状态。

边界 数值 / 语义
检查点容量 常量 512 项,与时间阈值共同限制回退窗口
业务侧最大回退年龄 locationd 显式使用 0.8 秒
Cython 默认 1.0 秒,CarKalman 未覆盖
IMU 时间一致性 传感器纳秒时间与消息单调时间差不超过 0.1 秒
相机观测时间 timestampEof - 0.1 s

业务层的 0.8 秒预检与底层「检查点可用」不是同一个条件,不能只通过前者就保证样本被接纳。init_state() 会清空检查点,手动 set_filter_time() 本身不会清空;paramsd 停止活动时明确两者一起调用。消息按 logMonoTime 仅在当前循环批次内排序,跨批次乱序仍由滤波窗口处理。

批量观测边界

内部先断言 z.size == R.size == extra_args.size,再预测到本批时间,并按传入顺序逐条更新。同一批只使用一个时间 t 和一个观测 kind;每条 z 的维度必须匹配其方阵 R。

KalmanFilter.get_R() 虽能构造 N 份 R,但 predict_and_observe() 未相应构造 N 组 extra_args,底层默认仅一组空参数。原 Python 包装类探针确认 N=2 时确实转发两条样本、两份 R,且包装层没有第五个实参来指定 extra_args。结合 Cython 默认值与 C++ 断言,经该包装层传多条观测存在断言失败的条件性风险;未运行真实 Cython / C++,不把它写成已复现故障。经底层接口显式传 N 组参数是另一种调用方式。

同组探针还确认:init_state() 优先选 covs_diag,再选 covs,两者都缺失才取当前滤波协方差;get_R() 按观测维数和批次长度复制默认噪声矩阵,副本互不别名;未知 kind 且未显式提供 R 时在 Python 层抛 KeyError。空样本列表仍会把空数据与形状 (0,dim,dim) 的 R 转发给底层。当前 locationd.handle_log() 与 paramsd.handle_log() 可见调用均一次传一条观测。

车辆硬件与插件化含义

传感器插件契约至少须写明:同一单调时钟域的采样时间与接收时间、单位与坐标系、观测类型 / 维度、逐条协方差、逐条附加参数、序号 / 失效标记、迟到 / 丢帧策略。超窗样本返回 None 应成为可观测的丢弃事件,不应默默解释为测量值零。

驱动或服务层不得仅因重放后状态恢复到最新时间,就假定曾经返回给业务层的迟到估计也是最新值。若插件层需要批量送样,应先确定 extra_args 的显式构造及批内时间语义,并用离线回放验证,而不是直接扩容当前包装调用。

常见问题

0.8 秒是保证能回退的历史长度吗?

不是。回退窗口同时受时间阈值与 512 条检查点容量限制。locationd 显式使用 0.8 秒最大回退年龄,而 Cython 默认是 1.0 秒(CarKalman 未覆盖);业务层预检与底层「检查点可用」不是同一个条件。

迟到观测的返回值能当当前状态用吗?

不能。返回的 Estimate.xk/Pk 是新观测刚处理完的结果,返回前的重放可能继续推进对象内部状态。迟到样本的返回元组不能当作「当前最新状态」,应从 state() / covs() 读取重放后的状态。

为什么一次传多条观测有风险?

因为 get_R() 能构造 N 份 R,但 predict_and_observe() 没有相应构造 N 组 extra_args,底层默认仅一组空参数;C++ 内部又断言 z.size == R.size == extra_args.size。这是条件性断言失败风险,未运行真实 Cython / C++ 复现。

同批观测可以有不同的时间和类型吗?

不可以。同一批只使用一个时间 t 和一个观测 kind,每条 z 的维度必须匹配其方阵 R。需要不同时间或类型,应拆成不同的批。

延伸阅读

参考资料

  • 项目 02 —— openpilot-research/REDNOSE_EKF_REWIND_BATCH_CONTRACT_DEEP_DIVE.md(取用了回退与重放顺序、512 检查点容量、批量观测断言与插件边界分析)。

说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

更新记录

首次发布 2026-10-11 最近核对:2026-10-11

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注