☰
阶梯碳交易与电制氢的综合能源热电优化MATLAB复现与调试要点
2026/10/2 10:17:58 网站建设 项目流程

我最近在复现一个挺典型的综合能源系统调度模型:考虑阶梯式碳交易机制与电制氢的热电优化。这类题目在期刊论文里已经不算新鲜,但真正落到MATLAB代码上,坑比想象中要多。很多同学拿到一个开源的yalmip框架,照着论文抄一遍约束,结果要么约束写错导致无解,要么分段碳价的0-1变量处理得不对导致结果逆直觉。这篇就结合我的实际复现过程,把建模、代码实现和调试的细节都摊开讲清楚,希望能帮你少走弯路。

整套路线的核心是:综合能源系统里燃气轮机、余热锅炉、电锅炉、储能、电解槽和氢燃料电池协同出力,在负荷需求约束下做热电优化,同时把碳排放成本用阶梯碳交易的方式塞进目标函数里,再看电制氢设备怎么参与削峰填谷和降低碳排。听起来不算复杂,但各设备的时间耦合约束、热功率平衡方式、碳配额分配方法,每一条都直接影响结果是否合理。文章主要面向正在做综合能源系统优化、碳交易机制建模或电制氢调度方向的研究生,以及想快速复现论文代码的工程师。

1. 整体思路:为什么要把碳交易和电制氢放到同一个优化里

1.1 热电优化到底在优化什么

综合能源系统的核心问题,简单说就是在满足电、热(甚至气、氢)负荷的前提下,决定每一台设备在每个时段的出力,让总成本最低。常规做法是给定燃气轮机的热电比、电锅炉的效率、储能充放电功率上限,用混合整数线性规划求解下一日96个时段的设备出力计划。

但这里有几个容易被忽略的细节。第一,热负荷和电负荷的峰值时段往往错位,冬天晚上热电联产机组为了追热负荷会多发不少电,导致弃风或者电网倒送。第二,储热罐虽然能解耦热电时移,但它的容量和充放热速率有限,真正能转移的热量不多。第三,如果把碳排放成本加进来,单纯按热电比跑出来的解很可能不是碳排放最优的——燃气轮机烧气越多,碳排越高,但电价低时从电网买电又会让外部碳排转移进系统内。这就是为什么要引入阶梯碳交易:用价格信号把碳排放的外部成本内部化。

我自己的理解是,碳交易机制在这个模型里更像是一个灵敏度极高的惩罚项。它改变的是设备出力的优先级排序:当碳价足够高时,系统会倾向于少开燃气轮机、多用储热和电制氢来满足热负荷,甚至通过氢燃料电池在晚间高峰放电同时产热,替代一部分燃气轮机出力。这个逻辑在单目标优化里是自然涌现的,而不是人为规定必须减排多少。

1.2 阶梯碳交易和普通碳税的区别

普通碳排成本计算一般是:总碳排放量乘以一个固定碳价,比如每吨50元。而阶梯式碳交易机制借鉴了阶梯电价思路,把碳排放量分成几个区间,不同区间执行不同碳价,超排越多、边际碳价越高。在数学上,这会让目标函数中的碳成本部分变为凸分段线性函数,从而更强烈地抑制高排放设备的出力和购电行为。

具体到建模,先要确定系统的免费碳排放配额。常见算法有两种:一种是按设备类型乘以单位出力排放基准值,另一种是按整个系统的历史碳排放强度逐年递减。以燃气轮机和电网购电为主时,配额可以定义为:

[ E_{quota} = \alpha \sum P_e^{GT} + \beta \sum P_e^{grid} + \gamma \sum P_h^{GB} ]

实际写代码时,我会把配额按调度周期总负荷折算成逐时可用量,也可以在目标函数里直接对整个调度周期的总排放量和总配额做差值,然后计算阶梯碳成本。两种方式结果差异不大,但后者的分段区间会很大,0-1变量数量少,求解更快。

阶梯碳价的表达式类似:

