我做了六年多能源微网调度,前前后后调过的优化模型少说也有几十个版本。从最早单纯做日前经济调度,到后来被风电、光伏的预测误差和负荷波动折磨到崩溃,最终落地到“多时间尺度滚动优化+双层调度”这一套架构,中间踩过的坑、推倒重来的模型,说实话比写出来的论文多得多。这期就把这套东西从头到尾拆开讲一遍。
先说说为什么非要搞多时间尺度滚动优化。微网这东西,源、荷两侧的不确定性是绕不开的硬约束。光伏凌晨没出力,中午突然爬坡到满发;热负荷早高峰和晚高峰完全两个曲线;燃气轮机的爬坡速率又限制着电、热两套输出的自由度。你要是用一个静态的日前计划硬扛一整天,遇到天气突变结果基本就是一个字——赔。热储能SOC顶到上限,电储能跟着过放,然后弃风弃光,整个系统的经济性一塌糊涂。滚动优化的核心逻辑说白了就是“走一步看一步”:不是一次性把全天的计划都锁死,而是每隔15分钟、1小时这样的尺度重新算一遍,到了新时刻就刷新预测数据再优化一次。这样每一步的计划都能吃到最新的信息,预测误差就被分摊掉了。
双层调度则是另一个维度的问题。微网里的决策其实分两个层级:上层算“明天怎么安排设备出力”,追求的是整天、整周的经济性最优;下层管“当下15分钟怎么把偏差纠回来”,追求的是跟踪精度和运行安全性。这两层目标函数不一样、时间尺度不一样、约束条件也不一样,硬塞到一个模型里算,轻则求解时间爆炸,重则压根无解。把两层拆开,用传递变量衔接起来,才是工程上真正能落地的解法。
这篇文章就把这套“基于多时间尺度滚动优化的多能源微网双层调度模型”从目标函数到约束条件、从参数配置到求解调试,完整地捋一遍。不光给能直接抄的MATLAB/YALMIP代码思路,还会把我调试过程中踩过的典型坑列成速查表。适合正在做微网优化调度研究的入门者,也适合已经在写代码但效果总是不对劲、想回头检查建模细节的朋友。
1. 先搭框架:多能源微网调度的核心难题在哪里
1.1 “多能源”到底多在哪——电、热、气三套系统的耦合关系
传统电力调度只盯着一个变量:有功功率。多能源微网完全不同,你面对的是电、热、气三套物理系统,它们各自有各自的动态响应时间。电力系统是毫秒级响应,热能系统是分钟级滞后,天然气网络相对慢一些。当这三套系统耦合在一个微网里的时候,调度模型里的变量就不是简单的“发多少电”,而是“多少电能用于电负荷、多少电转热、燃机多少出力同时带出电和热、蓄热罐充放多少热”。
我做的模型里主要有这几类设备:燃气轮机(CHP)、光伏、风电、电储能(ESS)、电锅炉(EB)、蓄热罐(TST)。光伏和风电是不可控电源,只能预测并尽量消纳;燃气轮机是唯一具备连续调节能力的主力电源;电储能的响应快但是能量吞吐有限,SOC必须在安全区间;蓄热罐本质上是一个能量缓冲器,它的角色特别关键——因为热负荷波动大而且燃机供热的调节比供电更笨重,蓄热罐可以削峰填谷,让燃气轮机不需要频繁大幅度调整出力。
如果只建立单一电力系统模型,忽略热的耦合,那燃气轮机的“热电联产特性”就被丢掉了。实际中燃机的电出力和热出力不是独立的:在凝汽工况下,电出力提升的同时热出力也在提升,你要把可运行域画成一个二维区域。很多初学者的模型把热电比设成常数,这个简化在离线仿真里看不出大问题,但一旦接入真实负荷曲线,日内的偏差能导致热力不足,整个微网就得靠电锅炉顶上,经济性立刻劣化。
1.2 不确定性冲击下,单一时间尺度为什么必然失效
单一时间尺度调度的本质问题可以归结为一句话:用一锤子买卖应对不断变化的行情。日前计划基于的是24小时前的预测数据,而光伏出力预测在早上10点到下午3点之间的误差动辄20%。要是日前计划把中午时段的燃机出力压得很低、期望靠光伏顶住负荷,结果云端厚了,光伏实际出力只有预测的60%,这时候电储能又顶不住大功率缺口,怎么办?只能急调电锅炉或者切负荷。
这就是为什么行业里最终普遍采用多时间尺度的核心原因。我实现的这套模型分了三层时间尺度:日前调度(24小时,分辨率1小时)、日内滚动(未来4小时,分辨率15分钟)、实时反馈(当前时刻,分辨率5分钟或者更短)。每一层干的事不一样,用一个通俗的比喻来说:
- 日前调度是“看天气预报决定明天穿什么衣服”;
- 日内滚动是“到了明天上午发现降温了,回宿舍加一件外套”;
- 实时反馈是“出了门发现下雨,立刻撑伞”。
三层各管各的事,但底层的信息要传上去、上层的目标要传下来。这就是“双层”介入的地方。
1.3 双层模型的定位:为什么不能用一个大模型全包
有朋友会问:既然滚动优化已经能处理不确定性,为什么还要分层?直接建一个混合整数线性规划大模型,把所有约束全放进去,每个滚动步都求解一次,不就行了吗?
我从实际求解的角度说一下问题出在哪。微网里燃气轮机的启停状态是0-1整数变量,电锅炉、储能充放状态也带整数变量。模型一旦包含未来24小时、分辨率15分钟的数据,那就是96个时段的混合整数规划,变量规模轻松上百上千个。YALMIP调用CPLEX或者GUROBI解这种大模型,纯靠硬件硬算,单次求解可能就要几分钟甚至更久。实时控制环节哪里等得了几分钟?等求解完了,风速又变了,预测误差又出来了。
另一方面,不同层级的控制目标不一样。日前层追求的是全时段总运行成本最低,它希望燃机在某个时段多开一点、储能在低谷充电,这些决定是从经济性最优的角度出发的。日内层追求的则是“在尽量不牺牲经济性的前提下,把功率偏差压到最小”,它需要的是灵活调整的余量。把这两类目标混在一起加权,惩罚系数调起来极其痛苦。分层以后,上层给下层传递的是计划值和可调边界,下层在上层框定的可行域内自行优化,这样既保住了经济性,又保留了实时校正的灵活性。
2. 双层模型的核心拆解:目标函数、约束与传递变量
2.1 上层日前调度层的模型结构
上层日前调度层的目标函数是最经典的全时段运行成本最小化,我用的表达式是:
min F = Σ (C_fuel + C_OM + C_purchase) - Σ C_sell
其中C_fuel是燃气轮机的燃料成本,用二次函数拟合,C_OM是设备运维成本,C_purchase是向大网购电的费用,C_sell是余电上网的收入。考虑碳排放约束的时候,还可以在这个基础上加一个碳税项,把单位碳排放成本乘上去就行。我建议初学者先把这一项注释掉,先把主逻辑跑通。
约束条件分两块:功率平衡约束和设备约束。
电功率平衡约束长这样: P_pv + P_wt + P_chp_e + P_dis - P_char - P_eb + P_buy - P_sell = P_load
热功率平衡约束: H_chp + H_eb + H_dis_tst - H_char_tst = H_load
设备约束里,重点注意燃气轮机的热电运行域。简化版本里我用电出力的上下限和热电比来表示,但如果你想做得更精确,用凸包顶点法描述可行域会让模型贴合真实机组很多。储能约束里的核心是SOC递推公式:
SOC(t+1) = SOC(t) + (η_char * P_char - P_dis / η_dis) * Δt / E_cap
这里两个效率η_char和η_dis不相等,意味着储能在“充多少放多少”之间存在内在损耗,这直接导致模型倾向于减少不必要的充放循环。
2.2 下层日内滚动层的目标函数设计
下层日内滚动层因为要频繁求解,所以我在设计时特意把模型保持成线性规划或二次规划,尽量避免引入整数变量。怎么做到?让滚动层继承上层给出的燃机启停状态和储能的充放状态,这一层只需要调整出力大小,不重新决策启停。
日内滚动层的目标函数长这样:
min G = Σ (α * ΔP + β * ΔH + γ * ΔSOC + ω * penalty_slack)
其中ΔP、ΔH是各设备出力的调整量加权和,ΔSOC是储能SOC相对计划值的偏差,penalty_slack是失负荷的松弛变量。目标不是“重新找一个全局最优点”,而是“让当前出力尽量贴近日前计划,同时解决掉预测误差带来的不平衡”。这个目标函数里惩罚系数的比例,直接决定了下层优化的行为:
- 如果α设得太大,储能会被“冻住”,不主动充放来配合系统调节,灵活性下降;
- 如果γ设得太大,储能又会频繁调节SOC,导致寿命损耗加大;
- 如果penalty_slack设得太小,模型可能会自己选择弃负荷而不是调节设备出力。
我调试下来相对稳妥的经验值是:失负荷罚项系数要比设备调整量系数大10到50倍,系统的安全约束才有机会被优先满足。系数太小算出来的结果虽然“经济”但是不可行,现实中一接入负荷就掉链子。
2.3 层间传递变量:衔接的好坏决定模型成败
双层模型能不能跑好,很大程度取决于“层间传什么”。我在这套模型里设计的传递变量有三类:计划值、状态值、上下限。
第一类是计划值,就是日前调度算出来的设备出力计划序列。日内滚动会往后看4个小时,但4小时窗口内各设备的参考值都来自日前计划。第二类是状态值,包括储能SOC的参考轨迹、蓄热罐的蓄热量参考轨迹、燃气轮机的启停状态和最小运行/停机时间约束。第三类是上下限,这个最容易被忽略。日前层除了给出计划值,还应该给出调整范围,比如储能的SOC允许偏离参考值±15%,燃机出力可以在计划值基础上上下浮动20%。如果没有界限,日内层为了满足当前时刻的功率平衡,可能把储能SOC拉得很深,等到了晚高峰想放没得放,这就是“滚动优化的短视问题”。
这个上下限边界的设计需要跟调度员经验挂钩,不要对称设置。光伏大发时段,储能充电区间可以放宽,放电区间收窄;晚高峰时段反过来。这种非对称边界能大大减少日内层和日前层打架的概率。
3. 滚动优化的实现细节:从设计到代码落地
3.1 多时间尺度怎么切分:三层循环的调度策略
我最终实现的调度策略是嵌套三层循环。最外层是日前调度,一次性优化24小时;中间层是日内滚动,每15分钟触发一次,每次优化未来4小时、16个时段的计划;最内层是实时控制,每5分钟触发一次,只优化未来15分钟,并把结果直接发给微网能量管理系统执行。
实现流程用伪代码表述如下:
for d in 1:D // 每个调度日 读取日前预测数据(光伏、风、电负荷、热负荷) solve 日前模型(24h, 1h分辨率) 下发 日前计划P_ref、SOC_ref、启停状态 for i in 1:96 // 每个日内时段,15分钟 if 时间到达滚动触发点 读取最新超短期预测数据(未来4h) 读取当前实测状态SOC、温度、出力 solve 日内滚动模型(4h, 15min分辨率, 固定启停) 下发 未来15min计划 for j in 1:3 // 每个实时控制步,5分钟 读取实时量测数据 如果有偏差则调用实时调整策略 执行 出力指令 end end end这个循环的复杂度不高,但关键点是“触发条件”。我一开始做的是定时触发,每到整15分就重算一次。后来发现,如果预测误差很小、系统运行平稳,反复重算纯属浪费算力。改进方案是增加一个偏差阈值触发,当电网频率偏差或者联络线功率偏差超过死区时才触发新一轮滚动优化。这招在算力受限的嵌入式控制器上特别实用。
3.2 预测数据怎么滚动刷新:不能只换数字,还要换分布
滚动优化和“旧数据反复算”之间的本质区别在于预测刷新。很多人做日内滚动时,只是把日前预测数据里的最新时点数值替换成实测值,其他时段保持不变。这种做法只解决了“当前时刻的偏差”,没有解决“未来时段预测精度随临近而提升”这件关键的事。
实际上,超短期预测的精髓在于它的误差会随时近性而缩小。比如说,日前预测明天上午10点的光伏出力是500 kW,到了当天早上9点再预测,10点光伏出力的误差可能就缩到10%以内;早上9点45分再预测,误差进一步缩到3%。所以正确做法是:每次滚动时,要把未来4小时窗口内所有时段的预测值都刷新一遍,而不是只替换当前点。
我在这套模型里做的是双层预测数据接口:上层用ECMWF数值天气预报数据和历史负荷回归模型,上下层之间的数据交互通过一个缓存区实现。每到一个滚动时刻,缓存区里的全窗口预测都会被新预测覆盖,保证模型读到的是“当前时刻能拿到的最新信息”。
3.3 YALMIP环境下双层模型的具体代码实现
虽然理论上模型挺复杂,但实际用MATLAB+YALMIP实现并没有想象中那么难。我贴一个简化版的日内滚动求解核心代码,注释里给出关键配置。
% 日内滚动优化简化示例 % 变量定义 P_chp = sdpvar(N_h, 1); % 燃机出力 P_dis = sdpvar(N_h, 1); % 储能放电 P_char = sdpvar(N_h, 1); % 储能充电 H_chp = sdpvar(N_h, 1); % 燃机供热 H_eb = sdpvar(N_h, 1); % 电锅炉供热 slack = sdpvar(N_h, 1); % 失负荷惩罚变量 % 目标函数 Objective = 0; for t = 1:N_h Objective = Objective + alpha * (P_chp(t) - P_chp_ref(t))^2; Objective = Objective + beta * (H_eb(t) - H_eb_ref(t))^2; Objective = Objective + omega * slack(t); end % 约束条件 Constraints = []; for t = 1:N_h % 电功率平衡 Constraints = [Constraints, ... P_pv_pred(t) + P_wt_pred(t) + P_chp(t) + P_dis(t) - P_char(t) ... - P_eb(t) + P_buy(t) - P_sell(t) == P_load_pred(t) - slack(t)]; % SOC递推约束 if t < N_h SOC(t+1) = SOC(t) + (eta_char * P_char(t) - P_dis(t) / eta_dis) * dt / E_cap; Constraints = [Constraints, SOC_min <= SOC(t+1) <= SOC_max]; end % 燃机出力限幅 Constraints = [Constraints, P_chp_min <= P_chp(t) <= P_chp_max]; % 储能不能同时充放 Constraints = [Constraints, P_char(t) >= 0, P_dis(t) >= 0]; end % 求解 options = sdpsettings('solver', 'gurobi', 'verbose', 0); optimize(Constraints, Objective, options);这段代码已经可以跑通一个15分钟分辨率的日内滚动窗口。值得注意的细节是:我在电功率平衡约束里把失负荷变量slack放在负荷一侧,而不是放在发用两侧任意一侧,这样做在求解器里不容易引起数值问题。另外,为避免储能在相邻时段出现频繁的充/放切换,我给充放变量加了“互斥”约束,在连续时间模型里可以用P_char * P_dis = 0表达,但在MILP里更常见的做法是引入二值变量。如果你用YALMIP和Gurobi,直接用implies约束也行,不过求解速度会下降。
3.4 求解器选择与参数迭代:Gurobi和CPLEX的取舍
既然是混合整数规划,求解器的选择就直接决定了这套模型能不能实用。我自己用过的求解器有Gurobi、CPLEX和CBC三个。结论很直接:大模型上CBC基本跑不动,Gurobi和CPLEX在性能上没有数量级的差别,但Gurobi在Linux服务器上的部署便利程度更好,许可证管理也更省心。
在实际配置里,我推荐给Gurobi设置几个关键参数。第一个是MIPGap,设为0.01甚至0.005,求解质量和速度就能平衡好。如果你想快速验证模型逻辑,建议直接把MIPGap设成0.05,速度快一个数量级,虽然结果不是最优,但是用来检查约束有没有写错完全够用。第二个是TimeLimit,我习惯设为120秒。滚动优化窗口最大只有4个小时、16个时段,120秒如果还解不出来,大概率是模型里数值病态,不是求解器的问题。第三个是Threads,如果服务器是多核,给4到8个线程,对整数变量的分支定界有很大提升。这些都是在真实工程环境里实打实有用的小技巧。
4. 算例分析与参数配置:拿一个典型微网系统跑通全流程
4.1 微网系统参数与负荷曲线设定
为了让模型“活”起来,我搭建了一个典型的多能源微网测试系统。光伏装机500 kW,风电装机300 kW,燃气轮机额定电出力200 kW,热电比1.5,电储能容量400 kWh,蓄热罐容量800 kWh,电锅炉额定功率100 kW。微网与大电网的联络线功率上限是300 kW,允许余电上网。
负荷曲线方面,我设了一个典型的冬季日场景:电负荷早高峰出现在8:00到11:00,晚高峰17:00到21:00,峰值约420 kW;热负荷全天稳定在250 kW左右,早晚略有波动。光伏出力近似正弦型,峰值出现在12:30左右,约400 kW;风电出力则设定成一个随机波动序列,平均值150 kW。这个场景的特点是中午光伏大发、电负荷较低,有非常明显的弃光风险;晚高峰光伏归零而负荷抬升,需要燃气轮机和储能协同顶上去。
4.2 结果对比:单层日前调度 vs 双层滚动调度
跑完整套模型后,我做了一个关键对比:一组用纯日前调度,另一组用双层滚动优化。结果差异非常显著。
纯日前调度模式下,光伏预测误差导致中午时段出现了两小时的弃光,弃光率约12%,同时因为下午实际光伏出力低于预测,燃机被迫在16:00开始爬坡加出力,而它的爬坡速率只有5%/分钟,晚高峰前夕出现了短暂的失负荷风险。整套系统日运行成本约2860元,热舒适度评分也掉到了89%。
双层滚动模式下,日前层给出的计划基本维持不变,但日内滚动层在中午光伏大发时段提前降低了燃机出力,同时让电储能多充电、蓄热罐多蓄热,把弃光率压到了3%以内。晚高峰前滚动层又提前调用储能放电,燃机出力平稳过渡,系统没有任何失负荷。最终日运行成本约2470元,下降了13.6%,热舒适度评分稳定在97以上。
这个结果完全符合预期:滚动的价值不在于“找到最优解”,而在于“在不确定性下不做错误的决定”。单层模型的最优解看的是预测数据,预测错了,最优就变成最差了。滚动优化的核心优势是它随时在纠偏,让系统始终在真实可行域里运行。
4.3 关键参数灵敏度:惩罚系数与滚动窗口的调参经验
参数灵敏度分析也是必须做的环节。我试过把滚动窗口从1小时拉到4小时再拉到8小时,结论是窗口长短不能一概而论。窗口太短,模型只看到眼前的问题,很容易在窗口边界处产生“接缝”,比如在窗口结束时刻储能SOC会贴着下限走;窗口太长,模型的预测数据后半段精度下降,优化结果反而被坏数据误导。
在我这套系统里,4小时滚动窗口配合15分钟分辨率是最稳的组合。整个日内96个时段,每个滚动步只需要优化16个时段,求解时间不到5秒,实时性完全够用。
另外,关于层间SOC参考值追踪惩罚系数γ,我也做过系统的灵敏度扫描。γ从0.01升到1,储能的日均等效循环次数从4.2次下降到2.8次,但系统的弃光率从1%升到了6%。这就意味着你把SOC追踪抓得太死,储能就失去了调节作用。我的建议是:这个系数不要拍脑袋定,做一个简单的参数扫描,在“储能寿命损耗”和“系统调节能力”之间找到你自己的平衡点。
5. 常见问题与排查技巧实录
5.1 求解器报“infeasible problem”怎么排查
双层模型最让人崩溃的错误就是求解器提示infeasible,而且经常是一崩溃就是一整套流程,查起来没头绪。按照我的经验,排查顺序应该是:先查约束有没有互相矛盾,再查变量边界有没有设反,最后查数据的单位有没有统一。
最容易出现的低级错误是SOC递推约束的初始值不对。比如SOC(0)设在0.9,但E_cap只有100 kWh,第一时段允许的最大充电功率是50 kW,时段长度0.25小时,那么充电0.25小时SOC变化只有12.5%,但约束里SOC上限写成了0.95,递推出来的SOC就超限,于是模型infeasible。解决方式很简单:把SOC上限往高调或者初始值往低调,并顺手把充放电功率上限跟E_cap做一致性检查,别让它们在数量级上差出好几倍。
另一个隐藏很深的坑是热功率平衡里的蓄热罐模型。蓄热罐的充放热功率上限和它的蓄热容量之间必须满足组合约束:如果当前蓄热量已经接近满罐,那么同一时刻你不可能还给蓄热罐灌大功率热量。忽视这个约束,模型也可能一直infeasible,而且报错信息还不明显。
5.2 双层交互出现“振荡”怎么处理
双层模型还有一种典型病态表现:上层给出的参考值使得下层不断在两个极端方案之间切换。比如某时段上层计划燃机出力300 kW,下层第一次滚动把它压到270 kW;下一次滚动,被压的燃机出力导致热缺口,下层又把燃机拉到295 kW;再下一次又压回去。系统出力呈锯齿状振荡,设备磨损加剧,储能SOC也跟着上下抖。
我的处理办法是给下层模型加一个“调整步长”约束,也就是每次滚动的决策变量相对上一次实际执行的调整量不能超过预设上限。比如燃气轮机的出力变化幅度每次不超过20 kW,储能充放电功率变化每次不超过10 kW。这样做相当于给模型加了惯性,虽然会牺牲一点点最优性,但系统运行的平稳性大幅提升,设备也不能因为频繁调整而提前报废。
5.3 实用建议:从离线仿真到半实物接入的落地经验
最后说一点从纯仿真模型到实际微网控制系统对接的经验。不要高估模型在离线的表现,也不要低估接入实时数据后的问题量级。我在接入一套实验微网时,最先暴露的不是模型逻辑错误,而是数据质量问题——光伏采集器跳变、热负荷传感器延迟、通信丢包导致某些时刻数据缺失。如果不做数据预处理,滚动优化器会把异常值当成真实负荷,然后给出离谱的调度指令。
建议在滚动优化器前面加一个轻量级的数据校验模块,做三件事:检查数据是否有界,把超出物理极限的点标记为异常;检查数据变化率,超过设备爬坡能力的跳变直接剔除;对缺失值用上一时刻的值或最近窗平均值填补。这个小模块写起来不到200行代码,但能避免80%以上的调度指令异常。我在实际项目中就是把这段代码放在控制器和滚动优化器之间,效果立竿见影,优化器里的“幻觉数据”少了,指令曲线也平滑了。
还有一个细节容易忽略:实时层拿到的储能SOC值是从BMS上报的,它跟模型里推算出来的SOC往往有偏差。不要试图让模型里的SOC等于BMS的实时SOC,而是要用BMS数据对模型里的SOC做周期性校准,比如每15分钟做一次卡尔曼滤波修正。否则时间一长,模型里的SOC会漂移,最终导致储能被过度使用或实际容量利用率不足,这些问题都会直接传导到调度指令的可靠性上。
另外,我个人的习惯是把每次滚动优化的输入输出全部记录下来,存成统一的日志格式,包括预测数据、实际数据、优化结果、求解耗时、是否收敛、退化标志。这样如果某天系统表现异常,翻日志就能很快定位,是预测数据的问题还是模型数值问题,还是下层执行机构响应慢了,不用猜。调试双层模型这件事情,很多时候拼的不是建模能力,而是定位问题的手段。