ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

综合能源系统动态调度:时间尺度优化与MPC滚动控制

综合能源系统动态调度:时间尺度优化与MPC滚动控制

做综合能源系统优化调度的人,只要碰过“确定性日前计划加开环执行”这个套路,基本都摔过跟头。仿真模型里算得漂漂亮亮的调度曲线,一放到带预测误差的环境里,不到半天就开始跑偏,热负荷飘得离谱,储能SOC慢慢越限,机组在某个时段突然爬坡越限,整套计划直接作废。我自己最早搭电热联供调度模型的时候就被这个问题折磨过,后来才真正意识到:综合能源系统的动态调度,难点从来不在“目标函数多写几个惩罚项”,而在“怎么让调度决策跟着时变工况走,而不是一次算完就撒手不管”。

标题里真正值钱的,其实是两个词——“时间尺度优化”和“MPC滚动控制”。前者告诉你调度决策不能全部挤在一个时间粒度里做,后者告诉你控制器要不停“往前滚、反复算”,用反馈去修正预测误差。这篇就围绕这个主题展开,讲清楚动态调度为什么必须分时间尺度、MPC滚动调度模型的数学结构和代码复现思路,以及我在实际跑通这套流程时踩过的坑。适合正在复现综合能源调度代码的研究生或工程师,有一定优化建模基础,但想把“论文里的MPC”变成“能跑出曲线的程序”的那类读者。

1. 动态调度为什么必须分时间尺度:先看懂标题的后半句

1.1 静态调度解决不了的两个核心问题

很多人一上来就建一个24小时优化模型,目标函数是运行成本最小,约束是功率平衡、设备上下限、储能SOC动态。这种模型本身没错,但它本质上是一个“一次性的开环优化”:假设预测完全准确,然后一次算出全天计划,最后把计划曲线直接当作实际运行曲线。

问题在于,真实系统里预测永远不可能准确。光伏出力受云量影响,负荷曲线跟天气、人群活动强相关,热负荷更是随建筑热惯性滞后好几个小时。预测误差积累到一定程度,计划就会失去意义。我做过一个很简单的数值实验:假设光伏预测误差在±5%、热负荷误差在±10%之间波动,开环执行一天下来,电量平衡偏差和热量平衡偏差都能累积到几百兆瓦时量级,储能的充放电计划早就不匹配了。这还只是仿真里的“伪实测”,真实系统里还有设备故障、通信延迟和模型失配。

另一个更致命的问题是设备的时间常数差异巨大。电储能响应是秒级,燃气机组爬坡是分钟级,热网管道传输延迟可能达到几十分钟甚至几小时,天然气网还有管存效应,相当于气体这个“软性储能”在管网里来回波动。这些异质惯性放在同一个时间刻度里做优化,要么为了照顾慢设备把控制周期拉得很长,导致快设备失去调节能力;要么为了照顾快设备把时间粒度压得很短,导致慢设备的日运行计划算不出来。单一尺度根本无法兼顾。

1.2 分层调度的基本框架:三种时间粒度各干各的

时间尺度优化的核心思路,就是别让一个模型把所有事干完,而是把调度决策按时间粒度拆开,各层各管一段。

调度层级时间分辨率优化周期主要任务
日前调度1小时或15分钟24小时机组启停计划、储能日运行轨迹、购售电计划
日内滚动调度(MPC)15分钟或5分钟预测时域若干步跟踪日前计划,修正负荷与新能源预测偏差
实时控制1分钟或秒级单时刻设备层PID或AGC,执行日内指令

日前层算的是“明天大概怎么跑”,只做机组组合级的离散决策和储能基线轨迹;日内层才是动态调度的核心,用MPC每过一个控制周期就重新优化一次,把“预测不准”的影响摊薄到每一次滚动刷新里。实时层更像是一个底层执行机构,保证设备在指令附近稳定运行。三层配合起来,不同时间常数的设备各就各位。

1.3 MPC在这一套里到底扮演什么角色

