做虚拟电厂调度优化的人,十个里有九个前期都会反复纠结同一个问题:一个模型里到底该用多细的时间颗粒度。如果你正在用MATLAB复现顶刊论文里的“日前调度+日内调度”两个时间尺度框架,这篇文章应该能帮你把模型骨架、接口设计和代码落地一次理清。
虚拟电厂不是一台机组,而是一堆性格各异的设备被塞进同一个调度框里。光伏看天吃饭,储能响应快但容量有限,可调负荷得提前打招呼,电动汽车的充电行为又带着随机性。把所有资源都扔进一个24小时等间隔模型里,要么计算规模失控,要么预测误差直接毁掉计划。这正是很多论文会把调度周期拆成日前和日内两个尺度的原因:用大时间尺度定整体策略,用短时间尺度做局部修正。
这篇文章不会去照搬某一篇论文的公式,而是把两时间尺度调度优化项目从问题背景、数学建模、不确定性处理到MATLAB代码实现的关键环节都拆开讲一遍,同时把我复现过程中踩过的坑单独拎出来说明白。适合正在做毕业设计、论文复现,或者刚接手虚拟电厂调度项目的同学参考。
1. 一个调度模型拆成两个时间尺度,本质上是在解决“预测不可能全对”
1.1 虚拟电厂里的资源不是同一种脾气
虚拟电厂里既有光伏、风电这类出力随机性很强的分布式电源,也有储能、电动汽车这类响应速度快但能量受限的单元,还有空调、热水器、工业负荷这类需要提前协商才敢动的可调负荷。
面对这些性格各异的资源,如果你只做一个时间尺度的模型,无非两种结果:把分辨率做得特别细,比如全用5分钟一个点,一天288个点,设备一多,整型变量爆炸;或者把分辨率做得特别粗,比如一个小时一个点,结果日内实际运行时,光伏突然被云遮住,储能还没来得及调整充电计划,整个平衡就偏了。
所以最好的工程做法是把调度拆成两层:日前调度先把第二天的设备启停、购电计划、储能基点功率定下来,日内调度再根据最新的预测数据和实际状态,在更短的时间窗口里对功率分配做修正。这个过程很像做饭:上午先想好晚饭做几个菜,知道冰箱里大致有什么;到了傍晚开火前,再看着当前食材和锅里的实际情况,临时调整每道菜的用量和火候。
1.2 日前调度和日内调度,到底“分工”在哪里
我复现的那类顶级SCI框架,核心设计思想就在这里:日前模型求解的是“未来24小时应该怎么安排”,关注的是整体经济性和启停决策;日内模型求解的是“接下来1到4小时怎么改”,关注的是跟随最新预测、修正偏差、避免设备越限。
从数学角度看,日前模型是一天只解一次的大规模优化问题,日内模型是每隔一个控制周期就滚动求解一次的小规模优化问题。两者的变量类型、约束形式、目标函数都不完全一样,连求解器的设置也完全不同。
我把两套模型的定位整理成了一张表,方便对照:
| 对比维度 | 日前调度 | 日内调度 |
|---|---|---|
| 优化周期 | 24小时 | 未来1~4小时 |
| 时间分辨率 | 15/30/60分钟 | 5/15分钟 |
| 主要决策 | 启停、购电、储能基点 | 功率修正、负荷调整 |
| 预测误差 | 大,需要留安全裕度 | 较小,可以精确跟随 |
| 计算频率 | 一天一次 | 每个控制周期滚动 |
| 典型求解时间要求 | 分钟级以内即可 | 秒级到分钟级 |
这两层模型不是各算各的,它们之间存在非常明确的输入输出传递关系。很多人复现失败,问题往往不在模型本身,而在两层之间的接口没有设计好。
2. 数学建模:先搞清楚两个模型各自的边界和接口
2.1 日前优化:把明天24小时的粗骨架定下来
日前调度的目标函数在绝大多数论文里都写成一个最小化总运行成本的形式,我在复现时按常见的做法补全成了这样:
min Σt [ 购电成本(t) + 储能运行成本(t) + 可调负荷调节成本(t) + 机组启停成本(t) + 偏差惩罚(t) ]这里面的每一项都不是随便加的。购电成本对应虚拟电厂从上级电网买电的费用,储能运行成本用来约束电池不要频繁深度充放,可调负荷调节成本是为了体现对用户舒适度的补偿,启停成本是给燃气轮机组这类设备加的,偏差惩罚则是在目标函数里用一个很大的系数保证约束尽量满足。
日前模型的核心约束是每个时段的功率平衡:
P_grid(t) + P_pv(t) + P_dis(t) + P_wind(t) = P_load(t) + P_ch(t) + P_export(t)注意这里的P_export是虚拟电厂向电网倒送电的功率。有些复现代码不考虑倒送,直接把多余出力硬塞给电网,这在含高比例光伏的场景下会明显违背电网容量的物理限制,算出来的结果没有参考价值。
储能部分用线性模型描述,最常见的写法是:
SOC(t) = SOC(t-1) + [η_ch * P_ch(t) - P_dis(t) / η_dis] * Δt / E_bat 0 ≤ P_ch(t) ≤ P_ch_max 0 ≤ P_dis(t) ≤ P_dis_max SOC_min ≤ SOC(t) ≤ SOC_max这里必须注意单位。如果时间是以小时为单位,Δt是1;如果日内模型用15分钟,Δt就是0.25。很多人的SOC轨迹算着算着就漂了,绝大多数都是这一项的单位没统一。我习惯把所有功率统一成千瓦(kW),能量统一成千瓦时(kWh),时间统一成小时,调试起来会省很多事。
2.2 日内优化:在已定骨架上做局部修正
很多人把日内调度理解成“把日前模型换一套预测数据重跑一遍”,这是典型的误解。日内模型里必须出现一个日前模型没有的东西:相对于日前计划功率的偏差项。
我在复现时用的日内目标函数长这样:
min Σt [ 购电偏差成本 + 储能寿命损耗成本 + 可调负荷调整成本 + (各设备实际功率 - 日前约定功率)^2 的惩罚项 ]最后一项是关键。它表示日内优化不是完全自由的,它要尽量贴近日前已经定下来的策略。如果没有这一项,日内模型可能会为了让这一刻的电费最低,把储能从深度放电直接拉到深度充电,这种剧烈反转在工程上是不可接受的。
日内调度的功率平衡约束和日前结构相似,但多了每个设备的调整范围限制:
-Δ_max ≤ P_id(t) - P_da(t) ≤ Δ_max这个Δ_max不是常数,它取决于设备类型。储能的调节能力大,因为响应速度很快;可调负荷的调节范围小,因为它涉及用户舒适度;燃气轮机有爬坡约束,日内能调的幅度也有限。把每个设备的Δ_max设成不同值,就会比一刀切更贴近实际。
2.3 两个模型之间靠什么传递信息
这是整个复现过程中最隐蔽、也最容易出错的地方。根据我自己重做多轮的经验,日前和日内之间至少需要传递三类信息:
第一类是日前计划功率基线,也就是前一天为每个时段定好的各设备出力参考值。日内模型里所有“偏差项”都要以它为基准。
第二类是开机状态。如果日前模型决定某一台机组在凌晨两点是关着的,日内模型不可能让它突然在凌晨两点半启动,因为启动过程本身需要时间。这个状态通常作为日内模型的固定参数传入,而不是作为决策变量重新选择。
第三类是储能SOC的交接点。日前模型的结束时段SOC会作为日内模型初始时段SOC的参考值,但由于日内实际运行和预测有偏差,真实的SOC不可能和日前计划完全一致。我的做法是:日内模型第一个时段不强制SOC等于日前计划值,而是让SOC初值等于当前实测值,把日前计划的SOC轨迹作为软约束参考,不要一刀切硬锁死。
很多复现代码出现“日内模型无解”的问题,十有八九是SOC初值被硬编码成日前计划值,结果实际光照和负荷稍稍偏离预测,模型就找不着可行解。这个问题在后面求解器参数部分还会再提到。
3. 不确定性处理:场景法、区间法与滚动时域在代码里的落地
3.1 场景生成与削减:随机规划的常规套路
顶刊论文里处理光伏和负荷预测误差,场景法出现的频率非常高。基本思路是:假设预测误差服从一个均值为零的正态分布,围绕日前预测曲线生成大量场景,再用场景削减方法挑出少数几个代表性场景,最后把目标函数写成期望成本形式。
我在MATLAB里实现时的常用流程是这样的:先根据历史预测误差数据拟合标准差,一般来说,光伏出力预测误差的标准差在晴天小、阴天大,所以标准差本身也应该随时间变化,不能全程用同一个数。然后用蒙特卡洛抽样生成500到1000条场景,最后用聚类或快速前向削减法把场景数量降到10到20条。
代码骨架大致是:
nScen = 500; for s = 1:nScen P_pv_scen(:, s) = P_pv_forecast + stdPv .* randn(nT, 1); end [reducedScen, reducedProb] = fastSceneReduction(P_pv_scen, 10);场景削减这一步千万别省。直接把500条场景全部丢进优化模型,每个场景都会复制一份变量和约束,问题规模会膨胀到普通笔记本跑不动。我见过有人用100条场景做日前随机规划,求解器跑了几个小时还没退出,后来削减到15条场景,成本计算结果改变不到1%,求解时间缩短到了十几秒。
3.2 区间鲁棒法:适合日内模型的另一条路线
场景法适合日前这种一天才解一次的模型,因为计算时间要求不高。但日内模型是滚动求解的,每隔15分钟或1小时就要重新跑一轮,如果每一轮都带着场景法跑,计算压力很快就会上来。
更实际的做法是采用区间鲁棒优化:不对预测误差做概率建模,而是给光伏出力和负荷功率定义一个置信区间,在约束里加入一个不确定预算参数Γ,控制“最坏情况出现的程度”。在MATLAB里,区间法实现起来比场景法简单很多,因为它不需要复制场景,只需要把原来的确定性约束改写成带线性鲁棒项的形式。
区间法的问题在于保守性比较强。Γ取最大时,约束会针对所有误差同时取最坏值,成本会明显偏高;Γ取0时又退化成确定性模型。实际复现时可以把Γ作为一个可调参数扫描,画一条“成本-鲁棒性”曲线,这通常也是论文里展示结果的一部分。
3.3 滚动时域修正:最稳妥的工程化选择
我复现过不少论文之后的一个体会是:真正跑得稳定、代码可维护性又好的方案,往往是“日前随机规划 + 日内滚动确定性修正”。日内模型不追求用复杂的场景法或鲁棒法处理不确定性,而是每到一个新时刻,更新最新的光伏短临预测,基于当前实际状态重新求解一次确定性问题,只执行第一个控制动作,然后滚动到下一步。
这种思路对应到代码里,就是一个for循环嵌套,外层是时间推进,内层是求解短时优化。篇幅有限,这里先给一个最简骨架,后面的章节我还会展开讲细节。
xDA = solveDayAhead(daProb); % 日前求解一次 for k = 1:K updateForecast(); % 更新短临预测 idProb = buildIntradayProblem(xDA, k); % 搭建日内模型 xID = solveIntraday(idProb); % 求解日内模型 applyFirstControl(xID); % 执行第一个控制量 end很多人做日内滚动时,会在代码里把每个周期都当成独立问题来搭模型,结果每构建一次约束矩阵就要循环几百行,效率极低。正确做法是把日内模型的约束结构保持不变,只更新一部分参数矩阵,这样既能避免重复建模,也方便调试。
4. MATLAB代码实现的关键环节:矩阵、求解器与参数传递
4.1 先把所有设备参数整理成一个结构体
复现这类论文,代码的整洁度决定你能不能活过调试期。我强烈建议在项目最开始就定义好一个结构体,把所有设备参数、价格参数、预测曲线统统放进去,后面所有建模函数都从这个结构体取数。
我常用的开头是这个样子的:
vpp.nBus = 5; vpp.nT = 96; % 15分钟一个点,一天96点 vpp.dt = 0.25; % 单位:小时 vpp.PchMax = 500; % 储能最大充电功率,kW vpp.PdisMax = 500; % 储能最大放电功率,kW vpp.Ebat = 2000; % 储能容量,kWh vpp.socMin = 0.1; vpp.socMax = 0.9; vpp.socInit = 0.3;这里有个经验:所有参数尽量统一单位,功率用kW,能量用kWh,电价用元/kWh。如果从原始论文里抄参数时没有注意单位,经常会遇到量级差10的3次方的情况,结果完全对不上。
4.2 用基于问题的建模API替代手拼矩阵
很多老教程喜欢用MATLAB传统的求解器接口,把目标函数和约束手动拼成f、Aeq、beq矩阵。对小型问题这没问题,但两个时间尺度模型一结合,变量动辄几百上千,手拼矩阵很容易在索引上出错,而且代码基本不可读。
我复现时用的是MATLAB自带基于问题(Problem-Based)的建模方式,声明optimvar变量,用表达式直接写目标函数和约束,然后交给求解器转换。核心骨架大致如下:
nT = 96; Pch = optimvar('Pch', nT, 'LowerBound', 0, 'UpperBound', PchMax); Pdis = optimvar('Pdis', nT, 'LowerBound', 0, 'UpperBound', PdisMax); SOC = optimvar('SOC', nT, 'LowerBound', socMin, 'UpperBound', socMax); uOn = optimvar('uOn', nT, 'Type', 'integer', 'LowerBound', 0, 'UpperBound', 1); prob = optimproblem('Objective', objExpr); prob.Constraints.balance = Pgrid + Ppv + Pdis == Pload + Pch; prob.Constraints.socDynamic = SOC(2:nT) == SOC(1:nT-1) + ... (etaCh * Pch(2:nT) - Pdis(2:nT) / etaDis) * vpp.dt / vpp.Ebat;用这种建模方式,约束的名字清晰,变量索引出错时MATLAB会给出更直观的提示,代码交给别人接手也能看懂。需要注意的是,整数变量数量不要滥用。如果只是储能和可调负荷,不需要全设备都上整型变量;只有启停状态、档位选择这类问题才需要,因为整型变量一多,混合整数线性规划的求解时间会指数级上升。
4.3 日内滚动时域的代码骨架
日内滚动优化的完整流程,我会在代码里维护一个主循环,每个周期重新求解。关键点是“预测数据必须跟着当前时刻走”,不能把一整天的预测提前读入后就不管了。
DA = buildDayAheadProblem(vpp, forecastDA); xDA = solve(DA); optionsId = optimoptions('intlinprog', 'MaxTime', 120, 'Display', 'off'); for k = 1:96 [pvId, loadId] = updateShortTermForecast(k, vpp); ID = buildIntradayProblem(vpp, xDA, pvId, loadId, k); [xID, fval] = solve(ID, optionsId); if ineqFlag(xID, xDA, k) warning('时段 %d 偏差越界', k); end applyControl(xID.Variables, k); end这里有个很多人都会忽略的问题:日内模型的变量窗口长度。短视窗口如果只取当前时段一个点,优化结果容易发生“抖振”,也就是这个时刻充电、下一时刻放电,来回切换。如果窗口取未来8到12个时段,储能动作就会平滑很多。我在复现时习惯取未来4小时,也就是16个15分钟点作为日内优化窗口,既保留了前瞻性,又不会把扩大后的预测误差引进来。
4.4 求解器参数:别小看这些“暗坑”
MATLAB自带的intlinprog、linprog完全足够跑通小型算例。但默认参数在混合整数问题上经常表现不佳,需要主动设置。
options = optimoptions('intlinprog', ... 'MaxTime', 300, ... 'IntegerTolerance', 1e-5, ... 'ConstraintTolerance', 1e-6, ... 'Display', 'iter');我复现时踩过不少坑,可以集中说一下:
第一,日内模型求解时间必须控制住。滚动时域每15分钟就要解一次,如果求解器一次要跑5分钟,整个系统就废了。解决思路是先把整型变量尽量留在日前模型中,日内模型尽量做成线性规划,实在必须带整型变量时,给MaxTime设一个硬上限,到时间就接收当前的可行解。
第二,目标函数里的二次惩罚项会拖慢求解。如果论文里用的是偏差平方项,在intlinprog里是不支持的,因为那是二次函数。我的做法是把平方项分段线性化,用两到三段折线逼近,这样既能保持整型规划的高效求解,又能近似表达成本曲线的弯曲特性。
第三,惩罚系数必须拉开量级。正常情况下,偏差惩罚项的系数应该比正常运行成本高一个数量级以上,否则求解器会优先选择牺牲功率平衡而不是花钱调整,结果解出来的方案在物理上是不可执行的。
5. 复现之后怎么判断模型有没有跑对
5.1 先从零误差场景开始
拿到别人论文里的代码或者自己刚写完的代码,第一件事不是直接喂真实数据,而是先构造一个“完美预测”场景:光伏预测等于实际值,负荷预测等于实际值,没有任何偏差。
在这种条件下,日前调度和日内调度算出来的结果应该非常接近。如果这时候两者的结果还是差得离谱,说明两层模型之间的接口信息没有传对,比如日内模型少了日前基线约束,或者SOC初值设置错了。
这个检查步骤特别重要,因为它把“模型逻辑错误”和“不确定性处理效果”两件事彻底分开。只有先保证零偏差情况下模型自洽,后面加入预测误差时,结果差异才能合理归因于滚动修正的功劳。
5.2 观察储能SOC的“日终止条件”是否被病理化
在两时间尺度模型里,储能SOC是最容易出问题的变量。日前模型通常会给SOC加一个日终止条件,比如当天结束时SOC要回到初始值0.3,这是为了保证储能可以连续运行。但日内模型如果也对短时窗口末端的SOC做同样的硬约束,就会让每个滚动周期都试图把SOC拉回同一个值,导致系统完全失去跨时段调节能力。
我的经验是:日前模型加日终止硬约束,日内模型不加,只加SOC上下限约束。如果想让日内模型的末端SOC也有方向性,可以加一个非常软的惩罚项鼓励它靠近日前计划值,而不是硬性等于。
5.3 用结果反推设计价值
我复现这套框架之后,会刻意做一个“只做日前调度、不日内修正”的对照组,用来评估日内修正带来的收益。这里拿我自己构造的小型测试系统举例,数值不来自真实系统,只是用来说明问题:
| 方案 | 运行成本 | 功率平衡平均偏差 |
|---|---|---|
| 仅日前调度 | 100.0 | 6.8% |
| 日前+日内滚动 | 103.5 | 1.2% |
有人看到日内调度反而提高了成本,会觉得模型有问题。其实不是。成本略高是因为日内修正需要用储能和可调负荷去追平预测误差,这部分调节动作会带来额外的寿命损耗和舒适度补偿费用。真正的收益体现在功率平衡偏差从6.8%降到1.2%,也就是系统运行的可靠性和电能质量大幅提升。两时间尺度的价值从来不是单纯省钱,而是在几乎不损失经济性的前提下,大幅提高对预测误差的适应性。
5.4 复现时的完整自查清单
最后把我调试这类代码时常用的清单列在下面,每一条都是我实际踩过坑之后总结出来的:
- 单位统一:功率、能量、时间单位是不是全局一致;
- 功率平衡约束:有没有把倒送电网的功率写进去;
- SOC动态:时间步长的Δt有没有套错;
- 日内偏差约束:是否对每个设备都给了合理的调节范围;
- 惩罚系数:是否为偏差项设置了足够大的权重;
- 整数变量范围:是否出现过变量爆炸,能否把部分整型变量移到日前;
- 求解器选项:是否给MaxTime、Tolerance设置了合理值;
- 零偏差自检:完美预测情况下两层模型结果是否基本一致。
这八条如果都能通过,整个框架基本不会在复现环节出大问题。
6. 做完这轮复现,我最大的感受
关于两时间尺度虚拟电厂调度,我最大的感受是:很多人一开始把注意力全放在“怎么把优化模型写出来”上,最后发现真正难的地方反而是那些论文里一笔带过的连接细节。日前模型和日内模型的边界怎么切,SOC怎么交接,调节能力怎么分配,偏差惩罚怎么定,这些才是决定代码能不能真正跑起来、结果能不能站得住的关键。
我踩过一次挺深刻的坑:第一次做日内滚动时,为了图省事,直接把日前模型的全部约束复制到日内模型里,再叠加上偏差限制。结果每一轮求解都出现“互相矛盾的约束”,要么SOC越界,要么功率平衡对不上,到最后才发现是因为我把日前计划里的开机状态当成了日内模型的可变变量。后来把开机状态固定为日前结果后,模型瞬间就稳定了。这件事让我养成了习惯:每次改动两层模型的接口,都要先画一张简单的表格,列清楚哪些变量是日前传给日内固定的,哪些是日内重新决策的,哪些是两个模型共享的参数。别嫌麻烦,这个表能在后面调试时省下大量时间。
如果你正在复现类似框架,我的建议是不要急着堆数据规模。先用一个设备数量少、时段数少的小算例把两层模型跑通,确认接口逻辑完全正确,再把规模一步步扩大。这套流程虽然慢,但每一步都有明确的验证目标,比起一次性把完整系统塞进求解器,等到报错再面对几百个变量索引无处下手,要划算得多。
最后再分享一个实用小技巧:无论哪天突然发现结果不对,先检查单位,再检查接口变量,最后才怀疑求解器参数。按这个顺序排查,百分之八九十的问题都能在五分钟内定位。