Skip to main content
Kinematics models convert between wheel velocities and vehicle pose (position and orientation). Donkeycar supports multiple kinematic models for different robot configurations.

Overview

Kinematics models enable:
  • Pose estimation - Calculate position from wheel encoders
  • Velocity control - Convert desired motion to wheel speeds
  • Path following - Navigate using GPS or visual odometry
  • Differential drive - Control two-wheeled robots
  • Ackermann steering - Model car-like vehicles

Coordinate Systems

Pose Representation

class Pose2D:
    def __init__(self, x=0.0, y=0.0, angle=0.0):
        self.x = x        # horizontal position (meters)
        self.y = y        # vertical position (meters)  
        self.angle = angle  # orientation (radians)
From donkeycar/parts/kinematics.py:24-28.

Angle Normalization

def limit_angle(angle):
    """Limit angle to pi to -pi radians"""
    return math.atan2(math.sin(angle), math.cos(angle))
From donkeycar/parts/kinematics.py:11-15.

Unicycle Model

The unicycle model represents differential drive robots with two independently driven wheels.

Forward Kinematics

Convert wheel distances to pose:
from donkeycar.parts.kinematics import Unicycle

# Robot with 0.15m between wheels
unicycle = Unicycle(axle_length=0.15)

# Update with wheel encoder readings
left_distance = 0.5  # meters
right_distance = 0.6  # meters
timestamp = time.time()

# Returns: distance, velocity, x, y, angle,
#          x_velocity, y_velocity, angular_velocity, timestamp
result = unicycle.run(left_distance, right_distance, timestamp)

distance, velocity, x, y, angle, vx, vy, omega, ts = result
From donkeycar/parts/kinematics.py:303-408.

Implementation Details

The update equations from donkeycar/parts/kinematics.py:361-376:
# Changes from last update
delta_left_distance = left_distance - self.left_distance
delta_right_distance = right_distance - self.right_distance
delta_distance = (delta_left_distance + delta_right_distance) / 2
delta_angle = (delta_right_distance - delta_left_distance) / self.axle_length
delta_time = timestamp - self.timestamp

forward_velocity = delta_distance / delta_time
angle_velocity = delta_angle / delta_time

# New position and orientation
estimated_angle = limit_angle(self.pose.angle + delta_angle / 2)
x = self.pose.x + delta_distance * math.cos(estimated_angle)
y = self.pose.y + delta_distance * math.sin(estimated_angle)
angle = limit_angle(self.pose.angle + delta_angle)

Inverse Kinematics

Convert desired velocity to wheel speeds:
from donkeycar.parts.kinematics import InverseUnicycle

inverse = InverseUnicycle(
    axle_length=0.15,
    wheel_radius=0.0315,
    min_speed=0.1,
    max_speed=3.0
)

# Desired motion
forward_velocity = 1.0  # m/s
angular_velocity = 0.5  # rad/s

# Convert to wheel speeds
left_speed, right_speed, timestamp = inverse.run(
    forward_velocity, 
    angular_velocity
)
From donkeycar/parts/kinematics.py:415-458. The conversion formula from donkeycar/parts/kinematics.py:451-452:
left_linear_speed = forward_velocity - angular_velocity * self.axle_length / 2
right_linear_speed = forward_velocity + angular_velocity * self.axle_length / 2

Bicycle Model

The bicycle model represents car-like vehicles with Ackermann steering.

Forward Kinematics

from donkeycar.parts.kinematics import Bicycle

# Car with 0.3m wheelbase
bicycle = Bicycle(wheel_base=0.3)

# Update with encoder and steering
forward_distance = 1.0  # meters traveled
steering_angle = 0.2    # radians (left positive)
timestamp = time.time()

# Returns: distance, velocity, x, y, angle,
#          x_velocity, y_velocity, angular_velocity, timestamp  
result = bicycle.run(forward_distance, steering_angle, timestamp)
From donkeycar/parts/kinematics.py:31-163.

Angular Velocity

For car-like vehicles:
def bicycle_angular_velocity(wheel_base, forward_velocity, steering_angle):
    """Calculate angular velocity from steering angle"""
    return forward_velocity * math.sin(steering_angle) / wheel_base
From donkeycar/parts/kinematics.py:262-274.

Inverse Kinematics

Convert desired motion to steering angle:
from donkeycar.parts.kinematics import InverseBicycle

inverse = InverseBicycle(wheel_base=0.3)

# Desired motion
forward_velocity = 2.0  # m/s
angular_velocity = 0.3  # rad/s

# Convert to steering angle
velocity, steering_angle, timestamp = inverse.run(
    forward_velocity,
    angular_velocity
)
From donkeycar/parts/kinematics.py:169-208. The conversion from donkeycar/parts/kinematics.py:246-259:
def bicycle_steering_angle(wheel_base, forward_velocity, angular_velocity):
    """Calculate steering angle from angular velocity"""
    # Derivation from bicycle model:
    # angular_velocity = forward_velocity * tan(steering_angle) / wheel_base
    # steering_angle = atan(angular_velocity * wheel_base / forward_velocity)
    return limit_angle(math.asin(angular_velocity * wheel_base / forward_velocity))

Differential Drive Control

Convert steering and throttle to left/right wheel speeds:
from donkeycar.parts.kinematics import TwoWheelSteeringThrottle

# Create converter
converter = TwoWheelSteeringThrottle(steering_zero=0.01)

# Convert controls
throttle = 0.5   # -1 to 1
steering = -0.3  # -1 (left) to 1 (right)

left_throttle, right_throttle = converter.run(throttle, steering)
From donkeycar/parts/kinematics.py:656-677. The algorithm from donkeycar/parts/kinematics.py:645-653:
left_throttle = throttle
right_throttle = throttle

