Skip to content
This repository was archived by the owner on May 1, 2024. It is now read-only.

Commit 7ac5758

Browse files
committed
restored INDI and LQR controller (disabled by default)
use dp_lat_controller params to force enable them
1 parent 931cc12 commit 7ac5758

File tree

7 files changed

+248
-2
lines changed

7 files changed

+248
-2
lines changed

README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ I have decided to make this side project open source for users who wish to:
3030

3131
By making this project open source, I hope to alleviate some of the frustration and complaints about not being able to access the dragonpilot source code.
3232

33-
I encourage users to consider purchasing a [comma 3](https://shop.comma.ai) for the best and up-to-date openpilot experience.
33+
I encourage users to consider purchasing a [comma 3x](https://shop.comma.ai) for the best and up-to-date openpilot experience.
3434

3535

3636
## Limitations
@@ -43,13 +43,14 @@ I encourage users to consider purchasing a [comma 3](https://shop.comma.ai) for
4343
* Services are not optimized for resource usage, and using all services may result in overheating issues.
4444
* Language files can only be generated in a PC due to missing Qt5 tools.
4545
* webjoystick is disabled as it requires additional python modules. (aiohttp and others)
46-
* Starting from August 7th, 2023, comma has removed ESP/GPS support from Pandas. You can find more details about this change in this [link](https://github.com/commaai/panda/commit/c66b98b2a67441faa4cfcd36c3c9d9f90474cd08).
46+
* Starting from August 7th, 2023, comma has removed ESP/GPS support from Panda. You can find more details about this change in this [link](https://github.com/commaai/panda/commit/c66b98b2a67441faa4cfcd36c3c9d9f90474cd08).
4747
* Going forward, I will focus solely on maintaining the safety aspects of the code, ensuring that vehicle support and safety declarations remain up to date.
4848
* For safety concern, End-to-End / vision only longitudinal control only available in 0.8.16 driving model.
4949

5050

5151
## Configuration
5252

53+
* For research purposes, the INDI and LQR lateral controllers have been restored. Please use the `dp_lat_controller` parameter to override the default controller (0 = DEFAULT, 1 = INDI, 2 = LQR).
5354
* If you are not a Comma Two device, you can use the `dp_no_fan_ctrl` parameter to disable fan-related detection and control.
5455

5556
=======================

common/params.cc

+1
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ std::unordered_map<std::string, uint32_t> keys = {
212212
{"dp_no_gps_ctrl", PERSISTENT},
213213
{"dp_no_fan_ctrl", PERSISTENT},
214214
{"dp_logging", PERSISTENT},
215+
{"dp_lat_controller", PERSISTENT},
215216
};
216217

217218
} // namespace

selfdrive/car/interfaces.py

+32
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from openpilot.selfdrive.controls.lib.drive_helpers import V_CRUISE_MAX, get_friction
1616
from openpilot.selfdrive.controls.lib.events import Events
1717
from openpilot.selfdrive.controls.lib.vehicle_model import VehicleModel
18+
from openpilot.common.params import Params
1819

1920
ButtonType = car.CarState.ButtonEvent.Type
2021
GearShifter = car.CarState.GearShifter
@@ -101,6 +102,13 @@ def get_params(cls, candidate: str, fingerprint: Dict[int, Dict[int, int]], car_
101102
ret = CarInterfaceBase.get_std_params(candidate)
102103
ret = cls._get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs)
103104

105+
# rick - override lat controller
106+
dp_lat_controller = int(Params().get("dp_lat_controller"))
107+
if dp_lat_controller == 1: # indi
108+
ret = cls.configure_indi_tune(ret.lateralTuning)
109+
elif dp_lat_controller == 2: # lqr
110+
ret = cls.configure_lqr_tune(ret.lateralTuning)
111+
104112
# Vehicle mass is published curb weight plus assumed payload such as a human driver; notCars have no assumed payload
105113
if not ret.notCar:
106114
ret.mass = ret.mass + STD_CARGO_KG
@@ -191,6 +199,30 @@ def configure_torque_tune(candidate, tune, steering_angle_deadzone_deg=0.0, use_
191199
tune.torque.latAccelOffset = 0.0
192200
tune.torque.steeringAngleDeadzoneDeg = steering_angle_deadzone_deg
193201

202+
@staticmethod
203+
def configure_lqr_tune(tune):
204+
tune.init('lqr')
205+
tune.lqr.scale = 1500.0
206+
tune.lqr.ki = 0.05
207+
tune.lqr.a = [0., 1., -0.22619643, 1.21822268]
208+
tune.lqr.b = [-1.92006585e-04, 3.95603032e-05]
209+
tune.lqr.c = [1., 0.]
210+
tune.lqr.k = [-110.73572306, 451.22718255]
211+
tune.lqr.l = [0.3233671, 0.3185757]
212+
tune.lqr.dcGain = 0.002237852961363602
213+
214+
@staticmethod
215+
def configure_indi_tune(tune):
216+
tune.init('indi')
217+
tune.indi.innerLoopGainBP = [0.]
218+
tune.indi.innerLoopGainV = [4.0]
219+
tune.indi.outerLoopGainBP = [0.]
220+
tune.indi.outerLoopGainV = [3.0]
221+
tune.indi.timeConstantBP = [0.]
222+
tune.indi.timeConstantV = [1.0]
223+
tune.indi.actuatorEffectivenessBP = [0.]
224+
tune.indi.actuatorEffectivenessV = [1.0]
225+
194226
@abstractmethod
195227
def _update(self, c: car.CarControl) -> car.CarState:
196228
pass

selfdrive/controls/controlsd.py

+6
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
from openpilot.selfdrive.controls.lib.latcontrol import LatControl, MIN_LATERAL_CONTROL_SPEED
2323
from openpilot.selfdrive.controls.lib.longcontrol import LongControl
2424
from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
25+
from openpilot.selfdrive.controls.lib.latcontrol_indi import LatControlINDI
26+
from openpilot.selfdrive.controls.lib.latcontrol_lqr import LatControlLQR
2527
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle, STEER_ANGLE_SATURATION_THRESHOLD
2628
from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque
2729
from openpilot.selfdrive.controls.lib.events import Events, ET
@@ -157,6 +159,10 @@ def __init__(self, sm=None, pm=None, can_sock=None, CI=None):
157159
self.LaC = LatControlAngle(self.CP, self.CI)
158160
elif self.CP.lateralTuning.which() == 'pid':
159161
self.LaC = LatControlPID(self.CP, self.CI)
162+
elif self.CP.lateralTuning.which() == 'indi':
163+
self.LaC = LatControlINDI(self.CP, self.CI)
164+
elif self.CP.lateralTuning.which() == 'lqr':
165+
self.LaC = LatControlLQR(self.CP, self.CI)
160166
elif self.CP.lateralTuning.which() == 'torque':
161167
self.LaC = LatControlTorque(self.CP, self.CI)
162168

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import math
2+
import numpy as np
3+
4+
from cereal import log
5+
from common.filter_simple import FirstOrderFilter
6+
from common.numpy_fast import clip, interp
7+
from common.realtime import DT_CTRL
8+
from selfdrive.controls.lib.latcontrol import LatControl
9+
10+
11+
class LatControlINDI(LatControl):
12+
def __init__(self, CP, CI):
13+
super().__init__(CP, CI)
14+
self.angle_steers_des = 0.
15+
16+
A = np.array([[1.0, DT_CTRL, 0.0],
17+
[0.0, 1.0, DT_CTRL],
18+
[0.0, 0.0, 1.0]])
19+
C = np.array([[1.0, 0.0, 0.0],
20+
[0.0, 1.0, 0.0]])
21+
22+
# Q = np.matrix([[1e-2, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 10.0]])
23+
# R = np.matrix([[1e-2, 0.0], [0.0, 1e3]])
24+
25+
# (x, l, K) = control.dare(np.transpose(A), np.transpose(C), Q, R)
26+
# K = np.transpose(K)
27+
K = np.array([[7.30262179e-01, 2.07003658e-04],
28+
[7.29394177e+00, 1.39159419e-02],
29+
[1.71022442e+01, 3.38495381e-02]])
30+
31+
self.speed = 0.
32+
33+
self.K = K
34+
self.A_K = A - np.dot(K, C)
35+
self.x = np.array([[0.], [0.], [0.]])
36+
37+
self._RC = (CP.lateralTuning.indi.timeConstantBP, CP.lateralTuning.indi.timeConstantV)
38+
self._G = (CP.lateralTuning.indi.actuatorEffectivenessBP, CP.lateralTuning.indi.actuatorEffectivenessV)
39+
self._outer_loop_gain = (CP.lateralTuning.indi.outerLoopGainBP, CP.lateralTuning.indi.outerLoopGainV)
40+
self._inner_loop_gain = (CP.lateralTuning.indi.innerLoopGainBP, CP.lateralTuning.indi.innerLoopGainV)
41+
42+
self.steer_filter = FirstOrderFilter(0., self.RC, DT_CTRL)
43+
self.reset()
44+
45+
@property
46+
def RC(self):
47+
return interp(self.speed, self._RC[0], self._RC[1])
48+
49+
@property
50+
def G(self):
51+
return interp(self.speed, self._G[0], self._G[1])
52+
53+
@property
54+
def outer_loop_gain(self):
55+
return interp(self.speed, self._outer_loop_gain[0], self._outer_loop_gain[1])
56+
57+
@property
58+
def inner_loop_gain(self):
59+
return interp(self.speed, self._inner_loop_gain[0], self._inner_loop_gain[1])
60+
61+
def reset(self):
62+
super().reset()
63+
self.steer_filter.x = 0.
64+
self.speed = 0.
65+
66+
def update(self, active, CS, VM, params, last_actuators, steer_limited, desired_curvature, desired_curvature_rate, llk):
67+
self.speed = CS.vEgo
68+
# Update Kalman filter
69+
y = np.array([[math.radians(CS.steeringAngleDeg)], [math.radians(CS.steeringRateDeg)]])
70+
self.x = np.dot(self.A_K, self.x) + np.dot(self.K, y)
71+
72+
indi_log = log.ControlsState.LateralINDIState.new_message()
73+
indi_log.steeringAngleDeg = math.degrees(self.x[0])
74+
indi_log.steeringRateDeg = math.degrees(self.x[1])
75+
indi_log.steeringAccelDeg = math.degrees(self.x[2])
76+
77+
steers_des = VM.get_steer_from_curvature(-desired_curvature, CS.vEgo, params.roll)
78+
steers_des += math.radians(params.angleOffsetDeg)
79+
indi_log.steeringAngleDesiredDeg = math.degrees(steers_des)
80+
81+
# desired rate is the desired rate of change in the setpoint, not the absolute desired curvature
82+
rate_des = VM.get_steer_from_curvature(-desired_curvature_rate, CS.vEgo, 0)
83+
indi_log.steeringRateDesiredDeg = math.degrees(rate_des)
84+
85+
if not active:
86+
indi_log.active = False
87+
self.steer_filter.x = 0.0
88+
output_steer = 0
89+
else:
90+
# Expected actuator value
91+
self.steer_filter.update_alpha(self.RC)
92+
self.steer_filter.update(last_actuators.steer)
93+
94+
# Compute acceleration error
95+
rate_sp = self.outer_loop_gain * (steers_des - self.x[0]) + rate_des
96+
accel_sp = self.inner_loop_gain * (rate_sp - self.x[1])
97+
accel_error = accel_sp - self.x[2]
98+
99+
# Compute change in actuator
100+
g_inv = 1. / self.G
101+
delta_u = g_inv * accel_error
102+
103+
# If steering pressed, only allow wind down
104+
if CS.steeringPressed and (delta_u * last_actuators.steer > 0):
105+
delta_u = 0
106+
107+
output_steer = self.steer_filter.x + delta_u
108+
109+
output_steer = clip(output_steer, -self.steer_max, self.steer_max)
110+
111+
indi_log.active = True
112+
indi_log.rateSetPoint = float(rate_sp)
113+
indi_log.accelSetPoint = float(accel_sp)
114+
indi_log.accelError = float(accel_error)
115+
indi_log.delayedOutput = float(self.steer_filter.x)
116+
indi_log.delta = float(delta_u)
117+
indi_log.output = float(output_steer)
118+
indi_log.saturated = self._check_saturation(self.steer_max - abs(output_steer) < 1e-3, CS, steer_limited)
119+
120+
return float(output_steer), float(steers_des), indi_log
+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import math
2+
import numpy as np
3+
4+
from common.numpy_fast import clip
5+
from common.realtime import DT_CTRL
6+
from cereal import log
7+
from selfdrive.controls.lib.latcontrol import LatControl
8+
9+
10+
class LatControlLQR(LatControl):
11+
def __init__(self, CP, CI):
12+
super().__init__(CP, CI)
13+
self.scale = CP.lateralTuning.lqr.scale
14+
self.ki = CP.lateralTuning.lqr.ki
15+
16+
self.A = np.array(CP.lateralTuning.lqr.a).reshape((2, 2))
17+
self.B = np.array(CP.lateralTuning.lqr.b).reshape((2, 1))
18+
self.C = np.array(CP.lateralTuning.lqr.c).reshape((1, 2))
19+
self.K = np.array(CP.lateralTuning.lqr.k).reshape((1, 2))
20+
self.L = np.array(CP.lateralTuning.lqr.l).reshape((2, 1))
21+
self.dc_gain = CP.lateralTuning.lqr.dcGain
22+
23+
self.x_hat = np.array([[0], [0]])
24+
self.i_unwind_rate = 0.3 * DT_CTRL
25+
self.i_rate = 1.0 * DT_CTRL
26+
27+
self.reset()
28+
29+
def reset(self):
30+
super().reset()
31+
self.i_lqr = 0.0
32+
33+
def update(self, active, CS, VM, params, last_actuators, steer_limited, desired_curvature, desired_curvature_rate, llk):
34+
# def update(self, active, CS, CP, VM, params, last_actuators, desired_curvature, desired_curvature_rate):
35+
lqr_log = log.ControlsState.LateralLQRState.new_message()
36+
37+
torque_scale = (0.45 + CS.vEgo / 60.0)**2 # Scale actuator model with speed
38+
39+
# Subtract offset. Zero angle should correspond to zero torque
40+
steering_angle_no_offset = CS.steeringAngleDeg - params.angleOffsetAverageDeg
41+
42+
desired_angle = math.degrees(VM.get_steer_from_curvature(-desired_curvature, CS.vEgo, params.roll))
43+
44+
instant_offset = params.angleOffsetDeg - params.angleOffsetAverageDeg
45+
desired_angle += instant_offset # Only add offset that originates from vehicle model errors
46+
lqr_log.steeringAngleDesiredDeg = desired_angle
47+
48+
# Update Kalman filter
49+
angle_steers_k = float(self.C.dot(self.x_hat))
50+
e = steering_angle_no_offset - angle_steers_k
51+
self.x_hat = self.A.dot(self.x_hat) + self.B.dot(CS.steeringTorqueEps / torque_scale) + self.L.dot(e)
52+
53+
if not active:
54+
lqr_log.active = False
55+
lqr_output = 0.
56+
output_steer = 0.
57+
self.reset()
58+
else:
59+
lqr_log.active = True
60+
61+
# LQR
62+
u_lqr = float(desired_angle / self.dc_gain - self.K.dot(self.x_hat))
63+
lqr_output = torque_scale * u_lqr / self.scale
64+
65+
# Integrator
66+
if CS.steeringPressed:
67+
self.i_lqr -= self.i_unwind_rate * float(np.sign(self.i_lqr))
68+
else:
69+
error = desired_angle - angle_steers_k
70+
i = self.i_lqr + self.ki * self.i_rate * error
71+
control = lqr_output + i
72+
73+
if (error >= 0 and (control <= self.steer_max or i < 0.0)) or \
74+
(error <= 0 and (control >= -self.steer_max or i > 0.0)):
75+
self.i_lqr = i
76+
77+
output_steer = lqr_output + self.i_lqr
78+
output_steer = clip(output_steer, -self.steer_max, self.steer_max)
79+
80+
lqr_log.steeringAngleDeg = angle_steers_k
81+
lqr_log.i = self.i_lqr
82+
lqr_log.output = output_steer
83+
lqr_log.lqrOutput = lqr_output
84+
lqr_log.saturated = self._check_saturation(self.steer_max - abs(output_steer) < 1e-3, CS, steer_limited)
85+
return output_steer, desired_angle, lqr_log

selfdrive/manager/manager.py

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def manager_init() -> None:
5151
("dp_no_fan_ctrl", "0"),
5252
("dp_logging", "1"),
5353
("dp_0813", "1"),
54+
("dp_lat_controller", "0"),
5455
]
5556
if not PC:
5657
default_params.append(("LastUpdateTime", datetime.datetime.utcnow().isoformat().encode('utf8')))

0 commit comments

Comments
 (0)