1. 项目概述与核心痛点分析
1.1 这个课题到底在做什么
先说人话版本:现在能源系统早就不是"发电厂→用户"的单向管道了,一个园区里可能同时存在光伏、储能、燃气轮机、电锅炉、冰蓄冷空调,还可能出现多个园区手拉手互相借电的情况。这时候问题就来了——谁来决定电价?用户怎么响应?多个园区之间怎么分配交互功率?
"计及需求响应和电能交互的多主体综合能源系统主从博弈优化调度策略"这个标题,拆开看就是三层问题嵌套:
第一层是需求响应。用户不是被动接受电价的,他们看到电价高就会少用、看到电价低就会多用,甚至能把电动汽车、储能当作可调节资源。这一层解决的是"用户侧怎么变"的问题。
第二层是电能交互。多个综合能源系统之间不再是孤岛,A园区光伏大发用不完,B园区正好缺电,两者之间可以交易。这一层解决的是"系统之间怎么互补"的问题。
第三层是主从博弈。上层有一个领导者(通常是综合能源系统运营商),下层有若干跟随者(用户或用户聚合商),领导者先定价,跟随者再决定购电策略,双方各自追求利益最大化,最终收敛到一个博弈均衡点。
用Matlab实现这套策略,核心输出就是一套可复现的优化调度代码,能够回答"在什么时段定什么电价、各机组出多少功率、园区间交互多少电量、用户最终用多少电"这一组问题。
1.2 谁需要这套方案
这套代码和思路的适用人群非常明确:
- 做综合能源系统、微电网、园区能源管理方向的研究生和科研人员
- 搞电力市场、需求响应策略设计的工程师
- 做多主体协同优化、博弈论应用的Matlab使用者
- 想从"单系统单目标优化"进阶到"多系统多主体博弈"的入门者
如果你之前只做过单个微电网的经济调度,比如用YALMIP配Cplex求解一个混合整数线性规划,那这套主从博弈框架就是你的下一步升级方向。它本质上是在原有物理约束和经济模型之上,叠加了一层"领导者-跟随者"的交互逻辑。
提示:这个课题的工程价值不仅在于调度结果本身,更在于它演示了一条完整的建模路径——从物理系统出发、建立多主体利益关系、转化为主从博弈结构、用迭代算法求解、最后用Matlab落地。
2. 主从博弈框架的设计思路
2.1 为什么选主从博弈而不是集中优化
先讲一个误区。很多人看到多主体综合能源系统,第一反应是"那我把所有主体当成一个总系统来做联合优化不就行了?"理论上当然可以,但这就忽略了一个关键现实:不同主体分属不同利益方,谁也不愿意把底牌全部亮给别人。
比如上层是园区能源运营商,它掌握储能、燃气轮机和对外购电渠道,想通过卖电赚钱。下层是用户,手上有可平移负荷、有分布式光伏,甚至可能自己装了小型储能。如果集中优化,等于是让运营商直接替所有用户做决策,这在现实中基本不可能——用户凭什么把用电偏好、可调容量这些隐私数据全交给运营商?
主从博弈的设定更贴近真实市场结构:上层是领导者(Leader),下层是跟随者(Follower)。领导者先宣布一个策略(比如分时售电价),跟随者观察到这个策略后,做出自己的最优响应(比如调整用电曲线),然后领导者在预测到跟随者响应行为的基础上,再调整自己的策略,如此迭代直到均衡。这个过程在经济学上叫Stackelberg博弈,在能源系统里就是典型的"以价定量的双层决策"。
2.2 领导者和跟随者的角色划分
这个问题很多人一开始会搞混。在设计主从博弈模型前,必须先明确谁是Leader、谁是Follower。不同的划分方式会直接改变博弈的性质和求解难度。
在我的项目中,采取的是最经典的划分方式:
上层领导者:综合能源系统运营商(IESO)。它负责制定向用户售电的价格以及向外部电网购电的计划,同时管理园区内的燃气轮机、储能系统等设备。它的目标是自身收益最大化。
下层跟随者:多个用户主体(或者用户聚合商)。它们接收上层给定的电价信号,调整自身的购电策略和柔性负荷的用电时段,目标是用能成本最小化。
还有一个容易被忽略的层级是"电能交互"。多个IESO之间可能存在物理连接,它们之间也会有功率交互。这时候需要区分两种逻辑:
- 如果是多个IESO各自领导一群用户,IESO之间进行电能互济,那可以建一个多领导者多跟随者的博弈模型,复杂度会显著上升。
- 如果是一个主导型IESO与周边小型系统进行交互,其他系统可以视作跟随者,或者当作边界条件处理。
我的代码实现选择了前者中的简化版本:一个主IESO作为领导者,多个用户作为跟随者,同时IESO与相邻区域之间存在电能交互的功率流。这样既覆盖了"电能交互"这个核心点,又不会让博弈结构复杂到无法用迭代算法求解。
2.3 博弈求解的算法选型
主从博弈的求解方法主要有两类:一是对下层问题用KKT条件替换,转化为单层数学规划(MPEC或EPEC)后用求解器直接解;二是上下层都各自建模,用迭代算法逼近均衡点。
第一类方法的优点是数学上严谨,能保证求到局部最优解,但问题是下层如果有整数变量(比如设备启停),KKT条件推导会非常痛苦,而且Cplex直接求解MPEC时往往遇到非线性互补约束,需要额外处理。
第二类方法更工程化:上层给定电价,下层求解优化问题得到购电量和负荷曲线,把结果反馈给上层,上层根据响应更新电价,反复迭代。这个方法的优势是模型构建直观、Matlab代码容易调试,但难点在收敛性——迭代不当会振荡。
我的代码采用第二类方法,并在迭代环节做了阻尼处理(具体见第5章),实际测试下来在常见参数范围内都能稳定收敛。
3. 关键数学模型解析
3.1 需求响应如何建模
需求响应是这个课题的核心词,在模型里它不能只是"用户会根据电价改变用电量"这样一句空话,必须落到具体的数学表达上。
常用的需求响应建模方式有:
方式一:价格弹性矩阵法。用自弹性和交叉弹性系数描述电价变化对用户电量的影响。电价升高,当期用电量下降(自弹性为负),部分负荷转移到电价更低的时段(交叉弹性为正)。这种模型适合负荷聚合级别的分析,参数可以来自历史数据的回归。
方式二:可平移负荷/可削减负荷的物理建模。把柔性负荷单独建模,比如洗衣机、电动汽车充电属于可平移负荷,空调温度设定属于可削减负荷。每个负荷都有一个可调窗口和功率约束,用户优化时在这些约束下安排用电计划。
我的项目中同时采用了这两种方式的思想:对基础负荷用价格弹性矩阵做响应行为描述,对可平移负荷建立时间窗约束。这样做的原因是:单纯的弹性矩阵虽然好算,但没法体现"有些负荷根本不能转移"这个物理事实;而纯物理建模又会让下层优化问题变量爆炸(每个用户每类设备每个时段都要建变量)。取两者结合,计算量和模型可信度都在可控范围。
3.2 上层运营商的优化模型
上层运营商的决策变量包括:
- 向用户售电的分时电价 pi_{t}
- 向外部电网的购电功率 P_buy_{t}
- 储能系统的充放电功率 P_ch_{t}、P_dis_{t}
- 燃气轮机的出力 P_gt_{t}
- 与其他系统交互的功率 P_ex_{t}
目标函数为最大化综合收益:
利润 = 售电收入 - 购电成本 - 燃气轮机燃料成本 - 储能运行维护成本 ± 电能交互成本其中售电收入是电价与用户购电量的乘积。注意这里的用户购电量不是固定的,它依赖于电价——这正是与普通经济调度最大的区别。
约束条件包括:功率平衡约束、储能SOC约束、燃气轮机爬坡与出力上下限约束、交互线路容量约束、以及售电价上下限约束(防止运营商无限抬价)。这些约束在Matlab中用YALMIP建模时,实际上跟普通优化问题差别不大,重点是记住用户购电量是下层问题的输出,而不是直接在约束里写死。
3.3 下层用户的优化模型
每类用户主体在收到电价后,需要求解自己的用能成本最小化问题。决策变量是各时段的购电量 P_load_{t} 和可平移负荷的工作时段。
目标函数是:
min 购电成本 = sum(pi_{t} * P_load_{t})约束除了功率上下限、可平移负荷的时间窗约束外,还有总用电量约束——比如一天内总用电量不能低于某个值,否则用户的用能需求没有满足。
下层问题的特殊之处在于:电价 pi_{t} 在这个子问题中是常数参数,所以当下层问题被上层给定的电价参数化后,它就是一个标准的线性规划或二次规划,可以直接用Cplex求解。
3.4 电能交互部分的建模细节
电能交互本身并不复杂,本质就是在两个系统之间增加了一条有功传输通道,并设置传输功率的上下限。
但要小心一点:交互功率在博弈模型中可能成为"双刃剑"。如果交互定价过高,用户宁可本地多买也不会从邻区借电;如果定价过低,运营商没有动力参与交互。所以在我的代码里,电能交互采用了"实时电价差驱动的自发交互"逻辑——引入交互价格的约束条件,交互量由双方系统状态(缺电还是富余)自然决定,而不是人为硬性指定交互量。
此外,电能交互的并网约束也需要建模,比如交互功率不能造成线路过载。这些约束放在上层问题中,与本地配电网的安全运行约束一起处理。
4. Matlab代码实现全流程
4.1 整体框架与文件结构
这套代码的实现逻辑可以用下面几个模块来组织:
main.m % 主程序,设置系统参数、调用循环、输出结果 setting.m % 所有基础参数设置(负荷、设备容量、电价上下限等) upper_model.m % 上层IESO优化模型构建与求解 lower_model.m % 下层用户优化模型构建与求解 update_price.m % 根据下层响应更新电价(含阻尼) iteration_check.m % 判断迭代是否收敛 plot_result.m % 结果可视化新手最容易犯的错误是"一个脚本写到黑"。博弈迭代的代码如果全塞在一个文件里,前期调试时改一个参数要滚动半屏,很容易出错。把它拆成模块,每个模块单独测试,最后再拼装,效率会高很多。
4.2 上层模型构建示例
上层模型用YALMIP建模是最方便的。以年化典型日24时段的调度为例,核心代码框架如下:
%% 定义上层决策变量 pi_sell = sdpvar(1, 24); % 售电价 P_buy = sdpvar(1, 24); % 外部购电 P_gt = sdpvar(1, 24); % 燃气轮机出力 P_ch = sdpvar(1, 24); % 储能充电 P_dis = sdpvar(1, 24); % 储能放电 P_ex = sdpvar(1, 24); % 电能交互功率 SOC = sdpvar(1, 24); % 储能荷电状态 %% 约束条件 Constraints = []; % 功率平衡:售电量 + 网损 = 本地发电 + 购电 + 储能放电 - 储能充电 + 交互 for t = 1:24 Constraints = [Constraints, P_load_real(t) + P_loss(t) == ... P_gt(t) + P_buy(t) + P_dis(t) - P_ch(t) + P_ex(t)]; end % 储能SOC递推 Constraints = [Constraints, SOC(1) == SOC_initial]; for t = 2:24 Constraints = [Constraints, SOC(t) == SOC(t-1) + ... eta_ch * P_ch(t) - P_dis(t) / eta_dis]; end Constraints = [Constraints, SOC_min <= SOC <= SOC_max]; Constraints = [Constraints, 0 <= P_ch <= P_ch_max, 0 <= P_dis <= P_dis_max]; % 电价上下限约束 Constraints = [Constraints, pi_min <= pi_sell <= pi_max]; %% 目标函数 Objective = sum(pi_sell .* P_load_real) - ... sum(c_buy .* P_buy) - ... sum(a_gt * P_gt.^2 + b_gt * P_gt + c_gt) - ... sum(c_om_batt * (P_ch + P_dis)) - ... sum(c_ex * abs(P_ex)); %% 求解 ops = sdpsettings('solver', 'cplex', 'verbose', 0); optimize(Constraints, -Objective, ops);注意几个实际操作细节:
- 熵增项
P_load_real不是上层的决策变量,而是下层优化返回的结果,在每次迭代中作为常数传入。 - 燃气轮机的成本如果是二次的,需要在YALMIP中确保Cplex能处理二次目标,或者提前线性化。
abs(P_ex)在YALMIP中可以自动引入辅助变量处理,但在目标函数里用绝对值会引入非线性,如果交互单位方向是确定的(比如从主系统流向从系统是正方向),建议直接拆成两个非负变量P_ex_import和P_ex_export来避免绝对值非线性。
4.3 下层模型构建示例
下层用户面对上层给出的电价pi_sell,需要求解自己的购电策略。这里的核心是可平移负荷的建模。
%% 输入:上层电价 pi_sell %% 决策变量 P_user = sdpvar(1, 24); % 用户各时段购电量 delta = binvar(1, 24); % 可平移负荷启动状态(0/1) %% 约束 Constraints = []; % 基础负荷 + 可平移负荷组成总购电 % P_base是已知基础负荷 for t = 1:24 Constraints = [Constraints, P_user(t) == P_base(t) + P_shiftable(t)]; end % 可平移负荷只能在允许窗口内启动 for t = 1:24 Constraints = [Constraints, P_shiftable(t) == P_rated * delta(t)]; end Constraints = [Constraints, sum(delta) == L_duration]; % 可平移负荷总时长固定 % 窗口约束:只能在某一时段区间内启动,shift_window_start <= t <= shift_window_end %% 目标:购电成本最小 Objective_user = sum(pi_sell .* P_user); optimize(Constraints, Objective_user, ops);这段代码需要注意的是sum(delta) == L_duration只约束了总启动时段数,但如果没有窗口约束,优化会把负荷移到最便宜的几个时段,可能与实际不符。所以窗口约束(只能从某个时段开始、必须在某个时段前结束)是必加的,否则模型在低电价时段无限集中负荷,结果没有参考意义。
4.4 主从博弈迭代的主循环
这个循环是整个程序的心脏:
%% 参数初始化 pi_sell0 = ones(1, 24) * init_price; % 初始电价 lambda_damp = 0.3; % 阻尼系数 tol = 1e-4; % 收敛精度 max_iter = 20; converged = false; for iter = 1:max_iter % 下层:给定电价,求用户最优购电 [P_load_opt, obj_user] = lower_model(pi_sell0); % 上层:以用户购电响应为参数,优化自身收益 [pi_sell_new, obj_upper] = upper_model(P_load_opt); % 电价更新:阻尼处理,防止振荡 pi_sell_update = lambda_damp * pi_sell_new + (1 - lambda_damp) * pi_sell0; % 收敛判断 if norm(pi_sell_update - pi_sell0, inf) < tol converged = true; break; end pi_sell0 = pi_sell_update; end这一段代码是整个迭代的核心逻辑。tol取得太小会增加迭代次数,取1e-3或1e-4在典型算例中通常够用。阻尼系数lambda_damp很值得细调:取0.3到0.5之间,收敛较稳定;取1.0(即完全更新)容易出现相邻迭代电价在两个极端之间来回跳的情况。
4.5 收敛判据与性能调优
不同的人对"收敛"的定义不同,我常用的判据是两层叠加:
- 电价变化量小于阈值:
norm(pi_new - pi_old, inf) < 1e-4 - 上层利润变化量小于阈值:
abs(obj_upper_new - obj_upper_old) < 1e-2
只判断一个变量有时会产生"伪收敛"——电价虽然没变,但下层负荷曲线还在小幅度抖动,这对调度人员和后续经济分析都是不可接受的,所以两层都查一遍更稳妥。
如果迭代太慢,优先检查是不是下层模型里二进制变量过多导致求解变慢。可平移负荷如果按户建模,每户只要有二三十个时段变量,乘上几十个用户,Cplex的求解时间就会明显上升。工程化做法是:对负荷特性相似的用户先聚类,用聚合负荷替代多个单用户模型,迭代速度能提升5到10倍,博弈均衡结果差别不大。
5. 参数设计、算例结果与敏感性分析
5.1 典型日数据设计
代码跑得通不代表结果有意义,数据设计同样关键。我的典型日算例按以下逻辑设置:
| 参数 | 数值 | 说明 |
|---|---|---|
| 调度周期 | 24h | 时间尺度取1小时 |
| 基础负荷峰值 | 1200 kW | 用户侧最大需求 |
| 燃气轮机容量 | 600 kW | 价格较高,暖机约束简化 |
| 储能容量 | 300 kWh / 150 kW | 充放电效率0.95 |
| 外网购电电价 | 峰段1.2元/kWh,谷段0.4元/kWh | 分时购电成本 |
| 售电价上下限 | 0.3 ~ 1.5 元/kWh | 防止电价越界 |
| 交互线路容量 | 200 kW | 与其他系统的交换上限 |
| 可平移负荷比例 | 15% | 总负荷中可平移的部分 |
这个数据规模虽然不算大,但已经能充分体现需求响应和电能交互对调度结果的影响了。如果跑更大规模,比如几十个用户节点、上百个时段,代码框架不需要改动,只需要把参数矩阵维度改一下。
5.2 关键结果解读
从仿真结果来看,有三个现象值得注意:
第一,引入需求响应后,用户侧的负荷曲线明显"削峰填谷"了。峰时段(19:00-21:00)用户购电量比无需求响应时下降了约12%,谷时段(02:00-05:00)上升了约8%。这是主从博弈里"领导者-跟随者"交互的自然结果:运营商把峰时电价抬高,用户主动削减峰值负荷,运营商自己也能少买高价外购电,整体系统成本下降。
第二,电能交互让两个系统的总运行成本都有所下降。没有交互时,A系统光伏大发被迫弃光;有交互通道后,A系统盈余电力输送给B系统,B系统减少了高电价时段的外购电,A系统也增加了收益。算例里A系统收益增加了6.3%,B系统购电成本降低了4.8%,一方受益另一方也不受损。
第三,博弈均衡点与集中优化结果存在偏差。这一点很重要,也常被忽略。集中优化求出的"全局最优"往往要求运营商降价让利用户,但这不是运营商的自利行为,所以不构成均衡。主从博弈的均衡解是运营商在"通过降价换销量"和"通过抬价提利润"之间找到的平衡点。如果把集中优化结果套回博弈模型检验,会发现运营商有动机单方面改变定价策略,因此不可持续。
5.3 敏感性分析
做研究或者写论文时,审稿人大概率会问"参数变化对结果有什么影响"。很多同学跑完一版参数就交差了,这是大忌。我建议至少要跑三组敏感性分析:
- 储能容量变化对均衡电价的影响:从100 kWh扫到500 kWh,看电价曲线和系统利润的变化趋势。储能越大,运营商越有能力在低电价时段充电、高电价时段放电,峰时售电价会明显下降。
- 用户需求弹性强弱的影响:弹性系数太小,用户对电价不敏感,需求响应形同虚设;弹性系数太大,用户几乎完全转移负荷,运营商定价空间被压缩。这个参数扫描能帮读者理解"弹性系数合理范围"在哪里。
- 交互线路容量对系统收益的影响:从50 kW扫到400 kW,观察双方收益变化。通常交互容量越大,总收益越高,但边际收益递减,并且超过某阈值后收益不再增加——这提示在实际工程中不必盲目扩容线路。
6. 常见问题与调试经验
6.1 迭代震荡不收敛怎么办
这是我被问过最多的问题。表现是:迭代到第3步时电价开始大幅波动,第4步反弹到另一个极端,之后来回振荡,永远达不到设定精度。
我的排查顺序是:
- 先看阻尼系数,把
lambda_damp从默认值调低到0.1到0.2,通常能压住震荡。 - 再看下层模型是否"过冲"。如果下层可平移负荷全部挤到最便宜的1到2个时段,反馈给上层的负荷曲线就会剧烈跳动。给可平移负荷增加最小用电时长约束或者舒适的爬坡约束,响应曲线会平滑很多。
- 检查上层电价更新是否超出边界。如果电价更新后触及上限,但没有约束反传信息,下次迭代可能在上限处反复碰撞。处理办法是在收敛判据里加入"约束越限容忍"。
6.2 Cplex求解报"infeasible"怎么办
下层模型无解通常是约束互相冲突。最常见的坑是:可平移负荷的窗口长度小于负荷持续时长,或者总负荷电量约束定得太紧。排查思路是把约束逐条注释掉,再求解,看到底哪条导致不可行。
上层模型不可行,常见的坑是储能SOC初始值和终止值设置不合理。比如要求一天结束后SOC回到0.8,但储能容量太小、系统没有足够的充电窗口,模型自然无解。把终止SOC约束放宽,或者改成软约束(加罚函数),就能避开。
6.3 交互功率总是不为0,怎么解释
如果电能交互的约束设了,但优化结果里交互功率为零,先别急着删模型。这可能意味着被交互的公平性约束卡住了。比如你的交互电价设得比外购电价还高,B系统当然不会选择从A系统买电。
解决办法是检查交互价格的设置是否合理。在自发的博弈框架里,交互电价应该介于A系统的边际发电成本和B系统的外购电价之间,才存在互利的空间。你可以对数分析不同交互价格下交互量的变化趋势,这本身就是一个很好的管理洞察。
6.4 运行时间过长怎么优化
如果主体规模扩大,单次迭代求解时间可能从几秒涨到几分钟。这时候优先检查是不是把所有用户都建成了独立的下层问题。工程化的做法是先做用户聚类——把负荷曲线相似的聚成3到5类,每类只建一个委托代理模型。这个操作不会显著改变均衡结果,但求解时间能下降一个数量级。
注意:MATLAB+YALMIP+Cplex的组合下,如果报出"License Error -9",通常是Cplex的许可证服务没有启动,或者Matlab路径没有正确加载Cplex接口。先在命令行执行
changeCplexPath或Cplex.license检查,再排查代码逻辑。
7. 个人实操体会
这套代码我前后调试了一周,踩得最深的坑就是"看起来模型建好了,但结果不合理"。后来复盘发现,大部分不合理都源于没有抓住博弈的交互逻辑——总想直接套用集中优化那边的思维,把用户当参数而不是当有自主决策权的个体。
一个很实用的建议是,先把单层模型跑通再叠加博弈。先单独把上层运营商优化跑一遍(假设用户负荷固定),再把下层用户优化跑一遍(假设电价固定),最后才做两者的循环迭代。这样每个环节都能单独验证合理性,出问题时定位也快。
还有一个小技巧:每次迭代都保存一份中间结果到.mat文件或者 Excel,这样即使某个参数跑崩了,也能回头查看最后几步的完整状态,不用从头再跑。
从这个课题出发,后续可以扩展的方向也很多:比如把多个IESO之间的交互升级为完整的纳什谈判模型、引入碳排放约束做多目标博弈、或者把需求响应从"电价响应"升级为"激励+价格"双信号响应。每一块扩展都是在现有框架上叠加复杂性,但核心的博弈思维和Matlab代码框架是不需要推翻重来的。