MPC,也就是模型预测控制,在综合能源系统动态调度里承担的是日内滚动优化的角色。它的工作方式用大白话说就是三件事:先基于当前状态和预测信息算未来一段时间的优化轨迹,但只执行第一步;等下一个采样时刻到了,再用最新的实测状态和更新的预测数据重新算一遍,然后再执行第一步,如此循环滚动。

这个过程跟人开车很像。你从A地到B地,不会在出发时把方向盘每一个角度都定死,而是先规划一个大方向,开一段看一段,遇到前方路况变化就重新规划。MPC的“开一段看一段”就是滚动优化,“遇到路况变化”就是反馈校正。在综合能源系统里,MPC用这种机制把日前计划当作参考轨迹,实时盯着实测状态走,一旦光伏突然被云挡住或者热负荷突然升高,它在下一轮优化里就把偏差补偿掉,而不是干等一天结束再复盘。

为什么MPC适合综合能源系统而不是PID或者简单规则?因为它的预测模型不要求很精确,只要模型在有限时域内的变化趋势是对的,靠高频滚动和状态反馈就能维持不错的跟踪效果。这一点我从实际复现里体会非常深:模型误差不致命,不做反馈才致命。

2. MPC滚动控制调度模型怎么建:原理、目标函数与约束

2.1 从状态变量到控制变量:先分清模型里三类量

搭MPC模型前,先得把系统中的变量分类清楚,否则后面约束很容易写成一锅粥。

第一类是状态变量,基本就是储能类设备的当前状态量,比如蓄电池SOC、储热罐的蓄热量,如果有热网管道温度模型,管道出水温度也算状态。状态变量决定系统记忆,下一时刻的状态由当前状态和决策变量共同决定。

第二类是控制变量,也就是调度员能直接下的指令,包括各机组出力、储能充放电功率、电锅炉/燃气锅炉产热量、联络线购电功率等。MPC每步算出来的就是这些控制变量的序列。

第三类是扰动变量,包括光伏出力、风电出力、电负荷和热负荷的实测与预测值。扰动变量不是我们能控制的,但会直接影响优化结果,相当于模型里的“外生输入”。

这三类量区分清楚之后,MPC每步的求解流程就很明确了:拿到当前实测状态,结合扰动预测序列,在预测时域内求解一个有限时域优化问题,得到最优控制序列,取第一组下发执行。下一时刻用实测状态覆盖模型预测的状态,整个循环就闭合了。

2.2 目标函数不是单存一个成本项就行

目标函数的标准写法是下面这个形式,我要特别强调括号里每一项为什么存在:

min ∑_{k=0}^{P-1} [ 购电成本(k) + 燃料成本(k) + 启停惩罚(k) + 弃风弃光惩罚(k) + 日前偏差惩罚(k) ]

购电成本和燃料成本是系统运行经济性的主体,前者来自分时电价和联络线功率,后者来自燃气轮机或燃气锅炉的耗气量。光有这两个可不够,如果目标里不加启停惩罚,MPC为了让成本最小,会让机组在预测窗口内频繁启停,仿真出来的机组出力曲线像锯齿一样,现实中机组寿命直接报废。所以在目标函数里给启停动作加一个比较小的惩罚项,换来的是运行平稳。

弃风弃光惩罚也很关键。当新能源出力大于系统消纳能力时,如果允许弃电但没有惩罚,优化器一定会选择弃掉高价新能源,因为这样成本最低。真实的调度要求是“尽量消纳”,所以弃风弃光要作为高成本动作出现,给它一个大惩罚系数。

最后这个日前偏差惩罚是衔接时间尺度优化的关键点。长时间尺度的日前优化算出了储能SOC参考轨迹和购电计划,日内MPC如果不看这个参考,完全按自己短视窗口来,很容易出现“储能每步都为了局部最优把SOC往低处放”的漂移问题。我在模型里给MPC的控制轨迹加一个对日前计划的软跟踪项,系数不能太大,否则预测一偏差系统就僵住;也不能太小,否则参考轨迹形同虚设。我常用的范围是跟踪项权重取成本项权重的1%到5%,这个量级需要配合具体算例调。

