楼宇温控负荷需求响应:基于MPC的优化控制实战解析 夏季电网负荷一到高峰时段管理方最先盯上的往往就是楼宇的空调系统随便一栋中型写字楼的中央空调峰值负荷都能占到整楼用电的40%以上。这也就解释了为什么需求响应Demand Response里楼宇温控负荷一直是最热门也最难啃的一块。难点在于楼宇本身是一个大惯性的热系统室温变化滞后明显如果只在收到削峰信号后把设定温度往上调两度很容易出现温度过冲、用户投诉或者信号结束后的负荷回弹。我见过太多项目都在这个问题上栽跟头直到换上了模型预测控制MPC方案才真正跑通。模型预测控制在这类场景下的核心价值是它始终围绕“预测未来状态 滚动优化 反馈校正”这条主线运转能够提前把未来几小时的室温变化、用电功率和舒适约束全部塞进一个优化问题里求出一组最优的空调设定值轨迹再逐步执行。相比于简单粗暴的规则式响应MPC可以做到在满足舒适度边界的前提下最大化降低负荷或者精准跟踪需求响应信号。这篇内容我就拿自己调试过的一套楼宇温控负荷需求响应优化系统的代码来拆解把每个模块到底实现什么功能、为什么这么写、参数怎么定、坑在哪里统统讲清楚。适合已经接触过一些控制算法或者建筑能耗优化但还没真正把MPC落到楼宇项目里的朋友参考。1. 这套系统的整体设计思路为什么是 MPC 而不是更简单的控制方案1.1 楼宇温控负荷在这件事里到底是什么角色做需求响应首先要理解楼宇热惯性这个天然优势。一面混凝土墙体、一整栋楼的结构体和内部家具在用电设备关停或者降载之后并不会马上让室温剧烈变化这种“蓄热”特性相当于给楼宇配备了一个隐藏的虚拟储能库。削峰时段可以提前预冷也就是把建筑冷却到舒适带上限以下的某个温度然后在负荷高峰时减少制冷功率让楼宇靠自身热惯性维持室温。反过来在填谷时段可以提前把温度放高一点在低价时段集中制冷。问题在于这个“预冷”和“释能”的过程要协调多个时间段的约束比如预冷温度不能低于舒适区、释能时温度不能超过舒适区上限、制冷机组功率不能越限。这类带约束的时序优化用规则表很难做全用开环控制又扛不住扰动所以要从算法层面去解决。1.2 MPC 与其他控制方案的对比常规的楼宇控制方案主要有三种PID反馈、基于规则的分段式调节、以及MPC。直接把它们放在一张表里看优势劣势会更直观控制方案核心思想优势劣势PID根据设定值与实际值偏差做比例、积分、微分调节实现简单不用模型无法处理约束也不具备前瞻性适合在设定值附近做微调规则式调节按固定的温度阈值或时间段开关机组、升降设定值工程上容易实现运维好理解场景一变就要重调规则响应过程中容易过冲难兼顾电费与舒适度MPC基于预测模型滚动求解未来有限时域内的最优控制序列天然处理多变量、多约束能在需求响应信号与舒适度之间做折中能把需求响应当成“跟踪目标”而非“触发动作”依赖模型精度和算力开发调试门槛高我实际测下来MPC在两种场景下优势最突出一是需求响应信号的波形比较复杂比如阶梯式削峰、动态价格、手动调度指令这些需要“提前预判”的情况二是楼宇里既有空调又有通风、照明等多套可控设备需要协同决策的时候。MPC可以统一建模统一优化规则式方案则要为每一种组合单独写一套逻辑。1.3 系统总体架构与代码模块划分我负责的项目里整套系统代码分为五个核心模块后续章节会逐个展开楼宇热动力学预测模型模块把房间、墙体、空调换热过程抽象为状态空间模型用来预测未来室温与功耗。需求响应信号处理模块负责对接电网或上级管理平台下发的DR事件、价格序列把它们转换成MPC目标函数里的参考轨迹。优化求解与滚动窗口模块调用优化器求解二次规划问题得到接下来的控制序列并实现“只执行第一步滚动求解”的闭环。状态估计与反馈校正模块用卡尔曼滤波或者简化的输出反馈修正预测模型的偏差避免模型失配累积。数据通信与指令下发模块读取楼宇传感器数据、下发设定值指令以及把执行结果记录下来供分析。整体执行链路是数据采集 - 状态估计 - 生成参考信号 - 构造优化问题 - 求解 - 下发第一步指令 - 等下一个控制周期继续。2. 核心模块代码功能拆解2.1 楼宇热动力学模型的建模函数MPC的“预测”能力靠的就是这个模型。楼宇热过程通常用RC网络等效模型来描述复杂一点可以做多阶多容模型但工程上为了在预测精度和求解效率之间平衡一般简化成2R2C或者3R2C模型就够了。一个最基本的房间热平衡可以写成C * dT_zone / dt (T_oa - T_zone) / R_wall Q_hvac Q_internal Q_solar其中C是房间热容R_wall是墙体等效热阻T_oa是室外温度Q_hvac是空调向房间释放的冷量或热量Q_internal是人员设备负荷Q_solar是太阳辐射得热。把所有连续方程离散化成状态空间形式后代码里可以这样表达import numpy as np class BuildingThermalModel: 简化2R2C楼宇热模型 状态: x [T_wall, T_zone] 输入: u [Q_hvac] 空调制冷功率 (kW) 扰动: d [T_oa, Q_internal, Q_solar] def __init__(self, C_zone5.0, C_wall20.0, R_wall0.12, R_win0.05, dt60.0): self.dt dt # 控制周期单位秒 # 热容、热阻定义 self.C_zone C_zone # 室内空气热容 kWh/K self.C_wall C_wall # 墙体热容 kWh/K self.R_wall R_wall # 墙体热阻 K/kW self.R_win R_win # 窗户热阻 K/kW self.A None self.B None self.E None self._build_state_space() def _build_state_space(self): # 这里根据热平衡方程离散化为状态空间 # x[k1] A x[k] B u[k] E d[k] dt self.dt / 3600.0 # 转成小时 A np.array([ [1 - dt / (self.C_wall * self.R_wall), dt / (self.C_wall * self.R_wall)], [dt / (self.C_zone * self.R_wall), 1 - dt / (self.C_zone * self.R_win) - dt / (self.C_zone * self.R_wall)] ]) B np.array([ [0.0], [dt / self.C_zone] ]) E np.array([ [dt / (self.C_wall * self.R_wall), 0.0, 0.0], [dt / (self.C_zone * self.R_win), dt / self.C_zone, dt / self.C_zone] ]) self.A, self.B, self.E A, B, E def predict(self, x0, u_seq, d_seq): 根据当前状态和未来输入、扰动序列预测未来各时刻状态 x0: 初始状态向量 [T_wall, T_zone] u_seq: 未来控制序列 (N,) d_seq: 未来扰动序列 (N, 3) x x0.copy() preds [] for k in range(len(u_seq)): x self.A x self.B * u_seq[k] self.E d_seq[k] preds.append(x.copy()) return np.array(preds)这个模块里的关键点在于初始状态x0不是直接用传感器测到的室温而是通过状态估计模块融合出来的值后面第2.4节细说。C_zone、C_wall这些参数建议用实验数据辨识不要拍脑袋定。识别方法可以是给楼宇做一个阶跃测试锁住空调功率看室温上升或下降曲线再用最小二乘法拟合RC参数这样预测精度能提升一大截。2.2 需求响应信号处理模块需求响应信号不是只有“削峰”这一种。我实际工程中遇到的至少有三类电网削峰指令要求在某个时段把楼宇总用电功率降到给定的基线以下。对应到MPC就是给一个上限值比如未来两小时有功功率不超过300kW。动态电价序列分时电价或者实时电价MPC目标函数里加电费项自动把大功率设备挪到电价低的时段。调节里程/爬坡指令要求楼宇跟随一个AGC的功率指令此时MPC的优化目标从“最低电费”变成了“跟踪功率参考轨迹”。对应到代码上我把这些信号统一转换成目标函数里的参考值向量ref_seqimport pandas as pd import numpy as np class DRSignalProcessor: 需求响应信号处理模块 将不同类型的DR指令统一转换为 优化问题中的参考轨迹或者约束 def __init__(self, dr_typepeak_shaving): self.dr_type dr_type self.power_baseline 0.0 self.dr_schedule [] # [(start_time, end_time, limit/price)] def load_dr_event(self, event_json): 从外部接口加载DR事件 event_json示例: { type: peak_shaving, start: 2025-07-21 14:00, end: 2025-07-21 16:00, limit: 280.0, # 单位kW price: [0.8, 1.2], # 动态电价 } self.dr_type event_json.get(type, peak_shaving) self.dr_schedule event_json.get(schedule, []) if self.dr_type peak_shaving: # 削峰场景未来N步的功率上限 return self._build_power_limit_ref(event_json) elif self.dr_type price_based: return self._build_price_signal(event_json) def _build_power_limit_ref(self, event_json): # 返回参考idxs: index in horizon, limit_value refs [] for item in self.dr_schedule: start_idx int(item[start_minute] / 15) # 假设15分钟一个采样点 end_idx int(item[end_minute] / 15) refs.append((start_idx, end_idx, item[limit_kw])) return refs def _build_price_signal(self, event_json): # 返回未来若干时刻的电价数组 price_series event_json[price_series] return np.array(price_series)核心设计思路是把需求响应信号统一映射到目标函数或者约束条件中。削峰场景下它表现为功率上限约束价格场景下它表现为目标函数中的电费项爬坡跟踪场景下它表现为参考功率的跟踪项。这种抽象方式的好处是控制主程序完全不用关心DR信号来源的细节来什么信号都能跑。2.3 优化求解与滚动窗口实现这是整段代码的核心。MPC每一步要做的事情是在当前时刻k基于当前状态估计x_est求解未来Np个预测时域、Nu个控制时域内的最优控制序列使得目标函数最小然后把第一个控制量下发出去。下一个采样周期状态更新再重新求解。目标函数我在项目里是这样设计的min Σ_k [ w_p * (P_total(k) - P_ref(k))^2 w_c * (T_zone(k) - T_set(k))^2 w_d * (Δu(k))^2 ]第一项是需求响应跟踪P_ref是DR给的参考功率第二项是舒适度偏差惩罚第三项是控制动作变化量惩罚防止空调功率频繁波动。经典MPC的滚动优化循环代码大致长这样import cvxpy as cp import numpy as np class MPCController: def __init__(self, thermal_model, Np24, Nu12): self.model thermal_model self.Np Np # 预测时域 self.Nu Nu # 控制时域 self.dt 15 # 控制周期15分钟 self.u_min 0.0 # 空调最小功率 kW self.u_max 150.0 # 空调最大功率 kW self.t_min 22.0 # 舒适度下限 self.t_max 26.0 # 舒适度上限 self.w_p 1.0 # DR跟踪权重 self.w_c 0.5 # 舒适度权重 self.w_d 0.1 # 平滑权重 def solve(self, x0, t_set, p_ref, d_seq): 求解一步MPC问题 x0: 当前状态估计 t_set: 期望温度序列 (Np,) p_ref: DR参考功率序列 (Np,) d_seq: 扰动预测序列 (Np, 3) A, B, E self.model.A, self.model.B, self.model.E nx A.shape[0] u cp.Variable(self.Nu) x_pred [x0] power_pred [] for k in range(self.Np): # 控制序列在Nu之后保持不变 if k self.Nu: u_k u[k] else: u_k u[-1] x_next A x_pred[-1] B * u_k E d_seq[k] x_pred.append(x_next) # 假设电功率与热功率近似线性关系 power_pred.append(u_k * 3.5 0.8) # 系数需要根据实际制冷能效比修正 # 构造目标函数 cost 0.0 for k in range(self.Np): cost self.w_p * cp.square(power_pred[k] - p_ref[k]) cost self.w_c * cp.square(x_pred[k][1] - t_set[k]) for k in range(self.Nu): if k 0: cost self.w_d * cp.square(u[k] - u[k-1]) # 约束 constraints [] for k in range(self.Np): constraints [x_pred[k][1] self.t_min, x_pred[k][1] self.t_max] for k in range(self.Nu): constraints [u[k] self.u_min, u[k] self.u_max] prob cp.Problem(cp.Minimize(cost), constraints) prob.solve(solvercp.OSQP, verboseFalse) if prob.status in [optimal, optimal_inaccurate]: return u.value, prob.value else: return None, None def run_rolling_horizon(self, x0, t_set, p_ref, d_seq, horizon_len): 执行滚动优化逐次求解并只应用第一步 u_traj [] for step in range(horizon_len): u_opt, _ self.solve(x0, t_set, p_ref, d_seq) if u_opt is None: # 求解失败回退到规则模式 u_apply self._fallback_control(t_set[0], x0[1]) else: u_apply u_opt[0] u_traj.append(u_apply) # 这里需要外部系统反馈实际状态用于下一步的状态估计 # 实际项目中 x0 estimator.get_updated_state(measurement) d_seq np.vstack([d_seq[1:], d_seq[-1]]) # 扰动序列向前平移 t_set np.roll(t_set, -1) p_ref np.roll(p_ref, -1) return u_traj这里有几个工程细节值得记一下预测时域Np通常取24步15分钟一个点即6小时太短了看不到负荷转移的收益太长了求解负担重且远期扰动的预测误差大。控制时域Nu可以比Np短在Nu之后保持控制量不变这样既降低求解变量数量也有不错的控制效果。我用Nu12、Np24整体效果稳。功率和热量之间不要用固定COP换算我就是因为一开始图省事用了固定COP结果功率跟踪误差特别大。后来把冷水机组的COP随室外温度和负载率变化曲线拟合进去偏差降了两个百分点。2.4 状态估计与反馈校正模块MPC的预测用的是模型但模型总归不是实际对象真值和模型之间一定有偏差。室内温度传感器读数可以直接用但墙体温度、设备发热量这些都是不可测的这就要靠状态估计把模型和实测数据融合起来。我在这里用的是带输出校正的状态估计方案可以简单理解为“卡尔曼滤波”的等效闭环思路class StateEstimator: 基于模型预测与实测数据的加权校正 用实际室温反馈修正状态预测抑制模型偏差 def __init__(self, thermal_model, K_gain0.3): self.model thermal_model self.K_gain K_gain def update(self, x_pred, t_zone_measured): x_pred: 上一步模型预测出的当前状态 t_zone_measured: 当前实测室温 # 将实测室温与预测室温的偏差按比例反馈到状态修正 error t_zone_measured - x_pred[1] x_corrected x_pred.copy() x_corrected[1] self.K_gain * error # 墙体温度项只做轻微修正因为墙体温度观测不到 # 但可以按一定比例间接修正 x_corrected[0] 0.1 * self.K_gain * error return x_corrected实际项目中如果没有时间上卡尔曼滤波这个简化版本也能用K_gain一般取0.2到0.4之间。它解决的问题是模型预测的室温如果一直比实测高0.5度反馈校正会把预测拉回实测轨迹附近防止优化器基于错误的预测状态做决策。3. 关键算法实现细节与参数调优3.1 预测时域和控制时域的选择预测时域长度本质上取决于两个因素楼宇热时间常数和需求响应事件的持续时间。热时间常数描述的是“楼宇有多懒”混凝土结构办公室的热时间常数通常在2到6小时之间这意味着空调动作的影响会在几小时内逐渐显现。如果把预测时域设成1小时等于只看到变化刚开始的阶段无法评估提前预冷对峰段的削减效果决策就会偏短期甚至产生反效果。我在项目里按下面的经验取值采样周期15分钟兼顾传感器通信与执行机构响应预测时域6小时即24步控制时域3小时即12步3.2 目标函数权重的整定逻辑权重有三个DR跟踪权重w_p、舒适度权重w_c、平滑权重w_d。网上很多资料只说“根据工程经验调试”但这个说法过于笼统。我自己整定权重的路线是分三步第一步先把w_p设为1.0w_c设为0w_d设为0只做功率跟踪看清系统功率侧的极限能力。第二步把w_c从0逐步加到0.1、0.5、1.0观察舒适度偏差分布找到舒适度开始对功率跟踪产生明显限制的拐点。第三步加上w_d做平滑处理用小步长扫描取值直到控制量波动频繁度下降并且功率跟踪误差不显著变大。一个比较有效的参考做法是把几个目标的物理单位先归一化。功率的偏差单位是kW温度的偏差单位是℃直接相加没有物理意义。我建议先做归一化normalized_power_error (P - P_ref) / P_rated normalized_temp_error (T_zone - T_set) / temp_bandwidth然后再设置权重这时候w_p和w_c的量级都落在0到1之间物理含义就清晰了权重比也有实际参照。默认值w_p1.0、w_c0.3、w_d0.05是比较好的起点。3.3 约束条件处理的工程化MPC用来杀鸡焉用牛刀的爽感很大程度来自约束处理。但工程上约束不能设得太理想化。我踩过最深的一个坑是把温度上限设成硬约束26度结果夏季高温天优化器直接无解。因为当室外温度高达38度时室内冷负荷可能超过空调最大功率能处理的边界你再怎么优化也没办法把室温压到26度以下。解决方案是把温度舒适度改成软约束# 温度约束改为软约束引入松弛变量 t_slack cp.Variable(self.Np) # 松弛惩罚系数远大于其他权重 w_slack 100.0 cost w_slack * cp.sum(t_slack) for k in range(self.Np): constraints [x_pred[k][1] self.t_min - t_slack[k], x_pred[k][1] self.t_max t_slack[k], t_slack[k] 0.0]这样当系统确实无法满足温度边界时会用松弛变量兜底求解器至少能给出一个可行解不至于让系统在极端天气下直接失去控制。3.4 求解器选型经验优化问题规模不大是可微凸问题用二次规划求解器就够了。我个人在Python环境里常用cvxpy封装后端求解器试过OSQP、ECOS和CLARABEL。OSQP在算力资源紧张的边缘设备上表现最好启动快内存占用低。ECOS精度高一些但对大型问题求解速度稍慢。CLARABEL对大规模稀疏问题友好适合后期扩展到多区域协同场景。实际项目里我只用OSQP就够了单步求解实测在几十毫秒内完成完全能在15分钟控制周期内做完多轮热重启迭代。4. 工程落地中的常见问题与排查实录4.1 模型预测与实测偏差太大最常见的原因是建筑模型参数失真。楼宇使用几年后墙体保温性能、门窗气密性都有变化用设计图纸上的参数直接仿真误差会一天比一天明显。这事没有一劳永逸的办法最有效的手段是定期做参数辨识校准。我用过一个比较简单的校准流程选取夜间或者工作日非高峰段关闭部分机组给楼宇做一个有规律的功率阶跃变化。连续采集24小时室内外温度数据。用最小二乘或者单纯形搜索法反推RC参数中的R和C。每季度做一次这样的辨识把新的参数更新进模型文件。实测下来做了参数校准之后6小时预测轨迹的平均温度误差可以从1.8度缩小到0.6度以内。4.2 求解时间过长导致控制周期卡顿有段时间在客户的工控机上跑单步求解动不动要好几秒钟原因出在墙体模型阶数太高把一栋楼做成了30多个房间的详细RC网络再乘以预测时域问题规模一下就大了好几圈。后来把同一楼层、同朝向、相似使用模式的房间做了聚合30个房间聚合成了4个热区求解时间立刻降到几十毫秒控制效果几乎没有肉眼可见的差别。对楼宇MPC来说模型并不是越细越好细模型带来的参数不确定性反而可能让预测精度变差。聚合之后再调参整个项目调试节奏会顺很多。4.3 需求响应基线漂移与负荷反弹这类问题不在控制算法本身而是需求响应执行后期的常见现象。如果削峰时段结束后立刻把设定温度调回舒适值空调会全功率运行追赶降温形成比正常情况还低的负荷尖峰这就是“负荷反弹”。在电费结算时这个反弹尖峰可能直接抵消掉削峰带来的收益。MPC天然能缓解这个问题因为在预测时域内可以安排一个平缓恢复阶段。我在目标函数里加了两个策略配合一是DR事件结束前1小时就开始逐步放松温度约束让空调功率平缓回升二是对控制动作变化量做了更强的惩罚防止功率跳变。实测反弹幅度能降低三分之一以上。4.4 传感器异常对MPC的影响MPC的行为非常依赖当前状态估计值如果某个温度传感器故障或者输出严重漂移状态估计会失真随之而来的优化结果也完全不可信。我在实际部署中加了多层保护对传感器原始数据做有效性校验超出物理范围或者连续多个周期变化率异常的数据直接标记无效。一个热区至少保留两个温度传感器用中位数而不是平均值作为区域温度。如果所有传感器都失效系统自动切换回规则控制模式不再依赖MPC。这套安全兜底逻辑在部署初期尤其重要因为现场通信不稳定、传感器被装修遮挡、工作人员误操作线缆等等问题实在太常见了。4.5 控制指令下发和实际执行不一致还有一个很容易被忽视的问题MPC算出最优功率指令但楼宇空调系统实际执行的不是“功率指令”而是“温度设定值”。所以代码层面还需要把功率设定值翻译成温度设定值通过楼宇自控系统BA系统下发。这个翻译过程如果直接按静态模型换算在现场会有明显偏差。我最终的做法是加载一个当日实时查表关系把前一天的历史数据按室外温度分箱统计出每个温度区间内不同设定值对应的平均功率曲线。虽然方法不高级但在现场效果非常实用。可以看一下这个查表模块的代码框架class PowerToSetpointMapper: 将MPC输出的功率指令映射为可执行的温度设定值 基于历史运行数据的分箱统计 def __init__(self, hist_df): self.lookup_table self._build_lookup_table(hist_df) def _build_lookup_table(self, df): # df 至少有: power, setpoint, outdoor_temp 三列 tbl {} oa_bins [20, 24, 28, 32, 36] for oa_min, oa_max in zip(oa_bins[:-1], oa_bins[1:]): subset df[(df[outdoor_temp]oa_min) (df[outdoor_temp]oa_max)] # 对每个设定值求平均功率 avg_power subset.groupby(setpoint)[power].mean() tbl[(oa_min, oa_max)] avg_power return tbl def map_power_to_setpoint(self, target_power, outdoor_temp): for oa_bins, series in self.lookup_table.items(): if oa_bins[0] outdoor_temp oa_bins[1]: # 找到功率最接近的设定值 idx (series - target_power).abs().idxmin() return idx # 温度区间匹配不到时返回默认设定值 return 24.05. 这套系统未来可以怎么扩展5.1 从单体楼宇到集群协同我之前做的还只是单栋楼的MPC但实际电网调度端更关心的是一个片区内几十栋楼组成的虚拟机组能提供多少调节能力。扩展方向是把每栋楼当成一个独立的“虚拟电池”在上层再加一个集群优化层给每栋楼分配不同的DR调节量。底层的楼宇MPC继续负责本地的约束和舒适度上层集群优化器只下发功率参考值形成一个双层架构。这个方向我在仿真环境跑通过比较吃通信时延和楼宇模型的标准化程度。5.2 与更多可控设备联动除了空调系统楼宇里还有很多可以参与响应的负荷比如储能电池、充电桩、照明系统、电梯回馈装置。把它们的约束和动态特性统一建模进MPC的优化问题里目标函数依然是电费、功率跟踪和舒适度/服务质量的加权这样楼宇的调节潜力可以整体放大。这个扩展的难点在于不同设备的时间常数差异很大真要做在一个时间尺度上需要在模型预测时域上做分层处理。5.3 数据驱动与机理模型的融合单纯的物理模型在参数辨识和复杂场景泛化上有天花板。我现在的下一步计划是把基于图神经网络的预测模块与机理模型做残差融合机理模型负责外推趋势网络模型负责捕捉非线性残差。这种混合建模方式匹配MPC的框架很自然因为MPC需要的不只是当前一步预测准确还是未来N步的滚动轨迹都尽可能准确数据驱动模型在处理非平稳气象扰动时优势更明显。不过要务实一点这类扩展从研究到落地需要大量数据积累短期内还是以经典MPC加定期参数校正为主。先把基础框架跑稳定后面再逐步叠加数据驱动模块会更稳妥。关于这套系统我最后再多说几句如果从头再让我做一次楼宇需求响应项目我依然会首选MPC框架但会更早地去建立详细的用能基线测量和模型校准机制。很多团队觉得MPC难落地其实算法本身并不复杂复杂的是现场数据质量、设备通信协议和运维习惯这些工程琐事。对于想尝试的朋友我建议第一次试点选数据基础比较好、楼宇自控系统开放接口比较成熟的单栋楼不要一上来就做集群。先跑稳单栋楼的削峰场景把模型校准闭环和异常保护机制做扎实了再往多设备和集群方向扩展。这套思路下来项目成功率会有很明显的提升。