先说结论:这篇一区文章复现的难度不在求解器调用,而在把物理解耦变成数学表达时,符号方向和边界条件特别容易出错。尤其是碳捕集设备的“捕碳量”与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_carbon);
optimize(cons, C_carbon, ops);
C_min = value(C_carbon);
% 生成ε序列
epsilon_list = linspace(C_max, C_min, N);
for k = 1:N
e = epsilon_list(k);
cons_e = [cons, C_carbon <= e];
optimize(cons_e, C_opex, ops);
pareto_carbon(k) = value(C_carbon);
pareto_opex(k) = value(C_opex);
end
注意这里ε序列我是从排放成本最大往最小走,主要是因为约束不等式用≤写起来更自然。有些文章会反过来取,也完全不影响结果。关键点是每次求解完毕后,要把上一个解作为下一个解的初始可行解传入,能显著加快收敛,尤其是N取到50以上时,节省的时间肉眼可见。
YALMIP里可以用assign和optimize的 warm start 实现在线初始化,不过Gurobi本身会自动利用上一轮的basis信息,实测下来即使不手动做,重复求解同一模型也很快。
3. 碳捕集设备与P2G设备的建模细节
3.1 碳捕集设备的“捕碳能耗”才是碳排放成本的关键
碳捕集设备的建模不能只写一个捕集率。它的运行特性是:捕集到的CO2量越大,设备自身消耗的电能越多,而这部分电能如果是外购的,会在电网侧产生间接排放,冲抵一部分减排效果。这个细节直接决定了帕累托前沿的曲率,是整个模型里最有价值的一段。
我的建模方式是:
- 从烟气进入捕集器到分离出高浓度CO2,捕集量为G_cap。
- 捕集能耗为λ_cap × G_cap,其中λ_cap是单位捕集能耗系数。
- 捕集能耗作为电能负荷加到电平衡等式左侧。
- 捕集下来的CO2去向分流:W_cap给P2G,剩余为封存量。
在代码里,对应约束形如:
code复制P_ccs = lambda_ccs * G_cap; % 捕集能耗
G_cap <= delta_ccs * G_flue; % 捕集率上限
G_cap >= 0;
[H2O_constraints...]
G_flue是CHP和锅炉烟气中CO2总量,delta_ccs是捕集率上限。λ_cas这个值在原文的灵敏度分析里通常会给出一个范围,复现时建议先用中间值跑通模型,再拉高拉低看影响,能明显看出碳排放成本目标曲线的平移。
3.2 捕集的CO2并不是越多越好
如果捕集下来的CO2全部封存,相当于负碳技术,碳排放成本很可能直接被压到零附近。但原文论文往往给P2G设定了CO2需求量上限,原因在于:如果P2G不同时消耗CO2,捕集设备就得考虑封存成本或封存容量约束,而标题强调的是“P2G与碳捕集设备协同”,核心场景是捕集下来的CO2供P2G用,剩余部分才进入碳封存。
所以建模时我建议加一个不等式:
W_p2g ≤ G_cap
并且P2G侧生成的天然气进入气网后,会替代一部分外购天然气,这个替代量要折算成天然气成本减少。换句话说,碳捕集+ P2G的组合同时作用于两个目标:一是减少净碳排放(降低碳排放成本),二是减少外购天然气量(降低运维成本)。两者并非完全同向变动,因为P2G多产气意味着多耗电,外购电增加会导致运维成本上升。
3.3 P2G设备的效率与气网回注约束
P2G建模里有三个参数容易搞混:电解槽效率、甲烷化效率、总转换效率。我直接用总转换效率η_P2G表示“单位电能转化为天然气化学能的比例”,这样在目标函数和碳平衡里都比较好处理:
P2G输入电功率P_p2g,输出天然气功率G_p2g = η_P2G × P_p2g,同时消耗的CO2量是:
W_p2g = (G_p2g / LHV_NG) × θ_co2
其中θ_co2是每合成单位体积(或单位热能)天然气所需CO2质量。这里单位的坑非常多,下文专门写。
P2G回注气网时还要注意气网平衡约束的方向。如果气网只有一个天然气源(外购天然气),加入P2G后平衡等式为:
G_buy + G_p2g = G_chp + G_boiler
很多新手把G_p2g写到等号左边当负荷,那等于人为把P2G产气抵消掉,整条气网平衡就错乱了。我复现时核对的第一件事,就是看天然气平衡等式里P2G的符号。
4. Matlab主程序与求解链路实现
4.1 参数表与数据准备
复现一篇文章,参数表必须和原文一一对应。我建议先把所有设备和价格参数集中到一个Excel或m脚本里,方便统一修改。下面是我常用的参数清单,可以直接抄:
| 参数类别 | 参数名 | 典型值 | 说明 |
|---|---|---|---|
| 电网 | 外购电价 | 分时电价 | 峰谷平三段 |
| 电网 | 外购电碳排放因子 | 0.785 kgCO2/kWh | 根据原文电网数据 |
| 气网 | 外购气价 | 0.35 元/kWh | 天然气管网折合热值价 |
| CHP | 发电效率 | 0.42 | 低位热值基准 |
| CHP | 热电比 | 1.2 | 视运行模式可调 |
| 锅炉 | 效率 | 0.9 | 燃气锅炉 |
| P2G | 总转换效率 | 0.65 | 电解+甲烷化 |
| P2G | 碳需求系数 | 0.2 kgCO2/kWh | 每kWh天然气需CO2 |
| 碳捕集 | 捕集率上限 | 0.85 | 烟气CO2捕集率 |
| 碳捕集 | 单位能耗 | 0.15 kWh/kgCO2 | 捕集分离能耗 |
| 碳排放 | 碳配额初始值 | 依系统排放总量设定 | 配额内免费 |
| 碳排放 | 碳价 | 90 元/tCO2 | 依据情景设置 |
参数表做完以后,建议先把所有效率的单位统一为“低位热值基准”,这是综合能源系统仿真最常见的坑。P2G产出的天然气能量如果按高位热值记,效率数值会小一截,而碳排放因子通常按低热值给,两组数据混用会出现完全不可信的帕累托前沿。
4.2 用YALMIP建模的骨架代码
主程序我推荐直接用YALMIP建模,配合Gurobi求解。如果手头没有Gurobi,用Cplex或Mosek也可以,这些求解器都能处理含0-1变量的MIQP/MILP问题。核心变量定义部分:
code复制T = 24; % 调度时段
% 决策变量
P_chp = sdpvar(1, T, 'full'); % CHP电出力
H_chp = sdpvar(1, T, 'full'); % CHP热出力
P_p2g = sdpvar(1, T, 'full'); % P2G耗电
G_p2g = sdpvar(1, T, 'full'); % P2G产气功率
P_ccs = sdpvar(1, T, 'full'); % 碳捕集能耗
G_cap = sdpvar(1, T, 'full'); % 捕集CO2质量流
G_flue = sdpvar(1, T, 'full'); % 烟气CO2总量
P_buy = sdpvar(1, T, 'full'); % 外购电
G_buy = sdpvar(1, T, 'full'); % 外购天然气
u_chp = binvar(1, T); % CHP启停
P_boiler = sdpvar(1, T, 'full'); % 锅炉热出力
变量全部用“full”显式声明类型,能避免YALMIP在自动推断变量类型时把连续变量误判成二进制变量,这是我遇到过的最无语的求解卡死原因之一。
约束部分按母线平衡来组织:
code复制% 电平衡
cons = [cons, P_buy + P_chp == P_load + P_p2g + P_ccs];
% 热平衡
cons = [cons, H_chp + P_boiler == H_load];
% 天然气平衡
cons = [cons, G_buy + G_p2g == G_chp + G_boiler];
% CHP耦合约束(简化线性化)
cons = [cons, H_chp == K_chp * P_chp]; % 或四边形可行域约束
cons = [cons, P_chp <= P_chp_max * u_chp];
cons = [cons, P_chp >= P_chp_min * u_chp];
这里CHP耦合关系如果直接用定热电比会丢掉抽凝式机组的可行域细节,建议实现成一组线性不等式围成的二维多边形区域,标准写法见Miller和Lennon两篇关于CHP可行域的描述,代码上等价于:
code复制cons = [cons, P_chp <= P_chp_max];
cons = [cons, H_chp <= H_chp_max];
cons = [cons, H_chp >= alpha1 * P_chp + beta1];
cons = [cons, H_chp <= alpha2 * P_chp + beta2];
4.3 碳相关约束的代码位置
碳排放成本函数的计算要多花点心思。我的写法是把总CO2产生量、捕集量、净排放量分别算出来:
code复制CO2_produced = co2_chp * P_chp + co2_boiler * P_boiler + co2_grid * P_buy;
% 捕集的部分
CO2_captured = G_cap;
% P2G消耗的CO2
CO2_consumed_by_p2g = theta_p2g * G_p2g;
% 净排放
CO2_net = CO2_produced - CO2_captured + CO2_released_by_p2g??
% 注意:CO2_consumed_by_p2g在甲烷化产物进入气网燃烧后会重新排放
这里有一个细节必须想明白:P2G把CO2转化为天然气,天然气再被CHP或锅炉燃烧后,CO2又回到烟气里。所以P2G消耗的CO2本质上不是碳封存,而是碳循环。如果简单地在净排放里减掉CO2_consumed_by_p2g,就相当于把循环碳也当成减排,结果碳排放成本会被严重低估。
正确做法是:碳捕集后分为两部分,一部分由P2G循环利用,另一部分永久封存。在净排放计算中,只有永久封存的部分才减去;P2G循环部分不参与净排放计算。但P2G消耗的CO2如果来自捕集,这部分CO2在气网燃烧后会再次产生,所以净排放里要算两次吗?不对,它们在首次产生时已计入CO2_produced,捕集后进入P2G,再燃烧再产生,需要再加一次。计算如下:
- 初始烟气排放:CO2_produced_initial = co2_chp * P_chp + co2_boiler * P_boiler
- 捕集量:G_cap(其中P2G消耗了W_p2g,封存为G_storage)
- 实际排放到大气 = (初始烟气未捕集的部分)+(外部购电间接排放)
- 循环燃烧再次排放:W_p2g → CHP燃烧重新排放
- 净碳排放 = CO2_produced_initial - G_cap + CO2_grid + W_p2g
等价于:
code复制CO2_net = (CO2_flue - G_cap) + CO2_grid + CO2_p2g_reburn;
其中CO2_p2g_reburn等于P2G产生的天然气被CHP或锅炉燃烧后产生的CO2,但注意,这部分天然气如果不产自P2G,本来就需要外购并燃烧,届时同样会排放。为体现协同减排效果,比较基准应该是“无P2G、无碳捕集”的系统。论文里通常会直接给出碳排放成本目标表达式的具体形式,复现时严格按公式走即可,逻辑上不要自作聪明加减。
4.4 求解循环与结果绘图
ε约束法的核心循环跑完后,输出两个向量:pareto_opex和pareto_carbon,直接plot就是帕累托前沿。要注意的是,每一个前沿点都对应着一组完整调度方案,可以挑几个特征点(左端点、右端点、拐点)把典型的24小时机组出力曲线画出来,便于后续分析。
绘图用fill函数把帕累托前沿凹下去的包络区域标出来,效果比散点图直观。需要注意的是,如果某次循环求解返回warnings(比如infeasible或numerical issue),要把这个点单独标出来,不要悄悄丢掉。ε接近C_min时出现无解是正常现象,前提是C_min本身比预期值大了1e-6之类的数值容差,但如果从某个ε开始连续十多个点全部无解,那就说明下界求错了,回去检查碳排放成本目标式的符号。
求解循环的完整代码如下:
code复制N = 30;
pareto_opex = nan(1, N);
pareto_carbon = nan(1, N);
epsilon_list = linspace(C_max, C_min, N);
for k = 1:N
cons_k = [cons, C_carbon <= epsilon_list(k)];
diagnostics = optimize(cons_k, C_opex, ops);
if diagnostics.problem == 0
pareto_opex(k) = value(C_opex);
pareto_carbon(k) = value(C_carbon);
else
fprintf('k=%d infeasible, epsilon=%.4f\n', k, epsilon_list(k));
end
end
figure;
plot(pareto_carbon, pareto_opex, 'o-', 'LineWidth', 1.5);
xlabel('碳排放成本 (元)');
ylabel('运维成本 (元)');
grid on;
4.5 变量初始化与求解性能优化
风力、光伏、负荷的时序数据用原文或公开数据集替换。求解性能方面,我实测在24时段、30个ε点下,总求解时间大约35秒,如果卡到几分钟以上,优先检查是否引入了非必要的非线性约束。比较隐蔽的是碳捕集能耗和捕集量之间的乘积关系,如果写成连续变量相乘,问题就变成二次约束规划,Gurobi求解MIQP反而慢。这里我直接将λ_ccs处理为常数,因此P_ccs = lambda_ccs * G_cap保持线性,对大电网下lp收敛极其友好。
5. 复现过程中最值得记录的五个坑
5.1 YALMIP变量类型推断导致的求解卡死
YALMIP有一个习惯:如果你定义sdpvar时没有显式注明类型,它可能在求解器转换阶段自动推导为二进制变量。我在复现时曾把P_p2g写成了sdpvar(T, 1),没有加'full'参数,结果Gurobi把它当成连续变量倒没问题,但当碳排放约束收得很紧时,求解器开始在P2G启用状态附近反复试探,耗时特别长。后来我把所有变量统一按1×24行向量、显式'full'声明,问题立刻消失,耗时缩短了一个数量级。如果你遇到“为什么求解器解这么慢”,先检查变量维度和YALMIP的变量类型推断日志。
5.2 ε取值区间不对,帕累托前沿不完整
这个问题几乎人人都会遇到。ε区间取错有两种表现:一种是区间过宽,导致前几个点无解、后几个点全是最小排放值;另一种是区间过窄,前沿被迫截断,看起来很完整实际丢失了极端点。
要确定C_min和C_max,必须先保证单目标优化时目标函数的量纲和约束中C_carbon的量纲完全一致。我把碳排放成本统一折算成元(而不是吨),而约束里写的是元,这样C_cc在数值上直接可比。如果按吨写,ε序列也要按吨区间生成,一旦混用,前沿会变成一条水平线或垂直线。
5.3 单位混乱:kW与MW、热值与电能混用
这个坑非常隐蔽,因为Matlab不会报错。划重点:
- 外购电的单位是kW,但P2G产气的热功率单位也是kW。
- 天然气买卖按热功率(kWh)计,但CO2质量按kg计,合成天然气时能量与CO2质量之间的换算要乘以密度和热值。
- 如果风力、光伏按MW输入而负荷按kW输入,电平衡等式两边数值差1000倍,但求解器依然能解出一个荒谬结果,且目标函数值看起来正常。
我的建议是设置一个base基准值,所有功率量纲统一折算成同一个基准的标幺值,或者干脆全部用kW和kWh,坚决不混用。所有换算常数在参数表里用注释写明来源,便于逐行核对。
5.4 热负荷平衡容易忽略的“热电比单点”陷阱
很多论文复现只给CHP一个固定的热电比,比如1.3,然后直接把电出力和热出力等同起来,这种做法对含P2G的系统特别危险。因为P2G耗电后,如果靠CHP增加电出力来满足电平衡,热出力也跟着增加,而热负荷不一定能消纳这么多热,就会导致系统必须“弃热”或启动电锅炉来耗热。
原文如果考虑热储或者电锅炉,这些设备要在模型里体现。如果原文完全不考虑热力松弛,你只能通过给CHP一个可行域(而不是固定热电比)来让模型自动折中。我复现时把CHP建模成可以调整热电比的抽凝机组,帕累托前沿才终于跑出了下凹段。
5.5 碳流方向的符号问题
碳流在P2G与碳捕集之间的方向,看起来简单,但写进约束时非常容易出错。很多人直接写:
code复制G_cap <= W_p2g + W_storage
从物理意义上说不通。捕集下来的CO2全部被P2G和封存分掉,应该写成:
code复制W_p2g + W_storage == G_cap
这样才不会把“捕集量”和“P2G消耗量”混在一个不等号里。同时,W_p2g有上限,取决于P2G的转化效率与合成天然气需求量;如果W_p2g超过了G_cap,剩余CO2要从外部补充,这在系统里一般不允许,因此再加上W_p2g <= G_cap,最终才是合理的物理约束。
另外还有一个隐藏约束:只有捕集下来的CO2才能给P2G用。如果模型允许P2G直接购买外部CO2,问题就变了性质,变成CCU(碳捕集利用)和CCS之间的选择,这不是本文的模型设定,别加这条。
6. 复现完成后如何自我验证
6.1 用边界条件检验模型逻辑
模型跑通后先别急着画图。先做一个最简单的检验:把碳排放成本目标从约束中放开(ε取C_max),此时结果应该等于纯运维成本单目标优化的结果,这个值你在第2章已经算过,直接对比即可。如果不一样,说明主循环里目标函数或约束复制错了。
再做一个检验:把P2G和碳捕集设备的容量全部设为0,模型应退化为“电网+气网+CHP+锅炉”的传统热电联供系统,电平衡和热平衡依然成立,碳排放成本变成碳税乘以原始排放量。跑一遍看看有没有数值异常,尤其是热平衡,这是最快找出模型低级错误的方法。
6.2 帕累托前沿的收敛性
用N=10、20、30分别跑三遍,看前沿是否随N增大而变密但不变形。如果N=10时的前沿端点与N=30时明显不同,说明个别ε点求解不彻底或提前终止了,需要调大Gurobi的MIPGap参数。一般综合能源系统MILP模型规模不大,完全可以设成1e-4甚至0,让Gurobi达到全局最优。
还有一点,别忘了看求解日志里是否有数值警告。Gurobi的警告大多数情况下不影响结果,但如果连续出现“numerical trouble”或“feasibility”警告,就要回去检查是否存在极大极小数值共存的约束,比如CO2质量(量级在几百kg)与电能(量级在几千kW)相乘,可以考虑给质量再乘一个比例因子,让所有约束数值落在0.01到10000之间。
6.3 和原文结果能否对应上
一区文章的复现,很难做到数值完全一样,原因有三:原文用的电价曲线、负荷曲线可能没给全;原文某些效率值给的是一个范围,你取的是中间值;求解器版本和MIP gap不同。所以对比时看三点即可:
- 趋势是否一致:碳排放成本上升时运维成本怎么变化,应该几乎单调递增。
- 拐点位置是否接近:帕累托前沿的转折点(斜率突变处)在原文中的位置,决定了两目标权衡的临界点。
- 极端场景的调度策略是否合理:左端点(碳排放最小)应该出现P2G大功率运行+碳捕集满负荷,右端点(运维成本最小)应该出现CHP几乎满发。
如果这三条都满足,复现就算基本成功。非要追求完全一致的话,就得把原文审稿附件、数据附表都翻出来,逐项比对参数,那是另一个维度的工作了。
按我个人的体会,这种带双目标优化与碳捕集协同的综合能源系统复现,最耗时的地方从来不是ε约束法的代码,而是把碳流平衡、CHP可行域、单位统一几件事理顺。模型本身不算复杂,属于标准MILP框架,一旦你理解了两个目标的冲突来源,后面的求解只是重复劳动。希望这份复现笔记能让你少踩几个我踩过的坑,尤其是第5节那五个点,大多数跑不出完整帕累托前沿的问题,最后都能追溯到这五个原因上。
