Skip to content

Latest commit

 

History

History
1119 lines (826 loc) · 43.3 KB

File metadata and controls

1119 lines (826 loc) · 43.3 KB

四旋翼无人机轨迹规划教程

导读

本教程解决一个核心问题:给定一组路径点(Waypoints),如何生成一条光滑、安全且可执行的飞行轨迹? 各章节层层递进:

章节 解决的核心问题 与其他章节的关系
一、光滑轨迹生成基础 什么是"光滑"?用什么数学工具描述轨迹? 定义问题框架,引入多项式轨迹和约束体系
二、Minimum Snap 轨迹优化 如何将"最光滑轨迹"转化为可求解的数学优化问题? 依赖一(约束体系),构建 QP 问题
三、Corridor 约束与时间分配 如何保证轨迹不撞障碍物?各段飞多久? 依赖二(QP 框架),加入不等式约束和工程细节
四、Minimum Snap 闭式求解 不用迭代求解器,能否直接算出最优解? 依赖二(QP 问题),提供高效的解析解方法

与控制教程的关系

  • 本教程的前置知识是控制教程中的"三、微分平坦性"——它解释了为什么最小化 snap(四阶导数)是有物理意义的(snap 对应控制力矩)
  • 本教程的输出(位置/速度/加速度参考轨迹)是控制教程中各控制器(PID、SE(3)、MPC)的输入

阅读建议:按顺序 一 → 四 即可。如果只关心原理不关心实际的工程实现细节,可以跳过三中的时间分配和归一化部分。

规划教程(4 个代码块):

一、光滑轨迹基础 — 5 次多项式插值,P/V/A 曲线

二、Minimum Snap — 多 waypoint 最小 snap 轨迹 + KKT 求解

三、Corridor 约束 — 安全通道 + 障碍物 + 时间分配策略可视化

四、闭式求解 — 闭式解 vs QP 结果对比验证


一、光滑轨迹生成基础

1.1 轨迹生成的条件

轨迹生成需要满足以下条件:

边界条件:

  • 起始位置(确定)
  • 终点位置(确定)

中间条件:

  • 精确位置(确定,即 Waypoints)

个人理解:Waypoints 由前端路径规划算法提供(如 $A^* $$RRT$$RRT^*$ 等),轨迹规划是在这些离散路径点基础上生成光滑的时间参数化曲线。

个人理解:轨迹规划是在路径规划( $A^*$$RRT$ 等给出离散 waypoints)之后的步骤。路径规划解决"去哪"的问题,轨迹规划解决"怎么去"的问题——将离散路径点连成一条满足动力学约束的光滑时间参数化曲线。

1.2 光滑性标准

个人理解:光滑性的度量标准通常是最小化某阶导数(即"输入"的变化率)的平方积分。阶数越高,轨迹越光滑,但多项式次数也越高。

对于四旋翼无人机,关键观察是:

  • 位置的 二阶导数(加速度)对应推力方向
  • 位置的 三阶导数(jerk)对应角速度
  • 位置的 四阶导数(snap)对应控制力矩

因此最小化 snap 等价于最小化控制力矩的变化,保证执行器输出平滑。

个人理解:最优化轨迹的核心思想——优化的是轨迹高阶导数的平方积分(变化率越小越光滑)。关键关系:

  • 若最小化 $K_r$ 阶导数,则每段多项式至少 $2K_r - 1$ 阶( $2K_r$ 个系数)
  • 相邻段需保证 $K_r - 1$ 阶导数连续
  • 例如 minimum snap( $K_r = 4$):每段至少 7 阶多项式,保证 0~3 阶导数连续

个人理解:为什么是 $2K_r$ 次多项式?因为最小化 $K_r$ 阶导数的平方积分,被优化的变量是多项式系数,每段需要 $2K_r$ 个约束(两端各 $K_r$ 个),所以多项式至少需要 $2K_r$ 个系数,即 $2K_r - 1$ 阶。对于 minimum snap( $K_r = 4$),每段至少 7 阶多项式(8个系数);实际中通常取 5 阶或 7 阶。

1.3 最小化 Snap 的轨迹生成关键点

  • 轨迹的约束(高阶约束):状态约束
  • 轨迹在交叉处的连续性:相邻段端点处各阶导数必须相等
  • 轨迹的 4 个导数:系数矩阵和约束
  • 最优化指标:轨迹 4 阶导数平方的积分

光滑直线段处的特殊处理:

个人理解:在直线段处,速度方向不变且加速度为零。但光滑轨迹在直线段两端(转弯处)的速度方向需要平滑过渡,不能出现速度/加速度突变,因此直线段与曲线段的衔接需要特殊处理。

1.4 约束体系

端点约束

各段端点处的 $k$ 阶导数连续性( $k = 0, 1, 2, \ldots$):

$$ f_j^{(k)}(T_i) = X_0^{(k)}, \quad f_j^{(k)}(T_i) = X_j^{(k)} $$

高阶连续性约束

相邻段在交界处的各阶导数必须相等:

$$ f_j^{(k)}(T_i) = f_{j+1}^{(k)}(T_i), \quad k = 0, 1, 2, \ldots, K_r - 1 $$

个人理解:以 minimum snap 为例,需要保证 0~3 阶导数连续(位置、速度、加速度、jerk),共 4 个连续性约束。这些约束确保飞行器经过路径点时不会出现突变的力或力矩。

1.5 五次多项式轨迹示例

对于 minimum jerk($K_r = 3$),每段使用 5 次多项式(6 个参数):

$$ X(t) = p_5 t^5 + p_4 t^4 + p_3 t^3 + p_2 t^2 + p_1 t + p_0 $$

边界条件(6个方程解6个参数):

  • $t = 0$:位置 $a$、速度 $\dot{a}$、加速度 $\ddot{a}$
  • $t = T$:位置 $b$、速度 $\dot{b}$、加速度 $\ddot{b}$

矩阵求解形式:

$$ \begin{bmatrix} 1 & 0 & 0 & 0 & 0 & 0 \ 0 & 1 & 0 & 0 & 0 & 0 \ 0 & 0 & 2 & 0 & 0 & 0 \ 1 & T & T^2 & T^3 & T^4 & T^5 \ 0 & 1 & 2T & 3T^2 & 4T^3 & 5T^4 \ 0 & 0 & 2 & 6T & 12T^2 & 20T^3 \end{bmatrix} \begin{bmatrix} p_0 \ p_1 \ p_2 \ p_3 \ p_4 \ p_5 \end{bmatrix} = \begin{bmatrix} a \ \dot{a} \ \ddot{a} \ b \ \dot{b} \ \ddot{b} \end{bmatrix} $$

1.6 时间参数化方案对比

方案1:相对时间(每段从 $t=0$ 开始)

  • 优势:数值稳定性比较高

