☰
基于NSGAII的峰谷分时电价电动汽车充电负荷多目标优化及Matlab实现
2026/9/30 9:23:51 网站建设 项目流程

这几年做电动汽车充电负荷优化相关的仿真项目,几乎每次都会被问到同一个问题:“为什么不能直接用加权求和,非得折腾NSGAII?”这个问题的答案,恰好就是今天这篇博文的核心。借这个“基于多目标优化遗传算法NSGAII的峰谷分时电价引导下的电动汽车充电负荷优化研究”项目,我把多目标优化的建模思路、NSGAII算法的核心机制、Matlab代码实现细节和调试经验一次性讲透。

这个项目解决的是一个很现实的矛盾:车主希望充电越便宜越好,电网希望负荷波动越小越好,两个目标天然冲突。峰谷分时电价给用户提供了错峰充电的价格杠杆,但用户到底怎么响应、怎么安排充电计划,才能同时照顾到钱包和电网,这就是NSGAII发挥价值的地方。如果你是电气工程、交通能源方向的在校学生,或者正在做充电桩规划、需求响应相关课题的工程师,这篇文章可以作为你从零开始复现同类研究的实操参考。代码实现基于Matlab,所有关键环节我都会给出可落地的写法。

1. 先把问题说清楚:峰谷分时电价下,充电负荷优化到底在优化什么

1.1 电动汽车充电带来的电网压力

先看一个最简单的场景:假设一个小区有200辆电动汽车,每辆车平均充电功率7kW,如果大家都习惯下班回家(18点到20点)插上充电枪,这200辆车同时充电就会产生1400kW的额外负荷。城市配电网一台配电变压器的容量通常在400kVA到800kVA之间,这个峰值叠加在原有的生活用电高峰上,变压器过载几乎是必然的。

充电负荷和普通家用负荷最大的不同在于它的弹性:电动汽车电池容量普遍在40kWh到100kWh之间,一次满充需要4到10个小时,而用户实际停车时间往往比充电时间长得多。这就意味着“什么时候充”存在很大的调度空间。把充电行为从负荷高峰挪到负荷低谷,对电网来说是降低峰谷差、减少变压器扩容成本的关键手段。

1.2 分时电价是杠杆,多目标优化是尺子

峰谷分时电价的原理并不复杂:在负荷低谷时段降低电价,在负荷高峰时段提高电价,用价格信号引导用户改变用电行为。但它面临一个尴尬局面——如果所有电动车都被引导到谷段开始时立刻充电,谷段本身又会出现一个新的负荷尖峰,这就是所谓的“恢复性峰值”。

所以要得到一个真正合理的充电方案,不能只看用户电费,也不能只看电网峰谷差,必须同时考虑多个指标。这就从单目标问题变成了多目标优化问题。NSGAII在这个场景里的价值,就是在一次运行中给出一个Pareto解集,让决策者看到“用户成本”和“电网负荷波动”之间的完整权衡关系,而不是拍脑袋定一个权重系数。

2. 优化问题建模:目标函数、决策变量与约束条件

多目标优化不是上来就调算法,建模才是决定结果上限的一步。模型建歪了,算法再强也白搭。下面是我在这个项目里采用的建模方式,直接对应Matlab代码中的目标函数文件。

2.1 决策变量与个体编码方式

在电动汽车充电优化问题中,最核心的决策是“每辆车什么时候开始充电”。假设研究区域内共有N辆电动汽车,每辆车给定一个充电开始时间,所有车的开始时间拼接起来就构成一个优化解。

在Matlab代码中,一个个体(Individual)被编码为一个N维向量:

% 种群中的每一个个体,存储了N辆车的充电开始时间 % 为了便于遗传算子操作,变量归一化到[0,1]区间 x = rand(1, N); % N辆车的起始充电时间归一化值 startHour = floor(x * 24); % 解码为0-23之间的具体小时

需要说明的是,这里的编码方式决定了搜索空间的大小和遗传算子的效果。把变量归一化到[0,1]而不是直接使用[0,23]的小时数,是为了让SBX交叉和多项式变异算子在不同量纲下保持稳定的搜索行为。如果你想加入“充电功率等级”作为决策变量,同样可以扩展到多维编码,代码逻辑不需要大改。

