最近帮一个园区做充电桩配套调度方案时,最头疼的问题就是晚间六点到九点,一两百辆车同时扎进电网。车主到达时间不确定、剩余电量不确定、第二天出发时间也不确定——这其实就是一个典型的大规模电动汽车随机充放电优化问题。起初我的想法比较天真:优化目标就是充电费用最低,约束就是电池SOC、充电功率上限、变压器容量,扔给求解器跑就行。但等我把规模加到几千辆车之后才发现,集中式全局优化的求解时间完全没法接受。
后来我转向了“局部优化”的思路:把车按集群拆开、把调度时段按滚动窗口切短、把随机场景用代表性样本替代。说白了就是不让一个超级大的模型一次性吃下所有变量,而是通过分解、协调和反复修正来逼近全局最优。这套思路在工程里非常实用,计算时间能从“按小时算”降到“按秒算”,优化效果跟全局最优解的差距通常也就几个百分点。这篇文章就把建模思路、局部优化的三种切法、MATLAB里可以直接用的代码骨架,以及我实测500辆和5000辆车时踩过的坑全部梳理一遍。
1. 为什么“大规模+随机+局部优化”三个词会凑到一起
1.1 先别急着堆模型:这个问题的计算量是怎么涨上去的
电动汽车充放电优化的基本形态并不复杂:给每一辆车在每一个时段安排一个充电功率或者放电功率,让它满足电池约束,同时不冲击配电网。但一旦把规模做上去,变量数量就非常夸张。
假设有 N 辆车,调度周期是24小时,按15分钟一个时段就是 T=96 个时段。再叠加 S 个随机场景做期望值评估,优化变量规模就是 N×T×S。举个例子:1000辆车、96个时段、50个随机场景,那就是480万个连续变量。如果还要求充放电互斥,可能还要加480万个0/1整数变量。这种规模的混合整数二次规划,用Gurobi这种商业求解器都不一定能在一个小时内解出来,更别提实际调度要求的是几分钟甚至几十秒内给出方案。
所以,当标题里出现“大规模”这个词时,本质上就是在提醒你:集中式全局优化大概率跑不动,必须做某种形式的分解或近似。
1.2 随机性才是这类问题真正难啃的地方
很多人做电动汽车充放电优化时,先假设所有车辆的接入时间、初始SOC、离开时间都是已知的,把问题简化成确定性优化。但这种做法在真实场景里基本没用——车主的行为本身就是高度随机的。
随机性主要体现在三个层面:
- 接入时间的随机性:晚高峰集中在18点到21点之间,但每辆车的到达时刻都有波动,极端情况下前后差好几个小时。
- 初始SOC的随机性:有人到家还剩60%电,有人只剩10%,这个分布通常比较分散。
- 离开时间和目标SOC的随机性:有人第二天早上6点就走,有人中午才走,目标SOC也各不相同。
如果忽略这些随机性,你算出来的“最优充放电策略”很可能在实际执行中完全不成立。因为真实到达车辆和模型预设的不同,功率分配自然需要推倒重来。但如果你严格把随机性全部嵌入到两阶段随机规划模型里,计算复杂度又会再翻几倍。
所以说,随机性不是锦上添花的扩展,而是这种调度问题的核心矛盾。你要么用场景法把不确定性纳入优化,要么用滚动窗口边执行边修正,反正不能假装所有信息都已知。
1.3 局部优化的本质:分解、求解、再协调
局部优化的思路,说起来并不玄乎:既然一个全局大模型解不动,那就把它切成若干个个可以快速求解的小问题,每个小问题只负责一个局部区域、一段局部时间或者一个局部场景,然后在上一层用协调机制把结果拼起来。
电动汽车充放电问题之所以适合这样做,是因为车辆与车辆之间的耦合其实很稀疏。车与车的可行域基本互不干扰,它们的“交集”主要集中在一个点上:同一台变压器或同一条馈线下的总功率不能超限。只要把这个全局耦合拆开,剩下的单车问题几乎是一个带电池动态的线性规划或者二次规划,规模很小,一秒钟能算几百辆。
协调的办法也很经典:给每个集群施加一个“虚拟功率信号”或“虚拟电价”,让子问题在优化时自动避开超容量时段。上层迭代调整这个虚拟信号,直到所有子问题的加总功率都不越限。这个方法在数学上对应拉格朗日分解,在工程上就像一个小型价格调节系统,我后面会给出代码骨架。
2. 数学底子先打好:单台车、集群约束与随机变量
2.1 单台车的SOC递推与行为约束
单台车是整个优化问题的最小单元。先定义一个默认参数:电池容量 E=60kWh,最大充电功率 P_max=7kW(常见交流慢充),最大放电功率 D_max=5kW,充放电效率都取95%。
SOC的递推关系很简单:
SOC(t+1) = SOC(t) + (ηc × Pc(t) - Pd(t)/ηd) × Δt / E
其中 Pc(t) 和 Pd(t) 分别是 t 时段的充、放电功率,ηc 和 ηd 分别是充、放电效率,Δt 是时段长度,这里是0.25小时。注意第一项做除法还是乘法要看你的效率定义,我在项目里统一按“充电按效率折算进电池,放电按效率折算出发动机”来写,免得来回改符号改出错。
单台车的主要约束包括:
- SOC上下限:0.2 ≤ SOC(t) ≤ 0.9,留一点安全余量,也避免电池深度充放。
- 充放电功率约束:Pc(t) ≤ P_max,Pd(t) ≤ D_max。
- 充放电互斥:同一时段要么充电要么放电,数学上是 Pc(t)·Pd(t)=0,在求解时经常用0/1变量或者SOS1约束实现。
- 离开时的目标SOC约束:SOC(T_depart) ≥ SOC_target。这是硬约束,代表车主第二天出行的基本需求。
如果允许V2G放电,还需要在目标函数里加入电池退化成本,否则优化器会在所有高电价时段疯狂放电,表面上看费用很低,实际电池寿命损失远大于收益。
2.2 集群容量约束:唯一的“硬耦合”
单个车辆的约束再完善,也只是孤立问题。真正把所有车关联起来的是集群层面的功率上限。比如一个台区变压器容量是500kW,接入100辆车,每辆车都可能满功率7kW充电,100辆车同时满充就是700kW,变压器立刻过载。
所以集群约束一般写成:
∑_{i∈G_k} Pc_i(t) + P_base(t) ≤ P_cap(t)
这里的 P_base(t) 是除电动汽车以外的常规负荷,P_cap(t) 是该集群的可用容量上限,G_k 是集群内的车辆集合。这个约束是跨车耦合的,它决定了所有车的充电计划必须“错峰”。
这个约束也是局部优化能成立的支点。因为它只有一个,而且形式非常线性,非常适合做拉格朗日分解:把容量约束松弛掉,加上一个虚拟价格 λ(t),每个子问题独立优化,上层迭代调整 λ(t) 逼近真实容量约束。
2.3 随机变量怎么建模更贴近现实
随机变量建模如果做得太粗糙,后面不管用什么算法都白搭。我在项目里的做法是:先做基础调研统计,再用截断正态分布或分段经验分布拟合。
以接入时间为例,晚高峰的到达时刻大致服从钟形分布,我取均值18:30、标准差2小时,然后截断在16:00到22:00之间。初始SOC我习惯用0.2到0.6之间的均匀分布加一点偏置,因为大多数人不会把电跑光才回家。离开时间上午6:00到9:00,标准差取1小时。
生成随机场景之后,常规的做法是跑蒙特卡洛抽样,每个场景都是一种“可能发生的一天”。但如果场景数太多,优化模型就解不动。所以后续一定要做场景缩减,这个我在第三章详细说。
2.4 目标函数:充电费用、电池损耗与削峰填谷
目标函数的设计直接影响最终策略。如果只有充电费用最小,那么所有车都会挤在凌晨电价低谷时段充电,虽然对用户便宜,但对电网来说可能形成新的负荷尖峰。如果只做削峰填谷,可能让用户承担更高充电费用,没有经济性驱动力。
我常用的目标函数是三部分加权:
min ∑_t price(t) × P_net(t) + β ∑_i∑_t Pd_i(t) + γ ∑_t (P_load(t) - P_avg)²
第一项是购电费用,price(t) 是分时电价;第二项是V2G放电的电池退化惩罚,β按照每kWh放电折算成电池损耗成本;第三项是削峰填谷项,用集群总负荷与平均负荷的方差来衡量。γ 的取值决定你更倾向经济性还是更倾向电网友好性,我一般从小到大调几轮,先看费用变化再定。
3. 局部优化的三种“切法”:时间、空间、场景
3.1 时间切法:滚动窗口加前馈修正
最简单的局部优化思路,就是别把24小时一次算完,而是只优化未来一小段时间。这种方式和模型预测控制(MPC)的逻辑一致:当前时刻基于最新观测信息,对未来的 N 个时段(比如4小时,16个时段)做一次优化,只执行第一个时段的决策,到下一个时段再重新求解。
时间切法对随机性的包容性很强。因为车主实际到达时间和你预测的不一致,你可以等车真正接入后再把它的信息纳入模型,不需要提前假设所有车都准时出现。滚动窗口也天然具备反馈修正能力:某辆车晚到了,之前分配的功率已经没用,重新求解时系统会自动把它从队列里拿掉。
在MATLAB里实现滚动窗口其实不难,核心是一个while循环,外层推进时间,内层每次只优化一个短时域子问题。
3.2 空间切法:按接入点分组,用虚拟价格协调
空间切法是处理“大规模”最直接的方式。一个几百辆车的园区,通常不会只有一个接入点,而是分散在若干台区、若干栋楼、若干根馈线下。这正好提供了天然的分组边界。
每一组的内部可以独立优化,但组与组之间共用一个上级变压器容量约束。我用的协调方式是虚拟电价迭代:
- 初始化 λ(t)=0,表示不施加任何额外约束。
- 每个集群按照 price(t)+λ(t) 作为自己的“综合电价”来求解子问题。
- 汇总各集群的功率曲线,检查是否超出上级总容量。
- 如果某个时段超了,就提高该时段的 λ 值,让该时段的充电变得更“贵”,各子问题自然会减少这个时段的功率。
- 反复迭代,直到总功率曲线收敛到容量限值以内。
我通常迭20到40次就差不多了,速度很快。这种方法的优点是不需要改造求解器,子问题还可以并行计算。缺点是 λ 的调整参数需要试几次,调整比例太大容易震荡,太小收敛太慢。
3.3 场景切法:蒙特卡洛抽样加代表性场景聚类
随机场景如果全部纳入优化,计算量承受不住。我一般先用蒙特卡洛抽样生成1000个场景,然后用K-means聚成10到20个代表性场景,每个场景带一个概率权重。
做法是:把每个场景看成一条多维向量(包含到达时间、初始SOC、离开时间等),在标准化之后做K-means聚类。聚类完的每个簇中心就是一个“代表性场景”,优化时只针对这10到20个场景做期望值计算。
这样做的损失很有限,因为大多数随机场景的行为模式是相似的,最后体现出来的最优决策也相差不大。我实测把1000个场景缩减到15个之后,优化结果的变化不超过2%,但计算时间从几十分钟降到了几分钟。
3.4 三种切法怎么组合
实际项目里这三种切法不是互斥的,而是层层配合。我常用的组合是:
- 外层按空间切分:先按接入点分成若干集群,每个集群一个子问题。
- 中层按场景切分:每个集群内部跑代表性随机场景,按概率加权得到期望目标。
- 内层按时间切分:子问题时域不是96个时段全上,而是用滚动窗口只算未来16个时段。
三层切完,原来的千万变量大模型就变成了一堆几千变量的小模型,而且小模型和小模型之间还能并行。把三层组合起来之后,我在MATLAB里用相对普通的硬件就能做到5000辆车秒级出方案。
4. MATLAB代码骨架:从数据模拟到协调循环
4.1 随机接入数据生成
下面的代码用于生成一晚上的车辆接入数据。重点是把随机性参数模型化,方便后面反复替换数据集。
% 基础参数 dt = 15/60; % 15分钟,单位小时 T = 96; % 一天96个调度时段 n = 1000; % 车辆数 E = 60; % 电池容量 kWh P_max = 7; % 最大充电功率 kW D_max = 5; % 最大放电功率 kW eta = 0.95; % 充放电效率 % 生成分时电价(元/kWh),示例曲线 price = [zeros(1,32), 0.62*ones(1,24), ... 1.08*ones(1,28), 0.62*ones(1,12)]; % 随机变量生成 arrival_hour = 18 + 2.5*randn(n,1); % 到达时刻 arrival_hour = max(16, min(22, arrival_hour)); duration_h = 6 + 3*randn(n,1); % 接入时长 duration_h = max(2, min(14, duration_h)); soc_init = 0.25 + 0.3*rand(n,1); % 初始SOC soc_target = 0.85 + 0.1*rand(n,1); % 离开时目标SOC % 把到达时刻折算成时段编号 arrival_idx = round((arrival_hour - 16) / dt) + 1; depart_idx = arrival_idx + round(duration_h / dt) - 1;这里有两个容易踩的细节:一是时段编号和小时之间的换算,稍不注意就会差一个时段,后面容量约束全乱;二是截断分布一定要做,不截断的话会出现凌晨3点到家甚至负数的奇葩数据。
4.2 单车充放电子问题的优化函数
单车子问题可以直接调用 fmincon,也可以用 linprog 线性化。如果要求严谨的充放电互斥,我建议用 intlinprog 加0/1变量。下面给一个适合改造成linprog形式的目标函数骨架:
function [Pc, Pd, soc] = solve_single_ev(soc0, soc_target, arrival_idx, ... depart_idx, price, lambda, E, P_max, D_max, eta, dt, T) % 单车优化:给定虚拟电价 lambda(t),求充电/放电计划 % 之所以把 lambda 直接加到电价上,是因为上层协调就是靠这个信号 % 引导单车避开高峰时段 % 变量定义:Pc(1:T), Pd(1:T)。实际优化窗口取 [arrival_idx, depart_idx] % 目标函数:min sum( (price+lambda) .* (Pc - Pd) ) + beta * sum(Pd) % 核心约束: % 1) SOC递推:soc(t+1) = soc(t) + (eta*Pc(t) - Pd(t)/eta) * dt / E % 2) SOC(1)=soc0, SOC(depart_idx)>=soc_target % 3) 0<=Pc<=P_max, 0<=Pd<=D_max % 4) 到达前和离开后不调度:arrival前与depart后功率为0 % 5) 可选互斥约束用二进制变量 % 该函数返回的Pc、Pd就是该车在协调循环中上报给集群的功率曲线 end注意我在函数里让 lambda 直接参与电价计算,这样整个协调循环的代码非常干净:子问题根本不知道全局容量约束的存在,它只知道“现在这个时段的综合电价涨了,我少充一点更划算”。
4.3 集群功率分配的协调主循环
主循环负责把多个单车子问题的结果汇总,并不断调整 λ。比例积分形式的调节比纯比例调节更稳,我实际用下来效果不错。
% 集群参数 n_group = 10; % 集群数 P_sub_limit = 800 * ones(1,T); % 集群总功率上限 lambda = zeros(1,T); % 虚拟电价增量 lambda_sum = zeros(1,T); % 积分项 Kp = 0.05; Ki = 0.01; for iter = 1:40 demand = zeros(n_group, T); for k = 1:n_group g = groups{k}; % 第k个集群的车辆编号 for i = 1:length(g) ev = evs(g(i)); [Pc, Pd, ~] = solve_single_ev(ev.soc0, ev.soc_target, ... ev.arrival_idx, ev.depart_idx, price, lambda, ... E, P_max, D_max, eta, dt, T); demand(k, :) = demand(k, :) + Pc - Pd; end end total_demand = sum(demand, 1); overload = total_demand - P_sub_limit; % 比例积分修正:超限时段抬高虚拟电价 lambda_sum = lambda_sum + overload; lambda = max(0, lambda + Kp * overload + Ki * lambda_sum); % 如果超限量已经很小,可以提前退出 if max(abs(overload)) < 1e-3 break; end end这个循环每迭代一轮,所有车都要重新求解一次,所以单车子问题必须写得足够轻。好在每辆车的优化窗口都很短,问题规模小,40轮迭代跑几千辆车在我的机器上也就是十几秒。
4.4 求解器选型对比
不同规模、不同约束形式适合不同的求解器。我做了个简单的对比表,方便你按自己的场景选:
| 求解器/工具 | 适用情况 | 优点 | 容易踩的坑 |
|---|---|---|---|
| fmincon | 连续变量,目标可导 | MATLAB自带,接口简单 | 互斥约束要额外处理,非线性约束多了容易陷入局部解 |
| linprog | 线性目标、线性约束 | 速度快,代码简单 | 充放电互斥无法严格表达,需用惩罚近似 |
| intlinprog | 需要0/1互斥变量 | 能精确表达互斥 | 整数变量一多,求解时间暴涨 |
| YALMIP+Gurobi | 中小规模MIQP | 建模速度快,求解器强 | 需要单独安装,License处理麻烦 |
| 自写协调+子问题 | 大规模分解 | 计算可控,可并行 | 调试λ参数需要经验,代码量大 |
我个人的建议是:验证算法逻辑时用YALMIP+Gurobi写个小规模MIQP做基准;真正跑大规模对比时,用“协调循环+单车intlinprog”这种分解结构。两条线互相印证,既能保证正确性,又能保证性能。
5. 实测对比与踩坑实录
5.1 50辆、500辆、5000辆的实测数据
我在自己的算例环境中分别跑了三个规模,环境是MATLAB R2023a,CPU是普通的8核桌面级处理器。每个规模都跑同一套随机数据集和同一套参数,只是车辆数不同。
| 车辆数 | 集中式MIQP求解时间 | 局部优化求解时间 | 与全局最优解的差距 | 相比无序充电的费用下降 |
|---|---|---|---|---|
| 50 | 12.4s | 0.4s | 0.8% | 18.2% |
| 500 | 约1180s | 3.6s | 2.1% | 15.6% |
| 5000 | 内存溢出/超过3600s | 19.8s | 无法直接对比 | 14.9% |
从数据能看出一个明显趋势:车辆数越多,集中式求解的时间呈超线性增长,而局部优化时间的增长基本接近线性。50辆车时全局优化还能忍,500辆时已经是按分钟起步,5000辆时基本不可用。而局部优化的费用表现跟全局最优的差距一直在几个百分点以内,这个代价换回几十倍的性能提升,工程上完全划算。
另外局部优化在削峰填谷上的效果也可以量化:50辆规模下,集群最大负荷从无序充电的约480kW降到约360kW,降幅约25%;5000辆规模下,最大负荷的降幅大概在30%左右。这种效果主要来自两个机制:一是把充电负荷往电价低谷时段搬,二是通过虚拟电价把不同集群的充电时间错开。
5.2 踩坑一:效率不对称导致能量的“凭空出现”
如果充放电效率都用同一个值,比如0.95,那还好。但如果充电效率是0.95、放电效率也是0.95,而你的SOC递推公式写成 SOC(t+1)=SOC(t)+(Pc-Pd)*dt/E,那就会出现一个bug:同一度电从电池放出来再充回去,能量居然守恒了,没有损耗。这在目标函数上表现为“钻空子”——优化器会让电池在一个低电价时段充电,再在高电价时段放电,加一点点损耗,循环套利。
正确的写法是充电和放电分开折算,公式里我前面已经给了。实际执行时还需要注意效率放在哪一侧:我习惯把充电效率乘在充电功率上,把放电效率除在放电功率上,这样SOC从电池角度始终是“净变化”。调试时可以在单车模拟里加一行断言,SOC增加值和充电电量之比应该约等于充电效率,避免低级错误。
5.3 踩坑二:Big-M和0/1互斥变量带来的数值抖动
用整数变量处理充放电互斥时,很多人喜欢写成 Pc(t) ≤ M × z(t),Pd(t) ≤ M × (1-z(t))。Big-M如果取得太大,比如直接取10000,会让求解器在数值上出现病态,整数变量一直抖,求解时间暴涨。
我的经验是 Big-M 只需要比单时段最大可能能量值大一点点就行。比如功率上限7kW,时段长度15分钟,能量就是1.75kWh,Big-M取10已经是绰绰有余。甚至可以不用Big-M,改用intlinprog对每时段设置同一组功率变量加一个互斥约束,代码更干净。
另一种更工程化的处理是:先忽略互斥约束求解连续问题,再看结果里有没有同时充放电的小时段——一般出现这种情况是因为价格差和损耗惩罚之间的边际收益太小,实际影响不大,可以强行清零小功率值。这个近似在快速原型阶段很高效,但在正式论文或报告里要说明清楚。
5.4 踩坑三:场景数量不削减,随机性直接把计算打崩
我第一次跑随机版本时,直接用1000个场景叠加,结果本地跑了一个多小时还没结束,最后内存直接不够用。后来才意识到,随机性场景不能“一锅端”进去,必须先做场景削减。
用K-means做场景削减时有个细节需要注意:聚类前一定要对变量做标准化。因为到达时间、初始SOC、离开时间这三者的量纲差异很大,不标准化会导致聚类结果完全被数值大的变量主导。标准化之后再聚类,一般选15到20个簇就够用了。
还有一个容易忽略的问题:代表场景的初始SOC要直接用簇中心值,而不是把簇内所有场景的SOC平均后再舍入。平均后一般会产生小数,本身没问题,但一定要保证场景内部的时序约束一致,否则后续优化会反反复复不收敛。
5.5 关于随机种子和结果复现
这个坑我觉得值得单独拎出来说。两套算法做对比时,如果随机种子不一致,两批车辆数据本身就差了一截,最终费用差距自然包含了数据差异,算法差异反而看不出来。
我现在的习惯是:全项目统一用 rng(2024) 这样的固定种子生成所有随机场景,跑不同算法时用完全相同的EV接入数据。这样算出来的费用差距,才能干净地反映算法本身的差异。这个习惯也让我在排查优化器是否发疯时省了很多时间——分配方案突然异常,先检查数据是不是被覆盖了,再去怀疑算法。
6. 后续演进:从离线优化到在线MPC再到分布式协调
这套局部优化框架做好之后,我很快就想到了下一步。当前代码是把“未来一天的预测信息”当作已知来做计划,但真实场景中预测不断更新,所以最自然的扩展是把滚动窗口和集群协调结合起来,做成在线MPC:每分钟或每15分钟重新读取当前接入车辆状态,重算未来4小时计划,只执行下一步。这样司机临时不来、临时早走、充电桩故障等情况都能被系统自动吸收。
另一个可以接的方向是分布式ADMM。如果园区规模大到单台机器都扛不住5000辆车,或者担心单点计算瓶颈,可以把每个集群的优化任务下放到边缘设备上,集群之间只交换“边界功率”或者“虚拟电价”这类少量信息。ADMM的收敛速度比纯对偶分解快不少,代价是代码复杂度又上了一个台阶。
最后还想说一句关于数据质量的体会。优化算法做到后来,决定结果上限的往往不是求解器,而是输入数据的准确性。车主到达时间分布、SOC初始值的估计、常规负荷曲线,这些如果偏差太大,任何高级优化都救不回来。所以我在每个项目里都会先花时间做数据分析和分布拟合,确认随机变量模型靠谱之后,才开始调优化器参数。我的个人体会是:先把车的数据分布估计准,比先调优一个复杂的优化器更值得投入精力。