方案2:绝对时间(使用全局时间 $T_i$

  • 优势:数学表达清楚

个人理解:方案 2 的致命缺点——如果使用绝对时间,例如 $t = 1000$ 秒,那么 $t^7 = 10^{21}$,远超浮点数精度,数值会爆炸。方案 1 每段从 $t = 0$ 开始,$t$ 的范围始终在段持续时间内,数值可控。

个人理解:实际工程中通常采用方案1(相对时间),因为数值稳定性更重要。绝对时间在 $t$ 较大时会导致 $t^N$ 数值爆炸。例如 $t = 100$ 秒时 $t^7 = 10^{14}$,远超 float 精度。

Python 验证

import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']  # Windows 自带黑体
plt.rcParams['axes.unicode_minus'] = False    # 解决负号显示为方框的问题
# ============================================================
# 五次多项式两点插值 —— 无人机从静止起飞到目标点
# 给定起点和终点的位置、速度、加速度,求解6x6矩阵方程得到多项式系数
# ============================================================

# --- 边界条件设置(模拟无人机垂直起飞) ---
T = 3.0          # 飞行时间 3 秒
# t=0 时刻:地面静止
pos0, vel0, acc0 = 0.0, 0.0, 0.0
# t=T 时刻:到达 10m 高度,悬停(速度和加速度均为零)
posT, velT, accT = 10.0, 0.0, 0.0

# --- 构造 6x6 约束矩阵 ---
# X(t) = p0 + p1*t + p2*t^2 + p3*t^3 + p4*t^4 + p5*t^5
A = np.array([
    [1, 0,   0,       0,         0,          0        ],   # X(0)   = pos0
    [0, 1,   0,       0,         0,          0        ],   # X'(0)  = vel0
    [0, 0,   2,       0,         0,          0        ],   # X''(0) = acc0
    [1, T,   T**2,    T**3,      T**4,       T**5     ],   # X(T)   = posT
    [0, 1,   2*T,     3*T**2,    4*T**3,     5*T**4   ],   # X'(T)  = velT
    [0, 0,   2,       6*T,       12*T**2,    20*T**3  ],   # X''(T) = accT
])
b = np.array([pos0, vel0, acc0, posT, velT, accT])

# --- 求解多项式系数 ---
coeffs = np.linalg.solve(A, b)
print("多项式系数 [p0, p1, p2, p3, p4, p5]:", np.round(coeffs, 4))

# --- 在时间轴上采样并计算 位置/速度/加速度 ---
t = np.linspace(0, T, 200)
pos = sum(coeffs[i] * t**i for i in range(6))
vel = sum(i * coeffs[i] * t**(i-1) for i in range(1, 6))
acc = sum(i * (i-1) * coeffs[i] * t**(i-2) for i in range(2, 6))

# --- 绘图 ---
fig, axes = plt.subplots(3, 1, figsize=(8, 8), sharex=True)
titles = ["位置 (m)", "速度 (m/s)", "加速度 (m/s²)"]
data = [pos, vel, acc]
colors = ["#1f77b4", "#ff7f0e", "#2ca02c"]

for ax, title, y, c in zip(axes, titles, data, colors):
    ax.plot(t, y, color=c, linewidth=2)
    ax.set_ylabel(title, fontsize=12)
    ax.grid(True, alpha=0.3)
    ax.axhline(0, color='gray', linewidth=0.5)

# 标注起点和终点
axes[0].plot(0, pos0, 'ko', markersize=8, label='起飞点')
axes[0].plot(T, posT, 'r*', markersize=12, label='目标悬停点')
axes[0].legend(fontsize=10)

axes[-1].set_xlabel("时间 (s)", fontsize=12)
fig.suptitle("五次多项式轨迹 —— 无人机从地面起飞到 10m 悬停", fontsize=13)
plt.tight_layout()
import os; os.makedirs('assets', exist_ok=True)
plt.savefig('assets/planning1.png', dpi=150, bbox_inches='tight')
plt.show()

运行结果:


二、Minimum Snap 轨迹优化

2.1 多项式轨迹的数学表达

一段 $N$ 阶多项式轨迹:

$$ p(t) = p_0 + p_1 t + p_2 t^2 + \cdots + p_N t^N = \sum_{i=0}^{N} p_i t^i $$

$N+1$ 个参数,写成向量形式 $\mathbf{p} = [p_0, p_1, \ldots, p_N]^T$

位置(0阶):

$$ p(t) = [1, t, t^2, \ldots, t^N] \cdot \mathbf{p} $$

速度(1阶):

$$ v(t) = \dot{p}(t) = [0, 1, 2t, \ldots, Nt^{N-1}] \cdot \mathbf{p} $$

加速度(2阶):

$$ a(t) = \ddot{p}(t) = [0, 0, 2, 6t, \ldots, N(N-1)t^{N-2}] \cdot \mathbf{p} $$

Jerk(3阶,加加速度):

$$ j(t) = p^{(3)}(t) = \left[0, 0, 0, 6, 24t, \ldots, \frac{N!}{(N-3)!}t^{N-3}\right] \cdot \mathbf{p} $$

Snap(4阶):

$$ s(t) = p^{(4)}(t) = \left[0, 0, 0, 0, 24, \ldots, \frac{N!}{(N-4)!}t^{N-4}\right] \cdot \mathbf{p} $$

个人理解:$k$ 阶导数可以统一写为 $p^{(k)}(t) = \mathbf{c}_k(t)^T \mathbf{p}$,其中 $\mathbf{c}_k(t)$ 的第 $i$ 个元素为 $\frac{i!}{(i-k)!} t^{i-k}$(当 $i \geq k$ 时),否则为 0。

2.2 多段轨迹表达

对于 $M$ 段轨迹(通过 $M+1$ 个 waypoints),每段有自己的参数向量:

$$ p_a(t) = \begin{cases} [1, t, t^2, \ldots, t^N]^T \mathbf{p}_1, & t_0 \leq t \leq t_1 \ [1, t, t^2, \ldots, t^N]^T \mathbf{p}_2, & t_1 \leq t \leq t_2 \ \vdots \ [1, t, t^2, \ldots, t^N]^T \mathbf{p}_M, & t_{M-1} \leq t \leq t_M \end{cases} $$

每段轨迹的参数 $\mathbf{p}i = [p{i0}, p_{i1}, \ldots, p_{iN}]^T$ 为第 $i$ 段的 $N+1$ 个系数。

将所有段参数堆叠为一个大向量:

$$ P = [\mathbf{p}_1^T, \mathbf{p}_2^T, \ldots, \mathbf{p}_M^T]^T \in \mathbb{R}^{M(N+1)} $$

2.3 优化目标的选择

不同的优化目标对应不同的光滑性度量:

优化目标 表达式 含义 最低多项式阶数
Minimum Snap $\min \int (p^{(4)}(t))^2 dt$ 最小化力矩变化 7 阶
Minimum Jerk $\min \int (p^{(3)}(t))^2 dt$ 最小化角速度变化 5 阶
Minimum Acce $\min \int (p^{(2)}(t))^2 dt$ 最小化加速度 3 阶

个人理解:一般不选择最小化速度或位置,因为最小化低阶导数会让轨迹过于"懒惰"(趋向于不动),失去实际意义。最小化加速度以上的导数才有物理意义(能量、力、力矩)。

个人理解:对于四旋翼,最常用的是 minimum snap,因为 snap 直接关联到控制力矩输入,最小化 snap 等价于最小化能量消耗和执行器磨损。Minimum jerk 用于对角速度敏感的场景(如搭载相机的航拍无人机)。

2.4 目标函数的解析推导

目标函数 $f(t) = \sum p_i t^i$ 的四阶导数:

$$ f^{(4)}(t) = \sum_{i \geq 4} i(i-1)(i-2)(i-3) \cdot t^{i-4} \cdot p_i $$

其平方为:

$$ (f^{(4)}(t))^2 = \sum_{i \geq 4} \sum_{j \geq 4} i(i-1)(i-2)(i-3) \cdot j(j-1)(j-2)(j-3) \cdot t^{i+j-8} \cdot p_i p_j $$

对第 $k$ 段积分(从 $T_{k-1}$$T_k$):

$$ J_k(T) = \int_{T_{k-1}}^{T_k} (f^{(4)})^2 dt = \sum_{i \geq 4, j \geq 4} \frac{i(i-1)(i-2)(i-3) \cdot j(j-1)(j-2)(j-3)}{i+j-7} (T_k^{i+j-7} - T_{k-1}^{i+j-7}) \cdot p_i p_j $$

写成二次型:

$$ J_k(T) = \mathbf{p}_k^T Q_k \mathbf{p}_k \quad \rightarrow \text{半正定二次型} $$

其中 $Q_k$ 矩阵的第 $(i, j)$ 元素( $i, j \geq 4$)为:

$$ (Q_k)_{ij} = \frac{i(i-1)(i-2)(i-3) \cdot j(j-1)(j-2)(j-3)}{i+j-7} (T_k^{i+j-7} - T_{k-1}^{i+j-7}) $$

个人理解:约束的层级——首先保证 0 阶导数(位置)在交界处连续(即轨迹经过 waypoint),然后逐级添加高阶导数连续约束(速度、加速度、jerk),保证相邻两段在交界处平滑过渡。

2.5 总目标函数

$$ J = \sum_{k=1}^{M} \mathbf{p}_k^T Q_k \mathbf{p}_k = P^T \begin{bmatrix} Q_1 & & \ & \ddots & \ & & Q_M \end{bmatrix} P = P^T \mathbf{Q} P $$

2.6 约束矩阵构造

约束条件分为以下几类:

① 初始点约束(位置、速度、加速度等的等式约束):

个人理解:如果使用相对时间(每段从 $t=0$ 开始),初始点约束在 $t_0 = 0$ 处代入,多项式的低次项系数直接对应初始的位置、速度、加速度值,形式非常简洁。

$$ [1, t_0, t_0^2, \ldots, t_0^N] \mathbf{p}_1 = X_0^{(0)} \quad \text{(位置)} $$

$$ [0, 1, 2t_0, \ldots, Nt_0^{N-1}] \mathbf{p}_1 = X_0^{(1)} \quad \text{(速度)} $$

$$ [0, 0, 2, \ldots, N(N-1)t_0^{N-2}] \mathbf{p}_1 = X_0^{(2)} \quad \text{(加速度)} $$

② 中间第 $i$ 段的位置约束:

$$ p_i(t_i) = p_{i+1}(t_i) = X_i $$

③ 终端约束(位置、速度、加速度等式约束)

$$ [1, t_M, t_M^2, \ldots, t_M^N] \mathbf{p}_M = X_M^{(0)} $$

④ 连续性约束(相邻段在交界处各阶导数相等)

$$ \mathbf{p}_i^{(k)}(t_i) = \mathbf{p}_{i+1}^{(k)}(t_i), \quad k = 0, 1, \ldots, K_r - 1 $$

约束数量统计:

个人理解:约束数量统计(以 minimum snap, $M$ 段为例):起始 PVA = 3 个,终端 PVA = 3 个,中间 waypoint 位置 = $(M-1)$ 个,中间连续性(P, V, A 各 1 个 $\times (M-1)$ 个交界)= $3(M-1)$ 个。总计 $= 6 + 4(M-1)$。当约束数 $<$ 未知参数数时,剩余自由度用于最小化目标函数。

约束类型 数量
起始点 PVA 3
终端 PVA 3
中间点位置($M-1$ 个点) $M - 1$
中间点连续性($K_r$ 阶 $\times$ $(M-1)$ 个交界) $K_r(M-1)$
总计 $6 + (K_r + 1)(M - 1)$

2.7 QP 问题的标准形式

最终问题转化为线性等式约束的二次规划(QP):

$$ \min_{P} \quad P^T \mathbf{Q} P $$

$$ \text{s.t.} \quad A_{eq} P = d_{eq} $$

个人理解:这是一个凸二次规划问题( $\mathbf{Q}$ 半正定),有全局最优解。可以使用标准 QP 求解器(如 OSQP、qpOASES)高效求解。

Python 验证

import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']  # Windows 自带黑体
plt.rcParams['axes.unicode_minus'] = False    # 解决负号显示为方框的问题
# ============================================================
# Minimum Snap 轨迹生成 —— 无人机依次飞过多个航点
# 使用 KKT 系统(拉格朗日乘子法)求解等式约束 QP 的闭式解
# ============================================================

# --- 航点定义(二维:x, y) ---
waypoints = np.array([
    [0.0, 0.0],   # 起点
    [2.0, 2.5],   # 中间点1
    [5.0, 4.0],   # 中间点2
    [8.0, 3.0],   # 中间点3
    [10.0, 5.0],  # 终点
])
M = len(waypoints) - 1  # 4 段
N = 7                    # 7阶多项式(8个系数,minimum snap)

# --- 时间分配(按段长等比例分配,总时间8秒) ---
dists = np.linalg.norm(np.diff(waypoints, axis=0), axis=1)
T_total = 8.0
T_seg = T_total * dists / dists.sum()  # 各段时间

def build_Q_seg(n, T, kr=4):
    """构造单段 snap 代价矩阵 Q(相对时间,t in [0, T])"""
    Q = np.zeros((n+1, n+1))
    for i in range(kr, n+1):
        for j in range(kr, n+1):
            ci = np.prod(range(i, i-kr, -1))   # i!/(i-kr)!
            cj = np.prod(range(j, j-kr, -1))
            Q[i, j] = ci * cj * T**(i+j-2*kr+1) / (i+j-2*kr+1)
    return Q

def poly_coeff_vec(n, t, deriv=0):
    """返回 n 阶多项式在 t 处 deriv 阶导数的系数行向量"""
    c = np.zeros(n+1)
    for i in range(deriv, n+1):
        c[i] = np.prod(range(i, i-deriv, -1)) * t**(i-deriv)
    return c

# --- 构造总 Q 矩阵(分块对角) ---
dim_total = M * (N+1)
Q_all = np.zeros((dim_total, dim_total))
for k in range(M):
    idx = k * (N+1)
    Q_all[idx:idx+N+1, idx:idx+N+1] = build_Q_seg(N, T_seg[k])

# --- 构造等式约束矩阵 Aeq @ P = beq ---
constraints_rows = []
constraints_rhs_x = []
constraints_rhs_y = []

def add_constraint(row_vec, rhs_x, rhs_y):
    constraints_rows.append(row_vec)
    constraints_rhs_x.append(rhs_x)
    constraints_rhs_y.append(rhs_y)

# 起始点约束:位置、速度、加速度、jerk(段0,t=0)
for d in range(4):
    row = np.zeros(dim_total)
    row[0:N+1] = poly_coeff_vec(N, 0, d)
    val = waypoints[0] if d == 0 else np.array([0.0, 0.0])
    add_constraint(row, val[0], val[1])

# 终端约束:位置、速度、加速度、jerk(最后一段,t=T_M)
for d in range(4):
    row = np.zeros(dim_total)
    idx = (M-1)*(N+1)
    row[idx:idx+N+1] = poly_coeff_vec(N, T_seg[-1], d)
    val = waypoints[-1] if d == 0 else np.array([0.0, 0.0])
    add_constraint(row, val[0], val[1])

# 中间航点位置约束 + 连续性约束
for k in range(M-1):
    # 第 k 段终点位置 = waypoints[k+1]
    row = np.zeros(dim_total)
    idx = k * (N+1)
    row[idx:idx+N+1] = poly_coeff_vec(N, T_seg[k], 0)
    add_constraint(row, waypoints[k+1, 0], waypoints[k+1, 1])
    # 连续性:第 k 段终点 = 第 k+1 段起点(0~3阶导数)
    for d in range(4):
        row = np.zeros(dim_total)
        row[idx:idx+N+1] = poly_coeff_vec(N, T_seg[k], d)
        idx2 = (k+1) * (N+1)
        row[idx2:idx2+N+1] = -poly_coeff_vec(N, 0, d)
        add_constraint(row, 0.0, 0.0)

Aeq = np.array(constraints_rows)
beq_x = np.array(constraints_rhs_x)
beq_y = np.array(constraints_rhs_y)

# --- KKT 系统求解:[2Q, A^T; A, 0] [P; lam] = [0; b] ---
n_eq = Aeq.shape[0]
KKT = np.zeros((dim_total + n_eq, dim_total + n_eq))
KKT[:dim_total, :dim_total] = 2 * Q_all
KKT[:dim_total, dim_total:] = Aeq.T
KKT[dim_total:, :dim_total] = Aeq

def solve_axis(beq):
    rhs = np.zeros(dim_total + n_eq)
    rhs[dim_total:] = beq
    sol = np.linalg.solve(KKT, rhs)
    return sol[:dim_total]

Px = solve_axis(beq_x)
Py = solve_axis(beq_y)

# --- 采样轨迹并计算各阶导数 ---
t_all, x_all, y_all = [], [], []
derivs = {d: ([], []) for d in range(5)}  # 0~4阶
t_cum = 0.0
for k in range(M):
    ts = np.linspace(0, T_seg[k], 100)
    idx = k * (N+1)
    px, py = Px[idx:idx+N+1], Py[idx:idx+N+1]
    for d in range(5):
        cx = np.array([poly_coeff_vec(N, t, d) @ px for t in ts])
        cy = np.array([poly_coeff_vec(N, t, d) @ py for t in ts])
        derivs[d][0].extend(cx)
        derivs[d][1].extend(cy)
    t_all.extend(ts + t_cum)
    t_cum += T_seg[k]

# --- 绘图 ---
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# 左图:二维轨迹
ax = axes[0]
ax.plot(derivs[0][0], derivs[0][1], 'b-', linewidth=2, label='Minimum Snap 轨迹')
ax.plot(waypoints[:, 0], waypoints[:, 1], 'ro-', markersize=8, linewidth=1, label='航点')
for i, wp in enumerate(waypoints):
    ax.annotate(f'P{i}', wp, textcoords="offset points", xytext=(5, 8), fontsize=10)
ax.set_xlabel('X (m)'); ax.set_ylabel('Y (m)')
ax.set_title('无人机 Minimum Snap 二维轨迹'); ax.legend(); ax.grid(True, alpha=0.3)
ax.set_aspect('equal')

# 右图:各阶导数随时间变化(仅 x 轴分量)
ax2 = axes[1]
labels = ['位置 (m)', '速度 (m/s)', '加速度 (m/s²)', 'Jerk', 'Snap']
for d in range(5):
    ax2.plot(t_all, derivs[d][0], label=labels[d], alpha=0.85)
ax2.set_xlabel('时间 (s)'); ax2.set_ylabel('X 轴分量')
ax2.set_title('X 轴各阶导数曲线'); ax2.legend(fontsize=8); ax2.grid(True, alpha=0.3)

plt.tight_layout()
import os; os.makedirs('assets', exist_ok=True)
plt.savefig('assets/planning2.png', dpi=150, bbox_inches='tight')
plt.show()

运行结果:


三、Corridor 约束与时间分配

3.1 Minimum Snap 优化的矩阵形式

$$ J = \min P^T R P $$

其中 $R$ 为分块对角矩阵:

$$ R = \begin{bmatrix} Q_1 & 0 & \cdots & 0 \ 0 & Q_2 & \cdots & 0 \ \vdots & & \ddots & \vdots \ 0 & 0 & \cdots & Q_M \end{bmatrix} $$

个人理解:$Q_k$ 矩阵中出现的 $(t_i - t_{i-1})^{r+c-7}$ 项只依赖于该段的时间长度 $\Delta T_k = t_k - t_{k-1}$,这也是为什么用相对时间(每段从 0 开始)更方便——直接用 $T_k$ 代入即可。

3.2 时间参数化

个人理解:实际求解时采用相对时间——每段轨迹的起始时间设为 0,只关心段内经过的时间 $\Delta T_k$。这样避免了绝对时间导致的数值问题。

个人理解:求解完成后,需要对整条轨迹进行 Feasibility Check(可行性检查)——逐段检查 P.V.A. 是否满足物理约束(最大速度、最大加速度、最大推力等),如果某段超限则需要调整该段的时间分配。

3.3 Corridor 约束

在基本的等式约束 QP 中,轨迹只需经过指定的 waypoints。但实际飞行中,轨迹在 waypoints 之间可能偏离过大,进入障碍物区域。Corridor 约束通过引入不等式约束来保证轨迹始终在安全通道内。

个人理解:之前的等式约束要求轨迹必须精确经过每个 waypoint。但使用 Corridor 后,中间 waypoint 的约束可以从等式松弛为不等式——轨迹不必精确经过该点,只需保持在 Corridor 范围内即可。这给优化器更多自由度,生成的轨迹更光滑。

对每个关键路径点 $P_k$,构造一个矩形的 Corridor:

$$ X_{min} \leq P_k \leq X_{max}, \quad Y_{min} \leq P_k \leq Y_{max} $$

个人理解:Corridor 通常取矩形(轴对齐的方盒),每个 waypoint 周围构建一个半宽为 $r$ 的安全通道。轨迹只需留在 Corridor 内,不必精确穿过 waypoint。每个 Corridor 在每个轴方向上产生 2 个不等式约束(上界和下界)。

不等式约束形式:

$$ [1, t_i, t_i^2, \ldots, t_i^N] \mathbf{p} \leq P(t_i) + r $$

$$ [1, t_i, t_i^2, \ldots, t_i^N] \mathbf{p} \geq P(t_i) - r $$

个人理解:Corridor 的宽度 $2r$ 通常由路径规划阶段的障碍物间隙决定。 $r$ 越大,轨迹越自由(更光滑),但安全裕度越小; $r$ 越小,轨迹被迫贴近 waypoints,可能出现较大的 snap。

3.4 约束总结

类型 约束内容 数量
等式约束 起始 PVA 3
等式约束 终止 PVA 3
等式约束 中间点 PVA 连续 $3 \times (N_{poly} - 1)$
不等式约束 中间点位置 Corridor $2 \times (N_{poly} - 1)$

个人理解:引入不等式约束后,问题从等式约束 QP 变为一般 QP,仍然是凸优化,但不再有闭式解,需要迭代求解器。

3.5 轨迹离散化与 Corridor

个人理解:为了确保轨迹不会在 waypoint 之间偏出 Corridor,需要在每段轨迹上增加离散采样点并检查约束。采样点越密,安全性越高,但约束数量也越多。轨迹段数 = waypoint 数 - 1。

个人理解:实用的迭代策略:① 先不加 Corridor 约束求解一次,得到初始轨迹;② 检查轨迹是否有超出 Corridor 的部分(波峰、波谷处最容易超出);③ 在超出的位置添加 Corridor 不等式约束,重新求解;④ 重复直到所有位置都满足约束。

个人理解:实际做法是在每段轨迹上采样若干中间时刻点,检查这些点是否都在 Corridor 内。如果某些点超出了 Corridor,有两种处理策略:

  1. 增加约束点:在超出位置添加新的不等式约束,重新求解
  2. 增加 waypoint:在超出位置拆分段,添加新的路径点

3.6 时间分配策略

个人理解:总飞行时间确定后(如根据总路程和期望平均速度估算),还需要将总时间合理分配到各段。如果各段时间分配不当(某段过长或过短),轨迹质量会大幅下降。

各段时间的分配标准:

方法一:匀速分配

假设每段内速度恒定,按段长度比例分配总时间:

$$ t_i' = T_{total} \cdot \frac{|P_{i+1} - P_i|}{\sum_j |P_{j+1} - P_j|} $$

方法二:梯形速度分配

假设每段执行梯形速度曲线(加速→匀速→减速),以 $a_{max}$ 加速到 $V_{max}$,再以相同减速度减到 0,得到每段所需时间。

设段间时间分别为 $t_1', t_2', \ldots, t_N'$,累计时间为:

$$ t_0 = 0, \quad t_1 = t_1', \quad t_2 = t_1 + t_2', \quad \ldots, \quad t_N = t_{N-1} + t_N' $$

个人理解:时间分配后如果某段的速度或加速度超出物理极限,说明该段时间分配不足。解决方法:增大该段的时间分配,重新求解轨迹,再次 check feasibility(可行性检验)。反复迭代直到所有段都满足动力学约束。也可以使用 time scaling(时间缩放)方法统一调整。

个人理解:时间分配是轨迹规划中非常关键的一步——时间分配不当会导致:

  • 时间过短:加速度/jerk/snap 过大,超出执行器能力
  • 时间过长:飞行效率低,总时间浪费

常用的迭代策略:先用匀速或梯形分配初始时间 → 求解轨迹 → 检查最大速度/加速度是否超限 → 对超限的段增加时间 → 重新求解,直到满足约束。

3.7 轨迹归一化

个人理解:当某段时间很短时,多项式系数 $p_i$ 可能非常大(因为要在短时间内完成位移),而 $p_i \cdot T^N$ 可能溢出 float 精度。因此通常使用 double 精度浮点数,并且配合时间归一化来控制数值范围。

为避免数值问题,引入轨迹时间归一化。对每段定义归一化时间:

$$ \xi = \frac{t - t_{i-1}}{T_i}, \quad \xi \in [0, 1] $$

其中 $T_i = t_i - t_{i-1}$ 为第 $i$ 段的持续时间。

原始多项式变为归一化多项式:

$$ \hat{p}(\xi) = p(\xi T) = p_0 + p_1 T\xi + p_2 T^2 \xi^2 + \cdots = \sum \hat{p}_i \xi^i $$

其中归一化系数 $\hat{p}_i = p_i \times T^i$

归一化后的 PVA 关系:

$$ p(t) = \hat{p}(\xi) = \sum \hat{p}_i \xi^i $$

$$ V(t) = \frac{\hat{p}'(\xi)}{T} $$

$$ a(t) = \frac{\hat{p}''(\xi)}{T^2} $$

个人理解:工作流程:先将时间归一化到 $[0, 1]$,在归一化时间域下求解多项式系数 $\hat{p}_i$,求解完成后再反归一化得到真实时间域下的系数 $p_i = \hat{p}_i / T^i$

个人理解:归一化将所有时间段映射到 $[0, 1]$,使得多项式系数的数值范围可控。求解完成后需要反归一化: $p_i = \hat{p}_i / T^i$。注意反归一化后的求导也要做对应的尺度变换。

Python 验证

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
plt.rcParams['font.sans-serif'] = ['SimHei']  # Windows 自带黑体
plt.rcParams['axes.unicode_minus'] = False    # 解决负号显示为方框的问题
# ============================================================
# Corridor 约束可视化 + 时间分配对比
# 展示航点、安全走廊、障碍物以及两种时间分配策略的速度差异
# ============================================================

np.random.seed(42)

# --- 航点与障碍物定义 ---
waypoints = np.array([[0, 0], [3, 2], [6, 4], [9, 3], [12, 5]])
obstacles = [
    {"center": [1.5, 3.8], "w": 1.2, "h": 1.0},  # 障碍物1:走廊上方
    {"center": [4.5, 1.2], "w": 1.0, "h": 1.2},  # 障碍物2:走廊下方
    {"center": [7.5, 5.8], "w": 1.4, "h": 0.8},  # 障碍物3:走廊上方
    {"center": [10.5, 1.5], "w": 1.0, "h": 1.0}, # 障碍物4:走廊下方
]
corridor_half = 1.2  # 走廊半宽

# --- 生成一条穿过走廊的光滑曲线(用样条插值模拟) ---
from numpy.polynomial import polynomial as P_mod
t_wp = np.linspace(0, 1, len(waypoints))
t_dense = np.linspace(0, 1, 300)
# 简单参数化插值
degree = min(4, len(waypoints) - 1)
cx = np.polyfit(t_wp, waypoints[:, 0], degree)
cy = np.polyfit(t_wp, waypoints[:, 1], degree)
traj_x = np.polyval(cx, t_dense)
traj_y = np.polyval(cy, t_dense)

# ==================== 图1:Corridor 可视化 ====================
fig, axes = plt.subplots(1, 2, figsize=(15, 6))
ax1 = axes[0]

# 绘制走廊矩形
for wp in waypoints:
    rect = Rectangle(
        (wp[0] - corridor_half, wp[1] - corridor_half),
        2 * corridor_half, 2 * corridor_half,
        linewidth=1.5, edgecolor='#2ca02c', facecolor='#2ca02c',
        alpha=0.15, linestyle='--', label='安全走廊' if wp is waypoints[0] else ''
    )
    ax1.add_patch(rect)

# 绘制障碍物
for i, obs in enumerate(obstacles):
    rect = Rectangle(
        (obs["center"][0] - obs["w"]/2, obs["center"][1] - obs["h"]/2),
        obs["w"], obs["h"],
        linewidth=1.5, edgecolor='red', facecolor='red', alpha=0.35,
        label='障碍物' if i == 0 else ''
    )
    ax1.add_patch(rect)

# 绘制轨迹和航点
ax1.plot(traj_x, traj_y, 'b-', linewidth=2, label='规划轨迹')
ax1.plot(waypoints[:, 0], waypoints[:, 1], 'ko', markersize=8, zorder=5, label='航点')
for i, wp in enumerate(waypoints):
    ax1.annotate(f'P{i}', wp, textcoords="offset points", xytext=(6, 8), fontsize=10)

ax1.set_xlabel('X (m)'); ax1.set_ylabel('Y (m)')
ax1.set_title('Corridor 约束示意 —— 轨迹在走廊内避开障碍物')
ax1.legend(loc='upper left', fontsize=9); ax1.grid(True, alpha=0.3)
ax1.set_aspect('equal'); ax1.set_xlim(-2, 14); ax1.set_ylim(-2, 9)

# ==================== 图2:两种时间分配的速度对比 ====================
ax2 = axes[1]
seg_lengths = np.linalg.norm(np.diff(waypoints, axis=0), axis=1)
T_total = 10.0  # 总飞行时间

# 方法一:匀速分配(各段时间相等)
T_uniform = np.full(len(seg_lengths), T_total / len(seg_lengths))
# 方法二:按距离等比例分配
T_prop = T_total * seg_lengths / seg_lengths.sum()

# 计算各段平均速度
v_uniform = seg_lengths / T_uniform
v_prop = seg_lengths / T_prop

# 绘制速度阶梯图
seg_labels = [f'段{i+1}\n({seg_lengths[i]:.1f}m)' for i in range(len(seg_lengths))]
x_pos = np.arange(len(seg_lengths))
width = 0.35
bars1 = ax2.bar(x_pos - width/2, v_uniform, width, color='#ff7f0e', alpha=0.8, label='匀速时间分配')
bars2 = ax2.bar(x_pos + width/2, v_prop, width, color='#1f77b4', alpha=0.8, label='按距离比例分配')

# 标注数值
for bar, v in zip(bars1, v_uniform):
    ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.05,
             f'{v:.2f}', ha='center', fontsize=8)
for bar, v in zip(bars2, v_prop):
    ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.05,
             f'{v:.2f}', ha='center', fontsize=8)