[ C_{CO2} = \begin{cases} c_1 (E - E_{quota}), & 0 \le E-E_{quota} \le l_1 \ c_1 l_1 + c_2 (E-E_{quota}-l_1), & l_1 < E-E_{quota} \le l_2 \ c_1 l_1 + c_2 l_2 + c_3 (E-E_{quota}-l_1-l_2), & E-E_{quota} > l_2 \end{cases} ]

这里需要注意,当边际碳价递增时,目标函数是凸的,用yalmip里的piecewise或者直接引入0-1变量线性化都行。但如果碳价设置成“阶梯递减”,比如为了鼓励超额减排而给负碳价,目标函数就变成非凸了,MILP的凸性假设会被破坏,此时必须用0-1变量把每个区间的成本单独建模。

1.3 电制氢在这个系统里扮演的角色

电制氢(P2G)设备的加入,给系统增加了一条新路子:用电低谷时段,电价便宜甚至弃风时,电解槽制氢储存起来;用电高峰或者天然气贵时,氢燃料电池放电子同时产热。这样一来,电、热和氢三条能量流在时间维度上有了新的耦合。

但必须警惕的是,P2G的实际效率并不高。碱式电解槽电到氢的效率约60%-70%,氢燃料电池氢到电的效率约50%,整体电到电的循环效率只有30%-40%。所以在我的模型中,P2G很少是为了“效率”而加入,更多是作为碳减排手段或利用低谷弃电的灵活性资源。这时碳交易机制和P2G是协同作用的:高碳价促使系统把燃气轮机的出力份额转移给氢燃料电池,而氢气的来源又必须是低谷可再生能源电解水,否则全生命周期碳排反而更高。

因此在目标函数里,除了燃料成本、购电成本、碳交易成本,还要加上电解槽和燃料电池的运行维护成本。这个成本虽然数值不高,但能避免模型为了减碳让P2G设备满负荷空转,产生毫无意义的能耗。

2. 模型搭建与关键约束的数学表达

2.1 系统结构与设备模型

我用的系统结构如下:电网从外部购电,燃气轮机发电后余热进入余热锅炉供热,燃气锅炉补足缺失热负荷,电锅炉将电力直接转为热,储热罐负责热负荷的时移,电解槽制得的氢气存入储氢罐,再由氢燃料电池在需要时发电并供热。

每个设备的模型基本由效率和容量约束组成,以燃气轮机为例:

  • 电出力范围:(P_{GT}^{min} \le P_t^{GT} \le P_{GT}^{max})
  • 爬坡约束:(-\Delta P_{GT}^{down} \le P_t^{GT} - P_{t-1}^{GT} \le \Delta P_{GT}^{up})
  • 热出力:(H_t^{GT} = P_t^{GT} \cdot r_{GT}),其中 (r_{GT})是热电比
  • 燃料成本:(C_{fuel,t} = (P_t^{GT} + H_t^{GT}) / \eta_{GT} \cdot p_{gas})

实际写代码时,最折磨人的不是这些基本约束,而是启动成本和最小启停时间。如果论文模型允许燃气轮机频繁启停,那问题就是一个混合整数规划,决策变量里除了连续出力还有启停0-1状态。梯级碳交易本身已经需要0-1变量分段,再加上启停变量,整体求解规模会明显增大。

不过,很多复现者直接把燃气轮机设为“必开”设备,尤其在冬季热负荷较高的场景。这时启停变量可以省略,只要满足热负荷就一定运行,模型简化后求解速度很快,且结果容易被审稿人接受。毕竟你对比的是碳交易和P2G带来的调度差异,而不是机组组合问题。

2.2 电制氢与氢燃料电池的建模细节

电解槽模型比较标准:

[ P_t^{EL} \le P_{EL}^{max} ]

[ m_t^{H2} = \eta_{EL} P_t^{EL} / LHV_{H2} ]

储氢罐的动态约束则是:

[ S_t^{H2} = S_{t-1}^{H2} + m_t^{H2} - m_t^{FC} ]

[ S_{H2}^{min} \le S_t^{H2} \le S_{H2}^{max} ]

燃料电池模型同样有电出力和热出力:

