
虚拟电厂多时间尺度调度这块在电力系统方向的SCI论文里一直属于高频方向尤其是搭上“顶级SCI复现”这种关键词之后很多人拿来当毕业设计或者科研敲门砖。我也在Matlab环境里把这个流程完整复现过一遍从数据生成、模型搭建到结果后处理一路踩过来说实话这套框架本身不复杂但网上能搜到的开源版本质量参差不齐不少代码丢了约束、混淆了时间尺度口径跑出来的结果根本没法服人。这篇文章把我自己复现“日前日内”两级调度优化的完整思路、数学模型、Matlab实现路径以及调试中遇到的实际问题都写出来适合正在做虚拟电厂、微电网或者分布式能源聚合调度的同学参考也适合刚接触优化建模、想拿MatlabYalmip快速把论文复现跑通的人。1. 为什么要拆成“日前日内”两级来做调度先说一个很多人刚接触这个题目时会有的疑问既然有了日内调度为什么还要费劲做日前直接以小时级甚至分钟级滚动优化不就完了吗答案在于预测精度和使用场景的错位虚拟电厂聚合了光伏、风电、储能、柔性负荷之后面对的源荷不确定性是双重的光伏和风电的出力预测误差在提前24小时的尺度上可能达到20%甚至更高而负荷侧的波动在日内又会出现明显的跟踪误差。如果只做日前调度决策出来的机组组合和储能充放电计划在真正执行时大概率偏差很大如果只做日内滚动优化又来不及对机组启停这类需要提前准备的变量做全局决策。两级调度的本质就是用“日前定结构、日内修正偏差”的分工来兼顾经济性和可靠性。1.1 两个时间尺度的分工逻辑日前调度通常以1小时为分辨率也有用15分钟精细到96点的优化范围为完整的24小时它的决策重点是离散变量也就是机组的启停状态、储能是否参与充电/放电这种需要提前锁定的设备状态。这些变量一旦定了日内阶段一般不会轻易翻转因为机组启停有最小开停机时间约束储能频繁切换状态也会加速寿命衰减。日内调度则以更短的滚动窗口来运行常见的是5到15分钟一个时段在未来4到6小时内不断滚动求解它的任务是承接日前已经固定的状态变量微调连续变量也就是机组出力的增减、储能充放电功率的大小、与配电网交换功率的修正。这种两级结构本质上对应了数学上的分层优化或模型预测控制思想。日前模型相当于一个包含整数变量的混合整数规划日内模型在状态变量被冻结之后降为连续变量优化求解速度会快很多。理解了这个分工才能看懂为什么复现代码里日前模型要写成需要商业求解器才能快速求解的混合整数模型日内模型却可以在滚动窗口内反复求解这是整个框架设计的第一层逻辑。1.2 虚拟电厂在两级调度里的角色定位虚拟电厂的核心价值不是自建电厂而是把分散的分布式资源聚合成一个可以统一响应、统一调度的整体。在我复现的模型里虚拟电厂运营商扮演的是中间人角色向上对接电网调度签订联络线功率协议或者参与备用电能市场向下统筹各分布式资源的运行计划。这里有个容易忽略的细节虚拟电厂内部的分布式光伏和风电通常不具备调度能力是不可控电源只能作为“负的负荷”处理。真正可调的资源主要是储能系统、柴油或燃气机组、以及可转移或可中断的柔性负荷。所以在建模时要把可控和不可控资源分开处理光伏风电的出力曲线直接作为输入参数由预测得到决策对象只针对储能、可控机组和柔性负荷。这个区分直接影响约束条件的写法不可控资源的偏差要靠储能和机组来平衡而不是反过来让光伏去盯负荷曲线。我见过有的复现代码把光伏出力也设成了决策变量相当于把不可控资源写成了可控资源这就完全背离了物理约束结果自然没有参考价值。1.3 不确定性的处理程度决定了模型复杂度多时间尺度调度绕不开不确定性建模但不同的论文处理深度差很多。有的只做了确定性模型也就是用预测曲线作为输入两级优化之间通过偏差惩罚来弥补预测误差有的则在日前阶段引入场景法或鲁棒优化对光伏风电的多个可能出力场景求期望成本最小或最坏情况下的最小化。复现时第一步要看清原论文用的是哪种框架因为这个选择几乎决定了整个模型的规模和求解难度。从工程复现的角度讲我建议先跑确定性模型把两级调度的框架和代码流程跑通再考虑在日内阶段加滚动更新的预测数据。上来就写带有几百个场景的随机优化参数没设对的话不仅求解时间暴涨而且调试难度指数级上升最后可能连收敛性都说不清楚更别谈和原论文图表对比了。2. 复现前必须吃透的数学模型目标函数与约束条件很多复现项目翻车的根源在于没有把目标函数和约束逐条对照原论文推导清楚而是直接拿网上的某个代码就开始跑最后跑出的结果是错的却找不出原因。我自己复现时花了整整两天把模型一条条写在纸上再和代码逐一对应这一步的收获远大于调参本身。接下来我把这个模型的核心数学结构拆开讲一下以及每个约束在代码里到底起到了什么作用。2.1 目标函数两级成本的构成与衔接日前调度的目标函数一般是整个调度周期内的总运行成本最小化包括可控机组的燃料成本和启停成本、储能的充放电损耗成本有时折算为运维成本、以及向配电网购售电的成本。日内调度则在日前计划已定的基础上目标函数变成对日前计划的偏离惩罚加实际运行成本最小化。这里要注意一个关键设计日内模型通常不是完全推翻日前计划而是在目标函数中加入偏差惩罚项用权重系数控制偏离程度。这个权重系数的取值在代码里往往以罚因子或固定参数出现但它的实际含义是“修改日前计划的代价”如果设得太小日内结果会严重偏离日前计划两级调度就失去了意义如果设得太大日内修正能力被锁死预测误差带来的影响全部落在系统安全上。原论文一般会给出这一项的系数或说明取值范围复现时务必查找不要自己随便定。目标函数的代码写法在MatlabYalmip环境下相对统一都是先定义决策变量然后把各项成本写成一个求和表达式最后用optimize求解。确定性和随机性模型的差别主要在这条表达式之外还要不要加对场景的期望求和建议初学时先把单场景写清楚。成本项涉及的决策变量量纲日前/日内机组燃料成本各机组每时段出力元/MWh两者均含机组启停成本机组启停状态变量元/次日前为主储能损耗成本充放电功率元/MWh两者均含购售电成本联络线交换功率元/MWh两者均含偏差惩罚实际出力与日前计划差值元/MWh仅日内2.2 功率平衡整个模型的中枢约束功率平衡约束是电力系统调度模型里永远存在的等式约束但虚拟电厂场景下它的写法有一些特殊之处。简单版本是所有可控机组出力、储能放电功率、光伏出力、风电出力之和加上从配网购电的功率必须等于负荷总需求加上储能充电功率再加向配电网售电的功率。写成偏差形式就是每个时段的净功率偏差必须为0。这个约束看似简单却把光伏、风电、储能、机组、电网、负荷全部串联起来任何一侧数据错位都会导致整个约束不满足进而影响所有决策变量。在Matlab代码里这个约束对应每时段一条等式比如对于T个时段就形成T行约束矩阵。我复现时习惯把功率平衡约束单独放在一段并且在矩阵里加注释标明每个求和项对应哪类资源这样一旦报错可以快速定位是不是某个设备参数矩阵的维度出了问题。2.3 机组和储能的典型约束机组侧有三类约束最容易漏写出力上下限、爬坡约束、最小开停机时间。出力上下限在Matlab里往往直接通过变量的上下界来实现也可以用不等式约束写。爬坡约束是相邻时段出力差不能超过爬坡速率乘以时段长度它在日前以1小时为步长和日内以15分钟为步长时爬坡限值的数值是完全不一样的这一步最容易把单位搞错。最小开停机时间约束涉及历史状态变量写起来比较繁琐如果原论文限制了单次调度周期内启停次数不多有时复现代码会简化掉但简化之后要能解释得通不然审稿人一问就露馅。储能侧的核心是SOC荷电状态递推方程和充放电互斥约束。SOC递推方程写成代码时需要特别注意是离散事件形式下一时段SOC等于当前SOC减去放电消耗再加上充电增加充放电效率通常不相等充电效率约90%多放电效率约95%效率折算的先后顺序要写对。充放电互斥约束是为了防止储能同时充电和放电这种情况在数学上通过两个二元变量加一个大M约束来实现也可以用Yalmip自带的implies逻辑约束表达。这里有个数值小坑如果初始SOC没给对或终值SOC约束设得过紧储能可能完全无法按预期工作整条SOC曲线会贴着边界走输出结果看起来就很不自然。2.4 不确定性处理预测场景与备用约束如果原论文在日前模型中显式处理了不确定性复现起来就要加一个场景集合。最简单的处理方式是给每个光伏风电出力场景增加一组平衡约束目标函数改为对各场景成本求和取数学期望但如果场景数量较多模型规模会快速膨胀。更省事的做法是把不确定性折算成系统备用约束比如要求可控机组和储能在任意时刻保留一定比例的向上/向下调节容量这样就不需要枚举场景模型规模基本不变。许多SCI论文用的是“日前保守调度日内滚动修正”的双层框架日内模型通过不断加入最新的预测信息来补偿日前保守性带来的经济性损失这个思路在工程复现中性价比很高也是我建议优先尝试的方案。3. Matlab复现的一条可执行路径从数据准备到求解器调用模型吃透之后代码层面的实现顺序就非常重要了。初学者容易一上来直接写优化模型主体结果数据预处理没做、参数矩阵维度没对齐等模型一运行各种报错铺面而来。我的习惯是严格按照“数据准备 → 模型构建 → 求解与后处理”三段式来组织代码每段用函数或脚本来切分这样无论是排查问题还是替换成自己的数据都方便。3.1 输入数据的组织方式先把所有输入数据集中放在一个结构体或Excel文件中不要散落在各个脚本里。输入数据至少包括以下几个部分设备参数表各可控机组的出力上下限、爬坡速率、启停成本、燃料成本系数储能参数额定容量、初始SOC、SOC上下限、充放电效率、最大充放电功率负荷曲线日前96点或24点的预测负荷光伏/风电预测曲线与负荷同样的时间分辨率和时段数电价曲线分时购电价和售电价如果模型区分购售电价以24点的日前调度为例最基础的数据就是4条长度为24的曲线和若干标量参数。如果日内调度是15分钟滚动4小时那么输入数据需要有预测序列按滚动窗口更新的机制这个在复现时一般通过一个for循环内不断更新预测数据来实现。数据类别字段示例维度是否必须机组参数Pmin, Pmax, ramp, cost机组数×4是储能参数Ecap, soc0, eff_ch, eff_dis, Pchmax, Pdis_max1×6是负荷预测P_load(t)T×1是新能源预测P_pv(t), P_wind(t)T×2是分时电价price_buy(t), price_sell(t)T×2视模型而定3.2 用Yalmip搭建模型的核心代码骨架我复现时选择MatlabYalmip这套组合原因很简单Yalmip不需要手动构造大规模系数矩阵直接把优化变量当成矩阵符号来写约束和目标函数代码可读性和开发效率都远高于直接写求解器API。配合商业求解器中小规模日前调度模型求解时间通常在几秒到几十秒之间完全够用。下面是一个兼容日前模型的核心代码框架关键变量我都加了注释可以直接照着搭% 假设已有输入数据结构体 data T 24; % 调度时段数 N_g length(data.gen); % 机组数量 % 决策变量 % 日前阶段机组出力、启停状态、储能充放电、联络线交换功率 P_g sdpvar(N_g, T, full); % 机组出力 u_g binvar(N_g, T); % 机组启停状态1表示开启 P_ch sdpvar(1, T); % 储能充电功率 P_dis sdpvar(1, T); % 储能放电功率 SOC sdpvar(1, T); % 储能荷电状态 P_buy sdpvar(1, T); % 从配网购电 P_sell sdpvar(1, T); % 向配电网售电 Constraints []; % 功率平衡约束 for t 1:T Constraints [Constraints, ... sum(P_g(:, t), 1) P_dis(t) data.P_pv(t) data.P_wind(t) P_buy(t) ... data.P_load(t) P_ch(t) P_sell(t)]; end % 机组出力上下限 for g 1:N_g Constraints [Constraints, ... data.gen(g).Pmin * u_g(g, :) P_g(g, :), ... P_g(g, :) data.gen(g).Pmax * u_g(g, :)]; end % 储能SOC递推假设时段间隔1小时容量单位MWh Constraints [Constraints, SOC(:, 1) data.soc0]; for t 1:T-1 Constraints [Constraints, ... SOC(t1) SOC(t) P_ch(t) * data.eff_ch - P_dis(t) / data.eff_dis]; end % 充放电互斥 Constraints [Constraints, ... P_ch 0, P_ch data.Pchmax * (1 - binvar(1, T))]; % 更严谨的写法是引入二维互斥变量此处简写示意 % 目标函数示例燃料成本购电成本-售电收入 Objective sum(sum(data.gen_cost * P_g)) ... sum(P_buy .* data.price_buy - P_sell .* data.price_sell); % 求解 ops sdpsettings(solver, cplex, verbose, 2); optimize(Constraints, Objective, ops); % 提取结果 P_g_result value(P_g); SOC_result value(SOC);上面这段代码只是把关键约束的写法展示出来真实复现的时候还要把各个成本项的系数、启停成本、爬坡约束全部补齐。写出第一版优化模型之后最好先用一组故意设定的简单数据比如只有1台机组、1台储能、固定负荷验证正确性再换到完整数据上否则模型一跑就是一堆红报错根本分不清楚是哪个约束崩了。3.3 日内滚动优化的实现思路日内模型和日前模型的区别不在约束形式而在运行逻辑。日内通常不是一次性算出未来24小时所有出力而是每到一个新时段把预测数据向前滚动一个窗口然后求解未来几小时的最优计划但只取出当前时段的决策结果作为执行值。这个“滚动求解、只执行第一步”的机制在代码里对应一个for循环每轮循环内部建一个独立的优化子问题提取value(P_g)时只取第一列。实现滚动优化时要注意一个非常关键的问题决策变量在循环体内部重复定义而Yalmip的sdpvar每次创建都是一个新的变量对象不会自动覆盖旧模型。所以每轮循环里要么把旧的约束和变量对象全部清除要么在每轮循环内部把变量Constraint数组全部重新构建一次。我一开始图省事在循环外定义变量循环内只更新约束结果不断叠加旧约束导致模型越来越庞大求解时间越来越慢最后干脆变成了一个巨型模型。正确做法是把日内调度的模型构建和求解全部封装在一个子函数里每次滚动循环调用这个子函数并传入最新的预测数据窗口。4. 调试中容易翻车的五个隐藏坑代码跑通只是一步能跑出和论文差不多的结果才是真正考验人的地方。我复现过程中前前后后踩了不少坑有些问题花了大半天才定位出来。这里挑五个最典型的写出来希望能帮后面的人少走弯路。4.1 时间尺度不一致导致的约束失效日前和日内两个阶段如果采用不同的时间步长容易出问题的点在爬坡约束和功率平衡约束的量纲换算上。比如说日前用1小时时段日内用15分钟时段那么日内模型里机组的爬坡允许值就不再是日前模型里的数值而是要除以4再乘15分钟对应的时间或者用爬坡速率MW/min直接乘以步长。很多复现代码直接复制日前的约束到日内模型结果爬坡约束变得过松或过紧机组的出力曲线出现明显抖动一看就是从代码层面埋了雷。处理办法是提前定义好时间步长变量dt_da和dt_id所有涉及时间换算的约束统一引用这个变量。比如爬坡约束写成P_g(:, t1) - P_g(:, t) Ramp_up * dt不管日后改成什么分辨率只要改dt就行不需要翻整个代码逐个改数字。4.2 SOC递推方程的效率折算顺序储能SOC递推看似简单但经常出错的地方在于充电效率和放电效率的折算方式。充电时电网侧输入的电能要乘以充电效率才变成存储到电池中的能量放电时电池释放出的能量要除以放电效率才是真正供给负荷侧的电能。如果顺序搞反比如把放电也写成乘以放电效率那么储能在放电过程中能量会越放越多最终SOC曲线在尾部出现不合理上翘与此同时系统的成本会明显偏低因为储能被无偿放大成了能量源。调试这个方法很简单单独把储能拎出来给定一组固定的充放电功率序列人工手算一遍SOC变化和代码输出对比。差异超过1e-6就说明递推方程写法有问题。这一步走通之后储能模块基本就稳了。4.3 整数变量初始化与求解器选择日前模型因为有启停变量和储能互斥变量属于混合整数规划求解器的选择直接影响能否快速找到可行解。Yalmip只是建模层真正求解还要调用底层的求解器。如果用默认的内建求解器在小规模算例上可能能跑但规模稍微上去就慢得离谱。我实际对比下来商业求解器在小规模VPP调度中求解速度比开源求解器快很多而且数值稳定性更好不容易出现“明明有可行解却被告知无解”的情况。如果暂时没有商业求解器授权可以用开源求解器先跑通逻辑但要注意它处理整数变量的能力偏弱建议把模型规模控制小一点比如先把日前时段数从24降到12验证求解框架没问题再恢复到完整规模。4.4 初始化状态变量对结果的影响两级调度中日内模型的初始状态从哪里来通常是从日前计划的对应时段取值。但是日前模型和日内模型如果时间轴不对齐这个取值就会张冠李戴。举个例子日内过程从凌晨开始滚动第1轮窗口的初始SOC应该等于日前调度第1时段的最终SOC。如果代码里错取了日内自己的初始SOC常量相当于忽略了日前阶段的储能运行结果日内调度就失去了“衔接”的意义两条SOC曲线在交接处会出现明显的跳变。在代码里排查这个问题最直接的办法是把日前和日内模型输出的关键变量放到同一张图里对比尤其是SOC曲线和机组出力曲线。如果日前模型结果和日内模型结果在时间轴上错位或数值跳变迅速去检查状态传递部分的索引代码。4.5 潮流与联络线功率方向的符号约定向配电网购电和售电的方向性在目标函数和功率平衡中的符号非常容易写反。如果定义P_buy为正表示从配网购电P_sell为正表示向配电网售电那么功率平衡约束里它们是分列两侧的如果定义的是联络线的净交换功率正值购电、负值售电约束和目标的写法又完全不同。这种符号问题的隐蔽性在于它不一定会导致模型无法求解而是会让系统里的成本项反复横跳最后结果里购售电曲线呈现诡异的锯齿状。为避免这个问题我一般在代码注释的最前面写清楚所有变量的符号约定然后把“方向相反时结果会发生什么变化”作为一种自检手段把负荷数据全部翻倍观察联络线功率是否符合直觉把售电价抬高观察是否出现套利性的放电增加。通过这类简单的一致性测试可以很快发现隐藏的符号错误。问题现象可能原因快速排查方向求解时间异常长约束重复叠加或整数变量过多检查循环内是否重复累积约束无可行解时间步长不一致或状态变量传递错误检查和初始SOC相关的等式约束SOC曲线末尾上翘充放电效率折算方向写反单独测试储能模块功率平衡总有偏差某项资源被重复计入或漏掉逐项统计每时段的功率构成结果和论文图表差异过大参数单位错误或成本权重设置不当检查各成本项的量纲和数量级5. 结果分析从曲线里读出系统的调度逻辑调试通过之后分析结果不是简单地贴几张图就完事而是要能解释每条曲线的变化原因这样复现才算真正闭环。以我复现的典型结果为例日前调度通常会在低电价时段安排储能充电在高电价时段安排放电机组在负荷高峰时段开机光伏出力高峰时段则压低机组出力甚至让储能充电吸收多余电力。这些行为看起来是“理所当然的”但在代码里所有这些都是由目标函数和约束条件自动推导出来的正因为模型逻辑正确结果才呈现出经济学直觉上的合理性。日内调度与日前调度的对比要关注这样一个核心指标日内模型对日前计划的修正幅度。如果修正幅度整体较小说明日前预测足够准确两级调度的收益更多体现在日前全局优化上如果修正幅度很大说明日前预测偏差不可忽略日内修正才是系统保持功率平衡的关键。复现论文时通常要把这个信息用一张对比图呈现将日前计划曲线、日内实际执行曲线和实际新能源出力曲线画在一起再标注每个时段的偏差大小这个图就是论文中常见的“两级调度结果对比图”。我自己的做法是在结果后处理脚本里写一个通用函数输入变量名和自定义标签自动生成美观的对比图并保存到指定目录。这样日后换数据、换模型都只需要替换输入。要提一句的是结果图的横坐标和分辨率必须与原论文保持一致否则对比起来会很别扭前人复现的时候最容易忽视这个细节。写在最后的一点经验整个VPP多时间尺度调度模型从数学推导到Matlab复现流程不算短但每一步的坑都是可以提前预判的。个人体会比较深的一点是不要急着找现成的完整代码来“一键复现”论文里的模型参数、约束简化方式和数据设置都和具体场景强相关直接套别人的代码很容易出现系统性偏差。先花时间把原论文的目标函数、约束条件逐一抄到纸上在Matlab里逐块验证再拼装成完整模型这个“慢工”反而是整体耗时最少的路。另外一个实用建议搭建模型时把日前和日内两个子模块都写成函数输入全部统一为结构体这样既能方便测试也为之后扩展成实时调度或加入更多设备类型留了余地。我目前在这个框架上已经跑过含多台储能、多个可控机组以及柔性负荷参与的扩展算例改动量基本只集中在数据结构和约束拼接处模型主体完全复用。以后如果你想把鲁棒优化或随机规划加进去这个模块化的框架也能直接扩展不必推倒重来。