☰
计及调停主动性的风光水火储多能互补协调调度Matlab实现
2026/9/26 23:26:01 网站建设 项目流程

我搞了段时间的电力系统优化调度,发现一个有意思的现象:很多做多能互补的模型,本质上还是"调度中心说了算"的单向指令模式——风、光、水、火、储各自跑各自的出力,调度只负责平衡。但真实运行里,火电机组的深度调峰意愿、水电的库容焦虑、储能的SOC损耗,甚至风电场面对弃风时的配合程度,这些"主动性"因素如果被调度模型忽视,最后算出的"最优解"往往在执行环节寸步难行。

这篇东西就围绕"计及调停主动性的风光水火储多能系统互补协调优化调度"展开,对应的是一套完整的Matlab代码实现方案。它适合三类人:一是刚入门做多能互补调度的研究生,想找个能跑通、能改参数的代码框架;二是做电网调度运行、需要面对多类电源协调问题的工程师;三是对优化建模有兴趣、想看看YALMIP怎么落地一个真实MILP问题的算法爱好者。我这里会把这套模型的数学逻辑、代码结构、关键参数设置和实测结果全部拆开讲,不藏私。

1. 为什么传统调度思路在"风光水火储"面前失效了

先聊一个很多人都没意识到的问题:传统的经济调度,本质是"平衡优先、经济其次"的单向分配。系统需要多少负荷,调度就按等耗量微增率原则把出力分给各个机组。这个思路在电源结构单一、调节性能接近的时代没什么问题,但放到风光水火储五类电源同台竞争的场景里,立刻暴露短板。

1.1 五类电源的"性格"完全不一样

风电和光伏看天吃饭,出力曲线有自己的脾气,预测误差随着时间尺度拉大而膨胀;水电受来水和库容双重约束,调峰能力强但"后劲"取决于水库水位;火电调节速度慢,深度调峰时煤耗剧增,频繁启停更是伤机组寿命;储能响应最快,但容量有限,SOC上下限一旦卡死,它的可调度空间就是一条逐渐收缩的曲线。

如果用一个刚性模型把这五类电源按统一规则调度,最容易出现的结果是:火电被要求大幅压负荷运行,煤耗指标难看,机组寿命受损;水电被要求在某时段满发,结果后续来水预测偏差导致水库放空,下一轮调度电动摇。这就是典型的"模型最优、现实次优"。

1.2 调停主动性的提出背景

调停主动性这个概念,说白了就是让各类电源从"被动接受出力指令"变成"主动申报调节意愿"。每台机组、每个储能电站、甚至风电场和光伏电站,在参与调度以前,按照自身状态提交一个可调节区间和对应的调节意愿度。调度模型不是直接锁定它们的出力,而是把"主动性"作为决策变量之一,和功率平衡、成本目标放在同一个优化框架里求解。

乍一听好像只是换了个说法,但实际效果差异很大。火电可以在低谷时段明确申报"我最多压到60%额定出力,要再往下我就得抬高报价或者拒绝",储能可以在SOC较低时主动申报"我这轮基本只充电不放,或者放出50%容量就停",风电场在预测高发时段申报"我愿意承担部分弃风,换取后续时段的优先并网权"。调度模型把这些意愿纳入优化,求出的解不再是"理论最优",而是"可执行最优"。

1.3 对Matlab实现来说这意味着什么

从建模角度看,引入调停主动性就直接改变了模型形态。传统经济调度是线性规划(LP)甚至可以手算,而计及调停主动性的多能互补调度,至少需要引入0-1状态变量来处理机组启停、调节档位选择、意愿满足与否等逻辑关系,目标函数里还要迭加主动调节成本项和违约惩罚项。这类问题在Matlab里最顺手的工具是YALMIP工具箱加外部求解器(CPLEX或Gurobi),模型本身属于混合整数线性规划(MILP)。

关键点在于:很多入门者以为MILP就直接丢给求解器跑就完了,实际上模型规模一大,约束方程写得稀烂照样跑不动。我后面会详细讲这套代码里怎么利用矩阵化约束生成、变量索引设计和求解器参数调优来保证计算效率。

2. "调停主动性"到底在模型里怎么量化

概念讲得再热闹,落到数学里只有一个问题:调停主动性用什么变量表示,它怎么进入目标函数和约束条件。这里有个常见的误区——有人把调停主动性直接等同于可调容量,觉得"能调的越多主动性越高",这是不对的。

2.1 主动性的三层量化结构

我在代码里把调停主动性拆成了三层结构,分别对应不同的决策维度:

