Visual studio C++:LQR轨迹跟踪仿真

2023-05-16

前言:

        因为工作需要开始学习车辆横纵向控制,然后学到了LQR,正好写一个博客把程序保存下来。为了加强C++代码能力,本次仿真的所有文件均用C++完成。

 

代码结构梳理

        开始之前非常感谢这位大佬给出的参考:【自动驾驶】LQR实现轨迹跟踪,这次项目大部分都是将该博客从python翻译成C++,当然其中也发现了一些问题,后续再谈。

该项目用多个模块组成,分别为LQR、LQR_node、tool、trajectory、matplot5个模块。

1.LQR_node为主函数节点,负责调用轨迹生成模块、LQR控制器模块和画图;

2.LQR为LQR控制器模块,控制器中构造了模型参数A、B,计算黎卡提方程等功能;

3.trajectory为轨迹生成模块,并且计算出坐标点对应的曲率值;

4.tool为工具模块,定义了项目中需要的数据类型和一些角度处理函数(虽然没用到);

5.matplot为画图模块,调用了python的matplot功能进行作图;

        该项目用到的库有Eigen、python、matplotlibcpp,其中最为重要的是Eigen库,建议提前看一下该库的基本命令。

 

准备工作

1.项目配置Eigen库:

安装和使用C++线性代数库eigen(Windows下minGW+VS code, VS2019配置方式)

2.项目配置matplot库:
VS C++调用python进行画图matplotlib

 windows下配置C++版本的matplotlib绘图工具matplotlibcpp

 

别忘了把解决方案配置换成Release,我在这里卡了好久

8a03efdacf364857a42cd7d10cf55494.png

 

代码

1.tool.h

#pragma once
#include <iostream>
using namespace std;
#define pi acos(-1)

//定义路径点
typedef struct waypoint {
	int ID;
	double x, y, yaw, K;//x,y,yaw,曲率K
}waypoint;

//定义小车状态
typedef struct vehicleState {
	double x, y, yaw, v, kesi;//x,y,yaw,前轮偏角kesi
}vehicleState;

//定义控制量
typedef struct U {
	double v;
	double kesi;//速度v,前轮偏角kesi
}U;

double normalize_angle(double angle);//角度归一化 [-pi,pi];

double limit_kesi(double kesi);//前轮转角限幅 [-pi/2,pi/2];

2.tool.cpp

#include<iostream>
#include<tool.h>

double normalize_angle(double angle)//角度归一化 [-pi,pi];
{
	if (angle > pi) {
		angle -= 2.0 * pi;
	}
	if (angle <= -pi) {
		angle += 2.0 * pi;
	}
	return angle;
}

double limit_kesi(double kesi) {
	if (kesi > pi / 2) {
		kesi = pi / 2;
	}
	if (kesi < -pi / 2) {
		kesi = -pi / 2;
	}
	return kesi;
}

3.LQR.h

#include <iostream>
#include <Eigen/Dense>
#include "tool.h"
using namespace std;

typedef Eigen::Matrix<double, 3, 3> Matrix3x3;
typedef Eigen::Matrix<double, 3, 1> Matrix3x1;
typedef Eigen::Matrix<double, 2, 1> Matrix2x1;
typedef Eigen::Matrix<double, 2, 2> Matrix2x2;
typedef Eigen::Matrix<double, 3, 2> Matrix3x2;
typedef Eigen::Matrix<double, 2, 3> Matrix2x3;

//状态方程变量: X = [x_e  y_e  yaw_e]^T
//状态方程控制输入: U = [v_e  kesi_e]^T

class LQR
{
private:
	Matrix3x3 A_d;
	Matrix3x2 B_d;
	Matrix3x3 Q;
	Matrix2x2 R;
	Matrix3x1 X_e;
	Matrix2x1 U_e;
	
	double L;//车辆轴距
	double T;//采样间隔
	double x_car, y_car, yaw_car, x_d, y_d, yaw_d;//车辆位姿和目标点位姿
	double v_d, kesi_d;//期望速度和前轮偏角
	double Q3[3];//Q权重,三项
	double R2[2];//R权重,两项
	int temp = 0;
public:
	void initial(double L_, double T_, vehicleState car, waypoint waypoint, U U_r, double* Q_, double* R_);//初始化
	void param_struct();//构造状态方程参数
	Matrix2x3 cal_Riccati();//黎卡提方程求解
	U cal_vel();//LQR控制器计算速度
	void test();
};



