做微电网经济调度做得多了有一个感觉特别明显模型本身并不复杂难的是把每一类约束都建对并把成本算清楚。前阵子我整理了一个用 MATLAB YALMIP CPLEX 做微电网日前最优调度的例程目标就是一天 24 小时运行费用最小。这个例程把光伏、储能、与主网交互偶尔还会带柴油机一起纳入调度范围求解器采用 CPLEX代码结构清晰适合直接拿去改造成自己的项目。如果你正准备做微电网经济调度、综合能源系统优化或者只是想把 YALMIP 的建模套路彻底搞明白这篇内容就是照着你能复现、能理解、能扩展的方向写的。我会把模型怎么拆、约束怎么写、CPLEX 怎么配、结果怎么读以及对新手最不友好的那些坑全部摊开来说。1. 项目理解与求解思路拆解1.1 这个例程到底在解决什么问题微电网的“最优调度”本质上回答一个问题在已知明天负荷曲线和光伏出力曲线的前提下储能怎么充、怎么放从主网购多少电往主网卖多少电才能让一整天的运行成本最低。这里的核心关键词是“一天”。它属于典型的多时段优化问题时间尺度一般取 1 小时也就是 24 个时段。有些项目会细化到 15 分钟一个点那就是 96 个时段。时段越长决策变量越多模型规模越大但建模思路完全一致。例程采用 24 时段一是方便手工验算二是可视化时横轴直观。这个问题的难点不在“求最小值”这个动作而在于约束之间的耦合。储能电池的荷电状态是时序递推的前一小时怎么充放电直接影响后一小时还能不能出力。主网购电和售电不能同时发生充电和放电也不能同时发生这些逻辑关系都要用约束表达出来。真正把问题建明白求解器才可能给出一个经济上合理、物理上可行的调度方案。1.2 为什么选 YALMIP CPLEX 这套组合先说 YALMIP。它本身不是求解器而是 MATLAB 环境下的一个建模层。你可以把所有变量、目标、约束用接近数学公式的语法写出来最后调用任意后端求解器求解。这种“模型与求解器分离”的设计最大好处是调试方便——约束写错了可以直接看定义多了哪些变量一目了然。CPLEX 则是 IBM 旗下的商业求解器处理线性规划LP和混合整数线性规划MILP性能非常强。微电网调度一旦涉及储能充放电状态、购售电互斥逻辑一定会出现二进制变量模型就变成 MILP。CPLEX 在分支定界算法上的成熟度很高求解速度和稳定性都比 MATLAB 自带的 intlinprog 好尤其当模型规模扩展到几十个微网节点、几百个时段时差距会更明显。也有人用 Gurobi完全没问题。YALMIP 对 Gurobi 的支持同样很好。不过你这个项目既然指定了 CPLEX我们就按 CPLEX 的配置来讲包括路径设置、许可证验证、求解器参数调整这些细节都会覆盖。需要说明的是CPLEX 对学术用户是免费提供的下载安装时需要按 IBM 的申请流程走。1.3 整体模块怎么划分一个完整的调度例程我习惯拆成 4 个模块数据输入模块负荷曲线、光伏预测、分时电价、储能参数、微网与主网交互限值。模型构建模块定义决策变量、目标函数、各类约束。求解与结果提取模块调用 CPLEX提取各时段最优功率值、储能 SOC、总成本。结果可视化模块画负荷平衡图、储能曲线、购售电功率图、电价折线。严格来说前两个模块是核心后两个模块是辅助。但辅助模块不能省——调度结果不是一组数字就够了你需要通过图表快速判断“这个方案合理吗”没有可视化排查模型错误会非常痛苦。2. 模型核心构成与数学表达2.1 目标函数怎么拆“以一天的运行费用最小”这句话看起来很简洁但真正落到代码里目标函数的拆解能看出建模水平。一个标准的微电网日前调度目标函数我通常写成下面四项购电费用从主网购电按分段或实时电价结算是成本项。售电收益微网向主网倒送电按上网电价结算是收益项。燃料成本如果有柴油机按输出功率和油耗曲线计算。启停成本如果柴油机频繁启停要加二进制变量描述启停动作给一个惩罚系数。以 24 时段为例目标函数可以写成min sum(购电价(t) * P_buy(t)) - sum(上网价(t) * P_sell(t)) sum(P_dg_fuel(t))这看起来就是一个线性函数。储能本身不产生成本它的“成本”体现在充放电效率带来的能量损失以及 SOC 约束对调度空间的限制。电池寿命衰减这类成本如果项目要求也可以加只要在原目标函数上累加一个与循环次数相关的惩罚项即可。这里有个初学者容易忽略的点售电的收益项前面是负号。因为你是在求最小化收益越多目标函数值越小。写反了符号结果会变成“拼命买电再低价卖出”完全不符合物理直觉。2.2 可调度资源的建模方式光伏在我这个例程里采用“作为已知出力”的处理方式。也就是光伏预测曲线给定以后它就作为功率平衡等式左侧的正项参与计算不需要额外决策变量。这种方式对日前调度足够用。如果你想做光伏出力的不确定性那是另一套随机优化或鲁棒优化的框架不在本文范围内。储能是这个模型里最有意思的部分。它有两个连续变量充电功率、放电功率和两个二进制变量充电状态、放电状态。连续变量表示功率大小二进制变量负责互斥逻辑。此外还有一个 SOC 变量贯穿所有时段描述电池的能量状态变化。柴油机是可选的。加上它之后功率平衡等式里多了一个源项目标函数多了一个燃料成本项。如果柴油机还有最小开机时间、最小停机时间、爬坡限制那就是一大类约束属于典型的机组组合问题。单微网例程通常只加一个简单功率上下限再象征性加个爬坡约束就够了。可中断负荷在高级版本里会引入“负荷削减量”这个决策变量削减负荷可以降低购电需求但要在目标函数里加一个补偿成本。这个功能可以后面扩展入门例程先不处理。2.3 功率平衡与并网友好约束功率平衡是最核心的等式约束形式如下光伏出力(t) 储能放电(t) 购电(t) 柴油机出力(t) 基础负荷(t) 储能充电(t) 售电(t)这个等式在每个时段都要成立如果没有它模型会自己找一个“发电和用电对不上”的方案。等式右边的售电极容易漏掉——部分教材在建这个模型时默认微网不从主网买电的同时还卖电于是把购售电合并成一个“与主网交互功率”变量可正可负。这样做在学术上没问题但在实际项目中购电价和上网价往往不相等合并变量会让你无法区分购售电收益差异。我建议从一开始就拆成两个变量。并网友好约束还包括购电功率上限、售电功率上限。有实际工程经验的读者会注意一个细节调度结果里不应该出现“又买又卖”。虽然目标函数在价格正常情况下会自然规避这种浪费但在电价波动剧烈甚至出现负电价时模型可能会钻空子。因此还应该加入一组二进制变量强制购电和售电互斥。2.4 为什么必须要二进制变量这是很多刚从线性规划入门的朋友最容易困惑的地方。电池最大充电功率是 50 kW我直接写0 P_ch 50这不就是线性约束吗为什么还要u_ch这个二进制变量原因很简单充电和放电不能同时进行。如果没有二进制变量模型完全可能出现“一边充 50 kW、另一边放 50 kW”的方案功率平衡等式风雨不动但能量的循环损耗在白日损耗。要禁止这种行为最干净的写法是P_ch P_ch_max * u_ch P_dis P_dis_max * u_dis u_ch u_dis 1第三个约束是逻辑约束保证两个二进制变量不同时为 1。只要这个约束成立充电和放电功率就不可能同时为正。购售电互斥同理。虽然“低买高卖”逻辑在经济学上看很荒唐但模型不会自动拒绝你必须在约束里显式枚举出来。3. 环境准备与求解器配置细节3.1 MATLAB 版本选择与工具箱YALMIP 运行在 MATLAB 之上并不挑特定版本。我手里测试过的环境从 R2021b 到 R2024b 都没问题。理论上你只要不是特别老的版本都能跑通。所需要的工具箱只需要一个基础 MATLAB 环境严格说 YALMIP 依赖符号运算工具箱但实际上它的核心功能在大多数安装中都能正常工作。如果安装 MATLAB 时只装了默认组件也通常够用。这个例程不需要 MATLAB 的 Optimization Toolbox。虽然 YALMIP 可以用sdpsettings(solver,intlinprog)调用 MATLAB 自带的求解器但项目指定 CPLEX我们就只关心 CPLEX 那条链路。3.2 YALMIP 的安装YALMIP 的安装说实话没什么难度难点在于你下载到的版本是否与 MATLAB 版本兼容。我个人建议直接从 GitHub 仓库获取最新源码或者从 YALMIP 官网下载 zip 包。操作步骤下载 zip 包并解压。打开 MATLAB在“主页 - 设置路径 - 添加并包含子文件夹”中把解压目录整个添加进去。执行命令yalmip(clear)和which sdpvar如果返回了 sdpvar 的路径说明安装成功。需要注意的是网上有些第三方博客分享的 YALMIP 版本非常老旧可能在较新 MATLAB 上出现 undefined function 问题。遇到这种情况不要怀疑代码先把 YALMIP 升级到当前版本再说。3.3 CPLEX 的安装与激活检查CPLEX 安装同样有一个关键点安装完成之后YALMIP 需要知道去哪个目录找 CPLEX 的接口文件。在 Windows 上CPLEX 安装目录下通常有一个cplex\matlab文件夹里面是 MATLAB 接口函数。你需要把这一层路径添加到 MATLAB 路径中。我的操作步骤是从 IBM Academic Initiative 申请 CPLEX 学术版。安装完成后找到安装根目录例如C:\Program Files\IBM\ILOG\CPLEX_Studio2210。在 MATLAB 中添加路径C:\Program Files\IBM\ILOG\CPLEX_Studio2210\cplex\matlab。执行yalmiptest命令检查 CPLEX 是否被识别。这里有个常见的换坑点很多人把路径添加成了cplex\bin\x64_win64那是可执行文件的位置不是 MATLAB 接口。YALMIP 要通过 MATLAB 的 MEX 接口调用 CPLEX所以一定要指向cplex\matlab目录。3.4 联调验证先跑一个小规模样例在开始整模型之前我先建议你做一个 5 分钟级别的验证构造一个只有 3 个时段的极小模型用 YALMIP 定义变量调用 CPLEX 求解看看能否正常返回结果。比如x sdpvar(1,1); y binvar(1,1); Constraint [x 0, x 10*y, y 0.5]; Objective x 2*y; optimize(Constraint, Objective, sdpsettings(solver,cplex)); value(x) value(y)如果这里能顺利求出结果说明 YALMIP 到 CPLEX 的链路是通的。后面所有问题都可以聚焦在模型本身。如果这一步就报错那就先把环境问题解决再往下走不要带着坏环境调试模型。4. 核心代码实现与现场调试过程4.1 数据准备曲线怎么组织数据准备是最无聊但最影响结果的部分。负荷曲线和光伏曲线我习惯直接用 24x1 的列向量定义在脚本里示例数据取一个夏季典型日的量级。实际项目里这些数据一般来自预测系统或历史数据导出的 CSV你在例程中只要把load(data.mat)替换成自己的读表代码即可。时间参数设置如下T 24; % 调度时段数 dt 1; % 单位时段1 小时分时电价我也用 24 个元素的列向量。这里要注意电价向量必须与负荷、光伏在同一个时段对应关系上下标错一位整个调度结果就会完全变样。我曾经在项目里因为读 Excel 时少读了一行导致 0 点到 1 点的电价变成 23 点的值结果储能调度曲线偏移了一小时排查了很久才发现。储能参数我通常集中定义成一组变量E_max 200; % 电池容量 kWh E_min 20; % 最低SOC对应能量 E_init 100; % 初始能量 E_final 100; % 调度结束时的期望能量 eta_ch 0.95; % 充电效率 eta_dis 0.95; % 放电效率 P_ch_max 50; % 最大充电功率 kW P_dis_max 50; % 最大放电功率 kW并网友好参数P_buy_max 250; % 最大购电功率 P_sell_max 250; % 最大售电功率这里的效率参数很容易被忽略但影响巨大。充放电效率都是 0.95 时电池完整循环一次的能量损失超过 10%在电价峰谷差只有 0.3 元/kWh 的情况下储能套利空间会被效率损耗吃掉一大半。调度模型正是通过这种“看不见的损耗”决定最优策略所以效率参数一定要给准确。4.2 决策变量定义与索引技巧建模的第一步是声明变量。我建议把连续变量和二进制变量分成两组定义并写清楚注释。示例P_buy sdpvar(T, 1); P_sell sdpvar(T, 1); P_ch sdpvar(T, 1); P_dis sdpvar(T, 1); E sdpvar(T1, 1); % T1 个时刻的SOC u_buy binvar(T, 1); % 购电状态 u_sell binvar(T, 1); % 售电状态 u_ch binvar(T, 1); % 充电状态 u_dis binvar(T, 1); % 放电状态为什么要用T1维的E这是初学者常见的困惑点。储能 SOC 有递推关系E(t1) E(t) eta_ch*P_ch(t) - P_dis(t)/eta_dis。这里t从 1 到 24 时E(t)和E(t1)一共会涉及到第 1 到第 25 个时刻点。如果不声明E(25)最后一个时段的储能能量就无处安放。第 1 个时间点的 E 是你设定的初始状态第 25 个时间点是调度结束后的状态。这个T1的细节能看出一个人有没有写过完整的储能调度模型。我看过不少网上的例程为了省事把 E 定义成 T 维结果最后一段的约束只能改写成近似形式导致最终 SOC 与预期不符。4.3 约束循环与批量拼接YALMIP 支持在学校撸循环也支持矩阵化一次性定义约束。对于 24 时段的规模循环写法最易读也最不容易出错。示例Constraints []; % 初始与最终SOC约束 Constraints [Constraints, E(1) E_init]; Constraints [Constraints, E(T1) E_final]; for t 1:T % 储能SOC递推 Constraints [Constraints, E(t1) E(t) eta_ch*P_ch(t) - P_dis(t)/eta_dis]; Constraints [Constraints, E_min E(t1) E_max]; % 充放电功率与互斥 Constraints [Constraints, 0 P_ch(t) P_ch_max*u_ch(t)]; Constraints [Constraints, 0 P_dis(t) P_dis_max*u_dis(t)]; Constraints [Constraints, u_ch(t) u_dis(t) 1]; % 购售电与互斥 Constraints [Constraints, 0 P_buy(t) P_buy_max*u_buy(t)]; Constraints [Constraints, 0 P_sell(t) P_sell_max*u_sell(t)]; Constraints [Constraints, u_buy(t) u_sell(t) 1]; % 功率平衡 Constraints [Constraints, ... PV(t) P_dis(t) P_buy(t) Load(t) P_ch(t) P_sell(t)]; end这里有两个设计细节值得展开。第一个是功率平衡等式里P_dis(t)和P_ch(t)分别作为源项和负荷项出现。很多人一开始会把储能“净放电”定义成P_dis - P_ch从而把功率平衡写成PV P_buy P_dis - P_ch Load然后试图用一个变量代表净功率。这种做法必须注意如果你还想单独限制充放电功率上限就没办法用单一净功率表达因为同一时段充 20 kW 放 100 kW 的净放电 80 kW和单纯的放 80 kW 物理上不同、损耗也不同。所以我始终推荐充、放分离建模。第二点是uv uv ... 1这种互斥约束属于典型的“逻辑约束线性化”技巧。如果你直接在 YALMIP 里用implies函数表达逻辑关系也能做但对于单微网这种简单互斥条件直接线性不等式更高效也没有额外引入大 M 常数。4.4 目标函数与求解器参数设置目标函数在前文已经提到代码实现时注意价格向量的列方向要与决策变量保持一致。示例% 购电成本 - 售电收益 Objective sum(buy_price .* P_buy) - sum(sell_price .* P_sell); % 如果含柴油机 Objective Objective sum(fuel_cost .* P_dg);求解设置ops sdpsettings(solver, cplex, verbose, 2); ops.cplex.mip.tolerances.mipgap 1e-4; ops.cplex.timelimit 120; sol optimize(Constraints, Objective, ops);这里我给的mipgap 1e-4是工程上比较合理的精度要求。对于 24 时段单微网模型CPLEX 通常几秒就达到这个精度。如果你设得太严比如1e-9求解时间可能不必要地增加好几倍而调度结果的成本差异微乎其微。实际调度场景允许 0.01% 的偏差完全够用。timelimit 120是保险丝。如果模型因为某种原因陷入困境最多 2 分钟就退出方便观察输出日志判断问题所在。项目交付时我通常把这个时限设为 10 分钟再配合 MIP gap 双重保障。4.5 求解结果提取与可视化调用optimize之后首先检查sol.info是不是Successfully solved。如果这个值不是 0说明求解器没有给出最优解这时候直接读取变量的值只会得到无意义的数据。这一步很多人会跳过。提取结果if sol.problem 0 P_buy_opt value(P_buy); P_sell_opt value(P_sell); P_ch_opt value(P_ch); P_dis_opt value(P_dis); E_opt value(E); total_cost value(Objective); else % 输出错误信息 disp(sol.info) end可视化我建议画一个组合图第一幅画功率平衡负荷、光伏、储能净出力、购电功率的叠加柱状图或楼梯图第二幅画 SOC 曲线第三幅画分时电价与购售电功率。这样能快速看到储能在低价时段充电、高价时段放电购电曲线跟随电价走势变化。5. 常见问题排查与经验速查下面这些坑是我在实际项目里反复遇到过的按出现频率排列。5.1 Infeasible模型无解第一次跑完整模型时很少有直接成功的。最常见的原因是储能初始 SOC 和最终 SOC 约束太紧。比如你设定E_final 100但电池容量只有 200负荷又特别低导致没有足够空间在白天把能量消纳掉最后时刻达不到 100模型直接报不可行。排查流程我是这么做的先注释掉E(T1) E_final这个最终约束放宽模型看还能不能求解。如果问题还在检查功率平衡等式是否在极端时段无法满足。例如夜间光伏为零负荷又很高购电上限不够就会无解。用 CPLEX 的输出日志定位不可行约束。YALMIP 会把约束编号输出但更快的办法是直接在 MATLAB 里打印sol.info配合Constraints的结构逐块注释排查。一个实用技巧在功率平衡等式右侧加一个很小的松弛变量slack(t)取值范围[0, 1e-4]目标函数里给一个巨大的惩罚系数。这样即使模型略微不平衡也能找到可行解并通过看value(slack)判断到底是哪个时段出了问题。这个变量只是调试用机器发布前删掉即可。5.2 CPLEX 未找到或报路径错误YALMIP 里报No solver found或者 CPLEX 报找不到动态库90% 是路径配错了。排查要点在 MATLAB 中执行yalmiptest看输出表格里 CPLEX 这一行是 ok 还是失败。检查是否添加了cplex\matlab路径而不是cplex\bin。检查 CPLEX 版本与 MATLAB 位数是否一致。现代 MATLAB 基本都是 64 位老旧的 32 位 CPLEX 接口无法加载。重启 MATLAB 让路径和环境变量生效。有一个容易被忽略的问题Windows 系统下CPLEX 运行时需要 VC 运行库。如果安装不完整MATLAB 调用时会报Invalid MEX-file或者The specified module could not be found。解决办法是安装对应版本的 VC Redistributable或者重新执行 CPLEX 安装程序修复。5.3 求解速度慢二进制决策变量太多单微网 24 时段模型二进制变量最多也就 96 个购电状态、售电状态、充电状态、放电状态各 24 个CPLEX 毫无压力。真正慢的场景是扩展到 96 时段、多个微网、多台柴油机、储能还要考虑退化成本分段线性化这时候二进制变量会迅速飙升。几个有效加速手段设置 MIP gap 为1e-3差一点点最优性但时间大幅缩短。在目标函数里避免大系数不平衡。例如价格是 0.5 元/kWh电池充电功率是 50成本在 600 元左右目标函数量级是 10^3。如果某个惩罚项写成 1e6会造成数值问题求解器分支定界效率会显著下降。去掉无明显作用的冗余约束。我在一个项目里为了保证物理正确性加了一大堆上下限约束但实际上这些约束已经被其他约束隐含推导出来了。CPLEX 的预处理器能识别大部分冗余但少量冗余仍然会增加建模时间约束在极端情况下拖慢求解。5.4 结果出现同时充放电或同时购售电如果你的结果从数值上看同时买了电又卖了电或者电池一边充一边放原因基本只有一个互斥约束没加或者加法不当。检查模型里是否真的包含u_ch u_dis 1和u_buy u_sell 1。有一种特殊情况储能充放电效率不对称的时候模型可能通过“同时充放”来“制造能量”。例如放 1 kWh 需要充入 1/0.95 kWh如果充电效率是 0.95放电效率也是 0.95那么同时充放必然导致净损耗模型不会主动做。但如果你把效率参数配置错误比如充放效率都配成 1.05让能量越循环越多模型就会疯狂同时充放来无限套利。这个看起来是逻辑问题其实是参数问题。5.5 储能 SOC 曲线看起来很怪最典型的怪现象是SOC 曲线在一天中频繁触顶或触底。这可能不是模型错误而是容量约束本来就很紧。但如果 SOC 曲线完全没有按照电价峰谷规律走比如高价时段不出力就要回头查目标函数符号是否写反。还有一个经验把 SOC 曲线的初始和结束值设成同一个值会得到一个更直观的“日循环”结果。如果你设置E_final高于E_init相当于强制电池在一天内充满这可能会让它在夜间也强行充电和电价信号产生冲突。实际项目里如果第二天预测电价和负荷模式类似设成相同值更合理。6. 实战心得与扩展方向6.1 把确定性模型升级到鲁棒或随机优化这个例程目前是确定性模型光伏和负荷都是已知曲线。项目里如果光伏渗透率较高怎么处理不确定性必然会成为下一步问题。你可以在这个模型基础上把光伏出力拆成基准场景和误差区间引入鲁棒优化里的不确定集合或者用场景法做两阶段随机规划。YALMIP 对这两类模型都支持只是决策变量和约束的组织方式要改一改。我个人的建议顺序是先掌握确定性 MILP再扩展鲁棒优化。因为鲁棒优化本质上是在 YALMIP 里把约束改写成“最坏情况不等式”变量的数量会爆炸如果基础模型不熟debug 鲁棒模型会非常崩溃。6.2 把脚本改造成函数当我这个例程用于实际项目时我不会让数据、建模、求解全部塞在一个脚本中。一般会封装成如下形式function [result, status] microgrid_schedule(load_curve, pv_curve, paras) % 内部声明变量、构建约束、调用求解器 % 输出 result 是一个结构体包含各功率最优值这样做的好处很多。最直接的是你可以批量计算不同季节、不同参数的场景而不用每次手动改脚本数据。比如灵敏度分析中可以用循环把储能容量从 100 扫到 500跑出一组总成本趋势曲线这对项目方案论证特别有用。6.3 表达形式上的“工程感”调试这个模型的过程中我最深的一个感受是模型写出来很快但调到能交付的状态很慢。而决定调试效率的往往是建模时有没有把约束和变量组织得便于隔离。我的习惯是把约束分组后用注释把大块隔开。每个约束块单独检查时先用optimize跑一遍看可行性再逐个取消注释恢复。这样虽然多花了建模时间但排错时能精准定位是储能约束出问题还是平衡约束出问题。越是复杂的多能系统越要分段建模。所谓“工程感”很大程度上就是这种可控的组织方式。如果你打算长期在微电网调度这个方向上深耕建议你保留这个例程的“最小可用”版本后续每加一类新约束就先在这个版本上测试确认没有破坏原有逻辑再迁移到大模型里。否则模型规模上来后一个逻辑错误可能要花掉一整天去定位。这个例程不是我写过的最大调度模型但它足够完整地展示了从数据、建模、求解到可视化的一条主线价值链条。你只要把它的每一条约束吃透以后面对更复杂的微电网系统——多台 DG、多储能、多时段、多目标——都能踩在前面的经验上往前走。