☰
碳交易与需求响应下综合能源系统优化:MILP建模与MATLAB实现
2026/10/7 11:25:12 网站建设 项目流程

做综合能源系统优化的人,最近应该都在关注碳交易和需求响应这两个方向。我手上的这个项目,正好就是“MATLAB代码:碳交易机制下考虑需求响应的综合能源系统优化运行”,里面还特别提到柔性负荷。所以这篇文章我打算把它掰开揉碎,讲讲这个项目的数学模型怎么搭、MATLAB代码怎么组织、哪些地方容易踩坑,以及我实测下来的一些心得。如果你也在做类似方向,或者正准备用MATLAB跑综合能源优化,这篇应该能帮你省不少时间。

这个项目的核心不是单纯做能源调度,而是把“碳排放”变成了一个可以量化的成本,再叠加用户侧的柔性调节能力,让系统在满足电、热、冷负荷的前提下,整体运行成本最低。听起来不复杂,但真正建模的时候,涉及能量平衡、设备出力限制、储能时序约束、需求响应约束、碳配额交易等一系列环节,最后变成一个混合整数线性规划(MILP)问题。这类问题在MATLAB里用YALMIP+Gurobi组合来解,是比较成熟的做法。

1. 项目要解决的实际问题与整体思路

1.1 综合能源系统里到底在优化什么

综合能源系统(Integrated Energy System)的典型特征是“多能互补、协同优化”。常见设备包括光伏、风电、燃气轮机(CHP)、燃气锅炉、电锅炉、电储能、热储能,以及外购电和外购气。这些设备把电、气、热、冷几种能量耦合在一起,所以优化变量不只是某个设备的功率,而是一个时间序列上的设备组合策略。

整个优化问题的目标是,在一个调度周期内(通常取24小时),在满足各类负荷需求的前提下,让系统的总运行成本最小。总成本包括向电网购电的费用、向气网购气的费用、各设备运行维护费用,以及碳交易成本。如果系统还有可调节负荷,比如电动汽车充电桩、空调、蓄热式电锅炉,那就还要加入需求响应带来的负荷平移或削减,这时候目标函数里还会体现需求响应收益或成本。

我一开始接触这个题目的时候,容易陷入一个误区:以为只要把设备模型写出来,用优化算法不停地搜索就行。实际上,综合能源优化最有挑战的部分是“约束”,而不是“目标”。设备之间的耦合关系、储能的前后时段连接、柔性负荷的转移量都必须精确表达,否则求解出来的结果要么不可行,要么不符合物理常识。

1.2 碳交易机制如何进入优化模型

碳交易机制的核心是:给排放主体分配一个碳排放配额,实际排放量如果超过配额,就需要在碳市场购买碳排放权;如果低于配额,则可以把多余的配额出售。因此,碳成本不再是简单的“排放量乘以碳价”,而是一个和配额挂钩的分段函数。

放到综合能源系统里,碳排放主要来自两个方面:一是从电网购电对应的间接排放,二是燃气轮机和燃气锅炉消耗天然气产生的直接排放。有些系统可能还有碳捕集设备,或者电转气设备,这个先不展开,但思路是一样的。

在建模时,我们需要引入两个非负变量来表示“超出配额量”和“剩余配额量”,然后碳成本可以写成:

[ C_{carbon} = P_{CO2} \times E_{over} - P_{CO2} \times E_{spare} ]

同时满足:

[ E_{actual} - E_{quota} = E_{over} - E_{spare} ]

其中 (E_{over}) 和 (E_{spare}) 不能同时为正,但由于优化目标会自动使成本最小化,所以通过线性约束和第二者的非负性就能保证逻辑正确。这个处理方法是教科书里常见的线性化技巧,也是我在YALMIP里比较喜欢用的方式。

碳价的引入会直接影响设备出力决策。比如碳价较高时,系统倾向减少CHP机组出力,转而增加光伏、风电消纳或者提高储能放电,甚至从外部购买更清洁的电能。这种“看不见的手”被翻译成优化模型中的成本项,非常直观。

1.3 需求响应和柔性负荷在模型中扮演的角色

需求响应(Demand Response,简称DR)可以简单理解为用户根据市场价格或激励信号,主动调整用电行为。在综合能源系统里,柔性负荷是指那些在一定时间范围内可以灵活调节的负荷,它们不一定非要在某个固定时刻消费能量,而是可以在时间上转移,或者在一定限度内削减。

