# 测试说明

**用途：** 说明合成 pytest 与旧车 S2 线下试验的边界（避免误读）。日常跑通请先看根目录 [README](../README.md)。

| 试验 | 是否默认 pytest | 在证明什么 | 不在证明什么 |
|---|---|---|---|
| **合成数据** | 是 | 算法链路正确、能收回已知 yaw / δt | 实车安装精度、平移可交付 |
| **旧车 S2** | 否（线下手工） | 主机时间旧数据上流水线能跑完；质量门会拒绝坏结果 | 外参真值；新车可用性 |

---

## 1. 自动化测试（合成数据 / `pytest`）

入口：`tests/test_v1_pipeline.py`  
命令：

```powershell
cd <仓库根目录>
python -m pytest -q
```


| 测试                                                            | 输入           | 在测什么                | 期望结果                                       |
| ------------------------------------------------------------- | ------------ | ------------------- | ------------------------------------------ |
| `test_rotation_handeye_recovers_yaw`                          | 合成运动对（无点云）   | 旋转手眼能否收回已知 yaw      | 旋转误差 < 1°                                  |
| `test_time_offset_on_synthetic`                               | 合成会话（故意加 δt） | 模长相关粗估时间偏置          | |δt 误差| < 0.05 s                           |
| `test_signed_time_offset_refine_improves_or_keeps`            | 同上 + 真值 R    | 有符号三轴 δt 精修         | 不比粗估明显更差                                   |
| `test_preintegration_bias_jacobian_matches_finite_difference` | 随机陀螺序列       | 旋转预积分 `J_bg`        | 与有限差分一致（松阈值）                               |
| `test_imu_preintegration_recovers_constant_accel_translation` | 常值加速度        | 完整预积分 Δv/Δp         | 接近解析值                                      |
| `test_imu_preintegration_bias_jacobian_finite_difference`     | 随机 IMU       | `J_bg`/`J_ba` 一阶修正  | 与重积分接近                                     |
| `test_synthetic_pipeline_rejects_noisy_icp_but_keeps_time_audit` | synthetic end-to-end | strict rotation quality gate + time audit | noisy ICP is blocked; delta-t remains accurate |
| `test_synthetic_pipeline_full_se3_smoke` | synthetic end-to-end | full-SE(3) smoke test | returns an explicit accepted/rejected/blocked status |
| `test_planar_yaw_is_not_full_rotation_or_translation_observable` | pure-yaw motion pairs | degeneracy detection | full rotation/translation observability is rejected |
| `test_multi_axis_motion_is_rotation_and_translation_observable` | multi-axis motion pairs | positive observability case | rotation and translation pass |
| `test_translation_prior_is_reported_but_not_accepted_when_unobservable` | planar motion + CAD prior | prior semantics | prior is reported but not accepted as calibration |
| `test_handeye_rejects_a_small_fraction_of_gross_rotation_outliers` | motion pairs with a gross outlier | residual-distribution gate | solve is rejected |
| `test_motion_pairs_reject_low_fitness` | low-fitness registration | fitness gate | no motion pair is emitted |
| `test_motion_pairs_reject_imu_and_lidar_discontinuities` | timestamp gaps | continuity gates | cross-gap pairs are rejected |


合成数据由 `tools/generate_synthetic_session.py` 生成（墙面点云 + 已知外参 yaw 与 δt）。  
一键复现见根目录 README：`tools/reproduce_synthetic.py`。

---



## 2. 旧车 S2 线下试验（不在默认 pytest 里）



### 用了什么数据


| 项       | 内容                                                                         |
| ------- | -------------------------------------------------------------------------- |
| 车辆 / 批次 | 旧 **S2** 验证集（`S2_scheme1_validation`）数据在网盘的“室外车数据\IMU-雷达标定数据”              |
| 典型路径    | `D:\IMU_calibration\work\S2_scheme1_validation\`（历史目录名；含 `imu.csv` + 雷达会话） |
| IMU 时间  | **主机 UTC 接收时间**（串口块到达时刻），不是 IMU 设备时间                                       |
| 雷达时间    | dlog 导出的主机侧 `unix_time_ns`，不是 MSOP 设备时间                                    |
| 帧率特征    | 雷达约 **1 Hz** 量级，关键帧间隔偏长                                                    |
| 配置烟测    | `config/s2_old_smoke.yaml`（仅声明为旧数据烟测，不当交付）                                 |


这些数据**只能用来验证流水线能否跑通**，不能当作新车外参真值来源。  
`blocked` 是质量门的**预期结果**，不是「算法突然坏了」。

### 做了什么测试

对同一批 S2 中间格式多次跑 `cli run`，例如：

- 预积分加强后的输出目录（本机历史名如 `out_scheme2_preint`）
- 有符号 δt / 联合精修后的输出（本机历史名如 `out_scheme2_phaseA`）
- 用 `tools/compare_s2_runs.py` 对比两次 `summary.json`

命令形态（路径按本机实际修改；雷达会话目录若仍叫 `scheme2_session` 为历史命名）：

```powershell
python -m imu_lidar.cli run `
  --vehicle-config config\s2_old_smoke.yaml `
  --imu D:\IMU_calibration\work\S2_scheme1_validation\imu.csv `
  --lidar D:\IMU_calibration\work\S2_scheme1_validation\scheme2_session `
  --output path\to\out_s2 `
  --mode rotation_only `
  --time-offset-search-s 2.0

python tools\compare_s2_runs.py path\to\out_old\summary.json path\to\out_new\summary.json
```



### 得到什么结果（记录摘要）


| 指标       | 预积分加强一轮       | 有符号 δt / 精修一轮      |
| -------- | ------------- | ------------------ |
| `status` | `blocked`（预期） | `blocked`（预期）      |
| 手眼 RMS   | 约 **15.0°**   | 约 **14.5°**        |
| 手眼中位数    | —             | 约 **6.8°**         |
| δt       | 约 **−2.0 s**  | 约 **−1.75 s**（有修正） |
| 相关峰      | 很弱（约 0.18）    | 仍弱（约 0.13）         |
| 结论       | 链路可跑          | 残差略降，但 **不当交付外参**  |


原因归纳：

1. 时间戳是**主机时间**，相关峰弱，δt / yaw / 零偏互相耦合；
2. 雷达约 1 Hz，运动对间隔长，IMU 侧更易漂；
3. 质量门主动 `blocked`，避免把坏结果当成安装参数。

**正式标定**必须改用设备时间（IMU `device_timestamp`、雷达 MSOP 设备时）重新采集后再跑。

---



## 3. 配准结果怎么目视检查

标定跑完后（合成或实车）：

```powershell
python tools\visualize_pair_3d.py `
  --lidar examples\synthetic_session\lidar `
  --imu examples\synthetic_session\imu.csv `
  --summary examples\synthetic_session\out\summary.json `
  --pair-index 0 `
  --save-png examples\synthetic_session\out\pair0_overlay.png
```

交互窗口快捷键：`1`–`4` 切换叠点模式；`N`/`]` 下一运动对，`P`/`[` 上一运动对。  
无显示器时加 `--no-gui --save-png ...` 只出俯视图 PNG。