2.3 核心约束怎么搭:储能、平衡、爬坡一个都不能少

约束部分我按出现频率和重要性依次说。

能量平衡约束是所有调度模型的骨架。电平衡即各电源出力加联络线购电,等于电负荷加储电充电加电锅炉耗电,热平衡则要求CHP产热、燃气锅炉产热、储热放热之和等于热负荷加储热充热。这个约束的物理含义很清楚,但写代码时注意单位统一,我见过太多人把kW和MW混着用导致结果莫名其妙。

储能约束是MPC模型里最有“动态味道”的部分。蓄电池的SOC递推方程如下:

SOC(k+1) = SOC(k) + (η_ch · P_ch(k) - P_dis(k) / η_dis) · Δt / E_cap

充电效率一般取0.9到0.95,放电效率类似,E_cap是储能容量,Δt是单步时长。除了SOC递推,还需要限制SOC上下限,一般设在10%到90%,防止过充过放;充放电功率也各有上限。最关键的是充放电互斥约束:同一个时刻不能既充电又放电。这个约束要用一个0-1变量z来实现,P_ch ≤ z · P_ch_max,P_dis ≤ (1-z) · P_dis_max。

爬坡约束针对的是机组出力调整速度,比如一台100MW的燃气轮机爬坡速率是5MW/min,15分钟时间分辨率下每步爬坡上限就是75MW。写成约束就是:

P_unit(k+1) - P_unit(k) ≤ ramp_up · Δt

P_unit(k) - P_unit(k+1) ≤ ramp_down · Δt

如果MPC的日内时间分辨率是5分钟,同样5MW/min爬坡率,每步上限就只有25MW。这也是为什么时间尺度分层必须考虑设备爬坡特性——时间粒度太细,机组爬坡约束会严重限制调节能力。

联络线功率约束我每次都会刻意加上,包括购电上限、售电上限,有些地方还要考虑逆功率限制。这个约束在纯仿真的代码里经常被忽略,但实际系统中联络线容量是硬边界,MPC预测轨迹一旦越界下发到现场根本执行不了。

3. 代码复现:从数据准备到滚动闭环仿真落地

3.1 整体工程结构与数据准备

复现这套系统,我建议把工程分成几个独立模块,而不是把所有代码堆在一个脚本里。我常用的目录结构是这样的:

ies_mpc/ ├── data/ │ ├── load_heat.csv │ ├── pv_wind.csv │ └── tariff.csv ├── model/ │ ├── device_params.py │ ├── constraints.py │ └── objective.py ├── solver/ │ └── optimize_step.py ├── simulation/ │ └── plant_simulator.py └── run_main.py

data目录放负荷、新能源、电价等时序数据。公开的综合能源系统数据集比较难找,复现阶段完全可以用程序生成合成数据,关键是保留典型日曲线的形状特征。比如电负荷用“早晚高峰、午间低谷”的正弦叠加随机噪声生成,热负荷用“全天基本平稳、夜间略高”的趋势生成,光伏用正态形曲线乘云量随机系数生成。先把调度算法跑通,再替换成真实数据,这个顺序能省掉大量查数据格式的时间。

device_params.py里放设备参数,我建议做成一个字典,清晰且容易改。比如:

device_params = { "chp": {"p_min": 20, "p_max": 100, "ramp": 75, "heat_ratio": 1.2, "var_cost": 0.35}, "eb": {"p_min": 0, "p_max": 40, "efficiency": 0.98, "var_cost": 0.1}, "battery": {"e_cap": 60, "soc_min": 0.1, "soc_max": 0.9, "p_ch_max": 15, "p_dis_max": 15, "eta_ch": 0.95, "eta_dis": 0.95}, "grid": {"p_import_max": 50, "p_export_max": 50} }

这些参数是我在测试算例里常用的典型值,实际复现时按自己的设备规格改就行。模型层把目标函数和约束写成独立模块,方便做对比实验。

