拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

主从博弈下区域综合能源系统低碳调度:建模与Matlab实现

主从博弈下区域综合能源系统低碳调度:建模与Matlab实现

提到区域综合能源系统调度,大部分人第一反应还是传统经济调度的路子:一个目标函数、一堆约束条件、一次求解完事。但等你真正拿到一套基于多主体主从博弈的区域综合能源系统低碳经济优化调度代码,打开主程序看到"上层模型""下层模型"两个模块,还有Stackelberg博弈、KKT条件、碳交易配额这些词反复出现,多半会愣一下——为什么要拆成上下两层?直接把整个园区当一个整体优化不就完了吗?这个问题如果不先想透,后面改参数、调代码、分析结果都会发懵。

这篇文章我就结合我自己复现和调试这类分层主从博弈模型的经历,从模型为什么要分层讲起,一直讲到Matlab代码实现、算例设计、常见坑位,把这条链路完整走一遍。适合正在做综合能源系统调度、需要复现论文算法、或者准备用小论文毕设的读者参考。

1. 从"集中式最优"到"主从博弈":为什么单一决策模型在园区里走不通

1.1 集中调度的隐含假设与实际冲突

传统电力系统经济调度之所以能用一个集中模型描述,是因为在很长一段时间里,电网公司既是电网的运营者,也是发电资源的调度者,自上而下发指令,电厂和用户都按指令执行。这个模式成立的前提是:系统里只有一个决策主体,或者所有主体都愿意放弃自己的利益诉求,服从统一调度。

但区域综合能源系统的情况完全不同。园区里至少有两类角色:一类是综合能源服务商或园区运营商,手里握着燃气轮机、储能、余热锅炉这些物理设备,负责电、热、冷等多种能源的供应;另一类是负荷聚合商或者直接就是终端用户,他们关心的是自己用能方不方便、舒不舒服、花多少钱。这两类角色的利益是错位的:运营商想把能源价格定得高一点、把设备利用率拉满,从而多赚钱;用户希望能源价格低一点,用能计划能贴合自己的生活习惯。你让用户无条件削峰填谷,他凭什么配合?如果运营商只是下发一个"你必须在低谷时段多用电"的指令,用户完全可以不接招。

所以,当我们讨论区域综合能源系统的"优化调度"时,实际上面对的是一个多决策主体问题。每个主体都有自己的目标函数,也有自己的约束条件,谁也没有办法完全命令谁。这个时候,"一个中心、两个基本点"式的集中优化就不适用于实际场景了,或者说,它只是理论上的理想情况。

1.2 主从博弈如何把"决策权"分给不同主体

主从博弈(Stackelberg博弈)恰好就是为这种"上下级决策"场景设计的。它的核心思想是:有一个领导者先行动,公布自己的策略,比如运营商公布分时售电价、售热价;跟随者观察领导者的策略后,再做出自己的最优反应,比如负荷聚合商根据电价决定各时段买多少电、买多少热。关键是,领导者做决策时不能无视跟随者的反应,他必须"预测"到:如果我定这个价格,用户会怎么响应?这个响应反过来又会怎样影响我的收益?

这个"先动优势"和"理性预测"的结构,天然就是分层的。上层是领导者的决策问题,下层是跟随者的响应问题,上下层通过价格信号和购能计划这两个变量耦合在一起。把这两个问题写成一个整体来看,它就是一个双层优化模型,也就是我们常说的"分层模型"。

为什么分层模型比纯集中式模型更贴近实际?原因在于它把不同主体的利益边界画清楚了。上层运营商在最大化自己收益的时候,要考虑下层用户的反应;下层用户在最小化用能成本的时候,只能把价格当作给定参数。这个结构逼着上层"收敛着点定价",否则过度抬价会导致用户减少购能,最终反而损害自己的收入。这才是真实市场里价格的自我约束机制。

