碳交易与需求响应下综合能源系统优化:MILP建模与MATLAB实现

发布时间:2026/10/7 11:25:15
碳交易与需求响应下综合能源系统优化:MILP建模与MATLAB实现 做综合能源系统优化的人最近应该都在关注碳交易和需求响应这两个方向。我手上的这个项目正好就是“MATLAB代码碳交易机制下考虑需求响应的综合能源系统优化运行”里面还特别提到柔性负荷。所以这篇文章我打算把它掰开揉碎讲讲这个项目的数学模型怎么搭、MATLAB代码怎么组织、哪些地方容易踩坑以及我实测下来的一些心得。如果你也在做类似方向或者正准备用MATLAB跑综合能源优化这篇应该能帮你省不少时间。这个项目的核心不是单纯做能源调度而是把“碳排放”变成了一个可以量化的成本再叠加用户侧的柔性调节能力让系统在满足电、热、冷负荷的前提下整体运行成本最低。听起来不复杂但真正建模的时候涉及能量平衡、设备出力限制、储能时序约束、需求响应约束、碳配额交易等一系列环节最后变成一个混合整数线性规划MILP问题。这类问题在MATLAB里用YALMIPGurobi组合来解是比较成熟的做法。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(t1) 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 intlinprogMATLAB自带的混合整数线性规划求解器免费小规模够用如果模型规模不大变压器节点不超过几十个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(t1) 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.322100075001600.28..................把这些数据保存成一个Data.xlsx在data_load.m里用readmatrix读取。一个容易被忽视的问题变量和数据的维度要一致。如果你在模型里定义了N24那所有负荷、电价、新能源出力都必须是1×24的向量。猎奇地把矢量转成列向量经常导致矩阵维度不匹配报错。4. 结果分析用数据说话才有说服力4.1 优化结果如何展示做完优化不能只看目标函数值还要系统性地检查输出变量。我自己的习惯是画三张图电力平衡图横轴时间堆叠各电源光伏、风电、购电、CHP、储能放电上方叠加负荷曲线直观看出每个时段谁在供电成本构成柱状图把购电成本、购气成本、运维成本、碳交易成本分别画出来看谁的占比最大碳交易量图展示每个时段的实际排放、配额和净购买量配合碳价可以算出总碳成本。图示的好处是能快速发现“反常识”现象。比如我看到过因为碳价太高系统宁愿在电价高峰时段减少购电让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文件再用一段独立的脚本去绘图和分析避免每次都要重新求解。如果你要做多场景对比这个习惯能让你省下数小时的重复计算时间。等所有结果都跑完再回到主脚本统一出图做报告或写论文的素材自然就齐了。