[ P_t^{FC} = \eta_{FC}^{e} \cdot m_t^{FC} \cdot LHV_{H2} ]

[ H_t^{FC} = \eta_{FC}^{h} \cdot m_t^{FC} \cdot LHV_{H2} ]

这里有三个值得强调的点。第一,氢的计量单位建议统一转换为kW或MW,不要用kg,否则不同论文的参数很难直接对比。很多代码里把LHV和效率混在一起,最后算出的氢流量数值跟设备容量完全对不上。第二,储氢罐的初始容量和结束容量需要保持一致,否则模型会“偷吃”氢气,比如第一天疯狂制氢,最后一天把存量放完,总成本看起来很漂亮,却完全不符合调度周期重复运行的要求。第三,燃料电池的热电比是固定的,并不能像燃气轮机那样根据热需求灵活调节。因此它本质上还是热电联产机组,只是燃料换成了氢,如果热负荷低而电价高,燃料电池也不能只发电不产热,这往往是系统无解的原因之一。

2.3 阶梯碳交易的分段线性化实现

碳交易成本的分段写法,我会在MATLAB代码里用一组0-1变量加上连续变量来处理。假设把总碳排放量与配额的差值 (E_{net}) 分成 (K) 段,每段长度 (l_k)、碳价 (c_k),则每个分段用一个连续变量 (z_k) 表示“落在该段的排放量”,同时用0-1变量 (\delta_k) 表示“是否启用到该段”。

约束如下:

[ E_{net} = \sum_{k=1}^K z_k ]

[ l_k \delta_{k+1} \le z_k \le l_k \delta_k ]

[ C_{CO2} = \sum_{k=1}^K c_k z_k ]

这里最关键的是确保0-1变量满足单调顺序性:如果使用了第 (k+1) 段,那第 (k) 段必须满额。这要求额外加约束:

[ \delta_{k+1} \le \delta_k ]

在yalmip中实现时,可以简化成用binvar定义delta,然后每一段的边界写清楚。但一个常见错误是把段边界系数写反,导致第一段的区间变成 ([0, l_1]),第二段却是 ([0, l_2]),没有真正实现“先填满第一段才能用第二段”。检查方式很简单:在求解器输出后查看每个 (z_k) 的取值,如果发现后一段有值而前一段没满额,那线性化约束就写错了。

如果你不想手动写这些0-1约束,也可以用yalmip的pwf或者piecewise函数,但建议新手还是手动展开,因为调试更直观,而且不同求解器对piecewise的支持不完全一样。

2.4 目标函数与约束汇总

最终目标函数包含以下几项:

  • 燃气轮机与燃气锅炉的燃料成本
  • 电网购电成本
  • 电制氢和氢燃料电池的运维成本
  • 阶梯碳交易成本

约束则包含电功率平衡、热功率平衡、氢平衡、各设备容量与爬坡、储能状态等。以电功率平衡为例:

[ P_t^{grid} + P_t^{GT} + P_t^{FC} + P_t^{dis} = P_t^{load} + P_t^{EL} + P_t^{EB} + P_t^{ch} ]

热功率平衡类似:

[ H_t^{GT} + H_t^{GB} + H_t^{EB} + H_t^{FC} + H_t^{dis} = H_t^{load} + H_t^{ch} ]

这里要特别注意热负荷平衡方向上储能项的正负,以及电锅炉的耗电量在电平衡中是负荷项。如果这俩符号搞反了,模型结果会直接离谱——储能变成了发电机,电锅炉变成了电源,整个系统自带永动机属性。

3. MATLAB代码实现:框架、变量与求解器选择

3.1 代码总体结构

我的项目代码目录大致长这样:

IES_Carbon_P2G/ ├── data/ │ ├── load_elec.mat │ ├── load_heat.mat │ ├── price_elec.mat │ └── param.xlsx ├── scripts/ │ ├── case_setting.m │ ├── build_model.m │ ├── solve_model.m │ └── plot_results.m └── results/

case_setting.m里放负荷曲线、设备参数和碳交易分段参数。build_model.m负责创建yalmip变量、写出约束和目标函数。solve_model.m设置求解器参数并调用optimize。plot_results.m画电热氢功率平衡图。

