Skip to content

Bicycle Kinematic Model: First Principles

The Vehicle Dynamics Foundation for the reference airside AV stack's Simulation and Control


1. Derivation from First Principles

1.1 Assumptions

  1. Rigid body: Vehicle doesn't deform
  2. No tire slip: Tire velocity is aligned with tire heading (kinematic, not dynamic)
  3. Planar motion: Vehicle moves in the x-y plane only (no pitch, roll)
  4. Two-wheel equivalent: Left and right wheels collapsed to single front and rear wheel on centerline

1.2 Geometry

            δ (steering angle)

    ┌──────●──────┐  Front axle (steered)
    │      │      │
    │      │ L    │  L = wheelbase = 3.15m (third-generation tug)
    │      │      │
    │      ●      │  Rear axle (reference point)
    └─────────────┘

     (x, y, θ) — state: position and heading of rear axle

1.3 Kinematic Equations (Continuous Time)

The rear axle traces a path. The front axle steers.

ẋ = v · cos(θ)           — x velocity
ẏ = v · sin(θ)           — y velocity
θ̇ = v · tan(δ) / L      — yaw rate (turning)

where:
  (x, y)  — rear axle position [m]
  θ       — heading angle [rad], 0 = east, π/2 = north
  v       — velocity at rear axle [m/s]
  δ       — front wheel steering angle [rad]
  L       — wheelbase [m] (distance between front and rear axles)

Derivation of θ̇:

The instantaneous turning radius R is related to steering angle by:
  tan(δ) = L / R

The yaw rate is:
  θ̇ = v / R = v · tan(δ) / L

1.4 Discrete-Time Model (Euler Integration)

the reference airside AV stack's pysim uses dt = 0.02s (50Hz):

python
def kinematic_step(x, y, theta, v, delta, L=3.15, dt=0.02):
    """One step of the kinematic bicycle model."""
    x_new = x + v * math.cos(theta) * dt
    y_new = y + v * math.sin(theta) * dt
    theta_new = theta + (v * math.tan(delta) / L) * dt

    # Normalize theta to [-π, π]
    theta_new = (theta_new + math.pi) % (2 * math.pi) - math.pi

    return x_new, y_new, theta_new

For better accuracy (RK4 integration):

python
def kinematic_step_rk4(x, y, theta, v, delta, L=3.15, dt=0.02):
    """Fourth-order Runge-Kutta integration."""
    def f(state):
        x, y, theta = state
        return np.array([
            v * np.cos(theta),
            v * np.sin(theta),
            v * np.tan(delta) / L
        ])

    state = np.array([x, y, theta])
    k1 = f(state)
    k2 = f(state + dt/2 * k1)
    k3 = f(state + dt/2 * k2)
    k4 = f(state + dt * k3)

    state_new = state + dt/6 * (k1 + 2*k2 + 2*k3 + k4)
    return state_new[0], state_new[1], state_new[2]

2. third-generation tug Parameters

python
# From airside_python_sim and av_comms config
third-generation tug_PARAMS = {
    'wheelbase': 3.15,              # meters (L_eff, validated from bag data)
    'max_steering_angle': 0.8762,   # radians (50.2 degrees)
    'max_speed': 6.67,              # m/s (24 km/h)
    'max_acceleration': 1.0,        # m/s² (approximate)
    'max_deceleration': 2.0,        # m/s² (hydraulic braking)
    'track_width': 1.8,             # meters (approximate)
    'vehicle_length': 5.5,          # meters (approximate)
    'vehicle_width': 2.0,           # meters (approximate)
    'turning_radius_min': 3.15 / math.tan(0.8762),  # = 2.63m at max steer
}

2.1 Effective Wheelbase (L_eff)

The kinematic bicycle model uses an "effective" wheelbase that may differ from the physical wheelbase due to:

  • Tire compliance (cornering stiffness)
  • Steering geometry (Ackermann approximation error)
  • Load distribution