4.LQR.cpp

#include <iostream>
#include <LQR.h>

using namespace std;

void LQR::initial(double L_, double T_, vehicleState car, waypoint waypoint, U U_r, double *Q_, double *R_) {

	L = L_;
	T = T_;
	x_car = car.x; y_car = car.y; yaw_car = car.yaw;
	x_d = waypoint.x; y_d = waypoint.y; yaw_d = waypoint.yaw;
	v_d = U_r.v;kesi_d = U_r.kesi;

	for (int i = 0; i < 3; i++) {
		Q3[i] = Q_[i];
	}
	for (int j = 0; j < 2; j++) {
		R2[j] = R_[j];
	}
}

void LQR::param_struct() {
	Q << Q3[0], 0.0, 0.0,
		0.0, Q3[1], 0.0,
		0.0, 0.0, Q3[2];
	//cout << "Q矩阵为:\n" << Q << endl;
	R << R2[0], 0.0,
		0.0, R2[1];
	//cout << "R矩阵为:\n" << R << endl;
	A_d << 1.0, 0.0, -v_d * T * sin(yaw_d),
		0.0, 1.0, v_d* T* cos(yaw_d),
		0.0, 0.0, 1.0;
	//cout << "A_d矩阵为:\n" << A_d << endl;
	B_d << T * cos(yaw_d), 0.0,
		T* sin(yaw_d), 0.0,
		T* tan(kesi_d)/L, v_d* T / (L * cos(kesi_d) * cos(kesi_d));
	//cout << "B_d矩阵为:\n" << B_d << endl;
	X_e << x_car - x_d, y_car - y_d, yaw_car - yaw_d;
	cout << "X_e矩阵为:\n" << X_e << endl;
	
}

Matrix2x3 LQR::cal_Riccati() {
	int N = 150;//迭代终止次数
	double err = 100;//误差值
	double err_tolerance = 0.01;//误差收敛阈值
	Matrix3x3 Qf = Q;
	Matrix3x3 P = Qf;//迭代初始值
	//cout << "P初始矩阵为\n" << P << endl;
	Matrix3x3 Pn;//计算的最新P矩阵
	for (int iter_num = 0; iter_num < N; iter_num++) {
		Pn = Q + A_d.transpose() * P * A_d - A_d.transpose() * P * B_d * (R + B_d.transpose() * P * B_d).inverse() * B_d.transpose() * P * A_d;//迭代公式
		//cout << "收敛误差为" << (Pn - P).array().abs().maxCoeff() << endl;
		//err = (Pn - P).array().abs().maxCoeff();//
		err = (Pn - P).lpNorm<Eigen::Infinity>();
		if(err < err_tolerance)//
		{
			P = Pn;
			cout << "迭代次数" << iter_num << endl;
			break;
		}
		P = Pn;
			
	}
	
	//cout << "P矩阵为\n" << P << endl;
	//P = Q;
	Matrix2x3 K = -(R + B_d.transpose() * P * B_d).inverse() * B_d.transpose() * P * A_d;//反馈率K
	return K;
}

U LQR::cal_vel() {
	U output;
	param_struct();
	Matrix2x3 K = cal_Riccati();
	Matrix2x1 U = K * X_e;
	//cout << "反馈增益K为:\n" << K << endl;
	//cout << "控制输入U为:\n" << U << endl;
	output.v = U[0] + v_d;
	output.kesi = U[1] + kesi_d;
	return output;
}

void LQR::test() //控制器效果测试
{
	/*param_struct();
	while (temp < 1000) {
		Matrix2x3 K = cal_Riccati();
		Matrix2x1 U = K * X_e;
		//cout <<"state variable is:\n" <<X_e << endl;
		//cout <<"control input is:\n"<< U << endl;
		Matrix3x1 X_e_ = A_d * X_e + B_d * U;
		X_e = X_e_;
		temp++;
	}*/
	Matrix3x3 C,D,F;
	C << 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0;
	F << 1.0, 0.0, 0.0, 0.0, 4.0, 0.0, 0.0, 0.0, 7.0;
	D = (C - F);
	double BBBB = D.lpNorm<Eigen::Infinity>();
	cout << BBBB << endl;
}

5.trajectory.h

