做虚拟电厂优化调度研究的朋友,一定体验过那种“图表能复现、逻辑也对得上,但结果就是不对”的崩溃。我第一次照着某篇SCI论文的多时间尺度框架去写Matlab代码时,光是日前调度和日内调度的衔接就调了整整一周,后来才意识到问题出在储能SOC的边界传递上。这篇博文就把我这段经历消化后的完整方案写出来:虚拟电厂为什么必须做日前和日内两个时间尺度的优化,两个尺度分别解决什么问题,模型怎么建,Matlab环境下用什么思路去实现,以及复现这类SCI论文时最容易踩的坑。无论你是刚接触虚拟电厂方向的研究生,还是已经在做电力系统优化、想快速把一个多时间尺度框架跑通的开发者,这篇内容都应该能帮你少走不少弯路。
我默认你具备基本的电力系统知识和Matlab操作能力,但我会尽量把每个环节的“为什么”也说清楚,方便不同基础的人都能顺着思路把代码搭起来。
1. 为什么虚拟电厂必须走“日前+日内”双时间尺度
1.1 单一时间尺度解决不了的两个矛盾
虚拟电厂本质上是一个聚合平台,把光伏、风电、储能、柴油机、可调负荷这些分散资源打包成一个整体,对外参与电力市场交易、对内协调各类设备的运行。问题在于,虚拟电厂面对的信息是分层的:明天的电价、明天的光伏出力、明天的负荷曲线,这些都是预测值,误差不可避免;而到了当天,光伏实际出力可能和预测差一大截,负荷也在不断波动。
如果只做一个时间尺度的优化,比如只做日前调度,把一天的机组出力和储能充放电计划全部定死,那么当天一旦出现预测偏差,整个计划就可能失衡,不是出现功率缺口,就是储能容量被提前耗尽。如果只做日内调度,完全不参考日前的开机安排,又会面临机组启停来不及、储能频繁深度充放电、整体经济性失控的问题。这两个矛盾,单一时间尺度都兜不住。
1.2 双时间尺度的分工逻辑:先定节奏,再微调动作
所谓“日前+日内”,本质上是一种分层决策思想。日前调度做的是“节奏安排”:提前一天,基于24小时或96个时段的分时电价、预测风光出力、预测负荷,决定哪些机组需要开机、储能何时充电何时放电、可调负荷在哪些时段被调用——这时候时间分辨率通常是1小时,因为日前阶段的核心诉求是全局经济性和机组启停的合理分配。
日内调度做的是“动作修正”:到了当天,滚动获取更新的光伏功率、负荷实测数据,以15分钟甚至5分钟为步长,对未来4到6小时进行滚动优化,根据最新的预测偏差调整储能出力、微调柴油机功率、决定是否追加切负荷。它不会轻易改变日出确定的开机状态,而是在开机计划已经定的前提下,做更精细的功率分配。
用一个生活化的比喻:日前调度像一个家庭月初做预算,确定这个月哪几天有大额开销、什么时候该存钱;日内调度则是每周根据实际开销调整本周的零花钱使用方式。预算不变,但具体怎么花可以弹性调整。
1.3 两个尺度的关键参数对比
| 对比维度 | 日前调度 | 日内调度 |
|---|---|---|
| 执行时间 | 前一天 | 当天滚动执行 |
| 时间分辨率 | 1小时(部分论文为15分钟) | 15分钟或5分钟 |
| 优化时域 | 24小时 / 96个时段 | 滚动窗口4-6小时 |
| 主要决策变量 | 机组启停状态、储能充放电计划、可调负荷调度 | 储能出力和各单元功率调整量 |
| 核心诉求 | 全局经济性、启停安排合理性 | 跟随最新预测、修正偏差 |
| 不确定来源 | 第二天的预测误差 | 超短期预测误差 |
理解了这个分工,才能明白后面模型里的目标函数和约束为什么那样设。很多复现者把两个尺度的代码混在一起,目标函数、决策变量全搅成一锅,结果自然对不上。
2. 虚拟电厂各单元建模:决定优化结果上限的细节
2.1 虚拟电厂的典型构成和建模思路
一个标准的虚拟电厂测试系统,通常包含光伏电站、储能系统、柴油机(或燃气轮机)、可调负荷四类可调度资源,外加与大电网的交互购售电。建模的好坏直接决定了优化结果的上限——模型太粗糙,结果没有参考价值;模型太复杂,求解时间又让人无法接受。复现SCI论文时,单元建模的核心是平衡“论文所用的精度”和“代码可实现的复杂度”。
我建议把每个单元单独封装成一套参数结构体,用Matlab的struct数组统一管理,这样写约束时索引清晰,后续改参数也不用到处翻代码。
2.2 光伏和风电的出力建模
光伏的功率输出通常采用环境预测直接给定出力序列的方式,而不是去建物理模型。也就是说,把光伏预测功率作为一个已知参数序列输入优化模型。公式上可以写为:
[ P_{PV}(t) = P_{PV}^{forecast}(t) ]
如果考虑不确定性的椭圆/区间场景,一般会在预测值上加减一个偏差带。日前调度用的是典型预测场景,日内调度则根据最新的超短期预测值更新这个序列。
风机的建模与光伏类似,区别在于风机出力波动幅度更大,爬坡事件更频繁,日内调度的修正压力也更大。在代码实现上,光伏和风电建模其实只涉及一个参数向量,但要注意单位统一:论文里经常混用MW和kW,一旦换算错误,所有结果都失真。
2.3 储能系统的关键约束
储能是整个虚拟电厂的“缓冲器”,也是模型中最容易出错的部分。核心约束有四个:
荷电状态(State of Charge,SOC)递推约束: [ SOC(t+1) = SOC(t) + \eta_{ch} \cdot P_{ch}(t) \cdot \Delta t / E_{rated} - P_{dis}(t) \cdot \Delta t / (E_{rated} \cdot \eta_{dis}) ] 其中 ( \eta_{ch} ) 和 ( \eta_{dis} ) 是充放电效率,( \Delta t ) 是时段时长(小时),( E_{rated} ) 是额定容量。
充放电功率上限约束: [ 0 \le P_{ch}(t) \le P_{ch}^{max} \cdot u_{ch}(t) ] [ 0 \le P_{dis}(t) \le P_{dis}^{max} \cdot u_{dis}(t) ] ( u_{ch} ) 和 ( u_{dis} ) 是0-1变量,保证不会同时充放电。
SOC上下限约束: [ SOC^{min} \le SOC(t) \le SOC^{max} ]
终端SOC约束,比如要求每个调度周期结束时的SOC恢复到初始值,这个在日前调度中尤其常见,保证储能不以耗尽电量换取短期经济性。
我见过很多人复现时把充放电效率写反,或者忘了乘以 ( \Delta t ),导致SOC曲线要么剧烈波动要么越界。这一步值得多花点时间核对量纲。
2.4 柴油机、可调负荷和购售电建模
柴油机建模相对传统:出力上限约束、爬坡率约束、最小启停时间约束、启动成本和燃料成本。燃料成本一般用二次函数近似:
[ C_{DG}(P) = a \cdot P^2 + b \cdot P + c ]
这个二次项在MILP里面需要做分段线性化,复现时要留意论文中给的是直接二次成本还是分段成本。
可调负荷分为可削减负荷和可转移负荷两类。可削减负荷的特点是特定时段可以少用电,但需要支付补偿成本;可转移负荷则要求总用电量在时间窗内守恒,只是用电时间平移。建模时前者用削减量作为决策变量并加入惩罚项,后者需要额外加入“转移后的负荷总和等于原负荷总和”的约束。
与外电网交互的部分,购买电力和售电通常分别设置价格和功率上限。复现时还要注意是否允许同时购售电——很多SCI论文设定了一个互斥约束,防止套利。
2.5 一套可参考的测试系统参数
下面是我复现时常用的一套典型参数,可以用作代码验证,单位统一用MW和MWh:
| 单元 | 参数 | 数值 |
|---|---|---|
| 光伏 | 额定容量 | 40 MW |
| 储能 | 额定容量 | 20 MWh |
| 储能 | 最大充/放功率 | 5 MW |
| 储能 | 充/放电效率 | 0.95 |
| 柴油机1 | 出力范围 | 5-20 MW |
| 柴油机2 | 出力范围 | 2-10 MW |
| 可调负荷 | 最大削减比例 | 30% |
| 电网交互 | 购电功率上限 | 50 MW |
这套参数配合典型的分时电价(峰、平、谷三段),基本能跑出一个符合直觉的结果:低谷充电、高峰放电、柴油机只在负荷峰值时增发。
3. 日前调度模型:全局经济性和机组启停的优化
3.1 明确决策变量和目标函数
日前调度的决策变量分为两类:0-1变量和连续变量。0-1变量包括柴油机的启停状态、储能的充放电状态、是否购电/售电;连续变量包括各时段的柴油机出力、储能充放电功率、可削减负荷量、购售电功率。
目标函数是所有成本之和最小,通常包含五块:
- 向外部电网购电的成本(减去售电收益)
- 柴油机的燃料成本和启动成本
- 储能设备的充放电老化折算成本
- 可削减负荷的补偿成本
- 失负荷的惩罚成本(如果允许失负荷的话)
写成数学形式大致是:
[ \min \sum_{t=1}^{T} \left[ C_{buy}(t) \cdot P_{buy}(t) - C_{sell}(t) \cdot P_{sell}(t) + \sum_{i} C_{DG,i}(t) + C_{ESS}(t) + C_{curtail}(t) + C_{loss}(t) \right] ]
这个目标函数的每一项在代码里都是一个数组,用YALMIP表达时非常直观。
3.2 约束条件的完整集合
日前调度的约束分五组:
- 功率平衡约束:每个时段光伏+风电+柴油机+储能放电+购电 = 负荷 + 储能充电 + 售电 + 可调负荷削减前的被削减部分(或者说削减后的负荷等于所有电源供电之和)。
- 机组运行约束:柴油机的出力上下限、爬坡约束、启停状态与出力逻辑。
- 储能约束:SOC递推、充放功率限制、互斥限制、SOC上下限、终端SOC约束。
- 可调负荷约束:削减量在0和最大可削减量之间。
- 电网交互约束:购售电功率上限及互斥约束。
这里有一个非常值得注意的点:为了给日内调度留出余地,日前调度往往会对储能的SOC设定一个“目标带”,而不是简单把终端SOC等于初始值。也就是说,让储能在日末落在比如40%至60%的区间里,这样第二天上午即便光伏预测不准,储能也还有足够的上下调节空间。这个细节是很多论文中“多时间尺度协调”思想的真正体现。
3.3 日前调度是典型的MILP问题
因为包含0-1变量,日前调度是一个混合整数线性规划(MILP)。复现时建议用YALMIP这个Matlab建模工具箱,配合商业求解器求解。建模时用binvar声明0-1变量,用sdpvar声明连续变量,约束写成一个set或约束数组,然后调用optimize。
下面是一段简洁的日前调度核心建模框架:
%% 日前调度建模框架(MILP) T = 24; u_DG = binvar(2, T, 'full'); % 2台柴油机启停状态 P_DG = sdpvar(2, T, 'full'); % 柴油机出力 P_ch = sdpvar(1, T, 'full'); % 储能充电功率 P_dis = sdpvar(1, T, 'full'); % 储能放电功率 u_ch = binvar(1, T, 'full'); u_dis = binvar(1, T, 'full'); SOC = sdpvar(1, T+1, 'full'); P_buy = sdpvar(1, T, 'full'); P_sell = sdpvar(1, T, 'full'); P_curt = sdpvar(1, T, 'full'); % 可削减负荷量 Constraints = []; % 功率平衡 for t = 1:T Constraints = [Constraints, ... P_PV(t) + sum(P_DG(:,t)) + P_dis(t) + P_buy(t) == ... P_load(t) - P_curt(t) + P_ch(t) + P_sell(t)]; end % 储能SOC递推、上下限、充放互斥...(略) Objective = sum(C_buy .* P_buy) - sum(C_sell .* P_sell) ... + sum(C_DG_fuel) + ... ; % 详细成本项自行补全 ops = sdpsettings('solver', 'gurobi', 'verbose', 2); optimize(Constraints, Objective, ops);这段代码只是框架,实际复现时要把所有约束写全。求解完成后,日前调度会输出一组完整的次日计划:各时段柴油机的启停和出力、储能的充放电曲线、购售电计划、可削减负荷的调用时段。这份计划就是日内调度的“参考基准”。
4. 日内调度:滚动修正和日前计划的衔接设计
4.1 为什么日内调度必须采用滚动窗口
日内调度的本质是模型预测控制(MPC)思想。所谓滚动窗口,是指在每个执行时刻,只对未来一段有限时域做优化,执行最前面一个时段的决策,然后等下一个时刻到来,配合新的实测数据,再把窗口向前滚动一步。这比一次性算完整日的日内计划要灵活得多,因为每次优化都能吸收最新的预测信息。
时间步长一般取15分钟,这样一天有96个时段。滚动窗口取4到6小时,也就是16到24个时段。对虚拟电厂来说,15分钟步长已经能覆盖大部分负荷波动和光伏波动的特征,5分钟步长计算负担会明显加重,除非论文特别要求,否则15分钟是复现时的合理选择。
4.2 日内调度的目标函数和日前计划传递
日内调度同样以成本最小为目标,但一个关键差异是:目标函数里必须加入“对日前计划的偏离惩罚”。如果完全不设偏离惩罚,日内调度可能会把储能的充放电方式彻底改写,导致日前制定的机组启停策略失去意义,虚拟电厂对外表现出的特性也会和申报给电网的计划不一致。
常见的处理方法是把日前计划的各单元出力作为参考值,在日内目标函数中加入偏差项,例如:
[ \min \sum_{t \in W} \left[ \text{成本项} + \omega_1 \cdot (P_{ch}(t) - P_{ch}^{DA}(t))^2 + \omega_2 \cdot (P_{dis}(t) - P_{dis}^{DA}(t))^2 + \omega_3 \cdot |P_{DG}(t) - P_{DG}^{DA}(t)| \right] ]
其中 ( W ) 表示滚动窗口内的时段集合,上标 ( DA ) 表示日前计划值,( \omega ) 是偏离惩罚权重。实际代码中,二次项在YALMIP里可以直接表达,求解器处理起来也很快。
还有一个容易被忽视的衔接点:日内调度开始时的储能SOC初值,必须取日前计划在对应时段的SOC值,或者取当前实测的SOC值。如果把SOC初始化成一个固定值,日内滚动几轮之后储能就会“漂移”出合理范围。
4.3 日内调度的执行流程
我把日内调度在代码里的执行步骤拆成六步,复现时可以直接照这个流程来实现:
- 初始化:读取日前调度结果,设定当前时刻,载入最初的实测负荷、光伏功率数据。
- 更新预测:用最新的超短期预测覆盖滚动窗口内的光伏和负荷序列。
- 构建优化模型:决策变量是窗口内各时段的储能出力、柴油机调整量、可削减负荷量,约束条件与日前类似,但窗口更短、执行时段的变量更多。
- 求解并执行第一个时段决策:把
optimize结果中第一时段的储能、柴油机出力指令下发给各个资源模拟执行。 - 推进窗口:当前时刻加15分钟,读取新的实测值,裁剪预测序列到新窗口。
- 重复第2到第5步,直到一天结束。
这种滚动逻辑用循环实现很清晰,但要特别注意索引的换算:日前调度是24个时段,日内调度是96个时段,两者之间的对应关系是t_da = ceil(t_intra / 4),错过这一步,日前计划根本取不准。
5. Matlab实现关键细节:从模型到可运行代码
5.1 环境与求解器配置
复现这类优化问题,我首推Matlab + YALMIP + Gurobi的组合。YALMIP负责建模,Gurobi负责求解MILP,两者配合在学术复现里最省心。没有Gurobi的话,Cplex或者开源的CBC也能跑,但求解速度会慢一些。
安装好以后,在Matlab里运行yalmiptest确认求解器被识别。Gurobi的licence需要单独申请,一般学术license覆盖大学场景,提前备好就行。
5.2 参数预处理和索引映射
代码的第一步不是建模,而是把参数整理成向量或矩阵。所有与时段相关的数据(负荷、光伏预测、电价)都要统一长度,日前为24,日内为96。我之前因为负荷向量长度没对齐,目标函数里出现维度不匹配,optimize直接报错,排查半天才发现是多了一个元素。
索引映射建议单独写一个函数或者一段注释清晰的映射语句:
% 日前时段到日内时段的映射 t_da = ceil((t_intra - 1) / 4) + 1; % 15min转1h5.3 储能互斥约束和SOC递推在代码中的实现
储能互斥约束有三种常见写法:直接把SOC递推关系写成等式约束;用0-1变量u_ch和u_dis分别约束充放功率;再加一个互斥不等式u_ch + u_dis <= 1。第三种写法最常用,也是在YALMIP里表达最直观的方式。
SOC递推约束在代码里写成:
for t = 1:T Constraints = [Constraints, ... SOC(1, t+1) == SOC(1, t) + ... (eta_ch * P_ch(1, t) - P_dis(1, t) / eta_dis) * dt / E_rated]; enddt要注意单位:如果时间分辨率为1小时,dt=1;如果是15分钟,dt=0.25。我一再强调这一点,是因为我见过不少复现代码里这里出错的案例,包括我自己早期也栽过一次。
5.4 求解参数设置与耗时预估
用Gurobi求解这类MILP,建议主动设置三项参数:
ops = sdpsettings('solver', 'gurobi', ... 'gurobi.MIPGap', 1e-3, ... % 相对MIP间隔 'gurobi.TimeLimit', 300, ... % 单次求解时间上限 'verbose', 1);对于上面那套测试系统,日前调度(24时段,2台柴油机+储能+可调负荷)通常几秒到几十秒内收敛;日内调度因为滚动执行96次,单次窗口16个时段,全部加起来也就几分钟。如果模型加了复杂的启停约束或分段成本线性化,耗时可能翻倍,此时适当把MIPGap放到1e-2也能接受,毕竟工程复现看重的是趋势合理而不是绝对最优。
5.5 结果输出和可视化
跑完之后,把日前和日内的曲线画在同一张图上对比:光伏出力的预测值和实际值、储能SOC曲线、柴油机出力曲线、购售电功率。日内曲线跟随实际场景调整的幅度、日前和日内SOC曲线的差异,这些直接展示了多时间尺度的协调效果。
建议把日前结果存成result_da.mat,日内模块加载它作为参考计划,这样两个尺度的代码可以分开调试,互不干扰,也符合模块化复现的习惯。
6. SCI复现过程的易错点、调试路线和经验总结
6.1 复现前先做数学公式清单,再写代码
复现SCI论文遇到结果不一致,九成问题出在“模型理解偏差”而不是“代码能力”。我复现时有一个固定习惯:第一遍看论文,把目标函数、每个约束的数学表达式、变量定义全部手工抄到一张清单上;第二遍对着符号逐项检查代码里的变量是否一一对应。特别是那些带有上下标的约束——储能SOC的时序索引、滚动窗口的边界时段——一旦错位,后面所有结果都会偏离。
尤其要注意论文中公式里括号、下标和求和范围的细节。很多顶级期刊的公式排版有隐藏信息,比如SOC约束里的t=1,...,T+1还是t=1,...,T,直接决定变量长度。
6.2 六个高频坑点排查表
| 症状 | 最可能原因 | 排查方式 |
|---|---|---|
| 模型无解 | 功率平衡约束与单元上下限冲突 | 检查光伏和负荷曲线是否匹配;放宽备用约束试跑 |
| SOC越界 | SOC递推公式里漏乘或乘错时间步长 | 手算前三步SOC变化,逐步比对 |
| 日内曲线和日前计划完全脱节 | 缺少偏差惩罚项或权重为0 | 检查目标函数是否加入参考值偏差项 |
| 储能同时充放电 | 互斥约束未生效 | 打印u_ch+u_dis序列,确认每时点不超过1 |
| 求解时间无法接受 | 二次成本项未线性化,或MILP规模过大 | 对二次成本做分段线性逼近;加大MIPGap |
| 结果与论文图差距大 | 参数单位或基数没对齐 | 检查MW/MWh、价格/成本系数量纲 |
6.3 调试路线:先简化再还原
不要一上来就复现完整模型。我的调试习惯是先跑一个“降级版”:把0-1变量全部去掉,让储能自由充放、柴油机自由出力,看看连续松弛后的问题能不能得到合理结果;能的话再逐层加回0-1约束、启停约束、爬坡约束、偏差惩罚。每一步加约束后都跑一遍,并对比结果变化。这样做的好处是出错时能精准定位到“是哪一组约束搞坏了模型”,而不是在完整模型里面对几十条约束无从下手。
另一个实用技巧是:单独写一个小规模数值测试,比如2个时段、1台柴油机、1个储能,手算最优解,然后看代码结果是否一致。这个测试用例跑通后,再扩展到24时段、96时段。看似多花了半小时,但能避免在复杂模型里反复试错。
6.4 结果合理性判据:如何确认代码跑对了
当代码跑通后,怎么判断结果真的合理?我总结了四个经验判据:
- 储能SOC曲线应该有明显的周期性:低谷时段充电上升,高峰时段放电下降,末端回到目标区间。如果SOC曲线是杂乱锯齿状,多半是效率参数或约束写错。
- 柴油机只在负荷峰值或光伏出力低的时段分配较高出力,平时保持较低出力甚至停机。如果柴油机全天满发,说明目标函数里燃料成本权重可能设错了。
- 购电高峰期与高电价时段重合,售电则正好相反。如果出现高电价时段大量购电,要么是功率平衡需要,要么是约束写错了。
- 日内调度相对日前调度的调整量不会突变:偏差惩罚的作用就是让日内曲线贴着日前计划走,只有在预测偏差大的时段才会明显偏离。如果日内全天大幅偏离日前计划,说明惩罚权重需要重新校准。
6.5 关于“复现”本身的一点看法
复现SCI论文和做自己的优化研究是两回事。复现的难点不在求解器,而在“把论文没写清楚的细节补全”。很多论文的附录里有详细的参数表,但也有一部分细节藏在图表里,需要靠观察图的曲线特征去反推约束设计,比如从SOC曲线初末值判断终端约束的类型,从柴油机出力阶梯判断爬坡约束的松紧。
我的建议是:拿到论文后先快速搭一个“最小闭环”,即用最简单的约束跑通流程,确认日前日内数据传递的路径没问题,再逐步替换成论文里的复杂约束。这个闭环一旦通了,后面所有的调试都有参照物,心理压力也会小很多。等你把一套多时间尺度框架完整跑通,后面换论文、换数据,基本就是改参数和微调约束的工作量了。