1. 这个项目的起点:为什么非要把DR和碳交易放进同一个能源系统里
我最早接触这个方向,是因为一个园区级综合能源系统的规划问题。冷、热、电、气四种能源形式同时存在,设备侧有燃气轮机、余热锅炉、燃气锅炉、吸收式制冷机、电制冷机,还有储能装置。如果只按照传统“负荷跟随”的思路去调度,系统能跑通,但经济性和碳排放指标都不好看。真正想做出点效果,必须把综合需求响应和碳交易机制一起放进优化模型里,让供给侧设备和需求侧行为共同参与决策。
综合需求响应(Integrated Demand Response,IDR)和单一种类需求响应的最大区别,是它同时覆盖电、热、冷、气四种负荷。过去我们做电力需求响应,只需要考虑“电在某个时段涨了价,用户要不要少用”。但到了综合能源系统里,电负荷可能换成热负荷,热负荷可能由气来供,冷负荷可以由电驱动也可以由热驱动。每一种能源的价格变化,都可能引发跨能源形式的负荷转移。这时候再看单一需求响应,很容易漏掉系统里最有价值的调节手段。
碳交易机制则完全是另一个维度。它的本质是给碳排放标了价。你排得少,配额富余,可以卖出获利;你排得多,配额不够,就要花钱去市场买。加入碳交易之后,系统优化的目标函数里多了一项跟碳排放挂钩的成本项,于是天然气和电网电力的选择逻辑就变了。电网电力的排放因子通常明显高于天然气,也就是说,同样满足一单位的电需求,多用电可能比多用气产生更多碳排放费用。这就可能改变燃气轮机的“热电联供”运行策略:以前只看电价比气价便宜还是贵,现在还要加上碳价这个权重。
更关键的是,综合需求响应和碳交易不是两个独立的模块,它们会互相影响。需求响应改变了负荷曲线,负荷曲线决定了设备出力,设备出力又决定了实际碳排放量,最终影响碳配额的盈余和缺口。反过来,碳价升高会让系统更倾向于少用高排放能源,这种倾向又会改变不同时段对用户的能源报价,用户侧的需求响应行为也随之改变。所以这个项目从一开始就不是“先算DR再算碳交易”的顺序问题,而是两者必须耦合到同一个优化模型里同步求解。
整个项目做下来,我的体会是:只要理解了“DR改变负荷形状、碳交易改变成本结构、两者共同改变设备运行方式”这条逻辑链,后面建模型、写约束、跑案例就顺了。很多人卡在第一步,是因为把需求响应和碳交易拆成了两件独立的事,最后模型建出来要么是“有DR没碳价”,要么是“有碳价但用户侧完全刚性”,这样的结果在实际工程里参考价值很低。
2. 能量枢纽的设备建模:从物理设备到可计算的数学方程
2.1 系统内能源设备的主体构成
这个项目标题里提到“系统内含能源设备主...”,实际搭建的时候,我采用的是典型的能量枢纽结构。能源输入端有两路:一路是电网购电,一路是天然气网购气;可再生能源部分可以接光伏和风电,作为电力的补充。设备侧的核心转换单元包括四类:第一类是热电联产机组(燃气轮机配余热锅炉),同时产生电和热;第二类是燃气锅炉,直接补热;第三类包括吸收式制冷机和电制冷机,分别把热和电转换成冷;第四类是储能装置,我选了蓄电池、蓄热罐、蓄冷罐三种,分别对应电、热、冷三种储能形式。
这里有一个容易被忽略的设计点:气这种能源形式,在设备侧不仅是燃料,也具备独立的冷热电以外的流动。换句话说,天然气的一部分直接供给燃气轮机和燃气锅炉,另一部分理论上可以通过P2G设备转化或存储在气体网络中。但由于目前大多数园区项目不具备实际P2G需求,我在核心模型里仍然把“气”作为输入能源来建模,只在扩展场景里预留了气体储罐和天然气负荷转移的接口。这样处理的好处是,既能满足标题里“冷热电汽四种能源形式”的覆盖要求,又不会把模型复杂度推到没有必要的高度。
2.2 关键设备的数学建模方式
建立优化模型时,设备模型不能写成黑箱,必须给出输入、输出和效率之间的数学关系。下面表格是我最终采用的设备建模思路,涵盖了系统里最主要的能源设备:
| 设备 | 输入能源 | 输出能源 | 建模关键表达式 |
|---|---|---|---|
| 燃气轮机 | 天然气 | 电、高温烟气 | (P_{gt} = \eta_{gt}^e \cdot F_{gt}),余热回收 (H_{hr} = \eta_{hr} \cdot (1-\eta_{gt}^e) \cdot F_{gt}) |
| 余热锅炉/换热器 | 高温烟气 | 热 | (H_{gb} = \eta_{gb} \cdot H_{hr}) |
| 燃气锅炉 | 天然气 | 热 | (H_{boiler} = \eta_{boiler} \cdot F_{boiler}) |
| 吸收式制冷机 | 热 | 冷 | (C_{ac} = COP_{ac} \cdot H_{ac}) |
| 电制冷机 | 电 | 冷 | (C_{ec} = COP_{ec} \cdot P_{ec}) |
| 蓄电池 | 电 | 电 | 荷电状态迭代更新,含充放电效率 |
| 蓄热罐/蓄冷罐 | 热/冷 | 热/冷 | 储能量迭代更新,含自损耗系数 |
| 光伏/风电 | 太阳能/风 | 电 | 按预测出力上限约束,不参与优化出力 |
这里最需要注意的是燃气轮机的建模。很多初学者喜欢直接用固定热电比,写成 (H = k \cdot P),这样虽然简单,但在变工况场景下误差非常大。我在项目里对燃气轮机采用的是“发电效率随负载率变化”的分段线性化模型,把负载率从 0.3 到 1.0 分成 4~5 段,每一段对应不同的发电效率和热电比。这样模型虽然增加了一些二进制变量,但换来的是调度结果在真实设备上可用性大幅提升。
对于储能设备,核心约束是相邻时段的能量递推关系。以蓄电池为例:
[ SOC_{t} = SOC_{t-1} + \eta_{ch} \cdot P_{ch,t} - \frac{P_{dis,t}}{\eta_{dis}} - \tau \cdot E_{rated} ]
其中 (\tau) 是自放电率,实际运行中很多人会把这个项省略,但如果是做连续数天的调度,自放电和蓄热罐的热损失对长周期结果影响很明显,建议保留。另外充放电不能同时进行,这一点要用二进制变量约束:
[ P_{ch,t} \le M \cdot u_{ch,t}, \quad P_{dis,t} \le M \cdot (1 - u_{ch,t}) ]
2.3 四种能源形式的平衡约束骨架
能量平衡是整个模型最基础、也是绝对不能出错的约束。在每一个调度时段 t,系统必须满足以下四类平衡关系:
电力平衡:光伏出力 + 风电出力 + 燃气轮机出力 + 蓄电池放电 + 电网购电 = 电负荷 + 电制冷机耗电 + 蓄电池充电
热力平衡:余热锅炉供热 + 燃气锅炉供热 + 蓄热罐放热 = 热负荷 + 吸收式制冷机耗热 + 蓄热罐充热
冷力平衡:吸收式制冷机供冷 + 电制冷机供冷 + 蓄冷罐放冷 = 冷负荷 + 蓄冷罐充冷
天然气平衡:天然气网购气量 = 燃气轮机耗气 + 燃气锅炉耗气 + 气负荷
实际项目中油气和电网也有互动,比如电网购电量参与碳排放计算,这就要在后续章节里单独处理。我在工程实施中建议把这四个等式约束单独封装成函数,输入是设备决策变量,输出是各时段不平衡量,调试模型的时候直接把不平衡量打出来,一眼就能看出是哪个环节的约束写错了。
3. 综合需求响应:四类负荷分别怎么“软”下来
综合需求响应落到模型里,核心是把原本固定的“负荷”改成可调决策变量。我在这个项目里没有把所有负荷都做成一刀切的可削减量,而是把电、热、冷、气四类负荷分别清洗出来,按响应机制分成三类:可转移负荷、可削减负荷、可平移负荷。这里直接决定了系统能“让出”多少调节空间。
3.1 价格型需求响应:用需求价格弹性实现跨时段转移
价格型需求响应的思路是:用户会根据电价、热价、气价的相对高低,主动调整用能时段和用能方式。模型里最常用的写法是价格弹性矩阵法。对于电力负荷来说,某一时段的用电量变化量 (\Delta L_t) 与各时段电价变化率 (\Delta \pi_t) 之间的关系可以写成:
[ \frac{\Delta L_t}{L_t^0} = \sum_{t'} e_{t,t'} \cdot \frac{\Delta \pi_{t'}}{\pi_{t'}^0} ]
其中 (e_{t,t'}) 是自弹性系数((t=t'))和交叉弹性系数((t \neq t'))。自弹性是负数,表示价格上涨导致本时段用电减少;交叉弹性一般是正数,表示其他时段价格上涨,负荷会转移到本时段。这个模型的好处是能直接反映“削峰填谷”的微观经济逻辑。
我在项目中把这个方法推广到了热和冷负荷上。热负荷的交叉弹性描述了“现在少用热、过一个小时多用热”;冷负荷的响应则更复杂一点,因为冷负荷既可以由电制冷机供,也可以由吸收式制冷机供,所以冷负荷的价格响应在模型里还会触发“电制冷和吸收式制冷之间的设备切换”,这实际上已经变成了综合能源系统里特有的交叉价格响应。
3.2 激励型需求响应:可中断负荷和可削减负荷的补偿项
激励型需求响应更适合用“用户签订合同,运维商在必要时切负荷并支付补偿”的方式表达。我在模型里为可中断负荷设置了最大中断量约束和最少连续供电时间约束。比如某工业用户允许在特定时段最多中断 10% 的功率,但一个时段被中断后,下一个时段必须恢复供电,这个约束可以更贴近实际用户意愿。
可削减负荷的公式比较简单:
[ L_{cut,t} \le \gamma_t \cdot L_{t}^0 ] [ L_{t} = L_{t}^0 - L_{cut,t} ]
其中 (\gamma_t) 是最大削减比例。对应地,目标函数中需要增加补偿费用:
[ C_{DR} = \sum_t \left( \lambda_{inter} \cdot L_{inter,t} + \lambda_{cut} \cdot L_{cut,t} \right) ]
这里有一个我踩过坑的地方:补偿单价不是统一常数,而是分阶梯的。比如削减 5% 以内是单价一档,5% 到 10% 是更高一档,如果不做分阶梯,优化模型会把削减容量优先分配到补偿单价最低的用户头上,哪怕这个时段系统非常缺电,造成了不合理的调度结果。
3.3 把DR写进约束和费用里,要注意接口的一致性
需求响应模块写完之后,需要把它和能量平衡约束对接起来。我实际项目的做法是:把“基础负荷预测值”当作参数输入到模型,把“实际负荷值”当作决策变量,并加上“实际负荷 = 基础负荷 - 削减量 - 转移量 + 转入量”的关系式。这样改动的代价是每条平衡约束里都要把负荷变量替换掉,但收益是模型可以自由模拟纯价格型、纯激励型或者两者混合的场景。
还需要注意一个细节:需求响应模型本身和碳交易模型是耦合的。当你允许电负荷削减时,电网购电量下降,碳排放下降;当你允许热负荷转移时,燃气轮机可能在另一个时段多出力,天然气消耗增加,碳排放也随之变化。我在做灵敏度分析时发现,在碳价较高的情景下,DR的削减行为会被进一步放大,因为“少用高碳能源”的收益不仅省了能源费,还省了碳配额购买费用。这个耦合效应必须在目标函数里一起体现,不能分开算两遍。
4. 碳交易机制如何进入优化目标:配额、碳价、阶梯价格
4.1 碳排放总量怎么算
要在模型里引入碳交易,第一步是把碳排放源列清楚。这个项目里碳排放主要来自两个地方:一是从电网购入的电量,因为电网上游主要是火电;二是天然气燃烧产生的排放。光伏和风电没有碳排放,储能设备本身也不直接排放。
计算公式如下:
[ E_{total} = \sum_t \left( \alpha_{grid} \cdot P_{buy,t} + \alpha_{gas} \cdot F_{gas,t} \right) ]
其中 (\alpha_{grid}) 是电网购电的碳排放因子,单位是 kg/kWh;(\alpha_{gas}) 是天然气的碳排放因子,单位是 kg/m³ 或 kg/kWh。这两个系数在不同地区、不同季节差异很大。我在项目里专门做了一组对比:如果电网排放因子取较高值,系统会更偏向多烧气少购电;如果取较低值,燃气轮机的利用小时数明显下降。
4.2 免费配额和碳市场买卖
碳交易机制里,免费配额是系统白拿的碳排放额度。这个配额往往按基准法计算,也就是说根据系统供应的能源量和行业基准排放强度来定。项目里可以写成:
[ E_{quota} = \sum_t \left( \delta_t \cdot L_{e,t} + \varepsilon_t \cdot H_{t} + \varphi_t \cdot C_{t} + \zeta_t \cdot L_{gas,t} \right) ]
这里的 (\delta_t)、(\varepsilon_t)、(\varphi_t)、(\zeta_t) 是分时段分种类的基准排放强度。值得注意的是,配额不一定够用。如果实际排放 (E_{total}) 大于配额,多余部分要在碳市场购买;如果小于配额,富余部分可以在市场上出售。
模型里只需要把它写成:
[ C_{carbon} = \lambda_{CO2} \cdot \max(0, E_{total} - E_{quota}) - \lambda_{CO2}^{sell} \cdot \max(0, E_{quota} - E_{total}) ]
在很多实际算例中,买卖价格并不相等。卖出价通常会低于买入价。如果不区分买卖价格,优化模型可能利用差价套利,产生虚假的收益,这一点一定要在模型里明确分开。
4.3 阶梯型碳价的建模方法
项目里我直接用的阶梯型碳价,而不是固定单价。原因是固定碳价下,只要配额不足就按同一个价格买碳,无法体现“排放越多,边际成本越高”的政策导向。阶梯碳价可以这样写:
[ \lambda_{CO2} = \begin{cases} \lambda_1, & 0 \le E_{excess} \le E_1 \ \lambda_2, & E_1 < E_{excess} \le E_2 \ \lambda_3, & E_{excess} > E_2 \end{cases} ]
这是一个非线性的分段函数。直接放进线性模型里会有麻烦,我采用的是加辅助变量的做法:把超出配额的排放量拆成几段,每段引入一个连续变量和一个二进制变量,用来约束落在对应区间内。例如:
[ E_{excess} = e_1 + e_2 + e_3 ] [ 0 \le e_i \le E_i^{max} \cdot z_i ] [ \sum_i z_i = 1 ]
这样碳交易成本变成了线性表达式:
[ C_{carbon} = \lambda_1 \cdot e_1 + \lambda_2 \cdot e_2 + \lambda_3 \cdot e_3 ]
分段数量不用太多,3段就能看出来“阶梯碳价比固定碳价更能抑制排放”的效果。如果分段太多,模型求解时间会明显增长,但收益增量很小,性价比不高。
5. 优化模型主框架和求解器选型
5.1 目标函数的结构
整个项目的优化目标是最小化综合运行成本。目标函数我按五部分组织:
[ \min \left( C_{energy} + C_{om} + C_{dr} + C_{carbon} - R_{sell} \right) ]
其中 (C_{energy}) 是购电和购气的能源成本;(C_{om}) 是设备运行维护成本;(C_{dr}) 是需求响应补偿成本;(C_{carbon}) 是碳交易成本;(R_{sell}) 是系统向电网售电的收入。这里不需要再单独加一个碳收益项,因为富余配额卖出已经体现在 (C_{carbon}) 公式的负数项里。
关于设备启停成本,我在前几版模型里没加,后来发现结果经常出现“设备每15分钟开关一次”的振荡现象,这才意识到必须给燃气轮机和燃气锅炉加上启停成本约束,用二进制变量记录启停动作,并在目标函数中增加一次性的启停费用。
5.2 为什么用混合整数线性规划而不是纯线性规划
这个模型最终是一个混合整数线性规划(MILP)问题,因为里面存在大量二进制变量。包括储能的充放状态、设备启停状态、阶梯碳价分段选择、可中断负荷的中断状态。我记得第一次跑24小时调度时,光二进制变量就接近 300 个。看起来不多,但由于储能的状态递推和平衡约束耦合在一起,如果用穷举法求解,基本上没有可行性。
我建议用 Python 的 Pyomo 框架搭模型,求解器用 Gurobi 或者 CPLEX。下面是模型主结构的简化示意:
import pyomo.environ as pyo model = pyo.ConcreteModel() # 时段集合 T = range(24) model.T = pyo.Set(initialize=T) # 决策变量:燃气轮机出力、锅炉出力、各储能充放功率、DR削减量 model.P_gt = pyo.Var(model.T, within=pyo.NonNegativeReals) model.H_boiler = pyo.Var(model.T, within=pyo.NonNegativeReals) model.P_ch = pyo.Var(model.T, within=pyo.NonNegativeReals) model.P_dis = pyo.Var(model.T, within=pyo.NonNegativeReals) model.L_cut = pyo.Var(model.T, within=pyo.NonNegativeReals) # 二进制变量:储能充放状态 model.u_ch = pyo.Var(model.T, within=pyo.Binary) model.u_dis = pyo.Var(model.T, within=pyo.Binary) # 目标函数略,此处只示意变量结构 model.objective = pyo.Objective(expr=sum( model.P_gt[t] * c_gas_eff + model.H_boiler[t] * c_gas for t in model.T), sense=pyo.minimize) # 互斥约束:储能不同时充放 def rule_battery(m, t): return model.P_ch[t] + model.P_dis[t] <= 500 * u_ch[t] + 500 * u_dis[t] model.battery_mutex = pyo.Constraint(model.T, rule=rule_battery) # 求解 solver = pyo.SolverFactory('gurobi') results = solver.solve(model, tee=True) run_time = results.solver.time print("求解时间", run_time)如果项目用的是 MATLAB,我一般用 Yalmip 加 Gurobi。下面是等价的 Yalmip 结构:
T = 24; P_gt = sdpvar(1, T); H_boiler = sdpvar(1, T); u_ch = binvar(1, T); u_dis = binvar(1, T); Constraints = []; Constraints = [Constraints, P_gt >= 0, H_boiler >= 0]; Constraints = [Constraints, P_ch <= M * u_ch, P_dis <= M * u_dis, P_ch + P_dis <= 200]; Objective = sum(P_gt * c_gas_eff + H_boiler * c_gas); ops = sdpsettings('solver', 'gurobi', 'verbose', 1); optimize(Constraints, Objective, ops);5.3 求解效率优化:减少二进制变量的小技巧
我第一版模型跑一个 24 小时算例,耗时 400 多秒,后来经过三个调整降到 25 秒以内。
第一个技巧是变量缩放。把功率单位从 kW 改成 MW,数值范围变小,Gurobi 的求解精度和收敛速度明显改善。第二个技巧是给储能互斥约束加入“如果不充电则放电、如果不放电则充电”的逻辑约束,减少病态解空间。第三个技巧更关键:把负荷数据按季节典型日聚类,可以从全年 8760 小时缩减为 4 个典型日乘以 24 小时,这样进行全年规划时既保留了季节差异,又不需要跑完整年的连续优化。
另外,如果模型规模继续增大,可以考虑用线性松弛做一次预热,把得到的解作为初始解喂给 MILP 求解器。我在一个后续扩展版本里用了这个技巧,把含 2000 多个二进制变量的算例求解时间从一小时缩短到十几分钟。对于一般的研究级综合能源系统项目,第一版根本不需要上启发式算法,直接把 MILP 调优到位就行。
6. 算例对比:DR和碳交易各贡献了多少收益
模型建完以后,我设计了四组场景来回答一个很实际的问题:综合需求响应和碳交易机制分别给系统带来了多少经济收益和碳排放改善。
场景 A:基础场景,没有任何DR,也没有碳交易。系统只按能源成本和设备约束运行。 场景 B:只引入综合需求响应,不做碳交易。用户侧可以转移和削减负荷。 场景 C:不引入综合需求响应,只做碳交易。系统必须为碳排放买单,但不改变负荷形状。 场景 D:同时引入综合需求响应和碳交易,也就是项目最终的完整模型。
这几组场景的典型结果趋势如下:
- 场景 C 的碳排放量比场景 A 下降最明显,说明碳交易直接通过成本压力倒逼供给侧设备调整。
- 场景 B 的碳排放降低幅度相对小,但它对“峰值购电”的削减很有效。因为DR让负荷曲线更平缓,系统不需要在高峰时段高成本购电购气。
- 场景 D 的总成本最低,碳排放也达到了次优水平。这说明综合需求响应和碳交易机制在成本上互补:DR省的是能源交易费用,碳交易省的是碳排放费用。
从敏感性分析来看,碳价从每吨 50 元涨到 200 元时,系统购气量占比会下降。但这里有一个反直觉的发现:碳价过高时,燃气轮机发电反而可能下降,因为高电价时段从电网购电的边际成本也许比本地燃气轮机发电加碳成本更划算。具体结果受电价结构和设备效率影响很大,所以不能人云亦云地认为“碳价越高就一定越鼓励燃气轮机满发”。
DR方面最大的变量是负荷可调比例。我在算例里把可调比例从 5% 提到 20%,发现系统总成本下降幅度呈现边际递减趋势。原因是当可调比例太大时,需求侧转移的负荷会在其他时段形成新的峰值,反而增加了系统在低效工况下的运行成本。这个比例在真实项目中一般通过问卷调查和历史负荷曲线聚类来确定,直接拍脑袋设成 30% 很容易得到不靠谱的优化结果。
7. 做这类项目最容易踩的5个坑
7.1 碳配额基准值随手填,结果全靠运气
碳配额的设定会直接影响碳交易成本的大小。有些人直接用一个常数乘以总负荷,算出来的结果看起来“有效果”,但经不起推敲。正确做法是分能源品种、分行业设置基准排放强度,至少匹配项目的真实主体设施。我在项目里用历史运行数据反推排放强度,再做配额敏感性分析,得到了一个合理区间,而不是一个拍出来的数字。
7.2 需求响应和碳交易的时间粒度不一致
综合能源系统里,电负荷通常是小时级甚至15分钟级,热负荷动态响应慢,冷负荷又有建筑热惯性。如果所有负荷都用同一个调度时段,需求响应的效果会被高估。我建议电负荷用15分钟粒度,热冷负荷用小时级粒度,中间通过聚合约束衔接。当然这会增加时间耦合约束的复杂度,但结果更可靠。
7.3 燃气轮机的热电比被固定成常数
这是模型失真的最大来源之一。真实燃气轮机在部分负载下发电效率和热电比都会变化,如果模型里写死一个热电比,优化器可能会让燃气轮机在低效区间运行而不自知。我把发电效率和热电比都做了分段线性化,并且给每段加了负载率区间约束。虽然模型变量增加,但调度结果和厂家实际运行数据的偏差明显减小。
7.4 储能设备状态没有跨天衔接
做单日调度时,蓄电池初始SOC和结束SOC一般可以自由设定。但如果做连续多日调度,最后一天结束时的储能状态会直接影响前一天的优化决策。我在项目里加了“调度周期结束时储能回到初始状态”的循环约束,或者允许终值在合理范围内,让结果具备可持续性。否则模型会把储能能量在最后一天“用干”,这在实际工程中是不可能接受的。
7.5 求解器报“infeasible”就急着调数据
MILP模型报无解时,大多数情况不是数据错了,而是约束互斥了。比如储能充电功率上限、放电功率上限分别约束没问题,但充电状态和放电状态互斥的约束把所有可行解都排除了。这时候应该用“松弛不可行约束组”的方式定位。我用 Gurobi 的 IIS(Irreducible Inconsistent Subsystem)功能找最小不可行约束集合,好几次都发现是峰谷时段负荷转移的耦合约束写重复了,和电磁数据一点关系都没有。
我个人在实际操作中还有一个习惯:先把所有二进制变量去掉,跑一个纯线性松弛版本,如果线性版本无解或者目标值异常,那就先别急着上整数变量,一定是基础约束出了问题。等项目逻辑理顺了,再逐步加回二进制变量,这样调试效率是最高的。这个习惯帮我省了大量排查时间,建议做综合能源系统优化的朋友都试一下。