2.2 目标函数设计:用户成本与电网负荷波动

本项目取两个关键目标,对应两目标优化问题。

第一个目标是用户总充电费用最小化。充电费用等于充电电量乘以对应时段的电价,要注意跨时段充电的情况需要分段计算电费:

function cost = calTotalCost(startHour, chargePower, duration, price) % 对每辆车:根据充电跨过的时段累加电费 % 假设充电功率恒定,分段统计峰平谷电量 cost = 0; for i = 1:length(startHour) h0 = startHour(i); h1 = min(24, h0 + duration(i)); % 充电结束时刻 % 实际实现中需要针对峰平谷区间做积分累计 for t = h0:h1-1 cost = cost + price(t+1) * chargePower(i); end end end

第二个目标是电网负荷波动最小化。常见做法有两种:一是最小化负荷曲线的峰谷差,二是最小化负荷方差。我推荐用负荷方差,因为它对整条曲线的形状都有约束力,不会出现“削了旧峰起了新峰”的情况。叠加基础负荷后,总负荷曲线为:

function f2 = calLoadFluctuation(baseLoad, evLoad) total = baseLoad + evLoad; meanLoad = mean(total); f2 = sum((total - meanLoad).^2) / length(total); end

两个目标都是最小化问题。这个设计背后有一个值得注意的细节:如果把“用户充电费用”换成“用户充电等待时间”,或者把“负荷方差”换成“新能源消纳率”,NSGAII的框架本身不需要变动,只需要替换目标函数的计算逻辑。这也是用成熟优化算法做扩展研究的优势所在。

2.3 约束条件:不是所有解都合法

约束条件在多目标优化中经常被忽视,但它直接决定了算法输出的解能不能真正落地。本项目设置了四类约束:

  • 电池SOC约束:充电结束时SOC不超过100%,起始SOC不低于某个下限(比如20%),这对应充电时长不能无限长。
  • 充电功率约束:恒功率充电模式下,功率不能超过充电桩额定功率。
  • 充电时间约束:充电结束时间不能超过车主设定的离场时间。
  • 变压器容量约束:任意时刻的总负荷(基础负荷+充电负荷)不能超过变压器额定容量。

在NSGAII中处理约束的常规做法是约束支配法。也就是说,任意可行解都支配所有不可行解;两个不可行解之间,违反约束程度小的支配违反程度大的。代码实现时,我会为每个个体额外保存一个“约束违反量”字段,参与非支配排序的比较逻辑。

3. NSGAII算法核心原理:为什么它能同时优化多个目标

3.1 Pareto支配与快速非支配排序

多目标优化与单目标优化的本质区别在于“谁更优”的判断标准。在单目标问题里,数值小就是优;在多目标问题里,解A可能目标1比解B好,目标2却比解B差,这时A和B互不支配。

严格定义是:如果解A在所有目标上都不劣于解B,且至少在一个目标上严格优于解B,那么A支配B。所有不被任何其他解支配的解组成第一层Pareto前沿,去掉它们之后,剩余解中再次选出不被支配的,组成第二层,以此类推。这个分层过程叫非支配排序。

NSGAII的快速非支配排序算法用两个关键数组提升效率:每个个体被支配的次数np,以及该个体支配的个体集合Sp。每轮遍历找出所有np=0的个体作为当前前沿层,然后对这些个体支配的个体的np减1,重复直到分层完成。整体复杂度为O(MN²),M是目标数,N是种群大小,对充电优化这类几十辆车、几百个种群的规模完全够用。

3.2 拥挤度距离:让解均匀分布的关键

非支配排序保证了种群向Pareto前沿收敛,但如果只有这个机制,所有解会挤在前沿的某个局部区域,失去多样性。NSGAII的解决办法是计算拥挤度距离。

拥挤度距离的计算方法很简单:对同一前沿层的个体,按某个目标值排序,边界个体的拥挤度设为无穷大(保证被保留),中间个体的拥挤度等于它在每个目标上相邻两个个体距离之和:

