我最初看到“高比例可再生能源并网下虚拟电厂多时间尺度调度”这组词时,第一反应和很多人一样:直接找一篇SCI论文,把Matlab代码跑通,最后照着图把结果复现出来,不就完事了?后来真正动手才发现,最磨人的不是求解器,不是约束条件,而是一开始就要把一个非常实际的问题想明白——高比例可再生能源并网后,系统确实缺灵活性,但让储能去顶灵活性,每一度电、每一次循环都要花钱,这笔账到底该怎么算。
高比例可再生能源并网、虚拟电厂、多时间尺度调度、衰减建模,这几个关键词拼在一起,描述的是一个典型的电力系统经济调度问题。虚拟电厂把分布式光伏、分散风电、储能、可调负荷聚合成一个可统一调度的对象;多时间尺度调度解决的是风电光伏预测误差随预测时间缩短而逐步收敛的问题;衰减建模解决的是储能被频繁调用之后寿命损耗怎么计入调度成本的问题。这篇博文把这套逻辑拆开,讲清楚为什么目标函数要这么写,约束为什么那么设,以及用Matlab复现时哪些点最容易出错。
这篇内容适合正在复现相关SCI论文的研究生、做微电网或虚拟电厂调度算法的工程师,也适合刚接触储能优化建模、想知道“为什么我的储能老被充满放空”的同学。
1. 先把账算明白:虚拟电厂里的“灵活性”和“成本”指的是什么
很多人一上来就写目标函数,风电光伏出力、储能功率、主网交互功率全放进去,变成一个大而全的优化问题,然后去调求解器。这种做法不是不行,而是你没有先想清楚:这个系统到底缺的是什么,储能参与调度时到底消耗了什么。
1.1 净负荷曲线已经不再是“负荷曲线”
传统调度看的是负荷曲线,所有机组跟着负荷走。可再生能源并网比例提高之后,调度真正要看的是净负荷,也就是原始负荷减去风电和光伏出力:
P_net(t) = P_load(t) - P_wind(t) - P_pv(t)
这看起来只是一个数学平移,但物理意义完全不同。光伏大发的中午,净负荷可能被压得很低,甚至变成负值;傍晚光伏退坡,净负荷快速爬升,形成所谓的“鸭子曲线”。负荷侧的一小时爬坡可能只有几十兆瓦,但净负荷的一小时爬坡可能达到几百兆瓦。
我见过不少调度模型的初始设置是把风光的预测出力当成“负的负荷”直接扣掉,然后只调度火电和储能去跟踪净负荷。这样做其实没问题,但你要意识到,净负荷的波动和爬坡才是这个系统真正的调节需求来源。灵活性说白了,就是系统在给定时间窗内向上或向下改变出力的能力。
分布式电源有爬坡限制,主网交互有容量限制,可调负荷有舒适度约束,真正能快速双向调节的往往是储能。虚拟电厂的价值,就是把分散的小容量可调资源打包,让它们在净负荷变化最剧烈的时间窗口形成合力。
1.2 储能的成本不是“电费”,而是寿命损耗
如果不做衰减建模,储能的调度结果通常会非常激进:价格高时拼命放,价格低时拼命充,甚至一天内反复深度充放。从电价套利的角度看,这样确实能增加收益;但从电池寿命角度看,成本高得吓人。
储能单次充放电的边际运行成本很低,几乎可以忽略不计,因为电费本身已经算在购售电成本里了。真正需要计入调度目标的是循环寿命损耗。电池每经历一次充放电,内部活性物质都在发生变化,循环次数被消耗,容量逐渐衰减。等到循环次数用尽,整个储能系统就需要更换,而更换成本是以百万甚至千万为单位计算的。
我曾经用过一个非常直观的估算方式。假设一套1MWh储能系统总投资80万元,按100%充放电深度循环,额定循环寿命4000次,那么这套储能整个生命周期大约能完成4000个满充满放循环。如果按每个循环对应的总能量吞吐来算,每完成一次完整的充放循环,相当于消耗了80万除以4000,也就是200元的寿命成本。这200元不是电费,而是电池被用掉的那点寿命对应的钱。
如果不把这一项放进目标函数,优化器会认为储能出力是零成本,结果就是储能频繁动作,但实际全生命周期算总账时,这套系统可能提前好几年报废,综合成本反而更高。
1.3 灵活性平衡最终会变成一个多目标折中
高比例可再生能源并网下的调度,本质上是在多个互相冲突的目标之间找平衡:
- 尽量多用风电光伏,减少弃风弃光
- 尽量少买主网电,降低购电成本
- 尽量少让火电启停,减少燃料与运维成本
- 尽量少调用储能,延长电池寿命
- 同时保证系统功率平衡和备用容量充足
这些目标无法同时达到最优。弃风弃光惩罚设得太低,优化器会倾向于直接切掉过剩的可再生能源,因为那样最省钱;储能寿命成本设得太高,优化器会尽量不用储能,结果净负荷爬坡没人接,系统出现灵活性缺口。
多时间尺度调度要做的,不是一次性找到一个完美解,而是用“日前计划、日内滚动、实时修正”三层结构,让这些目标在不同预测精度下逐步收敛。理解了这个逻辑,后面再看代码就很顺了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 虚拟电厂多时间尺度调度的三层递进逻辑
很多刚接触这个方向的人会误以为多时间尺度调度就是三个不同时间分辨率的优化模型分别跑一遍,然后拼在一起。其实不是。三层调度之间是递进修正关系,不是并列关系。
风电和光伏的预测误差有一个非常明显的规律:预测时间尺度越长,误差越大。提前24小时预测光伏发电,可能偏差百分之二三十;提前1小时预测,偏差可能降到百分之十以内;到了实时前5分钟,偏差已经很小了。多时间尺度调度就是顺应这个规律,把调度决策拆分到不同阶段,让每个阶段的决策都基于当时可用的最新预测来做。
2.1 日前计划解决“开机与备用”问题
日前调度的时间尺度一般是24小时,时间分辨率1小时,甚至15分钟。它的核心任务不是把风光出力预测得多准,而是确定那些需要提前准备的大状态量:分布式燃气机组要不要开、储能第二天大致维持在什么荷电区间、主要购电曲线长什么样。
在这个阶段,风电光伏用的是日前预测值,预测误差还比较大,所以必须用备用约束来兜底。我通常会在日前模型里加入向上备用和向下备用两个不等式约束,让系统在最坏预测偏差下仍然具备调节能力。
备用约束本质上是把“灵活性需求”提前固化成数学约束。比如,净负荷在下一个调度时段可能比预测值高20兆瓦,那系统必须确保在15分钟内能再增加20兆瓦出力,这20兆瓦可以来自储能放电、分布式电源爬坡或者增大主网购电。
日前模型的决策变量里必须有表示机组启停的整数变量,因为机组启停不能每15分钟随便变,需要提前安排。这个阶段的目标是让全天总运行成本最低,但它给出的结果并不直接执行,而是作为日内调度的参考轨迹。
2.2 日内滚动解决“预测偏差修正”问题
日内滚动调度的时间分辨率可以取15分钟,滚动周期一般是未来4小时或6小时。每到一个新时段,系统获取最新的超短期预测,把日前计划当作参考值,重新求解一次优化问题。
关键操作只有一个:永远只执行滚动优化结果中的第一个时段,执行完之后进入下一个时段,重新获取预测,再滚动求解。如果每15分钟滚动一次、每次向前看96个点,其实是用未来4小时的预测信息来辅助当前15分钟的调度决策。这样做的好处是,当前时段的决策不会只看眼前,而是会考虑未来几小时净负荷的变化趋势,给储能留出合理的调节空间,避免这15分钟把储能放空,导致下一个15分钟无电可用。
我在实现日内层时,会让日前计划以“软约束”的形式出现,而不是硬性固定。硬约束会造成一个问题:日前预测误差较大时,日内仍然被迫贴着日前计划走,滚动修正的意义就没了。软约束的做法是在目标函数里加一个偏离日前计划的惩罚项:
min C_operation + C_degradation + w * |P_intra - P_day_ahead|
这里的w不能设得太大,否则日内层不敢修正,整个多时间尺度调度退化成单层调度;也不能太小,否则日内出力可能在相邻时段剧烈抖动,储能频繁改变充放电方向。
2.3 实时层解决“分钟级扰动”问题
实时调度的时间粒度一般到5分钟甚至1分钟。这个阶段预测误差已经不大,调度目标也从“经济性最优”转向“可执行性与频率调节”。
实时层不会重新做大规模优化,因为5分钟内连续风电爬坡或者负荷突变很难用复杂MILP去解。更常见的做法是把日内层给出的下一个15分钟计划当作基点,把偏差量按调节成本从小到大分配给各资源。储能调节速度快,优先承担小幅高频偏差,但每次动作的吞吐量都会折算成衰减成本;分布式电源次之;主网交互通常只在偏差较大时调整。
很多人写论文时会把这一层合并到日内层里,只做日前和日内两个尺度,这在工程上也说得过去。但如果标题明确写了“多时间尺度”,那还是建议把实时层单独保留,哪怕只做一个线性分配模型,也能让复现论文的逻辑更完整。
2.4 三层之间到底传递什么数据
容易忽略的是层与层之间传递的数据结构。我自己的实现习惯是这样的:日前层输出每个时段的计划出力、机组启停状态、储能末端SOC参考值;日内层读入最新预测后,把日前计划作为软参考,输出未来4小时细化计划,但只下发第一个时段的指令;实时层读入日内层下发指令后,在5分钟尺度上分配偏差。
数据流里最容易被忽略的是SOC状态。储能SOC是跨时段耦合变量,日前计划结束后SOC是多少,日内层要从这个SOC继续算,否则储能功率会出现不连续跳变。我在代码结构上把SOC作为输出对象在层间显式传递,而不是每次重新初始化成0.5,这样复现结果才能稳定。
3. 储能衰减建模:不要只做SoC限制就收工
储能衰减建模是这篇SCI复现内容里最容易糊弄过去,但也最影响结果可信度的部分。很多人处理储能寿命的方式是给SOC设一个范围,比如0.2到0.9,然后任务完成。这个硬约束确实能防止储能过充过放,但它完全没有回答最核心的问题:储能到底被“用掉”了多少寿命。
3.1 先理解循环寿命和吞吐量的关系
电池厂商给的循环寿命通常是一个固定深度下的数字,比如4000次循环@80%DoD。这里的循环不是指随便充一次电或者放一次电,而是指完成一个从满充到满放再回到满充的完整过程。所以一次80%DoD的放电,加上后续等量充电,才算0.8个完整等效循环。
在调度优化里,我们不可能直接数循环次数,因为充放电过程是一段连续曲线,一次完整的深度循环被切成无数小段,跨越无数个优化时段。雨流计数法能从历史SOC曲线里提取循环次数,但它是一种后验算法,边优化边计数基本不可行,效率低且难以嵌入混合整数线性规划。
工程上最常用的是能量吞吐线性化模型。基本想法是:电池每处理1kWh能量,就消耗一部分循环寿命,这部分损耗可以线性折算成成本。这个模型不区分深度循环还是浅循环,只按累计吞吐量计算,虽然会牺牲一些精度,但胜在能够嵌入优化模型,而且结果趋势可信。
3.2 把衰减成本写进目标函数的具体形式
假设储能系统额定容量为E(kWh),系统投资成本为J(元),在额定条件下总共有N个完整等效循环寿命。一个完整循环意味着电池先后完成一次充电和一次放电,累计内部吞吐量约为2E kWh。所以单位吞吐量对应的寿命损耗成本为:
lambda = J / (2 * E * N)
这个lambda的含义是每向电池内部处理1kWh能量需要计提的寿命成本。举个例子,E=1000kWh,J=800000元,N=4000次,计算出来lambda大约是0.1元/kWh。这意味着储能每累计向内部充放1kWh能量,就相当于消耗了0.1元电池寿命。
在优化模型中,每小时段的储能吞吐量可以写成:
E_deg(t) = min(P_dis(t)/eta_dis, P_ch(t) * eta_ch) 的形式,或者简化为两端求和
然后目标函数里加一项:
C_deg = sum(lambda * (P_dis(t) / eta_dis + P_ch(t) * eta_ch) * dt)
这里有一个细节值得重点提醒:效率到底要不要乘进去?不同论文的处理方式并不完全一致。如果P_ch和P_dis定义为储能交流侧功率,那么充电时真正进入电池内部的能量是 P_ch * eta_ch;放电时电池内部释放的能量是 P_dis / eta_dis。严格说,充放电损耗对应的这部分电量并没有参与电化学反应,也基本不消耗循环寿命,所以衰减成本应该按电池内部能量吞吐来算,而不是直接按交流侧功率之和来算。
当然,如果认为模型精度不需要那么高,直接按交流侧充放电功率之和近似处理也可以,但要注意,换算系数不同会导致结果产生百分之几到百分之十几的偏差。我自己做复现时会把效率放到SOC递推里去计算,衰减成本用内部吞吐量,这样参数设定和物理意义是一致的。
3.3 衰减成本模型做完之后,储能调度行为会发生什么变化
把衰减成本加进目标函数后,最直观的变化是储能不再“有电价差就充放”。调度模型会主动比较每一次充放电产生的收益和衰减成本:如果某个时段的光伏大发,电价很低,充入1kWh电可以赚到未来峰时0.4元的价差,但这次充电要消耗0.1元寿命成本,那这笔交易还是值得的;如果价差只有0.05元,那储能宁可闲着,也不去凑热闹。
另一个常见行为变化是SOC工作区间会变窄。没有衰减成本时,为了应对晚间高峰,储能可能白天一路充到95%,傍晚又放到10%;加了衰减成本后,模型发现90%以上和10%以下的区间虽然还可发可用,但边际收益不高,却要承担深度循环带来的寿命损耗,于是SOC会被压在0.2到0.8之间,只在最关键的净负荷爬坡时刻突破这个区间。
这是衰减建模对调度结果最重要的价值:它不是简单让储能少动作,而是让储能把每一次动作都用在刀刃上。
4. Matlab实现:变量、约束与求解器搭配
写Matlab代码之前,我建议先不要急着打开编辑器,而是在纸上列出优化问题的变量、约束、目标。变量定义清楚之后,代码结构自然就出来了,后面调bug也容易得多。
4.1 决策变量先做成一张表
根据典型的虚拟电厂调度问题,我建议定义这些决策变量:
| 变量 | 类型 | 含义 | 维度 |
|---|---|---|---|
| P_buy | continuous | 主网购电功率 | T |
| P_sell | continuous | 主网售电功率 | T |
| P_dg | continuous | 分布式电源出力 | T |
| u_dg | binary | 分布式电源启停 | T |
| P_ch | continuous | 储能交流侧充电功率 | T |
| P_dis | continuous | 储能交流侧放电功率 | T |
| SOC | continuous | 储能荷电状态 | T+1 |
| P_curtail | continuous | 弃风弃光功率 | T |
T的取值要看时间尺度。如果是日前层,T是24或96;如果是日内滚动层,T是16或24,对应未来4小时或6小时,按15分钟一个点算。
使用YALMIP建模时,定义变量的代码大概是:
matlab复制T = 96; % 日前96个时段,每段15分钟
dt = 0.25; % 单位:小时
P_buy = sdpvar(1, T, 'full');
P_sell = sdpvar(1, T, 'full');
P_dg = sdpvar(1, T, 'full');
u_dg = binvar(1, T);
P_ch = sdpvar(1, T, 'full');
P_dis = sdpvar(1, T, 'full');
SOC = sdpvar(1, T+1, 'full');
P_curtail = sdpvar(1, T, 'full');
我在大量复现调度论文后有一个体会:不要嫌变量多,YALMIP建模的瓶颈从来不是变量数量,而是变量之间的物理关系没有定义清楚。功率平衡、SOC递推、备用约束,这三条写清楚,问题就基本完成了一半。
4.2 约束条件必须包含的六类
功率平衡是第一个约束。虚拟电厂与主网交互、分布式电源、风光、储能放电共同满足负荷,储能充电看作额外负荷:
P_buy(t) - P_sell(t) + P_dg(t) + P_wind_used(t) + P_pv_used(t) + P_dis(t) - P_ch(t) = P_load(t) - P_dr(t) - P_curtail(t)
这里P_dr是可削减负荷,如果模型里没有需求响应,就直接删掉这一项。弃风弃光用P_curtail表示,它的值等于预测出力减实际使用出力,理论上始终非负。
第二个约束是储能SOC递推。这里最容易出单位错误,所以代码注释我一般都写得很细:
matlab复制% SOC递推公式
E_bat = 1000; % kWh
eta_ch = 0.95;
eta_dis = 0.95;
Constraints = [Constraints, SOC(1) == 0.5];
for t = 1:T
Constraints = [Constraints, SOC(t+1) == SOC(t) + ...
(eta_ch * P_ch(t) - P_dis(t) / eta_dis) * dt / E_bat];
end
Constraints = [Constraints, 0.2 <= SOC <= 0.9];
SOC上下限0.2和0.9不是拍脑袋定的,它对应的是储能系统推荐的运行窗口。如果你的复现论文里没有明确写这个值,那就按出厂推荐值来。如果目标函数里有衰减成本,SOC上下限可以适当放宽,让模型自己决定要不要深度充放,这样更有利于观察衰减成本对调度的影响。
第三个约束是充放电互斥。物理上储能不能同时充电和放电。若只靠目标函数中的电价机制,通常不会出现同时充放,但为了防止数值优化产生无意义的解,最好显式加入互斥约束:
matlab复制z_bat = binvar(1, T);
Constraints = [Constraints, P_ch <= 500 * z_bat];
Constraints = [Constraints, P_dis <= 500 * (1 - z_bat)];
第四个约束是分布式电源的出力上下限和爬坡约束。出力上下限需要乘上启停变量:
matlab复制Constraints = [Constraints, u_dg * P_dg_min <= P_dg <= u_dg * P_dg_max];
爬坡约束表达的是相邻时段出力变化不能太大。这个约束在日前层很重要,在日内层如果时间分辨率变成15分钟,爬坡上限也要相应缩小。
第五个约束是备用容量。高比例可再生能源并网场景里,备用约束决定了系统能不能应对预测误差。向上备用约束可以写成:
P_dg_max * u_dg(t) + (SOC(t) - 0.1) * E_bat / dt + P_buy_max - P_buy(t) >= Reserve_up(t)
这个式子的物理意义很直接:分布式电源还能再发的部分、储能还能再放的部分、主网购电还能再增加的部分,三者之和必须大于系统要求的向上备用。
第六个约束是末端SOC约束。日前调度通常要求全天结束时SOC回到初始值附近,方便第二天继续运行。可以写成:
Constraints = [Constraints, SOC(T+1) >= 0.5 - epsilon];
Constraints = [Constraints, SOC(T+1) <= 0.5 + epsilon];
日内滚动窗口不要加太强的末端SOC硬约束,否则当前窗口第一时段的控制动作会被未来窗口末端目标绑架。我在日内层用的是软约束,让SOC末端靠近日前计划值,而不是强行归位。
4.3 目标函数的组装顺序
目标函数也不要一口气写完,而是分块加,方便后续检查。我通常这样组织:
matlab复制C_grid = sum(price_buy .* P_buy - price_sell .* P_sell) * dt;
C_dg = sum(a_dg .* P_dg.^2 + b_dg .* P_dg + c_dg .* u_dg) * dt;
C_deg = lambda_deg * sum(eta_ch .* P_ch + P_dis ./ eta_dis) * dt;
C_curtail = M_curtail * sum(P_curtail) * dt;
Objective = C_grid + C_dg + C_deg + C_curtail;
如果分布式电源成本是二次函数,而你想用线性规划求解,可以把二次项做分段线性化;如果直接用YALMIP加Gurobi这类支持二次规划的求解器,写成二次项也没关系。不过使用二次成本会让问题变成MIQP,求解速度明显变慢。复现论文时如果对成本曲线没有详细数据,我用分段线性近似的次数更多,因为它既能保持MILP结构,又方便换求解器。
求解设置方面,如果装了Gurobi或CPLEX,就用这些商业求解器;如果没装,Matlab优化工具箱里的intlinprog也能跑小规模例子。但要注意,intlinprog只接受系数矩阵形式,YALMIP封装之后求解器配置要改成:
matlab复制ops = sdpsettings('solver', 'intlinprog', 'verbose', 0);
sol = optimize(Constraints, Objective, ops);
if sol.problem == 0
P_buy_opt = value(P_buy);
SOC_opt = value(SOC);
else
disp('求解失败');
disp(sol.info);
end
4.4 三层滚动主循环框架
如果你已经写好了三个独立的求解函数,主循环的框架建议这样写:
matlab复制for k = 1:96
% 日内滚动:以最新的超短期预测为准
[intra_plan, info] = solveIntraday(pred_intra(:, k), dayAhead_ref, ...
SOC_init, k, T_window);
% 只执行第一个时段的结果
execute = extractFirstPeriod(intra_plan);
SOC_init = intra_plan.SOC(2);
% 进入实时层修正
real_adjust = solveRealTime(execute, measure(:, k), 5);
end
调用函数而不是把所有代码堆在主脚本里,是我做优化复现的重点习惯。多时间尺度模型的代码天然适合函数化:日前层一个函数、日内层一个函数、实时层一个函数,每个函数输出统一结构体。这样当你发现SOC没有正确传递时,不用从头翻几百行脚本找逻辑问题。
5. 结果解读:怎么验证你的模型真的在做“平衡”
Matlab把结果算出来之后,很多人第一步就是画SOC曲线、画功率平衡堆叠图,觉得形状对了就算复现成功。这其实不够。调度优化模型有一个隐藏风险:它可能在你没注意到的地方偷偷违反物理约束,或者目标函数里某个惩罚项权重过大,把结果带偏。
5.1 对比实验至少要设置四组
要说明“灵活性与储能成本平衡”的效果,单跑一组算例没有说服力。建议至少设置四组对比:
- A组:无储能参与,只靠分布式电源和主网购电
- B组:有储能,但不计衰减成本
- C组:有储能,且计衰减成本
- D组:完整多时间尺度调度,包含衰减成本和滚动修正
记录每组的总运行成本、弃风弃光率、储能累计等效循环次数、系统爬坡满足率。B组和C组对比能看出衰减建模对储能使用策略的影响;C组和D组对比能看出多时间尺度滚动修正带来的经济性改善。
我自己跑出来的典型结果往往是这样的:B组的总运行成本最低,但储能等效循环次数最高,如果算上寿命损耗后的综合修正成本,未必最优;C组的运行成本比B组略高,可是储能的透支程度大幅下降;D组的弃风弃
