Files
IQ.Pilot/iqdbc_repo/iqdbc/lvbs/car/tesla/torque_blend.py
2026-08-22 23:42:42 -05:00

313 lines
14 KiB
Python

"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
import math
import numpy as np
from collections import namedtuple
from dataclasses import replace
from iqdbc.car import structs, rate_limit, DT_CTRL
from iqdbc.car.vehicle_model import VehicleModel
from iqdbc.car.lateral import apply_steer_angle_limits_vm
from iqdbc.car.tesla.values import CarControllerParams
from iqdbc.lvbs.car.tesla.values import TeslaFlagsIQ
DT_LAT_CTRL = DT_CTRL * CarControllerParams.STEER_STEP
class TorqueBlendParams(CarControllerParams):
ANGLE_LIMITS = replace(CarControllerParams.ANGLE_LIMITS, MAX_ANGLE_RATE=5)
STEERING_DEG_PHASE_LEAD_COEFF = 8.0
# angle override # todo implement steering torque inertia compensation to increase gains
STEER_OVERRIDE_MIN_TORQUE = 0.5 # Nm - based on typical steering bias + noise
STEER_OVERRIDE_MAX_TORQUE = 2.5 # Nm max torque before EPS disengages, LKAS takes over at 1.8Nm
STEER_OVERRIDE_MAX_LAT_ACCEL = 1.5 # m/s^2 - determines angle rate - speed dependent - similar to Tesla comfort steering mode
STEER_OVERRIDE_LAT_ACCEL_GAIN_LIMIT = 10 # deg/Nm stability and smoothness for angle control # todo this could be increased after solving feedback stability
# angle ramping
STEER_OVERRIDE_MAX_LAT_JERK = 2.0 # m/s^3 - determines angle ramping rate - speed dependent
STEER_OVERRIDE_MAX_LAT_JERK_CENTERING = TorqueBlendParams.ANGLE_LIMITS.MAX_LATERAL_JERK # m/s^3 - for low speed angle ramp down
# stability and smoothness for angle ramp control - at very low speeds this takes precedence over jerk settings
STEER_OVERRIDE_LAT_JERK_GAIN_LIMIT = 100 # deg/s/Nm - should be less than CarControllerParams.ANGLE_LIMITS.MAX_ANGLE_RATE / DT_CTRL / STEER_OVERRIDE_TORQUE_RANGE
STEER_OVERRIDE_TORQUE_RANGE = STEER_OVERRIDE_MAX_TORQUE - STEER_OVERRIDE_MIN_TORQUE
# model fighting mitigation
STEER_DESIRED_LIMITER_ALLOW_SPEED = 6.0 # m/s - below this speed the desired angle limiter is active
STEER_DESIRED_LIMITER_ACCEL = 100 # deg/s^2 when override angle ramp is active
STEER_DESIRED_LIMITER_OVERRIDE_ACTIVE_COUNTER = 0.7 # second
# limit model acceleration when engaging
STEER_RESUME_RATE_LIMIT_RAMP_RATE = 500 # deg/s^2 - controls rate of rise of angle rate limit, not angle directly
TorqueBlendDataIQ = namedtuple("TorqueBlendDataIQ",
["steeringAngleDeg", "lat_active", "control_type"])
def get_steer_from_lat_accel(lat_accel, v_ego: float, VM: VehicleModel):
"""Calculate the maximum steering angle based on lateral acceleration."""
curvature = lat_accel / (max(1, v_ego) ** 2) # 1/m
return math.degrees(VM.get_steer_from_curvature(curvature, v_ego, 0)) # deg
def apply_bounds(signal: float, limit: float) -> float:
"""Limit input to a range."""
return float(np.clip(signal, -limit, limit))
def apply_deadzone(signal: float, deadzone: float) -> float:
"""Apply deadzone to input."""
return signal - apply_bounds(signal, deadzone)
def calc_override_angle_limited(torque: float, vEgo: float, VM: VehicleModel, lat_accel) -> float:
"""
Map driver torque to lateral acceleration and convert to steering angle.
Limit gain for stability with EPS and torque sensor interaction.
"""
# lateral accel is linear in respect to angle so it's fine to interpolate it with torque
torque_to_angle = get_steer_from_lat_accel(lat_accel, vEgo, VM) / STEER_OVERRIDE_TORQUE_RANGE
# limit the gain to prevent jerkiness and instability
gain_limit = STEER_OVERRIDE_LAT_ACCEL_GAIN_LIMIT
override_angle_target = torque * min(torque_to_angle, gain_limit)
return override_angle_target
def calc_override_angle_delta_limited(torque: float, vEgo: float, VM: VehicleModel, lat_jerk) -> float:
"""
Map driver torque to lateral jerk and convert to steering speed.
Limit gain for stability with EPS and torque sensor interaction.
"""
# prevents windup in carcontroller rate limiter
lat_jerk = min(lat_jerk, TorqueBlendParams.ANGLE_LIMITS.MAX_LATERAL_JERK)
# lateral accel is linear in respect to angle so it's fine to interpolate it with torque
torque_to_angle = get_steer_from_lat_accel(lat_jerk, vEgo, VM) / STEER_OVERRIDE_TORQUE_RANGE
# limit the gain to prevent jerkiness and instability
gain_limit = min(STEER_OVERRIDE_LAT_JERK_GAIN_LIMIT, CarControllerParams.ANGLE_LIMITS.MAX_ANGLE_RATE / DT_CTRL / STEER_OVERRIDE_TORQUE_RANGE)
override_angle_rate = torque * min(torque_to_angle, gain_limit)
# prevent windup in angle rate limiter
return apply_bounds(override_angle_rate * DT_LAT_CTRL, TorqueBlendParams.ANGLE_LIMITS.MAX_ANGLE_RATE)
class SteerRateLimiter:
"""Handles rate limiting of steering angle changes with a configurable rate."""
def __init__(self):
self._last = 0.0
def reset(self, angle: float) -> None:
"""Reset the rate limiter state with the given angle."""
self._last = angle
def update(self, angle: float, angle_delta_lim: float) -> float:
angle_lim = rate_limit(angle, self._last, -angle_delta_lim, angle_delta_lim)
self._last = angle_lim
return angle_lim
class SteerAccelLimiter:
"""
Second-order limiter for steering angle:
- Limits angular acceleration (change in allowed angular rate).
- Enforces a hard max angular rate.
"""
def __init__(self):
self.delta_rl = SteerRateLimiter()
self.angle_cmd = 0.0
def reset(self, angle: float) -> None:
self.delta_rl.reset(0)
self.angle_cmd = angle
def update(self, angle_target: float, max_rate: float, accel: float, decel: float, dt: float) -> float:
if dt <= 0.0:
return self.angle_cmd
# acceleration limits per update step
accel_delta = max(0.0, accel) * (dt * dt)
decel_delta = max(0.0, decel) * (dt * dt)
err = angle_target - self.angle_cmd
err = apply_bounds(err, max(0.0, max_rate) * dt)
# acceleration (towards target) or deceleration (away from target)
if err * self.delta_rl._last < 0:
delta = decel_delta
else:
delta = accel_delta
# Handle large decel (enabled with inf value)
if decel == np.inf and err * self.delta_rl._last < 0:
# if output crosses the target or target crosses the output
self.delta_rl._last = 0
angle_out = self.angle_cmd
else:
self.delta_rl._last = self.delta_rl.update(err, delta)
if decel == np.inf:
# if we are close to target, snap to it before we cross it
self.delta_rl._last = apply_bounds(self.delta_rl._last, abs(err))
angle_out = self.angle_cmd + self.delta_rl._last
# Integrate
self.angle_cmd = angle_out
return angle_out
class TorqueBlendController:
def __init__(self):
self.coop_apply_angle_last = 0
self.blend_apply_angle_last_sat = 0
self.override_angle_accu = 0
self.override_active_counter = 0 # Counter for how many cycles torque is below threshold
self.resume_rate_limiter_delta = SteerRateLimiter()
self.resume_rate_limiter = SteerRateLimiter()
self.override_accel_rate_limiter = SteerAccelLimiter()
self.debug_angle_desired_limited = 0
def apply_override_angle_direct(self, lat_active: bool, driverTorque: float, vEgo: float, VM: VehicleModel) -> float:
"""
Emulates steering springiness based on lateral acceleration exerted on the steering rack.
We rely on apply_override_angle_ramp to reach the max angle at low speeds.
At low speed lateral acceleration approaches infinity and it is not good proxy
for the torque to target angle conversion and needs to be limited
"""
if not lat_active:
return 0.0
## torque to position
# ignore torque sensor offset and disturbances
steering_torque_with_deadzone = apply_deadzone(driverTorque, STEER_OVERRIDE_MIN_TORQUE)
angle_override = calc_override_angle_limited(steering_torque_with_deadzone, vEgo, VM, STEER_OVERRIDE_MAX_LAT_ACCEL)
return angle_override
def apply_override_angle_relative(self, lat_active: bool, driverTorque: float, vEgo: float,
VM: VehicleModel, unwind_weight: float = 1.0) -> float:
"""
Converts steering torque to steering rotation rate.
Physically angle rate is related to viscous damping of tires rotating on the ground.
Here, however, the angle rate target is obtained from lateral jerk limit
as a reasonable safe rate which decays quadratically with vehicle speed.
"""
if not lat_active:
self.override_angle_accu = 0
return 0
# unwind accumulator toward zero if the previous loop saturated (apply_steer_angle_limits_vm)
unwind = (self.coop_apply_angle_last - self.blend_apply_angle_last_sat) * unwind_weight
if self.override_angle_accu * unwind > 0:
unwind = apply_bounds(unwind, abs(self.override_angle_accu))
self.override_angle_accu -= unwind
# torque biasing emulates the steering centering when released:
if self.override_angle_accu > 0 and abs(vEgo) > 0.1:
torque_biased = driverTorque - STEER_OVERRIDE_MIN_TORQUE
elif self.override_angle_accu < 0 and abs(vEgo) > 0.1:
torque_biased = driverTorque + STEER_OVERRIDE_MIN_TORQUE
else:
# when override_angle_accu is reset this turns off everything
torque_biased = apply_deadzone(driverTorque, STEER_OVERRIDE_MIN_TORQUE)
# higher rate when centering
angle_override_delta = calc_override_angle_delta_limited(torque_biased, vEgo, VM,
STEER_OVERRIDE_MAX_LAT_JERK if (torque_biased * self.override_angle_accu) > 0
else STEER_OVERRIDE_MAX_LAT_JERK_CENTERING)
# ramp the angle
new_override_angle_accu = self.override_angle_accu + angle_override_delta
# snap to 0 if sign changes and driver torque is steering centering zone
if (new_override_angle_accu * self.override_angle_accu) < 0 and abs(driverTorque) < STEER_OVERRIDE_MIN_TORQUE:
new_override_angle_accu = 0
self.override_angle_accu = new_override_angle_accu
return self.override_angle_accu
def apply_override_angle_combined(self, lat_active: bool, driverTorque: float, vEgo: float, VM: VehicleModel) -> float:
"""
Combines direct and relative override angles based on direct angle override limitations (stability and practical range depending on vehicle speed).
Effectively vehicle-speed based transition.
"""
if not lat_active:
return 0
# calculate capability of direct angle override (fully active above ~36kph)
direct_override_capability = (calc_override_angle_limited(STEER_OVERRIDE_TORQUE_RANGE, vEgo, VM, STEER_OVERRIDE_MAX_LAT_ACCEL) /
get_steer_from_lat_accel(STEER_OVERRIDE_MAX_LAT_ACCEL, vEgo, VM))
angle_override_direct = self.apply_override_angle_direct(lat_active, driverTorque, vEgo, VM)
relative_weight = 1.0 - direct_override_capability
angle_override_relative = self.apply_override_angle_relative(lat_active, driverTorque, vEgo, VM,
unwind_weight=relative_weight)
return angle_override_direct * direct_override_capability + angle_override_relative * relative_weight
def overriding_steer_desired_accel_limit(self, lat_active: bool, apply_angle: float, vEgo: float, steeringTorque: float) -> float:
"""
Acceleration rate limiter - limits acceleration but allows for quick deceleration (no overshoot)
"""
if not lat_active:
self.override_accel_rate_limiter.reset(apply_angle)
return apply_angle
if abs(steeringTorque) >= STEER_OVERRIDE_MIN_TORQUE:
self.override_active_counter = 0
else:
self.override_active_counter += DT_LAT_CTRL
self.override_active_counter = min(self.override_active_counter, STEER_DESIRED_LIMITER_OVERRIDE_ACTIVE_COUNTER)
max_angle_rate = CarControllerParams.ANGLE_LIMITS.MAX_ANGLE_RATE / DT_LAT_CTRL # MAX_ANGLE_RATE is per frame units so convert to real rate
# this ensures no acceleration limit when override is disabled:
max_angle_accel = max_angle_rate / DT_LAT_CTRL # ensures max deceleration
if vEgo < STEER_DESIRED_LIMITER_ALLOW_SPEED:
# Interpolate between STEER_DESIRED_LIMITER_ACCEL and max_angle_accel based on counter progress
max_angle_accel = np.interp(
self.override_active_counter,
[0, STEER_DESIRED_LIMITER_OVERRIDE_ACTIVE_COUNTER],
[STEER_DESIRED_LIMITER_ACCEL, max_angle_accel]
)
# max_angle_rate / DT_LAT_CTRL ensures max deceleration
return self.override_accel_rate_limiter.update(apply_angle, max_angle_rate, max_angle_accel, np.inf, DT_LAT_CTRL)
def resume_steer_desired_rate_limit(self, lat_active: bool, apply_angle: float, steering_angle: float) -> float:
"""Limits steering wheel acceleration when resuming steering"""
if not lat_active:
# reset and bypass
self.resume_rate_limiter_delta.reset(0)
self.resume_rate_limiter.reset(steering_angle)
return steering_angle
angle_rate_delta_lim = self.resume_rate_limiter_delta.update(CarControllerParams.ANGLE_LIMITS.MAX_ANGLE_RATE,
STEER_RESUME_RATE_LIMIT_RAMP_RATE * DT_LAT_CTRL**2)
apply_angle_lim = self.resume_rate_limiter.update(apply_angle, angle_rate_delta_lim)
return apply_angle_lim
def update(self, apply_angle, lat_active, CP_IQ: structs.IQCarParams, CS: structs.CarState, VM: VehicleModel) -> TorqueBlendDataIQ:
# estimate real steering angle by adding rate to the tesla filtered angle
steeringAngleDegPhaseLead = CS.out.steeringAngleDeg + CS.out.steeringRateDeg / STEERING_DEG_PHASE_LEAD_COEFF
angle_coop_enabled = CP_IQ.flags & TeslaFlagsIQ.COOP_STEERING.value
# avoid sudden rotation on engagement
apply_angle = self.resume_steer_desired_rate_limit(lat_active, apply_angle, steeringAngleDegPhaseLead)
if angle_coop_enabled:
# apply_angle = self.overriding_steer_desired_accel_limit(lat_active, apply_angle, CS.out.vEgo, CS.out.steeringTorque)
self.debug_angle_desired_limited = apply_angle #! debug
apply_angle += self.apply_override_angle_combined(lat_active, CS.out.steeringTorque, CS.out.vEgo, VM)
# final rate limit - matching panda safety
self.coop_apply_angle_last = apply_angle
self.blend_apply_angle_last_sat = apply_steer_angle_limits_vm(apply_angle, self.blend_apply_angle_last_sat, CS.out.vEgoRaw,
CS.out.steeringAngleDeg, lat_active, TorqueBlendParams, VM)
return TorqueBlendDataIQ(self.blend_apply_angle_last_sat, lat_active, 1) # 1 = angle control