这种结构的好处是,想改某个参数不用翻一长串求解代码。尤其做场景对比时,我会把param.xlsx里的碳价上限、配额比例、是否开启P2G设成开关量,直接循环跑。

3.2 YALMIP变量定义要点

时间尺度和滚动优化我采用典型日24小时,步长1小时,总时段数 (T=24)。连续变量有:

P_GT = sdpvar(1, T); % 燃气轮机发电 H_GT = sdpvar(1, T); % 燃气轮机热出力 P_EB = sdpvar(1, T); % 电锅炉耗电 H_EB = sdpvar(1, T); % 电锅炉热出力 P_EL = sdpvar(1, T); % 电解槽耗电 m_H2 = sdpvar(1, T); % 制氢量 S_H2 = sdpvar(1, T); % 储氢容量 C_CO2 = sdpvar(1, T); % 每个时段的碳成本 z_seg = sdpvar(3, T); % 三个阶梯段的排放量 delta_seg = binvar(3, T); % 阶梯段是否启用

这里我建议不要把碳排放成本一次性算在总排放量上,而是逐时计算再累加,否则碳配额分配需要乘以总时段数,反而容易乱。其实逐时和总周期两种方式在数学上等价,只要配额也按逐时折算。

定义变量时最容易犯的错是用repmat生成同一个变量而不是向量。比如错误写法是P_GT = sdpvar(1, T);嫌麻烦,直接写成P_GT = sdpvar(1,1);,后面约束全靠标量循环生成。这样代码运行极慢,而且调试时没法用矩阵操作。建议优先写成向量或矩阵形式,yalmip的约束添加用矩阵方式写更清晰。

3.3 约束的yalmip写法示例

以储氢罐动态为例:

S_H2(1) == S_H2_0 + m_H2(1) - m_H2_FC(1); for t = 2:T S_H2(t) == S_H2(t-1) + m_H2(t) - m_H2_FC(t); end S_H2(end) == S_H2_0; % 周期调度约

注意这里周期性约束S_H2(end) == S_H2_0有时候会导致模型过紧,尤其当电解槽效率不高、初始储氢量又大时,可能为了把末尾存量拉回初始值而让燃料电池在最后一个小时强制放电。实际调试时如果发现末时段设备出力异常,可以把这个约束替换成不等式,比如允许末尾存量在一个区间内浮动。

阶梯碳交易的分段约束,我会这样写:

E_net = sum(E_emission) - E_quota_total; % 总碳排减去总配额 z = sdpvar(Kseg, 1); delta = binvar(Kseg, 1); E_net == sum(z); for k = 1:Kseg z(k) >= 0; if k == 1 z(k) <= len(k) * delta(k); else z(k) <= len(k) * delta(k); z(k) >= len(k-1) * delta(k); % 这里是很多教程写错的地方 end delta(k) <= delta(k-1); % k=2..Kseg end C_CO2_total = c_seg' * z;

注意上面的z(k) >= len(k-1) * delta(k)是一种严格写死下界的做法,但它会强迫第k段一旦启用就必须直接填满超过第k-1段的全部长度。如果求解器给出的最优解里,第k段的排放量没有达到第k-1段长度,那这个约束就会导致不可行。更稳妥的写法是不给z强制下界,只给上界依赖于delta的约束,同时依靠目标函数中碳价递增来保证先使用低价段。我实际测试下来,后一种写法对MILP求解器(cplex/gurobi)更友好。

3.4 求解器选择与性能调优

MATLAB下yalmip默认求解器可能是sedumi或linprog,但对于混合整数规划,必须外接cplex、gurobi或scip。我用的是gurobi,求解速度在24时段、约200个连续变量、100个0-1变量的问题上通常几秒内完成。如果提示无解,不要急着改约束,先设置check命令查看哪条约束不可满足。

ops = sdpsettings('solver', 'gurobi', 'verbose', 2, 'debug', 1); optimize(Constraints, Objective, ops);

