
去年我处理一批小区配电网负荷数据时发现晚高峰的变压器报警频率明显上来了。查下去很大一块增量来自电动汽车的集中充电住户下班回家插上枪充电负荷和家庭用电顶峰叠在一起配变很难吃得消。那段时间“有序充电”和“V2G”在行业群里被频繁讨论。V2GVehicle-to-Grid的思路是把电动汽车当成一块移动储能电池在负荷高峰或者电网需要的时候让车反过来给电网送电实现削峰填谷。听起来很聪明但真正要写优化模型时你会发现有两个时间参数绕不过去——电动汽车的停留时间以及它的充电时间。停留时间决定了这辆车在什么时候具备被调度的能力充电时间则决定了车主开走之前电池能不能补到目标电量。这篇文章想把我跑通的一个Matlab项目完整拆开把这两个时间因素建模进V2G调度问题用粒子群算法求解给出可复现的代码思路和调参经验。适合正在做电动汽车有序充电、微网能量管理、智能算法应用相关课题的同学参考。1. 为什么V2G调度绕不开停留时间和充电时间这两道坎很多刚接触V2G调度的同学一开始会把问题简化成“所有车24小时都在线”然后直接套优化算法。这种方案在论文里跑起来很漂亮但放到实际场景里基本没法用。原因就出在两个时间维度上。1.1 停留时间是调度资源的时间边界V2G调度本质上是在“车连着电网”的时间窗口内做能量分配。车停在充电桩上的时间段才是可以被优化的资源窗口。举个例子某辆车傍晚18:00回到小区插上充电枪第二天早上7:00开走可调度窗口是13个小时不是24小时。如果建模时不考虑停留时间粒子群算法很可能“安排”这辆车在上午10点向电网放电而那个时候车早就开走了方案完全不成立。更要命的是不同车辆的停留时间差异非常大。小区里的车晚上停10到13小时写字楼楼下的车白天停5到8小时商场充电桩的车可能只停2到3小时。这些车的可调度时段互相错开如果统一用一个固定的“在线时段”去描述就会导致一部分车的调度能力被高估一部分被低估。我处理这类问题时会先把每辆车的到达时段和离开时段单独存成一个数组再生成一个“可用调度矩阵”只有在这个矩阵为真的时隙里算法才允许给车辆分配充放电功率。1.2 充电时间是用户出行的能量底线充电时间这里要说得更细一点。它不单指“充满一次电要多久”而是指调度结束时这辆车的电池SOC能不能达到车主离开时的目标电量。如果车主开走时电量比来的时候还低那V2G对他来说就是不折不扣的负体验方案再优化也无法落地。在建模时我习惯先根据每辆车的初始SOC、目标SOC和电池容量计算它在这个停留窗口内至少需要充入多少能量所需充电能量 目标SOC - 初始SOC× 电池容量必要充电时间 所需充电能量 /最大充电功率 × 充电效率比如说一辆车电池容量60kWh初始SOC有20%目标SOC要充到90%那它至少需要补进0.7 × 60 42kWh。按7kW交流慢充、0.92的充电效率计算必要充电时间大约是42 /7 × 0.92≈ 6.5小时。如果这辆车只停5小时那这个调度问题本身就无解要么降低目标SOC要么提高充电功率要么换快充桩。这就是“充电时间”对V2G调度可行性的硬约束。1.3 两者共同决定可调度裕度把停留时间和充电时间放在一起看就可以得到一个很有用的指标——单辆车的可调度裕度。定义是停留窗口内去除必要充电时间后剩余还能用于响应V2G指令的时间长度用能量表达就是可调度能量裕度 ≈停留时间 - 必要充电时间× 最大充放电功率充电时间越接近停留时间这辆车的V2G响应能力就越弱反过来停车时间长、电池容量大、充电需求小的车辆就是天然的优质调度资源。我在做数据预处理时会先算一遍每辆车的这个裕度把裕度过低甚至为负的车辆从参与调度的集合里剔除掉这样既能保证方案可行也能减少粒子群搜索的维度。2. 调度模型的数学化目标函数、决策变量和约束怎么写V2G调度问题本质上是一个带约束的非线性优化问题。要把粒子群算法用上去首先得把模型写到数学层面把目标函数、决策变量、约束条件都明确下来。2.1 目标函数选型我为什么用负荷方差V2G调度的优化目标可以有多种取向运营商收益最大、用户充电成本最低、负荷削峰填谷效果最好等等。我在这个项目中做的是小区配电网场景最关心的是负荷曲线平稳性和配变过载风险所以我选的目标函数是全天负荷方差最小化写成表达式就是min f_obj (1/T) · Σ (P_total_t - P_mean)²其中P_total_t是第t个时段的配电台区总有功负荷P_mean是全天平均负荷。负荷方差小了意味着曲线更平稳峰谷差自然被压缩变压器过载概率也更低。有人可能会问为什么不直接用峰谷差作为目标函数峰谷差确实直观但它只关注两个极值点对整条曲线的形态不敏感。比如某天最高峰和最低谷刚好出现在两个没有车辆参与调度的时段那调度算法的努力都白费了。负荷方差是对全时段能量分配的全局评价粒子群在搜索时能感受到每个粒子位置的细微变化收敛过程更顺滑。当然如果实际业务考核指标就是峰谷差那也可以把目标函数换成峰谷差代码改动不大。2.2 决策变量的数学约定有符号功率决策变量定义成什么呢就是每辆车在每个时段内的充放电功率。我习惯用有符号功率来统一表示正数表示充电负数表示放电取值范围是[-P_max_i, P_max_i]。这样做的直接好处是不会出现同一辆车同一时段既充电又放电的冗余解粒子群的搜索空间也能少浪费一块。有了功率变量电池SOC的递推关系就要写出来。这里有个很容易踩的坑——充放电效率不能合并成一个效率来算。正确写法是这样SOC_i(t1) SOC_i(t) [η_c · max(P_i,t, 0) min(P_i,t, 0) / η_d] · Δt / E_i写成代码就是分段判断。P为正的时候按充电效率η_c折算进电池P为负的时候电池放出的能量要除以放电效率η_d实际SOC消耗比“功率乘时间”算出来的更多。这个细节不处理的话仿真结果会偏乐观。2.3 约束条件和示例参数设置这个模型的约束条件我整理成以下几组每一组都有它的物理含义停留窗口外功率必须为0P_i,t 0当t不在[T_arrive_i, T_leave_i]范围内功率边界-P_max_i ≤ P_i,t ≤ P_max_i电池SOC边界SOC_min ≤ SOC_i(t) ≤ SOC_max通常取0.1到0.9离开时SOC目标约束SOC_i(T_leave_i) ≥ SOC_target_i配变容量约束可选基础负荷 充电负荷 - V2G放电负荷 ≤ 变压器额定容量。为了跑出一个具体的仿真案例我在项目里设置了20辆车、一天96个时段每15分钟一个。部分车辆参数做个示例说明车辆编号到达时段离开时段初始SOC目标SOC电池容量(kWh)最大功率(kW)1傍晚时段次日清晨0.250.906072午间时段傍晚时段0.400.807573傍晚时段次日清晨0.150.855074上午时段下午时段0.300.804075傍晚时段次日清晨0.200.90807时段编号直接映射到一天中的实际时间跨天的车辆离开时段编号累加即可不要用取模的方式把第二天清晨映射回当天不然会覆盖掉别的时段数据。这里是最容易出现隐性bug的地方。3. 粒子群算法求解V2G问题的关键设计编码、修复与罚函数模型有了接下来就是算法层面的设计。粒子群算法本身不复杂但要让它在V2G调度问题上发挥稳定编码方式和约束处理比公式记忆更重要。3.1 标准粒子群的核心更新公式粒子群算法的基本思想是模拟鸟群觅食时个体经验和群体信息共享的行为。每个粒子代表问题的一个候选解粒子在搜索空间中飞行速度更新依赖三个因素当前速度的惯性、个体历史最优位置pbest和群体历史最优位置gbest。标准更新公式是v w · v c1 · r1 · (pbest - x) c2 · r2 · (gbest - x)x x v这里w是惯性权重c1和c2是学习因子r1和r2是[0,1]之间的均匀随机数。惯性权重w的取值对收敛影响很大。我用的是线性递减策略从0.9递减到0.4。前期w大粒子飞行速度快全局搜索能力强不容易落入局部最优后期w小粒子在当前最优解周围精细搜索收敛更稳定。这个策略在V2G调度这种高维约束问题上的表现比固定权重要好。3.2 位置编码把调度方案压缩成一维向量如果直接把所有车辆、所有时段的功率拼成一个N×T矩阵当粒子那么即使只有20辆车、96个时段粒子维度也是20×961920维而且里面还有大量不可调度的位置。粒子群在这种高维空间里搜索效率很低很多维度等于在“空转”。我的做法是先把调度方案压成一维向量。具体分两步走第一步建立可用调度索引。遍历所有车辆把每辆车在停留窗口内的时段索引收集起来放到一个总向量里。比如第i辆车停留时段是[73, 74, 75, ..., 112]那么这40个时段就是该车的可用调度位置。第二步粒子维度D就等于所有车可用调度时段的个数之和。粒子的每个位置代表某辆车在某个可用时段的充放电功率。在计算适应度时再把这个一维向量映射回N×T的完整功率矩阵不可调度位置全填0。这样做的好处很明显。粒子维度从1920降到可能只有几百维搜索空间大大缩小收敛速度和稳定性都明显提升。很多文献里直接用全矩阵编码跑出来的结果不是说不可以但计算代价确实更高。3.3 约束修复先裁剪后惩罚粒子群更新位置和速度之后产生的新解不一定满足约束。我的处理方式是“能裁剪的裁剪该惩罚的惩罚”把约束分成两类第一类是硬约束直接修复。功率超出[-P_max, P_max]的直接裁剪到边界停留窗口外的位置强制置为0。这些约束是物理上不可能违反的不能靠罚函数去“软化”。第二类约束比如SOC全程不越界、离开时SOC达到目标值用罚函数来处理。因为SOC是功率变量在一段时间内积分的结果前面时段的功率会影响后面的SOC简单裁剪反而容易破坏粒子结构。罚函数的形式是F f_obj λ1 · Σ max(0, SOC_target_i - SOC_leave_i) λ2 · Σ max(0, SOC_min - SOC_all(i,t)) λ3 · Σ max(0, SOC_all(i,t) - SOC_max)几个λ系数需要按目标函数的量级来调。我通常先跑一次不带罚项的试验看目标函数大概在什么量级然后让罚项量级比目标函数高1到2个数量级。如果罚系数设得太小粒子群会无视约束设得太大目标函数的作用会被淹没算法只顾着把罚项降下来削峰效果反而不好。4. Matlab代码实现从数据准备到结果输出代码部分我按一个可运行的完整流程来讲从参数设置、适应度函数、PSO主循环到绘图。下面这段代码是核心逻辑的提取。4.1 数据准备与可用调度索引构建%% 基础参数 T 96; % 一天96个时段每15分钟 N 20; % 电动汽车数量 dt 0.25; % 时段时长单位小时 P_max 7; % 最大充放电功率kW %% 生成基础负荷曲线示例双峰典型日负荷 base_load 500 120 * sin(2*pi*(0:T-1)/T pi/3) ... 180 * exp(-((0:T-1) - 70).^2 / 200) ... 150 * exp(-((0:T-1) - 30).^2 / 300); %% 定义车辆结构数组 veh struct(T_arrive, {}, T_leave, {}, ... SOC_init, {}, SOC_target, {}, E_bat, {}, P_max, {}); % 示例给第1辆车赋值 veh(1).T_arrive 73; % 约18:00到达 veh(1).T_leave 112; % 次日约4:00离开 veh(1).SOC_init 0.25; veh(1).SOC_target 0.90; veh(1).E_bat 60; veh(1).P_max 7; % ... 其余车辆按类似方式填充 %% 构建可用调度索引与掩码 avail false(N, T); for i 1:N avail(i, veh(i).T_arrive:veh(i).T_leave) true; end % 将所有可用位置串成一维索引 idx_list find(avail); mask zeros(numel(idx_list), 1); mask(:) 1; % 一维掩码后续用于强制不可调度位置为0这里有个容易被忽略的点车辆到达和离开时段必须在数据准备时先检查一遍确保离开时段大于到达时段否则填充avail矩阵时会出错。还有跨天车辆直接用累加时段编号不会和当天数据冲突。4.2 解码函数与适应度函数粒子在迭代过程中是一维向量但算负荷和SOC时需要完整的N×T矩阵所以写一个解码函数很关键。function P decode_particle(x, avail, veh, N, T) P zeros(N, T); col 1; for i 1:N idx find(avail(i,:)); if isempty(idx), continue; end n length(idx); P(i, idx) x(col:coln-1); col col n; end end适应度函数是核心里面包含目标函数、SOC递推和罚项计算function fitness v2g_fitness(x, base_load, veh, avail, N, T, dt, lambda) P decode_particle(x, avail, veh, N, T); SOC zeros(N, T1); for i 1:N SOC(i, 1) veh(i).SOC_init; end penalty 0; for t 1:T for i 1:N p P(i, t); if p 0 SOC(i, t1) SOC(i, t) p * veh(i).P_max / 7 * dt / veh(i).E_bat; % 简化效率处理 else SOC(i, t1) SOC(i, t) p / veh(i).P_max * 7 * dt / veh(i).E_bat; end % SOC上下限罚项 if SOC(i, t1) 0.1 penalty penalty (0.1 - SOC(i, t1)); elseif SOC(i, t1) 0.9 penalty penalty (SOC(i, t1) - 0.9); end end end % 离开时目标SOC罚项 for i 1:N leave_idx find(avail(i,:), 1, last); if SOC(i, leave_idx1) veh(i).SOC_target penalty penalty (veh(i).SOC_target - SOC(i, leave_idx1)); end end total_load base_load(:) sum(P, 1); f_obj mean((total_load - mean(total_load)).^2); fitness f_obj lambda * penalty; end上面这段代码为了阅读方便做了效率简化处理实际项目里我会把eta_c和eta_d分别写进每个车辆结构体在SOC递推时读取。还有SOC递推里的功率单位要和电池容量单位统一这一点靠代码注释提醒自己很容易被忽略。4.3 PSO主循环粒子群主循环的框架如下nPop 60; maxIter 200; c1 1.5; c2 1.5; vmax 2; % 速度上限按功率量级设定 w_min 0.4; w_max 0.9; D numel(idx_list); x 2 * P_max * rand(D, nPop) - P_max; % 初始化位置 v zeros(D, nPop); pbest x; pbest_fit inf(1, nPop); gbest x(:,1); gbest_fit inf; for iter 1:maxIter w w_max - (w_max - w_min) * iter / maxIter; for k 1:nPop % 粒子k的适应度 fit v2g_fitness(x(:,k), base_load, veh, avail, N, T, dt, 1e4); if fit pbest_fit(k) pbest(:,k) x(:,k); pbest_fit(k) fit; end if fit gbest_fit gbest x(:,k); gbest_fit fit; end end % 速度位置更新 for k 1:nPop r1 rand(D,1); r2 rand(D,1); v(:,k) w * v(:,k) c1 * r1 .* (pbest(:,k) - x(:,k)) ... c2 * r2 .* (gbest - x(:,k)); v(:,k) max(v(:,k), -vmax); v(:,k) min(v(:,k), vmax); x(:,k) x(:,k) v(:,k); % 功率边界裁剪 x(:,k) max(x(:,k), -P_max); x(:,k) min(x(:,k), P_max); % 不可调度位置强制为0通过掩码 x(:,k) x(:,k) .* mask; end best_record(iter) gbest_fit; end这段代码里有个值得注意的细节粒子初始化的范围如果太大粒子群前期会花很多时间在无效区域搜索如果太小容易过早收敛到某个局部区域。我用的是[-P_max, P_max]均匀随机初始化因为这是功率变量的物理边界没有超范围的浪费。另外速度上限vmax这里取了2意思是每次位置更新的步长不超过2kW。vmax太大粒子会“飞”过头太小收敛速度又太慢。这个值一般按变量范围的20%到30%去试然后看收敛曲线调。4.4 绘图与结果导出仿真结束后的可视化我通常会画三张图%% 绘制基础负荷/无序充电负荷/优化后负荷对比 figure; plot((1:T)*dt, base_load, k-, LineWidth, 1.2); hold on; plot((1:T)*dt, base_load sum(P_noctrl,1), r--, LineWidth, 1.2); plot((1:T)*dt, base_load sum(P_opt,1), b-, LineWidth, 1.5); xlabel(时间(h)); ylabel(功率(kW)); legend(基础负荷, 无序充电, PSO优化); grid on; %% 收敛曲线 figure; semilogy(1:maxIter, best_record, b-, LineWidth, 1.5); xlabel(迭代次数); ylabel(最优适应度); title(粒子群收敛曲线); grid on; %% 单辆车的SOC轨迹 figure; plot((1:T1)*dt, SOC_opt(1,:), b-, LineWidth, 1.2); xlabel(时间(h)); ylabel(SOC);画图前一定要加rng(2024)之类的随机种子否则每次运行结果不同调参会变得非常痛苦。这是我自己早期吃过大亏的地方后来凡是跑有随机性的算法第一行代码必定固定随机种子。5. 仿真结果复盘与粒子群参数调优心得代码能跑通之后很大一部分工作量在调参和结果分析上。这一节把我的实测经验和调参过程讲透。5.1 一次典型仿真结果我看到了什么在我设定的这个场景里20辆车、96时段、15分钟粒度如果不加任何调度策略也就是所有车辆到达后立刻以最大功率充电晚高峰时段的总负荷会进一步抬高峰谷差很明显。改用粒子群优化后充电负荷会主动避开基础负荷的高峰段部分车辆在晚间高峰时段短暂放电整体负荷曲线变得平缓。从数值上看典型的负荷方差降幅在25%到35%左右峰荷大致能压到无序充电方案的75%上下。当然这个数字只对当前场景有效换车辆数、换基础负荷曲线、换电池参数结果都会变。但它至少说明粒子群在这个问题上是有效的而且效果可感知。收敛曲线方面我观察到最优适应度通常在前30到60代快速下降100代以后逐步趋于平稳150代后基本没有明显变化。这说明200代对这个规模的问题是够用的。如果车辆数翻倍我一般会把迭代次数同步增加到400代。5.2 三组粒子群参数对比试验我把粒子群参数做过一组对比试验结果很有代表性种群规模迭代次数惯性权重策略收敛情况目标函数改善301000.9→0.4早熟易陷入局部最优改善不充分602000.9→0.4稳定收敛效果最好最佳1004000.9→0.4改善不明显耗时翻倍与60/200接近结论很明确种群规模不是越大越好。V2G调度问题的决策变量维度虽然几百维但变量之间物理约束很强粒子群在60个粒子左右就能找到比较稳定的解。再往上加计算时间线性增长目标函数的改善却很有限。惯性权重用线性递减策略的效果优于固定值尤其是当w固定在0.6时收敛曲线到后期有明显振荡改成0.9→0.4递减后振荡消失。5.3 停留时间长度对调度效果的灵敏度这个项目标题里点名了“停留时间”所以我也专门做了灵敏度分析。方法很简单保持其他参数不变只改变所有车辆的停留时间分布观察优化效果。平均停留时间可调度时段占比负荷方差改善幅度4小时约25%约8%8小时约45%约18%12小时约65%约30%趋势完全符合预期停留时间越长粒子群的优化空间越大负荷曲线改善越明显。这说明在实际项目中如果某个停车场的特点是“短时停留为主”那V2G调度的潜力天然有限而小区、办公园区这类长时间停留的场景才是V2G真正能发挥价值的地方。6. 从仿真到落地容易被忽略的问题和我的处理思路最后这部分不是模型本身而是我在把这类仿真代码往实际场景推进时积累的一些经验。很多坑不跑到实际部署阶段根本发现不了。6.1 时段粒度与计算量的权衡我上面用的是15分钟粒度一天96个时段这是目前V2G调度研究里比较常用的折中方案。如果把粒度细分到5分钟一天就是288个时段决策变量个数虽然没变还是取决于可用调度位置数但SOC递推和时间相关计算量会增加近3倍。而调度效果并不会因为粒度变细就有同等比例的提升因为车辆本身到达和离开的时间精度也到不了分钟级。如果做实时调度更实际的做法是滚动优化。每次只优化未来4小时到8小时把决策窗口缩短粒子维度降下来求解速度会快很多调度策略也能跟随实际情况不断刷新。6.2 停留时间预测不准怎么办在真实场景里车主的停留时间是一个随机变量预测值可能和实际值偏差很大。一个车主说好晚上停8小时结果凌晨2点有急事开走了。如果调度算法把他的车安排在后半夜放电那他一早开走时SOC大概率不够。我的处理思路是预留“保底充电保护期”。具体做法是在构建可用调度索引时不等同于实际停留窗口而是在离开时段之前预留30到60分钟这个保护期内只允许充电不允许放电。也就是说即便车辆提前离开它也有最短时间完成部分补电。此外在线调度场景下我还会设置一个最小SOC保护阈值比如在离开前2小时检查SOC是否接近目标如果不够就强制切换到纯充电模式。6.3 电池寿命与调度经济性的平衡V2G在技术上可行在工程上还有一个绕不开的现实问题——频繁充放电会加速动力电池循环寿命衰减。如果调度模型完全不管电池损耗算法会在可行域内“可着劲儿”放电因为从削峰角度看放电越多效果越好。但实际从车主角度这很不划算。我习惯在目标函数里加一项非常小的“放电代价”项比如min f f_obj μ · Σ max(0, -P_i,t) · Δtμ是一个很小的权重单位是“每放电1kWh折算的等效成本”。这个项的物理含义是让算法在削峰效果接近的情况下尽量少用深度放电而是通过转移充电时段来削峰。这样做的好处是结果会温和很多车主的接受度更高。μ的具体值要按实际电价和电池更换成本去标定我一般在仿真阶段取目标函数量级的1%左右起步再逐步调整。PSO在V2G调度这类中小规模优化问题上性价比其实很高不必一上来就上深度强化学习。核心还是把问题边界约束看清楚编码设计和罚函数设置到位。如果你用这套思路去扩展自己的项目可以尝试把目标函数换成多目标版本比如同时优化负荷方差和用户充电成本用加权和或者Pareto前沿的方式处理。调试中如果遇到不收敛的情况优先检查两个地方罚函数系数量级和粒子初始化范围这两个是最好用的排错起点。