做电力系统优化调度这些年我越来越觉得调峰两个字背了太多锅。新能源装机比例一上来电网净负荷曲线越来越陡调峰压力从火电一家硬扛慢慢变成水电、储能都要一起上。但真到了建模的时候很多同学还是把机组当成调度指令一下就必须顶上去的被动设备求出来的结果确实是数学上的全局最优拿到现场却很难执行——因为深度调峰对机组寿命和运行损耗的代价根本没进到目标函数里。这个「计及调峰主动性的风光水火储多能系统互补协调优化调度」问题要解决的恰恰是这件事把机组参与调峰的意愿、成本和能力边界显式写进优化模型再用Matlab把整个模型落地成可计算的程序最终得到一份不仅理论上最优、而且工程上真正可执行的调度计划。这篇文章我会从问题本身讲起把调峰主动性的建模思路、目标函数和约束的设计考量、Matlab实现的代码骨架、算例设计以及调试过程中踩过的坑一并梳理适合正在做电力系统优化调度、综合能源系统方向研究或者准备用Matlab做调度模型但还没完全找到门路的同学参考。1. 先把问题说透调峰主动性的本质是什么1.1 传统调度为什么不主动传统经济调度模型的目标函数基本是煤耗成本最小化在这种框架下所有机组都是被动执行者。模型会给火电下达很深的调峰指令让它压到最小技术出力以下运行。你算出来的方案成本很低但现场运行人员拿到这份计划往往直接改掉。原因很简单火电深度调峰不是没有代价的。机组长期在低负荷区运行锅炉燃烧稳定性变差受热面交变应力加剧检修周期缩短单位煤耗和NOx排放都会明显上升。这些代价没有折算进目标函数调度模型自然倾向让火电无限下探。结果就是理论计划与实际执行脱节也就是我常说的算得出但调不动。调度本质上是一个资源配置问题机组不是无条件响应的设备而是有自身技术边界和成本结构的参与者。调峰主动性的核心就是把这些隐形成本显式化让模型知道深度调峰是要付代价的只有当新能源消纳收益超过这些代价时才值得调用深度调峰能力。1.2 五类电源在系统中的角色定位风、光、水、火、储这五类电源各自的调节特性和成本结构差异很大搞清楚各自的角色建模才能有的放矢。风电的反调峰特性最明显夜间出力往往很大而夜间恰好是负荷低谷期这就制造了系统最棘手的调峰需求。光伏则相反白天出力与负荷高峰在时序上比较重合属于正调峰特性但完全靠天吃饭。新能源的共同问题就是不确定性——预测总有误差需要系统留出足够的备用和调节余量。水电是很好的调节资源启停快、爬坡速率高、运行成本极低但也有天然的限制。水电机组存在振动区在水头与出力不匹配的区间内运行会对转轮造成损伤同时受到来水量的约束一天之内发电量不是无限的存在日电量上限。火电是系统基荷和调峰的主力但最小技术出力一般在额定容量的30%-50%爬坡速率也受限深度调峰区间的运行成本随深度呈非线性上升。储能作为快速双向调节资源响应速度是分钟级甚至秒级的但容量有限存在荷电状态约束和循环寿命成本本质上是一个日循环装置不能指望它连续几天持续吞吐。互补的逻辑就很清晰了。白天负荷高峰时段由光伏、水电、储能协同承担火电保持较高出力夜间风电大发、负荷走低此时水电调节能力有限日电量约束就需要火电深度压负荷配合储能充电来消纳风电。调峰主动性的建模就是让这个互补过程在成本信号的引导下自然发生而不是人为硬性规定。1.3 调峰主动性的三种建模落地方式把主动性这个概念落到数学模型里面目前工程和学术上常见的有三种思路。第一种是调峰成本函数法。把火电的调峰区间划分成常规调峰区和深度调峰区常规区按正常煤耗成本计算深度区额外叠加损耗成本、投油稳燃成本等惩罚项。用分段线性函数逼近这些成本配合Big-M方法线性化得到的模型是标准的混合整数线性规划问题求解高效且结果稳定。这是目前最主流、也最适合入手的方案。第二种是参与意愿约束法。每台机组根据自身状态给出一个可接受的调峰深度上限比如1号机组刚大修过愿意压到50%额定出力2号机组临近检修期只愿意压到75%。模型只能在各个机组的意愿区间内调度从根本上避免了强行深度调峰。第三种是寿命损失折算方法。把储能的循环寿命折算成每次充放电的边际成本把水电机组避开振动区的收益折算进去。这本质上是一种精细化的成本分摊思路。实际应用中我推荐把三种思路结合起来成本函数法打底意愿约束兜底寿命成本做修正。核心都是回答同一个问题——机组愿不愿意被这样调以及愿意到什么程度。2. 优化调度模型的完整数学框架2.1 目标函数把主动性写进成本结构目标函数是整个模型的灵魂。我习惯把它写成五项加总的形式火电运行成本、机组启停成本、水电运行成本、储能等效损耗成本以及弃风弃光惩罚项。火电运行成本是重点。常规煤耗成本可以用二次函数拟合但在调峰主动性框架下需要做分段线性化处理正常出力区间内单位成本斜率较低进入深度调峰区间后斜率显著增大表示机组越往低压单位发电成本越高。这个处理让模型在做要不要让火电再往下压20MW的决策时必须权衡新能源消纳收益和火电深度调峰代价之间的高低。储能损耗成本可以用等效循环成本来近似。假设电池循环寿命为N次总购置成本为C则每次完整循环的等效成本是固定的。实际建模时可以简化为储能功率的线性函数储能每充放一度电产生一个小额的损耗成本。这里要注意储能本身不是电源它不产生能量只搬运能量所以目标函数中不能给储能的放电量加权当出力收益否则模型会为了让储能放电而人为制造充放循环。弃风弃光惩罚项非常关键。从经济角度讲新能源的边际发电成本趋近于零只要系统能消纳就尽量消纳。如果不加这个惩罚项模型会倾向于通过弃风弃光来规避调峰难度得到偷懒式的调度结果。惩罚系数的量级一般设置为常规单位发电成本的1.5-2倍既能引导消纳又不会让模型为了消纳几度电而付出过大的调节代价。水电运行成本可以取一个很小的常数或直接忽略因为水本身的成本远低于燃料成本重点通过水量约束来限制它的发电总量。2.2 约束条件体系不仅可行还要工程合理约束条件决定了模型求解结果能不能在实际系统中用。我的经验是按系统层-机组层-储能层三层来组织约束。系统层最核心的是功率平衡约束和备用约束。功率平衡要求每一时刻所有电源出力之和等于负荷风电、光伏、水电、火电、储能放电之和减去储能充电功率等于该时刻的负荷。这个约束如果出了问题通常表现为求解结果中某一时刻的功率莫名偏大或偏小。备用约束一般分解为上备用和下备用上备用要求系统在负荷突增或新能源出力骤减时能补上缺口下备用要求负荷骤降或新能源大发时能把出力压下来。机组层的约束包括出力上下限、爬坡约束和最小开关机时间。火电爬坡约束要特别注意时间尺度的匹配如果决策变量是15分钟粒度爬坡能力的限制就需要按15分钟折算如果是1小时粒度就按小时折算。最小开关机时间约束是防止机组频繁启停这个约束加不加对火电开停机方案的影响非常大尤其是在日负荷波动剧烈的场景下。储能层的约束包括充放电功率限制、SOC递推关系、以及最关键的一条同一时刻不能同时充电和放电。这条约束必须用二进制变量来强制实现。SOC的递推要写对时间索引我见过很多人在这一步出错导致SOC越界后面会专门讲这个坑。水电的约束除了出力上下限外还要加日电量约束或水量平衡约束。否则模型会默认水电全天满载出力这显然是不现实的。2.3 风光不确定性的处理从确定性到鲁棒性新能源出力的不确定性是绕不开的问题。最基础的方案是直接用预测曲线做确定性调度然后用旋转备用容量来兜底不确定性。这个方案实现简单求解速度快缺点是备用容量设置保守时可能会增加运行成本。进阶方案是场景法。生成多个新能源出力场景每个场景赋予一个概率目标函数改为各场景运行成本的期望值约束条件对所有场景同时满足。这个方法计算量随场景数量线性增加求解时间会明显上升一般在几十个场景以内是可控的。再进阶是鲁棒优化用不确定区间来描述风电光伏出力优化最坏情况下的系统运行成本。但这个对建模水平要求比较高非线性约束容易导致求解困难。我的建议是入门阶段先用确定性模型配合备用约束跑通全流程理解各模块的原理之后再逐步引入场景法扩展。2.4 线性化处理把工程问题转成计算机能解的形式很多同学在建模时习惯把约束写得非常真实结果目标函数里出现了非线性项白白失去了使用商业求解器的机会。YALMIP配合Gurobi或CPLEX求解器最擅长处理的是线性规划、整数线性规划以及二次规划问题。如果是非线性约束求解难度会急剧上升。调峰成本的分段线性化是最典型的例子。火电深度调峰的附加成本本质上是一个递增的曲线我们可以用几条线段去逼近它。每条线段对应一个出力区间引入辅助变量和Big-M系数把非线性成本函数转化为一组线性不等式。这个过程写过一次之后其实不难重要的是理解推导逻辑用额外变量表示非线性的凸组合再用约束把这个变量限定在合理的取值范围内。储能充放电状态也可以用Big-M处理。储能放电功率和充电功率各设一个连续变量再加一个二进制变量控制互斥关系当二进制变量为1时只能放电为0时只能充电。这两组约束的系数选取要小心M值取功率上限的2倍左右比较合适太大容易引起数值问题太小会错误地约束解空间。3. Matlab程序实现从模型到可运行代码3.1 工具箱选型YALMIP加商用求解器是黄金组合Matlab实现优化调度模型最大的工程问题其实不是写代码而是建模型的方式。手动把目标函数和约束展开成矩阵形式再调求解器接口容易出错且极难调试。我用过linprog直接建模的方式也手写过约束矩阵体验非常糟糕。比较推荐的做法是用YALMIP作为建模层用它声明决策变量、写目标函数和约束再调用Gurobi、CPLEX这类底层求解器去求解。YALMIP的语法和数学模型几乎是一一对应的写起来直观调试也方便。它支持sdpvar声明连续变量、binvar声明二进制变量、约束用条件拼接非常接近在纸上写数学模型的体验。求解器的选择上Gurobi和CPLEX在学术界都有免费授权性能也是第一梯队。Gurobi的MIP求解器在默认参数下表现就很好CPLEX则在某些类型的问题上有独特优势。如果暂时没有这两个求解器的许可证也可以先用开源的CBC求解器跑通模型但求解速度和稳定性会有明显差距。安装配置时要注意版本兼容。Matlab版本、YALMIP版本、Gurobi版本三者之间需要匹配特别是Gurobi每年都会更新旧版YALMIP可能无法识别新版Gurobi的接口文件。我习惯在全新的环境里先跑一个最小化的示例程序验证求解器通路再开始正式建模避免在模型写完之后才发现求解器配不上。3.2 代码架构与模块划分一份好的调度模型代码应该是分模块的而不是一个脚本文件从头写到尾。我常用的四模块结构是这样的数据输入模块负责读取风电预测出力、光伏预测出力、负荷曲线、机组参数、储能参数等全部整理成结构体方便后续引用。模型构建模块负责声明决策变量、写约束和目标函数。求解模块配置求解器参数、调用optimize函数求解。结果输出模块负责提取各变量的数值、计算关键指标并绘图。分成模块的好处很多换一套数据只需要改数据文件换个求解器只需要改求解模块的一两行代码模型报错时可以用模块为边界做二分定位快速找到问题出在哪一层。还有一个实用的小技巧所有参数都通过结构体传参避免在函数之间传递大量散乱变量既减少出错代码也更好读。3.3 核心代码片段拆解下面给出一段典型的核心代码骨架包含决策变量声明、约束构建和求解调用省略了具体的参数赋值部分% 决策变量声明 P_thermal sdpvar(2, 24); % 2台火电、24小时的出力 u_on binvar(2, 24); % 火电启停状态 SOC sdpvar(1, 25); % 储能SOC25个点含初始时刻 P_dis sdpvar(1, 24); % 储能放电功率 P_ch sdpvar(1, 24); % 储能充电功率 z_dis binvar(1, 24); % 储能放电状态标识 Constraints []; for t 1:24 % 功率平衡约束 Constraints [Constraints, ... sum(P_thermal(:, t)) P_wind(t) P_pv(t) P_hydro(t) P_dis(t) - P_ch(t) P_load(t)]; % 储能充放电互斥约束 Constraints [Constraints, 0 P_dis(t) z_dis(t) * P_sto_max]; Constraints [Constraints, 0 P_ch(t) (1 - z_dis(t)) * P_sto_max]; % 储能SOC递推 Constraints [Constraints, SOC(t1) SOC(t) - P_dis(t)/E_rated P_ch(t)/E_rated]; Constraints [Constraints, SOC_min SOC(t1) SOC_max]; end % 目标函数火电燃料成本 深度调峰惩罚 储能损耗 弃电惩罚 objective fuel_cost(P_thermal) penalty_depth(P_thermal) ... battery_cost(P_dis, P_ch) curtailment_penalty(P_wind, P_pv); % 求解配置 ops sdpsettings(solver, gurobi, verbose, 2, mipgap, 0.001); optimize(Constraints, objective, ops); % 结果提取 P_thermal_opt value(P_thermal); SOC_opt value(SOC);这里有个细节需要特别注意SOC的下标从1到25而功率的索引是1到24因为SOC递推需要用到0到24共25个时刻的状态。如果粗心把SOC定义成1到24递推到最后会少一个时刻导致末端SOC完全不受约束结果会出现储能最后时刻把电全放光的假象。关于目标函数里的fuel_cost和penalty_depth我这里用了函数封装实际写的时候要展开成分段线性成本表达式。YALMIP对分段线性函数有多种写法最简单的是用多个凸函数表达式取最大值的语法但要注意和Big-M展开的结果保持一致。3.4 求解器配置的实战要点求解器配置有几个参数直接影响求解质量。MIP gap设得太大会导致提前终止得到次优解设得太小则求解时间暴涨。我一般先设一个较大的gap快速跑通模型确认逻辑无误后再收紧到0.1%或0.05%级别做正式计算。日志信息的解读也是基本功。Gurobi的输出日志里Incumbent表示当前找到的最好可行解Best Bound表示目标值的理论下界两者的差值就是gap。当gap趋向于0时说明找到了最优解。如果在日志中看到gap长时间停滞不动往往是约束太紧或者模型数值有问题。求解失败时不要直接怀疑求解器。先用check函数检查约束残差YALMIP的check命令会显示每条约束的最大违约量如果某些约束的残差达到几十甚至上百基本可以确定是模型写错了而不是求解器的问题。这个习惯能省下大量排查时间。4. 算例设计与结果解读方法论4.1 基础数据准备的要点我自己跑模型时用的是一套简化的24时段数据包含两台火电、一台水电、一个风电场、一组光伏和一个储能系统。这组数据虽然不来自真实电网但参数的量级和比例关系是贴近实际工程的用来验证模型逻辑足够了。火电参数值得认真设计因为调峰主动性的效果主要体现在火电的行为上。1号机组额定容量200MW最小技术出力60MW这意味着常规调峰区间是60-200MW如果允许深度调峰还可以向下压到40MW左右但这部分出力要支付高额惩罚成本。2号机组容量100MW最小技术出力40MW。两台机组一个偏大、一个偏小才能在算例中体现出调峰成本差异带来的调度策略差异。爬坡速率我设置为额定容量的2%即1号机组每小时可以调整4MW——这个值在工程上属于偏保守的估计好处是不会出现相邻时段出力剧烈跳变的极端方案。风电和光伏的数据要体现出时间特性和反调峰特性。风电在凌晨1点到5点出力较高白天午间反倒有所下降光伏在11点到15点达到峰值傍晚降到零。这样组合起来系统的净负荷曲线会呈现非常经典的双峰特性白天午间光伏压低了净负荷傍晚光伏退坡后出现一个陡峭的晚高峰凌晨风电大发时段又出现低谷。这个曲线形状是考验调峰模型效果的最佳场景。储能参数设定为50MW功率、100MWh容量SOC下限20%、上限95%。电池效率按90%计算充放电损耗在SOC递推公式中体现。日初SOC设为50%同时约束日末SOC也回到50%。这个约束很关键它保证了储能是一个日循环装置不会把前一天的剩余电量留到第二天去放。4.2 对比方案设计有调峰主动性和没调峰主动性算例的价值不在于跑出一个漂亮的曲线而在于通过方案对比把调峰主动性的作用凸显出来。我设计了两个方案方案A是最传统的经济调度模型目标函数只有煤耗成本加启停成本完全不考虑深度调峰附加成本和储能损耗成本。方案B按本文的模型加入火电深度调峰分段惩罚、储能等效循环成本和弃电惩罚。对比两个方案的关键指标包括火电最小出力达到多少、火电在深度调峰区间运行时长的比例、储能充放电循环次数、弃风弃光率、总运行成本。这些指标分别从不同的角度反映调度策略的工程合理性。我在实际计算中发现了一个特别有意思的现象方案A中火电经常性压到40MW左右的极限出力运行模型觉得这是很经济的做法因为深度调峰在目标函数里不需要付代价。方案B中火电的最低出力基本停在55MW附近——再往下压要么触发深度调峰惩罚要么不如让储能多承担一些调节任务。储能循环次数从方案A的不到1次增加到方案B的1.5次弃风率略有上升但控制在较低水平。这说明在模型里加入主动性成本后调度结果的调节压力发生了明显转移从让火电硬扛转向多种资源分摊。4.3 结果曲线怎么读结果画的图我一共做了四张系统功率平衡曲线、火电各机组出力及调峰深度分布、储能SOC与充放电功率曲线、弃风弃光电量柱状图。功率平衡曲线要注意看每个时刻的功率差额是否严格为零。如果曲线有缺口基本可以判定是约束构建阶段出了问题要回去查约束条件而不是急着分析结论。火电出力曲线重点看早晚两个时段的行为差异。SOC曲线应该是一条平滑的、在上下限之间均匀波动的曲线如果看到SOC在某几个时刻出现剧烈跳变就要检查是不是约束索引写错或者储能效率设置不合理。SOC曲线整体偏平说明储能没有充分利用有些时段模型宁肯弃风也不让储能充电SOC触顶或触底则说明储能容量和功率配比可能与系统需求不匹配。这些曲线的解读能力很重要因为模型的对错和优劣往往通过曲线形态就能提前感知到不需要等到统计指标出来再判断。5. 常见问题与调试排查手册5.1 YALMIP建模阶段的典型报错YALMIP报错信息偶尔会让人摸不着头脑但大多数问题的根源就几类。solver not found是最常见的意思是YALMIP没有找到匹配的求解器。原因基本上是求解器没装好、路径没添加或者模型的问题类型超出了求解器能力比如模型里出现了非凸项而求解器只支持凸问题。处理方式是先跑一个最简单的LP问题测试求解器通路是否正常再逐步增加模型复杂度。矩阵维度不一致的报错也很频繁。SOC维度是25出力维度是24写递归约束时索引错位的情况非常普遍。我的建议是写约束时先打印一下每个变量的尺寸确认无误再拼接。另外YALMIP允许约束直接做逻辑运算但如果把两个维度根本不匹配的表达式放在一起比较很难在第一时间发现。还有一个隐蔽的问题是变量命名冲突。如果在脚本里用变量名t作为循环变量而YALMIP内部函数也用t容易在循环内出现莫名错误。我的习惯是循环变量用k或tt避免和系统函数发生冲突。5.2 求解器层面的数值与性能问题模型规模大了之后求解时间会成为主要矛盾。二进制变量是最大的性能杀手每增加一个二进制变量MIP问题的复杂度都可能呈指数级上升。检查一下代码里是否有多余的二进制变量比如某些约束本可以用连续变量和凸性表达却被强行拆成了二进制逻辑。数值尺度问题在电力调度模型中非常典型。负荷是MW量级几百到几千成本是元/MWh量级几十到几百两者相乘就是10的4到6次方。这个量级差异本身不至于让求解器崩溃但会导致MIP gap的计算出现误差。稳妥的做法是用标幺值或归一化处理把功率和成本的量级都压到1附近。这会显著改善求解速度尤其在跑几十个场景的随机优化时差别非常明显。如果不幸遇到gap长时间不收敛可以尝试三个手段给求解器设置一个合理的时间上限先接受一个次优可行解调整MIP focus参数加速寻找可行解而不是最优解检查是否存在冗余约束导致求解空间过紧。记住一条求解器长时间算不出来的时候往往不是求解器不行而是模型本身有数值或逻辑问题。5.3 结果不合理时的排查思路结果不合理主要集中在这几类储能同时充放电、末端SOC不收敛、火电频繁启停、弃风率异常高。储能同时充放电几乎可以确定是互斥约束没写对。检查二进制度量是否覆盖了全部时段以及v加w小于等于1的约束是否真的被包含进了Constraints变量。如果你用了Constraints [Constraints, ...]持续追加但某次误用了赋值而不是拼接互斥约束就可能在后续覆盖中被丢掉。这类问题最坑因为别的约束还在模型不会报错只是行为怪异。末端SOC不收敛的原因很可能是初始SOC和末端SOC约束只写了一个。如果只约束了初始SOC而没约束末端模型会倾向于在最后一个时段把存储的能量全部释放去满足负荷。如果只约束了末端而没有重视初始前几个时段的储能行为会显得不合逻辑。两种情况都必须避免。火电频繁启停意味着最小开关机约束缺失或太松。加入最小开机和最小停机时间约束后启停次数会显著下降但同时会轻微增加运行成本这个代价是合理的。弃风率异常高时先检查弃风惩罚系数是否足够大。如果系数设置得比调峰成本还低模型当然会选择弃风。另外注意PV和风电的出力上限约束是否正确有时候因为数据读入错误导致预测出力被设置成非常小的值模型会无风可消纳。5.4 几个关键参数的调参心得调峰惩罚系数不要拍脑袋它的物理含义是深度调峰带来的额外煤耗和寿命损耗。一个比较合理的参考基准是机组进入不投油的深度调峰区后单位发电成本大约上升10%-30%。储能损耗成本可以直接由电池的全生命周期总吞吐电量和购置成本相除得到如果设计得比弃电惩罚还高模型就不会主动用储能了。弃风惩罚系数设置在常规成本1.5-2倍的量级既鼓励消纳又不至于扭曲整个调度策略。我自己调试模型的时候经常是先给惩罚系数一个初值跑出方案B然后对比方案A的结果观察火电的最低出力是否移动了。如果火电行为和方案A完全一致说明惩罚系数太小调峰主动性的约束没有真正生效如果火电不仅不深度调峰连常规调峰都只做了一半说明惩罚系数过大矫枉过正了。找到那个火电愿意调但又犹豫的区间参数就算调对了。最后再分享一点个人体会。我一开始做这个模型时犯的错就是把目标函数建得太贪心深度调峰成本非要搞成高次非线性函数结果求解时间从十几秒飙升到十几分钟还时不时出数值病态。踩过几次坑之后我的习惯是先做线性化、能分段的就分段能赋权重的就赋权重先把模型跑通再逐步补充精度。所有花在模型线性化上的时间最终都会在求解阶段加倍赚回来。这个方案本身也还有很大的扩展空间比如把日前调度延伸到日内滚动把单一储能换成多个储能加需求响应把单目标换成调峰深度与运行成本的多目标权衡——顺着这套框架往下补能做的方向非常多。