ax2.set_xticks(x_pos); ax2.set_xticklabels(seg_labels, fontsize=9)
ax2.set_ylabel('平均速度 (m/s)'); ax2.set_title('两种时间分配策略的速度对比')
ax2.legend(fontsize=9); ax2.grid(True, alpha=0.3, axis='y')

# 在图上添加时间信息
info_uniform = '  '.join([f'{t:.1f}s' for t in T_uniform])
info_prop = '  '.join([f'{t:.1f}s' for t in T_prop])
ax2.text(0.02, 0.95, f'匀速分配时间: [{info_uniform}]', transform=ax2.transAxes,
         fontsize=8, verticalalignment='top', color='#ff7f0e')
ax2.text(0.02, 0.88, f'比例分配时间: [{info_prop}]', transform=ax2.transAxes,
         fontsize=8, verticalalignment='top', color='#1f77b4')

plt.tight_layout()
import os; os.makedirs('assets', exist_ok=True)
plt.savefig('assets/planning3.png', dpi=150, bbox_inches='tight')
plt.show()

运行结果:


四、Minimum Snap 闭式求解

4.1 问题重述

回顾 QP 问题:

$$ \min_P \quad P^T \mathbf{Q} P \quad \text{s.t.} \quad A_{eq} P = d_{eq} $$

