☰
区域多能源集群协同优化中的联合需求侧响应建模与MILP求解
2026/10/17 16:24:55 网站建设 项目流程

说实话,我在这类模型上吃过不少亏。做综合能源系统建模的人,最近绕不开的一个方向就是“考虑区域多能源系统集群协同优化的联合需求侧响应模型”。以前大家做需求侧响应,基本只动电负荷,但在多能源系统里,热、电、气是耦合在一起的,只调电往往受热负荷拖累,联动不起来。这篇文章把我从单区域模型一步步扩展到多区域集群、再把电热联合需求侧响应嵌进优化模型,最终用Matlab代码跑通整个求解流程的完整经验写出来,适合正在写相关课题、做论文复现或者准备做项目仿真的人参考。里面没有花里胡哨的包装,全是建模思路、约束怎么构建、代码怎么组织、出问题了怎么排查这些实打实的东西。

1. 模型整体思路与方案拆解

1.1 区域多能源系统集群是个什么状态

所谓区域多能源系统,你可以理解成一个具备电、热、气等多种能量输入输出能力的“小区级综合能源管家”。每一个区域内部,通常包含燃气轮机(CHP)、电锅炉、储能电池、光伏等设备,它们共同满足区域内部的电负荷和热负荷。之所以叫“多能源”,是因为电和热不是独立存在的——燃气轮机发电的同时会产热,电锅炉吃电却能供热,这种耦合关系让系统的调度比单一电网要复杂得多。

那“集群”又是什么意思?一个区域解决了,为什么还要把多个区域拉在一起优化?

原因很现实:单个区域有自己的资源禀赋。有的区域光伏装机多,白天出力大但本地用不完;有的区域燃气资源充足,缺的是电负荷需求;有的区域负荷峰谷差异明显,储能容量却有限。如果各区域各管各的,也就是独立优化,就会出现“光伏多的弃光、缺电的烧高价气”这种资源错配。而把多个区域组成一个集群,通过联络线让区域之间互通电力——某些场景也可以考虑热力互联——优化问题的可行域就变大了,整体运行成本往往能显著下降。

集群协同优化的核心就是:在统一的数学模型里,同时决定每个区域内部设备的出力、储能充放电、需求侧响应量,以及区域之间的交换功率,目标是让整个集群的总成本最小,而不是某个单独区域的成本最小。

1.2 联合需求侧响应在模型里解决什么问题

需求侧响应在能源系统里不是一个新概念,传统电力系统里就有,主要思路是让用户根据电价信号或者激励政策调整用电行为,实现错峰用电。放在多能源系统里,问题就不那么简单了。

一个区域里热负荷大多数情况下是刚性的。冬天要供暖,热水要供应,居民不会因为你电网紧张就不开暖气。可问题是,供热设备往往用电——电锅炉要耗电,CHP发电的同时也在供热。如果你只做“电的需求侧响应”,把电负荷削下来一块,却不管热负荷,系统为了维持供热,电锅炉照样得从电网取电,削峰效果会被严重稀释。反过来,如果能在一段时间内适当下调热负荷,比如在非核心时段允许供热温度有一点柔性区间,就能腾出更多的电力调节空间,让整个系统在更高成本时段少调用高价机组。

这就是“联合需求侧响应”的含义:把电负荷和热负荷放进同一个响应框架里,统一建模、统一调度。具体操作上,常用的手段是两个,一个是负荷削减,就是允许在高峰时段切除一部分可中断负荷;另一个是负荷转移,就是允许一部分可平移负荷从高峰时段挪到低谷时段。热侧的柔性被建模成“热负荷削减量”,同样有上下限约束和补偿成本。

我在实际建模中体会到,联合DR真正的价值不是省下那点补偿费用,而是它改变了系统的负荷曲线形态,让机组组合的出力策略更平滑。尤其是集群协同里,多个区域的DR叠加在一起,峰谷差会被明显压平,CHP和电锅炉的高成本出力区间就会被压缩。

1.3 求解方案怎么选:MILP是主流路径

模型确定之后,接下来最关键的决策就是用什么求解方案。这个方向的主流做法是把优化问题构建成混合整数线性规划(MILP),用Matlab里的Yalmip工具箱建模,然后调商业求解器求解。我用的就是这条路径,最终也在多区域算例上顺利跑通。

为什么是MILP而不是智能算法(粒子群、遗传算法这些)?原因在于这个模型里天然存在0-1整数变量:储能电池的充放电状态、机组启停状态,本质上是互斥的,连续变量表达不了这种互斥关系。MILP框架能把这类逻辑关系严密地表达出来,再加上有分支定界算法作为支撑,求解结果有全局最优性或明确的gap上界。智能算法虽然表面上实现简单,但参数敏感、收敛性没法保证,而且这个模型变量规模动辄上千,智能算法计算一次目标函数就要解一大批非线性等式,实际跑起来反而不靠谱。

