尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

计及N-k安全约束的含光热电站优化调度Matlab实现

计及N-k安全约束的含光热电站优化调度Matlab实现 做电力系统优化调度的朋友这几年一定没少被“含光热电站”和“N-k安全约束”这两个词刷屏。光热电站和光伏不一样它带储热系统可以把中午的光照能量挪到晚上用这让它从一个标准的“新能源”变成一个具备可调度性的电源。而N-k安全约束是从传统N-1安全校验升级来的更严苛要求不只校核单重故障还要考虑任意k个元件同时退出后的系统安全性。把这两者放进同一个优化调度模型再分别用IEEE14节点和IEEE118节点做算例是很多研究生和一线调度算法工程师的日常任务。我最近从头到尾复现了这样一套Matlab代码从目标函数搭建、光热电站运行约束、N-k预想故障集生成到求解器的调用和结果处理完整走了一遍。这篇内容就详细拆一下我的建模思路和编码结构以及在14节点和118节点上踩过的坑希望能给正在复现类似工作的朋友节省几天时间。光热电站本身不是新技术但和N-k安全约束放一起模型复杂度和工程实用性都上来了。很多人一开始只做了简单的经济调度把光热当成一个可平移出力的电源忽略故障场景下的安全校验结果评审一提问就露馅。另一部分人把N-k功能加上去之后用的是全枚举在118节点上根本跑不动。这类问题不是换一个求解器就能解决的而是从建模开始就要想清楚哪些能线性化、哪些必须用场景集、哪些可以迭代筛选。下面按我实际开发的顺序来讲。1. 为什么这个题目值得做光热电站与N-k约束的“化学反应”1.1 光热电站从“靠天吃饭”到“大号热水瓶”光热电站Concentrating Solar PlantCSP和光伏、风电有一个本质区别它有一个中间传热储热环节。一般来说光热电站由聚光集热系统、储热系统和汽轮机发电系统组成。白天太阳辐照充足时定日镜或槽式集热器把太阳能转换成热能一部分热能直接送去发电另一部分储存在储热罐里的熔盐或导热油中晚上或者云层遮挡时再抽出储热来驱动汽轮机。这个“热量搬家”的能力让它不再完全依赖瞬时光照可以在调度计划里像火电一样调整出力曲线。如果你只把光热电站简单处理成“一个出力上下限固定的机组”那就丢掉了一大半的价值。在优化调度模型中光热电站的核心约束是一组时间耦合的储热动态方程通俗点说就是每个时段储热罐里的“热量余额”会随着充热和放热而增减。这个约束把不同时段的决策变量串在一起让整个优化问题从单时段静态问题变成24小时联动问题。这也是为什么很多人一开始会把SOC写错导致无解或者结果畸形。1.2 N-k安全约束从N-1惯性到组合爆炸的焦虑传统电力系统运行中最常用的是N-1准则也就是任意一个线路、变压器或发电机故障退出后系统还能保持安全稳定运行。这个准则是几十年的工程经验沉淀计算量也可控。但现实世界并不总是按N-1出牌极端灾害天气可能让多条线路在同一时段故障设备和通信连锁故障也可能导致多重元件退出。因此研究N-k安全约束尤其是k2甚至更高逐渐成为学术界和部分工程验证的热点。N-k约束放进优化调度后第一个冲击就是场景数量爆炸。以IEEE118节点算例为例光线路就有186条如果做N-2故障枚举光线路组合就超过17000种再加上发电机故障、线路与发电机混合故障组合数轻松到几万。如果在每个故障场景下面都复刻一份完整的潮流约束这个优化模型就算你用的是Gurobi也可能在几小时内拿不到可行解。所以凡是能真正跑通的N-k调度模型基本都用到了故障场景筛选、关键故障集识别或迭代求解的思路而不是盲目全枚举。这一点后面我会专门讲。1.3 为什么选IEEE14和118节点作为测试床IEEE14节点系统是经典的教学算例支路数少发电机台数少适合验证模型逻辑。你可以在14节点上把光热电站模型、N-k场景约束、目标函数权重全部调试正确再迁移到118节点上做规模测试。IEEE118节点系统则更接近实际电网的复杂度节点多、回路多、机组类型丰富是检验N-k算法性能的好场地。两个算例配合使用正好覆盖了“逻辑正确性”和“规模化可行性”两个层次。很多论文里默认把这两个系统当作“可再生能源消纳”和“安全约束优化调度”的标准测试平台数据和参数都容易获取复现结果也可以和公开文献对比。如果你还在纠结用更大的系统我建议先别急28节点或者300节点系统会引入更多噪声调试起来会让你怀疑人生。2. 数学模型把光热电站和N-k约束写成能让求解器听懂的话2.1 目标函数与基础运行约束我实现的模型面向24小时日前调度时间步长取1小时。目标函数是最小化总运行成本主要包括三部分常规火电机组的燃料成本、光热电站的运行维护成本以及两个惩罚项——切负荷惩罚和弃光弃热惩罚用公式写成min Σ_t [ Σ_g (a_g * P_g(t)^2 b_g * P_g(t) c_g) M_OM * P_csp(t) λ_shed * Σ_d LS_d(t) λ_spill * Spill(t) ]其中P_g(t)是常规机组g在时段t的出力P_csp(t)是光热电站的发电出力LS_d(t)是节点d的切负荷量Spill(t)是光热电站的弃热量。切负荷惩罚系数λ_shed必须设得足够大一般要远大于一个机组的单位发电成本否则求解器会偷偷用切负荷来代替昂贵的机组出力得到“最便宜但不可用”的方案。基础约束首先是全网功率平衡这是最朴素的一条所有机组出力、光热出力、其他可再生能源出力如果有风电我会做简化处理之和加上系统从外部的注入必须等于总负荷需求减去切负荷量。其次是常规机组出力上下限和爬坡约束以及线路传输容量约束。正常算例下不加入N-k约束时这个基础模型只是一个普通的直流最优潮流加机组组合问题运行时间很短。2.2 光热电站的储热、出力与导热循环约束光热电站建模是整个模型的亮点也是很多人容易出错的地方。我采用一个简化但物理合理的模型把光热电站分成三个环节光场热量收集环节、储热罐储放热环节、蒸汽发电环节。光场收集到的热功率Q_sf(t)由太阳能直接辐射DNI(t)、光场采光面积A和光场效率η_sf决定Q_sf(t) η_sf * A * DNI(t)这个Q_sf(t)在每次调度时是预测好的已知量。光场的热量有三个去向一部分流入储热罐充电一部分直接进入发电环节多余的按弃热处理关系如下Q_sf(t) Q_ch(t) Q_pb_in(t) Q_spill(t)其中Q_ch(t)是储热罐的充热功率Q_pb_in(t)是进入蒸汽发生器的热功率Q_spill(t)是弃热功率。储热罐的动态约束可以写成SOC(t1) SOC(t) η_ch * Q_ch(t) - Q_dis(t) / η_dis - Q_loss(t)其中SOC(t)是储热罐在时段t开始时的储热量Q_dis(t)是放热功率η_ch和η_dis分别是充放热效率。储热量要满足上下限约束充放热功率也要满足最大充放速率限制。实际代码里我还会加一条“充热和放热不能同时发生”的约束通过一个二进制变量实现。如果不加这条约束目标函数会在充放效率不等的情况下钻空子比如同时充放热白白套利。发电环节相对简单光热电站的电出力P_csp(t)等于进入发电环节的热功率乘以热电转换效率η_pb并受到最大发电容量的限制。还需要注意储热罐的SOC初始值和结束值要设置成同一个数我这个模型里取50%的储热容量代表一天调度周期内热量进出相对平衡不会出现“白拿前一天的热量来压低本日成本”的作弊行为。2.3 预防型N-k安全约束的数学表达与松弛处理N-k安全约束在调度模型里通常分成两大流派预防型preventive和校正型corrective。预防型要求基态调度方案在所有预想故障场景下都直接可行不依赖故障后的重新调度校正型则允许故障发生后再调整一部分机组出力只要调整量在可接受范围内。我目前实现的是预防型因为它的决策变量少更容易让模型收敛并且在14节点和118节点之间迁移时逻辑更稳定。预防型N-k约束的数学表达式很直观对每一个预想故障场景s把故障对应的线路、发电机从系统模型中移除然后要求故障场景下的直流潮流方程、节点功率平衡、支路潮流上限、发电机出力上下限都仍然满足。关键点是故障场景下和基态共用同一组机组出力决策变量也就是说机组在故障前后的出力指令不改变系统依靠网络拓扑的调整和自然潮流分布来满足安全要求。这里必须引入一个危险场景下的松弛处理如果对每个故障场景都要求所有节点不切负荷很可能因为N-k故障后系统解列成孤岛而出现无解。为避免这种问题我给每个故障场景的每个节点都加了一个切负荷变量LS_s(t)并在目标函数里用大罚因子约束它尽量为0。这样模型不会因为个别极端场景直接报“不可行”而是会在满足绝大多数安全约束的前提下把极端故障场景下的切负荷量压缩到最小。3. 从理论到Matlab数据准备与N-k场景生成3.1 用Matpower读取IEEE14和118节点数据做优化调度我强烈建议别自己手搓节点数据直接用Matpower工具箱加载标准测试系统。Matlab环境里装好Matpower后读取数据非常方便define_constants; mpc14 loadcase(case14); % IEEE14节点 mpc118 loadcase(case118); % IEEE118节点 baseMVA mpc14.baseMVA; bus14 mpc14.bus; gen14 mpc14.gen; branch14 mpc14.branch;Matpower的bus矩阵里存了节点编号、负荷有功无功、电压幅值等gen矩阵里存了发电机节点、有功出力上限、无功上限、成本系数等branch矩阵里存了线路/变压器的起始节点、终止节点、电阻、电抗、充电电容、长期载流容量等。这些列的顺序是有讲究的建议直接用define_constants里定义好的常数列索引比如branch(:, F_BUS)、branch(:, T_BUS)、branch(:, RATE_A)不要凭感觉硬编码列号否则换一个算例就全乱了。我做光热接入时不是在14节点和118节点外新增母线而是把一台常规火电机组替换成光热电站。比如在14节点系统中母线4原有一台机组我把它替换为额定功率相同的光热机组同时把原机组的燃料成本改成一个很小的维护成本。这样做的好处是网络拓扑不需要大改光热电站的并网节点已经存在直流潮流的节点导纳矩阵也不受影响。3.2 光热电站参数与光照数据的“人为设定”要小心标准算例里没有光热电站参数只能自己设定。我会参考公开文献和现实中光热电站的设计给出一组典型参数光热装机容量50MW储热时长6小时光场采光面积按聚光倍数确定储热罐最大储热量为300 MWh充放热效率均为0.95热电转换效率0.42。下面是我在代码初始化中常用的一组数据参数数值单位额定电功率50MW储热容量300MWh最大充热功率80MWth最大放热功率80MWth充热效率0.95-放热效率0.95-热电转换效率0.42-储热初始SOC50%-DNI曲线我会用一个带波动的单峰曲线模拟晴天日出、正午最强、日落为零的过程。如果你有实测的DNI数据可以直接替换。这里提醒一句DNI数据的时间分辨率和调度步长必须一致否则会出现“每小时综合DNI”和“每小时瞬时DNI”混淆的问题导致光场产热量和预期差一大截。3.3 故障场景集的生成策略与去重技巧N-k场景集的生成是整个流程里最需要打磨的地方。对于N-1直接遍历所有支路和所有发电机逐个设置故障即可。对于N-2不能一上来就全组合。我的做法是先做一次预筛选以基态调度结果为基础枚举所有单个支路和单个发电机故障快速计算直流潮流找出会导致线路越限的“严重故障”集合。对严重故障集合做两两组合作为N-2候选场景。对候选N-2场景再快速计算潮流剔除不会造成新增越限的冗余场景。用这种方法在118节点上可以把N-2场景量从一万多压缩到几百个模型规模立刻降下来。场景去重也很重要因为同一组元件故障可能在网络矩阵中形成同一种拓扑不同的组合可能产生相同的B矩阵会造成冗余约束。我一般在生成场景时计算一个字符串指纹比如把故障支路编号排序后拼成散列值用一个containers.Map去重。4. 核心代码实现与求解器选型4.1 模块化代码结构先跑通再优化这套Matlab代码我建议拆成几个清晰的模块不要在脚本里写2000行。我的目录结构大概是main_14bus.m % 14节点主入口 main_118bus.m % 118节点主入口 load_csp_data.m % 读取光热电站参数和DNI曲线 build_scenarios.m % 生成并筛选N-k故障场景 build_scof_model.m % 用YALMIP构建优化模型 solve_and_plot.m % 求解并绘制外送功率、SOC等曲线模块化的好处是14节点调试时你只需要改main_14bus.m里的系统名称和光热节点位置模型构建函数完全复用。我不建议在代码里到处写死节点数而是用size(bus, 1)、size(branch, 1)、size(gen, 1)来动态获取。这样一来切换到118节点时只要保证对应矩阵存在就不会出现因为常量写死而导致的越界报错。4.2 YALMIP建模关键约束代码片段我用YALMIP建模求解器用Gurobi或Cplex。决策变量主要分三类基态机组出力、光热电站相关变量、每个故障场景下的节点相角变量和切负荷变量。下面挑核心约束展示一下代码结构。先定义基态变量T 24; nGen size(gen, 1); nBus size(bus, 1); nBranch size(branch, 1); P_g sdpvar(nGen, T, full); % 常规机组出力 P_csp sdpvar(1, T, full); % 光热电站电出力 SOC sdpvar(1, T1, full); % 储热罐SOC状态 Q_ch sdpvar(1, T, full); % 充热功率 Q_dis sdpvar(1, T, full); % 放热功率 Q_spill sdpvar(1, T, full); % 弃热功率 u_csp binvar(1, T, full); % 充放热互斥标志基态功率平衡约束可以直接用矩阵形式表达。常规机组在母线gen(: , GEN_BUS)上光热机组在光热并网节点上其他节点负荷从bus矩阵里读取于是C []; P_inj zeros(nBus, T); for i 1:nGen P_inj(gen(i, GEN_BUS), :) P_inj(gen(i, GEN_BUS), :) P_g(i, :); end P_inj(cspBus, :) P_inj(cspBus, :) P_csp; P_inj P_inj - loadBusPower; % 负荷维数为nBus*T theta_base sdpvar(nBus, T, full); C [C, B0 * theta_base P_inj]; C [C, theta_base(refBus, :) 0];其中B0是由支路电抗算出的直流潮流节点导纳矩阵可以调用Matpower的makeBdc生成。为了防止求解器出现“孤岛虚警”我会在直流潮流矩阵中把参考节点所在行固定让所有相角都以参考节点为基准。光热电站的SOC约束写成循环因为每一时段都用同一个递推关系C [C, SOC(1) 0.5 * S_max, SOC(T1) 0.5 * S_max]; for t 1:T C [C, SOC(t1) SOC(t) eta_ch * Q_ch(t) - Q_dis(t) / eta_dis]; C [C, 0 SOC(t1) S_max]; C [C, 0 Q_ch(t) Q_ch_max * u_csp(t)]; C [C, 0 Q_dis(t) Q_dis_max * (1 - u_csp(t))]; C [C, P_csp(t) eta_pb * Q_pb_in(t)]; C [C, Q_sf(t) Q_ch(t) Q_pb_in(t) Q_spill(t)]; C [C, P_csp(t) P_csp_max]; end这里u_csp(t)是二进制变量为1表示充热为0表示放热。很多初学的人会忘记这个互斥约束最终结果里充放热同时为正看起来SOC变化不大实际物理上是不可能的而且效率损失很容易被目标函数利用。N-k场景约束的建模稍微绕一点。每个场景s要有一套节点相角变量theta_s共享基态的P_g和P_csp但使用故障后的B阵Bs{s}for s 1:nScen theta_s{s} sdpvar(nBus, T, full); shed_s{s} sdpvar(nBus, T, full); % 切负荷变量 C [C, Bs{s} * theta_s{s} P_inj_after_cut(s)]; C [C, 0 shed_s{s} loadBusPower]; C [C, 0 theta_s{s}(refBus, :) 0]; % 参考节点相角为0 % 支路潮流约束需要按故障场景的可用支路重新判断 for l 1:size(availBranch{s}, 1) fbus availBranch{s}(l, F_BUS); tbus availBranch{s}(l, T_BUS); Pl (theta_s{s}(fbus, :) - theta_s{s}(tbus, :)) / availBranch{s}(l, X); C [C, -availBranch{s}(l, RATE_A) / baseMVA Pl availBranch{s}(l, RATE_A) / baseMVA]; end end注意这里我故意把故障后节点注入功率写成P_inj_after_cut(s)是因为场景不同某些机组故障退出后相应节点的注入功率就不是P_g了需要把故障机组的出力归零同时把切负荷变量加入功率平衡方程。如果切负荷变量只出现在目标函数里而没有进入直流潮流方程那它只是一句空话起不到松弛作用。4.3 从14节点跑到118节点性能瓶颈与改进措施IEEE14节点下N-1场景加上几个筛选出的N-2场景总场景数最多几十个基础模型加上场景约束后节点规模不大Gurobi一两秒就能求出最优解。到了IEEE118节点场景几百个每个场景有一组相角变量和几十条支路约束模型会膨胀到几十万行约束此时直接丢给求解器很容易内存溢出。我的解决思路是先用14节点调试好模型然后在118节点上做两件事。第一把YALMIP的求解选项打开设置gurobi时使用timelimit和mipgap例如ops sdpsettings(solver, gurobi, verbose, 2, ... gurobi.MIPGap, 0.01, gurobi.TimeLimit, 600);第二采用迭代安全约束方法先求解不包含N-k场景的基态模型然后用这个解对所有筛选后的N-k场景做一次性潮流校验找出越限场景把这些场景作为新的约束加入模型再次求解。重复这个过程直到没有新增越限场景。这本质上是将大批量N-k约束延迟到需要时才生成能够极大降低单次求解规模。实测118节点下通常迭代3到5轮就能覆盖所有关键场景总耗时从全枚举的几小时降到十几分钟。5. 复现过程踩坑记录从不可行解到场景爆炸5.1 故障场景无解别迷信“大M”直接切负荷松弛我最初第一版代码没有加切负荷变量结果任何一个发电机故障场景都会导致系统功率缺额因为其他机组受到爬坡和出力上限限制无法完全弥补。模型直接报“infeasible”我还以为是场景生成错了排查了很久。后来在故障场景里引入切负荷变量并把罚因子设成每兆瓦时8000元问题才解决。这里要强调罚因子不是越大越好。我试过设成10^7量级模型数值稳定性变差求解器容易出现数值警告甚至解出来完全不可行。通常取所有机组边际成本的10到50倍就可以了比如火电边际成本在200到400元/MWh罚因子取5000到10000元/MWh比较合理。5.2 光热电站SOC建模的两个低级错误第一个错误是漏了时间步长。很多样例代码里把SOC递推写成SOC(t1) SOC(t) Q_ch(t) - Q_dis(t)如果调度步长是1小时这样没问题但当我改成半小时步长时忘了乘Δt能量守恒就完全乱套。我建议代码里统一用一个全局变量DeltaT所有功率乘时间步长转换成能量避免以后换步长时改漏。第二个错误是初始SOC和末尾SOC设置。如果不设置SOC(T1)等于某个固定值求解器会选择在第一个时段就把储热罐放空或者在最后一个时段疯狂充热只为了让目标函数更小而实际运行完全不可行。我在第一版里没约束末尾SOC结果光热电站夜间出力几乎为零储热罐被“白嫖”了一整天第二天根本没有可用热量。后来强制SOC(1)SOC(T1)50% S_max才得到真正合理的调度结果。5.3 118节点N-2场景爆炸全枚举是最后手段不是第一手段最初我试图在118节点上把所有N-2线路场景都加进去YALMIP建表达式建了快10分钟求解器分配内存直接卡死。后来改成“先N-1预筛再关键N-2组合”场景量从17000个降到了几百个。这里还要注意筛选出来的场景数量取决于基态运行点运行点一变原来的“非关键场景”可能变得关键。所以迭代法必须循环到不再产生新场景才算收敛不能只做一次筛选就结束。迭代法的伪代码我写在下面方便移植1. 初始化 scenario_set {运行状态无故障} 2. 求解含 scenario_set 的优化调度模型得到基态机组出力 P_g 3. 对候选故障集合中所有未纳入 scenario_set 的场景 用 P_g 计算直流潮流检查支路越限量 4. 如果没有任何场景越限则停止 5. 否则把所有越限场景加入 scenario_set回到第2步这个方法在数学上是一种切平面法虽然不能百分之百证明覆盖所有N-k场景但在工程上非常有效很多文献里也是这样处理的。如果要做严格数学保证可以参考“安全约束生成算法”或“Benders分解”的收敛性分析。5.4 数值稳定性参考节点、单位、矩阵奇异直流潮流矩阵B如果遇到故障把系统分裂成孤岛会直接奇异求解器报NaN。我的处理方式是生成故障场景时先做连通性检查把导致孤岛的场景剔除或者合并处理。对于每个场景我还会设置参考节点相角为0否则无限多组解会让求解器头疼。另外虽然Matpower里用p.u.值但储热罐的SOC我用MWh发电出力用MW时间步长为小时这两者之间的换算关系是能量功率×时间。单位混用最容易导致约束尺度差几个数量级YALMIP虽然能处理但数值问题会在118节点这种规模下被放大。6. 结果分析N-k约束是如何改变调度方案的6.1 加不加N-k约束调度结果差别很明显以14节点为例不加N-k约束时系统让光热电站在夜间满发完全替代了部分高成本火电总成本最低。加了N-1约束后为了保证某些关键线路故障后系统不越限光热电站在部分时段的出力必须压低同时预留一定火电旋转备用。到了N-2约束形势更紧张个别时段光热电站甚至停止出力给火电让路。算下来N-2约束比N-1约束增加约5%到8%的总运行成本但显著降低了故障后的切负荷风险。这是一个很有说服力的结果能在图表里直观展示成本和安全裕度之间的权衡。6.2 光热电站在N-k场景中的特殊价值光热电站相比普通火电在故障场景下有一个优势储热系统可以让它在短时间内增大出力或者减少出力相当于一个天然的可再生备用容量。但由于储热容量有限如果基态把储热全部放空故障时就无法增加出力。所以N-k安全约束会强迫调度把储热SOC维持在一个较高的水平比如在下午时段不把热量全用来发电而是留一部分给傍晚故障概率较高的时段。从结果曲线看带N-k约束后的SOC曲线会比不带约束的更“平缓”中后段SOC保持在高位这就是安全约束在“看不见的地方”起作用。6.3 后续扩展的几个方向这套14/118节点代码跑通之后有很多方向可以继续扩展。一是把直流潮流升级为交流潮流N-k约束里加入无功和电压约束但这会变成非线性或二阶锥问题求解难度大幅提升。二是考虑风光预测不确定性把N-k场景和随机概率场景放在一个两阶段鲁棒优化框架里。三是引入校正型安全约束允许故障后重新调度机组这样经济性更好但需要的场景变量更多可以配合Benders分解来做。从我个人的实际体会来说复现这类“计及N-k安全约束的含光热电站优化调度模型”最重要的不是急着调求解器参数而是先把小算例的模型逻辑吃透。14节点系统价值远远大于一开始就扑到118节点系统上因为你能很快看到SOC曲线、机组出力和线路潮流判断到底哪个约束在起作用。等14节点跑通并验证完毕再切到118节点你会发现很多坑都已经提前填平了。最后再分享一个小技巧每次修改模型参数后先用sdpvar的value函数把关键变量的解打印出来画成曲线贴到脚本上方一旦结果异常一眼就能发现是储热过程电出力跳变还是故障场景约束把光热电站压制住了。这套调试流程比盯着求解器日志里的目标函数值要高效得多。
返回列表