ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

电动汽车有序充电多目标优化:NSGA-II算法与Matlab实战解析

电动汽车有序充电多目标优化:NSGA-II算法与Matlab实战解析 做电动汽车充电负荷优化尤其是用峰谷分时电价去引导有序充电我第一反应就是多目标优化。原因很简单电网要削峰填谷用户要省电费这俩目标天然冲突你又不能只盯着一边。NSGA-II这个算法在处理这种带约束的组合优化问题时效果稳定而且代码实现也不复杂配合Matlab的矩阵运算跑起来非常顺手。这篇博客我想把整个项目的思路从头到尾捋一遍从问题建模到NSGA-II的每个关键环节再到Matlab里怎么落地以及我在实战调试中踩过的坑一次性说清楚。不管你是刚接触多目标优化还是已经能跑通基本遗传算法都可以把这套框架拿过去改改数据直接用。1. 这个项目的核心思路到底在解决什么问题1.1 电动车的痛点无序充电的代价先看一个最常见的场景一个住宅小区有100辆电动汽车大家下班回家后大概18:00到19:00陆续插上充电枪充电功率按3.5kW算100辆同时充就是350kW的额外负荷正好叠加在居民晚高峰负荷曲线上。配变容量要是按峰值选这一下就可能顶到红线变压器过载、线路损耗增加、保护开关跳闸都是连锁反应。从用户角度看无序充电也不划算。晚高峰大部分处于峰时电价充一度电可能接近1.2元一晚上充30度电就是36块钱如果等到半夜谷时再充电价可能只有0.3元左右同样的电只需9块钱。价格差摆在那用户一定会被引导但问题来了如果所有车主都只响应价格全部挪到凌晨0点充电那凌晨又会出现一个新的负荷尖峰谷时电价反而被“充爆”。所以充电负荷优化本质上是把每辆车的充电时段当作决策变量在满足所有车充电需求的前提下同时照顾用户费用和电网负荷曲线平滑程度。这件事自己拍脑袋排是排不出来的必须借助优化算法。1.2 峰谷分时电价为什么能引导充电行为峰谷分时电价本质上是用价格这个杠杆去改变用电行为。电网把一天划分成峰时、平时、谷时峰时段电价高、谷时段电价低用户为了省钱自然会愿意把能平移的负荷挪到低价时段。这个逻辑很简单但在大规模电动车接入的场景里单纯靠价格信号会造成“响应同步”的问题。这就需要优化算法介入在给定的分时电价下算法要同时算出每一辆车最适合的充电开始时间、充电时长以及哪几个时段充电。注意这里并不是所有车都跑到谷时充电因为车有自己的入网和离网时间有的车第二天早上要用车必须在离开前充够电有的车电池容量大谷时长度可能不够还有小区变压器容量上限同一时段也不能充太多车。这些约束叠加在一起分时电价起的是“引导方向”的作用真正把计划排出来的是多目标优化。1.3 为什么偏偏要选多目标优化和NSGA-II我一开始也想偷懒把两个目标用加权系数合并成一个单目标然后扔给遗传算法跑。试了几次发现权重特别难定而且每次权重一变最优解就在Pareto前沿上滑来滑去你根本拿不到完整的前沿形状很难向别人解释“为什么这个解是合理的”。换成NSGA-II就不一样了。它是基于非支配排序的精英保留遗传算法一次运行能同时输出一整条Pareto前沿也就是一组互不支配的折中解。你可以在前沿上选点想要用户费用低就选中偏左的解想要负荷波动小就选中偏下的解想要兼顾就选拐点附近的解。相比单目标加权这种方式信息量大得多也符合工程决策习惯。而且NSGA-II的快速非支配排序和拥挤度距离机制在解集多样性和算法速度之间平衡得非常好Matlab实现起来也就几百行核心代码。2. 数学建模把充电问题变成优化问题2.1 目标函数设计算钱和算电网波动建模第一步是明确决策变量。我采用的是二进制编码方式设优化周期为24小时每个时段取1小时共T24个时段小区里有N辆车。对每辆车i在时段t是否充电用变量x(i,t)表示1代表充电0代表不充电充电功率固定为P_i3.5kW。如果每个时段的功率固定那么每天充电量就等于充电时段数乘上功率再乘上效率。第一个目标函数是用户总充电费用最小化C_cost Σ_{i1}^{N} Σ_{t1}^{T} x(i,t) × P_i × price(t) × Δt其中price(t)是t时段的电价Δt1小时。这个函数直接衡量所有车一天充电总共花了多少钱。第二个目标函数我一开始用的是电网负荷峰谷差也就是max(L_t) - min(L_t)L_t是t时段的总负荷等于基础负荷加上所有充电功率之和。但调试中发现峰谷差只盯着最高点和最低点会忽略负荷曲线的整体形态。比如解A峰谷差可能小但是中间时段有剧烈波动的次高峰电网同样不舒服。后来我改成负荷方差或者负荷方差系数这样能让整条曲线更平稳。实际代码里我用的是方差D_var var(L_t)当然你也可以在Pareto前沿上用不同的目标形式做对比这个后面会讲。2.2 约束条件电池、时间、功率一个都不能少约束直接决定了问题现实不现实也是代码里最容易被写崩的地方。我整理了四个关键约束。第一是时间窗约束每辆车i有自己的到达时间t_arr_i和离开时间t_dep_i只有在这个区间内才允许充电其余时段x(i,t)必须为0。这个用矩阵掩码就能搞定。第二是充电量约束每辆车在离网时要达到期望SOC。假设车辆电池容量为Cap_i充电效率为η起始SOC为s_start_i期望SOC为s_target_i那么充电总量必须满足Σ_t x(i,t) × P_i × Δt × η ≥ (s_target_i - s_start_i) × Cap_i注意这里是不等式因为多充一点问题不大只要不超过100%就行。第三是功率约束任一时刻所有充电车辆的总功率不能超过变压器可用的充电容量上限P_limit即Σ_i x(i,t) × P_i ≤ P_limit这个约束如果太紧可能会导致无解所以我一般把它作为一个软惩罚项而不是硬性约束。第四是SOC上下限约束每辆车的SOC任何时候不能低于最低值也不能超过100%。但如果充电过程是线性且一直充到离网这个约束在充电量约束里已经基本覆盖所以我在实际代码里没有单独再写。2.3 电价模型与典型场景假设为了让读者能复现我直接把场景参数全部列出来。住宅小区基础负荷选取的是夏季某典型工作日负荷曲线峰值出现在19:00约180kW凌晨谷负荷约60kW。100辆电动车电池容量统一为60kWh充电功率3.5kW充电效率0.9。每辆车的起始SOC在0.2到0.6之间随机分布期望SOC统一为0.9。到达时间取17:00到20:00之间的随机数离开时间取次日7:00到9:00之间的随机数。电价采用三段式峰谷分时电价峰时段10:00-15:00、18:00-21:00电价1.2元/kWh平时段7:00-10:00、15:00-18:00、21:00-23:00电价0.7元/kWh谷时段23:00-次日7:00电价0.35元/kWh。注意电价和基础负荷在18:00-21:00同时进入高峰这正是问题最棘手的地方最需要用车的时刻恰恰是电网最紧张、电价最贵的时刻。这种场景设计让优化有足够的张力Pareto前沿不会缩成一个点。3. NSGA-II算法原理解密与Matlab实现思路3.1 快速非支配排序与拥挤度距离NSGA-II的核心有两个非支配排序和拥挤度距离。非支配的概念很好理解如果一个解的每个目标都不比另一个解差并且至少存在一个目标比另一个解更好那么这个解就支配另一个解。比如有两个解解A的费用是2000元、负荷方差是300解B的费用是1800元、负荷方差是350那么A尽管不支配B因为B费用更低但B也不支配A它们就是非支配关系。把所有解按照支配关系分层不能被任何解支配的那群解属于Rank 1去掉Rank 1后剩下的解中再不能被其他解支配的那群解属于Rank 2以此类推。非支配排序之后还要对同一Rank内的解计算拥挤度距离。这个距离用来描述解在目标空间里的稀疏程度一个解的拥挤度距离越大说明它周围越空越值得保留来维持解集的多样性。计算方法是针对每个目标把该Rank内的解按目标值排序然后计算相邻解之间目标差的和边界解的拥挤度设为无穷大。Matlab实现时我通常会写一个函数输入种群所有个体的目标值输出每个个体的Rank和CrowdingDistance。如果不想自己写排序也可以直接调用全局优化工具箱但自己写一遍能更清楚算法细节对后续改进帮助很大。3.2 遗传操作编码、交叉、变异怎么设计这是整个实现里最个性化的一部分。我用二进制编码将每个个体表示成一个长度为N×T的染色体向量每一位表示对应车辆对应时段是否充电1表示充0表示不充。比如100辆车×24时段染色体长度是2400这个规模对NSGA-II来说不算特别大但已经完全超出“全部随机搜索”能处理的范围。交叉操作我测试过两种一种是均匀交叉即以0.5的概率交换两个父代的每个基因另一种是单点/分段交叉。实测下来均匀交叉在2400维二进制串上太“暴力”会把已有的充电时段组合打散导致子代违规率很高。我最终采用的是分段均匀交叉把染色体按车辆分成N段每辆车内部的充电时段作为一个整体以一定概率交换整段基因这样能保留车辆级的充电特征。变异操作更简单以变异概率pm0.01对每个基因取反。但变异之后不能直接扔进适应度函数因为随机取反可能让某辆车充电时长不足或者过量。所以变异之后接了一个修复函数如果某辆车充电时长低于需求就在它的可用时间内随机补足如果高于需求就随机删掉多余时段。修复函数是保证算法收敛的定海神针没有它非支配排序会产生大量不可行解前沿乱得没法看。3.3 主循环与精英保留策略NSGA-II的主循环是“生成-合并-筛选”的过程。每一代父代种群P通过锦标赛选择选出父本经过交叉变异生成子代种群Q然后把P和Q合并成2N大小的种群R。对R进行非支配排序按Rank从1开始往下一代种群填充直到填到N个为止。如果填到最后一个Rank时空间不够就根据拥挤度距离从大到小选择把距离最大的个体留下。这个步骤里精英保留策略保证了最优解不会因为交叉变异的随机性而丢失。合并父代和子代再一起排序意味着上一代的优秀个体总能参与下一代的竞逐算法收敛性有保障。我在Matlab里的主循环大致长这样for gen 1:max_gen % 锦标赛选择 parent tournament_select(pop, pop_size); % 交叉变异生成子代 offspring crossover(parent, pc); offspring mutate(offspring, pm); % 修复子代染色体 offspring repair(offspring, data); % 合并父代和子代 combo [pop, offspring]; % 非支配排序拥挤度计算 combo non_dominated_sort(combo); % 精英保留生成下一代 pop select_next_generation(combo, pop_size); end这里每个函数都对应一个独立的m文件调试的时候可以单步跑通一个函数再跑下一个。4. Matlab代码实现的核心环节拆解4.1 程序整体框架准备数据、初始化种群、进化写这类项目我历来建议先搭骨架再补细节。程序最外面是主脚本负责设置参数、加载数据、初始化种群、调用进化循环、最后输出结果。数据结构我用的是Matlab的struct数组每个个体有position1×N*T向量、cost1×2目标向量、rank、crowding这四个字段。这种结构虽然不是效率最高的但可读性特别好后面要加新的字段也方便。初始化种群时不能全随机生成否则大部分个体充电需求不满足后续修复代价太大。我的做法是对每辆车先在它的可用时间窗内随机选择若干个时段使得初始计划恰好满足充电需求可用时间窗通过一个available矩阵给出来。这样生成的每个个体天然满足时间窗和充电量约束种群一开始就是可行解进化过程舒适很多。数据准备方面我用了base_load_data数组存放24小时的基础负荷price数组存放24小时的电价其余参数放在data结构体里。特别提一句Matlab里如果直接用循环算100辆车24小时的目标函数会很慢我所有目标函数都写成向量化运算一次sum和reshape就搞定运行速度基本可以接受。4.2 适应度函数代码解析适应度函数是目标函数的封装。我给它起名叫calc_objective输入一个个体染色体和data结构体输出该个体的两个目标值。代码核心部分很简单function [cost, var_load] calc_objective(x, data) % x: 1 x (N*T) 的二进制向量 X reshape(x, data.N, data.T); X X .* data.available; % 时间窗约束直接置0 P_charge sum(X .* data.P_i, 1); % 每个时段总充电功率1 x T L data.base_load P_charge; % 总负荷 cost sum(data.price .* P_charge) * data.delta_t; var_load var(L); end注意到充电量约束并没有在这里直接判断原因是初始化种群已经保证了每个个体不违规但交叉变异后可能违规所以我在repair函数里处理。这样做的优点是适应度函数干净没有罚函数参数要调整缺点是轻微依赖修复函数的质量如果修复函数写不好后续种群会积累违规解。如果想把充电量约束做成软惩罚可以对每个个体再算一个violation把惩罚量加到费用目标上但这会引入权重我为了保持Pareto前沿的纯净性没有这么做。4.3 关键参数设置与敏感性分析参数设置我用过很多组最终稳定在下面这组种群大小200进化代数150交叉概率0.9变异概率0.01锦标赛选择大小2。2400维的染色体种群太小比如50很快就早熟种群太大比如500计算量明显增加但前沿改善有限。150代对于这个规模在普通i5笔记本上跑大概要两三分钟属于可以接受的范围。我还做了一组交叉概率敏感性测试0.7以下收敛变慢前沿尾部缺失0.95以上子代破坏严重非支配层级变多整体效率下降。变异概率我一般固定0.01如果发现陷入局部前沿会临时提高到0.05运行几代再降回来相当于变相做了自适应变异。5. 仿真结果怎么看Pareto前沿与充电负荷优化效果5.1 Pareto前沿的解读跑完150代我习惯先把最后一代种群的Pareto前沿画出来。X轴是用户总费用Y轴是负荷方差每个点是一个非支配解。由于两个目标数值范围不一样画图时有时候会把两个目标归一化到0-1之间但在解释结果时还是用原始数值更直观。典型的Pareto前沿是一个向左下凸的弧线。最左边端点对应“几乎不考虑电网波动”的解所有车尽量在谷时充电费用可以压到很低但负荷方差很大因为谷时集中充电导致凌晨出现高峰最下边端点对应“几乎不考虑费用”的解所有车在停留时间窗内尽量平滑充电负荷曲线很稳但用户可能在峰时也充电费用很高。我们需要的前沿就夹在这两个极端之间。从工程决策角度看我会选在曲率拐点附近的解用Matlab里knee point detection方法找或者直接经验看当费用从低到高增加一点负荷方差就能明显下降的那个位置就是性价比最高的解。5.2 优化前后充电负荷曲线对比再看时间序列曲线。无序充电的情况下总负荷会在19:00附近出现一个很高的尖峰基础负荷本来就有180kW加上充电负荷可能有300多kW峰谷差非常难看。经过NSGA-II优化后大部分车的充电时段被挪到23:00以后尤其是2:00-6:00深谷时段充电负荷在这里缓慢增加与基础负荷叠加后整体曲线明显平坦。但注意优化后的曲线并不是完全平坦的因为有些车停留时间很晚或离开很早必须在晚上21:00前或早上7:00前完成充电因此早晚还是有小幅凸起。这说明约束导致了不可避免的短期负荷凸起是现实情况没必要强行压平。好的Pareto前沿上我们可以挑一个折中解画出对应的充电计划甘特图或者充电功率时间曲线。Matlab里用stairs命令画功率曲线很好看社区里很多人这么做。5.3 电价引导的实际效果与权衡我顺便分析了不同峰谷比情形。将谷时电价固定在0.3元峰时电价分别设为0.8、1.2、1.6元跑同一组数据。结果很有意思峰时电价越高Pareto前沿上费用最低的端点会把更多充电行为压到谷时导致凌晨负荷峰值更高而峰谷比很小时算法反而更倾向于在平时段或前后分散充电电网曲线更平滑。这说明峰谷分时电价不是越大越好。电价引导如果过度会造成“谷峰转移”问题把晚高峰期挪到凌晨虽然削峰了但可能填谷过度形成新峰。引入多目标优化后我们能看到这种权衡的量化结果对于电网制定电价值得参考。这个模型还可以继续扩展把碳排放、电池寿命衰减、充电桩利用率都作为额外目标NSGA-II结构不变只需要加目标函数。6. 我在实际调试中踩过的坑与解决心得6.1 典型问题1初始种群生成导致不可行解第一次写初始化种群时我图省事直接随机生成0/1矩阵然后祈祷修复函数能把它们救回来。结果发现修复函数每代要处理百分之三四十的个体而且修复后的“随机解”会改变染色体结构导致遗传算法几乎退化成纯随机搜索。后来我把初始化改成“可用时段内随机选取满足时长需求的时段”种群可行率直接接近100%修复压力骤降。具体做法是对每辆车先计算需要充电的小时数再用randperm从可用时段数组中随机抽取对应的时段标记为1。这样每个个体天然满足充电量需求代码省心很多。对于离开时间早的车有时可用时段数不够我会把它期望SOC调低一点这样每个车都有可行解。6.2 典型问题2NSGA-II收敛慢或早熟有段时间代码跑30代就停在一条很差的Pareto前沿上怎么调变异率都没用。后来发现问题出在目标函数数值量纲差异费用是几千负荷方差是几万非支配排序时方差差异很容易主导判断费用几乎不起作用。我给两个目标做了归一化处理让它们都在0-1范围内情况立刻好转。另外交叉算子从均匀交叉换成按车辆分段的交叉也明显改善了收敛速度。分段交叉能保留每辆车内部充电时段的规律搜索方向更接近“车辆级别的调整”而不是逐基因乱飞。如果你发现前沿尾部稀薄可以增大种群到300或尝试多项式变异。6.3 干货建议这样调参和扩展模型最后分享几个我用真金白银换来的建议。第一先跑小规模案例比如10辆车打开调试模式观察每一代的Rank1个体数量确认无误后再跑100辆第二每一代都把当前Pareto前沿和上一代对比画成动态图很快能发现是不是早熟第三不要迷信工具箱内置的ga实现自定义NSGA-II可以让你自由控制编码和修复这对带约束组合优化太重要了。这个项目后续还可以往两个方向扩展一是把充电功率从固定值改成连续可调用实数编码同时优化每时刻功率这样目标函数会更平滑二是加入V2Gvehicle-to-grid模式把电动车从纯负荷变成可控储能目标函数里加上放电收益和电池损耗。我目前正在做V2G方向已经碰到不少新坑等跑通之后再单独整理一篇出来。
返回列表