理解了这一点,后面看Matlab代码里的"上层模型"和"下层模型"两个模块,就不会再觉得它们是随意拆分的了。

2. 分层模型架构拆解:上层IESO与下层负荷聚合商的决策边界

2.1 上层:IESO的价格制定与设备出力联合决策

上层模型对应的是区域综合能源系统运营商,我这里统一叫IESO(Integrated Energy System Operator)。IESO的目标函数通常可以写成这样一个结构:最大化自身净收益,包括售电和售热收入,减去从上级电网购电的成本、购买天然气的成本、设备运行维护成本,再减去(或加上)碳交易产生的成本(或收益)。

IESO的决策变量有两类。第一类是能源价格,包括各时段的售电价格、售热价格,有些模型里还会售冷;第二类是设备出力,包括燃气轮机的电出力、热出力、储能的充放电功率、与外网购电交互功率等。为什么要把价格和出力放在一起优化?因为两者是联动的:电价高了,用户买得少,燃气轮机可能就得少发;电价低了,用户买得多,燃气轮机就得顶上。价格信号是引导下层行为的杠杆,而出力计划是兑现收益的执行手段,它们不能分开决策。

上层约束条件在代码里也很典型:电功率平衡约束(佛系理解就是"发出的电加上买来的电等于卖出去的电加上损耗")、热功率平衡约束、燃气轮机爬坡约束、出力上下限约束、储能SOC约束,还有价格上下限约束。价格上下限这个约束容易被忽略,但它很重要——实际场景里价格不可能无限高,监管和市场接受度都会给价格设天花板。在模型里如果不设价格上限,优化器很可能给出一个离谱的高价,然后靠着下层用户不得不买的刚性负荷来薅羊毛,这在数学上可行,但物理和市场上都说不通。

2.2 下层:负荷聚合商在价格下的用能响应

下层模型对应负荷聚合商或者终端用户群体。下层的主体不拥有供能设备,他们的决策变量主要是各时段从IESO购买的电量、热量,以及自己用电行为的调整量,比如可转移负荷的平移量、弹性负荷的削减量。

下层目标函数相对简单,通常是最小化总用能成本,也就是各时段的购电量和对应电价乘积求和,加上购热量和对应热价乘积求和。如果模型里还考虑用能舒适度,会在目标函数里加入一个用能满意度惩罚项,或者把用能需求设计成有上下界的区间,而不是固定值。这个细节很有用:如果负荷是刚性的,那么无论上层怎么定价,用户都必须买那么多能,博弈的意义就大打折扣。把负荷设计成有一定弹性的,价格才能真正发挥作用,用户也才能在峰谷价差下调整自己的用能曲线。

下层的约束条件主要就是:各时段购电量、购热量不超过上限;一天总用能量满足某个最低需求;可转移负荷的转移总量守恒(转出去的负荷等于转进来的负荷);以及负荷调整前后的用能体验不能跌破某个阈值。这些约束刻画的是一个"有理性但有限制"的用户:他想省钱,但也不能为了省钱让工厂停产、让屋里冷到没法待。

2.3 上下层之间的耦合变量与反馈闭环

双层模型和普通单层模型最核心的区别,在于上下层之间存在耦合变量,而且这个耦合是闭环的。上层把能源价格作为决策变量传给下层;下层把这些价格当作给定参数,优化出自己的购能计划;这个购能计划又反过来作为上层售能收入的依据,进入上层的目标函数。上层在下一次迭代或者求解时,要基于下层的反应重新评估自己的价格策略是否最优。

在Matlab代码里,这个耦合关系通常表现为:上层模型中的售能收入项 = 各时段价格 × 下层返回的购能量;下层模型中的购能成本项 = 各时段价格 × 购能量。变量是同一批,只是从不同角度看名字不同。用博弈论的语言说,最后要达到的是一个均衡点:在这个点上,上层给定自己当前的价格决策,下层已经做到了最优;而下层当前的最优购能计划,又恰好支撑了上层价格决策的最优性。谁都没有单方面改变策略的动机了,这个解就是Stackelberg均衡。

