最近好几个朋友都在问虚拟电厂调度优化的代码怎么搭,尤其是“日前+日内”这种两时间尺度框架,问的人多了我就把之前做的一个复现项目拿出来整理一下。这个项目对应的是一篇SCI论文的核心思路——虚拟电厂(Virtual Power Plant, VPP)内部包含风电、光伏、储能、微型燃气轮机和柔性负荷,需要在日前和日内两个时间尺度上做协调调度,从而平抑可再生能源波动、降低运行成本。Matlab代码实现,核心用Yalmip建模、Cplex或Gurobi求解,整套框架可以直接跑通,替换数据就能用在自己的研究里。
先说清楚这篇博文能给你什么。如果你正在做虚拟电厂优化调度方向,或者刚接触多时间尺度调度想找一个可以下手的代码框架,这篇文章会从头到尾拆解这套方案的数学模型、代码结构、核心函数实现,顺便把我在复现过程中踩过的坑一并列出来。不管是想复现论文、写毕业设计,还是给自己的研究搭个起点,都可以参考。
1. 问题背景与研究动机:为什么虚拟电厂需要“多时间尺度”调度
1.1 单时间尺度调度不够用,问题出在哪
先聊一个最基本的问题,虚拟电厂为什么不直接用传统的单时间尺度调度,非要搞日前加日内两套流程?答案其实特别朴素:预测永远是不准的。
风电出力和光伏出力受天气影响极大,如果提前24小时做一份固定的计划,第二天实际风一停、云一来,整个计划就全废了。储能电量可能放不完,燃气轮机可能爬坡爬不上来,甚至可能出现供需失衡。单时间尺度本质上等于把所有宝都押在预测精度上,这在工程实际里非常危险。虚拟电厂内部有可控机组、储能、柔性负荷这么多灵活性资源,如果不能在临近实际运行时重新排一遍,这些资源的调节能力就被浪费了。
所以学术界和工程界都在推多时间尺度协调调度:在日前时间尺度做“大方向”的机组组合与电量预分配,在日内时间尺度做“精细化”的滚动修正,让计划跟上实际。
1.2 日前调度与日内调度的职责划分
这套框架里,两个时间尺度的分工非常明确。
日前调度以1小时为分辨率,覆盖24小时,目标是在已知预测数据的前提下,确定各机组启停状态、出力计划、储能充放电策略以及与外部电网的交换功率。它解决的是“明天大致怎么安排”的问题,可以理解为战役级的作战计划。燃气轮机什么时候开、什么时候停,储能在哪些时段充满、哪些时段放空,这些决策周期长、容易受到时间耦合约束影响的,都要在这一层定下来。
日内调度以15分钟或1小时为滚动周期,通常在每个调度时刻基于最新超短期预测重新优化未来4到6小时。它解决的是“接下来几个小时怎么调”的问题,属于战术级修正。比如日前计划8点让储能放电,但早上发现光伏出力比预测值高出不少,那日内调度就会调整策略,让储能少放甚至反向充电,避免功率过剩。
两个时间尺度之间存在信息传递与修正关系:日内的调度结果会跟踪并修正日前计划,但不推翻日前已经确定的机组启停状态。这样既保证了系统运行的稳定性,又给了计划足够的柔性空间。
1.3 这套方案适合解决什么类型的SCI研究问题
如果你在准备论文,这套“日前+日内”双时间尺度框架可以支撑的研究方向非常明确:
- 含高比例可再生能源的虚拟电厂经济运行,重点分析不确定性对调度成本的影响
- 储能系统在虚拟电厂中的价值评估,对比有无储能两种情况下的运行成本差异
- 需求响应资源参与调度后的削峰填谷效果,柔性负荷如何配合机组出力
- 多虚拟电厂协调调度,不过那就要多代理框架了,代码量会成倍增加
一句话总结,凡是涉及“预测—计划—调整”链条的优化调度问题,几乎都能套上多时间尺度框架。我这个项目就锁定在单VPP、多资源、确定性与滚动修正相结合的场景,和绝大多数SCI论文的基础设定一致。
2. 虚拟电厂整体建模与资源约束拆解
2.1 内部组成单元和数学模型
虚拟电厂从外部看像一个发电厂,实际上内部是分散的资源集合,需要通过调度控制中心统一协调。我这里采用的模型中包含四类常规资源,外加一个外部电网交互接口。
第一是微型燃气轮机,属于可控机组,输出功率有上下限,还有爬坡速率限制,机组启停状态用0-1变量表示,启动和停机时会产生启停成本。第二是储能系统,用荷电状态SOC来描述能量变化,充放电功率有上限,SOC有安全运行区间,同时不能同时充放电,这需要引入二进制变量做互斥约束。第三是可再生能源,风电和光伏在这套模型里处理成“可调度但不可控”的出力,意思是预测值给定,可以弃风弃光但要产生惩罚成本。第四是柔性负荷,这里简化为可平移负荷和可削减负荷,分别支付一定的补偿费用。
这些模型共同构成了虚拟电厂的内部物理边界。你在复现的时候不需要每个单元都做得特别复杂,在基础版本里先保证约束完备,后续再往精细化方向加就行。
2.2 日前调度的目标函数与约束细节
日前调度的目标函数是典型的最小化运行成本问题。我把成本拆成了五个部分:燃气轮机的燃料成本、启停成本、储能系统的充放电损耗折算成本、向外部电网购电成本、弃风弃光惩罚成本。写成数学形式就是用高斯线性函数表达燃料成本,把系统总成本做最小化,其中每项成本都在对应时间尺度上逐一累加,注意各个成本项的权重系数要放在同一量纲下比较。
约束条件这部分是代码的核心,我整理成几组来看:
功率平衡约束是刚性约束,必须严格满足,表述为所有发电出力加储能放电加购电,等于负荷加储能充电加售电,小时刻的功率平衡是整个调度模型的基石。机组约束包括燃气轮机出力上下限、爬坡约束和最小启停时间约束,其中爬坡约束在日前调度模型中用小时级变化量来表示。储能约束包括充电功率、放电功率上限,同时充电状态和放电状态互斥,SOC的递推关系式表示当前时刻SOC等于上一时刻SOC加上充电效率折算后的充电量再减去放电量除以放电效率。可再生与电网交互约束则包含风电和光伏出力上限,以及联络线传输功率上下限。
2.3 日内调度的模型升级与滚动窗口设计
日内调度在数学形式上与日前调度结构相似,但有几个关键差异需要特别关注,这也是代码实现中的难点。
分辨率的变化与预测数据的更新是第一个差异。日内调度通常使用15分钟的分辨率,我这里用的案例是15分钟一个断面、4小时滚动窗口,相当于16个断面每次滚动优化。每个调度周期开始时,用最新的超短期预测值替换掉原来的预测数据,形成闭环修正。启停机状态的固定是第二个差异。日内调度不再改变日前已经确定的机组启停计划,换句话说燃气轮机的开关机状态在日内是固定参数,只优化出力调整量。这样做的好处是避免机组频繁启停带来的机械损耗和运行风险,也让两阶段之间的衔接逻辑更清晰。功率修正量与偏差惩罚机制的引入是第三个差异。目标函数里增加了一项日前计划偏差惩罚项,如果实际调整后的出力与日前计划值偏差过大,会产生额外成本,这个约束条件保证了日内调度不会疯狂偏离日前框架。
滚动窗口的逻辑也值得展开说一下。窗口内的调度问题覆盖未来16个断面,但只执行第一个断面的决策结果,每15分钟整点重新求解一次,窗口整体向前滑动。这种做法的优势是每次决策都基于最新信息,同时滚动窗口的长度保证了对未来趋势的“预见性”,不会做出特别短视的决策。
2.4 为什么选择MILP建模而非启发式算法
这个问题经常有同学问,复现论文的时候机器人学派的遗传算法、粒子群那么流行,为什么我们这套用混合整数线性规划?
原因是虚拟电厂调度本质上是一个有大量0-1变量和线性约束的决策问题,天然适合MILP建模。现在主流的商业求解器Cplex和Gurobi对MILP的求解速度非常快,几秒钟就能拿到全局最优解或接近最优的解。相比之下启发式算法虽然有较强的非线性适应能力,但结果不稳定,每次跑出来的解可能都不一样,而且全局收敛性无法保证,写论文的时候审稿人也更认可精确求解方案。
当然MILP也有局限,如果约束全部改为非线性,比如加入潮流方程、阶梯电价非线性分段、储能老化成本曲线等,模型就会变成MINLP,这时求解难度急剧上升。作为初学者我的建议是先从线性化开始,分段线性化、大M法处理逻辑约束,把这些基本技能练熟,再去碰非线性。
3. Matlab代码实现:Yalmip建模与核心模块详解
3.1 代码总结构与初始化
这套代码的目录结构采用标准的仿真项目组织方式,便于扩展和维护。主程序按顺序执行参数设置、数据读取、日前调度求解、日内滚动调度求解、结果输出与可视化,每个模块都拆成独立的脚本或函数文件。
初始化部分我特别强调一点:所有时间相关的参数都要集中定义,方便统一修改。代码开头定义了两个时间尺度的基础参数,包括日前调度的时段数24小时、日内调度的时段数4天中每天的96个点位、滚动窗口长度16个点、日内调度分辨率0.25小时。同时还定义了系统基准容量等关键标幺值,这样后面所有物理量都可以在标幺值体系下处理,避免量纲混乱。
%% 基础参数定义 mpc = struct(); mpc.baseMVA = 100; % 基准容量 MVA mpc.T_DA = 24; % 日前调度时段数 mpc.T_ID = 96; % 日内调度时段数(15min分辨率) mpc.window = 16; % 日内滚动优化窗口长度 mpc.dt = 0.25; % 日内调度时间步长 h3.2 日前调度优化模型的Yalmip实现
日前调度的核心建模代码不长,但每一行都对应前面讲的约束条件,建议对照着看。这里用Yalmip工具箱定义优化变量,再用Cplex作为求解后端。
SDPVAR部分定义决策变量:燃气轮机出力、启停状态、启动操作变量、停机操作变量、储能充电功率、放电功率、SOC状态、购电功率、售电功率,以及可再生能源出力变量和负荷削减变量。
约束条件部分按组添加,这里截取关键代码段展示功率平衡约束和储能约束的写法。
%% 日前调度模型 % 决策变量定义 P_MT = sdpvar(mpc.T_DA, 1); % 燃气轮机出力 u_MT = binvar(mpc.T_DA, 1); % 启停状态 P_ch = sdpvar(mpc.T_DA, 1); % 储能充电功率 P_dis = sdpvar(mpc.T_DA, 1); % 储能放电功率 SOC = sdpvar(mpc.T_DA, 1); % 荷电状态 P_buy = sdpvar(mpc.T_DA, 1); % 购电功率 P_sell = sdpvar(mpc.T_DA, 1); % 售电功率 P_wt = sdpvar(mpc.T_DA, 1); % 风电出力 P_pv = sdpvar(mpc.T_DA, 1); % 光伏出力 P_load_cut = sdpvar(mpc.T_DA, 1);% 可削减负荷量 Constraints = []; % 功率平衡约束 Constraints = [Constraints, P_MT + P_dis + P_wt + P_pv + P_buy == ... P_load_forecast + P_ch + P_sell + P_load_cut]; % 储能SOC递推与约束 Constraints = [Constraints, SOC(2:end) == SOC(1:end-1) + ... (P_ch(1:end-1) * eta_ch - P_dis(1:end-1) / eta_dis) * dt_DA]; Constraints = [Constraints, SOC_min <= SOC <= SOC_max]; Constraints = [Constraints, 0 <= P_ch <= P_ch_max * (1 - z)]; Constraints = [Constraints, 0 <= P_dis <= P_dis_max * z];储能充放电互斥约束里我引入了一个二进制变量z,配合大M法的思想,用(1-z)和z分别控制充电和放电通道的开关,这个做法是最稳妥的。有同学会想用P_ch * P_dis == 0这种非线性表达,但那样一来模型就成了非凸约束,求解速度会大幅下降,不建议在初版代码里尝试。
燃气轮机的最小启停时间约束相对复杂,需要引入一个连续时间计数器变量来处理。核心约束写成:如果在某一时刻机组处于运行状态且之前已经连续运行了最小时间,那么它才能在这一时刻关停。代码实现时我直接用了一个约束组,用状态变量的历史值判断,写起来会多一些循环。不过对单台机组场景,也可以简化处理。
3.3 日内滚动调度的循环实现
日内滚动调度是整个代码里最考验细节处理能力的部分,核心是处理滚动窗口和时间索引。直接用代码说明比较直观。
%% 日内滚动调度 % 取日前调度得到的机组启停状态作为固定值 u_MT_fixed = value(u_MT); for k = 1:96-window+1 % 当前滚动窗口:k 到 k+window-1 t_idx = k : k + window - 1; % 定义窗口内的决策变量 P_MT_id = sdpvar(window, 1); P_ch_id = sdpvar(window, 1); P_dis_id = sdpvar(window, 1); SOC_id = sdpvar(window+1, 1); P_buy_id = sdpvar(window, 1); P_sell_id = sdpvar(window, 1); P_wt_id = sdpvar(window, 1); P_pv_id = sdpvar(window, 1); P_load_cut_id = sdpvar(window, 1); % 固定启停状态 Constraints = [Constraints, P_MT_id <= u_MT_fixed(t_idx) * P_MT_max]; Constraints = [Constraints, P_MT_id >= u_MT_fixed(t_idx) * P_MT_min]; % 其余约束与日前类似,但使用滚动窗口内的超短期预测数据 % ... % 求解并只取第一个时段结果 optimize(Constraints, Objective, options); P_MT_result(k) = value(P_MT_id(1)); % ... 其他变量的结果存储 end这个循环里最需要注意的是SOC状态变量的跨窗口衔接。每个窗口的第一个SOC值应该等于上一窗口最后执行的SOC值,不能重新初始化。我的做法是在每个窗口优化前先把初始SOC变量设为已知值:
if k == 1 Constraints = [Constraints, SOC_id(1) == SOC_init]; else Constraints = [Constraints, SOC_id(1) == SOC_result(k-1)]; end这个细节直接决定了滚动调度结果的连续性和正确性,我在第一次实现时就因为没处理好导致SOC曲线出现断层,查了好久才发现问题。
3.4 求解器配置与运行加速技巧
求解器配置上,如果安装了Cplex,Yalmip会自动识别,不需要额外设置。如果用的是Gurobi也可以用同样的方式调用。这里给出一个经过验证的求解器参数配置,实测可以显著加速:
options = sdpsettings('solver', 'cplex', ... 'verbose', 1, ... 'cplex.mip.tolerances.mipgap', 0.0001, ... 'cplex.mip.strategy.startalgorithm', 4);重点说几个参数的意义。mipgap设到1e-4基本可以认为是求到了精确解,再往下调不会带来实际收益,只会拖慢求解速度。startalgorithm设为4表示自动选择初始点算法。对于大模型可以进一步开启presolve。
日内滚动调度95次求解,如果每次都从头构建模型会非常耗时,我这里实际的做法是对每个循环单独构建优化变量和约束,虽然代码上看起来是重复工作,但对Yalmip来说每次都是从零开始生成一个完整模型,95次求解总体用时大概两三分钟,可以接受。如果你需要更快的速度,可以把模型外提只更新数据,但代码可读性会下降很多,不利于论文复现的读者理解。
4. 案例设计与结果分析的关键环节
4.1 算例参数设定与数据准备
案例设计在很大程度上决定了仿真结果能不能讲出好故事。我用的算例是一个中等规模的虚拟电厂,包含1台燃气轮机、1个风电场、1个光伏电站、1套储能系统以及可削减负荷。具体参数如下表:
| 参数名称 | 数值 | 单位 |
|---|---|---|
| 燃气轮机最大出力 | 60 | MW |
| 燃气轮机最小出力 | 10 | MW |
| 燃气轮机爬坡速率 | 20 | MW/h |
| 风机最大出力 | 50 | MW |
| 光伏最大出力 | 30 | MW |
| 储能容量 | 40 | MWh |
| 储能最大充放电功率 | 10 | MW |
| 储能SOC上下限 | [0.1, 0.9] | - |
| 联络线最大交换功率 | 80 | MW |
| 系统峰值负荷 | 120 | MW |
典型日的风电、光伏和负荷曲线我采用了公开数据集的参考曲线做平滑处理。数据准备阶段关键的一点是:日前调度用的是预测曲线,日内调度用的超短期预测曲线是在日前预测值基础上叠加一个随机偏差生成的,这样仿真的结果才能反映日内修正的真实价值。
4.2 日前调度结果怎么看
运行完日前调度后,第一件事就是画一张24小时的功率平衡堆叠图。这类图能直观看到各个时段是谁在供应电力:夜间的负荷主要由燃气轮机和风电承担,白天光伏出力上升后燃气轮机出力开始下降,储能在光伏大发时段充电、在晚高峰时段放电。这种“光伏压火电、储能错峰调节”的模式正是虚拟电厂调度希望看到的效果。
还有一个需要重点检查的是燃气轮机的启停计划。好的日前调度结果里,燃气轮机不会频繁启停,而是会在负荷高峰段连续运行8到10小时,比如从早高峰一直运行到晚上,这段时间可能跨过下午光伏出力高企或晚高峰来临,启停次数少说明机组组合合理,整体经济性也好。
4.3 日内调度结果与日前计划的偏差分析
日内调度的核心价值要从对比中体现。把日前计划和日内实际执行曲线画在一张图上,你会看到两者的差异很微妙。
举例来说,日前预测光伏出力峰值在午间12点到14点达到30MW,日内实际超短期预测显示天气情况变差,光伏出力只有预测值的70%。日内调度器会立即做出几个调整:一是小幅增加燃气轮机出力填补缺口,二是减缓储能充电速度,三是可能略微增加购电。最终的功率平衡仍然满足,但储能SOC曲线会比日前计划更晚到达上限。这一套“发现问题—实时修正—保持平衡”的过程,就是日内调度的意义所在。
为了量化日内调度的优势,我还计算了不可调功率偏差指标,对比如下:
| 指标名称 | 仅日前调度 | 日前+日内调度 |
|---|---|---|
| 弃风弃光电量(MWh) | 12.6 | 4.8 |
| 功率不平衡量(MWh) | 18.3 | 3.2 |
| 运行总成本(万元) | 45.8 | 40.2 |
数字非常直观,日内调度让弃电减少了60%以上,不平衡量更是下降了80%多。这些数值就是一篇论文的“卖点”,审稿人一眼就能看到多时间尺度调度的价值。
4.4 成本成分分析与敏感性讨论
除了功率平衡,成本成分分析是另一个值得深挖的话题。我的仿真结果显示总运行成本中占比最大的是购电成本和燃气轮机燃料成本,储能带来的损耗成本占比不到5%。如果外部电网电价在某些时段特别高,日内调度会自动减少购电,把压力转给燃气轮机和储能,这说明虚拟电厂对市场价格信号是敏感的。
敏感性分析可以从储能容量和预测误差两个维度切入。储能容量从20MWh逐步增加到60MWh,运行成本会先显著下降而后趋于平缓,形成一个类似边际效益递减的曲线。预测误差增大时日内调度的价值就更突出——误差越大,修正带来的收益越明显。这套仿真可以为虚拟电厂前期规划提供很好的参考依据。
5. 代码复现过程中的常见问题与调试心得
5.1 求解器报错的典型场景和定位方法
我在做这个项目时遇到的第一个坎就是Cplex在部分场景下返回infeasible问题。出现这个情况不用慌,绝大多数时候不是模型真没有可行解,而是约束条件自相矛盾。排查思路是:先注释掉不关键的约束,比如把储能互斥约束去掉,看模型是否变可行,如果变可行了,问题就出在互斥约束与SOC初值之间。还有一次是因为爬坡约束和启停约束冲突,燃气轮机刚启动就要求它爬坡到高出力,而最小技术出力本身就高于爬坡允许的范围,这在物理上就做不到,需要检查最小持续运行时间约束和爬坡约束的参数。Yalmip提供了一个比较有用的诊断工具,可以检查不可行约束集,例如通过assign和check来定位具体哪条约束出了问题。
5.2 耗时优化与代码执行效率的实践经验
整套代码跑一遍大约需要一分半钟到三分钟,其中日内调度的层层循环占了整体耗时的大部分。如果觉得太慢可以从几个方向优化:把非必需的调试输出全部关掉;日内滚动窗口内的约束如果结构完全一致,可以考虑用矩阵拼接的方式一次性构建模型,然后循环内只更新变化的那部分参数,我试过这种方式整体能提速50%左右,但代码复杂度会显著提高;尝试更换求解器,同一模型Gurobi通常比Cplex要快15%到30%,不过两种求解器的授权方式不同,看个人条件选择吧。
5.3 论文复现与自行扩展的边界建议
这套代码的定位是“论文复现框架”,帮助你建立标准化的调度仿真流程,而不是说直接就能复现所有SCI论文里的结果。原因是每篇论文的假设条件、数据来源、参数设置都不一样,甚至目标函数里一个小权重系数的差异都会导致结果不同。所以在复现时我的建议是关注框架而非具体数值。读懂代码里“变量定义—约束添加—求解—结果提取—图形绘制”这条主干线,然后替换成你自己场景的参数和数据。
如果后续要扩展,比较推荐的几个方向包括:加入电价的实时波动和需求响应机制、把单个虚拟电厂扩展到多虚拟电厂协调调度、引入鲁棒优化或随机规划处理不确定性、加入碳交易或绿证交易成本作为新的目标函数项。每一步都在现有框架上做加法,不要推倒重来。
6. 一些实操体会
最后分享一个我在多次做虚拟电厂调度仿真之后的核心感受:多时间尺度调度不是一个单纯“套公式”的优化问题,它是一个建模权衡问题。日前阶段做太多决策会丧失灵活性,日内阶段做太多决策又会牺牲经济性,两个尺度的边界和衔接逻辑才是论文真正的创新点。这套代码的价值不在于能算出多漂亮的数值,而在于给你一个可以自由调整的资源模型、时间尺度、目标函数的标准框架,在这个基础上做修改和研究都会顺畅得多,这也是我把代码整理成完整可运行版本的原因,希望接手的人都少踩一些我踩过的坑。