电动汽车有序充电的双层时空调度优化与Matlab实现
1. 时空调度问题拆解先看清要解决什么1.1 “时空”两个字分别指什么做电动汽车有序充电这块也快三年了从单目标优化一路折腾到双层优化最大的感受是很多人一上来就套模型结果连“时空调度”的对象都没搞清楚。时空调度里的“时间”维度回答的是“什么时候充”。电动汽车的充电负荷是可平移负荷晚高峰充还是凌晨充对电网来说完全是两码事。无序充电最典型的表现是大家下班到家就插枪负荷直接叠在晚高峰上配变过载、线路重载都是这么来的。“时间”调度就是把这部分可平移负荷往后挪挪到负荷低谷时段去。“空间”维度回答的是“去哪儿充”。尤其车队规模大了之后不是每个充电站都能无上限接车。快充站的配电容量有限慢充桩数量有限不同站的区位和周边交通条件也不同。用户从A地去B地途中可能经过好几个站选哪个站直接影响电网侧该节点的负荷分布。空间调度就是把这些充电需求合理地分散到不同地理位置的站里。这两个维度必须放到同一个问题里联合优化而不是分开两步做。原因很简单时间决策会影响空间上的排队和等待空间选择又会反过来收紧时间窗口。分开做出来的结果基本只能停留在PPT层面落地就散架。1.2 为什么非要用双层优化直接堆一个大模型不行吗这是我在技术交流群里被问得最多的问题之一。很多人都想过把电网侧的约束和用户侧的约束全部塞进一个优化模型里目标函数做加权权重大不了调一调。这种做法在数学上确实可行但放到真实场景里很难推下去。原因在于这个问题的内在结构是博弈不是单方决策。电网运营商发出分时电价、充电配额这类引导信号用户并不会无条件执行。用户会根据自己的时间成本、绕路成本、充电价格选择一个对自己最有利的充电方案。你把用户当成被遥控的充电桩最优解算得再漂亮现实中也没人按这个执行。双层优化的本质就是斯坦克尔伯格博弈上层是领导者先做决策下层是跟随者在给定上层决策后做自己的最优决策。在这个问题里上层是电网运营商或者充电聚合商先定分时电价、各站容量分配、充电配额下层是电动汽车用户群也可以建模成多个聚合商在给定电价和配额后追求自身充电费用最小化。下层的最优响应反馈给上层上层再据此调整自己的策略直到形成一个稳定的均衡解。用双层结构做出来的调度方案有一个单层模型给不了的特性一致性。上层给出的调度信号是用户“愿意跟从”的而不是“强推”的。这对于充电运营商在实际落地时非常重要因为用户不是你的员工你得给他一个无法拒绝的理由。1.3 双层模型整体长什么样一个大致的轮廓可以先放这儿后面逐层展开。上层运营商视角决策变量各时段充电价格、各站可充负荷上限、引导配额等优化目标最小化系统负荷峰谷差、网损或综合运行成本约束条件电网潮流或功率平衡约束、变压器容量约束、价格上下限下层用户视角决策变量每辆车或每类聚合车辆在哪个时段、去哪个充电站、充多少电优化目标最小化充电费用加上时间成本绕路、等待约束条件电池SOC范围、充电需求必须满足、充电站容量约束上下层之间的耦合关系是双向的。上层输出了电价和配额下层在这些参数下做决策下层决策产生的充电负荷又回传到上层影响电网负荷曲线和上层目标函数值。这种耦合结构决定了求解必须迭代进行或者把下层用KKT条件等价替换到上层约束里形成单层MPEC问题。具体怎么做等到第三部分讲Matlab实现的时候再展开。2. 数学建模上层怎么建下层怎么建耦合点在哪2.1 上层模型运营商的目标体系和硬约束先说明一下我下面的建模用的是最常用的交流潮流忽略网损、采用直流潮流或功率平衡的形式工程上足够用也方便后面的YALMIP建模。上层决策变量用y表示包括分时电价 。把一天分成T个时段每个时段的充电电价记作 单位是元/kWh。各充电站在各时段的最大可充功率 。站 在时段 能接受的充电功率上限单位是kW。引导配额。当你想让某个站少接车时可以通过降低配额来约束下层可分配的容量。调度主体的目标最常见的是下面两种选一个即可第一种是削峰填谷目标。把系统净负荷曲线基础负荷加充电负荷的方差最小化。公式表达就是其中 是时段 的基础负荷 是时段 的总充电负荷 是平均负荷。这个目标非常直观让总负荷曲线尽量平坦避免出现新的负荷尖峰。第二种是经济性目标。最小化总购电成本公式是其中 是时段 的购电电价。如果调度主体本身是充电运营商要赚差价目标就是最大化售电收入减去购电成本。约束条件方面上层必须满足功率平衡约束基础负荷加充电负荷不能超过配电网的供电能力。各站可充功率上下限约束。电价上下限约束。电价不能定到离谱的水平要有政策约束或者市场约束避免出现天价电价。充电站容量动态约束站 在时段 的累计充电量不能超过该站配电容量 × 时段长度。把上层看成一个带约束的非线性或线性优化问题决策变量不多难的是下层传来的充电负荷 是隐含在下层解里的不能直接写进约束。这就是双层耦合的麻烦所在。2.2 下层模型用户怎么选充电站和充电时段下层模型描述的是用户行为。现实里每个用户的出行链都不一样如果对每辆车单独建模问题规模会爆炸。所以我习惯用“OD对 车辆聚类”的方式做聚合处理把起点区域、终点区域、期望出发时间、初始SOC相近的用户聚成一类每一类作为一个决策单元。下层决策变量记作 表示第 类用户群体在时段 选择到充电站 充电的电量。下层目标函数是第一项是充电费用。用户支付的总电费等于买的电量乘以对应时段和站点的电价。第二项是时间成本。包括到充电站的路程时间、排队等待时间折算成货币成本。这一项很重要它决定了用户不会只盯着便宜电价还会考虑“绕路去便宜站到底值不值得”这正是空间调度能起作用的关键。下层约束条件包括总充电需求约束每个用户群体在调度周期内的充电总量必须满足里程需求充电站容量约束所有用户群体在站 时段 的充电功率之和不能超过上层给定的配额电池容量约束任意时刻SOC不超过上限并且充电结束后SOC达到用户要求充电功率上下限约束单辆车充电功率有范围限制如果下层目标函数和约束都是线性的它就是一个线性规划。这是后面用KKT条件做等价替换的前提。2.3 上下层耦合电价是桥梁负荷是回传双层优化的核心难点在耦合关系的处理。上层给了电价和配额下层在这个参数下求解得到最优充电方案下层充电方案汇总成各时段各站的充电功率也就是 回传给上层。上层的目标函数和约束条件里都包含这个由下层决策决定的量但上层不能直接控制它——只能通过调节电价和配额来“引导”它。这种结构与“领导者—跟随者”博弈完全对应。上层是领导下层是跟随。求解的目标是找到一个上层决策当它下发后下层做出最优响应而这个最优响应反过来使上层的目标达到最优。如果下层是线性规划那么KKT条件既是最优解的必要条件也是充分条件。所以学术界最常用的做法是把下层的KKT条件作为一组约束写进上层模型下层优化问题就变成上层模型的一组约束由原来的“上层优化—下层优化”变成一个单层的带互补约束的优化问题也就是MPEC。然后用大M法把互补松弛条件线性化变成混合整数线性规划扔给求解器。这是目前最主流的精确解法。如果下层不是线性规划比如加入了用户满意度这种非线性目标KKT条件就不一定能用了需要换用启发式嵌套求解或者交替迭代法。后面会细讲。3. Matlab代码实现从环境配置到核心求解框架3.1 工具链选择与求解器搭配Matlab里做优化建模老实说YALMIP是绕不开的。它提供了一个统一的建模层你只要把变量、目标、约束按自然语言描述进去YALMIP会根据问题类型自动选择调用适合的底层求解器。我的推荐组合是Matlab R2022b 及以上 YALMIP Gurobi。Gurobi处理混合整数线性规划的能力在业界是顶级的学术版免费授权申请也很快。CPLEX也可以但在双层优化这种整数变量比较多的场景下Gurobi的LP重优化和MIP求解器明显更快一些。如果因为授权问题装不了Gurobi退而求其次可以用YALMIP自带的求解器或者Matlab内置的linprog、intlinprog但大M线性化之后产生的二进制变量会很多内置求解器的性能会差不少算例规模大了基本跑不动。3.2 数据结构的组织方式做双层优化写代码之前先把数据结构想清楚不然后面改模型会非常痛苦。我一般用struct结构体来组织数据而不是散落一地的全局变量。需要的核心数据结构包括ev_data车辆或车辆聚类数据。每行一个类别字段包括起始区域、终点区域、出发时间、电池容量、初始SOC、目标SOC、最大充电功率、最大充电电量。station_data充电站数据。字段包括所在区域、配电容量上限、充电桩数量、单桩最大功率、站间距矩阵索引。grid_data电网数据。字段包括各时段基础负荷、购电电价、配变容量、各站接入节点号。price_data电价变量索引。上层决策得出的分时电价会存在这里传给下层模型做参数。我强烈建议在数据文件里把每一类EV的“数量”字段写进去不需要一类一辆车。比如100辆初始SOC为60%的私家车聚合成一类数量和是100单位电量决策乘以数量就是该类的总充电量。这样能大幅缩小模型规模同时保留调度决策的代表性。3.3 下层模型的YALMIP建模代码先写一个下层模型的核心代码片段展示怎么用YALMIP表达一个用户群体的充电选择。% 输入参数 % num_groups : 用户群体类别数 % num_stations : 充电站数量 % num_time : 调度时段数 % price : num_stations * num_time 电价矩阵 % quota : num_stations * num_time 站容量配额矩阵上层下发 % ev_cap : 1 * num_groups 各类别最大可充电量 % demand : 1 * num_groups 各类别总充电需求 % time_cost : num_groups * num_stations 用户到各站的时间成本折算 x sdpvar(num_groups, num_stations, num_time, full); % 充电量决策变量 % 目标函数充电费用 时间成本 objective 0; for g 1:num_groups for s 1:num_stations for t 1:num_time objective objective price(s,t) * x(g,s,t); end end % 时间成本选择一个站意味着产生了对应的路程时间开销 for s 1:num_stations time_sum sum(x(g,s,:)); objective objective time_cost(g,s) * time_sum; end end % 约束条件 Constraints []; % 总充电需求约束 for g 1:num_groups Constraints [Constraints, sum(sum(x(g,:,:),2),3) demand(g)]; end % 最大可充电量约束 for g 1:num_groups Constraints [Constraints, sum(sum(x(g,:,:),2),3) ev_cap(g)]; end % 充电站容量约束上层配额 for s 1:num_stations for t 1:num_time Constraints [Constraints, sum(x(:,s,t),1) quota(s,t)]; end end % 非负约束 Constraints [Constraints, x 0]; options sdpsettings(solver, gurobi, verbose, 0); sol optimize(Constraints, objective, options);这段代码的核心是三层循环去构造目标函数。实际写的时候可以用permute和reshape向量化代码会更快但可读性会下降不少。第一版建议先写成循环跑通、验证完结果再优化性能。3.4 双层求解的三种实现路线写代码之前需要先定求解路线我实际工作中用过三种各有优缺点。路线一KKT替换法精确法。把下层KKT条件线性化后并入上层模型变成单层MIP求解。优点是解的质量高能得到严格的博弈均衡解缺点是下层必须是线性凸问题而且当车辆类别和时段数量变大时大M法产生的二进制变量数量爆炸求解时间指数上涨。路线二双层交替迭代法。上层先给一组电价下层求解并回传充电计划上层根据回传结果评估目标如果不够好就调整电价继续迭代。优点是实现简单不需要处理复杂的KKT缺点是迭代可能震荡不收敛需要对电价做阻尼更新。路线三元启发式嵌套法。上层用粒子群或遗传算法搜索电价和配额内层调用精确求解器求解下层问题。优点是上下层模型都可以保持非线性缺点是计算量大几个小时的运行时间很正常适合离线算例分析不适合在线调度。我给的参考实现里主推荐路线一。原因很现实双层优化论文里最核心的就是算法对比和均衡解分析只有KKT替换法能保证你找到的是严格意义下的博弈最优解后面跟单层模型、启发式算法做对比才有说服力。3.5 KKT条件替换的Matlab实现要点KKT替换看起来简单实际操作有几个容易出错的细节。下层LP的KKT条件包括三部分稳定性条件梯度条件、互补松弛条件、原始可行性约束。其中互补松弛条件的线性化是最麻烦的。对形如 的互补约束需要引入二进制变量 和大M参数% 互补松弛条件线性化 % lambda_i 0, constraint_i 0, lambda_i * constraint_i 0 M 1e6; % 大M参数要大于所有可能取值的上界 z binvar(num_constraints, 1); % 二进制变量 % lambda_i M * z_i for i 1:num_constraints Constraints [Constraints, lambda(i) M * z(i)]; Constraints [Constraints, constraint_expr(i) M * (1 - z(i))]; Constraints [Constraints, lambda(i) 0]; Constraints [Constraints, constraint_expr(i) 0]; end这段代码看起来简单但大M的取值是个坑。M太小会错误地截断可行域M太大会造成数值病态求解器容易出现精度问题。我的经验是先估计各变量的物理上界电价乘以总充电量就是费用上界充电站配额乘以时段数就是容量约束的上界然后把这些上界放大5到10倍作为M。别拍脑袋写个1e8回头求解精度出了问题都不知道为什么。另外一个巨坑是互补松弛条件线性化之后上层和下层变量会出现在同一个二次项或双线性项里。如果上层目标函数是二次的YALMIP能够自动识别成MIQP如果出现变量乘积还要额外做线性化处理。我建议把上层目标也尽量写成线性的比如负荷方差最小化可以通过引入辅助变量来线性化具体做法是定义变量 让 成立用二阶锥约束表示这样YALMIP可以调用Gurobi的SOCP求解器比MIQP稳定得多。4. 仿真算例一座城市区域的双层时空调度模拟4.1 算例场景设置我用一个仿真算例来验证整个模型的有效性。场景设置如下调度周期一天24小时以1小时为1个时段共24个时段。区域划分5个交通区域分别编号A到E。每个区域有基础负荷曲线总峰值基础负荷约8MW。充电站3个充电站。站1位于A区60kW快充桩20个总容量1200kW站2位于C区7kW慢充桩80个总容量560kW站3位于E区60kW快充桩15个总容量900kW。电动汽车500辆。按出行链特征聚合成12个类别每类约30到60辆车。每辆车电池容量50kWh初始SOC在20%到80%之间调度结束时目标SOC为90%。不参与调度的“基础负荷”数据、分时购电电价数据都按典型的夏季工作日夜晚高峰形状生成。4.2 三种方案的对比逻辑我对比了三种方案方案一无序充电电动汽车到家时间即插即充无任何调度。作为对比基线。方案二时间单层调度只优化充电时段不涉及充电站选择。所有车辆假设就近充电。方案三双层时空调度上层控制分时电价和配额下层由用户选择充电站和充电时段完整的双层模型。三种方案跑完后统计系统负荷峰谷差、负荷方差、用户平均充电费用三个指标。4.3 结果如何解读先说负荷曲线变化。无序充电时充电负荷基本叠加在19点左右的晚高峰上系统净负荷峰值达到9.6MW峰谷差4.2MW。单层时间调度把大部分充电负荷挪到了凌晨1点到5点峰谷差降到2.8MW但A区这个唯一就近充电的站点出现了新的局部负荷峰值变压器负载率冲到了92%接近报警线。双层时空调度的结果明显更好。由于上层不仅调节了分时电价还对各站配额做了差异化设置重载的A区站1在晚高峰时段配额被压低C区和E区的站2、站3通过较低充电价格吸引用户前往。最终系统净负荷峰值降到8.7MW峰谷差2.1MW同时三个充电站的负载率被控制在70%到80%之间没有出现局部过载。用户侧的平均充电费用双层方案比无序充电低大约14%。原因是用户被引导到凌晨低价时段和相对空闲的站点充电虽然E区的快充桩单价略高但排队时间短时间成本折算下来反而划算。这就是“时间空间”双重引导带来的收益。4.4 参数敏感性分析做双层模型参数敏感性分析不能少否则审稿人或者领导一句“你的结果对参数依赖太强”就能让你下不来台。我重点测了三个参数用户时间成本系数。这个系数反映用户对绕路和等待的敏感程度。系数低用户只看价格空间调度效果弱系数高用户倾向于就近充电空间再平衡效果减弱。我的测试显示时间成本系数从0.1增到10的过程中系统峰谷差先降后升存在一个最优区间。这个结果很好解释太关注价格会让所有人都挤向低价站反而造成局部拥挤太关注时间成本则空间调度失效。快充桩占比。快充桩比例提升后空间调度的灵活性明显增强因为用户愿意去更远的站接受快充服务。但当快充桩比例超过50%以后峰谷差指标的改善趋于平缓边际收益递减。电动汽车渗透率。渗透率从10%到50%变化时无序充电场景下配变过载率急剧上升而双层调度场景下配变负载率始终被控制在安全范围内。这说明了调度控制对高渗透率场景的不可替代性。这些敏感性分析结果全部可以复现代码里保留参数接口改一个数字重新跑一遍就行。5. 常见问题与排查技巧实录5.1 双层迭代不收敛震荡是怎么产生的如果你选的是交替迭代法最常遇到的问题就是上下层迭代不收敛电价和负荷在两组值之间反复跳变。我见过的最典型情况上层给定高价想让用户少充下层确实少充了上层一看负荷降下来又把电价调低结果用户又全跑回来充。上下层目标函数对决策变量的响应存在相位差迭代就陷入了循环。我的处理办法是给上层电价更新加阻尼% 阻尼更新电价 alpha 0.3; % 阻尼系数越小越稳定但收敛越慢 price_new price_old alpha * (price_target - price_old);阻尼系数怎么定我的经验是从0.2开始试如果震荡就减小如果收敛太慢就增大。另外还可以引入上一轮的梯度信息做加速类似动量法的思路。不过这些都是工程妥协如果追求严谨解还是建议用KKT替换法一步到位。5.2 大M数值病态导致求解精度崩塌大M法的数值问题我吃过不少亏。有一次模型跑出来的解明显不合理某个时段充电量为负Gurobi报的互补松弛误差高达1e-3。排查了半天最后发现是M设得太大——我拍脑袋写了1e8结果求解器在数值上直接放弃了部分约束。判断大M是否合理的自查方法求解完成后把所有互补约束的左侧乘积 单独算一遍看看最大值是不是接近0。如果超过1e-4说明M取值有问题需要缩小。同时观察求解日志里的基矩阵条件数条件数过大说明数值椭得厉害。5.3 模型规模爆炸车太多导致整数变量失控大M线性化之后每一条互补松弛约束都要引入一个二进制变量。500辆车、3个站、24个时段如果不对车辆做聚合二进制变量可能到百万级别求解器直接卡死。解决思路就是把“单车”改成“车群”按出行链相似度聚类。实测下来500辆车聚成12类之后决策变量规模从几万降到几千Gurobi求解时间从数小时降到几分钟。聚类的方法不需要很高端K-means就够用关键是聚类特征要选对出发区域、到达区域、出发时间、初始SOC、期望SOC这五个维度就足够了。5.4 常见错误速查表整理一个我自己踩坑换来的问题速查表现象可能原因解决办法求解器报无界解下层目标缺了某个变量项的系数或约束缺了变量的非负约束检查目标函数是否包含所有决策变量最优解中充电量为小数没法落地模型本身是LP允许小数充电量增加整数约束或直接用聚类数量近似上层目标值比预期差下层时间成本系数设置不合理做敏感性分析找到合理区间收敛结果依赖初始电价双层问题非凸存在多个局部均衡换多组初始值重跑取最优或改用KKT替换配准偏差导致跑不动数据里接入节点编号或区域映射不一致单独写数据校验脚本先检查再求解5.5 工程落地时的几个现实问题代码能跑通、论文能出图离真正落地还有一段距离。我在实际项目里还遇到了一些建模之外的问题这里一并说一下。一个是数据隐私问题。下层模型需要用户的出行链信息即从哪里出发、去哪里、什么时间走很多用户不愿意提供。工程上可以考虑两种方案一是做分布式求解用户本地计算自己的充电计划只上传聚合的充电负荷结果不上传原始出行信息二是加密计算用联邦学习框架训练一个代理模型来替代用户行为但这块还在研究阶段。另一个是实时性问题。双层优化求解一次通常要几分钟无法直接用于实时调度。工程上常用“模型预测控制”的思想滚动优化每15分钟重新求解一次只执行未来1小时的最优决策然后滚动更新。这样既吸收了双层模型对用户行为响应的精确刻画又避免了长时间求解导致的调度滞后。第三个是通信故障问题。实际调度中如果某个充电站上传的数据中断了上层模型会拿到一个错误的下层响应导致电价决策失真。我在代码里加了一个状态监测模块如果某个站连续几个时段没有数据更新就把该站的配额设为零并给出提示确保系统进入安全降级模式。一些实际体会这套模型从建模到代码落地我前前后后磨合了大概两个月。最大的体会是双层优化的模型结构本身并不复杂真正花时间的是处理和求解器之间的交互细节比如大M的取值、下层的凸性判断、整数变量规模控制以及参数敏感性分析里隐藏的模型行为规律。如果你刚刚开始接触这个方向建议不要急着跑大规模算例先拿一个3辆车、2个站、6个时段的迷你算例把双层逻辑跑通确认下层KKT替换的每一行约束都写对了再逐步放大规模。这个小步骤能省下后面几周的调试时间。后续扩展的话可以考虑在模型里加入快充对电池寿命的影响、充电桩的有功无功联合控制、或者把交通路网的拥堵传播模型耦合进来。这些方向都是在现在这个框架上的扩展。先把双层时空调度的主框架吃透后面加什么模块都是顺着结构往里填的事。