将约束分为已知(Fixed)和未知(Free)两部分:

$$ A_i \mathbf{p}_i = \mathbf{d}_i, \quad A = [A_0, A_E]^T, \quad \mathbf{d}_i = [\mathbf{d}_0, \mathbf{d}_F]^T $$

其中 $\mathbf{d}_0 = [P_0, V_0, A_0]^T$ 为初始条件。

4.2 矩阵结构

总约束矩阵 $A_{total}$$6k \times 6k$ 维矩阵(对于 7 阶多项式,每段 8 个参数),包含所有段的约束。

$$ \mathbf{p} = A^{-1}\mathbf{d}_\alpha $$

个人理解:闭式解的计算量主要在 $A$ 矩阵求逆和 $R_{PP}$ 矩阵求逆,其余都是矩阵乘法,效率很高。但闭式解只能处理等式约束——Corridor 的不等式约束无法直接纳入,只能将 Corridor 近似为 waypoint 等式约束(即要求轨迹精确经过 Corridor 中心点)。

个人理解:闭式解适用于实时性要求高的场景——当没有不等式约束(无 Corridor),且不想引入 QP solver 的依赖和计算开销时,闭式解是最佳选择。

4.3 Fix/Free 变量分离

核心思想:将所有端点处的导数值作为决策变量,分为两类:

  • Fixed ($\mathbf{d}_F$):已知的约束值(起点 PVA、终点 PVA、中间点位置等)
  • Free ($\mathbf{d}_P$):待优化的自由变量(中间点的速度、加速度等)

