上篇聊了MPC的核心原理——预测、优化、滚动执行。MPC相比LQR最大的优势就是能处理约束。但"能处理约束"这句话说起来轻松,真正落地的时候,约束的类型、建模方式、求解难度,每个环节都有坑。

之前做四足机器人步态规划的团队就踩过一个大坑:他们在MPC里只加了力矩约束,没加关节角度约束。结果优化出来的控制量让髋关节转到了极限位置,电机直接堵转报错。约束这东西,漏一个就可能出事故。

今天就把MPC中的约束处理掰开揉碎讲一遍。

一、三类约束:输入、状态、输出

MPC中的约束分三大类:

输入约束:|u| <= u_max。这是最常见的,物理意义也最明确——电机最大力矩、阀门最大开度、推力器最大推力。输入约束是"硬"的,执行器物理上就做不到,你必须尊重。

状态约束:x_min <= x <= x_max。比如关节角度限位、飞行器最大倾斜角、电池SOC上下限。状态约束比输入约束更难处理,因为它约束的是系统内部变量,有些状态你还测不到。

输出约束:y_min <= y <= y_max。输出y = Cx + Du,所以输出约束本质上是状态和输入的联合约束。比如机械臂末端位置不能超过某个区域,无人机飞行高度不能超过限高。

从数学上看,这三类约束在MPC的优化问题中都是线性不等式约束。只要系统是线性的、代价函数是二次的,整个优化问题就是一个标准的QP(二次规划)。

二、输入约束的实现

输入约束最简单,直接写就行:

u_min <= u(k+i|k) <= u_max, i = 0, 1, ..., N-1

实际工程中,输入约束经常是不对称的。比如电机的正转力矩和反转力矩不一样大,或者液压缸的推力和拉力不同。这时候约束写成:

u_min_j <= u_j(k+i|k) <= u_max_j

每个通道j的上下限可以不同。

还有一种常见情况是输入变化率约束(input rate constraint):

|u(k) - u(k-1)| <= delta_u_max

这个约束限制控制量的变化速度,防止执行器被突然的大信号打坏。在MPC中实现也不难,只需要把u(k) - u(k-1)作为一个新的约束加进去。

讲真,输入约束是MPC最自然的约束类型。很多团队选择MPC就是因为它能优雅地处理输入约束——LQR做不到这一点。

三、状态约束的 tricky 之处

状态约束比输入约束麻烦得多。原因有几个:

第一,状态约束需要所有状态都可观测。如果某些状态你测不到也估不准,状态约束就没法严格执行。实际中你可能需要用软约束(soft constraint)来处理——允许少量违反,但在代价函数中严重惩罚。

第二,状态约束可能导致优化问题不可行。你想想,如果当前状态已经违反了某个约束(比如关节角度已经超了限位),那优化问题一开始就无解了。这时候你需要做一些特殊处理,比如松弛约束或者切换到安全模式。

第三,状态约束可能和稳定性冲突。为了保证闭环稳定性,MPC通常需要终端约束或者终端代价。如果状态约束太紧,可能把终端不变集压缩到零,导致稳定性无法保证。

软约束的实现方式:引入松弛变量s,把硬约束x_min <= x <= x_max变成:

x_min - s <= x <= x_max + s, s >= 0

然后在代价函数中加上rho * s^2,rho是一个很大的权重。这样优化器会尽量不违反约束,但在极端情况下允许少量违反,避免优化问题无解。

四、代码示例:带完整约束的MPC

import cvxpy as cp
import numpy as np

def mpc_with_constraints(x_current, N, A_d, B_d, Q, R, P,
                          u_max, x_max, delta_u_max):
    nx, nu = 4, 1
    X = cp.Variable((nx, N+1))
    U = cp.Variable((nu, N))
    
    cost = 0
    constraints = []
    
    for i in range(N):
        cost += cp.quad_form(X[:,i], Q) + cp.quad_form(U[:,i], R)
        constraints.append(X[:,i+1] == A_d @ X[:,i] + B_d @ U[:,i])
        # 输入约束
        constraints.append(U[:,i] <= u_max)
        constraints.append(U[:,i] >= -u_max)
        # 状态约束(软约束)
        constraints.append(X[:,i+1] <= x_max)
        constraints.append(X[:,i+1] >= -x_max)
        # 输入变化率约束
        if i > 0:
            constraints.append(U[:,i] - U[:,i-1] <= delta_u_max)
            constraints.append(U[:,i-1] - U[:,i] <= delta_u_max)
    
    cost += cp.quad_form(X[:,N], P)  # 终端代价
    constraints.append(X[:,0] == x_current)
    
    prob = cp.Problem(cp.Minimize(cost), constraints)
    prob.solve(solver=cp.OSQP, warm_start=True)
    
    if prob.status == 'infeasible':
        print("警告:MPC优化问题不可行!")
        return np.zeros(nu)
    
    return U[:,0].value

注意代码里的几个细节:

  • 输入约束是硬约束,直接加不等式
  • 状态约束这里也写成硬约束,实际中可能需要改成软约束
  • 输入变化率约束只在i>0时加(第一步没有"前一步")
  • warm_start=True很重要——用上一次的解作为初始猜测,能大幅加速求解
  • 不可行检测:如果prob.status是'infeasible',说明约束冲突了

五、约束处理的工程经验

经验一:约束优先级。不同约束的重要程度不一样。力矩约束是硬的,违反了会烧电机;关节角度约束是软的,少量违反可以接受。在MPC中可以用不同的松弛权重来区分优先级。

经验二:约束太多会拖慢求解。每加一个约束,QP问题的约束矩阵就多几行。约束数量从50增加到200,求解时间可能翻3-5倍。所以要精简约束,只保留真正必要的。

经验三:不可行处理是必须的。实际运行中,MPC优化问题偶尔会不可行(传感器跳变、外部冲击等)。你必须有一个fallback策略——要么松弛约束重新求解,要么切换到备份控制器。不做不可行处理,系统就会卡死。

之前有个做无人机配送的团队,MPC里加了20多个约束(速度、加速度、倾斜角、角速度、电机转速...),结果在阵风干扰下偶尔不可行,无人机直接自由落体。后来加了软约束松弛和不可行检测,才解决这个问题。

六、面试实战

Q:MPC中硬约束和软约束有什么区别? A:硬约束必须满足,否则优化问题无解。软约束允许违反,但会在代价函数中惩罚。工程中通常输入约束用硬约束,状态约束用软约束,保证优化问题始终可行。

Q:约束太多怎么办? A:几个策略:1)减少预测时域N;2)用活动集法(active set method)只处理活跃约束;3)约束裁剪(constraint tightening),提前去掉明显不会激活的约束。

Q:MPC优化问题不可行怎么办? A:首先检查约束设置是否合理。如果约束本身没问题,用软约束松弛保证可行性。同时加一个fallback控制器,在MPC不可行时接管。

小结

MPC的约束处理是它最大的优势,也是工程落地最复杂的部分。输入约束最直接,状态约束最tricky,输出约束是前两者的组合。

工程上记住三点:硬约束保安全,软约束保可行,不可行处理保命。这三点做到了,MPC的约束处理基本就稳了。

下篇聊MPC的实时求解——怎么让QP在几毫秒内解完,以及怎么把MPC部署到嵌入式平台上。


如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。

「机器人软件开发面试·从入门到精通」连载系列 

上一篇:第194篇 模型预测控制MPC原理——预测+优化+滚动执行

下一篇预告:第196篇 MPC实时求解——计算效率和嵌入式部署的挑战

有任何问题欢迎评论区留言,我会尽量回复。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