python
# From fit_wheelbase.py — fitted to real third-generation tug bag data
# Method: minimize error between model prediction and GPS/odometry trajectory
# Result: L_eff = 3.15m
# Validation error: mean 4.76m over 116m trajectory (4.1%)

2.2 Minimum Turning Radius

R_min = L / tan(δ_max) = 3.15 / tan(0.8762) = 3.15 / 1.198 = 2.63m

This is the minimum turning radius of the rear axle.
The outer front wheel traces a larger radius:
R_outer = sqrt(R_min² + L²) = sqrt(2.63² + 3.15²) = 4.10m

The vehicle body sweeps an area between R_inner ≈ 1.73m and R_outer ≈ 4.10m

3. Ackermann Steering Geometry

3.1 Ideal Ackermann

In reality, the inner and outer wheels must steer at different angles to avoid tire scrub:

    δ_outer        δ_inner
       ↗              ↗
  ●────────────────────●   Front axle
  │         L          │
  │                    │
  ●────────────────────●   Rear axle
        R (turning radius)

Ideal Ackermann:
  cot(δ_outer) - cot(δ_inner) = W / L

  where W = track width = 1.8m, L = wheelbase = 3.15m

  For the bicycle model, δ is the equivalent single-wheel angle:
  δ ≈ (δ_inner + δ_outer) / 2  (approximation)
  More precisely: tan(δ) = L / R

3.2 third-generation tug Steering Conversion

From airside_av_comms:

Steering command (ROS): angular.z in radians
  → Convert: angle_rad → sign-inverted percentage counts
  → third-generation tug: ±95 counts = ±50.2° (0.8762 rad)
  → Scale: counts = angle_rad * (95 / 0.8762) = angle_rad * 108.4

Feedback: steering position sensor → percentage counts → radians

4. Crab Steering (third-generation tug Side Drive)

4.1 Crab Mode Kinematics

In crab mode, all four wheels steer in the same direction:

    δ_crab         δ_crab
       ↗              ↗
  ●────────────────────●   Front axle
  │                    │
  │                    │
  ●────────────────────●   Rear axle
       ↗              ↗
    δ_crab         δ_crab

Kinematic equations in crab mode:
  ẋ = v · cos(θ + δ_crab)    — moves sideways!
  ẏ = v · sin(θ + δ_crab)
  θ̇ = 0                      — heading doesn't change

This is pure translation at angle δ_crab relative to heading.

4.2 Transition Mode

third-generation tug can transition between Ackermann and crab steering:

State machine: ACKERMANN → TRANSITION → CRAB → TRANSITION → ACKERMANN

During transition, front and rear wheels steer at different rates
to smoothly change from differential to parallel steering.

5. Model Predictive Control (MPC) with Bicycle Model

5.1 Linearized State Space

For MPC, linearize around the current state:

State: x = [X, Y, θ, v]ᵀ
Input: u = [a, δ]ᵀ (acceleration, steering)

Continuous: ẋ = f(x, u)
Linearized: ẋ ≈ A·x + B·u

A = ∂f/∂x = [0  0  -v·sin(θ)  cos(θ)]
             [0  0   v·cos(θ)  sin(θ)]
             [0  0   0         tan(δ)/L]
             [0  0   0         0       ]

B = ∂f/∂u = [0               0            ]
             [0               0            ]
             [0               v/(L·cos²(δ))]
             [1               0            ]

Discretized: x_{k+1} = A_d·x_k + B_d·u_k
  A_d = I + A·dt,  B_d = B·dt  (Euler)

5.2 MPC Formulation

min_{u_0,...,u_{N-1}} Σ_{k=0}^{N-1} [x_k^T Q x_k + u_k^T R u_k] + x_N^T P x_N

subject to:
  x_{k+1} = f(x_k, u_k)            — bicycle model dynamics
  |δ| ≤ δ_max = 0.8762 rad          — steering limits
  |v| ≤ v_max = 6.67 m/s            — speed limits
  |a| ≤ a_max = 2.0 m/s²            — acceleration limits
  |δ̇| ≤ δ̇_max (steering rate limit)  — actuator rate limit

