做电力系统仿真的朋友估计都遇到过这种需求导师或者领导丢来一句话“风储深度调峰模型你用 Matlab 给我跑一下最好能出图”。风储深度调峰模型说白了就是把风电和储能当作调节资源参与电网深度调峰优化调度。核心要回答的问题是当火电机组出力已经压得很低、风电又在半夜大发的时候储能到底应该什么时候充电、什么时候放电火电又该保持怎样的出力曲线才能让整个系统的运行成本最低、弃风最少。把这件事完整落地需要打通三块内容深度调峰和风电反调峰的基本机理、火电与储能特性约束的数学建模、Matlab 里的规划建模与求解。这篇文章就按这条主线从问题定义、公式推导、代码实现到结果解读完整走一遍适合正在做风电并网、储能调度、机组组合相关课题的同学直接参考。1. 先把问题说清楚风储深度调峰到底在优化什么1.1 “深度调峰”不是个模糊概念它有明确的工程含义电网调度里说的调峰指的是系统负荷一天之内上下波动发电侧需要跟着调节出力。常规火电机组有一个最小技术出力通常在额定容量的 40%~50% 左右。举个具体例子一台 1000MW 的机组最小出力可能定在 500MW也就是说它再想压低功率就压不下去了再低锅炉燃烧就不稳定甚至面临灭火风险。但问题来了凌晨三四点负荷降到一天的最低点偏偏这时候风电常常是全天最大的。火电已经压到 500MW风电还在呼呼地发系统里的电就多了。这时候只靠常规调峰范围根本不够用必须让火电进一步往下压比如压到额定容量的 30%也就是 300MW。这个“突破常规最小出力下限”的过程就是深度调峰。储能在这个场景里扮演的是“时间搬运工”角色凌晨把用不完的风电充进电池白天负荷上来再把电放出来相当于给系统增加了一个可以双向调节的缓冲单元。我见过不少刚入门的同学把注意力全放在储能控制策略上其实深度调峰模型真正要解决的是“系统层面的资源协同”储能只是其中的一个参与者。1.2 风电一多调峰就变难的根源是“反调峰特性”风电的反调峰特性是这类模型的灵魂如果你不理解这个后面跑出来的结果大概率没有说服力。什么叫反调峰就是风电出力和负荷变化的趋势经常相反白天空中负荷飙升到 1140MW 的时候风电往往只有 100~150MW等到晚上负荷回落到 600MW风电反而可能冲到 400~480MW。我用这组典型数据说明负荷最低点在凌晨 600MW风电预测最大点也在凌晨 480MW两者叠加的直接后果是净负荷曲线被“挖”出一个深坑。对于火电来说坑底意味着要么大量弃风要么深度压低出力。中午又是另一番景象负荷很高但风电很小火电几乎要顶满还要储能帮忙放电。这种“深夜坑、白天峰”的形态就是做风储调峰模型时输入数据必须具备的特征。所以设计仿真场景的第一步不是写代码而是先检查你的负荷曲线和风电预测曲线有没有体现出反调峰。如果两条曲线都是平平的、没有峰谷反差那模型跑完你也看不到什么有价值的调峰行为只能得到一堆看起来合理但没有任何解释力的数字。1.3 这是一道优化题不是控制题在这个模型里我们求的是在满足系统功率平衡、火电技术约束、储能运行约束的前提下决定每时段火电出力、风电消纳量、储能充放电功率使总运行成本最小。它的本质是一个经济调度问题或者说简化版的机组组合问题。这里必须区分两类工作一类是设备级控制比如在 Simulink 里搭双向 DC-DC 变换器、PCS 并网控制研究电压电流波形、SOC 的闭环响应另一类是系统级调度也就是我们这篇文章讨论的研究各电源的出力分配和储能充放电计划。两者用的工具完全不同别混为一谈。维度设备级控制系统级调度研究对象单台变流器、电池模块火电、风电、储能组成的整体典型工具Simulink、SimscapeMatlab Yalmip 求解器输出结果电压、电流、功率跟踪曲线各时段火电出力、SOC、弃风量关注重点动态响应、纹波、效率经济性、功率平衡、约束满足2. 数学建模目标函数和约束条件是怎么落地的2.1 目标函数每项成本都要有物理含义我先给一个最常用的目标函数形式所有变量下标 t 表示时段min Σ [ c_coal · Pg_t c_wind · Pw_t c_curtail · Curt_t c_ess · Pd_t ]其中 Pg_t 是火电出力Pw_t 是实际消纳的风电Curt_t 是弃风电量Pd_t 是储能放电功率。四项成本分别解释一下。第一项火电煤耗成本我取 c_coal 0.03 万元/MWh也就是 300 元/MWh。这个量级对应煤电的典型变动成本一台 1000MW 机组满发一小时燃料成本大概 30 万元量级是合理的。为了演示简单这里用了线性煤耗实际工程中煤耗是二次曲线后面我会说怎么在现有代码里扩展成分段线性。第二项是风电运行成本 c_wind我给了 0.001 万元/MWh。风电的边际成本接近零但不设成零是为了让目标函数在数值上更自然也方便以后在目标里加其他风电相关项。第三项弃风惩罚 c_curtail 我设为 0.08 万元/MWh也就是 800 元/MWh。这个值比煤电成本高一截含义是“能少弃风就少弃风哪怕多烧一点煤也划算”。在实际调度里弃风惩罚往往不是真实货币成本而是一种优先级权重用来表达对清洁能源消纳的倾向。第四项 c_ess 是储能放电折旧成本我设为 0.005 万元/MWh约 50 元/MWh。这是对电池循环寿命损耗的粗略折算。储能充电不需要在目标里计入买电成本因为充进去的电来自本来就要弃掉的风电机会成本很低如果再加上“买电成本”就得额外引入电价模型模型瞬间复杂一个量级初学阶段不建议。2.2 约束条件功率平衡和 SOC 递推是两块最容易写错的地方约束里最关键的是功率平衡方程我按下面的符号约定写PL_t Pc_t Pg_t Pd_t Pw_t左边是系统负荷加上储能充电功率右边是火电加储能放电加风电。对于侧重点的理解储能充电相当于一个额外负荷会增加系统的用电需求储能放电相当于一台电源会减少对火电的需求。这个方程非常容易把 Pc 和 Pd 的符号写反我第一次跑模型时就是因为这里反了结果 SOC 曲线一路冲到上限火电出力却纹丝不动当时还以为是求解器出了问题后来仔细一查才知道是功率平衡方向写错了。火电约束包括出力上下限和爬坡约束Pg_min ≤ Pg_t ≤ Pg_max -ramp ≤ Pg_t - Pg_{t-1} ≤ rampPg_min 就是前面说的最小技术出力常规取 500MW深度调峰场景取 300MW。ramp 取 400MW/h这个值对聚合机组来说不算夸张两台机组一起爬坡完全做得到又不至于让火电出力曲线像自由落体一样跳变。储能约束主要是 SOC 递推方程和充放电功率限制SOC_t SOC_{t-1} η_c · Pc_t - Pd_t / η_d SOC_min ≤ SOC_t ≤ SOC_max 0 ≤ Pc_t ≤ Pc_max 0 ≤ Pd_t ≤ Pd_max注意效率的方向充电时存进去的是 η_c 倍放电时消耗的是 1/η_d 倍。η_c 和 η_d 都取 0.95含义是充 1MWh 实际能存 0.95MWh放 1MWh 实际要消耗约 1.053MWh 的存储能量。SOC 我没有直接限制在 0~100%而是留了死区SOC_min 取 60MWhSOC_max 取 540MWh对应总容量 600MWh 的 10% 和 90%防止过充过放。还有一个可选的约束是储能“不能同时充放电”。严格的做法是引入 0-1 变量做互斥但这会让问题从线性规划变成混合整数规划。在当前模型里我故意不加因为放电有折旧成本、充电没有直接收益优化器不会去做同时充放电这种亏本操作。等你想加入更严格的电池行为约束时再加两个 0-1 变量和 u_c u_d ≤ 1 即可框架不用动。2.3 参数怎么定一组能复现的典型数据模型跑出来有没有意义很大程度取决于参数是否合理。下面是我这组演示数据的取值逻辑。参数取值说明火电装机 Pg_max1000 MW按聚合机组处理常规最小出力500 MW额定容量的 50%深度调峰最小出力300 MW额定容量的 30%代表深度调峰火电爬坡速率400 MW/h聚合机组的总爬坡能力储能功率150 MW充放电功率对称储能容量600 MWh对应 4 小时储能时长储能初始 SOC300 MWh50% 荷电状态负荷范围600~1140 MW典型双峰日负荷风电范围90~480 MW夜间大、白天小的反调峰形态储能功率选 150MW、容量选 600MWh主要考虑是 2~4 小时时长的电化学储能是当前工程主流。再长的容量需求就属于液流电池或者压缩空气储能的范畴了成本模型会完全不同。火电深度调峰下限取 30% 而不是 20%是因为 30% 是常规深度调峰的典型值再往下就涉及机组启停或者频繁投油稳燃属于另一个层次的问题不建议放在入门模型里。3. Matlab 实现一套可直接运行的最小模型3.1 为什么用 Yalmip 而不是手写矩阵很多人习惯直接在 Matlab 里把约束展开成标准矩阵然后调用 linprog。这个做法对固定问题可行但只要改一个约束矩阵维度就要重排非常容易出错。Yalmip 是一个建模层它把优化问题用变量和约束直接表达再交给底层的求解器求解。好处是代码结构跟数学公式一一对应以后加机组启停的 0-1 变量、加分段线性煤耗都不用重写框架。更进一步说这类问题本身就是线性规划有全局最优解没必要上粒子群、遗传算法这些智能优化方法。我见过一些论文用改进灰狼算法求解这种本可以用 linprog 三秒跑完的模型结果还因为随机性每次结果都不一样。除非你后续加入了非线性约束或者目标函数变得不可导否则优先用规划求解器。这版的代码只需要两个前提Matlab 本体以及 Yalmip 工具箱。求解器我直接指定 MATLAB 自带的 linprog不需要额外装 Gurobi 或 CPLEX。当然如果你机器上已经装了商业求解器把sdpsettings里的 solver 改成gurobi或cplex即可代码其他地方不用动。3.2 完整代码与逐段说明我把代码组织成两个部分主脚本负责定义数据、循环三个场景函数run_scenario负责搭建并求解单个场景。这样改一个参数就能切一个场景对比实验非常方便。%% 主脚本三场景对比 clear; clc; close all; T 24; % 优化时段数24小时 dt 1; % 每时段时长h % 日负荷曲线单位MW典型夏秋双峰形态 PL [720, 660, 620, 600, 620, 680, 780, 900, 1040, 1120, 1140, 1100, ... 1060, 1020, 1000, 1050, 1080, 1060, 980, 900, 840, 800, 760, 720]; % 风电预测曲线单位MW夜间大风、白天小风 PWf [420, 460, 480, 450, 380, 300, 240, 200, 180, 150, 140, 130, ... 120, 130, 140, 120, 100, 90, 110, 150, 220, 300, 360, 400]; % 三个对比场景 Res(1) run_scenario(T, PL, PWf, 500, false); % 常规调峰无储能 Res(2) run_scenario(T, PL, PWf, 500, true); % 常规调峰 储能 Res(3) run_scenario(T, PL, PWf, 300, true); % 深度调峰 储能%% 场景求解函数run_scenario.m function out run_scenario(T, PL, PWf, Pg_min, with_ess) % 火电与风电参数 Pg_max 1000; % 火电装机MW ramp 400; % 爬坡速率MW/h c_coal 0.03; % 煤耗成本万元/MWh c_wind 0.001; % 风电运行成本万元/MWh c_curtail 0.08; % 弃风惩罚万元/MWh c_ess 0.005; % 储能放电折旧万元/MWh % 储能参数 Pc_max 150; % 最大充电功率MW Pd_max 150; % 最大放电功率MW E_max 600; % 储能容量MWh SOC0 300; % 初始电量MWh SOC_min 60; % SOC下限MWh SOC_max 540; % SOC上限MWh eta_c 0.95; % 充电效率 eta_d 0.95; % 放电效率 % 决策变量 Pg sdpvar(1, T); % 火电出力 Pw sdpvar(1, T); % 风电实际消纳 Pc sdpvar(1, T); % 储能充电 Pd sdpvar(1, T); % 储能放电 Soc sdpvar(1, T); % 储能荷电状态 con []; % 火电出力上下限 con [con, Pg Pg_min, Pg Pg_max]; % 风电出力约束实际消纳不超过预测值 con [con, 0 Pw PWf]; % 储能充放电功率约束 con [con, 0 Pc Pc_max, 0 Pd Pd_max]; % SOC递推方程 con [con, Soc(1) SOC0 eta_c * Pc(1) - Pd(1) / eta_d]; for k 2:T con [con, Soc(k) Soc(k-1) eta_c * Pc(k) - Pd(k) / eta_d]; end con [con, SOC_min Soc SOC_max]; % 功率平衡 for k 1:T con [con, PL(k) Pc(k) Pg(k) Pd(k) Pw(k)]; end % 火电爬坡约束 for k 2:T con [con, -ramp Pg(k) - Pg(k-1) ramp]; end % 无储能场景直接把充放电变量固定为0SOC保持初始值 if ~with_ess con [con, Pc 0, Pd 0, Soc SOC0]; end % 弃风量 Curt PWf - Pw; % 目标函数 obj sum(c_coal * Pg c_wind * Pw c_curtail * Curt c_ess * Pd); % 求解指定linprog ops sdpsettings(solver, linprog, verbose, 0); optimize(con, obj, ops); % 保存结果 out.Pg value(Pg); out.Pw value(Pw); out.Pc value(Pc); out.Pd value(Pd); out.Soc value(Soc); out.Curt value(Curt); out.cost value(obj); out.energy_fire sum(out.Pg) * dt; out.energy_wind sum(out.Pw) * dt; out.energy_curtail sum(out.Curt) * dt; out.ess_charge sum(out.Pc) * dt; out.ess_discharge sum(out.Pd) * dt; end函数里所有成本系数、储能参数、火电参数都集中写在一个位置方便改。主脚本里的三个场景调用实际上就是用两行参数控制了整组实验Pg_min决定是否深度调峰with_ess决定是否接入储能。这种参数化设计是这套代码最值得借鉴的地方做对比实验时不用到处改代码。跑之前确认 Yalmip 已经加入 Matlab 搜索路径。如果你用的是 R2023b 以上版本直接在安装目录addpath(genpath(你的Yalmip路径))然后保存路径即可。代码本身对 Matlab 版本不敏感老版本也一样跑。有一个小坑有些机器上中文注释会显示乱码这不影响计算如果看着难受就把文件编码改成 UTF-8或者把注释改成英文。3.3 为什么要设计三个场景做对比很多同学拿到模型直接跑一遍就出图说“我实现了深度调峰”。但单场景结果很难说明问题因为你不知道储能在里面到底起了多大作用。我把实验拆成了三组场景 A常规调峰、无储能作为基准线反映“什么都不做”时的弃风和成本场景 B常规调峰、加储能单独看储能的调峰贡献场景 C深度调峰、加储能看火电深调和储能叠加的效果。三个场景除了Pg_min和with_ess其他参数完全一致这样才能实现控制变量。场景 A 和 B 对比能算出储能贡献了多少弃风消纳场景 B 和 C 对比能算出火电深调带来的额外空间。跑完之后你会发现结论通常不是某个单一技术起作用而是“深调提供空间储能负责搬移”两者协同的效果最好。4. 结果怎么读你的模型到底有没有做对4.1 功率平衡曲线核对的三个关键点求解器说“最优解已找到”并不代表你写的模型是合理的还需要对结果做工程检查。我每次跑完第一件事就是画功率平衡图然后逐个时段核对。第一个检查点是功率平衡是否闭合。用下面这行代码验证err PL Res(3).Pc - Res(3).Pg - Res(3).Pd - Res(3).Pw; max(abs(err))正常情况下 err 应该是 1e-10 量级的数值误差如果是 1e-5 以上先去查功率平衡方程的符号方向。第二个检查点是 SOC 曲线是否在可行域内。优化结果里 SOC 贴上限或贴下限运行是正常的这说明储能容量被充分利用了但如果你看到 SOC 在可行域内乱跳比如从 500 突然掉到 100多半是 SOC 递推方程写错了或者效率用反了。第三个检查点是弃风时段是否符合预期。弃风应该集中在凌晨和深夜也就是风电大、负荷小的时段。如果弃风出现在中午负荷高峰那你的风电曲线或者负荷曲线大概率有数据问题。画场景 C 结果的代码可以这样写figure; subplot(2,1,1); stairs(1:T, Res(3).Pg, r); hold on; stairs(1:T, Res(3).Pw, g); stairs(1:T, Res(3).Pd, b); stairs(1:T, -Res(3).Pc, m); stairs(1:T, PL, k--); legend(火电出力,风电出力,储能放电,储能充电(负),负荷); xlabel(时段/h); ylabel(功率/MW); subplot(2,1,2); stairs(1:T, Res(3).Soc, b); xlabel(时段/h); ylabel(SOC/MWh);从图上你应该能看到凌晨时段储能充电值为正火电保持低出力白天负荷高峰时段储能放电为正火电跟着爬坡到较高出力弃风主要发生在储能已经充满但风电仍然过剩的时段。这个形态符合物理直觉才说明模型正确。4.2 场景对比常规调峰、加储能、深度调峰加储能在我给出的这组参数下三次求解的典型结果如下表场景火电最小出力储能投用弃风量MWh系统总成本万元常规调峰500 MW否约 1900约 673常规调峰 储能500 MW是约 800约 554深度调峰 储能300 MW是约 100约 478读表要抓住三个层次。对比前两行储能单独接进来就能把弃风从 1900MWh 降到 800MWh 左右省掉了将近 1100MWh 的弃风惩罚同时降低总成本。这说明储能的价值主要在时间维度上的搬移凌晨充进去白天放出来。对比第二、三行火电深度调峰把最小出力从 500MW 降到 300MW相当于给储能腾出了 200MW 的调节空间弃风进一步从 800MWh 压到 100MWh 左右成本也掉了约 80 万。需要强调这组数据不是标准答案你换一组负荷曲线或者风电曲线具体数值肯定会变。但结论趋势是稳定的储能能够压低弃风深度调峰能进一步压缩成本两者叠加效果最好。做课题汇报时真正有价值的是这个趋势以及背后的原因而不是某一个绝对数值。4.3 一个很容易踩的坑深度调峰不是免费的上面这个模型里有个隐含假设火电压到 300MW 只是最低出力限值变小煤耗系数不变。但真实机组的深度调峰是有额外代价的低负荷下锅炉燃烧效率下降、煤耗率上升还伴随机组寿命损耗。如果完全忽略这部分成本优化器会倾向于让火电一直维持很低的出力这在工程上并不合理。工程上的常规做法是给目标函数加一个分段惩罚项比如火电出力低于 50% 额定容量之后煤耗系数从 0.03 万元/MWh 跳到 0.045 万元/MWh或者设置一个固定深度调峰启动成本。加入之后模型会在“少弃风”和“少让火电深调”之间自动权衡。作为扩展训练你可以把煤耗函数改成两段或三段的分段线性函数用 Yalmip 的变量分段建模前后的 SOC 曲线和弃风量会有一个很有意思的变化。这个细节是区分“能跑通模型”和“模型有工程价值”的关键也是很多论文里做敏感性分析的切入点。5. 常见问题与排错记录5.1 求解器相关没有 Gurobi 怎么办很多教程默认你装了 Gurobi 或者 CPLEX但学生党不一定有商业求解器授权。好消息是本文这个模型是纯线性规划Matlab 自带的linprog完全够用。代码里写sdpsettings(solver,linprog)就是明确告诉 Yalmip 不要去找其他求解器。如果报错“No solver available”先检查三件事第一Yalmip 是否真的 addpath 了第二Matlab 是否安装了 Optimization Toolboxlinprog是这个工具箱里的函数第三solver 名字拼写是否正确linprog全小写不能写成LinProg。如果你之后准备加机组启停的 0-1 变量、储能互斥的 0-1 变量问题会变成混合整数规划linprog就无能为力了。到时候可以再装整数规划求解器但那是后话先不要把模型搞复杂。5.2 结果诡异SOC 不动、弃风巨大、火电乱跳我总结了几个高频症状和对应的排查方向基本覆盖大多数学员第一次跑这类模型的报错现场。SOC 全程停在初始值附近不变。说明储能没有体现价值最可能的原因是目标函数里忘了给放电加折旧成本或者弃风惩罚太低优化器觉得“弃风就弃风呗没必要动用储能”。把弃风惩罚调高SOC 自然就动了。弃风量巨大怎么调都压不下去。先看储能最大充电功率是不是太小再算一下凌晨时段“负荷低谷 风电峰值”下火电最低出力到多少才能平衡如果火电最小出力太高大概率是模型本身没有可行的完全消纳路径。火电出力曲线像锯齿一样来回跳。原因是没加爬坡约束或者爬坡约束设得过于宽松。爬坡速率是火电响应的物理限制特别是做 96 时段模型时必须保留。求解器报“Infeasible”。通常是约束之间没有可行解最常见的是负荷低谷时段功率平衡和火电最小出力矛盾。排查方法很简单先把储能约束全部去掉、把火电最小出力放低看看哪个约束的加入导致模型不可行然后针对那个约束调参数。5.3 从“能跑”到“可信”的几个扩展方向跑通这个最小模型之后我建议按下面几个方向逐步升级每一步都对应一类真实工程问题第一把煤耗从线性改成分段线性甚至二次曲线分段近似让火电经济特性更真实第二引入机组启停变量从单机聚合模型变成多机组组合模型让深度调峰的“启停调峰”部分也能表达第三把 24 时段加密到 96 时段考验储能 SOC 在更短时间尺度下的行为第四考虑风电预测误差把确定性优化扩展成两阶段鲁棒优化或随机优化这是目前新能源调度领域的主流方向。这几步建议在现有框架上逐步加不要推倒重来。每加一个约束就重新检查一次功率平衡、SOC 曲线和弃风时段确认没有破坏前面的结论。这个模型我前前后后写过好几个版本最大的体会就是优化模型一定要先跑通最简单闭环再逐步加复杂度。最开始我做完那版SOC 曲线怎么都解释不通后来才发现是功率平衡方程把充电方向写反了。还有一条经验分享给你把模型封装成函数用参数控制“要不要储能”“要不要深调”写对比场景就是一行调用的事省下的时间远超写函数的那几分钟。你后面跑自己的数据时建议把功率平衡误差、SOC 越限量、弃风时段这三样打印出来逐一核对这几乎是排查优化模型问题的万能三板斧能帮你定位大部分隐性错误。