3.2 建模代码关键片段:以Python和Pyomo为例

我习惯用Python配合Pyomo建模,因为代码跟数学表达式的对应关系非常直观,而且求解器可以灵活替换:小算例用开源求解器跑,大算例切换到商业MILP求解器。MPC每步的优化模型可以这样声明:

m = ConcreteModel() m.k = Set(initialize=range(horizon), doc="预测时域步数") # 状态变量 m.soc = Var(m.k, bounds=(soc_min, soc_max), initialize=soc_init) # 控制变量 m.p_chp = Var(m.k, bounds=(chp_p_min, chp_p_max)) m.p_ch = Var(m.k, bounds=(0, p_ch_max)) m.p_dis = Var(m.k, bounds=(0, p_dis_max)) # 充放电互斥0-1变量 m.z_ch = Var(m.k, within=Binary) m.z_dis = Var(m.k, within=Binary) def rule_soc_dynamic(m, k): if k < horizon - 1: return m.soc[k+1] == m.soc[k] + (eta_ch * m.p_ch[k] - m.p_dis[k] / eta_dis) * dt / e_cap return Constraint.Skip m.soc_dynamic = Constraint(m.k, rule=rule_soc_dynamic)

SOC递推约束写起来很简单,但有一个细节:预测时域末端的SOC没有约束,这一步要为后面的漂移问题埋下伏笔,所以我会在目标函数里加末端惩罚。

电平衡约束写法类似:

def rule_electric_balance(m, k): return (pv[k] + wind[k] + m.p_chp[k] + m.p_dis[k] + p_grid_import[k] == load[k] + m.p_ch[k] + p_eb[k]) m.electric_balance = Constraint(m.k, rule=rule_electric_balance)

注意这里p_grid_import是控制变量还是给定值,取决于你日前计划是否把联络线功率固定。如果日内MPC允许调整购电,那联络线功率也要设为变量并加上下限约束。

模型写完后,求解就在optimize_step.py里封装成一个函数:输入当前时刻的状态和预测序列,构建模型,调用求解器,提取首步控制指令返回。这个函数是整个滚动闭环的核心接口。

3.3 滚动闭环到底怎么搭:每步只取首指令

闭环仿真主循环的逻辑非常简单,但很多人写的时候会忘掉“用实测状态更新模型初值”这一步。参考框架如下:

for t in range(T - 1): # 1. 获取最新预测序列(实际工程中来自预测模块,复现时用带噪声的曲线模拟) pv_pred = generate_forecast(pv_true, t, horizon, noise=0.05) load_pred = generate_forecast(load_true, t, horizon, noise=0.03) # 2. 构建并求解MPC优化问题,返回首步控制指令 u = solve_mpc_step(t, soc_current, pv_pred, load_pred, params) # 3. 执行首步指令 actual_state = plant_simulator.step(t, u) # 4. 更新实测状态(重点是这里,下一轮优化必须用实测值,而不是用上一轮预测值) soc_current = actual_state["soc"]

第4步是MPC能兜住预测误差的根本原因。如果你因为省事直接在每一步接着用模型预测的SOC往下算,那滚动闭环跟开环优化没有本质区别,预测误差照样累积。复现时有一个很有效的检验方法:给光伏预测人为加一个很大的阶跃误差,比如第30步真实出力突然比预测低20%,如果闭环代码正确,储能和联络线会在接下来几步内自动补上缺口;如果SOC还是按旧预测一路走,那肯定是状态更新环节写错了。

4. 复现过程中的典型坑:求解、惯性约束与参数调优

4.1 求解时长失控:MILP没你想的那么快

MPC的日内滚动要跑很多个时刻,如果以15分钟为周期仿真30天,就是2880次优化求解。如果模型里机组启停0-1变量多、预测时域长,每次求解要几十秒甚至几分钟,整个仿真就跑不动了。我最早把预测时域设成96步(对应24小时15分钟粒度),结果求解器直接在SOC约束上被耗尽耐心。