当然MILP也有前提:模型里的非线性能量流关系需要线性化。比如储能充放电的互斥逻辑用Big-M法转换成线性约束,设备效率如果恒定,那功率与燃料/产热就是线性关系。这些处理做完之后,整个模型就是标准的MILP,交给求解器处理即可。

2. 数学模型:目标函数与约束条件

2.1 目标函数怎么拆

目标函数是所有优化问题的核心,整个建模过程中最需要想清楚的就是“你让求解器最小化什么”。我做的这个模型,目标函数选择的是集群总运行成本最小化,涵盖四个方面:

第一,外购电成本。区域集群和上级电网之间可以买卖电力,从电网购电需要付费,这通常是成本的大头。第二,燃料成本。燃气轮机烧天然气,每个时段有燃气消耗,对应成本。第三,设备运维成本。CHP、电锅炉、储能电池运行过程中有维护损耗,按出力大小折算成本,量级不大但不能省略。第四,DR补偿成本。用户响应你的号召削减负荷或者转移负荷,必须给补偿,这部分成本直接和DR调节量成正比。此外,为了鼓励消纳可再生能源,还可以在目标函数里加一个弃风弃光的惩罚项,让求解器尽量避免削减光伏出力。

目标函数写成数学形式就是这样:

min F = Σ_t Σ_i ( c_buy_i(t) * P_buy_i(t) + c_gas * G_chp_i(t) + c_om * (P_chp_i(t) + P_eb_i(t) + P_ess_dis_i(t) + P_ess_ch_i(t)) + c_dr * (L_cut_i(t) + L_shift_out_i(t)) + c_curtail * P_curtail_i(t) )

这里下标 i 表示第 i 个区域,t 表示时段。每一项的物理含义都不难理解:外购电力乘以电价;天然气消耗量乘以气价;设备出力量乘以运维系数;DR削减量和转移量乘以补偿单价;弃光量乘以惩罚系数。我之前见过一些人把目标函数写得特别复杂,堆了很多piecewise的项,其实大多数情况下没必要。简洁的目标函数更容易调试,也更方便从结果中分析成本构成。

2.2 设备与网络约束怎么写

目标函数定了之后,约束条件才是模型的大头。约束写不全或者方向写反,求解器直接就给你报 infeasible。我按照“区域内部守恒—设备自身限制—集群耦合限制”三层来组织约束,条理会清楚很多。

第一层是区域内部的电功率平衡约束。每个区域每一时刻,输入功率(购电、光伏、CHP发电、储能放电)必须等于输出功率(电负荷、电锅炉用电、储能充电),同时还要考虑DR削减和转移的影响:

P_buy_i(t) + P_pv_i(t) + P_chp_i(t) + P_ess_dis_i(t) = P_load_i(t) - L_cut_i(t) - L_shift_out_i(t) + L_shift_in_i(t) + P_eb_i(t) + P_ess_ch_i(t)

注意等式左右两边的每一项方向都要仔细核对。这个约束是区域内部电量的“账本”,任何一项写反,后面所有设备的出力都会跟着错。热功率平衡同理:

H_chp_i(t) + H_eb_i(t) = H_load_i(t) - H_cut_i(t)

H_cut是热负荷削减量,是联合DR模型里新增的关键变量。

第二层是设备自身约束。燃气轮机出力有上下限,还有爬坡约束——上一个时段和这一个时段的出力差不能超过某个速率,否则机组物理上爬不上去;电锅炉出力有上下限;光伏出力有预测上限,实际出力不能超过预测值,超出部分就是弃光量。储能电池是建模重点,它的核心约束是SOC(荷电状态)递推公式:

SOC_i(t+1) = SOC_i(t) + η_ch * P_ess_ch_i(t) - P_ess_dis_i(t) / η_dis

SOC要在上下限范围内,且不能同时充电和放电——这就是前面说的需要引入0-1变量的地方。用Big-M法处理互斥约束,写出来就是:

u_ch_i(t) + u_dis_i(t) <= 1

P_ess_ch_i(t) <= u_ch_i(t) * M

P_ess_dis_i(t) <= u_dis_i(t) * M

M取一个足够大的数,通常比该设备容量的上限稍大即可,不要取得过大,否则数值稳定性和求解速度都会受影响。

第三层是集群耦合约束。区域之间的联络线有传输功率上限,这决定了集群协同的“力度”:

