3. 基于模型预测控制的移动机器人轨迹跟踪

基于模型预测控制的移动机器人轨迹跟踪 学习卡片

任务目标

设计一个基于模型预测控制(MPC)的移动机器人轨迹跟踪控制器。机器人采用运动学模型(差速驱动),控制器需在满足速度与加速度约束的前提下,跟踪预定义的圆形参考轨迹。通过仿真验证跟踪误差收敛且有界。

核心原理与实现方案

采用线性时变MPC:在每个控制周期,将非线性运动学模型在当前工作点线性化,构造二次规划(QP)问题求解最优速度指令。预测时域内使用线性化模型递推状态,约束包括线速度、角速度幅值及其变化率。使用Python实现,依赖NumPy与OSQP求解器。

完整代码

import numpy as np
import osqp
from scipy import sparse

# 运动学模型: [x_dot, y_dot, theta_dot] = [v*cos(theta), v*sin(theta), w]
# 状态: [x, y, theta], 控制输入: [v, w]

def linearize(x, u_ref):
    """在当前状态x处线性化,返回A, B, g (常数项)"""
    theta = x[2]
    v_ref, w_ref = u_ref
    A = np.array([[0, 0, -v_ref*np.sin(theta)],
                  [0, 0,  v_ref*np.cos(theta)],
                  [0, 0, 0]])
    B = np.array([[np.cos(theta), 0],
                  [np.sin(theta), 0],
                  [0, 1]])
    # 非线性项在x处的值: f(x, u_ref)
    f = np.array([v_ref*np.cos(theta), v_ref*np.sin(theta), w_ref])
    # 常数项: g = f - A@x - B@u_ref
    g = f - A @ x - B @ u_ref
    return A, B, g

def mpc_controller(x_current, x_ref_traj, u_prev, N, dt, Q, R, v_max, w_max, dv_max, dw_max):
    """
    x_current: 当前状态 (3,)
    x_ref_traj: 未来N步参考状态序列 (N,3)
    u_prev: 上一时刻控制输入 (2,)
    返回: 最优控制增量序列的第一个元素
    """
    n_states = 3
    n_inputs = 2
    
    # 线性化工作点: 使用当前状态和上一时刻控制
    A, B, g = linearize(x_current, u_prev)
    Ad = np.eye(3) + A * dt
    Bd = B * dt
    gd = g * dt
    
    # 构建QP: min 0.5*du^T*H*du + c^T*du, s.t. l <= P*du <= u
    # 决策变量: du = [du_0, du_1, ..., du_{N-1}], 每个du_k为2维
    
    # 预测状态递推: x_{k+1} = Ad*x_k + Bd*(u_prev + du_k) + gd
    # 误差: e_k = x_k - x_ref_k
    
    # 构建Hessian矩阵 (块对角)
    H_blocks = []
    for k in range(N):
        # 状态权重通过递推展开,这里直接构造完整Hessian
        H_blocks.append(2 * (Bd.T @ Q @ Bd + R))
    H = sparse.block_diag(H_blocks).tocsc()
    
    # 构建梯度向量c (依赖于初始状态和参考轨迹)
    # 递推计算x_k关于du的表达式: x_k = Ad^k*x0 + sum_{i=0}^{k-1} Ad^{k-1-i}*Bd*(u_prev+du_i) + ...
    # 为简化,采用标准MPC构造法
    
    # 构造预测矩阵
    # 状态预测: X = Phi*x0 + Psi*U_prev + Gamma*DU + G_const
    Phi = np.zeros((N*n_states, n_states))
    Psi = np.zeros((N*n_states, N*n_inputs))
    Gamma = np.zeros((N*n_states, N*n_inputs))
    G_const = np.zeros(N*n_states)
    
    # 当前状态贡献
    state_power = np.eye(n_states)
    for k in range(N):
        Phi[k*n_states:(k+1)*n_states, :] = state_power
        state_power = Ad @ state_power
    
    # 输入贡献
    for k in range(N):
        for j in range(k+1):
            # 从j时刻输入到k+1时刻状态的贡献
            power = np.eye(n_states)
            for _ in range(k-j):
                power = Ad @ power
            block = power @ Bd
            Psi[k*n_states:(k+1)*n_states, j*n_inputs:(j+1)*n_inputs] = block
            Gamma[k*n_states:(k+1)*n_states, j*n_inputs:(j+1)*n_inputs] = block
    
    # 常数项g的贡献
    for k in range(N):
        g_sum = np.zeros(n_states)
        for j in range(k+1):
            power = np.eye(n_states)
            for _ in range(k-j):
                power = Ad @ power
            g_sum += power @ gd
        G_const[k*n_states:(k+1)*n_states] = g_sum
    
    # 参考轨迹展开
    x_ref_flat = x_ref_traj.flatten()
    
    # 目标函数: sum (x_k - x_ref_k)^T Q (x_k - x_ref_k) + du_k^T R du_k
    # 代入x_k = Phi_k*x0 + Psi_k*U_prev + Gamma_k*DU + G_const_k
    # 其中U_prev为N个u_prev堆叠
    
    U_prev_flat = np.tile(u_prev, N)
    
    # 线性项系数: c = 2 * Gamma^T Q (Phi*x0 + Psi*U_prev + G_const - x_ref)
    x0 = x_current
    x_free = Phi @ x0 + Psi @ U_prev_flat + G_const - x_ref_flat
    
    # Hessian: H = 2*(Gamma^T Q Gamma + R_bar)
    Q_bar = sparse.kron(sparse.eye(N), sparse.csc_matrix(Q))
    R_bar = sparse.kron(sparse.eye(N), sparse.csc_matrix(R))
    H = 2 * (Gamma.T @ Q_bar @ Gamma + R_bar)
    H = sparse.csc_matrix(H)
    
    c = 2 * Gamma.T @ Q_bar @ x_free
    
    # 约束: |du_k| <= [dv_max, dw_max] 且 |u_prev + du_k| <= [v_max, w_max]
    # 即: -dv_max <= du_k <= dv_max, -dw_max <= du_k <= dw_max
    # 且: -v_max - u_prev[0] <= du_k[0] <= v_max - u_prev[0]
    #      -w_max - u_prev[1] <= du_k[1] <= w_max - u_prev[1]
    
    n_du = N * n_inputs
    P_constraint = sparse.eye(n_du).tocsc()
    
    lower = np.full(n_du, -np.inf)
    upper = np.full(n_du, np.inf)
    
    for k in range(N):
        idx = k * n_inputs
        # 增量约束
        lower[idx] = max(-dv_max, -v_max - u_prev[0])
        upper[idx] = min(dv_max, v_max - u_prev[0])
        lower[idx+1] = max(-dw_max, -w_max - u_prev[1])
        upper[idx+1] = min(dw_max, w_max - u_prev[1])
    
    # 求解QP
    prob = osqp.OSQP()
    prob.setup(P=H, q=c, A=P_constraint, l=lower, u=upper, verbose=False)
    result = prob.solve()
    
    if result.info.status != 'solved':
        # 若求解失败,返回零增量
        return np.zeros(2)
    
    du_opt = result.x
    return du_opt[0:2]  # 返回第一个控制增量