$$ \mathbf{d}' = \begin{bmatrix} \mathbf{d}_F \ \mathbf{d}_P \end{bmatrix} $$

结构化向量——按时间顺序排列所有端点的导数值:

$$ \mathbf{d} = \begin{bmatrix} p(t_0), v(t_0), a(t_0), p(t_1), v(t_1), a(t_1), \ldots, p(t_N), v(t_N), a(t_N) \end{bmatrix}^T $$

维度为 $3(M+1) \times 1$($M$ 段轨迹,$M+1$ 个端点,每个端点 3 个量 PVA)。

4.4 变量替换

由于 $\mathbf{d}$ 中各元素的排列顺序(按时间)与 $\mathbf{d}'$ 中的排列顺序(Fixed/Free 分开)不同,需要一个重排矩阵 $C$

$$ \mathbf{d} = M\mathbf{d}', \quad \mathbf{d}' = C\begin{bmatrix} \mathbf{d}_F \ \mathbf{d}_P \end{bmatrix} $$

因此多项式参数向量可以表示为:

$$ P = A^{-1}\mathbf{d} = A^{-1}MC\begin{bmatrix} \mathbf{d}_F \ \mathbf{d}_P \end{bmatrix} = K\begin{bmatrix} \mathbf{d}_F \ \mathbf{d}_P \end{bmatrix} $$

