电动汽车充电桩集中“抢电”的场景我见过不止一次。某园区配电容量原本按办公负荷设计,自从地下车库装了十几台快充桩之后,傍晚下班时段办公负荷刚降下去,充电负荷又拉满,两台变压器一度逼近满载。电网侧看这个负荷曲线,白天一个尖峰、傍晚又一个尖峰,峰谷差被进一步拉大。后来给充电桩加了一套基于削峰填谷的电动汽车多目标优化调度策略,把充电任务往负荷低谷平移,实测下来负荷方差明显降了,车主的平均充电费用也比原来低。这篇文章把整套策略的来龙去脉、数学模型、Matlab代码框架和调参经验整理出来,适合正在做电动汽车有序充电、虚拟电厂负荷优化、配电网规划方向的同学参考。
1. 一块充电桩到底给电网添了多少麻烦
1.1 充电负荷的三个“坏脾气”
电动汽车充电负荷和传统居民负荷最大的不同,是它有三个很明显的“坏脾气”:随机性强、集中度高、和原负荷的峰值时刻经常重叠。
随机性强表现在用户接入时间和充电需求完全不可控——同一台充电桩,上午可能空着,中午来了两辆车抢,傍晚又是另一个高峰。集中度高体现在充电功率本身不是个小数目:一台普通快充桩在额定功率下跑两小时,顶得上一栋小居民楼半个小时的用电量,如果几十台桩同时启动,功率增长率非常可观。更麻烦的是,这三个特点往往叠加在电网原本已经吃紧的时段——居民小区傍晚的负荷本来就高,车主们下班回家顺手插上充电枪,充电曲线和居民用电曲线同步抬升,形成“峰上加峰”。
这三个特点单独看都还好,组合在一起就是调度难题:没有预判、没有灵活调节能力的无序充电,会让电网的备用容量需求和变压器损耗同步上升。这也是为什么业内越来越强调“有序充电”——不是不让充,而是分时、分区、分功率地把充电负荷排布到更合理的时间段里。
1.2 峰谷差是电网运营的核心痛点
电网运行里有个非常朴素的指标叫峰谷差,也就是一天当中最高负荷和最低负荷之间的差值。峰谷差越大,说明负荷曲线越“不平滑”,电网需要按照峰值来配置变压器容量、输配电线路和保护设备,但低谷时段这些设备大部分时间是闲置的,综合利用率并不高。
打个比方就容易理解了:一座食堂的建设规模是按午餐人流量来定的,但一天只忙一顿饭,其余时间大量餐位空着,食堂的运营成本全摊在午餐这顿饭上。电网里的变压器、线路也是类似的逻辑,容量按峰值配置,折旧和基础成本却要全天分摊。如果削峰填谷能把峰值挪到低谷,减少峰谷差,那么同样一套配电设备可以承载更多实际用电量,购电成本、线损和设备寿命都会向着有利的方向变化。
对这个项目而言,负荷方差是衡量负荷曲线平稳度的常用数学指标,它可以近似刻画峰谷差。把负荷方差作为优化目标之一,等于直接在数学上引导充电负荷从高峰时段向低谷时段转移。
1.3 为什么偏偏选削峰填谷这个思路
我之前见过一些项目直接照搬“源侧加储能”来扛峰值负荷,但储能设备成本高、回收周期长,对小规模充电站并不划算。而电动汽车本身就是一块“移动储能”,它的电池在停靠时段完全可以调节充电时序,这是天然的柔性资源。削峰填谷的思路不新增硬件,只调整充电时间,成本上要显著优于储能扩容。
关键在于制定策略时不能只考虑电网目标,还要考虑用户目标。如果为了削峰就把车主的充电需求拖到凌晨,会直接影响用户体验和后续配合度。所以这类课题通常会把“用户充电费用最小化”和“电网负荷方差最小化”放在一起做多目标优化,这比单目标调度要复杂,但更接近工程实际。
2. 多目标调度:用户省钱和电网平稳怎么同时满足
2.1 两个目标各自的数学表达
先明确我们要优化的两个目标。
目标一:用户充电费用最小化。在一个给定的充电周期里,电动汽车的总充电费用等于各时段充电功率乘以对应电价的累计值。数学上写成:
f1 = sum(P_i × price_i × dt)
其中P_i是电动汽车在第i个时段内的充电功率,price_i是该时段电价,dt是时段长度。如果某时段电价高,优化器就会倾向于把功率往电价低的时段转移。
目标二:电网负荷方差最小化。设基础负荷为L_base,电动汽车充电总功率为P_ev,那么总负荷在任一时刻为L_base + P_ev。负荷方差定义为各时段总负荷偏离平均负荷的平方和:
f2 = sum((L_base_i + P_ev_i - mean_load)^2)
方差越小,说明负荷曲线越平坦,峰谷差越小。这个目标会惩罚在高峰时段集中充电的行为。
实际代码里这两个目标会被合并成一个加权目标函数,也可以分别跑完再做帕累托选择。我的项目里采用加权和的形式,把两个目标归一化后再加权,便于计算和调参。
2.2 目标打架时的权衡方式
两个目标不总是打架,但确实会出现冲突场景。比如电价曲线和基础负荷曲线高度相关——高峰电价对应高峰负荷——那么想让用户少花钱,就要避开高峰充电,这同时也有利于电网负荷方差,此时两个目标方向一致。但如果某地区夜间电价很低而基础负荷也比较低,电网希望把负荷填到低谷、均匀化,用户也希望晚上充电便宜,方向也基本一致。
真正冲突的场景出现在约束条件上:用户必须在离开前达到一定电量,如果接入时间很短、充电窗口有限,那么不管电价高不高、负荷平不平,都必须在这个时间窗内尽量快充,可移动的调度空间就被压缩了。此时多目标问题会出现妥协解,也就是在通勤刚需场景下,最优解同时照顾两个目标不可能做到完美,只能在帕累托前沿上找一个折中方案。
2.3 多目标问题中的“最优解”到底指什么
单目标优化很好理解:找到让目标函数值最小的那一组变量。多目标优化则没有一个让所有目标同时最小的唯一解,而是有一组帕累托最优解。所谓帕累托最优,就是不存在另一组方案能在某一个目标上更好,同时不劣化其他目标。
实际工程里我们不需要枚举整条帕累托前沿,通常会根据电网和用户的偏好预设权重:电网看重负荷平稳就用更大的权重给方差项,用户更在意价格就用更大权重给费用项。多目标优化的价值在于,它允许你在同一个框架下改变偏好,而不是每次调整需求都重写一套调度逻辑。这也是这类课题的核心不在“求解某一天的最优方案”,而在“权衡机制本身”。
3. 数学模型搭建:变量、约束和目标函数的一次性梳理
3.1 电动汽车接入过程怎么描述
我把每辆电动汽车在时间轴上的“可用窗口”描述为 [到达时刻, 离开时刻]。在这个窗口内车辆停在车位并接入充电桩,窗口之外车辆不在,不能充电。到达时刻对应的初始SOC(电池荷电状态)是随机生成的,一般根据出行习惯在某个区间内波动;离开时刻用户要求的期望SOC则是约束条件之一。
举个例子:某辆车的电池容量是60 kWh,到达时SOC是30%,目标是离开时达到90%,那么需要补充的电量就是 60×(0.9-0.3)=36 kWh。如果充电窗口是傍晚6点到次日早上7点,总共13个小时,理论上最低平均充电功率只需要 36/13≈2.77 kW,远低于充电桩的额定功率。这说明只要时间窗口够长,调度策略就有很大的“平移空间”。
这一点是整个削峰填谷可行性的根本来源:车辆大部分时间处于停驶状态,快充需求实际上只占少数场景。调度算法要做的,就是把这个“最低平均功率”往上抬一点、在更便宜的时段多充一点,同时保证各项约束不越界。
3.2 决策变量和功率上下限怎么定
优化问题的决策变量是每辆车在每个调度时段内的充电功率P_i(t),其中t从接入时刻一直到离开时刻。为了防止出现负数(即只做单向充电,不允许向电网反送电),变量下限设为0,上限设为充电桩额定功率与电池允许最大充电功率之间的较小值。
定义域方面需要注意时间粒度。常见的做法是把一天划分为96个时段,每个时段15分钟。用15分钟粒度既不会让变量规模爆炸,又能比较细致地刻画电价变化和负荷变化。如果粒度过细,比如一分钟一个时段,几十辆车就会产生几十万个变量,求解压力会非常大;如果粒度过粗,比如一小时一个时段,电价切换点可能被平滑掉。我的经验是15分钟最合适,数据量和精度之间的平衡点刚好在这个位置。
3.3 三类约束条件及工程含义
约束条件可以分成三类来梳理:
第一类是充电功率范围约束:任意时段充电功率不超过最大值。这条约束对应充电桩硬件能力和电池允许的充电倍率,必须严格满足,否则算出来的调度方案在现实中无法执行。
第二类是电量状态约束:任意时刻SOC保持在安全区间内,比如0.1到0.9之间(具体看电池管理系统限制),避免过度充电或过度放电。另外最终时刻SOC必须达到用户期望值。工程含义就是用户不能因为调度而“没电可开”,这是用户满意度的底线。
第三类是总负荷约束:某个时刻的总负荷不超过配电网的容量上限,比如变压器最大容量。这个约束在负荷峰值时段尤其重要,如果基础负荷加上所有充电功率超过变压器容量,就会导致过载跳闸。
这三类约束从执行层面看缺一不可。不少初学者把重点放在目标函数上,结果跑出来的方案要么充电桩功率超出额定值,要么用户深夜需要用之前电量不足,实际意义就大打折扣了。正确做法是先把约束条件理清楚,写成矩阵或向量形式,再去优化目标函数。
4. Matlab代码实现:从基础负荷数据到优化结果
4.1 程序结构总览
我用Matlab写整套代码时,把程序拆成了六个模块,每个模块一个脚本或函数:
- 数据读入模块:读入一天96点的基础负荷曲线和分时电价。
- 电动汽车参数生成模块:随机生成每辆车的到达时间、离开时间、初始SOC、目标SOC、电池容量、桩功率上限。
- 目标函数模块:接收调度矩阵,计算电网负荷方差和用户充电费用。
- 约束处理模块:把不满足约束的方案通过惩罚项反馈回目标函数。
- 优化求解模块:使用粒子群算法迭代寻优。
- 结果输出模块:画负荷曲线、SOC曲线、费用对比,输出调度表。
这样的拆分让调试方便很多。比如我想看某个约束有没有生效,可以单独跑约束处理模块检查;想换优化算法时,只需要替换第5个模块,目标函数和约束不用动。实际项目中我建议不管是做课程设计还是工程预研,都保持这种模块化习惯,后期改起来成本低很多。
4.2 目标函数计算代码
目标函数核心部分我一般这样写,先算总负荷,再算方差和费用,最后加上约束惩罚:
function [fitness, penalty] = objectiveFunction(schedule, baseLoad, price, vehicles, dt) % schedule: [车辆数 x 时段数] 每辆车各时段充电功率 % baseLoad: 1 x 时段数,基础负荷曲线 % price: 1 x 时段数,分时电价 % vehicles: 结构体数组,包含每辆车的参数 numSlots = length(baseLoad); numVehicles = size(schedule, 1); totalLoad = baseLoad; for i = 1:numVehicles totalLoad = totalLoad + schedule(i, :); end % 目标一:电网负荷方差 meanLoad = mean(totalLoad); gridObjective = sum((totalLoad - meanLoad).^2); % 目标二:用户充电费用 costObjective = 0; for i = 1:numVehicles costObjective = costObjective + sum(schedule(i, :) .* price) * dt; end % 约束惩罚:SOC越界 / 最终能量不足 penalty = 0; for i = 1:numVehicles initSOC = vehicles(i).initSOC; targetEnergy = vehicles(i).targetSOC * vehicles(i).capacity; socEnergy = initSOC * vehicles(i).capacity + cumsum(schedule(i, :) * dt); finalEnergy = socEnergy(end); % 最终电量必须达到目标 if finalEnergy < targetEnergy - 1e-6 penalty = penalty + 1e5 * (targetEnergy - finalEnergy)^2; end % SOC不得超过上限,这里以上下限为0.1, 0.9为例 if any(socEnergy / vehicles(i).capacity > 0.9) || any(socEnergy / vehicles(i).capacity < 0.1) penalty = penalty + 1e5; end end % 加权组合 w1 = 0.5; w2 = 0.5; fitness = w1 * gridObjective / numSlots + w2 * costObjective / 1e3 + penalty; end注意代码里的系数1e3和1e5不是随便拍的,是为了让两个目标和惩罚项量纲差不多,避免优化器只盯着数值最大的那一项跑。不同电价水平、不同车辆数目下这些归一化系数需要重新调,否则算法容易陷入局部最优。
4.3 粒子群优化器主循环
粒子群算法(PSO)实现起来不难,核心就是位置更新和速度更新。每辆车每个时段的充电功率构成一个高维向量,我把整个向量当成一个粒子的位置。初始化时在可行域内随机撒粒子,然后按经典公式迭代:
function [bestPos, bestFitness] = psoOptimizer(objectiveHandle, dim, lb, ub, options) nParticles = options.nParticles; maxIter = options.maxIter; % 初始化粒子位置和速度 positions = lb + (ub - lb) .* rand(nParticles, dim); velocities = zeros(nParticles, dim); pBest = positions; pBestFitness = arrayfun(@(i) objectiveHandle(positions(i, :)), 1:nParticles); [gBestFitness, bestIdx] = min(pBestFitness); gBest = pBest(bestIdx, :); w = 0.9; % 惯性权重 c1 = 1.5; c2 = 1.5; % 学习因子 for iter = 1:maxIter % 线性递减惯性权重 w = 0.9 - (0.9 - 0.4) * iter / maxIter; velocities = w .* velocities ... + c1 * rand(nParticles, dim) .* (pBest - positions) ... + c2 * rand(nParticles, dim) .* (gBest - positions); positions = positions + velocities; % 边界处理 positions = max(min(positions, ub), lb); % 计算新适应度 for i = 1:nParticles fit = objectiveHandle(positions(i, :)); if fit < pBestFitness(i) pBestFitness(i) = fit; pBest(i, :) = positions(i, :); end end [curBestFit, curBestIdx] = min(pBestFitness); if curBestFit < gBestFitness gBestFitness = curBestFit; gBest = pBest(curBestIdx, :); end end bestPos = gBest; bestFitness = gBestFitness; end这段代码只是一个基础版,实际跑的时候需要把 schedule 矩阵展开成行向量,因为粒子位置只能是一维向量。处理完维度转换之后,再把行向量reshape回二维矩阵喂给目标函数。
4.4 结果输出怎么看
出图是算法调试里最直观的一环。我一般会画三张图:第一张模拟一天的原负荷和优化后负荷曲线对比,第二张是车辆SOC随时间的曲线,第三张是迭代收敛曲线。优化后的日负荷曲线能够直接看出削峰填谷效果是否明显,SOC曲线能判断每辆车有没有在离开前充满,收敛曲线则能反映出算法稳定性。这三张图都验证通过之后,调度方案才敢往下一步推。
5. 结果怎么解读:削峰填谷是否真见效,用指标说话
5.1 负荷曲线对比是第一印象,但不是全部
第一次跑通代码后,我把原负荷曲线和优化后负荷曲线叠在同一张图里看,直观效果通常是很明显的:晚间尖峰被削下去一块,凌晨低谷被填起来一块。但这里有个容易误判的点——方差确实下降了,但要看是不是通过过度牺牲某些时段换来的。比如为了规避晚间高峰,把所有充电全部推到凌晨,那也是削峰填谷,可半夜充电功率可能把原本的低谷抬成了新高峰。
所以还需要看一个衍生指标:最大负荷值。优化后的最大负荷应当显著低于无序充电的最大负荷,才说明“削峰”做到了;低谷时段的抬升幅度要控制在合理范围,才说明“填谷”有效。如果最大负荷没降多少,只是把峰谷差“揉”小了,那策略还需要继续调。
5.2 费用和SOC达标率要一起看
只盯着电网指标跑优化,很容易做出“所有车都在凌晨3点充电”这种方案,费用最低、负荷最平稳,但用户完全没法用。所以要同时看用户侧两个指标:平均充电费用下降比例,以及最终SOC达标率。
最终SOC达标率是所有车辆在离开时刻达到目标电量的比例,这是可行性的硬指标。我的做法是输出一张车辆列表,列出每辆车的到达时间、离开时间、初始SOC、目标SOC、实际最终SOC、充电费用,挨个检查是否有不达标车辆。如果出现不达标,先看是不是约束惩罚系数太小导致优化器“宁罚不怕”;再调大惩罚系数,或者把目标SOC约束从软约束改成硬约束。
5.3 收敛曲线揭示的算法状态
PSO的收敛曲线能暴露很多问题。我归纳一下常见的三种情况:
第一种是收敛太快、过早停滞:迭代不到20次就跌到平台,说明惯性权重衰减太快,或者粒子群规模太小,多样性不足。处理方法是调大初始权重、增加粒子数量。
第二种是曲线震荡、始终不收敛:搜索空间过大但边界太宽,粒子反复越界又弹回来,适应度一直跳。处理方法是缩小变量范围,把充电窗口外的充电功率设成零,而不是只靠边界裁剪。
第三种是后半程还在缓慢下降:很可能是$c1$、$c2$学习因子太小,粒子后期缺乏向全局最优靠近的动力。可以在300次迭代的基础上延长迭代次数,或者改用自适应权重策略。这些都是经验值类的调试项,需要根据具体数据规模反复试,没有一次性通用的最佳参数。
6. 调参与避坑心得:一次完整的排查链路
6.1 症状:方案看起“完美”但所有车都在凌晨充电
有一次我跑完优化,看到负荷曲线非常漂亮,方差降得很明显,收敛曲线也很快稳定。但打开车辆列表一看,发现问题很大——几乎每辆车的充电时间都被挪到了凌晨2点到5点,个别车甚至在用户离开前两小时才开始补电。费用倒是省了不少,电网负荷也平稳了,可这根本不是能被接受的调度方案:用户凌晨大概率在睡觉,车桩通信如果中断,充电完全可能无法执行;而且万一用户临时提前用车,电量根本不够。
这类问题在结果图上不容易看出来,因为负荷曲线和收敛曲线都“好看”,必须看个体车辆的调度明细才能暴露。
6.2 定位:目标函数分量被“归一化”架空
当时我检查的第一步是先单独计算优化方案的gridObjective和costObjective,发现费用项在整个目标函数里的占比几乎可以忽略。原因是我在一个电价比较平缓的数据集上沿用了一组为“峰谷差很大”场景标定的归一化系数,费用项被除以1e3之后,数值远小于方差项,优化器当然优先压电网方差,用户费用目标等于被架空。
6.3 修复:重新标定权重后恢复平衡
定位之后,修复方法就比较直接了。我把两个目标先拆开跑两次“单目标”预实验,得出各自量级,然后设定归一化系数让它们在加权组合中的贡献大致相当。具体来说,如果gridObjective原始量级是8万,costObjective是6000,我就用1e3作为费用项的归一化分母,而不是直接用1e3拍脑袋。这样两个目标在适应度函数里是“真竞争”的关系,而不是一方被另一方吞掉。
6.4 初值敏感性问题
PSO对初值分布比较敏感,如果初始粒子全挤在可行域边缘,很容易陷入局部最优。我常用的处理方法是先用均匀随机分布初始化,再额外加入几个“先验方案”:一个全时段平均充电方案、一个充到最低电价时段的方案、一个按“先到先充”顺序充电的方案。这样即使随机部分分布不理想,先验方案也能给种群一个相对不错的起点,收敛速度和稳定性都有改善。
另外,每次运行的结果带有随机性,我会在写报告或对比实验时固定随机种子,保证结果可复现。这是做研究类项目时容易被忽略却非常重要的一步,不然今天跑出一个结果,明天再跑又是另一个结果,根本没法定量比较参数影响。
7. 边界条件与扩展方向
7.1 从仿真到部署还有几道坎
上面整套代码在模拟数据上跑得很顺,但从“仿真结果”到“实际投产”之间还有几道坎。
第一,真实负荷预测有误差。基础负荷数据在调度开始前其实是未知的,需要用历史数据预测,预测误差会直接传导到优化结果上。实际工程里一般会在优化方案基础上加一个安全裕量,比如预留5%到10%的变压器容量。
第二,通信和执行的延迟。车辆是否真的按调度方案执行,取决于车桩通信链路和充电桩控制接口的响应速度。如果调度方案下发之后车辆已经不在停车位,整个计划就要重新落地。所以真实系统需要加一层实时滚动修正,每15分钟重新跑一次优化,而不是一天只出一次方案。
第三,多代理冲突。当大量车同时响应调度时,可能出现“群体振荡”——所有车都按照同一个低电价时段充电,反而制造出新的高峰。这是单向优化框架很难完全化解的问题,需要引入随机化策略或博弈论方法,在群组层面做错峰。
7.2 V2G双向充电带来的变化
如果允许电动汽车向电网放电(V2G),调度模型从单目标“什么时候充”变成“什么时候充、什么时候放”,复杂度上了一个台阶。放电功率会产生负的负荷项,理论上可以更精准地削峰,但也带来电池循环寿命折损的问题,需要在目标函数里加入电池退化成本项。
我在某个模拟项目里做过一组对比:单向充电策略能把最大负荷降低15%左右,而允许少量V2G放电后最大负荷可以多降几个百分点,但代价是部分电池的等效循环次数增加,用户电池质保和衰减风险也随之上升。这个代价是否值得,取决于地区峰谷价差和电池置换成本,不能一概而论。
7.3 后续值得尝试的三个方向
如果要把这个课题继续往下延伸,我觉得比较有实际价值的方向有三个:一是考虑实时电价预测,把预测误差纳入随机优化框架;二是引入排队论处理充电站到达流程,让调度方案更贴合真实运维场景;三是把这套逻辑迁移到社区级光储充电一体化的场景中,把光伏出力预测和储能SOC也拉进同一个优化问题。
每个方向都会在模型和代码层面带来新的复杂度,但只要基础框架搭得模块化,扩展起来并不伤筋动骨——这也是我一开始就坚持拆分模块的原因。做这类调度项目最忌讳的就是一上来就堆代码,先把目标和约束想清楚,Matlab只是最后落地的工具而已。