在智能小区充电这个场景里,代理商是一个经常被低估的关键角色。它一边要从更上一级的电网批发买电,另一边要面对一群各自有出行计划的电动汽车车主。给充电定什么价、定多少,直接决定了车主的充电行为,而车主的充电行为又反过来决定代理商的利润和小区变压器的安全。这个互相咬合的决策链条,用主从博弈来建模是最自然的选择。这篇文章记录的是我在Matlab里复现“基于主从博弈的智能小区代理商定价策略与电动汽车充电管理”这套模型的完整过程,包括模型怎么分层、求解用什么思路、代码怎么组织、参数怎么设,以及最后遇到的一堆调试问题。
如果你正准备做充电调度、虚拟电厂、电力市场或者能源系统方向的项目,这篇文章应该能帮你把“双层优化”从概念落实到代码。我不会只丢给你一套看似高深、实际很难跑通的文件,而是把每一步为什么这么做讲清楚。尤其建议那些刚接触主从博弈的同学从头看一遍,后面调试时的坑大多藏在建模阶段的细节里。
1. 主从博弈的建模思路:代理商与车主之间的决策关系拆解
1.1 为什么定价问题天然适合主从结构
主从博弈,也叫Stackelberg博弈,描述的是决策有先后、地位有高低的多方交互过程。领导者先公布自己的策略,跟随者观察到策略后做出最优反应;领导者不是闭着眼睛乱出牌,而是把跟随者的最优反应函数提前装进自己的优化目标里,再决定怎么出牌。这个“我算准了你会怎么应对”的结构,用来描述充电定价特别合适。
为了帮助理解,可以拿菜市场类比:批发商先公布批发价,摊贩根据批发价决定进多少货。批发商知道摊贩的进货量随价格变化的规律,于是定一个预期收益最大的批发价。这里批发商是领导者,摊贩是跟随者。在小区充电场景中,代理商就是那个批发商,电动汽车车主就是摊贩——代理商公示充电电价,车主看到电价后结合自己的行程决定什么时段充多少电。
如果你假设两边同时出牌,得到的均衡叫Nash均衡。但在真实充电流程里,充电价格公示在前、车主充电决策在后,决策时序天然是“先定价、后充电”。所以主从结构比同时博弈更贴合现实。文献和教材里经常说的“上层代理商定价、下层车主充电决策”,正是对这种Stackelberg结构的落地描述。
1.2 智能小区里的角色层级:代理商不是唯一的玩家
不过,智能小区里的参与者不止代理商和EV车主两方。代理商向上一级购电,同时面对居民基础负荷和电动车充电负荷;小区里往往还有屋顶光伏,变压器的容量又有限。准确地说,这个系统至少有三层:最上层是电网侧的购电价格,作为外部参数输入;中间层是代理商,负责制定充电电价和可能的服务费;最底层是电动汽车车主,根据电价和自身需求决定充电行为。居民基础负荷在这个模型里通常被当作不可调度的固定参数,但如果想扩展需求响应,也可以把它改造成可调节变量。
这个层级结构直接决定了建模的划分方式:电网侧给出购电价、基础负荷和变压器容量;代理商层给出决策变量和利润函数;车主层给出各自的约束和目标函数。代码的组织也应该按照这个层级来。我在第一次写代码时曾试图把所有变量都堆进一个大优化里求解,结果既难调试,也很难解释清楚每个决策到底是谁做的。把角色分清楚,是建模之外最容易忽略、却最重要的一步。
1.3 逆向归纳法:从车主的反应倒推代理商的策略
主从博弈的标准求解思路是逆向归纳法:先求跟随者的最优反应函数,再把这个函数代回领导者的目标中求解。放到充电场景里,第一步是给定任意电价向量λ,解出所有车主的最优充电功率P(λ),这会得到每个时段充电总负荷与电价之间的映射关系;第二步是把这个P(λ)看作λ的函数,代入代理商利润函数,求解让利润最大的电价。
这个均衡解的意义在于,它不是凭空构造的最优点,而是现实中代理商和车主交互稳定后可能达到的状态。假设代理商单方面抬高某个时段的电价,车主就会把充电需求挪到别的时段,代理商的收入并不增加;假设代理商压低电价,充电量虽然涨了,但单位利润下降。均衡电价恰恰是这两股力量互相拉扯后的结果。理解这一点,比记住任何公式都重要,因为后面每次仿真结果不理想时,你都能回到这个逻辑上找原因。
2. 代理商定价策略的数学模型与决策边界
2.1 决策变量、目标函数与收益构成
先把时间离散化。常用做法是取T=24,每个时段1小时;如果希望充电曲线更平滑,可以取T=96,每个时段15分钟。下面以1小时为例。代理商在每个时段t的决策变量是电动汽车充电电价π_t^EV,单位元/kWh;居民负荷执行固定价格π_base;代理商向上购电的批发电价π_t^buy由外部给定。
代理商的总收益可以切成三块来看:售电给EV的充电收入、售电给居民的固定收入、可能还有充电服务费。成本侧主要就是向上级的购电成本。目标函数写成文字公式大致是:
Profit = Σ_t ( π_t^EV × P_t^EV + π_base × P_t^base - π_t^buy × (P_t^EV + P_t^base) )注意这个式子里P_t^EV不是任意给定的,它是下层车主对电价π_t^EV做最优反应得到的充电总功率。所以代理商并不能直接“安排”电动车在哪充电,只能通过改变π_t^EV去间接影响P_t^EV。这正是双层问题与单层优化问题的本质区别,也是初学者最容易搞混的地方。有人会把P_t^EV当成决策变量直接求解,那样得到的其实是“理想调度”而不是“代理商定价博弈”的结果。
2.2 约束条件的取舍与定价边界
约束条件方面,起步阶段主要守三条边界。第一是电价上下限,仿真中通常给0.3~1.5元/kWh,防止代理商定出离谱价格;第二是变压器容量约束,即小区总负荷不能超过配变上限,写成P_t^base + P_t^EV ≤ P_trans;第三是下层各辆车的充电功率和行程约束。充电桩数量、单桩功率上限这些通常放在下层约束里,不放在上层。
在初版建模中,约束条件宁可少加也不要乱加。我见过不少同学一上来就把网络潮流、电压偏移、三相平衡全部塞进模型,结果问题变得极其难解,bug都不知道从哪找起。第一版把电价上下界、变压器容量和下层行程约束守住,已经能得到很有说服力的结果。网络约束属于进阶扩展,可以在基础版本跑通之后再逐步加入,否则你会陷入“跑不出结果但不知道是模型错还是求解器错”的困境。
2.3 需求价格弹性视角下,为什么不是电价越高越好
经济学里判断一个商品能不能无限涨价,要看需求价格弹性。充电需求在夜间低谷期弹性很大,电价稍微高一点,车主就把充电推迟到更便宜的时段;但在第二天早上出发前,很多车主的充电需求变得刚性,不管电价多高都只能在那个窗口期充够电,这个时段的弹性就很小。聪明的定价者当然希望在低弹性时段适当提价获取利润,在高弹性时段压低价格吸纳充电量。
但EV充电负荷和普通居民负荷不一样,它并不是均匀分布在一天里的。只要某个时段电价上升,下层线性规划就会自然地减少该时段充电功率,于是P_t^EV下降。这带来一个反直觉的结论:电价定得越高,充电量越少,总收入反而可能下降。因此均衡价格很多时候是“需求转移”推出来的,而不是靠拍脑袋定上限得出来的。你在分析仿真结果时,如果发现电价曲线几乎是平的,先别急着怀疑代码,可以回去看看是不是价格上下界设置得太紧、或者下层模型里车主没有足够的时段自由度。
3. 电动汽车充电管理子问题:车主的算账逻辑怎么建模
3.1 单辆车的充电决策变量与约束
现在进入底层模型。对第i辆车,决策变量是它每个时段t的充电功率x_{i,t},单位kW。约束条件主要有三类,第一条是功率边界:0 ≤ x_{i,t} ≤ P_i^max,也就是充电桩的功率上限,家用慢充桩常见7kW;第二条是停留约束:如果车主不在小区,也就是stay_{i,t}=0,那么对应时段的功率强制为0;第三条是能量需求约束:总充电量至少要达到出行所需电量,写成Σ_t x_{i,t} ≥ Q_i^need。
实际建模时,Q_i^need可以简单用出行前目标SOC与当前SOC之差乘上电池容量得到,也可以根据车主昨日行驶里程推算。有些同学还会给模型加上“充电不超过电池容量”的约束,这个值得做:写成Σ_t x_{i,t} ≤ (targetSOC - initSOC) × capacity,防止出现“夜间电价低就拼命充到100%”的极端行为。这些约束看起来简单,却是下层问题可行性检查的重要一环,很多无解问题都出在这里。
3.2 车主目标函数:充电费用最小化及扩展
车主的目标很简单:在给定电价λ_t之后,最小化总充电费用。目标函数大致是:
min Σ_t λ_t × x_{i,t} × ΔtΔt表示时间步长,1小时就取1。如果想加入更接近现实的行为因素,可以考虑电池损耗系数、充电次数惩罚或者对出行不便的惩罚,但对第一版模型我完全不建议加。任何非线性项都会让下层失去线性规划的结构,求解难度立刻上了一个台阶,而且你很难判断结果到底是模型本身的行为还是数值坏掉了。
初学者可能不理解为什么车主不选择“到点就充”,而是把充电量集中安排在价格最低的时段。这其实是线性规划目标函数决定的:如果某时段价格低,目标函数就倾向于提高该时段功率;如果两个时段价格一样,线性规划的解可能不唯一。为了让结果稳定,可以给目标函数加一个很小的二次正则项,比如0.001×x²,这样得到的充电曲线更平滑、也更好画图解释。
3.3 下层问题的凸性:这是整个求解可行性的基石
下层问题是一个线性规划,可行域是凸集,最优解是全局最优;如果把目标改成二次代价,就变成凸二次规划,同样能保证全局收敛。这个凸性非常重要,它保证了两件事。第一,给定电价之后,车主的反应是确定且可计算的;第二,在数学上可以用KKT条件把下层问题等价转化为上层问题的约束,从而把双层问题转成单层问题。就算你不打算做KKT转化,凸性也保证了迭代法在电价变化不太剧烈时能收敛到有意义的结果。
这里有一个值得强调的直觉:车主的充电决策并不是在“充或不充”之间二选一,而是在不同时段之间转移充电量。这种可转移性就是充电负荷参与需求响应的本质,也是主从博弈能够通过价格信号优化整体负荷曲线的基础。如果你在仿真里看不到明显的充电时段转移,多半是价格差异太小,或者停留时段约束设得太死。试着把谷段电价调低、让车主的停留窗口长一点,转移效果立刻会出来。
4. Matlab代码实现:嵌套求解流程与关键函数片段
4.1 三种可行的求解路线对比
在Matlab里求主从博弈均衡,常见路线有三条。我把它们放在一张表里对比:
| 求解路线 | 核心思路 | 优点 | 缺点 |
|---|---|---|---|
| KKT转单层 | 把下层最优性条件并入上层约束,交给求解器一次求解 | 数学上严格,适合商业求解器 | 推导繁琐,乘子符号容易错 |
| 迭代法 | 上层传电价给下层,下层返回充电功率,循环更新价格直到收敛 | 实现简单,模块清晰 | 收敛依赖初值和步长 |
| 外层启发式搜索 | fmincon/PSO作为外层搜电价,内层每轮调用下层LP | 对非光滑目标更耐受 | 计算慢,重复性稍差 |
我自己建议:目标是复现文献结果并用于论文的话,第一版务必用迭代法或外层搜索。不要一上来就搞KKT转单层,因为KKT的正确性很难直观验证,一旦某个乘子符号写反,整个结果就是错的,而且错误还会体现在“看似合理”的数值上。等你用迭代法跑通了完整逻辑,再去研究KKT的严格形式也不迟。
4.2 代码结构:先写下层,再包上层
代码结构上,我强烈建议把下层封装成独立函数。输入是电价向量和一个电动车参数结构体,输出是该车各时段充电功率向量。上层目标函数单独写,输入电价和所有EV充电功率,输出代理商利润。主脚本负责组装数据、调用优化器、绘制结果。这样每个模块都能单独测试,后面扩展V2G、储能、光伏时也只需要增加对应模块,不需要把整个模型推翻重写。
我见过一个反面案例:有人把所有循环、约束都写在主脚本里,两百多行代码揉成一团,结果想改一下电价上下限,找了半天才找到那行常量。把函数拆开之后,每个问题都可以单独打印调试,效率高很多。
4.3 关键代码片段:从下层到上层再到外层循环
先看下层,也就是单车充电决策函数:
function x = solve_ev_charging(price, ev, dt) % 给定电价向量 price(Tx1),返回该车充电功率 x(Tx1) T = numel(price); % 目标:min sum(price .* x) * dt f = price * dt; % 功率边界:停留时段最多为最大充电功率,非停留时段为0 ub = ev.max_power * ev.stay; % stay 是 Tx1 的0/1向量 lb = zeros(T, 1); % 需求约束:总充电量 >= 出行所需电量,写成 A*x <= b 形式 A = -ones(1, T); b = -ev.required_energy; % 可选:防止过度充电,加上限约束 if ev.need_upper A = [A; ones(1, T)]; b = [b; ev.max_energy]; end x = linprog(f, A, b, [], [], lb, ub); end注意这里price的单位是元/kWh,x的单位是kW,dt单位是小时,所以price*dt就是目标函数中每个时段的费用系数。ev结构体里至少要有max_power、stay、required_energy这几个字段。
再看上层目标,也就是代理商利润:
function profit = agent_profit(price_ev, P_ev, P_base, price_buy, price_base, P_trans, dt) % 所有向量均为 Tx1 revenue_ev = sum(price_ev .* P_ev) * dt; revenue_base = sum(price_base .* P_base) * dt; cost = sum(price_buy .* (P_ev + P_base)) * dt; % 变压器过载罚项,软约束处理 penalty = 1e3 * max(0, max(P_ev + P_base) - P_trans); profit = revenue_ev + revenue_base - cost - penalty; end把下层和上层包在一起,得到wrapper函数:
function profit = wrapper(price, ev_set, P_base, price_buy, price_base, P_trans, dt) P_ev = zeros(size(price)); for i = 1:numel(ev_set) x = solve_ev_charging(price, ev_set(i), dt); P_ev = P_ev + x; end profit = agent_profit(price, P_ev, P_base, price_buy, price_base, P_trans, dt); end最后在主脚本里调用优化器:
price_init = 0.7 * ones(T, 1); lb = 0.3 * ones(T, 1); ub = 1.5 * ones(T, 1); obj = @(p) -wrapper(p, ev_set, P_base, price_buy, price_base, P_trans, dt); options = optimoptions('fmincon', 'Display', 'iter', 'MaxFunctionEvaluations', 5000); price_opt = fmincon(obj, price_init, [], [], [], [], lb, ub, [], options);如果fmincon收敛不理想,可以换成patternsearch,它的稳定性在非光滑目标上更好:
[price_opt, ~] = patternsearch(obj, price_init, [], [], [], [], lb, ub, [], options);跑完外层优化之后,price_opt就是均衡充电电价。把它再喂给solve_ev_charging,得到每辆车的最优充电功率,再累加得到总充电负荷,最后画图。
4.4 工具箱选型与版本建议
整个实现只需要MATLAB自带的Optimization Toolbox,用到linprog、fmincon、patternsearch这些函数。如果想用YALMIP建模,也可以用它搭配商业求解器,但初版我建议别碰。YALMIP确实能简化KKT条件的表达,可一旦求解器报错,新手往往分不清是模型问题还是接口问题。基础版本跑通之后,再考虑引入YALMIP也不迟。
版本方面,R2016以后的MATLAB跑这些函数基本没有什么兼容性问题。唯一要注意的是linprog接口在旧版本和新版本之间略有差异,如果你代码直接复制老书上的写法,可能需要稍微调整参数顺序。
5. 仿真参数设定、对比实验设计与结果解读
5.1 一个可以直接起步的参数集
为了让模型能跑起来,参数要合理但不必追求完全真实。下面这套参数是我常用的起步配置:
| 参数 | 参考取值 | 说明 |
|---|---|---|
| 仿真时长/步长 | 24h / 1h | 想要平滑曲线可改96步 |
| 电动汽车数量 | 20辆 | 先小规模验证逻辑 |
| 慢充桩功率上限 | 7 kW | 典型家用交流桩 |
| 电池容量 | 50 kWh | 中等容量 |
| 初始SOC | 0.3~0.8随机 | 用rand生成即可 |
| 出行所需能量 | 15~30 kWh | 对应日行驶50-100公里 |
| 停留时段 | 18:00至次日8:00 | 下班回家场景 |
| 居民基础负荷峰值 | 150~200 kW | 可选用典型日负荷曲线 |
| 变压器容量 | 250 kVA | 要留出基础负荷的余量 |
| 购电价 | 谷0.3、峰0.8元/kWh | 分时曲线 |
| 充电电价上下限 | 0.3~1.5元/kWh | 防止定价过高 |
| 居民售电价 | 0.6元/kWh | 固定价 |
这些参数只是起步值,重点在于把模型跑通。我自己的习惯是先用3辆车、甚至1辆车验证数学正确性,再扩展到20辆。直接上100辆车,一旦出现无解,你很难判断是哪辆车参数惹的祸。
5.2 对照实验设计:怎么证明博弈优化有效
为了说明主从博弈优化的价值,至少要对比几个方案。方案A是无序充电,假定所有车回到家立即以最大功率充电直到充满,这是最原始的baseline;方案B是固定电价下的最优充电,即电价恒定为0.6元、车主做理性响应;方案C是主从博弈均衡,代理商按博弈定价、车主优化响应。如果条件允许,可以加一个方案D:理想化统一调度,假设代理商能直接控制所有EV充电功率,作为理论上界。
对比指标取几个关键数就够了:负荷峰谷差、变压器最大负载率、代理商净利润、车主平均充电费用。主从博弈方案应该在峰谷差和负载率上明显优于方案A和B,在车主费用上优于方案A,在代理商利润上优于方案B。当然,如果博弈定价偏向于吸引更多车主充电,代理商单位利润可能不高,但总利润可能因为充电量上升而增加,这部分需要结合目标函数具体分析。
5.3 读仿真结果时重点看什么
读结果时,我习惯固定在三个位置花时间。第一,电价曲线形状。均衡电价应当呈现明显时段差异,白天用电高峰价格高、夜间谷段价格低,且高电价时段与基础负荷高峰位置相关。如果均衡电价几乎是一条平线,说明下层需求对价格不敏感,或者价格上下界设得太紧。第二,充电功率分布。把所有EV逐时段的充电功率累加画柱状图,应该能看到充电负荷向夜间转移;如果按车辆参数画充电热力图,可以直观看出哪些车把充电放到了凌晨。第三,收敛过程。记录迭代过程中电价向量与上一轮差的二范数,看它是否稳定下降。如果曲线来回震荡,说明阻尼系数或初值需要调整。
还有一点容易被忽略:代理商利润和车主费用是两个方向的目标。不断提高电价当然对代理商更有利,但车主福利会下降。在帕累托意义上,主从博弈的均衡是“给定权力结构下的最优”,而不是社会福利最大化的结果。写论文时把这层关系讲清楚,比堆一堆漂亮的图表更能体现你对模型的理解。
6. 调试过程中的常见问题与工程经验
6.1 下层无解:停留时间不够充出需求电量
最常见的问题是下层线性规划直接返回infeasible,原因通常是停留时间太短,或者required_energy设得太大。举个例子,如果某辆车18:00到家、次日8:00离开,停留14小时,7kW的桩理论上能充入98kWh,远超一般电池容量,所以这个例子一般不会出问题。但如果你把时间步长取到15分钟、车辆数量又大,偏偏有几辆车初始SOC特别低,就可能出现需求电量大于停车期内可充入电量的情况。
最省事的解决办法是检查参数并限制required_energy,比如强制要求初始SOC不能低于0.2,同时出行需求不超过20kWh。另一种做法是在模型层面引入松弛变量,把“必须满足”改成“尽量满足并加惩罚”,这样下一层永远不会无解,但数学形式上会复杂一些。两个办法都有效,实际项目里我一般先用前者,因为调起来快。
6.2 电价震荡不收敛:阻尼与初值的取舍
迭代法不收敛的现象非常典型:第一轮电价定得较高,下层把所有充电需求转移到低价时段;上层看到低价时段需求上涨,又上调那个时段的价格;下一轮,需求又转移到别的时段……如此循环,电价在两个极端之间摆来摆去。对策很简单,一是给电价更新加阻尼,比如新电价取0.5×计算值+0.5×当前值;二是电价初值尽量贴近购电价,不要随便设一个很极端的数。
这种震荡背后的根源是下层LP的极点解对电价变化是阶跃式的,所以价格更新步长必须足够小。你可以把这件事类比成开车:在冰雪路面上猛打方向,车轮必然来回画龙;小步修正方向,车反而走得稳。
6.3 上层目标不光滑:fmincon失灵时换patternsearch
我在早期用fmincon跑上层时,遇到过目标函数突然跳变到很大负数的情况。检查后发现,下层LP在某个边界处无解,wrapper返回空的x,求和时形成NaN。解决方法是判断linprog的退出状态:如果exitflag不是1,就返回一个很小的利润或者极大的惩罚值,让优化器主动避开不可行区域。
此外,下层LP的最优解通常落在极点,所以上层目标相对电价不是完全光滑的,fmincon在这种问题上有时会来回震荡。实测下来,patternsearch虽然慢一点,但面对非光滑目标时的稳定性明显更好。如果你发现fmincon迭代了半天还在原地打转,直接换patternsearch往往立竿见影。
6.4 规模扩展时的性能优化建议
规模扩大时,最直接的瓶颈是对每辆车循环调用linprog。20辆车没有问题,200辆就会明显变慢。优化方向有三个:第一,把参数完全相同的车辆合并成组,每组只解一次LP,物理意义不变但计算量大幅下降;第二,用parfor替代for循环,并行求解各车的下层问题;第三,如果时间粒度取到96步,注意约束矩阵维度增长,内存占用会明显上升。
不过说句实在话,对绝大多数复现和论文验证场景,20到50辆车、每小时一个点时,这套代码在普通笔记本电脑上完全跑得动。我见过有人非要把规模撑到几百辆车、时间粒度搞到15分钟,结果光跑一组参数就要一个晚上,完全没必要。先跑通、后扩展,才是做仿真效率最高的路子。
最后分享一点我自己的体会:主从博弈最大的价值不在于数学上的精巧,而在于它把“先定价、后响应”这个真实的商业时序搬进了模型。我最初复现这个项目时,也是一心想把KKT转换用起来,结果被各种乘子和互补松弛折腾了整整两周。后来老老实实从3辆车的小算例做起,半天就跑通了。所以如果你现在正准备开始,我的建议是:参数越小越好、下层函数越短越好、先别追求复杂扩展。等模型跑出合理的均衡电价和充电曲线之后,你自然会见识别下一步该往V2G、储能协同还是多代理商竞争方向走。到那时候,你踩过的每一个坑都会变成判断新模型是否正确的最好工具。