其中 $K = A^{-1}MC$

个人理解:$M$ 矩阵处理连续性约束(将相邻段共享的端点值合并),$C$ 矩阵将合并后的变量按 Fixed/Free 重排。$K$ 矩阵建立了从"端点导数值"到"多项式系数"的线性映射。

4.5 闭式解推导

代入目标函数:

$$ \min J = P^T \mathbf{Q} P $$

$$ J = \begin{bmatrix} \mathbf{d}_F \ \mathbf{d}_P \end{bmatrix}^T K^T \mathbf{Q} K \begin{bmatrix} \mathbf{d}_F \ \mathbf{d}_P \end{bmatrix} $$

$K^T \mathbf{Q} K$ 分块为 $2 \times 2$ 块矩阵:

$$ J = \begin{bmatrix} \mathbf{d}_F^T & \mathbf{d}_P^T \end{bmatrix} \begin{bmatrix} R_{FF} & R_{FP} \ R_{PF} & R_{PP} \end{bmatrix} \begin{bmatrix} \mathbf{d}_F \ \mathbf{d}_P \end{bmatrix} $$

展开为标量表达式:

$$ J = \mathbf{d}_F^T R_{FF} \mathbf{d}_F + \mathbf{d}_F^T R_{FP} \mathbf{d}_P + \mathbf{d}_P^T R_{PF} \mathbf{d}_F + \mathbf{d}_P^T R_{PP} \mathbf{d}_P $$