debug模式下,yalmip会给出冲突约束定位,极大提升排查效率。我还习惯在求解后调用yalmip('clear')清空上一次的变量空间,否则多次运行脚本时旧约束残留在工作区,干扰新问题。

性能优化上,需要注意两点。第一,尽量增加约束的稀疏性,不要用双层for循环嵌套把每个变量和每个约束都挂上,yalmip会自动按向量和矩阵批量生成。第二,0-1变量尽量少,如果阶梯碳交易分5段以上,每个时段就有5个0-1变量,24小时就是120个,虽然对gurobi不算什么,但有些新手用免费求解器如lpsolve就会卡死。我自己一般只分3段碳价,效果已经足够说明机制。

4. 仿真结果分析与场景对比

4.1 场景设置:怎么设计对比才算完整

为了说明碳交易和电制氢的作用,我设置四组场景:

场景是否考虑阶梯碳交易是否含P2G
S1否否
S2是否
S3否是
S4是是

这样能分别看出碳交易独立作用、P2G独立作用以及二者耦合效应。S1是基准场景,碳成本只按排放量乘固定碳价,甚至可以不考虑碳约束。S2在S1基础上把固定碳价替换成阶梯碳价。S3在S1基础上加上电解槽和燃料电池设备。S4则是完整模型。

每组场景跑完后,我对比三个指标:总运行成本、总碳排放量、弃风弃光量(如果有可再生能源)。实际结果中,S4的总成本通常不是最低,因为多增加了P2G设备运维和储氢损耗,但碳排放量一定最低。这时候要小心写论文时别把“成本最低”和“碳排最低”混为一谈,应该强调S4是在碳交易约束下达成的“碳经济双赢”,或者用单位减排成本来评价。

4.2 典型日设备出力图怎么看

从结果图我能明显看到,加入阶梯碳交易后,燃气轮机的出力在高峰时段不再满负荷运行,尤其是当碳价处于第三段高位时,系统甚至会牺牲一部分热出力,改用储热罐放电和电锅炉产热来满足热负荷。电锅炉在低谷电价时段加大耗电,相当于把电转成热存起来,替代燃气轮机的一部分热出力。

加入P2G后,电解槽主要在凌晨1点到5点运行,因为此时电价最低、且系统内的风电出力上调空间大。储氢罐在白天缓慢释放氢气,燃料电池在下午和晚高峰放电并供热,打配合燃气轮机避开高价碳时段。这种时序是模型自发生成的,不需要人工设置折旧规则。

我常提醒自己,要看储能设备SOC曲线是否在合理范围内,避免出现反复充放的小锯齿。如果SOC曲线锯齿密集,大概率是约束里缺了充放状态互斥变量,电储能和热储能同时充放,虽然在数学上可能不是最优,但由于MILP的可行性容差,可能误以为合理。这时候需要加入P_ch * P_dis == 0的互补约束,或者用0-1变量强制不能同时充放。

4.3 碳价灵敏度分析怎么做

灵敏度分析是论文里的常用部分,也是验证模型鲁棒性的手段。我会把阶梯碳价的第一段基准价从每吨20元扫到200元,每次求解都记录总碳排放量和总成本。画成曲线后,通常会看到碳排放量出现一个明显的拐点,拐点之前碳价影响不大,拐点之后系统开始大规模削减燃气轮机电出力和购电比例。

这个拐点跟P2G设备的单位制氢成本密切相关。如果电解槽效率低、设备成本高,那么碳价必须涨到很高,系统才愿意用氢循环替代燃气轮机。如果P2G成本设置过低,模型可能出现“无论碳价多少都优先用氢”的不合理结果,这时候要检查氢燃料电池的热电比和电锅炉成本参数是否失衡。

5. 复现代码时的常见问题与避坑清单

5.1 无解的第一反应不是调求解器,而是调约束

很多新手一看到infeasible problem就怀疑是求解器设置错了,然后乱调mipgap。我自己遇到的大部分无解问题,从根源上都出在能量平衡约束上。比如热功率平衡里漏了电锅炉的热输出,或者储热罐的热功率单位是kW、而燃气轮机热出力单位是MW,量纲差1000倍,约束直接无解。

