资讯详情

YALMIP源码解析:从sdpvar到求解器的完整链路

📅 2026/9/16 17:03:24 | 华诺云谱 👁 阅读
YALMIP源码解析:从sdpvar到求解器的完整链路
简介YALMIP-R20230609.zip 是 MATLAB 优化建模工具 YALMIP 最新 R20230609 版本的完整源码包面向需要构建和求解线性、二次、非线性、整数及动态规划等优化模型的科研人员与工程开发者适合有 MATLAB 基础并希望学习内部实现或进行二次定制的用户。资源共包含 1307 个文件以 1295 个 m 文件为主涵盖主入口、参数配置、求解器注册等核心模块另有少量 txt 说明文档、mat 数据文件、yml 配置和 c 源码用于辅助编译与扩展压缩包整体仅 1.08MB轻量且易部署。目前已有 309 人学习下载源码开放便于深入研读。通过该包可以系统梳理 YALMIP 的模型转换流程了解分支定界、双线性求解等内置算法同时查看底层 C 语言辅助模块的实现为优化算法研究、求解器接口开发或自定义扩展提供完整可用的参考基底。1. 从 sdpvar 到求解器YALMIP 源码包 R20230609 到底改了什么YALMIP 在 MATLAB 优化建模里几乎是事实标准因为它让研究者不需要关心矩阵分解和求解器 API直接用数学符号写约束。这里解析的是 R20230609 源码包里面包含了yalmip.m主接口、compileinterfacedata.m调度器、bnb_branch_and_bound.m和bmibnb_branch_and_bound.m两个分支定界求解器以及findhash.c这样的 MEX 源文件。适合两类人一类是只把 YALMIP 当命令行工具用但想知道optimize背后发生了什么的人另一类是准备把自定义求解器挂进 YALMIP 框架或者想改源码行为的开发者。下面不会停留在sdpvar用法上而是直接对着源码文件讲调度链路和参数影响。2. 解开 YALMIP 的建模核心sdpvar 符号变量与约束链路的源码实现2.1 sdpvar 并不是普通的双精度数组在 MATLAB 命令行输入x sdpvar(2,1)得到的是一个用sdpvar对象封装的符号变量。源码里yalmip.m是顶层构造函数它会检查输入参数并根据维度创建sdpvar类的实例。这个对象内部保存着lmi结构和Variable信息而不是数值。从sdpvar到optimize中间要经历四个阶段变量登记、约束收集、模型编译、求解器调用。在 R20230609 源码中compileinterfacedata.m是核心调度文件。它的输入是Constraint对象和目标函数输出是求解器需要的标准结构。比如对于线性规划YALMIP 需要把约束拆成A x b和Aeq x beq的形式同时还要提取变量上下界。这个文件大概有 3000 行里面处理了几乎所有求解器的数据转换分支。sdpvar 内部字段作用left/right二元运算时的左右操作数对象vars一个稀疏矩阵记录每个变量在线性基中的索引n/m变量的行列维度type表示变量类型如real、integer、binaryextra保存与变量相关的辅助信息比如对角占优约束的标记在实际建模时type字段决定了后续分支定界器是否将变量当作整数处理。如果你在命令行执行intvar(2,1)type会被强制改为integer并且vars索引会记录到不同的整数变量列表中。2.2 一个线性规划模型从定义到求解的完整链路x sdpvar(2,1); % 两个决策变量 F [x(1)x(2) 1, x(1) 0, x(2) 0]; % 约束集合 obj -x(1) - 2*x(2); % 目标函数最小化 optimize(F, obj, sdpsettings(solver,linprog));这里optimize的第三个参数指定了 MATLAB 自带的linprog求解器。当compileinterfacedata.m识别出问题是 LP 后会调用linprog对应的接口函数call_linprog。call_linprog会从 YALMIP 的内部结构中提取稀疏矩阵A、右侧b、目标向量f和上下界lb、ub然后执行求解。如果不指定求解器YALMIP 会通过sdpsettings中的默认求解器顺序选择一个可用的。参数说明sdpsettings(solver,linprog)中的solver是最高优先级的选项verbose控制日志输出级别0 表示静默1 是标准输出2 会显示求解器的详细迭代信息savesolveroutput设为 1 时YALMIP 会把求解器返回的原始结构保存在sol.solveroutput中便于排查问题。2.3 约束对象的内部表示与哈希索引在源码包中findhash.c和findhashsorted.c是两个 C 实现的哈希查找函数。YALMIP 处理大规模问题时需要频繁查找变量在线性项中的位置比如表达式x(1) 2*x(2)中的系数存储。哈希表的作用就是把稀疏变量的索引映射到紧凑的数组下标避免用ismember这种 O(n) 扫描。配合findhash.cYALMIP 在使用sdpvar定义变量时会自动给每个变量分配一个独一无二的id。这个id存储在yalmip的全局YALMIP变量中。修改findhash.c后需要重新编译 MEX 文件否则 MATLAB 仍会加载旧的.mex文件。常见做法是在 MATLAB 中先执行clear mex再运行mex findhash.c。提示在 R20230609 源码中definesolvers.m维护了一个求解器结构体数组每个元素包含tag、version、subversion、solvertype等字段。当 YALMIP 启动时会调用一次这个函数来注册所有已知求解器。如果你手动安装了新求解器但没有出现在solver列表中多半是definesolvers.m里对应的check字段没有通过存在性验证。3. 从 optimize 到底层求解器compileinterfacedata 的调度逻辑与 solver 选择3.1 YALMIP 如何判定问题类型并挑选求解器打开definesolvers.m可以看到 YALMIP 内置了超过 50 个求解器的描述。每个求解器都有solvertype字段比如LP、QP、SDP、NLP、MIQP等。compileinterfacedata.m在对模型做预分析后会根据约束和目标的类型生成一个候选求解器列表。候选列表的生成逻辑是先检测是否有二元变量或整数变量如果有就只保留支持整数规划的求解器然后检测是否有二次项若有则去掉只支持线性规划的求解器。满足条件的求解器按definesolvers.m中的顺序排列sdpsettings的solver选项可以强制指定。例如用 Gurobi 求解混合整数线性规划时常见配置是opts sdpsettings(solver,gurobi,verbose,1); opts.gurobi.MIPGap 0.01; % 设置MIP相对间隙 opts.gurobi.TimeLimit 60; % 限制60秒 optimize(F, obj, opts);这里的opts.gurobi.MIPGap是 Gurobi 特有参数YALMIP 会通过call_gurobi接口把它们写入 Gurobi 的params结构。在 YALMIP R20230609 中对 Gurobi 13.0.3 的支持是正常的但要注意call_gurobi.m中读取版本号的逻辑版本字符串解析失败时可能回退到默认选项。3.2 compileinterfacedata 的输出结构compileinterfacedata最终返回一个model结构里面包含求解器需要的全部数据。以 LP 为例关键字段包括字段名含义典型形状F_struc所有线性约束的稀疏系数矩阵(m, n1)第一列是右侧常数c目标函数中线性部分系数(n, 1)Q二次目标项系数矩阵(n, n)稀疏lb/ub变量下界和上界(n, 1)binary二元变量标识索引(1, n) 逻辑向量integer整数变量标识索引(1, n) 逻辑向量对于二次规划Q会在预解析阶段被对称化。对于具有非凸约束的问题compileinterfacedata会把模型标记为nonconvex并交给bnb_branch_and_bound这样的非线性分支定界求解器处理。我在调试时通常会打印model.solver.tag来确认真实调用的是哪个求解器避免被sdpsettings里的默认行为误导。3.3 求解器路径和版本检测的坑YALMIP 通过which(gurobi)来检测 Gurobi 是否在 MATLAB 路径上。如果你安装的是 Gurobi 13.0.3但definesolvers.m里写的是旧版本号YALMIP 仍然可以识别因为检测逻辑主要看是否存在gurobi可执行函数而不是严格比对版本。但有个坑是 Gurobi 的 MATLAB 接口在 10.0 之后修改了参数名比如MIPGap改成了MIPGapAbs等。YALMIP 的call_gurobi是通过fieldnames动态映射选项的所以如果你自作聪明在sdpsettings里直接写opts.gurobi.MIPGapAbs可能会因为大小写不匹配而失效。注意查看call_gurobi.m中options字段的拼接方式YALMIP 会把opts.gurobi下的所有字段转换成 Gurobi 的.prm结构。因此建议统一用大驼峰命名并且以 Gurobi 官方文档为准。遇到「求解器返回空解」时优先检查sol.info和sol.solveroutput不要只看sol.problem。4. 分支定界与非线性求解bnb_branch_and_bound 和 bmibnb 的实现要点4.1 内置求解器的适用边界YALMIP 并不是求解器它只负责建模和翻译。但对于没有商业求解器的用户源码包里的bnb_branch_and_bound.m和bmibnb_branch_and_bound.m提供了两个内置备用方案。bnb面向非凸二次约束和混合整数二次规划bmibnb则能处理更一般的多项式非线性约束比如x*y x^2 1这种双线性项。这些内置求解器适合小规模教学和验证不适合生产环境。因为它们依赖局部分支切分没有预处理和割平面增强求解 50 个整数变量的问题就可能跑到几分钟。但作为源码学习材料它们非常适合用来理解分支定界的节点选择、松弛解算和上下界更新。4.2 一个非凸问题的求解演示假设我们要最小化一个非凸目标sin(x) (y-2)^2同时满足双线性约束x*y 1x sdpvar(1,1); y sdpvar(1,1); obj sin(x) (y-2)^2; F [x*y 1, 0 x 5, 0 y 5]; ops sdpsettings(solver,bmibnb,debug,1); optimize(F, obj, ops);bmibnb会先对非线性项做凸松弛比如把sin(x)用分段线性包络近似然后对边界进行二分。每到一个节点它求解一个松弛子问题来获得全局下界如果该节点可行解的目标值小于当前上界就更新上界。debug1会在求解失败时输出模型转换的中间文件方便定位是哪个约束导致不可行。bmibnb 参数默认值作用bmibnb.maxiter100分支迭代上限bmibnb.numglobal1全局搜索初始节点数bmibnb.roottight0是否在根节点做更强的边界收紧bmibnb.presolver1是否启用预解析参数说明solver指定bmibnb即为强制调用内置全局求解器bmibnb.maxiter控制最大迭代次数默认 100bmibnb.numglobal是全局搜索的启动数量默认 1。实际使用时我会先设置verbose2看每层分支的边界情况如果下界长时间不下降就说明松弛太松散需要添加更多切比雪夫点。4.3 callmpcvx 与 MPC 的循环求解callmpcvx.m是 YALMIP 和 MPC 工具包的衔接文件。在模型预测控制中每个采样周期都要解一个二次规划或混合整数二次规划。YALMIP 的optimize每次调用都会重新编译模型这在线性时不变 MPC 中会产生大量重复编译开销。更好的做法是使用export导出模型然后在每个采样周期只更新初始状态。[A,B] ...; % 系统矩阵 nx size(A,1); nu size(B,2); N 10; % 预测时域 U sdpvar(repmat(nu,1,N), repmat(1,1,N)); X sdpvar(repmat(nx,1,N1), repmat(1,1,N1)); constraints []; objective 0; for k 1:N constraints [constraints, X{k1} A*X{k} B*U{k}]; constraints [constraints, -1 U{k} 1]; objective objective X{k}*Q*X{k} U{k}*R*U{k}; end ops sdpsettings(solver,gurobi); optimize([constraints, X{1} x_initial], objective, ops);callmpcvx在源码中维护了一个求解器缓存它尝试复用上一次的求解器实例以减少重复初始化成本。但要注意如果你的 MPC 约束中包含时变矩阵比如参考轨迹变化缓存会失效此时应该主动调用clearmpcvx来清空。提示在 R20230609 中callmpcvx的主要作用是处理mpcvx求解器接口而不是通用的 MPC 工具函数。官方推荐使用YALMIP自带的optimizer对象来加速 MPC 在线计算。optimizer会把模型编译过程预先执行一遍后续只需传入已知参数。5. 源码级调优从 findhash.c 看懂变量哈希索引与自定义求解器扩展5.1 重新编译 MEX 加速大规模变量操作findhash.c实现了一个开放定址哈希表用于查找sdpvar对象的id到内部索引的映射。当你创建了上万个变量时每构造一个线性表达式都要做大量查找。源码中默认表大小是 1024当元素数量超过 70% 负载因子时会触发findhash.c中的扩容逻辑。如果你要处理超大规模稀疏问题可以修改findhash.c中的初始表大小并重新编译mex findhash.c mex findhashsorted.c编译完成后用clear mex确保新版本生效。一般我会在findhash.c里加一行调试代码打印每次 rehash 的触发次数if (counters-collision 1000) ...这样可以直观看到容量瓶颈。对于大多数用户这一层优化收益不大但当你写自己的扩展模块时理解哈希索引可以帮助你避免在 MATLAB 层做 O(n^2) 的变量查找。5.2 自定义求解器挂接到 YALMIP 的最小步骤要将一个自定义求解器接入 YALMIP需要在definesolvers.m的求解器列表末尾追加一个结构体然后在compileinterfacedata.m的求解器分派里增加一个分支再写一个call_mySolver.m函数。其中call_mySolver必须接收model结构并返回problem状态和x解向量。特别要注意sol.problem的 0 表示成功1 表示不可行2 表示无界3 表示求解器自身出错。在definesolvers.m中追加时至少需要设置solver struct(... tag, MYSOLVER, ... version, 1.0, ... solvertype, LP, ... check, () exist(mysolver_license,file));check字段是一个函数句柄YALMIP 通过它判断该求解器是否可用。这样设置后sdpsettings(solver,MYSOLVER)就能生效。修改完源码后记得在 MATLAB 中重新执行yalmip(clear)否则旧的definesolvers缓存不会刷新。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。