对自由变量 $\mathbf{d}P$ 求导并令其为零(注意 $R{PF} = R_{FP}^T$):

$$ \frac{\partial J}{\partial \mathbf{d}_P} = 2 R_{FP}^T \mathbf{d}_F + 2 R_{PP} \mathbf{d}_P = 0 $$

4.6 闭式解

$$ \boxed{\mathbf{d}_P = -R_{PP}^{-1} R_{FP}^T \mathbf{d}_F} $$

求得 $\mathbf{d}_P$ 后,回代得到完整的多项式系数:

$$ P = K \begin{bmatrix} \mathbf{d}_F \ \mathbf{d}_P \end{bmatrix} $$

4.7 闭式解 vs QP 求解器

比较项 闭式解 QP 求解器
计算速度 快(矩阵求逆 + 乘法) 慢(迭代求解)
等式约束 支持 支持
不等式约束 不支持 支持
Corridor 约束 需近似为等式约束 直接支持
适用场景 实时性要求高、无 Corridor 有 Corridor / 动态约束

个人理解:闭式解的核心优势是计算速度快——只需矩阵求逆和乘法,无需迭代求解。主要的计算瓶颈在于 $A$ 矩阵的求逆和 $R_{PP}$ 的求逆。当段数 $M$ 较大时,$A$ 为稀疏带状矩阵,可以利用稀疏结构加速。在实际应用中,如果飞行环境简单(无 Corridor 约束),闭式解是首选;如果需要处理障碍物约束,则必须使用 QP 求解器。

Python 验证

import numpy as np
import matplotlib.pyplot as plt
from scipy.linalg import block_diag
plt.rcParams['font.sans-serif'] = ['SimHei']  # Windows 自带黑体
plt.rcParams['axes.unicode_minus'] = False    # 解决负号显示为方框的问题
# ============================================================
# Minimum Snap 闭式求解 vs KKT/QP 求解 —— 3航点(2段)对比验证
# 闭式解:d_P = -R_PP^{-1} R_FP^T d_F
# KKT解:通过拉格朗日乘子法求解等式约束QP
# ============================================================

N = 7   # 7阶多项式
M = 2   # 2段(3个航点)
n_c = N + 1  # 每段系数数 = 8

# --- 航点定义(一维 x 轴,简化演示) ---
waypoints = np.array([0.0, 5.0, 12.0])  # 起点、中间点、终点
T_seg = np.array([2.0, 3.0])            # 各段时间

def poly_coeff_vec(n, t, deriv=0):
    """n阶多项式在t处deriv阶导数的系数行向量"""
    c = np.zeros(n + 1)
    for i in range(deriv, n + 1):
        c[i] = np.prod(range(i, i - deriv, -1)) * t ** (i - deriv)
    return c

def build_Q_seg(n, T, kr=4):
    """构造单段 snap 代价矩阵"""
    Q = np.zeros((n + 1, n + 1))
    for i in range(kr, n + 1):
        for j in range(kr, n + 1):
            ci = np.prod(range(i, i - kr, -1))
            cj = np.prod(range(j, j - kr, -1))
            Q[i, j] = ci * cj * T ** (i + j - 2*kr + 1) / (i + j - 2*kr + 1)
    return Q

# ===================== 方法1:KKT 求解 =====================
Q_all = block_diag(*[build_Q_seg(N, T) for T in T_seg])
dim = M * n_c

rows, rhs = [], []
def add_eq(row, val):
    rows.append(row); rhs.append(val)

# 起点约束:p=0, v=0, a=0
for d in range(3):
    r = np.zeros(dim); r[:n_c] = poly_coeff_vec(N, 0, d)
    add_eq(r, waypoints[0] if d == 0 else 0.0)
# 终点约束:p=12, v=0, a=0
for d in range(3):
    r = np.zeros(dim); r[n_c:] = poly_coeff_vec(N, T_seg[1], d)
    add_eq(r, waypoints[2] if d == 0 else 0.0)
# 中间航点位置约束(段0终点)
r = np.zeros(dim); r[:n_c] = poly_coeff_vec(N, T_seg[0], 0)
add_eq(r, waypoints[1])
# 连续性约束(0~3阶导数)
for d in range(4):
    r = np.zeros(dim)
    r[:n_c] = poly_coeff_vec(N, T_seg[0], d)
    r[n_c:] = -poly_coeff_vec(N, 0, d)
    add_eq(r, 0.0)