#include <iostream>
#include <vector>
#include "tool.h"
using namespace std;



class trajectory {
private:
	vector<waypoint> waypoints;

public:
	//set reference trajectory
	void refer_path();
	vector<waypoint> get_path();

};

6.trajectory.cpp

#include <iostream>
#include <vector>
#include <trajectory.h>
#include <math.h>
using namespace std;

void trajectory::refer_path() {
	waypoint PP;
	for (int i = 0; i < 1000; i++)
	{
		PP.ID = i;
		PP.x = 0.1 * i;//x
		PP.y = 2.0 * sin(PP.x / 5.0) + 2.0 * cos(PP.x / 2.5);//y
		//直线
		//PP.y = 5.5;
		PP.yaw = PP.K = 0.0;
		waypoints.push_back(PP);
	}

	for (int j = 0; j < waypoints.size(); j++) {
		//差分法求一阶导和二阶导
		double dx, dy, ddx, ddy;
		if (j == 0) {
			dx = waypoints[1].x - waypoints[0].x;
			dy = waypoints[1].y - waypoints[0].y;
			ddx = waypoints[2].x + waypoints[0].x - 2 * waypoints[1].x;
			ddy = waypoints[2].y + waypoints[0].y - 2 * waypoints[1].y;
		}
		else if (j == (waypoints.size() - 1)) {
			dx = waypoints[j].x - waypoints[j - 1].x;
			dy = waypoints[j].y - waypoints[j - 1].y;
			ddx = waypoints[j].x + waypoints[j - 2].x - 2 * waypoints[j].x;
			ddy = waypoints[j].y + waypoints[j - 2].y - 2 * waypoints[j].y;
		}
		else {
			dx = waypoints[j + 1].x - waypoints[j].x;
			dy = waypoints[j + 1].y - waypoints[j].y;
			ddx = waypoints[j + 1].x + waypoints[j - 1].x - 2 * waypoints[j].x;
			ddy = waypoints[j + 1].y + waypoints[j - 1].y - 2 * waypoints[j].y;
		}
		waypoints[j].yaw = atan2(dy, dx);//yaw
		//计算曲率:设曲线r(t) =(x(t),y(t)),则曲率k=(x'y" - x"y')/((x')^2 + (y')^2)^(3/2).
		double AAA = sqrt(pow((pow(dx, 2) + pow(dy, 2)), 3));
		waypoints[j].K = (ddy * dx - ddx * dy) / (sqrt(pow((pow(dx, 2) + pow(dy, 2)), 3)));
	}
}

vector<waypoint> trajectory::get_path() {
	return waypoints;
}

7.matplotlibcpp.h

这个配置matplot库的时候会有这个头文件,代码里面直接调用就可以画图啦。

8.LQR_node.cpp

//                            _ooOoo_  
//                           o8888888o  
//                           88" . "88  
//                           (| -_- |)  
//                            O\ = /O  
//                        ____/`---'\____  
//                      .   ' \\| |// `.  
//                       / \\||| : |||// \  
//                     / _||||| -:- |||||- \  
//                       | | \\\ - /// | |  
//                     | \_| ''\---/'' | |  
//                      \ .-\__ `-` ___/-. /  
//                   ___`. .' /--.--\ `. . __  
//                ."" '< `.___\_<|>_/___.' >'"".  
//               | | : `- \`.;`\ _ /`;.`/ - ` : | |  
//                 \ \ `-. \_ __\ /__ _/ .-` / /  
//         ======`-.____`-.___\_____/___.-`____.-'======  
//                            `=---='  
//  
//         .............................................  
//                  佛祖保佑             永无BUG

//          佛曰:  
//                  写字楼里写字间,写字间里程序员;  
//                  程序人员写程序,又拿程序换酒钱。  
//                  酒醒只在网上坐,酒醉还来网下眠;  
//                  酒醉酒醒日复日,网上网下年复年。  
//                  但愿老死电脑间,不愿鞠躬老板前;  
//                  奔驰宝马贵者趣,公交自行程序员。  
//                  别人笑我忒疯癫,我笑自己命太贱;  
//                  不见满街漂亮妹,哪个归得程序员?

#include <iostream>
#include <LQR.h>
#include <vector>
#include <trajectory.h>
#include <stdlib.h>
#include "matplotlibcpp.h"
using namespace std;
namespace plt = matplotlibcpp;