P_ex_i(t) <= P_line_max_i

区域内联络线交换的功率加总起来,还要满足集群层面的交换平衡。如果是简单的辐射状网络,一般设定所有区域只在同一母线上交换电力,那 Σ P_ex_i(t) = 0 即可。意思是有区域送电出去,就有区域收电进来。

2.3 需求侧响应的建模细节

这个模型的特别之处就是“联合DR”,需要专门把DR约束写清楚。电力侧,我建模了可削减负荷和可平移负荷两类。可削减负荷每个时段削减量有上限,而且整个调度周期内削减总量不能超过一定比例,否则用户受不了:

0 <= L_cut_i(t) <= L_cut_max_i * P_load_i(t)

Σ_t L_cut_i(t) <= α_i * Σ_t P_load_i(t)

α一般取5%~20%,具体看场景。可平移负荷则需要约束“转出去的量”和“转进来的量”在总量上守恒:

Σ_t L_shift_out_i(t) = Σ_t L_shift_in_i(t)

你这样想:用户把某个时段的用电设备挪到了另一个时段,总用电量没有变,只是时间分布变了。如果不加这条守恒约束,模型会以最低成本疯狂削减负荷,DR补偿成本也会失真。

热力侧,直接对热负荷削减量做约束:

0 <= H_cut_i(t) <= H_cut_max_i * H_load_i(t)

Σ_t H_cut_i(t) <= β_i * Σ_t H_load_i(t)

β的取值通常比α要大一些,因为热负荷的柔性稍微强一点,比如供暖温度微调在短时间内用户感知不明显。需要注意的是,热负荷削减之后,CHP的产热和电锅炉的出力必须同步调整,所以热功率平衡约束里一定要把这部分“被减掉的热需求”减掉,否则就会出现“一边削减热负荷,一边还在满负荷供热”这种物理上不可能的结果。

我在做这个模型时踩过一个坑:最开始只加了热负荷削减量的上下限,忘了加总量比例约束,结果求解器给出的最优解是在所有时段把热负荷削减到零,DR补偿成本极低,整体成本“虚低”。后来加上总量比例约束之后,结果才回到物理合理的范围。这类DR模型如果不加比例约束,基本都会出现这种极端化问题,建议从一开始就把约束写全。

3. Matlab代码实现:从变量定义到求解

3.1 环境配置与数据结构设计

代码层面,我的实现环境是Matlab + Yalmip工具箱 +商业求解器。Yalmip的安装不复杂,把工具箱路径加到Matlab里就能用。求解器方面,Gurobi、CPLEX这类商业求解器的MILP求解性能远好于Matlab自带的intlinprog,数据规模稍大一点差距就很明显。

模型里我把时间范围设置为T=24(一天24个时段),区域数量N=3。用矩阵而不是循环来组织变量,是让代码清晰的关键。把所有区域的变量都定义成矩阵形式,行是区域编号,列是时段编号。

%% 参数定义 T = 24; % 时段数 N = 3; % 区域数量 P_load = rand(N, T) * 100; % 电负荷,单位MW,这里是示例数据 H_load = rand(N, T) * 50; % 热负荷,单位MWth P_pv_forecast = rand(N, T) * 40; % 光伏预测出力 % Yalmip变量定义 P_buy = sdpvar(N, T, 'full'); P_chp = sdpvar(N, T, 'full'); P_eb = sdpvar(N, T, 'full'); P_ess_ch = sdpvar(N, T, 'full'); P_ess_dis = sdpvar(N, T, 'full'); SOC = sdpvar(N, T, 'full'); L_cut = sdpvar(N, T, 'full'); L_shift_out = sdpvar(N, T, 'full'); L_shift_in = sdpvar(N, T, 'full'); H_cut = sdpvar(N, T, 'full'); P_pv = sdpvar(N, T, 'full'); % 光伏实际出力 P_ex = sdpvar(N, T, 'full'); % 从集群网络注入该区域的电量 u_ch = binvar(N, T, 'full'); % 储能充电状态 u_dis = binvar(N, T, 'full'); % 储能放电状态

定义变量的原则是:凡是模型里需要的决策量,都要在变量定义阶段想清楚,后面约束构建时缺一个变量就得回头改一大片。建议在模型里逐个对照:目标函数里出现的量,是否都有对应变量;约束里出现的量,是否都在变量列表里。

3.2 约束构建的技巧

Yalmip里约束构建的写法比较灵活,可以直接在循环里累加约束,也可以用向量化的方式批量构造。我个人习惯先用循环构建“设备内部约束”,再处理跨设备、跨区域的耦合约束,这样排查问题时定位更快。