Q = diag(q_x, q_y, q_θ, q_v)  — state tracking weights
R = diag(r_a, r_δ)             — input effort weights
P = solution of discrete-time algebraic Riccati equation (terminal cost)

6. Integration with World Model

6.1 Physics Prior for Predictions

The bicycle model provides a physics prior for the world model:

python
def enforce_kinematic_feasibility(predicted_trajectory, dt=0.2):
    """Check if a world model prediction is kinematically feasible."""
    for t in range(1, len(predicted_trajectory)):
        x0, y0, theta0 = predicted_trajectory[t-1]
        x1, y1, theta1 = predicted_trajectory[t]

        # Implied velocity
        v = math.sqrt((x1-x0)**2 + (y1-y0)**2) / dt

        # Implied steering angle
        dtheta = theta1 - theta0
        if abs(v) > 0.01:
            delta = math.atan(dtheta * L / (v * dt))
        else:
            delta = 0

        # Check feasibility
        if abs(v) > third-generation tug_PARAMS['max_speed']:
            return False, f"Speed {v:.1f} exceeds max {third-generation tug_PARAMS['max_speed']}"
        if abs(delta) > third-generation tug_PARAMS['max_steering_angle']:
            return False, f"Steering {delta:.2f} exceeds max {third-generation tug_PARAMS['max_steering_angle']}"

    return True, "Feasible"

6.2 World Model Loss Regularization

Add a kinematic feasibility loss to world model training:

python
def kinematic_loss(predicted_ego_trajectory, dt=0.2, L=3.15):
    """Penalize kinematically infeasible ego predictions."""
    positions = predicted_ego_trajectory[:, :2]  # (T, 2)
    headings = predicted_ego_trajectory[:, 2]     # (T,)

    velocities = torch.diff(positions, dim=0) / dt  # (T-1, 2)
    speeds = velocities.norm(dim=1)                   # (T-1,)
    heading_rates = torch.diff(headings) / dt          # (T-1,)

    # Implied steering angles
    implied_steering = torch.atan(heading_rates * L / (speeds + 1e-6))

    # Penalize exceeding limits
    speed_violation = F.relu(speeds - 6.67)
    steering_violation = F.relu(implied_steering.abs() - 0.8762)

    return speed_violation.mean() + steering_violation.mean()

7. Validation: the reference airside AV stack's 4.1% Error Result

Test: Replay real third-generation tug bag data commands through kinematic bicycle model
  Input: Recorded steering and velocity commands from CAN
  Output: Predicted trajectory from bicycle model
  Ground truth: GPS/odometry trajectory from bag

Result:
  Total distance: 116m
  Mean position error: 4.76m (4.1%)
  Max position error: ~8m at end of trajectory (accumulated)

Error sources:
  1. No tire slip modeling (dynamic effects at turns)
  2. Hydraulic steering actuator delay (not modeled)
  3. Effective wheelbase varies with load
  4. GPS/odometry ground truth has its own errors
  5. Euler integration numerical error (small at 50Hz)

4.1% is good enough for:

  • Nav stack testing (trajectory following, behavior FSM)
  • Scenario-based validation (waypoint reaching, obstacle response)
  • World model training data augmentation (approximate physics)

Not good enough for:

  • Precise docking (< 5cm accuracy needed)
  • Safety-critical collision prediction (need better dynamic model)
  • Crab steering mode (different kinematics not in bicycle model)

Sources

  • Rajamani, R. "Vehicle Dynamics and Control." Springer, 2012
  • Kong et al. "Kinematic and Dynamic Vehicle Models for Autonomous Driving Control Design." IV, 2015
  • Werling et al. "Optimal Trajectory Generation for Dynamic Street Scenarios in a Frenet Frame." ICRA, 2010
  • reference airside AV stack airside_python_sim source code (validated model)
  • reference airside AV stack fit_wheelbase.py (parameter identification)

Public research notes collected from public sources.