这个均衡概念在后面写代码时非常关键,因为不管你用哪种求解方法,最后判别的都是这个均衡条件是否成立。

3. 碳交易机制如何把"低碳"变成一张经济账

3.1 基准线配额与碳排放核算

讲低碳经济优化调度,绕不开碳交易机制。我国碳市场目前主流的是基准线法配额分配:主管部门根据行业的排放基准和历史活动水平,给排放主体发放一定量的免费碳排放配额。如果你的实际碳排放量低于配额,富余的部分可以拿到市场上出售,变成收益;如果实际排放量超出配额,就得到市场上购买缺口,产生额外成本。

具体到区域综合能源系统的算例里,碳排放核算一般包括两块:一是从上级电网购电对应的间接碳排放,需要用购电量和电网平均碳排放因子折算;二是天然气在燃气轮机里燃烧产生的直接碳排放,用购气量和天然气碳排放因子折算。光伏、储能这种零碳设备不产生碳排放。也有的模型更精细,会在碳排放约束里加入碳捕集与封存(CCS)设备,但基础版本通常不涉及,先把前两块算清楚就够用了。

这里有个单位上的坑,后面实操部分我还会展开说:天然气排放因子的单位往往是kgCO2/m³,电网排放因子的单位是kgCO2/kWh,而碳交易的价格单位是元/t,做目标函数的时候如果不把千克换算成吨,结果会差三个数量级,整个模型的最优解都会跑偏。

3.2 碳成本进入目标函数后的调度行为变化

碳交易机制的妙处在于,它没有直接给调度问题增加一个复杂的低碳约束,而是把"碳排放量"换算成一项成本,塞进了原本只有经济账的目标函数里。碳交易成本公式可以写成:碳交易成本 = 碳价 ×(实际排放量 - 免费配额)。当实际排放量大于配额时,这一项是正的成本;当实际排放量小于配额时,这一项变成负的,相当于额外收入。

这样一来,原本"经济性"和"低碳性"这两个看似矛盾的目标,在碳交易的框架下被统一成一个经济目标了。调度模型不再需要做复杂的多目标加权或帕累托前沿搜索,只需要把碳交易成本写进上层目标函数,优化器就会自动权衡:多买一度煤电,成本上升了,碳排也上升了,可能要多买碳配额;改用气电多发一度电,购电成本可能下降,但购气成本上升、碳排放也增加,到底划不划算,完全由碳价这个杠杆来调节。

这就是低碳经济优化调度的本质:不是强制要求系统必须减排多少,而是通过碳价让减排行为变得"有利可图"或者"得不偿失",让优化器自己找到那个经济最优和碳排放之间的平衡点。碳价低的时候,系统会倾向于少考虑碳排放,怎么省钱怎么来;碳价高的时候,减排本身就成了省钱的手段,系统会自动调整设备出力和价格策略,把碳排放压下来。

3.3 碳价敏感性分析的设计思路

既然碳价是模型里最关键的杠杆参数,那么算例里做碳价敏感性分析就是水到渠成的事。典型做法是固定其他参数不变,把碳价从0元/t开始,按20元/t、50元/t、100元/t、150元/t这样梯度增加,观察几个输出指标的变化:系统总碳排放量、IESO总收益、平均售电价、用户总购能成本、燃气轮机出力占比等。

做这种敏感性分析的意义在于验证模型行为的合理性。一个可信的模型应该呈现出这样的规律:碳价升高,碳排放量单调下降,但下降速度会越来越慢,因为便宜且边际减排空间大的措施先用完了,再减排就得动大手术,成本很高;同时系统总成本和用户购能成本通常也会上升,因为减排是要付出代价的。如果代码跑出来的结果是碳价越高、碳排放越高,或者碳价变化对碳排放毫无影响,那基本可以断定模型在哪一层写错了——最常见的是碳交易成本没有真正进入上层目标函数,或者是把碳排放当成了硬约束,导致碳价只是个摆设。

