做虚拟电厂优化调度这个方向,绕不开碳交易。我最近把“基于阶梯碳交易的含P2G-CCS耦合和燃气掺氢的虚拟电厂优化调度”这套题完整用Matlab实现了一遍,模型涵盖了风电、光伏、储能、燃气机组、P2G、CCS、掺氢管路和碳交易成本,不是纸上谈兵的那种示意图,而是能跑出24小时调度结果、能做参数敏感性分析的完整代码。这篇就把整个建模思路、代码组织方式和调试过程中踩过的坑一次讲清楚。适合正在做电力系统优化、虚拟电厂、综合能源方向毕业设计或论文复现的朋友,也想顺便给准备用Matlab+Yalmip搭这类MILP模型的人一份可以直接抄的作业。
1. 先搞清楚这个题在算什么:虚拟电厂、碳市场和多能耦合
1.1 虚拟电厂在调度什么:一堆单元凑成一个“电厂”
虚拟电厂本身不是一个物理电厂,它是把分散的风机、光伏、储能、燃气机组、电转气设备、碳捕集装置等聚合起来,作为一个整体参与电网调度和碳市场。单看每一个单元可能都很小,但聚合起来就有了和传统电厂博弈的体量。调度问题的本质,是在满足负荷需求、设备运行约束、碳配额约束的前提下,决定每个时段各台设备的出力,让总成本最低。
这个题的调度决策变量听起来就不少:每个时段的燃气机组出力、储能充放电功率、从电网买电或卖电的功率、P2G消耗的电功率、CCS捕集的CO2量、掺入燃气机组的氢气量、储氢罐的充放量等等。它们之间不是独立的,电功率平衡、燃气网络平衡、碳配额平衡把这些变量绑在一起。这也是这类题目真正难的地方:不是变量多,而是约束之间的耦合关系容易出错。
1.2 为什么要把碳交易做成“阶梯”的
如果单纯用固定碳价,那其实非常简单,目标函数里加一项“排放量×单价”就行,最优解不会因为碳价的分段变化产生结构性调整。但实际碳市场的定价逻辑是超额越多、单价越贵,这样才逼着高排放机组减碳。阶梯碳交易就是把超排量分成几段,每一段采用不同的碳价,价格逐段递增。
以常见论文中的参数为例,可以把超排量分成4段:
| 超排区间(t) | 碳价(元/t) |
|---|---|
| 0~1000 | 50 |
| 1000~2000 | 80 |
| 2000~3000 | 100 |
| 3000以上 | 130 |
同样是超排2500吨,固定碳价70元时成本是17.5万元;阶梯碳价下,前1000吨按50算,中间1000吨按80算,最后500吨按100算,总成本是18.5万元。多出来的1万元就是阶梯带来的惩罚增量。解决这类问题会用到分段线性化,后面代码部分专门讲。
1.3 P2G-CCS耦合与燃气掺氢:一个减碳,一个用氢
P2G,即电转气,本质是电解水制氢,氢气可以继续甲烷化生成天然气,也可以直接掺入燃气管道。CCS是碳捕集与封存,把燃气机组排出的CO2捕集下来,避免直接排入大气。P2G-CCS耦合的核心,是捕集下来的CO2可以作为甲烷化反应的原料,和P2G产出的氢气反应生成甲烷,这样既消纳了新能源电力,又把原本要封存的CO2变成了可用燃料,形成闭环。
燃气掺氢则是另一条更轻量的减碳路径,不需要额外建甲烷化装置,直接把P2G产出的部分氢气混入天然气管道,供燃气机组燃烧。氢气燃烧不产生CO2,同样的发电量下碳排放天然降低。这两个手段在同一个模型里出现时,需要仔细处理氢的分配:多少氢送去甲烷化,多少氢直接掺入燃气机组,这是气平衡约束的核心。
2. 数学模型从零搭:目标函数、约束和线性化
2.1 目标函数:不是只有运行成本,还有碳账单
目标函数我写成运行总成本最小化,包含五大块:购电成本、燃料成本、运维成本、碳交易成本,再减掉售电收益。公式写出来就是一个求和:
C_total = Σ [ C_buy(t) - C_sell(t) + C_fuel(t) + C_om(t) + C_co2(t) ]
购电成本是分时电价乘购电功率,燃料成本包括天然气费用和掺氢的氢气费用,运维成本按各设备出力乘一个单位运维系数来算。碳交易成本单独成项更好,因为它是阶梯分段函数,没法简单乘一个常数,放进燃料成本里会让自己后面写代码时分不清。
这里有一个很多新手会忽略的点:外购电也有隐含碳排放。虚拟电厂从电网买电,等于间接使用了电网中的火电,所以碳排放账本里要加一项“电网排放因子×购电量”。如果模型里没有这一项,结果往往会让系统通过大量购电来替代本地燃气机组,这在碳约束下是不合理的。
2.2 碳排放和配额:先把账算清楚
碳排放量不是简单把燃气机组烧的气乘个排放因子就完事。这个模型里有三个环节会影响最终净排放:
- 燃气机组燃烧排放:燃烧天然气产生CO2,掺氢部分不产生CO2。
- 外购电间接排放:按电网平均排放因子折算。
- CCS捕集扣除:被CCS捕集并送去甲烷化或封存的CO2,不再算作实际排放。
用公式写就是:
E_total(t) = λ_grid × P_buy(t) + λ_gas × V_gas(t) - E_cap(t)
其中E_cap(t)是CCS捕集掉的CO2量。CCS捕集的CO2再去甲烷化,最终又以甲烷的形式回到燃气管道甚至被燃气机组燃烧,这部分碳排放已经包含在V_gas里,如果V_gas中算的是外购天然气,而甲烷化产出的甲烷单独计量,就要注意别重复计算。我在代码里直接用“实际燃烧的天然气体积×排放因子”算机组排放,CCS捕集扣除,甲烷化槽里的CO2作为中间物料处理,这样最不容易错。
配额分配一般按机组出力和一个单位配额系数来计算,比如每发一度电给多少吨配额。把总配额记为E_free,净排放超过配额的部分就是超排量E_punish,这是进入阶梯碳交易成本函数的输入量。注意超排量不能是负的,配额盈余在简化模型中直接视为作废,不做跨周期存储。
2.3 阶梯碳成本的分段线性化实现
阶梯碳交易成本函数是分段凸函数,直接写进MILP需要线性化。我的做法是引入二进制变量指示排量落在哪个区间,再把每一段的排放量拆成独立变量。假设超排量E被分成K段,第k段上限W(k),单价p(k),代码可以这样组织:
% E_punish 为超排量,拆成 x(1)...x(K) 各段,y(k) 表示第k段被填满 E_punish == 0; for k = 1:K E_punish = E_punish + x(k); x(k) >= 0; end x(1) <= W(1); for k = 2:K y(k) <= y(k-1); % 单调,前段不满后段不能填 x(k) <= W(k) * y(k-1); % 进入第k段的前提是前一段被填满 x(k) >= W(k) * y(k); % 第k段被填满则排量达到上限 x(k) <= W(k); end C_co2 = sum(p .* x);这里的关键是y(k)的单调约束和x(k)的上下界配合。实际求解时如果碳价严格递增,即使不写y的约束,单纯靠目标函数最小化也会自动优先使用低价段,但加上这些二元变量后模型更严谨,不会因为数值原因出现“跳段”的异常结果。
2.4 P2G-CCS耦合模块的约束
P2G环节是电转氢,效率按电解槽的折算系数处理:
H_h2(t) = η_p2g × P_p2g(t)
H_h2是产氢量,η_p2g是电解槽效率,可以理解为单位耗电对应的产氢量。产出的氢往两个方向走:一部分进入甲烷化反应器,一部分直接掺入天然气管道。甲烷化是一个CO2加氢生成甲烷的过程,从化学计量关系看,1吨CO2大约需要消耗0.18吨氢气(按CO2+4H2反应折算,实际工程中还要考虑反应效率),我这里直接用一个常数系数处理:
E_ch4(t) = μ_meth × H_meth(t)
E_ch4是甲烷化消耗的CO2量,H_meth是投入甲烷化的氢量。CCS捕集的CO2一部分送去甲烷化,剩余部分封存:
E_cap(t) = E_cap2meth(t) + E_cap2seq(t)
同时CCS捕集本身要耗电,耗电量与捕集量成正比:
P_ccs(t) = β_ccs × E_cap(t)
实际参数中,捕集每吨CO2耗电大约200~300kWh,这看起来不大,但在24小时逐时调度里,CCS耗电会显著改变电功率平衡,特别是夜间负荷低谷时段,P2G和CCS两个耗电大户叠加,往往制造出新的负荷峰谷,这也是算例里最值得观察的现象。
2.5 燃气掺氢模块的约束
燃气掺氢不能拍脑袋给一个比例,要按体积比建模。设天然气耗量为V_gas(t)、掺入氢气体积为V_h2(t),约束是:
V_h2(t) ≤ h_max × (V_gas(t) + V_h2(t))
h_max是体积掺氢比上限,工程上常见15%~20%。这里有个容易踩的坑:体积比不是能量比。按标准状态下天然气低热值约35.8 MJ/m³、氢气低热值约10.8 MJ/m³计算,20%体积掺氢对应的能量占比只有:
0.2×10.8 / (0.2×10.8 + 0.8×35.8) ≈ 7%
也就是说,掺20%体积的氢,实际上只替代了约7%的燃料能量。建模时如果不区分这两个概念,燃气机组出力关系和碳排放削减量都会算歪。燃气机组的电出力由总燃料能量乘以发电效率来确定:
P_gt(t) = η_gt × ( V_gas(t)×LHV_gas + V_h2(t)×LHV_h2 )
碳排放按实际燃烧的天然气量计算:
E_gt(t) = λ_gas × V_gas(t)
掺氢比例越高,V_gas越少,碳排放越低,但同时氢气成本通常高于天然气成本,所以模型会自动在碳价和氢价之间做权衡。这个权衡正是整道题的精华。
2.6 剩下的常规约束
除了上面几块,常规约束还包括:电功率平衡,即风机、光伏、燃气机组、储能放电、购电之和等于负荷、P2G耗电、CCS耗电、储能充电、售电之和;储能SOC动态约束,考虑充放电效率和容量上下限;燃气机组的出力上下限和爬坡约束;风机光伏的出力上限。这些约束本身难度不大,但它们和P2G-CCS、掺氢、碳交易耦合在一起之后,任何一个不平衡方程写错,都会导致无解或者结果违背物理直觉。
3. Matlab代码实现:Yalmip建模到结果可视化
3.1 先装好环境:Yalmip + Gurobi/CPLEX
这个模型是个MILP,Matlab里最顺手的组合是Yalmip建模加Gurobi或CPLEX求解。Yalmip负责把变量、约束、目标函数组织起来,求解器负责算。Gurobi对MILP的支持非常成熟,几百个变量、几百条约束的模型基本秒解。如果只有学术个人用途,可以去官网申请免费license;没有Gurobi的话,换成CPLEX或者开源求解器SCIP也能跑,只是性能会差一些。
装Yalmip只有一个要求:把文件夹加入Matlab路径。之后在命令行输入yalmiptest,看到连续变量、二进制变量、LP、MILP都返回正确,就说明环境OK了。很多人的问题出在下载了Yalmip却忘了安装求解器,或者式子里不小心出现了非线性项,导致Yalmip调用fmincon而不是MILP求解器,速度慢一个数量级还容易不收敛。
3.2 代码结构:别把模型全塞在主脚本里
我见过很多同学把参数、变量、约束、目标函数、求解、画图全部写在一个脚本里,跑通一次没问题,但换一组参数或改一个约束时,改起来非常痛苦。我用的是模块化结构,文件拆成这样:
main.m:主入口,负责调用各模块并出图params_base.m:全部系统参数集中在这里build_variables.m:定义连续变量和二进制变量build_constraints.m:组装约束build_objective.m:计算目标函数solve_dispatch.m:求解并整理结果plot_results.m:结果可视化
这样做的最大好处是调试方便。模型无解时,可以先把build_constraints.m里的约束一部分一部分注释掉,快速定位是哪条约束把可行域挤没了。参数敏感性分析时,只需要改params_base.m,主逻辑一行不用动。
3.3 关键代码段解析
变量定义部分,最核心的可以这样写:
T = 24; P_gt = sdpvar(1, T); % 燃气机组电出力 V_gas = sdpvar(1, T); % 燃气机组耗气体积 V_h2 = sdpvar(1, T); % 掺入氢气体积 P_p2g = sdpvar(1, T); % P2G耗电 H_h2 = sdpvar(1, T); % P2G产氢 E_cap = sdpvar(1, T); % CCS捕集CO2量 P_ccs = sdpvar(1, T); % CCS耗电 P_buy = sdpvar(1, T); P_sell = sdpvar(1, T); E_bat = sdpvar(1, T); P_ch = sdpvar(1, T); P_dis = sdpvar(1, T); u_gt = binvar(1, T); % 燃气机组启停状态 E_punish = sdpvar(1, T); % 超排量约束装配时,有些量要通过辅助变量表达,比如CCS耗电和捕集量的线性关系,直接用一个等式约束接上即可。注意sdpvar默认是实数变量,碳交易拆出来的每段排量、储能充放电功率、P2G产氢这样的物理量都要显式加>= 0约束,不然后面求出来的结果可能是负值,虽然目标函数通常不会让它们取负,但加上更稳妥。
求解部分就三行:
ops = sdpsettings('solver','gurobi','verbose',2,'gurobi.MIPGap',1e-4); sol = optimize(Constraints, C_total, ops); if sol.problem == 0 % 求解成功,提取变量 else disp(sol.info); end提取结果时,推荐用value(变量名),但要注意Yalmip返回的矩阵维度,如果一个1×T的变量求出来是1×T的cell,多半是某个地方约束书写有误导致变量降维或增维了。还有一个经验:结果里所有功率单位统一用MW,碳量单位统一用吨,成本单位统一用元,尽量不要混用kW和MW,不然后面算总成本时容易差三个数量级。
3.4 怎么验证模型是对的
模型写完之后,第一步不是看图,而是做物理一致性检查。我通常检查三件事:
- 每个时段的电功率平衡等式两边分别求和,残差应该在1e-6量级。
- 储能SOC的最后一个时刻应该回到初始值附近,否则说明24小时周期没有闭合。
- 碳排放账本:燃气机组排放减去CCS捕获量,再与最终碳交易费用反推的碳量对比,应该对得上。
推荐再跑两个极端case。第一个是碳价全设为0,此时系统会倾向于多用燃气机组、少上P2G和CCS,因为后者只会增加成本;第二个是把掺氢比例上限设为0,此时氢气全部走甲烷化路线,系统还有没有可行解,直接暴露燃气网络的物料平衡是否写对。通过这两个case,基本能排除掉90%的建模错误。
4. 调试排坑实录:我从报错到出图的完整过程
4.1 求解器报错和许可证问题
最常见的报错是No suitable solver for LCP、The solver is not available或者Gurobi license expired。第一个信息说明Yalmip判断当前问题类型后没有找到可用求解器,多半是装了Yalmip但没把Gurobi加入路径,或者在solvesdp/optimize里没有正确指定求解器名称。第二个是license问题,Gurobi新版本的license文件要放在用户目录的gurobi.lic位置,路径写错或者到期都会报这个。
还有一类情况比较隐蔽:Matlab自带的linprog能解线性规划,但解不了带整数变量的MILP,如果sdpsettings里没有指定求解器,Yalmip可能会降级用bnb、intlinprog或者干脆报错。我的建议是,一开始就把操作写成:
ops = sdpsettings('solver','gurobi');不要依赖默认选择,否则换台电脑运行结果就可能不一样。
4.2 无解或INFEASIBLE:排查思路
MILP无解是会让新手崩溃的问题。我自己用的排查套路是从后往前逐段释放约束。先用一个最简单的可行case,比如不启用碳交易约束、不启用CCS和P2G,只保留最基本的电平衡和机组出力约束,确认能跑通。然后再逐步加入储能、P2G、CCS、掺氢、碳交易,每加一个模块就求解一次,直到哪一步开始无解,问题就锁定在哪一类约束。
还有一种常见原因是等式约束冲突。例如电功率平衡里,风机、光伏、燃气、储能、购电被设定为等于负荷加P2G加CCS耗电加储充加售电,但如果某个时段的可再生上限加上燃气最大出力和购电最大容量仍然小于负荷与各耗电之和,模型就会无解。这时需要把某些单元的容量上限调大,或者把某些耗电设备的运行区间改成可中断的,给调度留出自由度。
4.3 运行慢:问题在变量太多或M太大
这个规模级别的MILP一般不会慢,但如果感觉求解时间异常,重点检查两个地方。第一,二进制变量的数量。阶梯碳交易拆段用了一组y变量,燃气机组启停用了一组u变量,如果还有储能充放电状态变量,加起来可能到几十上百个二进制变量,Gurobi跑起来通常没问题,但如果你把某类0-1变量从连续变量混用,求解器可能把整段非线性化之后疯狂分支。
第二,大M法的M参数取值。我用big-M处理约束时,M取的是该变量可行域的上界加上一个余量,而不是随手写一个大数。M太大容易导致数值问题,求解器内部会出现病态矩阵,M太小又会把可行域错误缩小。比如约束像x(k) <= W(k) * y(k-1),这里W(k)本身就是天然的上界,不需要额外乘大数。
4.4 数值陷阱与单位问题
单位不一致是这类程序最常见的隐性bug。我之前在调试一个算例时,风电预测数据是kW,负荷数据是MW,燃气机组爬坡约束用的是MW,结果电平衡约束怎么都对不上,排查了半天才发现是单位混了。建议在params_base.m的开头用注释明确写清楚全局单位,或者干脆全部统一成MW和吨。碳排放因子的单位也要小心,比如天然气排放因子常用kg CO2/m³,而碳交易成本以吨为单位,这里差1000倍。
出图时也经常遇到问题。比如氢气流量的单位如果用了kg,而天然气流量用了m³,两者画在同一张图上量级会差很多。我的做法是画图前统一折算成吨标准煤或者千瓦时,既能直观对比,又不暴露单位混乱。
5. 可以继续往哪儿扩展
这套模型跑通之后,扩展方向非常多。比如把P2G产出的氢气不仅用于燃气掺氢和甲烷化,还可以加上氢燃料电池或者氢储能环节,让氢成为独立储能介质;也可以在目标函数里加入绿证收益,或者考虑需求响应资源和备用容量约束;如果要做更贴近实际的项目,可以把单时段确定性模型升级为多场景随机规划或分布鲁棒优化,用典型场景集处理风光不确定性。
我在实际测试中还发现一个值得关注的现象:掺氢比例上限从10%提到20%时,系统的总碳排放并没有等比例下降,因为碳成本节省带来的收益被氢成本抵消了一部分,最优调度策略甚至可能在低负荷时段减少掺氢量,把氢优先用于甲烷化,因为这个路线更赚钱。这说明这类模型的价值就在于帮决策者量化不同减碳路径之间的经济博弈,而不是简单拍一个技术比例。
我个人做下这套题最大的体会是,不要急着堆模型复杂度,先把一个不带碳交易、不带P2G的普通虚拟电厂调度跑通,再逐步往上加模块。每加一个模块,都要回到平衡方程重新推导一遍,而不是直接复制别人的代码。Yalmip在这些场景里非常顺手,只要建模合规,求解和结果提取几乎不用操心。最后想说,能源系统优化这种东西,模型不是越复杂越好,关键是每个约束都要能解释清楚它在物理世界里对应哪根管道、哪台设备、哪张账单。把这一步做扎实,后面的论文和项目都会顺畅很多。