做虚拟电厂(VPP)调度优化的同学,应该都有过这种体会:看到论文里“日前调度+日内调度”的双层框架觉得思路很清晰,但真要自己在Matlab里复现,面对一堆时间尺度、约束条件和非线性项,往往不知道从哪下手。这篇博客,我想把一套复现过的多时间尺度虚拟电厂调度优化完整拆开来讲,包括日前和日内两个优化模型怎么设计、目标函数和约束怎么写、源荷不确定性怎么处理、以及Matlab+Yalmip实现过程中的关键代码骨架和调试心得。内容偏实操,适合正在做虚拟电厂、微电网优化调度课题的硕博生和工程师参考。
先说清楚这套框架到底解决什么问题。虚拟电厂聚合了光伏、风电、燃气轮机、储能和可调负荷,不同设备的时间响应特性差异很大——燃气轮机开机需要提前几个小时做计划,储能响应是分钟级的,而光伏和风电出力随时在波动。如果只做一次性的日前优化,没法应对第二天实际运行时的预测偏差;如果全部放到日内去做分钟级优化,又面临“短视”问题,只顾眼前经济性而偏离中长期计划。所以主流做法就是拆成两个时间尺度:日前做小时级的经济调度,确定机组启停和运行计划;日内做分钟级的滚动修正,跟踪日前计划的同时平抑功率波动。下面我按完整的复现流程来展开。
1. 多时间尺度调度框架设计思路
1.1 为什么非得拆成日前和日内两个尺度
在动手写代码之前,需要先理解两个时间尺度各自的职责边界,不然模型写到一半很容易混。
日前调度(Day-Ahead Scheduling)的定位是“计划层”。它基于第二天的负荷预测、光伏和风电出力预测,以系统运行成本最小为目标,把24小时划分成24个时段(典型分辨率1小时),求解出各机组每个小时的启停状态、出力大小、储能充放电功率、以及与上级电网的交互功率。因为分辨率较低、变量规模可接受,可以引入0-1整数变量来刻画机组启停,构建混合整数线性规划(MILP)模型求解。它回答的是“明天大致怎么运行”的问题。
日内调度(Intraday Scheduling)的定位是“修正层”。当时间推进到当天,最新一版的超短期预测(通常精确到未来4小时的15分钟间隔)已经拿到,日前计划里的预测误差和突发波动都暴露出来了。日内调度基于实时更新的数据,以更细的时间分辨率(典型15分钟)重新优化,修正机组出力、调整储能充放电,同时保证修正后的结果不会偏离日前计划太远。
这两层的关系不是独立优化,而是层级递进:日前调度给日内调度提供计划参考值,日内调度在跟踪参考值的基础上做经济修正。用通俗的话说,日前调度是“先定大方向”,日内调度是“方向盘微调”,二者缺一不可。
1.2 框架结构与信息交互流程
我自己复现时采用的是三层信息流结构,代码里也按照这个逻辑组织模块:
- 第一层:数据输入。包括24小时负荷预测曲线、光伏/风电预测出力曲线、分时电价、设备参数(燃气轮机容量/爬坡率、储能容量/充放电功率上限等)。
- 第二层:日前调度优化。输出日前计划矩阵,包含各时段燃气轮机出力、启停状态、储能SOC、联络线功率。
- 第三层:日内滚动优化。采用模型预测控制(MPC)思想,每15分钟触发一次,向前优化4小时(即16个时段),每次只执行当前时段的指令,下一时刻重新来过。
这套结构的核心逻辑是滚动时域(Receding Horizon):每次都用一个更新了最新预测数据的“滑动窗口”去求解,不是一次算完就结束。窗口长度取4小时是工程上比较常用的折中——太短(比如1小时)会让日内修正缺乏前瞻性,储能容易把电量耗尽;太长(比如8小时)计算量增大,且远期预测精度下降,参考意义不大。
注意:日前调度和日内调度的模型并不是完全割裂的,日内模型的目标函数中必须包含“对日前计划的跟踪”这一项,否则日内模型会发现偏离计划更“省钱”,进而造成两个时间尺度的结果互相矛盾。这个是很多初学者容易踩的坑。
2. 日前调度数学建模与求解要点
2.1 日前调度的目标函数与约束条件怎么写
以一个简化虚拟电厂为例,假设系统包含2台微型燃气轮机(容量500kW)、1套储能电池(容量1MWh、最大充放电功率250kW)、光伏阵列(装机800kW)、风电(装机500kW),并且可以与上级电网交换功率。日前调度的目标函数包含以下几项成本:
- 购电成本:从上级电网购电的费用,分时电价不同
- 燃气轮机燃料成本:出力越大成本越高,可以用分段线性函数近似
- 启停成本:机组状态变化产生的费用,属于固定成本
- 弃风弃光惩罚:为了让模型优先消纳可再生能源而设置的虚拟惩罚项
目标函数可以写成如下形式(这里只展示核心表达式,省略具体系数矩阵):
% Yalmip风格的目标函数示意(日前调度) Objective = sum(sum(Price_DA * P_buy)) ... % 购电成本 + sum(sum(Cost_MT(P_mt))) ... % 燃气轮机燃料成本 + sum(sum(StartCost * z_start)) ... % 启动成本 + sum(sum(ShutCost * z_stop)) ... % 停机成本 + Penalty_W * sum(sum(P_wind_curt)) ... % 弃风惩罚 + Penalty_S * sum(sum(P_pv_curt)); % 弃光惩罚约束条件这块,重要性排序如下(我踩过坑的地方都标了星号):
功率平衡约束:任意时刻,系统内所有电源出力加购电功率等于负荷加充电功率加卖给电网的功率。这是最基本的等式约束,写不好一定无解或者结果异常。
燃气轮机出力上下限与启停耦合约束:机组在停机状态下出力必须为0。这个约束必须借助0-1变量来表达,写成P_mt <= P_max * u_mt,其中u_mt是启停二进制变量。少了这个约束,模型会让停着的机组出力,结果完全不对。
爬坡约束:相邻时段出力变化受爬坡率限制,这和机组启停状态无关,是纯不等式约束。
储能约束:包括充电功率上下限、放电功率上下限、SOC状态递推关系。其中SOC递推必须写成:
% SOC递推等式 soc(t+1) == soc(t) - (P_ch * eta_ch - P_dis / eta_dis) / Cap_E * dt;同时要限制SOC上下限(一般不允许到0或满容量,留10%安全余量)。
2.2 混合整数线性化的几个关键写法
虚拟电厂模型本身是带非线性的(比如储能充放电同时产生损耗、燃气轮机效率随负载率变化),但为了用成熟的MILP求解器,需要做线性化处理。实际操作中我用到最多的三个写法:
一是充放电不能同时发生的约束。储能同一时刻要么充要么放,用互补约束P_ch <= M * u_ch、P_dis <= M * (1-u_ch),其中u_ch是0-1变量,M是足够大的正数。注意M不能取得太大,否则会引入数值稳定性问题,一般取储能最大功率即可。
二是燃气轮机分段线性成本。燃料成本本质上是一条凸曲线,用分段线性函数逼近,每段引入一个权重变量,权重之和为1且相邻段权重有SOS2约束。Yalmip里直接支持SOS2,但我建议初学者先用最简单的两段线性逼近,精度对研究场景完全够用,而且调试方便很多。
三是日前调度里的时间耦合。储能SOC跨时段传递是典型的时序约束,初学者容易写成只含当前时段的表达。正确的是把SOC作为状态变量在约束中连续传递,让能量在时间维度“流动”起来。这里我推荐用矩阵形式批量写约束,而不是for循环逐条添加,既能减少出错概率,求解速度也会快很多。
3. 日内滚动调度的动态修正机制
3.1 MPC滚动窗口与时域设置
日内调度模型的核心是如何设置滚动窗口。我采用的参数设置是:滚动周期15分钟,预测时域4小时(即16个时段),控制时域15分钟(即每个优化周期只执行第一个时段的结果)。整个日内运行过程持续24小时,相当于做了96次滚动优化。
这里有个实操细节:虽然控制时域是15分钟,但在每次优化中,这16个时段的控制变量都会同时求解出来,只是除了第一个时段外,其余结果会被丢弃。这种策略保证每一次决策都利用了最新信息,原理和自动驾驶里的MPC控制一模一样。
执行流程可以这样描述:
- 初始化:读取日前调度计划,将其作为日内调度的参考值。
- 数据更新:获取当前时刻的超短期负荷/光伏/风电预测值。
- 模型求解:以“尽可能贴合日前计划+运行成本最小”为目标,在16个时段上求解优化问题。
- 指令下发:只执行当前时段的那组控制指令。
- 滚动推进:窗口前移15分钟,回到步骤2重新循环。
3.2 日前计划跟踪项的权重怎么设计
日内调度目标函数由两部分构成:一是运行经济性(购电成本、储能损耗等),二是对日前计划的偏离惩罚。二者是矛盾的,需要用权重系数平衡。这个权重如果设得过大,日内模型就变成了“死板的执行器”,完全失去了修正预测误差的能力;设得过小,日内结果会大幅偏离日前计划,相当于日前调度白做了。
我实测下来的经验是:先在最简单场景下(预测误差为0)把权重调到基本不影响经济性优化结果的水平,再叠加误差场景微调。具体的取值经验:
% 目标函数中的跟踪惩罚项 Objective_Intra = Objective_DA_Ref ... % 经济性目标 + Lambda_track * ... % 跟踪权重 sum(sum(abs(P_mt - P_mt_DA_ref))) ... + Lambda_soc * ... % SOC跟踪权重 sum(sum(abs(soc - soc_DA_ref)));对于Lambda_track,取值为燃料成本系数的1/3到1/5量级比较合理——既能让机组出力朝日前计划靠拢,又保留对最新预测的响应能力。SOC的跟踪权重可以稍大一些,因为储能的跨时段耦合强,SOC跑偏容易导致后续时段无法调节。
实测提示:如果日内调度结果中燃气轮机出现频繁的启停切换,大概率是Lambda_track取值过小。试着增大跟踪权重,或者给启停状态也增加“相对于日前状态不变”的惩罚项,就能让日内出力曲线平稳很多。
4. 源荷不确定性与场景生成处理
4.1 场景生成与缩减的方法选择
多时间尺度调度的“两端”都受不确定性影响:日前调度用的预测与实际偏差大,日内调度用的超短期预测偏差小但仍存在。为了在优化模型中体现这种偏差,有两种做法:
第一种是确定性方法,用预测值直接参与优化,靠日内调度来不断修正,这是入门级也够用的方案,计算快、代码简单;第二种是随机优化方法,通过生成大量场景来覆盖不确定性范围,让优化结果对“可能发生的各种情况”更稳健。
我在复现时采用了随机优化的思路:用拉丁超立方采样对光伏、风电、负荷的预测误差进行场景生成,然后用同步回代消除法(或者直接k-means聚类)把上千个场景缩减成10个典型场景,让每个场景赋予一定概率权重参与优化。
场景缩减这个环节是必做的,直接跑上千个场景的MILP,求解时间会爆炸到无法接受。10个场景的规模在计算精度和时间上的性价比最高。缩减的原则是让缩减后的场景集与原场景集的概率距离最小,通俗讲就是保留那些差异最大、最具代表性的情况。
4.2 确定性优化与随机优化的效果对比
我分别跑过两种方案,结果差异很直观:确定性方案求解时间大约是1分钟级别,日内需要频繁大幅修正出力,燃气轮机的总启停次数偏多,运行成本偏高;随机优化方案求解时间会到5~10分钟甚至更久,但日内修正幅度明显变小,运行轨迹更平滑,总成本大约降低了3%~6%(取决于预测误差的标准差设置)。
从研究角度,建议论文中同时给出两种方案的结果,用对比数据说明随机优化的必要性。如果只是工程实现,确定性方案加上日内滚动修正已经完全够用,毕竟随机优化的收益在误差较大时才明显,误差小时反而增加不必要的保守性和计算成本。
还有个重要细节:随机优化的日前计划往往比确定性方案更“保守”——它会预留更多储能空间、设置更高的爬坡能力备用。这个在结果图上表现为SOC曲线更平缓,上下边界不会触顶触底,这其实是正确的稳健性表现。
5. Matlab代码实现与工程结构解析
5.1 工程文件结构与模块划分
这套代码如果只有一个脚本从头写到尾,调试会非常痛苦。我采用模块化工程组织,每个模块一个.m文件,主要有:
main_DA.m:日前调度主程序,加载数据、构建模型、调用求解器、保存结果。main_ID.m:日内滚动调度主程序,循环调用滚动优化函数。build_DA_model.m:日前调度模型构建函数,输入数据结构体,返回Yalmip优化问题。build_ID_model.m:日内调度模型构建函数,结构与日前类似。data/generate_scenarios.m:场景生成与缩减,输出各场景概率。plot_results.m:结果可视化,绘制日前/日内出力对比、SOC曲线等。params.m:集中设置所有参数,方便批量修改。
这里有一个设计经验:把模型构建函数和主循环分开是关键。因为日内调度每次滚动都要调用一次build_ID_model,如果模型构建过程里参数写死,测试不同窗口长度、不同跟踪权重时就要频繁改函数内部代码,非常容易被误改。用结构体统一传参后,主循环代码几乎不用变,只需要修改params.m里的配置。
5.2 核心代码骨架与关键参数设置
下面给出一段代码骨架,是日内滚动调度主循环的核心逻辑,可以作为自己实现的参考:
%% 日内滚动调度主循环骨架 horizon = 16; % 滚动优化窗口:16个15分钟时段(4小时) period = 0.25; % 时段长度:0.25小时 N_slots = 96; % 全天96个时段 x_opt_history = []; % 记录所有时段的控制指令 for k = 1:N_slots % 1. 更新当前时刻的超短期预测 P_load_pred = get_ultra_short_load(k, horizon); P_pv_pred = get_ultra_short_pv(k, horizon); P_wind_pred = get_ultra_short_wind(k, horizon); % 2. 构建日内优化模型 [model, sol] = build_ID_model(P_load_pred, P_pv_pred, P_wind_pred, ... params, DA_plan); % 3. 求解(Yalmip + Cplex/Gurobi) optimize(model.Constraints, model.Objective, sdpsettings('solver','cplex')); % 4. 只取第一个时段的控制指令 x_opt = value(sol.x); x_opt_history = [x_opt_history; x_opt(1, :)]; % 5. 更新系统状态(SOC等) params.initial_soc = update_soc(params.initial_soc, x_opt(1, :)); end线性规划求解器我推荐用Cplex或Gurobi,在Yalmip里配置非常简单。在求解器验证上,一个非常实用的建议是:先跑通一个没有整数变量的版本(即先假设机组始终在线),如果这个版本结果正确,再引入0-1变量。这样可以把“模型本身的非线性和我自己新增的整数建模错误”这两个因素隔离开来。我自己调试过程中80%的报错都是整数变量相关的逻辑错误,比如M矩阵取值太小导致可行域被错误压缩。
6. 复现过程中的常见问题与排查技巧
6.1 高频问题速查与解决方案
这里把我在复现过程中实际遇到的高频问题整理成一张速查表,排查方向都很具体:
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
| 求解器提示无解(Infeasible) | 功率平衡约束写错或约束过紧 | 先去掉与整数相关的约束,测试连续模型是否可行 |
| 储能全天不充电也不放电 | SOC递推约束没有跨时段耦合 | 检查SOC约束是否用for循环逐时添加,且初始条件是否传递 |
| 机组停机时仍有出力 | 启停耦合约束缺失 | 检查是否写了P_mt <= P_max * u_mt |
| 日内修正幅度剧烈抖动 | 跟踪权重过小或预测数据异常 | 调大Lambda_track,用平滑滤波器处理预测数据 |
| 求解时间超过10分钟 | 整数变量太多或M取值过大 | 减少场景数量、增大MILP的MIPGap、换Gurobi试一下 |
| 结果曲线出现明显的非物理跳变 | 分辨率转换错误 | 检查时段换算关系,1小时等于4个15分钟时段,系数换算务必正确 |
6.2 几个提高调试效率的实战技巧
最后一个部分分享几个调试技巧,这些在多数论文附录和代码注释里都不会写。
第一个技巧:分段验证法。把整个程序分成“只调日前”、“只调日内但不加跟踪惩罚”、“日内加上跟踪项”三个阶段,每完成一个阶段都画图检查一遍。这种分段验证法大大缩短了定位bug的时间,比一次性跑完再从头查要高效得多。
第二个技巧:结果可视化是排查问题最有效的手段,没有之一。把日前调度结果和日内调度实际执行结果画在同一张图上,偏差一目了然——如果两条线大幅分离,优先检查跟踪权重和数据更新逻辑;如果日内曲线出现毛刺,优先检查预测数据质量和模型约束设置。研究过程中,把所有优化结果画出来看一遍,比我对着数据看半天发现问题快太多了。
第三个技巧:一定要用真实场景测试边界条件。比如在光伏曲线为0的阴雨天场景下,观察系统是否开启了燃气轮机且储能是否正常充电;在负荷尖峰时段观察是否出现购电功率越限。边界场景能检验模型到底有没有“理解”物理逻辑,比看统计指标靠谱得多。
结尾:几点实际体会
整套框架从我最初开始复现到现在,前前后后跑了快两个月,中间推翻了三次建模方案。回头看,最深的体会是:多时间尺度调度的难点不在于某一层模型写不出来,而在于两层模型之间的参数传递和权重平衡——你自己的日前计划做得好不好,直接在日内修正幅度和总成本上体现出来。如果按从“框架理解-数学建模-场景处理-Matlab实现-调试验证”这样的顺序一步步推进,大部分问题都能在动手写代码阶段就规避掉。另外建议拿到任何复现项目时先自己手动推导一遍小规模算例(比如把24小时缩短成6小时、去掉一个机组),确认逻辑无误后再放大规模,这个习惯能帮你节省大量的排查时间。如果你也在做相关课题,希望这篇内容能帮你少走一些弯路。