Files
2026-09-03 18:23:24 -05:00

275 lines
12 KiB
Python

import numpy as np
from iqdbc.car.common.filter_simple import FirstOrderFilter
from iqdbc.car.common.conversions import Conversions as CV
from iqdbc.car.common.pid import PIDController
from iqdbc.car import DT_CTRL
class LongControlJerk():
JERK_LIMIT_MIN = 0.5
JERK_LIMIT_MIN_NO_LEAD = 0.7
JERK_LIMIT_MAX = 5.0
FILTER_GAIN_DISTANCE = [10, 50]
FILTER_GAIN_DISTANCE_CHANGE = [0, 20]
FILTER_GAIN_MAX = 0.95
FILTER_GAIN_MIN = 0.75
FILTER_GAIN_NO_LEAD = 0.95
def __init__(self, dt=DT_CTRL):
self.dy_up = 0.
self.dy_down = 0.
self.jerk_up = 0.
self.jerk_down = 0.
self.dt = dt
self.accel_last = 0.
self.distance_last = 0.
self.jerk_limit_min = self.JERK_LIMIT_MIN_NO_LEAD
def update(self, enabled, override, distance, has_lead, accel, critical_state):
# jerk limits by accel change and distance are used to improve comfort while ensuring a fast enough car reaction
# override mechanics reminder:
# (1) sending accel = 0 and directly setting jerk to zero results in round about steady accel until harder accel pedal press -> lack of control
# (2) sending accel = 0 and allowing a high jerk results in a abrupt accel cut -> lack of comfort
if not enabled:
self.jerk_up = 0.
self.jerk_down = 0.
self.dy_up = 0.
self.dy_down = 0.
elif override:
self.jerk_up = self.JERK_LIMIT_MIN
self.jerk_down = self.JERK_LIMIT_MIN
self.dy_up = 0.
self.dy_down = 0.
elif critical_state: # force best car reaction
self.jerk_up = self.JERK_LIMIT_MAX
self.jerk_down = self.JERK_LIMIT_MAX
self.dy_up = 0.
self.dy_down = 0.
else:
jerk_limit_min_target = self.JERK_LIMIT_MIN if has_lead else self.JERK_LIMIT_MIN_NO_LEAD # jerk limit min base line
jerk_limit_min_delta = abs(self.JERK_LIMIT_MIN_NO_LEAD - self.JERK_LIMIT_MIN) * self.dt
if self.jerk_limit_min < jerk_limit_min_target:
self.jerk_limit_min = min(self.jerk_limit_min + jerk_limit_min_delta, jerk_limit_min_target)
elif self.jerk_limit_min > jerk_limit_min_target:
self.jerk_limit_min = max(self.jerk_limit_min - jerk_limit_min_delta, jerk_limit_min_target)
if has_lead:
distance_change = (self.distance_last - distance) / self.dt if 0 not in (self.distance_last, distance) else 0
filter_gain_dist = np.interp(distance, self.FILTER_GAIN_DISTANCE, [self.FILTER_GAIN_MAX, self.jerk_limit_min]) # gain by distance
filter_gain_dist_change = np.interp(abs(distance_change), self.FILTER_GAIN_DISTANCE_CHANGE, [self.jerk_limit_min, self.FILTER_GAIN_MAX]) # gain by distance change
filter_gain = max(filter_gain_dist, filter_gain_dist_change) # use highest gain
else:
filter_gain = self.FILTER_GAIN_NO_LEAD
j = (accel - self.accel_last) / self.dt
tgt_up = abs(j) if j > 0 else 0.
tgt_down = abs(j) if j < 0 else 0.
# how fast does the car react to acceleration
self.dy_up += filter_gain * (tgt_up - self.jerk_up - self.dy_up)
self.jerk_up += self.dt * self.dy_up
self.jerk_up = np.clip(self.jerk_up, self.jerk_limit_min, self.JERK_LIMIT_MAX)
# how fast does the car react to braking
self.dy_down += filter_gain * (tgt_down - self.jerk_down - self.dy_down)
self.jerk_down += self.dt * self.dy_down
self.jerk_down = np.clip(self.jerk_down, self.jerk_limit_min, self.JERK_LIMIT_MAX)
self.accel_last = accel
self.distance_last = distance
def get_jerk_up(self):
return self.jerk_up
def get_jerk_down(self):
return self.jerk_down
class LongControlLimit():
LOWER_LIMIT_FACTOR = 0.024
LOWER_LIMIT_MAX = LOWER_LIMIT_FACTOR * 8
LOWER_LIMIT_MIN = LOWER_LIMIT_FACTOR * 2
UPPER_LIMIT_FACTOR = 0.0625
UPPER_LIMIT_MAX = UPPER_LIMIT_FACTOR * 3
LIMIT_MIN = 0.
LIMIT_DISTANCE = [10, 100] # limit range
LIMIT_DISTANCE_CHANGE_DOWN = [0, 20] # high precision for worst case high speed approaching a stopped lead
LIMIT_DISTANCE_CHANGE_UP = [0, 5] # precisely follow an accelerating lead especially from stop
LIMIT_DISTANCE_CHANGE_UP_ACT = [0, 60]
DISTANCE_FILTER_RC = [0.15, 0.6] # smooth noisy distance signal for distant leads
DISTANCE_TIMEOUT = 1. # seconds
def __init__(self, dt=DT_CTRL):
self.upper_limit = self.LIMIT_MIN
self.lower_limit = self.LIMIT_MIN
self.dt = dt
self.distance_last = 0.
self.distance_filter = FirstOrderFilter(0.0, rc=self.DISTANCE_FILTER_RC[0], dt=dt, initialized=False)
self.distance_valid_timer = 0
def update(self, enabled: bool, speed: float, set_speed: float, distance: float, has_lead: bool, critical_state: bool):
# control limits by distance are used to improve comfort while ensuring precise car reaction if neccessary
# also used to reduce an effect of decel overshoot when target is breaking
# limits are controlled mainly by distance of lead car
if not enabled or critical_state: # force most precise accel command execution
self.upper_limit = self.LIMIT_MIN
self.lower_limit = self.LIMIT_MIN
self.distance_valid_timer = 0
elif not has_lead:
if self.distance_valid_timer < self.DISTANCE_TIMEOUT: # fluctuation block: keep alive
self.distance_valid_timer += self.dt
else: # force most precise
self.upper_limit = self.LIMIT_MIN
self.lower_limit = self.LIMIT_MIN
else:
distance_change_raw = (self.distance_last - distance) / self.dt if 0 not in (self.distance_last, distance) else 0
distance_filter_rc = np.interp(distance, self.LIMIT_DISTANCE, self.DISTANCE_FILTER_RC)
if (self.distance_last == 0 or self.distance_valid_timer != 0) and distance != 0: # for new lead detection reset filter and correctly force current state upon next iteration
self.distance_filter = FirstOrderFilter(0.0, rc=distance_filter_rc, dt=self.dt, initialized=False)
distance_change = distance_change_raw
else:
self.distance_filter.update_alpha(distance_filter_rc)
distance_change = self.distance_filter.update(distance_change_raw)
self.distance_valid_timer = 0
# how far can the true accel vary downwards from requested accel
upper_limit_dist = np.interp(distance, self.LIMIT_DISTANCE, [self.LIMIT_MIN, self.UPPER_LIMIT_MAX]) # base line based on distance
upper_limit_dist_change = np.interp(-min(0, distance_change), self.LIMIT_DISTANCE_CHANGE_UP, [self.UPPER_LIMIT_MAX, self.LIMIT_MIN]) # limit by distance change up
upper_limit_dist_change = np.interp(distance, self.LIMIT_DISTANCE_CHANGE_UP_ACT, [upper_limit_dist_change, upper_limit_dist]) # distance change activation
self.upper_limit = min(upper_limit_dist, upper_limit_dist_change) # use lowest limit
# how far can the true accel vary upwards from requested accel
set_speed_diff_up = max(0, abs(speed) - abs(set_speed)) # set speed difference down requested by user or speed overshoot (includes hud - real speed difference!)
set_speed_diff_up_factor = np.interp(set_speed_diff_up, [1, 1.75], [1., 0.]) # faster requested speed decrease and less speed overshoot downhill
lower_limit_dist = np.interp(distance, self.LIMIT_DISTANCE, [self.LOWER_LIMIT_MIN, self.LOWER_LIMIT_MAX]) # base line based on distance
lower_limit_dist_speed = lower_limit_dist * set_speed_diff_up_factor
lower_limit_dist_change = np.interp(max(0, distance_change), self.LIMIT_DISTANCE_CHANGE_DOWN, [self.LOWER_LIMIT_MAX, self.LIMIT_MIN]) # limit by distance change down
self.lower_limit = min(lower_limit_dist_speed, lower_limit_dist_change) # use lowest limit
self.distance_last = distance
def get_upper_limit(self):
return self.upper_limit
def get_lower_limit(self):
return self.lower_limit
def sigmoid_curvature_boost_meb(kappa: float, v_ego: float, kappa_thresh: float = 0.0) -> float:
# compensate non linear behaviour: boost low curvatures
# this is either a model issue (nerfing low curvatures) or a specific steering rack behaviour
v_points = np.array([20.0, 40.0])
boost_values = np.array([1.5, 2.1]) # increase boost amplitude with speed
boost = float(np.interp(v_ego, v_points, boost_values))
steepness_values = np.array([5000.0, 3200.0]) # increase boost area with speed
steepness = float(np.interp(v_ego, v_points, steepness_values))
abs_kappa = abs(kappa)
boost_factor = 1.0 + (boost - 1.0) / (1 + np.exp(steepness * (abs_kappa - kappa_thresh)))
return np.sign(kappa) * abs_kappa * boost_factor
def map_speed_to_acc_tempolimit(v_ms):
acc_tempolimit_kph = { # DBC Mapping
1: 5, 2: 7, 3: 10, 4: 15, 5: 20, 6: 25, 7: 30, 8: 35,
9: 40, 10: 45, 11: 50, 12: 55, 13: 60, 14: 65, 15: 70,
16: 75, 17: 80, 18: 85, 19: 90, 20: 95, 21: 100, 22: 110,
23: 120, 24: 130, 25: 140, 26: 150, 27: 160, 28: 200,
30: 250
}
v_kph = int(round(v_ms * CV.MS_TO_KPH))
acc_value = 0
for val, limit in sorted(acc_tempolimit_kph.items()):
if v_kph >= limit:
acc_value = val
else:
break
return acc_value
def get_acc_warning_meb(self, acc_hud):
# this works as long our radar does not fault while using OP
if (acc_hud["ACC_Status_ACC"] in (3, 4) # ACC active or in override mode
and acc_hud["ACC_EGO_Fahrzeug"] == 2 # a warning for the lead car is active
and acc_hud["ACC_Optischer_Fahrerhinweis"] != 0 # there is an optical warning
and acc_hud["ACC_Akustischer_Fahrerhinweis"] != 0 # there is a sound warning
and acc_hud["ACC_Display_Prio"] == 0): # this warning has highest priority
return True
return False
class MultiplicativeUnwindPID(PIDController):
def __init__(self, k_p, k_i, k_f=0., k_d=0., pos_limit=1e308, neg_limit=-1e308, rate=100, min_cmd=1e-10, ki_red_time=1.0):
super().__init__(k_p, k_i, k_f=k_f, k_d=k_d, pos_limit=pos_limit, neg_limit=neg_limit, rate=rate)
self.min_cmd = abs(min_cmd)
self.ki_red_time = float(ki_red_time)
self.rate = rate
self.override_prev = False
self.i_unwind_factor = 1.0
def _calc_unwind_factor(self, override):
if not override or self.override_prev:
return
if self.ki_red_time <= 0.0:
self.i_unwind_factor = 1.0
return
if abs(self.i) <= self.min_cmd:
self.i_unwind_factor = 0.0
return
steps = max(int(self.ki_red_time * self.rate), 1)
factor = (self.min_cmd / abs(self.i)) ** (1.0 / steps)
self.i_unwind_factor = min(factor, 1.0)
def update(self, error, error_rate=0.0, speed=0.0, override=False, feedforward=0., freeze_integrator=False):
self.speed = speed
self.p = float(error) * self.k_p
self.f = feedforward * self.k_f
self.d = error_rate * self.k_d
if override:
self._calc_unwind_factor(override)
self.i *= self.i_unwind_factor
if abs(self.i) < self.min_cmd:
self.i = 0.0
else:
if not freeze_integrator:
self.i = self.i + error * self.k_i * self.i_rate
# Clip i to prevent exceeding control limits
control_no_i = self.p + self.d + self.f
control_no_i = np.clip(control_no_i, self.neg_limit, self.pos_limit)
self.i = np.clip(self.i, self.neg_limit - control_no_i, self.pos_limit - control_no_i)
control = self.p + self.i + self.d + self.f
self.control = np.clip(control, self.neg_limit, self.pos_limit)
self.override_prev = override
return self.control
class LatControlCurvature():
def __init__(self, pid_params, limit, rate):
self.pid = MultiplicativeUnwindPID((pid_params.kpBP, pid_params.kpV),
(pid_params.kiBP, pid_params.kiV),
k_f=pid_params.kf, pos_limit=limit, neg_limit=-limit,
rate=rate, min_cmd=6.7e-6, ki_red_time=2.0)
def reset(self):
self.pid.reset()
def update(self, CS, CC, desired_curvature):
actual_curvature_vm = CC.currentCurvature # includes roll
speed_floor = max(CS.vEgo, 0.1)
yaw_rate_pose = CC.angularVelocity[2] if len(CC.angularVelocity) > 2 else actual_curvature_vm * speed_floor
actual_curvature_pose = yaw_rate_pose / speed_floor
actual_curvature = np.interp(CS.vEgo, [2.0, 5.0], [actual_curvature_vm, actual_curvature_pose])
desired_curvature_corr = desired_curvature - CC.rollCompensation
error = desired_curvature - actual_curvature
freeze_integrator = CC.steerLimited or CS.vEgo < 5
output_curvature = self.pid.update(error, feedforward=desired_curvature_corr, speed=CS.vEgo,
freeze_integrator=freeze_integrator, override=CS.steeringPressed)
return output_curvature