Constraints = []; %% 区域内部约束 for i = 1:N for t = 1:T % 电功率平衡 Constraints = [Constraints, P_buy(i,t) + P_pv(i,t) + P_chp(i,t) + ... P_ess_dis(i,t) == P_load(i,t) - L_cut(i,t) - ... L_shift_out(i,t) + L_shift_in(i,t) + P_eb(i,t) + P_ess_ch(i,t)]; % 热功率平衡 Constraints = [Constraints, H_chp(i,t) + H_eb(i,t) == ... H_load(i,t) - H_cut(i,t)]; % 储能SOC递推 if t < T Constraints = [Constraints, SOC(i,t+1) == SOC(i,t) + ... eta_ch * P_ess_ch(i,t) - P_ess_dis(i,t) / eta_dis]; end % 储能SC约束 Constraints = [Constraints, SOC(i,t) >= SOC_min, SOC(i,t) <= SOC_max]; Constraints = [Constraints, u_ch(i,t) + u_dis(i,t) <= 1]; Constraints = [Constraints, P_ess_ch(i,t) <= u_ch(i,t) * M]; Constraints = [Constraints, P_ess_dis(i,t) <= u_dis(i,t) * M]; end % 平移负荷总量守恒 Constraints = [Constraints, sum(L_shift_out(i,:)) == sum(L_shift_in(i,:))]; % 削减总量限制 Constraints = [Constraints, sum(L_cut(i,:)) <= alpha * sum(P_load(i,:))]; Constraints = [Constraints, sum(H_cut(i,:)) <= beta * sum(H_load(i,:))]; end %% 集群耦合约束 Constraints = [Constraints, P_ex >= -P_line_max, P_ex <= P_line_max]; Constraints = [Constraints, sum(P_ex(:)) == 0]; % 集群交换功率平衡

循环写起来慢,但胜在可读性好。如果你习惯向量化思维,也可以把大部分电力平衡约束用矩阵形式一次性写入,但调试的时候不如循环直观。对于N=3、T=24这种规模,循环构造约束的耗时完全可以接受,没必要为了那几毫秒牺牲可调试性。

3.3 求解器调用与结果处理

约束和目标函数都组装好之后,调用求解就是一行代码的事:

%% 目标函数 c_buy = 0.5; c_gas = 0.35; c_om = 0.02; c_dr = 0.3; c_curtail = 0.8; objective = sum(sum(c_buy * P_buy + c_gas * G_chp + ... c_om * (P_chp + P_eb + P_ess_ch + P_ess_dis) + ... c_dr * (L_cut + L_shift_out) + c_curtail * (P_pv_forecast - P_pv))); %% 求解 options = sdpsettings('solver', 'gurobi', 'verbose', 1, ... 'gurobi.MIPGap', 0.001); % 设置MIP gap为千分之一 diagnostics = optimize(Constraints, objective, options); %% 结果输出 if diagnostics.problem == 0 P_buy_opt = value(P_buy); P_chp_opt = value(P_chp); SOC_opt = value(SOC); else disp('求解失败'); disp(diagnostics.info); end

注意几个细节。sdpsettings里的solver要严格匹配你机器上安装并配置好的求解器。MIPGap设成0.001意味着求解器允许最优解和目标值之间存在0.1%的差距,对于工程问题足够精确,同时能明显缩短求解时间。如果你试着把它设成0,求解器可能要花很久去验证精确最优性,实际收益微乎其微。

结果输出阶段,我建议把Gurobi返回的诊断信息保存下来,方便事后分析。value()函数是Yalmip获取变量数值的标准方式,千万不要直接用变量本身去参与数值计算——那些变量在求解完成之前本质上是符号对象,直接参与数值运算大概率会报维数错误或者给出莫名其妙的输出。

有了这些基础数据之后,后续可以做敏感性分析、成本构成分析,也可以画出各区域负荷曲线与设备出力的对比图。我个人习惯在调试阶段先看总成本是否合理,再看每类约束的最大残差,最后才看具体某个设备的调度曲线。这个顺序能帮你最快定位模型的问题。

4. 常见报错与调试经验实录

4.1 模型不可行怎么办

我敢说所有做优化模型的人,遇到最多的就是infeasible。这个问题有两个常见来源。

第一个是约束写错了方向。尤其是等式约束里正负号放错,基本必现infeasible。比如电功率平衡里,储能充电应该是“消耗功率”,写在等式右边;放电是“供给功率”,写在等式左边。如果符号写反,求解器要找一组让系统凭空多出能量的变量值,自然找不到。

