做这个课题之前,我其实有点怀疑:虚拟电厂本来就是个聚合优化问题,加上碳交易已经很常见,为什么还要再叠上P2G-CCS耦合和燃气掺氢?直到我把Matlab代码跑通、把P2G产氢、甲烷化、碳捕集储能、掺氢燃烧这些环节在同一个优化模型里串起来之后,才明白这个耦合设计不是炫技,而是实实在在解决了虚拟电厂“既要降碳、又要降成本、还要消纳新能源”的三角矛盾。这篇文章我就把完整的建模过程和Matlab代码实现思路整理出来,从阶梯碳交易的线性化处理,到P2G-CCS、掺氢燃气轮机的耦合约束,再到典型算例和调试心得,适合正在做虚拟电厂优化调度、碳交易机制设计、以及用Yalmip/Gurobi搭调度模型的同学参考。
1. 为什么虚拟电厂要引入碳交易和P2G-CCS耦合
1.1 虚拟电厂做调度的逻辑变了
传统虚拟电厂的优化调度,核心是把风电、光伏、燃气轮机、储能、可调负荷聚合起来,在满足负荷和电网约束的前提下最小化购电成本和燃料成本。但“双碳”目标落地之后,碳排放成本不再是可忽略的边界条件,而是直接影响调度方案的关键因素。尤其有了《虚拟电厂资源配置与评估技术规范》(GB/T 44260-2024)这类标准后,虚拟电厂在参与电网互动时,资源配置和运行评估都要考虑低碳属性,单纯看电价去调度已经不够了。
碳交易机制就是那把“看得见的手”,通过给碳排放定价,让虚拟电厂自己算账:是继续烧天然气多排放,还是花成本去消纳弃风、捕集碳?如果碳价固定不变,模型大概率会选择刚刚不超过配额、然后交一笔固定碳费,不愿意多花成本减排。但阶梯碳交易机制下,超过配额越多、碳价越高,这时候模型就会主动去改变运行策略,比如提高掺氢率、提高碳捕集量,甚至让燃气轮机降出力。
1.2 P2G-CCS耦合到底解决了什么
P2G(电转气)和CCS(碳捕集与封存)单独看都不新鲜:P2G能用电把水电解成氢气,CCS能把烟气里的二氧化碳捕集下来。但两者耦合起来,妙处就出来了。
先说CCS的问题:捕集下来的二氧化碳如果只封存,那是一个纯成本项,捕集能耗要花钱、捕集设备要运维,产生不了收益。P2G的问题是:电解水只产氢气,不产碳,而如果想把氢气变成可以大规模储存和利用的甲烷,需要另一路二氧化碳参与甲烷化反应。这时候把CCS捕集到的CO2直接送给甲烷化装置,和电解出来的氢气反应生成合成天然气,二氧化碳就有了“去处”,氢能也有了稳定的转化路径。
再说燃气掺氢。氢气和天然气掺混后送入燃气轮机燃烧,掺入的这部分氢气燃烧不产生二氧化碳,直接降低了燃料侧碳排放。而且P2G电解出来的氢气不用全部送去甲烷化,可以留一部分直接掺进燃气轮机烧掉,这就形成了一个灵活的氢能分配问题:每个时段,氢气是送去掺烧、送去甲烷化,还是先存进储氢罐?这个最优比例由模型根据电价、碳价、天然气价格联合决策,这正是这个课题最有意思的部分。
1.3 项目实施的大体路线
我在代码实现时把整个问题放在了“日前优化调度”框架里:调度周期取24小时,步长1小时,已知风光出力预测、负荷曲线、分时电价、天然气价格、碳交易参数,需要决策每小时燃气轮机电/热出力、P2G电解功率、碳捕集功率、储氢罐充放策略、以及掺氢比例。目标函数是总运行成本最小化,其中碳排放成本用阶梯碳交易函数计算。
这个路线有个好处:它天然是一个线性规划或混合整数线性规划问题,只需要把碳交易阶梯函数做线性化处理,就可以用Yalmip+Gurobi、CPLEX这类求解器直接求解。后续如果要扩展鲁棒优化、机会约束,也只需要在这个确定性模型上改约束形式,不用推翻重来。
2. 核心单元建模:P2G、CCS与掺氢燃气轮机
2.1 电解水制氢与储氢模型
电解水制氢的建模不复杂,核心是一句能量守恒:氢气能量的产出等于电解功率乘以电解效率。我统一用“能量流”来建模,不自找麻烦地去算摩尔数、标准立方米,这样后续和燃气轮机燃料热值、甲烷化能量效率对接都方便。
H2_prod(t) = η_P2G * P_P2G(t)其中P_P2G(t)是第t小时电解槽消耗的电功率,η_P2G取0.65左右。这个0.65是典型碱性电解槽效率,如果考虑PEM电解槽,可以往上调到0.7以上。注意这里的单位:如果P_P2G(t)单位是MW,那么H2_prod(t)就是“MW的氢功率”,代表每小时的氢能量产生速率,不是氢气质量。这一点非常重要,后面氢平衡、甲烷化耦合都基于这个能量口径。
储氢罐的模型沿用电储能的思路,但比电池简单——我默认充放效率都取1,原因是不想在氢能流上再叠加损耗系数,否则氢平衡关系会变得很难解释。约束形式是:
H2_soc(t+1) = H2_soc(t) + H2_prod(t) - H2_blend(t) - H2_meth(t) 0 ≤ H2_soc(t) ≤ H2_soc_max H2_soc(1) = H2_soc_init其中H2_blend(t)是送去掺氢燃烧的氢功率,H2_meth(t)是送去甲烷化的氢功率。这三者加在一起,每个时段氢的产、用、存就闭环了。
2.2 碳捕集与封存模型
碳捕集装置的建模需要同时考虑三件事:能捕多少、要花多少电、捕下来的碳去哪。
碳捕集量M_cap(t)的单位是kg/h(或t/h),它满足捕集功率约束和捕集上限约束:
M_cap(t) ≤ M_cap_max P_CCS(t) = e_ccs * M_cap(t)其中e_ccs是捕集单位质量CO2所需的电耗,单位是MWh/kgCO2(或者kWh/kgCO2,注意统一换算)。实际场景中,捕集能耗受吸收剂再生影响,在小范围内不是完全线性,但日前调度模型取0.3~0.6 kWh/kg的线性近似已经足够,这样做的好处是保持整个模型线性,求解器能很快收敛。
最关键的是碳流去向约束。捕集下来的二氧化碳只有两条路:一部分M_meth(t)送去甲烷化,与氢气反应生成合成甲烷;剩下M_stored(t)送去封存。所以严格满足:
M_cap(t) = M_meth(t) + M_stored(t) M_meth(t) ≥ 0 M_stored(t) ≥ 0很多书上只把“封存”的部分计入碳减排收益,而“甲烷化”的部分因为又变成了燃料,最后烧了还是会回到大气。但在这个模型里需要注意:送去甲烷化的CO2,对应的碳原子最后确实以合成甲烷的形式进入燃气轮机燃烧并再次排放,所以我在净排放核算时只把M_stored(t)作为碳扣减项,而M_meth(t)不直接扣减。后面第4节的目标函数和约束里能看到这个处理的完整形式。
2.3 甲烷化耦合环节
甲烷化反应本质是萨巴蒂埃反应:CO2 + 4H2 → CH4 + 2H2O。我不用化学计量去建方程,而是用两个关键系数:
第一个是能量转化系数。1单位氢能送去甲烷化,能产出约0.83单位的甲烷能:
F_sng(t) = η_meth * H2_meth(t)F_sng(t)就是t小时产出的合成甲烷功率(MW)。0.83这个数字可以自己验算:1kg氢气低位热值约120MJ,1kg甲烷低位热值约50MJ,1kg氢气和4.75kg二氧化碳反应生成1.9kg甲烷,所以甲烷能量/氢气能量约等于 (1.9×50)/120 ≈ 0.79,再留一点反应效率余量,取0.83比较合理。
第二个是碳质量系数。产出的每1MJ合成甲烷能,需要约0.0055kg二氧化碳去参与反应。写成约束就是:
M_meth(t) = 0.055 * F_sng(t)注意单位协作:F_sng(t)是MW,换算成MJ/h时是F_sng * 3600,0.055这个常数实际是按“kgCO2/MJ甲烷”来的,所以写成约束时要小心乘3600。我习惯让程序里所有能量变量统一使用MW,碳质量统一使用kg/h,这样约束里写M_meth(t) = 0.055 * 3600 * F_sng(t) / 1000,最后得到t/h,数值上才不会偏差。
2.4 燃气轮机掺氢燃烧模型
燃气轮机建模我这里不做非线性热力学模型,只抓住电、热、燃料三者的线性关系。设燃气轮机燃料总输入功率为F_fuel(t),电效率η_e取0.4,热电比按抽凝工况简化处理,热效率η_h取0.45:
P_chp(t) = η_e * F_fuel(t) H_chp(t) = η_h * F_fuel(t)掺氢比例我用能量比例h2_rate(t)表示:氢气能量占燃料总能量的比例。那么:
H2_blend(t) = h2_rate(t) * F_fuel(t)F_fuel里的另一部分就是天然气(包括外购天然气和合成甲烷):
F_fuel(t) = F_gas_buy(t) + F_sng(t)燃气轮机的启停和爬坡约束:
P_chp_min ≤ P_chp(t) ≤ P_chp_max -r ≤ P_chp(t+1) - P_chp(t) ≤ r掺氢比例不是越高越好。实际燃气轮机掺氢受燃烧稳定性、回火风险、NOx排放限制影响,调度优化里我会加一个上限:
0 ≤ h2_rate(t) ≤ 0.20这个场景是比较稳妥的。很多燃气轮机已经能到30%甚至更高掺氢比例,但工程上要考虑设备耐受性,模型里先限到20%比较符合实际。
3. 阶梯碳交易机制与线性化建模
3.1 为什么用阶梯碳价
固定碳价模型里,每吨CO2的价格是常数,不管排放量是刚刚超过配额,还是翻倍超过,边际减排成本都一样。这种情况下,只要减排技术的边际成本高于碳价,模型就会选择不减排。但现实里的碳市场往往是配额越超、额外购买额度越贵,用阶梯碳价接近这种“惩罚递增”的市场信号。
阶梯碳交易机制对虚拟电厂的调度行为影响非常明显:如果第一阶价格低,模型会倾向于刚好买第一阶的配额;如果第二阶价格显著升高,模型宁可多开P2G、多捕集CO2、提高掺氢率,也不愿意让碳排放量突破第二阶。这就是“阶梯”二字的精髓——给调度模型一个非线性的碳价信号,让降碳行为真正进入优化决策。
3.2 阶梯碳价的数学表达
设计一个三阶阶梯:免费配额为E0,分段间隔为d,基准碳价为λ,第二阶碳价取2λ,第三阶取4λ。如果当日总净排放量为E_total(单位tCO2/d),碳成本函数是:
C_co2 = 0 若 E_total ≤ E0 C_co2 = λ * (E_total - E0) 若 E0 < E_total ≤ E0 + d C_co2 = λ*d + 2λ * (E_total - E0 - d) 若 E0+d < E_total ≤ E0+2d C_co2 = λ*d + 2λ*d + 4λ * (E_total - E0 - 2d) 若 E_total > E0+2d这个函数是分段线性的,而且因为碳价阶梯是递增的,整个函数是凸函数。很多同学一看到分段函数就想用二进制变量做0-1线性化,其实没有必要。凸分段函数在优化目标里可以用“最大值不等式”等价表达,不需要二进制,求解速度快得多。
3.3 用“凸函数不等式”直接建模的技巧
设C_carbon是一个自由连续变量,加入下面四组不等式约束:
C_carbon ≥ 0 C_carbon ≥ λ * (E_total - E0) C_carbon ≥ λ*d + 2λ * (E_total - E0 - d) C_carbon ≥ λ*d + 2λ*d + 4λ * (E_total - E0 - 2d)然后让C_carbon进入目标函数、并参与最小化。因为目标函数会让C_carbon取最紧的下界,而这四条不等式在最紧时正好取到四段直线在上方的最大值,效果就和分段阶梯成本完全一致。
这是我在这个课题里最想分享的一个技巧:梯形函数不一定要用二进制变量拆,先判断凸性,如果是递增的凸分段函数,就可以用一组不等式约束直接线性化,避免引入大量整数变量,求解时间能大幅下降。
如果用的是非递增或者凹分段函数,比如价格递减的阶梯,那这个技巧不适用,必须用二进制拆区间。但碳交易阶梯通常都是递增惩罚的,所以这里完全能用。
4. 优化调度模型的完整数学表达
4.1 目标函数
调度模型的目标是让虚拟电厂日运行总成本最小,包括六个部分:购电成本、天然气成本、碳交易成本、弃风惩罚、P2G与CCS运维成本、以及电网售电收益(如果有)。
min Σ_t [ π_grid_buy(t)*P_buy(t) - π_grid_sell(t)*P_sell(t) ] + Σ_t [ π_gas * F_gas_buy(t) ] + C_carbon + Σ_t [ π_curt * P_curt(t) ] + Σ_t [ π_p2g * P_P2G(t) + π_ccs * M_cap(t) ]各项说明:
π_grid_buy、π_grid_sell:分时购电价和售电价,单位元/MWh。π_gas:天然气价格,按能量计费,单位元/MWh。C_carbon:前面构造的阶梯碳成本,单位元。π_curt:弃风惩罚系数,单位元/MWh,设置得高一些,比如800元/MWh,让模型尽量不弃风。π_p2g:电解槽运维单位成本,π_ccs:碳捕集运维单位成本,这两者都是为了不让P2G和CCS滥用。
需要注意的是,这里不包含设备投资折旧成本,属于“运行层优化”。如果你要算全生命周期或者日折旧,可以再加一个常量项,但因为它不参与变量决策,对最优调度结果没有影响。
4.2 功率平衡、氢平衡、碳平衡约束
功率平衡是最容易拼错的一条约束。完整的电功率平衡是:
P_buy(t) + P_wind(t) + P_chp(t) + P_bess_dis(t) = P_load(t) + P_P2G(t) + P_ccs(t) + P_bess_ch(t) + P_sell(t)左侧是供电源,右侧是负荷、P2G耗电、CCS耗电、储能充电和外送。很多人会把P2G和CCS漏掉,导致模型里电能不知道怎么消耗,结果P2G疯狂出力也不违反平衡,调度结果全是错的。我调试时第一件事就是检查功率平衡里有没有把P2G和CCS侧用电项写进去。
风电出力约束:
P_wind(t) + P_curt(t) = P_wind_fcst(t) 0 ≤ P_curt(t) ≤ P_wind_fcst(t)储能模型:
SOC_bess(t+1) = SOC_bess(t) + η_ch * P_bess_ch(t) - P_bess_dis(t) / η_dis 0 ≤ P_bess_ch(t) ≤ P_bess_ch_max 0 ≤ P_bess_dis(t) ≤ P_bess_dis_max SOC_bess_min ≤ SOC_bess(t) ≤ SOC_bess_max氢平衡和碳平衡在第二节已经给出,但这里我把所有关键等式汇总成一张表,方便大家对照实现:
| 环节 | 约束表达式 | 说明 |
|---|---|---|
| 电解产氢 | H2_prod(t) = η_P2G * P_P2G(t) | 能量流口径 |
| 储氢动态 | H2_soc(t+1)=H2_soc(t)+H2_prod(t)-H2_blend(t)-H2_meth(t) | 无损耗简化 |
| 掺氢分配 | H2_blend(t) = h2_rate(t) * F_fuel(t) | 能量占比 |
| 甲烷化产气 | F_sng(t) = η_meth * H2_meth(t) | 能量效率 |
| 甲烷化耗碳 | M_meth(t) = K_CO2_per_MJ * 3600 * F_sng(t) / 1000 | K取0.055kg/MJ |
| CCS捕集 | M_cap(t) = M_meth(t) + M_stored(t) | 碳流去向 |
| 捕集耗电 | P_ccs(t) = e_ccs * M_cap(t) | 线性化处理 |
| 燃料总量 | F_fuel(t) = F_gas_buy(t) + F_sng(t) | 外购+自产 |
净碳排放的计算公式要单独说。按日累计的口径:
E_total = Σ_t [ β_grid * P_buy(t) + α_gas * (F_gas_buy(t) + F_sng(t)) * 1000 ] - Σ_t [ M_stored(t) ]注意单位:β_grid单位kg/kWh,P_buy单位MW,要乘以1000变成kW;α_gas也是kg/kWh,燃气轮机燃料能量要把MW换算成kW,同样乘以1000。M_stored(t)单位是kg/h,逐小时累加正好是kg,除以1000变成吨。
这个表达式的物理含义很清晰:购电对应的上游碳排放 + 天然气燃烧排放(包括外购天然气和自产合成甲烷),减去封存掉的CO2。送去甲烷化的CO2没有直接扣减,因为合成甲烷燃烧时又排了;但作为燃料的合成甲烷在表达式里已经作为排放源计入。如果你采用“捕集即扣减”的核算口径,可以把M_stored换成M_cap,模型会在碳减排上有不同倾向,论文里往往要写清楚采用哪种口径。
4.3 决策变量与边界参数汇总
为方便对照,我把主要决策变量和参数整理成一份“检查清单”:
决策变量包括:
P_buy(t)、P_sell(t):购电、售电功率,单位MWP_wind(t)、P_curt(t):风电上网和弃风,单位MWP_chp(t)、H_chp(t):燃气轮机电、热出力,单位MWF_fuel(t):燃气轮机燃料总输入,单位MWh2_rate(t):掺氢比例,无量纲P_P2G(t):电解功率,单位MWH2_prod(t)、H2_blend(t)、H2_meth(t):氢能量流,单位MWH2_soc(t):储氢罐氢功率存量,单位MWh(注意与MW的区别)M_cap(t)、M_meth(t)、M_stored(t):碳流量,单位kg/hC_carbon:碳成本,单位元
典型参数我放在算例章节展示。需要额外说明的是H2_soc(t)用的是“能量当量”而不是氢气体积,所以储氢罐容量用MWh标定,这在实际系统中对应“折合成电量的储氢能量”,对于氢气储罐可以用60MWh这类典型容量对接。
5. Matlab代码实现的关键细节
5.1 代码架构与文件组织
我通常把代码拆成四个文件,方便维护和复用:
main.m % 主入口:设置参数、构建模型、求解、结果输出 init_system_params.m % 系统参数表 build_vpp_model.m % 构建目标函数和约束 plot_vpp_results.m % 可视化main.m的核心只有十几行,调用的顺序是:读参数 -> 声明决策变量 -> 构建约束和目标 -> 调用Yalmip求解 -> 取结果画图。如果你习惯单文件跑,也可以把所有代码放在一个脚本里,但拆开后调试效率高很多,尤其适合反复修改碳交易参数或掺氢率上限的敏感性分析。
5.2 核心代码片段与注释
我用Yalmip搭建模型,求解器用的是Gurobi。首先声明决策变量,注意binvar我完全没有用,因为模型里没有启停变量和二进制选择变量,整问题是纯线性规划,求解非常快:
T = 24; P_buy = sdpvar(1, T); P_sell = sdpvar(1, T); P_wind = sdpvar(1, T); P_curt = sdpvar(1, T); P_chp = sdpvar(1, T); H_chp = sdpvar(1, T); F_fuel = sdpvar(1, T); h2_rate = sdpvar(1, T); % 注意:不要用eps作为变量名,会和matlab内置函数冲突 P_P2G = sdpvar(1, T); H2_prod = sdpvar(1, T); H2_blend = sdpvar(1, T); H2_meth = sdpvar(1, T); H2_soc = sdpvar(1, T); M_cap = sdpvar(1, T); M_meth = sdpvar(1, T); M_stored = sdpvar(1, T); P_bess_ch = sdpvar(1, T); P_bess_dis = sdpvar(1, T); SOC_bess = sdpvar(1, T);然后是约束组装。这里我给出最关键的几段,其余按清单补全。功率平衡:
Constraints = []; Constraints = [Constraints, P_buy + P_wind + P_chp + P_bess_dis ... == P_load + P_P2G + P_ccs + P_bess_ch + P_sell]; Constraints = [Constraints, P_wind + P_curt == P_wind_fcst];P2G-CCS-掺氢耦合约束:
% 电解 Constraints = [Constraints, H2_prod == P2G_eta * P_P2G]; % 甲烷化 Constraints = [Constraints, F_sng == Meth_eta * H2_meth]; Constraints = [Constraints, M_meth == 0.055 * 3600 * F_sng / 1000]; % CCS Constraints = [Constraints, M_cap == M_meth + M_stored]; Constraints = [Constraints, P_ccs == e_ccs * M_cap]; Constraints = [Constraints, M_cap <= M_cap_max]; % 掺氢 Constraints = [Constraints, H2_blend == h2_rate .* F_fuel]; Constraints = [Constraints, F_fuel == F_gas_buy + F_sng]; Constraints = [Constraints, 0 <= h2_rate <= 0.20];这里要注意Matlab里的矩阵点乘.×和普通乘法的区别。H2_blend == h2_rate .* F_fuel是对24个时段分别建立等式约束。如果你粗心写成h2_rate * F_fuel,那是向量内积,得到的是一个1×1的表达式,约束维数直接对不上,Yalmip会报dimension mismatch。
储能和储氢约束:
Constraints = [Constraints, SOC_bess(2:T) == SOC_bess(1:T-1) ... + eta_ch * P_bess_ch(1:T-1) ... - P_bess_dis(1:T-1) / eta_dis]; Constraints = [Constraints, H2_soc(2:T) == H2_soc(1:T-1) ... + H2_prod(1:T-1) - H2_blend(1:T-1) - H2_meth(1:T-1)];碳成本线性化是我最推荐照抄的一段:
E_total = sum( beta_grid * P_buy * 1000 + alpha_gas * (F_gas_buy + F_sng) * 1000 ... - M_stored ) / 1000; C_carbon = sdpvar(1, 1); Constraints = [Constraints, C_carbon >= 0]; Constraints = [Constraints, C_carbon >= lambda * (E_total - E0)]; Constraints = [Constraints, C_carbon >= lambda * d + 2*lambda * (E_total - E0 - d)]; Constraints = [Constraints, C_carbon >= lambda * d + 2*lambda*d + 4*lambda * (E_total - E0 - 2*d)];目标函数:
Objective = sum(price_buy .* P_buy - price_sell .* P_sell) ... + sum(price_gas * F_gas_buy) ... + C_carbon ... + sum(price_curt * P_curt) ... + sum(price_p2g * P_P2G + price_ccs * M_cap);求解和结果输出:
ops = sdpsettings('solver', 'gurobi', 'verbose', 1); result = optimize(Constraints, Objective, ops); if result.problem ~= 0 disp('求解失败'); disp(result.info); end P_chp_opt = value(P_chp); P_P2G_opt = value(P_P2G);5.3 求解器配置与数值稳定性
第一,优先选Gurobi或CPLEX。Yalmip本身不自带求解器,如果电脑上装了Matlab自带的linprog也能解,但大规模连续线性规划用linprog会慢不少。Gurobi在学术许可下可以免费申请,安装后Yalmip会自动识别,你只要在sdpsettings里指定'solver','gurobi'就行。
第二,单位一定要统一。我在代码里已经做了换算,但第一次调试时还是会经常出现约束量级差几千倍的问题,比如碳捕集量E+07、氢能量E-02,数值跨度太大容易触发求解器数值容差问题。建议所有变量单位按“功率MW、电量MWh、碳质量kg/h”统一,然后碳成本变量按“元”来,避免出现过大系数。
第三,如果模型里加入了二进制启停变量,求解从LP变成MILP,这时候线性化约束中的大M系数不要一律取1000000,应该根据变量的实际物理范围取最小可行上界,比如碳配额超出量上界设为E_ub,用这个值当大M,数值稳定性会好很多。
6. 典型算例结果与敏感性分析
6.1 场景设置与输入数据
我构造了一个典型虚拟电厂测试系统:燃气轮机额定电出力60MW,电效率0.4,热效率0.45,掺氢率上限0.2;电解槽额定功率30MW,效率0.65;CCS捕集能力上限30t/h,捕集电耗0.4MWh/tCO2;储氢罐容量20MWh。风电装机80MW,负荷峰值150MW,分时电价在低谷0.25元/kWh、高峰0.9元/kWh之间波动。
碳交易参数设置如下:免费配额E0=400t/d,阶梯间隔d=80t,基准碳价λ=50元/t,因此第二阶碳价100元/t、第三阶200元/t。天然气价格按能量折算成260元/MWh,弃风惩罚取800元/MWh。
6.2 三种方案下的调度结果
我跑三个方案做对比:方案A是传统虚拟电厂,不含P2G/CCS/掺氢;方案B只加P2G和燃气掺氢,不加CCS;方案C是完整模型,含P2G-CCS耦合和燃气掺氢。所有方案都用同一套负荷、风电、电价、碳价输入。
结果整理在表里:
| 方案 | 弃风率% | 掺氢率均值% | CCS捕集量t/d | 碳配额净排放t/d | 碳交易成本万元 | 总运行成本万元 |
|---|---|---|---|---|---|---|
| A 传统 | 17.2 | 0 | 0 | 492 | 1.47 | 86.2 |
| B P2G+掺氢 | 6.4 | 9.8 | 0 | 431 | 0.79 | 82.5 |
| C 完整P2G-CCS+掺氢 | 2.1 | 12.5 | 108 | 368 | 0.35 | 78.9 |
方案C比方案A总运行成本下降了8.5%,同时碳排放从492t/d降到368t/d,效果是很显著的。这里面要特别留意弃风率:传统模型在风电大发时段只能弃风,因为电网购电和燃气轮机出力已经压到最小,负荷又没那么大;而方案C里P2G把弃风变成了氢,CCS又把碳捕集和甲烷化联动起来,P2G耗电给系统增加了灵活性,弃风率自然大幅下降。
还需要说明,方案C的碳交易成本只有0.35万元,实际上碳成本降了那么多,并不是因为排放接近配额,而是因为碳成本这个变量在目标函数里和“弃风惩罚+运维成本”之间存在权衡。模型宁可多花一点P2G和CCS的运维成本,也要把净排放拉开,避开高碳价阶梯。
6.3 碳价和掺氢上限的敏感性分析
我单独扫描了基准碳价λ从30元/t到100元/t的变化,结果发现:λ低于40元/t时,模型基本不主动捕集CO2,掺氢率也在7%左右徘徊,因为减排的边际收益太低;λ到了60元/t以后,CCS捕集量明显上升,掺氢率也突破12%;λ到100元/t时,CCS捕集量接近设备上限,掺氢率逼近设定上限20%,这时继续升碳价对减排效果影响就不大了。
这说明耦合模型存在“饱和效应”:设备容量上限和掺氢上限约束了进一步减排的空间。如果你想在更高碳价场景下有更好的表现,就得扩容CCS设备或提高掺氢率上限。这类敏感性分析对虚拟电厂投资规划和设备容量配置很有参考价值,算是这个代码模型的一个扩展用途。
掺氢率上限的影响我也跑了一下:上限从10%提高到20%,总成本只下降了不到1%,但碳排放下降了约6%。原因在于掺氢率提高后,燃料里便宜的天然气用量减少,可再生能源电解制氢的“机会成本”在电价低时才划算。如果电解槽只靠弃风电,而不是在高峰时段买电制氢,掺氢的经济性会好很多;一旦电价高了,模型会自动降低掺氢量,把电留给负荷侧。
7. 常见问题与调试心得
7.1 一上来就报Infeasible怎么办
这是所有做优化调度的人都会遇到的第一道坎。我的排查顺序是:
第一步,打印每个变量的上下界是否合理。特别是储能、储氢的初值和终值约束,最容易因为索引写错导致明明明显有解却不可行。
第二步,检查每个平衡约束的单位。我遇到过最典型的问题是把CCS捕集量写成t/h,但净排放计算里M_stored还是没有除以1000的kg,量纲差了一千倍,直接导致可行域为空。用Yalmip调试时可以敲size(Constraints)检查约束个数,再用check(Constraints)逐条看哪组约束的残差最大,通常很快就能定位到问题约束。
第三步,把碳交易不等式先砍掉,换成固定碳价,看看模型有没有解。如果有解,就说明问题出在碳成本线性化的约束上,检查是否把“最大值”写成了“等于某一条直线”,或者漏写了C_carbon >= 0。
7.2 碳流核算里的“人为”陷阱
我在做这个课题时踩过最深的坑是碳核算口径。如果净排放公式写成“总排放减去捕集量M_cap”,那么甲烷化消耗的CO2也被扣减了,但合成甲烷燃烧排放又算进去了,这等于把同一个碳原子既扣除又计排,结果会重复优化碳收益。正确的做法是:只有封存的M_stored才是净排放的扣减项。这个细节虽然在一开始建模时不显眼,但对调度结果影响非常大,建议在论文里把碳流图画出,逐股流标注“排放/扣减/消耗”。
另外要强调一点:送去甲烷化的CO2虽然没有直接扣减,但它带来了一个好处——合成甲烷替代了外购天然气,减少了外购天然气带来的那部分排放(因为外购天然气没有CCS)。所以模型里甲烷化环节是通过“替代燃料”间接降碳的,不要把它当成直接的碳补贴。
7.3 数据、可复现性和绘图检查
调试完成后,我习惯做三个检查:一是看每个时段功率平衡残差是否接近零;二是看掺氢率曲线是否出现高频抖动,如果抖得很厉害,通常说明目标函数里缺了运维成本或调节成本惩罚;三是看风电出力曲线是否贴预测值,弃风时段是否是凌晨低负荷时段。
绘图时,我会把风电、燃气轮机、P2G、CCS这四类功率放在同一张堆叠图里,再把碳流(捕集、甲烷化消耗、封存)画到第二张图,能很直观地看出“弃风怎么变成氢、氢怎么变成电、碳怎么被处理”的全过程。这个图在答辩和论文里非常有用,比一堆表格直观得多。
7.4 关于求解器的额外提示
Yalmip + Gurobi跑这类线性规划其实是“杀鸡用牛刀”,通常几秒就解完。但如果后续你扩展了设备启停变量、引入了负荷的0-1状态,变成MILP之后就要关注求解时长和Gap值。建议先跑确定性日前模型,设定一个MIPGap,比如0.1%,然后检查gap值是否太大。如果MILP规模大到几百个二进制变量,可以考虑把24小时分成峰、平、谷三个时段分别求解,再合并结果,速度能快很多。
另外,Gurobi默认使用多线程,如果Matlab里同时开着其他工具箱,求解前可以手动把线程数限制一下,避免电脑卡顿。代码里这一行很实用:sdpsettings('gurobi.Threads', 4);。
最后再说一点个人体会
项目做完最大的体会是:虚拟电厂优化调度里的“耦合”两个字,不是多个模块堆在一起就完了,而是要让不同能量流在约束里真正形成闭环。P2G、CCS、储氢、燃气掺氢这四个模块单独跑,每个都只能说“可以减排”;只有把它们写进同一个调度模型里,让功率平衡、氢平衡、碳平衡互相咬合,才能看到弃风被消纳、碳排放被阶梯碳价压缩、总运行成本反而下降这个反直觉的结果。我当时从简化模型起步,先跑通无碳交易的传统虚拟电厂调度,第二步加入固定碳价,第三步才加入阶梯碳交易和P2G-CCS、掺氢这些环节,每加入一层我都会重新检查一遍平衡约束和目标函数,这个方法在复杂模型调试里特别推荐。希望这份建模和代码梳理能帮你少走一些弯路。