
做多能源微网调度的人应该都有过这种体验光伏预测中午十二点明明写着“晴、满发”结果十一点半一片云飘过来电口和热口的计划全乱了。电负荷还好说热负荷一旦欠供管道温度掉下去想补回来至少要一两个小时。我去年把一个基于多时间尺度滚动优化的多能源微网双层调度模型从论文搬进了实际工程跑了大半年踩了不少坑今天把这些东西捋干净聊聊为什么用双层、为什么做多时间尺度滚动以及模型到底怎么搭、怎么调、怎么排查问题。这个方案适合正在做微网能量管理、园区综合能源调度的同学参考也适合刚入门、想理解分层优化和滚动优化到底是怎么回事的人。1. 项目背景这个模型到底在解决什么1.1 多能源微网调度的本质难点多能源微网不是“电、热、气各管各”拼在一起它的难点在耦合。典型配置里会有燃气轮机CHP同时出电和出热电锅炉把电转成热热泵从电/气里再转化一次电储能和热储能则是两个时间常数完全不同的缓冲器。这带来一个很尴尬的问题电系统的响应是秒级到分钟级热系统的响应是分钟级到小时级但它们的出力又被同一台CHP绑在一起。传统做法是“日前做一次计划当天照着跑”这在配电网侧单独调度时勉强能用因为电网络惯量小、调度员可以随时对着偏差做手动调整。可一旦加上热网、加上储能、加上气价电价联动的多个可调资源单靠一次日前计划根本扛不住预测误差。光伏预测偏差10%可能就是几十千瓦热负荷峰值预测偏差10%就直接影响用户体感温度而CHP一旦定下一个“电出力”热出力就不是自由的了。1.2 预测误差是绕不开的“真凶”做调度的都清楚无论用多先进的预测算法负荷和新能源出力一定有误差。误差分为几类日前级误差气象预报不确定光伏/风电预测误差可达20%以上负荷预测也有5%~10%的偏差。日内级误差提前几小时的预测明显更准但趋势仍在变比如云团移动会修正光伏曲线。实时级误差分钟级的短时波动比如一台大型设备启停造成的负荷跳变。如果只在一个尺度上做计划要么因为预测不准导致实际运行偏离计划要么为了保险被迫加大备用容量成本全部白白浪费。解决问题的思路不是“把预测做到完美”而是“把一个不准的计划重复更新成相对准的计划”——这就是多时间尺度滚动优化的逻辑起点。2. 整体思路拆解双层、多尺度、滚动三者缺一不可2.1 多时间尺度分工不同目标也不同我在这个项目里用的标准配置是三层时间尺度这也是工程上最常见的搭法时间尺度时间分辨率优化范围更新频率主要目标日前调度1小时24小时每天一次确定机组启停、储能计划、购电计划日内滚动调度15分钟未来4小时每1小时滚动一次修正预测偏差、调整储能出力实时调整1~5分钟未来15分钟每个控制周期平抑短时波动、跟踪日内计划这个分层逻辑和开车导航很像出门前看全程路线规划日前路上每段重新导航日内滚动遇到突然插队或红灯再临时减速避让实时调整。你不能因为红绿灯就不做路线规划也不能做完全程规划就闭眼开车三层配合才稳。2.2 为什么用双层而不是一个大模型很多初学者一上来就试图把24小时、每个15分钟、所有设备一次性建模成一个混合整数规划MILP这不是不行但工程上非常不划算。原因有三条第一模型规模爆炸。时间分辨率从1小时改成15分钟优化时段从24变成96二进制变量直接翻四倍如果再算上实时控制模型变成连续在线优化商用求解器也扛不住。第二决策性质不同。日前需要确定“启停状态”这类离散变量日内滚动则主要是在启停不变的前提下调整“出力大小”把这两类问题搅在一起求解效率和稳定性都会变差。第三信息更新不同步。日前用的预测数据和日内用的预测数据本来就不是一批强制在一个模型里共用一套约束反而让计划失真。所以我采用的双层结构是上层负责日前计划求解含启停的MILP输出各机组开停机状态和基准出力下层负责日内滚动调度在上层给出的启停状态固定的前提下用15分钟精度的数据重新优化未来4小时的出力分配和储能充放。下层模型规模小、二进制变量少求解速度快完全能跟得上小时级滚动更新的节奏。2.3 滚动优化只信眼前但每次重新抬头看路滚动优化本质就是工程控制里的模型预测控制MPC思想。它和一次性开环优化的区别就是你每一次都只用当前时刻到未来4小时的窗口做优化但只执行第一个时刻的决策等时间推进后再把窗口滑动一次用最新的预测、最新的状态量重算。这里有个特别重要的点为什么只执行第一步不把整个窗口都执行掉因为预测窗口后面的部分本身是“假”的基于不准确的预测做出的后续决策没有意义。滚动优化的精妙之处在于“反馈校正”——每一个优化周期开始前都要把当前真实状态储能的SOC、热罐温度、机组实际出力读进来作为初值再重新优化这样模型永远不会和现实脱节。2.4 双层模型里“上传下达”的衔接机制两层不是各算各的必须有明确的衔接信息。我在工程里做了三个传递通道上层把机组启停状态传给下层下层在滚动优化中把启停变量固定成已知参数上层把每个时段的储能SOC期望轨迹传给下层下层在优化时加入“尽量贴近日前SOC计划”的松弛目标上层把购电/购气计划作为参考值传给下层下层用“偏差惩罚”而不是“硬约束”来接纳它。这样下层的灵活性既被保留又不会跑出上层的骨架太远。特别是“偏差惩罚”这个设计我建议所有做双层调度的人都试试比硬约束好用得多。3. 核心模型构建从目标函数到约束条件的完整拼图3.1 上层模型日前计划层上层模型的目标函数我简写成这样min F 购电成本 购气成本 机组启停成本 运行维护成本 - 售电收益其中购电成本是分时电价下各时段购电功率求和购气成本是燃气轮机加上燃气锅炉的气耗乘气价启停成本是一个分段函数运维成本按各机组出力的比例系数估算。约束条件主要分几类第一功率平衡约束。电平衡是光伏、风电、CHP发电、储能放电、购电之和等于电负荷加储能充电加电锅炉用电加售电热平衡是CHP热出力、热锅炉、热储能放热之和等于热负荷加热储能充热。第二CHP的热电耦合约束。这是多能源微网最容易写错的地方。不能简单写成“电出力加一个常数就是热出力”要用可行域描述。常见做法是用一个凸多边形近似定功率下标定的热电可行域我记得我项目里用的是四个极点围出来的四边形约束形如 AP BH ≤ C每一台机组三条到四条线性约束。第三储能约束。电储能SOC递推式是SOC(t1) SOC(t) η_ch * P_ch - P_dis / η_dis还要限制SOC上下限、充放电功率上下限以及一个时间窗内的充放电次数或能量约束。热储能同理但热罐的损耗比电池快要考虑自然散热项我一般加一个小时的降温比例系数。第四机组爬坡约束和旋转备用约束。备用约束容易被忽略但它直接影响系统抗风险能力通常要求系统在任一时段至少有最大单机容量或预测负荷5%的向上备用。3.2 下层模型日内滚动调度层下层的目标函数变掉了不再是单一的运行成本最小而是“调整代价最小”加上“跟踪日前计划”的双目标。我用的形式是min F_low Σ 购电调整惩罚 Σ 购气调整惩罚 Σ 储能出力调整惩罚 Σ 与日前计划偏差的平方项为什么要用“偏差惩罚”而不是直接继续用运行成本因为到了日内阶段时间和市场价格都已经部分确定如果只追求最小成本很可能出现频繁改计划、机组出力来回跳的振荡现象。加了偏差惩罚之后下层就只在“该调的时候调、不该调的时候不调”工程上稳定得多。下层的约束比上层要简化但又更细化启停状态固定为上层结果不再优化二进制变量平衡约束、储能递推、爬坡约束都按15分钟分辨率重写新增一个“调整量一致性约束”实际出力 日前基准 调整量调整量有上下限防止一次调整过大。3.3 滚动窗口的实现逻辑下层模型求解时不是一次解96个时段而是解16个时段4小时×15分钟。每次求解完只有第一个15分钟的决策真正下发到执行机构然后整个窗口向前滑动1小时有时候也用15分钟滑动一次重新读取最新状态和最新预测再求解下一个周期。这里有一个很多人会忽略的细节状态量的初值不能只取“当前SOC”要把上一个周期模型算出来的理论SOC和SCADA系统里测到的实际SOC做一次融合。因为模型算出来的SOC是天衣无缝的但实际充放电效率做不到那么理想二者会越拉越大。我在代码里做了个简单校正把实测SOC作为硬初值并用上一小时的实际充放电量修正效率和损耗参数实测下来效果改善很明显。3.4 参数怎么定给出可直接用的参考值这个模型里有几个关键参数不同项目差别很大但可以先给一组我实际用过的基准值参数数值说明日前分辨率1小时常见做法也可以0.5小时日内窗口长度4小时太短看不到储能完整充放太长预测不准确日内分辨率15分钟兼顾精度和求解速度滚动更新周期1小时每整点重新优化轻量又及时日前偏差惩罚系数0.8~1.2倍边际成本用边际成本来标定惩罚力度储能SOC下限/上限10%/90%防止过放和精度死区备用率5%~8%负荷由最大单机故障容量决定这些值不是拍脑袋定的。窗口长度4小时是因为电储能典型充放周期是2小时左右4小时能覆盖一个完整充放过程再加一点余量同时又不会把整天的预测误差带进来。分辨率15分钟则是在求解速度和调节颗粒度之间的平衡再细到5分钟下层模型规模变大对预测精度要求也急剧提高收益并不明显。4. 实操过程从数据准备到代码组织4.1 整体流程我在项目里跑的完整流程是六步每天凌晨0点更新一次未来24小时的负荷、光伏、风电、电价、气价预测数据求解上层日前MILP得到机组启停计划、储能SOC计划、购电购气基准把上层结果写成一个计划文件传给日内层每个整点读取最新预测和实时SCADA状态构建未来4小时窗口的优化数据求解下层滚动优化模型取第一个15分钟决策下发等下一个整点回到第4步循环滚动执行。这个流程看起来简单但每一步都有隐藏的坑。比如第1步的数据更新很多项目是“只更新预测数据不更新基础参数”这没问题但如果你把某一天的节假日负荷特性忘记处理整天的优化都会跑偏。我建议每个项目都建一个数据校验脚本每天跑优化前自动检查数据是否越界、是否有缺失值、时间戳是否连续省掉大量排查时间。4.2 滚动窗口代码的骨架逻辑下层滚动优化的代码结构我习惯分成四块数据读取、状态更新、建模求解、结果落库。建模求解核心部分大致是这个思路# 滚动优化的主循环 for hour in range(24): # 1. 读取最新的预测数据和实时状态 forecast load_forecast(hour, next_4h) # 未来4小时负荷/光伏预测 state read_scada_state(hour) # 当前SOC、温度、实际出力 plan load_dayahead_plan(hour) # 日前计划基准 # 2. 建立下层优化模型 model build_intraday_model(forecast, state, plan, horizon16) # 3. 求解提取第一个指令序列 model.optimize() action extract_first_action(model) # 4. 下发指令并记录结果 dispatch(action) log_rolling_result(model, hour)这里我要提醒一个非常容易踩的坑load_forecast(hour, next_4h)这个窗口的索引必须和模型里的时段索引严格对齐。很多bug就出在“窗口滑动了、但预测数据还是从原来位置取的”导致模型看到的永远是第一天的数据。我后面专门写了一个shift_window()的工具函数每次滚动前强制校验窗口头尾时刻和系统当前时刻一致。4.3 求解器选型与参数设置这类优化问题最常用的就是Gurobi和CPLEX开源方案可以用SCIP或HiGHS。我用的是Gurobi原因很简单对大MILP的求解性能稳定而且提供了对偶单纯形法和自动的预求解优化。工程环境中求解器参数的设置比很多人想象的重要。我常用的几组参数是MIPGap设到0.01~0.02不需要追求完美最优解滚动优化本身就有反馈校正1%的次优完全可接受TimeLimit设60秒上层模型如果60秒解不出来把MIPGap放松到0.05再解一次启用Presolve默认开启即可对下层模型设置 ModelSense 为最小化并开启并发求解线程数。有一次我在调试时发现某一天的下层模型连续几个整点都解不出来检查半天才发现是当天的热负荷预测曲线里出现了一个尖峰导致热储能充放约束和CHP热出力上限冲突。解决方法是在模型中加了一个松弛变量允许一定程度的短时欠供但把欠供惩罚系数设得极高。从那之后模型再没因为极端数据卡死过。4.4 必要的伪代码双层衔接上下层之间的数据传递我建议做成一个专门的中间数据结构叫DayAheadPlan里面包含每个小时的机组启停状态、各机组出力基准、购电购气基准、SOC期望轨迹。日内层每次构建模型时把这个结构体传进去所有约束都基于它。这样代码清爽也不会因为改了上层模型导致下层到处报错。5. 常见问题与排查技巧实录5.1 模型不可行我做这个项目时遇到最多的问题就是不可行。表现出来就是某一天的下层模型求解器返回infeasible调度终端没有任何指令输出。排查思路按优先级来先查数据当天预测数据有没有越界、缺失、时序错乱再查状态量实时SOC是不是被手动设成了0.99超出模型允许上限然后查计划基准日前计划的启停状态和日内负荷是否严重不匹配最后查约束冲突CHP热电可行域和爬坡约束有没有同时锁住。解决不可行问题的工程手段是加松弛变量。给电平衡、热平衡、备用约束各加一个非负松弛变量并给它们设定极高的惩罚系数。这样模型在极端情况下宁可付出高昂惩罚也不会直接无解系统至少还能给出一个可执行的次优方案而不是瘫在那里。我在生产环境里一直保持这个做法代价是偶尔会出现一条轻微的功率缺额记录但比起整个调度系统停摆这完全值得。5.2 结果振荡和计划反复这是滚动优化的经典毛病因为每次滚动都看到了新的预测前后两个周期的决策可能出现明显的“跳变”。比如第1小时决定让CHP满发第2小时因为光伏预测上调又让CHP降到40%第3小时再升回来。机组频繁变负荷对设备寿命和气耗都不是好事。我的处理策略有三个层层递进第一增加偏差惩罚把日前计划当作锚点不让日内调整幅度过大第二给调整量加限幅约束单个周期出力调整不超过额定容量的20%第三在滚动周期之间加一个决策平滑项即本周期决策和上一周期已执行决策的差尽量小。这三招加完振荡问题基本消失。我还建议对每个滚动周期做一次结果记录把“当前预测下的最优决策”和“实际执行的决策”分开存下来复盘时一眼就能看出哪些决策是无效振荡。5.3 求解时间越来越长下层模型按理说规模不大但如果你把模型写得不当求解时间会莫名其妙恶化。最常见的原因是二进制变量或者整数变量意外出现在了下层模型里。我踩过一个坑把电储能的充放电状态用一个二进制变量表示防止“同时充放”结果这个变量被连带带到了下层下层变成一个小的MILP每个滚动周期多花十几秒高峰期压力很大。更好的做法是建立线性约束充放电功率之和不超过储能额定功率且两者不同时为正通过两个连续变量的线性组合来模拟互斥从而去掉二进制。如果实在要考虑充放电效率差异也可以接受一个小的MILP但必须明确把它控制在解算时间可以接受的范围内。5.4 边界条件和状态传递的隐蔽错误我遇到过一个非常隐蔽的bug上层模型的SOC计划是按0~23小时编号下层模型从小时1开始滚动初始SOC取的是计划第0小时的SOC结果因为数组索引差了一位整个上午的储能充放策略完全和计划脱节。这种问题用眼睛看几乎发现不了必须靠日志数据定位。因此我会在每个滚动周期里输出三个SOC日前计划的SOC、上一周期模型预测的本时刻SOC、SCADA实测SOC。三个值一旦持续出现系统性偏差就往索引、时间对齐、效率参数三个方向查。5.5 预测更新不及时实测中发现滚动优化的收益很大程度上取决于预测更新的质量。不少项目名义上做了日内滚动实际用的还是日前那一版预测数据那滚动优化就退化成“重复开环优化”收益寥寥。我给项目加了一个自动化检查每天自动对比新旧两版预测的差异度如果光伏或负荷预测的变动量级低于设定阈值就在日志里提示预测数据可能没有更新。这个提示帮我抓到过好几次数据链路断掉的问题。6. 实操总结与经验沉淀6.1 一次典型日内滚动案例回放说一个印象深刻的场景。某个工作日下午光伏预测在整点更新时突然从满发修正为60%出力同时电负荷因为气温上升比预测高了8%。如果沿用日前计划CHP要继续保持原来的高电出力电储能也只能按原计划充电系统会面临明显的电力缺口。日内滚动优化在这个时刻自动做了几件事把CHP电出力上调并且热出力跟着变化热负荷缺口由热锅炉补上暂停电储能充电甚至让它转为放电从电网增购了一部分电量。整个过程从预测更新到指令下发不到两分钟系统没发生任何负荷削减。这是我做这个项目最有成就感的一次也让我确信多时间尺度滚动优化的价值不是在“正常日子”里体现的而是在预测突变时体现的。6.2 几个值得长期坚持的小习惯根据个人实际排障经验有几个习惯建议从一开始就建立。一是每次运行都保留完整的输入输出日志特别是每个滚动周期的预测数据、决策结果、实测数据三元组这是日后回溯问题的唯一抓手。二是模型里每一个惩罚系数、限制系数都写清楚来源和标定过程不要用“调来调去恰好能跑”的系数否则换一套数据就可能崩。三是先跑纯连续变量版本确认经济性和逻辑都正确之后再加二进制变量和复杂约束一步到位写MILP只会让排查变得无比痛苦。四是对比实验一定要做拿同样的场景跑一次纯粹日前开环调度再跑一次双层滚动调度对比实际运行成本和预测偏差用数字说服自己这个架构值不值。这个项目做下来我最大的体会是双层加滚动不是炫技而是对“预测不准”这个现实的最朴素回应。调度的真功夫不在于把模型写得多复杂而在于知道什么时候该信任计划、什么时候该修正计划以及如何在两层之间留出合适的灵活性。后续我还在尝试把日内窗口长度改成自适应——预测波动大的时段窗口缩短、波动小时段窗口拉长方向是对的效果还在验证中。有机会再写一篇专门分享自适应窗口的调试过程。