解决思路有三个,按优先级排列。

先把预测时域缩短到能覆盖慢动态的范围,比如24步或36步。对综合能源系统来说,只要覆盖热网惯性的时间常数,就足够MPC做出合理决策,没必要一次看满24小时。

再设置求解器参数,比如MIP Gap。商业求解器和开源求解器都支持设置gap,典型值取0.5%或1%。意思就是允许优化结果跟最优解差1%以内,换取几十倍的速度提升。对于调度问题,1%的成本误差完全可接受,而求解速度的提升是实打实的。

最后用热启动,把上一步的最优解作为下一步的初始解传给求解器。因为前后两步的模型结构几乎一样,解空间也接近,热启动能让求解器少走大量分支。

4.2 热网和燃气网的惯性必须建模,要么时滞要么长时域

很多复现代码把热负荷当普通电负荷一样处理,算出来热出力曲线超频震荡。原因很简单:热网管道有传输延迟,电厂产热量送到用户端要好几分钟甚至几十分钟,而MPC只往前看了四五步,看到近端热负荷下降就立刻减出力,过一会儿热负荷回升又被迫加出力,来回横跳。

处理方式有两种。第一种是把热网简化成时滞环节:热出力在满足负荷之前先经过一个延迟时间τ,τ由管长和流速决定。写成约束就是管网出口供热功率等于产热功率延迟τ个采样周期,模型仍然是线性的,很容易求。第二种更省事,直接扩大预测时域,让它覆盖热惯性的时间尺度。我实际对比过,预测时域从6小时拉到12小时后,热出力曲线立刻平滑很多。

天然气网类似,管存效应相当于一个天然的气体缓冲储能。如果模型篇幅允许,可以在气网节点上设置一个虚拟储气罐变量,容量和注入/流出边界按管道管存参数来标定。这比建模复杂的偏微分方程实用得多,调度层面的精度完全够用。

4.3 储能SOC漂移:开环没问题,一滚动就出问题

这是我在复现中印象最深的一个坑。开环24小时优化,SOC曲线在0.1到0.9之间正常循环,看起来完全没问题。但装进MPC滚动框架后跑30天,SOC曲线慢慢往下沉,最终卡在0.1下限附近再也没上来。

原因在于每一步MPC都只优化有限时域,如果未来预测窗口中负荷和电价没有给足充电激励,优化器就会倾向于“先用掉储能”,因为电池不放电就产生不了收益。日复一日,SOC就被一点点放干。

解决办法是在目标函数里加上对SOC参考轨迹的跟踪项。参考轨迹可以由日前调度层提供,如果没有日前层,也可以用固定策略,比如“每天某个时刻SOC尽量回到0.5”。跟踪项权重取成本项的1%到2%,既能把SOC稳住,又不至于牺牲太多经济性。还有一个小技巧是给预测时域末端的SOC加一个软约束或罚项,让优化器明白“不能把这批电用到最后不管”。

4.4 日前计划与MPC冲突:软跟踪比硬约束好使

MILP模型是完全确定性的,如果你把“MPC必须严格跟踪日前计划”写成硬约束,比如要求储能SOC跟踪误差为零,那预测一偏差就会出现无可行解。这个在调试期特别折磨人。

最实用的方案是把计划跟踪从硬约束改成软约束,具体做法是引入松弛变量,让跟踪误差可以存在但要付出代价。松弛变量的惩罚系数就是前面说的日前偏差惩罚项权重。如果发现系统为了跟踪计划而放弃了对真实预测误差的响应,就把权重调小;如果发现MPC完全无视日前计划的储能基线,就把权重调大。我自己的习惯是先按计划跟踪松紧做一组敏感性分析,画出“成本对跟踪权重的曲线”,取曲线拐点附近的值。

5. 常见问题速查表与模型自检三板斧

5.1 复现中的高频问题整理

把我在调试过程中遇到最多的问题整理成了一个表,方便对照排查。