举几个典型例子:

  • 可转移负荷:比如洗衣机、洗碗机,可以在一天内任意时段运行,但总用电量固定;
  • 可削减负荷:比如空调,可以短暂降低功率,但舒适度不能无限牺牲;
  • 可平移负荷:比如蓄热电锅炉,可以把热量的产生时间从峰时段挪到谷时段,蓄热后再释放。

在优化模型中,对每种柔性负荷都需要单独建模。以可转移负荷为例,通常引入整数变量表示负荷在某个时段是否启动,同时限制一天内启动一次,并保持总能量需求不变。可削减负荷则简单一些,只要在允许的削减范围内,允许负荷低于原始预测值,但会带来一定的中断补偿成本,这个成本也要计入目标函数。

需求响应的加入,让优化问题从一个“纯供给侧调度”变成了“源-网-荷-储协同优化”。这也是近几年研究的热点,因为随着新能源渗透率提高,单一靠调节发电侧越来越困难,必须让负荷侧也跟随系统状态变化。

2. 数学建模:把物理问题翻译成求解器能懂的语言

2.1 目标函数的完整展开与单位统一

目标函数是模型的“中枢神经”。我这里给出一个比较典型的写法,具体可根据设备和成本类型调整:

[ \min ; C = C_{buy} + C_{gas} + C_{om} + C_{carbon} ]

其中:

  • (C_{buy} = \sum_{t} P_{grid}(t) \times price_{grid}(t)),购电费用,按分时电价计算;
  • (C_{gas} = \sum_{t} F_{gas}(t) \times price_{gas}),购气费用,气价一般固定;
  • (C_{om} = \sum_{t} \sum_{i} P_i(t) \times c_{om,i}),各设备运维成本;
  • (C_{carbon}) 就是上文提到的碳交易成本。

这里的单位必须统一。比如功率用kW,能量用kWh,电价用元/kWh,气价用元/m³,再通过设备效率把天然气耗量折算成kW或kWh。很多新手做MATLAB代码时容易在单位换算上出错,导致求解结果偏离实际。我的习惯是全部折算成功率和能量单位,也就是把天然气热值换算成电当量,同时记录转换后的系数。

需求响应成本如果存在,比如削减负荷的赔偿费用,也要加在目标函数里。但更多情况下,需求响应是通过优化负荷曲线“削减了购电成本”,相当于一种负成本,因此并不需要显式地在目标函数里加一项,只要负荷约束和费用计算是联动的,效果自然体现。

2.2 约束条件的分类写法与易错点

约束条件大致分四类:能量平衡约束、设备运行约束、储能约束、需求响应约束。下面一个一个说。

能量平衡约束是刚性的。比如电平衡:

[ P_{pv}(t) + P_{wind}(t) + P_{buy}(t) + P_{chp,e}(t) + P_{ess,dis}(t) = P_{load}(t) + P_{ess,ch}(t) + P_{elec_boiler}(t) ]

热平衡类似,把热源(CHP余热、燃气锅炉、电锅炉、热储能放热)放在左边,热负荷和热储能充热放在右边。要注意的是,如果模型里有冷负荷,还需要建立制冷设备模型,可能是吸收式制冷机或电制冷机,它们又进一步耦合了电和热。

设备运行约束包括上下限、爬坡约束、启停逻辑。对于CHP机组,出力范围不是固定的矩形,而是有一个“电-热可行域”。如果简化处理,可以用一个线性表达式表示电功率和热功率的关联,比如 (P_{chp,h}(t) = k \times P_{chp,e}(t)),k是热电比。但更精细的做法是引入二元变量表示机组的启停状态,再添加最小启停时间约束,这就把模型变成MILP了。

这部分在MATLAB里用YALMIP表达很顺手,比如用binvar定义启停状态,用约束条件把连续功率变量和启停状态关联起来。麻烦的是约束数量会增多,如果系统规模一大,求解时间指数级上升。所以实际项目中,通常会把「最小启停时间」这种强组合约束简化掉,或者只在关键机组上使用。

储能约束是另一大难点。储能电池的SOC(荷电状态)是一个跨时段的状态变量:

[ SOC(t+1) = SOC(t) + \eta_{ch} \cdot P_{ch}(t) \cdot \Delta t / E_{cap} - P_{dis}(t) \cdot \Delta t / (E_{cap} \cdot \eta_{dis}) ]

同时要加充放电功率限制、SOC上下限、为了简化模型也可以规定一天结束时SOC回到初始值。这里有一个常见翻车点:充放电同时为正。虽然优化目标会尽量规避,但模型里最好加一个约束 (P_{ch}(t) \cdot P_{dis}(t) \le 0),更稳妥的办法是引入二元变量,强制要么充电要么放电。因为这个约束在连续变量下是非凸的,直接加会破坏线性结构,所以很多代码里干脆让目标函数足够“惩罚”同时充放电,或者用大M法线性化。

需求响应约束要特别关注柔性负荷的时间衔接。比如可平移负荷,假设它需要连续运行两个小时,总功率为 (P_{shift}),那么要引入二元变量 (u(t)) 表示是否在t时刻启动。约束可以写为:所有时段启动变量的和为1,并且在启动时段后面两个小时内负荷功率等于 (P_{shift})。这个约束在YALMIP里实现不难,但对初学者来说,很容易忘记保持“连续性”,导致负荷被拆得支离破碎。

2.3 碳交易约束的线性化处理

前面提到过,碳交易成本可以用两个正变量 (E_{over}) 和 (E_{spare}) 来线性表示。实际编程时,还有一个细节:实际碳排放总量要写成每个环节排放量的和。不同能源的排放因子不同,比如购电的排放因子取决于电网平均碳排放强度,燃气的排放因子取决于CO₂排放系数和热值。可以把这些因子整理成一个常量矩阵,然后和变量相乘。

配额计算有两种常见方式:一是根据历史排放量,二是按基准线法。在综合能源优化中,多用基准线法,比如给单位供电量和供热量一个碳配额系数,乘以对应的输出能量得到总配额。这样做的原因是把配额和系统产出绑定,避免因负荷波动导致配额不合理。在实现时,我建议把“配额计算”单独写一个函数,方便以后调整政策参数。

3. MATLAB实现:从零搭建一套可复现的代码框架

3.1 求解器选择与环境配置

MATLAB里做优化,建模层我无脑推荐YALMIP。它只是一个建模语言层,不负责具体求解,需要搭配一个求解器。常用组合是:

  • YALMIP + Gurobi(商用求解器,教育版免费,速度快)
  • YALMIP + Cplex(同样经典,但IBM Cplex现在的安装不如Gurobi方便)
  • YALMIP + intlinprog(MATLAB自带的混合整数线性规划求解器,免费,小规模够用)

如果模型规模不大,变压器节点不超过几十个,intlinprog也能跑通。但如果你想做多场景分析、热灵敏测试,建议直接用Gurobi。因为我实际测下来,同一个小案例,intlinprog可能需要几秒钟,Gurobi可能不到0.1秒就给出最优解,差距在迭代节点策略上非常明显。

安装步骤不细讲,提醒一点:YALMIP加入路径后,每次MATLAB重启都需要重新加,最好写到startup.m文件里,或者用savepath保存路径。我因为重装MATLAB忘记这步,白着急了半小时。

3.2 代码文件结构规划

好代码的秘诀不是代码本身,而是文件组织。我推荐这样拆分:

  • main.m:主脚本,设置时间步长、调度周期、场景参数,调用各个模块;
  • data_load.m:定义所有设备参数、负荷曲线、电价、气价、新能源预测值、碳价等;
  • build_problem.m:核心建模函数,输入数据,输出YALMIP优化模型;
  • solve_problem.m:调用求解器,并返回结果状态、目标值、变量取值;
  • plot_results.m:画图,如各设备出力曲线、SOC曲线、成本柱状图、碳交易量对比图。

这样做的最大好处是可调试性强。比如发现求解不可行,你可以先检查build_problem里的约束,而不是在几百行脚本里找。另外,数据驱动模型和代码分离,方便做参数敏感性分析时只改动数据文件。

3.3 核心建模代码片段解析

下面给一段简化版的YALMIP建模核心代码,方便理解结构(注意:这是示意代码,实际项目要更具你的系统扩充)。