第一层是"可调域申报",对应的是机组/储能在当前时段愿意承担的出力范围上下界。火电可以申报60%-100%额定出力,水电按当前水头和库容申报调度范围,储能按SOC状态申报充放电区间。这一层直接进入约束条件,把原来固定的出力上下界变成随主动性变化的动态区间。

第二层是"调节意愿系数",一个0到1之间的常数值,表示该电源对参与本次调停的主观积极程度。意愿系数为1表示全力配合,为0.5表示半推半就,接近0表示消极应对。这层系数进入目标函数的成本项——意愿度越低,调度它产生的额外成本越高,优化器自然会优先调度意愿系数高的电源。

第三层是"主动性违约惩罚",当约束条件无法满足某个电源申报的可调域时,不允许直接硬性突破,而是引入一个松弛变量表示"调度越界量",在目标函数里按高额惩罚系数扣分。这样模型可以在极端工况下"柔性越界",但正常工况下不会随便动这个空间。

2.2 目标函数里怎么体现

我用一个统一的运行成本最小化目标,包含常规项和主动性相关项。常规项是火电煤耗成本、机组启停成本、风电光伏运维成本、储能充放电折损成本;主动性相关项是主动调停补偿成本(意愿系数越低补偿越高)和越界惩罚成本(松弛变量的一次项)。

这样说有点干,直接看公式逻辑。火电煤耗成本用二次函数线性化近似,每个时段每台火电机组有三段线性化区间;启停成本用0-1状态变量表达;调停补偿成本是我的核心创新点——它跟机组的主动调节深度挂钩,越是强迫机组离开它申报的舒适区间,补偿单价越高。实际操作中我把补偿成本设计成分段线性递增函数:一个厂内没说谎的情况是,同一个调节幅度,主动申报的机组补偿单价便宜,被动拉动的机组补偿单价贵,优化器自然形成"优先调用主动资源"的行为。

2.3 约束条件里的动态边界

约束这块是代码里最容易写错的地方。传统调度里"机组出力大于等于最小出力"是静态的,而计及调停主动性后,上下界变成和决策变量耦合的动态值。

具体做法是:每台机组在每个时段申报一个基础可调区间[P_min_basic, P_max_basic],同时申报一个扩展可调区间[P_min_ext, P_max_ext],两个区间之间用主动调节决策变量来桥接。如果某个时段优化器需要使用超出基础区间的出力,就必须激活调停决策变量,并在目标函数里付出主动调节成本。储能那边还要额外处理SOC转移方程,充电、放电、待机三种状态的逻辑耦合,SOC的上下限约束也要分成"基础区间"和"扩展区间"两层。

这个结构表面上增加了变量数量,实际上给了优化器更大的可行域。传统模型在系统峰谷差拉大时容易无解,而动态边界模型可以通过付出一定调停成本获得可行解,或者明确给出"无法满足最少需要牺牲哪些电源的主动性"这一决策信息。

3. 互补协调模型的完整数学构建

这一段是大多数人看代码以前最头疼的部分。我尽量把数学模型讲清楚,但不堆公式到让人劝退。整套模型最终是一个MILP问题,变量规模取决于调度时段数(我这里用24小时)和电源数量(测试系统用了2台火电、1个风电场、1个光伏电站、3座梯级水电站、1个储能电站)。

3.1 决策变量设计

变量设计是整个Matlab代码的地基,一开始没想清楚后面全乱。我分成五组:

火电机组运行变量:PG(i,t)出力连续变量、u(i,t)启停0-1变量、SU/SD开机停机状态变量、三段线性化辅助变量。注意一个细节:由于调停主动性引入,PG的上下界不是常数,我额外定义了一个辅助变量delta_P(i,t)表示主动调节深度,它和PG通过等式连接。

水电调度变量:PH(j,t)出力变量、水量泄放变量、库容变量。梯级水电之间还有上下游的水量延迟耦合,这个用时间平移矩阵处理,代码里是个稀疏矩阵乘法。

新能源出力变量:风电光伏的出力计划变量和弃电变量。弃电变量其实也是调停主动性的体现——允许弃风弃光,但弃电量计入目标函数的惩罚成本。

储能变量:充放电功率、二进制充放电状态变量、SOC状态变量。核心约束是同一时段不能同时充放电,这个用Big-M约束和二进制变量实现。

主动性决策变量:每个电源的主动调节深度变量和越界松弛变量。松弛变量的系数必须设置成明显高于其他成本项,否则优化器会滥用它来偷懒取巧。

