最近项目里要评估一个虚拟电厂调度方案的效果,文献读下来之后发现,真正难的不在调度的数学框架,而在储能系统容量衰减——大多数公开的调度模型默认电池容量恒定,但实际运行时每一轮充放电都在悄悄损耗寿命。我们这次要复现的这篇SCI论文,恰好把储能容量衰减、发电侧调节能力和多用户负荷的灵活性整合进了日前-日内多时间尺度调度里。整篇文章我会按复现的顺序来写,从模型分层到Matlab代码实现,再到求解过程中踩过的坑,给正在做同类课题的开发者一条能直接参考的路径。
如果你也是第一次碰虚拟电厂优化调度,可能最先被问倒的问题是:用户负荷到底怎么参与调度?储能到底算电源还是算负荷?容量衰减又怎么进入优化模型?这些问题论文里都有数学答案,但落到代码里往往又是一层窗户纸。这篇博客里我会把它们一层层捅破。
1. 为什么虚拟电厂调度要拆成日前、日内、实时三层
1.1 从"聚合"到"联动":虚拟电厂调度的核心矛盾
虚拟电厂的本质不是把一堆分布式资源物理连接起来,而是通过一个统一调度中心,把分散的燃气机组、风电、光伏、储能和用户侧可调负荷打包成一个对外可控的整体。这个"可控"听起来容易,做起来难,难在来源的响应速度完全不一样。
燃气机组响应以分钟级计算,储能响应可以到秒级,用户负荷则更像一个"慢变量"——你让一个工厂把某个工序挪到两个小时之后,它做得到;你让它两分钟内砍掉一大半功率,大多数生产流程不允许。所以虚拟电厂调度不能只建一套统一约束,而是要把决策按时间尺度分层,每一层处理不同响应速度的资源,再把层与层之间的计划衔接起来。
多时间尺度调度就是这个思路的自然产物:先做日前大计划,再做日内小修正,最后留一个实时反馈通道应付突发。分层之后,每个尺度上的变量少了、预测精度高了,模型求解难度也降下来了。
1.2 日前安排"怎么省",日内处理"怎么稳"
具体到我们复现的这篇模型,日前调度以1小时为一个时段,一共24个时段,决策内容是每台机组的启停状态、出力水平、储能的充放电计划、可转移负荷的搬移区间、可削减负荷的削减量。它依赖的数据是未来24小时的负荷预测和新能源出力预测,本质是在一组长期约束下求解一个经济性最优的基准方案。
日内调度则是滚动优化,通常每15分钟或1小时滚动一次,预测时域压缩到未来数小时。它不推翻日前计划,只修正日前计划对实时情况的偏差。举个例子:日前预测下午2点风电大发,所以安排储能尽量充满;但是下午1点实际风速低于预期,日内就会把充电功率调小,改为让燃气机组多出一部分电。这种"日前定框架、日内调偏差"的分工,是整套模型的核心逻辑。
还有一层实时反馈,是在更短的时间尺度上处理频率波动和设备扰动。论文模型里这部分往往做了简化,因为实时控制的重点已经转向自动发电控制和储能变流器响应,不是优化调度要解决的主要矛盾。能把日前和日内两层跑通,这套复现就算成功了大半。
1.3 复现一篇SCI调度论文,究竟要复现什么
很多第一次做复现的开发者会把注意力全放在算法上,比如是不是用了什么高级优化算法、是不是有AI加持。但大多数电力调度类的SCI论文,核心价值在模型而非算法——它定义了目标函数、约束边界、资源建模方式,算法往往就是混合整数线性规划配商业求解器。
这篇虚拟电厂多时间尺度调度论文也一样。你需要复现的三件事是:第一,目标函数里各项成本怎么组合;第二,储能容量衰减这个非线性、时变因素怎么被线性化并塞进约束;第三,发电侧爬坡、用户侧可调量怎么变成一组可求解的线性不等式。把这三件事在Matlab和Yalmip框架里翻译成代码,求解结果和论文数据对得上,复现就算完成了。
2. 储能容量衰减:模型里最容易"失踪"的成本项
2.1 电池不是永动机:三档衰减建模层次
我看过不少调度方向的代码,储能模块基本就是一组功率上下限加SOC转移公式,完全忽略寿命损耗。如果只是做72小时以内的短时经济调度,这种忽略问题不大;但虚拟电厂里的储能是日常反复充放的设备,一天两充两放很常见,容量衰减对长期运行成本的影响不可小视。
储能容量衰减的建模大致有三个层次:
| 建模层次 | 基本思路 | 求解难度 | 适用场景 |
|---|---|---|---|
| 第一层:忽略 | 储能容量恒定,寿命无限 | 低 | 短期仿真、教学示例 |
| 第二层:线性吞吐量模型 | 衰减量与累计充放电量成正比 | 中 | 大多数调度研究 |
| 第三层:非线性寿命模型 | 考虑放电深度、温度、倍率 | 高 | 电池热管理、长期规划 |
论文里通常采用第二层到第三层之间的模型:把每个时段的充放电功率折算成一个等效的容量损耗量,这个损耗量线性叠加进可用容量里。既保留了衰减效应,又不至于让模型变成非线性,求解器处理起来很轻松。
2.2 衰减成本怎么进目标函数,还不把模型逼成非线性
最简单的处理思路是给储能建立一个"寿命代价"账户:电池有一个总的等效循环寿命,比如某锂电池在标准条件下可以等效完成5000次满充满放,那么每一次充放电都在消耗这个寿命池。把储能更换成本除以总寿命损耗量,就得到单位损耗成本。
在代码里,我会在目标函数中给每个时段加上一项储能衰减成本$C_{deg}$,它正比于该时段的充放电量。这样优化器在决定是否让储能多充一次电时,除了看电价差或新能源消纳收益,还要扣除对应的电池损耗钱。有时候算下来,低价时段充电、高价时段放电的峰谷套利,可能还不够覆盖衰减成本,那么优化器就会理性地减少储能使用。
这个设计还有一个好处:它天然抑制了储能的"滥用"行为。很多模型跑出来储能曲线非常极端,要么满充要么满放,就是因为没有寿命成本约束。加入衰减成本后,储能出力会变得平滑,调度结果也更接近实际运行。
2.3 容量衰减和调度策略互相耦合:为什么会形成循环
容量衰减不只是一个成本项,它还会反过来改变系统的调节能力。电池容量衰减后,相同功率下的SOC变化更快,可用能量窗口变窄,系统对风电波动的消纳能力下降,于是需要燃气机组出更多力,碳排放和燃料成本上升。
我把这种耦合用一个简单的迭代过程来描述:某次调度让储能在高倍率下频繁充放→可用容量下降→下一轮调度发现储能调节空间变小→被迫让燃气机组多出力→总成本升高。如果不考虑衰减,你根本看不到这层连锁反应,也就无法解释为什么同一套调度策略在不同电池健康状态下结果差这么多。
复现论文时,我重点检查了储能模块的状态更新顺序:先更新容量衰减,再计算SOC,还是反过来。这个顺序对日内滚动优化的影响很大,下面第5节里会细说。
3. 发电侧和用户侧灵活性怎么塞进同一套优化约束
3.1 灵活性的"标尺":爬坡率、可调区间、响应时间
不同资源说自己是"灵活的",但衡量的标尺完全不同。发电侧灵活性通常看三个指标:最小技术出力、爬坡速率、最小启停时间。最小技术出力决定了机组能压到多低;爬坡速率决定了单位时间内出力能变多少;最小启停时间则限制了机组不能频繁启停。
用户侧灵活性的度量方式又不一样。一台工业电机可以让某道工序推迟运行,但总用电量不变,这属于时间平移型灵活性;一栋商业建筑的空调可以在用电高峰压低负荷,但只能压30分钟左右,这属于削减型灵活性。把它们统一写进约束,需要抽象成"功率边界+能量约束+时间窗口"的组合。
储能则介于二者之间:它在时间上可以把电从低电价时段搬到高电价时段,在功率上可以双向调节,在响应上比机组快得多,但没有电量再生能力,充进去多少放电时基本要还回来多少。建模上它跟可转移负荷非常像,只是方向相反。
3.2 多用户负荷不能简单相加:分组建模才是正解
初始建模时我踩过一个大坑:把多用户负荷直接加总成一条总负荷曲线,然后对总负荷做灵活性调节。结果运行出来的方案在数学上可行,但实际上没有任何一个用户执行得了。
原因是不同用户的约束边界根本不同。居民用户的可削减量可能只有空调负荷的20%,而且集中在傍晚;工业用户的可转移负荷动辄几百千瓦,但要提前几个小时通知;商业用户的可削减窗口又和工作时间强相关。把这些差异性约束平均到一条总负荷曲线上,等于把所有用户都按"平均人"处理,结果自然不可执行。
正确的做法是把用户按照可调特性分组,每组单独建一套约束,然后在母线功率平衡方程中把这些分组负荷相加。模型变量会多一点,但每组用户的灵活性约束能够被精确表达。考虑到现代求解器处理几千个变量的MILP并不困难,多花这点建模功夫非常值得。
3.3 储能是灵活性枢纽:把发电不确定和负荷波动连起来
我在模型里发现一个有意思的现象:如果把储能模块去掉,只剩下机组和用户负荷灵活性,系统基本上只能靠机组爬坡硬扛风电波动,遇到晚高峰风电出力下降的情况,必须用成本很高的快速机组顶上。加进储能之后,用户负荷侧的多余调节能力和发电侧的爬坡能力被储能连接成了一个整体。
具体到约束上,功率平衡方程变成了这样:在母线层,燃气机组出力加上风电出力,加上储能放电功率,减去储能充电功率,等于刚性负荷加上可削减负荷的实际值再加上可转移负荷的当前功率。储能在这里充当了缓冲池——当风电多、负荷少时,储能吸收多余电力;当风电少、负荷多时,储能释放电力。这比让燃气机组反复爬坡要经济得多,也比让用户频繁调整要更可操作。
4. Matlab复现的代码骨架:从参数表到求解器
4.1 顶层数据结构设计
复现的第一步是把论文里的系统参数搬到Matlab里。我用结构体数组来组织参数,方便后续统一引用。核心参数包括机组参数、储能参数、负荷参数和预测曲线四块。
%% 基础参数定义 T = 24; % 日前调度时段数(小时) N_g = 4; % 分布式燃气机组数量 N_user = 3; % 用户类型数量(居民/商业/工业) % 储能系统参数 E_cap = 2.5; % 储能额定容量(MWh) P_ch_max = 0.8; % 最大充电功率(MW) P_dis_max = 0.8; % 最大放电功率(MW) eta_ch = 0.95; % 充电效率 eta_dis = 0.92; % 放电效率 SOC_init = 0.5; % 初始荷电状态 SOC_min = 0.1; % SOC下限 SOC_max = 0.9; % SOC上限 rep_cost = 1200; % 储能更换成本($/kWh) total_life_loss = 5000; % 等效总寿命损耗量(MWh)这样设计的好处是,后边无论是修改场景还是做敏感性分析,都只需要改参数结构体,不需要动约束代码。
4.2 决策变量与目标函数怎么写
我使用Yalmip工具箱来定义变量和约束。机组启停用二进制变量,储能充放电状态也用二进制变量区分;储能充放电功率、用户削减量、可转移负荷量用连续变量。目标函数按照第2节的设计,把燃料成本、启停成本、储能衰减成本和负荷削减补偿都加进去。
%% 决策变量 P_g = sdpvar(N_g, T, 'full'); % 机组出力 u_g = binvar(N_g, T, 'full'); % 机组启停状态 P_ch = sdpvar(1, T, 'full'); % 储能充电功率 P_dis = sdpvar(1, T, 'full'); % 储能放电功率 soc = sdpvar(1, T+1, 'full'); % 荷电状态 L_curtail = sdpvar(N_user, T, 'full'); % 用户削减负荷 % 目标函数:燃料成本 + 启停成本 + 衰减成本 + 削减补偿 obj = fuel_cost + startup_cost + deg_cost + curtail_comp;求解时直接用optimize(Cons, obj)交给求解器。需要特别强调的是,储能衰减成本项一定要让Yalmip认出它是线性表达式,否则求解器会退回非线性求解,速度慢几个量级。
4.3 约束表达:功率平衡、SOC更新和容量衰减
功率平衡约束是整套模型的锚点,SOC更新约束是储能模块的核心,容量衰减约束决定了可用容量随时间变化。下面列出最关键的几行。
Cons = []; % 功率平衡 for t = 1:T Cons = [Cons, sum(P_g(:,t)) + P_wind(t) == ... P_load(t) + P_ch(t) - P_dis(t)]; end % SOC更新 for t = 1:T Cons = [Cons, soc(t+1) == soc(t) + ... P_ch(t)*eta_ch/E_cap - P_dis(t)/(E_cap*eta_dis)]; end % 容量衰减:每个时段损耗正比于充放电电量 for t = 1:T Cons = [Cons, deg(t) == alpha_ch * P_ch(t) + alpha_dis * P_dis(t)]; Cons = [Cons, E_avail(t+1) == E_avail(t) - deg(t)]; end这里alpha_ch和alpha_dis是容量衰减系数,分别对应充电和放电过程对寿命的影响。实际设置时,我通常让放电衰减系数略大于充电衰减系数,因为高倍率放电对锂电池的损伤更明显。
4.4 求解器选型与运行时间控制
这类多时间尺度调度问题是典型的混合整数线性规划,模型规模不算太大,但日内的滚动优化要反复求解几十次,求解器选型直接影响总耗时。我实测下来,Yalmip配合商业求解器(比如CPLEX、Gurobi这类)在中小规模系统上每次求解都在秒级以内。如果机器上没有商业求解器,可以先用开源的CBC顶一版,但求解时间会明显拉长。
另一个控制运行时间的技巧是给求解器设置MIP gap,比如设置相对间隙为0.5%。调度问题的解空间天然存在平台期,非要追求绝对最优不仅耗时,实际意义也不大。把gap放宽到0.5%之后,单次求解时间经常能缩短一半以上,而目标函数值的偏差可以忽略不计。
5. 复现途中踩过的坑:不可行解、数据不自洽、结果对不上
5.1 SOC边界与容量衰减更新顺序的坑
这个坑特别隐蔽。最开始我在代码里先更新了可用容量E_avail,然后在同一轮循环里用新容量去归一化SOC,结果SOC曲线出现跳跃——下午3点还是0.55,下一个时段直接跳到0.62,没有任何物理意义。
原因在于,SOC是一个相对量,它的分母是电池当前最大可用容量,分子是当前剩余电量。衰减导致分母小幅变小,但剩余电量本身没有突变。正确做法是SOC始终在标称容量E_cap上计算剩余能量的比例,衰减只影响可用的能量边界E_avail,不参与SOC归一化。把这两层解耦之后,SOC曲线就平滑了。
5.2 可转移负荷缺了能量守恒,出现"假灵活性"
另一个印象深刻的坑是可转移负荷约束。一开始我只加了每个时段可转移功率的上下限,没有加总能量守恒约束。结果优化器找到一个"完美"方案:把所有可转移负荷全部挪到风电最便宜的那几个小时,其他时段用户负荷几乎为零。目标函数确实更优了,但实际中没有任何用户会接受这一整天只在一个时段用电。
解决办法是在约束里加一条"转移前后总用电量不变"的约束:可转移负荷在一天内的累计功率之和等于它本来的累计功率之和。同时给可转移负荷设置一个时间窗口限制,比如工业用户只能在上午9点到下午5点之间转移负荷,避免优化器把负荷挪到半夜。
5.3 解不出来的时候,怎么快速定位是哪类约束的问题
MILP报"不可行"是最让人头疼的事情,几百行约束根本不知道哪条出了问题。我自己的排查套路是:先把所有二进制变量固定,让问题变成纯线性规划,看是否可行;如果可行,说明问题出在离散整数组合上。然后按资源模块分批注释掉约束:先放开储能约束,如果变得可行,说明问题出在储能SOC或充放电互斥条件;再放开用户负荷约束,如果变得可行,说明问题在负荷侧的削减或转移约束。
这个方法听起来笨,但效率极高。有一次我只花了十分钟就定位出问题:储能充放电互斥约束和SOC初始值设置冲突,SOC初始值设成了0.1,但充电功率下界和效率折算之后SOC必然低于0.1,约束直接互相矛盾。把初始SOC改成0.15,问题迎刃而解。
5.4 结果和论文对不上,先从参数表查起
复现者最焦虑的时刻是跑完结果一看,和论文表格里的成本差了一大截。这时候别怀疑模型错了,先检查三个地方:第一,负荷曲线和风电曲线的单位是否一致,很多SCI论文用MW,代码里可能写成kW;第二,机组最小技术出力是否在求解后被严格遵守,如果有约束漏了,机组会在低负载区间运行,成本自然偏低;第三,储能初始SOC你设的是多少,论文里可能是0.5,你设成0.9到0.1的充放空间完全不同。
把这三个检查做完,80%以上的"结果对不上"都能找到原因。如果还差一点,大概率是预测误差或用户负荷分类方式论文里没有完全说明,属于正常偏差,成本误差控制在3%以内就可以接受。
6. 复现之后还能干什么:算例扩展与敏感性分析
6.1 把单日调度扩展为典型日集合
论文复现完成后,我做的第一步扩展是把单日调度扩展为多个典型日集合。虚拟电厂调度最怕"挑日子",单一日的负荷和风电曲线再好,也不能代表系统在春秋、夏冬不同季节的表现。实际操作方式是把每个季节选2到3个典型日,分别求解,然后看储能衰减速度的季节差异和用户的灵活性资源在不同时段的贡献度。
这个扩展不需要改模型主体,只需要改输入数据和循环结构。我习惯写一个外层脚本遍历所有典型日,把每天的调度结果存成结构体,最后汇总统计分析。这样做的另一个好处是:如果目标函数里要加年度设备退化成本,直接基于典型日的运行天数加权即可,非常顺手。
6.2 参数敏感性分析:衰减系数改变了什么决策
我们用同一套基础场景,把储能的衰减系数从0调到基准值的1.5倍,观察系统总成本和储能利用次数。结果一目了然:衰减系数为0时,储能几乎每个时段都在充放电,曲线疯狂波动;衰减系数翻倍之后,储能只在收益最明显的高峰低谷时段动作,燃气机组承担了更多基础负荷。用户侧灵活性资源的调用频率在两种情况下也有差异——衰减成本高时,系统更多依赖可转移负荷去匹配风电,而不是靠储能吸收。
这种敏感性分析在论文里可能只是一个图表,但亲手跑一遍才能真正理解储能经济性对调度决策的牵引作用。如果你想从"复现"走向"改进",从这里切入是性价比最高的路径。
6.3 可以继续扩展的方向:容量规划与实时控制
复现模型本身是纯调度问题,但模块稍微改动就可以延伸到容量规划:把储能的额定容量E_cap变成决策变量,目标函数里加上储能建设投资成本,约束里加上投资预算上限,就变成了一个投资-调度联合优化问题。这一类问题近年来发论文的空间很大,模型基础却是我们现在打下的这套。
还有一个方向是把日内模型再往实时层延伸,加入储能变流器的响应时间约束和电压约束,让调度模型直接对接控制层。我后续计划用这套模型做滚动预测控制,把日内15分钟级别的调度指令直接下发给储能变流器,目前看模型框架基本够用,只需要在输出接口上多花一些功夫。
如果让我给正在复现这类模型的开发者一个建议,我会说:先别急着把模型建得复杂,先把储能容量衰减模块的单体逻辑写干净。把SOC更新和容量衰减解耦、把衰减成本写进目标函数、把用户负荷按组分组建模,这三件事做好,整个调度框架会稳很多,后面加什么扩展都顺理成章。多时间尺度调度的核心从来不是把模型堆得多大,而是让每个时间层上的决策都能真实反映物理设备的运行规律。