%% 定义变量 % 电功率变量:光伏、风电、购电、CHP电出力、储能充电、储能放电、电负荷 P = sdpvar(1, N, 'full'); % 示例:一个变量 % 实际中根据设备数量逐个定义,或者用sdpvar(repelem(dim), N) %% 定义二进制变量 on_off_chp = binvar(1, N); % CHP启停状态 u_shift = binvar(1, N); % 可平移负荷启动标志 %% 约束 Constraints = []; % 电平衡约束 Constraints = [Constraints, P_pv + P_wind + P_buy + P_chp_e + P_dis - P_ch == P_load]; % 可平移负荷连续性约束 % 假设启动后连续运行2小时 for t = 1:N-1 Constraints = [Constraints, u_shift(t) + u_shift(t+1) == 1]; % 简化示例 end %% 目标函数 Objective = sum(P_buy .* price_e) + sum(F_gas .* price_gas) + sum(om_mat .* P_devices); Objective = Objective + carbon_cost; %% 求解 options = sdpsettings('solver','gurobi'); optimize(Constraints, Objective, options);

说实话,这段代码省略了大量细节,但你可以看到核心逻辑是:用sdpvar定义连续变量,用binvar定义0-1变量,然后用Constraints向量累积约束,最后把目标函数交给求解器。逻辑清晰,也很容易用MATLAB自带的调试工具断点检查。

3.4 数据准备与时间尺度处理

我做24小时调度时,通常把时间步长设为1小时,一天24个点。如果你研究的是分钟级响应,N可能变成96个点(15分钟间隔),但那样求解规模会大幅增加。我的建议是:刚开始先用1小时尺度跑通逻辑,后面再做精细化。

数据准备阶段,比较头疼的是“真实负荷曲线”和“新能源出力曲线”的获取。如果没有实测数据,可以用典型日负荷曲线乘以峰值负荷系数,或者用历史均值加噪声。下面的示例参数是典型日设置:

时段电负荷/kW热负荷/kW光伏出力/kW风电出力/kW电价/(元/kWh)
1120080001800.32
2100075001600.28
..................

把这些数据保存成一个Data.xlsx,在data_load.m里用readmatrix读取。一个容易被忽视的问题:变量和数据的维度要一致。如果你在模型里定义了N=24,那所有负荷、电价、新能源出力都必须是1×24的向量。猎奇地把矢量转成列向量,经常导致矩阵维度不匹配报错。

4. 结果分析:用数据说话才有说服力

4.1 优化结果如何展示

做完优化,不能只看目标函数值,还要系统性地检查输出变量。我自己的习惯是画三张图:

  1. 电力平衡图:横轴时间,堆叠各电源(光伏、风电、购电、CHP、储能放电),上方叠加负荷曲线,直观看出每个时段谁在供电;
  2. 成本构成柱状图:把购电成本、购气成本、运维成本、碳交易成本分别画出来,看谁的占比最大;
  3. 碳交易量图:展示每个时段的实际排放、配额和净购买量,配合碳价可以算出总碳成本。

图示的好处是能快速发现“反常识”现象。比如我看到过因为碳价太高,系统宁愿在电价高峰时段减少购电,让CHP带更多电出力,结果燃气成本上升了,总成本反而变高。这种结果如果不看图,光看目标函数值很难定位问题。

4.2 碳价和需求响应的灵敏度测试

一个完整的项目不能只跑一组参数就交差。通常要对关键参数做敏感性分析,比如碳价从50元/吨逐步升到200元/吨,观察碳排放总量和总成本的变化。我的经验是:

  • 碳价提高,系统碳排放量下降,但下降速率不是线性的;
  • 当碳价超过某个阈值后,再继续涨价,对减排的促进作用边际递减;
  • 需求响应容量的增加能够显著降低系统购电峰值,同时减少高价时段购电,从而降低总成本。

因此,项目里最好做成两个对比算例:算例1是不考虑碳交易(碳价为0)和需求响应;算例2是完整版。对比结果能直观说明引入这两个机制的系统效益,也是论文和报告里最需要的核心图片。

4.3 抓住“成本”与“排放”的双重矛盾

优化目标如果是“最小总成本”,那么系统不一定选择碳排放最少的方案,而是找“经济上最优”的均衡点。比如天然气便宜但碳排放高,如果碳价较低,系统可能会增加CHP出力;碳价高时,则更愿意用光伏和储能。