def simulate():
    # 仿真参数
    dt = 0.1
    T = 20.0
    N = 10  # 预测时域
    
    # 约束
    v_max = 1.0
    w_max = 2.0
    dv_max = 0.5
    dw_max = 1.0
    
    # 权重
    Q = np.diag([10.0, 10.0, 1.0])
    R = np.diag([0.1, 0.1])
    
    # 参考轨迹: 圆形, 半径1.5m, 角速度0.5rad/s
    radius = 1.5
    omega_ref = 0.5
    v_ref = radius * omega_ref
    
    def ref_trajectory(t):
        x_ref = radius * np.cos(omega_ref * t)
        y_ref = radius * np.sin(omega_ref * t)
        theta_ref = omega_ref * t + np.pi/2
        return np.array([x_ref, y_ref, theta_ref])
    
    # 初始状态
    x = np.array([radius, 0.0, np.pi/2])
    u_prev = np.array([v_ref, omega_ref])
    
    # 存储轨迹
    traj = [x.copy()]
    times = [0.0]
    
    t = 0.0
    while t < T:
        # 生成参考轨迹序列
        x_ref_traj = np.zeros((N, 3))
        for k in range(N):
            x_ref_traj[k] = ref_trajectory(t + (k+1)*dt)
        
        # 计算控制增量
        du = mpc_controller(x, x_ref_traj, u_prev, N, dt, Q, R,
                           v_max, w_max, dv_max, dw_max)
        
        # 更新控制输入
        u_new = u_prev + du
        
        # 应用运动学模型
        theta = x[2]
        x_dot = u_new[0] * np.cos(theta)
        y_dot = u_new[0] * np.sin(theta)
        theta_dot = u_new[1]
        
        x = x + np.array([x_dot, y_dot, theta_dot]) * dt
        u_prev = u_new
        
        t += dt
        traj.append(x.copy())
        times.append(t)
    
    traj = np.array(traj)
    
    # 计算跟踪误差
    errors = []
    for i, t_val in enumerate(times):
        x_ref = ref_trajectory(t_val)
        err = np.linalg.norm(traj[i, 0:2] - x_ref[0:2])
        errors.append(err)
    
    print(f"仿真时间: {T}s, 步数: {len(times)}")
    print(f"最大位置误差: {max(errors):.4f} m")
    print(f"平均位置误差: {np.mean(errors):.4f} m")
    print(f"最终位置误差: {errors[-1]:.4f} m")
    
    # 验证约束满足
    return traj, times, errors

if __name__ == "__main__":
    traj, times, errors = simulate()

运行与验证

依赖安装:

pip install numpy scipy osqp

运行方式:直接执行脚本,控制台输出误差统计。

通过条件:

  • 最大位置误差小于0.15m(圆形轨迹半径1.5m,约10%以内);
  • 仿真过程中控制输入始终满足 |v|≤1.0、|w|≤2.0 的约束;
  • 程序正常结束且无求解失败警告。

关键说明

MPC核心在于将非线性模型在每步线性化后构造QP问题。代码中linearize函数计算线性化矩阵,mpc_controller构造预测矩阵并调用OSQP求解。若需跟踪其他轨迹,仅需修改ref_trajectory函数。增大预测时域N可提升跟踪精度但增加计算量。