function crowding = calcCrowding(pop, m) % pop为当前前沿个体,m为目标数量 num = length(pop); crowding = zeros(num, 1); for j = 1:m [~, idx] = sort([pop.obj]', 1); % 按第j个目标排序 crowding(idx(1)) = Inf; crowding(idx(end)) = Inf; for k = 2:num-1 crowding(idx(k)) = crowding(idx(k)) + ... abs(pop(idx(k+1)).obj(j) - pop(idx(k-1)).obj(j)); end end end

拥挤度大的个体意味着它周围的解比较稀疏,保留它能让解集覆盖更完整的前沿。在环境选择时,NSGAII的精髓是“非支配排序定层级,拥挤度定优先级”:先选低层级个体,如果同一层级要筛选,优先淘汰拥挤度小的。

3.3 精英保留与遗传操作

NSGAII是带精英保留策略的遗传算法。每一代,父代种群(规模N)经过锦标赛选择、模拟二进制交叉SBX、多项式变异生成子代种群(规模N),然后把父代和子代合并成规模2N的种群,再用非支配排序+拥挤度排序从中选出N个进入下一代。

精英保留的意义在于:优秀解永远不会因为遗传操作的随机性而丢失。合并后再筛选,等于每一代都做了一次“全局择优”,这也是NSGAII相比早期NSGA收敛性大幅提升的原因。

SBX交叉算子是NSGAII的标配,它的特点是以一定概率在两个父代之间产生与父代距离相近的子代,模拟二进制编码下单点交叉的效果。多项式变异则是以一定概率对某个决策变量的小幅扰动。

3.4 为什么不用加权求和法

这个项目里完全可以做加权求和然后用单目标遗传算法求解,而且在某些情况下效率更高。但我还是选了NSGAII,原因是加权系数的选择本身就是一个难题:用户费用和负荷波动两个目标量纲完全不同,一个单位是“元”,一个单位是“kW²”,权重系数没有直观物理意义。而且对于非凸的Pareto前沿,加权求和法无法找到凹区域内的解,会漏掉一部分有价值的折中方案。

NSGAII直接解出整个Pareto前沿,让决策者在事后根据实际偏好(比如“电网压力大就偏向负荷波动小的解,电价敏感时段就偏向费用低的解”)选择方案,这个思路在工程应用和学术研究里都更稳妥。

4. 峰谷分时电价引导机制:价格信号如何改变充电行为

4.1 典型分时电价结构

分时电价在工程仿真中通常分成峰、平、谷三段,部分地区还有尖峰时段。本项目采用一个典型的三段式结构:

时段覆盖时间电价(元/kWh)
峰段10:00-12:00, 18:00-22:001.2
平段08:00-10:00, 12:00-18:00, 22:00-24:000.8
谷段00:00-08:000.4

谷段和峰段电价相差3倍,这是激发用户响应意愿的核心动力。对于一辆电池容量60kWh的电动车,一度电差0.8元,一次满充就能省下近50元,一个月通勤用户能省一两百块。价格杠杆的力度足够改变绝大多数车主的充电习惯。

4.2 用户响应行为与价格弹性

用户侧的响应不是铁板一块,不同车主的时间约束和价格敏感度都不同。有些车主下班后必须立刻充电,第二天一早要用车;有些车主在单位能免费充,根本不管分时电价。因此,分时电价引导下的充电负荷优化必须考虑用户响应的异质性。

在建模上,我采用“价格弹性系数+随机起始时间”的方式:每辆车对应一个基础充电需求时段(由车主出行规律决定),电价信号会在可调度范围内改变充电开始时间,弹性越大,越倾向于在谷段充电。在代码里,这个弹性体现在决策变量的搜索边界上:一辆车如果18:00到家、次日07:00出发,那它的充电开始时间搜索区间就是18:00到次日07:00减去充电时长的范围;如果用户允许延迟充电,再进一步压缩到谷段范围。

4.3 引导效果评价指标

光看最终Pareto前沿还不够,评价分时电价引导效果需要几个直观指标:

  • 峰谷差削减率:优化后总负荷峰谷差相对无引导场景的下降比例。
  • 用户充电成本降幅:优化后的单车平均充电费用与无序充电的对比。
  • 谷段充电电量占比:落在谷时段的充电电量占全部充电电量的比例,这个指标直接反映价格杠杆的引导效率。
  • 充电负荷转移量:从峰段转移到谷段的充电电量。

在Matlab代码的输出阶段,这几个指标都会从最优折中解(即Knee点)中统计出来,作为研究结论的核心数据。实测下来,典型的算例中用户成本可以下降30%到45%,峰谷差削减率在20%到35%之间,具体数值取决于用户弹性系数和充电桩渗透率。

5. Matlab代码实现:从算法框架到仿真结果

5.1 程序整体架构

我习惯把整个项目拆成六个文件,各模块解耦,方便替换数据和算法参数:

EV_NSGAII/ ├── main.m % 主程序入口 ├── initPop.m % 初始化种群,生成合法的充电开始时间 ├── objFunc.m % 目标函数计算(成本+负荷波动+约束) ├── nonDominatedSort.m % 快速非支配排序 ├── crowdingDistance.m % 拥挤度距离计算 ├── selection.m % 锦标赛选择 ├── crossoverMutation.m % SBX交叉+多项式变异 ├── priceData.m % 分时电价配置 └── drawResults.m % 结果可视化

主程序main.m的整体流程是:设置基础数据(车辆参数、基础负荷曲线、电价参数、算法参数)→ 初始化N个个体 → 计算目标函数 → 进入迭代循环(非支配排序 → 选择 → 交叉变异 → 合并筛选)→ 达到最大代数后输出Pareto解集 → 可视化。

5.2 关键函数实现细节

NSGAII的实现细节很多,重点说两个最容易被写错的地方。

第一个是目标函数的向量化计算。如果逐车逐小时用for循环计算费用,种群规模100、迭代200代的情况下会非常慢。我用的是预先构造“时段电价矩阵”的思路:每个个体代表的N辆车,每辆车根据充电起止时间映射到对应的电价区间,然后一次性矩阵乘出总费用。Matlab的矩阵运算效率远高于循环,这一点在参数敏感性分析(需要跑几十组对比实验)时能节省大量时间。

第二个是约束违反量的计算。在Matlab里,约束违反量可以返回一个连续值而不是0/1布尔值,这样可以保留违反程度的差异信息:

function CV = calConstraintViolation(x, evParams) % 越界量累加,作为约束违反程度 CV = 0; % 检查每辆车的充电结束时刻是否超过最晚允许时刻 exceed = max(0, x.startHour + x.duration - x.deadline); CV = CV + sum(exceed); % 检查任意时刻总负荷是否超过变压器容量 totalLoad = baseLoad + calEVLoad(x); overload = max(0, totalLoad - transformerCapacity); CV = CV + sum(overload); end

5.3 参数设置与调参经验

NSGAII的标准参数设置如下,这套参数在绝大多数充电优化算例中都能稳定收敛:

% main.m中的算法参数配置 popsize = 100; % 种群规模 maxgen = 200; % 最大迭代次数 pc = 0.9; % 交叉概率,SBX的分布指数设为20 pm = 1/N; % 变异概率,通常取1/决策变量个数 etac = 20; % SBX交叉分布指数 etam = 20; % 多项式变异分布指数 tournamentSize = 2; % 锦标赛选择规模

需要特别强调的是,种群规模和迭代次数不是越大越好。我在实际测试中发现,对于50辆车以内的算例,种群规模120、迭代150代已经能得到形态完整的Pareto前沿;再加大参数,计算时间线性增长但前沿改善非常有限。决策变量数量N变大时,变异概率pm应该相应减小,否则高频变异会让种群长期处于高扰动状态,难以收敛。

5.4 使用内置函数还是自编NSGAII

Matlab从2017b版本开始提供全局优化工具箱,内置了gamultiobj函数可以直接求解多目标优化问题。很多初学者会问:自己写NSGAII是不是重复造轮子?

我的观点是分情况。如果你的目标只是快速拿到结果,比如课程设计或者应用型课题,直接用gamultiobj完全够用,它的默认算子就是基于NSGAII思路实现的,只要写好目标函数并设置好options即可:

options = optimoptions('gamultiobj', ... 'PopulationSize', 100, ... 'MaxGenerations', 200, ... 'Display', 'iter'); [x, fval] = gamultiobj(@(x) objFunc(x, evParams), N, [], [], [], [], lb, ub, options);

但如果你的研究需要修改遗传算子、自定义约束处理逻辑、追踪每一代的种群变化,或者对比不同交叉变异策略的效果,那自编NSGAII就是必须的。这个项目中我采用自编实现,一方面是为了完全可控的算法行为,另一方面方便替换成NSGAIII、MOEA/D等其他算法做对比实验。

6. 实验结果分析与避坑指南

6.1 Pareto前沿与最优折中解

运行仿真后,Matlab可以绘制出Pareto前沿图,横轴是用户充电成本,纵轴是负荷波动方差。在我的测试算例中,Pareto前沿呈现明显的L形弯曲:成本从300元降到200元附近时,负荷方差的增加相对平缓;但成本继续往下压,负荷方差会急剧恶化。这个形态说明两个目标之间存在一个“拐点”,工程上叫做Knee点。

选择Knee点作为最终充电方案时,可以遵循最小距离法:把两个目标分别归一化后,计算Pareto解集中距离理想点(两个目标都取最优的点)最近的个体。这个个体在Matlab中通过排序筛选即可得到,不需要额外复杂的操作。

6.2 优化前后充电负荷曲线对比

绘制对比图是整个项目最有说服力的部分。用同一套基础负荷数据,分别绘制无序充电、谷时段集中充电(不加优化、只靠电价引导)、NSGAII优化充电三种场景下的小区总负荷曲线。

无序充电场景在19:00到20:00之间会形成一个明显的新峰值;谷时段集中充电虽然把主峰移到了凌晨02:00左右,但因为所有车一窝蜂涌到谷段开始充电,负荷曲线同样出现尖锐的“二次峰”。NSGAII优化的结果则是一方面把大部分充电电量引导到谷段,另一方面通过错峰安排让充电负荷在整个谷段内铺开,曲线形态最平缓。这就是多目标优化相比简单价格引导的本质优势。

6.3 常见问题与调试技巧

调试NSGAII代码时,最容易遇到的问题集中在三类。

第一类是种群严重不收敛,Pareto前沿形状很乱。这通常不是算法问题,而是约束处理写错了。我在调试时会在每次迭代后输出可行解的数量和约束违反量的均值,如果可行解比例长期低于30%,优先检查约束是否过严,或者初始种群是否全部落在了不可行区域。

第二类是结果出现“假最优解”,所有个体的充电开始时间都挤在边界值上。这通常是决策变量编码范围设置不当。比如把可充电时段设成了0到24,但实际上每辆车18:00才能开充,算法只能在18:00到24:00这段可用的搜索空间里随机探索,其余部分全是无效搜索。解法是把每辆车的决策变量搜索边界收紧到它的实际可用时间窗口。

第三类是运行速度慢到无法接受。除了前面提到的向量化计算,还有一个容易被忽略的点:非支配排序计算的调用次数。如果每一代的每个个体都重新对整个种群计算支配关系,复杂度是O(N³)。正确的做法是用稀疏矩阵记录支配关系和被支配计数,只更新发生变化的部分。N=100时感受不明显,N=500以上差别就很显著了。

另外提醒一个实操细节:Matlab的随机数生成器默认是全局共享的,跑参数对比实验前一定要设置随机器种子,否则每次优化结果都不同,无法公平对比:

rng(42); % 固定随机种子,保证实验结果可复现

6.4 项目扩展方向

这个项目做完之后,我感受最深的不是NSGAII算法本身,而是建模选型对问题求解的巨大影响。后续可以从几个方向扩展:考虑充电功率的连续可调,把“每辆车每个时刻的充电功率”作为决策变量,问题就从一维时间调度变成时空二维调度;加入电动汽车到电网的放电功能后,目标函数还要增加电池损耗成本;如果研究区域包含多个充电站,还可以把“去哪个充电站充”作为离散决策变量,构成一个混合整数多目标优化问题。

这些扩展用NSGAII框架都能接得住,改动主要集中在编码方式和目标函数计算模块。算法层面的非支配排序、拥挤度计算、精英保留这些核心机制,一行都不用动。这也是NSGAII在工程研究里如此普及的深层原因——它像一个稳定的操作系统,上层应用再怎么变,底层逻辑始终可靠。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询