1. 项目到底在算什么:综合能源系统调度模型的设计思路
1.1 电-热-气三网怎么耦合
先把这个项目从“标题翻译成人话”:考虑综合需求响应和阶梯型碳交易机制的综合能源系统优化调度,核心是在 MATLAB 里搭建一个电、热、气多能互补的园区级能源系统,并求出未来 24 小时的最优运行方案。这里的“最优”不是拍脑袋,而是以一个总成本最小化的目标函数为基准,同时满足能量平衡、设备出力范围、储能约束等条件,最终输出各机组的启停、出力、储能充放、与外网购电、甚至需求侧负荷调整的具体数值。
我复现时通常会把系统拓扑固定为下面这张图,论文里八成也是这个结构:外部电网作为电力来源之一,天然气网供燃气轮机和燃气锅炉使用;系统中配有光伏、风机、电锅炉、燃气轮机(CHP)、燃气锅炉、蓄电池和热储能。电负荷由光伏、风机、燃气轮机、外网购电和蓄电池共同满足;热负荷由燃气轮机余热、燃气锅炉、电锅炉和热储能满足。天然气负荷直接来自气网,如果论文里有“气负荷”,则在地理管网络中混入。
这个结构对应的就是“能源集线器”思想:多种能源在输入端经过转换、存储、分配,最后以统一的电、热、气负荷输出。为什么要用集线器而不是完整电网建模?因为大部分复现论文根本不考虑拓扑网络约束,它们关心的是跨能源品种的调度决策,而不是潮流计算。如果你非要把支路潮流、节点电压、天然气管道压力全部加进去,模型会从 MILP 变成复杂得多的 MINLP,求解时间会从几秒变成几小时,论文复现的性价比极低。
另一个关键点是时段粒度。多数综合能源系统优化调度论文用 24 个时段,步长 1 小时。少数会用到 96 时段、15 分钟粒度,那主要是为了刻画储能和需求响应的实时性。MATLAB 复现时我建议先用 24h 跑通,再改 96 时段。直接在 96 时段起步,一旦约束条件写错,排查成本会非常高。
1.2 调度策略的复现目标与建模路径选择
要弄清楚这个项目到底输出什么:每个时段设备出力、储能充放功率、电网交互功率、需求侧响应量、碳交易成本和总运行成本。因此本质上是一个多时段最优决策问题。
从建模路径看,几乎全部这类论文都是“先列目标函数,再写约束,最后丢给求解器”。目标函数是运行成本最小化,运行成本包括购电购气费用、设备运行维护费用、需求响应补偿费用、碳交易费用,如果系统中有启停状态变量,还要加上启停成本。约束条件则分为四组:能量平衡约束、设备特性约束、储能动态约束、需求响应约束。
在 MATLAB 平台上有两条技术路线。一条是使用 YALMIP 工具箱建模,后端调用 Gurobi 或 Cplex 求解;另一条是直接用 MATLAB Optimization Toolbox 的optimproblem和intlinprog。我在复现这类论文时强烈推荐 YALMIP,原因在后文会展开。核心在于模型里会大量出现 0-1 变量,比如储能不能同时充放、需求响应动作的时序约束等,YALMIP 对二进制变量的表达更自然,代码可读性好,后期修改约束条件也更方便。
复现论文前的“假设清单”很重要。比如光伏出力是否取预测值、电负荷是否刚性、热负荷是否允许削减、天然气价格是否是分段阶梯价、外网购电是否存在上限。这些看似细节的东西,直接决定了你的模型可不可解。
2. 综合需求响应建模的两个层次
2.1 价格型需求响应:弹性矩阵与负荷转移
很多人把需求响应简单理解成“削峰填谷”,这个理解太浅了。综合需求响应的第一个层次是价格型需求响应,它建立在“用户会随着电价变化调整用电行为”这个经济学假设上。用户不会因为某时段电价高了就完全不用电,而是在可容忍范围内把一部分负荷从高峰挪到低谷。
建模时最常用的是弹性矩阵法。设用户原始电负荷为 (L_0(t)),经过需求响应后的负荷为 (L(t)),电价变化率为 (\Delta p^e(t)/p_0^e(t)),则: [ L(t)=L_0(t)\left[1+\sum_{s} e(t,s)\frac{\Delta p^e(s)}{p_0^e(s)}\right] ] 其中 (e(t,s)) 是弹性系数。(t=s) 时是自弹性,通常为负值表示涨价会抑制用电;(t\ne s) 时是交叉弹性,表示其他时段电价变化对本时段负荷的影响,这在峰谷电价转移中非常关键。
实际复现时,线性化的方式是把上式变成一组线性等式约束,引入一个“负荷调整量” (\Delta L(t)),并且对调整量加上限制。比如某园区原始电负荷为 1000 kW,峰时段电价从 1.2 元/kWh 涨到 1.5 元/kWh,自弹性取 -0.3,那么这个时段大约会有 7.5% 的负荷被移走。但建模时你真按这个公式算,会发现一个很现实的问题:交叉弹性矩阵如果设置不合理,很容易导致某些时段负荷变成负数,这明显不可能。所以我建议给调整后的负荷加上上下限约束,比如 (0.5L_0(t) \le L(t) \le 1.2L_0(t)),同时让所有时段的总负荷保持一个合理范围。
价格型需求响应解决了“用户主动避峰”,但它假设用户很理性,且全部按价格信号行动。现实中用户往往需要额外激励才愿意调整,于是就有了激励型需求响应。
2.2 激励型需求响应与 IDR 的跨能源替代
激励型需求响应跟价格型本质区别在于:前者靠价格引导,后者靠补偿合约。在模型里,价格型需求响应不直接进目标函数花钱,只是通过价格弹性改变负荷曲线;而激励型需求响应会在目标函数里加入一笔补偿成本。
典型场景是“可削减负荷”和“可转移负荷”。可削减负荷意味着用户允许系统在某些时段将其负荷切掉一部分,但每削减 1 千瓦时要给用户补偿。建模时需要一个削减量上限: [ 0 \le \Delta L_{cut}(t) \le \Delta L_{cut}^{max}(t) ] 可转移负荷更复杂,它往往是一个确定的工作周期,比如工业电解槽、污水处理装置,必须连续运行若干小时,但可以提前或推迟启动。这类负荷通常用 0-1 变量去约束启动状态,表示“这台设备在第 3 小时启动,连续运行 4 小时后关闭,只能整体平移,不能拆分”。
到这里只是传统需求响应。所谓综合需求响应,还包含一个“跨能源替代”的维度:用户不仅削减电负荷,还可以在电、热、气之间互相转换。比如一个工厂既有电锅炉又有燃气锅炉,电价很高时,把一部分热量需求由燃气锅炉承担;气价高时再切回电锅炉。这等于把原来独立的电负荷、热负荷响应统一成一个综合决策。
我在建模时会把终端负荷拆成刚性负荷、可转移负荷、可削减负荷、可替代负荷四类,然后分别写约束。刚性负荷必须满足,可转移负荷满足调峰约束和总量不变约束,可削减负荷满足上限约束,可替代负荷在能量等值条件下参与电热气联调。这样做的好处是,需求响应不再只是“把负荷曲线改一下”,而是真正参与了设备出力决策,这也是标题里“综合”二字的由来。
3. 阶梯型碳交易机制如何线性化
3.1 配额、实际排放和交易量三步走
碳交易机制的建模有三步:先算碳配额,再算实际碳排放量,最后根据差值在碳市场购买或出售配额。这三个概念如果没理清,后面很容易把符号搞反。
免费碳配额通常按产出基准法分配,最常用的形式是: [ E_{free} = \alpha \cdot D_{load} ] 其中 (D_{load}) 是系统总负荷或总发电量,(\alpha) 是单位电量或单位负荷对应的免费配额系数。不同论文的配额基准差异很大,有的按发电量给配额,有的按负荷侧给配额,还有的按历史排放值给配额。复现前你先看摘要和 3.2 模型构建部分,确定用的是哪种,别看到公式就抄。
实际排放量要逐设备算。燃气轮机燃烧天然气排放二氧化碳,燃气锅炉也排,外购电通常算作间接排放,因为发电侧上游已经排了碳。表达式可以写成: [ E_{actual} = \sum_t \left[ \mu_{gt} P_{gt}(t) + \mu_{gb} H_{gb}(t) + \mu_{grid} P_{buy}(t) \right] \cdot \Delta t ] 这里的排放系数要统一量纲。我复现时最头疼的是单位混用,有的论文给 (t/(MWh)),有的给 (kg/(kWh)),稍不注意就差了 1000 倍。
然后碳交易量为: [ E_{trade} = E_{actual} - E_{free} ] 当 (E_{trade} > 0) 时,系统配额不够用,需要购买;当 (E_{trade} < 0) 时,配额有富余,理论上可以出售获利。注意,模型里若允许出售配额,目标函数的碳交易成本会变成负值,这相当于给减排行为发了补贴。如果论文里不考虑卖出,就把负值直接清零。
3.2 阶梯价格的分段函数与 MILP 建模
阶梯型碳交易机制的关键在于:碳价不是一刀切的常数,而是随着购买量增加逐级抬高。这个设计非常符合实际碳市场逻辑——排放越多,超额部分越贵,倒逼企业减排。
我复现时常用一个三段或五段的阶梯价格表。举个例子:
| 碳交易量区间 | 区间上限 | 碳价 |
|---|---|---|
| 第 1 段 | 0 ~ a1 | λ₀ |
| 第 2 段 | a1 ~ a2 | λ₀ + δ |
| 第 3 段 | a2 ~ a3 | λ₀ + 2δ |
其中 λ₀ 是基础碳价,δ 是阶梯增量。如果交易量为 (E_{trade}),则成本不是简单的 (\lambda E_{trade}),而必须分段累加。这个函数是分段线性的,但存在不连续折点,直接放进线性规划会出问题。
正确的做法是引入分段变量 (x_1, x_2, x_3),满足: [ E_{trade} = x_1 + x_2 + x_3 ] 并且: [ 0 \le x_1 \le a_1, \quad 0 \le x_2 \le a_2 - a_1, \quad 0 \le x_3 \le a_3 - a_2 ] 成本: [ C_{co2} = \lambda_0 x_1 + (\lambda_0+\delta) x_2 + (\lambda_0+2\delta) x_3 ]
但这里藏着一个大坑:如果不加限制,求解器会把交易量全塞到第 1 段,因为第 1 段碳价最低。你必须用二进制变量强制“装满前面的段,才能用后面的段”。典型写法是引入两个二进制标志位 (z_1, z_2),并用大 M 法约束:
- 若 (x_2 > 0),则 (x_1 = a_1)
- 若 (x_3 > 0),则 (x_2 = a_2 - a_1)
用大 M 公式表达: [ x_2 \le M z_1,\quad x_1 \ge a_1 - M(1 - z_1) ] [ x_3 \le M z_2,\quad x_2 \ge (a_2-a_1) - M(1 - z_2) ] 其中 (z_1=1) 表示第 1 段已经装满,(z_2=1) 表示第 2 段已经装满。这套约束写进 YALMIP 后,模型就变成标准的 MILP 问题,Gurobi 可以直接求解。注意大 M 不能取太小,否则数值不稳定;也不能取太大,否则求解器会浪费大量分支。我一般取系统最大可能排放量的 5~10 倍。
4. 目标函数与约束条件的代码化装配
4.1 目标函数:把成本项和碳交易成本写进一个表达式
综合能源系统优化调度的目标函数一般可以写成下面这个形式:
目标函数 = 购电成本 + 购气成本 + 运行维护成本 + 需求响应补偿成本 + 碳交易成本
其中购电成本是每个时段的外购电功率乘以该时段电价,再按时长累加。购气成本是燃气轮机和燃气锅炉消耗的天然气量乘以气价。运行维护成本通常简化成机组出力的线性函数,比如某台设备每发一度电运维成本 0.02 元。需求响应补偿成本是最容易被忽略的,它让模型愿意在碳价高、电价高的时候去削减负荷,而不是一味硬扛。
碳交易成本我单独强调:如果你用第 3 节的分段线性模型,那么 (C_{co2}) 本身也是分段累加结果,而不是一次性项。
写进 YALMIP 时,目标函数就是一个sdpvar表达式。比如:
Objective = sum(price_buy .* P_grid) * dt ... + price_gas * (P_chp/eta_chp + H_gb/eta_gb) * dt ... + c_om * (P_chp + H_chp + H_eb + P_dis) * dt ... + c_dr * sum(DR_cut + DR_shift) * dt ... + C_co2;这里有一个经验:最好把全部成本统一到同一个单位再代入。比如时间步长 ( \Delta t=1) 小时,电价如果是元/kWh,功率用 kW,则购电成本单位是元,刚好对齐。如果你用 MW,电价用元/MWh,那也算能对齐。最怕的是功率用 kW,电价却用元/MWh,算出来会差 1000 倍。
4.2 核心约束:平衡、设备出力、储能与爬坡
约束条件是整个复现最容易翻车的地方,也是论文“看着简单、写着一堆 bug”的重灾区。
第一组是电能平衡约束。每一时刻所有电源出力等于所有负荷需求: [ P_{grid}(t) + P_{chp}(t) + P_{pv}(t) + P_{dis}(t) = P_{load,afterDR}(t) + P_{eb}(t) + P_{chg}(t) ] 这里的 (P_{dis}) 是蓄电池放电,(P_{chg}) 是充电功率。很多人会忘记电锅炉也是一个电负荷,如果直接写成 (电源=总电负荷),那电锅炉消耗的电就没地方去了。
第二组是热能平衡约束: [ H_{chp}(t) + H_{gb}(t) + H_{eb} + H_{dis,thermal} = H_{load,afterDR}(t) + H_{chg,thermal} ] 第三组是机组特性约束,比如燃气轮机的热电比约束: [ H_{chp}(t) = R_{chp} P_{chp}(t) ] 也可以写成一个可运行域,包含最小出力、爬坡速率和热电耦合。最简单实用的写法是给 (P_{chp}) 和 (H_{chp}) 分别设上下限,再加一个线性耦合约束。
第四组是储能约束,包括:
SOC(t+1) = SOC(t) + (eta_chg * P_chg(t) - P_dis(t) / eta_dis) / Cap_storage; 0 <= P_chg(t) <= b_chg(t) * P_chg_max; 0 <= P_dis(t) <= b_dis(t) * P_dis_max; b_chg(t) + b_dis(t) <= 1;最后这个约束非常重要,它保证电池不会同时充放电。如果不加这一条,模型就会出现“用一个设备同时充放电来薅羊毛”的无厘头结果。
第五组是爬坡约束: [ P_{chp}(t) - P_{chp}(t-1) \le Ramp_{up} ] [ P_{chp}(t-1) - P_{chp}(t) \le Ramp_{down} ] 爬坡约束经常被新手漏掉,漏掉之后模型会给出一个非常“生猛”的调度曲线:机组出力从 800 kW 瞬间跳到 200 kW。这在现实中根本做不到,但求解器完全不觉得有问题。
5. MATLAB 实操:从数学符号到可运行程序
5.1 工具选型:YALMIP + 求解器为什么是默认组合
这个复现用 MATLAB 自带的优化工具箱也能做,但我不推荐。原因有三点。第一,optimproblem的变量定义方式偏“矩阵化”,遇到循环索引非常别扭;第二,约束横跨平衡式、不等式、二进制变量、分段线性函数,YALMIP 的表达更接近数学符号;第三,YALMIP 可以无缝切换 Gurobi、Cplex、Mosek 等求解器,论文里如果要求对比不同求解器,你只需要改一行sdpsettings。
整个环境建议这样配:
- MATLAB 版本不要太老,我建议 R2021 以上。
- YALMIP 装最新版,直接从 GitHub 仓库下载并
addpath就能用。 - 求解器优先 Gurobi,如果学校没有 Gurobi 授权,可以用 Cplex,再不行就用 MATLAB 自带的
intlinprog,但求解时长会增加。 - 在
sdpsettings里设置'solver', 'gurobi',并打开求解日志。
我踩过最明显的坑是求解器版本和 YALMIP 对不上,导致调用时提示gurobi: solver not found。这个问题十有八九是gurobi.m这个文件没被 MATLAB 找到,不一定是安装失败。解决方法是把 Gurobi 安装目录下的 MATLAB 路径手动addpath(genpath(...)),然后运行yalmiptest检查是否成功识别。
5.2 参数模板:直接照抄的基准参数表
复现论文最怕的设置不一致。下面这张表是我自己跑这类综合能源系统优化调度常用的基准参数,可以作为起步模板,但具体数值一定要随论文原文修正。
| 参数 | 数值 | 单位 | 说明 |
|---|---|---|---|
| 调度时段数 | 24 | h | 步长 1h |
| 电负荷峰值 | 1200 | kW | 峰谷分布需自行设置 |
| 热负荷峰值 | 700 | kW | 供暖季典型值 |
| 光伏装机 | 300 | kW | 出力曲线取典型日 |
| 燃气轮机容量 | 500 | kW | 电出力上限 |
| 燃气轮机热电比 | 1.2 | - | 余热产出与电出力比例 |
| 燃气锅炉效率 | 0.9 | - | 热效率 |
| 电锅炉效率 | 0.95 | - | 电转热效率 |
| 蓄电池容量 | 600 | kWh | |
| 蓄电池充放效率 | 0.95 | - | 充放对称 |
| 外购电上限 | 800 | kW | 受上级变压器容量限制 |
| 天然气价格 | 2.5 | 元/m³ | 可折算为 0.35 元/kWh |
| 碳配额系数 α | 0.45 | tCO2/MWh | 负荷侧配额基准 |
| 基础碳价 λ₀ | 65 | 元/tCO2 | |
| 碳价阶梯增量 δ | 15 | 元/tCO2 |
这张表有一个细节:碳配额系数如果按负荷电量给,单位是 tCO2/MWh,那么计算时要把所有电量都折算到 MWh,不能一边用 kW 一边用 MWh。我在第一次复现时就是因为这里单位乱掉,导致碳配额计算出来比碳排放还大,结果模型疯狂卖配额,目标函数变成负无穷。
5.3 核心代码骨架与求解器调用
下面是一个可以直接跑通的 MATLAB + YALMIP 核心骨架,先看整体结构。
n = 24; dt = 1; % 小时 P_load0 = [3, 3, 3, 3, 3, 3, 4, 5, 6, 7, 8, 9, 10, 9, 8, 8, 7, 7, 8, 9, 10, 9, 6, 4] * 100; % kW H_load0 = [4, 4, 4, 4, 4, 4, 5, 5, 6, 6, 6, 6, 5, 5, 5, 5, 6, 6, 7, 7, 6, 5, 5, 5] * 100; % 决策变量 P_grid = sdpvar(n, 1); P_chp = sdpvar(n, 1); H_chp = sdpvar(n, 1); H_gb = sdpvar(n, 1); H_eb = sdpvar(n, 1); P_eb = sdpvar(n, 1); P_dis = sdpvar(n, 1); P_chg = sdpvar(n, 1); b_chg = binvar(n, 1); b_dis = binvar(n, 1); SOC = sdpvar(n+1, 1); DR_el = sdpvar(n, 1); % 电负荷削减量 DR_ht = sdpvar(n, 1); % 热负荷削减量 % 设备参数 eta_chp = 0.35; eta_gb = 0.9; eta_eb = 0.95; R_chp = 1.2; Cap_bat = 600; Constraints = []; % 电能平衡 Constraints = [Constraints, P_grid + P_chp + 30 + P_dis ... >= P_load0 - DR_el + P_eb + P_chg]; % 热能平衡 Constraints = [Constraints, H_chp + H_gb + H_eb >= H_load0 - DR_ht]; % 热电耦合 Constraints = [Constraints, H_chp == R_chp * P_chp]; Constraints = [Constraints, P_eb == H_eb / eta_eb]; % 设备上下限 Constraints = [Constraints, 0 <= P_chp <= 500]; Constraints = [Constraints, 0 <= H_chp <= 700]; Constraints = [Constraints, 0 <= H_gb <= 800]; Constraints = [Constraints, 0 <= H_eb <= 600]; % 电池约束 Constraints = [Constraints, SOC(1) == 300]; Constraints = [Constraints, 0 <= P_chg <= b_chg * 200]; Constraints = [Constraints, 0 <= P_dis <= b_dis * 200]; Constraints = [Constraints, b_chg + b_dis <= 1]; Constraints = [Constraints, SOC(t+1) == SOC(t) + (0.95*P_chg - P_dis/0.95)/Cap_bat]; % 需求响应上限 Constraints = [Constraints, 0 <= DR_el <= 0.2 * P_load0]; Constraints = [Constraints, 0 <= DR_ht <= 0.2 * H_load0]; % 目标函数 price_buy = [4,4,4,4,4,4,5,6,7,8,8,9,10,9,8,8,7,7,8,9,10,9,6,4] * 0.1; % 元/kWh Objective = sum(price_buy .* P_grid) * dt ... + 0.35 * (P_chp / eta_chp + H_gb / eta_gb) * dt ... + 0.02 * (P_chp + H_chp + H_eb + P_dis) * dt ... + 2 * sum(DR_el + DR_ht) * dt ... + C_co2; ops = sdpsettings('solver', 'gurobi', 'verbose', 1); sol = optimize(Constraints, Objective, ops); if sol.problem == 0 P_grid_opt = value(P_grid); P_chp_opt = value(P_chp); % .... 后续画图 else disp(sol.info); end这段代码能跑,但它只是骨架。真实复现时还要加上碳交易分段约束、爬坡约束、可转移负荷的二进制约束。如果你把碳交易分段约束直接粘进去,必须先把 (C_{co2}) 按第 3.2 节的方法定义成分段变量表达式。
还有一个细节:电能平衡我写成了>=而不是==,这在初期调试时比较友好,因为模型有富余空间就不会那么脆。但正式复现时,如果论文是“保证供需平衡”,通常要改回==,否则系统可能无成本地弃掉多余出力。如果你要允许弃风弃光和切负荷,那就加松弛变量,并在目标函数里加惩罚项,而不是简单用>=糊弄过去。
6. 复现过程中的高频问题与排查方法
6.1 无解和“可解但不可行”的排查路径
我在复现这类模型时遇到的第一大坑是模型直接报infeasible。Gurobi 会给你一个提示:Model is infeasible,但不会告诉你哪条约束错。这时候最蠢的办法是一条条猜,最有效的办法是先从变量范围查起。
第一步查设备容量和负荷量级。举个例子,某段时间热负荷是 1200 kW,但所有热源设备加起来最大热出力只有 900 kW,那热平衡必然无解。这种问题在电热耦合系统里尤其常见,因为燃气轮机的热电比是固定的,热出力跟着电出力走,可能出现“电平衡刚好满足,但热出力不够用”的矛盾情况。
第二步查 SOC 约束。很多论文会要求储能首末时段电量相等,也就是 (SOC(1)=SOC(25)=SOC_0)。如果你把 SOC 的初值设成 300 kWh,期末也要求回到 300 kWh,而在夜间充放电能力不足,就会出现无解。我的建议是先用松弛版本跑通:只设 SOC(1),不设 SOC(n+1),确认系统本身可行,再逐步收紧约束。
第三步用 YALMIP 自带命令排查,比如sol.info、sol.problem,更有用的是checkset(Constraints),它会逐条显示约束的最大违反量。哪条约束 violate 得离谱,问题就在哪。这个方法我到现在都在用。
6.2 碳交易阶梯约束的经典“便宜段”错误
碳交易分段约束如果只写了变量范围,没写装满顺序,就会出现一个经典错误:系统把大量碳配额需求全部塞进第一段低价区间,导致碳交易成本被严重低估,最后求解器给出一个“特别环保”但其实不符合机制的调度结果。
更隐蔽的问题是使用了大 M 法但 M 取得不合适。M 太小会把可行域卡死,M 太大又会让求解器花费大量时间处理数值病态。我复现时经常看到有人写M = 1e6然后整个模型求解不动,最后换M = 5000就能秒出结果。针对这个模型,大 M 取最大碳交易量的 3~5 倍就够。
如果你手头的 YALMIP 版本支持pw函数,还可以用自带的分段线性表达。但我个人建议新手还是老老实实写二进制变量和线性约束,因为pw的工业代码在用户不懂细节时反而更容易产生隐藏 bug,一旦算错,排查成本比手写还高。
6.3 量纲与数值缩放问题
这个坑我要单独拉出来说,因为十个人复现至少有八个人在这里出问题。综合能源系统模型里的物理量跨度极大:设备功率是几百 kW,碳排放配额是几十吨,碳价是几十元每吨,需求响应补偿是几元每千瓦时。如果你把所有成本直接相加,目标函数可能是一个 (10^8) 级别的数,这让求解器在数值上非常难受。
我的习惯是统一用“元”做成本单位,功率用“kW”,能量用“kWh”,碳配额用“tCO2”,然后观察目标函数量级。如果总成本达到 (10^7) 以上,我就把目标函数除以一个基准数,比如 (10^6),相当于用“万元”作为目标单位。这样 Gurobi 的默认容差设置会工作得很好,求解速度明显提升。
另一个常见单位坑是天然气热值。很多论文里天然气价格给的是“元/m³”,但燃气轮机效率是以热值计算的。你要先把天然气价格换算成“元/kWh”,换算公式是: [ c_{gas_kWh} = \frac{c_{gas_m3}}{天然气低位热值(kWh/m^3)} ] 天然气低位热值一般取 9.5 kWh/m³ 左右。如果忽略了这一步,购气成本会高出十倍,结果自然是燃气机组一点不敢用,全系统都靠电网买东西,出力曲线变得非常畸形。
7. 结果怎么验证才算复现成功
7.1 关键输出曲线怎么读
代码跑通只是第一步,结果合理才算复现成功。我一般从五张图开始看。
第一张图是电平衡堆叠图,横轴是 24 个时段,纵轴是功率,把光伏出力、风电出力、燃气轮机出力、外网购电、蓄电池放电叠成堆叠柱状图。堆叠图如果出现负数,说明平衡约束或变量符号有问题。
第二张图是热平衡堆叠图,方法与电平衡类似,但要特别注意燃气轮机余热出力必须和电出力同步变化。如果看到燃气轮机在某个时段电出力很低但热出力很高,那就违反了热电比约束。
第三张图是需求响应前后的负荷曲线对比。原始负荷是一条高峰在白天、低谷在凌晨的曲线,响应后的曲线应该出现“峰削谷填”的效果。如果两曲线几乎完全重合,说明需求响应补偿价格设得不够高,或者响应量上限设得太低,约束被“闲置”了。
第四张图是蓄电池 SOC 曲线,正常应该是一条连续平滑变化的曲线,不会频繁跳变。如果 SOC 在每个时段都在剧烈波动,说明储能的充放成本权重太低,储能在那里单纯做套利,而且效果差。
第五张图是碳交易量和碳交易成本的曲线。碳交易量应该和燃气机组、外购电的使用强相关,且阶梯成本在跨过段点时会有跳变。如果碳交易成本是一条完美直线,大概率是分段约束没生效。
7.2 对比实验与灵敏度分析怎么设计
论文复现还有一步不能省:把“考虑综合需求响应”和“考虑阶梯型碳交易机制”的贡献分别拆出来。一般设置四个场景:
- 场景一:都不考虑,只做基础经济调度。
- 场景二:只考虑综合需求响应。
- 场景三:只考虑阶梯型碳交易机制。
- 场景四:两者都考虑。
然后对比总成本、碳排放量和设备出力结构。如果场景四的总成本比场景三还高,不一定说明模型错了,可能只是需求响应补偿成本高于它节省的燃料成本;但如果场景四的碳排放和场景三完全一样,那就是需求响应约束根本没有激活,需要检查补偿价格或负荷弹性系数。
灵敏度分析也很重要,比如扫描基础碳价 (\lambda_0) 从 50 元/吨到 120 元/吨,观察燃气轮机出力和外购电量的变化。如果碳价升高后外购电反而减少,那基本上就说明模型有 bug,因为更高碳价应该让系统少用碳排高的能源,多用碳排低的能源。用这一条就能快速判断结果是否符合机理。
我在实际复现这类论文时最大的体会是,所谓“复现成功”不一定是和原文结果一模一样,而是你的模型在机理上与论文传达的核心逻辑一致。碳交易阶梯机制必须真的让碳价随排放量上升,需求响应必须真的改变负荷曲线,储能必须真的在低谷充电、高峰放电。这三个“必须”满足了,哪怕你用的参数跟论文略有差异,结果也大概率在可接受范围内。做这种复现项目,最忌讳的是代码跑出一个漂亮图,但图里的数据和物理机理完全对不上。先保证每一个变量都有明确的物理含义,再谈算法优化和模型扩展。