4. 二次规划(QP)基础:QP 问题标准形式,求解器介绍(OSQP, qpOASES)

好,咱们进入正题。轨迹平滑这件事,说白了就是找一个既满足约束、又足够顺滑的路径。那怎么找?二次规划(QP)就是最常用的数学工具之一。我个人习惯把 QP 看作一个「带镣铐的优化问题」——目标函数是二次的,约束是线性的,求解起来高效又稳定。

4.1 QP 问题的标准形式

先看标准形式,别被公式吓到。其实就三部分:

minimize     1/2 * x^T * P * x + q^T * x
subject to   l <= A * x <= u

这里:

  • x:你要优化的变量,比如轨迹上的每个点
  • P:一个对称正定矩阵,控制「平滑度」的权重
  • q:线性项,通常跟参考路径有关
  • A:约束矩阵,比如道路边界、速度限制
  • l, u:下界和上界,说白了就是「不能越界」

重点:为什么是 1/2 * x^T * P * x?因为求导后系数刚好消掉,数学上更干净。嗯,这里要注意,P 必须是对称正定的,否则问题可能无解。

我在项目中遇到过一件事:有一次我直接用单位矩阵当 P,结果轨迹虽然平滑,但偏离参考线太远。后来我调整了 P 的对角线元素,让某些维度更「硬」一些,效果就好多了。你想想看,这就像调弹簧的刚度——太软了乱晃,太硬了又颠簸。

4.2 求解器介绍

标准形式搞清楚了,接下来就是谁来算的问题。市面上 QP 求解器不少,但我最常用的就两个:OSQP 和 qpOASES。为啥?一个快,一个稳。

4.2.1 OSQP(Operator Splitting QP)

OSQP 是我个人比较偏爱的求解器。它基于交替方向乘子法(ADMM),说白了就是「分而治之」——把大问题拆成几个小问题,迭代求解。

  • 优点:收敛快,支持稀疏矩阵,适合大规模问题
  • 缺点:对病态问题敏感,需要调参
  • 适用场景:实时路径规划,比如自动驾驶中的局部轨迹优化

我的经验:OSQP 的默认参数通常够用,但如果你发现收敛慢,可以试试调小 rho 值。我曾经在一个高速场景下,因为没调参,求解器卡了 200 多步才收敛——差点把车开到路肩上。

4.2.2 qpOASES

qpOASES 走的是另一条路——基于积极集法(Active Set)。它特别适合小规模、但需要高精度的场景。

  • 优点:精度高,对约束变化响应快
  • 缺点:不适合大规模问题,内存占用高
  • 适用场景:模型预测控制(MPC)中的小规模 QP

避坑指南:我曾经在 qpOASES 里遇到一个坑——如果约束条件变化太剧烈,求解器会频繁重启,导致计算时间暴增。后来我加了一个「约束缓冲带」,让变化平缓一些,问题就解决了。

4.3 两种求解器的对比

特性 OSQP qpOASES
算法原理 ADMM(交替方向乘子法) 积极集法(Active Set)
适用规模 大规模(万级变量) 小规模(千级变量)
收敛速度 快(线性收敛) 中等(二次收敛)
精度 中等 高
内存占用 低 高
实时性 优秀 良好

怎么选?我的建议是:如果你做的是实时轨迹规划,变量多、要求快,选 OSQP;如果你做的是离线优化,或者对精度要求极高,选 qpOASES。当然,两个都试试也没坏处——我经常在同一个项目里来回切换,看哪个更顺眼。

4.4 核心逻辑流程图

下面这张图展示了 QP 求解的核心流程,从问题建模到最终输出轨迹:

QP 求解核心流程 1. 问题建模 2. 构建标准形式 3. 选择求解器 OSQP(ADMM) qpOASES(积极集) 4. 输出平滑轨迹 图:QP 求解流程——从建模到输出

4.5 实际使用示例

光说不练假把式。下面我用 OSQP 写一个简单的轨迹平滑示例。假设我们有 10 个路径点,需要让它们更平滑,同时不能偏离道路边界太远。

import osqp
import numpy as np
from scipy import sparse

# 假设有 10 个路径点,每个点有 x, y 坐标
n = 10
x_ref = np.linspace(0, 10, n)  # 参考路径
y_ref = np.sin(x_ref)          # 带噪声的参考

# 构建 QP 问题
# 目标:最小化加速度(二阶差分)
P = sparse.eye(2*n) * 2.0  # 平滑权重
q = np.zeros(2*n)

# 约束:道路边界(上下各 0.5 米)
A = sparse.eye(2*n)
l = np.concatenate([x_ref - 0.5, y_ref - 0.5])
u = np.concatenate([x_ref + 0.5, y_ref + 0.5])

# 创建求解器
prob = osqp.OSQP()
prob.setup(P, q, A, l, u, verbose=False)

# 求解
res = prob.solve()
x_opt = res.x[:n]
y_opt = res.x[n:]

print("平滑完成!")
print(f"优化前最大偏差:{np.max(np.abs(y_ref - np.sin(x_ref))):.3f}")
print(f"优化后最大偏差:{np.max(np.abs(y_opt - np.sin(x_ref))):.3f}")

小技巧:如果你发现求解结果不理想,可以试试调整 P 矩阵的权重。我一般从 1.0 开始试,然后根据轨迹的平滑度逐步增加。记住,权重越大,轨迹越平滑,但偏离参考路径也越远——这是个 trade-off。

4.6 常见问题与避坑

  • 问题:求解器报错「问题不可行」
    原因:约束太紧,没有可行解
    解决:放宽约束边界,或者检查是否有矛盾约束
  • 问题:求解速度慢
    原因:矩阵规模太大,或者参数没调好
    解决:试试稀疏矩阵,或者调小迭代次数上限
  • 问题:轨迹出现抖动
    原因:权重设置不合理,或者约束变化太剧烈
    解决:增加平滑项的权重,或者加一个低通滤波器

嗯,以上就是 QP 的基础内容。说白了,QP 就是个「带约束的最小二乘」——你只要把问题写成标准形式,剩下的交给求解器就行。我个人觉得,理解 QP 的关键不在于数学推导,而在于知道怎么把实际问题「翻译」成标准形式。这个能力,多做几个项目自然就有了。

可可精品资料库