参考资料
1. 基本概念
从控制理论角度出发的讲解可以翻看博客。
1.1 运动学模型的离散状态方程
对于一个离散时间系统:
X
(
k
+
1
)
=
A
X
(
k
)
+
B
u
(
k
)
\mathbf{X}(k+1)=A \mathbf{X}(k)+B \mathbf{u}(k)
X(k+1)=AX(k)+Bu(k)
其中,
A
∈
R
n
×
n
A \in R^{n \times n}
A∈Rn×n ,
B
∈
R
n
×
m
B \in R^{n \times m}
B∈Rn×m。 关于最优问题,就在于如何选择合适的
u
0
,
u
1
,
…
u_{0}, u_{1}, \ldots
u0?,u1?,… ,使得状态量
x
0
,
x
1
,
…
x_{0}, x_{1}, \ldots
x0?,x1?,… 足够小,因此得到好的调节和控制;或者使得
u
0
,
u
1
,
…
u_{0}, u_{1}, \ldots
u0?,u1?,… 足够小,以使用更少的能量。这两个量通常相互制约,如果采用更大的输入
u
\mathbf{u}
u,就会驱使状态量更快达到0。
这是一个典型的多目标优化最优控制问题,为了表示控制系统达到稳定控制所付出的代价,定义如下二次型代价函数:
J
=
∑
k
=
1
N
(
X
T
Q
X
+
u
T
R
u
)
J=\sum_{k=1}^{N}\left(\mathbf{X}^{T} Q \mathbf{X}+\mathbf{u}^{T} R \mathbf{u}\right)
J=k=1∑N?(XTQX+uTRu)
其中,
Q
Q
Q为半正定的状态加权矩阵,
R
R
R为正定的控制加权矩阵,且两者通常取为对角阵;
Q
Q
Q矩阵元素变大意味着希望状态量
X
\mathbf{X}
X能够快速趋近于零;
R
R
R矩阵元素变大意味着希望控制输入能够尽可能小。
在轨迹跟踪中,前一项优化目标表示跟踪过程路径偏差的累积大小,第二项优化目标表示跟踪过程控制能量的损耗,这样就将轨迹跟踪控制问题转化为一个最优控制问题。
给定一个大小为
n
×
n
n\times n
n×n的实对称矩阵A ,若对于任意长度为
n
n
n的非零向量
x
x
x,有
x
T
A
x
>
0
x^TAx>0
xTAx>0恒成立,则矩阵A是一个正定矩阵。
对于上述目标函数的优化求解,使用线性二次型调节器 ( Linear-Quadratic Regulator),解出的最优控制规律u是关于状态变量
X
X
X的线性函数
u
=
[
?
(
R
+
B
T
P
B
)
?
1
B
T
P
A
]
X
=
K
X
\mathbf{u}=\left[-\left(R+B^{T} P B\right)^{-1} B^{T} P A\right] \mathbf{X}=K \mathbf{X}
u=[?(R+BTPB)?1BTPA]X=KX 其中,P是下述黎卡提方程的解 :
P
=
A
T
P
A
?
A
T
P
B
(
R
+
B
T
P
B
)
?
1
B
T
P
A
+
Q
P=A^{T} P A-A^{T} P B\left(R+B^{T} P B\right)^{-1} B^{T} P A+Q
P=ATPA?ATPB(R+BTPB)?1BTPA+Q
形如
y
′
=
P
(
x
)
y
2
+
Q
(
x
)
y
+
R
(
x
)
y'=P(x)y^2+Q(x)y+R(x)
y′=P(x)y2+Q(x)y+R(x)的非线性微分方程称为黎卡提方程。 针对黎卡提方程,可以采用循环迭代的思想求解P: 1)令等式右边的P_old=Q ; 2)计算等式右边的值为P_new 3)比较P_old 和P_new ,若两者的差值小于预设值, 则认为等式两边相等;否则再令P_old=P_new ,继续循环。 备注 AtsushiSakai 的PythonRobotics的代码就是这么求解的
1.2 LQR求解步骤
综上,采用LQR算法进行控制率求解的步骤概括为:
- 确定迭代范围
N
N
N
- 设置迭代初始值
P
N
=
Q
f
P_{N}=Q_{f}
PN?=Qf?,其中
Q
f
=
Q
Q_f=Q
Qf?=Q
- 循环迭代,
t
=
N
,
…
,
1
t=N, \ldots, 1
t=N,…,1
P
t
?
1
=
Q
+
A
T
P
t
A
?
A
T
P
t
B
(
R
+
B
T
P
t
B
)
?
1
B
T
P
t
A
P_{t-1}=Q+A^{T} P_{t} A-A^{T} P_{t} B\left(R+B^{T} P_{t} B\right)^{-1} B^{T} P_{t} A
Pt?1?=Q+ATPt?A?ATPt?B(R+BTPt?B)?1BTPt?A - 从
t
=
0
,
…
,
N
?
1
t=0, \ldots, N-1
t=0,…,N?1,循环计算反馈系数
K
t
=
?
(
R
+
B
T
P
t
+
1
B
)
?
1
B
T
P
t
+
1
A
K_{t}=-\left(R+B^{T} P_{t+1} B\right)^{-1} B^{T} P_{t+1} A
Kt?=?(R+BTPt+1?B)?1BTPt+1?A
- 最终得优化的控制量
u
t
?
=
K
t
X
t
u_{t}^{*}=K_{t} X_{t}
ut??=Kt?Xt?
2. python实现
2.1 车辆模型
我们以后轴中心为车辆中心的单车运动学模型为例,它的离散状态方程如下:
X
(
k
+
1
)
=
[
1
0
?
T
v
r
sin
?
ψ
r
0
1
T
v
r
cos
?
ψ
r
0
0
1
]
X
(
k
)
+
[
T
cos
?
ψ
r
0
T
sin
?
ψ
r
0
T
tan
?
ψ
r
L
v
r
T
L
cos
?
2
δ
r
]
u
(
k
)
=
A
X
(
k
)
+
B
u
(
k
)
\begin{aligned} \mathbf{X}(k+1)&= \left[\begin{array}{ccc} 1 & 0 & -T v_{r} \sin \psi_{r} \\ 0 & 1 & Tv_{r} \cos \psi_{r} \\ 0 & 0 & 1 \end{array}\right] \mathbf{X}(k)+\left[\begin{array}{ccc} T \cos \psi_{r} & 0 \\ T \sin \psi_{r} & 0 \\ \frac{T\tan \psi_{r}}{L} & \frac{v_{r}T}{L \cos ^{2} \delta_{r}} \end{array}\right]\mathbf{u}(k) \\ &=A \mathbf{X}(k)+B \mathbf{u}(k) \end{aligned}
X(k+1)?=???100?010??Tvr?sinψr?Tvr?cosψr?1????X(k)+???Tcosψr?Tsinψr?LTtanψr???00Lcos2δr?vr?T?????u(k)=AX(k)+Bu(k)? 式中,
v
r
v_r
vr?代表参考轨迹上每一个轨迹点要求的速度值;
δ
r
\delta_r
δr?是每一个轨迹点的参考前轮转角控制量。
X
=
x
?
x
r
e
f
\mathbf{X}=x-x_{ref}
X=x?xref?为状态量误差,
u
=
u
?
u
r
e
f
\mathbf{u}=u-u_{ref}
u=u?uref?为控制量误差。
期望的系统响应特性有以下两点:
- 跟踪偏差能够快速、稳定地趋近于零,并保持平衡;
- 前轮转角控制输入尽可能小。
采用线性二次调节原理解决这个问题。
python实现代码如下。
import math
class KinematicModel_3:
"""假设控制量为转向角delta_f和加速度a
"""
def __init__(self, x, y, psi, v, L, dt):
self.x = x
self.y = y
self.psi = psi
self.v = v
self.L = L
self.dt = dt
def update_state(self, a, delta_f):
self.x = self.x+self.v*math.cos(self.psi)*self.dt
self.y = self.y+self.v*math.sin(self.psi)*self.dt
self.psi = self.psi+self.v/self.L*math.tan(delta_f)*self.dt
self.v = self.v+a*self.dt
def get_state(self):
return self.x, self.y, self.psi, self.v
def state_space(self, ref_delta, ref_yaw):
"""将模型离散化后的状态空间表达
Args:
delta (_type_): 参考输入
Returns:
_type_: _description_
"""
A = np.matrix([
[1.0,0.0,-self.v*self.dt*math.sin(ref_yaw)],
[0.0, 1.0, self.v*self.dt*math.cos(ref_yaw)],
[0.0,0.0,1.0]])
B = np.matrix([
[self.dt*math.cos(ref_yaw), 0],
[self.dt*math.sin(ref_yaw), 0],
[self.dt*math.tan(ref_delta), self.v*self.dt/(self.L*math.cos(ref_delta)*math.cos(ref_delta))]
])
return A,B
2.2 相关参数设置
N=100
Q = np.eye(3)*5
R = np.eye(2)*1.
dt=0.1
L=2
v = 2
x_0=0
y_0=-3
psi_0=0
2.3 生成轨迹曲线
class MyReferencePath:
def __init__(self):
self.refer_path = np.zeros((1000, 4))
self.refer_path[:,0] = np.linspace(0, 100, 1000)
self.refer_path[:,1] = 2*np.sin(self.refer_path[:,0]/3.0)+2.5*np.cos(self.refer_path[:,0]/2.0)
for i in range(len(self.refer_path)):
if i == 0:
dx = self.refer_path[i+1,0] - self.refer_path[i,0]
dy = self.refer_path[i+1,1] - self.refer_path[i,1]
ddx = self.refer_path[2,0] + self.refer_path[0,0] - 2*self.refer_path[1,0]
ddy = self.refer_path[2,1] + self.refer_path[0,1] - 2*self.refer_path[1,1]
elif i == (len(self.refer_path)-1):
dx = self.refer_path[i,0] - self.refer_path[i-1,0]
dy = self.refer_path[i,1] - self.refer_path[i-1,1]
ddx = self.refer_path[i,0] + self.refer_path[i-2,0] - 2*self.refer_path[i-1,0]
ddy = self.refer_path[i,1] + self.refer_path[i-2,1] - 2*self.refer_path[i-1,1]
else:
dx = self.refer_path[i+1,0] - self.refer_path[i,0]
dy = self.refer_path[i+1,1] - self.refer_path[i,1]
ddx = self.refer_path[i+1,0] + self.refer_path[i-1,0] - 2*self.refer_path[i,0]
ddy = self.refer_path[i+1,1] + self.refer_path[i-1,1] - 2*self.refer_path[i,1]
self.refer_path[i,2]=math.atan2(dy,dx)
self.refer_path[i,3]=(ddy * dx - ddx * dy) / ((dx ** 2 + dy ** 2)**(3 / 2))
def calc_track_error(self, x, y):
"""计算跟踪误差
Args:
x (_type_): 当前车辆的位置x
y (_type_): 当前车辆的位置y
Returns:
_type_: _description_
"""
d_x = [self.refer_path[i,0]-x for i in range(len(self.refer_path))]
d_y = [self.refer_path[i,1]-y for i in range(len(self.refer_path))]
d = [np.sqrt(d_x[i]**2+d_y[i]**2) for i in range(len(d_x))]
s = np.argmin(d)
yaw = self.refer_path[s, 2]
k = self.refer_path[s, 3]
angle = normalize_angle(yaw - math.atan2(d_y[s], d_x[s]))
e = d[s]
if angle < 0:
e *= -1
return e, k, yaw, s
2.4 角度归一化
def normalize_angle(angle):
"""
Normalize an angle to [-pi, pi].
:param angle: (float)
:return: (float) Angle in radian in [-pi, pi]
copied from https://atsushisakai.github.io/PythonRobotics/modules/path_tracking/stanley_control/stanley_control.html
"""
while angle > np.pi:
angle -= 2.0 * np.pi
while angle < -np.pi:
angle += 2.0 * np.pi
return angle
2.5 解代数里卡提方程
def cal_Ricatti(A,B,Q,R):
"""解代数里卡提方程
Args:
A (_type_): 状态矩阵A
B (_type_): 状态矩阵B
Q (_type_): Q为半正定的状态加权矩阵, 通常取为对角阵;Q矩阵元素变大意味着希望跟踪偏差能够快速趋近于零;
R (_type_): R为正定的控制加权矩阵,R矩阵元素变大意味着希望控制输入能够尽可能小。
Returns:
_type_: _description_
"""
P = [None] * (N + 1)
Qf=Q
P[N]=Qf
for t in range(N,0,-1):
P[t-1]=Q+A.T@P[t]@A-A.T@P[t]@B@np.linalg.pinv(R+B.T@P[t]@B)@B.T@P[t]@A
return P
2.6 LQR控制算法
def lqr(robot_state, refer_path, s0, A, B, Q, R):
"""
LQR控制器
"""
x=robot_state[0:3]-refer_path[s0,0:3]
P = cal_Ricatti(A,B,Q,R)
K = [None] * (N)
u = [None] * (N)
"""计算反馈系数K和优化的控制量U"""
for t in range(N):
K[t] = -np.linalg.pinv(R + B.T @ P[t+1] @ B) @ B.T @ P[t+1] @ A
u[t] = K[t] @ x
u_star = u[N-1]
return u_star[0,1]
2.7 主函数
from celluloid import Camera
def main():
reference_path = MyReferencePath()
goal = reference_path.refer_path[-1,0:2]
ugv = KinematicModel_3(x_0, y_0, psi_0, v, L, dt)
x_ = []
y_ = []
fig = plt.figure(1)
camera = Camera(fig)
for i in range(500):
robot_state = np.zeros(4)
robot_state[0] = ugv.x
robot_state[1] = ugv.y
robot_state[2]=ugv.psi
robot_state[3]=ugv.v
e, k, ref_yaw, s0 = reference_path.calc_track_error(robot_state[0], robot_state[1])
ref_delta = math.atan2(L*k,1)
A, B = ugv.state_space(ref_delta,ref_yaw)
delta = lqr(robot_state, reference_path.refer_path,s0, A, B, Q, R)
delta = delta+ref_delta
ugv.update_state(0, delta)
x_.append(ugv.x)
y_.append(ugv.y)
plt.cla()
plt.plot(reference_path.refer_path[:,0], reference_path.refer_path[:,1], "-.b", linewidth=1.0, label="course")
plt.plot(x_, y_, "-r", label="trajectory")
plt.plot(reference_path.refer_path[s0,0], reference_path.refer_path[s0,1], "go", label="target")
plt.grid(True)
plt.pause(0.001)
if np.linalg.norm(robot_state[0:2]-goal)<=0.1:
print("reach goal")
break
main()
跟踪效果如下:
完整python代码文件见github仓库
|