目录
- 第一章:最优化控制和基本概念
- [1.1 引言:为什么需要MPC?](#1.1 引言:为什么需要MPC?)
- [1.2 最优化控制:找到"最好"的方案](#1.2 最优化控制:找到"最好"的方案)
- [1.3 用数学表达驾驶行为](#1.3 用数学表达驾驶行为)
- [1.4 定义"好的驾驶"](#1.4 定义"好的驾驶")
- [1.5 模型预测控制:向前看的司机](#1.5 模型预测控制:向前看的司机)
- 第二章:最优化数学建模推导
- [2.1 再谈"向前看"的控制策略](#2.1 再谈"向前看"的控制策略)
- [2.2 为什么传统控制方法不够好?](#2.2 为什么传统控制方法不够好?)
- [2.3 MPC的"魔法":预测+优化](#2.3 MPC的"魔法":预测+优化)
- [2.4 MPC问题的"食谱"](#2.4 MPC问题的"食谱")
- [2.5 解决非线性问题:计算机如何"思考"最佳驾驶策略](#2.5 解决非线性问题:计算机如何"思考"最佳驾驶策略)
- [2.6 实际问题:计算量和实时性](#2.6 实际问题:计算量和实时性)
- [2.7 总结:MPC的魅力](#2.7 总结:MPC的魅力)
- 第三章:一个详细的建模例子
- [3.1 从零开始:构建我们的"虚拟赛车手"](#3.1 从零开始:构建我们的"虚拟赛车手")
- [3.2 构建"思考模型":车辆是如何运动的?](#3.2 构建"思考模型":车辆是如何运动的?)
- [3.3 定义"好司机"的标准:目标函数](#3.3 定义"好司机"的标准:目标函数)
- [3.4 设定"游戏规则":约束条件](#3.4 设定"游戏规则":约束条件)
- [3.5 "一步一步来":完整的MPC建模过程](#3.5 “一步一步来”:完整的MPC建模过程)
- [3.6 将模型转化为实际代码:以Python为例](#3.6 将模型转化为实际代码:以Python为例)
- [3.7 小结:从概念到代码](#3.7 小结:从概念到代码)
- 第四章:基于Python的MPC避障变道控制系统实现
- [4.1 引言:从概念到实践](#4.1 引言:从概念到实践)
- [4.2 开发环境准备](#4.2 开发环境准备)
- [4.3 基础系统架构设计](#4.3 基础系统架构设计)
- [4.3.1 系统参数定义](#4.3.1 系统参数定义)
- [4.3.2 定义系统模型](#4.3.2 定义系统模型)
- [4.3.3 障碍物模型与碰撞约束](#4.3.3 障碍物模型与碰撞约束)
- [4.4 构建MPC优化问题](#4.4 构建MPC优化问题)
- [4.4.1 目标函数设计](#4.4.1 目标函数设计)
- [4.4.2 约束条件设计](#4.4.2 约束条件设计)
- [4.4.3 配置求解器](#4.4.3 配置求解器)
- [4.5 决策状态机设计](#4.5 决策状态机设计)
- [4.6 仿真实现](#4.6 仿真实现)
- [4.6.1 仿真循环设计](#4.6.1 仿真循环设计)
- [4.6.2 结果可视化](#4.6.2 结果可视化)
- [4.7 参数调优与性能分析](#4.7 参数调优与性能分析)
- [4.8 小结与展望](#4.8 小结与展望)
第一章:最优化控制和基本概念
学习完本文,你将能够实现以下仿真效果:
- 本文区别于传统MPC教程(那种类似教科书的讲解,上来就是大段公式推导劝退小白)
- 本文主要用大白话讲解
- 要想进阶学习,强烈推荐 DR_CAN 在B站的教程【MPC模型预测控制器】1_最优化控制和基本概念
- 关于状态空间入门,MPC的基础知识,可参考往期文章:LQR 状态空间入门:以 "倒立摆" 仿真案例建模( MPC 是 LQR 的泛化)
1.1 引言:为什么需要MPC?
想象你正在开车。作为一个老司机,你会怎么做?你会不断观察前方路况,预判接下来几秒钟的情况,并做出相应的控制决策------踩油门、刹车或转向。这实际上就是"预测控制"的基本思想。
模型预测控制(MPC)就像是一个"会思考的司机",它不只看当前,还会"向前看"一段时间,规划出最佳的控制策略。与传统控制方法相比,MPC最大的特点就是"有预见性",这使它特别适合处理复杂的控制问题,比如自动驾驶中的避障变道。
别担心如果你对控制理论不太熟悉!我们会用简单直观的方式来解释这些概念,循序渐进。
1.2 最优化控制:找到"最好"的方案
"最优化控制"听起来很高大上,但其实它就是在一堆可能的方案中,找到最符合我们期望的那个。就像是你在导航软件中,可能会有多条路线可选,但你会根据自己的需求(最快到达、最省油或最少收费)来选择最佳路线。
以自动驾驶汽车变道为例。我们有两种变道方案:
- 方案A:缓慢平滑地变道,像优雅的绅士一样
- 方案B:快速切到旁边车道,像赛车手一样激进
哪个方案"更好"呢?这取决于情境: - 如果你是正常驾驶,没有障碍物,可能更喜欢平滑舒适的方案A
- 如果你是在紧急避险,可能需要快速反应的方案B
这就是"最优"的相对性------没有绝对的最好,只有最适合当前需求的方案。最优化控制就是让计算机根据我们的需求自动找到这个"最适合"的方案。
1.3 用数学表达驾驶行为
虽然数学看起来有些枯燥,但它能精确描述我们的控制问题。别担心,我们会用开车的例子来解释这些公式。
想象你开车时的情况。车辆的状态可以用几个关键变量描述:
- 横向位置(车道上的左右位置)
- 纵向位置(车辆前进的距离)
- 方向角(车头朝向)
- 车速
作为驾驶员,你的控制动作包括:
- 踩油门或刹车(控制加速度)
- 转动方向盘(控制转向角)
这些变量之间的关系可以用数学模型来描述:
横向位置变化 = 车速 × sin(方向角) × 时间间隔
纵向位置变化 = 车速 × cos(方向角) × 时间间隔
方向角变化 = (车速/轴距) × tan(转向角) × 时间间隔
车速变化 = 加速度 × 时间间隔
这个模型描述了"如果我这样踩油门和打方向盘,车会怎么走"。虽然看起来有点复杂,但实际上就是把我们开车的经验用数学语言表达出来了。
1.4 定义"好的驾驶"
什么是好的驾驶呢?通常我们关心几个方面:
- 准确性:能否精确地跟随预定路线
- 舒适性:避免急加速、急刹车或急转弯
- 安全性:与其他车辆和障碍物保持安全距离
用数学语言,我们可以把这些期望总结为一个"得分函数"(也叫目标函数):
总得分 = 路线偏离的惩罚 + 操作不平稳的惩罚 + 安全风险的惩罚
具体来说,对于路线跟踪问题:
路线偏离的惩罚 = Q1×(横向位置偏差)² + Q2×(纵向位置偏差)² + Q3×(方向角偏差)² + Q4×(速度偏差)²
操作不平稳的惩罚 = R1×(加速度)² + R2×(转向角)²
其中Q和R是权重系数,用来调整各项的重要性:
- Q1, Q2大:更注重精确跟随预定路线
- Q4大:更注重保持目标速度
- R1, R2大:更注重操作平稳,避免急加速和急转弯
自动驾驶的目标就是找到一系列控制动作(踩油门/刹车和转向),使这个总得分尽可能低。
1.5 模型预测控制:向前看的司机
现在,让我们把上面的理念组合起来,理解什么是模型预测控制(MPC)。
想象一下,你是一个正在高速公路上行驶的司机。你会怎么做?
- 观察当前状态:我在哪个车道?当前速度是多少?
- 预测前方情况:如果我保持当前速度和方向,未来几秒内会发生什么?
- 评估多种方案:如果我变道呢?如果我减速呢?
- 选择最优方案:哪个方案能最安全、最舒适地到达目的地?
- 执行并重复:采取行动,然后在下一刻重新评估
MPC正是按照这个思路工作的智能控制算法:
-
获取当前状态 :
通过传感器测量车辆当前的位置、速度、方向等信息。
-
预测未来可能的状态 :
使用车辆运动模型,预测在不同控制动作下(不同的加速度和转向角),未来几秒内车辆可能的运动轨迹。
例如:如果未来5秒内,我们打算这样踩油门和转向,车辆会沿着这条轨迹运动。
-
优化控制方案 :
计算哪种控制方案能使总体目标函数(前面提到的"得分")最小。
MPC会同时考虑:
- 轨迹跟踪的准确性
- 控制动作的平稳性
- 各种安全约束(例如不超速、不偏离车道)
-
滚动执行 :
只执行优化方案的第一步,然后在下一个控制周期重新开始整个过程。
这就像是司机不断调整方向盘和油门,而不是一次性决定整个行程的控制。
MPC的独特之处在于它能够:
- 看得远:考虑未来一段时间内的行为
- 考虑全面:同时兼顾多个目标
- 适应变化:随时调整控制策略
- 处理约束:明确考虑车辆和环境的物理限制
这种"向前看"的控制策略,使MPC特别适合处理自动驾驶这类复杂问题,尤其是需要规划轨迹并避开障碍物的场景。
接下来,我们将深入了解如何构建MPC控制器的数学模型,别担心,我们会继续用直观的例子来解释这些概念!
第二章:最优化数学建模推导
2.1 再谈"向前看"的控制策略
在第一章中,我们把MPC比喻成一个"向前看"的司机。现在,让我们更深入地了解这个司机是如何"思考"的。
假设你正在驾车,前方有一个障碍物。你的大脑会自动进行一系列计算:
- 我现在在哪里?速度多快?
- 如果我这样操作方向盘和油门,几秒后会到哪里?
- 哪种操作方式最安全舒适?
MPC控制器的工作方式与此类似,但更加系统化和精确。接下来,我们将用更通俗的语言解释这个过程。
2.2 为什么传统控制方法不够好?
在了解MPC的细节之前,我们先思考为什么需要这么复杂的控制方法?传统的控制方法(如PID控制)有什么不足?
想象你开车跟随前车。传统的"跟车控制"大致是这样工作的:
- 如果与前车距离太近,就踩刹车
- 如果与前车距离太远,就踩油门
- 控制力度与距离误差成正比
这种方法简单直接,但有几个明显缺点:
- 没有预见性:只看当前状态,不考虑未来
- 难以处理约束:比如最大速度、加速度限制
- 不易优化多目标:难以同时兼顾安全、舒适和效率
这就像是一个只盯着前车尾灯的新手司机,而MPC则像是能够看到远处路况的老司机,可以提前规划和调整。
2.3 MPC的"魔法":预测+优化
MPC的核心思想可以概括为两个关键词:预测 和优化。
预测:使用车辆模型,根据当前状态和控制动作,预测未来几秒内车辆可能的行驶轨迹。
想象你手里有一个汽车模拟器,可以快速模拟"如果我这样踩油门和打方向盘,车会怎么走"。MPC就内置了这样一个模拟器,不过是用数学方程而不是3D图形。
这个"模拟器"基于车辆的物理模型:
下一时刻的前进距离 = 当前位置 + 当前速度×时间间隔×方向角的余弦值
下一时刻的横向位置 = 当前横向位置 + 当前速度×时间间隔×方向角的正弦值
下一时刻的方向角 = 当前方向角 + (当前速度/轴距)×时间间隔×转向角的正切值
下一时刻的速度 = 当前速度 + 加速度×时间间隔
注意这个模型是非线性的------转向角对车辆运动的影响不是简单的加减乘除关系。这就是为什么我们需要特殊的非线性优化方法。
优化:在众多可能的控制方案中,找到能使目标函数(舒适性、安全性、效率的综合评分)最优的那一个。
这就像是在众多可能的驾驶操作中,找到最"完美"的那个操作序列。
2.4 MPC问题的"食谱"
如果把MPC问题比喻成一道菜,那么其"食谱"包含以下核心元素:
-
状态和控制变量:
- 状态变量:描述车辆情况(位置、速度、方向等)
- 控制变量:驾驶员的操作(加速度、转向角)
-
预测模型 :
告诉我们"如果执行这些控制操作,车辆将如何运动"。就像是一个简化的物理引擎。
-
目标函数 :
评价"驾驶质量"的打分标准,通常包括:
- 路径跟踪精度(车辆是否按预定路线行驶)
- 控制平滑度(操作是否平稳舒适)
- 终点状态(最终是否到达目标位置和速度)
-
约束条件:
- 车辆物理约束:速度不能超过限制,转向角不能过大
- 道路边界约束:车辆必须在道路范围内
- 安全约束:与障碍物保持安全距离
2.5 解决非线性问题:计算机如何"思考"最佳驾驶策略
现在到了关键问题:计算机如何找到最佳的控制序列?
传统的优化问题往往是线性的或二次型的,可以用标准方法高效求解。但车辆控制是非线性问题------转向角对位置的影响不是简单的线性关系。这就像是一个复杂的导航问题,不能用简单的直线距离来计算。
有两种主要方法来处理这种非线性问题:
-
线性化方法 :
我们可以在当前工作点附近,将非线性模型近似为线性模型。这就像是用很多小的直线段来近似一条曲线。
例如,我们可以将模型简化为:
下一状态 ≈ 当前状态 + 状态变化率×控制输入
这种方法计算快速,但在大幅度转向等情况下可能不够准确。
-
直接非线性优化 :
现代计算机已经强大到可以直接处理非线性优化问题。使用专业的优化工具(如IPOPT)和自动微分技术,可以高效地求解这类问题。
这就像是用一个强大的导航系统,可以考虑道路的弯曲、坡度等复杂因素,而不仅仅是直线距离。
实际应用中,MPC控制器会在几十毫秒内完成一次优化计算,找到从当前状态到目标状态的最佳控制序列。
2.6 实际问题:计算量和实时性
在理想世界中,我们可以考虑无限远的未来,评估无数种可能的控制策略。但在现实中,计算资源是有限的,特别是在需要实时响应的自动驾驶场景。
为了保证MPC的实时性,工程师们采用了一系列"聪明的偷懒"方法:
-
有限预测时域 :
只看未来几秒(而不是整个旅程),这就像是开车时只关注前方100米的路况,而非整条路线。
-
控制序列简化 :
可以假设后半段控制是恒定的,减少优化变量。
-
热启动 :
使用上一步的优化结果作为当前步骤的初始猜测,加速收敛。这就像是基于之前的驾驶经验来调整当前的驾驶策略。
-
并行计算 :
利用现代计算机的多核特性,并行处理不同方面的计算任务。
2.7 总结:MPC的魅力
MPC控制器的魅力在于它能够像一个富有经验的司机一样"思考"和"规划":
- 预见性:通过模型预测未来可能的状态
- 全局优化:综合考虑多种因素,找到整体最优方案
- 约束处理:能够明确处理速度限制、道路边界等约束
- 适应性:不断重新规划,适应变化的环境
在下一章中,我们将通过一个具体的例子,一步步展示如何构建和实现一个用于车辆避障变道的MPC控制器。别担心数学公式,我们会用通俗易懂的语言和生动的比喻来解释核心概念!
第三章:一个详细的建模例子
3.1 从零开始:构建我们的"虚拟赛车手"
在前两章中,我们学习了MPC的基本概念和工作原理。现在,让我们动手构建一个实际的MPC控制器,就像是打造一个"虚拟赛车手",能够自动驾驶车辆完成避障变道任务。
首先,我们需要明确这个"虚拟赛车手"需要知道什么信息,能做什么操作:
状态信息(赛车手能看到的):
- 车辆的位置坐标(x,y)
- 车辆的航向角(ψ)
- 车辆的速度(v)
控制动作(赛车手能操作的):
- 加速度(a):踩油门或刹车
- 转向角(δ):方向盘转角
这两组变量,分别构成了我们的状态向量 和控制向量。
3.2 构建"思考模型":车辆是如何运动的?
我们的"虚拟赛车手"需要有一个"思考模型"------了解车辆在不同控制动作下将如何运动。这就是车辆的数学模型。
想象一辆自行车:前轮可以转向,后轮固定。这种简化的"自行车模型"足以描述大多数车辆的基本运动特性:
下一时刻的x位置 = 当前x位置 + 当前速度 × 时间间隔 × cos(航向角)
下一时刻的y位置 = 当前y位置 + 当前速度 × 时间间隔 × sin(航向角)
下一时刻的航向角 = 当前航向角 + (当前速度/轴距) × 时间间隔 × tan(转向角)
下一时刻的速度 = 当前速度 + 加速度 × 时间间隔
如果你学过一点物理,会发现这就是基本的运动学方程。如果没学过也没关系,你可以把它理解为:
- 车辆会沿着航向角方向前进(x和y方程)
- 转向时,航向角会根据转向角度和车速变化(航向角方程)
- 加速或刹车会改变车速(速度方程)
3.3 定义"好司机"的标准:目标函数
现在,我们需要定义什么是"好的驾驶"。在变道避障任务中,我们通常关心:
- 轨迹跟踪:车辆应该尽量靠近目标车道
- 速度控制:车辆应该保持适当的巡航速度
- 平稳操作:避免急转弯和急刹车
- 安全性:与障碍物保持安全距离
这些期望可以转化为数学上的"目标函数"。想象我们给司机的驾驶表现打分:
总分 = 轨迹跟踪分数 + 速度控制分数 + 操作平稳分数 + 安全分数
具体来说:
轨迹跟踪分数 = -Q1×(当前y位置-目标y位置)² // 负号表示"越小越好"
速度控制分数 = -Q2×(当前速度-目标速度)²
操作平稳分数 = -R1×(加速度)² - R2×(转向角)²
安全分数 = -大数×(如果接近障碍物)
其中Q1、Q2、R1、R2是权重系数,用来调整各项指标的重要性:
- Q1大:更注重精确跟踪车道
- Q2大:更注重保持目标速度
- R1大:更注重加速平稳
- R2大:更注重转向平稳
3.4 设定"游戏规则":约束条件
除了追求高分,我们的"虚拟赛车手"还必须遵守一些"游戏规则",这些在数学上称为"约束条件":
-
物理约束:
速度范围:0 ≤ 速度 ≤ 20 m/s 加速度范围:-5 m/s² ≤ 加速度 ≤ 3 m/s² 转向角范围:-30° ≤ 转向角 ≤ 30°
-
道路约束:
车辆必须在道路范围内:道路左边界 ≤ y位置 ≤ 道路右边界
-
安全约束:
与障碍物距离必须大于安全距离:距离 ≥ 安全距离
这些约束条件就像是"红线",不允许被踩踏。在数学优化中,这些约束条件会限制可行解的范围。
3.5 "一步一步来":完整的MPC建模过程
让我们梳理一下构建MPC控制器的完整过程,就像是制作一道精美菜肴的步骤:
-
定义问题参数:
python# 时间参数 Ts = 0.1 # 控制周期(秒) N = 10 # 预测步数 # 车辆参数 L = 2.5 # 轴距(米) # 约束参数 v_min, v_max = 0, 20 # 速度范围(米/秒) a_min, a_max = -5, 3 # 加速度范围(米/秒²) delta_min, delta_max = -30, 30 # 转向角范围(度,需转换为弧度) # 目标参数 lane1_y = 0 # 第一车道中心线y坐标 lane2_y = 3.5 # 第二车道中心线y坐标 target_v = 10 # 目标速度(米/秒)
-
构建车辆模型:
python# 状态方程(简化表示) def vehicle_model(x, y, psi, v, a, delta, Ts, L): next_x = x + Ts * v * cos(psi) next_y = y + Ts * v * sin(psi) next_psi = psi + Ts * (v / L) * tan(delta) next_v = v + Ts * a return next_x, next_y, next_psi, next_v
-
定义目标函数:
pythondef objective_function(states, controls, target_y, target_v): total_cost = 0 for i in range(N): # 轨迹跟踪代价 y_error = states[1, i] - target_y total_cost += Q1 * y_error**2 # 速度跟踪代价 v_error = states[3, i] - target_v total_cost += Q2 * v_error**2 # 控制平滑代价 total_cost += R1 * controls[0, i]**2 # 加速度惩罚 total_cost += R2 * controls[1, i]**2 # 转向角惩罚 return total_cost
-
设置约束条件:
python# 状态约束 for i in range(N+1): # 速度约束 v_min <= states[3, i] <= v_max # 道路边界约束 road_left <= states[1, i] <= road_right # 控制约束 for i in range(N): # 加速度约束 a_min <= controls[0, i] <= a_max # 转向角约束 delta_min <= controls[1, i] <= delta_max # 障碍物避让约束(如果存在障碍物) for i in range(N+1): distance_to_obstacle(states[:, i]) >= safe_distance
-
构建优化问题:
python# 决策变量 states = 变量(4, N+1) # [x, y, psi, v] 在未来N+1个时刻的值 controls = 变量(2, N) # [a, delta] 在未来N个时刻的值 # 目标函数 minimize(objective_function(states, controls, target_y, target_v)) # 约束条件 subject_to: # 初始状态约束 states[:, 0] = 当前状态 # 系统动力学约束 for i in range(N): states[:, i+1] = vehicle_model(states[:, i], controls[:, i]) # 状态和控制约束 ... (如第4步所述)
3.6 将模型转化为实际代码:以Python为例
在实际应用中,我们通常使用专业的优化库来构建和求解MPC问题。以Python和CasADi库为例:
python
import numpy as np
import casadi as ca
# 定义问题参数
Ts = 0.1 # 采样时间(秒)
N = 10 # 预测步数
L = 2.5 # 车辆轴距(米)
# 定义状态和控制变量
x = ca.SX.sym('x')
y = ca.SX.sym('y')
psi = ca.SX.sym('psi')
v = ca.SX.sym('v')
states = ca.vertcat(x, y, psi, v)
n_states = states.numel()
a = ca.SX.sym('a')
delta = ca.SX.sym('delta')
controls = ca.vertcat(a, delta)
n_controls = controls.numel()
# 定义系统模型
rhs = ca.vertcat(
v * ca.cos(psi),
v * ca.sin(psi),
(v / L) * ca.tan(delta),
a
)
f = ca.Function('f', [states, controls], [states + Ts * rhs])
# 创建优化问题
opti = ca.Opti()
X = opti.variable(n_states, N+1)
U = opti.variable(n_controls, N)
P = opti.parameter(n_states + 2) # [当前状态, 目标y, 目标速度]
# 构建目标函数
obj = 0
Q = np.diag([0.1, 10, 0.1, 0.5]) # 状态权重
R = np.diag([0.1, 1.0]) # 控制权重
for k in range(N):
# 计算状态误差
st_err = X[:, k] - ca.vertcat(X[0, k], P[n_states], 0, P[n_states+1])
obj += ca.mtimes([st_err.T, Q, st_err])
# 计算控制代价
obj += ca.mtimes([U[:, k].T, R, U[:, k]])
# 终端代价
st_err_N = X[:, N] - ca.vertcat(X[0, N], P[n_states], 0, P[n_states+1])
obj += 10 * ca.mtimes([st_err_N.T, Q, st_err_N]) # 终端代价权重更大
opti.minimize(obj)
# 添加约束条件
opti.subject_to(X[:, 0] == P[:n_states]) # 初始状态
for k in range(N):
opti.subject_to(X[:, k+1] == f(X[:, k], U[:, k])) # 系统动力学
# 控制约束
a_min, a_max = -5, 3 # m/s²
delta_min, delta_max = -np.pi/6, np.pi/6 # ±30度
opti.subject_to(opti.bounded(a_min, U[0, :], a_max))
opti.subject_to(opti.bounded(delta_min, U[1, :], delta_max))
# 状态约束
v_min, v_max = 0, 20 # m/s
y_min, y_max = -1.75, 5.25 # 道路边界(假设两车道,每车道3.5米宽)
opti.subject_to(opti.bounded(v_min, X[3, :], v_max)) # 速度约束
opti.subject_to(opti.bounded(y_min, X[1, :], y_max)) # 道路边界约束
这看起来有点复杂,但这就是一个基本的MPC控制器框架。在下一章中,我们会进一步讲解如何将这个框架应用到实际的变道控制问题中。
3.7 小结:从概念到代码
在本章中,我们以自动驾驶车辆变道为例,详细展示了如何构建一个MPC控制器:
- 定义状态 (车辆位置、方向、速度)和控制(加速度、转向角)变量
- 构建车辆运动模型,描述状态如何随控制输入变化
- 设计目标函数,定义什么是"好的驾驶"
- 添加约束条件,限定可行的操作范围
- 构建优化问题,将其转化为数学形式
- 使用优化工具求解得到最优控制序列
你可能会觉得这些数学看起来很复杂,但实际上它们只是把我们开车时的思考过程用数学语言表达出来了。如果理解了基本概念,编写MPC控制器代码其实并不那么困难。
在下一章中,我们将基于本章构建的框架,实现一个完整的车辆变道MPC控制器,并通过仿真测试其性能。
第四章:基于Python的MPC避障变道控制系统实现
4.1 引言:从概念到实践
在前面几章中,我们学习了MPC的基本原理和建模方法。现在让我们动手实现一个完整的车辆避障变道控制系统,将理论知识应用到实际中。我们将使用Python和CasADi库来构建和求解MPC优化问题。
本章将实现一个智能避障变道控制系统,它能够根据环境中障碍物的位置自主决定何时变道、如何安全避障,并在超越障碍物后返回原车道。这个系统将展示MPC在处理复杂约束和多目标优化方面的强大能力。
与传统控制方法相比,MPC方法在这类场景中具有明显优势:
- 预见性控制:能够提前预测和规划避障路径
- 约束处理:自然地处理车辆物理限制和安全约束
- 多目标优化:同时考虑安全性、舒适性和效率
4.2 开发环境准备
首先,我们需要安装和导入必要的Python库:
python
# 可以通过pip安装这些库
# pip install casadi numpy matplotlib
import casadi as ca # 符号计算和优化
import numpy as np # 数值计算
import matplotlib.pyplot as plt # 可视化
from matplotlib.patches import Rectangle # 绘制车辆和障碍物
from matplotlib.transforms import Affine2D # 处理旋转变换
from matplotlib.animation import FuncAnimation # 创建动画
import time # 计时
# 中文字体设置
try:
plt.rcParams['font.sans-serif'] = ['SimHei'] # 指定显示中文的字体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
except Exception as e:
print(f"中文字体设置警告: {e}")
4.3 基础系统架构设计
4.3.1 系统参数定义
首先定义系统的各种参数:
python
# 时间参数
Ts = 0.1 # 采样时间 (s)
Np = 20 # 预测时域步数
# 车辆参数
L = 2.5 # 车辆轴距 (m)
vehicle_length = 4.0 # 车辆长度 (m)
vehicle_width = 1.8 # 车辆宽度 (m)
# 约束参数
v_max = 20.0 # 最大速度 (m/s)
v_min = 0.0 # 最小速度 (m/s)
a_max = 3.0 # 最大加速度 (m/s^2)
a_min = -5.0 # 最大减速度 (m/s^2)
delta_max = np.deg2rad(30) # 最大转向角 (rad)
delta_min = -delta_max # 最小转向角 (rad)
# 道路参数
lane_width = 3.5 # 车道宽度 (m)
lane1_y = 0.0 # 第一车道中心线 y 坐标
lane2_y = lane_width # 第二车道中心线 y 坐标
# 状态和控制维度
nx = 4 # 状态量数量: [x, y, psi, v]
nu = 2 # 控制量数量: [a, delta]
# 权重参数 - 这些参数对MPC性能有重要影响
Q = np.diag([1.0, 100.0, 0.5, 1.0]) # 状态权重 [x, y, psi, v]
R = np.diag([0.1, 1000.0]) # 控制权重 [a, delta]
P = Q * 0.01 # 终端权重
# 航向角速度变化惩罚权重 - 使转向更平滑
w_psi_dot_change = 18000.0
# 目标速度
target_v_val = 10.0 # 目标巡航速度 (m/s)
4.3.2 定义系统模型
使用CasADi定义车辆的运动学模型:
python
# 定义符号变量
x_sym = ca.SX.sym('x') # x位置
y_sym = ca.SX.sym('y') # y位置
psi_sym = ca.SX.sym('psi') # 航向角
v_sym = ca.SX.sym('v') # 速度
states_sym = ca.vertcat(x_sym, y_sym, psi_sym, v_sym)
a_sym = ca.SX.sym('a') # 加速度
delta_sym = ca.SX.sym('delta') # 转向角
controls_sym = ca.vertcat(a_sym, delta_sym)
# 定义系统动力学方程 - 自行车模型
rhs = ca.vertcat(
v_sym * ca.cos(psi_sym), # x方向速度
v_sym * ca.sin(psi_sym), # y方向速度
(v_sym / L) * ca.tan(delta_sym), # 航向角变化率
a_sym # 速度变化率(加速度)
)
# 创建离散时间模型函数 - 使用前向欧拉法
f = ca.Function('f', [states_sym, controls_sym], [states_sym + Ts * rhs])
4.3.3 障碍物模型与碰撞约束
为了实现避障功能,我们需要定义障碍物模型和碰撞约束:
python
# 障碍物参数
obs_length = vehicle_length # 障碍物长度 (m)
obs_width = vehicle_width # 障碍物宽度 (m)
obs_x_initial = 35.0 # 障碍物初始x位置 (m)
obs_y_fixed = lane1_y # 障碍物固定在第一车道
obs_speed_x = 4.0 # 障碍物速度 (m/s),低于车辆目标速度
# 碰撞检测参数
veh_r = vehicle_length / 2.5 # 车辆等效碰撞半径
obs_r = obs_length / 2.0 # 障碍物等效碰撞半径
safe_dist = 0.05 # 额外安全距离
# 道路边界参数,考虑车辆宽度
road_y_min = lane1_y - lane_width / 2 # 道路左边界
road_y_max = lane2_y + lane_width / 2 # 道路右边界
vehicle_y_min = road_y_min + vehicle_width / 2 # 车辆中心最小y坐标
vehicle_y_max = road_y_max - vehicle_width / 2 # 车辆中心最大y坐标
4.4 构建MPC优化问题
4.4.1 目标函数设计
使用CasADi的Opti接口构建MPC优化问题,首先定义决策变量和参数:
python
# 创建优化器实例
opti = ca.Opti()
# 定义决策变量
X = opti.variable(nx, Np + 1) # 状态变量,包括初始状态
U = opti.variable(nu, Np) # 控制变量
# 定义参数(当前状态和参考值)
# [当前状态(4), 目标y位置(1), 目标速度(1), 障碍物x(1), 障碍物y(1), 上一时刻航向角速度(1)]
P = opti.parameter(nx + 5)
然后构建目标函数,包括状态误差惩罚、控制输入惩罚和平滑性惩罚:
python
# 构建目标函数
obj = 0
# 状态追踪误差和控制输入惩罚
for k in range(Np):
# 参考状态(只关心y位置和速度跟踪,x方向自由前进)
x_ref_k = ca.vertcat(X[0, k], P[nx], 0, P[nx+1])
# 状态误差代价
state_error = X[:, k] - x_ref_k
obj += ca.mtimes([state_error.T, Q, state_error])
# 控制输入代价
obj += ca.mtimes([U[:, k].T, R, U[:, k]])
# 终端代价
x_ref_N = ca.vertcat(X[0, Np], P[nx], 0, P[nx+1])
terminal_error = X[:, Np] - x_ref_N
obj += ca.mtimes([terminal_error.T, P, terminal_error])
# 添加航向角速度变化惩罚,使转向更平滑
psi_dot_list = []
for k in range(Np):
current_psi_dot = (X[3, k] / L) * ca.tan(U[1, k])
psi_dot_list.append(current_psi_dot)
# 惩罚与上一控制周期航向角速度的变化
obj += w_psi_dot_change * (psi_dot_list[0] - P[nx+4])**2
# 惩罚预测时域内的航向角速度变化
for k in range(1, Np):
obj += w_psi_dot_change * (psi_dot_list[k] - psi_dot_list[k-1])**2
# 设置优化目标
opti.minimize(obj)
4.4.2 约束条件设计
添加系统的各种约束条件:
python
# 初始状态约束
opti.subject_to(X[:, 0] == P[:nx])
# 系统动力学约束
for k in range(Np):
opti.subject_to(X[:, k+1] == f(X[:, k], U[:, k]))
# 控制输入约束
opti.subject_to(opti.bounded(a_min, U[0, :], a_max)) # 加速度约束
opti.subject_to(opti.bounded(delta_min, U[1, :], delta_max)) # 转向角约束
# 状态约束
opti.subject_to(opti.bounded(v_min, X[3, :], v_max)) # 速度约束
# 道路边界约束
opti.subject_to(opti.bounded(vehicle_y_min, X[1, :], vehicle_y_max))
# 碰撞避免约束
for k in range(1, Np + 1): # 从k=1开始,因为k=0是当前状态
# 计算车辆与障碍物之间的距离平方
dist_sq = (X[0, k] - P[nx+2])**2 + (X[1, k] - P[nx+3])**2
# 要求距离大于安全距离
min_dist_sq = (veh_r + obs_r + safe_dist)**2
opti.subject_to(dist_sq >= min_dist_sq)
4.4.3 配置求解器
配置优化求解器并为热启动做准备:
python
# 求解器设置
opts = {
'ipopt.print_level': 0, # 抑制详细输出
'print_time': 0, # 不打印求解时间
'ipopt.max_iter': 6000, # 最大迭代次数
'ipopt.tol': 1e-5, # 收敛容差
'ipopt.acceptable_tol': 1e-4, # 可接受收敛容差
'ipopt.mu_init': 1e-2, # 初始互补松弛变量
'ipopt.constr_viol_tol': 1e-4 # 约束违反容差
}
opti.solver('ipopt', opts)
4.5 决策状态机设计
为了实现智能避障变道,我们需要一个决策状态机来决定何时变道、何时返回原车道:
python
# 状态机状态定义
class ManeuverState:
DRIVING_LANE_1 = 0 # 在第一车道正常行驶
CHANGING_TO_LANE_2 = 1 # 正在向第二车道变道
DRIVING_LANE_2 = 2 # 在第二车道行驶
RETURNING_TO_LANE_1 = 3 # 正在返回第一车道
COMPLETED = 4 # 完成整个避障变道任务
# 状态转换参数
evade_trigger_distance = 25.0 # 开始变道的触发距离 (m)
pass_obstacle_distance = obs_length / 2 + vehicle_length / 2 + 3.0 # 通过障碍物的距离阈值
return_trigger_x_offset = vehicle_length / 2 + 6.0 # 触发返回车道的距离阈值
# 状态机逻辑
def update_maneuver_state(state, x_vehicle, y_vehicle, x_obstacle):
"""根据当前状态和车辆与障碍物的相对位置更新状态和目标车道"""
if state == ManeuverState.DRIVING_LANE_1:
# 如果接近障碍物,开始变道
distance_to_obstacle = x_obstacle - x_vehicle
if 0 < distance_to_obstacle < evade_trigger_distance:
return ManeuverState.CHANGING_TO_LANE_2, lane2_y
return state, lane1_y
elif state == ManeuverState.CHANGING_TO_LANE_2:
# 如果已经接近第二车道中心线,认为变道完成
if abs(y_vehicle - lane2_y) < 0.3:
return ManeuverState.DRIVING_LANE_2, lane2_y
return state, lane2_y
elif state == ManeuverState.DRIVING_LANE_2:
# 如果已经超过障碍物一定距离,开始返回第一车道
if x_vehicle > x_obstacle + pass_obstacle_distance:
return ManeuverState.RETURNING_TO_LANE_1, lane1_y
return state, lane2_y
elif state == ManeuverState.RETURNING_TO_LANE_1:
# 如果已经接近第一车道中心线,认为返回完成
if abs(y_vehicle - lane1_y) < 0.3:
return ManeuverState.COMPLETED, lane1_y
return state, lane1_y
else: # COMPLETED
return state, lane1_y
这个状态机根据车辆与障碍物的相对位置,自动决定何时开始变道、何时返回原车道,实现了自主决策功能。与简单的基于时间的变道相比,这种方法更加智能和安全。
4.6 仿真实现
4.6.1 仿真循环设计
下面是实现仿真循环的代码,包括状态机更新、MPC求解和系统状态更新:
python
# 仿真设置
sim_time = 12.0 # 仿真总时间 (s)
t_history = np.arange(0, sim_time, Ts)
n_steps = len(t_history)
# 初始化历史记录数组
x_history = np.zeros((nx, n_steps)) # 状态历史
u_history = np.zeros((nu, n_steps-1)) # 控制输入历史
obs_history = np.zeros((2, n_steps)) # 障碍物位置历史
state_history = np.zeros(n_steps, dtype=int) # 状态机状态历史
target_y_history = np.zeros(n_steps) # 目标车道历史
# 初始状态设置
x_current = np.array([0.0, lane1_y, 0.0, target_v_val * 0.8]) # 初始速度略低于目标
x_history[:, 0] = x_current
obs_x = obs_x_initial
obs_y = obs_y_fixed
obs_history[:, 0] = [obs_x, obs_y]
# 状态机初始化
maneuver_state = ManeuverState.DRIVING_LANE_1
target_y = lane1_y
target_y_history[0] = target_y
state_history[0] = maneuver_state
# 控制器变量
psi_dot_prev = 0.0 # 上一时刻航向角速度,用于平滑控制
# 为热启动准备初始猜测值
U_guess = np.zeros((nu, Np))
X_guess = np.tile(x_current, (Np + 1, 1)).T
print("开始仿真...")
for i in range(n_steps - 1):
# 当前时间
current_time = t_history[i]
# 更新障碍物位置
obs_x = obs_x_initial + obs_speed_x * current_time
obs_history[:, i+1] = [obs_x, obs_y]
# 更新状态机和目标车道
maneuver_state, target_y = update_maneuver_state(
maneuver_state, x_current[0], x_current[1], obs_x
)
state_history[i+1] = maneuver_state
target_y_history[i+1] = target_y
# 设置MPC参数
current_param = np.concatenate([
x_current, # 当前状态 [x, y, psi, v]
[target_y, target_v_val], # 目标y位置和速度
[obs_x, obs_y], # 障碍物位置
[psi_dot_prev] # 上一时刻航向角速度
])
opti.set_value(P, current_param)
# 设置初始猜测值(热启动)
opti.set_initial(X, X_guess)
opti.set_initial(U, U_guess)
# 求解MPC优化问题
try:
sol = opti.solve()
# 提取结果
X_optimal = sol.value(X)
U_optimal = sol.value(U)
# 保存历史数据
u_history[:, i] = U_optimal[:, 0] # 只应用第一个控制输入
# 更新系统状态(使用车辆模型而非简单欧拉积分)
x_next = f(x_current, U_optimal[:, 0]).full().flatten()
x_current = x_next
x_history[:, i+1] = x_current
# 更新初始猜测(热启动)
X_guess = np.hstack((X_optimal[:, 1:], X_optimal[:, -1].reshape(-1, 1)))
U_guess = np.hstack((U_optimal[:, 1:], U_optimal[:, -1].reshape(-1, 1)))
# 更新上一时刻航向角速度
psi_dot_prev = (x_current[3] / L) * np.tan(U_optimal[1, 0])
except Exception as e:
print(f"优化求解失败: {e}")
break
print("仿真完成!")
4.6.2 结果可视化
实现静态图表和动态动画的可视化:

以上代码讲解比较碎片化,
完整代码如下(含可视化代码)
python
import casadi as ca # 导入 CasADi 用于符号计算和自动微分,是构建NMPC的核心库
import numpy as np # 导入 NumPy 用于数值计算,特别是数组操作
import matplotlib.pyplot as plt # 导入 Matplotlib.pyplot 用于绘图
import time # 导入 time 模块,用于计时等操作
from matplotlib.patches import Rectangle # 从 Rectangle 用于绘制车辆和障碍物形状
from matplotlib.transforms import Affine2D # Affine2D 用于车辆的旋转和平移变换
# FuncAnimation 用于创建动画
from matplotlib.animation import FuncAnimation
# --- 中文字体设置 ---
# 尝试设置 Matplotlib 的字体以支持中文显示
try:
# 设置 sans-serif 字体为 'SimHei' (黑体),这是一种常用的中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
# 设置坐标轴负号显示为 False,以便正确显示负号 (在某些中文字体配置下,负号可能显示为方框)
plt.rcParams['axes.unicode_minus'] = False
except Exception as e:
# 如果字体设置失败,打印警告信息
print(f"中文字体设置警告: {e}。如果中文显示不正确,请确保已安装如 SimHei 等中文字体,并正确配置 Matplotlib。")
# --- NMPC (非线性模型预测控制) 参数 ---
Ts = 0.1 # 采样时间 (s):控制器更新和系统离散化的时间间隔
Np = 20 # 预测时域:控制器向前预测的步数
L = 2.5 # 车辆轴距 (m):前轮和后轮轴之间的距离,用于运动学模型
v_max = 20.0 # 最大速度 (m/s)
v_min = -2.0 # 最小速度 (m/s) (允许倒车)
a_max = 3.0 # 最大加速度 (m/s^2)
a_min = -5.0 # 最大减速度 (m/s^2) (即负向加速度的最小值)
delta_max = np.deg2rad(30) # 最大转向角 (rad):将角度从度转换为弧度
delta_min = -delta_max # 最小转向角 (rad):通常与最大转向角对称
# 车辆绘图尺寸
vehicle_plot_length = 4.0 # 绘图用车辆长度 (m)
vehicle_plot_width = 1.8 # 绘图用车辆宽度 (m)
nx = 4 # 状态量数量:[x (全局坐标系X位置), y (全局坐标系Y位置), psi (车辆航向角), v (车辆速度)]
nu = 2 # 控制量数量:[a (加速度), delta (前轮转向角)]
# --- 权重矩阵调整 ---
# 状态权重矩阵 Q: 对应状态 [x, y, psi, v] 的误差惩罚
# Q矩阵参数说明:
# Q[0,0] = 1.0: x方向位置权重
# - 增大:车辆会更严格地跟踪x方向的参考轨迹
# - 减小:车辆在x方向有更大的自由度,可以更灵活地调整
# Q[1,1] = 100.0: y方向位置权重
# - 增大:车辆会更严格地保持在车道中心,减少横向偏移
# - 减小:车辆在y方向有更大的自由度,可以更灵活地变道
# Q[2,2] = 0.5: 航向角权重
# - 增大:车辆会更严格地保持期望航向角,转向更平滑
# - 减小:车辆可以更灵活地改变航向,但可能导致转向不够平滑
# Q[3,3] = 1.0: 速度权重
# - 增大:车辆会更严格地跟踪目标速度
# - 减小:车辆在速度控制上更灵活,但可能导致速度波动
Q = np.diag([1.0, 100.0, 0.5, 1.0])
# 控制权重矩阵 R: 对应控制量 [a, delta] 的变化惩罚
# R矩阵参数说明:
# R[0,0] = 0.1: 加速度权重
# - 增大:加速度变化更平滑,但响应可能变慢
# - 减小:加速度变化更灵活,但可能导致控制不够平滑
# R[1,1] = 1000.0: 转向角权重
# - 增大:转向更平滑,但可能导致转向响应不够及时
# - 减小:转向更灵活,但可能导致转向不够平滑
R = np.diag([0.1, 1000.0])
# 终端权重因子:用于增强终端状态的跟踪精度
# 增大:更注重终端状态的准确性,但可能导致中间过程不够平滑
# 减小:更注重整个过程的平滑性,但终端状态可能不够准确
P_factor = 0.01
P = Q * P_factor
# 航向角速度变化惩罚权重
# 增大:转向更平滑,但可能导致转向响应不够及时
# 减小:转向更灵活,但可能导致转向不够平滑
w_psi_dot_change = 18000.0
# --- 道路参数 ---
lane_width = 3.5 # 车道宽度 (m)
lane1_y = 0.0 # 第一车道中心线 y 坐标 (m)
lane2_y = lane_width # 第二车道中心线 y 坐标 (m)
# --- 道路边界参数 ---
road_y_min_abs = lane1_y - lane_width / 2 # 道路绝对下边界
road_y_max_abs = lane2_y + lane_width / 2 # 道路绝对上边界
# 车辆中心Y坐标的约束,考虑车辆宽度
vehicle_y_constraint_lower = road_y_min_abs + vehicle_plot_width / 2
vehicle_y_constraint_upper = road_y_max_abs - vehicle_plot_width / 2
# --- 障碍物参数 ---
obs_x_initial = 35.0 # 障碍物初始 x 坐标 (m) (中心点)
obs_y_fixed = lane1_y # 障碍物 y 坐标 (m) (中心点, 假设障碍物在第一车道中心)
obs_speed_x = 4.0 # 障碍物向右移动的速度 (m/s)
obs_length = vehicle_plot_length # 障碍物长度 (m)
obs_width = vehicle_plot_width # 障碍物宽度 (m)
obs_color = 'orange' # 障碍物颜色
# 碰撞检测参数
veh_r = vehicle_plot_length / 2.5 # 车辆等效碰撞半径 (m)
obs_effective_collision_radius = obs_length / 2.0 # 障碍物等效碰撞半径 (m)
# 安全距离参数说明:
# 增大:车辆会与障碍物保持更大的距离,更安全但可能导致变道不够及时
# 减小:车辆可以更接近障碍物,变道更及时但安全性降低
safe_dist = 0.05
target_v_val = 10.0 # 目标巡航速度 (m/s)
# --- CasADi 符号变量与模型 ---
# 定义状态变量
x_sym = ca.SX.sym('x_s') # x位置
y_sym = ca.SX.sym('y_s') # y位置
psi_sym = ca.SX.sym('psi_s') # 航向角
v_sym = ca.SX.sym('v_s') # 速度
states_sym = ca.vertcat(x_sym, y_sym, psi_sym, v_sym)
# 定义控制变量
a_sym = ca.SX.sym('a_s') # 加速度
delta_sym = ca.SX.sym('delta_s') # 转向角
controls_sym = ca.vertcat(a_sym, delta_sym)
# 定义车辆运动学模型
rhs = ca.vertcat(v_sym * ca.cos(psi_sym), # x方向速度
v_sym * ca.sin(psi_sym), # y方向速度
(v_sym / L) * ca.tan(delta_sym), # 航向角变化率
a_sym) # 加速度
f_discrete = ca.Function('f_discrete', [states_sym, controls_sym], [states_sym + Ts * rhs])
# --- NMPC 问题构建 ---
opti = ca.Opti()
X_dv = opti.variable(nx, Np + 1) # 状态变量
U_dv = opti.variable(nu, Np) # 控制变量
P_param = opti.parameter(nx + 5) # 参数:[x_curr (nx), target_y, target_v, obs_x_center, obs_y_center, prev_psi_dot]
# 构建目标函数
obj = 0
for k in range(Np):
# 目标x设为当前预测的x,不强制x方向的跟踪,让车辆自由前进
x_ref_k = ca.vertcat(X_dv[0, k], P_param[nx], 0, P_param[nx+1]) # 目标航向角为0
obj += ca.mtimes([(X_dv[:, k] - x_ref_k).T, Q, (X_dv[:, k] - x_ref_k)]) # 状态误差惩罚
obj += ca.mtimes([U_dv[:, k].T, R, U_dv[:, k]]) # 控制量惩罚
# 终端状态惩罚
terminal_state_ref = ca.vertcat(X_dv[0, Np], P_param[nx], 0, P_param[nx+1])
terminal_state_error = X_dv[:, Np] - terminal_state_ref
obj += ca.mtimes([terminal_state_error.T, P, terminal_state_error])
# 航向角速度变化惩罚
psi_dot_preds_list = []
for k_pd in range(Np):
current_psi_dot_expr = (X_dv[3, k_pd] / L) * ca.tan(U_dv[1, k_pd])
psi_dot_preds_list.append(current_psi_dot_expr)
if Np > 0:
obj += w_psi_dot_change * (psi_dot_preds_list[0] - P_param[nx+4])**2
for k_cost in range(1, Np):
obj += w_psi_dot_change * (psi_dot_preds_list[k_cost] - psi_dot_preds_list[k_cost-1])**2
opti.minimize(obj)
# 添加约束条件
opti.subject_to(X_dv[:, 0] == P_param[:nx]) # 初始状态约束
for k in range(Np):
opti.subject_to(X_dv[:, k+1] == f_discrete(X_dv[:, k], U_dv[:, k])) # 系统动力学约束
# 控制量约束
opti.subject_to(opti.bounded(a_min, U_dv[0, :], a_max)) # 加速度约束
opti.subject_to(opti.bounded(delta_min, U_dv[1, :], delta_max)) # 转向角约束
opti.subject_to(opti.bounded(v_min, X_dv[3, :], v_max)) # 速度约束
# 碰撞约束与道路边界约束
for k in range(1, Np + 1): # 从 k=1 开始,因为 k=0 是当前状态
# 车辆中心预测位置: X_dv[0, k], X_dv[1, k]
# 1. 碰撞约束
dist_sq_obs = (X_dv[0, k] - P_param[nx+2])**2 + \
(X_dv[1, k] - P_param[nx+3])**2
min_dist_sq_allowed = (veh_r + obs_effective_collision_radius + safe_dist)**2
opti.subject_to(dist_sq_obs >= min_dist_sq_allowed)
# 2. 道路边界约束
opti.subject_to(X_dv[1, k] >= vehicle_y_constraint_lower)
opti.subject_to(X_dv[1, k] <= vehicle_y_constraint_upper)
# 求解器设置
opts = {
'ipopt.print_level': 0, 'print_time': 0, 'ipopt.max_iter': 6000,
'ipopt.tol': 1e-5, 'ipopt.acceptable_tol': 1e-4,
'ipopt.mu_init': 1e-2, 'ipopt.constr_viol_tol': 1e-4
}
opti.solver('ipopt', opts)
# --- 仿真循环 ---
sim_time = 12.0 # 可以适当延长仿真时间观察返回车道行为
t_history = np.arange(0, sim_time, Ts)
x_history = np.zeros((nx, len(t_history)))
u_history = np.zeros((nu, len(t_history)-1))
target_y_history = np.zeros(len(t_history))
obs_x_center_history = np.zeros(len(t_history))
obs_y_center_history = np.zeros(len(t_history))
# 初始状态设置
x_current = np.array([0.0, lane1_y, 0.0, target_v_val * 0.8])
x_history[:, 0] = x_current
target_y_history[0] = lane1_y
obs_x_center_history[0] = obs_x_initial
obs_y_center_history[0] = obs_y_fixed
psi_dot_at_last_interval = 0.0
U_guess_current = np.zeros((nu, Np))
X_guess_current = np.tile(x_current, (Np + 1, 1)).T
# 初始控制猜测值设置
num_accel_steps = Np // 2
if x_current[3] < target_v_val and num_accel_steps > 0:
desired_avg_accel = (target_v_val - x_current[3]) / (num_accel_steps * Ts)
applied_accel = np.clip(desired_avg_accel, a_min, a_max)
U_guess_current[0, :num_accel_steps] = applied_accel
elif x_current[3] > target_v_val and num_accel_steps > 0:
desired_avg_decel = (target_v_val - x_current[3]) / (num_accel_steps * Ts)
applied_decel = np.clip(desired_avg_decel, a_min, a_max)
U_guess_current[0, :num_accel_steps] = applied_decel
# 初始状态预测
temp_x_guess = x_current.copy()
X_guess_current[:, 0] = temp_x_guess
for k_rollout in range(Np):
u_rollout = U_guess_current[:, k_rollout]
temp_x_next_full = f_discrete(temp_x_guess, u_rollout)
temp_x_guess = temp_x_next_full.full().flatten()
X_guess_current[:, k_rollout+1] = temp_x_guess
# 状态机参数设置
maneuver_state = "DRIVING_LANE_1"
current_target_y = lane1_y
evade_trigger_distance = 25.0 # 触发变道的距离阈值
pass_obstacle_distance = obs_length / 2 + vehicle_plot_length / 2 + 3.0 # 通过障碍物的距离阈值
return_trigger_x_offset = vehicle_plot_length / 2 + 6.0 # 触发返回车道的距离阈值
stabilize_distance_after_return = 15.0 # 返回车道后的稳定距离
print("开始仿真...")
for i in range(len(t_history) - 1):
vehicle_x_position = x_current[0]
current_sim_time_step = t_history[i]
# 更新障碍物位置
current_obs_x_center = obs_x_initial + obs_speed_x * current_sim_time_step
current_obs_y_center = obs_y_fixed
obs_x_center_history[i] = current_obs_x_center
obs_y_center_history[i] = current_obs_y_center
obstacle_front_x = current_obs_x_center - obs_length / 2
vehicle_front_x_ref = vehicle_x_position
# 状态机逻辑
if maneuver_state == "DRIVING_LANE_1":
current_target_y = lane1_y
if vehicle_front_x_ref > obstacle_front_x - evade_trigger_distance and \
vehicle_front_x_ref < current_obs_x_center + obs_length :
maneuver_state = "CHANGING_TO_LANE_2"
print(f"t={t_history[i]:.1f}s: State -> CHANGING_TO_LANE_2 (veh_x={vehicle_x_position:.1f}m, obs_front_x={obstacle_front_x:.1f}m)")
elif maneuver_state == "CHANGING_TO_LANE_2":
current_target_y = lane2_y
if vehicle_front_x_ref > current_obs_x_center + pass_obstacle_distance:
maneuver_state = "DRIVING_LANE_2"
print(f"t={t_history[i]:.1f}s: State -> DRIVING_LANE_2 (veh_x={vehicle_x_position:.1f}m, obs_center_x={current_obs_x_center:.1f}m)")
elif maneuver_state == "DRIVING_LANE_2":
current_target_y = lane2_y
if vehicle_front_x_ref > current_obs_x_center + return_trigger_x_offset:
maneuver_state = "RETURNING_TO_LANE_1"
print(f"t={t_history[i]:.1f}s: State -> RETURNING_TO_LANE_1 (veh_x={vehicle_x_position:.1f}m, obs_center_x={current_obs_x_center:.1f}m)")
elif maneuver_state == "RETURNING_TO_LANE_1":
current_target_y = lane1_y
if vehicle_front_x_ref > current_obs_x_center + return_trigger_x_offset + stabilize_distance_after_return and \
abs(x_current[1] - lane1_y) < 0.2 and \
abs(x_current[3] - target_v_val) < 1.5 :
maneuver_state = "COMPLETED_MANEUVER"
print(f"t={t_history[i]:.1f}s: State -> COMPLETED_MANEUVER (veh_x={vehicle_x_position:.1f}m, y={x_current[1]:.2f}m, obs_center_x={current_obs_x_center:.1f}m)")
elif maneuver_state == "COMPLETED_MANEUVER":
current_target_y = lane1_y
target_y_history[i] = current_target_y
if i == len(t_history) - 2:
target_y_history[i+1] = current_target_y
# 设置NMPC求解器参数
current_obstacle_center_for_nmpc = np.array([current_obs_x_center, current_obs_y_center])
param_psi_dot_val = psi_dot_at_last_interval
opti.set_value(P_param, np.concatenate([x_current,
[current_target_y],
[target_v_val],
current_obstacle_center_for_nmpc,
[param_psi_dot_val]]))
opti.set_initial(X_dv, X_guess_current)
opti.set_initial(U_dv, U_guess_current)
try:
# 求解NMPC问题
sol = opti.solve()
u_optimal_sequence = sol.value(U_dv)
x_predicted_sequence = sol.value(X_dv)
u_apply = u_optimal_sequence[:, 0]
U_guess_current = np.hstack((u_optimal_sequence[:, 1:], u_optimal_sequence[:, -1].reshape(-1,1)))
X_guess_current = np.hstack((x_predicted_sequence[:, 1:], x_predicted_sequence[:, -1].reshape(-1,1)))
except Exception as e:
print(f"求解器在步骤 {i} (t={t_history[i]:.1f}s) 失败: {e}。应用零控制或前一控制。")
u_apply = np.array([0.0, 0.0]) if i == 0 else u_history[:, i-1]
# 当求解失败时,重置猜测值
U_guess_current = np.zeros((nu, Np))
temp_x_guess_fail = x_current.copy()
current_X_for_reset = temp_x_guess_fail.copy()
X_guess_reset = np.zeros_like(X_guess_current)
X_guess_reset[:,0] = current_X_for_reset
for k_reset in range(Np):
u_reset = U_guess_current[:, k_reset]
temp_f_val = f_discrete(current_X_for_reset, u_reset)
if isinstance(temp_f_val, ca.DM):
current_X_for_reset = temp_f_val.full().flatten()
else:
current_X_for_reset = ca.evalf(f_discrete(current_X_for_reset, u_reset)).full().flatten()
X_guess_reset[:, k_reset+1] = current_X_for_reset
X_guess_current = X_guess_reset
# 更新控制历史
u_history[:, i] = u_apply
# 计算航向角速度
current_v_for_psi_dot = x_current[3]
current_delta_for_psi_dot = u_apply[1]
psi_dot_at_last_interval = (current_v_for_psi_dot / L) * np.tan(current_delta_for_psi_dot)
# 更新系统状态
x_next_full = f_discrete(x_current, u_apply)
x_next = x_next_full.full().flatten()
x_next[2] = np.arctan2(np.sin(x_next[2]), np.cos(x_next[2])) # 归一化角度
x_next[3] = np.clip(x_next[3], v_min, v_max) # 限制速度
x_history[:, i+1] = x_next
x_current = x_next
# 更新最后一个时间步的障碍物位置
last_time_step = t_history[-1]
obs_x_center_history[-1] = obs_x_initial + obs_speed_x * last_time_step
obs_y_center_history[-1] = obs_y_fixed
# 更新最后一个时间步的目标车道
if maneuver_state == "COMPLETED_MANEUVER" or maneuver_state == "RETURNING_TO_LANE_1":
target_y_history[-1] = lane1_y
elif maneuver_state == "DRIVING_LANE_2":
target_y_history[-1] = lane2_y
else:
target_y_history[-1] = current_target_y
print("仿真数据生成完毕,开始生成动画...")
# --- 动画与绘图 ---
fig, axs = plt.subplots(4, 1, figsize=(12, 15), sharex=True, gridspec_kw={'height_ratios': [3, 1, 1, 1]})
plt.subplots_adjust(hspace=0.3)
# 设置第一个子图(车辆轨迹)
axs[0].set_xlabel('x 坐标 (m)')
axs[0].set_ylabel('y 坐标 (m)')
axs[0].set_title('NMPC 车辆避障与换道仿真动画 (参数优化后)')
# 绘制道路边界和车道线
axs[0].axhline(road_y_min_abs, color='k', linestyle='-', lw=1.5, label='道路下边界')
axs[0].axhline(road_y_max_abs, color='k', linestyle='-', lw=1.5, label='道路上边界')
axs[0].axhline(lane1_y, color='lightgray', linestyle=':', lw=1, label='第一车道中心')
axs[0].axhline(lane2_y, color='lightgray', linestyle=':', lw=1, label='第二车道中心')
# 创建障碍物图形
obstacle_bottom_left_x_init = obs_x_center_history[0] - obs_length / 2
obstacle_bottom_left_y_init = obs_y_center_history[0] - obs_width / 2
obstacle_shape_anim = Rectangle((obstacle_bottom_left_x_init, obstacle_bottom_left_y_init),
obs_length, obs_width,
facecolor=obs_color, alpha=0.7, label='障碍物',
edgecolor='black', lw=0.5, zorder=5)
axs[0].add_artist(obstacle_shape_anim)
# 设置网格和坐标轴
axs[0].grid(True, linestyle=':', alpha=0.7)
axs[0].axis('equal')
# 设置显示范围
min_x_traj, max_x_traj = np.min(x_history[0,:]), np.max(x_history[0,:])
max_obs_x_final = obs_x_initial + obs_speed_x * t_history[-1] + obs_length / 2
min_obs_x_initial = obs_x_initial - obs_length / 2
axs[0].set_xlim(min(min_x_traj, min_obs_x_initial) - 10, max(max_x_traj, max_obs_x_final) + 20)
axs[0].set_ylim(road_y_min_abs - lane_width*0.5, road_y_max_abs + lane_width*0.5)
# 创建车辆图形
vehicle_rect_anim = Rectangle((-vehicle_plot_length / 2, -vehicle_plot_width / 2), vehicle_plot_length, vehicle_plot_width,
facecolor='cornflowerblue', alpha=0.6, edgecolor='black', lw=0.7, zorder=10)
arrow_anim_line, = axs[0].plot([], [], lw=1.5, color='darkblue', zorder=11)
rear_axle_path_line, = axs[0].plot([], [], color='royalblue', linestyle='-', lw=1.0, alpha=0.7, label='车辆轨迹 (参考点)')
axs[0].plot([],[], color='lime', linestyle='--', lw=1.0, alpha=0.9, label='目标Y轨迹')
axs[0].legend(loc='upper left', fontsize='small')
# 创建时间文本显示
time_text = axs[0].text(0.02, 0.95, '', transform=axs[0].transAxes, fontsize='medium',
bbox=dict(boxstyle="round,pad=0.3", fc="wheat", ec="black", lw=0.5, alpha=0.7))
# 设置速度子图
axs[1].set_ylabel('速度 (m/s)')
axs[1].axhline(target_v_val, color='green', linestyle='--', label='目标速度')
line_v, = axs[1].plot([], [], lw=1.5, color='purple', label='实际速度')
axs[1].legend(fontsize='small', loc='lower right')
axs[1].grid(True, linestyle=':', alpha=0.7)
axs[1].set_ylim(min(v_min, np.min(x_history[3, :])) -1, max(v_max, np.max(x_history[3, :])) + 1)
# 设置航向角子图
axs[2].set_ylabel('航向角 (度)')
line_psi, = axs[2].plot([], [], lw=1.5, color='teal', label='实际航向角')
axs[2].legend(fontsize='small', loc='lower right')
axs[2].grid(True, linestyle=':', alpha=0.7)
min_psi_deg = np.rad2deg(np.min(x_history[2, :])) if len(x_history[2,:]) > 0 else -10
max_psi_deg = np.rad2deg(np.max(x_history[2, :])) if len(x_history[2,:]) > 0 else 10
margin_psi = 10
if abs(max_psi_deg - min_psi_deg) < 1e-6 :
min_psi_deg -= margin_psi
max_psi_deg += margin_psi
axs[2].set_ylim(min_psi_deg - margin_psi, max_psi_deg + margin_psi)
# 设置加速度和转向角子图
axs[3].set_xlabel('时间 (s)')
axs[3].set_ylabel('加速度 (m/s^2)', color='orangered')
line_a, = axs[3].plot([], [], lw=1.5, color='orangered', label='加速度')
axs[3].tick_params(axis='y', labelcolor='orangered')
axs[3].legend(fontsize='small', loc='upper left')
axs[3].grid(True, linestyle=':', alpha=0.7)
axs[3].set_ylim(a_min - 1, a_max + 1)
# 创建双y轴显示转向角
ax3_twin = axs[3].twinx()
ax3_twin.set_ylabel('转向角 (度)', color='darkgoldenrod')
line_delta, = ax3_twin.plot([], [], lw=1.5, color='darkgoldenrod', linestyle='--', label='转向角')
ax3_twin.tick_params(axis='y', labelcolor='darkgoldenrod')
ax3_twin.legend(fontsize='small', loc='upper right')
ax3_twin.set_ylim(np.rad2deg(delta_min) - 5, np.rad2deg(delta_max) + 5)
# 绘制目标轨迹
axs[0].plot(x_history[0,:len(target_y_history)], target_y_history, color='lime', linestyle='--', lw=1.0, alpha=0.9, label='目标Y轨迹')
axs[0].legend(loc='upper left', fontsize='small')
# 动画初始化函数
def init():
axs[0].add_patch(vehicle_rect_anim)
obs_bl_x_init = obs_x_center_history[0] - obs_length / 2
obs_bl_y_init = obs_y_center_history[0] - obs_width / 2
obstacle_shape_anim.set_xy((obs_bl_x_init, obs_bl_y_init))
arrow_anim_line.set_data([], [])
rear_axle_path_line.set_data([], [])
time_text.set_text('')
line_v.set_data([], [])
line_psi.set_data([], [])
line_a.set_data([], [])
line_delta.set_data([], [])
return [vehicle_rect_anim, arrow_anim_line, rear_axle_path_line, time_text,
line_v, line_psi, line_a, line_delta, obstacle_shape_anim]
# 动画更新函数
def update(frame):
# 更新车辆位置和姿态
veh_center_x = x_history[0, frame]
veh_center_y = x_history[1, frame]
psi_rad = x_history[2, frame]
# 更新车辆图形变换
transform = Affine2D().rotate_around(0, 0, psi_rad) + Affine2D().translate(veh_center_x, veh_center_y) + axs[0].transData
vehicle_rect_anim.set_transform(transform)
# 更新车辆方向箭头
arrow_length = vehicle_plot_length * 0.4
arrow_end_x = veh_center_x + arrow_length * np.cos(psi_rad)
arrow_end_y = veh_center_y + arrow_length * np.sin(psi_rad)
arrow_anim_line.set_data([veh_center_x, arrow_end_x], [veh_center_y, arrow_end_y])
# 更新车辆轨迹
rear_axle_path_line.set_data(x_history[0, :frame+1], x_history[1, :frame+1])
# 更新障碍物位置
current_frame_obs_x_center = obs_x_center_history[frame]
current_frame_obs_y_center = obs_y_center_history[frame]
obstacle_bottom_left_x = current_frame_obs_x_center - obs_length / 2
obstacle_bottom_left_y = current_frame_obs_y_center - obs_width / 2
obstacle_shape_anim.set_xy((obstacle_bottom_left_x, obstacle_bottom_left_y))
# 更新状态显示
current_display_maneuver_state = "UNKNOWN"
target_y_idx = min(frame, len(target_y_history) - 1)
current_target_y_for_display = target_y_history[target_y_idx]
veh_x_pos_frame = x_history[0, frame]
frame_obs_x_center = obs_x_center_history[frame]
frame_obs_front_x = frame_obs_x_center - obs_length / 2
# 更新状态显示文本
actual_y_pos = x_history[1,frame]
if abs(actual_y_pos - lane1_y) < 0.25 and \
veh_x_pos_frame > frame_obs_x_center + return_trigger_x_offset + stabilize_distance_after_return * 0.8 and \
abs(current_target_y_for_display - lane1_y) < 0.1 :
current_display_maneuver_state = "完成机动"
elif abs(current_target_y_for_display - lane1_y) < 0.1:
if veh_x_pos_frame < frame_obs_front_x - evade_trigger_distance * 0.5 :
current_display_maneuver_state = "第一车道行驶"
elif veh_x_pos_frame > frame_obs_x_center + pass_obstacle_distance :
current_display_maneuver_state = "返回第一车道"
else:
current_display_maneuver_state = "第一车道 (近障)"
elif abs(current_target_y_for_display - lane2_y) < 0.1:
if veh_x_pos_frame < frame_obs_x_center + pass_obstacle_distance * 0.9 :
current_display_maneuver_state = "变道至第二车道"
else:
current_display_maneuver_state = "第二车道行驶"
# 更新状态文本
time_text.set_text(f'时间: {t_history[frame]:.1f} s\n速度: {x_history[3, frame]:.2f} m/s\n状态: {current_display_maneuver_state}\n目标Y: {current_target_y_for_display:.1f}m\n实际Y: {actual_y_pos:.2f}m')
# 更新速度曲线
line_v.set_data(t_history[:frame+1], x_history[3, :frame+1])
# 更新航向角曲线
line_psi.set_data(t_history[:frame+1], np.rad2deg(x_history[2, :frame+1]))
# 更新控制量曲线
if frame < len(u_history[0,:]):
time_for_controls = t_history[:frame+1]
control_slice_a = u_history[0, :frame+1]
control_slice_delta = u_history[1, :frame+1]
line_a.set_data(time_for_controls, control_slice_a)
line_delta.set_data(time_for_controls, np.rad2deg(control_slice_delta))
elif frame > 0 :
time_for_controls = t_history[:frame]
control_slice_a = u_history[0, :frame]
control_slice_delta = u_history[1, :frame]
line_a.set_data(time_for_controls, control_slice_a)
line_delta.set_data(time_for_controls, np.rad2deg(control_slice_delta))
else:
line_a.set_data([], [])
line_delta.set_data([], [])
return [vehicle_rect_anim, arrow_anim_line, rear_axle_path_line, time_text,
line_v, line_psi, line_a, line_delta, obstacle_shape_anim]
# 创建动画
num_frames = len(t_history)
ani = FuncAnimation(fig, update, frames=num_frames,
init_func=init, blit=True, interval=Ts*1000, repeat=False)
# 保存动画
animation_filename = 'NMPC_避障变道仿真动画_v6_优化边界.gif'
try:
ani.save(animation_filename, writer='pillow', fps=int(1/Ts))
print(f"动画已保存为: {animation_filename}")
except Exception as e:
print(f"保存动画失败: {e}")
print("请确保已安装 Pillow (for GIF) 或 FFmpeg (for MP4),并将其添加到系统路径。")
print("如果问题持续,尝试在 FuncAnimation 中设置 blit=False。")
plt.show()
4.7 参数调优与性能分析
MPC控制器的性能很大程度上取决于参数的选择。下面分析一些关键参数及其影响:
-
权重矩阵 Q、R 的影响:
- Q[1,1](横向位置权重):增大可使车辆更精确地跟踪目标车道,但可能导致转向过激
- R[1,1](转向角权重):增大可使转向更平滑,但可能降低车道跟踪精度
- 平衡Q和R是关键,我们的实现中使用较大的转向角权重(R[1,1]=1000.0)确保平滑转向
-
航向角速度变化惩罚 w_psi_dot_change:
- 这个参数对转向平滑性至关重要
- 增大会使转向更平滑,但可能降低响应速度
- 减小会使转向更灵活,但可能出现转向抖动
-
预测时域 Np:
- 增大可提高预见性,使变道更平滑,但计算量增加
- 减小可降低计算负担,但可能导致近视行为
- 我们选择了适中的Np=20,在预见性和计算效率之间取得平衡
-
安全距离参数:
- 增大安全距离会使避障更加保守,安全裕度更大
- 减小安全距离允许更激进的避障,但安全裕度降低
- 我们的实现使用较小的安全距离(safe_dist=0.05),依靠碰撞半径保证安全
-
状态机参数:
- evade_trigger_distance:决定何时开始变道,增大会提前启动变道
- pass_obstacle_distance:决定何时可以返回原车道,增大会延迟返回
- 这些参数需要根据车辆速度和障碍物速度进行调整
通过仿真实验,我们可以观察到以下结果:
- 避障效果:控制器能够成功检测到前方障碍物,并在合适的时机启动变道
- 平滑变道:由于航向角速度变化惩罚,变道过程平滑自然
- 速度控制:车辆能够在变道过程中维持接近目标速度的行驶状态
- 返回原车道:超越障碍物后,车辆能够安全地返回原车道
4.8 小结与展望
在本章中,我们实现了一个基于MPC的智能避障变道控制系统,它展示了MPC在处理复杂自动驾驶场景中的优势:
- 预见性控制:MPC能够提前预测车辆和障碍物未来的运动轨迹,规划最优避障路径
- 约束处理:MPC自然地处理了道路边界、车辆物理限制和碰撞避免等约束
- 多目标优化:同时兼顾了车道跟踪、速度控制、操作平滑和安全避障等多个目标
- 自主决策:结合状态机,实现了基于环境感知的自主变道决策
这个控制系统还有以下改进空间:
- 多障碍物避让:扩展系统以处理多个动态障碍物
- 不确定性处理:考虑测量噪声和预测误差,增强鲁棒性
- 计算优化:改进算法以降低计算负担,实现更高的控制频率
- 复杂道路环境:处理弯道、交叉路口等更复杂的道路场景
MPC作为一种强大的预见性控制框架,在自动驾驶领域具有广阔的应用前景。它能够平衡安全性、舒适性和效率,适应复杂多变的驾驶环境,是实现高级自动驾驶的关键技术之一。
通过本教程,我们从MPC的基本概念出发,经过数学建模,最终实现了一个实用的避障变道控制系统。希望这个案例能帮助读者理解MPC的工作原理和应用方法,为进一步探索和应用MPC技术奠定基础。