Chapter 5: Odometry and Dead Reckoning
Lesson 1: Wheel Odometry Computation
This lesson derives the complete wheel-encoder odometry pipeline for planar wheeled robots, with an emphasis on differential-drive bases. We formalize the measurement model from encoder ticks to wheel arc lengths, derive the exact discrete-time pose update under constant curvature, and prove key limiting cases used for numerical stability. Implementations are provided in Python, C++, Java, MATLAB, and Wolfram Mathematica.
1. Conceptual Overview
Wheel odometry (dead reckoning from wheel encoders) estimates the robot planar pose \( \mathbf{x}_k = [x_k, y_k, \theta_k]^T \) by integrating incremental wheel motion. The input is typically the encoder tick increments \( \Delta N_{L,k}, \Delta N_{R,k} \) over a sampling interval.
The odometry computation is a deterministic map: \( (\Delta N_{L,k}, \Delta N_{R,k}) \) \( → \) \( (\Delta \phi_{L,k}, \Delta \phi_{R,k}) \) \( → \) \( (\Delta s_{L,k}, \Delta s_{R,k}) \) \( → \) \( (\Delta x_k, \Delta y_k, \Delta \theta_k) \) \( → \) \( (x_{k+1}, y_{k+1}, \theta_{k+1}) \).
flowchart TD
A["Encoder tick increments (dN_L, dN_R)"] --> B["Wheel angle increments (dphi_L, dphi_R)"]
B --> C["Wheel arc lengths (ds_L, ds_R)"]
C --> D["Compute (delta_s, delta_theta)"]
D --> E["Integrate pose increment (dx, dy, dtheta)"]
E --> F["Update pose (x, y, theta)"]
F --> G["Publish/Log odometry"]
Throughout, we assume the planar body frame convention used in mobile robotics: \( x \) forward, \( y \) left, and yaw \( \theta \) measured counterclockwise from the global \( x \)-axis.
2. Encoder Quantization and the Tick-to-Angle Map
Let \( N_{\text{tpr}} \) be the encoder ticks per motor shaft revolution (after quadrature decoding if applicable), and let \( g \) be the gear ratio defined as motor revolutions per wheel revolution. Then the wheel angle increment is:
\[ \Delta \phi_{w,k} \;=\; 2\pi \,\frac{\Delta N_{k}}{N_{\text{tpr}}\,g}. \]
The corresponding wheel arc length increment (assuming pure rolling at the wheel-ground contact) is: \( \Delta s_{w,k} = r_w \Delta \phi_{w,k} \), i.e.
\[ \Delta s_{w,k} \;=\; r_w \, 2\pi \,\frac{\Delta N_k}{N_{\text{tpr}}\,g}. \]
Remark (quantization): encoder ticks impose a minimum resolvable arc length \( \delta s = r_w\,2\pi /(N_{\text{tpr}}g) \). Quantization effects become prominent at low speeds and short sampling intervals; we will revisit their impact on drift in later lessons.
3. Differential-Drive Odometry: From Wheel Arcs to Body Twist
For a differential-drive base with left/right wheel arc increments \( \Delta s_{L,k} \) and \( \Delta s_{R,k} \), and baseline (track width) \( b \), the standard incremental relations are:
\[ \Delta s_k \;=\; \frac{\Delta s_{R,k} + \Delta s_{L,k}}{2}, \qquad \Delta \theta_k \;=\; \frac{\Delta s_{R,k} - \Delta s_{L,k}}{b}. \]
These follow directly from differential-drive kinematics (introduced in Chapter 2) applied over a short interval: the forward motion is the average wheel travel, while the yaw increment is proportional to the wheel travel difference.
If a sampling time \( \Delta t_k \) is available, the corresponding body-frame linear and angular velocities are approximated by:
\[ v_k \;\approx\; \frac{\Delta s_k}{\Delta t_k}, \qquad \omega_k \;\approx\; \frac{\Delta \theta_k}{\Delta t_k}. \]
In this lesson we focus on the incremental pose update (odometry integration), which is well-defined even if \( \Delta t_k \) varies.
4. Exact Pose Update Under Constant Curvature
The planar unicycle kinematics (a standard reduction for differential drive) are:
\[ \dot{x}(t) = v(t)\cos\theta(t),\qquad \dot{y}(t) = v(t)\sin\theta(t),\qquad \dot{\theta}(t) = \omega(t). \]
Assume over the sample interval \( [t_k,t_{k+1}] \) that \( v(t)=v_k \) and \( \omega(t)=\omega_k \) are constant (piecewise-constant twist model). Then \( \theta(t) = \theta_k + \omega_k(t-t_k) \). Integrating \( \dot{x},\dot{y} \) yields the exact discrete update.
Proposition (exact integration): Let \( \Delta \theta_k = \omega_k \Delta t_k \) and \( \Delta s_k = v_k \Delta t_k \). If \( \Delta \theta_k \neq 0 \), then:
\[ x_{k+1} \;=\; x_k + \frac{\Delta s_k}{\Delta \theta_k} \Big(\sin(\theta_k+\Delta\theta_k)-\sin\theta_k\Big), \]
\[ y_{k+1} \;=\; y_k - \frac{\Delta s_k}{\Delta \theta_k} \Big(\cos(\theta_k+\Delta\theta_k)-\cos\theta_k\Big), \qquad \theta_{k+1} \;=\; \theta_k + \Delta\theta_k. \]
Proof: For \( \omega_k \neq 0 \), define \( \tau = t-t_k \), so \( \theta(t)=\theta_k+\omega_k\tau \). Then:
\[ x_{k+1}-x_k \;=\; \int_{0}^{\Delta t_k} v_k \cos(\theta_k+\omega_k\tau)\,d\tau \;=\; \frac{v_k}{\omega_k}\Big(\sin(\theta_k+\omega_k\Delta t_k)-\sin\theta_k\Big). \]
Using \( \Delta s_k = v_k\Delta t_k \) and \( \Delta\theta_k=\omega_k\Delta t_k \) gives the stated form. Similarly:
\[ y_{k+1}-y_k \;=\; \int_{0}^{\Delta t_k} v_k \sin(\theta_k+\omega_k\tau)\,d\tau \;=\; -\frac{v_k}{\omega_k}\Big(\cos(\theta_k+\omega_k\Delta t_k)-\cos\theta_k\Big). \]
Finally, \( \theta_{k+1}-\theta_k = \int_0^{\Delta t_k}\omega_k d\tau = \omega_k\Delta t_k = \Delta\theta_k \). This completes the proof.
Substituting the differential-drive relations from Section 3, \( \Delta s_k = (\Delta s_{R,k}+\Delta s_{L,k})/2 \) and \( \Delta\theta_k = (\Delta s_{R,k}-\Delta s_{L,k})/b \), produces the exact wheel-encoder odometry update for a differential-drive robot.
5. Straight-Line Limit and a Practical Branch Condition
Implementations must be numerically stable when \( |\Delta\theta_k| \) is small. In practice we branch on a small threshold \( \varepsilon \).
flowchart TD
S["Compute delta_theta"] --> Q{"abs(delta_theta) < eps ?"}
Q -->|yes| L["Use straight-line update: \ndx=delta_s*cos(theta), dy=delta_s*sin(theta)"]
Q -->|no| A["Use arc update with \nR=delta_s/delta_theta"]
L --> U["theta ← theta + delta_theta"]
A --> U
U --> N["Normalize angle to (-pi, pi]"]
Proposition (continuous limit): The exact arc update converges to the straight-line update as \( \Delta\theta_k → 0 \). Specifically,
\[ \lim_{\Delta\theta_k → 0} \frac{\Delta s_k}{\Delta \theta_k}\Big(\sin(\theta_k+\Delta\theta_k)-\sin\theta_k\Big) \;=\; \Delta s_k \cos\theta_k, \]
\[ \lim_{\Delta\theta_k → 0} -\frac{\Delta s_k}{\Delta \theta_k}\Big(\cos(\theta_k+\Delta\theta_k)-\cos\theta_k\Big) \;=\; \Delta s_k \sin\theta_k. \]
Proof: Using first-order Taylor expansions \( \sin(\theta+\delta)=\sin\theta+\delta\cos\theta+o(\delta) \) and \( \cos(\theta+\delta)=\cos\theta-\delta\sin\theta+o(\delta) \), we obtain:
\[ \sin(\theta_k+\Delta\theta_k)-\sin\theta_k \;=\; \Delta\theta_k\cos\theta_k + o(\Delta\theta_k), \]
\[ \cos(\theta_k+\Delta\theta_k)-\cos\theta_k \;=\; -\Delta\theta_k\sin\theta_k + o(\Delta\theta_k). \]
Dividing by \( \Delta\theta_k \) and taking the limit proves the result. This justifies the practical branch condition \( |\Delta\theta_k| < \varepsilon \).
6. Implementation Details and Conventions
Angle normalization: Odometry should keep \( \theta \) bounded (e.g., to \( (-\pi,\pi] \)) for numerical robustness and for downstream controllers that assume a wrapped yaw. A standard approach is: \( \theta \leftarrow \operatorname{atan2}(\sin\theta,\cos\theta) \).
Per-wheel calibration: For best performance, treat left and right wheel radii as distinct (\( r_L \neq r_R \)) and calibrate them (Chapter 2, Lesson 5 introduced wheel-parameter calibration). Your encoder map becomes:
\[ \Delta s_{L,k} = r_L\,2\pi \frac{\Delta N_{L,k}}{N_{\text{tpr}}g}, \qquad \Delta s_{R,k} = r_R\,2\pi \frac{\Delta N_{R,k}}{N_{\text{tpr}}g}. \]
Sign conventions: Ensure encoder sign matches the wheel forward direction used in the kinematic model. If your left encoder increases when the wheel moves backward, incorporate a sign flip in software.
Sampling: Odometry can be computed at each encoder interrupt (event-based) or at a fixed rate (time-based). The formulas above operate on increments; either approach is valid if increments are computed correctly.
7. Python Implementation (Core Math + Integration Hooks)
Python is commonly used for prototyping and for robotics middleware
scripting (e.g., ROS 2 with rclpy). The core odometry logic
should remain independent of middleware; the integrator simply consumes
tick increments and outputs pose.
File: Chapter5_Lesson1.py
"""
Chapter5_Lesson1.py
Wheel Odometry Computation — Differential Drive (planar SE(2))
This module implements a numerically stable wheel-encoder odometry integrator:
- encoder tick increments -> wheel angle increments
- wheel angle increments -> wheel arc lengths
- wheel arc lengths -> robot pose increment (x, y, theta)
- exact integration for constant curvature over each sample
"""
from __future__ import annotations
from dataclasses import dataclass
from math import pi, sin, cos, atan2
def normalize_angle(theta: float) -> float:
# Wrap angle to (-pi, pi]
return atan2(sin(theta), cos(theta))
@dataclass
class Pose2D:
x: float
y: float
theta: float # yaw [rad]
@dataclass
class DiffDriveParams:
r_l: float
r_r: float
b: float
ticks_per_rev: int
gear_ratio: float = 1.0
class DifferentialDriveOdometry:
def __init__(self, params: DiffDriveParams, pose0: Pose2D | None = None) -> None:
self.params = params
self.pose = pose0 if pose0 is not None else Pose2D(0.0, 0.0, 0.0)
def ticks_to_wheel_angle(self, dN: int) -> float:
denom = float(self.params.ticks_per_rev) * float(self.params.gear_ratio)
return 2.0 * pi * (float(dN) / denom)
def update_from_ticks(self, dN_L: int, dN_R: int) -> Pose2D:
dphi_L = self.ticks_to_wheel_angle(dN_L)
dphi_R = self.ticks_to_wheel_angle(dN_R)
ds_L = self.params.r_l * dphi_L
ds_R = self.params.r_r * dphi_R
delta_s = 0.5 * (ds_R + ds_L)
delta_theta = (ds_R - ds_L) / self.params.b
x, y, th = self.pose.x, self.pose.y, self.pose.theta
eps = 1e-12
if abs(delta_theta) < eps:
x += delta_s * cos(th)
y += delta_s * sin(th)
th = normalize_angle(th + delta_theta)
else:
R = delta_s / delta_theta
x += R * (sin(th + delta_theta) - sin(th))
y += -R * (cos(th + delta_theta) - cos(th))
th = normalize_angle(th + delta_theta)
self.pose = Pose2D(x, y, th)
return self.pose
File: Chapter5_Lesson1_Ex1.py
"""
Chapter5_Lesson1_Ex1.py
Exercise: numerical wheel odometry computation from a short tick sequence.
"""
from Chapter5_Lesson1 import DiffDriveParams, DifferentialDriveOdometry, Pose2D
params = DiffDriveParams(r_l=0.06, r_r=0.06, b=0.34, ticks_per_rev=4096, gear_ratio=1.0)
odo = DifferentialDriveOdometry(params, pose0=Pose2D(0.0, 0.0, 0.0))
seq = [(120, 120), (120, 150), (120, 150), (120, 150), (120, 120)]
for k, (dL, dR) in enumerate(seq, start=1):
pose = odo.update_from_ticks(dL, dR)
print(f"k={k} x={pose.x:.4f} y={pose.y:.4f} theta={pose.theta:.4f} rad")
Robotics libraries (integration layer): In ROS 2
(Python), odometry is usually published as
nav_msgs/Odometry with pose in tf2 frames
(e.g., odom → base_link). The class above
provides the pose update; the ROS node simply maps encoder callbacks to
update_from_ticks.
8. C++ Implementation (Real-Time Friendly Core)
C++ is common in embedded and real-time robot stacks. The following
implementation is allocation-free in the update path and uses only
elementary operations. In ROS 2, this core is typically called from an
rclcpp node that publishes
nav_msgs/msg/Odometry.
File: Chapter5_Lesson1.cpp
// Chapter5_Lesson1.cpp
#include <cmath>
#include <cstdint>
#include <iostream>
struct Pose2D {
double x{0.0};
double y{0.0};
double theta{0.0};
};
struct DiffDriveParams {
double r_l{0.05};
double r_r{0.05};
double b{0.30};
int32_t ticks_per_rev{2048};
double gear_ratio{1.0};
};
static inline double normalizeAngle(double theta) {
return std::atan2(std::sin(theta), std::cos(theta));
}
class DifferentialDriveOdometry {
public:
explicit DifferentialDriveOdometry(const DiffDriveParams& p, const Pose2D& pose0 = Pose2D())
: params_(p), pose_(pose0) {}
double ticksToWheelAngle(int32_t dN) const {
const double denom = static_cast<double>(params_.ticks_per_rev) * params_.gear_ratio;
return 2.0 * M_PI * (static_cast<double>(dN) / denom);
}
Pose2D updateFromTicks(int32_t dN_L, int32_t dN_R) {
const double dphi_L = ticksToWheelAngle(dN_L);
const double dphi_R = ticksToWheelAngle(dN_R);
const double ds_L = params_.r_l * dphi_L;
const double ds_R = params_.r_r * dphi_R;
const double delta_s = 0.5 * (ds_R + ds_L);
const double delta_theta = (ds_R - ds_L) / params_.b;
const double eps = 1e-12;
if (std::abs(delta_theta) < eps) {
pose_.x += delta_s * std::cos(pose_.theta);
pose_.y += delta_s * std::sin(pose_.theta);
pose_.theta = normalizeAngle(pose_.theta + delta_theta);
} else {
const double R = delta_s / delta_theta;
const double th = pose_.theta;
pose_.x += R * (std::sin(th + delta_theta) - std::sin(th));
pose_.y += -R * (std::cos(th + delta_theta) - std::cos(th));
pose_.theta = normalizeAngle(th + delta_theta);
}
return pose_;
}
Pose2D pose() const { return pose_; }
private:
DiffDriveParams params_;
Pose2D pose_;
};
int main() {
DiffDriveParams p;
p.r_l = 0.05; p.r_r = 0.05; p.b = 0.30; p.ticks_per_rev = 2048; p.gear_ratio = 1.0;
DifferentialDriveOdometry odo(p);
for (int k = 0; k < 200; ++k) {
odo.updateFromTicks(40, 60);
}
const Pose2D out = odo.pose();
std::cout << "Final pose: x=" << out.x << " y=" << out.y << " theta=" << out.theta << " rad\n";
return 0;
}
Robotics libraries (typical stack): C++ odometry nodes
often use Eigen (linear algebra), tf2 (frame
transforms), and ROS 2 message types (nav_msgs,
geometry_msgs). The math above is the minimal, portable
core.
9. Java Implementation (Portable Core for JVM Robotics)
Java robotics appears in educational stacks and selected production
contexts. For ROS 2, Java bindings (e.g., rcljava) can
publish odometry, while the update math remains identical.
File: Chapter5_Lesson1.java
// Chapter5_Lesson1.java
public class Chapter5_Lesson1 {
public static class Pose2D {
public double x, y, theta;
public Pose2D(double x, double y, double theta) { this.x = x; this.y = y; this.theta = theta; }
@Override public String toString() {
return String.format("Pose2D(x=%.6f, y=%.6f, theta=%.6f rad)", x, y, theta);
}
}
public static class DiffDriveParams {
public double rL, rR, b;
public int ticksPerRev;
public double gearRatio;
public DiffDriveParams(double rL, double rR, double b, int ticksPerRev, double gearRatio) {
this.rL = rL; this.rR = rR; this.b = b; this.ticksPerRev = ticksPerRev; this.gearRatio = gearRatio;
}
}
public static double normalizeAngle(double theta) {
return Math.atan2(Math.sin(theta), Math.cos(theta));
}
public static class DifferentialDriveOdometry {
private final DiffDriveParams p;
private Pose2D pose;
public DifferentialDriveOdometry(DiffDriveParams p, Pose2D pose0) { this.p = p; this.pose = pose0; }
public double ticksToWheelAngle(int dN) {
double denom = ((double)p.ticksPerRev) * p.gearRatio;
return 2.0 * Math.PI * (((double)dN) / denom);
}
public Pose2D updateFromTicks(int dN_L, int dN_R) {
double dphiL = ticksToWheelAngle(dN_L);
double dphiR = ticksToWheelAngle(dN_R);
double dsL = p.rL * dphiL;
double dsR = p.rR * dphiR;
double deltaS = 0.5 * (dsR + dsL);
double deltaTheta = (dsR - dsL) / p.b;
double th = pose.theta;
double eps = 1e-12;
if (Math.abs(deltaTheta) < eps) {
pose.x += deltaS * Math.cos(th);
pose.y += deltaS * Math.sin(th);
pose.theta = normalizeAngle(th + deltaTheta);
} else {
double R = deltaS / deltaTheta;
pose.x += R * (Math.sin(th + deltaTheta) - Math.sin(th));
pose.y += -R * (Math.cos(th + deltaTheta) - Math.cos(th));
pose.theta = normalizeAngle(th + deltaTheta);
}
return pose;
}
public Pose2D pose() { return pose; }
}
public static void main(String[] args) {
DiffDriveParams p = new DiffDriveParams(0.05, 0.05, 0.30, 2048, 1.0);
DifferentialDriveOdometry odo = new DifferentialDriveOdometry(p, new Pose2D(0.0, 0.0, 0.0));
for (int k = 0; k < 200; k++) { odo.updateFromTicks(40, 60); }
System.out.println("Final pose: " + odo.pose());
}
}
10. MATLAB and Simulink Notes
MATLAB is widely used for control and robotics prototyping. The core update can be implemented with base MATLAB. In Simulink, the same computation maps naturally to blocks: (i) tick-to-angle gains, (ii) wheel arc computation, (iii) delta computation, (iv) branch for small yaw increment, (v) integrator/discrete update for pose.
File: Chapter5_Lesson1.m
% Chapter5_Lesson1.m
function Chapter5_Lesson1()
params.rL = 0.05;
params.rR = 0.05;
params.b = 0.30;
params.ticksPerRev = 2048;
params.gearRatio = 1.0;
pose = [0; 0; 0]; % [x; y; theta]
for k = 1:200
pose = odo_update_from_ticks(pose, 40, 60, params);
end
fprintf('Final pose: x=%.6f y=%.6f theta=%.6f rad\n', pose(1), pose(2), pose(3));
end
function theta = normalize_angle(theta)
theta = atan2(sin(theta), cos(theta));
end
function dphi = ticks_to_wheel_angle(dN, params)
denom = double(params.ticksPerRev) * double(params.gearRatio);
dphi = 2*pi * (double(dN) / denom);
end
function pose = odo_update_from_ticks(pose, dN_L, dN_R, params)
dphiL = ticks_to_wheel_angle(dN_L, params);
dphiR = ticks_to_wheel_angle(dN_R, params);
dsL = params.rL * dphiL;
dsR = params.rR * dphiR;
deltaS = 0.5*(dsR + dsL);
deltaTheta = (dsR - dsL)/params.b;
x = pose(1); y = pose(2); th = pose(3);
eps = 1e-12;
if abs(deltaTheta) < eps
x = x + deltaS*cos(th);
y = y + deltaS*sin(th);
th = normalize_angle(th + deltaTheta);
else
R = deltaS/deltaTheta;
x = x + R*(sin(th + deltaTheta) - sin(th));
y = y - R*(cos(th + deltaTheta) - cos(th));
th = normalize_angle(th + deltaTheta);
end
pose = [x; y; th];
end
Robotics System Toolbox: provides coordinate-frame utilities and standard message types. The recommended design is: keep the deterministic integrator in a MATLAB function (as above), then wrap it in a Simulink MATLAB Function block or a ROS 2 interface block if you are publishing to a robotics middleware.
11. Wolfram Mathematica Implementation (Symbolic + Numeric)
Mathematica is useful for symbolic verification (e.g., the straight-line limit proofs via series expansions) and for generating reference numerical results.
File: Chapter5_Lesson1.nb
(* Chapter5_Lesson1.nb
Wheel Odometry Computation — Differential Drive (planar SE(2))
*)
ClearAll[NormalizeAngle, TicksToWheelAngle, OdoUpdateFromTicks];
NormalizeAngle[theta_] := ArcTan[Sin[theta], Cos[theta]];
TicksToWheelAngle[dN_, ticksPerRev_, gearRatio_: 1] :=
2 Pi (N[dN]/(N[ticksPerRev] N[gearRatio]));
OdoUpdateFromTicks[{x_, y_, th_}, dNL_, dNR_, rL_, rR_, b_, ticksPerRev_, gearRatio_: 1] :=
Module[{dphiL, dphiR, dsL, dsR, deltaS, deltaTh, eps = 10^-12, R},
dphiL = TicksToWheelAngle[dNL, ticksPerRev, gearRatio];
dphiR = TicksToWheelAngle[dNR, ticksPerRev, gearRatio];
dsL = rL dphiL; dsR = rR dphiR;
deltaS = (dsR + dsL)/2;
deltaTh = (dsR - dsL)/b;
If[Abs[deltaTh] < eps,
{x + deltaS Cos[th], y + deltaS Sin[th], NormalizeAngle[th + deltaTh]},
R = deltaS/deltaTh;
{x + R (Sin[th + deltaTh] - Sin[th]),
y - R (Cos[th + deltaTh] - Cos[th]),
NormalizeAngle[th + deltaTh]}
]
];
params = <|"rL" -> 0.05, "rR" -> 0.05, "b" -> 0.30, "ticksPerRev" -> 2048, "gearRatio" -> 1.0|>;
pose = {0.0, 0.0, 0.0};
pose = Nest[OdoUpdateFromTicks[#, 40, 60, params["rL"], params["rR"], params["b"], params["ticksPerRev"], params["gearRatio"]] &,
pose, 200];
pose
12. Problems and Solutions
The following problems are designed to match typical textbook exercises in mobile robotics and odometry. Unless stated otherwise, assume a differential-drive robot with wheel radii \( r_L=r_R=r \) and baseline \( b \).
Problem 1 (Tick-to-Pose Computation): A robot has \( r=0.06\,\text{m} \), \( b=0.34\,\text{m} \), \( N_{\text{tpr}}=4096 \), \( g=1 \). At pose \( (x_k,y_k,\theta_k)=(0,0,0) \), it observes tick increments \( (\Delta N_{L,k},\Delta N_{R,k})=(120,150) \). Compute \( (x_{k+1},y_{k+1},\theta_{k+1}) \) using the exact arc update.
Solution:
First convert ticks to angles and arc lengths:
\[ \Delta \phi_{L,k} = 2\pi\frac{120}{4096}, \qquad \Delta \phi_{R,k} = 2\pi\frac{150}{4096}, \]
\[ \Delta s_{L,k} = r\Delta\phi_{L,k}, \qquad \Delta s_{R,k} = r\Delta\phi_{R,k}. \]
Then compute:
\[ \Delta s_k = \frac{\Delta s_{R,k}+\Delta s_{L,k}}{2}, \qquad \Delta\theta_k = \frac{\Delta s_{R,k}-\Delta s_{L,k}}{b}. \]
Since \( \Delta\theta_k \neq 0 \), use the arc update with \( \theta_k=0 \):
\[ x_{k+1} = \frac{\Delta s_k}{\Delta\theta_k}\Big(\sin(\Delta\theta_k)-0\Big), \qquad y_{k+1} = -\frac{\Delta s_k}{\Delta\theta_k}\Big(\cos(\Delta\theta_k)-1\Big), \qquad \theta_{k+1}=\Delta\theta_k. \]
Numerically, you may verify the result by running
Chapter5_Lesson1_Ex1.py (the first turning step matches
this computation).
Problem 2 (Derive the Arc Update): Starting from \( \dot{x}=v\cos\theta \), \( \dot{y}=v\sin\theta \), \( \dot{\theta}=\omega \), assume \( v,\omega \) are constant on \( [t_k,t_{k+1}] \). Derive the closed-form update for \( (x_{k+1},y_{k+1}) \).
Solution: This is exactly the proof in Section 4. In particular:
\[ x_{k+1}-x_k = \int_{0}^{\Delta t_k} v\cos(\theta_k+\omega\tau)d\tau = \frac{v}{\omega}\Big(\sin(\theta_k+\omega\Delta t_k)-\sin\theta_k\Big), \]
\[ y_{k+1}-y_k = \int_{0}^{\Delta t_k} v\sin(\theta_k+\omega\tau)d\tau = -\frac{v}{\omega}\Big(\cos(\theta_k+\omega\Delta t_k)-\cos\theta_k\Big). \]
Substituting \( \Delta s=v\Delta t_k \), \( \Delta\theta=\omega\Delta t_k \) yields the standard form.
Problem 3 (Straight-Line Limit via L'Hospital): Prove the limit \( \lim_{\Delta\theta → 0} \frac{\sin(\theta+\Delta\theta)-\sin\theta}{\Delta\theta} = \cos\theta \) and conclude the straight-line odometry update.
Solution: Define \( f(\Delta\theta)=\sin(\theta+\Delta\theta)-\sin\theta \) and \( g(\Delta\theta)=\Delta\theta \). Then \( f(0)=g(0)=0 \) and L'Hospital applies:
\[ \lim_{\Delta\theta → 0}\frac{f(\Delta\theta)}{g(\Delta\theta)} = \lim_{\Delta\theta → 0}\frac{f'(\Delta\theta)}{g'(\Delta\theta)} = \lim_{\Delta\theta → 0}\cos(\theta+\Delta\theta) = \cos\theta. \]
Multiplying by \( \Delta s \) gives \( \Delta x=\Delta s \cos\theta \). A symmetric argument yields \( \Delta y=\Delta s \sin\theta \).
Problem 4 (Baseline Estimation from In-Place Rotation): Suppose you command an ideal in-place rotation where the wheels move equal distances with opposite signs: \( \Delta s_{R} = -\Delta s_{L} = s \). Show that the resulting yaw change satisfies \( \Delta\theta = 2s/b \) and derive an estimator for \( b \).
Solution: Using \( \Delta\theta = (\Delta s_R - \Delta s_L)/b \):
\[ \Delta\theta = \frac{s-(-s)}{b} = \frac{2s}{b} \quad ⇒ \quad b = \frac{2s}{\Delta\theta}. \]
In practice, measure \( \Delta\theta \) with an external reference (e.g., a protractor-like fixture, motion capture, or a high-quality gyro in a controlled calibration setup). Then compute \( b \) from the above.
Problem 5 (Systematic Radius Mismatch and Curvature Bias): Let the true radii be \( r_L=r(1-\epsilon) \), \( r_R=r(1+\epsilon) \) with small \( |\epsilon| < 1 \), but your odometry assumes \( r_L=r_R=r \). For a commanded straight motion where the encoders report equal ticks \( \Delta N_L=\Delta N_R \), show that the true yaw increment is nonzero to first order in \( \epsilon \).
Solution: Equal ticks imply equal wheel angle increments: \( \Delta\phi_L=\Delta\phi_R=\Delta\phi \). True arc lengths are:
\[ \Delta s_L = r(1-\epsilon)\Delta\phi, \qquad \Delta s_R = r(1+\epsilon)\Delta\phi. \]
Therefore the true yaw increment is:
\[ \Delta\theta_{\text{true}} = \frac{\Delta s_R-\Delta s_L}{b} = \frac{r(1+\epsilon)\Delta\phi - r(1-\epsilon)\Delta\phi}{b} = \frac{2r\epsilon\Delta\phi}{b}. \]
Thus, even when the encoders appear balanced, a radius mismatch induces a curvature bias proportional to \( \epsilon \). This is one of the classic systematic odometry error sources studied in calibration literature.
13. Summary
We built the complete wheel-odometry computation chain: encoder ticks → wheel angles → wheel arc lengths → differential-drive increments \( (\Delta s,\Delta\theta) \) → exact discrete-time pose update. We proved the constant-curvature integration formula and its straight-line limit, and we provided robust implementations across major robotics programming environments.
Next lesson (Chapter 5, Lesson 2) will extend dead reckoning with IMU integration for ground robots.
14. References
- Borenstein, J., Feng, L. (1996). Measurement and correction of systematic odometry errors in mobile robots. IEEE Transactions on Robotics and Automation, 12(6), 869–880.
- Borenstein, J., Everett, H.R., Feng, L. (1996). Navigating Mobile Robots: Systems and Techniques. A.K. Peters / CRC Press (monograph with extensive odometry analysis).
- Kelly, A. (2004). Mobile Robot Localization from Large-Scale Appearance Mosaics. The International Journal of Robotics Research, 23(11), 1111–1129. (contains practical odometry integration discussions).
- Siegwart, R., Nourbakhsh, I.R., Scaramuzza, D. (2011). Introduction to Autonomous Mobile Robots (2nd ed.). MIT Press. (Chapters on odometry and mobile kinematics).
- Thrun, S., Burgard, W., Fox, D. (2005). Probabilistic Robotics. MIT Press. (Foundational treatment of odometry as a motion model; probabilistic aspects used later in this course).
- Uick, R., et al. (2007). Odometry error analysis for differential drive mobile robots. Robotics and Autonomous Systems, 55(6), 447–460.
- Antonelli, G. (2013). Interconnected Dynamic Systems: An Introduction to Networked Control Systems. Springer. (useful background on discrete integration and implementation considerations).
- Wang, C.-C., Thorpe, C., Thrun, S. (2003). Online simultaneous localization and mapping with detection and tracking of moving objects. IEEE International Conference on Robotics and Automation, 2918–2924. (context: odometry as a backbone signal).