4. 双层模型的求解路线:KKT单层化与启发式嵌套怎么选

4.1 KKT条件替换下层:原理与适用前提

双层模型写出来了,接下来是怎么求解。最经典的路线是把下层优化问题用它的KKT条件替换掉,从而把双层问题转成单层问题。

这个思路的理论依据是:当下层问题是凸优化问题(线性规划或二次凸规划),并且满足约束规范条件(比如Slater条件)时,KKT条件是下层问题取得最优解的充分必要条件。也就是说,与其单独去解下层问题,不如把下层问题的KKT方程组直接作为约束,塞进上层问题里。这样,原来"先算下层再算上层"的嵌套结构,就变成一个"上层约束多了很多组方程"的单层优化问题。

转化后的单层问题在学术上有个名字叫MPEC(Mathematical Program with Equilibrium Constraints,带均衡约束的数学规划)。这个转化最大的好处是:一次求解就能得到整个双层问题的均衡解,不需要反复迭代,也不需要人为设置收敛判据。但代价是模型里的约束数量大幅膨胀,而且KKT条件里的互补松弛约束是非线性的,处理起来比较麻烦。

4.2 互补松弛条件的大M线性化

KKT条件里最让人头疼的是互补松弛条件,它长这个样子:拉格朗日乘子 μ 和约束函数的乘积必须等于0,μ ≥ 0,约束函数的值 ≥ 0,但两者不能同时大于0。这是一个非线性的非凸约束,直接把整个问题变成了难以求解的非线性规划。

工程上最常用的处理办法是大M法:引入一个二进制变量,把互补松弛条件拆成两条线性不等式。思路是:当约束起作用时(约束函数=0),令二进制变量为1,强制拉格朗日乘子为0附近的浮动范围;当约束不起作用时(约束函数>0),令二进制变量为0,强制乘子固定为0。用数学语言说,就是 μ ≤ M·z,g(x) ≤ M·(1-z),z ∈ {0,1}。这样一来,原来的非线性互补条件就被近似成一对线性约束,问题变成了混合整数线性规划MILP,可以直接交给CPLEX、Gurobi这类商业求解器处理。

大M的取值是整个线性化里最考究的地方。M太小,会把原本应该为正的乘子或者约束函数错误地压到0附近,导致求解出来的"最优解"根本不是原问题的解;M太大,又会让MILP的线性松弛变得很松,分支定界过程要走很多节点,求解速度急剧下降。我的经验是,M应该根据每个约束的实际物理上下界来取,比如价格上限约束对应的M取价格上限的2到5倍,而不是全篇统一拍一个大数。这个细节后面专门讲,因为它太容易被坑了。

4.3 启发式嵌套与交替迭代:实现简单但收敛要看运气

另一条求解路线是启发式嵌套,典型做法是:上层用一个智能算法比如粒子群、遗传算法来搜价格策略,每更新一次价格,就把价格传给下层,用YALMIP+CPLEX快速求解下层线性规划,拿到购能计划后返回上层,计算上层目标函数,作为智能算法的适应度评价,如此反复迭代到收敛。

这条路线实现起来比KKT单层化直观很多,不用处理互补松弛,也不用引入二进制变量,代码结构清晰。但代价也很明显:智能算法本身收敛慢,每次适应度评价都要完整解一次下层问题,如果粒子数50、迭代次数100,就意味着要解5000次下层线性规划,算力开销非常大。而且智能算法是启发式,不保证找到全局最优解,每次跑结果可能还有随机波动。这就需要把随机种子固定、把收敛判据设置合理,还得花时间调粒子个数、惯性权重这些参数。

