1. 这个课题到底在做什么
先说清楚一件事:这个标题看起来很专业,但把它拆开看,其实就是一个“怎么让一个园区/微网里的各种能源设备协同工作,既省钱又减碳”的优化问题。我当初决定复现这个课题,是因为它在综合能源系统这个方向上综合了三个很热门的点:综合需求响应、阶梯型碳交易、优化调度。三个点任何一个拿出来都能写一篇论文,合在一起能做的文章更多。
这个项目解决的实际问题是什么?传统优化调度只考虑“怎么让设备运行最省钱”,但实际场景中还有两个绕不开的现实:一是用户的负荷并不是一成不变的,它能被价格信号或者激励手段引导着改变,这就是需求响应;二是碳排放已经开始有成本了,而且排放越多单位成本越高,这就是阶梯型碳交易。把这两个机制放进优化模型里,调度策略就会发生质变——它不再只是“省钱”,而是“在省钱和减碳之间找到最优平衡”。
适合谁来复现?首先是做综合能源、微电网方向研究的在校学生,尤其是需要写论文但缺少算例验证的群体,这个课题可以当作论文的核心模型支撑;其次是刚入行做能源系统优化工程师的人,可以通过这个项目理解能源调度建模的完整流程;最后是那些在看“新型电力系统”“双碳”类项目但不知道怎么落地的人,这个项目提供了一套可运行的代码逻辑,比只看概念强一百倍。
我当时花了两周多时间完整复现下来,踩了不少坑。这篇文章把整个复现过程、模型细节、代码框架、坑点和验证结果全部写出来,希望能帮你省掉我当初走的弯路。
2. 整体设计思路与关键技术拆解
2.1 为什么选MATLAB来做这件事
综合能源系统优化调度,本质上是一个带约束的混合整数线性规划(MILP)问题。可选工具其实不少:Python有PuLP和Pyomo,商业软件有GAMS和Lingo。但我最后选了MATLAB,原因很现实:
第一,YALMIP工具箱对MILP问题的建模支持非常成熟。YALMIP是MATLAB环境下的一个建模层,你只要用sdpvar声明决策变量、用constraints定义约束、用optimize调用求解器就行,不用自己去处理矩阵维度匹配这种底层细节。相比之下,Pyomo虽然也不错,但国内很多学生和工程师对MATLAB更熟悉,接手和修改模型更方便。
第二,世面上这个方向的参考代码大部分都是MATLAB写的。你搜“综合能源 优化调度 代码”,十个里有八个是.m文件。用MATLAB复现,参考最多、讨论环境最好。
第三,后处理太方便了。24小时调度结果画出来,功率平衡图、电价曲线叠加图、储能SOC曲线图,MATLAB的绘图能力明显比Python的matplotlib顺手,尤其是对不常写Python的人来说。
2.2 模型架构:三个核心模块怎么咬合在一起
整个系统的逻辑可以这么理解:一个典型综合能源系统里,有电、气、热三种能量载体。燃气轮机(CHP)吃天然气发电,发电的同时产热;燃气锅炉也能产热;光伏只管发电;电储能能充能放。用户侧有电负荷、热负荷、冷负荷(冷负荷用吸收式制冷机从热转换来)。
在这个物理架构上,我又叠加了两个“市场机制”模块:
- 综合需求响应(IDR):用户不是静态的负荷,而是可以根据价格信号调整用电行为。比如电价高的时候少用一点电,把用电时段挪到电价低的时候。这个机制通过价格弹性矩阵来量化用户的响应行为。
- 阶梯型碳交易(TCT):系统每天会获得一个免费碳排放配额,实际排放量如果超过配额,就需要购买碳配额,而且超过越多价格越贵——这就是“阶梯”的含义。这个机制让碳排放变成了一个边际成本递增的优化因子。
两个模块咬合的方式是:需求响应改变了各时段的负荷曲线,碳交易改变了系统的运行成本结构,两者共同输入到优化调度模型中,最终输出“每个时段每台设备该发多少电、产多少热、储能充放多少”的最优策略。
2.3 这套策略好在哪里:三大优势
- 经济性提升:通过需求响应削峰填谷,降低高价时段购电量和机组出力,直接减少运行成本。
- 低碳性改善:碳交易成本纳入目标函数后,系统会自发倾向于多使用光伏、减少燃气轮机出力等高碳环节,碳排放总量明显下降。
- 机制协同增益:IDR和TCT单独用都有效果,但合在一起时会产生叠加效应——需求响应降低了高电价时段的净负荷,高电价往往伴随高碳因子时段,因此同时降低了碳排和成本。
3. 核心模型拆解:每一步为什么这么建
3.1 目标函数:四个成本项一个都不能少
优化调度的目标函数,我设置的是系统综合运行成本最小。注意,这里的“综合”意味着不是只算购电购气费用,而是把碳交易成本、需求响应补偿成本、设备运维成本全部纳入。目标函数长这样:
min C = C_buy + C_operation + C_DR + C_carbon其中:
- C_buy(购能成本):从电网买的电费,加上从气网买的天然气费。电价按分时电价设定(峰、平、谷),天然气按固定单价。
- C_operation(运维成本):各设备单位出力乘以运维系数,比如光伏运维成本很低,燃气轮机运维成本相对较高。
- C_DR(需求响应补偿成本):激励型需求响应中,用户被削减的负荷要给补偿,补偿单价一般高于电价,低于缺电损失。
- C_carbon(碳交易成本):实际碳排放量超过免费配额的部分,按阶梯价格购买碳配额。这部分在传统调度模型里经常被忽略,但一旦加入,结果变化非常明显。
一个关键心得:很多人建目标函数时会把碳交易成本做成一个简单的线性项“排放量×碳价”。但阶梯型碳交易的核心思想是“超得越多,单价越贵”——这部分必须引入分段线性化处理,否则模型就成了普通的碳税模型,阶梯机制就失去了意义。这一点后面在代码部分还会专门讲。
3.2 约束条件:平衡、上下限、爬坡、储能一个都不能漏
建模的时候最容易犯的错误是只重视目标函数而忽略了约束条件缺漏导致的解不可行。我梳理了以下四类约束:
能量平衡约束——这是优化的骨架。电功率平衡、热功率平衡、冷功率平衡三个等式在每个时段都必须成立。电平衡中,光伏出力+燃气轮机发电+储能放电+购电 = 电负荷 + 储能充电 + 电制冷机耗电;热平衡中,燃气轮机余热+燃气锅炉产热+储热罐放热 = 热负荷 + 吸收式制冷机耗热。冷负荷则通过电制冷机和吸收式制冷机共同满足。平衡方程少一项,调度结果就会偏离实际,这是最基础也是最容易出错的地方。
设备出力上下限约束——每台设备都有最大最小技术出力。比如燃气轮机最小出力往往是额定功率的30%左右,低于这个值运行效率极低且可能损害设备。这部分我直接用决策变量的上下界表达。
爬坡约束——燃气轮机和燃气锅炉出力不能瞬间大幅变化。比如某机组十分钟爬坡率可能只有20%额定功率,折算到一小时是有限的爬坡速率。这个约束用来保证调度指令在设备层面是可执行的。
储能SOC约束——储能设备的充放电功率、以及每个时段末的SOC值都有约束。一般情况下我会设定调度周期始末SOC保持一致,防止模型“偷”能量——比如一直放电来满足负荷而从不充电。
需求响应约束——弹性模型下用户负荷调整量不能超过负荷转移上限;激励模型下可中断负荷的总削减量也要有上限。
3.3 综合需求响应:价格弹性怎么建模
需求响应在这篇复现里我同时考虑了价格型和激励型两种。
价格型需求响应通过价格弹性矩阵实现。核心公式是:负荷变化率与电价变化率之间的弹性关系。自弹性系数一般为负(电价升高,当前时段负荷下降),交叉弹性系数为正(某个时段电价升高,负荷会转移到其他时段)。构建弹性矩阵时要注意矩阵的对称性,以及“每一行之和应该接近0”——因为总用电量不会因为价格变化而凭空消失,只是被转移。
激励型需求响应则更直接:设定可中断负荷,单位削减量给固定补偿。系统在调度时权衡“削减负荷支付的补偿”和“削减后少买电的节省”之间的关系。
3.4 阶梯型碳交易:配额怎么算、阶梯怎么分
碳交易机制的建模分两步。
第一步是免费配额计算。电网购电对应的碳排放、燃气轮机消耗天然气对应的碳排放,这两个是系统的主要碳源。免费配额一般按实际负荷乘以一个分配基准系数给出。比如某园区日总负荷1000 MWh,基准系数0.7 t/MWh,那么免费配额就是700吨。
第二步是阶梯碳价。实际碳排放小于配额时不需要购买碳配额;超过配额的部分,分三段计费:第一段超出量(比如0~500吨)按基价20元/吨计算,第二段超出量(500~1000吨)按1.5倍即30元/吨,第三段再往上按2倍即40元/吨。这样做的效果是:系统稍微多排一点还可以接受,但如果想大量超排,边际成本会快速上升,模型就会主动去“减碳”。
3.5 一套完整算例的参数配置(可直接抄作业)
| 参数项 | 数值 | 说明 |
|---|---|---|
| 调度周期 | 24h,步长1h | 典型日调度 |
| 分时电价峰/平/谷 | 1.2/0.8/0.4 元/kWh | 峰10:00-15:00、18:00-22:00 |
| 天然气价格 | 2.5 元/m³ | 固定单价 |
| 燃气轮机额定功率 | 200 kW | 电效率35%、热电比1.2 |
| 燃气锅炉额定功率 | 300 kW | 热效率90% |
| 光伏额定容量 | 150 kW | 出力曲线按典型日数据 |
| 电储能容量 | 200 kWh | SOC范围0.1~0.9 |
| 需求响应最大负荷转移率 | 15% | 各时段可转移负荷比例上限 |
| 碳配额基准 | 0.7 t/MWh | 基于总负荷 |
| 碳交易阶梯 | 0.7/1.2/1.5 元/kg CO₂ | 分三段阶梯 |
这套参数是典型的园区级微网规模,用MATLAB跑一遍大概需要几十秒到几分钟,取决于求解器性能。
4. MATLAB工程实现:从建模到求解的完整过程
4.1 求解器选型与安装配置
MILP求解器我推荐用CPLEX或者Gurobi。学术用户都可以申请免费license,安装好之后在MATLAB里运行yalmiptest命令确认YALMIP能够正确调用求解器。如果不想申请商业求解器,MATLAB自带的intlinprog从R2014版本起也支持MILP,但求解速度会慢一些,处理中大规模问题时可能耗时长。
提示:解决MILP问题上,使用Gurobi配合YALMIP在模型规模较大时优势明显(几十秒vs几分钟),务必优先尝试。申请的学术license覆盖全部功能,没有限制。
4.2 工程根目录结构:代码怎么组织才不乱
我在复现时把代码分成五个模块,每个模块职责单一、互不牵扯:
IES_Dispatch/ ├── main.m % 主程序,定义参数、调用模型、画图 ├── data/ │ ├── load_curve.xlsx % 电/热/冷负荷24小时数据 │ ├── pv_output.xlsx % 光伏24小时出力数据 │ └── price_paras.m % 分时电价、天然气价格参数 ├── model/ │ ├── build_model.m % 定义决策变量、目标函数、约束条件 │ ├── carbon_tct.m % 阶梯碳交易成本计算模块 │ └── demand_response.m % 价格型/激励型需求响应负荷调整模块 ├── solver/ │ └── run_optimize.m % 调用YALMIP和求解器执行优化 └── utils/ ├── plot_results.m % 绘制功率平衡、SOC、成本对比图 └── case_compare.m % 多案例结果对比为什么这么分?后续论文里要改参数、换场景、加约束时,你只需要改动对应模块,而不用在几百行的main函数里翻来翻去找代码。这种分层结构也是工程交付的底线——你发的论文审稿人一般不看代码,但你自己后续三个月要反复改。
4.3 核心代码解析:变量声明与约束构建
这是build_model.m的核心片段,展示如何用YALMIP声明决策变量并构建约束:
%% 决策变量(24时段) P_grid = sdpvar(1, 24); % 电网购电功率 kW P_chp = sdpvar(1, 24); % 燃气轮机发电功率 kW P_gb = sdpvar(1, 24); % 燃气锅炉产热功率 kW P_pv = sdpvar(1, 24); % 光伏出力 kW P_ch = sdpvar(1, 24); % 储能充电功率 kW P_dis = sdpvar(1, 24); % 储能放电功率 kW SOC = sdpvar(1, 25); % 储能SOC状态(25个点,首尾相接) u_chp = binvar(1, 24); % 燃气轮机启停状态(0-1变量) u_gb = binvar(1, 24); % 燃气锅炉启停状态 %% 目标函数 C_buy = sum(P_grid .* price_elect) + sum((P_chp/0.35 + P_gb/0.9) * price_gas); C_om = sum(P_chp * k_om_chp + P_gb * k_om_gb + P_pv * k_om_pv + ... (P_ch + P_dis) * k_om_ess); C_carbon = carbon_cost(P_chp, P_grid); % 阶梯碳交易成本 C_dr = sum(dr_incentive .* P_dr); % 需求响应补偿成本 Objective = C_buy + C_om + C_dr + C_carbon; %% 约束条件 Constraints = []; % 电功率平衡 for t = 1:24 Constraints = [Constraints, P_grid(t) + P_chp(t) + P_pv(t) + P_dis(t) - P_ch(t) ... == P_e_load(t) - dP_e(t)]; end % 热功率平衡 for t = 1:24 Constraints = [Constraints, P_chp(t) * 1.2 + P_gb(t) ... == P_h_load(t) - dP_h(t) + P_abs_cool(t)]; end % 储能SOC递推 for t = 1:24 Constraints = [Constraints, SOC(t+1) == SOC(t) + P_ch(t)*eta_ch - P_dis(t)/eta_dis]; end % 设备上下限与爬坡约束 Constraints = [Constraints, 0 <= P_grid <= 500, 60 <= P_chp <= 200, 0 <= P_gb <= 300, -80 <= P_chp(2:24) - P_chp(1:23) <= 80, % 爬坡 SOC(1) == 0.2, SOC(25) == 0.2, 0.1 <= SOC <= 0.9, P_ch <= 50*u_chp, P_dis <= 50*u_chp];这里有三个细节我要特别说明:
第一,天然气成本的计算。燃气轮机耗气量不能直接用输出功率乘一个单价,而要除以发电效率。输出100 kW电、效率35%,实际燃料输入是285 kW左右。这块漏算效率的话成本会偏低,调度结果会过度倾向使用燃气轮机。
第二,0-1变量不是必须的。如果设备启停成本忽略不计,上下限约束就够了,可以不做0-1变量。但考虑启停成本时,没有0-1变量模型就无法表达“这个设备到底开没开”,所以必须引入binvar。这个小改动会让模型从LP变成MILP,求解时间可能有数量级变化,但对结果的合理性至关重要。
第三,储能SOC约束首尾一致。很多初学者的代码少了SOC(1)==SOC(25),结果调度结果储能一直在放能,到周期结束SOC降到下限,整体看起来运行成本低了,但这是“透支”了下一周期的能量。这个问题在复现时最容易踩。
4.4 阶梯碳交易的线性化处理:打破非线性壁垒
碳成本如果直接写成“排放量×价格”,它在超出每段边界时会有一个跳跃,这是包含阶跃函数的非线性函数。非线性项不能直接放进MILP,所以必须做分段线性化。
我的做法是用三个连续变量x1, x2, x3分别表示三个阶梯段的排放超出量,然后每段乘以对应的碳价:
E_total = E_chp + E_grid; % 总排放量 E_free = load_total * alpha; % 免费配额 E_over = E_total - E_free; % 三个阶梯段:0~E1按p1,E1~E2按p2,超过E2按p3 Constraints = [Constraints, E_over == x1 + x2 + x3, 0 <= x1 <= E1, 0 <= x2 <= E2-E1, x3 >= 0]; C_carbon = p1*x1 + p2*x2 + p3*x3;这种分段线性化是MILP的标准套路,代码简单但逻辑严密。实际建模时我不建议用E_over直接去定义C_carbon,因为一旦非线性的判断条件(比如if E_over > E1)出现,模型就不再是线性了。用连续变量拆分是最干净的做法。
4.5 价格型需求响应:弹性矩阵的正确打开方式
价格型需求响应需要在优化前预先计算负荷修正量,还是可以在优化过程中通过变量表达?我的经验是:如果弹性矩阵是固定的,可以直接预先计算出各时段负荷调整量,然后在平衡方程里作为固定参数减去,不用增加决策变量。这样模型更简单。
% 弹性矩阵法:计算价格型需求响应后的负荷 lambda_base = [0.4*ones(1,7), 0.8*ones(1,3), 1.2*ones(1,6), ... 0.8*ones(1,3), 1.2*ones(1,4), 0.8*ones(1,1)]; lambda_new = ... % 实际执行的实时电价(场景设置) elast_matrix = diag(-0.2*ones(1,24)); % 自弹性 % 交叉弹性:峰时段转移到谷时段的弹性系数0.03 for i = 1:24 for j = 1:24 if abs(i-j) == 1 && lambda_base(i) > lambda_base(j) elast_matrix(i,j) = 0.03; end end end dP_e = P_e_load .* (elast_matrix * (lambda_new - lambda_base) ./ lambda_base');这里有个容易绕晕的点:弹性矩阵维度是24×24,反应的是24个时刻两两之间的转移关系。构建矩阵时要注意自弹性和交叉弹性的符号差别,还要注意矩阵的对称性(i对j的弹性等于j对i的弹性)。很多复现版本把这部分直接简化成线性函数,虽然能跑,但失去了需求响应的核心内涵——用户对电价的反应不是简单线性关系,而是有转移效应的。
5. 案例设置与结果分析:复现到底得到了什么
5.1 四组对照实验:逐步验证机制价值
为了验证优化策略的效果,我设置了四组案例做对比分析:
- 案例A(基准调度):没有需求响应,没有碳交易。
- 案例B(只加碳交易):系统碳排放不再免费,要按阶梯价格购买超额配额。
- 案例C(只加需求响应):用户负荷可以按价格弹性调整。
- 案例D(协同策略):同时考虑需求响应和阶梯碳交易。
这样设计的逻辑是:单一机制的效果可以先被独立验证,最后再看两者协同是否能产生“1+1>2”的效果。这四组案例也是论文里最经典的图表结构。
5.2 一天24小时的调度结果长什么样
复现完成后我画出了系统最优调度图,关键结果可以总结为几个特征:
光伏出力完全消纳。在白天光伏高峰时段,储能充电增加,燃气轮机出力压低,电网购电也压低,系统自动“优先使用绿电”。这个现象在加了碳交易后尤其明显——因为光伏发电的边际碳成本是零。
燃气轮机夜间出力升高,白天降低。夜间电价是谷段,用电便宜,但热负荷还存在,燃气轮机需要发电来产热满足热负荷。白天电负荷高、热负荷相对较低时,燃气轮机发电的余热有一部分会被浪费,所以模型会倾向于多用光伏、少开燃气轮机。
储能呈现“谷充峰放”规律。凌晨低谷时段给储能充满电,白天峰段放电给负荷供电。加了需求响应后储能的充放次数会增多,因为负荷曲线被压平了,储能承担了更多“平移电量”的角色。
需求响应效果直观可看:负荷曲线的高峰明显被削低,低谷被填高,削峰率大约在10%~15%之间,与预设的最大转移率吻合。
5.3 成本结果:协同策略能省多少
| 指标 | 案例A | 案例B | 案例C | 案例D |
|---|---|---|---|---|
| 总成本(元) | 18642 | 19406 | 18256 | 17893 |
| 购电成本(元) | 8621 | 7456 | 7962 | 6831 |
| 碳交易成本(元) | 无 | 1850 | 无 | 731 |
| 碳排放总量(kg) | 9680 | 6120 | 9020 | 5640 |
从数据上可以读出几个关键结论:
加了碳交易之后,总成本反而上升了。这是我在初跑时差点误读的地方——案例B总成本比案例A高,不是因为模型出错,而是因为案例A“不承担碳成本”,等于把碳排放的外部性全部转嫁了。从社会总成本角度看,案例B才是真实的。换一种对比方式:如果把案例A的隐含碳排放也按阶梯碳价折算进去,A的真实成本会非常高。
协同策略(案例D)的总成本最低。这验证了IDR与TCT的组合不是简单的线性叠加。需求响应把负荷高峰削平了,意味着高峰期的高碳机组出力降低;碳交易进一步压制燃气轮机的低效时段出力。两者互相配合,比各自单独应用的总成本都低。
6. 三个最容易踩坑的地方与排查经验
6.1 约束数量爆炸导致求解极慢
24时段、几十个设备、加上需求响应矩阵和碳分摊变量,模型规模可能达到上千个约束。求解器在某些MILP问题上跑几个小时也不奇怪。我之前遇到过Standard的intlinprog在某个案例下半小时没出解。
排查方法:先用LP标签的求解器(关闭0-1整数约束)跑一遍,看问题是否可行。如果LP只需几秒,说明问题卡在整数变量上;如果LP也极慢,说明约束写得有问题,可能存在大范围退化或数值病态。
解决办法:对0-1变量做初始值猜测。YALMIP的assign和solve支持热启动,把上一个案例的解作为当前案例的初始点。相邻案例之间变化不大时,这一步能把求解时间压缩到原来的1/5。
6.2 “负功率”与“弱平衡”——约束写错的表现
复现过程中CRITICAL的一次错误:储能SOC曲线全程走平,几乎不变。原因是我在SOC递推方程里写错了P_ch和P_dis的顺序,导致充电时SOC下降、放电时SOC上升——整个模型一“充电”就开始“负储能”,调度器选出了个颠倒的结果。
排查方法:专门绘制SOC曲线,看SOC的变化方向和充放电功率是否一致。如果充电时段SOC下降,100%是递推符号错了。
另一个经典错误:电平衡方程写成>=或<=而不是==。模型会偷偷“弃能”或“凭空发电”来降低成本。用==最稳妥,模型如果不可行会直接报错,而不是给一个能跑但有问题的结果。
6.3 参数量纲不一致:最隐蔽但是最致命
电价的单位是元/kWh,天然气价格是元/m³,碳配额是t/MWh,碳价是元/t——这四个单位放在一个目标函数里,量纲不统一会导致某一项的成本被严重高估或低估,调度结果必然失真。比如天然气耗量如果直接乘以2.5元/m³,而实际折算到kW应该用热值乘以效率来换算,最后成本会比真实值低得多,模型会疯狂用气不用电,结果完全不对。
我的解决方法是:在代码开头的参数区,把所有单位换算成统一的“元/kWh”基准。天然气先换算成kWh再参与成本计算,碳排量先计算总千克数再乘碳价。统一量纲后,模型输出才具有可比性。
7. 扩展思考:这套框架还能怎么演化
复现完成、案例跑通之后,这个模型的价值其实才刚刚开始。
最直接的扩展是多时段鲁棒优化。光伏出力和负荷都有不确定性,把确定性模型换成两阶段鲁棒模型,在第一阶段做设备启停决策,在第二阶段做运行调度,能显著增强策略的工程实用性。
其次是加入多能源互补的交易机制。园区内如果有多个能源主体(多个楼宇、多个设备业主),可以考虑博弈模型设计,让各个主体在独立决策的同时满足园区整体最优。这是P2P能源交易的研究热点,也是当前论文复用这个模型的一个方向。
还想提醒一点:如果想把这个模型投到交叉学科期刊或者顶会,建议在案例部分增加“碳价敏感性分析”和“需求响应潜力敏感性分析”——这两个参数是论文里审稿人最爱看的,它揭示了机制设计的稳定性和适用范围。
8. 复现过程中我最想留给你的一句话
代码可以抄,参数可以抄,但建模的思路是抄不来的——它需要你真正理解每一步背后的物理含义和经济逻辑。当时我在跑通模型后,又把四组案例分别调试对比了很多次,每次调整都会发现之前的调度策略在某些时段有可以优化的空间。这种“再想一层”的思维习惯,是我复现这个项目最大的收获。
最后分享一个实用小技巧:如果你在跑优化时发现结果特别完美(比如成本降了30%以上),先不要高兴,大概率是约束条件少了。真实系统里不会有那么多免费的优化空间,模型越“完美”,你越要回去检查是不是平衡方程里少写了一项——这是我在这个项目里最大的经验和教训。