排查建议:把约束一条条注释掉,看注释哪一条之后模型变得可行了,问题大概率就出在那条约束上。也可以在optimize之后查看diagnostics信息,Yalmip有时候会给出可疑约束的提示。还有一个小技巧:对连续变量类型的约束,先跑一遍LP松弛(把整数变量全部转为连续变量),如果LP都不可行,说明连续性约束本身就写错了;如果LP可行但MILP不可行,那问题才出在整数约束上。

第二个常见来源是数据量纲不一致。我第一版代码里电负荷数据单位是kW,光伏预测数据是MW,结果功率平衡怎么调都不对。把所有数据统一到同一单位(比如MW),是最基础的检查项,但很多人会忽略。

4.2 求解时间过长怎么优化

模型变多区域之后,变量规模会线性膨胀。3个区域24个时段,储能相关的0-1变量就有3242=144个,MILP求解时间通常还能接受。但如果区域数量增加到5个以上,或者时段粒度变成15分钟(T=96),求解时间可能从几十秒飙到几十分钟甚至更久。

我的经验是优先调整MIPGap。把gap从0.0001放宽到0.001,求解时间通常会缩短30%~60%,而目标值的偏差几乎可以忽略。要是还慢,就检查Big-M参数,M值取刚好大于设备上限的数即可,不要图省事统一设成1e6,过大的M会严重劣化MILP的下界质量,整数分支剪枝效率变得极低。

还有一点容易被忽略:储能SOC递推约束是跨时段的,这意味着变量之间的耦合范围是全天,求解器处理起来压力大。如果场景允许,可以试试减少SOC的离散精度,或者对储能使用按时间段分段的方式建模,但这类改动会改变模型语义,要谨慎。

4.3 怎么判断结果是物理合理的

求解器给你一个“最优解”,不代表这个解物理上合理。我见过好几次,模型求解成功,成本看着也不错,但画出来的SOC曲线一片混乱——一会儿满电一会儿见底,完全没有规律的充放循环。这种结果多半说明模型有隐含的怪约束,或者目标函数里的惩罚项设置不对。

判断结果是否合理,我基本看四个指标。第一,所有变量是否都在各自上下限范围内,这个用min和max扫一遍即可发现。第二,SOC曲线是否平滑、是否首尾一致。现实中储能一天的初始和结束SOC通常要求一致,否则下一天调度没法衔接。如果你建模时忘了加这个约束,求解器可能让SOC在最后时段直接跳到0来省成本。第三,DR削减量是否出现“全削减”或者“全不削减”这种模式,如果是,多半是约束缺失。第四,区域间的交换功率曲线是否符合预期,比如光伏多的区域应该在白天向外送电,而不是反向购电,如果出现了反直觉的潮流方向,那检查一下电价参数或者联络线约束的方向。

这些检查代码写起来都不难,关键是养成习惯,不要求解完看个总成本就觉得万事大吉。优化模型的输出是决策建议,如果底层逻辑有误,建议本身就没有意义。

4.4 常见问题速查表

现象可能原因排查与解决思路
模型报infeasible等式约束方向错误、数据量纲不统一逐条注释约束定位;检查所有数据的单位量纲
求解时间过长MIPGap太小、Big-M参数过大放宽MIPGap到0.001;将M设到设备上限的1.1倍
SOC曲线跳变剧烈缺少SOC初值或首尾一致性约束补充SOC初始值约束,必要时加首尾相等约束
DR削减量极端化缺少总量比例约束添加削减总量不超过负荷一定比例的约束
光伏出力低于预测值很多弃光惩罚项系数过低提高c_curtail,让消纳可再生能源在经济上更划算
求解器提示无穷解目标函数里漏了某项或者某变量没有进目标函数检查所有变量是否都出现在目标函数或约束中

5. 一点实践经验总结

最后分享一个很可能有用的小建议:做这类多区域集群优化模型,从最简单的案例开始迭代。不要一上来就3个区域加电热联合DR加储能加联络线约束,那只会给自己增加排查问题的难度。我实际的做法是先搭一个单区域、纯电负荷、没有DR的简化模型,跑通之后逐步增加热力耦合,再加DR,最后扩展成多区域集群。每个阶段跑通了,再进入下一步,出问题时很清楚是新加的哪个模块导致的。

如果你正在做类似的模型,希望这篇拆解能帮你少走一些弯路。尤其是DR约束这块,建模时先想清楚“削减量会不会被求解器恶意拉满”这类问题,提前把比例约束加上,能省不少调试时间。模型本身不是什么玄学,把物理过程翻译成数学约束,再用Matlab规范地实现出来,剩下的就是细心和耐心。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询