昨天一个师弟找我调微电网调度代码他运气不错电话里我一眼就看出了问题储能的SOC递推公式方向搞反了结果优化出来的策略是“越到晚上越充电白天全在放电”完全和分时电价对着干。这种事我见了不止一次。微电网最优调度说白了就是给出一组第二天的负荷预测、光伏风电出力预测、设备参数和分时电价用数学优化算出一份从零点到二十四点的设备出力计划柴油机什么时候开、发多少储能什么时候充电什么时候放电电不够了从主网购多少光伏发多了要不要卖给电网让一整天的总运行费用最低。这套东西在电力系统里叫日前经济调度是微电网能量管理系统EMS的核心模块。主流的做法就是MATLAB配YALMIP建模再用CPLEX求解器去解。这篇文章我把完整思路、建模步骤、代码骨架和调试中经常踩的坑一次性讲清楚适合刚接触微电网优化调度的研究生和工程师。1. 调度问题到底在优化什么1.1 目标函数一天的钱怎么算既然目标是“一天运行费用最小”第一步就是把费用拆明白。拿一个典型微电网来说日运行费用通常由四块构成。第一块是柴油发电机或者微型燃气轮机的燃料费用。燃料费用一般和出力有关工程上常用二次函数近似F_dg(t) aP_dg(t)^2 bP_dg(t) c*u_dg(t)其中u_dg(t)是开机状态取0或1c可以理解为空载油耗。注意如果直接把这个二次函数丢给CPLEX问题就从MILP变成了MIQP混合整数二次规划不是不能解但明显会更慢。实际工程中更顺手的方法是分段线性化或者像我后面例程里那样直接简化为线性燃料费用 斜率×出力 固定空载费用。单台小机组误差在可接受范围求解速度却快很多。第二块是向大电网购电的费用。某个时段如果微电网自己发的电不够就要从主网购电费用是购电量乘以该时段购电价。第三块是售电收益光伏出力大、负荷又小的时候多余电能卖给主网收益可以抵扣成本。第四块是储能损耗折旧虽然单个时段看着很小但乘上365天就值得考虑。日运行总费用写成一个离散和每个时段算一次费用再加起来。决策变量是各时段出力、储能充放电功率、购售电功率。优化器要做的就是在所有可行的出力组合里找到让总费用最小的那一个。1.2 微电网这个“家”都有哪些成员先盘点微电网里的常见设备建模的时候它们各有脾气。光伏和风电是跟随天气走的出力有预测值但调度时允许“弃光”和“弃风”也就是说实际投入使用的出力可以小于预测值。这给了优化器一定的灵活性比如中午光伏大发、电价又低、储能也满了宁可弃掉一部分也划算。柴油发电机是最灵活的主动电源想开就开、想停就停但有最小技术出力限制比如一台50kW的机组你不能让它只发10kW。它还有爬坡约束相邻两个时段之间出力变化不能太猛这是机械本身的物理限制。你可以类比成开车油门不能从零直接踩到底。储能是全场的调节能手但建模最麻烦。它的电量状态SOC是一个递推过程这一小时结束时的电量等于上一个小时结束的电量加上充电量、减去放电量还要折算充放电效率。充放电不能同时进行这个约束必须显式写出来否则优化器会钻空子同一小时又充又放白白制造损耗结果还自以为很聪明。和主电网的交互也要建模。联络线功率有上限而且购电和售电不能同在一个时段发生这部分常用0-1变量来控制。这就是一个典型微电网调度模型的全部要素。剩下的事情就是把这些要素用数学公式写清楚再交给求解器。2. 为什么是MATLABYALMIPCPLEX这个组合2.1 YALMIP是干什么的刚接触这套技术栈的人最容易问的问题是我直接写CPLEX的MATLAB接口不好吗为什么中间还夹着一个YALMIPCPLEX确实提供MATLAB API但它偏底层。如果要定义一大批变量、约束、目标函数代码会很冗长。YALMIP是一个建模工具箱来自瑞典学者Lofberg的开源项目。它的核心价值在于让你用接近数学公式的方式写优化模型sdpvar定义连续变量binvar定义0-1变量约束直接写等式或不等式最后调用optimize求解。YALMIP会自动把模型翻译成求解器需要的标准形式调用底层求解器计算。用YALMIP写微电网调度最大的好处是代码和论文里的公式几乎一一对应调试的时候能对照着看。另外你随时可以换求解器同一个模型把solver从cplex改成gurobi或cbc几乎不用动其他代码。这意味着你不被某个商业求解器绑架。2.2 CPLEX处理这类问题的优势微电网调度问题在数学上是一个混合整数线性规划MILP。为什么会有“整数”因为储能充放电状态的0-1、机组启停的0-1、网购电售电状态的0-1都是整数变量。MILP的求解难度比单纯线性规划LP高得多需要分支定界、割平面这些算法。CPLEX是IBM的商业求解器在MILP领域打磨了几十年分支定界策略、启发式算法、预求解过程都很成熟。同样一个大规模调度问题CPLEX通常比开源求解器CBC快很多尤其当整数变量上百个的时候。学术版还可以免费申请用学校邮箱下载IBM ILOG CPLEX Optimization Studio对学生和科研人员非常友好。纯用MATLAB自带linprog只支持连续变量遇到储能互斥、机组启停这类0-1变量就无能为力了。所以这套组合的好处很清晰YALMIP负责让建模“像人话”CPLEX负责把“人话”变成高效计算MATLAB负责写脚本、做数据处理、画结果图。2.3 安装配置里最容易出问题的几个点如果你是新机器按这个顺序来基本不会出错。先装MATLAB版本不要太老2020b以上比较省心。再装CPLEX安装路径里不要有中文。重点来了装完CPLEX之后要在MATLAB里把CPLEX的MATLAB接口目录加到路径里。CPLEX安装目录下通常会有一个cplex/matlab目录在MATLAB里运行addpath(D:\Program Files\IBM\ILOG\CPLEX_Studio221\cplex\matlab); savepath;不同版本路径不一样但目标明确就是要让MATLAB能找到cplexlink这个接口文件。然后是YALMIP从GitHub下载或者官网下载解压后把整个文件夹加到MATLAB路径。配置完怎么验证运行YALMIP自带检测命令yalmiptest输出里能看到CPLEX是否被识别状态是“ok”那就稳了。如果yalmiptest里CPLEX那一行显示Not found先检查路径再检查MATLAB是不是以管理员权限运行的。有一条实际经验如果你同时装了多个版本的CPLEXMATLAB会加载路径里的那一个。这时候在sdpsettings里显式指定solver为cplex就没问题但如果路径里有旧版接口YALMIP可能报版本不匹配常见错误是“Unrecognized function or variable cplexlink”。解决办法是清理路径只保留一个CPLEX接口目录。3. 从数据到数学模型的完整建模流程3.1 第一步准备一套站得住脚的数据调度模型不是凭空算的输入数据必须齐全。一个典型的日前调度需要以下几类数据。负荷预测是24个小时的序列单位kW。光伏和风电出力预测同样按小时给。这些数据来源可以是实际电网运行记录也可以根据典型日曲线生成。如果你手头没有实际数据一个合理的办法是参考论文里用的典型日曲线或者按规律自己造负荷在早晚出现两个高峰光伏在中午最高、夜间为0风电夜里略大。分时电价也很关键。很多地方实行峰谷分时电价购买电价在不同时段不一样。比如工业用户尖峰时段1.2元/kWh平时段0.8元/kWh谷时段0.4元/kWh。售电价通常按购电价的八折到九折算因为电网要赚差价。设备参数方面柴油机有最大最小出力、爬坡速率、燃料成本系数储能系统有容量、最大充放电功率、初始SOC、SOC上下限、充放电效率联络线有最大交换功率。这些参数看起来琐碎但任何一个填错优化结果都会走样。我自己吃过一次亏是单位搞混功率用了kW能量用了kWh时间步长又是1小时看起来数值一样实际上这里没有单位陷阱真正危险的是时间步长改成15分钟之后能量单位要乘以0.25忘了换算的话SOC递推直接爆掉。有个小技巧调试阶段所有数据都用简单的整数一眼能看出结果是否符合直觉。比如负荷恒定100kW光伏恒定0柴油机最小出力50kW储能容量200kWh电价峰谷分明最优策略应当是谷段充电、峰段放电柴油机在电价高峰顶上去。如果算出来不是这个规律说明模型里肯定有错。3.2 第二步选择决策变量决策变量是优化器可以自由调整的量。在微电网调度里典型的决策变量包括柴油机每小时出力P_dg(t)以及机组状态u_dg(t)储能每小时充电功率P_ch(t)、放电功率P_dis(t)以及互斥状态b_ch(t)、b_dis(t)储能SOC能量E_SOC(t)每小时从电网购电P_buy(t)、售电P_sell(t)及交互状态u_net(t)光伏实际使用出力P_pv_use(t)、风电实际P_wt_use(t)在YALMIP里连续变量用sdpvar0-1变量用binvar。为了方便直接定义成行向量时间维度在后面用循环展开。关于SOC为什么要定义成T1个点而不是T个因为递推公式里要从E_SOC(1)开始每一步算出E_SOC(t1)最后需要T1个状态点其中E_SOC(1)是已知初始值剩下的才是变量。这个细节非常容易疏忽。3.3 第三步把约束写成数学式子约束条件按照功能分四组。功率平衡约束是全场硬约束每个时段都必须满足P_pv_use(t) P_wt_use(t) P_dg(t) P_dis(t) P_buy(t) P_load(t) P_ch(t) P_sell(t)等式左边是电源侧加流入方向右边是负荷加充电和售电。为什么必须是等号因为微电网和主网连接处的功率失配会导致频率波动调度计划必须保证实时供需平衡。如果模型里写不等号优化器很可能会利用这个“宽松度”少出力结果不可用。柴油机约束出力要在最小技术出力和最大出力之间只有开机状态才能出力P_dg_minu_dg(t) P_dg(t) P_dg_maxu_dg(t)相邻时段还有爬坡约束-Ramp_down P_dg(t1) - P_dg(t) Ramp_up储能约束是重头戏。充放电功率各有上下限并且受互斥状态控制0 P_ch(t) P_ch_max * b_ch(t) 0 P_dis(t) P_dis_max * b_dis(t) b_ch(t) b_dis(t) 1SOC递推是等号约束E_SOC(t1) E_SOC(t) P_ch(t)*eta_ch - P_dis(t)/eta_dis注意充电效率乘以充电量、放电效率除以放电量两者不对称这是不能省的细节。SOC整体还要在上下限之间SOC_min E_SOC(t) SOC_max和主网的交互约束购电和售电不能同时发生且分别受联络线功率上限约束0 P_buy(t) P_net_max * u_net(t) 0 P_sell(t) P_net_max * (1 - u_net(t))这组约束用u_net(t)1表示购电u_net(t)0表示售电两个条件互相排斥非常经典。3.4 第四步明确求解目标并调用CPLEX目标函数和约束都定义好之后在YALMIP里用optimize(Constraints, Objective, ops)一行调用指定solver为cplex。ops sdpsettings(solver, cplex, verbose, 2); ops.cplex.mip.tolerances.mipgap 0.01; ops.cplex.mip.limits.timelimit 120; sol optimize(Constraints, Objective, ops);mipgap表示允许的整数最优间隙1%对工程调度完全够用设成0也不是不行只是会明显增加求解时间。120秒是时间上限防止凌晨预测数据变化导致后台优化卡死。verbose2让你能实时看到分支定界进度排查问题很有用。求解结束后还要检查状态if sol.problem 0 disp(求解成功); P_dg_opt value(P_dg); else disp(sol.info); endsol.problem 0代表求解成功其他返回值对应不同错误后面排查章节会详细讲。4. 一个能直接跑的完整例程4.1 主程序骨架下面给出一个最小可用例程。模型规模不大24个时段1台柴油机1套储能1条联络线。我故意把数据写得简单方便你代入感性地理解。%% 微电网日前经济调度 完整例程 % 使用 YALMIP CPLEX % 时间分辨率1小时调度周期24小时 clear; clc; close all; %% 1. 数据定义 T 24; dt 1; % 单位小时 % 负荷预测 (kW) P_load [120 115 110 105 100 95 98 110 135 150 165 175 180 170 160 150 ... 155 160 170 165 155 145 135 125]; % 光伏预测 (kW) 白天有出力 P_pv [0 0 0 0 0 0 10 45 90 130 160 175 170 150 120 80 40 5 0 0 0 0 0 0]; % 风电预测 (kW) 夜间略大 P_wt [30 32 35 38 36 34 32 30 28 26 25 24 26 28 30 32 35 33 30 28 26 25 24 23]; % 分时电价 (元/kWh) price_buy [0.4*ones(1,8), 0.8*ones(1,2), 1.2*ones(1,3), 0.8*ones(1,7), ... 1.2*ones(1,2), 0.8*ones(1,2)]; % 谷-平-尖-平-尖-平 price_sell 0.85 * price_buy; %% 2. 设备参数 P_dg_max 200; P_dg_min 50; % 柴油机出力上下限 kW Ramp_dg 60; % 爬坡限制 kW/h a_fuel 0.55; c_fuel 40; % 燃料成本系数a*P c*u B_rated 400; % 储能额定容量 kWh S0 0.2 * B_rated; % 初始SOC能量 kWh S_min 0.1 * B_rated; % SOC下限 S_max 0.9 * B_rated; % SOC上限 P_b_max 100; % 储能最大充放电功率 kW eta_ch 0.95; eta_dis 0.95; % 充放电效率 P_net_max 500; % 联络线最大交换功率 kW %% 3. 决策变量 P_dg sdpvar(1, T); u_dg binvar(1, T); % 柴油机启停 P_buy sdpvar(1, T); P_sell sdpvar(1, T); u_net binvar(1, T); % 1:购电 0:售电 P_ch sdpvar(1, T); P_dis sdpvar(1, T); b_ch binvar(1, T); b_dis binvar(1, T); E_SOC sdpvar(1, T1); % 注意T1 P_pv_use sdpvar(1, T); P_wt_use sdpvar(1, T); %% 4. 约束条件 Constraints []; % 功率平衡 Constraints [Constraints, P_pv_use P_wt_use P_dg P_dis P_buy ... P_load P_ch P_sell]; % 光伏和风电可用量约束 Constraints [Constraints, 0 P_pv_use P_pv]; Constraints [Constraints, 0 P_wt_use P_wt]; % 柴油机 Constraints [Constraints, P_dg_min*u_dg P_dg P_dg_max*u_dg]; for t 1:T-1 Constraints [Constraints, -Ramp_dg P_dg(t1) - P_dg(t) Ramp_dg]; end % 储能充放电功率及互斥 Constraints [Constraints, 0 P_ch P_b_max*b_ch]; Constraints [Constraints, 0 P_dis P_b_max*b_dis]; Constraints [Constraints, b_ch b_dis 1]; % SOC递推 Constraints [Constraints, E_SOC(1) S0]; for t 1:T Constraints [Constraints, E_SOC(t1) E_SOC(t) P_ch(t)*eta_ch - P_dis(t)/eta_dis]; end Constraints [Constraints, S_min E_SOC S_max]; % 与主网交互 Constraints [Constraints, 0 P_buy P_net_max*u_net]; Constraints [Constraints, 0 P_sell P_net_max*(1-u_net)]; %% 5. 目标函数 Cost_fuel sum(a_fuel*P_dg c_fuel*u_dg); Cost_buy sum(price_buy .* P_buy); Income_sell sum(price_sell .* P_sell); Cost_battery 0.01 * sum(P_ch P_dis); % 电池损耗折算 Objective Cost_fuel Cost_buy - Income_sell Cost_battery; %% 6. 求解 ops sdpsettings(solver, cplex, verbose, 2); ops.cplex.mip.tolerances.mipgap 0.01; ops.cplex.mip.limits.timelimit 120; sol optimize(Constraints, Objective, ops); if sol.problem 0 P_dg_opt value(P_dg); 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_SOC_opt value(E_SOC); P_pv_use_opt value(P_pv_use); P_wt_use_opt value(P_wt_use); disp([最小日运行费用: , num2str(value(Objective))]); else disp(sol.info); end %% 7. 结果可视化 figure; hold on; t 1:T; bar(t, P_load, k, FaceAlpha, 0.2); bar(t, P_pv_use_opt, y); bar(t, P_wt_use_opt, c); bar(t, P_dg_opt, r); bar(t, P_dis_opt, g); bar(t, P_buy_opt, b); legend(负荷,光伏实际,风电实际,柴油机,储能放电,网购电); xlabel(时段(h)); ylabel(功率(kW)); title(微电网调度出力结果); grid on;4.2 代码里几个关键点的详细说明先看E_SOC sdpvar(1, T1)这一行。我见过程序写法是定义成T个变量结果循环到E_SOC(t1)时直接越界报错。定义成T1个让E_SOC(1)存初始值E_SOC(2)到E_SOC(25)分别对应第1到24小时结束时的电量这样递推关系写起来顺且不丢状态。再看充放电互斥约束。我用了b_ch b_dis 1这里其实是“弱互斥”允许两个都为0也就是储能既不充也不放这符合物理事实。但是要小心边界情况如果优化器发现“充电再放电”能创造收益它会想方设法让两个二进制都为1所以这个约束绝不能省。另一种更严谨的写法是用implies(b_ch, P_dis0)逻辑上等价但实际调试中上面的写法更直观也不会产生数值困难。目标函数里Cost_battery 0.01 * sum(P_ch P_dis)是一个可选的折旧成本。它的作用不只是算钱更重要的是抑制储能无意义的反复充放让优化结果更符合电池寿命管理。实际项目可以根据电芯成本和循环寿命标定出每kWh损耗费用一般几分钱但不可缺少。还有个细节售电价price_sell 0.85*price_buy这会让优化器尽量少“低买高卖”因为购售存在价差套利空间被压缩结果更符合真实电网规则。如果把售电价设成和购电价一样优化器可能会在谷段大量购电、再同时段高价卖出形成无风险套利这在模型上虽然可行但在真实工程中电网侧不会允许。4.3 结果怎么判断对不对跑通程序之后先别急着收工。把结果画出来或者把关键数字打出来按这几条规律肉眼检查。储能SOC应当符合峰谷套利规律谷时段充电SOC上升峰时段放电SOC下降一天结束时SOC不一定要回到初始值但一般会落在允许范围内。柴油机出力和电价高相关高峰时段负荷大、电价高柴油机大概率满发或接近满发低负荷夜间的谷段柴油机可能直接停机。光伏和风电在模型里是“用满优先”因为边际成本为零只有在储能满、负荷小、售电又不划算的极端情况下才会弃光弃风。我在调试时有一个习惯先跑一个极端场景比如把储能容量设为零看柴油机和购电能不能撑着负荷再把柴油机关掉重跑看储能加新能源能不能撑住。这两个极端场景能暴露大部分约束错误。5. 常见问题与排查技巧实录5.1 求解器报错速查表你大概率会遇到下面这几个问题按表格里的思路排查效率会高很多。现象可能原因解决办法提示No suitable solverYALMIP没找到CPLEX检查CPLEX的MATLAB路径运行yalmiptest确认提示Infeasible problem约束条件互相矛盾逐段注释约束重跑找到冲突源检查功率平衡方向结果全是0或NaN变量维度不对数据没传进去检查sdpvar维度用size()打印确认求解时间过长整数变量太多或目标为二次分段线性化设置mipgap和时限减少不必要的0-1变量结果不符合直觉SOC递推方向错效率方向反查看SOC曲线走势检查充放电流向5.2 Infeasible也就是“模型不可行”怎么查这是新手最容易崩溃的情况。所谓不可行就是约束集合没有任何解满足。排查思路最关键的一条不要盯着整个模型看要“拆”。我会先把储能相关约束注释掉只保留功率平衡和柴油机约束看还有没有可行解。如果可行问题大概率在储能如果仍不可行再检查功率平衡或者柴油机约束。这个二分排查法在模型稍微复杂一点后非常有用。另一个高频原因是爬坡约束过紧。比如柴油机最小出力50kW上一时段停机P0下一时段要求出力100kW爬坡上限60kW那么从0直接跳到100就违反了爬坡约束。正确的做法是给爬坡约束也乘以状态变量写成更精细的形式不过简单例程里可以先放宽爬坡限值跑通再逐步收紧。还有一次我遇到的不可行是因为联络线功率上限设成了200kW但某个时段的负荷加充电需求是600kW光伏风电又弱柴油机最大200kW无论怎么组合都不够。这种问题往往是数据自相矛盾检查数据比检查模型更快。5.3 求解慢的真正原因和优化手段微电网日调度模型不算大24个时段一般几秒到几十秒就能解完。如果发现慢到几分钟还不出结果通常有两个原因。一是目标函数里有二次项。CPLEX对MIQP不是不能处理但分支定界的效率会下降一个档次。解决思路是把二次燃料成本线性化。常用的方法是分段线性逼近把出力范围切成几段每一段用一个线性函数近似用SOS2约束或者直接加0-1变量选择段。对工程调度来说三段线性化已经足够精确。二是整数变量过多。比如你给每台柴油机、储能、联络线都加了0-1变量还加了启动变量、停机变量24时段乘以5个机组就是120个0-1变量复杂度指数级上涨。优化手段包括把同一机组的启停和启动变量合并用mipgap放宽最优性要求1%的间隙很多时候够了设时间上限让求解器在限定时间内给出一个可行次优解。实际项目里最不该缺的就是这个时间上限否则某天数据异常后台优化进程一直占着CPU整个EMS都被拖垮。5.4 电池建模里那些“看起来对但实际错”的细节铺开来总结储能建模的坑集中在三个地方。第一个坑是充放电方向。很多初学者把功率平衡写成P_buy P_dg P_pv P_load P_ch P_dis把放电也当成了负荷结果储能放电越多系统“缺电”越严重优化器自然选择永远不放电。这里有一个万年不变的检查方法看结果里SOC是不是会下降SOC永远不掉放电功率那一路十有八九写反了。第二个坑是效率的方向。充电效率应该乘在充电功率上放电效率应该除在放电功率上。有人统一乘效率结果用充电效率去套放电过程储能每放一次就“凭空多出”一些能量虽然数值差别不大但会导致调度结果轻轻松松赚钱看起来很美实际跑不下去。第三个坑是SOC的边界约束忘了加。如果只约束递推不约束SOC上下限优化器可能把SOC算成负数储能变成无底洞。别觉得这是低级错误我见过不止一个程序因为少了S_min E_SOC S_max仿真结果里电费为负还兴奋了半天以为是优化威力大。6. 从简单例程到实际工程的扩展思路6.1 把单日调度改成滚动优化上面的例程是静态日前调度一次性算好24小时计划。但真实微电网的负荷和新能源出力预测不可能完全准所以实际EMS里更多采用滚动优化的方式。典型做法是每隔15分钟或1小时用最新实测数据更新预测重新求解一次未来若干小时的调度问题只执行第一个时段的指令到了下一个周期再滚动更新。这叫模型预测控制MPC思路。实现上不需要改太多代码把数据结构改成滑动窗口、把初始SOC改成当前实测值就够了。我建议刚上手的同学先把日前静态跑通再改成滚动两版代码对比着看对“约束如何随时间平移”会理解得很深。6.2 加入备用约束和不确定性经济调度如果只追求费用最小结果可能在突发情况下“太脆”。实际工程会在约束里加旋转备用每个时段留出一定比例的备用容量比如负荷的5%加上光伏出力的10%确保一台机组跳机或云层遮住光伏时还能补上。从模型上看就是增加不等式约束所有可用机组最大出力之和 负荷 备用需求。这种做法代价不大但对系统安全性的提升非常明显。再进一步如果你要严格考虑光伏、负荷预测误差问题就从确定性优化变成随机优化或鲁棒优化。常见框架是场景法抽一批典型随机场景每个场景对应一组平衡约束目标是最小化期望费用。这个扩展会让模型规模成倍增加但YALMIP仍然能表达只是求解时间要关注。6.3 我对这套技术栈的一点个人体会调到今天我的感受是微电网调度模型本身并不复杂复杂的是数据和工程细节。你花在排查SOC方向错误、单位换算、电价时段划分上的时间可能比写代码本身还多。最后再分享一个我自己的习惯每跑出一个合理结果会把这组数据存成baseline后续改模型或改参数都把新结果和baseline对比看差异是否符合直觉。跑多了你会发现优化问题“对不对”比“快不快”重要得多。同样的模型拉长到96个时段、加入多台机组之后才真正考验你对YALMIP的熟练程度和CPLEX参数调优的功底。先把这篇例程吃透再往上走你会顺利很多。