ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

综合能源优化调度与需求响应:MPC滚动调度的原理与工程实践

综合能源优化调度与需求响应:MPC滚动调度的原理与工程实践 简介面向建筑蓄热与需求响应优化的综合能源优化调度Matlab仿真程序包采用模型预测控制MPC与随机最优控制方法实现。案例背景是纽约市一栋带有被动和主动蓄热的办公楼结合三级需求费及系统运营商日前调度的每小时能源价格建立锥形需求响应程序并考虑人员热舒适性。压缩包共11个文件以9个m脚本为核心另含1个mat数据文件和1篇PDF论文。m脚本覆盖主程序、参数设置、MPC求解与结果绘图等完整环节PDF为参考文档《Model predictive control of thermal storage for demand response》便于对照理解理论。包体仅1.59MB结构紧凑运行优化例程需CVX工具箱。目前已有1727人浏览学习下载即可直接运行并得到结果图适合已具备优化控制基础的工程师与研究者快速复现MPC蓄热调度案例也可作为进一步研究需求响应策略的起点。1. 综合能源优化调度与需求响应MPC为什么比单日开环调度更值得做一个典型园区综合能源系统里光伏、储能、电锅炉、冷热负荷和电网关口挤在同一个功率平衡表上。传统做法是拿明天的负荷与发电预测解一次优化得到一整天的调度计划表然后照表执行。问题在于预测总会偏云飘过来光伏就少了气温突变空调负荷就涨了。模型预测控制MPC的做法是每隔15分钟把未来一段时间的预测重新拉进来算一次只执行第一步再用实测状态把模型拉回现实。需求响应加进来后事情更复杂了电价不再只是成本而是可以搬移储能充放时段的信号可平移负荷从“固定值”变成“可调资源”。综合能源优化调度和需求响应叠在一起本质是一个带约束、带预测误差修正、带多能耦合的多时段决策问题MPC正好吃这一套。但这个方向最容易被低估的恰恰是“代码清晰”四个字。MPC的代码里混着设备模型、优化目标、滚动循环和预测接口耦合在一起时参数随便动一下整个调度计划就翻车。这篇笔记按“建模—需求响应目标—代码分层—回测验证”的顺序把一套可以直接跑的滚动调度器拆开讲。适合想把现有综合能源调度方案升级成MPC的工程师也适合准备毕设或竞赛的算法侧同学照着复现。2. 用模型预测控制MPC做综合能源优化调度状态空间建模与H怎么定2.1 为什么综合能源优化调度适合用MPC而不是单日优化综合能源优化调度天生是个带约束的最优化问题电、热、气多条能量流互相耦合储能设备让时间维度上的决策连在一起。单日开环优化的思路是把整个调度周期当成一个静态问题算完就固化。这在预测完美的理想世界没问题但真实光照、气温、负荷都有波动误差会随着时间累积到下午再回头看你上午算的电池充放计划很可能已经完全跑偏。MPC属于模型预测控制里“滚动优化反馈校正”那一族。它把“未来一段时间的决策”反复求解但每次都只执行第一个动作。等下一个采样时刻到了用实测的SOC、温度、关口功率刷新状态再重新求解。这样一来预测误差不会一直积累而是每个周期被修正一次。综合能源系统里设备多、约束多MPC的约束建模能力在这里特别合适功率平衡、SOC边界、电转热设备的爬坡限制、需求响应削减窗口全都能写进同一个优化问题。我实际做这类项目时的体会是单日开环适合“预测很准、系统很小”的场景。一旦设备超过三个、或者需求响应考核按15分钟断面结算开环就吃不住了。MPC的价值不是算得“更优”而是算得“更跟得上现实”。这也是为什么在综合能源优化调度相关方案里MPC几乎成了标准配置。2.2 设备级状态空间模型怎么写电池、电锅炉与状态递推MPC需要一个能预测未来状态的数学模型。综合能源系统里最典型的设备是储能电池它的状态量是SOC荷电状态控制量是电池功率递推关系很简单# battery_model.py —— 储能电池的状态递推 class BatteryModel: def __init__(self, capacity_kwh100.0, eta_ch0.95, eta_dis0.95, p_max_kw50.0, soc_min0.2, soc_max0.9, dt_h0.25): self.capacity capacity_kwh self.eta_ch eta_ch # 充电效率0~1 self.eta_dis eta_dis # 放电效率0~1 self.p_max p_max_kw # 最大充/放电功率 self.soc_min soc_min self.soc_max soc_max self.dt_h dt_h # 采样周期单位小时 def soc_next(self, soc, p_bat): # p_bat 0 表示充电p_bat 0 表示放电 if p_bat 0: return soc self.eta_ch * p_bat * self.dt_h / self.capacity else: return soc p_bat / self.eta_dis * self.dt_h / self.capacity这段代码的逻辑说明很简单充电时实际进入电池的能量要乘充电效率放电时电池付出的能量要除放电效率。参数里面最容易改错的是dt_h——如果采样周期是15分钟这里必须是0.25不是15。容量和功率单位统一成kWh和kWSOC才是无量纲的0~1。很多新手上来把单位搞混结果SOC递推直接发散。电锅炉、换热站这类电转热设备可以简化成一个带损耗的一阶惯性环节。实际工程里不会把热网水力的偏微分方程写进MPC常见做法是把蓄热槽温度归一化当作一个类似SOC的状态# thermal_model.py —— 蓄热槽简化模型 class ThermalStorage: def __init__(self, capacity_kwh200.0, loss_coeff0.05, dt_h0.25): self.capacity capacity_kwh self.loss_coeff loss_coeff # 每个采样周期的热损比例 self.dt_h dt_h def temp_next(self, temp, p_heat): # temp 是归一化温度p_heat 是注入热功率单位 kW return (1 - self.loss_coeff) * temp p_heat * self.dt_h / self.capacity这类线性模型的好处是多个设备拼起来后整个系统仍然是一个线性状态空间优化问题保持凸性求解器跑得快、解稳定。真实系统里热损和温度的关系其实带非线性但在工作点附近线性化加一个损耗系数工程精度已经够用。回头看SOC递推和温度递推结构一致这给后面写统一约束铺了路。2.3 预测时域 H、控制时域与采样周期怎么配MPC的预测时域H是最重要的一个参数。它代表“每次优化往后看多远”。采样周期定了H就决定了优化问题的变量规模。我做过的项目一般采样周期固定在15分钟这和电表冻结周期、需求响应考核断面是一致的。预测时域 H对应时长优点典型问题9624小时完整覆盖全天电价曲线储能策略全局性好变量多含整数时MIP求解吃力4812小时覆盖晚高峰和夜间的谷电时段凌晨极端低谷价可能看不见246小时求解最快适合初期调通流程短视遇到电价跳变容易误判我一般建议初版用H48也就是往前看12小时。对工商业园区来说12小时能覆盖一个完整的晚高峰和部分夜间低谷储能有机会把电从高价时段搬到低价时段。如果预测时域压缩到6小时电池很容易在傍晚把电放光后半夜的谷价又没法用——转头看开环调度反而更稳这就是预测时域太短造成的“MPC不如规则”假象。控制时域M可以就取1也就是每次只做第一步动作让反馈修正发挥最大作用。把M拉长到3步以上滚动优化的计算量成倍增加但对综合能源这种慢过程收益很小。采样周期不建议小于5分钟除非系统里有AGC级别的快速需求响应否则MPC求解本身就要消耗时间白白浪费算力。2.4 功率平衡与设备约束的物理清单在写cvxpy之前先把物理约束列清楚。我这几年踩下来综合能源调度最容易漏掉的是下面几条电网关口功率上下限。变压器容量是硬约束超额就跳闸。电池同时充放。很多初版代码会写出“既充电又放电”的荒唐解需要在功率定义上做手脚或者用pos和neg拆分。SOC终值。回测时如果不约束SOC回到初始值MPC会把电池在最后一段放空来“省钱”比较就失真。电锅炉爬坡。热设备不能像电池那样瞬间满功率需要限制每步功率变化量。这些约束不需要提前写成一个函数但应该统一收集成一个constraints列表。下一章在cvxpy里逐个落成代码。3. 把需求响应写进MPC目标函数三类信号、可平移负荷与求解器选型3.1 需求响应在MPC里的三类接入方式需求响应不是一个单一的“削峰填谷”实际工程里常见三种信号价格型、激励型、直接负荷控制。价格型需求响应最简单分时电价、尖峰电价本身就是DR信号MPC的优化器会自己把储能充放和负荷平移搬到低价时段。这类DR不需要额外约束只要目标函数里用电价做系数。激励型需求响应是电网或聚合商发布削减事件约定你在某个时段削减多少负荷、每千瓦时给多少补偿。MPC里要做的是把“削减量基线-实际负荷”写进目标和约束。直接负荷控制是更硬的手段比如空调集群在晚高峰被直接限制功率MPC里通常写成一段时间内的功率上限约束有时带整数变量表达“被控时段”。三种方式不是互斥的。一套完整的综合能源优化调度系统往往价格型做基础优化激励型处理临时DR事件直接控制用于保安全。差别只在于约束和目标的写法。3.2 目标函数从“电费最低”改成“净成本最低”常规MPC的最小化目标是购电成本。加入需求响应后目标变成“购电成本-需求响应收益”也就是净成本最低。用cvxpy可以这样写# optimizer.py —— 需求响应目标函数 import cvxpy as cp H 48 # 预测时域 48 步步长15分钟共12小时 P_grid cp.Variable(H) # 电网购电功率 P_bat cp.Variable(H) # 电池功率正为充电 P_load_var cp.Variable(H, nonnegTrue) # 实际用电负荷可削减后 SOC cp.Variable(H 1) # SOC 状态轨迹 # 预测数据来自 data 层 price forecast[price][:H] # 分时电价单位 元/kWh baseline forecast[base_load][:H] # 需求响应基线负荷单位 kW dr_incentive forecast[dr_incentive][:H] # DR 补偿单价单位 元/kWh curtail baseline - P_load_var # 削减量 基线 - 实际用电 objective cp.Minimize( cp.sum(cp.multiply(price, P_grid)) - cp.sum(cp.multiply(dr_incentive, curtail)) )这段代码的逻辑说明第一项是买电的成本第二项是削减负荷拿到的补偿。“削减越多、补偿越多”会让优化器主动压低负荷但注意curtail不能无限放大因为它被P_load_var 0和负荷的实际可调范围限制住。参数说明里比较关键的是dr_incentive一定要和price同维度、同时段否则目标函数两项的单位量纲对不上权重就变成了玄学。功率平衡约束这样写constraints [] # 功率平衡电网购电 电池功率 实际用电负荷 constraints [P_grid P_bat P_load_var] # 电池功率上下限 constraints [P_bat 50.0, P_bat -50.0] # SOC 递推与边界 dt_h 0.25 capacity_kwh 100.0 eta_ch, eta_dis 0.95, 0.95 for k in range(H): constraints [ SOC[k 1] SOC[k] ( eta_ch * cp.pos(P_bat[k]) cp.neg(P_bat[k]) / eta_dis ) * dt_h / capacity_kwh ] constraints [SOC[k] 0.9, SOC[k] 0.2]这里用cp.pos和cp.neg把电池充放拆开避免了“同时充放”的物理荒谬。代价是目标函数在P_bat0处不可导但对凸优化求解器来说不是问题。3.3 可平移负荷从固定负荷变成决策变量可平移负荷比如工厂的清洗机、园区的充电桩它的特点是可以在一天内选择启动时间但一旦启动就要连续运行若干时段。想在MPC里建模就需要整数变量# 可平移负荷建模充电桩必须运行3个时段且只能在18:00之后启动 x_start cp.Variable(H, booleanTrue) # 启动标志 P_shift cp.Variable(H, nonnegTrue) # 平移负荷功率 DURATION 3 P_SHIFT_MAX 10.0 # kW START_MIN 24 # 第24步18:00从0点起算 START_MAX 40 # 第40步22:00 constraints [cp.sum(x_start) 1] # 一天只启动一次 constraints [cp.sum(x_start[:START_MIN]) 0] # 18点前不能启动 constraints [cp.sum(x_start[START_MIN:START_MAX]) 1] # 启动窗口限制 # 运行功率与启动标志联动 for k in range(H): constraints [P_shift[k] P_SHIFT_MAX * cp.sum(x_start[k - DURATION 1: k 1])]这里的逻辑说明用x_start标记启动点运行功率的上限用“过去DURATION个采样点内有没有启动”来约束保证负荷一旦启动就持续供电。这种写法是工程里常见的简化不严谨的地方在于它允许负荷在启动后的任意时点降到0但对大多数调度场景够用。参数说明START_MIN和START_MAX是按步数算的不是小时。步数小时/采样周期改采样周期时这块最容易忘。整数变量一进来问题就从QP变成MIP求解时间暴涨这也是第4章要单独讲避坑的原因。3.4 求解器选型OSQP、CLARABEL和GLPK_MI怎么切换cvxpy本身不求解它是个建模层底层求解器要自己指定。我的习惯是# 判断有没有整数变量 has_integer any( v.attributes.get(boolean) or v.attributes.get(integer) for v in prob.variables() ) if has_integer: prob.solve(solvercp.GLPK_MI, time_limit30) else: prob.solve(solvercp.CLARABEL)逻辑说明纯连续问题用CLARABEL它是cvxpy新版默认的求解器之一对中等规模的LP/QP比旧版OSQP更稳定。含整数变量的问题用GLPK_MI它支持MIP但速度一般所以给了30秒的求解上限。参数说明里要记住time_limit单位是秒不是毫秒超时后prob.status会变成user_limit或者solver_error不能当作正常解用。提示如果MIP求解器持续超时先做两件事——把H缩短一半或者把可平移负荷的启动窗口从全时域改成固定连续区间。检索窗口直接决定整数变量搜索空间这是比换求解器更有效的优化手段。4. 让MPC代码保持清晰分层结构、冷启动状态与五个踩坑排查记录4.1 代码四层结构数据、模型、优化器、主循环MPC代码最容易坏的地方不是数学而是状态在模型、优化器、主循环之间传递时“暗度陈仓”。我现在的做法是把代码拆成四层mpc_scheduler/ data/forecast.py # 预测与电价接口 models/battery.py # 设备模型 models/thermal.py # 热力设备模型 opt/optimizer.py # cvxpy 问题构建与求解 runner.py # 滚动主循环每一层的职责只有一条data层负责拿预测数据并做对齐models层提供状态递推函数optimizer层把模型和约束翻译成cvxpy问题runner层负责推进时间、调用测量值、执行第一步控制。换设备只改models换需求响应机制只改opt预测源换了只改data主循环基本不动。这就是“代码清晰”的落地含义。主循环的骨架长这样# runner.py —— 滚动优化主循环 class MpcRunner: def __init__(self, model_dict, solverCLARABEL): self.model_dict model_dict self.solver solver self.history [] def step(self, t, state_now, forecast): plan build_and_solve(self.model_dict, state_now, forecast, self.solver) if plan is None: plan fallback_rule(state_now, forecast) self.history.append((t, plan)) return plan[0] # 只执行第一步 def run(self, scenarios, steps): for t in range(steps): state_now measure_state(t, scenarios) forecast get_forecast(t, self.horizon) action self.step(t, state_now, forecast) scenarios.apply_action(t, action)这段代码的逻辑说清楚step是核心接收当前时刻、实测状态和预测数据返回第一步动作。plan is None就是第4.6条坑的兜底逻辑——求解失败时不能什么都不干要用规则策略接管。参数说明里最重要的设计是t是唯一的时间索引所有数据和状态都通过它取值杜绝“预测接口用了昨天的数据”这种低级错误。4.2 坑1第一轮解不可行问题几乎都出在状态初值现象MPC第一轮求解就报infeasible后面的轮次正常。原因第一轮的状态初值不是来自实测而是上一轮计划值的末尾。比如SOC初值设成了0.9但实际电池只有0.3于是从0.9往下推的SOC轨迹和功率上限冲突约束无解。解决每个采样时刻的SOC、蓄热槽温度、关口功率必须从量测或状态估计器读不能从上一轮plan里取。如果传感器缺失一个点宁可上一拍值加噪声也不要直接复制计划值。我把这条规则写死在runner里state_now只允许来自两个来源要么实测要么状态估计。4.3 坑2整数变量拖慢求解从0.2秒涨到20秒现象加入可平移负荷之后单轮求解时间从0.2秒变成20秒以上滚动周期根本跑不完。原因整数变量数量和预测时域H成正比。H96时一个可平移负荷就是96个布尔变量MIP求解器的搜索空间指数级增长。解决先砍启动窗口把x_start限制在连续的两个小时区间内布尔变量从96个降到8个。再不行把H从96降到48。还有一招是把可平移负荷离线枚举成几条固定曲线MPC只做“选哪条”不做“何时启动”。这三个手段我按顺序用基本能把求解时间打回1秒以内。4.4 坑3需求响应补偿算错问题出在基线口径现象回测里MPC方案需求响应收益很好看月底和聚合商对账时却对不上收益少了20%。原因模型里把“无DR计划”当成基线但结算时用的是聚合商核定的基线——通常是日前预测负荷或前5个工作日平均负荷。两边基数不一样削减量自然对不上。解决把baseline当成一个外部输入参数冻结不进优化变量也不由MPC内部推导。数据层单独做接口从需求响应平台或历史单据读基线。代码里加一个字段baseline_source: load_forecast / d-2_average / platform结算前先核对这个字段。4.5 坑4滚动执行时预测接口返回同一份数据现象MPC跑起来之后每个采样周期的决策完全一样退化成开环调度。原因预测接口实现时把时间索引写死了比如def get_forecast(t): return all_data每次拿到的都是完整序列MPC以为自己看得很远实际什么都没更新。解决在预测接口签名里强制带t和Hdef get_forecast(t, H): return { load: load_data[t:tH], price: price_data[t:tH], pv: pv_data[t:tH], }参数说明t的单位必须和采样周期一致。调试时在runner里打印t和state_now肉眼确认每个周期都在往前推。这一步排障成本最低却最常被忽略。4.6 坑5求解器返回“最优”但plan是空的状态status没检查现象某几轮plan全是0程序没报错但系统实际在裸奔。原因cvxpy在求解失败时不一定抛异常prob.status可能返回infeasible或unbounded此时prob.value是None。下游没检查直接把None转成0错得悄无声息。解决每次prob.solve()之后立刻查prob.status只有optimal才往下走否则走fallback策略。fallback我给的是“按固定时段充电到SOC0.7就停”宁可次优也不要违规越限。提示MPC的求解器是黑匣子回退策略就是后悔药。不要省那一行if判断。5. 回测你的MPC调度器验证协议、指标口径与一个调权重的小技巧5.1 回测协议拿历史数据证明MPC比开环省多少MPC调度器写完之后不能只看仿真动画好看要跑一套严格的回测协议。我一般按下面五步来第一步准备至少两周的历史数据时间分辨率15分钟负荷、光伏、电价、需求响应基线。第二步同一个场景下跑三个方案固定规则、单日开环、MPC滚动。第三步三者共用同一份预测数据预测数据可以由历史数据人为加噪声得到加噪声的目的是区分“预测误差下的鲁棒性”。第四步记录总电费、DR收益、削峰率、计算耗时。第五步重复跑多个星期用平均数和标准差下结论。回测结果表头大概长这样方案总电费(元)DR收益(元)削峰率(%)平均单步耗时(s)固定规则运行后填入0运行后填入无单日开环运行后填入运行后填入运行后填入单次≤1MPC滚动运行后填入运行后填入运行后填入单次≤5怎么读这张表MPC的优势主要体现在DR收益和削峰率上总电费不一定会比单日开环低因为可平移负荷和舒适度约束会牺牲一部分经济性。如果MPC在预测误差加大的场景里成本仍然可控说明滚动修正生效了。5.2 指标口径成本、收益与SOC起止如何对齐回测里一个常见的乌龙是成本算错变量是15分钟一个断面直接用功率乘时长算电费抄错单位或者SOC起止值不一样导致电池在回测里“偷”了一部分能量。统一口径有三条硬规则第一成本按能量表积分算sum(P_grid * dt_h * price)不要按平均功率乘全天时长。第二DR收益的削减量按结算基线算不是按模型里baseline - P_load_var的瞬时值。第三SOC起点和终点要一致。我一般加一个终值约束SOC[H] SOC[0]或者回测结束后打印SOC终值如果不一致就重新调整目标函数里的SOC权重。5.3 MPC调权重的一个小技巧分层调试MPC的目标函数常常是多项的购电成本、DR收益、舒适度惩罚。最蠢的做法是一上来就把所有项都打开然后盯着结果盲调。我的习惯是分三层往上加第一层DR收益系数先置0只看购电成本调电池模型和SOC边界。当48步预测下的成本曲线平滑了说明模型没有硬伤。第二层打开DR收益让激励价格引导负荷削减这时如果削峰率没变化大概率是基线或约束写错。第三层再加减负荷舒适度这类软约束用二次惩罚# 温度舒适度惩罚项 temp_ref 22.0 temp cp.Variable(H) weight_comf 0.5 objective weight_comf * cp.sum(cp.square(temp - temp_ref))这里weight_comf0.5的单位其实是“元/℃²”和电费的“元/kWh”量纲完全不同不能直接比较大小。正确做法是从0.1开始逐步加每加一次看一次目标函数里两项的真实数值贡献贡献比例大概在10%~20%时停下。这就是调权重的底层逻辑——不是调系数是调各项在目标函数里的量级占比。我第一次给真实园区上MPC的时候H取96MIP一直超时后来把H砍到48、把可平移负荷的启动窗口从全时域改成固定区间求解器才从“玄学”变成稳定输出。需求响应收益对不上账查到最后是基线口径的问题——这些坑基本都会遇到代码分层清晰了定位起来快很多。希望帮到你。本文还有配套的精品资源点击获取
返回列表