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

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, 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) 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:

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. 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:

_, _, 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:

  • slopelatAccelFactor: The conversion factor from steering torque to lateral acceleration
  • offsetlatAccelOffset: The bias term accounting for constant steering offsets
  • frictionCoefffrictionCoefficient: 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:

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】:

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:

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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →