Chapter 2: Wheeled Locomotion Kinematics (Mobile-Specific)
Lesson 3: Car-Like / Ackermann Steering Kinematics
This lesson develops the low-speed (pure-rolling) kinematics of car-like mobile robots with Ackermann steering. We derive the geometric steering condition, connect it to the single-track (bicycle) model, and obtain an exact discrete-time integration formula for piecewise-constant commands. Implementations are provided in Python, C++, Java, MATLAB/Simulink-style scripting, and Wolfram Mathematica.
1. Modeling Assumptions and State/Input
We model a car-like mobile robot moving on a planar surface under pure rolling and no lateral slip (the same rolling assumptions introduced in Chapter 2, Lesson 1). The robot pose is \( \mathbf{q} = [x,\; y,\; \theta]^\top \in \mathbb{R}^3 \), where \( (x,y) \) is the position of a body-fixed reference point (we use the midpoint of the rear axle), and \( \theta \) is the yaw (heading).
A standard kinematic command pair for car-like robots is forward speed and steering angle: \( \mathbf{u} = [v,\; \delta]^\top \), where \( v \) is the longitudinal speed at the rear axle midpoint, and \( \delta \) is the front steering angle of a virtual single wheel (single-track abstraction).
Geometric parameters: \( L \) (wheelbase: rear axle to front axle distance) and \( W \) (track width: left-to-right wheel separation). When needed, \( r \) denotes a wheel radius.
The central objective of Ackermann steering is: during a turn, the instantaneous velocity of each wheel contact point is aligned with that wheel’s rolling direction (i.e., zero lateral component). This implies that the extensions of the wheel planes intersect at a common point in the plane: the instantaneous center of rotation (ICR).
2. Ackermann Steering Geometry and the ICR Condition
Consider a planar turn with ICR located at a distance \( R \) from the midpoint of the rear axle (signed radius; \( R > 0 \) for a left turn, \( R < 0 \) for a right turn). Let \( \delta_L \) and \( \delta_R \) be the left and right front wheel steering angles.
Under pure rolling, the wheel plane of each front wheel must pass through the ICR. Using right-triangle geometry on the front axle, the lines from the front wheels to the ICR have lateral offsets \( R \mp W/2 \). Therefore, the steering angles satisfy:
\[ \tan(\delta_L) = \frac{L}{R - W/2}, \qquad \tan(\delta_R) = \frac{L}{R + W/2}. \]
Equivalently,
\[ \delta_L = \arctan\!\Big(\frac{L}{R - W/2}\Big), \qquad \delta_R = \arctan\!\Big(\frac{L}{R + W/2}\Big). \]
The inside wheel (for a left turn, the left wheel) must steer more sharply: since \( |R - W/2| < |R + W/2| \), we obtain \( |\delta_L| > |\delta_R| \) for the corresponding turn direction.
flowchart TD
A["Given geometry: wheelbase L, track W"] --> B["Choose signed turn radius R (R>0 left, R<0 right)"]
B --> C["Compute front steering angles"]
C --> DL["delta_L = atan( L / (R - W/2) )"]
C --> DR["delta_R = atan( L / (R + W/2) )"]
DL --> I["Wheel planes intersect at common ICR"]
DR --> I
2.1 Proof Sketch: Why the Wheel Planes Must Intersect
Let a wheel’s rolling direction be the unit vector \( \mathbf{t} \) in the plane of motion, and let the wheel contact point velocity be \( \mathbf{v}_c \). Pure rolling with no lateral slip implies \( \mathbf{v}_c \) is parallel to \( \mathbf{t} \), hence the normal component is zero: \( \mathbf{n}^\top \mathbf{v}_c = 0 \), where \( \mathbf{n} \) is the wheel’s lateral unit normal in the ground plane.
For planar rigid-body motion, all point velocities can be expressed using an instantaneous angular velocity \( \dot{\theta} \) about some ICR point \( \mathbf{p}_{ICR} \). In 2D, this implies the velocity at a contact point is orthogonal to the radius vector from the ICR. Therefore, for the wheel to have zero lateral slip, the radius vector from the ICR to the contact must be orthogonal to the wheel plane’s lateral direction, i.e., the line from wheel contact to ICR must lie in the wheel plane. Applying this simultaneously to both front wheels forces both wheel-plane extensions to pass through the same ICR, yielding the Ackermann relations above.
3. Single-Track (Bicycle) Model as a Kinematic Abstraction
A widely used low-speed approximation is the single-track (bicycle) model, which replaces the left/right wheel pairs by a single rear wheel aligned with the body and a single front wheel steered by \( \delta \). The bicycle model is exact for the rear axle midpoint motion when the vehicle obeys Ackermann geometry and lateral slip is negligible.
Let the rear axle midpoint be the reference point. Then the planar kinematics are:
\[ \dot{x} = v \cos\theta,\qquad \dot{y} = v \sin\theta,\qquad \dot{\theta} = \frac{v}{L}\tan\delta. \]
3.1 Derivation via Turning Radius
If the rear axle midpoint follows a circular arc of radius \( R \), then the yaw rate is \( \dot{\theta} = v/R \). The bicycle geometry implies \( \tan\delta = L/R \) (front wheel points toward the ICR), hence:
\[ R = \frac{L}{\tan\delta}, \qquad \dot{\theta} = \frac{v}{R} = \frac{v}{L}\tan\delta. \]
Define curvature \( \kappa \): \( \kappa = 1/R \). Then:
\[ \kappa = \frac{\tan\delta}{L}, \qquad \dot{\theta} = v\kappa. \]
3.2 Mapping Virtual Steering to Left/Right Front Angles
If the actuator provides a single steering command \( \delta \) (virtual bicycle angle), we compute \( R = L/\tan\delta \) and then apply Ackermann to obtain \( \delta_L, \delta_R \). Combining:
\[ R = \frac{L}{\tan\delta} \quad\Longrightarrow\quad \delta_L = \arctan\!\Big(\frac{L}{R - W/2}\Big), \;\; \delta_R = \arctan\!\Big(\frac{L}{R + W/2}\Big). \]
This mapping is what an Ackermann steering mechanism approximates mechanically; software often uses it explicitly in simulation and control.
4. Exact Discrete-Time Integration (Constant \(v,\delta\) over \(\Delta t\))
For odometry, simulation, and motion prediction, we frequently assume commands are constant over a short interval \( \Delta t \) and update the pose: \( \mathbf{q}_{k+1} \) from \( \mathbf{q}_k \). Let \( \omega = \dot{\theta} = (v/L)\tan\delta \).
4.1 Closed-Form Update
If \( |\omega| \) is not near zero, the motion is circular and integrates exactly:
\[ \begin{aligned} x_{k+1} ← x_k + \frac{v}{\omega}\Big(\sin(\theta_k + \omega\Delta t) - \sin\theta_k\Big),\\ y_{k+1} ← y_k + \frac{v}{\omega}\Big(-\cos(\theta_k + \omega\Delta t) + \cos\theta_k\Big),\\ \theta_{k+1} ← \theta_k + \omega\Delta t. \end{aligned} \]
If \( |\omega| < \varepsilon \) (approximately straight), use the limit \( \omega \rightarrow 0 \):
\[ \begin{aligned} x_{k+1} ← x_k + v\Delta t\cos\theta_k,\\ y_{k+1} ← y_k + v\Delta t\sin\theta_k,\\ \theta_{k+1} ← \theta_k. \end{aligned} \]
4.2 Proof (Integration by Substitution)
With constant \( \omega \), we have \( \theta(t)=\theta_k+\omega t \) for \( t\in[0,\Delta t] \). Then: \( \dot{x}=v\cos(\theta_k+\omega t) \), \( \dot{y}=v\sin(\theta_k+\omega t) \). Integrating:
\[ x_{k+1}-x_k = \int_0^{\Delta t} v\cos(\theta_k+\omega t)\,dt = \frac{v}{\omega}\Big(\sin(\theta_k+\omega\Delta t)-\sin\theta_k\Big), \]
\[ y_{k+1}-y_k = \int_0^{\Delta t} v\sin(\theta_k+\omega t)\,dt = \frac{v}{\omega}\Big(-\cos(\theta_k+\omega\Delta t)+\cos\theta_k\Big), \]
while \( \theta_{k+1}-\theta_k = \int_0^{\Delta t}\omega\,dt = \omega\Delta t \). The straight-line case follows by Taylor expansion or by applying the limit \( \omega \rightarrow 0 \).
flowchart TD
U["Inputs over dt: v, delta"] --> W["omega = (v/L)*tan(delta)"]
W --> C{"abs(omega) small?"}
C -->|yes| S["Straight update: \nx+=v*dt*cos(theta), y+=v*dt*sin(theta)"]
C -->|no| T["Circular update: \nuse sin/cos closed form"]
S --> Q["q_{k+1}"]
T --> Q
Q --> R["Repeat for next interval"]
5. Wheel Speeds Under Ackermann (Low-Speed Kinematics)
For a commanded rear-axle midpoint speed \( v \) and turn radius \( R \), the left/right rear wheels trace circles of radii \( R - W/2 \) and \( R + W/2 \) (signed). Their linear speeds (pure rolling) are proportional to radius:
\[ v_{RL} = v\frac{R - W/2}{R},\qquad v_{RR} = v\frac{R + W/2}{R}. \]
If wheel radius is \( r \), the wheel angular rates are:
\[ \dot{\phi}_{RL} = \frac{v_{RL}}{r},\qquad \dot{\phi}_{RR} = \frac{v_{RR}}{r}. \]
These relations are frequently used to convert between a “car-like” command interface \( (v,\delta) \) and actuator-level wheel rates in platforms that implement Ackermann steering with driven rear wheels.
6. Implementations
The following implementations include: (i) Ackermann left/right steering angle computation, (ii) bicycle kinematics, and (iii) exact discrete-time integration for piecewise-constant commands.
6.1 Python
Chapter2_Lesson3.py
"""
Chapter2_Lesson3.py
Autonomous Mobile Robots — Chapter 2, Lesson 3
Car-Like / Ackermann Steering Kinematics
This script implements:
1) Ackermann steering geometry (left/right wheel steering angles).
2) Bicycle (single-track) kinematic model.
3) Exact discrete-time integration for constant (v, delta) over dt.
4) A small simulation demo, plus an optional plot.
Recommended Python libs for AMR kinematics:
- numpy (arrays, numerics)
- matplotlib (plotting)
- scipy (optional for integration/optimization)
- sympy (optional for symbolic checks)
- ROS 1/2 interfaces (optional): geometry_msgs, nav_msgs, ackermann_msgs
Install (if needed):
pip install numpy matplotlib
"""
from __future__ import annotations
import math
from dataclasses import dataclass
from typing import List, Tuple
import numpy as np
@dataclass
class Pose2D:
"""Planar pose in SE(2) coordinates (x, y, yaw)."""
x: float
y: float
theta: float # yaw [rad]
def wrap_to_pi(angle: float) -> float:
"""Wrap an angle to (-pi, pi]."""
a = (angle + math.pi) % (2.0 * math.pi) - math.pi
return a
def curvature_from_steering(delta: float, L: float) -> float:
"""
Bicycle model curvature kappa = tan(delta)/L.
delta: virtual front wheel steering angle [rad]
L: wheelbase [m]
"""
return math.tan(delta) / L
def ackermann_wheel_angles_from_virtual(delta: float, L: float, W: float) -> Tuple[float, float]:
"""
Convert a *virtual* front steering angle delta (bicycle model) to the left/right
front wheel steering angles (Ackermann condition) for a vehicle with wheelbase L
and track width W.
Convention:
- delta > 0 means turning left (CCW yaw).
- Returned: (delta_left, delta_right) in radians.
Derivation:
kappa = tan(delta)/L, so turning radius R = 1/kappa (if kappa != 0).
Then:
delta_left = atan(L/(R - W/2))
delta_right = atan(L/(R + W/2))
for a left turn (R > 0). For right turn, signs are handled by R.
"""
kappa = curvature_from_steering(delta, L)
if abs(kappa) < 1e-12:
return 0.0, 0.0
R = 1.0 / kappa # signed
# Denominators are signed; atan handles sign. Avoid division by zero.
dl = math.atan2(L, (R - W / 2.0))
dr = math.atan2(L, (R + W / 2.0))
return dl, dr
def step_bicycle_euler(p: Pose2D, v: float, delta: float, L: float, dt: float) -> Pose2D:
"""
Forward Euler discretization of the bicycle model:
x_dot = v cos(theta)
y_dot = v sin(theta)
theta_dot = v/L * tan(delta)
"""
omega = v * math.tan(delta) / L
x_new = p.x + dt * v * math.cos(p.theta)
y_new = p.y + dt * v * math.sin(p.theta)
th_new = wrap_to_pi(p.theta + dt * omega)
return Pose2D(x_new, y_new, th_new)
def step_bicycle_exact(p: Pose2D, v: float, delta: float, L: float, dt: float) -> Pose2D:
"""
Exact integration of the bicycle model over dt assuming v and delta are constant.
Let omega = v/L * tan(delta). Then:
If |omega| is small:
x_{k+1} = x_k + v dt cos(theta_k)
y_{k+1} = y_k + v dt sin(theta_k)
theta_{k+1} = theta_k + omega dt
Else:
x_{k+1} = x_k + (v/omega)(sin(theta+omega dt)-sin(theta))
y_{k+1} = y_k + (v/omega)(-cos(theta+omega dt)+cos(theta))
theta_{k+1} = theta_k + omega dt
"""
omega = v * math.tan(delta) / L
if abs(omega) < 1e-10:
return step_bicycle_euler(p, v, delta, L, dt)
th2 = p.theta + omega * dt
x_new = p.x + (v / omega) * (math.sin(th2) - math.sin(p.theta))
y_new = p.y + (v / omega) * (-math.cos(th2) + math.cos(p.theta))
th_new = wrap_to_pi(th2)
return Pose2D(x_new, y_new, th_new)
def simulate_piecewise_constant(
p0: Pose2D,
controls: List[Tuple[float, float, float]],
L: float,
method: str = "exact",
) -> List[Pose2D]:
"""
Simulate with piecewise-constant controls.
controls: list of (v, delta, dt).
method: "exact" or "euler"
Returns: list of poses including initial.
"""
poses = [p0]
p = p0
for v, delta, dt in controls:
if method == "exact":
p = step_bicycle_exact(p, v, delta, L, dt)
elif method == "euler":
p = step_bicycle_euler(p, v, delta, L, dt)
else:
raise ValueError("method must be 'exact' or 'euler'")
poses.append(p)
return poses
def demo():
# Vehicle parameters
L = 2.7 # wheelbase [m]
W = 1.6 # track width [m]
# Initial pose
p0 = Pose2D(0.0, 0.0, 0.0)
# Control sequence: (v, delta, dt)
controls = [
(2.0, math.radians(15.0), 1.0),
(2.0, math.radians(15.0), 1.0),
(2.0, math.radians(0.0), 1.0),
(2.0, math.radians(0.0), 1.0),
]
poses_exact = simulate_piecewise_constant(p0, controls, L=L, method="exact")
poses_euler = simulate_piecewise_constant(p0, controls, L=L, method="euler")
pe = poses_exact[-1]
pu = poses_euler[-1]
print("Final pose (exact):", pe)
print("Final pose (euler):", pu)
# Ackermann left/right wheel angles corresponding to the *virtual* delta
delta_virtual = math.radians(15.0)
dl, dr = ackermann_wheel_angles_from_virtual(delta_virtual, L=L, W=W)
print(f"Virtual steering delta = {math.degrees(delta_virtual):.2f} deg")
print(f"Ackermann delta_left = {math.degrees(dl):.2f} deg")
print(f"Ackermann delta_right = {math.degrees(dr):.2f} deg")
# Optional plot
try:
import matplotlib.pyplot as plt
xe = [p.x for p in poses_exact]
ye = [p.y for p in poses_exact]
xu = [p.x for p in poses_euler]
yu = [p.y for p in poses_euler]
plt.figure()
plt.plot(xe, ye, marker="o", label="exact")
plt.plot(xu, yu, marker="x", label="euler")
plt.axis("equal")
plt.grid(True)
plt.xlabel("x [m]")
plt.ylabel("y [m]")
plt.title("Bicycle model simulation (exact vs euler)")
plt.legend()
plt.show()
except Exception as e:
print("Plot skipped:", e)
if __name__ == "__main__":
demo()
6.2 C++
Chapter2_Lesson3.cpp
/*
Chapter2_Lesson3.cpp
Autonomous Mobile Robots — Chapter 2, Lesson 3
Car-Like / Ackermann Steering Kinematics
*/
#include <cmath>
#include <iostream>
#include <tuple>
#include <vector>
struct Pose2D {
double x{0.0};
double y{0.0};
double theta{0.0}; // yaw [rad]
};
static double wrap_to_pi(double a) {
a = std::fmod(a + M_PI, 2.0 * M_PI);
if (a < 0) a += 2.0 * M_PI;
return a - M_PI;
}
static double curvature_from_steering(double delta, double L) {
return std::tan(delta) / L;
}
static std::pair<double, double> ackermann_wheel_angles_from_virtual(double delta, double L, double W) {
const double kappa = curvature_from_steering(delta, L);
if (std::abs(kappa) < 1e-12) return {0.0, 0.0};
const double R = 1.0 / kappa; // signed
const double dl = std::atan2(L, (R - W / 2.0));
const double dr = std::atan2(L, (R + W / 2.0));
return {dl, dr};
}
static Pose2D step_bicycle_exact(const Pose2D& p, double v, double delta, double L, double dt) {
const double omega = v * std::tan(delta) / L;
Pose2D out = p;
if (std::abs(omega) < 1e-10) {
out.x += dt * v * std::cos(p.theta);
out.y += dt * v * std::sin(p.theta);
out.theta = wrap_to_pi(p.theta + dt * omega);
return out;
}
const double th2 = p.theta + omega * dt;
out.x += (v / omega) * (std::sin(th2) - std::sin(p.theta));
out.y += (v / omega) * (-std::cos(th2) + std::cos(p.theta));
out.theta = wrap_to_pi(th2);
return out;
}
int main() {
const double L = 2.7;
const double W = 1.6;
Pose2D p{0.0, 0.0, 0.0};
struct Control { double v; double delta; double dt; };
const std::vector<Control> controls = {
{2.0, 15.0 * M_PI / 180.0, 1.0},
{2.0, 15.0 * M_PI / 180.0, 1.0},
{2.0, 0.0 * M_PI / 180.0, 1.0},
{2.0, 0.0 * M_PI / 180.0, 1.0}
};
for (const auto& u : controls) {
p = step_bicycle_exact(p, u.v, u.delta, L, u.dt);
}
std::cout << "Final pose (exact): x=" << p.x << " y=" << p.y << " theta=" << p.theta << "\n";
const double delta_virtual = 15.0 * M_PI / 180.0;
const auto lr = ackermann_wheel_angles_from_virtual(delta_virtual, L, W);
std::cout << "Virtual delta = " << delta_virtual << " rad\n";
std::cout << "Ackermann delta_left = " << lr.first << " rad\n";
std::cout << "Ackermann delta_right = " << lr.second << " rad\n";
return 0;
}
6.3 Java
Chapter2_Lesson3.java
public class Chapter2_Lesson3 {
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)", x, y, theta);
}
}
public static double wrapToPi(double a) {
double twoPi = 2.0 * Math.PI;
a = (a + Math.PI) % twoPi;
if (a < 0) a += twoPi;
return a - Math.PI;
}
public static double curvatureFromSteering(double delta, double L) {
return Math.tan(delta) / L;
}
// Return {deltaLeft, deltaRight}
public static double[] ackermannWheelAnglesFromVirtual(double delta, double L, double W) {
double kappa = curvatureFromSteering(delta, L);
if (Math.abs(kappa) < 1e-12) return new double[]{0.0, 0.0};
double R = 1.0 / kappa; // signed
double dl = Math.atan2(L, (R - W / 2.0));
double dr = Math.atan2(L, (R + W / 2.0));
return new double[]{dl, dr};
}
public static Pose2D stepBicycleExact(Pose2D p, double v, double delta, double L, double dt) {
double omega = v * Math.tan(delta) / L;
if (Math.abs(omega) < 1e-10) {
double x2 = p.x + dt * v * Math.cos(p.theta);
double y2 = p.y + dt * v * Math.sin(p.theta);
double t2 = wrapToPi(p.theta + dt * omega);
return new Pose2D(x2, y2, t2);
}
double th2 = p.theta + omega * dt;
double x2 = p.x + (v / omega) * (Math.sin(th2) - Math.sin(p.theta));
double y2 = p.y + (v / omega) * (-Math.cos(th2) + Math.cos(p.theta));
double t2 = wrapToPi(th2);
return new Pose2D(x2, y2, t2);
}
public static void main(String[] args) {
double L = 2.7;
double W = 1.6;
Pose2D p = new Pose2D(0.0, 0.0, 0.0);
// controls: (v, delta, dt)
double[][] controls = new double[][]{
{2.0, Math.toRadians(15.0), 1.0},
{2.0, Math.toRadians(15.0), 1.0},
{2.0, Math.toRadians(0.0), 1.0},
{2.0, Math.toRadians(0.0), 1.0}
};
for (double[] u : controls) {
p = stepBicycleExact(p, u[0], u[1], L, u[2]);
}
System.out.println("Final pose (exact): " + p);
double deltaVirtual = Math.toRadians(15.0);
double[] lr = ackermannWheelAnglesFromVirtual(deltaVirtual, L, W);
System.out.printf("Virtual delta = %.6f rad%n", deltaVirtual);
System.out.printf("Ackermann delta_left = %.6f rad%n", lr[0]);
System.out.printf("Ackermann delta_right = %.6f rad%n", lr[1]);
}
}
6.4 MATLAB / Simulink-Style Scripting
Chapter2_Lesson3.m
% Chapter2_Lesson3.m
% Autonomous Mobile Robots — Chapter 2, Lesson 3
% Car-Like / Ackermann Steering Kinematics
clear; clc;
L = 2.7; % wheelbase [m]
W = 1.6; % track width [m]
p = [0; 0; 0]; % [x; y; theta]
% controls: [v, delta, dt]
U = [
2.0, deg2rad(15.0), 1.0;
2.0, deg2rad(15.0), 1.0;
2.0, deg2rad(0.0), 1.0;
2.0, deg2rad(0.0), 1.0
];
P = zeros(size(U,1)+1, 3);
P(1,:) = p';
for k = 1:size(U,1)
v = U(k,1); delta = U(k,2); dt = U(k,3);
p = step_bicycle_exact(p, v, delta, L, dt);
P(k+1,:) = p';
end
disp("Final pose (exact):");
disp(P(end,:));
delta_virtual = deg2rad(15.0);
[dl, dr] = ackermann_wheel_angles_from_virtual(delta_virtual, L, W);
fprintf("Virtual delta = %.3f deg\n", rad2deg(delta_virtual));
fprintf("Ackermann delta_left = %.3f deg\n", rad2deg(dl));
fprintf("Ackermann delta_right = %.3f deg\n", rad2deg(dr));
figure; plot(P(:,1), P(:,2), '-o'); axis equal; grid on;
xlabel('x [m]'); ylabel('y [m]');
title('Bicycle model simulation (exact integration)');
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Local functions
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function a = wrap_to_pi(a)
a = mod(a + pi, 2*pi);
if a < 0, a = a + 2*pi; end
a = a - pi;
end
function kappa = curvature_from_steering(delta, L)
kappa = tan(delta) / L;
end
function [dl, dr] = ackermann_wheel_angles_from_virtual(delta, L, W)
kappa = curvature_from_steering(delta, L);
if abs(kappa) < 1e-12
dl = 0.0; dr = 0.0; return;
end
R = 1.0 / kappa; % signed
dl = atan2(L, (R - W/2));
dr = atan2(L, (R + W/2));
end
function p2 = step_bicycle_exact(p, v, delta, L, dt)
omega = v * tan(delta) / L;
if abs(omega) < 1e-10
x2 = p(1) + dt * v * cos(p(3));
y2 = p(2) + dt * v * sin(p(3));
t2 = wrap_to_pi(p(3) + dt * omega);
p2 = [x2; y2; t2];
return;
end
th2 = p(3) + omega * dt;
x2 = p(1) + (v/omega) * (sin(th2) - sin(p(3)));
y2 = p(2) + (v/omega) * (-cos(th2) + cos(p(3)));
t2 = wrap_to_pi(th2);
p2 = [x2; y2; t2];
end
6.5 Wolfram Mathematica
Chapter2_Lesson3.nb
(* Chapter2_Lesson3.nb
Autonomous Mobile Robots — Chapter 2, Lesson 3
Car-Like / Ackermann Steering Kinematics
*)
Notebook[{
Cell["Chapter 2 — Lesson 3: Car-Like / Ackermann Steering Kinematics", "Title"],
Cell["1. Ackermann Geometry", "Section"],
Cell[TextData[{
"Let wheelbase be L and track width be W. For a turn with signed radius R (positive = left turn), ",
"the inside/outside front wheel steering angles are:"
}], "Text"],
Cell[BoxData@ToBoxes@
HoldForm[{
δL == ArcTan[L/(R - W/2)],
δR == ArcTan[L/(R + W/2)]
}], "Input"],
Cell["2. Bicycle (Single-Track) Kinematic Model", "Section"],
Cell[TextData[{
"Virtual steering angle δ gives curvature κ = Tan[δ]/L and yaw rate ω = v κ."
}], "Text"],
Cell[BoxData@ToBoxes@
HoldForm[{
κ == Tan[δ]/L,
ω == v Tan[δ]/L
}], "Input"],
Cell["3. Exact Discrete-Time Integration (Constant v, δ over dt)", "Section"],
Cell["For ω != 0:", "Text"],
Cell[BoxData@ToBoxes@
HoldForm[{
xNext == x + (v/ω) (Sin[θ + ω dt] - Sin[θ]),
yNext == y + (v/ω) (-Cos[θ + ω dt] + Cos[θ]),
θNext == θ + ω dt
}], "Input"],
Cell["Verify limit as ω -> 0 equals straight-line motion:", "Text"],
Cell[BoxData@ToBoxes@
HoldForm[
Limit[{x + (v/ω) (Sin[θ + ω dt] - Sin[θ]),
y + (v/ω) (-Cos[θ + ω dt] + Cos[θ]),
θ + ω dt}, ω -> 0]
], "Input"]
},
WindowSize -> {1000, 700},
FrontEndVersion -> "14.0",
StyleDefinitions -> "Default.nb"
]
7. Problems and Solutions
Problem 1 (Ackermann angles from radius): A car-like robot has wheelbase \( L \) and track \( W \). For a left turn with rear-axle midpoint radius \( R > 0 \), derive \( \delta_L \) and \( \delta_R \) such that both front wheels roll without lateral slip.
Solution: For each front wheel, the wheel plane must pass through the ICR. The left front wheel’s lateral offset to the ICR is \( R - W/2 \); the right is \( R + W/2 \). Forming right triangles with opposite side \( L \) and adjacent side as the lateral offset:
\[ \tan(\delta_L) = \frac{L}{R - W/2},\qquad \tan(\delta_R) = \frac{L}{R + W/2}, \]
hence \( \delta_L = \arctan(L/(R - W/2)) \) and \( \delta_R = \arctan(L/(R + W/2)) \).
Problem 2 (Bicycle curvature): Show that in the bicycle model the curvature \( \kappa \) of the rear-axle midpoint trajectory satisfies \( \kappa = \tan\delta / L \).
Solution: By definition \( \kappa = 1/R \). The bicycle geometry gives \( \tan\delta = L/R \) (front wheel points toward the ICR). Therefore \( R = L/\tan\delta \) and \( \kappa = 1/R = \tan\delta/L \).
Problem 3 (Exact integration): Starting from \( \dot{x}=v\cos\theta \), \( \dot{y}=v\sin\theta \), \( \dot{\theta}=\omega \) with constant \( v \) and \( \omega \neq 0 \), derive the closed-form discrete update over \( \Delta t \).
Solution: Since \( \dot{\theta}=\omega \), \( \theta(t)=\theta_k+\omega t \). Integrate:
\[ x_{k+1}-x_k = \int_0^{\Delta t} v\cos(\theta_k+\omega t)\,dt = \frac{v}{\omega}\Big(\sin(\theta_k+\omega\Delta t)-\sin\theta_k\Big), \]
\[ y_{k+1}-y_k = \int_0^{\Delta t} v\sin(\theta_k+\omega t)\,dt = \frac{v}{\omega}\Big(-\cos(\theta_k+\omega\Delta t)+\cos\theta_k\Big), \]
and \( \theta_{k+1}=\theta_k+\omega\Delta t \).
Problem 4 (Straight-line limit): Show that the exact integration formulas reduce to straight motion as \( \omega \rightarrow 0 \).
Solution: Use first-order Taylor expansions: \( \sin(\theta+\omega\Delta t)=\sin\theta+\omega\Delta t\cos\theta+o(\omega) \) and \( \cos(\theta+\omega\Delta t)=\cos\theta-\omega\Delta t\sin\theta+o(\omega) \). Substitute into the exact increments:
\[ x_{k+1}-x_k = \frac{v}{\omega}\Big(\omega\Delta t\cos\theta_k+o(\omega)\Big) \rightarrow v\Delta t\cos\theta_k, \]
\[ y_{k+1}-y_k = \frac{v}{\omega}\Big(\omega\Delta t\sin\theta_k+o(\omega)\Big) \rightarrow v\Delta t\sin\theta_k. \]
Problem 5 (Rear wheel speeds in a turn): A robot turns with rear-axle midpoint radius \( R \neq 0 \) and midpoint speed \( v \). Derive \( v_{RL} \) and \( v_{RR} \) for the left and right rear wheels.
Solution: The yaw rate is \( \dot{\theta}=v/R \). The rear wheel radii are \( R - W/2 \) and \( R + W/2 \), so their speeds are radius times yaw rate:
\[ v_{RL} = (R - W/2)\dot{\theta} = (R - W/2)\frac{v}{R} = v\frac{R - W/2}{R}, \qquad v_{RR} = v\frac{R + W/2}{R}. \]
If wheel radius is \( r \), then \( \dot{\phi}_{RL}=v_{RL}/r \) and \( \dot{\phi}_{RR}=v_{RR}/r \).
8. Summary
We derived the Ackermann steering condition from the ICR requirement for pure rolling, connected it to the bicycle kinematic model, and obtained an exact discrete-time integration rule for piecewise-constant commands. These results are the kinematic core used later for car-like feasibility analysis (Chapter 3) and for prediction inside localization and planning loops (Chapters 6–16).
9. References
- Ackermann, G. (1817). Ueber die Einrichtung der Fuhrwerke. Historische technische Abhandlung.
- Kelly, A. (2004). A vector algebra formulation of mobile robot velocity kinematics. Carnegie Mellon University Robotics Institute Technical Report.
- Coulter, R.C. (1992). Implementation of the Pure Pursuit Path Tracking Algorithm. Carnegie Mellon University Robotics Institute Technical Report.
- Rajamani, R. (2006). Vehicle Dynamics and Control. Springer. (Chapters on kinematic bicycle model and steering geometry.)
- Snider, J.M. (2009). Automatic Steering Methods for Autonomous Automobile Path Tracking. Robotics Institute Technical Report.
- Wong, J.Y. (2001). Theory of Ground Vehicles (3rd ed.). Wiley. (Steering kinematics and low-speed assumptions.)
- Kong, J., Pfeiffer, M., Schildbach, G., & Borrelli, F. (2015). Kinematic and Dynamic Vehicle Models for Autonomous Driving Control Design. IEEE Intelligent Vehicles Symposium, 1094–1099.
- Polack, P., Altché, F., d’Andréa-Novel, B., & Lusetti, B. (2017). The kinematic bicycle model: A consistent model for planning and control? IEEE Intelligent Vehicles Symposium, 812–818.