# How the openpilot torqued Module Estimates and Compensates Steering Torque Feedback in Real-Time

> Discover how openpilot's torqued module estimates and compensates steering torque feedback in real-time using advanced least-squares regression and filtering for stable performance.

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

---

**The `torqued` module in openpilot continuously estimates the relationship between steering torque and lateral acceleration using total-least-squares regression on bucketed historical data, then filters and clips these parameters to provide stable real-time torque compensation.**

The `torqued` process runs continuously on the vehicle’s on-board computer as part of the commaai/openpilot stack. Its primary function is to measure how steering torque translates into lateral acceleration, computing live compensation parameters that reduce steering-wheel wobble and improve handling. These parameters are exposed through the `liveTorqueParameters` message for consumption by downstream control modules.

## Data Acquisition and Signal Buffering

The estimator begins by subscribing to critical CAN topics to gather raw vehicle dynamics. In [`selfdrive/locationd/torqued.py`](https://github.com/commaai/openpilot/blob/main/selfdrive/locationd/torqued.py), the `main()` function initializes a `SubMaster` that polls `livePose` alongside `carControl`, `carOutput`, `carState`, `liveCalibration`, and `liveDelay`【L48-L50】.

When messages arrive, the `TorqueEstimator.handle_log()` method appends key signals to de-queued buffers stored in `self.raw_points`. This includes timestamps, `lat_active` (lateral control status), `steer_torque`, `vEgo` (vehicle speed), and `steer_override` (driver interference flag)【L65-L78】. The buffer maintains a fixed-length history defined by `self.hist_len = HISTORY / DT_MDL` to ensure sufficient data for lag compensation【L54-L56】.

The module also applies measured lateral delay via `self.lag = msg.lateralDelay` from the `liveDelay` topic, accounting for communication and processing latency in the vehicle’s control loop【L80-L81】.

## Calculating Lateral Acceleration from Historical Torque

When the `livePose` buffer fills to `hist_len`, the estimator triggers a lateral acceleration calculation. The process first builds a calibrated pose using the `PoseCalibrator` class (from [`helpers.py`](https://github.com/commaai/openpilot/blob/main/helpers.py)) to correct for sensor-frame misalignment, then extracts yaw rate and vehicle roll.

The module interpolates driver-active flags, steer-override states, and vehicle speed across the historic window, then computes lateral acceleration using the kinematic relationship:

```python
lateral_acc = (vego * yaw_rate) - (np.sin(roll) * ACCELERATION_DUE_TO_GRAVITY)

```

Only samples meeting strict validity criteria are retained: lateral control must be active (`lat_active` true), the driver must not be overriding (`steer_override` false), speed must exceed `MIN_VEL`, torque magnitude must surpass `STEER_MIN_THRESHOLD`, and the calculated lateral acceleration must fall within `LAT_ACC_THRESHOLD`【L82-L100】. Valid points are added to the bucketed dataset via `self.filtered_points.add_point()`.

## Bucket-Based Data Organization

Rather than storing raw time-series data indefinitely, `torqued` organizes samples into `TorqueBuckets`, a subclass of `PointBuckets` defined in [`selfdrive/locationd/helpers.py`](https://github.com/commaai/openpilot/blob/main/selfdrive/locationd/helpers.py). This structure bins data by steering-torque range using `STEER_BUCKET_BOUNDS`, with each bucket retaining up to `POINTS_PER_BUCKET` recent points formatted as (torque, 1, lateral-acc)【L45-L52】.

The bucket system enables efficient outlier rejection and balanced dataset construction. The `is_valid()` method checks that each bucket contains at least `MIN_BUCKET_POINTS` and that the total point count exceeds `MIN_POINTS_TOTAL` before permitting parameter estimation【L70-L84】.

## SVD-Based Parameter Estimation

When `self.filtered_points.is_calculable()` returns true, the `estimate_params()` method executes a total-least-squares (TLS) regression via Singular Value Decomposition (SVD). This approach is robust to noise in both the torque and lateral-acceleration measurements:

```python
_, _, v = np.linalg.svd(points, full_matrices=False)
slope, offset = -v.T[0:2, 2] / v.T[2, 2]
_, spread = np.matmul(points[:, [0, 2]], slope2rot(slope)).T
friction_coeff = np.std(spread) * FRICTION_FACTOR

```

The TLS solution yields three critical parameters:
- **slope** → `latAccelFactor`: The conversion factor from steering torque to lateral acceleration
- **offset** → `latAccelOffset`: The bias term accounting for constant steering offsets
- **frictionCoeff** → `frictionCoefficient`: Derived from the standard deviation of the spread, representing hysteresis width【L46-L57】

## Real-Time Filtering and Safety Clipping

Raw estimates undergo aggressive smoothing via `FirstOrderFilter` instances stored in `self.filtered_params`. The filter uses a time-varying decay parameter that grows from `MIN_FILTER_DECAY` (50) to `MAX_FILTER_DECAY` (250) as more data accumulates, providing rapid initial convergence followed by steady-state stability:

```python
self.filtered_params[param].update(value)
self.filtered_params[param].update_alpha(self.decay)

```

After filtering, each parameter is clipped to sanity-checked ranges derived from the vehicle’s factory calibration (`offline_latAccelFactor`, `offline_friction`) and safety factors (`FACTOR_SANITY`, `FRICTION_SANITY`)【L59-L64】【L26-L30】【L92-L98】:

```python
latAccelFactor = np.clip(latAccelFactor,
                         self.min_lataccel_factor,
                         self.max_lataccel_factor)
frictionCoeff = np.clip(frictionCoeff,
                         self.min_friction,
                         self.max_friction)

```

## Publishing Live Torque Parameters

The `get_msg()` method constructs a `liveTorqueParameters` protobuf containing both raw estimates (for diagnostics) and filtered, clipped values (for control). The message includes `latAccelFactorFiltered`, `latAccelOffsetFiltered`, `frictionCoefficientFiltered`, along with metadata such as bucket counts, validity percentages, current decay values, and reset counters【L6-L40】【L12-L25】.

In the main execution loop, the module publishes this message at **4 Hz** by checking `if sm.frame % 5 == 0` before sending【L62-L66】.

## Parameter Persistence Across Restarts

To maintain calibration across system reboots, `torqued` caches the bucket points and filtered parameters every 60 seconds using the `Params` class. Data is stored under the key `"LiveTorqueParameters"`, allowing the estimator to resume from its previous state rather than relearning from scratch【L66-L70】.

## Using the TorqueEstimator in Custom Scripts

The following example demonstrates how to instantiate and feed the `TorqueEstimator` programmatically:

```python
from cereal import car
from openpilot.selfdrive.locationd.torqued import TorqueEstimator
from openpilot.common.params import Params
import messaging

# Load vehicle parameters (requires previous CarParams save)

params = Params()
car_params_msg = car.CarParams.from_bytes(params.get("CarParams", block=True))

# Initialize estimator

est = TorqueEstimator(car_params_msg)

# Feed mock messages (replace with real cereal messages in production)

def feed_example():
    t = 12345.0
    
    car_control_msg = messaging.new_message('carControl')
    car_control_msg.carControl.latActive = True
    est.handle_log(t, "carControl", car_control_msg)
    
    car_output_msg = messaging.new_message('carOutput')
    car_output_msg.carOutput.actuatorsOutput.torque = 0.1
    est.handle_log(t, "carOutput", car_output_msg)
    
    # Feed carState, livePose, liveDelay, liveCalibration similarly...

feed_example()

# Retrieve formatted message

live_msg = est.get_msg(valid=True, with_points=True)
print("Filtered latAccelFactor:", live_msg.liveTorqueParameters.latAccelFactorFiltered)
print("Friction coefficient:", live_msg.liveTorqueParameters.frictionCoefficientFiltered)

```

## Summary

- **Data ingestion**: The module buffers steering torque, vehicle speed, and pose data from CAN messages, applying a configurable lateral delay compensation.
- **Bucketed statistics**: Samples are organized into torque-range buckets to ensure balanced datasets for regression.
- **TLS estimation**: Total-least-squares via SVD extracts the torque-to-acceleration relationship while accounting for noise in both variables.
- **Adaptive filtering**: Exponential filters with variable decay smooth estimates, while hard clipping prevents dangerous parameter excursions.
- **Real-time publishing**: Filtered parameters broadcast at 4 Hz via `liveTorqueParameters`, with periodic caching to `Params` for persistence.

## Frequently Asked Questions

### What is the difference between raw and filtered torque parameters?

The **raw parameters** (`latAccelFactorRaw`, `frictionCoefficientRaw`) represent the immediate output of the SVD-based total-least-squares regression on the current bucketed data. The **filtered parameters** (`latAccelFactorFiltered`, etc.) pass through `FirstOrderFilter` instances that smooth rapid fluctuations and prevent jitter in the control loop. Downstream modules consume the filtered values for stable compensation.

### How does the torqued module handle driver override situations?

The estimator explicitly checks the `steer_override` flag from `carState` during the lateral acceleration calculation phase. When the driver is manually applying torque to the wheel, those samples are rejected and not added to the `filtered_points` buckets. This prevents driver inputs from corrupting the vehicle-specific torque-to-acceleration model.

### Why does the module use Total Least Squares instead of ordinary least squares?

Total Least Squares (TLS) via SVD accounts for measurement errors in **both** the steering torque (independent variable) and lateral acceleration (dependent variable). Ordinary Least Squares assumes perfect knowledge of the independent variable, which is unrealistic for noisy CAN signals. The TLS approach in `estimate_params()` produces more robust `latAccelFactor` and `latAccelOffset` values when both inputs contain uncertainty.

### How often does the torqued module update its parameter estimates?

The module publishes the `liveTorqueParameters` message at **4 Hz** (every 5 frames assuming a 20 Hz base rate). However, the underlying SVD estimation only occurs when `self.filtered_points.is_calculable()` returns true, meaning sufficient valid samples exist across the torque buckets. The exponential filters update continuously, allowing parameters to drift gradually toward new steady-state values rather than updating in discrete steps.