前阵子手头接了一个很有意思的复现任务:一篇计及动态绿证-碳排协同交易机制的综合能源系统鲁棒优化调度论文,要求用 Matlab 代码把模型跑通,并且把 24 小时调度结果、鲁棒边界、碳交易成本全部还原出来。这类论文在电力系统方向特别典型,但真正动手复现过的人都知道,难点从来不是 Matlab 语法,而是论文里那些写得极其抽象的市场机制,怎么一步步翻译成能丢进求解器的约束。
这篇文章把我从读论文到跑通代码的完整过程梳理一遍,包括绿证-碳排协同机制怎么理解、鲁棒模型怎么从两阶段问题变成可计算问题、Matlab+YALMIP 怎么搭建、以及我踩过的四个比较有代表性的坑。如果你也要复现同类型的综合能源系统调度论文,这篇文章可以直接当参考手册用。
1. 为什么复现这一篇:先把绿证-碳排协同交易的机制吃透
复现论文最忌讳一上来就看公式、抄代码。你得先把论文里面的市场机制彻底搞明白,否则后面目标函数和约束写出来,你都不知道它到底在约束什么东西。
1.1 动态绿证交易的三个关键词:配额、有效期、价格动态
绿证交易机制,全称是绿色电力证书交易机制。发电企业每发 1MWh 可再生能源电力,可以获得一张绿证。这张绿证可以在市场上交易,买方主要是承担可再生能源配额义务的售电企业或者高耗能用户。
动态这两个字,体现在三个维度上:
第一是配额动态。系统会按照一定时间周期给用电侧或售电侧下达可再生能源消纳责任权重,也就是一个最低比例。只要你在这个周期内的用电量达到某个数,就必须持有对应数量的绿证。这个比例在不同省份、不同时段会有差异。
第二是有效期动态。绿证不是永久有效的,论文里通常会设置一个有效期窗口,比如 24 个月内必须用于履约,过期作废。这就会导致调度端在做决策时,需要考虑绿证持有量在时间维度上的滚动变化,跟储能系统的荷电状态约束有点像。
第三是价格动态。绿证价格不是固定参数,它在不同调度时段根据市场供需关系变化。建模时常见的做法是:绿证价格与系统内可再生能源发电量挂钩,可再生能源发电多→绿证供给充足→价格下降;反之绿证价格上涨。也有论文用分时价格曲线或者价格弹性系数来描述这种动态关系。
1.2 碳排协同交易如何与绿证发生耦合
碳排交易机制大家相对更熟悉。系统给排放主体分配免费碳排放配额,实际排放超过配额的部分需要在碳市场购买,低于配额的部分可以出售获利。
关键就在协同二字。碳配额和绿证在物理上不是相互独立的,而是存在抵扣和联动关系。很多论文会引入类似这样的设定:
- 每张绿证可以折算成一定量的碳减排量,用于抵扣企业的碳排放量;
- 或者反过来,企业购买碳配额时,必须同时持有一定比例的绿证,才能完成履约。
这样一来,综合能源系统在调度决策时,就不再是单纯地基于能源价格做经济调度了。你必须同时考虑:如果多用气、多发电,碳排放量上去了,碳成本会增加;如果多用光伏风电,虽然运行成本低,但配套设备的投资和绿证购买也需要统筹。更复杂的是,绿证价格和碳价格之间还存在动态联动。所以论文标题里说的协同交易机制,本质上是一个双市场耦合约束问题,不是简单的两个成本项相加。
1.3 复现论文前要做好的参数整理清单
我复现论文的第一件事,是把所有需要用到的参数先整理成一张表,尤其是交易机制相关的那些,因为这部分论文里往往散落在不同章节,特别容易漏。
我整理出来的一份典型参数清单大致包含:
- 能源价格类:电网购电价、售电价、天然气价格,以及是否分时;
- 绿证类:初始绿证价格、绿证价格波动系数、配额比例、绿证有效期、绿证减排折算系数;
- 碳交易类:免费碳配额总量、碳价格、碳价格阶梯区间、超标惩罚单价;
- 设备类:CHP 机组电效率/热效率、燃气锅炉效率、储能容量/充放功率上下限/效率、P2G 或电锅炉参数;
- 不确定性类:风电/光伏预测出力、预测偏差比例、鲁棒不确定度预算 Γ;
- 系统类:24 小时电负荷/热负荷/气负荷曲线、分时电价曲线、联络线功率上限。
提示:很多复现失败的项目,问题都出在参数没对齐。论文里可能给的是某省某年的实际数据,你换了一组不同来源的负荷曲线,碳配额又按另一套标准设置,最后跑出来的成本结构根本不具备可比性。我建议把所有参数单独放进一个
params.m文件里集中管理,后续调参也方便。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 调度模型怎么建:从能量流到鲁棒优化的数学表达
机制层面理解清楚之后,接下来要做的是把物理系统和经济机制统一写成一个数学模型。这个环节直接决定后面代码的结构,急不得。
2.1 综合能源系统的设备拓扑与平衡约束
我复现的论文里,综合能源系统主要包含电、热、气三种能源形式,设备侧包括:
- CHP 热电联产机组:同时产电和产热,是电气热耦合的核心设备;
- 燃气锅炉:补充供热;
- 光伏、风电:可再生能源出力,存在不确定性;
- 储能电池:平抑电力波动;
- 电转气 P2G 或电锅炉:实现电气耦合,富余电力转化为气或热。
调度周期一般取 24 小时,时间分辨率 1 小时。那么最基本的三类平衡约束分别是:
电力平衡:
P_buy(t) + P_pv(t) + P_wt(t) + P_chp_e(t) + P_dis(t) = P_load(t) + P_chg(t) + P_p2g(t) + P_sell(t)
热力平衡:
Q_chp(t) + Q_gb(t) + Q_hst_dis(t) = Q_load(t) + Q_hst_chg(t)
天然气平衡:
V_gas_buy(t) + V_p2g(t) = V_load(t) + V_chp(t) + V_gb(t)
每个设备还要单独写运行约束。比如 CHP 机组有电出力上下限、热出力上下限、爬坡约束、热电比运行区间;储能电池有容量递推方程、充放功率上限、同一时刻不能同时充放的逻辑约束。
这些约束看着多,但逐个写进 Matlab 并不难,真正难的还是下一步的鲁棒问题。
2.2 两阶段鲁棒优化:未知的明天怎么写成可计算的模型
综合能源系统调度最核心的不确定性来自风电、光伏和负荷预测误差。预测值跟实际值之间总有偏差,而调度方案必须保证在最恶劣的偏差组合下依然可行,这就是鲁棒优化的思想。
论文里用的一般是两阶段鲁棒模型。第一阶段是预先决策,比如机组启停状态、购电售电计划、绿证配额交易量,这些决策必须在不确定性揭晓之前定下来。第二阶段是实时调整,在风光实际出力确定之后,通过调整储能充放电、P2G 功率、联络线功率等变量,尽可能经济地应对偏差。
数学上可以写成:
min_x ( c^T x ) + max_{u ∈ U} min_{y ∈ Ω(x,u)} ( d^T y )
其中:
- 第一阶段变量 x:机组的启停、购售电状态等 0-1 变量;
- 第二阶段变量 y:储能功率、P2G 功率、机组出力的调整量等连续变量;
- 不确定性变量 u:风电/光伏预测偏差、负荷波动等;
- U 是不确定集;
- Ω(x,u) 是给定第一阶段决策 x 和不确定性 u 后,第二阶段可行域。
不确定集最常用的是盒式不确定集:
U =
其中 Γ 是鲁棒调节参数,用于控制不确定性的保守程度。Γ=0 等价于确定性问题,Γ 越大,解的鲁棒性越强,但经济性越差。
2.3 目标函数到底在优化什么
目标函数是整个模型的核心。综合能源系统调度的目标函数不是单纯购能成本最小,而是要同时包含碳排放成本、绿证交易成本等多个部分。
一个典型的表达式是:
min ∑_t [
c_e_buy(t)·P_buy(t) + c_gas·V_gas(t) - c_e_sell(t)·P_sell(t)
- c_co2·(E_real(t) - E_quota(t))
- c_gec(t)·GEC_buy(t) - c_gec_sell(t)·GEC_sell(t)
- c_om·P_generation(t)
]
其中:
- c_e_buy 是购电价,c_gas 是天然气价;
- c_co2 是碳价,E_real 是实际碳排放量,E_quota 是免费配额,两者差值乘以碳价就是碳交易成本;
- c_gec 是动态绿证价格,GEC_buy 是购买绿证量,GEC_sell 是出售绿证量;
- c_om 是设备运行维护成本。
需要注意的是,E_real(t) 不能直接当成一个变量,它是由机组出力乘以排放因子计算得到的。比如 CHP 和燃气锅炉烧天然气,排放量等于天然气消耗量乘以排放因子。而免费配额 E_quota(t) 可能是按历史产量或者行业基准线给定,也可能是和绿证持有量挂钩。如果你在代码里把 E_real 当成独立变量写,很可能导致碳约束和能量平衡约束脱钩。
3. 用 Matlab 把模型敲出来:YALMIP 建模与 CCG 迭代
模型建好之后,下一步就是落地到 Matlab。我强烈建议用 YALMIP 工具箱建模,再配 Gurobi 或 CPLEX 求解。YALMIP 的好处是建模语言非常贴近数学表达式,不用自己去写低层的数据结构。
3.1 决策变量组织和 YALMIP 的引入
对整个调度周期 24 小时,用 sdpvar 定义连续变量,用 binvar 定义 0-1 变量。
matlab复制T = 24;
% 第一阶段决策变量
u_chp = binvar(1, T); % CHP启停
u_buy = binvar(1, T); % 购电状态
u_sell = binvar(1, T); % 售电状态
% 功率输出变量
x = sdpvar(1, T); % 电网购电功率
y = sdpvar(1, T); % 出售功率
P_chp = sdpvar(1, T); % CHP电出力
Q_chp = sdpvar(1, T); % CHP热出力
P_gb = sdpvar(1, T); % 燃气锅炉功率
P_bat = sdpvar(1, T); % 储能功率(正值放电,负值充电)
SOC = sdpvar(1, T); % 储能荷电状态
P_p2g = sdpvar(1, T); % 电转气功率
之所以把购电和售电拆开来写,是因为一般情况下同一个时段不能既买又卖,而这是一个典型的需要用 0-1 变量约束的逻辑条件。如果你图省事直接定义一个 P_trade 有正有负,那目标函数里边的买卖价差就没办法精确表达了。
3.2 目标函数、平衡约束和设备约束的代码化
目标函数写成:
matlab复制objective = 0;
for t = 1:T
objective = objective ...
+ c_elec_buy(t) * P_buy(t) ...
- c_elec_sell(t) * P_sell(t) ...
+ c_gas * V_gas(t) ...
+ c_co2 * (E_emission(t) - E_quota_free(t)) ...
+ c_gec(t) * GEC_buy(t) ...
- c_gec_sell(t) * GEC_sell(t) ...
+ c_om * (P_chp(t) + P_gb(t));
end
然后添加平衡约束:
matlab复制constraints = [];
for t = 1:T
% 电力平衡
constraints = [constraints, ...
P_buy(t) + P_pv_forecast(t) + P_wt(t) + P_chp(t) ...
+ P_bat(t) - P_p2g(t) - P_sell(t) == P_load(t)];
% 热力平衡
constraints = [constraints, ...
Q_chp(t) + Q_gb(t) == Q_load(t)];
% 储能递推约束
if t == 1
constraints = [constraints, SOC(t) == SOC_ini + P_bat(t) * eta_chg - P_bat(t) * eta_dis];
else
constraints = [constraints, SOC(t) == SOC(t-1) + P_bat(t) * eta_chg - P_bat(t) * eta_dis];
end
end
设备约束方面,CHP 机组最常见的是一组线性化的运行可行域约束。这里有个容易忽略的地方:CHP 的热电耦合关系不是简单的一次函数,而是通过一组线性不等式约束来近似,也就是所谓的可行域法。如果你直接写 Q_chp = k * P_chp,那模型就失真了。
储能约束要特别注意同时充放的问题,虽然物理上不可能发生,但优化求解器不会自动帮你规避,你必须显式加上:
matlab复制constraints = [constraints, P_bat(t) <= P_dis_max * u_bat_dis(t, 1)];
constraints = [constraints, -P_bat(t) <= P_chg_max * u_bat_chg(t, 1)];
constraints = [constraints, u_bat_dis(t, 1) + u_bat_chg(t, 1) <= 1];
绿证约束部分,核心是配额约束。假设一个周期内系统实际消纳的可再生能源电量必须达到用电量的 α 倍,而这个电量可以用绿证持有量来表征:
matlab复制constraints = [constraints, GEC_hold(T) >= alpha_quota * E_consumption_total];
GEC_hold 是一个递推变量,它表示绿证持有量随时间的变化:新购买绿证增加持有量,参与碳排抵扣的绿证消耗减少持有量,同时超过有效期的绿证作废。
3.3 子问题对偶求解与最恶劣场景识别
两阶段鲁棒优化的问题在于,max-min 结构没法直接交给求解器。通常的做法是用 C&CG 方法或者 Benders 对偶法,把原问题变成一个迭代求解的主问题和子问题。
主问题是最小化第一阶段成本加上一个辅助变量 η,这个 η 表示后续阶段的期望成本。每次迭代,主问题会新增一组与最恶劣场景 y^(*) 相关的约束。
子问题则是在已知第一阶段决策 x^(*) 的前提下,求解:对每个不确定场景 u,寻找最小的第二阶段成本,然后在外层寻求最大。这个 max-min 问题可以通过将内层 min 问题取对偶,合并成一个 max-max 问题,也就是一个 max 问题来求解。
如果第二阶段是纯连续线性规划,对偶之后就是一个线性规划,很方便求解。但如果第二阶段里有 0-1 变量,那就麻烦得多,这也是后面我要讲的坑之一。
子问题的 Matlab 框架大致如下:
matlab复制% 固定第一阶段解
% 更新不确定变量 u_wt, u_pv, u_load 的可变取值范围
% 对偶问题求解
sub_obj = ...; % max 形式
sub_cons = [...];
ops = sdpsettings('solver','gurobi','verbose',0);
optimize(sub_cons, -sub_obj, ops); % 对偶问题转为 min
worst_u = value([u_wt; u_pv; u_load]);
sub_obj_value = -value(sub_obj);
3.4 主问题迭代更新的完整骨架
CCG 迭代的完整流程我在代码里按下面的骨架实现:
matlab复制LB = -inf;
UB = inf;
GEC_buy = sdpvar(1, T); % 提前定义
% 用于存储每次迭代生成的割约束
cuts = [];
for iter = 1:max_iteration
% 求解主问题,获得 x_current 和 eta_current
optimize([base_cons, cuts, ...], MP_obj, ops);
x_current = value(x_vars);
eta_current = value(eta);
LB = max(LB, value(MP_obj));
% 固定 x,求解子问题,获得最恶劣场景和对应目标值
[worst_scenario, sub_cost] = solve_subproblem(x_current);
UB = min(UB, value(MP_obj) - eta_current + sub_cost);
% 判断收敛
if (UB - LB) / abs(UB) <= epsilon
break;
end
% 生成割约束并加入主问题
cuts = [cuts, eta >= cut_expression_using_worst_scenario];
end
有几个细节值得一说。第一,CCG 对初值非常敏感。主问题第一次求解时,如果目标函数里包含一个没有约束的 η 变量,求解器可能会把它拉到负无穷,导致 LB 异常。所以初值边界一定要给好,至少 η 的下界要设置一个物理意义下的合理值。
第二,子问题里所有不确定性变量在迭代中要能被覆盖更新。我的做法是在子问题里把风电、光伏、负荷的预测值重新定义为带偏差的变量,比如 P_wt_actual = P_wt_forecast + delta_wt * P_wt_capacity,然后让 delta_wt 在 [-1, 1] 范围内变化,同时受不确定度预算 Γ 约束。
4. 复现中的坑:我从论文公式到可运行代码遇到的四个问题
这部分是我最想写的。因为论文读起来都很顺畅,但真正动手写代码,问题一个接一个。
4.1 绿色证书变量到底是连续变量还是整数变量
我第一次建模时,想当然地把绿证持有量、购买量都设成了整数变量,因为绿证是一张一张算的。结果求解时间爆炸,24 小时的调度问题,Gurobi 跑了十几分钟还没收敛。
后来我看论文的后续内容,发现论文里的做法是把绿证当成连续变量来松弛处理。因为可再生能源实际发电量是连续的,证书数量本质上是一个连续量乘以折算系数,整数化的意义不大。把 GEC 相关变量全部改成连续变量之后,求解时间降到了几十秒级别。
提示:如果你的论文里没有显式强调绿证必须取整,那么一定要优先用连续变量。强行取整不但增加计算难度,而且可能让模型变得不可行的场景大幅增加。
4.2 双层结构在 Matlab 里最容易出现的求解器报错
CCG 迭代时,初学者最常见的报错是 YALMIP 提示 no suitable solver,或者 Nonconvex quadratic constraints 之类的问题。
这个问题的根源往往是:你在子问题里用了两个变量的乘积。比如风速不确定变量 ΔP_wt 和第二阶段决策变量 P_chp 出现在同一个约束里,原本是一个可以线性化的项,但如果不做变量替换,直接写进 YALMIP,它就会给你报非凸。
解决方法是把所有包含 u·y 形式的项,通过对偶转换或者大 M 法线性化。具体来说,在子问题对偶化之后,u 会以边界值形式出现在互补松弛条件里,可以采用强对偶条件替换,避免直接处理乘积项。
如果你是第一次写 CCG,我的建议是:先跑一个只有连续变量的简化版,把迭代框架跑通,再加入 0-1 变量和复杂约束。否则问题定位起来非常痛苦。
4.3 数据年份不一致导致的复现偏差
复现论文时我用了一套自己整理的省内实际负荷数据,在 24 小时内峰谷特性明显。但是论文里用的数据是另一个地区的,它的热负荷在冬季有很长的平台期。
这就导致一个很尴尬的结果:我的调度结果里边,储能在夜间一直充电,白天再放电;而论文的结果是储能在夜间就把电放完。对比之下,怎么看怎么像我的代码有 bug,但其实是负荷特性带来的正常差异。
这类数据不一致的问题在复现论文时常出现。我的经验是:
- 先不换数据,用论文给出的典型日曲线跑一遍模型,确认复现结果和论文趋势接近;
- 然后替换成自己的数据做敏感性分析,这时候的结果才有意义;
- 不要把两套数据混着用,特别是绿证价格、碳配额这类政策性参数,它们跟数据年份强相关。
4.4 迭代不收敛:如何通过松弛参数和初值设定来解决
CCG 迭代次数偏多或者收敛慢,是一个特别折磨人的问题。我遇到的情况是迭代 20 轮之后 UB-LB 仍然在 2% 左右波动,继续跑也无济于事。
排查之后发现两个原因:
第一个原因是子问题里存在大量最优解等价的退化场景。也就是说,最恶劣场景不止一个,可能同时存在多个风电场都在偏差上限,子问题目标函数值一样,但不代表主问题生成的割约束是一致的。这个问题的处理方式是多生成几条割约束,或者给子问题加一个正则项,打破退化。
第二个原因是收敛阈值设得太小。在实际工程中,鲁棒优化调度的解差 1%-2% 是完全可接受的,没必要追求 0.1% 以内的收敛。我把收敛阈值设为 1% 之后,迭代次数从 20 多次降到 6 次左右,效果立竿见影。
注意:CCG 的收敛判断不要只看目标函数相对误差,还要看一下第一阶段决策变量的变化幅度。如果目标函数值没怎么变,但 x 在每个迭代之间大幅跳跃,说明有多个近似最优解,这时候需要关注解的唯一性,而不是一味加大迭代次数。
5. 结果怎么看:鲁棒性分析、灵敏度测试与结论复盘
代码跑通只是第一步,复现还有更重要的一环是结果分析和模型行为验证。这一步直接决定你的复现是否具备说服力。
5.1 输出调度结果与关键指标
跑完模型后,我会输出这几个核心指标:
- 系统总运行成本,分项列出购电成本、购气成本、碳交易成本、绿证交易成本;
- 各时段 CHP 电出力和热出力曲线;
- 储能 SOC 变化曲线;
- 碳实际排放量 vs 配额量;
- 绿证持有量变化曲线。
其中绿证持有量的变化曲线特别能检验交易机制有没有生效。如果你看到绿证持有量在整个调度周期内完全不动,那说明绿证约束和碳排放约束没有被正确激活,模型只是在空转。
5.2 灵敏度测试:改变鲁棒预算和绿证价格
我会跑三组灵敏度分析,这三组也是论文里最常出现的图:
第一组是改变鲁棒不确定度预算 Γ,观察系统总成本和购电购气结构变化。理论上 Γ 越大,总成本越高,但是可再生能源消纳的保障程度也越高。如果你的结果显示 Γ 变大成本反而下降,那大概率是模型里有松弛项漏了限制条件。
第二组是改变绿证价格水平,观察绿证购买量和碳排量之间的关系。绿证价格上升时,系统应该倾向于多使用自有可再生能源或者直接承受碳配额罚款,而不是购买高价绿证。这个逻辑关系如果没跑出来,说明绿证和碳约束之间的耦合系数设置有问题。
第三组是改变碳价格,观察综合能源系统在发电结构上的切换过程。碳价升高时,CHP 运行占比应该下降,燃气锅炉可能上升,同时 P2G 的利用率也可能提高,因为电转气过程产生的氢气或者合成天然气在碳核算上有优势。
5.3 关于结果合理性的几个判断标准
复现论文不是跑出来就行,还要判断结果是否合理。我个人的几个粗筛标准:
- 储能 SOC 曲线应该在调度周期结束时与初始值接近,除非模型允许剩余能量变现;
- 电、热平衡约束在所有时段都应该严格满足,不能出现功率缺口靠惩罚项硬撑的情况;
- 碳交易成本不能让最后一天出现极端跳跃,除非论文里明确设置了周期末配额清算;
- 鲁棒优化得到的调度结果,至少在最优性上要优于或者等于确定性模型的成本,否则就要检查框架的推导过程是不是有问题了。
这三组灵敏度分析做完,你的复现报告基本上就可以说完成了。我自己跑完这一套下来,最大的体会是:这类论文复现的本质,不是把公式敲进 Matlab 那么机械,而是要重新走一遍作者的决策链条——从市场机制的设计意图,到数学模型对它的抽象,再到代码实现对数学模型的落地。每一个环节都有大量论文里没写清楚的隐性设计,这些恰恰是最费时间也最涨功力的地方。
最后分享一个小工具层面的建议:做鲁棒优化复现时,千万不要把所有代码写在一个大文件里,建议按模块拆分成 params.m、build_mp.m、build_sp.m、run_ccg.m、plot_results.m。你后面改某个机制参数、换一组负荷数据、甚至换一个求解器,都会轻松很多。我就是在经历了改一处变量被连带报错十几处之后,才痛下决心重构代码目录结构的。