还有一种介于两者之间的交替迭代法:不引入智能算法,而是上层和下层各自作为独立优化问题交替求解。先给一组初始价格,解下层得到购能计划;把购能计划代入上层,解上层得到新价格;再把新价格代入下层,循环往复,直到两次迭代之间的价格和负荷变化量小于阈值。这个方法在理论上和Stackelberg均衡并不严格等价,实际是否能收敛到均衡取决于问题的凹凸性。我试过的经验是:对于线性双层问题,交替迭代通常能收敛,但可能出现振荡,需要加上阻尼因子调整步长。

4.4 两条路线的对比与选型建议

我个人的选择逻辑很简单:如果算例规模不大,燃气轮机、储能、负荷聚合商数量都有限,优先用KKT单层化,虽然建模时间长一点,但求解稳定、结果可复现、不需要调智能算法的参数;如果后续想扩展多领导者、多均衡分析这类方向,再考虑启发式嵌套,因为KKT单层化在多领导者博弈里会变得非常复杂。

用一张表总结一下两条路线各自的特点:

对比项KKT单层化启发式嵌套/交替迭代
建模难度高,需处理KKT和互补松弛线性化低,模型直观易写
求解速度快,一次求解MILP慢,需多次求解下层LP
全局最优性对线性/二次凸问题能保证不保证,受算法参数影响
可复现性好,同参数结果完全一致较差,启发式有随机性
后续扩展多领导者、多均衡时复杂更灵活,易改博弈结构
适合场景中小规模、论文复现、结果对比大规模、非线性复杂设备、方法对比

5. Matlab代码实现的完整链路:从建模到均衡判定

5.1 环境准备:Matlab + YALMIP + 求解器

代码层面,我推荐的环境组合是Matlab + YALMIP + CPLEX或Gurobi。YALMIP是Matlab里非常成熟的建模工具箱,把变量定义、约束写入、目标函数表达、求解器调用都封装得很舒服。版本方面,Matlab R2020b之后的版本基本都够用,不需要追新版本,稳定最重要。

求解器上,如果你有学术版或者校网授权的CPLEX、Gurobi,直接用这两个;如果没有,可以考虑用YALMIP自带的求解器或者开源的SCS、ECOS,但处理MILP的效率会差不少。做KKT单层化之后的问题本质是MILP,强烈建议用商业求解器,否则计算时间可能让人崩溃。安装完之后,在Matlab命令行输入yalmiptest,看到一堆测试通过就没问题了。

5.2 代码结构与数据初始化

这类项目的代码我建议按模块切分,别把几百行全塞在一个脚本里。推荐这样一个结构:

main.m % 主程序,控制求解流程 data_define.m % 所有参数初始化(负荷曲线、设备参数、碳价等) build_upper.m % 上层模型:变量、目标、约束 build_lower.m % 下层模型:变量、目标、约束 solve_kkt.m % KKT单层化求解(可选路线) solve_iterative.m % 交替迭代求解(可选路线) plot_result.m % 结果可视化

数据初始化是很多人忽略但最容易出错的地方。典型参数包括:24时段的分时购电价格(峰平谷)、天然气价格、燃气轮机的电效率和热电比、储能容量和充放电效率、光伏出力曲线、各类负荷曲线、碳排放因子、免费配额、碳价。这些参数全部放在data_define.m里用结构体保存,比如params.price_buy = [...];params.emission_factor_grid = 0.997;这样,求解时不会出现变量作用域混乱的问题。

5.3 上层/下层模型的YALMIP建模示例

用YALMIP建模的关键是分清楚决策变量的类型。上层变量包括能源价格和出力,下层变量包括购能计划。写代码的时候,上下层的变量名一定要区分开,比如上层价格变量叫sell_price,下层购能变量叫load_buy,不要为了省事全都叫x,不然后面耦合的时候根本分不清谁是谁。

上层模型的YALMIP示例大致长这样:

