这两年做虚拟电厂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(t1) 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, T1); % 储能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(t1) 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(T1) 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变量定义成T1维度是因为SOC函数中t1的索引要取到第25个点很多新手把SOC定义成1×T运行报错“索引超出维度”其实就是边界点没留出来。3.4 日内滚动调度的代码结构日内滚动是“循环套单次优化”的结构。外层循环从第1个时段滚动到第96个时段15分钟间隔每个循环内做一次短周期优化。%% 日内滚动调度框架 H 16; % 滚动预测时长 4小时 16个15分钟 for k 1:15:96 % 每次滚动15分钟 % 截取当前时刻到未来4小时的预测数据 idx k:min(kH-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时段、单储能、单聚合商的小规模MILPGurobi通常秒解。真遇到求解慢的情况往往是模型规模失控时段数太多日内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跟踪软约束调λ把两条曲线拉到合理距离。每一步都有清晰的验收标准不会让人一头扎进去就出不来。