风电并网之后的调度安排,跟传统火电完全不是一个玩法。风速一阵风过来,风电场出力可能从满发掉到三成,功率预测曲线到了第二天,误差依然能到10%以上。这个项目想干的事情,就是把“风电随机性”明明白白地放进动态经济调度模型里,用Matlab搭建一套可运行的随机优化求解程序,最终输出计及风电不确定性的多时段机组出力和备用安排。文章面向电力系统方向的研究生、做新能源并网规划的工程师,以及所有需要上手随机调度的Matlab用户。我会从模型设计、场景生成、代码实现到调试心得,完整讲一遍。
1. 从确定性调度到随机性调度:模型设计的底层逻辑
1.1 风电随机性为什么让“确定性模型”失效
传统动态经济调度(DED)假设负荷和风电出力都是已知的,在单一预测场景下求总成本最低的机组出力计划。但实际上风电预测误差大,且误差随时间尺度增长,短时调度尤其敏感。用一个固定的预测值代替随机分布,结果就是:要么预留了过多旋转备用,成本虚高;要么备用不足,实际运行时可能发生切负荷。电力系统的调度决策是要在天气未定的情况下提前做出来的,所以这个“随机性”必须被建模。
从数学上看,确定性DED是“单一场景”问题,而随机DED必须处理“可能出现的情况集合”。目标函数不再是单一场景下的成本,而是所有可能场景下的期望成本,约束也要逐场景满足,这直接导致问题规模成倍扩大。但这一步绕不过去,因为调度计划要覆盖未来24小时甚至更长的窗口,风电出力的不确定性在任何时段都存在,忽略它就是在拿可靠性换经济性。
1.2 动态经济调度里,“动态”两个字到底指什么
静态经济调度只管单个时段,机组出力满足当前时刻的负荷和备用就完事。动态经济调度要连续决策每个时段(比如15分钟一个断面,共96个时段)的机组出力,并考虑机组爬坡速率约束、启停状态约束、水电和储能等跨时段耦合。风电随机性带来的另一个麻烦是:上一时段的低出力可能耗尽爬坡能力,导致下一时段面对高出力却无法降下来;反过来,高出力后的骤降也可能让机组爬坡跟不上。所以调度模型必须把时间耦合约束和随机场景同时放进决策层,这正是项目中最复杂的地方。
动态模型里还藏着“时序逻辑”问题。机组启停状态是日前决策,不能等看到实际风电再定;而机组出力可以在日内根据实际风电调整。时间上,启停变量是全时段耦合的,出力变量又跨场景独立,这种一阶段、二阶段混合结构,是随机动态调度区别于普通动态调度的根本原因。
1.3 场景法、机会约束还是鲁棒优化
在Matlab建模时,我比较过三条技术路线。
- 场景法(随机规划):生成大量风电出力场景,目标函数用场景概率加权求期望。实现直观,能直接得到不同场景下的出力调整方案,但场景多了模型规模大。
- 机会约束规划:要求系统在给定置信水平(比如95%)下满足备用约束,把随机约束转成概率约束。求解时需要分位点或线性化转换,代码稍复杂,但不会带来场景维度的爆炸。
- 鲁棒优化:只考虑最恶劣场景,保证所有可能情况都不越限。不用概率分布,但结果通常偏保守,经济性差。
这个项目最终采用“场景法+旋转备用附加约束”的组合。原因是:场景法能给出期望成本和逐场景可行解,旋转备用约束又能弥补极端场景样本不足的问题;在保证风险可控的同时,求解时间还在工程可接受的范围内。如果你要发表论文,建议在基础模型上再加机会约束对比,能显著提升说服力。
1.4 为什么选Matlab做主战场
不少工程问题我会用Python+Gurobi,但Matlab在这个场景下仍然有优势。YALMIP工具箱允许用接近自然语法的方式描述优化问题,矩阵计算、后处理和绘图一体化,配合Matpower可以省掉大量电网拓扑数据读写的功夫。代码写好后,无论是复现论文还是教学演示,都比纯Python方案更直观。
另外,Matlab的调试工具对数组维度检查很友好。随机调度模型里到处是三维变量,用Python的numpy写起来一旦维度广播出错,报错信息很飘;Matlab里敲一个size()就能肉眼确认。对于学生党,Matlab的矩阵思维还能帮助理解优化模型的结构,这点是隐形的学习收益。
2. 风电随机性建模:场景生成与缩减
2.1 风速概率分布到风电出力功率曲线的映射
风电出力的随机性根源是风速。风速通常用两参数Weibull分布描述,形状参数和尺度参数可以通过历史数据拟合。但调度中更常用的是风电功率预测值加上误差分布。风电功率预测误差的经验分布近似服从均值为0、方差随时间变化的拉普拉斯或正态分布,极限情况还会出现“削顶”现象——预测满发但实际出力受限。
建模时我采用这种处理方式:先利用预测风速曲线和风功率转换曲线,得到基准风电出力序列P_w_base(t),再在每个时段叠加一个随机误差项ε(t),误差标准差取该时段预测出力的一定比例。这里有个反直觉的点:满发区间的误差反而小,因为风速超过额定风速后出力已经被“钳制”在额定值,所以误差分布要用条件分布去处理,不能在全功率区间套同一个方差。
2.2 拉丁超立方采样加相关性的场景生成流程
生成随机场景不能简单对每个时段独立采样,因为相邻时段的风速有强相关性,任意生成会出现“这一小时满发,下一小时零出力”的极端跳变,和现实不符。我的做法是:
- 确定采样规模N,例如1000个初始场景。
- 对每个时段的预测误差随机变量,用拉丁超立方采样(LHS)得到N个等概率分位点样本。
- 根据历史风速数据估算时段间相关系数矩阵,用Cholesky分解处理相关性,让生成的误差序列在时间维度上平滑。
- 将误差叠加到基准出力序列上,并做上下限截断,保证出力在[0,额定值]范围内。
- 得到N个场景,每个场景有T个时段的风电出力序列。
这步的细节直接影响后续优化结果。LHS比普通蒙特卡洛的收敛速度更快,需要5000个样本才能达到的效果,LHS用1000个就差不多。相关系数矩阵如果随便设成单位阵,后续场景会显得“支离破碎”,机组爬坡约束会被虚假的极端跳变卡死。这一步我花了不少时间调试,最终发现用历史数据直接算相关矩阵最可靠。
2.3 场景缩减:从1000个到10个的关键手段
优化模型里每个场景都会成倍增加决策变量和约束数量,1000个场景对混合整数规划来说根本没法在可接受时间内求出可行解。所以必须做场景缩减。场景缩减的常用方法有两种:K-means聚类和同步回代消除。
我推荐用同步回代消除法,因为它在概率意义上保留了场景集的“最小期望距离”。基本思路是:
- 给每个场景一个初始概率1/N。
- 每次找到一对“距离最近”的场景,把概率小的那个合并到概率大的那个,并累加概率。
- 重复直到场景数达到预设目标。
用这个方法把1000个场景缩减到10个后,样本概率不再是均匀的,优化时要用每个场景的概率作为权重。这个权重一定要正确传给目标函数,否则最后算出来的是算术平均成本,不是期望成本。我自己第一次就是漏了概率归一化,结果期望成本比确定性模型还低,明显是错的。
2.4 数据清洗和相关性校验
实际风电数据不会像论文里那么干净。我遇到过几类问题:数据里包含风机停机检修导致的长期零出力、限电导致的出力平台期、还有传感器跳变毛刺。这些异常样本如果不清洗,拟合出来的误差分布和相关系数矩阵都会失真。处理办法是:先设置出力变化率阈值剔除毛刺,再用滑动窗口填充停机段,最后用Q-Q图看看残差是否接近假设分布。
3. 动态经济调度模型的数学表达与约束处理
3.1 目标函数:期望成本怎么“写进”优化器
随机动态经济调度的目标函数是总期望成本,我通常包含四项:
- 燃料成本:常规机组出力构造上的二次函数
a_i P_i^2 + b_i P_i + c_i。 - 启动成本:每次冷态启动或热态启动的费用,通常用一个分段函数表达。
- 失负荷惩罚:允许在某些场景下用拉负荷量平衡功率,但代价要足够高。
- 弃风惩罚:为了消纳风电,有时允许弃风,但要加入罚函数或目标项。
在期望成本框架下,二阶段变量(各场景下的机组出力、弃风量、失负荷量)都按场景概率加权。一阶段变量(机组启停状态、备用容量)在所有场景共享,这样才符合“先做机组组合决策,再根据实际场景调整出力”的时序逻辑。目标函数代码里,把场景概率prob(s)作为权重乘到每个场景的成本前面即可。
3.2 核心约束:功率平衡、出力上下限与爬坡
这部分需要逐条展开:
- 功率平衡约束:对每个场景s和每个时段t,所有常规机组出力加风电实际出力等于系统负荷加网络损耗(忽略损耗时直接等号)。由于场景不同,风电出力不同,所以这个约束必须拆到每个场景。
- 机组出力上下限:
P_i_min ≤ P_i(s,t) ≤ P_i_max,且受启停状态影响。 - 爬坡约束:
-RD_i ≤ P_i(s,t) - P_i(s,t-1) ≤ RU_i,这是动态调度的灵魂。 - 启停时间约束:如果机组开机/停机后需要最短运行/停机时间,需要额外引入连续状态变量。
由于场景法把每个场景都当作并列的世界线,爬坡约束要在每个场景内部独立满足,不能跨场景约束。这一点是初学经常写错的地方。另外,如果某个机组在所有场景中都要启动,则需要额外的启动变量和逻辑约束,否则求解器会把同一机组的状态变量搞混。
3.3 旋转备用和机会约束的等价转换
为了让系统应对随机波动,除了场景本身,还需要设置旋转备用约束。最简单的建模是要求系统各场景下可调用量之和大于某个比例(比如负荷的5%加上风电出力的10%)。但完全按每个场景的出力上限预留备用,备用量又太大。
更精细的做法是用机会约束:P{备用不足} ≤ α,α取0.05。把机会约束线性化后可以转成“某分位数≤可用备用”的形式。在Matlab里可以直接用quantile函数处理场景数据,把备用约束变成一组确定性约束。推荐先做场景法,确保约束全部满足,再辅助机会约束,避免一上来就写分位数造成不可行。
3.4 模型规模和求解器选择
一个典型的10机24时段模型,如果用10个场景,机组组合变量就有10×24=240个二进制变量;连续变量更多,场景法还会乘上场景数量。这个规模对混合整数二次规划(MIQP)来说中等偏大。求解器我优先用Gurobi或Cplex,能处理上千个二进制变量;没有授权的话也可以用SCIP或glpk,但速度会慢很多。YALMIP已经封装好了接口,换求解器只需要改一行配置。
4. Matlab代码实现与完整流程
4.1 代码文件结构
我把项目拆成五个脚本/函数,便于单独调试:
main.m:主程序,依次调用数据准备、场景生成、模型构建、求解和画图。load_case.m:定义系统参数、机组数据、负荷曲线与成本系数。scen_gen.m:生成并缩减风电场景。build_model.m:用YALMIP构建目标函数和约束,并调用求解器。plot_result.m:结果可视化。
这种拆分的好处是,场景生成和优化求解可以分开调参。比如我只想换一组历史风速数据,就只改scen_gen.m;想对比没备用约束的效果,就只改build_model.m里的约束集合。如果全部堆在main.m里,每次调试都要从头跑,单是场景生成就要浪费几分钟。
4.2 机组与负荷参数准备
这里需要确定数据格式。我用的是一个修正的6机系统,24时段,负荷曲线按典型日峰谷差约40%。机组参数表简化为表格:
| 机组 | 最大出力(MW) | 最小出力(MW) | 爬坡速率(MW/h) | a($/(MW^2h)) | b($/MWh) | c($/h) |
|---|---|---|---|---|---|---|
| G1 | 200 | 50 | 100 | 0.002 | 10 | 300 |
| G2 | 150 | 40 | 80 | 0.003 | 12 | 250 |
| G3 | 180 | 45 | 90 | 0.0025 | 11 | 280 |
| G4 | 120 | 30 | 60 | 0.004 | 14 | 220 |
| G5 | 100 | 25 | 50 | 0.005 | 15 | 180 |
| G6 | 80 | 20 | 40 | 0.006 | 16 | 150 |
这个表只是演示用,实际项目里要用场站真实参数。但要注意:煤耗曲线系数相差不要太大,否则目标函数里二次项和一次项的数值尺度会差出五个数量级,影响求解器数值稳定性。我在做真实数据时,会把成本系数统一折算到以“千美元”为单位,效果会好很多。
4.3 YALMIP建模的核心代码
下面是一段关键代码片段,展示如何在Matlab中定义场景变量和期望成本目标。
% 决策变量定义 Pg = sdpvar(nG, T, nScen, 'full'); % 机组出力 Pwc = sdpvar(nW, T, nScen, 'full'); % 实际消纳风电 u = binvar(nG, T, 'full'); % 启停状态(一阶段共享) % 定义启动成本辅助变量 su = sdpvar(nG, T, 'full'); % 目标函数:期望总成本 obj = 0; for s = 1:nScen for t = 1:T obj = obj + prob(s) * (sum(a .* Pg(:,t,s).^2 + b .* Pg(:,t,s) + c .* u(:,t)) + ...); end end % 启动成本约束 for t=2:T for i=1:nG con = con + [su(i,t) >= upcost(i) * (u(i,t) - u(i,t-1))]; end end这段代码里有个要点:启停状态u是跨场景共享的,而机组出力Pg是每个场景独立的三维变量。如果不注意区分,很容易把u也写成三维,导致不同场景出现完全不同的机组组合,这在物理上是不合理的,因为机组组合是在日前阶段决定的,不依赖于随机场景。
4.4 功率平衡与备用约束构造
功率平衡约束用YALMIP的集合方式添加:
con = []; for s = 1:nScen for t = 1:T con = con + [sum(Pg(:,t,s)) + sum(Pwc(:,t,s)) == load(t)]; con = con + [Pwc(:,t,s) <= Pw_scen(s,t)]; % 可用风电上限 con = con + [Pwc(:,t,s) >= 0]; con = con + [sum(Pg(:,t,s)) + sum(Pwc(:,t,s)) + reserves >= load(t)]; end end这组约束里,Pwc是实际被消纳的风电,不能直接等于场景出力,否则系统会强制消纳所有风电,可能造成无解。弃风量通过Pw_scen - Pwc隐式表达。备用容量通常由在线机组的可调空间构成,如果reserves是一个固定数值,那这个约束只是粗略的确定性备用;更准确的做法是把备用与在线机组的上限差绑在一起,写成sum(Pg_max .* u - Pg) >= reserve_required。
4.5 求解器配置与结果输出
求解器配置:
ops = sdpsettings('solver', 'gurobi', 'verbose', 2); ops.gurobi.MIPGap = 1e-4; ops.gurobi.TimeLimit = 600; result = optimize(con, obj, ops); if result.problem == 0 fprintf('求解成功,期望总成本:%.2f\n', value(obj)); else yalmiperror(result.problem) end注意sdpsettings的结构体字段名必须与当前求解器匹配,比如用Cplex时是ops.cplex.mip.tolerances.mipgap,而不是ops.gurobi...,这个坑我一踩就是半天。另外,verbose不要拉到2以上,否则屏幕会被刷满,关键输出反而看不清。result.problem返回0表示求解成功,其他数字对应不同错误类型,用yalmiperror函数能打出可读的提示。
5. 实操案例:10机24时段随机调度结果分析
5.1 算例场景设置
我这里用10机系统做演示,风电装机600MW,负荷峰值2800MW。生成初始场景1000个,缩减到10个,各场景概率用直方图展示后,能看出概率分布近似“尖峰瘦尾”。场景缩减后的典型概率可能长这样:高概率场景集中在预测出力的附近,而极端的低出力场景概率很小但依然保留,这是随机模型能够兼顾“大概率”和“小概率风险”的关键。
5.2 确定性调度与随机调度的成本对比
先跑一个基准确定性模型(忽略随机误差),再跑随机模型,结果对比列举:
| 指标 | 确定性模型 | 随机模型 |
|---|---|---|
| 总成本($) | 324500 | 336200 |
| 备用容量(MWh) | 180 | 260 |
| 失负荷期望(MWh) | 3.2 | 0.05 |
| 求解时间(s) | 6.3 | 85 |
随机模型成本高约3.6%,但失负荷风险大幅下降。这说明“经济性”和“可靠性”在随机调度里本质上是一个权衡问题。工程上如果风险成本考虑得足够高,随机模型的总费用甚至可能反超确定性模型。这里还有一个值得关注的现象:确定性模型的备用容量虽然只要180MWh,但实际运行中一旦风电预测偏大,备用根本不够用,失负荷次数会明显升高。随机模型多备了80MWh,成本增加不多,但可靠性曲线平滑很多。
5.3 不同场景数对结果的影响
场景数从5增加到20,期望成本变化很小,但求解时间几乎翻倍。我的建议是:验证算法精度时用50个场景做基准,实际应用时取10个场景,再配合机会约束兜底。这样既保留随机性信息,又控制计算规模。场景数一旦超过30,模型文件大小和求解内存都会明显上升,这时候不妨先检查是不是场景缩减环节没做好,而不是一味加场景。
5.4 结果可视化的关键图
画图时,我习惯输出三张图:风电场景曲线叠加图、机组出力时序图、各类备用容量图。场景叠加图用半透明色绘制,方便看置信区间;机组出力时序图用堆叠面积图,容易看出机组发电比例变化;备用容量图则用双纵轴展示备用和负荷。后处理有个小技巧:YALMIP求解后,value(Pg)返回的是三维数组,用squeeze去掉单维度,再mean(,3)求场景平均,就能画出期望出力曲线。如果直接plot(value(Pg)),它会给每个场景画一条线,图会乱成一团,新手经常在这翻车。
6. 常见问题与调试心得
6.1 场景数爆炸导致计算内存不足
场景太多,YALMIP自动展开所有变量后,模型文件可能达到几百兆。解决思路有几个:
- 使用
sdpsettings('expand',0)关闭自动展开,手动分场景处理二阶段约束,减轻符号运算负担。 - 用Benders分解把随机模型拆成主问题和子问题,但实现复杂。
- 最直接的办法是场景缩减,用10个场景替代1000个。
我在实际调试中优先用的是场景缩减,只有当模型本身特别小、场景需求又特别高时才考虑expand选项。expand关闭后,YALMIP处理循环约束的速度提升明显,但需要你手动确保所有约束都正确索引,不然很容易漏掉某场景,错得很隐蔽。
6.2 模型不可行,到底哪里出了问题
不可行的排查顺序很重要:
- 先跑不含风电的纯火电模型,确认基础数据没问题。
- 加入风电场景,但不加备用约束,确认功率平衡可行。
- 再逐条添加备用、爬坡、启停时间约束,用
optimize返回的result和YALMIP的check函数定位是哪条约束虚报了不满足。 - 别忘了检查首时段爬坡约束,初始出力与第一个时段出力的差值可能超出爬坡限制。
有一次我卡了三个小时,最后发现是初始状态里机组的前一时段出力设成了0,而首时段要求它出力100MW,爬坡约束直接爆了。把初始状态改为该机组的最小技术出力,问题立刻消失。这个坑说出来很简单,但新手很难想到去查初始条件。
6.3 求解器调参的实用建议
混合整数模型里,默认求解器参数往往不是最优的。我常用的几个参数:
- MIPGap设为1e-4或1e-5,避免默认1e-2导致结果质量差。
- TimeLimit设500-900秒,防止卡在无穷分支。
- 启停变量给一个合理的初始可行解,热启动能大幅减少求解时间。
- 如果求解器一直徘徊在0.1%-0.5%的gap附近,可能说明模型存在冗余约束或数值问题,检查有没有大的常数项(比如成本系数相差几个数量级)。
以Gurobi为例,你甚至可以在build_model.m里先求解一个松弛的线性模型,用它的结果来初始化二进制的启停变量,这招在实际算例中往往能把求解时间缩短30%以上。不过要注意,热启动给出的状态可能不是全局最优的,最终还是要靠求解器自己验收。
6.4 几个容易忽略的细节
最后说几个踩过坑的细节:
- 风电功率预测误差的标准差不能设成定值,应该跟预测功率大小相关,否则高出力时段会被放大到超过额定值。
- 场景缩减后各场景概率要做归一化,因为回代消去法可能会让概率总和略微偏离1。
- 备用约束里,风电可调出力通常只能算一部分,不能把实发风电全部当成备用,否则会高估系统调节能力。
- 用YALMIP求解二次目标时,如果遇到“quadratic constraint”报错,多半是把成本函数里的二次项写到了约束,而不是目标里。
这个模型后续还能往机组检修计划、储能日前调度、多风电场相关性建模方向扩展。如果手头有实际场站的功率预测数据,把scen_gen.m里的误差分布替换成实测经验分布,就能直接落到生产环境里用。我个人在整个复现过程中的体会是:随机动态经济调度的难点不在于优化算法本身,而在于把物理约束、概率信息和求解器特性三者对齐。多花时间在场景生成和结果校验上,比单纯堆算法要划算得多。