看到这个标题的时候我第一反应是虚拟电厂这个话题确实火但能把“日前调度”和“日内调度”真正放进同一个优化框架里讲明白的复现代码其实不多。多数所谓的多时间尺度调度要么是两段模型拼在一起要么干脆把日前结果直接当日内边界缺少闭环修正的逻辑。这篇复现的核心价值就是它把时间尺度拆成了“24小时全局规划”和“15分钟滚动修正”两层并且用Matlab实现了一套可运行的调度优化模型。我花了大概一周时间把整个代码逻辑走了一遍从数学建模、数据准备到求解器调用、结果画图中间也踩了不少坑。这篇文章就把整个项目的结构、建模思路、代码实现细节和大家容易忽略的问题一次性说清楚。如果你正在做虚拟电厂、微电网或者分布式能源调度的研究这篇内容可以直接作为你复现或者参考的基础。1. 先把项目看透虚拟电厂为什么要做多时间尺度1.1 虚拟电厂的“虚拟”到底指什么很多人第一次接触虚拟电厂这个概念时会被“虚拟”两个字迷惑。实际上它并不需要新建一座实体电厂而是把分散在用户侧、工业园区、乡村等地的分布式光伏、分散式风电、储能系统、微型燃气轮机、柔性负荷甚至电动汽车充放电资源通过通信和调度系统聚合起来统一参与电网运行和电力市场交易。对外它像一个能够响应调度指令的发电主体对内它需要协调各个分布式资源的出力、充放电状态和负荷调节能力。这就引出虚拟电厂建模的第一个核心问题聚合之后怎么协调因为里面的每一类资源特性都不一样。光伏和风电是间歇性电源出力受天气影响极大储能响应速度快但容量有限微型燃气轮机可以稳定出力和爬坡但运行成本高、有启停约束柔性负荷可以削峰但会影响用户舒适度。如果不用一个统一的优化框架去协调整个虚拟电厂在时间尺度上就会出现供需不匹配的问题。1.2 单时间尺度调度为什么不够用早期的微电网调度论文里很多都采用单层日前调度也就是根据第二天的负荷、风电、光伏预测曲线提前制定未来24小时里各机组每一时段的出力计划。这个方法在预测精度足够高的时候是可行的但现实情况是短期预测再准也会有几个百分点的误差特别是在风速突变和云层快速移动的时候光伏和风电的实际出力会明显偏离预测值。一旦实际出力偏离计划单层日前调度就会暴露出一个致命问题没有修正环节。比如日前计划里安排了储能系统在14点放电实际到14点光伏突然被一片云遮住出力少了200kW这时候如果没有日内调度来调节虚拟电厂的实时功率平衡就会被打破要么向主网购更多电要么出现偏差电量被考核。日内调度的意义就是利用更新、更准确的超短期预测数据对日前计划进行滚动修正。用生活里的场景类比日前调度相当于你出发前根据天气预报规划好一整天的行程日内调度则是你到了现场发现天气变了临时调整路线和交通工具。两者结合起来才是完整的多时间尺度调度。1.3 两个时间尺度的分工与联动逻辑在这一类顶刊复现项目里日前调度和日内调度的分工非常明确。日前调度层的优化周期通常是未来24小时时间分辨率有1小时也有15分钟也就是96点目标是把整个虚拟电厂第二天的运行成本最小化决策变量包括各机组出力、储能充放电功率、与主网的交换功率、需求响应量等。日内调度层则短得多典型的设计是向前滚动优化未来4小时每隔15分钟重新求解一次目标是在不走偏日前计划太远的前提下消除超短期预测误差带来的功率偏差。两层之间的联动是整个模型的关键。如果日内调度完全独立求解很可能出现日内方案和日前方案相差很大的情况导致设备频繁调节储能SOC被无形消耗。所以建模时一般会在日内调度目标函数中加入“出力偏差惩罚项”让日内结果尽量靠近日前计划的既定设定点同时又保留修正预测误差的灵活性。这个思路也是论文里的核心创新之一值得反复体会。2. 日前调度这套数学优化模型详细拆给你看2.1 目标函数有哪些成本项要加起来日前调度层的目标函数本质是一个最小化总运行成本的问题。如果你是做电力系统优化出身的读者会发现它的基本框架和经典经济调度很像只是变量和约束更复杂一些。典型的成本项至少包括以下几类向主网购电的费用这是根据分时电价计算的向主网售电的收益虚拟电厂多余电量可以反送电网这部分是负成本微型燃气轮机的燃料成本和启停成本储能充放电的运行损耗成本有时也写成折算后的寿命损耗成本需求响应补偿成本也就是调用可中断负荷时给用户的补贴用数学语言表达目标函数可以写为[ \min \sum_{t1}^{T} \left( C_{\text{buy},t} P_{\text{buy},t} - C_{\text{sell},t} P_{\text{sell},t} \sum_{g} C_{g,t} P_{g,t} C_{\text{ess},t} C_{\text{dr},t} \right) ]其中下标g表示第g台常规机组P为出力C为对应的成本系数。要注意区分购电和售电的价格并不相同购电价格一般是分时电价峰平谷不同售电价格可能是上网电价也可能等同于实时电价具体取决于论文设定的市场机制。复现代码时这个价格结构会直接影响优化结果通常用一个向量来存储全天的分时价格索引对应时段。我见过不少同学在这一步把购售电价格用一个常数代替结果整个模型失去了峰谷套利的可能性这是非常可惜的。2.2 约束条件怎么设才能真正反映虚拟电厂特性约束条件是模型里最考究的部分也是最容易出错的地方。整个优化问题的可行域就是由这些约束定义的。从工程角度看至少需要包含以下几类功率平衡约束是必须放在第一位的。它表示任意时刻虚拟电厂内部所有电源出力加上从主网购电功率等于负荷加上储能充电功率加上向主网售电的功率。这个约束的写法看起来简单但很容易在储能项符号上出错建议写之前先定义好储能充电和放电是两个非负变量而不是用一个可正可负的变量去建模否则后续约束表达式很容易乱。常规机组约束包括出力上下限和爬坡约束。出力上下限很好理解就是机组物理上能发的最小、最大功率。爬坡约束要体现机组在相邻时段之间的功率变化速度限制表达式通常写成[ -P_{\text{ramp},g} \le P_{g,t} - P_{g,t-1} \le P_{\text{ramp},g} ]这一约束在日前调度中特别重要因为时间分辨率是15分钟同样的爬坡速率约束在较小时段间隔下会对机组出力变化提出更快的要求如果你用的是1小时间隔的参数却没有改数值优化结果就可能要求机组在15分钟内完成原本设计为1小时的爬坡这在工程上是不合理的。储能约束是复现代码里最容易出现bug的地方。核心是把充放电功率、荷电状态SOC、容量限制联系起来。基本的递推关系是[ SOC_t SOC_{t-1} \eta_{\text{ch}} P_{\text{ch},t} \Delta t - \frac{P_{\text{dis},t}}{\eta_{\text{dis}}} \Delta t ]此外还需要约束SOC始终在安全上下限之间以及充放电功率不超过额定值。很多论文为了避免变量非线性问题会假设储能不能同时充电和放电这就要引入一个二进制变量来判断状态模型会从线性规划变成混合整数线性规划复杂度明显上升。另一类做法是把充放电同时进行视为允许但会浪费效率从而在目标函数里自然抑制这样就不用加二进制变量求解速度更快。需求响应约束通常是虚拟电厂区别于普通微电网的关键。典型做法是把部分负荷定义为可中断负荷允许调度中心在需要时削减一定比例的负荷但削减量有上限且单次削减持续时间不能太长。这里涉及用户舒适度的约束在数学模型里一般简化为每个调度周期内累计削减量不超过设定值。复现时我建议把需求响应的约束参数单独放在一个数据结构里方便后续测试不同策略。2.3 风光出力的不确定性用什么方式纳入模型因为我前面说了多时间尺度调度的一个重要原因是预测误差那就必须在日前调度模型中考虑不确定性。最常用的两类方法是随机场景法和鲁棒优化法。顶刊复现项目里碰到最多的是前者。场景法的基本思路是根据历史预测误差分布生成若干组可能的风光出力曲线每一条曲线对应一个场景然后让优化结果在所有场景下都满足约束同时最小化目标函数的期望值。这样得到的调度方案不会因为某一场景特别极端而完全没有可行解也不至于像鲁棒法那样过于保守。复现过程中场景生成一般用蒙特卡洛抽样来做。先假设风电和光伏预测误差服从正态分布用历史数据的均值方差来估计参数然后产生几百条出力曲线。但直接把几百条曲线放进优化模型会让变量爆炸所以还需要做场景削减比较常用的是快速前代削减法或者同步回代削减法。后者是两步走的思路先用概率距离判据找到两两之间距离最小的场景对然后删掉其中一个把被删场景的概率累加到保留场景上重复删除直到场景数量降到目标值比如10个或20个。这个处理能力会显著影响优化结果的鲁棒性。3. 日内调度层滚动优化的具体实现思路3.1 为什么日内调度必须滚动进行日内调度层的逻辑和日前层有本质区别。日前调度是一次性求解未来24小时的全时段优化问题日内调度则运行在滚动优化框架下也就是每隔一定时间触发一次求解每次只求解未来有限个时段待执行完当前时段后把新的预测数据带进来再次求解。这个设计有两个原因。第一个原因是超短期预测精度随时间推移明显提升。相对日前预测未来1到4小时的功率预测误差可以下降一半以上特别是光伏出力基于实时辐照度变化的超短期预测能捕捉到很多日前完全无法预见的波动。第二个原因是滚动优化天然具备反馈修正能力当实际执行中上一时段产生偏差后下一轮求解会把实际状态作为新的初始条件重新规划后续动作从而避免偏差累积。3.2 日内调度模型和日前模型的异同日内调度层的目标函数会在日前基础上多出两个东西。一个是偏差惩罚项用于限制日内决策变量偏离日前计划值的程度。这个惩罚项通常写成绝对值的线性形式也就是[ \lambda_{\text{dev}} \left| P_{g,t} - P_{g,t}^{\text{day-ahead}} \right| ]为了避免出现绝对值这种非线性项通常引入一个非负辅助变量来线性化增加两个不等式约束把差值的上界约束住。另一个是根据日内更新的电价信息调整购售电策略因此有些论文会让日内模型重新优化少量时段的购售电功率。约束方面日内调度和日前模型大部分相同但储能初始SOC不再是从0开始而是以当前实际SOC作为初始条件参与滚动。这看起来是细节实战中如果你没有正确更新每个滚动窗口的初始状态优化出来的第一时段储能功率会完全乱套。我的做法是在每个滚动窗口求解完成后只执行第一时段的决策然后根据实测数据更新SOC再滚动到下一个窗口。3.3 两个时间尺度的衔接日前计划怎么影响日内决策这一块是整个复现项目的灵魂。纯日内模型如果不看日前计划可能会出现频繁调节设备、储能SOC大幅波动、与主网交换功率振荡等不理想的结果。为了协调两层目标典型做法是在日内目标函数中加入“计划跟踪项”把日前计划作为参考轨迹。这里有三种常见策略我在实际编码时都试过。第一种是硬性约束要求日内出力与日前计划的偏差不得超过某阈值比如不超过日前计划的10%。这种策略最稳定但如果预测误差偏大可能会造成新的不可行约束。第二种是软惩罚即在目标函数中加入偏差惩罚系数偏差可以在一定范围内发生但越远代价越大这是最推荐的做法。第三种是自适应权重把惩罚系数设成动态变化的比如在峰时段加强跟踪、平时段放宽这个做法更像真实电力系统调度风格不过需要额外引入时段权重参数。复现时代码里最容易忽略的衔接机制是“日内滚动窗口只执行第一步”。很多初学者会把整个日内优化结果一次性执行到底完全失去了滚动修正的意义。正确的操作流程是求解得到一个4小时窗口各时段的最优决策但只把第一时段的结果下发执行记录实际SOC和实际出力等下一轮触发时重新加载超短期预测再次求解。这和模型预测控制MPC的经典套路是相通的。4. Matlab代码实现从0到1搭建完整调度框架4.1 代码整体结构设计拿到这类复现项目我建议先不要急着看细节代码先梳理整个项目的文件结构和执行主线。一个清晰合理的Matlab调度优化项目至少要包含五个模块数据初始化、参数配置、模型构建、求解执行、结果可视化。把它们分成不同脚本来写后续改参数和换场景时能省很多时间。典型的文件组织方式大致如下main.m整个流程的入口依次调用数据初始化、日前调度求解、日内滚动求解、画图load_data.m读取负荷、风光出力、电价等基础数据保存到结构体set_parameters.m定义虚拟电厂各设备参数比如储能容量、机组爬坡速率、各时段电价build_dayahead_model.m搭建日前调度的决策变量、目标函数、约束solve_dayahead.m调用求解器求解日前问题并把结果保存成基准计划build_intraday_model.m搭建日内滚动优化的模型solve_intraday.m实现滚动窗口的循环求解逻辑plot_results.m把结果画成各种功率曲线、SOC曲线、电价叠加图这个结构的核心思想是分离数据和逻辑。因为调度优化问题最花时间的往往不是求解而是反复调试数据格式和单位如果把全部都堆在main里后面改起来非常痛苦。我自己刚做复现时就是把所有模型写在一个脚本里结果每次想换一个预测误差参数都得全局搜索替换效率极低。4.2 参数设置与数据结构表对于复现项目参数设置的合理性直接决定结果是否和论文匹配。以下是我整理的一张重要参数表你可以把它当作代码的初始化清单参数名称典型值说明日前调度时段数T96对应24小时、15分钟分辨率日内滚动窗口长度H16对应未来4小时日内滚动触发间隔Δt1个时段即15分钟储能额定容量1 MWh根据微网总负荷规模设定储能最大充电功率0.25 MW一般取容量相关比例储能SOC上下限[0.1, 0.9]防止过充过放燃气轮机最大出力0.5 MW实际按聚合DG容量折算风电预测误差标准差10%按论文历史数据设定光伏预测误差标准差15%按论文历史数据设定购电峰谷电价[0.4, 0.8, 1.2] 元/kWh与市场时段配置一致我建议在代码里用一个结构体或者类来统一存储这些参数。比如用para.ess.capacity表示储能容量para.ess.ch_max表示最大充电功率。千万别把参数直接散落在代码各处这是后面排查问题时最容易抓狂的根源。4.3 用Matlab YALMIP搭建优化模型的核心步骤如果从零手写大规模线性规划的标准矩阵形式工程量非常大而且代码可读性差。实际复现顶刊模型时用Matlab的优化建模工具箱是最高效的路径。我个人习惯用YALMIP来建模它最大的优势是可以把变量、约束、目标函数用接近数学表达式的形式写出来模型维护成本低。之后调用商业求解器求解即可。下面展示一个简化版的日前调度模型搭建片段思路是定义决策变量、写目标函数、加约束、求解四步% 定义决策变量时段数T96 P_buy sdpvar(T, 1); % 向主网购电功率 P_sell sdpvar(T, 1); % 向主网售电功率 P_wt sdpvar(T, 1); % 风电实际出力 P_pv sdpvar(T, 1); % 光伏实际出力 P_g sdpvar(T, 1); % 燃气轮机出力 P_ch sdpvar(T, 1); % 储能充电功率 P_dis sdpvar(T, 1); % 储能放电功率 SOC sdpvar(T1, 1); % 储能荷电状态 % 日前的目标函数 objective sum(price_buy .* P_buy - price_sell .* P_sell ... c_g * P_g c_ess * (P_ch P_dis)); % 约束条件 Constraints []; % 功率平衡约束每个时段都满足 for t 1:T Constraints [Constraints; P_buy(t) P_wt(t) P_pv(t) P_g(t) P_dis(t) ... load_basic(t) P_ch(t) P_sell(t)]; end % 储能SOC递推约束 for t 1:T Constraints [Constraints; SOC(t1) SOC(t) eta_ch * P_ch(t) * dt / Ess_cap ... - P_dis(t) * dt / (eta_dis * Ess_cap)]; end % 求解 optimize(Constraints, objective, sdpsettings(solver,gurobi));这个片段是示意性质真实代码里还需要加上机组出力上下限、爬坡约束、储能SOC上下限、购售电功率上限等。注意YALMIP里变量和参数都是列向量时目标函数里直接用.*逐元素相乘能够得到每一时段的成本累加这在代码里是个很简洁的写法。4.4 场景削减的简化实现思路再具体说一下场景削减在Matlab里怎么落地。假设已经通过随机抽样生成了500条风电出力曲线现在要削减到20个代表场景。同步回代削减法的代码逻辑可以概括为初始化每个场景的初始概率为1/500以场景间的欧式距离作为相似度度量找到距离最小的一对场景删除其中一个并把它的概率加到另一个上面重复第3步直到只剩20个场景这个过程在Matlab里实现时要注意如果每对场景都计算Euclidean距离计算量是O(n²)的量级。500个场景就是12.5万对在循环里反复搜索效率会很低。实际代码我建议先算好整个距离矩阵然后每轮循环只需要从矩阵里找到最小值再更新受影响场景对应的行和列。场景削减的目标不是追求最优解而是在控制误差的前提下尽可能保留极端但概率较小的事件所以在削减之后最好对比一下削减前后的期望出力曲线确保峰值位置基本没有偏移。4.5 结果可视化怎么一眼看出调度是否合理代码跑的动只是第一步结果对不对还得靠画图来验证。我画图的核心原则是“同一张图里放相关的曲线观察它们之间的联动关系”。典型的三张图如下第一张是功率平衡图。把负荷、风电、光伏、燃气轮机、储能净出力、购售电功率叠加在一张图里用面积图或者堆叠图展示。这张图主要检查所有时段是否满足功率平衡如果某个时段面积明显对不上大概率是约束写漏了。第二张是储能SOC图。这张图要检查SOC是否一直在上下限范围内以及是否呈现出“谷时充电、峰时放电”的合理行为。如果曲线出现锯齿状抖动说明日内滚动修正过度需要加大偏差惩罚系数。第三张是电价和购售电功率的对比图。正常情况下购电应该集中在低电价时段售电应该集中在高电价时段。如果曲线趋势完全和电价相反说明电价单位或者符号处理有误这类问题不看图很难发现。5. 复现过程中最容易踩的坑5.1 求解速度慢或者内存爆炸我刚复现时同样遇到过这个问题日前调度设置了96时段又加入储能二进制变量最后用默认求解器求解结果跑了十几分钟才收敛。排查下来问题出在二进制变量的数量上。如果你为储能同时充电和放电各设置一个二进制变量96个时段直接多出192个二进制变量再叠加机组启停变量问题规模瞬间膨胀。解决思路有三个。一是把时间分辨率从15分钟改为1小时先验证模型逻辑是否正确再改回15分钟跑最终结果。二是给模型添加更多有效边界比如购电功率上限由线路容量决定储能充放电功率上限由PCS容量决定这些边界能显著缩小求解器的搜索空间。三是如果模型允许同时充放电但效率会受损可以完全去掉二进制变量让目标函数自然抑制无意义的充放电行为。5.2 出现无可行解是一段必修课这类优化模型最常见的报错就是Infeasible problem。出现这种问题第一反应不要怀疑求解器坏了而是要去检查约束是否自相矛盾。以我的经验最常出问题的位置是储能SOC递推约束和爬坡约束的组合如果储能初始SOC设置得太低而第一时段又要大功率放电满足负荷很可能无论怎么优化都找不到满足条件的SOC路径。排查时可以用一个很实用的技巧在约束里加入一个松弛变量让它乘以一个大惩罚系数进入目标函数。如果求解结果显示松弛变量不为0就说明问题确实不可行而且松弛变量对应的时段和约束就是矛盾源。这个技巧在开发阶段几乎每轮调试都要用等到模型稳定后再把这个松弛项删掉。5.3 结果趋势和论文趋势对不上复现论文时最容易出现的挫败感是模型能正常求解但画出来的曲线分布和论文图差得很远。这通常不是模型结构错误而是数据匹配问题。最常见的是量纲错误论文里功率用MW你的代码里用kW求解结果数值差1000倍曲线形状看起来就会完全不同。其次是预测误差参数和数据生成方式不同论文用的可能是实际历史数据没有直接开源你只能自己用随机数生成曲线形态当然不会一模一样。处理这个问题要看清楚复现目标复现的目的是验证模型逻辑和方法不是追求逐像素一致的图。只要你的功率平衡约束满足、SOC没有越限、购售电行为符合分时电价规律核心逻辑就和论文吻合。数值上有些差异属于正常现象。5.4 常见问题速查表问题现象可能原因解决思路求解速度极慢二进制变量过多、未给变量添加有限边界删除冗余二进制变量添加功率上下限无可行解SOC递推约束与初始值矛盾、爬坡约束冲突加入松弛变量定位矛盾约束储能SOC长期保持在边界循环逻辑没有正确更新初始SOC检查滚动窗口是否只执行第一时段功率平衡不满足充电/放电符号搞反在代码里单独验证单个时段的功率等式购电电价低时段反而少购电购电价格向量索引错位对比电价曲线和购电功率曲线的峰谷位置日内结果与日前结果偏差大偏差惩罚项未生效检查是否用了绝对值线性化辅助变量是否加入场景削减后结果不稳定削减数量太少或随机种子未固定增加场景数量设置随机种子6. 从成功复现到真正理解再到找到自己的研究方向6.1 复现论文的正确打开方式复现不是把论文代码下载下来跑一遍那么简单更不是对着公式抄一遍就完事。我分享一个自己总结的流程先通读论文把目标函数、约束条件、决策变量全部用数学公式的方式整理到纸上弄清楚每一个下标、每一个参数的含义然后不看代码自己尝试用Matlab YALMIP把模型搭出来遇到问题再回头对照开源代码看看对方在哪些地方和你理解得不同。这个过程虽然慢一些但对模型的理解深度完全不是直接跑代码能比的。6.2 可以继续扩展的几个方向这个项目跑通之后你可以沿着好几个方向做扩展。一是在模型中引入价格型需求响应把负荷建模为对电价敏感的可转移负荷而不只是简单的可中断负荷二是在日内调度层加入更精细的预测误差更新机制比如用指数平滑法或卡尔曼滤波更新预测曲线三是在储能建模中加入寿命损耗折算成本更贴近实际工程决策。这些扩展方向都和已有代码框架兼容改起来也顺手。6.3 关于参数调整的一些心得项目跑通之后建议你系统地做一次参数敏感性分析。做法是固定其他参数只改变某一个参数比如储能容量从0.5MWh变到2MWh记录总成本、购电曲线、SOC曲线这些指标的变化。这样你能真正理解每个参数在模型里扮演的角色也为以后自己写论文积累数据素材。我在实际操作中发现目前这套复现框架最让我受益的地方是它以非常简单明了的思路贯通了两个时间尺度让我真正理解了日前计划和日内修正之间绝不是简单的“上下级命令”关系而是一种带反馈的持续协商过程。这种认知比单纯会跑代码重要得多。