if steering < -steering_zero:
    left_throttle *= (1.0 + steering)
elif steering > steering_zero:
    right_throttle *= (1.0 - steering)

return left_throttle, right_throttle

Normalization

Steering Angle Normalization

Convert between radians and normalized -1 to 1:
from donkeycar.parts.kinematics import (
    NormalizeSteeringAngle,
    UnnormalizeSteeringAngle
)

# Configure for your vehicle
max_angle = math.pi / 4  # 45 degrees

# Radians to normalized
normalizer = NormalizeSteeringAngle(
    max_steering_angle=max_angle,
    steering_zero=0.01  # deadzone
)
normalized = normalizer.run(0.5)  # 0.5 radians -> normalized

# Normalized to radians  
unnormalizer = UnnormalizeSteeringAngle(
    max_steering_angle=max_angle,
    steering_zero=0.01
)
radians = unnormalizer.run(-0.5)  # -0.5 normalized -> radians
From donkeycar/parts/kinematics.py:521-597.

Angular Velocity Normalization

For bicycle (car-like) vehicles:
from donkeycar.parts.kinematics import (
    BicycleNormalizeAngularVelocity,
    BicycleUnnormalizeAngularVelocity
)

# Robot specifications
wheel_base = 0.3
max_velocity = 3.0
max_steering = math.pi / 4

# Normalize
normalizer = BicycleNormalizeAngularVelocity(
    wheel_base, max_velocity, max_steering
)
normalized = normalizer.run(1.5)  # rad/s -> -1 to 1

# Unnormalize
unnormalizer = BicycleUnnormalizeAngularVelocity(
    wheel_base, max_velocity, max_steering
)
rad_per_sec = unnormalizer.run(0.75)  # -1 to 1 -> rad/s
For unicycle (differential drive) vehicles:
from donkeycar.parts.kinematics import (
    UnicycleNormalizeAngularVelocity,
    UnicycleUnnormalizeAngularVelocity
)

wheel_radius = 0.0315
axle_length = 0.15
max_velocity = 3.0

# Normalize
normalizer = UnicycleNormalizeAngularVelocity(
    wheel_radius, axle_length, max_velocity
)

# Unnormalize
unnormalizer = UnicycleUnnormalizeAngularVelocity(
    wheel_radius, axle_length, max_velocity
)
From donkeycar/parts/kinematics.py:277-518.

Configuration

Measure your robot’s physical parameters:
# myconfig.py

# MEASURED ROBOT PROPERTIES
AXLE_LENGTH = 0.15      # Distance between left/right wheels (m)
WHEEL_BASE = 0.3        # Distance between front/back wheels (m)  
WHEEL_RADIUS = 0.0315   # Wheel radius (m)
MIN_SPEED = 0.1         # Minimum speed before stalling (m/s)
MAX_SPEED = 3.0         # Maximum speed at full throttle (m/s)
MAX_STEERING_ANGLE = math.pi / 4  # Maximum steering angle (rad)

# ENCODER
ENCODER_PPR = 20        # Pulses per revolution
MM_PER_TICK = WHEEL_RADIUS * 2 * math.pi * 1000 / ENCODER_PPR
From donkeycar/templates/cfg_path_follow.py:66-75.

Example: Path Following

Combine kinematics with path following:
import donkeycar as dk
from donkeycar.parts.kinematics import Bicycle, TwoWheelSteeringThrottle

V = dk.vehicle.Vehicle()

# Add kinematics
if cfg.DRIVE_TRAIN_TYPE.startswith("DC_TWO_WHEEL"):
    # Differential drive
    V.add(TwoWheelSteeringThrottle(),
          inputs=['throttle', 'steering'],
          outputs=['left/throttle', 'right/throttle'])
else:
    # Car-like vehicle with bicycle model
    bicycle = Bicycle(wheel_base=cfg.WHEEL_BASE)
    V.add(bicycle,
          inputs=['enc/distance', 'user/angle', 'timestamp'],
          outputs=['odom/distance', 'odom/velocity',
                   'pos/x', 'pos/y', 'pos/angle',
                   'vel/x', 'vel/y', 'vel/angular',
                   'timestamp'])

Utility Functions

Wheel Rotational Velocity

from donkeycar.parts.kinematics import wheel_rotational_velocity

wheel_radius = 0.0315  # meters
linear_speed = 2.0     # m/s

# Convert to rad/s
rad_per_sec = wheel_rotational_velocity(wheel_radius, linear_speed)
From donkeycar/parts/kinematics.py:600-610.

Unicycle Angular Velocity

from donkeycar.parts.kinematics import unicycle_angular_velocity

wheel_radius = 0.0315
axle_length = 0.15
left_velocity = 1.0
right_velocity = 1.2

omega = unicycle_angular_velocity(
    wheel_radius, axle_length,
    left_velocity, right_velocity
)
From donkeycar/parts/kinematics.py:463-492.

Best Practices

  1. Measure carefully - Accurate physical parameters are critical
  2. Calibrate encoders - Roll car exactly 1 meter, measure ticks
  3. Test in place - Verify rotation before translating
  4. Handle singularities - Check for division by zero
  5. Use small timesteps - Assumes linear motion between updates

Troubleshooting

Pose Drift

  • Verify encoder tick counts match actual distance
  • Check wheel slippage
  • Calibrate wheel radius and axle length

Incorrect Rotation

  • Verify axle_length measurement
  • Check encoder wiring (left/right swap)
  • Test steering angle sign convention

Unstable Control

  • Add deadzone for small velocities
  • Smooth encoder readings
  • Limit maximum angular velocity

Next Steps

Build docs developers (and LLMs) love