现象可能原因解决方法
模型求解时间超长整数变量过多、预测时域过长缩短预测时域,设置MIP Gap,启用热启动
储能SOC曲线持续下沉目标函数缺少SOC参考或末端惩罚加SOC跟踪项,预测时域末端加罚项
热出力曲线高频震荡预测时域覆盖不了热网惯性预测时域拉到6小时以上,或对热出力加热变化率约束
两步之间的MPC指令大跳变预测更新导致最优解突变对控制增量加惩罚项,或增加最小调整时间约束
某一步突然无可行解硬性计划跟踪约束与预测偏差冲突改成带松弛变量的软约束,加入弃电切负荷的可选动作
开环结果正常但闭环崩溃反馈状态没有用实测值更新检查主循环中状态更新逻辑,确保每步用实际SOC而非模型预测SOC
新能源出力高的时段被大量弃用弃风弃光惩罚系数太低调大弃电惩罚,或加入新能源消纳率硬约束

这个问题列表每一条都是我实际碰过的,不是从理论文档里抄的。尤其是“开环正常闭环崩溃”那个,排查的时候把代码里状态变量打印出来,一眼就能发现数值是否接上。

5.2 自检三板斧:先复现基础再上复杂度

复现代码写完别急着跑完整场景,先用三步法验证正确性。

第一步是单时刻可行性测试。取某一个采样时刻,把预测数据固定,单独求解一次MPC问题,检查目标函数值合理、所有约束满足、没有发现变量不初始化之类的低级错误。这一步能过滤掉90%的建模错误。

第二步是理想预测开环测试。假设预测完全准确,让MPC按时序跑完整天,但不引入任何反馈修正和预测误差。如果结果跟日前调度结果趋势一致,说明模型本身没问题。

第三步才是完整滚动闭环测试。把预测误差加进去,让MPC用实测状态滚动更新,比较闭环结果和理想预测开环结果的偏差。正常情况下闭环结果成本略高、但曲线更贴近实际运行边界。如果反而更差,就要回去检查反馈更新和扰动建模环节。

我见过很多人跳过了前两步直接上闭环,结果遇到问题根本分不清是模型错误还是滚动逻辑错误,排查成本翻好几倍。宁可在小规模算例上多花一小时,也别在完整算例上熬一整夜。

6. 做完整套复现后的一些体会

这套代码从零搭起来,我最大的感受是:综合能源系统动态调度的复杂度不在于单个MPC模型多难写,而在于把所有模块串成一个闭环之后,任何一个环节的“小毛病”都会被放大成整条曲线的异常。我以前总以为最优解算得越精确越好,现在反而更看重求解稳定性、滚动收敛性和对预测误差的鲁棒性,成本和最优解的微小偏差根本无关紧要,计划能平稳执行、状态不越限才是底线。

几个参数我反复调了很多轮。预测时域取36步左右通常比96步更实用,因为计算速度和曲线平滑性综合最好;SOC跟踪权重调了大概五轮才找到既不放空也不僵硬的区间;弃电惩罚直接设成购电成本的十倍,才让优化器在绝大多数情况下选择其他调节手段而不是一弃了之。这些数值在不同算例里肯定要重新标定,但调整思路是通用的。

如果你也要复现这个方向,我最后一条建议是:先在一个最简单的电热联供系统上跑通,只留一台CHP、一台燃气锅炉、一组蓄电池和一个储热罐,参数全部用理想值,别一上来就上多能耦合加管网时滞。等闭环跑顺了,再逐步加光伏、风电、气网和热网延迟。这样每一步出问题你都知道是刚加的模块引起的,而不是在二十个约束里大海捞针。另外一个小技巧是每个MPC步的决策结果都落一份CSV,跑完直接把“计划轨迹vs实际执行轨迹”叠图画出来。曲线里任何异常都能直观暴露出来,比盯日志高效得多。复现模型的价值不在于“把论文公式抄下来”,而在于跑出正确的动态响应之后,你对综合能源系统的运行机理会有一种完全不同的直觉。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进