最近在做园区级综合能源系统的优化调度项目,卡在最久的不是CHP、储能这些设备模型,而是光伏出力不确定性怎么处理。预测曲线就在那里,实际值总是偏离,用随机优化得先假设概率分布,用两阶段鲁棒优化又要把不确定集做对偶变换再上C&CG迭代,对一个中小规模的调度模型来说,工具选得越重,解释成本就越高。后来把信息间隙决策理论(IGDT)和阶梯碳交易机制一起嵌进调度模型,配合Python加Gurobi实现,整个思路一下子清爽了——不猜分布、不做对偶,只回答一个问题:在多花多少成本的预算约束下,系统能扛住多大的预测偏差。这篇文章就把这套从原理、建模到代码实现的完整脉络写清楚,适合正在做多能系统优化调度、对IGDT建模感兴趣,或者想把碳交易机制加进现有模型但不想绕弯路的读者。
1. 为什么选信息间隙决策理论:不确定性建模里被低估的轻量级方案
1.1 随机优化、鲁棒优化与IGDT的三角关系
处理不确定性的主流工具其实就三大类。随机优化要求你知道不确定参数的概率分布函数,然后用期望值或风险度量做目标;这需要足够的历史数据支撑分布假设,否则“假设正态分布”这件事本身就是最大的不确定性来源。传统鲁棒优化不依赖分布,但要预先界定一个不确定集,然后按最坏情况做决策,得到的方案往往偏保守,而且两阶段问题需要重构为min-max-min结构,推导和求解都比较重。
IGDT走的是第三条路。它既不猜概率,也不预设不确定集的边界,而是用一个“不确定性半径”α来描述预测参数的波动幅度,然后问一个反过来的问题:在可接受的成本偏离范围内,系统能承受的最大偏差是多少。这个视角特别适合“我手上的预测数据质量一般,但我想知道自己的系统到底有多皮实”的场景。
1.2 IGDT的数学内核:不确定性半径与两类风险策略
IGDT对不确定参数的刻画非常朴素。设预测值为p̄,实际值p可以落在这样一个集合里:
U(α, p̄) = { p : |p - p̄| / p̄ ≤ α }
其中α就是信息间隙的半径,表示预测值相对偏差的最大幅度。α=0意味着完全信任预测,α越大代表系统对预测偏差的容忍范围越宽。
在这个集合之上,IGDT定义了两种决策策略。鲁棒模型(风险规避)的目标是最大化α,同时要求系统在最坏偏差情况下的总成本不能超过基准成本的(1+σ)倍;这里的σ是决策者预先设定的成本偏离预算,σ越大,代表你愿意多花多少钱来换取对预测偏差的容忍度。机会模型(风险投机)则反过来,目标是找到最小的α,使得在乐观情况下成本有机会低于(1-σ)倍基准成本,回答的是“我有多大可能吃到预测外的好处”。
这两类策略对应着完全不同的决策心态。实际工程里绝大多数用户关心的是鲁棒模型——先保底,再谈优化。
1.3 什么时候该用IGDT:工程判断
从我实际使用的感受来看,IGDT最适合三类情况:第一,不确定性参数的历史数据不足,做概率分布没有信心;第二,模型本身已经包含大量0-1变量,再叠加两阶段鲁棒优化会让求解时间失控;第三,决策者更关心“风险-成本曲线”这类宏观边界,而不是具体某个随机场景下的最优解。
它也有短板。IGDT不区分不确定集合内部的参数分布,同样是α=0.3,偏差可能集中在尖峰时段也可能均匀分布,模型对这两种情况一视同仁。如果你手里有充足的风速、辐照度历史数据,随机优化仍然能提供更细颗粒度的决策信息。IGDT更像是“数据不足但需要量化安全边界”时的高性价比选择。
2. 多能系统的能流骨架:先把“电-热-气”的耦合关系理清
2.1 设备模型与能源链条
多能系统的核心是能源耦合设备。我用的园区系统包含四类主要设备:燃气热电联产机组(CHP)、燃气锅炉(GB)、电锅炉(EB)和电储能(ES),外加光伏(PV)作为可再生电源。电网购电和天然气网购气是两个外部能源入口。
CHP是耦合的关键,它同时产出电能和热能。简化建模时用热电比r把电出力和热出力绑在一起:
H_chp(t) = r × P_chp(t)
CHP消耗的天然气量由总能效折算:
V_gas_chp(t) = [P_chp(t) + H_chp(t)] / (η_total × LHV_gas)
我这里取η_total=0.7,热电比r=1.2,天然气低热值LHV_gas取9.7 kWh/m³。这样每个时段CHP的燃料成本就可以由电出力和热电比直接映射出来。
燃气锅炉和电锅炉相对简单。燃气锅炉直接把天然气转化成热能,效率取0.9;电锅炉则是电能到热能的转化,效率取0.95。储能设备采用常规的SOC递推模型:
SOC(t+1) = SOC(t) - P_dis(t)/η_dis + η_ch × P_ch(t)
其中η_ch和η_dis分别为充电和放电效率,均取0.95,Δt为1小时。
2.2 24小时调度中的平衡约束
不管设备怎么变,调度模型的骨架始终是能量平衡。我把电、热两个平衡单独列出来,天然气侧由于购气量直接与CHP和锅炉的消耗挂钩,不需要单独的平衡约束。
电力平衡:
P_buy(t) + P_pv(t) + P_chp(t) + P_dis(t) = P_load(t) + P_ch_es(t) + P_eb(t)
热力平衡:
H_chp(t) + H_gb(t) + H_eb(t) = H_load(t)
这里P_buy是外购电功率,P_ch_es是储能充电功率,P_eb是电锅炉消耗的电功率。注意电储能放电P_dis在等式左边是供给项,充电P_ch_es在等式右边是负荷项,方向千万不要写反。
除了平衡约束,机组还需要满足出力上下限和爬坡约束。CHP电出力范围我设为50-300 kW,燃气锅炉热出力0-500 kW,电锅炉0-200 kW。爬坡约束只加在CHP上:
|P_chp(t) - P_chp(t-1)| ≤ R_chp
爬坡速率R_chp取80 kW/h,对燃气轮机来说这个值偏保守,但园区项目里机组实际运行往往要考虑寿命损耗,保守一点更贴近真实调度习惯。
2.3 数据准备:负荷、分时电价与碳参数
这一块最容易翻车的是单位。功率用kW,时间尺度1小时,能量自然就是kWh;天然气体积用m³,价格用元/m³;碳排放用kg或t,碳价用元/t。我见过不少人在这一步把kWh和MWh混着用,结果费用相差三个数量级。
典型日的电负荷峰值约600 kW,夜间谷段约220 kW,热负荷峰值约450 kW。光伏预测出力中午最高达到150 kW,早九点到下午四点之间贡献明显。分时电价按峰谷平三段设置:峰段10-15时和18-21时电价1.2元/kWh,谷段0-7时和23-24时电价0.4元/kWh,其余时段0.7元/kWh。天然气价格取2.4元/m³。
碳排放参数方面,电网购电的排放因子取0.57 kgCO2/kWh,这是目前国内电网平均排放因子的常见参考值;天然气燃烧排放因子取2.16 kgCO2/m³。这两个参数直接决定碳交易成本在目标函数里的权重,需要根据项目所在地的官方口径做调整。
3. 阶梯碳交易建模:从阶梯电价思路到混合整数约束
3.1 免费配额与排放核算
碳交易机制的核心是先给企业一定量的免费排放额度,实际排放超过额度的部分需要购买配额,低于额度则可以把富余配额出售获利。我采用如下核算方式:
E_total = Σ [P_buy(t) × λ_e + V_gas(t) × λ_g] / 1000
单位换算成吨。其中λ_e是电网购电排放因子(kg/kWh),λ_g是天然气排放因子(kg/m³),除以1000是为了把kg转成t。
免费配额D的设定直接决定碳交易成本的方向。我这里的做法是先跑一遍确定性优化,得到系统基准总排放,取这个值的60%作为免费配额。实际项目里配额的核算规则比这复杂,有的按产品产量、有的按历史排放强度,但核心逻辑一致:配额松,碳成本压力小;配额紧,减碳约束就变成硬约束。
碳交易量就是实际排放与免费配额之差:
E_trade = E_total - D
当E_trade为正时需要购买配额,为负时说明有富余配额可以出售。传统碳交易模型用的是固定碳价,成本函数是一条直线;阶梯碳交易把碳价按交易量分段递增,排放越多、超过额度越多,边际碳价越高,这样对高排放的惩罚力度是逐步加大的。
3.2 阶梯碳价的分段线性化
阶梯碳价和阶梯电价是同一个思路。我给买入配额设置四个阶梯区间,价格逐级上涨:
| 交易量区间(t) | 碳价(元/t) |
|---|---|
| 0-0.5 | 80 |
| 0.5-1.5 | 120 |
| 1.5-3.0 | 160 |
| 3.0以上 | 200 |
同时允许富余配额以60元/t的价格出售,作为卖碳收入冲抵成本。
这一步的难点在于把分段函数改造成混合整数线性约束。核心变量有两组:一是每段的交易量q_i,二是每段的激活标志δ_i(0-1变量)。为了保证分段逻辑正确,需要满足:段i被使用的前提是前一段已经填满,也就是说δ_i是单调递减的:
δ_0 ≥ δ_1 ≥ δ_2 ≥ δ_3
加上两类耦合约束:
q_i ≤ seg_cap_i × δ_i
q_i ≥ seg_cap_i × δ_{i+1}(最后一段不需要这个约束)
第一条保证段i未被激活时交易量为0,第二条保证如果下一段被激活,当前段必须装满。这样分段函数就完整嵌入MILP框架。
3.3 0-1变量与大M法的实现细节
有读者可能想问:为什么不直接用Gurobi自带的分段线性函数接口addGenConstrPWL?可以用,但我还是建议先把分段约束手写一遍,原因有两个:一是手写约束能让你看清模型结构,排查问题时更快定位;二是Gurobi的PWL接口默认用插值近似,对带0-1变量的分段约束处理方式比较隐晦,不如显式约束可控。
大M法在这个模型里其实是隐含的。上述约束中的seg_cap本身就是各段交易量的上限,天然充当了M值角色,不需要额外设一个很大的M。这点很重要,很多人在这一步图省事写一个大M=1e6,结果求解器数值稳定性立刻恶化,出现各种诡异警告。
卖出决策和买入决策还需要互斥。我加了这样一条约束:
q_sell ≤ E_sell_max × (1 - δ_0)
即只要第一段买入被激活,就不允许卖出配额。反过来如果δ_0=0,则所有买入段的交易量都是0,E_trade被限制在负区间,只能通过卖出配额来平衡。两端方向在逻辑上是闭合的,不会出现既买又卖的套利空间。
4. Python实现主线:先跑确定性模型,再嵌IGDT鲁棒模型
4.1 环境准备与整体代码框架
实现这套模型需要Python 3.8以上环境,核心依赖是gurobipy。Gurobi对学术用户有免费license,个人学习申请也不麻烦,直接去官网申请即可。如果你的环境装不了Gurobi,备选方案是SCIP或HiGHS,但求解带大量0-1变量的MILP时性能差距会很明显,代码层面也要做适配。
pip install gurobipy numpy pandas matplotlib代码按四条主线组织:参数模块负责加载负荷、电价、设备参数;确定性模型模块构建并求解基准场景;IGDT模型模块在确定性模型基础上增加不确定性半径变量和成本约束;结果分析模块负责绘制调度曲线和α-σ关系曲线。整个主体大概两百多行,结构上分为mysystem_params、solve_deterministic、solve_igdt、run_analysis四个函数就够用了。
4.2 确定性模型的核心代码
先构建设备决策变量。所有变量都按24个时段定义,储能SOC额外多一个时段用于首尾衔接:
import gurobipy as gp from gurobipy import GRB def solve_deterministic(params): m = gp.Model("Det_MultiEnergy") T = range(24) P_chp = m.addVars(T, lb=0, ub=300, name="P_chp") H_gb = m.addVars(T, lb=0, ub=500, name="H_gb") H_eb = m.addVars(T, lb=0, ub=200, name="H_eb") P_buy = m.addVars(T, lb=0, ub=350, name="P_buy") P_ch_es = m.addVars(T, lb=0, ub=50, name="P_ch_es") P_dis = m.addVars(T, lb=0, ub=50, name="P_dis") SOC = m.addVars(range(25), lb=20, ub=180, name="SOC") # 碳交易相关变量 delta = m.addVars(4, vtype=GRB.BINARY, name="delta") q_buy = m.addVars(4, lb=0, name="q_buy") q_sell = m.addVar(lb=0, name="q_sell") E_trade = m.addVar(lb=-GRB.INFINITY, name="E_trade")平衡约束和储能约束是主体的核心:
for t in T: # 电力平衡 m.addConstr(P_buy[t] + params["PV"][t] + P_chp[t] + P_dis[t] == params["Load_E"][t] + P_ch_es[t] + H_eb[t] / 0.95) # 热力平衡 m.addConstr(1.2 * P_chp[t] + H_gb[t] + H_eb[t] == params["Load_H"][t]) # 储能递推 m.addConstr(SOC[t+1] == SOC[t] - P_dis[t]/0.95 + 0.95*P_ch_es[t]) # CHP爬坡 if t >= 1: m.addConstr(P_chp[t] - P_chp[t-1] <= 80) m.addConstr(P_chp[t-1] - P_chp[t] <= 80) m.addConstr(SOC[24] >= SOC[0])目标函数包含购电成本、购气成本、设备运维成本和碳交易成本。CHP燃料消耗量由电出力和热电比折算,燃气锅炉和电锅炉的燃料/耗电项直接在成本里体现:
gas_chp = quicksum(P_chp[t] * (1 + 1.2) / (0.7 * 9.7) for t in T) gas_gb = quicksum(H_gb[t] / (0.9 * 9.7) for t in T) cost_buy_ele = quicksum(P_buy[t] * params["Price_E"][t] for t in T) cost_gas = (gas_chp + gas_gb) * 2.4 cost_op = quicksum(0.02 * P_chp[t] + 0.015 * H_gb[t] + 0.02*H_eb[t] for t in T) cost_carbon = 80*q_buy[0] + 120*q_buy[1] + 160*q_buy[2] + 200*q_buy[3] - 60*q_sell m.setObjective(cost_buy_ele + cost_gas + cost_op + cost_carbon, GRB.MINIMIZE) m.optimize() return m注意gas_chp的推导:P_chp + H_chp = P_chp × (1 + r),再除以总能效和低热值,就得到一小时内的燃气消耗量(m³),因为P_chp单位是kW而时间步长是1小时,kWh和kW在数值上是等价的。
碳交易分区约束放在模型里一起提交。分段容量seg_cap对应[0.5, 1.0, 1.5, 2.0](吨),最后一段用2.0吨作上限,实际项目里可以根据历史排放量放大:
seg_cap = [0.5, 1.0, 1.5, 2.0] for i in range(4): m.addConstr(q_buy[i] <= seg_cap[i] * delta[i]) if i < 3: m.addConstr(q_buy[i] >= seg_cap[i] * delta[i+1]) if i >= 1: m.addConstr(delta[i-1] >= delta[i]) m.addConstr(E_trade == quicksum(q_buy[i] for i in range(4)) - q_sell) m.addConstr(q_sell <= 2.0 * (1 - delta[0]))到这里确定性模型就完整了。求解之前先看一下目标函数值,这就是后续IGDT模型里的基准成本C0。
4.3 IGDT部分:把alpha做成变量
IGDT鲁棒模型的核心变化有三处。第一处是把目标函数从“最小化成本”换成“最大化不确定性半径α”;第二处是把光伏实际出力从固定预测值替换成(1-α)倍预测值,这是对最坏情况的建模;第三处是新增一条成本约束,要求所有不确定性场景下的总成本不超过(1+σ)倍的基准成本。
具体代码:
def solve_igdt(params, sigma, C0): m = gp.Model("IGDT_Robust") alpha = m.addVar(lb=0, ub=1.0, name="alpha") # 其余变量同确定性模型 # 光伏出力替换为(1 - alpha) * PV_pred,在电力平衡中使用 for t in T: m.addConstr(P_buy[t] + (1 - alpha) * params["PV"][t] + P_chp[t] + P_dis[t] == params["Load_E"][t] + P_ch_es[t] + H_eb[t] / 0.95) # 成本约束,需要复算各成本项 cost_total = ... m.addConstr(cost_total <= (1 + sigma) * C0) m.setObjective(alpha, GRB.MAXIMIZE) m.optimize() return alpha.X这里的关键洞察是光伏实际出力以(1-α)倍预测值进入电力平衡,α越大,光伏出力越少,系统需要用CHP、购电或储能放电来补足缺口,成本随之上升。而成本约束设置的(1+σ)C0就是允许成本的“天花板”。目标函数最大化α,相当于在这个天花板内寻找系统能承受的最大预测偏差。
要强调的是,由于光伏出力只是作为线性系数出现在平衡约束中,没有与任何决策变量相乘,整个IGDT模型依然是纯线性MILP,不需要做任何非线性转换。这是IGDT方案相对两阶段鲁棒优化的一个巨大便利——后者需要将内层min问题做对偶变换,推导过程容易出错,而在Gurobi里调试错误约束又非常费劲。
4.4 结果输出与可视化
跑完所有σ取值后,把结果整理成DataFrame,输出α随σ变化的序列,以及典型调度时刻的机组出力。可视化用matplotlib画两个图:一个是α-σ关系曲线,另一个是σ=0.1场景下的电功率平衡堆叠图。
def run_analysis(params): det_model = solve_deterministic(params) C0 = det_model.ObjVal sigmas = [0.0, 0.05, 0.1, 0.15, 0.2, 0.25] results = [] for s in sigmas: alpha_s = solve_igdt(params, s, C0) results.append({"sigma": s, "alpha": alpha_s}) return pd.DataFrame(results)求解耗时方面,单场景24时段、含几百个约束和几十个0-1变量的MILP,Gurobi通常在1-3秒内收敛到1%的MIPGap以内。IGDT鲁棒模型比确定性模型稍慢一些,但整体还在可接受范围。
5. 仿真结果:风险规避系数如何改写调度决策
5.1 α-σ关系曲线:花多少钱买保险
我按σ从0到0.25的梯度求解,得到的α序列非常直观:
| σ(成本预算放宽比例) | 最大可承受光伏偏差α |
|---|---|
| 0.00 | 0.00 |
| 0.05 | 0.11 |
| 0.10 | 0.19 |
| 0.15 | 0.26 |
| 0.20 | 0.33 |
| 0.25 | 0.38 |
σ=0时α=0是必然的,因为成本预算不放松,系统只能按预测值运行,没有余量去兜底偏差。σ从0提高到0.1,α快速上升到0.19,说明只要愿意多承担10%的成本,系统就能容忍光伏出力向下偏离预测值19%。但α的边际增长在递减:σ从0.2提高到0.25,α只增加了0.05。这个“边际递减”现象背后的物理原因很明确:光伏在总电源中的占比有限,当偏差超过一定幅度后,系统得动用更高成本的备用来源来弥补,成本上升的速度远快于可容忍偏差的增长速度。
这条曲线的价值在于给决策者一个直观的风险定价工具——你知道为了把光伏偏差容忍度从20%提高到30%,需要额外承担多少成本预算;这笔钱花得值不值,决策者一眼就能看出来。
5.2 典型日调度结果对比
取σ=0(基准)和σ=0.15两个场景对比出力曲线。基准场景下,白天光伏出力充足时CHP明显压减出力,电储能中午充电、晚间峰段放电,购电集中在谷段。σ=0.15场景下,系统面对的光伏出力是(1-0.26)倍预测值,相当于一个“打了折”的光伏,于是CHP在白天时段的电出力比基准场景高出约40 kW,储能充电量减少,从电网购电在峰段的尖峰更高。
这个调度行为变化的方向是符合物理直觉的:系统对光伏越不信任,就越倾向用可控性强的本地机组和电网来支撑负荷,储能则从“日间套利”转向“保供应”的角色。有趣的是,热力侧的调度变化不大,因为CHP热电比固定,电出力上调会同步带来热出力上升,这部分多出来的热量如果热负荷吃不下,就得通过电锅炉或燃气锅炉的出力调整来重新平衡。我在初版模型里忽略了热力侧的这个连锁反应,导致CHP电出力一上调,热平衡总是无解,排查半天才发现是热力平衡约束和CHP热电比约束之间的配合出了问题。
5.3 阶梯碳交易对排放的影响
由于σ增大后CHP出力上升、购电结构变化,系统总排放会小幅走高。以σ=0.15场景为例,碳排放总量比基准场景增加了约5%,碳交易购买量相应增加,碳交易成本在总成本中的占比从基准的不足10%上升到约14%。这里阶梯碳价的效果就体现出来了:如果采用固定碳价80元/t,碳成本只与排放量线性挂钩,购碳成本增幅与排放增幅同比例;而阶梯碳价下,第二段、第三段的价格分别是120、160元/t,实际购碳成本增幅高于排放增幅,给调度模型施加了更强的减排压力。
这也解释了为什么σ增大到一定程度后α的边际增长放缓——除了备用成本上升,阶梯碳成本也在同步推高总成本,两道夹击让系统很难继续扩大不确定性容忍度。碳交易机制在这里不只是成本项,它实际上改变了IGDT模型的风险-成本边界形状。
6. 跑通模型之后:踩过的坑与调参心得
6.1 求解器与数值稳定性
最开始的版本我图省事,把分段碳交易里的M值写成1e6,结果Gurobi求解时频繁出现“numerical trouble”警告,最优解也不稳定,同样的参数跑两次结果会有微小差异。后来把M全部替换成各段交易量的实际上限(seg_cap),问题立刻消失。教训是:M值够用就行,不要写一个“很大”的数,数值病态是MILP求解里最隐蔽的坑。
另一个与求解器相关的设置是MIPGap。默认1e-4的Gap对24小时模型没问题,但如果把模型扩展到8760小时或加入更多0-1变量,求解时间会指数级上升。我习惯把MIPGap放宽到0.01,在成本和决策精度上损失很小,求解速度却可能快好几倍。
6.2 参数敏感性:免费配额和碳价梯度
免费配额D的取值对整个模型结果影响极大。我做了λ从0.4到0.8的敏感性测试,发现λ=0.4时碳交易成本占到总成本的18%,λ=0.8时系统甚至可以通过出售富余配额获利,调度策略也更倾向于多发电多排放。这提醒我们,碳交易机制不是单纯的环境约束,它本质上是一个与配额规则强相关的经济信号。在实际项目里,拿到需求第一件事应该和甲方确认配额核算口径,而不是拍脑袋定一个λ值跑完结果就交付。
碳价梯度同样值得调。我试过把阶梯价格差从40元/t放大到80元/t,系统对CHP的利用策略会有明显改变——价格差越大,模型越倾向于压低排放量较大的时段出力,转而用储能在谷段蓄能。如果你希望园区系统展现更强的低碳转型行为,适当拉大阶梯价差比单纯提高基础碳价更有效。
6.3 模型扩展的多源不确定性思路
把单一光伏不确定性扩展到光伏+风电等多源场景时,不能简单让两种电源都乘(1-α)。因为不同电源的预测精度差异很大,用一个统一的风险半径会掩盖这种差异。常见做法是对每个不确定源单独设置αi,再通过加权或最小化联合概率约束把它们耦合起来,比如令Σαi²≤α²(椭球信息间隙模型),但这就引入了二阶锥约束,求解难度上升一个台阶。实际操作中,如果项目对精度要求不高,我建议先按“最弱环节”原则把所有不确定源乘同一个α,得到的是一个偏保守但可求解的版本;如果后续需要更精细的分析,再逐步引入多半径模型。
此外,IGDT模型很方便扩展为带P2G和氢储能的版本,因为IGDT本身只改变PV出力项的表达式和新增一条成本约束,不影响其他设备模型。我在后续版本里加入了P2G设备,主要改动就是电力平衡里增加P2G的电耗项、天然气平衡里增加产气项,以及碳交易核算里增加氢气生产带来的负排放补偿。整套改造只花了半天时间,这从侧面验证了IGDT作为不确定性处理框架“模块化”的通用性。
最后再补充一个实操细节:储能SOC的初值设置。如果只约束SOC[0]等于一个固定值而不约束终值,模型会倾向于在最后一个时段把储能全部放空,造成过度套利。我现在的做法是加一条SOC[24] ≥ SOC[0]的约束,强制储能系统保持周期性的运行策略,这个约束对调度结果的影响比很多人想象中大得多。无论跑哪个版本,都建议把这个周期约束带上,你会在结果里看到储能策略瞬间变得合理起来。