% 时间维度 T = 24; % 上层决策变量:售电价、售热价、燃气轮机出力、储能充放电 sell_p = sdpvar(1, T); % 售电价 sell_h = sdpvar(1, T); % 售热价 p_gt = sdpvar(1, T); % 燃气轮机发电出力 h_gt = sdpvar(1, T); % 燃气轮机热出力 p_ch = sdpvar(1, T); % 储能充电功率 p_dis = sdpvar(1, T); % 储能放电功率 % 目标函数(示意):售能收入 - 购电成本 - 购气成本 - 碳交易成本 obj = -sum(sell_p .* load_buy_p + sell_h .* load_buy_h) ... + sum(price_buy_grid .* p_grid_buy) ... + sum(price_gas .* gas_consumption) ... + carbon_price * (total_emission - free_quota); % 注意这里load_buy_p、load_buy_h是下层返回的变量,若用KKT单层化则同时作为变量

下层模型的YALMIP示例:

% 下层决策变量:各时段购电量、购热量 load_buy_p = sdpvar(1, T); load_buy_h = sdpvar(1, T); % 目标函数(示意):购能成本最小 obj_lower = sum(sell_p .* load_buy_p + sell_h .* load_buy_h); % 约束 Constraints_lower = []; Constraints_lower = [Constraints_lower, load_buy_p >= load_p_min]; Constraints_lower = [Constraints_lower, load_buy_p <= load_p_max]; Constraints_lower = [Constraints_lower, sum(load_buy_p) >= total_p_demand]; % 热负荷、可转移负荷等按需添加

看到这里你可能会意识到,上下层两个问题的目标函数里都有sell_p .* load_buy_p这一项,只是符号相反。这个乘性耦合项正是双层模型的精髓所在——价格和购能互相影响,谁都不能单独决定最终结果。

5.4 KKT单层化与交替迭代的代码骨架

如果走KKT单层化路线,代码的核心是把下层问题的KKT条件手工写出来,或者利用YALMIP的kkt命令自动生成。YALMIP有一个很方便的功能:[kkt_system, details] = kkt(Constraints_lower, obj_lower);可以自动把下层问题的KKT条件导出来。不过我建议你至少手工推导一遍小规模例子,理解结构之后再用自动化命令,否则KKT条件哪里写错了都找不到。

手工构建KKT单层化的骨架大致是:

% 下层问题的拉格朗日乘子 mu_min = sdpvar(1, T); mu_max = sdpvar(1, T); % KKT条件:平稳性、原始可行性、对偶可行性、互补松弛 % 互补松弛用大M法线性化 M = 100; % 根据约束物理范围调整 z_min = binvar(1, T); z_max = binvar(1, T); % 互补松弛线性化(示意) Constraints_kkt = [Constraints_kkt, ... mu_min <= M * z_min, ... (load_buy_p - load_p_min) <= M * (1 - z_min)];

如果走交替迭代路线,主循环的骨架是:

% 初始价格 sell_p_init = 0.6 * ones(1, T); sell_h_init = 0.4 * ones(1, T); price_tol = 1e-4; alpha = 0.5; % 阻尼系数,抑制振荡 for iter = 1:max_iter % 1. 固定当前价格,求解下层,得到购能计划 [load_buy_p_now, load_buy_h_now] = solve_lower(sell_p_now, sell_h_now); % 2. 固定购能计划,求解上层,得到新价格 [sell_p_new, sell_h_new] = solve_upper(load_buy_p_now, load_buy_h_now); % 3. 价格更新:加阻尼,避免振荡 sell_p_now = alpha * sell_p_new + (1 - alpha) * sell_p_now; sell_h_now = alpha * sell_h_new + (1 - alpha) * sell_h_now; % 4. 收敛判定 if max(abs(sell_p_now - sell_p_old)) < price_tol break; end end

