1. 为什么我要啃 CasADi 这块硬骨头
第一次听到 CasADi 这个名字,是在一个做机械臂轨迹规划的小圈子里。当时我正被一个六轴机械臂的时间最优轨迹问题折磨得够呛,用传统的手写雅可比矩阵加数值优化,代码写了八百多行,跑一次要等十几秒,稍微改一下约束条件就得推倒重来。有个做控制的朋友甩给我一句:“你试试 CasADi 吧,符号框架加自动微分,写起来跟写数学公式差不多。”我当时半信半疑,一个开源框架能有多神?结果花了一个周末啃完基础文档,把原来的问题重写了一遍,代码量直接砍到一百多行,求解速度还快了一个数量级。从那以后,CasADi 就成了我工具箱里常驻的核心组件。
CasADi 到底是什么?简单说,它是一个用于非线性优化和最优控制的开源框架,核心能力是符号计算加自动微分。你可以把它理解成一个“数学公式翻译器加求导器”:你用接近数学表达式的语法把问题写出来,它自动帮你算出雅可比矩阵、海森矩阵,然后对接各种数值求解器(比如 IPOPT、SNOPT、qpoases)去求解。它解决的问题非常明确——当你需要处理带约束的非线性优化问题,尤其是涉及微分方程、动态系统、机器人运动学动力学这类场景时,手写导数既容易出错又极其耗时,CasADi 就是来把你从这种重复劳动里解放出来的。
这篇文章适合谁看?如果你正在做机器人控制、轨迹优化、模型预测控制(MPC)、系统辨识、或者任何需要反复求解非线性规划(NLP)的活儿,而且你已经被手动求导或者数值差分折磨过,那这篇内容就是为你准备的。如果你只是听说过 CasADi 但一直没敢下手,或者装完环境跑了个 demo 就不知道下一步该干嘛,那正好,我把从零上手到跑通第一个实际问题的完整路径都梳理一遍。我不会只给你贴官方文档的翻译,而是把我在实际项目里踩过的坑、绕过的弯、总结出来的高效用法都摊开来讲。
2. CasADi 的核心设计思路拆解
2.1 符号框架与数值求解的边界在哪里
很多人第一次接触 CasADi 会有一个误解,以为它是个“求解器”。其实不是。CasADi 本身不做数值求解,它是一个建模层和微分层。真正的求解工作是由它背后对接的求解器完成的,比如 IPOPT 负责非线性规划,qpoases 负责二次规划,SUNDIALS 负责微分方程积分。CasADi 的职责是:让你用一种统一的符号语言描述问题,自动生成求解器需要的所有导数信息,然后把问题打包成求解器能吃的格式递过去。
这个设计思路的好处非常明显。传统做法里,你写一个优化问题,目标函数和约束的梯度、雅可比、海森矩阵都得自己推公式、自己写代码。一旦问题结构变了,比如加了一个新约束,所有导数都得重新推。CasADi 用符号表达式记录你的计算图,然后通过自动微分(具体来说是反向模式 AD 和前向模式 AD 的结合)在需要的时候精确计算任意阶导数。注意,是精确计算,不是数值差分近似。数值差分不仅慢,而且步长选不好精度直接崩掉。自动微分没有这个问题,它利用链式法则在计算图上一层一层传播导数,精度是机器精度级别。
那什么时候该用 CasADi,什么时候不该用?我的经验是:如果你的问题可以用矩阵运算和基本数学函数(sin、cos、exp、log、sqrt 等)表达出来,而且你需要反复求导或者反复求解,那 CasADi 就是利器。反过来,如果你的问题本身很简单,比如一个无约束的二次函数最小化,手写解析解或者用现成的库更省事。CasADi 的价值在复杂问题和反复迭代的场景里才真正体现出来。
2.2 自动微分为什么比手写导数靠谱
我举个实际例子你就明白了。假设你要做一个两连杆机械臂的逆动力学计算,末端执行器的位置是关节角度的函数:
p = l1*cos(q1) + l2*cos(q1+q2)你要算末端位置对 q1 和 q2 的偏导数,手写的话就是:
dp/dq1 = -l1*sin(q1) - l2*sin(q1+q2) dp/dq2 = -l2*sin(q1+q2)这还只是两个关节。如果是六轴机械臂,末端位置对六个关节角的偏导数,手写雅可比矩阵就是一场噩梦,而且极容易在某个符号上出错。更麻烦的是,如果你后面还要算二阶导数(比如做基于梯度的优化需要海森矩阵),手写几乎不可能保证正确。
CasADi 的做法是:你只管写p = l1*cos(q1) + l2*cos(q1+q2),然后调用jacobian(p, [q1, q2]),它自动给你返回符号形式的雅可比表达式。你还可以进一步对这个雅可比再求导,得到二阶导数。整个过程不需要你手动推任何公式。我在实际项目里做过对比:一个包含 12 个状态变量、6 个控制输入的 MPC 问题,手写导数的版本调试了整整两天才跑通,而且每次改模型都要重新检查导数;用 CasADi 重写后,模型改完直接跑,导数自动更新,再也没有因为导数错误导致求解器不收敛的情况。
注意:自动微分虽然精确,但符号表达式如果写得过于复杂(比如大量嵌套的条件判断),会导致计算图膨胀,求导效率下降。我的经验是尽量用向量化操作,避免在符号表达式里写循环。
2.3 与同类工具的横向对比
市面上做优化建模的工具不止 CasADi 一个。我列几个常见的对比一下,方便你判断什么场景该选什么。
| 工具 | 核心特点 | 适用场景 | 学习曲线 |
|---|---|---|---|
| CasADi | 符号建模 + 自动微分 + 多求解器接口 | 最优控制、MPC、机器人 | 中等 |
| YALMIP | MATLAB 建模层,侧重凸优化 | 凸优化、LMI | 较低(MATLAB 用户) |
| CVXPY | Python 凸优化建模 | 凸优化、投资组合 | 较低 |
| Gurobi/CPLEX | 商业求解器,自带建模接口 | 线性/整数规划 | 中等 |
| Pyomo | Python 通用优化建模 | 通用数学规划 | 中等 |
CasADi 的差异化优势在于非凸问题和微分方程约束。YALMIP 和 CVXPY 主要面向凸优化,虽然也能处理一些非凸问题,但能力和 CasADi 不在一个量级。Gurobi 和 CPLEX 是求解器,建模能力相对有限。Pyomo 更偏向于传统数学规划,对动态系统的支持不如 CasADi 自然。所以如果你的问题涉及微分方程、动态系统、或者需要反复求高阶导数,CasADi 基本是首选。
3. 环境搭建与第一个可运行示例
3.1 安装路径选择与依赖处理
CasADi 的安装方式主要有三种:pip 安装、conda 安装、源码编译。我强烈建议新手走pip 安装,一条命令搞定:
pip install casadi如果你用 conda,也可以:
conda install -c conda-forge casadi源码编译只推荐给需要自定义求解器接口或者做二次开发的人,普通用户没必要折腾。编译过程涉及 CMake、IPOPT 源码、HSL 库等一系列依赖,我第一次编译花了整整一个下午,中间因为 HSL 库的许可证问题卡了很久。除非你有明确需求,否则 pip 版本足够用。
安装完成后,验证一下:
import casadi as ca print(ca.__version__)如果输出版本号,说明安装成功。这里有个小坑:CasADi 的 Python 包名是casadi,但导入时通常简写为ca,这是社区惯例,官方文档也这么用。
提示:如果你在 Windows 上安装,建议用 Python 3.8 到 3.11 之间的版本,太新的 Python 版本可能还没有预编译的 wheel 包。我在 Python 3.12 刚发布时试过,pip 找不到对应 wheel,只能降级。
3.2 符号变量定义与基本运算
CasADi 的核心对象是MX和SX。这两个都是符号类型,区别在于:
- SX:标量符号,适合小规模问题,展开后计算效率高。
- MX:矩阵符号,适合大规模问题,支持稀疏性,但单个元素的操作开销比 SX 大。
新手先用 SX 就行,简单直观。定义一个符号变量:
import casadi as ca x = ca.SX.sym('x') y = ca.SX.sym('y')这就定义了两个标量符号。你也可以定义向量:
v = ca.SX.sym('v', 3) # 3维向量 M = ca.SX.sym('M', 2, 2) # 2x2矩阵定义好符号后,就可以像写普通数学表达式一样构造函数:
f = x**2 + ca.sin(y) + x*y这个f现在是一个符号表达式,不是数值。你可以对它求导:
df_dx = ca.jacobian(f, x) df_dy = ca.jacobian(f, y)ca.jacobian返回的也是符号表达式。如果你想把它变成可调用的函数,用ca.Function:
f_func = ca.Function('f', [x, y], [f]) print(f_func(2, 3)) # 输出数值结果这个ca.Function是 CasADi 里非常核心的概念,后面做优化、积分、求解都要用到它。你可以把它理解成一个“编译后的数学函数”,输入数值,输出数值,内部的计算图已经优化过了。
3.3 一个完整的非线性规划求解实例
光说不练假把式。我们来看一个完整的例子:求解一个带约束的非线性优化问题。
问题描述:最小化(x1 - 3)^2 + (x2 - 2)^2,约束条件是x1^2 + x2^2 <= 5,x1 >= 0,x2 >= 0。
用 CasADi 写出来是这样的:
import casadi as ca # 定义符号变量 x1 = ca.SX.sym('x1') x2 = ca.SX.sym('x2') x = ca.vertcat(x1, x2) # 拼成向量 # 目标函数 obj = (x1 - 3)**2 + (x2 - 2)**2 # 约束条件 g = x1**2 + x2**2 # 不等式约束 g <= 5 lbg = -ca.inf ubg = 5 # 变量上下界 lbx = [0, 0] ubx = [ca.inf, ca.inf] # 构造 NLP 问题 nlp = {'x': x, 'f': obj, 'g': g} solver = ca.nlpsol('solver', 'ipopt', nlp) # 初始猜测 x0 = [1, 1] # 求解 sol = solver(x0=x0, lbx=lbx, ubx=ubx, lbg=lbg, ubg=ubg) print(sol['x'])跑一下,输出应该是接近[2.236, 0]或者[1.8, 1.6]附近的解,具体取决于初始点。这个例子里,我们只写了目标函数和约束的表达式,没有写任何导数。IPOPT 求解器需要的梯度、雅可比、海森矩阵,CasADi 全部自动生成。这就是它最核心的价值。
注意:
ca.nlpsol的第一个参数是求解器名字,第二个参数是求解器类型。IPOPT 是开源里最常用的非线性求解器,适合中小规模问题。如果你的问题有大量整数变量,IPOPT 就不合适了,得换别的工具。
4. 实操过程中容易踩的坑与排查技巧
4.1 求解器不收敛的常见原因
第一次用 CasADi 跑优化,十有八九会遇到求解器不收敛的情况。IPOPT 会返回一个状态码,比如success、max_iter、infeasible之类的。我整理了一个排查表,按优先级从高到低检查:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 迭代次数达到上限 | 初始点太差 | 换几个不同的初始点试试 |
| 约束不可行 | 约束条件互相矛盾 | 检查约束边界是否合理 |
| 梯度计算异常 | 表达式里有除零或 log 负数 | 检查符号表达式定义域 |
| 求解速度极慢 | 问题规模太大或表达式太复杂 | 用 MX 替代 SX,启用稀疏性 |
| 返回 NaN | 数值溢出 | 检查是否有指数爆炸项 |
我印象最深的一次是做一个轨迹优化,求解器一直返回infeasible。查了半天发现是约束里有一个sqrt函数,而某个中间变量的值在迭代过程中变成了负数,导致sqrt返回 NaN,整个求解崩溃。解决办法是在sqrt外面套一个ca.fmax(0, x),保证输入非负。这种问题在纯数值代码里也会遇到,但在符号框架里更容易被忽略,因为表达式是“一次性写好”的,运行时才暴露问题。
4.2 符号表达式膨胀与性能优化
CasADi 的符号表达式如果写得不好,会变得极其庞大。比如你在符号层面写了一个循环,每次迭代都往表达式里追加一项,最后这个表达式可能有几千个节点。求导的时候,计算图会爆炸,内存和时间都吃不消。
我的经验是:尽量用向量化操作,避免在符号层面写循环。比如你要计算一个求和:
# 不好的写法 total = 0 for i in range(100): total += x[i]**2 # 好的写法 total = ca.dot(x, x)ca.dot是 CasADi 内置的向量点积,生成的符号表达式简洁得多。类似的内置函数还有ca.sum1、ca.sum2、ca.mtimes等。另外,如果问题规模确实很大,考虑用MX代替SX。MX支持稀疏矩阵运算,不会把每个元素都展开成标量节点,内存占用和求导效率都会好很多。
还有一个技巧是把重复使用的子表达式提取出来。比如:
# 重复计算 f1 = ca.sin(x) + ca.cos(x) f2 = ca.sin(x) * ca.cos(x) # 提取公共项 s = ca.sin(x) c = ca.cos(x) f1 = s + c f2 = s * c这样 CasADi 在构建计算图时会共享子节点,减少冗余。
4.3 与 NumPy 的互操作细节
CasADi 的符号类型和 NumPy 数组之间的转换是新手经常卡住的地方。几个关键点:
ca.DM是 CasADi 的数值矩阵类型,可以和 NumPy 数组互相转换。ca.DM(np_array)把 NumPy 数组转成 CasADi 矩阵。np.array(ca_dm)把 CasADi 矩阵转回 NumPy 数组。- 符号表达式(SX/MX)不能直接转 NumPy,必须先变成
ca.Function再调用。
我踩过的一个坑是:在定义优化问题时,把 NumPy 数组直接传给了ca.SX.sym的维度参数,结果报错。正确的做法是用整数指定维度,或者用ca.DM包装数值数据。另外,CasADi 的矩阵索引是从 0 开始的,和 NumPy 一致,但切片语法略有不同,建议查一下官方文档的索引规则。
提示:如果你在 Jupyter Notebook 里用 CasADi,记得把符号表达式用
ca.Function包装后再打印,否则输出的是巨大的符号树,Notebook 会卡死。我吃过这个亏,一个不小心打印了一个未包装的表达式,浏览器直接无响应。
5. 从学习到实战的进阶路径
5.1 掌握 CasADi 需要补哪些数学基础
CasADi 本身不难学,但它背后的数学概念需要你有一定基础。我列几个关键点:
- 非线性规划:理解目标函数、约束、拉格朗日乘子、KKT 条件。不需要你会手推,但至少知道求解器在干什么。
- 自动微分:理解链式法则、前向模式、反向模式。知道为什么自动微分比数值差分准。
- 常微分方程:如果你要做最优控制,得知道 ODE 的基本形式和数值积分方法(如 RK4)。
- 稀疏矩阵:大规模问题里稀疏性决定性能,知道雅可比矩阵的稀疏结构怎么影响求解效率。
这些不需要你一次性全学会,边用边补就行。我的路径是:先跑通官方示例,然后拿一个自己手头的小问题练手,遇到不懂的概念再回头查资料。这样学得最快,也最扎实。
5.2 官方示例与文档的高效使用方法
CasADi 的官方文档质量很高,但内容很多,新手容易迷失。我的建议是:
- 先看User Guide的前三章,把 SX、MX、Function 这三个概念搞清楚。
- 然后直接跳到Examples页面,找和你问题最接近的示例,复制下来跑通。
- 跑通后,逐行修改参数,观察输出变化,理解每个参数的作用。
- 最后再回头补理论细节。
官方示例里有一个optimal_control的目录,里面包含了 MPC、轨迹优化、微分方程约束优化的完整代码。这些示例是我见过的最好的学习材料,比任何教程都管用。我当初就是把race_car那个示例反复改了十几遍,才真正理解 CasADi 在最优控制里的用法。
5.3 一个真实项目的简化复盘
最后分享一个我用 CasADi 做的实际项目片段。项目背景是一个三自由度的平面机械臂,需要做点到点的最优轨迹规划,约束包括关节角度限制、关节速度限制、以及末端执行器不能碰到一个圆形障碍物。
核心代码结构是这样的:
import casadi as ca # 状态变量:q1, q2, q3, dq1, dq2, dq3 q = ca.SX.sym('q', 3) dq = ca.SX.sym('dq', 3) x = ca.vertcat(q, dq) # 控制变量:关节力矩 u = ca.SX.sym('u', 3) # 动力学方程(简化版) M = ca.SX.zeros(3, 3) # 质量矩阵,实际项目里是 q 的函数 # ... 填充 M ... ddq = ca.solve(M, u) # 加速度 # 状态方程 xdot = ca.vertcat(dq, ddq) f = ca.Function('f', [x, u], [xdot]) # 障碍物约束 p_end = forward_kinematics(q) # 末端位置 obstacle_center = ca.DM([0.5, 0.5]) dist = ca.norm_2(p_end - obstacle_center) g_obs = dist # 约束:dist >= 0.2 # 构造最优控制问题 # ... 离散化、构造 NLP、调用 IPOPT ...这个项目里,CasADi 帮我省掉了所有动力学方程的符号推导和导数计算。质量矩阵 M 是 q 的复杂函数,手写导数几乎不可能,但 CasADi 自动处理了。整个开发周期从预计的两周压缩到三天,而且代码可读性极高,后续改约束条件只需要改几行。
注意:动力学方程里的
ca.solve(M, u)在 M 接近奇异时会出问题。实际项目里我加了一个正则化项M + 1e-6*ca.SX.eye(3),避免数值不稳定。这种细节官方文档不会写,但实际用的时候必须考虑。
6. 一些零散但重要的经验补充
6.1 版本兼容性与更新策略
CasADi 的版本更新比较频繁,不同版本之间的 API 偶尔会有变化。我的建议是:生产项目锁定版本,学习阶段用最新版。锁定版本可以避免因为 API 变动导致代码突然跑不了。我一般会在requirements.txt里写死版本号,比如casadi==3.6.3。学习阶段用最新版可以体验到新功能和性能改进。
另外,CasADi 的 Python 接口和 C++ 接口在功能上基本对齐,但有些高级特性(比如自定义求解器接口)只在 C++ 里提供。如果你只是做建模和求解,Python 版完全够用。
6.2 调试符号表达式的实用技巧
调试符号表达式比调试数值代码麻烦,因为你看不到中间值。我的几个常用技巧:
- 用
ca.Function把中间表达式包装成函数,传入具体数值,打印输出,验证逻辑。 - 用
ca.gradient和ca.jacobian检查导数表达式是否符合预期。 - 对于复杂表达式,分段验证:先验证一小部分,再逐步拼接。
- 用
ca.print_expression或者直接print符号表达式,但注意表达式太大时会刷屏。
我遇到过一个 bug:符号表达式里用了ca.if_else,但两个分支的维度不一致,导致运行时维度错误。这种问题在数值代码里很容易发现,但在符号层面需要仔细检查每个分支的输出形状。
6.3 社区资源与持续学习建议
CasADi 的社区不算特别大,但质量很高。官方论坛里有很多深度讨论,尤其是关于求解器调参和性能优化的帖子。另外,GitHub 上的 issue 区也值得翻,很多常见问题别人已经踩过坑了。
如果你想深入,我建议读一读 CasADi 作者的那篇论文(Andersson et al., 2019),里面详细讲了框架的设计哲学和自动微分的实现细节。读完之后,你对 CasADi 的理解会从“会用”上升到“知道为什么这么设计”。
最后再分享一个小技巧:CasADi 的ca.Function支持jit选项,可以启用即时编译加速。对于反复调用的函数,开启jit=True能显著提升性能。不过 JIT 编译第一次调用会慢一些,适合长期运行的任务。
这个内容后续还可以这样扩展:把 CasADi 和深度学习框架结合,用自动微分做端到端的优化控制;或者用 CasADi 做模型预测控制,对接实际硬件做实时控制。这些方向我都在探索,后面有机会再单独写。