#define pi acos(-1)
#define T 0.05//采样时间   很有意思的测试数据:T=0.5s,允许误差范围为±5.0m;T=0.1s,允许误差范围为±10.0m;T=0.05s;允许误差范围为±11m
#define L 0.5//车辆轴距
#define V_DESIRED 0.5//期望速度

vehicleState update_state(U control, vehicleState car) {
	car.v = control.v;
	car.kesi = control.kesi;
	car.x += car.v * cos(car.yaw) * T;
	car.y += car.v * sin(car.yaw) * T;
	car.yaw += car.v / L * tan(car.kesi) * T;
	//car.yaw = normalize_angle(car.yaw);
	return car;
}

class Path {
private:
	vector<waypoint> path;
public:
	//添加新的路径点
	void Add_new_point(waypoint& p)
	{
		path.push_back(p);
	}

	void Add_new_point(vector<waypoint>& p) 
	{
		path = p;
	}

	//路径点个数
	unsigned int Size()
	{
		return path.size();
	}

	//获取路径点
	waypoint Get_waypoint(int index)
	{
		waypoint p;
		p.ID = path[index].ID;
		p.x = path[index].x;
		p.y = path[index].y;
		p.yaw = path[index].yaw;
		p.K = path[index].K;
		return p;
	}


	// 搜索路径点, 将小车到起始点的距离与小车到每一个点的距离对比,找出最近的目标点索引值
	int Find_target_index(vehicleState state)
	{
		double min = abs(sqrt(pow(state.x - path[0].x, 2) + pow(state.y - path[0].y, 2)));
		int index = 0;
		for (int i = 0; i < path.size(); i++)
		{
			double d = abs(sqrt(pow(state.x - path[i].x, 2) + pow(state.y - path[i].y, 2)));
			if (d < min)
			{
				min = d;
				index = i;
			}
		}

		//索引到终点前,当(机器人与下一个目标点的距离Lf)小于(当前目标点到下一个目标点距离L)时,索引下一个目标点
		if ((index + 1) < path.size())
		{
			double current_x = path[index].x; double current_y = path[index].y;
			double next_x = path[index + 1].x; double next_y = path[index + 1].y;
			double L_ = abs(sqrt(pow(next_x - current_x, 2) + pow(next_y - current_y, 2)));
			double L_1 = abs(sqrt(pow(state.x - next_x, 2) + pow(state.y - next_y, 2)));
			//ROS_INFO("L is %f,Lf is %f",L,Lf);
			if (L_1 < L_)
			{
				index += 1;
			}
		}
		return index;
	}

};

class LQR_node {
private:
	vehicleState car;//小车状态
	double Q[3];
	double R[2];
	int lastIndex;//最后一个点索引值
	waypoint lastPoint;//最后一个点信息
	vector<double> x,y,x_p,y_p,v_a,v_d,kesi_a,kesi_d;

public:
	LQR* controller = new LQR();
	Path* path = new Path();
	trajectory* trajec = new trajectory();

	LQR_node()//初始化中添加轨迹、小车初始位姿
	{
		//ROS:
		addpointcallback();
		//robot:
		car.x = -1.325;
		car.y = 2.562;
		car.yaw = 0.964;
		car.v = 0.0;
		car.kesi = 0.1;
		
	}

	~LQR_node() {
		free(controller);
		free(path);
		free(trajec);
	}

	void addpointcallback(){
		trajec->refer_path();
		vector<waypoint> waypoints = trajec->get_path();
		path->Add_new_point(waypoints);
		cout << "path size is:" << path->Size() << endl;
		lastIndex = path->Size() - 1;
		lastPoint = path->Get_waypoint(lastIndex);
	}

	double slow_judge(double distance) {
		if (distance>=5.0&&distance <= 15.0) {
			return 0.35;
		}
		else if (distance>=0.1&&distance < 5.0) {
			return 0.15;
		}
		else if (distance < 0.1) {
			printf("reach goal!\n");
			plot_();
		}
		else
		{
			return V_DESIRED;
		}
	}

