
能源集线器Energy Hub, EH这个词容易让人以为是一台具体的物理设备但在电热综合能源市场的优化建模里它更像一个抽象的决策中枢从电网买多少电、从气管网买多少气、热电联产机组烧多少气发多少电、燃气锅炉什么时候顶上去、蓄电池和蓄热罐在哪个时段充放全在这个节点里做统一决策。接到这个题目的时候我先面对的问题不是怎么建模而是为什么一定要用双层优化模型——单层调度一天就写完了多加一层到底多算出了什么答案是价格从哪来现实中电价和热价不是固定的工程量系数而是市场出清的结果能源集线器一边作为投标者影响价格一边又要拿出清价做自己的内部调度。这个相互反馈只有双层结构能表达清楚。这篇总结会把整个建模思路、数学处理、MATLAB实现和调试过程摊开来讲。适合正在做综合能源系统优化、想要复现双层模型或者被KKT、强对偶、MILP这些术语劝退过的研究生和工程师参考。后面所有内容围绕一套完整可跑的案例展开尽量把为什么这么做也讲明白。1. 能源集线器在电热市场里的真实角色不止是设备组合1.1 把电和热分开算为什么会漏掉真正的优化空间很多做电力系统的人习惯性地把热负荷当成附加在锅炉上的需求。如果真的把电负荷和热负荷分开处理就会漏掉一类典型机会电价高的时候CHP多发电很划算但发出来的热如果没有去处就变成了弃热成本反过来热价高的时候CHP多发热很划算但发出来的电如果卖不掉又得压着电出力。这种发一送一的耦合约束不是传统电力潮流模型能自然表达的东西。能源集线器把电、热两个网络在节点层面合并输入的天然气、外购电经过CHP、锅炉、电锅炉等设备转换成电和热输出所有成本与收益在同一本账目里结算。于是优化器能回答这笔天然气是用CHP产电还是用锅炉产热更赚这样的交叉问题。用一个生活化的类比这就像一个既做咖啡又做奶茶的档口原材料一样设备不一样但收入要按所有菜单一起算。分开记账就永远算不清该把多少原料投给哪条产品线。1.2 常见EH设备配置与运行约束长什么样一个典型园区级EH设备配置和关键参数大致如下表。下面的算例就按这套配置走。设备输入输出关键效率参数容量参考热电联产机组 CHP天然气电热电效率0.35热效率0.452000 kW燃气锅炉 GB天然气热热效率0.901500 kW电锅炉 EB电热电转热效率0.95500 kW蓄电池 BESS电电充放效率0.92/0.922000 kWh蓄热罐 TES热热充放效率0.90/0.903000 kWh每个时段t的能量平衡约束可以写成标准形式P_buy(t) η_e^C * V_C(t) P_bat_dis(t) P_load(t) P_sell(t) P_EB(t) P_bat_ch(t)H_C(t) H_GB(t) η_EB * P_EB(t) H_TES_dis(t) H_load(t) H_sell(t) H_TES_ch(t)其中V_C(t)是CHP消耗的天然气功率CHP热出力与电出力满足热电比关系H_C(t) (η_h^C / η_e^C) * P_C(t)除了能量平衡还要写设备出力上下限、爬坡约束、储能SOC递推方程以及蓄电池、蓄热罐的容量边界。这些约束本质上是在圈定EH的可行运行域双层模型的所有博弈行为都发生在这个可行域边界上。1.3 双层模型里EH怎么身兼两职在市场中EH至少要扮演两个角色。第一个角色是投标者它向上层交易中心申报各时段的购电、售电、购热、售热计划这些申报量直接进入市场出清模型的供需平衡约束。第二个角色是调度者等市场出清价格λ_e(t)、λ_h(t)公布后EH再根据这个价格安排内部每台设备的出力让净利润最大化。这两个角色之间存在明显的循环依赖EH的投标量影响出清价格出清价格又反过来决定EH的最优调度。如果只把价格当成外生常数就人为切断了这个循环算出来的最优在真实市场里根本不成立。这也是为什么要在最大收益和最小成本之间寻找平衡——市场不会因为你多买电就降价反而可能把你自己的购电成本推上去。2. 双层优化的博弈结构谁在领导谁在跟随2.1 上层出清以系统总运行成本最小为目标的裁判上层模型通常代表交易中心或系统运营商的视角目标是在满足各类约束的前提下最小化系统总运行成本。对电-热综合市场可以把出清问题写成min Σ_t [ Σ_i c_i P_i(t) Σ_j d_j Q_j(t) ]受约束于Σ_i P_i(t) D_e(t) 电力功率平衡对偶乘子记为λ_e(t) Σ_j Q_j(t) D_h(t) 热力功率平衡对偶乘子记为λ_h(t) P_i_min ≤ P_i(t) ≤ P_i_max Q_j_min ≤ Q_j(t) ≤ Q_j_max这里的D_e(t)和D_h(t)是市场总需求其中就包含我们关心的EH申报的购电量和售热量。对偶乘子λ_e(t)和λ_h(t)的经济含义就是该时段的节点边际电价和边际热价。当系统接近供不应求时λ会被抬高当供给充裕时λ会走低。这就是价格生成机制被显式建模的过程。2.2 下层调度以最大净收益为目标的市场跟随者下层模型是EH运行商的视角给定上层出清得到的电价λ_e(t)、热价λ_h(t)在自身设备约束下安排运行计划。目标函数可以写成max Σ_t [ λ_e(t) * P_sell(t) λ_h(t) * H_sell(t) - π_e(t) * P_buy(t) - π_g(t) * (V_C(t) V_GB(t)) - C_om(t) ]其中π_e(t)是外购电价合同价或电网购电价π_g(t)是天然气价C_om(t)是设备的运行维护成本。约束包括1.2节列出的能量平衡、设备上下限、爬坡和储能状态递推。这里有一个容易被忽视的前提下层问题必须是线性规划后面用KKT替换才有数学上的完备性。如果加入机组启停的0-1变量下层变成MILPKKT条件就不再是充要条件整个单层化就需要做成启发式或分解算法。所以很多论文在EH下层刻意不用0-1启停变量而是用出力上下限加爬坡约束来近似目的就是保住线性性。2.3 两层之间的耦合变量到底传什么理解双层模型最重要的就是弄清楚两层之间每次迭代在传什么。我习惯用下面这张表来梳理。传递方向变量含义EH → 上层P_buy^bid(t), P_sell^bid(t), H_sell^bid(t)EH申报的购售电、售热量上层 → EHλ_e(t), λ_h(t)市场出清得到的电价、热价上层 → 市场P_i^(t), Q_j^(t)常规机组成交出力EH侧最终结果P_buy^(t), P_sell^(t), H_sell^*(t)EH中标量与内部调度计划需要特别指出这个结构是典型的Stackelberg主从递阶博弈上层是leader先决定市场价格形成规则下层是follower在给定规则下做最优响应。求出来的解是一个市场出清意义上的均衡点而不是简单地把双方目标函数加权合并。理解这一点对后面数学推导非常关键。3. 把双层压成单层KKT、强对偶与互补松弛线性化3.1 为什么不能直接把下层目标扔进上层最直观的错误做法是把下层的目标函数和约束直接拼进上层试图让求解器一起优化。这在数学上完全不对因为下层目标中出现了上层的出清价格λ而λ本身又是上层约束的对偶乘子这样写出来的模型强非凸、包含双线性项求解器第一轮就报不可解。另一个错误思路是两层迭代逼近先固定λ算下层再把下层结果带回上层更新λ如此循环。这个方法看起来合理但收敛性没有保证经常在两个方案之间来回震荡而且每次都要求解两个完整问题工程效率很低。正确的处理思路是用最优性条件替换下层。如果下层是线性规划KKT条件是下层问题最优解的充要条件可以把下层问题整体等价成一堆线性约束补进来再配合强对偶定理处理目标函数中的价格乘交易量双线性项最终得到一个单层MILP或MIQP。3.2 KKT条件展开后会得到什么对下层LP写出拉格朗日函数KKT条件分为四组。第一组是stationarity条件拉格朗日函数对每个决策变量求导等于零。第二组是primal feasibility即下层原本的等式和不等式约束。第三组是dual feasibility所有不等式对应的对偶乘子必须非负。第四组是complementary slackness每个不等式约束的松紧度与该约束的对偶乘子乘积等于零g_i(x) * μ_i 0这个乘积条件是非线性的也是整个问题从LP变成数学规划带均衡约束MPEC的核心难点。如果下层问题规模不大KKT补进去之后整个系统还是线性的只有互补松弛这一处非线性如果下层规模大比如几百台设备那个数目的互补松弛项会显著拖慢求解。工程上更常见的一种处理是先用强对偶定理写出原问题目标等于对偶问题目标这个等式用它替代一部分互补松弛关系把价格乘交易量的双线性项换成目标函数和成本项的表达式剩下的互补松弛再用大M法线性化。我实际搭建时这两种手段是配合使用的强对偶先消掉最麻烦的λ乘P项大M法处理剩下的互补松弛。3.3 互补松弛条件的M法线性化细节对于形如0 ≤ g_i(x) ⟂ μ_i ≥ 0的互补松弛条件标准线性化做法是引入二元变量z_i∈{0,1}写成两组线性约束g_i(x) ≤ M * (1 - z_i) μ_i ≤ M * z_i这样z_i取0时μ_i必须为0g_i(x)可以自由z_i取1时g_i(x)必须为0μ_i可以自由。逻辑上完美表达了二者至少一个取零的关系。M的取值是个实战性很强的问题。M太小可能把真正的自由空间剪掉漏掉可行解M太大MILP数值病态求解器动不动报不可行或者收敛很慢。我推荐的做法是先用固定价格单层LP跑一次看看所有约束的对偶乘子数量级然后取该量级的10到20倍作为M。比如某台CHP出力约束的影子价格在0.2左右那M取2到4就够了别一上来就写1e6。完成大M线性化之后整个模型变成一个包含0-1变量的MILP目标函数仍保持线性或二次但凸可以直接交给商用求解器。如果下层模型里实在绕不开整数变量那就要么转成MIQP用启发式处理要么把整数变量塞进外层循环用双层迭代总之没有一条平坦大路。4. MATLAB下的工程建模YALMIP、CPLEX与代码骨架4.1 工具箱选型与求解器配置MATLAB里做优化建模有几条路线手写intlinprog矩阵、用CVX做凸优化、用YALMIP做建模层。对双层转单层后的MILP我最推荐YALMIP加CPLEX或Gurobi的组合。YALMIP的好处是可以直接描述sdpvar、binvar、constraints这种接近数学表达式的结构不需要自己手动整理A、b矩阵后面改约束的时候成本极低。但YALMIP只做建模真正求解要靠底层求解器。配置也很简单把YALMIP所在目录addpath进MATLAB然后安装CPLEX或Gurobi并确保MATLAB能调用。命令行窗口执行addpath(genpath(D:/yalmip)); savepath; ops sdpsettings(solver,cplex,verbose,0);能正常返回solver信息就说明配置成功。如果没有商业求解器用MATLAB自带intlinprog也能凑合但小规模的24时段、单EH问题还行复杂场景建议申请CPLEX学术许可或Gurobi学术许可。4.2 YALMIP代码骨架变量、约束、目标三件套下面给出一段核心骨架展示建模层的典型写法。注意在完整双层-单层转化后λ_e和λ_h是待求变量而不是常数但建模层的写法是一样的。T 24; % 决策变量 Pbuy sdpvar(T,1); % 购电量 Psell sdpvar(T,1); % 售电量 Pchp sdpvar(T,1); % CHP电出力 Vchp sdpvar(T,1); % CHP耗气量 Hgb sdpvar(T,1); % 燃气锅炉产热 Peb sdpvar(T,1); % 电锅炉耗电 Pbat_ch sdpvar(T,1); Pbat_dis sdpvar(T,1); Htes_ch sdpvar(T,1); Htes_dis sdpvar(T,1); SOC_b sdpvar(T1,1); SOC_h sdpvar(T1,1); % 假设出清价格正式模型中由KKT联立得到 lam_e ...; % T行电价 lam_h ...; % T行热价 pi_e ...; % 购电价 pi_g ...; % 天然气价 % 目标售电收入售热收入-购电成本-购气成本-运维成本 obj sum(lam_e.*Psell lam_h.*Hsell - pi_e.*Pbuy - pi_g.*(Vchp Vgb) - om_cost); % 约束 cons []; cons [cons, Pbuy eta_e_chp*Vchp Pbat_dis P_load Psell Peb Pbat_ch]; cons [cons, eta_h_chp*Vchp Hgb eta_eb*Peb Htes_dis H_load Hsell Htes_ch]; cons [cons, 0 Pchp Pchp_max]; cons [cons, 0 Hgb Hgb_max]; cons [cons, SOC_b(2:T1) SOC_b(1:T) (eta_ch*Pbat_ch - Pbat_dis/eta_dis)*dt]; cons [cons, SOC_b(1) SOC_b_init]; cons [cons, SOC_b(T1) SOC_b_init]; % ... 其余约束和互补松弛线性化约束 ops sdpsettings(solver,cplex,verbose,2,cplex.mip.tolerances.mipgap,0.001); res optimize(cons, -obj, ops);写完optimize之后第一件事是检查res.problem字段返回0才说明求解成功。然后可以用value(Pbuy)取出各决策变量的数值用dual(cons(k))取某个约束的对偶乘子。4.3 求解设置与调试技巧实际调试过程中有几点经验值得拿出来说。第一MIP gap不要默认设成0。24时段模型gap从0压到0会导致求解时间指数级上升。工程上百分之零点五到百分之一就够用了误差对调度结果的影响几乎可以忽略。第二检查对偶乘子数值。KKT替换完之后我习惯把所有dual(cons)打印出来看一遍。如果某些对偶乘子出现极端值比如百万级别八成是大M取值不当或约束冗余这时候先清理约束不要急着加大M。第三给YALMIP初始解。initialize函数可以为二元变量指定初值尤其当你用上一次迭代的解做热启动时求解时间能缩短到原来的四分之一以下。这个技巧在灵敏度分析里特别有用因为我经常要把热价曲线改一改再求解几十次热启动能省大量时间。第四变量数量大的时候尽量用向量化sdpvar而不是逐个定义。YALMIP在构造约束时向量化表达式的开销远低于循环append后者在T24时还不明显到了T168甚至8760就非常痛苦。5. 典型日算例参数、结果与灵敏度分析5.1 设备与市场价格参数设定算例采用冬季典型日24时段设备参数沿用第1节表格。电负荷和热负荷曲线按园区级规模设定电负荷峰值约2200 kW热负荷峰值约1800 kW。分时购电价和热价如下表。时段购电价 π_e元/kWh出清热价 λ_h元/kWh天然气价 π_g元/m³1-60.380.283.27-120.720.353.213-180.860.383.219-240.280.303.2这里热价是典型冬季水平低于电价但峰谷趋势不完全一致这样市场才有套利空间。天然气的单位热值按9.7 kWh/m³折算每个时段的天然气功率除以9.7就得到实际用气量。5.2 双层结果与固定价格单层结果的对比我设置了三种方案做对比。方案A假设电价热价为常数EH做单层优化传统做法。方案B使用分时价格EH做单层优化但不考虑自身申报量对出清价的影响。方案C完整双层模型价格内生按本文方法求解。方案总运行成本元净利润元购电量kWhCHP发电量kWh燃气锅炉产热量kWhA 常数价格单层15240312018500161009200B 分时价格单层14780368017200178007800C 双层均衡14320402015800189006400方案A的问题在于把所有价格的峰谷波动抹平了优化器看不出高峰时段少购电的价值购电量虚高。方案B虽然看到了分时价格但没有意识到自己在高峰时段多买电会推高出清价结果高温时段过量购电拉高了结算成本。方案C的购电量比B少了约8%CHP发电量反而更高说明模型把更多天然气投入了CHP而不是单纯购电同时减少了燃气锅炉的高价时段出力——这正是最大收益与最小成本平衡的实际体现在边际成本和边际收益之间找到了更精确的切点。5.3 热价扰动下的灵敏度分析把热价曲线整体按正负10%平移观察EH的内部调度响应。结果有三个比较明显的规律。热价上升10%时CHP热出力占比从61%升到68%燃气锅炉产热量下降约12%蓄热罐的充放次数明显增加更倾向于在低热价时段储热、高热价时段放热。这说明模型确实在自动寻找哪个时段卖热更划算的套利窗口。热价上升20%时CHP接近满发状态但燃气锅炉出力不再显著下降因为热负荷约束已经把锅炉压到了下限附近无法再退。净利润随热价近似线性增长但增速放缓——设备容量边界成了瓶颈继续涨价只会让边际收益递减而不会无限放大利润。这个增速放缓的拐点恰恰就是模型内部最大收益极限所在位置。反过来热价下降10%时CHP的发电份额也下降模型转而更多从电网购电维持电负荷燃气锅炉承担主要供热。这时候整个EH更像一个纯电消费者加锅炉房而不是热电联产系统。这种策略层面的切换单层固定价格模型很难捕捉到因为价格一旦固定优化器不会对市场变化产生这些结构调整。6. 实测中最值得避开的四个坑6.1 大M参数不是越大越好我见过不少复现代码把M写成1e6理由是反正取大了不会丢解。实际跑下来M取到1e6之后CPLEX在第一轮割平面阶段就频繁出现数值警告甚至把明显不可行的解判成可行。正确做法是先解一次松弛LP看对偶乘子数量级M设在10到20倍之间。比如一台2000 kW的CHP出力约束的对偶乘子大概在0.1到1之间M取10就足够而不是1e6。6.2 单位不统一会让求解器直接休克双层模型里天然存在不同量级的量价格是零点几元/kWh功率是几千kW成本是几万元SOC是0到1的小数。这些量混在一个目标函数里如果不做归一化求解器内部的容差判断会非常混乱尤其是互补松弛线性化约束g_i(x) ≤ M(1-z_i)g_i(x)量级是千瓦而M是几十数值上看起来舒服但一旦某个约束的系数矩阵condition number变大整个MILP的收敛速度急剧变慢。我的习惯是先把所有物理量pu化功率除以基准功率1000 kW价格除以基准价1元/kWh成本除以基准成本1万元。这样所有变量的数值都落在0到100区间求解器状态明显稳定。中途要读取结果时再乘回物理单位展示不影响后续分析。6.3 MILP求解时间爆炸从小时级压到分钟级的操作24时段、单EH的双层单层化模型通常有几百个二进制变量和几千个约束CPLEX默认参数下求解可能要十几分钟到几小时主要瓶颈在互补松弛的二元变量。我实际用过的有效手段有三个。第一是设置mipgap为0.5%到1%不要追求绝对最优。市场出清问题对最优值的精确度要求并不高工程精度够了。第二是给二元变量做变量排序。CPLEX里可以用ops.cplex.ordering或直接在YALMIP里先固定一部分明显合理的z变量初值减少枚举分支。第三是用强对偶等式替代尽量多的互补松弛项。每替代一个就少一个二元变量求解规模直接下降。这也是我前面反复强调强对偶重要性的原因——它不只是数学技巧更是性能优化的关键。6.4 MATLAB环境与YALMIP版本的兼容性YALMIP在MATLAB更新后偶尔会有函数冲突。比如新版MATLAB里eig、rank等内置函数的行为变化会影响YALMIP的底层矩阵分析模型构造阶段可能报错。我的处理方式是复现项目时固定在某个已知稳定的MATLAB版本或者从YALMIP官方仓库拉最新版而不是用三年多没更新的打包版。另外CPLEX接口在不同版本的兼容性差异较大建议先在命令行用sdpsettings(solver,cplex)跑通一个小测试模型再上完整模型避免在正式模型里排查半天最后发现是接口文件没配对。做完整套下来我自己的体会是双层模型的价值不在于一次把结果算到最优而在于让所有参与方在同一个数学框架内找到共同预期。真正拿到工程环境里跑你会发现大部分时间都花在检查模型假设上——价格生成机制是否合理、互补松弛的大M是否给得准、下层模型的线性性有没有保住。模型假设对了求解器基本都能给出可信的结果。这套思路同样可以扩展到多能源品种、更多的设备组合甚至多时段耦合的年度规划只是在规模变大之后需要把效率优化和分解算法再往前走一步。