Aeq = np.array(rows); beq = np.array(rhs)
n_eq = len(beq)
KKT = np.zeros((dim + n_eq, dim + n_eq))
KKT[:dim, :dim] = 2 * Q_all
KKT[:dim, dim:] = Aeq.T
KKT[dim:, :dim] = Aeq
rhs_kkt = np.zeros(dim + n_eq); rhs_kkt[dim:] = beq
P_kkt = np.linalg.solve(KKT, rhs_kkt)[:dim]

# ===================== 方法2:闭式解 =====================
# 构造映射矩阵 A_map: 每段端点导数值 -> 多项式系数
# 每段8个系数,端点处有 0~3阶导数(起点4个+终点4个=8个方程)
def build_A_seg(n, T):
    """构造单段的端点约束矩阵(8x8)"""
    A = np.zeros((2*4, n+1))
    for d in range(4):
        A[d, :] = poly_coeff_vec(n, 0, d)       # 起点 d 阶导数
        A[4+d, :] = poly_coeff_vec(n, T, d)     # 终点 d 阶导数
    return A

A_map = block_diag(*[build_A_seg(N, T) for T in T_seg])  # 16x16
# d_alpha: 按段排列的端点导数值 [seg0起(p,v,a,j), seg0终(p,v,a,j), seg1起(...), seg1终(...)]
# M矩阵:合并连续性(seg0终 = seg1起),将16维d_alpha映射为12维独立变量d
# 独立端点集:seg0起(4), 中间点(4,共享), seg1终(4) = 12 个变量
M_mat = np.zeros((16, 12))
M_mat[0:4, 0:4] = np.eye(4)     # seg0 起点 -> 独立变量 0~3
M_mat[4:8, 4:8] = np.eye(4)     # seg0 终点 -> 中间点变量 4~7
M_mat[8:12, 4:8] = np.eye(4)    # seg1 起点 -> 中间点变量 4~7(连续性)
M_mat[12:16, 8:12] = np.eye(4)  # seg1 终点 -> 独立变量 8~11

# C矩阵:将独立变量重排为 [Fixed; Free]
# Fixed: seg0起_p(0), seg0起_v(0), seg0起_a(0), 中间_p(5), seg1终_p(12), seg1终_v(0), seg1终_a(0) = 7个
# Free:  seg0起_j, 中间_v, 中间_a, 中间_j, seg1终_j = 5个
fixed_idx = [0, 1, 2, 4, 8, 9, 10]  # 在12维独立变量中的索引
free_idx  = [3, 5, 6, 7, 11]
d_F = np.array([waypoints[0], 0, 0, waypoints[1], waypoints[2], 0, 0])
n_fixed, n_free = len(fixed_idx), len(free_idx)

C_mat = np.zeros((12, 12))
for i, fi in enumerate(fixed_idx):
    C_mat[fi, i] = 1.0
for i, fi in enumerate(free_idx):
    C_mat[fi, n_fixed + i] = 1.0

# K = A_map^{-1} @ M @ C
K = np.linalg.solve(A_map, M_mat @ C_mat)

# R = K^T Q K,分块为 R_FF, R_FP, R_PF, R_PP
R_full = K.T @ Q_all @ K
R_FF = R_full[:n_fixed, :n_fixed]
R_FP = R_full[:n_fixed, n_fixed:]
R_PP = R_full[n_fixed:, n_fixed:]

# 闭式解核心公式
d_P = -np.linalg.solve(R_PP, R_FP.T @ d_F)
d_prime = np.concatenate([d_F, d_P])
P_closed = K @ d_prime

# ===================== 采样与对比绘图 =====================
def sample_traj(P_vec, T_seg, N):
    t_all, pos_kkt, vel_kkt = [], [], []
    t_offset = 0.0
    for k in range(len(T_seg)):
        ts = np.linspace(0, T_seg[k], 150)
        p = P_vec[k*n_c:(k+1)*n_c]
        pos_kkt.extend([poly_coeff_vec(N, t, 0) @ p for t in ts])
        vel_kkt.extend([poly_coeff_vec(N, t, 1) @ p for t in ts])
        t_all.extend(ts + t_offset)
        t_offset += T_seg[k]
    return np.array(t_all), np.array(pos_kkt), np.array(vel_kkt)

t1, pos1, vel1 = sample_traj(P_kkt, T_seg, N)
t2, pos2, vel2 = sample_traj(P_closed, T_seg, N)

fig, axes = plt.subplots(2, 1, figsize=(10, 7), sharex=True)
# 位置对比
axes[0].plot(t1, pos1, 'b-', linewidth=2.5, label='KKT/QP 求解')
axes[0].plot(t2, pos2, 'r--', linewidth=2, label='闭式解')
t_wp = [0, T_seg[0], T_seg[0]+T_seg[1]]
axes[0].plot(t_wp, waypoints, 'ko', markersize=8, zorder=5, label='航点')
axes[0].set_ylabel('位置 (m)'); axes[0].legend(); axes[0].grid(True, alpha=0.3)
axes[0].set_title('Minimum Snap 闭式解 vs KKT 求解 —— 轨迹完全重合验证')

# 速度对比
axes[1].plot(t1, vel1, 'b-', linewidth=2.5, label='KKT/QP 速度')
axes[1].plot(t2, vel2, 'r--', linewidth=2, label='闭式解速度')
axes[1].set_ylabel('速度 (m/s)'); axes[1].set_xlabel('时间 (s)')
axes[1].legend(); axes[1].grid(True, alpha=0.3)

# 打印误差
err = np.max(np.abs(P_kkt - P_closed))
print(f"两种方法系数最大绝对误差: {err:.2e}")
fig.text(0.5, 0.01, f'系数最大绝对误差: {err:.2e}(验证两种方法等价)',
         ha='center', fontsize=11, color='green')
plt.tight_layout(rect=[0, 0.03, 1, 1])
import os; os.makedirs('assets', exist_ok=True)
plt.savefig('assets/planning4.png', dpi=150, bbox_inches='tight')
plt.show()

运行结果:


附录:规划部分符号表

符号 含义
$p(t)$ 位置轨迹(标量,单个轴)
$\mathbf{p}_i$ $i$ 段多项式系数向量
$P$ 所有段系数的堆叠向量
$N$ 多项式阶数
$M$ 轨迹段数
$T_i$ $i$ 段持续时间
$t_i$ $i$ 个 waypoint 的时间
$Q_k$ $k$ 段的 Hessian 矩阵
$\mathbf{Q}$ 总 Hessian 矩阵(分块对角)
$A_{eq}$ 等式约束矩阵
$d_{eq}$ 等式约束右端向量
$\mathbf{d}_F$ Fixed 约束值
$\mathbf{d}_P$ Free 变量(待优化)
$K$ 端点值到系数的映射矩阵
$R_{FF}, R_{FP}, R_{PP}$ 分块 Hessian 子矩阵
$K_r$ 优化的导数阶数(snap: $K_r=4$
$\xi$ 归一化时间 $\in [0, 1]$
$r$ Corridor 半宽