阻尼系数alpha是交替迭代法能不能稳定收敛的命门。我踩过好几次迭代发散或者两个价格之间来回跳的坑,后来在更新公式里加了0.5左右的阻尼因子,问题基本解决。记住:别一上来就用alpha = 1,那是纯替换,线性问题还好,稍微带点非线性就不稳了。

5.5 均衡判定与结果输出

不管用哪种方法,算完之后都要做一个均衡性的验证。具体做法是:把求解得到的最优价格代回下层模型,重新求解一次下层问题,看得到的购能计划是否和均衡解一致;再把购能计划代回上层,求解一次上层问题,看价格是否还是原来那一组。如果两边都对得上,说明这确实是Stackelberg均衡点。这一步在写论文时很有说服力,也特别适合用来排查代码错误——如果均衡验证对不上,要么是KKT条件写错,要么是交替迭代没收敛。

结果输出部分,除了画出各时段的电负荷、热负荷、价格曲线、燃气轮机出力、储能SOC,我还建议输出几个汇总指标:IESO总收益、用户总成本、系统总碳排放、碳交易成本、燃气轮机在总供能中的占比。这些指标是后面做碳价敏感性分析和场景对比的基础。

6. 算例验证:碳价如何改变均衡点与用能行为

6.1 算例设置:一个典型园区的基本参数

为了让模型验证有说服力,我设计了一个简化但典型的园区算例:一个区域综合能源系统包含一台燃气轮机、一组储能电池、若干光伏板,可以从上级电网购电,可以购买天然气。系统同时供应电负荷和热负荷,负荷有一定弹性,用户可以在一定范围内调整自己各时段的购能计划。

基础参数如下:电网购电价格为典型峰平谷分时电价,峰时0.83元/kWh、平时0.49元/kWh、谷时0.17元/kWh;天然气价格2.5元/m³,天然气热值按9.7kWh/m³折算;燃气轮机发电效率40%,热电比1.2;电网平均碳排放因子0.997 kgCO2/kWh,天然气碳排放因子2.162 kgCO2/m³;免费碳配额按基准线法取系统基准排放量的80%。这些参数都来自行业常用数据的量级,具体项目里需要根据实际地区和电网情况调整。

6.2 三组场景结果对比

我跑了三组场景:场景1是不考虑碳交易,也就是碳价设为0,免费配额设为一个极大值,等价于完全没有碳排放成本;场景2是碳价50元/t;场景3是碳价100元/t。其他参数完全一致。用KKT单层化求解,Gurobi做MILP求解器,单次求解时间在十几秒到几十秒量级,规模可以接受。

结果汇总如下:

指标无碳交易碳价50元/t碳价100元/t
系统总成本(万元)12.8613.2413.71
IESO收益(万元)4.124.354.58
系统总碳排放(t)21.618.316.9
燃气轮机发电占比52%61%67%
高峰时段外购电(MWh)3.22.41.8
平均售电价(元/kWh)0.580.610.63

这套数据和碳排放从21.6吨降到16.9吨的幅度,基本符合"碳价升高激励低碳替代"的物理直觉。

6.3 结果解读:价格-负荷-碳排的传导链条

看这张表,能清晰看到一条传导链条。碳价升高,碳交易成本在上层目标函数里权重变大,IESO开始调整自己的设备组合:减少从上级电网买煤电,增加燃气轮机的出力占比,因为燃气轮机的综合碳排放强度在算例参数下低于外购电网电。这个调整让系统总碳排放下降了,但天然气消耗增加,推动系统总成本上升。

更微妙的联动在价格端。由于燃气轮机满发后,热出力也跟着增加,热力供给变得充裕,IESO在制定价格策略时会把一部分成本增长转嫁给用户,表现为平均售电价小幅上升。用户面对更高的价格,会主动把一部分高峰时段负荷平移到低价时段,高峰外购电需求下降,这又反过来减轻了系统高峰时段的供能压力。整个系统在碳价信号下自发实现了"低碳-经济-负荷调节"的多重目标。