在撰写结论时,一定要分清“成本最优”和“排放最优”并不是一回事。如果项目标题里强调“碳交易机制”,那么你的解读就不应该只停留在成本维度,而应该同时报告总减排量、单位减排成本等指标,这样内容深度才够。

5. 常见坑点与调试心得

5.1 求解器报“Infeasible problem”的排查方法

这是新手最容易碰到的错误。出现不可行,说明你的约束之间互相矛盾。常见原因有:

  • 负荷峰值大于所有电源和购电上限之和,导致电平衡无法满足;
  • 储能初始SOC和最终SOC约束太严格,比如要求一天结束时SOC必须等于初值,而充放电损耗又导致不可能;
  • 可平移负荷连续运行时长约束与负荷的总电量约束冲突;
  • 变量维度错误,导致约束拼接到一起时矩阵维度不一致,但YALMIP有时会把这种错误误报为不可行。

我的排查套路是:先把所有约束一件一件注释掉,然后用optimize试探,找到哪个约束导致不可行。更粗暴但有效的方法是把约束中与储能、可平移负荷相关的约束暂时放宽,比如去掉SOC上下限、去掉启动次数限制,看模型是否能解。如果放宽后能解,再逐步加严,定位问题点。

5.2 二进制变量过多导致求解极慢

MILP问题的计算复杂度很大程度上取决于整数变量个数。每一台机组每个时段的启停状态是1个二进制变量,如果系统有5台机组、24个时段,就是120个整数变量;再加上可平移负荷的启动判定、储能充放状态、碳交易的切换逻辑,整数变量很容易超过300个,计算时间会变得难以接受。

对策有几个:

  • 去掉不明显影响结果的整数变量,比如低容量机组不做启停约束;
  • 对储能充放电状态,尝试用先进后用的线性松弛,不一定非要二元变量;
  • 设置求解器的MIP gap,比如options.gurobi.MIPGap = 0.01,允许有1%的误差,能显著缩短时间;
  • 把对称的机组做合并,减少变量数量。

5.3 使用YALMIP时的诊断工具

YALMIP有个很好用的命令diagnostics和check函数。在求解前,建议用diagnostics = optimize(...)获取求解状态,如果是非零状态,再用check(Constraints)检查哪个约束违反程度最大。

diagnostics = optimize(Constraints, Objective, options); if diagnostics.problem ~= 0 disp('模型问题:'); diagnostics.info end

我的习惯是每构建完一个模块就做一次小规模求解验证,而不是等整个模型搭好后再一次性求解。这样可以把错误控制在小范围内,尤其是能量平衡约束这种基础约束,出错后修复成本很高。

5.4 代码扩展性的一点建议

你现在做的是电热联供,但如果后面要把系统扩展到包含氢能、碳捕集、冷热电三联供,建议在建模时把设备参数、运行约束和成本计算都封装成struct,然后用循环统一处理设备。比如:

Devices.chp = struct('capacity', 800, 'eff_e', 0.35, 'eff_h', 0.45, 'om', 0.02, 'carbon', 0.8); Devices.boiler = struct('capacity', 1000, 'eff_h', 0.9, 'om', 0.015, 'carbon', 0.9);

这样增删设备只需要修改结构体,而不用改模型主体代码。真实项目中,这个方法救了我很多次,因为模型有几十个参数要调,一个Script面面俱到几乎不可能。

6. 我的一些实操体会

这个项目跑下来,我的第一个感觉是:建模容易,调通难。最容易卡住的是储能约束和可平移负荷约束,因为它们牵涉到跨时段状态,一个细节没写对,结果就会变得很奇怪,比如SOC曲线剧烈振荡、负荷被拆成碎片等。后来我把约束的合理性检查步骤前置,才把这类问题彻底解决。

另外一个体会就是,不要迷信复杂的求解器或更高阶的算法。像这类中等规模的能源调度问题,MILP已经是非常成熟的建模体系,YALMIP加Gurobi已经足够轻松应对。先把问题描述清楚、约束整理完整,远比你花时间研究启发式算法要靠谱。

最后留一个小技巧:每次调参跑完,我都会用savemat把变量存成.mat文件,再用一段独立的脚本去绘图和分析,避免每次都要重新求解。如果你要做多场景对比,这个习惯能让你省下数小时的重复计算时间。等所有结果都跑完,再回到主脚本统一出图,做报告或写论文的素材自然就齐了。

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

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

立即咨询