所以排查无解的顺序我固定为:

  1. 用check(Constraints)查看每条约束的残差;
  2. 重点检查储能动态约束的初值,比如储热罐初始容量设置超过上限;
  3. 打印出每个时段的负荷曲线和设备总出力上限,先手动核算一下有没有哪个时段最大可出力都小于负荷。

比如某时段电网购电上限是300kW,燃气轮机最大出力200kW,燃料电池100kW,合计刚好够600kW负荷,如果这时还加了一个“购买绿电最小比例”约束,就会无解。这类问题跟求解器毫无关系。

5.2 阶梯碳交易的线性化不收敛或结果异常

我遇到过一种情况:加了阶梯碳成本以后,总排放量不降反升。最后定位到原因是碳配额的定义写错了。我把配额写成了每个时段固定值,然后总排放量用的是总周期,结果相当于给了系统24倍的配额,碳成本几乎为零,当然不会约束排放。

另外,如果分段碳价的区间长度设置过大,所有超排量都在第一段内,模型几乎感知不到碳价的梯度,需要根据预期超排量来设定区间上界。一般来说,第一段的长度应该设置为配额量的10%-20%,后续区间按等比放大,这样碳排放量通常能落在第二到第三区间,这样0-1变量才会被激活,机制的约束作用才可见。

5.3 yalmip变量初始化与旧约束残留

yalmip的变量是对象,不是数值。如果你在一个脚本里重复运行optimize,并且没有清除约束集,那么第二次运行时的约束会叠加到第一次的变量上。虽然yalmip通常会覆盖同名变量,但如果变量名变化了,旧约束还留在Constraints里,就会导致模型无解或结果诡异。

我的习惯是每次求解前都执行:

yalmip('clear');

同时在脚本开头加上clear all; close all; clc;,尽量避免工作区残留。还有一点是用optimize后,如果你想查看某个变量的数值,应该用value(P_GT),而不是直接输出P_GT,后者只是yalmip的变量对象,显示出来是一堆表达式。

5.4 求解器参数与大规模问题的取舍

如果模型时段数扩展到96点,0-1变量数量会明显上升,尤其把P2G设备和碳交易分段都加进来后,求解时间可能从几秒飙升到几分钟。这时有几种处理方式:

  • 缩小阶梯分段数量,用3段代替7段;
  • 将储热罐、储氢罐的SOC离散为若干层,但会牺牲精度;
  • 把目标函数中的二阶项都线性化,确保整个模型没有非线性表达式,yalmip才不会误调用非线性求解器;
  • 设置mipgap为0.01甚至0.05,对工程结果影响不大,但求解速度快好几倍。

我在跑96时段时,常常把mipgap设置成0.02,这样既能保证结果可复现,又不至于让仿真实验卡死。如果审稿人要求严格最优解,可以在正式跑数据时再改成0.001,你要心里有数:这个问题本质上是MILP,存在最优解,只是求解时间需要权衡。

5.5 画图与结果导出

结果图我一般用stairs画阶梯曲线,因为调度计划本身是分段常数,比平滑曲线更能看出时段变化。燃料成本、碳排放量和各设备出力建议画在同一张时间轴图上,方便对照。导出数据时,我会用writematrix把结果写到Excel,便于后续做灵敏度统计和表格。

这里额外提一个小技巧:每次跑完场景后,用save命令保存工作区,给文件名加上场景标识,比如result_S4_150.mat,这样防止后面改了参数忘记记录结果。相信我,做碳价灵敏度时你会感谢自己。

我个人在这类模型复现上最大的体会是:碳交易和电制氢都不是越复杂越好。阶梯碳交易的核心在于用价格梯度改变边际成本,P2G的核心在于用氢储能解耦电、热、气多种能源的时空属性。如果代码里这两条逻辑没有在结果上体现出来,那大概率是建模细节出了问题,而不是理论思路不对。先从3时段的小规模模型验证逻辑,再扩展到24时段,最后做灵敏度分析,这样才能一步步把代码从“能跑”做到“结果可信”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询