这个结果也验证了主从博弈模型的独特价值:碳价不是直接命令用户节能减排,而是通过上层运营商的价格策略传递下去,最终引导用户改变行为。这个传导链条在集中式模型里是看不到的,因为集中式模型里用户没有决策权,价格也不会内生变化。

7. 最容易翻车的几个实操细节:大M、线性化与收敛性

7.1 大M取值不当导致的最优解偏移

大M法看着简单,实际操作里坑很深。我见过太多人图省事,把所有互补松弛约束的M统一设成1e6,结果MILP求解时间暴涨好几倍,有时还会因为数值问题解出一个明显偏离物理直觉的结果。

我的建议是分约束取M。对于价格上下限约束,M取价格上限值的2到5倍就够;对于负荷上下限约束,M取最大负荷的2倍左右;对于功率平衡类的约束,M取该节点最大功率的3倍左右。判断M是否合适的一个实用办法是:解完问题后,检查互补松弛条件在原问题中的实际残差——把最优解代回去看 μ·g(x) 是不是接近0,如果偏差超过1e-3量级,说明M取小了,需要调大。

7.2 单位换算与碳排放核算的隐蔽错误

单位换算是我见过发生频率最高的低级错误。碳排放因子里,电网排放因子通常是kgCO2/kWh,天然气排放因子是kgCO2/m³,而碳价是元/吨碳配额。模型目标函数里如果漏了从千克到吨的换算,等于碳价被放大了1000倍,整个模型会被碳交易成本主导,结果就是燃气轮机疯狂满发、用户负荷被削得很离谱,看起来"很低碳"但经济上完全不合理。

另外还要注意热值单位。天然气价格是元/m³,热值如果用kWh/m³,那么转换成天然气购气成本的时候,得先换算成单位热量的价格,再乘燃气轮机消耗的热量,中间每多一步换算就多一个出错的机会。我的建议是写代码的时候把所有物理量统一到同一个单位体系,比如全部用kW、kWh、元的SI单位体系,数据初始化阶段就完成换算,模型内部不要做任何隐含的单位调整。

7.3 交替迭代震荡的处理经验

如果你选择交替迭代路线,震荡是大概率会遇到的问题。具体表现是:迭代到某一步,价格在两组值之间来回跳,永远满足不了收敛判据。我遇到过的最常见原因是上层目标函数对价格和购能计划的敏感度太高,一步更新跨度太大,直接从解的一侧跳到另一侧。

处理办法有两个层次。第一层,加阻尼系数,把上一轮价格和新求解价格做加权平均,alpha从0.5开始调;第二层,如果阻尼系数调到0.2仍然振荡,说明问题本身的响应函数不光滑,这时候就别硬调了,换KKT单层化路线更省事。我的经验是,线性下层的双层问题里,KKT单层化的稳定性和效率都明显优于交替迭代,只有那些下层带整数变量、KKT条件写不出来的情况,才值得在交替迭代上花时间。

7.4 一个值得养成的调试习惯

最后分享一个我自己踩坑踩出来的习惯:任何一次跑完结果,先不要急着看目标函数值,先把"均衡验证"的那一步做了。把最优价格代回下层重新求解,把最优购能计划代回上层重新求解,看两边能不能复现原结果。这个验证不花多少时间,但能一次性过滤掉大M取错、KKT漏约束、迭代未收敛、单位换算错误这几类问题。我后来几乎把这一步当作代码的标配:求解完、验证完、再画图、再分析,顺序不能乱。

如果验证对不上,也不用慌。用一个小规模的测试算例,比如把24时段缩成4时段,设备数量减少到一台燃气轮机和一组储能,然后自己手工推导一遍KKT条件,和YALMIP自动生成的约束对比。绝大部分模型错误在这种小规模对照下会无所遁形。模型本身没问题了,再放回24时段的完整算例去跑,能省下大量盲目调试的时间。

返回列表