	//控制器流程
	void LQR_track() {
		U U_r;
		waypoint Point;

		//搜索路径点
		int target_index = path->Find_target_index(car);
		printf("target index is : %d\n", target_index);

		//获取路径点信息,构造期望控制量
		Point = path->Get_waypoint(target_index);
		//printf("waypoint information is x:%f,y:%f,yaw:%f,K:%f\n", Point.x, Point.y, Point.yaw, Point.K);
		
		//减速判断
		double kesi = atan2(L * Point.K, 1);
		double v_distance = abs(sqrt(pow(car.x - lastPoint.x, 2) + pow(car.y - lastPoint.y, 2)));
		printf("the distance is %f\n", v_distance);
		U_r.v = slow_judge(v_distance);U_r.kesi = kesi;
		printf("the desired v is: %f,the desired kesi is: %f\n", U_r.v,U_r.kesi);

		//权重矩阵
		Q[0] = 1.0; Q[1] = 1.0; Q[2] = 1.0;
		R[0] = 4.0; R[1] = 4.0;

		//使用LQR控制器
		controller->initial(L, T, car, Point, U_r, Q, R);//初始化控制器
		U control = controller->cal_vel();//计算输入[v, kesi]
		printf("the speed is: %f,the kesi is: %f\n", control.v, control.kesi);
		printf("the car position is x: %f, y: %f\n", car.x, car.y);

		//储存小车位姿用来画图
		x.push_back(car.x);
		y.push_back(car.y);
		v_a.push_back(car.v);
		v_d.push_back(U_r.v);
		kesi_a.push_back(car.kesi);
		kesi_d.push_back(U_r.kesi);
		
		//小车位姿状态更新
		car = update_state(control, car);
	}

	//控制启停函数
	void control() {
		int i = 0;
		
		while (i < 10000) {
			LQR_track();
			i++;
		}
		
	}

	//画图程序
	void plot_() {
		vector<double> time;
		for (int i = 0; i < path->Size(); i++) {
			x_p.push_back(path->Get_waypoint(i).x);
			y_p.push_back(path->Get_waypoint(i).y);
		}
		for (int j = 0; j < v_a.size(); j++) {
			time.push_back(double(j));
		}

		
		plt::subplot(3, 1, 1);
		plt::title("Car position");
		plt::plot(x_p, y_p, "-k", x, y, "-.r");
		plt::subplot(3, 1, 2);
		plt::title("Car speed");
		plt::plot(time, v_d, "-k", time, v_a, "-.r");
		plt::subplot(3, 1, 3);
		plt::title("Car kesi");
		plt::plot(time, kesi_d, "-k", time, kesi_a, "-.r");
		
		plt::show();
		exit(0);
	}


};

int main(char argc, char* argv) {
	LQR_node* node = new LQR_node();
	node->control();
	return 0;
}


 

仿真测试结果:

495a6c6c04374356bbec90e8103b9f21.png

         三幅图分别为车辆位置、车辆速度、车辆前轮转角黑色实线为期望值,红色虚线为实际值。速度为0.5,后面有减速处理,可以看到效果还是挺不错的。

 

 重要的说明!!

1、解黎卡提方程的问题

        在大佬python版本中发现了一个问题,经过解黎卡提方程后,得到的矩阵K只迭代了一次(如下图),这个地方我想了挺久的,最后还是按照迭代法求黎卡提(Riccati)方程的解这篇博客来解的,判定收敛的条件是无穷范数。

 28fe4054ca0d407ebf2918f68908fec1.png

 2.初始误差的选择(小车与轨迹起点的距离)

在调试过程中发现了一个很有意思的现象,采样时间与允许初始误差范围有关系,太大的初始误差可能会导致跟踪失败,仿真的期望速度为0.5m/s:

采样时间(s)允许初始误差范围(m)
0.5±5.0
0.1±10.0
0.05±11.0

        测试了三种调试频率,2Hz,10Hz,20Hz,在实验平台上一般使用20Hz的,不过取极限初始误差意义不是很大,自动驾驶里面不可能在离车辆11m的地方开始规划路径。。。但还是可以测试控制器极限性能。

3.权重的选择

        这个问题我调了很久,最后发现Q权重一定要比R小,不然速度就会提前计算到0,或者前轮转角值异常。我也不知道是怎么回事,相同的控制率算法写到python里面没有问题,C++有问题,就令我很费解。

4.完整代码

完整代码请见:LQR_cpp

5.后续工作

        过几天会把这个项目弄到实验平台上进行仿真和实验。 

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Visual studio C++:LQR轨迹跟踪仿真 的相关文章

随机推荐