You already know the the navigation estimator code deeply:
- predict() grows covariance from a fixed noise model (delta_trans, delta_theta, delta_t)
- update() shrinks covariance using line-sensor line measurements with Mahalanobis gating
- Slip detection sets covariance to INF via velocity window comparison
- isFinite() check in executePoseGoal() triggers ESTIMATED_STATE_NOT_FINITE
What you’re missing: The mathematical theory that lets you look at a covariance trace in a bag and immediately say “this blow-up is a slip, not a delocalization” or “this line-sensor update was rejected because innovation exceeded 2-sigma.” This track fills that gap.
┌────────────────────────┐
│ 05-failure-modes.md │
│ (AMR diagnosis) │
└────────────┬───────────┘
│ requires all below
┌────────────────────────────┼────────────────────────────┐
│ │ │
┌──────────▼──────────┐ ┌────────────▼────────────┐ ┌────────────▼──────────┐
│ 03-measurement- │ │ 04-imu-fusion.md │ │ 02-kalman-filter.md │
│ models.md │ │ (gyro, bias, line-sensor │ │ (EKF predict/update) │
│ (line constraints, │ │ theta update) │ │ │
│ Mahalanobis) │ └────────────┬────────────┘ └────────────┬──────────┘
└──────────┬──────────┘ │ │
│ └──────────┬──────────────────┘
└──────────────────────────────────────┤
▼
┌────────────────────────┐
│ 01-dead-reckoning.md │
│ (odometry, unicycle, │
│ arc integration) │
└────────────────────────┘
Reading order: 01 → 02 → 03 → 04 → 05 (strictly sequential — each builds on the previous)
| # | File | Est. Time | What It Gives You |
|---|---|---|---|
| 01 | 01-dead-reckoning.md |
3 hrs | Derive (x', y', θ') from encoder counts; understand AMR predict() noise terms |
| 02 | 02-kalman-filter.md |
4 hrs | KF → EKF equations; understand why covariance grows during motion and shrinks on measurement |
| 03 | 03-measurement-models.md |
3 hrs | Line sensor as a constraint; Mahalanobis distance; when AMR rejects a line-sensor update |
| 04 | 04-imu-fusion.md |
3 hrs | Gyro integration; bias estimation; how AMR theta update differs from XY update |
| 05 | 05-failure-modes.md |
3 hrs | Recognise slip vs delocalize vs collision from covariance patterns in bags |
Total: ~16 hours
After each file, you must answer these without opening any notes:
01-dead-reckoning.mdpredict(), which noise term (k_trans_noise vs k_rot_pos_noise) dominates during a tight turn?02-kalman-filter.mdF, Q, P?P?predict() does NOT use the odom message’s covariance fields. Why is this OK?03-measurement-models.mdclamp(state, meas.min, meas.max) instead of a standard Kalman gain. What does this mean geometrically?04-imu-fusion.md05-failure-modes.mdERROR_STATE_INVALID?scripts/analysis/ to plot covariance over time and identify the divergence point?Read: 01-dead-reckoning.md
Do: exercises/01-odometry-math.md
Connect: Open `predict()` and label each noise term against the math you learned.
Read: 02-kalman-filter.md — KF derivation, then EKF linearisation
Do: exercises/02-kalman-1d.md — build a 1D KF by hand (position + velocity)
Connect: The AMR prediction step is a simplified EKF (no Jacobian for covariance — uses fixed
noise model instead). Understand why this is an approximation and when it breaks.
Read: 03-measurement-models.md
Do: exercises/03-ekf-unicycle.md — implement EKF for a 2D unicycle in Python (30 lines)
Connect: Read `update()` . You can now read every line.
Read: 04-imu-fusion.md
Do: exercises/04-log-diagnosis.md — given a synthetic covariance trace, identify the failure
Connect: Look at imuCallback() callback. The gyro integration + bias correction will now make sense.
Read: 05-failure-modes.md
Do: exercises/05-robot-specific.md — use a real AMR bag (from attachments/) to find the
first moment covariance blows up, and name the cause.
Goal: At the end of this week, every ERROR_STATE_INVALID ticket you open should
have a 3-sentence hypothesis within 2 minutes of looking at the bag.
These code locations are your ground truth — the math in these notes should map 1:1:
| Concept | AMR Code Location |
|---|---|
| Odometry noise model | `predict()` lines 396+ |
| Covariance set to INF (slip) | `odometryCallback()` |
| Covariance set to INF (collision) | `imuCallback()` lines ~200–250 |
| Measurement update (line-sensor) | `update()` |
| Mahalanobis gate (2-sigma) | `update()` — innovation check |
| isFinite check | `executePoseGoal()` |
| Theta KF update | `` — Kalman gain for IMU theta |
| VelocityWindowTracker | `` |
electronics/05-spi-deep-dive.md ─→ line-sensor data arrives over SPI (v3.1+)
zephyr/study-notes/02-sensors.md ─→ how IMU data is read from ICM-42688
navigation-estimator/ (this track) ─→ what the estimator does with that data
ros2-handson/ (next track) ─→ how nav2 uses the estimated state for planning