1. 为什么一个搞控制的会盯上CasADi
第一次听说CasADi是在做一个四旋翼轨迹跟踪的项目。当时用传统的手写雅可比矩阵方式做非线性模型预测控制,状态量一多,求导推到怀疑人生,改一次模型就要重新推一遍偏导,调试周期长得离谱。后来组里一位做优化出身的朋友丢给我一句“你试试CasADi吧,自动微分加符号计算,能省掉你一半的命”,我才算正式入坑。
CasADi本质上是一个面向数值优化的开源框架,核心能力有三块:符号计算、自动微分、非线性规划求解接口。它最擅长的场景是最优控制、模型预测控制(MPC)、轨迹优化、参数估计这类需要反复求梯度、反复求解优化问题的活儿。如果你平时写控制算法还在手推雅可比,或者用数值差分近似梯度导致精度和速度都上不去,那CasADi基本就是为你准备的。
这篇是系列的第一篇,我不打算一上来就堆API,而是先把“它是什么、为什么快、怎么装、第一个能跑通的例子”这几件事讲透。适合两类人看:一类是刚接触最优控制、想找个趁手工具的学生;另一类是做机器人、自动驾驶、过程控制,需要把优化嵌进实时循环的工程师。看完你应该能独立跑通一个带约束的非线性优化,并且理解背后每一步在干什么。
2. CasADi到底解决了谁的痛点
2.1 手推雅可比的时代该结束了
做过非线性优化的都清楚,梯度信息决定了求解器能不能收敛、收敛得多快。以最经典的SQP(序列二次规划)和IPOPT(内点法)为例,它们都需要目标函数的梯度、约束的雅可比矩阵,甚至海森矩阵。传统做法有两种:一是人工推导解析导数,二是用有限差分近似。
人工推导的问题在于,模型稍微一改,比如把气动阻力项从二次改成三次,整张雅可比表就得重推,而且极容易出错,一个符号写反,求解器可能不报错但收敛到错误的点。有限差分的问题更直接:精度受步长影响大,状态维度高的时候计算量是O(n)次函数评估,实时性根本扛不住。
CasADi用的是算法微分(Algorithmic Differentiation,AD),也叫自动微分。它不是符号推导那种把表达式无限膨胀的方式,也不是数值差分的近似,而是把整个计算过程拆成基本运算,在计算图上用链式法则精确地传播导数。结果就是:导数精度达到机器精度,计算代价和一次前向计算同量级,而且你改模型不用改任何求导代码。
2.2 符号表达式和数值计算的无缝切换
CasADi另一个让我觉得顺手的地方,是它把符号变量和数值计算统一在同一个类型体系里。你可以先用符号变量SX或MX搭出整个优化问题的表达式,然后交给求解器;也可以把符号表达式编译成函数,传入数值直接算结果。这种“先搭图、后填数”的模式,特别适合MPC这种结构固定、参数每周期变化的场景。
举个直观的例子:你定义状态x和控制u都是符号,写出动力学x_next = f(x,u),CasADi会自动帮你把∂f/∂x和∂f/∂u都准备好。到了实时循环里,你只需要把当前测量值喂进去,它就能吐出数值结果和对应的雅可比。整个过程你一行求导代码都没写。
2.3 SX和MX:两种符号类型怎么选
这是新手最容易懵的地方。CasADi提供两种符号类型:
| 类型 | 特点 | 适用场景 |
|---|---|---|
| SX | 标量符号,展开成完整计算图,速度快但图会膨胀 | 维度小、表达式简单的问题 |
| MX | 矩阵符号,支持稀疏和函数调用,图更紧凑 | 维度大、有稀疏结构、需要复用子表达式 |
我的经验是:状态维度在几十以内、表达式不复杂的,直接用SX,编译出来的函数执行效率最高。一旦维度上百,或者动力学里有大量重复子表达式,就换MX,否则SX展开的图会大到编译都卡。这个选择在后面做MPC时会反复遇到,先有个印象。
3. 环境搭建:别在第一步卡住
3.1 安装方式的选择
CasADi官方提供多种安装途径,我按推荐程度排个序:
- pip安装:
pip install casadi,最省事,适合快速验证和纯Python开发。缺点是默认不带IPOPT等求解器的完整接口,某些高级功能受限。 - conda安装:
conda install -c conda-forge casadi,依赖管理干净,求解器支持比pip版全。 - 官方预编译包:从官网下载对应平台的压缩包,解压后配置环境变量。适合需要特定版本或要嵌入C++项目的场景。
- 源码编译:除非你要改源码或者需要极致定制,否则不建议,依赖链很长。
我自己的主力环境是conda,因为做MPC经常要配合IPOPT,conda-forge的包把接口都配好了,省心。
3.2 验证安装是否成功
装完别急着写复杂代码,先跑这三行:
import casadi as ca x = ca.SX.sym('x') print(ca.sin(x))如果输出类似sin(x)的符号表达式,说明核心库正常。再验证求解器:
print(ca.nlpsol('solver', 'ipopt', {'x': x, 'f': x**2}))能打印出求解器对象,说明IPOPT接口可用。如果这里报错说找不到ipopt,多半是安装方式没带求解器,换conda重装即可。
注意:Windows下用pip装CasADi有时会遇到DLL加载失败,通常是缺少运行时库。遇到这种情况优先换conda,或者装一下对应的Visual C++运行库,别在pip上死磕。
3.3 版本差异的坑
CasADi在3.5到3.6之间有一些API变动,比如casadi.Opti的某些方法和求解器选项的写法。如果你在网上抄的代码跑不通,先查一下版本。用ca.__version__看当前版本,然后对照官方文档的对应版本。我踩过一次坑:旧教程里opti.solver('ipopt', {'print_time': False})的写法在新版本里选项结构变了,排查了半天才发现是版本问题。
4. 第一个能跑通的非线性优化
4.1 从数学问题到代码
我们拿一个最经典的问题练手:带约束的Rosenbrock函数最小化。数学形式是
minimize (1-x)² + 100(y-x²)² subject to x² + y² = 1
这个问题有非线性目标和非线性等式约束,正好覆盖CasADi的核心用法。用Opti接口写出来是这样:
import casadi as ca opti = ca.Opti() x = opti.variable() y = opti.variable() opti.minimize((1 - x)**2 + 100 * (y - x**2)**2) opti.subject_to(x**2 + y**2 == 1) opti.solver('ipopt') sol = opti.solve() print(sol.value(x), sol.value(y))这段代码里,opti.variable()声明决策变量,opti.minimize()给目标,opti.subject_to()加约束,opti.solver('ipopt')指定求解器,opti.solve()触发求解。整个过程你只写了数学表达式,没有一行求导代码。
4.2 求解器在背后做了什么
IPOPT是内点法求解器,它需要目标梯度、约束雅可比、拉格朗日海森矩阵。这些CasADi全部通过自动微分提供。具体流程是:CasADi先把你的表达式构建成计算图,然后对图做算法微分得到一阶和二阶导数信息,再把这些信息以IPOPT要求的回调形式传进去。IPOPT每迭代一步,都会调用这些回调拿梯度和海森,直到满足收敛条件。
理解这一点很重要,因为它解释了为什么CasADi快:导数不是每次迭代重新推导的,而是在图构建阶段就准备好了传播路径,迭代时只是数值传播,代价极低。
4.3 求解结果怎么读
sol.value(x)拿到的是最优解数值。除此之外,sol.value()还能取目标值、约束乘子等。想看求解过程的详细信息,可以在solver里加选项:
opti.solver('ipopt', {'print_time': True}, {'print_level': 5})print_level调到5会打印每次迭代的残差、步长等信息。调试收敛问题时这个特别有用,能看出是卡在可行性还是卡在最优性。
提示:如果求解失败,先别改算法,先看IPOPT的返回状态。
sol.stats()里有迭代次数、最终残差、终止条件。很多“求解失败”其实是初值太离谱或者约束互相矛盾,跟求解器本身没关系。
5. 自动微分到底比数值差分强在哪
5.1 一个能直观对比的实验
光说理论没感觉,我们做个实测。定义一个稍微复杂点的函数,比如
f(x) = sin(x₁)·cos(x₂) + exp(x₁·x₂) + x₁³
分别用CasADi的自动微分和中心差分求梯度,对比精度和耗时。自动微分这边:
x = ca.SX.sym('x', 2) f = ca.sin(x[0])*ca.cos(x[1]) + ca.exp(x[0]*x[1]) + x[0]**3 grad_f = ca.Function('grad_f', [x], [ca.gradient(f, x)])中心差分就是手动构造(f(x+h)-f(x-h))/(2h)。在同一个点上跑,你会发现自动微分的结果和解析解完全一致(到机器精度),而中心差分即使选最优步长,误差也在1e-8量级,步长选不好直接掉到1e-4。
5.2 高维情况下的差距是数量级的
单点精度还只是开胃菜。真正拉开差距的是维度。数值差分求n维梯度需要2n次函数评估(中心差分),自动微分的前向模式只需要约1次到常数次。当n=100时,差分要200次评估,自动微分可能就几次。在MPC这种每周期都要算梯度的场景,这个差距直接决定能不能实时。
更关键的是,CasADi还支持前向模式和反向模式的自动微分。反向模式求梯度(一个标量对n个输入)的代价与n无关,这对目标函数是标量的优化问题简直是量身定做。CasADi会根据你的问题结构自动选择或让你手动指定模式。
5.3 稀疏性是隐藏的加速器
实际工程问题里,雅可比矩阵往往是稀疏的——大部分偏导是零。CasADi在构建计算图时会追踪稀疏结构,只对非零元素做微分传播。手推或者差分根本利用不了这个结构,而CasADi能自动识别。做大规模轨迹优化时,这个特性带来的加速比自动微分本身还明显。
我做过一个对比:一个200维状态的轨迹优化问题,用差分近似梯度求解要几分钟,换CasADi后降到几秒。加速来源里,自动微分占一部分,稀疏利用占更大一部分。
6. 新手最容易踩的几个坑
6.1 符号变量和数值变量混用
最常见的错误是把Python的float直接和符号变量做运算,比如x + 1.0没问题,但if x > 0就会报错,因为符号变量没有布尔值。涉及条件判断的地方,要用CasADi的ca.if_else或者ca.fmax/ca.fmin这类符号函数,不能直接用Python的if。
# 错误写法 # y = x if x > 0 else 0 # 正确写法 y = ca.if_else(x > 0, x, 0)这个坑我在写带死区的控制律时踩过,当时死活想不通为什么报类型错误,后来才反应过来符号变量不能当条件用。
6.2 忘记初始化或者初值给得太随意
非线性优化对初值敏感,IPOPT尤其如此。初值给得离解太远,轻则迭代次数暴增,重则直接发散。我的习惯是:能用物理直觉估一个初值就用物理直觉,实在没有就用上一周期的解做热启动。MPC里热启动几乎是标配,能省掉大量迭代。
opti.set_initial(x, x_prev) opti.set_initial(y, y_prev)6.3 约束写成等式还是不等式要想清楚
subject_to里用==是等式约束,用<=或>=是不等式。新手容易把本该是不等式的写成等式,导致可行域变成一个点或者空集,求解器直接报infeasible。比如控制量限幅应该是u <= u_max,写成u == u_max就变成强制等于上限了,完全不是一回事。
6.4 求解器选项乱设
IPOPT有一堆选项,tol、max_iter、acceptable_tol等等。新手有时候看别人代码里设了一堆就照抄,结果反而拖慢收敛。我的建议是:先用默认选项跑,跑不通再针对性调。最常需要调的是max_iter(默认3000,复杂问题可能不够)和print_level(调试时调高看过程)。
注意:
tol设得太小(比如1e-12)会导致迭代次数暴涨甚至无法收敛,因为浮点精度根本达不到。一般1e-6到1e-8就够了,除非你有特殊精度需求。
7. 从这个小例子能延伸出什么
跑通Rosenbrock问题只是起点。CasADi真正的价值在后面:把动力学方程写成符号表达式,构建多步预测的MPC问题,用自动微分拿到所有需要的导数,然后每个控制周期求解一次。这套流程在机器人、无人机、自动驾驶的轨迹跟踪里是标准打法。
下一篇我会接着讲怎么用CasADi搭一个完整的MPC:包括离散化动力学、构造预测时域内的优化问题、处理控制量约束和状态约束、以及热启动和实时性优化。如果你现在能把这篇的例子跑通,并且理解了自动微分和稀疏性这两件事为什么重要,那基础就算打牢了。
我个人在实际项目里的体会是,CasADi最大的价值不是省了求导的代码量,而是让你敢于改模型。以前改一个动力学项要重新推半天导数,现在改完表达式直接跑,试错成本降了一个数量级。这种“敢试”的自由,才是它真正改变工作方式的地方。