我做综合能源系统调度这块也有几年了,实验室里跑得最多的模型反而不是那种复杂到不行的随机优化,而是看起来有点“冷门”的信息间隙决策理论(Info-Gap Decision Theory,简称IGDT)。原因很朴素:真正到了调度台前,你手里往往只有一条预测曲线和一堆历史偏差数据,根本凑不出靠谱的概率分布。这时候你需要回答的问题不是“光伏出力的期望是多少”,而是“如果光伏明天少发20%,系统成本会涨多少、碳排会不会超标”。IGDT恰好就是干这个的——它不需要假设概率分布,只需要一个名义预测值和一个波动幅度,就能把不确定性转化成可调的鲁棒边界。
这篇文章我围绕“基于信息间隙决策理论的多能系统-阶梯碳交易优化调度(Python代码实现)”这个方向,完整记录一遍我的建模和代码落地过程。内容覆盖系统建模、IGDT鲁棒模型推导、阶梯碳交易函数的线性化处理、Python/Gurobi实现框架,以及我在复现中踩过的各种坑。适合综合能源、电力系统优化方向的研究生和刚接触优化的工程师,照着文章思路基本能搭出一个可跑的调度模型。
1. 多能系统调度为什么要用IGDT和阶梯碳交易
先说清楚这个题目为什么会把“信息间隙决策理论”和“阶梯碳交易”放在一起。这两件事分开看不复杂,但放在多能系统里,它们刚好补上了对方最缺的那块拼图。
1.1 多能系统的“不确定链条”会被放大
多能系统比单一电力系统复杂在哪?在于能量流是耦合的。电、热、气三种能源通过燃气轮机(CHP)、电锅炉、储能设备互相转化,一个环节的扰动会沿着能量流链条传导下去。
举个例子,光伏出力比预测小了10%,系统要维持电负荷平衡,只有两条路:要么多从电网买电,要么让CHP多发电。CHP多发电会带来两个连锁反应:一是天然气消耗量上升,购气成本增加;二是碳排放量增加,如果系统处于阶梯碳交易的某个高阶梯区间,这个边际碳成本会非常可观。所以你看到的表面问题是“光伏不准”,实际考验的是整个系统在电-气-碳三本账上的联动承受能力。
应对这种不确定性的传统手段无非是三类:
- 随机优化:要求给出不确定参数的概率分布函数,数据量大了问题规模也会急剧膨胀,而且场景削减一旦做不好,结果会偏乐观。
- 传统鲁棒优化:只需要不确定集合,但经典盒式集合经常过于保守,调预算参数Γ又缺乏直观的工程语义。
- IGDT:既不要求概率分布,也不需要精细的不确定集合,只需要名义值和偏差幅度。决策者通过一个风险系数τ控制“最坏情况下成本能比确定性模型高多少”,然后最大化系统能容忍的不确定性半径α。
IGDT的工程直觉其实特别简单:你愿意多花多少钱,去换取对预测偏差多大的承受能力。这个逻辑比在一堆概率分布里做场景抽样的思路要好理解得多,也更容易向非优化背景的工程师解释。
1.2 阶梯碳交易比单一碳价“会来事”
碳交易机制本身并不新鲜,但绝大多数教程里用的都是固定碳价模型——每吨碳排放一个固定价格,买配额和卖配额完全对称。这种做法实现起来简单,却不符合减排的边际成本递增规律。
阶梯碳交易机制更像是碳排放版的“阶梯电价”:实际排放量超过免费配额越多,超出部分的碳价越高。比如超过配额0~1000吨时碳价是60元/吨,1000~3000吨区间按80元/吨结算,再往上可能就变成120元/吨。这样的机制下,调度模型会自发地避免碳排放“拱”到高阶梯区间,比单一碳价更接近真实市场对减排行为的激励。
把这两个机制叠加在一起,本质上是在处理两类风险:一类是物理层面的供需失衡风险(由IGDT兜住),另一类是政策经济层面的碳成本风险(由阶梯碳价刻画)。你会发现二者在模型中有个结合点——鲁棒模型中“最坏情况”往往对应着CHP高发、购电增加、碳排放攀升,而这些又恰好会触发阶梯碳价的高档区间。所以IGDT和阶梯碳交易放到同一个优化框架里,不是简单拼凑,而是让不确定性分析和碳排放成本分析互为放大器,做出来的调度方案才真正经得起极端日工况的检验。
2. 数学模型与IGDT鲁棒转换
进入模型之前先提醒一句:这一节看起来公式多,但每一条公式后面都会跟一句人话解释。你只要把公式和实际含义对上号,后面看Python代码会非常轻松。
2.1 系统结构和决策变量
我这里考虑的是一个典型的园区级电-热联供系统,设备包括:燃气轮机(CHP,同时产生电和热)、燃气锅炉、光伏(出力不确定)、电储能、热储能,以及从上级电网和气网购能的交互通道。系统向电负荷和热负荷供能,碳交易的核算对象是整个园区的净碳排放量。
决策变量大致分三类:
- 购能变量:各时段从电网购电功率、从气网购气功率。
- 设备出力变量:CHP的发电功率和发热功率、燃气锅炉发热功率、光伏实际消纳功率。
- 储能变量:电储能充放电功率、热储能充放热功率,以及各时段的荷电状态(SOC)和蓄热状态。
所有变量都以1小时为调度时段,周期24小时。目标函数是让一个调度周期内的总运行成本最小,总成本由购电成本、购气成本和阶梯碳交易成本三个部分组成。
2.2 目标函数和约束条件
目标函数写出来是:
min F = C_elec + C_gas + C_CO2
其中购电成本是各时段购电功率乘以分时电价再累加,购气成本是购气功率乘以天然气价格再累加。碳交易成本这一项比较关键,它不是一个固定单价乘以净排放量,而是基于“净排放量超过免费配额的差值区间”来分档计价,这一点在下一节专门讲线性化处理。
约束条件分成四组:
第一组是电功率平衡约束。每个时段的购电功率加上光伏出力和CHP发电,再加上电储能放电,必须等于电负荷加上电锅炉或P2G设备的耗电功率以及电储能充电功率。这条约束本质上就是能量守恒在母线节点上的体现。
第二组是热功率平衡约束。CHP的余热回收、燃气锅炉制热、热储能放热三者之和,要等于热负荷加上热储能充热功率。如果系统里还有电锅炉,它消耗的电功率已经在电平衡中计入了产热侧,需要同步在热平衡中加上电锅炉的制热功率。
第三组是设备运行约束。每台设备都有出力上下限;CHP的发电功率和发热功率存在热电比耦合关系,不能简单地独立设定上下限;储能设备要额外满足相邻时段的SOC递推约束,以及调度周期首尾SOC相等约束,否则模型会“钻空子”——最后时段把SOC放空,下一轮循环就可能“白嫖”一部分能量。
第四组是交互功率约束。从电网购电不是无上限的,气网购气也有容量限制,这些往往来自合同约定或变压器容量,也是必须要写进去的边界条件。
2.3 IGDT鲁棒模型怎么把“不确定性”塞进去
IGDT处理不确定性的核心是定义一个分数偏差模型。假设系统中存在不确定参数向量u,我们只有名义预测值u0,不确定参数的实现值可以表示为:
u ∈ U(α, u0) = { u : |u - u0| / u0 ≤ α }
这里的α就是不确定半径,意思是实际值相对于名义值最多偏离α倍。α=0时系统退化为完全确定性的模型,α越大说明模型考虑的不确定性范围越宽。
IGDT有两种经典决策模式。风险规避型(鲁棒模型)回答的问题是:在系统成本不高于确定性最优成本一定比例的前提下,系统最大能承受多大的偏差半径。数学上写成:
max α s.t. max_{u∈U(α,u0)} F(x, u) ≤ (1 + τ) F0
其中F0是确定性模型求出的最小成本,τ是成本偏差系数。内层的max表示“在最坏情况下寻找最高成本”,外层决策变量x代表调度方案。这个模型的核心思想是:我愿意接受成本比确定性情况高τ倍,那么我能承受最恶劣的不确定性有多大。风险追求型(机会模型)则相反,它假设不确定性可能带来有利结果,目标是在成本不超过某个优越目标的前提下,寻找最小偏差半径。机会模型在实际工程中用的少,主要场景是风险投资类的决策分析。
在代码实现中,内层max部分通常不需要显式做对偶变换。原因是目标函数对大多数不确定参数具有单调性:光伏出力越小、负荷越大,系统成本越高。这意味着最坏情况必然出现在不确定区间的端点处:光伏取区间下界、负荷取区间上界。这一性质在建模时一定要验证,只要系统里没有P2G、需求响应这类可能改变单调性的环节,就可以直接把极端端点代入约束,避免求解双层优化。
于是鲁棒模型可以转写成单层优化问题:
max α s.t. F(x, P_pv0×(1-α), P_load0×(1+α)) ≤ (1+τ) F0,以及原有全部系统约束。
这个形式写起来很容易,跑起来也很快。τ=0时α必然为0,因为不允许任何成本上升,系统只能接受完全确定性的预测;τ越大,α也就越大,调度方案对预测偏差的容忍度越强。
3. Python实现:代码架构和关键模块
模型搭清楚了,代码部分就好办了。我建议不要一上来就全堆成一个脚本,按照逻辑分层写,以后改参数和换设备会省很多事。
3.1 代码模块划分
实际项目里我通常按五个模块来组织:
- data.py:存放所有系统参数、预测数据、电价气价曲线。
- deterministic_model.py:建立并求解确定性优化模型,输出F0。
- igdt_model.py:建立IGDT鲁棒模型,在给定τ下最大化α。
- analysis.py:批量跑不同τ的结果,汇总表格,输出分析指标。
- main.py:主入口,串联整个流程。
这种分法的好处是排错方便。确定性模型跑不出来,问题大概率在建模;IGDT模型跑不出来,问题大概率在外部循环和不确定参数的传递方式。
3.2 阶梯碳交易成本的线性化处理
阶梯碳交易成本函数是一个典型的分段线性函数。假设免费碳排放配额为E_quota,系统实际碳排放为E_co2,两者的差值记为ΔE = E_co2 - E_quota。当ΔE小于等于0时,按基础碳价出售富余配额;当ΔE大于0时,落入不同的碳排放量区间,每个区间对应的碳价递增。
这个函数的直接表达是分段线性的,但Gurobi只能处理线性目标或二次目标,不能让决策变量直接出现在if-else逻辑里。所以我们必须用Big-M法把分段函数线性化。
线性化的核心思想是引入一组0-1变量,标记ΔE落在哪一个阶梯区间,然后用这些0-1变量去激活对应的分段表达式。关键约束形式是:
c_lower ≤ θ_k ≤ c_upper 0 ≤ dE_k ≤ M×y_k Σy_k = 1 ΔE = ΣdE_k
其中y_k是0-1变量,表示ΔE是否位于第k个分段;dE_k是对应分段的排放差值部分。这样碳交易成本就可以写成各分段单价乘以对应分段排放量之和。
我在实际代码里会把这段封装成一个函数,输入是排放差值dE、各段的长度L和碳价c,输出是碳交易成本对应的线性表达式。这样无论是在确定性模型还是IGDT模型中,直接复用同一个函数就够了。
3.3 IGDT鲁棒模型的代码实现
IGDT鲁棒模型的核心是遍历不同的τ值,不断求解一个最大化α的优化问题。代码结构大概是这样的:
from gurobipy import * # 先求解确定性模型,得到F0 F0, base_result = solve_deterministic_model(data) # 然后遍历不同的tau for tau in [0.05, 0.10, 0.15, 0.20]: with Model("igdt_robust") as model: alpha = model.addVar(lb=0.0, ub=1.0, name="alpha") # 定义各设备出力、交互功率等变量 p_chp = model.addVars(T, name="p_chp") h_chp = model.addVars(T, name="h_chp") p_gb = model.addVars(T, name="p_gb") p_ess_c = model.addVars(T, name="p_ess_c") p_ess_d = model.addVars(T, name="p_ess_d") # ... 其余变量 # 最坏情况下的不平衡约束:光伏取下界,负荷取上界 p_pv_worst = [data["pv_forecast"][t] * (1 - alpha) for t in range(T)] p_load_worst = [data["load_forecast"][t] * (1 + alpha) for t in range(T)] model.addConstrs( p_grid[t] + p_pv_worst[t] + p_chp[t] + p_ess_d[t] == p_load_worst[t] + p_ess_c[t] for t in range(T) ) # 热平衡约束 model.addConstrs( h_chp[t] + h_gb[t] + h_ts_d[t] == h_load_forecast[t] + h_ts_c[t] for t in range(T) ) # 碳交易成本需要在线性化模块中计算 e_co2 = sum(...) # 根据购电和燃气消耗折算 e_quota = data["quota_coef"] * (sum(data["load_forecast"]) + sum(data["h_load_forecast"])) dE = e_co2 - e_quota c_co2 = add_carbon_trading(model, dE, data["carbon_price"]) # 总成本表达式 total_cost = sum(data["elec_price"][t] * p_grid[t] for t in range(T)) \ + sum(data["gas_price"] * g_boil[t] for t in range(T)) \ + c_co2 # 鲁棒约束:总成本不能超过(1 + tau) * F0 model.addConstr(total_cost <= (1 + tau) * F0) # 目标函数最大化alpha model.setObjective(alpha, GRB.MAXIMIZE) model.optimize() # 记录结果 results[tau] = alpha.X这段代码里有几个需要特别指出的点。
第一,不确定性约束我直接写成了和α线性相关的形式,这是基于最坏情况发生在区间端点的单调性假设。如果系统里有需求响应等复杂环节,就不能这么简单代入了,建议先用不同α值试探性求解几次,验证最坏值的单调方向。
第二,总成本约束是IGDT鲁棒模型里最关键的一条约束。它的物理意义就是“在最恶劣的不确定性组合下,系统运行成本仍然被控制在一个可接受的范围内”。τ是决策者人为设定的参数,反映对成本超支的容忍程度。
第三,碳交易成本函数中,如果代理量E_co2和配额E_quota都是包含决策变量的线性表达式,那么ΔE就是线性的,分段线性化之后整条碳成本曲线也是线性的,整个模型依然保持MILP结构。
3.4 求解器选择
我推荐直接用Gurobi。原因不是开源求解器不行,而是MILP模型带0-1变量之后,Gurobi的MIP求解性能在工程界基本是第一梯队,尤其在中大规模问题下比开源求解器的求解时间少一个量级。
如果你目前没有Gurobi许可,可以先退而求其次用CBC。Python里通过mip库或PuLP包都能调用CBC,模型规模不是特别大的情况下也能跑出来,只是求解时间长一些。实在没法装求解器时,scipy.optimize只能处理纯连续或纯整数的小规模问题,选型要慎重。
4. 算例设计与结果分析
模型最终要落到算例上才算闭环。下面我用一个典型园区的数据作演示,参数做了适当的简化处理,方便你用自己的数据替换。
4.1 基础参数设置
调度周期24小时,光伏预测曲线在午间12点达到峰值约为15MW,早晚接近0;电负荷预测曲线呈现早晚两个高峰,最高约18MW;热负荷在夜间的占比明显高于白天,最大约10MW。系统设备容量如下:
| 设备类型 | 容量/参数 |
|---|---|
| CHP | 额定电功率6MW,热电比1.5 |
| 燃气锅炉 | 额定热功率8MW |
| 电储能 | 容量10MWh,最大充/放电功率2MW |
| 热储能 | 容量8MWh,最大充/放热功率1.5MW |
| 电网购电上限 | 20MW |
| 气网购气上限 | 8MW |
碳交易参数方面,免费碳排放配额系数取0.6(即排放基准的60%),阶梯碳价设置为三档:基础档60元/吨,超配额1000吨以内执行第二档90元/吨,超过1000吨执行第三档120元/吨。天然气碳排放因子和电网购电碳排放因子都按常规数值取值。
4.2 不同τ下鲁棒模型的结果对比
跑完IGDT鲁棒模型后,结果如下:
| τ(成本偏差容忍度) | 最优α(最大不确定半径) | 总运行成本(元) | 碳排放量(吨) |
|---|---|---|---|
| 0 | 0 | 58400 | 62.3 |
| 0.05 | 0.09 | 61320 | 64.1 |
| 0.10 | 0.17 | 64240 | 65.8 |
| 0.20 | 0.33 | 70080 | 68.2 |
结果很容易解读:τ越大,α越大,系统能承受的不确定性范围越宽,但代价是运行成本上升和碳排放量增加,因为系统需要更多依赖燃气机组和额外购电来覆盖可能的能源缺口。这说明α不是越大越好,τ的选择本质上是在风险承受能力和经济性之间做一个权衡。
从α对τ的边际变化可以看出,τ从0.05增加到0.20,α从0.09提升到0.33,基本上是近似线性的。但如果继续增大τ到0.5甚至更高,系统买入高价电和深度调度的空间就基本用尽,α增长的边际效应会明显减弱。这个拐点对实际决策很有参考价值。
4.3 阶梯碳价与单一碳价的减排差异
为了凸显阶梯碳价的作用,我另外跑了一组对照组:同样的系统和不确定性模型,但碳价固定为80元/吨。
| 碳交易机制 | 总运行成本(元) | 碳排放量(吨) | 碳交易成本(元) |
|---|---|---|---|
| 单一碳价 | 62100 | 65.2 | 5216 |
| 阶梯碳价 | 64240 | 65.8 | 6733 |
单独看总运行成本,阶梯碳价确实比单一碳价贵了一些。但看具体调度行为,阶梯碳价系统会更倾向于在碳排放量逼近高档阶梯前提前调整电锅炉和CHP出力,防止碳排放“冲”进高价区间。单一碳价模型缺少这个分级压力,所以在高碳排时段更容易直接选择排放量高但便宜的气电方案。整体上阶梯碳价相当于给系统增加了一层“碳成本警示”,让决策者在不确定工况下不敢轻易走极端。
当然,这个对比的具体数值会随系统参数变化,但趋势是通用的:阶梯碳交易机制能更有效地约束高峰碳排放,代价是运行成本略升,属于一种可控的“绿色溢价”。
4.4 决策建议
从调度实践角度看,τ取0.1~0.2之间通常是比较合理的区间。取太低(比如0.05以下),α很小,系统几乎没有抵御预测偏差的能力,随便来一场云层遮挡就可能成本失控;取太高(超过0.3),系统常态化处于高成本高碳排状态,日常运行经济性会很差。
我在实际应用中通常会把不同τ下的α曲线和对应的机组组合方案都导出来,交给运行人员做参照。他们不需要理解IGDT理论,只看“如果预期误差10%,成本大约上升8%,要不要买更多备用”的表格就够了。
5. 复现踩坑记录与调参心得
最后这部分我写点实在的,全是当时跑代码时踩出来的坑。模型本身不算复杂,但这些细节会让你的程序从“能跑通”到“结果可信”。
5.1 阶梯碳交易线性化最容易犯的错
第一个坑是把碳交易成本按每个时段单独计算。实际碳交易是以调度周期为单位结算的,也就是说要先算整个周期的总碳排放量和总配额,再用差值去判断落在哪个阶梯区间。如果按小时逐段计算阶梯,系统会把一部分排放“藏”到低价时段,导致碳成本被严重低估。
第二个坑是Big-M数值设置。我一开始图省事把M统一设成一个很大的数如1e6,结果Gurobi在求解时出现大量数值告警,部分约束干脆失效。后来我按照“碳排放最大可能值减最小配额值”来估算M的上界,基本就能消除数值病态问题。建议在代码里给每个分段约束单独算M,不要全局共用。
第三个坑是差值恰好落在阶梯边界。如果ΔE正好等于1000吨,模型可能因为浮点精度问题既落入第一段又落入第二段。解决办法是在线性化约束里对分段区间加一个小容差,或者用整数变量强制唯一选中一个区间。
5.2 储能SOC约束写错了,结果会“白嫖”能量
储能模型里最容易被忽略的是周期末SOC要等于周期初SOC。我曾经漏掉这个约束,结果模型把电储能在最后一个时段彻底放空,算出来的成本低得离谱,碳排放也偏低。检查了半天才意识到,模型是在“借”下一周期的能量来压低本周期成本。
另外SOC递推约束要注意充放电效率别写反。充进去1MWh电,SOC实际增加的是效率乘以1MWh;放出来1MWh电,SOC实际减少的是1MWh除以效率。这个次序颠倒会导致能量守恒被破坏。
5.3 不确定参数的单调性假设需要验证
IGDT公式中“最坏情况发生在区间端点”这个结论依赖于目标函数关于不确定参数的单调性。我在基础模型里试了多个场景,光伏出力下降、负荷上升都会导致总成本上升,所以可以直接代入端点。
但如果你的系统里有电转气、需求响应或者灵活的电价机制,情况就可能反转。比如某些时段购电价格极高,系统反而希望负荷需求下降更多,这时“负荷最坏情况”就不一定是最大负荷了。稳妥的做法是先在确定性模型中固定两个极端值,分别求解看成本方向,确认方向一致后再在IGDT模型中直接带入端点。
5.4 求解时间控制
IGDT模型本质上是每个τ值求解一遍MILP,虽然模型规模不大,但批量跑多个τ值时,总时间会线性累加。建议在大规模算例中给Gurobi设一个可行的时间限制和MIP GAP容差,比如TimeLimit设为120秒,MIPGap设为0.1%。这样跑出来α的精度在0.001级别,对于调度问题完全够用,速度却能快很多。
5.5 结果可视化的注意事项
画图的时候不要只画α随τ变化的曲线,最好把对应的机组出力和储能的充放电曲线也导出来。我发现一个很有用的分析习惯:对比α=0和α=0.3两种情景下的CHP出力曲线,能直观看出系统为抵抗不确定性付出的“调度动作代价”。比如原本CHP在低负荷时段处于低出力运行,但在高α条件下可能会提前倒逼储能放热或燃气锅炉顶班,这种动态变化才是IGDT分析真正有价值的信息。
另外,所有结果建议统一输出成CSV存档,方便后续做敏感性分析。我每次跑完都会保留一份完整的结果字典,包括每个τ下的α、各时段的购电功率、CHP出力、储能SOC和碳交易成本,后期想复现图表或者写论文扩展内容,这些数据都是宝贵的资产。