3.2 核心约束条件的逐条拆解

功率平衡约束是最基本的,所有电源出力加总等于负荷预测值,常规操作。重点注意两个细节:一是如果系统没有考虑网络拓扑(即单节点模型),就必须加旋转备用约束来弥补忽视线路潮流的缺陷;二是功率平衡约束里的新能源出力用的是调度计划值,不是预测值,弃风弃光量通过等式约束差值体现。

机组技术约束包括出力上下限、爬坡约束和最小启停时间。由于主动性的引入,上下限约束不是简单的P_min ≤ PG ≤ P_max,而是P_min_basic + delta_P_min ≤ PG ≤ P_max_basic + delta_P_max。爬坡约束是跨时段耦合的,代码处理时要特别注意首时段没有前值,需要单独初始化。

水电约束是这套模型里最容易被轻视的部分。除了出力约束和泄放约束,还要处理库容动态平衡:V(t+1) = V(t) + 入水 - 泄放 - 发电用水。梯级水电站的约束包含上游泄放延迟,这里我假设水流延迟时间为1小时,所以用上游t时段的泄放量影响下游t+1时段的入水,这个逻辑在矩阵生成里用一步移位操作即可。

储能约束重点关注SOC转移:SOC(t+1) = SOC(t) + 充电效率*充电功率 - 放电/放电效率。充放电不能同时进行的约束用u_ch + u_dis ≤ 1实现,SOC上下限结合主动性扩展区间动态调整。还有一个隐藏约束是储能终态SOC要回到初始值附近,否则24小时调度会"偷吃"电池容量。

3.3 调停主动性的集成方式

主动性集成不在某一类约束内部,而是通过目标函数和动态边界跨域联动。核心逻辑可以用一句话概括:调度模型面对每个电源申报的舒适运行区间,允许突破但必须付费,付费函数设计成越接近边缘越陡。这套机制天然激励电源主动申报更真实的调节意愿——报得太保守,主动调节成本高,调度宁可调用其他电源;报得太激进,又会因为越界频繁产生额外成本。博弈论的味道很浓,但落地到MILP里就是分段线性函数加二进制变量选择,工程量不大但效果显著。

4. Matlab+YALMIP代码实现的两条关键链路

这章直接拆代码。整套实现跑在Matlab R2022b上,求解器用Gurobi 10.0,建模用YALMIP工具箱。如果你用的是CPLEX,接口思路完全一致,只是求解器设置参数名略有差异。我这里提供的代码框架是工程化的研究代码——不是教学演示代码,不会为了可读性牺牲效率,但每一段我都会解释它在模型里对应哪个数学表达式。

4.1 数据模块:把外部数据变成模型参数

代码最开始处理的是基础数据输入。这个模块要区分两类数据:静态参数和时变序列。静态参数包括机组数目、火电煤耗系数、储能容量和效率、水电库容上下限、爬坡率等;时变序列包括24小时负荷预测、风电功率预测、光伏功率预测、来水预测。

我的习惯是把所有数据组织成结构体Data,字段名和论文参数表一一对应。比如Data.Thermal.genCostA/B/C就是火电煤耗二次函数的三项系数,Data.Wind.windForecast就是24小时风电预测序列。这样最大的好处是参数敏感性分析时,只需要修改结构体对应字段,不用动模型代码。

这里有一个实际建议:不要在主脚本里硬编码数据,把数据规整放到一个loadData.m函数里。你的数据来源可能五花八门——CSV表格、Excel、数据库、预测系统的API,统一用函数封装后,模型代码完全不需要关心数据从哪来。

4.2 变量定义:YALMIP里别用循环堆变量

很多人第一次写YALMIP代码的习惯是双层循环建立所有变量,比如用两个for循环创建PG(i,t)。小模型没问题,一旦有了梯级水电和储能的多状态变量,这种代码不仅冗长,而且后面生成约束时索引非常痛苦。

我的做法是用YALMIP的sdpvar和binvar直接生成矩阵变量,一次性定义全部时段和全部机组。关键语法是定义矩阵变量而不是标量循环,比如PG = sdpvar(nThermal, nTime, 'full'),这一行就定义了所有火电机组在所有时段的连续出力变量,后面访问PG(i,t)的时候YALMIP会自动映射到正确位置。

二进制变量的定义同理:uThermal = binvar(nThermal, nTime, 'full')。有朋友问为什么不用binvar的第三个参数'full',因为默认情况下YALMIP会生成方形矩阵变量,如果你定义的维度是2×24但忘了加'full',它会报维度不匹配的错。这个细节让我浪费过半小时,现在写出来免得大家再踩。

