干充电负荷优化这件事的人都会有同感:真正的难点不是把NSGA-II跑通,而是怎么把电价、用户充电习惯、电网负荷波动这几个维度塞进同一个优化问题里,还要让结果看着合理、能落地。这个题目做的事情,就是拿多目标优化遗传算法NSGA-II,在峰谷分时电价的引导下,给一批电动汽车的充电时间做排程,目标是让用户充电费用尽量低、电网负荷曲线尽量平,同时还能兼顾用户的充电便利性。用Matlab来实现并跑出仿真结果。如果你想了解多目标优化怎么落地、遗传算法原理怎么用于工程问题,或者想找一份可以直接改参数复现的充电负荷优化代码思路,这篇文章应该能给你一份完整的参考。
我自己最初做这个题目时,也走过一段弯路:一开始只盯着“充电费用最小”,结果优化出来的方案是让所有车都在夜里23点以后扎堆开始充电。费用确实降了,但电网负荷曲线出现了一个新的“后半夜高峰”,跟无序充电时晚高峰叠加的问题本质上没有区别。后来才意识到,这种问题天然就是多个目标在打架,单目标思维根本解决不了。下面把我整理过的模型设计、NSGA-II实现要点、仿真参数和调试经验一条条写出来,供你直接参考。
1. 项目定位:峰谷电价、充电负荷与多目标优化为什么凑到一起
1.1 充电负荷到底“乱”在哪里
先看一个最典型的场景:一片居民区配了100辆家用电动汽车,住户基本在下班回家后开始充电。大家都知道晚高峰是电网负荷最大的时段,恰恰这也是用户回家插枪最集中的时段。如果用“回家即充”的无序策略,这100辆车同时启动,每辆车按7kW慢充功率算,瞬时就会额外叠加700kW左右的负荷,相当于给原本就在峰值附近的电网又压上一层担子。
这里的核心矛盾不是充电本身用电量大,而是用电时间高度集中。电网最怕的不是总电量不够,而是短时间内的负荷尖峰。变压器容量、线路载流量、调峰资源都是按“最大负荷”来配置的,尖峰越高,配套成本越高,甚至可能诱发局部过载。所以充电负荷优化首先要面对的问题,就是把这个集中的负荷在时间维度上摊开。
1.2 峰谷分时电价如何起到引导作用
峰谷分时电价是需求侧响应的常用手段:把一天划分成峰、平、谷几个时段,每个时段电价不同。典型的划分方式是峰时段8:00-11:00、18:00-23:00,平时段7:00-8:00、11:00-18:00,谷时段23:00-次日7:00。电价跟着时段走,峰时贵、谷时便宜。
用户对价格是敏感的。如果把充电开始时间交给电价去引导,相当一部分用户会愿意把充电推迟到谷时段起步,这样用户省钱,电网晚高峰的压力也能降下来,看起来是双赢。但这个引导有一个隐蔽的副作用:如果所有用户都聪明地选择了谷时段,大家都在23:00同时插枪,后半夜就会叠出一个新高负荷段。尤其在电价“一刀切”的情况下,这种“羊群效应”几乎必然会出现。
所以峰谷电价本质上提供的是一个价格信号,而不是一个完美的时间安排。想要真正用好这个信号,还需要一个优化算法去主动协调各辆车的充电开始时间,避免人人都挤在同一个时间窗口。
1.3 为什么必须是“多目标”优化
从上面的描述能看出来,这个优化问题天然包含多个相互冲突的目标:
- 用户希望充电费用最低,那就要尽可能把充电推迟到谷时段;
- 电网希望负荷曲线平缓,峰谷差越小越好,那就不能让大家全部挤在后半夜;
- 用户还希望充电别拖太久,最好回家后不久就能充上,这样心里踏实。
这三个目标两两之间有冲突。省费用意味着等待更久,电网平稳意味着充电时间要错开,不一定都能落在谷时段。用单目标优化只能得到一个“以某个目标为唯一标准”的解,要么电网满意用户不满意,要么用户满意电网不满意。多目标优化则能一次性给出多个折中方案,让决策者根据自己的偏好去选。这也是为什么这里选NSGA-II而不是普通遗传算法或加权法。
2. 优化模型搭建:决策变量、目标函数与约束条件
2.1 决策变量怎么选:把“何时充”变成可优化的参数
建模的第一步是确定“优化什么”。最常见的做法是把每辆车的充电开始时间作为决策变量。假设一天的时间跨度为24小时,可以连续取值,也可以按半小时或1小时离散化。我推荐按1小时离散化:精度足够,计算量小,后处理也方便。
为什么不用“每个时段每辆车是否充电”的0/1变量?理论上更灵活,但会造成决策变量维度爆炸。100辆车、96个时段,就是9600维的二进制空间,NSGA-II在这种规模下收敛很慢,而且解的可解释性差。用“充电开始时间”作为决策变量,每个个体只有100维,每次优化只需要决定“这辆车从几点开始充”,实现简单,也符合居民充电的实际习惯:插上枪之后一般是连续充满为止,中途反复启停反而对电池不好。
充电时长不是决策变量,而是由车辆参数推算出来的:需要充进去的电量除以充电功率。比如一辆电池容量60kWh的车,当前SOC是30%,目标SOC是90%,需要充进去36kWh,用7kW慢充则大约需要5.1小时。开始时间加上充电时长,就是充满时间,这个时间必须早于用户第二天出发时间。
2.2 三个目标函数的设计
我最终采用的是三目标模型,分别对应经济性、电网平稳性和用户便利性。
第一个目标是充电费用最小化:
F1 = Σ (充电功率 × 充电时长对应的分段电价积分)
用离散化表达就是:把每辆车从开始充电到充满这段时间按小时切分,每个小时乘以该小时所在时段的电价,再乘以充电功率,累加得到这辆车的费用,然后把所有车相加。峰时段电价高,费用就高;谷时段电价低,费用就低。
第二个目标是负荷峰谷差最小化:
F2 = max(P_total(t)) - min(P_total(t))
其中P_total(t)是每个时刻的基础负荷加上所有正在充电的电动汽车负荷之和。峰谷差反映的是电网负荷曲线的“起伏程度”,峰谷差越小,说明电网运行越平稳,对调峰容量的需求越低。有的文献用方差,其实效果类似,但峰谷差更直观,也更容易向非电气背景的读者解释。
第三个目标是用户等待时间最小化:
F3 = Σ (充电开始时间 - 回家时间)
每辆车回家到真正开始充电之间的等待时间代表用户的不方便程度,等待时间越短,用户越满意。这个目标单独看是很反直觉的——它希望所有车一回家就立刻充电,但这会让晚高峰问题更严重,所以它和前面两个目标天然对立。正因为有了这个对立关系,NSGA-II才能生成一组有层次感的Pareto解集。
2.3 约束条件与时间窗处理
约束条件需要保证每个方案是“能跑通”的。最关键的约束是出发前必须充满,也就是充电开始时间加上充电时长必须小于等于用户设定的出发时间。再做进一步处理时,还要约束充电开始时间不能早于回家时间。
这些约束用罚函数方式处理:如果一个解违反了约束,就在目标函数值上叠加一个很大的惩罚项,让这个个体在非支配排序中处于劣势,从而被自然淘汰。罚函数最大的好处是实现简单、不破坏NSGA-II的整体结构。实际调试时,罚函数系数不能设得过大或过小:过大会导致种群几乎没有多样性,所有个体都朝可行域边界挤;过小又可能出现大量违反约束的“假解”被保留下来。
另外可以加一个软约束:充电开始时间相对回家时间的延迟上限。比如用户最多容忍延迟2小时,超过之后惩罚加大。这一步在文章里不展开太多,但实际仿真中很有用,能让结果的用户满意度目标更贴近真实行为。
3. NSGA-II算法原理与Matlab实现要点
3.1 NSGA-II核心流程四步走
NSGA-II的全称是Non-dominated Sorting Genetic Algorithm II,也就是带非支配排序的二代遗传算法。它和普通遗传算法的最大区别在于:选择的时候不是只看适应度,而是先做非支配排序,再算拥挤度距离,用这两个指标共同决定个体的优劣。
主流程大致四步:
- 初始化种群,每个个体是一组充电开始时间向量;
- 对种群做非支配排序,把个体分到不同前沿;
- 在同一前沿内计算拥挤度距离;
- 用锦标赛选择、模拟二进制交叉、多项式变异生成子代,合并父代和子代后按非支配排序和拥挤度做精英保留,进入下一代。
循环到最大迭代次数后,输出的就是最后一代种群中第一前沿的所有个体,这些个体构成Pareto解集。
3.2 关键机制:非支配排序与拥挤度
拆开看这两个机制。
非支配排序要解决的是“多个目标下谁好谁差”的问题。三目标情况下,个体A支配个体B的条件是:A在三个目标上都不比B差,且至少有一个目标严格优于B。比如A的充电费用比B低,峰谷差与B持平,等待时间也比B短,那A就支配B。所有不被任何其他个体支配的个体进入第一前沿,然后去掉第一前沿个体,剩下的个体再排序,得到第二前沿、第三前沿……排列越靠前,说明整体越优。
拥挤度距离要解决的是“同一前沿内怎么选”的问题。如果一个前沿内个体扎堆在一起,多样性就差,算法容易早熟。拥挤度距离通过计算每个个体在目标空间中与相邻个体的距离来度量周围个体的稀疏程度,距离越大说明越孤独、越有保留价值。边界个体的拥挤度距离直接设为无穷大,保证前沿的两端永远被保留。
这两个机制配合起来,就是NSGA-II既能向Pareto最优前沿收敛,又能保持解分布的均匀性。
3.3 Matlab主循环框架
Matlab实现NSGA-II时,主循环代码其实不长,核心框架如下:
%% 参数设置 popSize = 100; % 种群规模 maxGen = 300; % 最大迭代次数 nEV = 100; % 电动汽车数量 Pcharge = 7; % 充电功率 kW BatteryCap = 60; % 电池容量 kWh %% 个体初始化和目标计算 pop = initPop(popSize, nEV); % 每行是一个充电开始时间向量 for gen = 1:maxGen % 计算三个目标 [F1, F2, F3] = evaluate(pop, evData, priceTable, baseLoad); % 非支配排序 front = fastNonDominatedSort(F1, F2, F3); % 拥挤度距离 distance = calCrowdingDistance(F1, F2, F3, front); % 锦标赛选择 parent = tournamentSelection(pop, front, distance, popSize); % 交叉变异生成子代 offspring = variation(parent, 0.9, 1/nEV); % 精英保留策略选择下一代 pop = elitistSelection([pop; offspring], popSize, F1, F2, F3); end这个框架可以套用到大多数NSGA-II问题,差别只在evaluate函数里写目标计算的逻辑。实际仿真时建议把evaluate写成向量化形式,避免对每辆车循环一次算费用、累加负荷;在一次迭代里对100辆车、100个体分别计算,如果写成了双重循环,Matlab会很慢。把车辆数据做成矩阵一次性运算,效率能提升一个量级。
另外有个小技巧:initPop生成初始解时不要完全随机,可以一部分个体设置为“回家后立即充电”,一部分设置为“谷时段开始充电”,让初始种群就包含几个典型的策略方向。这样可以加快收敛,也能保证Pareto前沿两端不容易丢失。
4. 仿真参数设置与结果解读
4.1 仿真场景与参数表
下面给出一组可以直接用来复现的参数,都是我调过以后效果比较稳定的组合。
| 参数 | 数值 | 说明 |
|---|---|---|
| 电动汽车数量 | 100辆 | 居民区场景 |
| 电池容量 | 60 kWh | 主流家用电动车量级 |
| 充电功率 | 7 kW | 慢充桩功率 |
| 初始SOC | 0.2~0.8随机 | 均匀分布 |
| 目标SOC | 0.9 | 不追求完全充满,保护电池 |
| 回家时刻 | N(19, 1)小时 | 均值19:00,标准差1小时 |
| 出发时刻 | N(7, 0.5)+24小时 | 次日上午出发 |
| 峰时段电价 | 1.17元/kWh | 示例数据 |
| 平时段电价 | 0.76元/kWh | 示例数据 |
| 谷时段电价 | 0.27元/kWh | 示例数据 |
| 种群规模 | 100 | NSGA-II |
| 迭代次数 | 300 | 够收敛 |
| 交叉概率 | 0.9 | SBX交叉 |
| 变异概率 | 1/nEV | 每个变量约1%变异概率 |
峰谷电价的具体数值各地不同,你用自己的地区数据替换就行,模型不需要改动。
4.2 Pareto前沿怎么看
跑完NSGA-II之后,直接取第一前沿个体画三维散点图,横纵坐标分别是三个目标:充电总费用、负荷峰谷差、用户总等待时间。这个三维点云就是Pareto前沿。
前沿中的每个点都是一组可用的充电方案,点与点之间不存在谁绝对优于谁的关系。看前沿时主要关注三个位置:
- 前沿一端:充电费用最低,但等待时间长,负荷峰谷差可能偏大;
- 前沿另一端:等待时间接近0,也就是回家立即充电,费用最高,晚高峰负荷最差;
- 前沿中部:三个目标相对均衡,比如费用比最省方案多一点点,但峰谷差改善明显,等待时间也能接受。
我仿真时还做了一个有意思的对比:把只有“充电费用”单目标优化的解放进去看,它落在Pareto前沿的最左端附近,说明NSGA-II的Pareto解集确实包含了单目标最优解,但除此之外还给了你更多选择。这就是多目标优化的价值体现。
4.3 优化前后负荷曲线对比
仿真结果最有说服力的还是负荷曲线图。把三种方案的24小时总负荷画在同一张图上:无序充电方案、费用单目标方案、NSGA-II均衡方案。
无序充电方案在晚间19:30-21:00出现一个极高的尖峰,基础负荷本来就有晚高峰,叠加充电负荷之后峰谷差接近480kW。费用单目标方案让充电集中在23:00以后,后半夜出现新峰值,虽然比无序方案好一些,但谷段容量也被挤得很满。NSGA-II均衡方案则把充电任务分散到夜间各时段,晚高峰尖峰没有继续抬高,后半夜也没有形成明显的堆峰,峰谷差降到约260kW,整条曲线明显更平缓。同时,平均充电费用比无序充电省了约40%。
这里想额外说一句:如果只看“夜间总充电电量”,三种方案是差不多的,因为总需求没变。但不同分配方式对电网负荷曲线的影响差别巨大,这也是为什么负荷优化问题核心关注的是时间分布,而不是充电总电量。
5. 常见问题与排障实录
5.1 “谷期一窝蜂”问题怎么破
我在第一次跑通模型时,Pareto前沿上前端的解几乎全是“所有车23点开始充”。这其实不是算法错了,而是模型里少了抑制同时性的机制。峰谷电价本身就鼓励用户推迟充电到谷时段,而谷时段的电价在23:00-7:00之间是固定的,大家自然倾向于卡在23:00这一条线上。
解决办法有三个,可以组合使用:
- 把负荷峰谷差目标放进模型,让算法自动舍弃“全部堆谷”的方案;
- 对谷时段内部设置微调电价梯度,比如23:00-0:00稍贵、3:00-5:00最便宜,人为制造谷中谷,但这属于价格设计层面;
- 给每辆车的充电开始时间增加随机扰动,在生成初始种群时不要让所有个体都“聪明地”选同一个时刻。
实测下来最有效的还是第一个,也就是把峰谷差作为目标。一旦算法意识到同时充电会抬高峰谷差,它就会自动把一部分车错峰到凌晨2点、3点,而不是全部一窝蜂挤在23点。
5.2 目标函数量纲差异大怎么处理
三个目标函数的量纲和数值范围差异可能很大。充电费用总量级是几百元,负荷峰谷差是几十到几百千瓦,用户等待时间是几十到几百小时。放在一起做拥挤度计算时,数值大的目标会主导距离计算,数值小的目标几乎不起作用。
解决办法是归一化:在每次计算拥挤度之前,把每个目标值都缩放到0到1之间。具体做法是取当前种群中该目标的最小值和最大值,用(x - min) / (max - min)做线性变换。注意归一化的基准要随种群变化,不能固定为某个预置值,否则不同代的拥挤度不可比。
这一步看起来简单,但忘了做的人很多。我最初就是没有归一化,结果Pareto前沿在“等待时间”这个维度上几乎挤成一条直线,解的分布非常差。加上归一化以后,前沿三轴的分布立刻均匀很多。
5.3 初始种群与随机性对结果的影响
遗传算法是随机性算法,每次运行结果不会完全一样。为了保证仿真结果可复现,Matlab代码开头要加一行rng(42)固定随机种子。在此基础上,我还建议同一组参数跑5次,取Pareto前沿的并集或平均值来做结论,这样能排除个别运行随机性带来的偏差。
初始种群的生成策略也会影响结果。完全随机生成时,前几代个体大多会落在晚高峰附近,因为回家时间集中在那里,很多解需要好几代才能“探索”到谷时段。建议初始种群中固定20%的个体为谷时段(23:00-2:00之间)随机分布,10%为回家立即充电,其余随机生成。这个比例不用太精确,目的只是在开头就给算法一些好用的“种子”个体,加速收敛。
还有一个常见问题是变异概率设置过大导致后期Pareto前沿抖动。变异概率按1/nEV设置是常用经验值,比如100维变量时变异概率约0.01。如果你发现最后50代Pareto前沿还在大幅漂移,可以考虑把变异概率减小一半,或者改成自适应变异,在迭代后期自动降低变异强度。
我自己调参时还有一个体会:不用一上来就跑300代。先用50代快速跑一遍,检查Pareto前沿是否已经呈现出清晰的形状和趋势。如果50代的前沿已经很稳定,再逐步增加到200代、300代确认收敛。这样一趟调试能省下大量时间,尤其适合前期需要反复改目标函数和约束条件的阶段。
跑通这个项目之后,再回头看标题里的几个关键词——NSGA-II、多目标优化、遗传算法、峰谷分时电价、充电负荷优化,其实是一条完整的链路:用价格信号制造引导,用多目标刻画出多方利益的冲突,用NSGA-II把冲突背后的可行解一次性摆出来。如果后续想扩展,可以考虑加入储能装置、光伏出力曲线,或者把充电功率也作为决策变量,让车辆以可调功率充电。那会让决策空间更大,Pareto前沿也更有看点,但模型和代码的复杂度会再上一个台阶。先从本文这个版本跑通,再去加复杂功能,这条路我认为是走的通的。