简介:YALMIP-R20210331.zip 是MATLAB优化建模工具箱YALMIP在2021年3月31日发布的版本,凭借灵活的建模能力,支持线性、非线性、二次、整数与混合整数规划等常见优化问题,并为动态优化与半定规划提供统一建模接口;适合科研人员、工程师及高校学生用于快速构建和求解优化模型。压缩包共1390个文件,整体仅1.23MB:1379个.m文件组成YALMIP的主要源码,涵盖主入口、分支定界逻辑以及各类求解器适配代码;4个txt文档和3个mat数据文件提供说明与示例数据,2个.c文件承担底层哈希查找等辅助功能。目前已有268人浏览并下载学习该版本,是轻量但功能完整的工具箱资源。用户解压后可直接获得完整源码、文档、示例与安装说明,既能通过sdpvar、intvar等命令定义半定与整数变量、建立约束并调用optimize求解,也可深入阅读.m/.c代码理解求解器对接和算法流程,适合入门学习与二次开发。
1. YALMIP-R20210331.zip:你电脑里的优化问题翻译官
我第一次拿到 YALMIP-R20210331.zip 这个文件名时,第一反应是它又是一个 MATLAB 工具包压缩包,但真正用起来才发现,YALMIP 解决的痛点是“建模”而不是“求解”。R20210331 是 2021 年 3 月 31 日发布的版本,在 YALMIP 以日期命名的历史里属于相当稳定的一代:新老 MATLAB 都能带得动,开源求解器接口齐全,很多论文代码默认用到的也是它。你如果要在 MATLAB 里写半定规划、线性矩阵不等式、鲁棒优化,又不想每次都被底层求解器的输入格式折磨,这个 zip 就是给你准备的。它能做的,是把你在纸上写好的数学约束,原样翻译成求解器看得懂的数值模型。
2. 为什么需要 YALMIP,以及 R20210331 这个版本特殊在哪
2.1 从“手推标准形”到“声明式建模”:YALMIP 的价值在哪
直接面对底层求解器是什么体验,我可以用 sedumi 举个例子。sedumi 只接受下面这种标准形式:
min c'*x s.t. A*x = b x >= 0如果我要解一个带线性矩阵不等式的控制问题,比如A'*P + P*A <= -Q,其中 P 是对称矩阵,那就得先把 P 展开成一个向量,再把矩阵不等式逐项转换成线性等式和不等式,最后还要把解出来的向量重新拼回矩阵。矩阵是 2×2 的时候还能忍,到 5×5、10×10 的时候,手推很容易出错,而且每换一个求解器,这套展开就要重来一遍。
YALMIP 这个名字是 “Yet Another LMI Parser” 的缩写,早期就是专门给线性矩阵不等式做解析的,后来慢慢长成了一个通用的优化建模层。它做的事情说起来很简单:你声明变量、写约束、写目标,它替你把模型编译成底层求解器需要的标准形式。至于内部是怎么做松弛、怎么引入辅助变量、怎么把半定约束拍平成线性等式,这套逻辑对我来说其实是个黑匣子,我用它的第一原则就是:不关心编译细节,只关心数学表达和求解结果。
这种建模方式叫“声明式建模”,和写普通 MATLAB 脚本的思路不太一样。普通脚本里,x = 1是把 1 赋给变量;YALMIP 里,P = sdpvar(2,2,'symmetric')是声明一个 2×2 对称符号变量,之后所有对 P 的加减乘除、比较大小,都在搭一个表达式树,而不是在做数值计算。等到调用optimize的那一刻,YALMIP 才把这棵树拍平成数值模型,交给求解器。
那为什么不用 CVX?这也是我经常被问到的。CVX 同样能做声明式建模,但 YALMIP 在几个点上更顺手:一是对约束对象的管理更细,可以方便地提取对偶变量;二是求解器接口覆盖面更广,从开源 sedumi 到商业 gurobi、mosek 都能切;三是它不限制你只能用凸优化,很多非凸问题也能声明出来丢给 fmincon 这类非线性求解器去试。对于做控制、做调度、做鲁棒优化的工程师来说,这些能力是刚需。
2.2 R20210331 版本:为什么我建议用它来统一环境
YALMIP 没有 1.0、2.0 这种大版本号,它的版本就是发布日期。R20210331 就是 2021 年 3 月 31 日发布的那一版。团队协作时最常见的坑是:你电脑上装的是 2023 年的新版,同事电脑上是 2019 年的老版,同一个模型在两个环境里跑出不一样的结果,然后开始互相怀疑对方的代码改过。这种折腾我经历过太多次,所以现在给团队的建议很直接:统一用 R20210331 当基线,不要追新。
为什么选这个版本而不选最新的?我的判断标准是“被用户踩过的坑足够多”。R20210331 发布时,MATLAB R2018b 到 R2021a 都还在活跃使用,它对这些版本的兼容性经受了大量论文复现和工业代码的检验。我自己在 MATLAB R2019b 和 R2023a 上都跑过同一套 YALMIP-R20210331 代码,结论是核心的 sdpvar、optimize、sdpsettings 行为都很稳定,没有出现老版本常见的奇奇怪怪报错。
这里必须澄清一个很普遍的误解:YALMIP-R20210331.zip 里面没有求解器。它不是“解优化问题的工具箱”,而是“帮你描述优化问题的工具箱”。你用它写出模型,真正算数的是 sedumi、sdpt3、gurobi、cplex 这些底层求解器。安装步骤永远是先解压 YALMIP,再另外装一个求解器,两边都加进 MATLAB 路径,才能干活。这个顺序搞反或者漏掉一步,后面就是无穷无尽的 “No suitable solver” 弹窗。
还有一个版本选择上的现实问题:如果你的 MATLAB 是 2023b 以后,老代码会不会报错?我的经验是概率不大,YALMIP 的核心 API 很多年没大变过,真正出问题的地方往往是 MATLAB 自身的语法变化,比如 string 类型、冒号索引行为。相比之下,R20210331 这种“中年版本”反而比刚发布的新版更让人放心,因为它的兼容性问题基本都被社区问遍了,搜一下就有答案。
2.3 半定约束到底有多麻烦:一个 LMI 带来的直观感受
先看一段极简的 YALMIP 代码,感受一下声明式建模的样子:
A = [-1 0; 0 -2]; Q = eye(2); P = sdpvar(2,2,'symmetric'); cons = [A'*P + P*A <= -Q, P >= 0];这段代码在数学上描述的是:寻找一个对称正定矩阵 P,使得闭环系统A'*P + P*A负定,也就是李雅普诺夫不等式。在 YALMIP 里,你写的约束和教科书上的公式几乎一一对应。P >= 0在 YALMIP 里对矩阵变量默认解释为“半正定约束”,也就是 P 的所有特征值非负,而不是逐元素比较,这一点非常重要,后面避坑章节我还会专门展开。
如果不用 YALMIP,直接用 sedumi 做这件事,你得先定义 P 的三个独立元素 P11、P12、P22,把A'*P + P*A <= -Q展开成 2×2 的四个不等式,再把矩阵不等式转换成向量的形式,最后还要处理对称约束。这一套下来,光是写对等式就已经很考验耐心了。矩阵规模一大,手推基本等于给自己埋雷。
所以 YALMIP 的定位很清晰:它不抢求解器的饭碗,它当翻译官。把工程师从“手推标准形”的重复劳动里解放出来,让你把精力留在建模本身。R20210331 这个版本,就是把这份翻译工作做得最稳的一版。
3. 安装与验证:把 YALMIP-R20210331.zip 变成能用的工具箱
3.1 装之前先回答三个问题
安装一个 MATLAB 工具箱,最大的风险不是解压失败,而是装完以后 MATLAB 里同时存在几个版本,互相抢路径。所以我每次装 YALMIP,都会先花两分钟回答下面三个问题,省得后面半夜对着莫名其妙的报错发呆。
第一个问题:你的 MATLAB 版本是多少?R20210331 这个版本在 R2018b 及以上的环境里基本没有兼容性问题。如果你还在用 R2014a 这种老古董,那大概率需要找更早的 YALMIP 版本,但这属于特殊情况,常规部署我默认读者手里是近五六年内的 MATLAB。
第二个问题:机器上是否已经装过 YALMIP?在命令窗口执行一次which sdpvar和which optimize,如果返回结果显示在某个旧目录下,说明已经有老版本存在。这时候直接覆盖安装会让 MATLAB 的路径列表里出现同名文件,新版旧版同时在线,谁在前谁生效,完全是看 addpath 的顺序,这种问题排查起来非常难受,还不如先卸干净。
第三个问题:zip 解压到哪里?我的建议是放到一个纯英文的稳定目录,比如D:\tools\。不要放中文路径,MATLAB 对路径里的中文字符支持时好时坏,老版本尤其容易翻车;不要放桌面,更不要放网盘同步目录,YALMIP 文件数量多,网盘实时同步会让 MATLAB 启动扫描路径变慢,极端情况下还会读到写到一半的文件,那种错误根本不是代码的问题。
3.2 解压与加路径:两条命令让 YALMIP 进入工作区
把 zip 解压以后,进入 MATLAB,执行下面这段:
% 把整个 YALMIP 目录树加进搜索路径 addpath(genpath('D:\tools\YALMIP-R20210331')); % 保存路径,避免下次启动又要重加 savepath;之所以用genpath而不是直接addpath('D:\tools\YALMIP-R20210331'),是因为 YALMIP 的目录结构不是单层的。根目录下还有@sdpvar、@constraint、@optimizer这些类定义目录,以及modules、solvers等子模块。只加根目录,MATLAB 找不到类的构造函数,命令窗口会直接报“未定义函数或变量 sdpvar”。genpath会把所有子目录递归加进去,一次到位。
savepath的作用是把当前路径列表写入 pathdef.m,让下次启动 MATLAB 时自动生效。如果你的机器对 MATLAB 安装目录没有写权限,savepath会返回警告,这时不用慌,在 MATLAB 启动目录里建一个startup.m,把那行addpath(genpath(...))放进去,效果一样。
还有一类情况我必须提一下:如果是在已有旧版 YALMIP 的机器上升级,解压新版本之后、加路径之前,最好先执行清空命令:
yalmip('clear'); clear classes;yalmip('clear')清的是 YALMIP 内部缓存,clear classes清的是 MATLAB 内存中已经加载的类定义。如果跳过这一步,内存里残留的还是旧版 sdpvar 对象,新路径加进去也覆盖不掉,表现就是代码逻辑明明一样,结果却和新版对不上,非常像玄学。
3.3 验证安装:yalmiptest 还是冒烟测试
路径加好之后,别急着跑大模型,先做一个三秒钟的冒烟测试。YALMIP 自带一个完整的测试套件yalmiptest,但它会把 LP、QP、SDP、SOCP 全都跑一遍,耗时比较长,而且如果没有可用求解器,它会在中途报很多“solver not found”的提示,反而把新手吓到。我一般先跑这个小实验:
% 声明一个 2x2 对称变量 P = sdpvar(2,2,'symmetric'); % 约束:P 半正定且 P >= I cons = [P >= eye(2)]; % 目标:最大化 trace(P),最优解显然就是 P = I diagnostics = optimize(cons, -trace(P)); if diagnostics.problem == 0 disp(value(P)); else disp(['求解失败,错误码:', num2str(diagnostics.problem)]); end这段代码的逻辑很直白:如果 YALMIP 和求解器都装好了,那么P >= eye(2)再加上最小化-trace(P),最优解必然是单位阵。输出里 P 应该是[1 0; 0 1],可能有非常小的数值误差,比如1.0000001,这是正常的,不用纠结。如果返回的错误码是 7,表示 YALMIP 找不到可用的求解器,那就回去装 sedumi 或 sdpt3,解压后同样用addpath(genpath(...))加进路径,再重新跑这个测试。
到这一步,YALMIP-R20210331 才算是真的能干活了。之后有时间再跑yalmiptest做全面体检,它会列出哪些求解器可用、哪些问题类型测试通过,这份报告对排查“为什么我的模型不能解”非常有参考价值。
4. 用 YALMIP 建模的第一个完整例子:从 sdpvar 到 optimize
4.1 sdpvar 不是 MATLAB 变量:理解声明与表达式树
新手最容易犯的一个错,是把 sdpvar 变量当成普通 MATLAB 变量去理解。P = sdpvar(2,2,'symmetric')之后,你在工作区看到的是一个对象,它的值不是矩阵,而是一个“符号变量声明”。你写P + eye(2),MATLAB 不会做加法,它是在构建一棵表达式树,记录“有一个叫 P 的符号变量,和一个单位阵相加”这件事。再写P >= eye(2),它也不是在比较大小,而是在创建一个约束对象。
这个区别在调试时特别关键。你如果在声明约束后立刻执行disp(P),看到的不是数值矩阵,而是类似 “Matrix2x2 symmetric” 的摘要信息。想提前看某个中间表达式的数值,必须用value(),但前提是变量已经被赋过值,比如执行过assign(P, eye(2)),或者在optimize求解成功之后。
所以建模阶段我一般不急着看变量的值,先看约束有没有写对,模型编译能不能通过,求解器有没有给出可行解。把 YALMIP 当作一门小语言来用,而不是当作数值计算函数来用,这个心理建设越早完成,后面的痛苦越少。
4.2 完整示例:一个带 Lyapunov 不等式的 SDP 从头跑到尾
下面是一个可以直接复制运行的完整例子,用的是控制里最典型的李雅普诺夫不等式验证问题:
% 系统矩阵:一个稳定的二阶系统 A = [-1 0; 0 -2]; Q = eye(2); % 声明对称矩阵变量 P P = sdpvar(2,2,'symmetric'); % 约束:A'*P + P*A 负定,且 P 正定 cons = [A'*P + P*A <= -Q, P >= 0]; % 目标:最小化 P 的迹,相当于找一个尽量“小”的可行解 obj = trace(P); % 指定求解器并求解 options = sdpsettings('solver', 'sedumi', 'verbose', 1); diagnostics = optimize(cons, obj, options); if diagnostics.problem == 0 P_val = value(P); disp('P 的最优解:'); disp(P_val); % 顺手验证约束残差 [res, feasible] = check(cons); disp(['最大约束残差:', num2str(max(res))]); disp(['是否严格可行:', num2str(feasible)]); else disp(['求解失败,错误码:', num2str(diagnostics.problem)]); end这段代码里,sdpvar(2,2,'symmetric')声明的不是 4 个独立元素,而是 3 个独立变量,分别对应主对角线两个元素和上三角元素,YALMIP 会自动把对称性约束处理好。A'*P + P*A <= -Q是一个矩阵不等式,对于 2×2 的矩阵,它约束的是整个矩阵负定,不是四个元素分别小于 Q。P >= 0同理,是半正定约束。
solve之后,value(P)拿到数值解,check(cons)返回每个约束的残差和可行性标志。如果一切正常,残差应该在1e-7量级以下。这里我有意把verbose设成 1,让求解器的迭代日志打印出来,第一次跑的人可以直观看到 sedumi 内部迭代过程,对理解“求解器到底在算什么”有帮助。如果觉得日志太吵,改回 0 即可。
4.3 sdpsettings 里最常用的四个参数
sdpsettings是 YALMIP 的参数入口,它返回一个结构体,里面嵌套着各求解器的专属字段。我平时用到最多的就四个,列成表方便对照:
| 参数名 | 作用 | 典型用法 |
|---|---|---|
solver | 指定底层求解器 | sdpsettings('solver','sedumi') |
verbose | 控制日志详细程度 | 0 静默,1 标准,2 详细 |
debug | 诊断不可行问题 | sdpsettings('debug',1) |
savesolveroutput | 保存求解器原始输出 | sdpsettings('savesolveroutput',1) |
solver参数是最常被问到的。YALMIP 默认会根据问题类型自动选求解器,但自动选择不一定最优。比如一个纯线性规划,gurobi 比 sedumi 快很多倍;一个半定规划,gurobi 又处理不了,得用 mosek 或 sdpt3。所以我的习惯是:小问题用默认,大问题和生产环境一定显式指定 solver,避免同一套代码在不同机器上因为可用求解器不同而跑出不同结果。
debug参数专门用来诊断“为什么不可行”。当optimize返回problem=1时,把debug打开,YALMIP 会尝试找出哪些约束最可能导致冲突,并输出更多提示信息,比对着模型瞎猜高效得多。代价是 debug 模式会显著变慢,所以只在认为模型本身可能有矛盾时才开。
还有一个参数经常被忽略但很重要:超时设置。YALMIP 不提供统一的超时字段,不同求解器的参数名不一样,比如 gurobi 是gurobi.TimeLimit,cplex 是cplex.TiLim。如果模型跑起来没完没了,先去查你用的那个求解器对应的超时字段,别在 YALMIP 层翻来翻去找不到。
4.4 value、assign、check:调试模型的三板斧
value、assign、check这三个函数是 YALMIP 调试的黄金组合。value从符号变量里提取数值,assign手动给变量赋一个试探值,check验证约束在当前赋值下是否满足。三个配合起来,可以在不求解的情况下快速判断模型本身是否自洽。
% 先给 P 赋一个已知可行解 assign(P, eye(2)); % 检查所有约束在这个赋值下的残差 [res, feasible] = check(cons); % 查看残差向量 disp(res); disp(feasible);这个操作的实用场景很常见:求解器返回不可行,但你觉得模型应该没问题。与其猜,不如亲手喂一个可行解进去,看看约束残差有多大。如果残差小到1e-6以下,说明约束本身写对了,问题出在求解器数值收敛上;如果残差非常大,比如某个约束残差是1e3,那大概率是约束方向写反了,比如<写成了>,或者不等式两边符号搞错。
check返回的第一个输出res是每条约束的残差向量,第二个输出feasible是总体可行性标志。残差的正负还有意义:正数表示约束离满足还有多远,负数表示约束被严格满足。我每次调试模型,第一件事就是看res的符号,它能告诉我到底是哪一条约束在捣乱。
5. 避坑指南:YALMIP-R20210331 最常见的 5 个翻车现场
5.1 P>=0 到底是半定还是逐元素?先弄清约束类型
现象:一个理论上肯定有解的模型,求解器返回不可行,或者解出来的矩阵明显不合理。
原因:YALMIP 对“矩阵变量”的>=默认解释为半正定约束,也就是要求矩阵所有特征值非负;但如果你把矩阵变成了向量,比如写了P(:) >= 0,或者只取了diag(P),约束类型就变成了逐元素约束。逐元素非负和半正定完全是两个概念,前者只约束对角线元素,后者约束的是整个矩阵的谱性质。很多人的代码看起来差不多,结果天差地别,就是这个细节。
解决:用is函数确认约束类型。
% 判断约束到底是半定还是逐元素 is(cons(1), 'sdp'); % 半定约束返回 1 is(cons(1), 'elementwise'); % 逐元素约束返回 1另外还要记住一个反向规则:如果变量是向量,>=永远是逐元素比较,不存在“向量半定”这种说法。要做半定约束,变量必须是方阵。
5.2 明明有解,optimize 却返回 Infeasible
现象:模型在数学上一定有可行解,你自己都能手算出一个解来,但optimize返回problem=1,控制台提示不可行。
原因:数值缩放问题。YALMIP 底层求解器用的都是浮点数,约束中同时出现1e8和1e-8这样量级悬殊的数时,求解器内部的容差很难同时满足两条约束,于是判定不可行。另一种常见情况是约束本身写成了非凸形式,比如两个变量相乘,YALMIP 不做凸性检查,直接把模型丢给非线性求解器,能不能解出来全看运气,这已经属于玄学范畴。
解决:先做缩放,把变量单位统一到相近数量级;或者换一个数值鲁棒性更好的求解器,比如把 sedumi 换成 sdpt3 或 mosek。还可以先开启 debug 模式看冲突点:
diagnostics = optimize(cons, obj, sdpsettings('debug', 1));如果 debug 输出没有明确指出哪条约束冲突,那就用第 4 章的做法,手动assign一组可行解,跑check,看是哪条约束的残差拦住了求解器。
5.3 模型里出现 NaN 或 Inf,先查变量初始化
现象:求解器日志里出现 “NaN in constraints”,或者模型编译阶段直接报错,提示表达式里有非数值量。
原因:大部分时候是变量没有正确赋值。比如在目标函数里写了log(P),但 P 在迭代过程中出现了非正特征值,log算出来直接是 NaN;或者约束用了未初始化的表达式,MATLAB 把它当成普通变量参与运算,结果自然是坏的。还有一种隐蔽情况:脚本文件里一个同名普通变量覆盖了 sdpvar 对象,名字一样、类型不同,后面的表达式全乱套。
解决:把非线性函数先注释掉,跑一个纯线性版本,确认模型框架没问题;再用value(x)查看每个中间变量的数值范围,定位 NaN 是从哪一步冒出来的。脚本开头加一行clear all; yalmip('clear')可以避免同名污染,但要记住,这行不能出现在函数文件里,否则会把整个函数工作区清空。
5.4 No suitable solver:YALMIP 不负责求解
现象:optimize返回problem=7,提示没有合适的求解器。
原因:最常见的是根本没装求解器。YALMIP-R20210331.zip 里只有建模层,不带任何求解器,这是新手最容易踩的坑,装完 YALMIP 就以为万事大吉,一跑才发现查无此求解器。第二类原因是装了求解器,但类型不匹配,比如模型里有半定约束,指定的却是只能处理 LP 和 QP 的 gurobi,YALMIP 会在编译阶段直接拒绝。
解决:先跑一次yalmiptest,它会列出当前环境里所有可用求解器以及各自支持的问题类型。开源环境我一般装 sedumi 和 sdpt3,一个轻量一个重型,覆盖大部分凸优化场景。商业环境考虑 mosek,它在半定规划上的性能明显比开源求解器高一个档次。指定求解器的写法在第 4 章已经给过,就不再重复。
5.5 同一套代码在不同机器上结果不一样
现象:代码完全一致,A 机器上problem=0,B 机器上problem=4(数值问题),或者两边都成功但最优解有肉眼可见的差异。
原因:这个坑和 YALMIP 本身关系不大,更多来自环境差异。MATLAB 版本不同,底层线性代数库 BLAS 的实现不同,某些内置函数的行为也变过,比如旧 MATLAB 里字符串数组和字符数组的区别在 R2021a 之后明显收紧。YALMIP-R20210331 这种老版本碰上太新的 MATLAB,偶尔会触发兼容性问题。
解决:显式指定 solver,不依赖默认;显式设置容差,比如sdpsettings('sedumi.tol', 1e-9);代码里尽量用 char 数组而不是 string 类型;在脚本开头打印yalmip('version'),日志里留下版本痕迹,排查时能少吵很多架。数值优化本来就会受浮点环境影响,结果是 1.0000001 还是 0.9999999 是正常的,不用纠结;但如果解的结构都变了,那就是版本或求解器差异,值得认真查。
6. 进阶:从能用 YALMIP 到用好 YALMIP
6.1 diagnostics.problem 不等于 0 时怎么看
optimize返回的diagnostics结构体里,problem字段是核心诊断信息。0 表示求解成功;1 表示不可行;2 表示无界;4 表示数值问题;7 表示没有可用求解器。每次模型异常,我第一件事就是打印这个错误码,再决定往哪个方向排查,而不是盯着求解器日志逐行看,那样效率太低。
if diagnostics.problem ~= 0 disp(['求解失败,错误码:', num2str(diagnostics.problem), ',提示:', diagnostics.info]); enddiagnostics.info是 YALMIP 给的文字说明,虽然有时比较笼统,但至少能指个方向。配合前面讲的assign和check,大部分模型问题都能在几分钟内定位。平时写代码,我也会习惯性地在每个optimize调用后面加这个判断,防止某个循环里模型变了导致静默失败,最后拿到一批没意义的数值。
6.2 把模型编译成 optimizer 对象,循环里不再重复建模
另一个我比较推荐的进阶技巧是把模型编译成optimizer对象。普通模式下,每次调用optimize都要重新编译一次模型;但在仿真里,往往只是参数在变,模型结构不变,重复编译纯粹是浪费。
% 声明参数变量 A A = sdpvar(2, 2, 'full'); % 变量和约束 P = sdpvar(2, 2, 'symmetric'); cons = [A'*P + P*A <= -eye(2), P >= 0]; % 编译成优化器对象:输入是 A,输出是 P opt = optimizer(cons, trace(P), sdpsettings('solver', 'sedumi'), A, P); % 在循环里直接调用,不用重新建模 A_test = [-1 0; 0 -2]; [P_sol, problem] = opt{A_test};optimizer把“建模”和“求解”拆开,建模只做一次,后续每次调用只改变参数重新求解。这个模式在蒙特卡洛仿真、模型预测控制里特别实用,能把总运行时间缩短一大截。需要注意opt{A_test}返回的结果是 cell 数组,P_sol{1}才是 P 的数值矩阵。
我自己现在的习惯是,拿到一个新环境或者一个新版本的 YALMIP,第一件事不是直接跑业务代码,而是先跑一个 3 秒冒烟测试,再跑一个自己领域内有代表性的完整模型,确认建模和求解都没问题后才开始正式工作。YALMIP-R20210331 我用了很久,它不完美,但作为团队基线版本,它是让我最省心的一个选择,希望帮到你。
本文还有配套的精品资源,点击获取