4.3 约束生成:矩阵化比逐条添加快一个数量级

约束生成这环节是代码性能的分水岭。两种写法对比鲜明:逐条Constraints = [Constraints, expr1 >= 0]; 在循环里反复拼接,模型规模一大,YALMIP构建时间能拖到几分钟;矩阵化写法是整体生成约束矩阵,一次性concat,构建时间几乎可以忽略。

功率平衡约束的矩阵化写法很典型。负荷向量loadProfile是24×1,所有电源出力加总可以表示为sum(PG, 1) + sum(PH, 1) + sum(PW, 1) + sum(PS, 1) + ...,这条约束写成Constraints = [Constraints, sum(PG,1) + sum(PH,1) + ... == loadProfile']。一行代码搞定48个时段的功率平衡约束(如果考虑调度时段数)。

存储约束里有个非常实用的技巧是处理Big-M约束。储能同时充放电禁止约束是典型的Big-M:P_ch ≤ Mu_ch, P_dis ≤ M(1-u_ch)。在YALMIP里直接写成矩阵化不等式,M取储能最大功率的1.05倍就够,不要取一个巨无霸数字——M太大会导致MILP的松弛问题病态,求解器很吃力。

4.4 目标函数和求解参数:要让求解器知道你要什么

目标函数用sum和cost系数矩阵做内积。这里YALMIP有个很好用的特性:目标函数可以直接写成sum(sum(CostMatrix .* DecisionMatrix))形式,其中CostMatrix是常量矩阵,维度与DecisionMatrix一致。火电煤耗分段线性化的辅助变量也需要加入目标,用一个名次写的两段式线性化即可。

求解设置我习惯放在独立函数里:optimize(Constraints, Objective, sdpsettings('solver', 'gurobi', 'verbose', 2, 'gurobi.TimeLimit', 600, 'gurobi.MIPGap', 0.01, 'gurobi.MIPFocus', 1))。TimeLimit和MIPGap是研究代码里最重要的两个参数——你要在"等一个全局最优"和"接受一个可解释的近似最优"之间做权衡。实测下来,10分钟限制+1%间隙在这个规模的模型上几乎不会损失经济性,但求解时间从数小时缩到几分钟以内,完全够用。

注意:YALMIP的约束等式里,变量之间要显式指定维度方向。上面提到的矩阵变量,YALMIP里每行对应每个时段,每列对应每个机组,方向错误会导致约束维度不匹配,报错信息还不友好。排查时先检查size(Constraints)==期望维度,再检查变量内部方向。

5. 算例实测:三组方案对照说明主动性的价值

建好模型后要做的是验证"计及调停主动性"这个改动真正带来了什么。我设计了一个三组对照测试,每组都在完全相同的负荷预测和新能源出力场景下运行,唯一区别是调停主动性的处理方式。

5.1 测试系统与参数设定

测试系统参数如下:2台火电机组(额定功率分别为100MW和50MW,煤耗系数不同),1个风电场(额定容量80MW,预测曲线带典型峰谷),1个光伏电站(额定60MW,标准日间曲线),3座梯级水电站(库容不同,上游到下游延迟1小时),1个磷酸铁锂储能电站(容量120MWh,最大充放功率30MW,SOC限值10%-90%)。负荷曲线采用典型夏季工作日模式,峰谷差约70MW,新能源渗透率峰值约45%。

三组方案分别为:方案A传统刚性调度(静态出力上下限,无主动性申报);方案B计及调停主动性(动态边界加主动调节成本);方案C强制全额消纳(新能源出力设为预测值且不允许弃电,其他电源被动平衡)。

5.2 结果一:总运行成本对比

方案A总运行成本为418.6万元,方案B为392.4万元,方案C为447.2万元。方案B相对方案A节约约6.3%,这8成的来源是火电避免了被迫深调的高煤耗区间——方案A中火电1在某时段被压到42%额定出力,煤耗率高得离谱;方案B中同样的爬坡深度分配给了水电和储能,火电只在基础区间内运行并用主动调节成本换算反而更优。

方案C成本最高的原因很直观:强制全额消纳让火电在光伏高峰时段被深度压负荷,同一天下午又必须快速爬升,频繁大幅调节的煤耗代价远超弃电惩罚。这个结果和"绿色优先"的直觉相悖,但恰好说明调停主动性的意义——让新能源"适度弃电"有时换来的社会总成本更低。

5.3 结果二:调停意愿满足率与SOC轨迹

这是一个更有意思的对比。统计各电源主动申报的可调区间在最终调度方案中被满足的程度:方案B的总体满足率为91.7%,而方案A为74.6%(因为方案A根本不考虑申报区间)。火电机组的主动性满足率最高,储能其次——原因是模型在目标函数里对违背申报区间设置了惩罚,只有系统确实无法平衡时才会突破它们。

储能SOC曲线差异明显。方案A储能SOC一直在90%高位徘徊,因为调度把它当成纯调节工具,时刻准备放电;方案B储能SOC呈"上午充电、下午放电、夜间恢复"的合理日循环,终态SOC距初始值偏差不到3%,为第二天调度保留了充足的调节空间。方案C储能SOC直接顶到上限,没有任何余量应对突发波动。这个对比中最直观的意义在于:如果调度模型尊重储能的调节意愿,储能可以在更长的时间尺度上发挥价值。

5.4 结果三:求解性能与可扩展性

模型变量总量为864个连续变量和96个0-1变量,约束条件数约1830条。方案B在TimeLimit=600s、MIPGap=1%的设置下,Gurobi求解耗时约147秒即达到稳定间隙;方案A由于少了动态边界和主动性决策变量,求解耗时92秒;方案C求解耗时最短,68秒。此外我还跑了一个扩展场景:机组数增加到10台、考虑线路潮流约束,方案B的求解时间升到409秒,MIPGap还是能压到1%以内。对研究或离线规划场景来说,这个性能完全够用。

6. 我在复现和调参过程中踩过的四个坑

最后聊点代码以外的东西。这个模型从一开始的粗暴实现到能稳定出结果的版本,前前后后花了不少时间,其中四个坑最值得拿出来说,都是那种"看起来报错莫名其妙、实际原因非常简单"的类型。

6.1 坑一:SOC范围越界导致无解,不是模型错是初值错

第一次跑完整24小时模型时,求解器一直报infeasible,我怀疑是某条约束写错了。排查了两小时,最后定位到储能SOC初值设成了50%,但模型第一时段就要求充电到80%,而储能充电功率上限只有30MW,1MWh的容量塞不下这个电量差距。修正方式是把SOC初值设为当前实际状态,同时把SOC方程里加一个可调小松弛变量,避免因初值偏差导致整体无解。这个小松弛变量只在首时段激活,正常运行时几乎为零,不影响最终方案。

6.2 坑二:Big-M取值过大导致MILP求解缓慢

储能同时充放电约束常用Big-M,刚开始我图省事设M=1e6,结果Gurobi每次迭代都要处理病态的约束矩阵,求解时间从几分钟膨胀到一小时都没闭合。后来改成M=最大充放功率×1.1,加上额外的逻辑约束,求解时间直接压缩到原来的三分之一。这个教训很通用——Big-M不是越大越好,够用就行,过大会让松驰数值不稳定。

6.3 坑三:爬坡约束的首时段和末时段处理

爬坡约束是跨时段约束,但首时段没有前值,末时段不需要考虑下一时段。如果简单地用循环生成PG(t+1)-PG(t) ≤ ramp那么t等于末时段时索引就越界。我的处理方式是单独生成一组首时段约束,主循环只跑2到24时段。还有一个隐患是:机组启动时段的出力爬坡往往比正常爬坡快,论文里会给启动爬坡速率不同数值,代码如果忽略这个细节,模型会过度约束启停行为。

6.4 坑四:目标函数量纲不匹配导致的最优解失真

火电煤耗成本是元,主动调节补偿是元,但弃风弃光惩罚如果用"元/MWh"而储能折损用"元/次",量纲虽然都是元但数值尺度差好几个数量级,优化器会被小量纲项带偏。我的做法是把所有成本项统一归一到"万元"量纲:煤耗成本系数除以10000、惩罚系数除以10等,并保证任何惩罚项单价都高于正常最贵的调度方式单价,否则模型会倾向于用惩罚替代实际调节。调参与求解前先运行敏感性分析脚本,确认目标函数里各项占比在合理区间,再跑正式结果。

这四个坑排掉之后,模型基本属于"一次搭好、参数常调"的稳定状态。现在的代码框架也支持继续扩展碳交易成本、需求响应、线路潮流安全约束等进阶模块,只要在目标函数和约束生成模块各自加一段,复用性很好。如果你准备在论文或项目里用这套模型,建议先从单节点版本跑通,再逐步加复杂度;公式推导部分参考文献里关于主动配电网与综合能源系统的建模思路,可以省很多验证时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询