ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于双层优化的大规模电动汽车充放电时空调度策略及Matlab实现

基于双层优化的大规模电动汽车充放电时空调度策略及Matlab实现 做电动汽车调度研究的同学大概率都遇见过一种困境模型建得很完整约束抠得很细但一跑出来调度中心自己很满意车主却根本不愿意配合。原因很简单你替车主做的决定没考虑车主自己的算盘。这篇博文要拆的“基于双层优化的大规模电动汽车充放电时空调度策略研究”正好就是针对这个问题来的而且配套Matlab代码实现。我会从数学模型、求解思路、代码结构、算例结果、还有实际踩坑排查这几个角度把整个项目完整讲清楚。适合正在做电动汽车有序充电、V2G、配电网互动方向的研究生也适合想快速搭建原型的工程师参考。1. 项目核心思路拆解为什么是“双层时空”1.1 大规模EV调度到底难在哪先看规模。一个中等城市可能同时有几千辆电动汽车在充电每辆车的接入时间、离开时间、初始SOC、目标SOC、最大充电功率、是否愿意V2G放电全都不一样。这些差异叠加在一起变量量级一下就上去了。如果直接用传统数学规划做多目标变量数量很容易突破几十万甚至上百万商业求解器也得算到崩溃。更麻烦的是空间差异。同样的充电功率放在负荷轻的变电站和负荷重的变电站对电网影响完全不同晚高峰时刻在商业区充电甚至可能导致配电变压器过载。所以调度不能只给一个总充电功率必须区分“时间”和“空间”两个维度。再加上V2G车不只是负载还可以当储能反向放电问题就从“什么时候充”变成了“什么时候充、什么时候放、在哪个站放”决策空间的复杂度成倍增长。我总结下来大规模EV时空调度主要有四个难点用户异构性强每辆车的电池、出行需求、充电习惯都不一样直接逐辆建模会带来大量变量。电网空间约束复杂不同充电站接入的配电网节点其变压器容量、线路潮流、电压水平各不相同。时间耦合严重车辆在不同时段充放电会形成跨时段的SOC动态约束一个时段的决策会影响后续时段的可充放电能力。用户意愿不可忽略你算出的最优方案如果让车主多花钱他转头就去别处充电了调度方案根本不落地。这四点凑在一起单层集中优化很难处理因为单层模型天然假定“所有用户服从统一指令”。但现实里车主是独立决策的所以我们需要把博弈关系显式建模进来这就是双层优化进入视野的原因。1.2 双层优化与单层优化的本质区别单层优化把所有EV看作一个集合假设它们服从统一指令直接求解“系统最优”适合车队统一调度或者完全可控的场景。而双层优化描述的是一个主从递阶结构上层运营商/电网先宣布电价或调度计划下层用户/聚合商在给定上层策略下按自身利益最大化或成本最小化做响应上层再根据响应结果调整策略。这不是把两个目标加权在一起的多目标优化而是两个层级之间有顺序、有主次的博弈问题。我经常用一个类比来解释单层优化像单位排班领导直接指定每个人几点上班员工只能接受双层优化更像打车平台调价平台先定一个价格司机再决定接不接单平台根据全平台接单率调整价格。电动汽车调度显然更接近后者因为调度中心不能强制车主在某时某地充放电只能通过价格信号和充电功率来引导。对电动汽车充放电问题来说双层模型的意义还体现在两层一是保护用户隐私运营商不需要知道每辆车的具体出行习惯只需要看到群体性响应结果二是更接近市场化机制用户有选择权系统方案执行起来才有现实基础。当然代价也明显——模型从单层变成双层之后求解难度会大幅上升不再是“一次优化出结果”而是要反复迭代或者做KKT转换这也是后面要重点讲的部分。1.3 时空两个维度如何在一个模型里互相嵌套搞明白双层结构之后再看“时空调度”四个字。时间维度很直观一天被划分成24个时段甚至96个时段每辆车的SOC逐步更新充放电功率在每个时段内保持或变化。空间维度则要把配电网拆成多个节点每个节点对应一个或多个充电站线路容量、变压器容量、节点电压都成为约束条件。但时空两个维度不是独立存在的它们是通过功率变量和SOC状态变量耦合在一起的。举例来说某辆车下午3点在A站快充了20度电那么晚上7点它就有能力在B站放电这就让“充电的时间和地点”影响了“放电的时间和地点”。反过来如果某一个时段A站因为变压器容量限制不能充太多用户可能改去B站或者改到凌晨再充这又改变了整体负荷的时间分布和空间分布。我举一个典型场景晚高峰时A站正好在重载支路上电价很高B站在轻载支路上电价较低。用户为了省钱愿意去B站但去B站可能要绕路所以下层模型里需要加入“出行距离/等待时间”的惩罚成本。上层在制定各站电价和充电功率时已经把用户改站、改时的响应行为算进去了这种预测-响应的闭环就是时空调度的核心。如果只分别建一个“时间优化模型”和一个“空间优化模型”两套结果根本对不上调度方案也没法执行。2. 数学模型搭建从目标函数到约束条件2.1 上层模型运营商/电网层的时空调度上层模型站在运营商或电网调度中心视角目标一般包含经济性和安全性两部分。经济性希望购电成本最小、放电反向馈网收益最大安全性希望配电网等效负荷曲线峰谷差小、节点电压不越限。实际项目里我习惯把两者写成加权和方便调节。典型目标函数长这样min F_up sum_t [ c_buy(t) * Pg(t) ] - sum_t [ c_feed(t) * Pdis_total(t) ] lambda * sum_t [ (L_grid(t) - L_avg)^2 ]其中Pg(t)是向上级电网购电的功率Pdis_total(t)是所有站点放电功率之和L_grid(t)是根节点处的净负荷也就是基础负荷加EV净负荷L_avg是L_grid在24小时内的平均值。第一项是购电成本第二项是放电收益第三项是削峰填谷的惩罚项lambda是权重系数。上层约束主要包括节点功率平衡约束对于每个站点对应的配电网节点注入功率等于该站充电功率减去放电功率再加上基础负荷和与相邻节点交换的功率。线路容量约束每条支路的潮流不能越限我用的是线性DistFlow模型不考虑无功优化这个精度对时空调度策略足够。变压器/主变容量约束上级变压器输入功率不能超过上限。站点充放电功率上下限由充电站配变容量、充电桩数量共同决定。与下层耦合的等式/不等式约束上层制定的各站充电功率必须能由下层用户实际响应支撑起来。这里有个容易忽略的点上层如果直接决定“各站充电功率”其实隐含了“用户一定会照做”的假设这就又退化成单层了。所以在双层模型里上层通常只决定价格信号或者“建议功率”然后通过迭代把下层响应反馈回来形成闭环。我后面在代码里会具体展示。2.2 下层模型用户群的充放电响应下层模型描述用户怎么响应上层的价格或调度指令。如果只考虑经济账用户会希望在电价最低的时段充电、在电价最高的时段放电但现实约束是“第二天还要用车”所以SOC必须保持在一个合理区间内。单辆车的下层优化模型可以写成min F_low sum_t [ pi_j(t) * Pch_i(t) - pi_sell_j(t) * Pdis_i(t) gamma_i * C_loss_i(t) ]约束条件包括SOC动态约束SOC_i(t1) SOC_i(t) Pch_i(t)*eta_ch/4 - Pdis_i(t)/eta_dis/4这里除以4是因为我们采用15分钟为一个时段或者按实际时间粒度调整。SOC上下限约束保证用户随时能开车出门最低SOC不能低于SOC_min_i。充电功率上下限0 Pch_i(t) Pch_max_i。放电功率上下限0 Pdis_i(t) Pdis_max_i。不能同时充放电约束Pch_i(t) * Pdis_i(t) 0。这个模型单独看很好理解但真正到大规模式会出问题如果500辆车、24个时段每辆车有48个充放电变量加24个SOC变量总变量数接近4万个约束数量也很大。虽然Matlab能硬算但在外层还要反复迭代速度会很难看。所以我在实际代码里做了“聚合响应”处理把用户按接入时段、离开时段、电池容量分成若干个用户群对每个群定义总充电功率和总放电功率用平均SOC做近似。这样变量数量大幅下降迭代速度能提升一个数量级而且得到的调度方案在统计意义上依然合理。这个方法对做大规模研究来说几乎是必须的不然你只能在500辆车的小算例上打转。2.3 双层协调机制与求解路线对比双层模型建好之后最大的问题是“怎么求”。目前业界常用三种路线迭代式求解上层先给定电价或功率下层求解得到用户响应上层根据响应更新策略反复迭代直到收敛。实现最简单适合快速跑通流程但收敛速度要看参数设置处理不好容易震荡。KKT条件转换当下层是连续线性规划且没有整数变量时可以写出下层的KKT最优性条件把它作为上层模型的约束把双层问题变成单层带互补约束的数学规划问题再用大M法线性化交给商业求解器。这种方法牺牲一部分建模简洁性但一次求解能拿到均衡解不需要迭代。启发式嵌套外层用遗传算法或粒子群搜索上层决策内层用线性规划求解下层响应。灵活性高能处理非凸、含整数的复杂问题但计算量最大一般用来做几百辆车的仿真分析。我把三种路线整理成一个对比表方便选型求解路线实现难度计算速度适用场景Matlab常用工具迭代式低中大规模、快速原型YalmipGurobi或直接linprogKKT转换高快单次求解小到中等规模、追求精确均衡YalmipGurobi大M法处理互补项启发式嵌套中慢非凸、含整数、敏感性分析GA工具箱 linprog本项目主体我推荐用迭代式先把模型跑通然后把KKT转换作为进阶优化方向。这样既能快速得到结果又能对“最优解在哪”有一个直观认识不会一上来就被KKT推导劝退。3. Matlab代码实现从0到1跑通双层循环3.1 环境配置与测试数据生成代码环境我建议使用Matlab R2022a及以上版本安装Optimization Toolbox。建模语言用Yalmip求解器用Gurobi或Cplex没有商业求解器的情况下也可以用内置的linprog/intlinprog先顶着但大规模双层迭代下速度差距非常明显。Yalmip在GitHub上可以下载Gurobi有学术授权申请之后配置起来很简单。测试数据不需要一上来就用真实数据集自己生成反而更容易控制场景。下面是一段生成测试数据的示例rng(42); Nt 24; % 时段数1小时一个时段 Ns 3; % 充电站数量 Nv 600; % 电动汽车数量 % 每辆车参数 C_bat 60; % 电池容量 kWh Pch_max 7; % 最大充电功率 kW Pdis_max 5; % 最大放电功率 kW SOC_init 0.2 0.4 * rand(Nv,1); % 初始SOC 20%~60% arrive randi([7, 20], Nv, 1); % 接入时段 leave min(arrive randi([1, 8], Nv, 1), 24); % 离开时段 % 三个站点所在节点的基础负荷kW t 0:Nt-1; P_base [1500 900*sin(t*pi/12).^2; 1200 800*sin((t2)*pi/12).^2; 1800 1000*sin((t1)*pi/12).^2];为什么要用rng(42)因为双层迭代对初始条件很敏感固定随机种子以后每个人跑出来的结果都一致方便你对照复现。arrive和leave是最关键的数据它们决定了每辆车什么时候能充放电直接影响时空调度结果。3.2 代码结构设计与变量定义代码不要全部塞在一个脚本里否则后面扩展条件一多根本跑不下去。我推荐下面这个文件结构main.m主程序负责参数设置、初始化、调用迭代求解、输出结果。data_generate.m生成测试数据包括EV参数、基础负荷、分时电价。upper_model.m构建并求解上层优化模型输入下层上一次响应输出各站充电/放电功率和电价。lower_model.m构建并求解下层用户响应输入上层电价/功率输出实际充放电需求。solve_iterative.m执行迭代主循环处理阻尼、收敛判断。plot_results.m绘制负荷曲线、SOC曲线、功率分配图。变量定义上核心的几个变量是Pch sdpvar(Ns, Nt, full); % 各站点各时段充电功率 Pdis sdpvar(Ns, Nt, full); % 各站点各时段放电功率 Pg sdpvar(1, Nt, full); % 根节点购电功率 pi_charge sdpvar(Ns, Nt, full); % 各站点充电价格 Q_user zeros(Ns, Nt); % 下层返回的用户响应功率注意sdpvar是Yalmip声明变量的方式第三个参数字符串full表示这是一个完整矩阵变量不是对称或对角矩阵。上下层之间通过pi_charge和Q_user这两个变量耦合迭代时它们不断更新。3.3 上下层模型核心代码解析上层模型的核心约束和目标是这样的Constraints []; % 根节点功率平衡基础负荷 站点净充电功率 购电功率 Constraints [Constraints, sum(Pch - Pdis, 1) P_base_root Pg]; % 站点充电功率上下限 Constraints [Constraints, 0 Pch Pch_ub]; Constraints [Constraints, 0 Pdis Pdis_ub]; % 与下层响应的耦合约束 Constraints [Constraints, sum(Pch, 2) Q_user_prev(:, 1)]; % 目标购电成本 - 放电收益 削峰填谷惩罚 Objective sum(c_buy .* Pg) - c_sell * sum(Pdis(:)) ... lambda * sum((Pg - mean(Pg)).^2); ops sdpsettings(solver, gurobi, verbose, 0); optimize(Constraints, Objective, ops);这里Q_user_prev是上一次迭代中下层返回给上层的实际充电需求。上层在做计划时不能拍脑袋决定至少要满足用户已经被激励出来的需求。这种“先预测响应再制定计划”的思路是迭代法的主心骨。下层模型如果采用弹性响应模型代码会非常简单适合快速迭代alpha 0.5; % 电量对价格的敏感系数 Q_user Q_base - alpha * (pi_charge - pi_ref); Q_user min(max(Q_user, Q_min), Q_max);Q_base是用户不受价格影响时的基准充电需求pi_ref是基准电价。这个模型虽然简单但能很好描述“电价越高、充电需求下降”的基本行为而且不需要求解器迭代效率非常高。如果你希望做得更精细可以对每个用户群构建一个LP问题用linprog求解目标函数和SOC约束都可以按2.2节里的公式写。3.4 迭代收敛处理与性能优化迭代式双层优化最常见的坑就是两侧来回震荡。我实际跑的时候发现不加任何平滑处理电价会在峰谷之间反复横跳永远停不下来。解决办法是给迭代加一个阻尼系数rho 0.3; % 阻尼系数一般取0.2~0.5 pi_new pi_old rho * (pi_calc - pi_old); Q_smooth Q_smooth rho * (Q_user - Q_smooth);收敛条件用相对变化量来判断if norm(pi_new - pi_old, fro) / norm(pi_old, fro) 1e-3 break; end有两类问题特别容易让迭代失效。一是下层含整数变量比如“只能充或只能放”的0-1状态迭代会出现抖动解决办法是先松弛成连续变量跑通之后再对结果做可行性修正。二是目标函数里符号写反放电收益写成了“加”而不是“减”导致用户无限放电系统永远收敛不了。这种错误非常隐蔽排查时一定要先看目标函数每一项的生理意义。性能优化方面最大的经验是不要在迭代循环里反复重新建模。Yalmip建模虽然方便但每次构建sdpvar和约束都会产生额外开销。如果问题规模固定可以提前把模型建好在迭代中只更新参数或者用assign给旧模型赋初值省掉重复解析的时间。另外能用矩阵运算的地方就不要用for循环Matlab的矩阵乘法效率远高于循环这一点在500辆车以上的算例中尤其明显。4. 算例验证与结果解读4.1 算例参数设置为了验证双层时空调度策略的效果我设置了一个可复现的小规模算例。基本参数如下表参数数值时段数24充电站数量3电动汽车数量500电池容量60 kWh慢充最大功率7 kWV2G放电最大功率5 kW充电效率0.95放电效率0.90峰时电价1.2 元/kWh平时电价0.8 元/kWh谷时电价0.4 元/kWh放电补贴1.0 元/kWh基础负荷曲线三个节点各取典型日负荷EV接入时间、离开时间、初始SOC按照上一节代码里的方式随机生成rng(42)固定种子后用同一批数据跑所有场景这样对比才有说服力。需要强调的是调度策略对EV接入时间分布非常敏感晚高峰前接入的车辆越多削峰空间越大V2G效果也越明显。4.2 核心结果与分析我把三种场景放在一起对比无序充电、单层有序充电、双层优化充放电。无序充电最简单即插即充单层有序充电忽略用户响应直接做系统最优双层优化则按照本文的Stackelberg博弈模型求解。结果整理成表场景负荷峰谷差(kW)运营商日购电成本(元)用户平均日费用(元)最大电压偏差无序充电8501230031.24.8%单层有序充电6201080026.83.2%双层优化充放电5301010023.52.6%这里有个现象值得多说。双层优化场景中部分车辆在19点到22点的高峰时段放电把电量反向送回配电网尤其是靠近重载节点的充电站V2G放电能直接缓解变压器压力同时大量充电负荷被引导到凌晨低谷时段填谷效果非常理想。用户平均日费用下降是因为放电补贴收入抵消了大部分充电成本但要注意模型里如果没有电池损耗惩罚用户会被激励过度放电实际落地时一定要加入损耗系数否则结果偏乐观。从技术指标看双层优化的峰谷差比无序充电下降了37.6%比单层有序充电也下降了14.5%。这说明“用户响应”不仅仅是一个让结果更真实的附加条件它本身就能带来额外的削峰潜力因为用户会依据价格信号主动调整行为。4.3 算法对比与规模扩展实测我还用同一套数据对比了三种求解路线迭代式、KKT单层化、遗传算法嵌套LP。结果如下求解方式500辆车计算时间收敛/求解次数目标值相对偏差迭代式约2分钟20~40次迭代比KKT高2%KKT单层化约5分钟单次基准值遗传嵌套LP约18分钟外层50代比KKT高4%500辆车时迭代式优势是速度快、模型改动小KKT单层化得到的均衡解更精确但建模复杂度高扩展到2000辆车时容易内存爆炸遗传嵌套LP最灵活能处理复杂非线性约束但计算时间太慢不适合在线调度。我在扩展到2000辆车时
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进