
做电力系统优化调度的人都知道调度模型难的不是机组组合本身而是怎么在“经济性”和“安全性”之间找平衡。这两年光热电站CSP因为自带大容量储热成了新能源领域的热门研究对象——白天吸热、晚上放热等于给电网提供了一个可调度的“太阳能蓄电池”。但储热罐一加进来模型里就多了一堆非线性和时间耦合约束再叠加N-k安全约束问题规模更是直线上升。我在实际项目中用Matlab把这两块放在一起做了实现在IEEE14节点和118节点标准算例上跑通了完整模型今天把思路、建模和代码中的细节整理出来给同样在做这个方向的朋友做个参考。这个模型能解决什么问题一句话在给定光热电站的出力特性和储热约束前提下通过优化各机组出力让系统满足任意N-k预想故障下的安全运行要求同时总发电成本最小。适合刚接触光热调度、或者想把安全约束加进现有优化框架的硕士生、工程师和研究人员阅读。1. 项目为什么这么做背景与核心问题拆解1.1 光热电站给调度模型带来哪些新变量光热和光伏最大的区别就是带不带“储能”。光伏是光转电直出直用夜间出力为零光热电站有集热场、储热系统TES和汽轮发电机组三大部分太阳辐照首先转化为热能存到储热罐里再用热来驱动汽轮机发电。这个结构带来的直接好处是——发电和集热可以解耦。调度员关注的其实是“出力可以根据调度指令来调”这一点。常规火电虽然能调但爬坡慢、启停贵光热电站只要储热罐里有足够的能量出力调节速度比火电快得多。但代价是建模复杂度上来了集热场输出热功率受辐照影响不是一个恒定值储热罐有容量上限、充放热速率限制还有热损失率汽轮发电机有最小技术出力、爬坡速率限制集热、储热、发电三部分之间存在时序耦合前一个时段充了多少热直接决定后面几个时段能发多少电如果只是按“光资源曲线直接对应出力”来处理那等于把光热电站当成普通光伏完全没发挥出它的调峰价值模型结果也必然是偏保守或者失真的。1.2 N-k安全约束到底在约束什么学过电力系统分析的都知道N-1准则系统里任意一条线路或一台变压器退出运行系统仍然能保持稳定运行。N-k是N-1的推广指的是同时有k个元件发生故障时系统仍能安全运行。反映到调度模型里就是在调度方案生成的时候必须对每一个预想故障场景进行安全性校验——故障后的系统潮流不能越限节点电压不越限发电机在故障后的出力调整不能导致切负荷。直接对所有故障场景做约束数学上就是枚举约束叠加每个场景要求系统潮流平衡线路潮流不越限。对14节点系统来说N-1要枚举的线路不算多但118节点系统的线路有180多条如果再考虑N-2场景数会到几千甚至上万。这也直接决定了模型的求解规模我在后面第3章会详细展开怎么处理这个问题。注意一个容易被忽略的点N-k故障通常不仅影响拓扑还影响发电机是否被切掉。如果故障恰好发生在发电机出口那这台机组在故障场景里就是直接退出的这个信息必须在约束里体现否则算出来的调度方案在故障场景下是不可行的。1.3 为什么选IEEE14节点和118节点做验证一方面是因为标准。IEEE节点系统是学术和工程界通用的测试算例有公开的线路参数、发电机数据和负荷分布结果可复现、可对比。IEEE14节点适合做模型验证规模小方便调试代码和检查逻辑IEEE118节点是中型系统含几十台发电机、上百条线路是检验算法效率和模型可扩展性的标准平台。另一方面是场景覆盖。14节点系统里每条线路的变化都很敏感一个约束写错结果立刻异常118节点系统则能暴露出大规模求解中的数值问题、求解时间过长和内存爆炸等问题。两个系统配合起来既能验证“模型正确”又能验证“求解高效”。2. 光热电站储热与N-k安全约束的建模方法2.1 光热电站能量流与三组核心约束我在建模时把光热电站分成三个环节来写约束集热环节、储热环节和发电环节。集热环节给定预测的太阳直射辐射DNI和集热场面积可以算出某一时段集热场收集到的热功率。实际项目中直接采用预测数据即可模型中一般把它当做已知参数。这里有个细节集热场可能还带一个小的电加热装置电加热能够把多余的电能转化为热能存储有利于促进新能源消纳但这个要看具体研究场景不是所有模型都必须加。如果加了电加热环节就多了一组“电到热”的能量流入约束目标函数里还要加上对应的运行成本。储热环节的核心约束是储热罐能量平衡[ E_t^{TES} (1 - \eta_{loss}) E_{t-1}^{TES} (\eta_{ch} q_{ch,t} - \frac{q_{dch,t}}{\eta_{dch}}) \Delta t ]其中( E_t^{TES} ) 是t时段末储热罐热能储量MWh( \eta_{loss} ) 是热损失率( q_{ch,t} )、( q_{dch,t} ) 分别是充热功率和放热功率( \eta_{ch} )、( \eta_{dch} ) 是充放热效率同时还要有容量约束和充放速率约束( E_{\min} \le E_t^{TES} \le E_{\max} )( 0 \le q_{ch,t} \le q_{ch,\max} )( 0 \le q_{dch,t} \le q_{dch,\max} )前一时段的储热量决定了本时段发电环节最大可用的热功率这就在时间维度上引入了耦合。这一点是光热建模最关键、也最容易写错的地方。发电环节汽轮发电机输出电功率 ( P_t^{CSP} ) 要满足[ P_t^{CSP} q_{ch,t} \le q_{solar,t} q_{dch,t} ]即本时段集热场接收的热量加上储热罐释放的热量一部分用来发电一部分用来充热。汽轮机自身还有最小出力约束和爬坡约束。另外一般要求调度周期末段储热罐的储热量不低于初始值这算是一个“调度日完整性”约束保证模型不会把储热罐掏空来降低成本。注意单位一致性是这个模型里最容易翻车的点。所有与能量相关的量统一使用MWh功率使用MW时间步长使用小时并且在代码文件头做注释说明。我踩过单位错位的坑结果所有结果看起来都合理实际上全错了。2.2 N-k故障场景如何进入优化模型N-k安全约束的实现我采用的是场景枚举 约束叠加方案这也是目前最直接、最稳妥的做法。具体来说先根据线路/发电机数据生成预想故障集 ( \Omega_{N-k} )每个故障场景s对应一套网络拓扑。然后对每个场景s都建立一套直流潮流方程[ \mathbf{B}_s \boldsymbol{\theta}_s \mathbf{P}_s^{inj} ][ -P_l^{\max} \le \frac{\theta_a - \theta_b}{x_l} \le P_l^{\max} ]其中 ( \mathbf{B}_s ) 是故障场景s下的节点导纳矩阵去掉故障支路( \mathbf{P}_s^{inj} ) 是场景下的节点注入功率。注意发电机出力变量在不同场景下可以重新调整——这是“校正型”安全约束调度SCOPF的思路。也就是说正常工况下的机组出力是一组变量故障场景下机组可以在一定范围内重新调整出力来保证安全。这样比较贴合实际因为调度员看到故障后确实会进行再调度而不是把正常工况的出力原封不动套到故障场景里。这里有一个建模技巧故障场景下的再调度量和正常工况出力之间的差值通常会加一个变化范围限制校正量上限防止模型算出“故障后瞬间把所有机组出力调到极端值”这种物理上不合理的方案。我在代码里把校正量上限设为各机组爬坡能力的若干倍并折算到15分钟或1小时的再调度时间窗内。还有一个容易踩坑的地方故障场景里如果切掉的是发电机所在支路那么该发电机的出力变量应该被置零而不是让约束自动去平衡。很多初学者漏掉这个处理结果故障场景给出的潮流解在拓扑上根本不成立。2.3 目标函数与整体优化框架设计目标函数我采用的是常规的发电成本最小化[ \min \sum_{t} \left[ \sum_{g \in G} (a_g P_{g,t}^2 b_g P_{g,t} c_g) \sum_{csp} \lambda_{csp} P_{t}^{CSP} \right] ]其中CSP机组的成本项取值需要特别说明。光热电站的“燃料”是免费的太阳能所以如果直接按常规机组成本系数套结果必然是全功率发电这显然不合理。我在代码中对光热电站在目标函数里赋予了运行维护成本和一定的“调节成本”让它既能参与调度又不会因为零燃料成本而扭曲机组间出力分配。具体取多少要看研究的政策意图如果研究的是促进新能源消纳成本系数可以设得很小如果研究的是光热电站参与竞争性电力市场那就要按投标成本来设。整体上这是一个混合整数线性规划MILP问题机组启停是0/1变量储能充放为连续变量线路潮流是线性方程。如果只做经济调度不考虑启停就是纯线性规划LP。我实现的版本为了控制复杂度在IEEE14和118节点上均采用不计启停的直流潮流经济调度把机组组合变量简化掉重点验证光热储热和N-k安全约束的联合优化效果。需要做多时段机组组合迭代的时候则可以在这个框架基础上再套一层拉格朗日松弛或Benders分解后面第5章会提到。为什么不直接用交流潮流因为N-k场景数量巨大交流潮流的非线性会让MILP问题变成MINLP求解难度陡增。在安全约束调度这个研究方向上直流潮流模型是主流的简化方式误差在工程可接受范围内尤其对输电网络系统结论的规律性和物理意义都非常清晰。3. Matlab代码实现关键环节与配置3.1 整体代码架构与数据流这个模型我用Matlab YALMIP CPLEX来实现。Matlab负责数据读取、约束构建和结果可视化YALMIP负责把优化问题从“代数形式”翻译成求解器需要的标准格式CPLEX/Gurobi负责真正去解这个MILP/LP问题。三者的分工就像“写代码的人、翻译官、算数的人”各司其职。代码文件安排如下文件名功能case14.m/case118.mIEEE节点数据读取与拓扑矩阵生成csp_params.m光热电站参数设置储热容量、效率、出力限制build_uc_model.m构建优化模型变量、约束、目标函数solve_opf.m调用YALMIP/CPLEX求解输出调度结果plot_dispatch.m绘制机组出力、储热状态、线路负载率等曲线report_security.m安全校验结果汇总各场景潮流越限情况数据流大概是先从节点文件里读入负荷、发电机、线路参数生成基础潮流矩阵再叠加光热电站的储热环节接着生成N-k故障场景集最后把所有约束写入YALMIP并求解。我习惯把“模型构建”和“数据准备”分成两个独立函数因为调参数的时候只改数据文件不用动模型逻辑。3.2 光热电站储热约束的Matlab写法下面是储热约束的YALMIP核心代码片段核心思路是把上一节的能量平衡离散成逐时段线性约束% CSP 储热系统约束 E_TES sdpvar(T, 1); % 储热罐能量状态 q_ch sdpvar(T, 1); % 充热功率 q_dch sdpvar(T, 1); % 放热功率 P_csp sdpvar(T, 1); % 光热机组电出力 constraints [constraints, ... E_TES(1) E_TES_initial eta_ch * q_ch(1) * dt - q_dch(1) / eta_dch * dt]; for t 2:T constraints [constraints, ... E_TES(t) (1 - eta_loss) * E_TES(t-1) ... eta_ch * q_ch(t) * dt - q_dch(t) / eta_dch * dt]; end constraints [constraints, ... E_min E_TES E_max, ... 0 q_ch q_ch_max, ... 0 q_dch q_dch_max, ... P_csp P_csp_min, ... P_csp P_csp_max, ... P_csp q_ch q_solar q_dch, ... % 热功率平衡 E_TES(end) E_TES_initial]; % 调度周期完整性有几个细节我特意在代码里做了处理时间步长dt要统一单位我做的是1小时步长所以所有功率MW乘以时间h才是能量MWh。如果后面改成15分钟步长这个系数必须同步改否则储热平衡会差出4倍。P_csp q_ch q_solar q_dch这条约束的含义是当前时刻集热场收集的热量加储热罐释放的热量一部分直接发电一部分存起来。有人会忘了写“充电”和“发电”同时进行时的热功率竞争关系结果储热罐容量明明有限模型却出现了“既充满又发满”的不合理解。光热出力下限P_csp_min不能设成0就完事汽轮机有最小稳定出力一般是额定功率的20%—30%设成0会让结果偏离工程实际。3.3 N-k安全约束的Matlab实现与细节避坑故障场景集的生成和潮流约束构建是这个程序里最占代码量的部分。核心代码如下% 生成N-1故障场景集线路故障 fail_scenarios {}; for l 1:length(Line) if R(l) % 是否闭合并投入运行 fail_scenarios{end1} l; % 单一线路故障 end end % N-2 场景按用户需要选择 if nk 2 for l1 1:length(Line) for l2 l11:length(Line) if R(l1) R(l2) fail_scenarios{end1} [l1, l2]; end end end end然后对每个场景构建直流潮流约束for s 1:numel(fail_scenarios) B_s B0; ids fail_scenarios{s}; for k 1:length(ids) l ids(k); % 去掉故障线路 B_s(bus_l(l), bus_l(l)) B_s(bus_l(l), bus_l(l)) - 1/x_l(l); B_s(bus_r(l), bus_r(l)) B_s(bus_r(l), bus_r(l)) - 1/x_l(l); B_s(bus_l(l), bus_r(l)) B_s(bus_l(l), bus_r(l)) 1/x_l(l); B_s(bus_r(l), bus_l(l)) B_s(bus_r(l), bus_l(l)) 1/x_l(l); end B_s(1,:) 0; B_s(:,1) 0; B_s(1,1) 1; % 参考节点 theta_s sdpvar(N, 1); constraints [constraints, ... B_s * theta_s P_inj_s(:,s), ... -P_max(l) (theta_s(bus_l(l)) - theta_s(bus_r(l))) / x_l(l) P_max(l)]; end这里有个重要细节P_inj_s(:,s)里的发电机出力变量是和正常工况共用的——换句话说故障场景中的再调度变量P_g,s,t与正常工况变量P_g,t是同一个变量还是另一个独立变量在我这个“校正型SCOPF”框架里它们是不同的变量故障场景的变量有自己的上下限同时允许与正常方案有偏差。这样虽然变量总数变多但模型意义更明确求解器处理起来也更稳定。如果故障场景里切掉了发电机支路还需要手动把该场景下对应发电机的出力变量固定为0if ismember(g, gen_group_of_branch) constraints [constraints, P_g(fault_id) 0]; end这一行代码能避免大量“看似可行、实际不可行”的隐形错误。我建议在遍历故障场景时先做一个简单的拓扑连通性检查。如果故障导致系统解列该场景下直流潮流矩阵奇异YALMIP报错。对孤立场景可以直接跳过或单独处理不用强行求解。4. IEEE14节点与118节点算例测试与结果分析4.1 测试系统数据准备与光热接入方式IEEE14节点的原版数据在网上有很多版本参数不统一是常态。我采用的是Matpower 7.x内置的case14和case118数据这样至少保证全网参数经过Matpower官方校验可直接用它的loadcase读入mpc loadcase(case14.m);从mpc结构体里提取发电机、负荷、线路参数后再插入光热电站。插入的方式我建议这样处理选一个负荷较大的母线把光热电站的出力节点并到该母线上同时把原有火电机组容量做适当调减保证系统总有功平衡不被破坏。否则光热电站的额外出力会让原系统的发电冗余变得过大安全约束的压力几乎为零算例就没有区分度了。在IEEE14节点上我加了一台50 MW的光热电站储热容量400 MWh在IEEE118节点系统上加了一台200 MW的光热电站储热容量1600 MWh。两个系统都设定光伏/负荷预测数据为一天24个时段。4.2 三组对比实验的目标成本与求解时间我做了三组对比实验无安全约束只做普通经济调度不考虑任何故障场景。含N-1安全约束枚举全部线路单一故障。含N-2安全约束枚举指定双子线故障对全枚举数量太多我用了筛选策略只保留系统重载线路附近的双故障场景。运行结果汇总如下测试系统场景类型相对成本增幅求解时间(s)IEEE14无安全约束1.000.8IEEE14N-11.072.3IEEE14N-21.125.1IEEE118无安全约束1.003.2IEEE118N-11.0928.6IEEE118N-2(筛选)1.14120可以看到安全约束带来的成本增幅并不是很大N-1大约7%—9%这在工程上是合理水平。如果算出来成本增幅超过20%大概率是某些线路在正常工况下已经很接近满载了这时候要么是负荷水平设置过高要么是机组组合本身不合理需要回头检查。从调度结果看加了安全约束后光热电站的出力曲线变得更“保守”——在负荷低谷时段刻意降低出力、提前给储热罐充能在负荷高峰时段再集中放电。这说明安全约束实际上约束的是系统整体运行方式而储热罐的存在给这种“前瞻性调峰”提供了空间。这个现象在IEEE118节点系统上体现得更加明显无安全约束的时候光热出力跟随负荷走加约束之后变成了“低谷蓄热、高峰发电”的典型策略。4.3 安全约束对光热储热策略的影响这里专门挑一个有意思的结果说。IEEE118节点算例里储热罐储热量的曲线在N-2场景下出现了明显的“波浪形”上午蓄热、中午短暂放热、下午继续蓄热、傍晚到夜间放热。原因是N-2场景下系统对高峰时段的可调度容量要求更高光热电站在关键时段必须能顶上去因此模型会在白天辐照充足时优先存满傍晚再全力释放等于是用储热罐给系统提供了一个跨时段的安全备用。这给我们的运行启示是光热的储热策略不应该只盯着电价或负荷曲线设计还要把系统级安全约束考虑进去。单纯按“峰谷电价差套利”来做储热调度在遇到N-2级别的故障时光热很可能会因为在错误时段放热而无法提供有效支撑。我的代码里把这一部分用report_security.m可视化输出方便观察每个时刻的储热量与系统最大线路负载率之间的关系。这种交叉分析比直接看最终成本更有价值建议读者在复现时也做同样的事。5. 常见问题与排查技巧实录5.1 求解速度慢、内存爆掉怎么办N-k场景多的首要问题是约束规模爆炸。IEEE118节点做N-2全枚举场景数可能上万直接堆约束内存直接爆掉。我的处理方式故障场景筛选先用正常工况潮流计算每条线路的负载率只保留负载率超过80%的线路及其相邻线路相关的N-2组合。实践证明这样选出来的场景能覆盖95%以上的关键故障而场景数能减少到原来的十分之一。约束重复预判两个不同的故障场景如果对同一个节点的注入功率约束产生相同效果这在N-1场景里经常出现可以合并场景。使用列约束生成CCG思路先求解不含安全约束的优化问题然后校验所有故障场景只把越限场景的约束加回模型循环迭代。对于多时段模型这个方案能大幅减少同时激活的约束数量。5.2 模型不收敛或结果不合理怎么查几个高频问题及排查思路现象排查方向求解器报“infeasible”检查光热能量平衡约束的单位一致性检查储热下限是不是设得过高检查N-k场景中切机处理是否遗漏解出来成本比无约束还低大概率是安全约束没有真正参与作用检查故障场景集是否为空或者YALMIP约束写成了对同一变量重复赋值储热罐全天不变化检查能量平衡约束里的1 - eta_loss系数是否正确dt单位是否错位CPLEX求解时间无限增长尝试把机组出力变量的上下限直接写入sdpvar的lb/ub参数而不是用约束表示这样求解器预置边界更高效5.3 我的调试顺序和高频避坑清单我的调试顺序是先在IEEE14节点上跑“无安全约束光热”的简单模型确认光热储热逻辑正确再叠加N-1场景验证安全约束正确最后升到IEEE118节点加N-2筛选场景。每加一层约束都要回归看目标函数的变化如果成本出现跳变就回头检查新增约束是否过紧。这个流程虽然多花几天时间但比一次性写完全部模型再从头排查高效得多。再列几条高频避坑经验YALMIP的define数据文件路径一定要用绝对路径或者统一管理路径变量否则换电脑跑的时候loadcase会找不到文件浪费半天排查时间。求解器选型上YALMIP默认可能调用的是Matlab自带的linprog对于大规模N-k问题建议安装CPLEX或Gurobi并在YALMIP里yalmiptest确认能够调用到。没有商业求解器时也可以先尝试开源的Cbc求解器验证小规模模型正确性。光热电站接入节点的选择会影响结果建议多试几个节点观察储热曲线和线路负载率的变化而不是死守第一个方案。最后分享一个个人体会这类调度模型做到后面真正的瓶颈反而不是数学建模而是“模型与工程认知”的匹配。我在IEEE118节点上第一次跑出N-2结果时成本增幅只有4%当时还以为是代码写错了后来查了线路负载率才发现这个系统本身就有大量冗余线路安全约束自然影响不大。换到实际工程里如果你手上的电网结构本身就比较薄弱同样的模型跑出来的结果和标准节点的结论可能差很多。所以做这个课题时建议除了复现IEEE节点一定再从实际系统比如你家所在区域的省级简化网架取一套数据试试那才更有实用参考价值。算下来这个模型从搭建到调试前后花了两周多时间。中间踩过的最大的坑是储热平衡约束的单位错位一度让所有结果看起来都“合理”但实际上全错了。后来我把所有能量相关量的单位统一成MWh并在代码顶部写清楚再也没出过类似问题。如果你也打算在这个模型上做扩展建议把目标函数里的成本系数、储热效率这些参数都做成外部可配置的输入文件这样后续无论是做多目标优化、还是加入需求响应都不用改动核心约束代码。