这两年做虚拟电厂(VPP)优化调度,我前前后后复现过不少公开论文里的模型,最实用、落地价值最高的仍然是“日前调度 + 日内调度”这套双时间尺度框架。网上流传的版本很多,但真正把两层逻辑讲清楚、代码能直接跑通的却不多。这篇博客就用Matlab + Yalmip把整个思路拆开揉碎,从为什么需要两个时间尺度、两层分别怎么建模、代码里哪几个点最容易踩坑,到调试经验和结果怎么看,一次性写清楚。如果你是刚接触虚拟电厂调度的研究生,或者做能源互联网项目的工程师,这篇文章可以直接当操作手册用。
我需要先把话说透:所谓“多时间尺度调度”,本质上不是两个模型简单叠加,而是两套决策逻辑的配合。一个负责“明天怎么干最划算”,一个负责“接下来15分钟怎么干最稳”。想明白这个,后面建模型、写代码、调参才有方向。
1. 先搞清楚问题:虚拟电厂为什么需要两个时间尺度?
1.1 虚拟电厂到底聚合了什么
虚拟电厂不是一座电厂,它是把分散的分布式光伏、分散式风电、储能系统、可调负荷(空调、热水器、充电桩)甚至电动汽车通过通信和调度平台“虚拟”地聚合起来,作为一个整体参与电力市场或接受电网调度。
聚合的目的很简单:单体资源太小,没有市场话语权;聚合到一起,就能像一个大电厂一样去申报曲线、参与调峰、赚取峰谷价差。但问题也随之而来——不同资源的响应速度、可控程度、预测精度完全不一样。光伏看天吃饭,风电随机性更大,负荷也有波动。如果调度指令只下发一次,根本扛不住全天的不确定性。
1.2 日前调度和日内调度的分工逻辑
工业化一点的叫法,叫“两阶段优化”或者“滚动调度”。
日前调度(Day-Ahead Scheduling)在前一天做,时间分辨率一般是1小时,调度周期覆盖24小时。它用的输入是预测值——预测明天的负荷曲线、明天的光伏出力、明天的风电出力,以及明天的电价曲线。它的任务是:在这么多不确定性还没发生的时候,先把储能充放电计划、可调负荷的启停时段、与大电网的购售电方案定下来,保证全天经济性最优。
日内调度(Intraday Dispatch)在运行当天做,时间分辨率一般15分钟甚至5分钟,预测周期通常4小时左右,并且每15分钟滚动一次,用最新的实测数据和超短期预测去更新模型,重新求解。它的任务是:在日前计划给出的大框架下,修补预测误差带来的偏差,保证供需实时平衡。
打个比方:日前调度是“做预算”,日内调度是“记账调账”。预算做得再细,真花起来总有出入,那就每隔一阵对一次账,发现偏了就小幅修正。
1.3 为什么不能只用一个时间尺度
只做日前调度,风光预测误差会让实际运行时刻面临供需失衡——预测说中午光伏能出300千瓦,实际云遮日只出了200千瓦,那缺失的100千瓦只能临时买高价电,或者违反实际约束。只做日内调度,没有日前计划做全局指引,每15分钟只看眼前,储能很可能在上午就把电放光了,傍晚高峰无电可放,整体经济性会损失得很难看。
所以两个时间尺度的关系是:日前定全局基调,日内做局部修正;日前重经济性,日内重可靠性。这也是顶级SCI论文里最常采用的框架逻辑。
2. 数学模型:两个时间尺度分别怎么建模
2.1 日前调度的目标函数与约束集合
日前调度模型的决策变量分几类:储能充放电功率、储能充放电状态(二元变量)、与大电网的购售电功率、可调负荷的调整量、各分布式电源的出力计划。
目标函数写的是最大化全天运行收益,等价于最小化全天总成本:
min Σ [ C_buy(t)·P_buy(t) - C_sell(t)·P_sell(t) + C_OM·P_gen(t) + C_pen·|ΔP_load(t)| ]
其中 C_buy 是购电电价,C_sell 是售电电价(一般售电价低于购电价),C_OM 是分布式电源的运维成本,C_pen 是可调负荷调整的惩罚系数。
约束条件比较重要的是下面这些:
- 功率平衡约束:任意时刻,系统内所有电源出力 + 购电 + 储能放电 = 负荷 + 储能充电 + 售电。这是最基本的“电不能存着不用”的逻辑。
- 储能SOC递推方程:SOC(t+1) = SOC(t) + (η_ch·P_ch(t) - P_dis(t)/η_dis)·Δt / E_B
- 储能SOC上下限:SOC_min ≤ SOC(t) ≤ SOC_max
- 充放电功率上下限,以及充放电互斥约束(不能同时充和放,二元变量实现)
- 购售电互斥约束(不能同时从电网买电又卖电)
- 与电网交换功率的爬坡/上限约束
- 可调负荷的调节量上下限
这里最容易被忽略的约束是“购售电互斥”。如果不加这个约束,价差存在时模型会同时买入和卖出套利,结果就是出现大量毫无物理意义的功率流动。实操里我用两个二元变量联合约束:u_buy + u_sell ≤ 1,同时让购电功率上限乘以这个状态变量。
2.2 日内滚动调度的改进与目标
日内模型的长相和日前类似,但有三个关键差别:
第一,预测更准了。光伏和负荷改用超短期预测数据,15分钟一个点,未来4小时共16个点,滚动向前。
第二,目标函数变了。日内调度的目标不再单纯是经济性最优,而是“经济性 + 偏差最小化”的加权组合:
min Σ [ λ1·ΔP_buy² + λ2·ΔSOC² + λ3·C_rt(t)·P_rt(t) ]
其中 ΔP_buy 表示与日前计划购电曲线的偏差,ΔSOC 表示储能SOC相对日前SOC轨迹的偏移。这里的核心思想是:日前计划的SOC轨迹是“目标轨迹”,日内调度尽量跟随,但允许小幅度偏离,偏离要付出惩罚。λ 权重的取值,直接决定了日内修正的“激进程度”。
第三,约束上多了“与日前计划的耦合约束”。常见做法是让储能在日内每4小时滚动结束时的SOC保持在日前计划值附近,设置一个允许偏差带,比如 ±5%。
2.3 两个时间尺度的信息传递与耦合关系
耦合关系是这套模型里最容易理解错的地方。我的理解是:日前调度生成的储能SOC曲线,不是硬性约束,而是“参考轨迹”;日内调度的每次滚动,都必须知道当前时刻的SOC实测值,以及从当前到滚动末端的日前计划SOC值。
实际工程里,还会引入一个“联络线功率偏差惩罚”,即日内调度的购电功率尽量贴近日前申报的购电曲线,否则会被考核。国内电力现货市场对偏差电量有考核机制,所以这个惩罚项不是理论上的,是真金白银的成本。
耦合建模一句话总结:日前输出计划曲线,日内把它当成软参考(软约束),而不是死值(硬约束)。软约束用惩罚系数体现,这样模型才既有“大局观”又有“灵活性”。
3. Matlab实现:从模型到代码的落地细节
3.1 工具选型与环境配置
Matlab环境下做混合整数线性规划(MILP),主流方案是Yalmip工具箱 + Gurobi或CPLEX求解器。Yalmip负责把数学公式写成代码,Gurobi负责求解。如果没有学术版Gurobi,也可以用Matlab自带的intlinprog,但求解速度和稳定性会差一些,尤其在日内滚动需要反复求解的场景下。
环境配置这块,Matlab R2020以上版本都可以,Yalmip建议装最新版。装好之后跑一跑文档自带的例子,确认求解器能被Yalmip识别。识别成功有一个简单的判断方法:在Yalmip里调用optimize时,命令行末尾显示的是“Cplex”或“Gurobi”而不是“FMINCON”,就说明接上了。
3.2 数据准备思路
这一步决定了模型能不能收敛、结果合不合理,比写求解代码更值得花时间。需要准备四类基础数据:
- 时间序列数据:典型日的24小时负荷曲线、光伏出力标幺曲线、风电出力标幺曲线。没有公开数据时,用正弦叠加随机数合成即可,论文复现阶段完全够用。
- 电价数据:分时电价曲线(峰平谷三段)。峰谷价差越大,储能的套利空间越明显,运行结果也越好看。
- 设备参数:储能容量、最大充放电功率、充放电效率、SOC上下限、初始SOC、分布式电源装机容量、爬坡约束。
- 超短期预测数据:日内滚动用的“最新预测”,通常在日前预测基础上加一个随机扰动来模拟。
3.3 核心代码框架与关键代码片段
整个代码框架可以分成五块:参数定义区、模型构建函数、求解与结果提取、结果可视化、日内滚动循环。下面给出一段我实测跑通的日前调度核心代码框架。
%% 日前调度主程序 % 参数定义 T = 24; % 调度时段数 dt = 1; % 时间分辨率1小时 E_B = 600; % 储能容量 kWh P_ch_max = 120; % 最大充电功率 kW P_dis_max = 120; % 最大放电功率 kW eta_ch = 0.95; % 充电效率 eta_dis = 0.95; % 放电效率 SOC_min = 0.1; SOC_max = 0.9; SOC_init = 0.2; SOC_end = 0.2; % 末端SOC约束 % 决策变量 P_ch = sdpvar(1, T); % 充电功率 P_dis = sdpvar(1, T); % 放电功率 u_ch = binvar(1, T); % 充电状态 u_dis = binvar(1, T); % 放电状态 P_buy = sdpvar(1, T); % 购电功率 P_sell = sdpvar(1, T); % 售电功率 SOC = sdpvar(1, T+1); % 储能SOC P_curtail = sdpvar(1, T); % 弃光/弃风功率 % 约束集合 constraints = []; % 功率平衡:负荷+充电+售电 = 光伏+风电+放电+购电 constraints = [constraints, P_load + P_ch + P_sell == ... P_pv + P_wind + P_dis + P_buy]; % 储能SOC递推 for t = 1:T constraints = [constraints, SOC(t+1) == SOC(t) + ... (eta_ch * P_ch(t) - P_dis(t) / eta_dis) * dt / E_B]; end % 充放电功率限值及互斥 constraints = [constraints, P_ch <= P_ch_max * u_ch, P_dis <= P_dis_max * u_dis]; constraints = [constraints, u_ch + u_dis <= 1]; % SOC限值 constraints = [constraints, SOC_min <= SOC, SOC <= SOC_max]; % 初始与末端SOC constraints = [constraints, SOC(1) == SOC_init, SOC(T+1) == SOC_end]; % 购售电互斥 u_buy = binvar(1, T); u_sell = binvar(1, T); constraints = [constraints, P_buy <= P_grid_max * u_buy, P_sell <= P_grid_max * u_sell]; constraints = [constraints, u_buy + u_sell <= 1]; % 目标函数:购电成本-售电收入 objective = sum(C_buy .* P_buy) - sum(C_sell .* P_sell); % 求解 ops = sdpsettings('solver', 'gurobi', 'verbose', 1); diagnosis = optimize(constraints, objective, ops);求解完成后,用value(P_ch)、value(SOC)提取结果,画图。这里有个小技巧:SOC变量定义成T+1维度,是因为SOC函数中t+1的索引要取到第25个点,很多新手把SOC定义成1×T,运行报错“索引超出维度”,其实就是边界点没留出来。
3.4 日内滚动调度的代码结构
日内滚动是“循环套单次优化”的结构。外层循环从第1个时段滚动到第96个时段(15分钟间隔),每个循环内做一次短周期优化。
%% 日内滚动调度框架 H = 16; % 滚动预测时长 4小时 = 16个15分钟 for k = 1:15:96 % 每次滚动15分钟 % 截取当前时刻到未来4小时的预测数据 idx = k:min(k+H-1, 96); P_pv_rt = P_pv_true(idx) + 预测误差扰动; P_load_rt = P_load_true(idx) + 预测误差扰动; % 读取日前计划当前SOC轨迹基准 SOC_ref = SOC_dayahead(idx); % 构建当前窗口内的优化模型 % ... (结构类似日前模型,追加偏差惩罚项) deviate_SOC = sdpvar(1, length(idx)); constraints = [constraints, SOC_seq - SOC_ref <= deviate_SOC]; constraints = [constraints, SOC_ref - SOC_seq <= deviate_SOC]; objective_rt = objective_base + lambda_soc * sum(deviate_SOC); % 求解并只取第一个控制动作下发 optimize(constraints, objective_rt); P_ch_schedule(k) = value(P_ch(1)); end这里有个工程上非常重要的细节:滚动优化只管“最近一个控制周期”,不是整段指令全部执行。求解出未来4小时的最优曲线,只取第一个点的功率下发执行,下一个周期重新预测、重新求解。如果一口气把4小时全执行完,那不叫滚动,那就又变成开环了。
4. 调试实战:求解失败与结果异常的排查记录
4.1 模型不可行(Infeasible)的常见原因
这是最让人抓狂的问题。结合我的排查经验,按概率排序通常是这几种原因:
- SOC末端约束设置不合理。比如储能容量600 kWh,初始SOC只有20%,你却强制要求24小时后SOC必须回到50%,而全天总充电量根本不可能达到这个水平,必然无解。
- 充放电互斥约束写错,出现“强制充电”与“强制放电”同时发生。
- 功率平衡等式两侧漏了一项。最常见的是忘记把弃风/弃光功率纳入平衡,导致光伏大发时段无解。
- 购售电互斥的Big-M取值太大或太小。M比最大功率小得太多,直接切掉了可行域。
排错手法推荐“约束二分法”:先把所有约束注释掉,然后一半一半加回来,跑到哪一步报无解,问题就锁定在哪一步。这比盯着公式空想要高效得多。
4.2 求解慢的优化策略
24时段、单储能、单聚合商的小规模MILP,Gurobi通常秒解。真遇到求解慢的情况,往往是模型规模失控:
- 时段数太多:日内96时段,直接建单次大模型,几万个变量,确实会慢。解决思路是拆短窗,日内滚动窗口拉长到6~8小时即可,不需要一次解全天。
- 引入了二次项:SOC递推中如果充放电功率乘了效率,再加二进制变量,模型从MILP变成MIQP。这部分通常还能接受,但目标函数里如果还有购电二次项,速度会明显下降。我的经验是目前调度阶段没必要用二次目标,线性目标足够。
- 冗余二元变量过多:每一个连续变量都配一个二元变量,变量数直接翻倍。把“不需要同时启停的资源”合并成一组聚合机组的决策,能省不少事。
另外记得给求解器设置MIP Gap——比如ops = sdpsettings('gurobi.MIPGap', 0.01),允许1%的次优解。对工程应用来说,1%的偏差完全无感,求解时间却可能缩到原来的十分之一。
4.3 结果异常的排查清单
- 储能一整天不动作。检查峰谷价差:峰谷价差 × 充放电效率乘积,如果小于1,储能充放一次其实是亏钱的,模型当然选择不动。合理设置价差参数。
- 购电和售电同时出现很大的值。这是互斥约束没生效,查看二元变量状态约束是否真的写进去了。
- 日内SOC曲线完全脱离日前轨迹,波动非常大。说明偏差惩罚权重λ取太小,日前轨迹被完全忽略。将λ调大一个数量级再看。
- 日内购电曲线频繁跳变,产生“锯齿状”功率。原因是预测扰动参数随机性太强,而滚动周期又短。可以调整功率变化率约束,或者在目标函数里加一个平滑正则项。
5. 结果分析与应用扩展
5.1 日前与日内调度结果怎么对比分析
代码跑通之后,最要紧的工作是验证两层调度的协调效果。建议画两张图:
第一张,对比日前计划SOC轨迹和日内实际SOC轨迹。如果两层协调得好,两条线应该比较接近,日内线在日前线附近小幅波动;如果日内线大幅偏离,说明耦合关系没设好,λ权重需要调整。
第二张,对比购电曲线。日前购电曲线是前一天申报的,日内实际购电曲线是修正后的。两条线的面积差代表了实际运行与申报计划的偏差,这个值越大,说明预测误差越大或模型应变能力越差。有价值的观察是:日内调度应该把中午光伏低估带来的“临时购电”及时调整为“减少购电”,而不是盲目追着日前计划跑。
评价指标方面,我习惯同时算三个数:全天运行总成本、弃光弃风率、联络线功率偏差量。总成本看经济性,弃风弃光率看资源利用,偏差量看计划执行度。一篇论文里这三个指标摆出来,审稿人基本就能快速理解模型的有效性。
5.2 从基础模型到进阶模型的扩展方向
基础双时间尺度模型跑通之后,往上加东西的思路很清晰:
- 引入不确定性场景。日前调度用随机规划(Scenario-Based),把风光预测误差建模成多个典型场景,替代单一预测曲线。日内调度可以引入模型预测控制(MPC)的反馈校正思想,进一步降低误差累积。
- 加入碳交易模块。虚拟电厂聚合分布式光伏本身就是减排资源,把碳排放约束加进目标函数,或者让储能参与碳市场套利,这是近年论文的热点方向。
- 考虑多虚拟电厂博弈。多个VPP在同一配电区域内运行,彼此之间会存在购售电竞争与合作可能。两阶段模型扩展成双层模型(Leader-Follower),上层是配电网运营商的定价策略,下层是VPP的调度响应。
- 把日内调度的控制策略换成显式MPC,用离线计算的方式替代滚动在线求解,可以解决算力受限的工程难题。
扩展方向之所以值得做,是因为双时间尺度框架本身就是一个“骨架”,预测模型、市场机制、博弈关系、控制策略都是可以在固定骨架里替换“积木”的部分。基础代码的模块化程度越高(数据文件、模型构建函数、求解函数分离),扩展起来越轻松,这也是我强烈建议从开始就用函数封装而不是脚本平铺的原因。
关于这套框架最终想再啰嗦几句
完整复现并调试双时间尺度虚拟电厂调度模型之后,我最大的体感是:模型本身并不复杂,难点全在“耦合关系”的精细控制上。日前调度把计划定得太死,日内就没法纠偏;定得太松,日前调度几乎失去意义,两层直接变成了两个独立模型。要让两层真正协同起来,靠的是那个惩罚权重参数——它会直接影响储能工作轨迹、购电曲线形状、总成本和计划偏差。
所以我会在代码里把关键权重都提出来,统一放进一个params.m文件里,标注清楚每个参数的作用和推荐调整范围。每次调参时只改这个文件,方便又不容易出错。这是我跑十几个版本之后形成的习惯,强烈建议你也这样组织代码。
如果你准备从零开始复现这套框架,按这条路径走就对了:先跑通日前调度单层模型,观察储能是否在低电价充电、高电价放电;再跑通日内滚动框架,先不加入耦合惩罚,看日内计划会不会“放飞自我”;最后加上SOC跟踪软约束,调λ把两条曲线拉到合理距离。每一步都有清晰的验收标准,不会让人一头扎进去就出不来。