# OpenPilot Steering Control Strategies: Angle vs Torque vs PID Explained

> Explore OpenPilot's steering control: Angle vs Torque vs PID. Learn how OpenPilot automatically selects the optimal strategy based on your vehicle's EPS hardware for enhanced control.

- Repository: [comma.ai/openpilot](https://github.com/commaai/openpilot)
- Tags: deep-dive
- Published: 2026-03-05

---

**OpenPilot supports three distinct lateral steering control strategies—Angle, Torque, and PID—which are selected automatically at startup based on the vehicle's EPS hardware capabilities defined in `CarParams.lateralControlMethod`.**

The commaai/openpilot repository implements multiple lateral control algorithms to accommodate different vehicle electronic power steering (EPS) systems. Each strategy represents a different approach to commanding the steering actuator, ranging from direct angle requests to closed-loop torque control. Understanding how these steering control strategies work and how OpenPilot chooses between them is essential for debugging vehicle-specific tuning issues.

## The Three Steering Control Strategies in OpenPilot

OpenPilot's lateral control architecture provides three concrete implementations of the base `LatControl` class, each targeting different steering command interfaces exposed by vehicle firmware.

### Angle Control (Direct Steering Wheel Commands)

The **Angle** strategy commands a specific **steering wheel angle** in degrees, functioning as a direct position controller. Implemented in [`selfdrive/controls/lib/latcontrol_angle.py`](https://github.com/commaai/openpilot/blob/main/selfdrive/controls/lib/latcontrol_angle.py), this controller calculates the target angle from the desired curvature and applies a saturation check based on the measured angle difference.

```python

# In a running OpenPilot loop

active = True                     # vehicle is enabled

angle_steer, angle_log = latcontrol.update(active, CS, VM, params,
                                          steer_limited_by_safety,
                                          desired_curvature,
                                          curvature_limited,
                                          lat_delay)

# `angle_steer` is the target steering wheel angle in degrees.

```

This method is selected for vehicles that expose direct angle control interfaces, such as some newer Tesla models, allowing the EPS to handle the low-level motor control internally.

### Torque Control (PID on Torque)

The **Torque** strategy sends a **steering torque** command in Newton-meters (Nm) to the EPS. Implemented in [`selfdrive/controls/lib/latcontrol_torque.py`](https://github.com/commaai/openpilot/blob/main/selfdrive/controls/lib/latcontrol_torque.py), this approach uses a classic `PIDController` that computes a torque value to correct steering-angle error while respecting vehicle-specific torque limits.

```python
torque, angle_des, torque_log = latcontrol.update(
    active, CS, VM, params,
    steer_limited_by_safety,
    desired_curvature,
    curvature_limited,
    lat_delay)

# `torque` (Nm) is sent to the vehicle's EPS.

```

According to the openpilot source code, this is the preferred method for many Toyota, Nissan, and Hyundai models that accept direct torque requests rather than angle setpoints.

### PID Control (Angle-Based with Torque Output)

The **PID** strategy implements a **PID loop on steering angle** that outputs a torque command. Implemented in [`selfdrive/controls/lib/latcontrol_pid.py`](https://github.com/commaai/openpilot/blob/main/selfdrive/controls/lib/latcontrol_pid.py), this controller computes a torque feed-forward term from the desired curvature, then feeds the angle error back through a PID controller, capping the final output by the maximum allowed torque.

```python
torque, angle_des, pid_log = latcontrol.update(
    active, CS, VM, params,
    steer_limited_by_safety,
    desired_curvature,
    curvature_limited,
    lat_delay)

# The PID controller (`PIDController`) computes `torque`.

```

This serves as the most widely compatible fallback method, converting angle errors into torque requests for EPS systems that do not support the other two native interfaces.

## How OpenPilot Selects the Steering Controller

The steering control strategy selection occurs during controller initialization through a **factory pattern** that inspects vehicle-specific parameters populated during the fingerprinting process.

### CarParams and the lateralControlMethod Field

The selection logic resides in [`selfdrive/controls/controlsd.py`](https://github.com/commaai/openpilot/blob/main/selfdrive/controls/controlsd.py), where `CP.lateralControlMethod` (populated from [`selfdrive/car/interfaces.py`](https://github.com/commaai/openpilot/blob/main/selfdrive/car/interfaces.py) during vehicle fingerprinting) determines which concrete class to instantiate:

```python

# controlsd.py (excerpt)

if CP.lateralControlMethod == "torque":
    self.latcontrol = LatControlTorque(CP, CI, DT)
elif CP.lateralControlMethod == "angle":
    self.latcontrol = LatControlAngle(CP, CI, DT)
else:                     # default → pid

    self.latcontrol = LatControlPID(CP, CI, DT)

```

The `CP.lateralTuning.which()` method returns the string identifier `"pid"`, `"torque"`, or `"angle"` based on the vehicle's declared capabilities in its fingerprint.

### Hardware Capability Mapping

- **Torque**: Assigned to vehicles with torque-based EPS interfaces (many Toyota, Nissan, and Hyundai models)
- **Angle**: Assigned to vehicles supporting direct angle commands (some newer Teslas)
- **PID**: Default fallback for all other supported platforms that require angle-error-based torque derivation

Thus, the **hardware capability** of the specific vehicle model determines which steering control strategy OpenPilot uses, with the selection made automatically during startup based on the parsed `CarParams`.

## Implementation Details and Code Examples

Each controller follows a consistent interface defined in [`selfdrive/controls/lib/latcontrol.py`](https://github.com/commaai/openpilot/blob/main/selfdrive/controls/lib/latcontrol.py), implementing an `update()` method that receives the current vehicle state, desired curvature, and various safety limits.

The factory instantiation in [`controlsd.py`](https://github.com/commaai/openpilot/blob/main/controlsd.py) (approximately lines 165-188) ensures that only one controller remains active throughout the drive, preventing strategy switching while the vehicle is enabled. This design allows the specific tuning parameters for each strategy—stored in `CP.lateralTuning`—to remain isolated and vehicle-specific.

## Summary

- **Three strategies**: OpenPilot implements Angle (direct position), Torque (PID on force), and PID (angle-error feedback) controllers in [`latcontrol_angle.py`](https://github.com/commaai/openpilot/blob/main/latcontrol_angle.py), [`latcontrol_torque.py`](https://github.com/commaai/openpilot/blob/main/latcontrol_torque.py), and [`latcontrol_pid.py`](https://github.com/commaai/openpilot/blob/main/latcontrol_pid.py) respectively.
- **Automatic selection**: The `CarParams.lateralControlMethod` field, populated during vehicle fingerprinting in [`selfdrive/car/interfaces.py`](https://github.com/commaai/openpilot/blob/main/selfdrive/car/interfaces.py), determines which strategy to instantiate.
- **Factory pattern**: [`selfdrive/controls/controlsd.py`](https://github.com/commaai/openpilot/blob/main/selfdrive/controls/controlsd.py) uses a simple conditional factory to create the appropriate controller at runtime based on the vehicle's EPS capabilities.
- **Hardware-dependent**: Torque control is preferred for compatible EPS systems, Angle for direct-actuation systems, and PID serves as the universal fallback.

## Frequently Asked Questions

### What determines which steering control strategy my car uses in OpenPilot?

Your vehicle's specific **EPS hardware capabilities** determine the strategy. During the fingerprinting process, OpenPilot reads the car's `CarParams` definition in [`selfdrive/car/interfaces.py`](https://github.com/commaai/openpilot/blob/main/selfdrive/car/interfaces.py) to check `lateralControlMethod`. Cars with direct torque interfaces receive the Torque controller, those with angle actuation receive the Angle controller, and all others default to the PID controller.

### Can I manually switch between Angle, Torque, and PID controllers?

No. The controller selection is **hardcoded per vehicle model** in the source code and determined at startup. While you could theoretically modify [`selfdrive/car/interfaces.py`](https://github.com/commaai/openpilot/blob/main/selfdrive/car/interfaces.py) to change `lateralControlMethod` for your specific fingerprint, this would likely result in unsafe steering behavior unless your EPS actually supports the alternative command type.

### Why does the Torque controller use PID while the PID controller also exists?

The **Torque** controller ([`latcontrol_torque.py`](https://github.com/commaai/openpilot/blob/main/latcontrol_torque.py)) runs a PID loop calculating the optimal torque to apply based on steering angle error. The **PID** controller ([`latcontrol_pid.py`](https://github.com/commaai/openpilot/blob/main/latcontrol_pid.py)) is an older compatibility layer that also runs a PID loop on angle error but was designed for vehicles where the Torque interface wasn't yet reverse-engineered. Torque control generally provides better performance when the EPS accepts direct torque commands.

### Where are the steering control strategies defined in the codebase?

The concrete implementations reside in [`selfdrive/controls/lib/latcontrol_angle.py`](https://github.com/commaai/openpilot/blob/main/selfdrive/controls/lib/latcontrol_angle.py), [`selfdrive/controls/lib/latcontrol_torque.py`](https://github.com/commaai/openpilot/blob/main/selfdrive/controls/lib/latcontrol_torque.py), and [`selfdrive/controls/lib/latcontrol_pid.py`](https://github.com/commaai/openpilot/blob/main/selfdrive/controls/lib/latcontrol_pid.py). The selection logic and factory instantiation occur in [`selfdrive/controls/controlsd.py`](https://github.com/commaai/openpilot/blob/main/selfdrive/controls/controlsd.py), while the vehicle-specific assignments are defined in [`selfdrive/car/interfaces.py`](https://github.com/commaai/openpilot/blob/main/selfdrive/car/interfaces.py) within each car's `CarParams` configuration.