Skip to content

Commit 5c4f6fb

Browse files
authored
Improve IMU preintegration stability (#115)
* Improve IMU preintegration stability * Organize IMU parameters
1 parent 5a59704 commit 5c4f6fb

31 files changed

Lines changed: 2103 additions & 139 deletions

CMakeLists.txt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,37 @@ if(BUILD_TESTING)
471471
COMMAND test_imu_preintegration_diagnostics_policy
472472
)
473473

474+
add_executable(test_imu_seed_consistency_policy
475+
test/test_imu_seed_consistency_policy.cpp)
476+
target_include_directories(test_imu_seed_consistency_policy PRIVATE
477+
include)
478+
add_test(
479+
NAME imu_seed_consistency_policy
480+
COMMAND test_imu_seed_consistency_policy
481+
)
482+
483+
add_executable(test_imu_queue_policy
484+
test/test_imu_queue_policy.cpp)
485+
target_include_directories(test_imu_queue_policy PRIVATE
486+
include ${EIGEN3_INCLUDE_DIRS})
487+
target_link_libraries(test_imu_queue_policy
488+
Eigen3::Eigen)
489+
add_test(
490+
NAME imu_queue_policy
491+
COMMAND test_imu_queue_policy
492+
)
493+
494+
add_executable(test_imu_initialization_policy
495+
test/test_imu_initialization_policy.cpp)
496+
target_include_directories(test_imu_initialization_policy PRIVATE
497+
include ${EIGEN3_INCLUDE_DIRS})
498+
target_link_libraries(test_imu_initialization_policy
499+
Eigen3::Eigen)
500+
add_test(
501+
NAME imu_initialization_policy
502+
COMMAND test_imu_initialization_policy
503+
)
504+
474505
add_executable(test_deskew_readiness_policy
475506
test/test_deskew_readiness_policy.cpp)
476507
target_include_directories(test_deskew_readiness_policy PRIVATE

docs/imu_estimation.md

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,43 +70,75 @@ It is registered in `CMakeLists.txt` and runs under `colcon test` as
7070
A LiDAR localizer must not let a bad IMU integration drag the pose. The guard
7171
policy (`imu_preintegration_guard_policy.hpp`) enforces that:
7272

73-
- IMU preintegration is used as the registration seed/prediction path; the
74-
accepted LiDAR registration measurement remains the published pose anchor;
73+
- the default-on consistency gate first compares IMU predictions with accepted
74+
LiDAR poses without applying them to registration; only the configured number
75+
of consecutive translation-and-rotation passes permits the next IMU seed;
76+
- after permission, IMU preintegration can be used as the registration
77+
seed/prediction path while the accepted LiDAR measurement remains the
78+
published pose anchor;
7579
- if the IMU prediction and the NDT correction disagree by more than
7680
`imu_prediction_correction_guard_translation_m` (2.0 m) or
7781
`imu_prediction_correction_guard_yaw_deg` (10.0°), or the smoother update is
7882
non-finite / implausibly large, the node publishes the LiDAR measurement pose
7983
and resets the IMU smoother to that measurement before continuing;
8084
- fallback mode is reserved for an already-disabled IMU preintegration state;
8185
- the guard is itself unit-tested (`test_imu_preintegration_guard_policy.cpp`).
86+
- finite threshold violations or non-finite predictions immediately revoke
87+
seed permission; a temporarily unavailable prediction cannot be used for
88+
that scan but does not erase an already established gate
89+
(`test_imu_seed_consistency_policy.cpp`).
8290

8391
This is the same design principle as the rest of the stack (see
8492
[pose_covariance.md](pose_covariance.md), the G3 reinitialization guard): a
8593
component that feeds itself must not be able to diverge unbounded.
8694

8795
## Parameters
8896

97+
### Backend and input units
98+
8999
| Parameter | Default | Meaning |
90100
| --- | --- | --- |
91101
| `use_imu` | `false` | Enable the legacy IMU correction path |
92102
| `use_imu_preintegration` | `true` | Use the guarded preintegration smoother from `/imu`, independent of `use_imu` |
93103
| `imu_preintegration_use_base_frame_transform` | `false` | Transform IMU samples into the base frame before integrating |
104+
| `imu_accel_scale` | `1.0` | Multiply incoming acceleration before integration; use `9.80665` only for drivers/bags that publish acceleration in `g` rather than m/s² |
105+
| `imu_dual_queue_enabled` | `true` | LIO-SAM-style optimization/prediction queues with correction-time repropagation and velocity/bias retention; set `false` only for the legacy single-stream A/B baseline |
106+
107+
### Seed safety and correction guards
108+
109+
| Parameter | Default | Meaning |
110+
| --- | --- | --- |
111+
| `imu_seed_consistency_gate_enabled` | `true` | Keep IMU prediction diagnostic-only until consecutive LiDAR comparisons pass |
112+
| `imu_seed_consistency_max_translation_error_m` | `0.5` | Maximum open-loop IMU-vs-LiDAR translation error for one pass |
113+
| `imu_seed_consistency_max_rotation_error_deg` | `5.0` | Maximum open-loop IMU-vs-LiDAR rotation error for one pass |
114+
| `imu_seed_consistency_required_consecutive_passes` | `5` | Consecutive passes required before an IMU prediction may seed registration |
115+
| `imu_prediction_correction_guard_translation_m` | `2.0` | Reset the IMU smoother if IMU-vs-NDT translation disagrees beyond this |
116+
| `imu_prediction_correction_guard_yaw_deg` | `10.0` | Reset the IMU smoother if IMU-vs-NDT yaw disagrees beyond this |
117+
| `imu_prediction_correction_guard_warmup_accepts` | `5` | Accepted NDT corrections required before enforcing the correction guard |
118+
119+
### Continuous-time deskew and diagnostics (experimental)
120+
121+
| Parameter | Default | Meaning |
122+
| --- | --- | --- |
94123
| `use_continuous_time_deskew` | `false` | Experimental default-off hook that deskews the pre-voxel scan using per-point timing and IMU-predicted relative motion |
95124
| `continuous_time_deskew_mode` | `relative_motion` | Experimental motion source: `relative_motion`, `imu_pose_history`, or `lidar_constant_velocity` |
96125
| `continuous_time_cloud_stamp_reference` | `start` | Whether the cloud header is the scan `start` or `end`; Koide Livox uses `start` |
97126
| `continuous_time_deskew_reference_time_sec` | `0.0` | Scan-relative reference time for the deskewed cloud; `0.0` means earliest point |
98127
| `continuous_time_pose_history_duration_sec` | `2.0` | Bounded rotation-only IMU pose-history horizon used by `imu_pose_history` |
99128
| `enable_localizability_guard` | `false` | Experimental XY scan-covariance proxy; suppress the previous-delta seed only below its threshold |
100129
| `localizability_min_xy_eigen_ratio` | `0.05` | Minimum XY covariance eigenvalue ratio for the proxy guard |
130+
131+
### Preintegration noise and factor model
132+
133+
| Parameter | Default | Meaning |
134+
| --- | --- | --- |
101135
| `imu_gyro_noise_density` | `0.01` | rad/s/√Hz |
102136
| `imu_accel_noise_density` | `0.1` | m/s²/√Hz |
103137
| `imu_gyro_random_walk` | `0.0001` | rad/s²/√Hz |
104138
| `imu_accel_random_walk` | `0.001` | m/s³/√Hz |
105139
| `imu_bias_prior_sigma_gyro` | `0.01` | gyro bias prior σ |
106140
| `imu_bias_prior_sigma_accel` | `0.1` | accel bias prior σ |
107141
| `imu_ndt_sigma_z` / `_roll` / `_pitch` | `0.1` / `0.05` / `0.05` | NDT prior σ on the axes IMU constrains most |
108-
| `imu_prediction_correction_guard_translation_m` | `2.0` | reset IMU smoother if IMU-vs-NDT disagree beyond this |
109-
| `imu_prediction_correction_guard_yaw_deg` | `10.0` | reset IMU smoother if IMU-vs-NDT yaw disagree beyond this |
110142

111143
Set the noise densities from your IMU datasheet (or an Allan-variance run) for
112144
best results; the defaults are MEMS-grade ballpark values.

docs/research_driven_development_plan.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,19 @@ alignment p95 `<=1.2`とした。全candidateがFAILしたため、既定値はd
194194
なお全modeでcrash、NaN、再初期化要求は0だった。共有機のCPU使用率が高いため、latency値は
195195
候補間gateにのみ使い、一般性能claimにはしない。
196196

197+
### R1 IMU/scan時刻同期buffer result (2026-07-14): negative
198+
199+
NDT計算中に未来時刻のIMUがsmootherへ入る可能性を切り分けるため、変換済みIMUをbufferし、
200+
対象cloud時刻までだけ積分するprototypeを同じ85--112 s区間で実行した。0.65 sのIMU gap後に
201+
回復する処理を含めても、deskew適用率は`3.8%`、trajectory coverageは`6.3%`、translation
202+
RMSEは`6.599 m`だった。最初の不正確なIMU seed後にregistrationが連続rejectされ、accepted
203+
poseからの正しい積分窓が1 sを超えたためである。時刻同期だけでは、初期速度0やbiasを含む
204+
open-loop seed誤差を解決できない。prototypeはrevertした。測定値と判断は
205+
[`../experiments/imu_scan_time_sync/README.md`](../experiments/imu_scan_time_sync/README.md)に残す。
206+
207+
次のIMU runtime候補は、初期速度を観測または推定し、NDTへ接続する前のopen-loop seed accuracy
208+
gateを通すことを前提とする。deskew適用率だけを上げる変更は引き続き採用しない。
209+
197210
## Phase R2: Localizability診断と退化方向の抑制
198211

199212
仮説: scalar fitnessだけではcorridor aliasや旋回時の弱拘束方向を判別できない。
@@ -325,6 +338,36 @@ Boreasでは閾値sweepを再開する前にdata contractを検証する。
325338
目安は1人で既存machineを使うengineering effortであり、dataset downloadやidle-machine待ちは
326339
含まない。各gateで仮説が反証された場合、次phaseへ機械的に進まず本書へnegative resultを追記する。
327340

341+
## IMU seed consistency gate result (2026-07-14)
342+
343+
直接のsmoother state保持やscan時刻queueの試作は、最初の不正確な予測がNDTのfeedback loopへ
344+
入った後にKoideで大きく悪化した。代わりにopen-loop比較を先行させる。IMU予測と受理済み
345+
LiDAR poseを比較し、0.5 mかつ5度以内を5回連続で満たすまでseedを禁止する。欠測、非有限値、
346+
閾値超過または非有限予測ではfail closedする。確立前の欠測は連続列を切るが、確立後の一時的な
347+
予測欠測は不一致の証拠ではないためpermissionを保持し、そのscanでは予測自体を使用しない。
348+
349+
Koide `outdoor_hard_01a`先頭30秒では、並進誤差中央値0.995 m、回転誤差中央値5.25度、
350+
最長合格列4/5だった。このため44 alignment rowでIMU seedは0回だった。これは安全gateの
351+
期待どおりの結果であり、IMU精度改善を意味しない。詳細とmachine-readable値は
352+
`experiments/imu_seed_consistency/`に保存した。
353+
354+
その後、noise densityの離散化で`density^2 / dt`が欠落していたことと、Koide Livox
355+
accelerationがm/s²ではなく`g`単位であることを特定した。単位修正、明示scale=9.80665、
356+
初期速度、状態保持、scan-bounded dual queueを組み合わせた先頭30秒の候補は、並進予測
357+
中央値0.064 m、回転0.763度、seed 38/47、reject streak 0、coverage 98.0%、並進RMSE
358+
0.079 mとなった。同じscaleのsingle-stream対照はcoverage 76.0%、並進RMSE 0.242 m、
359+
reject streak 8だった。詳細は`experiments/imu_dual_queue/`に保存した。
360+
361+
その後、1秒window境界に10 msのtimestamp/scheduler jitter許容を設け(1.1秒の実gapは拒否)、
362+
IMU coverageが無い受理scanではIMU factorを捏造せずpose-only anchorとしてvelocity/biasを保持した。
363+
`outdoor_hard_01a`先頭30秒、同bagのoffset 85秒、`outdoor_hard_02a`先頭30秒を各3 repeatした結果、
364+
dual queueは全9 runでreject streak 0、coverage worst 96.7%以上となった。このmulti-window/
365+
multi-sequence gateを通過したため`imu_dual_queue_enabled=true`へ昇格した。
366+
367+
dual queueとrelative-motion deskewを組み合わせた追加gateでは、`outdoor_hard_01a`は3/3通過したが、
368+
`outdoor_hard_02a`のtranslation RMSE中央値がno-deskew 0.095 mから0.146 mへ悪化した。
369+
したがってdual queue本体のみ採用し、continuous-time deskewはdefault-offを維持する。
370+
328371
## 直近の着手項目
329372

330373
最初の実装単位はR0である。
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Scan-bounded dual IMU queue experiment
2+
3+
Date: 2026-07-14
4+
5+
This candidate follows the LIO-SAM separation between an optimization IMU
6+
queue and a prediction/repropagation queue. It also preserves optimized
7+
velocity and bias across accepted LiDAR updates, integrates only causal samples
8+
through the scan timestamp, and keeps the consistency gate in front of the NDT
9+
seed.
10+
11+
## Bugs found before the passing probe
12+
13+
1. `gyro_noise_density` and `accel_noise_density` are continuous densities in
14+
`/sqrt(Hz)`, but the discrete measurement variance omitted division by
15+
`dt`. At 200 Hz this made the IMU factor roughly 200 times overconfident.
16+
2. Koide `outdoor_hard_01a` publishes Livox acceleration in `g`, not m/s².
17+
Offline LI-Init-style analysis measured the required scale as `9.80665`.
18+
3. Resetting velocity to zero at every correction produced metre-scale
19+
open-loop errors. The candidate transfers optimized velocity/bias and uses a
20+
bounded LiDAR finite-difference velocity only when a reset is unavoidable.
21+
22+
All three issues have analytical or behavioral regression tests. The
23+
dataset-specific acceleration scale remains an explicit parameter; it is not
24+
auto-guessed at runtime.
25+
26+
## Same-window result
27+
28+
Dataset: Koide `outdoor_hard_01a`, first 30 seconds. Both IMU modes used
29+
`imu_accel_scale=9.80665`; only the candidate enabled the dual queue.
30+
31+
| Metric | Current single stream | Dual queue candidate |
32+
| --- | ---: | ---: |
33+
| Finite open-loop comparisons | 40 | 43 |
34+
| Translation prediction median | 0.988 m | 0.064 m |
35+
| Rotation prediction median | 4.633 deg | 0.763 deg |
36+
| IMU seed rows | 2/46 | 38/47 |
37+
| Reject streak max | 8 | 0 |
38+
| Pose coverage | 22.80/30 s (76.0%) | 29.40/30 s (98.0%) |
39+
| Translation RMSE | 0.242 m | 0.079 m |
40+
| Rotation RMSE | 2.486 deg | 1.084 deg |
41+
| Translation end error | 1.391 m | 0.057 m |
42+
| Rotation end error | 14.314 deg | 0.354 deg |
43+
| Alignment latency median | 0.297 s | 0.272 s |
44+
| Alignment latency p95 | 0.412 s | 0.391 s |
45+
46+
Candidate artifacts are on the external SSD under
47+
`generated/imu_dual_queue_covariance_probe_20260714`; the single-stream control
48+
is under `generated/imu_single_queue_covariance_probe_20260714`.
49+
50+
## Decision
51+
52+
The initial probe was followed by three repeats on each of three fixtures.
53+
54+
| Fixture | Candidate median (worst) coverage | Translation RMSE | Rotation RMSE | End translation | End rotation | Reject max | Seed ratio | Latency p95 |
55+
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
56+
| `outdoor_hard_01a`, 0–30 s | 98.0% (98.0%) | 0.085 (0.085) m | 1.134 (1.185) deg | 0.054 (0.057) m | 0.330 (0.350) deg | 0 | 85.4% (84.8%) | 0.323 (0.336) s |
57+
| `outdoor_hard_01a`, 85–112 s | 96.7% (96.7%) | 0.073 (0.081) m | 0.895 (1.061) deg | 0.031 (0.034) m | 0.517 (0.535) deg | 0 | 69.0% (67.5%) | 0.442 (0.457) s |
58+
| `outdoor_hard_02a`, 0–30 s | 98.7% (98.3%) | 0.095 (0.096) m | 1.051 (1.141) deg | 0.117 (0.125) m | 1.101 (1.122) deg | 0 | 86.0% (74.5%) | 0.323 (0.375) s |
59+
60+
The remaining nondeterministic failure was traced to a `1.000281 s` scan
61+
interval being rejected by an exact `1.000000 s` guard. A 10 ms timestamp/
62+
scheduler tolerance admits this boundary case while still rejecting genuine
63+
`1.1 s` gaps. Accepted scans without causal IMU coverage now advance through a
64+
pose-only anchor rather than erasing optimized velocity and bias.
65+
66+
The dual queue passed all nine repeats and is promoted to
67+
`imu_dual_queue_enabled=true`. The comparison runner still provides
68+
`imu_preintegration` as the explicit legacy single-stream control.
69+
70+
Continuous-time deskew remains default-off. With the promoted dual queue it
71+
applied to at least 87.5% of `outdoor_hard_01a` rows and passed three repeats,
72+
but on `outdoor_hard_02a` its median translation RMSE regressed from `0.095 m`
73+
to `0.146 m` across three repeats. The `imu_dual_queue_deskew` runner mode is
74+
retained only for reproducible future experiments.
75+
76+
Repeated artifacts are on the external SSD under `runs/`:
77+
`imu_jitter_tolerance_ab_20260714`, `imu_jitter_corner_ab_20260714`,
78+
`imu_jitter_outdoor_hard_02a_ab_20260714`,
79+
`imu_dual_queue_deskew_ab_20260714`, and
80+
`imu_dual_queue_deskew_outdoor_hard_02a_20260714`.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
{
2+
"date": "2026-07-14",
3+
"imu_accel_scale": 9.80665,
4+
"repeat_count_per_fixture": 3,
5+
"dual_queue_no_deskew": {
6+
"outdoor_hard_01a_0_30": {
7+
"coverage_ratio_median": 0.979979,
8+
"coverage_ratio_worst": 0.979979,
9+
"translation_rmse_m_median": 0.084802,
10+
"translation_rmse_m_worst": 0.085000,
11+
"rotation_rmse_deg_median": 1.134165,
12+
"rotation_rmse_deg_worst": 1.185000,
13+
"translation_end_error_m_median": 0.054,
14+
"translation_end_error_m_worst": 0.057,
15+
"rotation_end_error_deg_median": 0.330,
16+
"rotation_end_error_deg_worst": 0.350,
17+
"reject_streak_max_worst": 0,
18+
"imu_seed_ratio_median": 0.85417,
19+
"imu_seed_ratio_worst": 0.84783,
20+
"alignment_latency_p95_sec_median": 0.323,
21+
"alignment_latency_p95_sec_worst": 0.336
22+
},
23+
"outdoor_hard_01a_85_112": {
24+
"coverage_ratio_median": 0.966688,
25+
"coverage_ratio_worst": 0.966688,
26+
"translation_rmse_m_median": 0.073120,
27+
"translation_rmse_m_worst": 0.081406,
28+
"rotation_rmse_deg_median": 0.895,
29+
"rotation_rmse_deg_worst": 1.060690,
30+
"translation_end_error_m_median": 0.031,
31+
"translation_end_error_m_worst": 0.034,
32+
"rotation_end_error_deg_median": 0.517,
33+
"rotation_end_error_deg_worst": 0.535,
34+
"reject_streak_max_worst": 0,
35+
"imu_seed_ratio_median": 0.69048,
36+
"imu_seed_ratio_worst": 0.675,
37+
"alignment_latency_p95_sec_median": 0.442,
38+
"alignment_latency_p95_sec_worst": 0.457
39+
},
40+
"outdoor_hard_02a_0_30": {
41+
"coverage_ratio_median": 0.986658,
42+
"coverage_ratio_worst": 0.983313,
43+
"translation_rmse_m_median": 0.094636,
44+
"translation_rmse_m_worst": 0.095766,
45+
"rotation_rmse_deg_median": 1.050963,
46+
"rotation_rmse_deg_worst": 1.140939,
47+
"translation_end_error_m_median": 0.117,
48+
"translation_end_error_m_worst": 0.125,
49+
"rotation_end_error_deg_median": 1.101,
50+
"rotation_end_error_deg_worst": 1.122,
51+
"reject_streak_max_worst": 0,
52+
"imu_seed_ratio_median": 0.86,
53+
"imu_seed_ratio_worst": 0.74510,
54+
"alignment_latency_p95_sec_median": 0.323,
55+
"alignment_latency_p95_sec_worst": 0.375
56+
}
57+
},
58+
"controls": {
59+
"outdoor_hard_01a_0_30_lidar_only": {
60+
"coverage_ratio_median": 0.75997,
61+
"translation_rmse_m_median": 0.336198,
62+
"rotation_rmse_deg_median": 3.340312,
63+
"reject_streak_max_median": 8
64+
},
65+
"outdoor_hard_01a_0_30_legacy_single_stream": {
66+
"coverage_ratio_median": 0.763311,
67+
"translation_rmse_m_median": 0.365202,
68+
"rotation_rmse_deg_median": 3.925431,
69+
"reject_streak_max_median": 7
70+
},
71+
"outdoor_hard_02a_0_30_lidar_only_valid_run": {
72+
"coverage_ratio": 0.243340,
73+
"translation_rmse_m": 0.338117,
74+
"rotation_rmse_deg": 3.813385,
75+
"reject_streak_max": 31
76+
},
77+
"outdoor_hard_02a_0_30_legacy_single_stream": {
78+
"coverage_ratio": 0.823322,
79+
"translation_rmse_m": 0.652040,
80+
"rotation_rmse_deg": 4.264858,
81+
"reject_streak_max": 6
82+
}
83+
},
84+
"dual_queue_deskew": {
85+
"outdoor_hard_01a_0_30": {
86+
"coverage_ratio_median": 0.979979,
87+
"translation_rmse_m_median": 0.080,
88+
"rotation_rmse_deg_median": 0.974,
89+
"reject_streak_max_worst": 0,
90+
"imu_seed_ratio_worst": 0.80851,
91+
"deskew_applied_ratio_median": 0.87755,
92+
"deskew_applied_ratio_worst": 0.875,
93+
"alignment_latency_p95_sec_worst": 0.363
94+
},
95+
"outdoor_hard_02a_0_30": {
96+
"coverage_ratio_median": 0.983313,
97+
"translation_rmse_m_median": 0.145606,
98+
"rotation_rmse_deg_median": 1.168211,
99+
"reject_streak_max_worst": 0,
100+
"imu_seed_ratio_worst": 0.57447,
101+
"deskew_applied_ratio_median": 0.81633,
102+
"deskew_applied_ratio_worst": 0.76596,
103+
"alignment_latency_p95_sec_worst": 0.478
104+
}
105+
},
106+
"decisions": {
107+
"imu_dual_queue": "promote_default_on_after_nine_of_nine_multi_window_multi_sequence_runs_pass",
108+
"continuous_time_deskew": "keep_default_off_due_to_repeatable_outdoor_hard_02a_accuracy_regression"
109+
},
110+
"artifact_roots": [
111+
"runs/imu_jitter_tolerance_ab_20260714",
112+
"runs/imu_jitter_corner_ab_20260714",
113+
"runs/imu_jitter_outdoor_hard_02a_ab_20260714",
114+
"runs/imu_dual_queue_deskew_ab_20260714",
115+
"runs/imu_dual_queue_deskew_outdoor_hard_02a_20260714"
116+
]
117+
}

0 commit comments

Comments
 (0)