先说结论:这篇一区文章复现的难度不在求解器调用,而在把物理解耦变成数学表达时,符号方向和边界条件特别容易出错。尤其是碳捕集设备的“捕碳量”与P2G设备“耗碳量”之间不是静态比例关系,二者要通过碳流平衡耦合在一起,很多复现代码跑出来的帕累托前沿不完整,根子就出在这。下面我把整个复现过程拆开讲,适合已经能跑通常规日前优化调度、想挑战双目标带碳排放处理的小伙伴参考。
1. 复现前的全局规划:系统架构与能流拓扑怎么搭
1.1 先把网架拓扑图老老实实画一遍
拿到论文标题,第一步不是开Matlab,而是把系统拓扑画在纸上。这套系统里有四个核心角色:燃气轮机热电联供机组、P2G电转气设备、碳捕集设备,以及外部电网和天然气网。它们之间的连接关系决定了所有约束方程的写法。
我复现时用的拓扑结构是这样:
- 电能母线:外部购电量、CHP发电量、P2G耗电量、普通电负荷、碳捕集设备耗电量,全部汇集在这条母线上。
- 热能母线:CHP余热、燃气锅炉补热,满足热负荷。
- 天然气母线:外部购气量分配给CHP和锅炉,P2G生成的天然气如果允许回注,也要并入这条母线。
- 碳流母线:CHP和锅炉燃烧产生的CO2进入碳捕集设备,捕集下来的CO2一部分交付P2G作为原料,剩余部分算封存。
这里要提醒一句:热电联供机组的排烟CO2浓度与锅炉排烟CO2浓度往往不同,论文里如果对碳捕集按“烟气来源”区分捕集成本,就要给每台设备单独建捕集模型,不能图省事合并成一个捕集器。我一开始就是把CHP和锅炉的CO2合在一起算,结果碳捕集能耗偏小,帕累托前沿的左端点偏移明显,后来拆开建模才和原文数据对齐。
1.2 能量流的三个层级关系
梳理完拓扑,接下来要分清三个层级的能流,因为写代码时每个层级对应一组不同的约束。
第一层是设备输入输出关系。比如CHP机组,输入天然气,输出电和热;P2G设备,输入电和水,输出氢再合成天然气;碳捕集设备,输入烟气中的CO2和额外电耗,输出高浓度CO2。这一层的核心是转换效率。
第二层是母线平衡关系。电能母线上所有供能之和等于所有用能之和,热母线同理,天然气母线同理。这一层是等式约束,最容易因为单位不一致而出问题。
第三层是碳流平衡关系。系统总排放CO2减去捕集量,得到净排放量,净排放量乘以碳价或配额价格,就是碳排放成本。P2G耗用掉的CO2属于碳循环利用,不应重复计入最终排放,但捕集过程本身消耗的电能又间接增加排放,这个循环关系要在目标函数里体现清楚。
画完这三层能流,我心里就有数了:所谓“双目标优化”,本质上是在“少烧天然气、多用电”和“电从哪里来、减排成本多高”之间找帕累托前沿。CHP少发一点电,碳排放减少,但电缺口要由外购电补上,外购电若来自高碳电网,则总排放不一定下降,这正是模型里最值得调参验证的部分。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 碳排放成本与运维成本的双目标:为什么选ε约束法
2.1 加权法在这里会漏掉帕累托前沿的凹段
双目标优化最常见的做法是先线性加权,把两个目标合成一个单目标,再反复调整权重生成帕累托前沿。实际做下来,加权法在这个问题上有一个要命的缺陷:当碳排放成本与运维成本的可行域边界是非凸曲线时,线性加权只能找到凸包上的支撑点,无法覆盖凹陷部分的帕累托最优解。
你可能会问,这套系统怎么就会出现非凸?问题出在CHP机组的电热耦合可行域上。CHP机组有背压式和抽凝式两种运行模式,抽凝式CHP的可行域是一个多边形区域,当P2G和碳捕集设备投入后,碳排放量关于CHP出力的函数在大范围变化时,会出现一段下凹区间。加权法从两个端点逐步移动权重,到凹陷段时目标组合值会突然跳变,得出的帕累托前沿缺一块。
2.2 ε约束法的核心流程
ε约束法把其中一个目标保留为目标函数,另一个目标转化为不等式约束,然后通过逐步收紧这个约束,得到一组帕累托最优解。这里我选择保留运维成本最小化作为目标函数,把碳排放成本作为约束,约束形式是:
碳排放成本 ≤ ε
ε从碳排放成本的最大可行值开始,逐步下降到最小值,每次下降都重新求解一次混合整数线性规划问题,就能得到一组完整的前沿点。
实现时先解两个单目标问题来确定ε的取值区间:
- 第一次只最小化运维成本,记录此时碳排放成本的取值C_max,这就是ε的上界(在完全不约束碳排放时的实际排放成本)。
- 第二次只最小化碳排放成本,得到碳排放成本最小值C_min,这就是ε的下界。
这两步必须用同一个约束集合来解,否则C_min和C_max不在同一个可行域内,后面的循环会经常出现无解。我吃过这个亏,第一次只改了目标函数却忘了把约束条件统一,结果第二个单目标的P2G出力范围比第一个小,ε区间整体偏窄。
2.3 ε区间划分的技巧
理论上ε在C_min到C_max之间均匀取N个点即可。但实际复现时我发现均匀取点在两端会出现冗余密集区,中间关键拐点反而取点稀疏。更稳的方式是:先均匀跑一遍,看哪个区间内目标变化剧烈,再在该区间加密取点。
具体到代码实现是这样的:
code复制% 单目标求区间
ops = sdpsettings('solver', 'gurobi', 'verbose', 0);
optimize(cons, C_opex, ops);
C_max = value(C_carb
