3. 基于OSQP的MPC求解器配置与验证
任务目标
本案例将完成一个标准线性MPC问题的OSQP求解闭环:构建二次规划(QP)形式的MPC优化问题,配置OSQP求解器参数(容差、迭代上限),验证求解结果满足约束且代价函数达到最优,并对比不同参数配置对求解精度与迭代次数的影响。
环境依赖
需要 osqp 与 numpy。安装命令:pip install osqp numpy。本案例使用OSQP原生Python接口,不依赖CasADi等高级封装,以便直接观察求解器参数配置。
MPC问题定义
考虑离散时间线性系统 x_{k+1} = A x_k + B u_k,预测时域 N=10,状态维度 n=2,控制维度 m=1。目标函数为 Σ(x^T Q x + u^T R u),约束为控制输入限幅 |u| ≤ 1 和状态约束 |x| ≤ 2。将MPC滚动优化转化为标准QP:
min 0.5 z^T P z + q^T z, s.t. l ≤ A_ineq z ≤ u_ineq
其中 z = [u_0, x_1, u_1, x_2, ..., u_{N-1}, x_N] 为决策变量。
关键完整代码
import numpy as np
import osqp
from scipy import sparse
# 系统模型
A = np.array([[1.0, 0.1], [0.0, 1.0]])
B = np.array([[0.0], [1.0]])
n, m = 2, 1
N = 10 # 预测时域
# 权重矩阵
Q = np.diag([1.0, 1.0])
R = np.array([[0.1]])
# 初始状态与目标
x0 = np.array([1.5, 0.5])
x_ref = np.zeros(n)
# 构建QP矩阵
nx = n * (N + 1) # x0..xN
nu = m * N # u0..uN-1
nz = nx + nu
# 等式约束: x_{k+1} = A x_k + B u_k
Aeq_rows, Aeq_cols, Aeq_vals = [], [], []
leq, ueq = [], []
# 初始状态约束 x0 = x_init
for i in range(n):
Aeq_rows.append(i); Aeq_cols.append(i); Aeq_vals.append(1.0)
leq.append(x0[i]); ueq.append(x0[i])
# 动态约束
for k in range(N):
row_offset = n * (k + 1)
u_offset = nx + m * k
for i in range(n):
row = row_offset + i
# x_{k+1} - A x_k - B u_k = 0
for j in range(n):
Aeq_rows.append(row); Aeq_cols.append(n*k + j); Aeq_vals.append(-A[i, j])
Aeq_rows.append(row); Aeq_cols.append(row_offset + i); Aeq_vals.append(1.0)
for j in range(m):
Aeq_rows.append(row); Aeq_cols.append(u_offset + j); Aeq_vals.append(-B[i, j])
leq.append(0.0); ueq.append(0.0)
# 不等式约束: |u_k| ≤ 1, |x_k| ≤ 2 (x0除外)
Aineq_rows, Aineq_cols, Aineq_vals = [], [], []
lineq, uineq = [], []
for k in range(N):
u_idx = nx + m * k
# u_k ≤ 1, -u_k ≤ 1
Aineq_rows.append(len(lineq)); Aineq_cols.append(u_idx); Aineq_vals.append(1.0)
lineq.append(-np.inf); uineq.append(1.0)
Aineq_rows.append(len(lineq)); Aineq_cols.append(u_idx); Aineq_vals.append(-1.0)
lineq.append(-np.inf); uineq.append(1.0)
for k in range(1, N+1):
for i in range(n):
x_idx = n*k + i
Aineq_rows.append(len(lineq)); Aineq_cols.append(x_idx); Aineq_vals.append(1.0)
lineq.append(-np.inf); uineq.append(2.0)
Aineq_rows.append(len(lineq)); Aineq_cols.append(x_idx); Aineq_vals.append(-1.0)
lineq.append(-np.inf); uineq.append(2.0)
# 目标函数: 0.5 z^T P z + q^T z
P = sparse.csc_matrix((nz, nz))
q = np.zeros(nz)
# 状态代价 (x_k - x_ref)^T Q (x_k - x_ref), k=1..N
for k in range(1, N+1):
for i in range(n):
idx = n*k + i
for j in range(n):
P[idx, idx] += Q[i, j] # 对角近似,非对角项忽略(Q为对角阵)
# 控制代价 u^T R u
for k in range(N):
u_idx = nx + m*k
P[u_idx, u_idx] += R[0, 0]
P = sparse.csc_matrix(P)
# 组装约束矩阵
A_total = sparse.vstack([
sparse.csc_matrix((np.array(Aeq_rows), (np.array(Aeq_cols), np.array(Aeq_vals))), shape=(len(leq), nz)),
sparse.csc_matrix((np.array(Aineq_rows), (np.array(Aineq_cols), np.array(Aineq_vals))), shape=(len(lineq), nz))
], format='csc')
l_total = np.concatenate([leq, lineq])
u_total = np.concatenate([ueq, uineq])
# 求解器配置与求解
solver = osqp.OSQP()
solver.setup(P=P, q=q, A=A_total, l=l_total, u=u_total,
eps_abs=1e-5, eps_rel=1e-5, max_iter=5000, verbose=False)
result = solver.solve()
# 提取控制序列
u_opt = result.x[nx:nx+m*N].reshape(N, m)
x_traj = result.x[:nx].reshape(N+1, n)
# 验证:约束满足性
u_max_viol = np.max(np.abs(u_opt)) - 1.0
x_max_viol = np.max(np.abs(x_traj[1:])) - 2.0
print(f"求解状态: {result.info.status}")
print(f"迭代次数: {result.info.iter}")
print(f"目标函数值: {result.info.obj_val:.6f}")
print(f"控制约束最大违反: {u_max_viol:.2e}")
print(f"状态约束最大违反: {x_max_viol:.2e}")
print(f"最优控制序列 u*: {u_opt.flatten()}")
运行与验证方法
直接运行上述代码。判定通过条件:
- 求解状态:
result.info.status输出solved,表示OSQP在迭代上限内收敛。 - 约束满足:
u_max_viol与x_max_viol均为负值或接近0(1e-4量级),说明控制与状态均未超出限幅。 - 最优性验证:目标函数值
obj_val为有限正数,且与手动计算代价一致(可自行用u_opt和x_traj代入代价函数验证)。
参数调优对比
将 eps_abs 和 eps_rel 从 1e-5 改为 1e-3,重新运行,观察迭代次数减少但目标函数精度下降(obj_val 偏差增大)。将 max_iter 改为 100,观察状态变为 maximum iterations reached,此时结果不可用——这验证了迭代上限配置对求解成功与否的决定性影响。
预期输出(示例)
求解状态: solved
迭代次数: 45
目标函数值: 1.234567
控制约束最大违反: -3.21e-06
状态约束最大违反: -1.08e-05
最优控制序列 u*: [-0.823 -0.412 -0.198 ...]
注意:具体数值由OSQP内部算法决定,不同版本可能有微小差异,但状态必须为 solved 且违反量在 1e-4 以下。