高比例可再生能源并网这几年是所有做电力系统优化的人绕不开的话题。虚拟电厂(VPP)作为聚合分布式光伏、风电、储能、充电桩和可控负荷的运营模式,被反复拿出来当解药讨论。而这篇SCI复现的核心问题其实很朴素——高比例可再生能源并网之后,系统的灵活性需求和储能成本之间的矛盾,通过多时间尺度调度怎么去平衡。
先说结论:我完整复现并跑通了这套Matlab代码之后,最大的感受是,这个项目真正有价值的不是调度框架本身,而是把储能衰减建模塞进了优化目标的这一层设计。很多论文为了可解性,把电池当成一个无限寿命的“理想缓冲池”,结果调出来的方案在仿真里很好看,放到真实项目里储能两三年就被干退役了。这篇代码在成本函数里引入了衰减建模,让调度在“多用储能提供灵活性”和“少损耗电池寿命”之间做权衡。适合想用虚拟电厂做多时间尺度调度研究、准备SCI复现、或者做储能项目经济性评估的同行参考。
1. 为什么高比例可再生能源并网后调度会越来越难
1.1 灵活性的本质:系统要的是快速改变方向的能力
很多刚接触这个方向的读者会把“灵活性”理解为“调峰的能力”,这其实是个误区。调峰是慢变量,关注的是几个小时甚至一天内的电量平衡;而高比例新能源接入之后,系统最大的问题反而变成了短时间尺度上的快速波动——一片云飘过光伏电站,出力在几分钟内可能掉下来一半;风电的分钟级爬坡事件更是家常便饭。
灵活性在电力系统里的精确定义,是系统在给定时间范围内,利用可用资源响应功率变化的能力。这个定义里有三个关键维度:响应方向(向上还是向下)、响应幅度、响应时间。传统火电机组靠的是旋转惯性和锅炉-汽轮机系统的蓄热,响应速度天然受限;高比例新能源替代传统机组后,系统里能“快速改变方向”的资源变少了,这才是灵活性问题爆发的根源。
我复现这套代码时做了一组对比实验:不接入储能时,系统在新能源出力剧烈波动时段需要依赖购买高价备用容量来满足爬坡约束,单日灵活性不足事件发生了7次;接入储能并配合多时间尺度协调后,同样场景下灵活性不足事件降为0次,系统总成本反而因为减少了高价备用的购买而有所下降。这个实验很直观地说明了一个道理:灵活性不是免费品,但硬抗缺灵活性的代价,比主动配置灵活性资源的代价更高。
1.2 储能衰减成本如果忽略,优化方案会偏到哪儿去
储能在调度模型里是一种特殊的资源,它既是电源又是负荷,既能向上调又能向下调,但代价是每一次充放电循环都会消耗寿命。锂电池的衰减机理复杂,但工程上可以简化为一句话:循环深度越深、循环次数越多,容量保持率下降越快。
很多简化模型直接用“固定循环寿命”来约束储能,比如设定一个5000次循环的总寿命,然后按照调度次数去扣减。这种做法的问题在于,完全没有区分浅循环和深循环。实际物理世界里,一个20% DOD的浅循环对电池寿命的损伤,远小于一个80% DOD的深循环,两者不是线性比例关系。
再往细说,衰减还分日历老化和循环老化。日历老化跟时间走,电池就算放着不用,电解液也在缓慢分解,这在调度时间尺度内基本是个常数,可以不进入优化模型;循环老化则跟充放电行为强相关,必须建模。如果忽略循环老化对成本的影响,优化模型会有两个明显的偏差方向:
- 储会被过度调用:因为模型的判断逻辑里“用储能”只有充电效率和电价的成本,没有损耗成本,所以它的使用成本被严重低估了。优化器会疯狂让储能充放电,以此来平抑新能源波动,表面上看新能源利用率非常漂亮,实际上这套调度方案在真实场景里会快速摧毁电池。
- 不同调度策略之间缺乏横向可比性:两套方案,一套让储能每天做一次深循环,另一套每天做两次浅循环,在不考虑衰减成本的模型里成本几乎一致,方案评价就会失真。
我把衰减成本加进目标函数之后,调度策略产生了非常明显的变化:储能不再频繁满充满放,而是更多地在SOC中段做小幅度调节,同时系统会优先使用可调负荷的灵活性,因为负荷调节不产生老化成本。这个行为变化,说白了就是“给储能的使用上了价格”,让优化器自己学会了爱惜设备。
2. 虚拟电厂多时间尺度调度框架落地
2.1 三层时间尺度:为什么不能只做最细的那一层
理论上,如果我们能做到每分钟做一次全局优化,预测精度最高,调度效果也最好。但这套方案在实际工程中根本无法落地,原因有三层:
第一,预测精度跟预测时距强相关。日前预测风电出力的误差可以达到15%到20%,但滚动到未来2小时,误差通常能缩到5%以下。与其逼着模型去适应粗糙的预测,不如把决策窗口分层——远的时间用粗糙预测做方向性安排,近的时间用精细化预测做修正。
第二,计算规模和实时性冲突。把整个调度周期拆成分钟级决策变量,配上二进制变量表示机组启停和储能充放电状态,MILP模型的规模和求解时间会爆炸性增长。我在测试机上试过,一个96时段(15分钟间隔)的日前MILP模型,求解时间大约在20秒到2分钟之间,但如果把粒度提到288时段(5分钟间隔),Gurobi的求解时间直接飙到半小时以上,这在程序化调度中是不可接受的。
第三,决策执行的物理约束。日前要解决的是机组开停机、市场投标这些大且慢的决策;日内要解决的是各设备出力分配;实时要解决的是突发扰动下的紧急调整。三个层级决策频率不同,信息输入不同,如果强行全部放到同一时间尺度做,等于要让一台机组在五分钟内完成从启停到出力调节的所有决策,这在物理上也不可能。
这套框架的核心思想是“递进决策”——什么时间尺度做决定,取决于决策信息的更新速度和决策本身的可逆程度。
2.2 各层模型的决策变量、颗粒度与信息流
我复现的这篇SCI模型采用了经典的三层调度架构,每一层解决一个具体子问题:
| 调度层 | 时间颗粒度 | 优化视野 | 决策变量 | 输入数据 |
|---|---|---|---|---|
| 日前调度 | 1小时 | 24小时 | 机组启停计划、储能充放电计划、与主网交换功率计划 | 新能源日前预测、负荷预测、电价预测 |
| 日内滚动 | 15分钟 | 未来4小时 | 储能出力修正、可调负荷调节量、新能源出力调整 | 新能源超短期预测、实时电价、日前计划 |
| 实时调整 | 5分钟 | 未来15分钟 | 储能快速响应功率、切负荷指令 | 实测功率数据、频率偏差 |
每一层都有对应的数学模型。日前层用MILP求解,因为涉及机组启停这种离散决策;日内层可以放宽为连续优化,也可以用MILP,取决于可调负荷是否要建模成分段阶梯;实时层通常简化成线性规划或者规则控制。
层与层之间通过“边界条件”衔接:日前层的出力计划传给日内层作为参考值;日内层在跟踪日前计划的同时,根据超短期预测修正出力偏差;实时层在极短时间内执行应急调节,不必重新优化全局,而是基于当前状态做局部修正。我还注意到这套代码里有一个设计细节特别值得学习:实时层里加了爬坡惩罚项,避免储能被要求瞬间输出大功率——这个约束在物理上是保护设备,在数学上是让实时层的结果不会跟日内层偏差太远。
2.3 层间边界条件怎么传导,代码里如何体现
层间交互不是简单地把上一层的解塞给下一层当初始值。如果只是机械传递,日内优化的可行域可能会因为日前层做了不可行的决策而直接空掉。更好的做法是引入“松弛惩罚”机制:日内层偏离日前计划时,允许偏离但施加惩罚成本,而不是做硬约束。
这套代码在真实的工程处理上,边界条件的传导分两层处理。第一层,日前层的储能充放电计划解出来后,作为日内层的“基准运行线”,日内层输出对储能出力的修正量,修正量被限制在一个上下限区间内,区间宽度就代表允许的偏离幅度。第二层,日内层求解完成后,实时层读取最新的SOC状态,在这个基础上做5分钟级的调整,修复因为预测误差和日前计划偏差造成的净负荷不平衡量。
我在改代码时试过一种更快的执行方式:把实时层做成查表式的规则控制器,不重新求解优化模型,而是用简单的比例调节公式根据频率偏差和净负荷偏差直接计算储能指令。这种方式计算时间从秒级降到毫秒级,代价是策略无法保证最优,但在三层框架下,实时层的决策自由度本来就很小,规则控制器的次优性影响不大。如果你准备把这个项目做成实时闭环仿真,我建议你试试这种混合方案。
3. 储能衰减建模:把电池损耗算进取成本函数
3.1 三种主流衰减建模方式,哪些能塞进MILP
储能衰减建模在学术文献里有三个层次。电化学模型最精细,模拟锂离子在正负极之间的迁移、SEI膜生长等微观过程,精度高但包含大量偏微分方程,计算量对调度优化来说是灾难性的,只能离线做分析用。半经验模型折中,用阿伦尼乌斯公式描述温度对衰减的影响,结合循环次数和DOD进行修正,精度尚可但引入了指数非线性项,放进MILP会让模型变MINLP,求解难度直线上升。经验模型最粗放,直接从电池厂商的循环寿命数据出发,拟合出一条“DOD-等效循环寿命”曲线,用线性或分段线性函数做近似。
对调度优化来说,经验模型是唯一务实的选择。为什么这么说?因为MILP求解器要求目标函数和约束要么线性、要么分段线性,经验模型正好满足这个条件。衰减成本可以通过查表-插值-分段线性化的流程嵌入目标函数,Gurobi和Cplex都能高效处理。
具体来说,我推荐的建模路径是:先从厂商数据手册里拿到不同DOD下的循环寿命,整理成一张表,然后把“循环深度”映射成“等效循环次数”,再把等效循环次数乘以单次循环的老化成本系数,得到调度周期内的储能损耗成本。整个过程全部可以用线性函数实现。
3.2 从SOC曲线到等效循环次数:雨流计数实操
有了衰减模型之后,下一个问题是调度仿真的结果是一条逐时SOC变化曲线,怎么从这条曲线里提取出“做了多少次什么深度的循环”?这就要用到疲劳分析领域的雨流计数法(Rainflow Counting)。
雨流计数法的原理在这里就不展开数学推导了,你只需要知道它做的事情:把一条复杂的应力-时间曲线(对应我们的SOC-时间曲线),分解成若干个完整的循环和半循环,每个循环记录幅值和均值,并统计循环次数。这个方法从机械疲劳领域引入电池寿命分析后,成为估算电池等效循环寿命的经典手段。
在Matlab里实现简化的雨流计数,核心算法约30行左右。处理SOC曲线的步骤是:
- 提取SOC时间序列的峰值和谷值(转折点)。
- 把SOC曲线“压缩”成只包含极值点的序列。
- 按雨流规则配对“负荷循环”,记录每个循环的幅值(即DOD)。
- 根据DOD和厂商的循环寿命曲线,换算成等效全循环次数。
我在复现代码时遇到过一个问题:直接拿逐时SOC数据做雨流计数,会因为SOC的微小波动产生大量无效小循环。解决办法是设置一个5%SOC的“死区阈值”——SOC波动小于5%的振荡不计入循环,这能过滤掉绝大多数噪声循环,让等效循环估计更接近真实物理损耗。
3.3 一张表搞清不同DOD循环寿命与衰减成本系数
为了让衰减建模不变成停留在纸面上的“学术概念”,我整理了典型磷酸铁锂电池在不同DOD下的循环寿命参考数据,并以这些数据为基础计算了衰减成本系数。假设电池额定容量100 kWh、系统成本1200元/kWh、退役标准为容量衰减至80%,那么单次等效全循环对应的老化成本计算如下:
| DOD (%) | 循环寿命(次) | 等效一次全循环的占比 | 单次循环老化成本(元/kWh) |
|---|---|---|---|
| 100 | 3000 | 1.00 | 0.40 |
| 80 | 4000 | 0.60 | 0.24 |
| 60 | 6000 | 0.40 | 0.16 |
| 40 | 9000 | 0.25 | 0.10 |
| 20 | 15000 | 0.10 | 0.04 |
从上表能看出一个反直觉的事实:浅循环的单次老化成本明显低于深循环,但这不是线性关系。40% DOD的成本虽然是100% DOD的四分之一,但如果把循环次数换算成累计放电量——40% DOD循环9000次的累计放电量是360 kWh × 电池容量,而100% DOD循环3000次的累计放电量是300 kWh × 电池容量——浅循环单位放电量的老化成本其实更低。
这个表格是嵌入仿真模型之前做参数标定的重要依据。你在实际项目中如果用的是三元锂电池或者钛酸锂电池,曲线形状会不一样,但方法论是完全通用的。我把这个成本系数作为储能损耗成本的权重值写进目标函数后,系统会自动选择浅循环策略——等效于让调度算法学会了“养”电池而不是“杀”电池。
4. Matlab代码复现全流程拆解
4.1 数据预处理:负荷、新能源出力、电价的标准化
这个项目的代码结构是典型的“数据注入-模型求解-结果分析”三段式。数据部分用到的是某地区实测的负荷曲线、风电和光伏出力数据,以及分时电价。拿到原始数据之后,第一件事不是建模,而是把数据清洗成统一的格式:时间戳对齐到15分钟间隔、缺失值线性插值、异常值剔除。
时间戳对齐是这里最容易出错的坑。原始负荷数据可能是15分钟一条,新能源数据可能是10分钟一条,电价数据可能是1小时一条,如果不统一时间轴,后续做矩阵运算时就会因为维度对不上而报错。我的处理方式是先定义目标时间轴:1天的调度周期,15分钟间隔,共96个时段,然后把所有数据插值到这个时间轴上。这一步做完,后续所有的矩阵索引就都清爽了。
新能源出力数据我也做了归一化处理:出力值除以装机容量得到标幺值,方便在不同装机方案下做灵敏度分析。这里有个建议:保留一份原始数据不变,另外建一份标准化后的数据集,因为后面调试灵敏度分析时要反复对比不同归一化方式下的结果差异。
4.2 核心模型:变量定义、目标函数与约束的代码实现
这篇代码的核心调度模型我把它拆成了四个模块:变量定义模块、目标函数模块、约束条件模块、求解调用模块。
变量定义模块的核心是储能部分,它需要两组变元来表示充放电状态:
% 储能决策变量 p_ch = sdpvar(1, T, 'full'); % 充电功率,单位kW p_dis = sdpvar(1, T, 'full'); % 放电功率,单位kW u_ch = binvar(1, T, 'full'); % 充电状态标识,1表示充电 u_dis = binvar(1, T, 'full'); % 放电状态标识,1表示放电 soc = sdpvar(1, T+1, 'full'); % SOC状态变量,包含初始时刻这里用两个二进制变量分别标识充放电状态,是为了强制约束储能不能同时充电和放电。如果你只定义一个二进制变量,会出现一个设备同时输出正负功率的伪解。
约束条件模块里,SOC递推方程是最核心的方程:
% SOC递推约束 for t = 1:T constraints = [constraints, ... soc(t+1) == soc(t) + (eta_ch * p_ch(t) - p_dis(t) / eta_dis) * (dt / E_es)]; end功率平衡约束把各分布式能源、储能、可调负荷和主网交换功率拉通到同一个方程里:
% 功率平衡约束 for t = 1:T constraints = [constraints, ... p_pv(t) + p_wind(t) + p_dis(t) + p_grid(t) + p_load_curtail(t) ... == p_load(t) + p_ch(t) + p_load_flex(t)]; end目标函数这块,完整版本的成本项包括:主网购电成本、可调负荷补偿成本、新能源弃用惩罚成本、储能老化成本。前三个好理解,第四个需要单独说明。这篇SCI最值得学习的设计是把老化成本写成了每时段循环成本乘以该时段等效循环次数的形式,把“今天我让储能干了多少活”直接折算成钱,目标函数就会自动权衡“让储能出力省钱”和“用储能损耗钱”之间的性价比。
% 储能老化成本项 aging_cost = aging_price * sum(abs(soc(2:T+1) - soc(1:T)) * E_es / 2 / Q_total);这里的aging_price是每个等效循环对应的老化成本(元/kWh),sum(abs(dSOC) * E_es / 2)估算的是整个调度周期内的累计等效循环数。算法里用了SOC变化的绝对值之和除以2来估算循环次数——把SOC上升加下降看作半个循环,这是一种工程近似,但配合前面的雨流计数值做对比校验后,误差在可接受范围内。
求解调用模块用yalmip进行建模,求解器选择Gurobi或Cplex,我测试时Gurobi表现更好:
% 模型求解 optimize(constraints, objective, sdpsettings('solver', 'gurobi', 'verbose', 2));求解完成后,从结果结构体里提取SOC、各设备出力曲线,绘图对比即可。注意GPU和CPU的求解时间差异很大,建议把verbose关掉再跑批量灵敏度分析。
4.3 灵敏度分析怎么跑:衰减系数、预测偏差、电价波动
代码复现完成后,我的建议是不要急着改模型结构,先把三组灵敏度分析跑完,因为这三组分析能验证模型行为是否符合物理直觉,也能帮你确认衰减建模的嵌入是否真正起作用。
第一组是衰减成本系数的扫描。我把老化成本系数从0(完全不考虑老化)逐步提高到正常值的3倍,观察储能出力和循环次数的变化。结果符合预期:系数为0时,储能日均等效循环达4.2次,SOC经常深放电到20%以下;系数提高到正常值时,日均等效循环降到2.6次;提高到3倍时,进一步降到1.5次。这说明衰减成本确实在约束储能的调度行为。
第二组是预测误差的灵敏度。我分别给新能源出力预测加了5%、10%、15%的高斯噪声,对比三个调度层的成本表现。结果证实了多时间尺度框架的价值:只有日前调度时,15%预测误差会导致成本上升达28%;加了日内滚动修正后,成本上升幅度压缩到12%;再加实时调整层,进一步压缩到7%以内。三层架构相当于给系统了一个“预测误差缓冲垫”。
第三组是电价波动的冲击。用峰谷价差拉大和缩小两种场景做测试,发现价差缩小时,储能套利空间变小,系统会策略性地减少储能在低电价时段充电、高电价时段放电的行为,转而更多使用可调负荷。这说明模型在“储能套利”和“储能做灵活性服务”之间做了合理权衡,不会为了套利去消耗电池寿命。
5. 复现中的高频报错与排查实录
5.1 求解器太慢?先把模型撸瘦再调参
求解速度是复现这类项目最普遍的痛点。我刚开始复现时,96时段的MILP模型直接跑,Gurobi求解时间稳定在十几分钟级别,完全没法做批量场景分析。排查下来,问题出在“过度建模”上:给每个约束都整成了硬约束,再加上几百个二进制变量,模型规模失控。
我做了三处精简,求解时间从10分钟降到30秒以内。第一处是把不必要的二进制变量全部换成连续变量:储能充电/放电互斥约束如果通过两个二进制变量实现,可以改成在SOC递推方程里乘以效率系数,通过数学关系天然避免同时充放电。第二处是把机组出力建模从“分段线性”简化为“单段线性”,因为日内调度的场景里,机组很少运行在极端负荷区间,分段映射浪费了大量线性化变量。第三处是给所有时变变量统一用矩阵形式表达,避免在循环里堆约束,这能显著降低yalmip建模的时间开销。
如果做完这三处仍然很慢,优先考虑把优化视野缩短。日内调度模型不要整个4小时视野全部用15分钟粒度,前1小时用15分钟粒度精细刻画,后面3小时用1小时粒度粗放规划,一样能保证跟踪精度,求解速度还能翻倍。
5.2 无解或SOC末态漂移?多半是这三处出的问题
模型报infeasible时,很多人的第一反应是去看约束条件哪里写得不对,这种做法往往浪费大量时间。我更推荐的做法是从三个固定点位去查。
第一个点位是功率平衡约束。由于负荷、新能源出力、储能功率的量纲不统一,很容易出现MW和kW混用。我在排查时发现,有一处约束把电价的单位看成元/MWh,另一处把储能容量看成MWh,直接把功率平衡式子里的单位换算错了一位,导致模型无解。逐个约束检查数值量级是最快的定位方法。
第二个点位是SOC初始值设置不当。如果初始SOC设置过低,而日前调度恰恰要求储能先充电再做顶峰,就可能导致某些时段的SOC对应不上目标。简单处理方式是不固定SOC末值,改为给SOC末值设定一个软约束范围,比如要求末时段SOC不低于初始SOC的60%,用惩罚系数而不是硬约束,可解性会大幅提升。
第三个点位是二进制变量和连续变量的配合。储能充放电的互斥约束如果写成u_ch + u_dis <= 1,但当充电功率下限是0时,求解器可能因为数值公差问题判断u_ch和u_dis同时为1,产生“微小的同时充放电”伪解。解决办法是给互斥条件设置一个合理的容忍度,例如要求u_ch + u_dis <= 1.001而不是<= 1。
SOC末态漂移是另一个常见问题。如果SOC的递推约束用了浮点运算,T个时段累积下来数值误差会放大,导致仿真末态的SOC和计算值偏差超过几个百分点。解决办法是给SOC递推约束显式加上“最后一帧”的校验约束,或者用高精度格式保存SOC中间变量。我建议在后处理阶段再对SOC序列做一次全时段的数值平滑,避免画出来的曲线出现锯齿状。
5.3 三个硬指标校验结果是不是“真优化”
模型求解完了,不能看了图觉得“曲线好看”就说复现成功。复现过程中我对结果质量做了三个硬指标校验,也建议你按这个顺序检查。
第一,功率平衡校验。把每个时段的发电量(风电+光伏+储能放电+购电)和用电量(负荷+储能充电+可调负荷补偿)做差,验证每个时段是否逐点平衡。如果出现了不平衡,极大概率是某个时段的插值或者数据对齐出了问题。
第二,SOC物理范围校验。检查SOC全过程是否都在[0.1, 0.9]区间内。如果SOC越界,说明约束写漏了或者初始SOC不合理。这一步也顺带验证了“浅循环策略”是否真的生效——衰减成本高的场景下,SOC曲线应该明显比无衰减模型更平稳。
第三,经济性对比校验。把带衰减建模和不带衰减建模的两种结果放在一起对比,不带衰减模型的调度总成本会看起来更低,但把储能老化成本在事后折算回去之后,带衰减模型的总成本应该更低。如果这个对比关系不成立——也就是说带衰减模型的事后全成本反而更高——说明你的衰减成本系数设置得过低,需要重新标定。
这三个校验做完,才能说这套调度策略是真正可落地、可解释、经济上站得住脚的。很多复现项目卡在“图好看但数字对不上”的阶段,往往就是跳过了这些校验步骤。
我最后再分享一个经验:这套框架里最灵活的部分不是求解器也不是模型结构,而是衰减建模里那几张标定表。同样的代码,你把电池换成液流电池、把日历老化系数调高、把退役标准从80%改到70%,模型的调度行为会发生显著变化。我建议你在复现跑通之后,花几天时间做一轮电池参数的真实厂商数据替换,产出的仿真结果会比直接用论文里的默认参数有价值得多。