ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

阶梯碳交易下含P2G-CCS与燃气掺氢的虚拟电厂优化调度

阶梯碳交易下含P2G-CCS与燃气掺氢的虚拟电厂优化调度 最近在复现一个课题就是标题里这串字基于阶梯碳交易的含 P2G-CCS 耦合和燃气掺氢的虚拟电厂优化调度。名字又长又绕拆开看其实就是四件事虚拟电厂、阶梯碳交易、P2G-CCS 耦合、燃气掺氢。说白了就是在一个园区级的虚拟电厂里把风电、光伏、燃气轮机、储能、电转气、碳捕集这些设备打包成整体在分时电价和碳交易的双重压力下求一个“明天怎么发电、怎么储能、怎么买卖电”的最优方案。这个课题适合谁看如果你是电力系统、综合能源方向的研究生或者在做园区能源调度、多能互补平台的工程师甚至是对碳交易机制想做落地的开发同学都能从里面扒到不少东西。我这次用 Matlab 加 YALMIP 工具箱把整条链路完整跑通了从参数初始化、约束建模、求解器配置到结果分析踩过不少坑下面把这些经验全部摊开讲从“为什么要这么建模”到“代码怎么写”一步一步说清楚。1. 到底在优化什么把碳和氢装进虚拟电厂的调度模型1.1 虚拟电厂把分散的“小电源”聚合成一个“大电厂”虚拟电厂不是一座实体电厂它没有烟囱也没有汽轮机但它能像一个真实电厂一样对外响应调度。实现方式是把分布式风电、光伏、燃气轮机、储能、柔性负荷这些分散资源通过一个聚合平台统一管理。打个比方虚拟电厂就像是“电力版的打车平台”——平台不拥有车但能调动一堆车。在这个项目里虚拟电厂内部除了常见的光伏、风电、燃气轮机和储能还多了两套比较特殊的设备P2G电转气和 CCS碳捕集与封存。这两套设备让虚拟电厂除了“电”之外还多出了“气”和“碳”两条物料流。如果只做传统电功率平衡这些耦合关系根本体现不出来所以必须放在同一个优化模型里统一决策。这也是这一类虚拟电厂调度问题与普通微电网调度最大的区别决策变量不只是“发多少电、充多少电”还包括“制多少氢、捕多少碳、掺多少氢”。1.2 阶梯碳交易让碳排放的边际成本随排放量上升碳交易的概念大家应该不陌生政府给排放源分配免费碳排放配额实际排放低于配额可以把多余配额卖出去超过配额必须花钱购买碳排放权。早期模型里往往把碳价设成一个固定值比如每吨 50 元超排多少就按这个单价买多少。但现实中的碳市场不是这么简单。超排越多监管压力和减排成本越大碳价应该是递增的。阶梯碳交易机制借鉴了阶梯电价的思想把超排量分成几个区间落在不同区间的部分执行不同价格超得越多单价越贵。比如免费配额 300 吨超排 0200 吨按 50 元/吨200500 吨按 80 元/吨500 吨以上按 120 元/吨。这样目标函数里碳成本就是一个凸的分段线性函数优化器会自动尽量避免高排放运行方式。我在这个项目里对比过固定碳价和阶梯碳价两种情况。固定碳价下碳成本是线性项只要碳价不高系统往往倾向于“交钱了事”换成阶梯碳价后一旦排放量跨过某个阶梯边际成本突然跳升系统就会更积极地去启停 P2G、CCS 或者调整掺氢比例来压低净排放。这个机制直接决定了调度结果差异。1.3 P2G-CCS 耦合把 CO₂ 从“废物”变成“原料”P2G 是电转气的缩写核心是电解水制氢再进一步可以走甲烷化路线让氢和二氧化碳反应生成甲烷。CCS 是碳捕集把燃气轮机排烟里的 CO₂ 抓下来。单独看这两套设备都有点尴尬。P2G 的甲烷化环节需要 CO₂如果外购既花钱又不环保CCS 捕下来的 CO₂ 如果只封存有点浪费。把两者耦合起来逻辑就很顺了燃气轮机烧天然气发电排出 CO₂CCS 把烟气里的 CO₂ 捕集下来P2G 电解水制氢再把这部分捕集到的 CO₂ 与氢气合成甲烷甲烷和剩余氢气又回到燃气轮机里烧。这样就形成了一个“发电—排碳—捕碳—用碳—产气—再发电”的闭环。在调度模型里这个耦合还有一个很实际的价值CCS 的耗电量是可以调节的相当于一个柔性负荷。风光大发、电网消纳不了的时候可以多给 CCS 喂电多捕碳、多产甲烷相当于把原本要弃掉的风光电力变成了气体燃料储存起来。这个操作比单纯弃风弃光划算得多。1.4 燃气掺氢给燃气轮机注入“绿色氢”燃气轮机烧的是天然气如果按一定比例掺入氢气同样发一度电的碳排放会明显下降因为氢气本身不含碳。尤其当氢气是由富余风光电力通过 P2G 制出来的就相当于把“绿电”转化成了“绿氢”再送到燃气轮机里燃烧整条链路的低碳属性非常可观。但掺氢不是想掺多少就掺多少。氢气的体积热值比天然气低掺进去之后混合燃料的热值会变超过一定比例燃烧特性会变化燃气轮机的出力和效率都需要修正。论文和工程实践里常见的掺氢比例上限在 10%30% 之间部分先进机型可以更高。在调度模型中掺氢比例可以设为决策变量也可以作为固定参数做敏感性分析。我在这个项目里作为敏感性参数分别跑过 0% 到 20% 的掺氢比例后面会具体说结果。2. 模型怎么建目标函数与关键约束的数学表达2.1 目标函数运行成本与碳成本的平衡整个优化问题的目标很直白在一个调度周期一般是 24 小时内让虚拟电厂的综合成本最小。综合成本包括四块min C_total C_fuel C_om C_grid C_carbon C_curtailC_fuel燃气轮机消耗天然气和氢气的燃料成本C_om各设备运行维护成本通常正比于出力C_grid与外部电网交互的购电成本和售电收益C_carbon阶梯碳交易成本可能是正支出也可能负收益C_curtail弃风弃光惩罚成本这里有个细节弃风弃光为什么要加到目标函数里而不是做成硬约束因为优化模型里如果允许硬性弃风弃光求解器会倾向于在成本高的时候直接砍掉新能源这不符合“尽量消纳新能源”的政策导向。给它一个惩罚系数模型就会在“多发电带来的收益”和“弃电惩罚”之间权衡。惩罚系数一般取单位发电收益的 1.52 倍太小不管用太大又等于硬约束。2.2 阶梯碳交易的计算方式阶梯碳交易在数学上是一个分段线性函数直接写进目标函数会有非光滑问题需要做线性化处理。具体做法是引入几个辅助变量。先算净碳排放量 E_net它等于所有燃气轮机的燃烧排放量减去 CCS 捕集量再减去 P2G 甲烷化反应消耗掉的 CO₂ 量。免费配额记为 E_free超出部分记为 E_ex有E_ex 0 E_ex E_net - E_free因为碳成本是目标函数里的一项正成本优化器会自然地把 E_ex 压到理论上最小的边界值。然后为每一档阶梯引入变量 e1、e2、e3约束e1 e2 e3 E_ex 0 e1 E_step1 0 e2 E_step2 0 e3 E_step3碳交易成本就是C_carbon lambda1 * e1 lambda2 * e2 lambda3 * e3如果排放量低于配额系统可以把剩余配额卖出获得收益这时目标函数里要减掉一项卖碳收益并引入另一个变量 E_sell约束逻辑对称。实现时最好把 E_sell 的上界限定为 E_free避免优化器为了赚卖碳收益而刻意压低发电量。2.3 P2G-CCS 耦合的能流关系P2G 和 CCS 之间有三个关键平衡关系电平衡、气平衡、碳平衡。电平衡上P2G 和 CCS 都是用电设备P_p2g(t) P_ccs(t) 是新增的两项电负荷气平衡上P2G 电解槽产氢量正比于耗电量H2_prod(t) eta_p2g * P_p2g(t)产出的氢气一部分直接送去燃气轮机掺烧一部分进入甲烷化反应器。甲烷化需要消耗 CO₂其消耗量正比于甲烷产量。CCS 捕集的 CO₂ 量正比于处理的烟气量和捕集率捕集过程本身也要耗电Q_capture(t) eta_ccs * G_flue(t) P_ccs(t) beta * Q_capture(t)这里 beta 是捕集单位 CO₂ 对应的电耗典型值在 0.20.3 MWh/t 左右。这个参数对结果影响很大如果设得太低CCS 会变成“拼命捕碳”的免费工具调度结果失真设得太高CCS 又几乎不会被启动。2.4 燃气掺氢机组的出力模型燃气轮机的出力与燃料输入的关系可以简化成P_gt(t) eta_gt * (F_gas(t) * LHV_gas F_h2(t) * LHV_h2)其中 F_gas 和 F_h2 分别是天然气和氢气的消耗流量LHV 是低位热值。定义掺氢比例为体积比或热值比代码里要注意统一单位。实际建模时为了保持线性可以先把掺氢比例固定下来或者作为决策变量但用线性等式约束关联消耗量。碳排放计算更简单E_gt(t) F_gas(t) * EF_gas F_h2(t) * EF_h2氢气的碳排放因子 EF_h2 取 0所以掺氢比例越高单位发电量的碳排放越低。2.5 完整约束条件清单除了上面展开的能流关系一个完整的虚拟电厂调度模型还必须有这些约束约束表达式说明电功率平衡风电光伏燃气发电放电购电 负荷充电P2GCCS售电每个时段都要满足机组出力上下限P_min P_gt(t) P_max含额定容量限制机组爬坡约束-R_down P_gt(t)-P_gt(t-1) R_up反映物理响应速度储能 SOC 动态SOC(t1) SOC(t) P_ch * η_ch - P_dis / η_dis含充放电互斥储氢/储气罐约束H_sto(t1) H_sto(t) H_prod(t) - H_use(t)气侧缓冲阶梯碳交易约束见 2.2分段线性化掺氢比例约束0 x_h2(t) x_h2_max避免过高比例把这些约束全部拼进优化模型就是一个标准的混合整数线性规划MILP问题。决策变量里有储能充放电的 0-1 互斥变量、机组启停变量如果考虑、购售电互斥变量等等。3. Matlab 代码实现从参数初始化到求解器配置3.1 参数初始化把物理设备“翻译”成数字用 Matlab 做这类调度我的技术栈很固定YALMIP 建模 Gurobi/CPLEX 求解。YALMIP 提供了类似数学公式的建模语法写约束和写论文公式差不多不用自己处理矩阵稀疏性非常省事。参数初始化是整个代码里最机械但最容易错的部分。我的建议是所有物理参数集中放在一个参数区用全大写命名单位统一成 MW 和 MWh。以我复现时用的参数为例T 24; % 调度周期 24 小时 dt 1; % 时间步长 1 小时 % 新能源 P_wind_forecast [数据向量]; % 风电预测出力 MW P_pv_forecast [数据向量]; % 光伏预测出力 MW P_load [数据向量]; % 电负荷 MW % 燃气轮机3 台单台 60 MW P_gt_min [5 5 5]; P_gt_max [60 60 60]; R_up [30 30 30]; R_down [30 30 30]; eta_gt 0.42; % 发电效率 EF_gas 0.2016; % 天然气碳排放因子 t/MWh热值基准 % 储能 E_bat_cap 30; % 容量 MWh P_ch_max 10; P_dis_max 10; eta_ch 0.95; eta_dis 0.95; SOC0 0.5 * E_bat_cap; % 初始 50% % P2G eta_p2g 0.6; % 电转氢效率 P_p2g_max 40; % 最大耗电 MW % CCS eta_ccs 0.9; % 捕集率 beta_ccs 0.25; % 捕集电耗 MWh/t Q_capture_max 20; % 最大捕集量 t/h % 碳交易 E_free 300; % 免费配额 t lambda [50 80 120]; % 阶梯碳价 元/t E_step [200 300]; % 第一、二档上限 t c_sell 50; % 卖出剩余配额价格 元/t % 价格 price_buy [分时购电价向量]; price_sell [分时售电价向量];3.2 决策变量用 YALMIP 声明优化变量决策变量分两类连续变量和 0-1 变量。连续变量定义成 sdpvar二进制变量用 binvar。以 T 为列数设备数量为行数方便矩阵化操作。% 连续变量 P_gt sdpvar(3, T); % 燃气轮机出力 MW P_w sdpvar(1, T); % 风电实际消纳 MW P_pv sdpvar(1, T); % 光伏实际消纳 MW P_ch sdpvar(1, T); % 储能充电功率 MW P_dis sdpvar(1, T); % 储能放电功率 MW P_p2g sdpvar(1, T); % P2G 耗电 MW P_ccs sdpvar(1, T); % CCS 耗电 MW P_buy sdpvar(1, T); % 购电功率 MW P_sell sdpvar(1, T); % 售电功率 MW Q_capture sdpvar(1, T); % CCS 捕集量 t/h H2_prod sdpvar(1, T); % P2G 产氢量 H2_use sdpvar(1, T); % 掺氢消耗氢量 SOC sdpvar(1, T1); % 储能电量连续变化 % 0-1 变量 z_ch binvar(1, T); % 储能充电状态 z_dis binvar(1, T); % 储能放电状态 z_grid binvar(1, T); % 购电1 / 售电0这里 P_w 和 P_pv 我用了决策变量而不是固定预测值目的是让模型决定是否需要弃掉一部分新能源。新能源实际消纳量必须小于等于预测出力同时目标函数里加上弃电惩罚模型就会自动在“消纳新能源”和“系统运行限制”之间找平衡。3.3 目标函数的代码实现目标函数分项写最后加起来。燃气成本按输出功率线性近似即可如果要更精确可以写成二次成本函数但会增加求解难度。我用的线性版本% 燃气轮机能耗成本单位燃料成本 * 输入功率简化 % 输入功率 输出功率 / 效率 c_gas 1.8; % 元/MWh热值实际要按气价换算 C_fuel sum(sum( c_gas / eta_gt * P_gt )); % 运行维护成本按出力比例计 c_om 0.02 * ones(3,1); C_om sum(sum( c_om * P_gt )) 0.015 * sum(P_p2g) 0.02 * sum(P_ccs); % 购售电成本 C_grid sum( price_buy .* P_buy ) - sum( price_sell .* P_sell ); % 弃风弃光惩罚 C_curtail 20 * sum( (P_wind_forecast - P_w) ) 20 * sum( (P_pv_forecast - P_pv) ); % 碳交易成本阶梯 E_step1 200; E_step2 300; % 两档区间上限第三档无上限 e1 sdpvar(1,1); e2 sdpvar(1,1); e3 sdpvar(1,1); E_ex sdpvar(1,1); E_sell sdpvar(1,1); C_carbon lambda(1)*e1 lambda(2)*e2 lambda(3)*e3 - c_sell * E_sell; obj C_fuel C_om C_grid C_carbon C_curtail;3.4 关键约束的代码写法先写电功率平衡。虚拟电厂内部的所有电源和负荷都进同一个节点每个时段都要平衡cons []; for t 1:T cons [cons, ... P_w(t) P_pv(t) sum(P_gt(:,t)) P_dis(t) P_buy(t) ... P_load(t) P_ch(t) P_p2g(t) P_ccs(t) P_sell(t)]; end储能约束是这类模型里最容易被忽略互斥的地方。充电状态和放电状态同时为 1就会出现同一块电池既充电又放电的“永动机”现象for t 1:T cons [cons, SOC(t1) SOC(t) (P_ch(t) * eta_ch - P_dis(t) / eta_dis) * dt]; cons [cons, P_ch(t) 0, P_ch(t) P_ch_max * z_ch(t)]; cons [cons, P_dis(t) 0, P_dis(t) P_dis_max * z_dis(t)]; cons [cons, z_ch(t) z_dis(t) 1]; end cons [cons, SOC(1) SOC0, SOC(T1) SOC0]; % 末端电量不低于初始P2G 和 CCS 的关系用代数等式连接% P2G 产氢 for t 1:T cons [cons, H2_prod(t) eta_p2g * P_p2g(t) / LHV_h2]; cons [cons, 0 P_p2g(t) P_p2g_max]; % CCS 捕集量与电耗关系 cons [cons, Q_capture(t) min(P_flue(t), Q_capture_max)]; % 简化 cons [cons, P_ccs(t) beta_ccs * Q_capture(t)]; end注意上面的 min 不能直接用会引入非线性正确做法是用一个变量表示“进入 CCS 的烟气量”再加上限约束由目标函数驱动它取到合理值。为了不偏离主题我这里给出的是简化示意实际代码里应该避免任何 max/min 出现在约束右侧。掺氢约束的核心是把氢气消耗量和掺氢比例关联起来for t 1:T cons [cons, H2_use(t) x_h2_max * (F_gas(t) H2_use(t))]; cons [cons, H2_use(t) H2_sto(t)]; % 氢气量不能超过库存 cons [cons, H2_sto(t1) H2_sto(t) H2_prod(t) - H2_use(t)]; end这里的掺氢比例约束用了线性化形式氢气流量占混合气体流量的比例不超过上限。简化处理下也可以直接把 x_h2 设为常数然后令 H2_use x_h2 / (1-x_h2) * F_gas。3.5 求解器选择与配置模型拼完之后用 optimize 一行求解。Gurobi 对 MILP 的支持很好学术免费是首选。代码里我一般这样配置ops sdpsettings(solver, gurobi, ... verbose, 2, ... gurobi.MIPGap, 0.01, ... gurobi.TimeLimit, 300); result optimize(cons, obj, ops); if result.problem 0 disp(求解成功); else disp(求解失败错误信息); disp(result.info); end如果你没有 Gurobi也可以用 CPLEX设置 solver 换成 cplex 即可。两个都装不了YALMIP 自带的 sedumi 只能解 LP/SDP碰到 0-1 变量大概率跑不动免费开源求解器可以选择 CBC。整体来说这类优化调度问题用 Gurobi 的体验最好MIPGap 设 0.01 基本足够。4. 仿真结果怎么分析方案对比与参数敏感性4.1 典型日数据怎么准备我复现时没有直接用实时数据而是先做了一件事把历史风、光、负荷数据按小时聚合成一个典型日曲线。典型的做法是用 K-means 聚类挑出代表性场景或者直接选取一个“风光大发、晚高峰明显”的典型日。典型日曲线的形状会直接影响调度结果。比如风电在凌晨 15 点大发此时负荷低、电价低P2G 大概率会在这段时间满负荷运行把富余电转成氢气存储傍晚 1721 点负荷高、电价高储能放电、燃气轮机顶峰同时 CCS 和掺氢来压碳。如果你直接拿一组随机数据碰运气结果很难看出机制。我建议先手动画一条典型日曲线把峰谷差拉明显再跑模型效果会好很多。4.2 阶梯碳价和固定碳价对比我用同一套参数分别跑了固定碳价50 元/t和阶梯碳价50/80/120 元/t两个版本。以我这边算例的结果来看趋势非常明显指标固定碳价阶梯碳价综合总成本万元182.6176.4碳排放量t256214弃风弃光率%6.83.1P2G 投运时长h815CCS 投运时长h613固定碳价下系统会把碳配额当成“廉价许可证”碳排放量明显偏高阶梯碳价下因为超排到第二、第三档要支付更高单价系统即使需要花更多电给 CCS 和 P2G仍然愿意通过它们来压排。总成本反而下降了 6 万元左右原因是少买了高价碳配额同时因为 P2G 产出的氢气替代了一部分外购天然气。这个对比解释了为什么政策端热衷于阶梯碳价它不仅是“多排多付”更是倒逼系统在运行层面主动调整调度行为。4.3 掺氢比例变化带来的影响接下来做掺氢比例的敏感性分析。固定其他参数把 x_h2 分别设为 0%、5%、10%、15%、20%结果如下掺氢比例碳排放量t综合总成本万元0%238178.55%225174.910%213172.815%202175.320%192180.6从表里能看出两件事。第一掺氢比例上升确实显著降低碳排放每提高 5 个百分点碳排放大约下降 1013 吨。第二总成本不是单调下降而是先降后升存在一个最优掺氢比例在我这组参数下大约是 10% 左右。原因是掺氢需要消耗 P2G 制出来的氢气而 P2G 耗电是有成本的掺氢比例越高制氢耗电越大风不够用的时候就要高价购电制氢得不偿失。所以实际工程里掺氢比例不是越高越好要用模型算出来。5. 常见问题与调试经验5.1 求解慢、一直转圈怎么办这种多设备、多时段的 MILP 模型变量数量是设备数乘以时段数再加一堆 0-1 变量规模很容易过千。如果求解器半天不出结果不要先怀疑电脑顺序排查第一把 0-1 变量尽量压缩。储能充放电互斥不一定需要两个变量可以用一个变量 z_bat充电状态为 1放电状态为 0再通过约束把功率和状态关联。第二用粗粒度快速测试。把 24 小时改成 6 小时或 8 小时跑通逻辑之后再恢复。第三调 MIPGap。学术研究里设 0.01 或 0.001 足够工程里 0.05 完全可用。第四Gurobi 里可以设置 Threads 参数开启多线程。5.2 找不到可行解怎么办第一步是看 result.info 里的错误信息。第二步我常用的办法是“约束还原法”把所有约束一次性注释掉然后逐块加回去。先只加功率平衡算一下再加储能约束再加 P2G/CCS 约束最后加碳交易线性化约束。哪一步开始无解问题就出在哪一步。最常见的坑是阶梯碳交易线性化时引入了冲突约束。比如 e1e2e3E_ex 和 E_exE_net-E_free如果 E_step 之和小于 E_ex 理论上可能的最大值那模型可能无解。解决办法是把第三档设为无上界0e3Inf用 1e6 代替。5.3 结果“反物理”怎么排查有次我跑出来的结果里凌晨电价最低时段 P2G 没怎么工作反而在晚上电价高的时候满负荷制氢。查了半天是 P2G 的运维成本设得比购电成本低太多模型觉得“制氢亏钱也不多”于是把氢气当副产品乱产。这种“反物理”现象的根本原因就是目标函数系数失配。另一个常见现象是储能既充电又放电。这基本可以断定是缺了 z_ch z_dis 1 这个互斥约束。还有一个现象是 CCS 捕碳量超过可处理烟气量那是 Q_capture 的上限约束没绑对。5.4 参数调优速查表现象可能原因解决办法P2G 一直满负荷P2G 运维成本过低提高 c_om 或外购气价CCS 从不投运捕集电耗 beta 设太高降到 0.20.25 MWh/t弃风弃光几乎为零惩罚系数设太大设为单位收益的 1.5 倍碳排放量总是贴配额走免费配额 E_free 设太大逐步缩小配额重跑掺氢比例一上去就崩储氢罐容量太小增大 H2 存储上限6. 进一步扩展的方向6.1 加入需求响应目前模型里电负荷是硬约束必须满足。实际虚拟电厂里可以引入可中断负荷、可转移负荷让部分负荷可以根据电价移动。比如把一部分工业负荷定义为“在电价谷时多用电、峰时少用电”。实现方式是把负荷拆成固定负荷和弹性负荷两块弹性负荷再设置转移量和上下限约束。6.2 场景随机优化风电光伏预测永远有误差。如果稍微激进一点可以把单确定性问题升级为两阶段随机优化第一阶段决定机组启停状态和储能初始策略第二阶段针对多个风光场景做经济调度目标函数变成期望成本最小。这个方向对理解鲁棒优化的阶段划分很有帮助。6.3 绿证与多主体博弈当前模型只考虑碳交易。政策体系里还有绿证交易可再生能源每发一度电可以产生一张绿证可以单独出售做多虚拟电厂时各主体之间为抢占碳配额和电量市场存在博弈可以用纳什均衡或交替方向乘子法建模。这些扩展方向一旦加上模型就从“单个园区调度”升级成了“多主体低碳能源市场”问题。最后分享一个我实际调试这类模型时养成的小习惯先用 T6 的粗粒度把所有约束跑通再切到 T24 完整算例。这样可以几分钟内定位到底是逻辑错误还是规模问题。拿到求解结果也不要急着画图先看目标函数各项的数值量级。比如碳成本如果比燃料成本低五六个数量级说明免费配额给大了或者碳价太低这个模型形同虚设要是弃风惩罚占比异常高先检查惩罚系数是否合理。这些检查步骤看似不起眼但能帮你省下大把排查时间。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进