1. 项目背景与核心痛点分析
1.1 为什么电动汽车充电负荷优化是刚需
做配电网规划或者充电设施运营的朋友应该都有感触,最近这两年电动汽车渗透率上来了,小区充电桩报装量猛增,台区变压器过载的问题开始频繁冒头。问题最典型的表现就是晚高峰时段——大家下班回家顺手把车插上充电,正好叠加空调用电高峰,一个630kVA的配变可能同时要喂几十辆车的充电功率,夏季高温天气尤其紧张。
这种情况如果只靠扩容解决,经济性很差——一台配变增容的预算可以买不少储能了,而且审批周期长,远水解不了近渴。更合理的思路是把手里的充电负荷当作可调节资源,通过价格信号引导车主避开尖峰时段充电,把用电需求平移到低谷时段。
但这里有个容易被忽略的难点:单纯做削峰填谷,可能会牺牲车主的方便性,延长充电时长;如果只追求车主充电费用最低,又可能造成新的负荷集中,加剧配变压力。所以这个优化问题天然是多目标的——电网侧要削峰填谷、降低网损,用户侧要节省充电费用、尽量维持SOC充足,两边诉求是有冲突的。
这就是为什么需要用多目标优化算法,而不是简单套一个单目标求解器。
1.2 峰谷分时电价为什么能引导充电行为
峰谷分时电价本质上是在时间维度上对不同用电行为做价格差异化。它的经济效益可以从两个层面去看:对电网公司来说,低谷时段发电机组利用率和输配电资产利用率都偏低,如果把负荷引导到低谷,相当于在不增加固定资产投入的前提下多卖电;对用户来说,谷段电价和峰段电价的差值往往能达到两三倍,夜间充电一个月省几十甚至上百元,积少成多也是一笔钱。
所以峰谷电价的引导逻辑是:让电价信号直接进入车主的充电决策函数,把一份本来无序的充电需求,变成响应价格的柔性负荷。在Matlab仿真中,我们需要把这种价格-行为联动关系量化成数学模型,而不是停留在文字描述上。
2. 整体建模框架与优化目标设计
2.1 充电负荷模型怎么搭才合理
做这个课题,第一步不是急着写NSGAII的代码,而是先搭好研究场景的数据底座。我在实际项目中采用的建模框架是这样的:
首先是电动汽车集群参数。要明确研究范围内有多少辆电动车、什么类型(私家车、出租车还是物流车)、各自的电池容量、续航里程、到达离开时间分布。私家车最常见的场景是上班通勤——早上八点开到单位,下午六点下班回家,到家用车时SOC大概还剩30%-60%不等。这些参数不是拍脑袋定的,可以参考新能源汽车国家监测与管理平台的统计数据,也可以用蒙特卡洛方式模拟车主的随机行为。
其次是充电功率特性。慢充桩一般是7kW交流,快充桩是60-120kW直流。小区场景以慢充为主,城市公共快充站以快充为主。不同场景下充电负荷优化策略会有差别——快充车辆停留时间短,价格引导的调节空间有限;慢充车辆一停就是十几个小时,可平移的充电窗口非常充裕。
最后是配电网网架约束。如果研究范围小到单台区,台区容量就是边界条件;如果大到馈线层面,还要考虑潮流约束和电压偏差。对于初学者,我建议先做单台区或者节点配电网的简化模型,把运行约束写成线性或简单非线性不等式,再逐步往精细化走。
2.2 两个目标函数的具体构造方式
接下来重点说说目标函数。
目标一:用户充电费用最小
我们以配电网辖区内的私家电动汽车集群为研究对象,设研究周期为一天,按15分钟为一个优化时段,共96个时段。目标函数之一是用户总充电费用:
[ f_1 = \sum_{i=1}^{N}\sum_{t=1}^{96} c(t) \cdot P_{i,t} \cdot \Delta T ]
其中 (c(t)) 是t时段的分时电价,(P_{i,t}) 是第i辆车的充电功率决策变量,(\Delta T) 是时段间隔。这个目标直接反映了车主对充电成本的敏感度。
目标二:负荷曲线削峰填谷
电网侧的目标是希望叠加充电负荷后的总负荷曲线尽可能平坦,削峰填谷效果好。比较常用的做法是让充电负荷跟随基础负荷的低谷区,或者让综合负荷的标准差最小。我通常用:
[ f_2 = \sqrt{\frac{1}{96}\sum_{t=1}^{96}(P_{base,t}+P_{ev,t} - \overline{P})^2} ]
其中 (P_{base,t}) 是台区基础负荷,(P_{ev,t}) 是电动汽车总充电功率,(\overline{P}) 是当日综合负荷均值。这个目标越小,说明叠加充电负荷后曲线越平稳,对配变和网架的压力越小。
除了这两个主要目标,实际研究中往往还要加约束条件:
- 充电功率上下限约束:(0 \le P_{i,t} \le P_{i,max})
- 电池SOC约束:(SOC_{i,min} \le SOC_{i,t} \le SOC_{i,max}),避免过充过放
- 车主充电需求约束:离开时SOC要达到车主设定值
- 台区变压器容量约束:任意时段的综合负荷不能超过配变额定容量
这些约束是硬性的,NSGAII处理它们的方式是在解码时加惩罚项,或者在编码环节从机制上保证可行性。
2.3 为什么说这是一个标准的双目标优化问题
很多人一听到“多目标优化”就发怵,其实把问题拆开看并不复杂。所谓多目标,就是我们的解不是一个数值,而是一组相互冲突的性能指标向量。
以本课题为例,如果只优化用户充电费用,优化器会尽最大可能把所有充电需求堆到深夜谷电时段,哪怕这样会让凌晨出现新的充电高峰,形成二次尖峰;如果只优化削峰填谷,优化器可能强制用户凌晨三点开始充电,弄得车主第二天要出门时电还没充满。任何单目标视角都会顾此失彼。
双目标的好处是能输出一整个Pareto前沿解集——比如一组充电方案,有的费用低但曲线波动稍大,有的曲线很平但电费高一点。到底是费用优先还是电网优先,决策者可以拿前沿解去权衡。这比传统加权系数法高明在:加权法每跑一次只能得到一个解,而且权重选择本身就很主观;NSGAII跑一次就能拿到几十上百个分布均匀的非支配解。
3. NSGAII算法原理与选型理由
3.1 快速非支配排序和拥挤度距离是怎么回事
NSGAII也就是带精英策略的快速非支配排序遗传算法,是多目标进化算法里应用最广泛的一支。它的核心机制可以拆成四块:
快速非支配排序。把种群里的个体按照Pareto支配关系分层——第一层是当前群体中的所有非支配解,第二层是去掉第一层后剩下的非支配解,以此类推。分层越靠前,说明个体综合表现越好。这里面有一个小优化点:排序时用O(MN²)的快速排序方法,比传统多次比较省时间,种群规模大的时候差异很明显。
拥挤度距离计算。同一层里的个体之间需要有“分散度”的概念——我们希望解集均匀铺满Pareto前沿,而不是扎堆在某一段。拥挤度距离的计算方法是:对每个目标,把该层个体按目标值排序,边界个体的距离设为无穷大,中间个体的距离是相邻两个个体目标函数值的归一化差之和。
精英保留策略。父代种群和子代种群合并后,按照先非支配排序层级、再拥挤度距离的顺序依次填充新一代种群。这保证了解的优秀基因不会在进化过程中丢失,是NSGAII比第一代NSGA性能明显提升的关键改进。
锦标赛选择 + 模拟二进制交叉 + 多项式变异。选择环节用二元锦标赛,交叉环节对于实数编码的决策变量常用SBX算子,变异用多项式变异PM。
3.2 为什么在Matlab里选NSGAII而不是其他算法
这个问题经常被问到。我的答案是:不是NSGAII在所有场景都是最优,而是在这个课题里它最合适。
首先,和经典GA比,NSGAII天然支持目标数组,不需要人工把多个目标加权成一个。其次,和粒子群PSO相比,NSGAII的种群机制天生适合多目标搜索,PSO虽然收敛快,但多目标版本需要额外维护外部档案和全局最优选择策略,调参复杂度高。再者,和后来的NSGAIII相比,对于两到三个目标的优化问题,NSGAIII的优势并不明显,反而要额外设置参考点,代码复杂度更高,对新手不太友好。
另外,Matlab的Global Optimization Toolbox里有现成的gamultiobj函数,底层实现就是NSGAII的变体,可以直接调用。对于想快速跑通流程的人来说,用gamultiobj非常省事;如果希望完全掌控细节、做二次开发,自己写NSGAII框架也不难。本课题我倾向于自己写主框架,但借用Matlab自带的交叉变异函数,灵活性最高。
注意:
gamultiobj自带的默认参数未必适合你的具体问题,种群大小、交叉概率、变异概率、Pareto前沿点数这些都需要针对具体场景调整。
4. Matlab代码实现全流程解析
4.1 初始化参数与数据准备
代码实现的第一步是搭建数据环境。下面给出一个可直接运行的参数初始化模板:
%% 基础参数设置 clc; clear; close all; %% 时间参数 deltaT = 15/60; % 单位时段长度,15分钟 T = 96; % 一天96个时段 %% 车辆参数 N_car = 50; % 电动汽车数量 batteryCap = 60; % 电池容量 kWh P_max = 7; % 充电功率上限 kW eff = 0.92; % 充电效率 SOC_init = 0.3; % 初始SOC SOC_need = 0.9; % 离开时目标SOC %% 分时电价,单位元/kWh price = 0.55*ones(1,T); price(1:24) = 0.28; % 谷段 00:00-06:00 price(73:96) = 0.28; % 谷段 18:00-24:00(这里仅示意,按实际方案调整) price(25:48) = 0.85; % 峰段 06:00-12:00 price(49:72) = 0.85; % 峰段 12:00-18:00 %% 基础负荷 kW load_base = 120 + 60*sin((1:T)/T*pi);注意上面的电价分段仅为示意,实际研究请按照你所在地区的真实分时电价文件填写,不同省份峰谷时段划分差异很大。
4.2 决策变量编码与解码逻辑
决策变量的编码方式是整个程序的关键设计之一。很多人写优化代码容易在这里翻车——决策变量设计得不好,会导致大量不可行解,算法半天跑不出合理结果。
我的做法是采用实数编码的充电起始时段矩阵。假设每辆车只需决定一个充电起始时段 (t_{start,i}) 和充电持续时间 (n_i),那么决策向量就是 (2N) 维实数。解码时:
- 起始时段向下取整到1-96范围内的整数
- 持续时段数由所需充电电量和充电功率决定:
E_need = (SOC_need - SOC_init) * batteryCap; % 所需充电电量 n_hours = ceil(E_need / (P_max * eff)); % 至少需要充满的小时数 n_periods = ceil(n_hours * 4); % 按15min时段换算在这个方案里,持续时间其实是由初始SOC和目标SOC硬性决定的,不是自由变量。这样做有一个好处:保证每个解天然满足车主的充电需求约束,不必额外加惩罚项。
4.3 NSGAII主循环的编写思路
这里我贴出主循环的核心部分,用自实现NSGAII的方式展示关键逻辑:
%% 算法参数 popSize = 200; % 种群大小 maxGen = 200; % 最大迭代次数 pc = 0.9; % 交叉概率 pm = 0.1; % 变异概率 %% 初始化种群 pop = zeros(popSize, N_car); % 每辆车一个起始充电时段变量 for i = 1:popSize % 随机生成合法的充电起始时段,范围限制在可充电区间 pop(i,:) = randi([1, 72], 1, N_car); end %% 主循环 for gen = 1:maxGen % 计算目标函数 [f1, f2] = evaluate_objectives(pop, price, load_base, ...); % 快速非支配排序 [fronts, rank] = fast_non_dominated_sort(f1, f2); % 计算拥挤度距离 crowding = crowding_distance(f1, f2, fronts); % 锦标赛选择 parentIdx = tournament_selection(rank, crowding, popSize); % 模拟二进制交叉与多项式变异 offspring = sbx_crossover(pop(parentIdx,:), pc); offspring = polynomial_mutation(offspring, pm); % 精英保留策略:合并父代与子代 combinedPop = [pop; offspring]; [f1c, f2c] = evaluate_objectives(combinedPop, price, load_base, ...); [fronts_c, rank_c] = fast_non_dominated_sort(f1c, f2c); crowding_c = crowding_distance(f1c, f2c, fronts_c); pop = elitist_selection(combinedPop, rank_c, crowding_c, popSize); end在实际项目中,我会把evaluate_objectives单独拆成一个函数文件,内部处理从决策变量到充电功率矩阵的转换过程。这里提醒一个容易忽略的细节:种群初始化时一定要限制充电起始时段的合法范围。如果起始时段太晚,车主离开前充不满,这个个体就是不可行的。限制合法范围之后解的质量会明显提升。
4.4 目标函数具体计算流程
目标函数计算的完整流程分三步:
第一步:由决策变量解码出每辆车的充电功率时间序列:
function P_ev = decode_chromosome(pop, ...) % pop 是种群矩阵,每行是一个个体 % 返回 P_ev: (个体数 * N_car * T) 三维矩阵 % 或者用循环逐个体计算二维 (N_car * T) 矩阵 end第二步:计算每辆车的充电费用,然后加总得到用户总费用目标 (f_1)。
第三步:把各时段所有车辆充电功率加总,叠加基础负荷,计算综合负荷曲线的标准差作为 (f_2)。
function [f1, f2] = evaluate_objectives(pop, price, load_base, params) nPop = size(pop, 1); f1 = zeros(nPop, 1); f2 = zeros(nPop, 1); for k = 1:nPop P_ev = decode_chromosome(pop(k,:), params); total_ev_load = sum(P_ev, 1); % 1 * T total_load = load_base + total_ev_load; f1(k) = sum(price .* total_ev_load) * deltaT; f2(k) = std(total_load); end end从代码可以看到,第二目标我用的是综合负荷标准差而不是单纯峰谷差,这样更贴合削峰填谷的本质——单纯控制峰谷差可能造成功率的频繁波动,标准差则能同时兼顾曲线平滑度。
5. 仿真结果分析与案例分析
5.1 Pareto前沿解集的呈现与解读
以50辆私家车、台区基础负荷峰值150kW的场景为例,跑完200代之后,得到的Pareto前沿大致呈现如下特征:
- 前沿是一条单调递减的曲线:费用目标 (f_1) 小的解,负荷标准差 (f_2) 必然偏大;反之亦然
- 极端解A(费用最低)的充电需求几乎全部挤在凌晨0-6点,用户平均充电费用可降低到无优化前的55%左右,但凌晨会出现约80kW的充电尖峰
- 极端解B(负荷最平)的充电分散在晚上和凌晨,负荷标准差小,但部分车辆在峰段充电,费用偏高
实际操作中建议这样选解:在Pareto前沿上找拐点区域——可以简单用边际替代率来判断。比如费用每降低10元,标准差要恶化5kW以上,说明已经过了效益拐点,再往下优化得不偿失。一般来说,选择靠近前沿中部偏左15%位置的解,综合表现往往最好。
5.2 对比实验:优化前后效果评估
光画个前沿图不够,要体现研究的价值,最好做三组场景对比:
| 场景 | 用户平均充电费用(元) | 最大综合负荷(kW) | 负荷标准差(kW) | 配变负载率峰值 |
|---|---|---|---|---|
| 无序充电 | 38.5 | 232 | 20.8 | 108%(过载) |
| 仅分时电价(无优化) | 31.2 | 215 | 17.5 | 96% |
| 分时电价+NSGAII优化 | 24.6 | 188 | 9.7 | 84% |
表格里的数据是我跑过的某个典型算例结果,具体数值会因为车辆参数和基础负荷假设不同而变化,但趋势是稳定的:仅仅引入分时电价,就能利用车主的自发响应降低费用;但真正让设备利用率回归安全的,是NSGAII充电调度这一层。
顺带说一个容易被忽略的点:无序充电场景中配变负载率超过100%,在真实工程里意味着台区要报增容项目,这笔费用动辄几十万元。而一套基于价格引导的充电调度方案,几乎零边际成本就能释放大约15%-20%的配变余量,这个经济效益非常可观。
5.3 收敛性判断与算法稳定性验证
NSGAII代码写完之后,一个常见疑问是“怎么判断结果收敛了”。我的判断方法有两条:
第一,观察每一代Pareto前沿的超体积指标。超体积是指前沿解集与参考点之间围成的面积,它综合考虑了收敛性和均匀性。Matlab里可以自己写一个简易超体积计算函数,也可以用external archive的思路每次迭代记录历史最优前沿,看它的超体积是否在连续几十代内不再增长。
第二,重复跑多次算法,对比Pareto前沿的差异。如果两次运行得到的前沿基本重叠,说明算法稳定性好;如果每次结果差异很大,很可能是种群大小不够或者交叉变异概率不合适。我在实践中把种群从100调到200,前沿稳定性就有明显提升,再往上加收益就递减了。
6. 常见问题与调试经验实录
6.1 算法陷入局部最优怎么办
这个问题的典型表现是:跑了几次,得到的Pareto前沿始终挤在一个很小的区域,费用和负荷标准差都差不多。
优先排查三个地方:
- 变异概率是不是太低了。多项式变异算子的分布指数默认20,变异概率0.1,如果发现解集多样性不够,把变异概率提高到0.15-0.2试试
- 交叉算子是不是过早收敛。SBX交叉的分布指数影响到子代与父代的相似程度,分布指数小一点(比如15),子代偏离父代更多,搜索范围更大
- 种群初始化不够多样。检查随机种子范围,充电起始时段如果只限制在很小区间,初始种群自然多样性不足
6.2 约束条件总是被违反
我做这个项目时踩过最深的坑就是约束处理。起初我在目标函数里直接加惩罚项,结果惩罚系数很难调——太小起不到约束作用,太大等于把两个目标函数之外又塞了一个隐式目标,反而干扰Pareto搜索方向。
后来我改用两段式处理:
- 机制上保证可行:通过编码设计把充电需求约束直接内嵌(起始时段+需求电量共同决定充电时间段),这样无论如何个体都是满足充电需求的
- 边界约束用修复策略:如果个体解码后超出配变容量限制,直接将其充电起始时段平移到更晚时段,重新检查,直到满足为止
这种方式比惩罚函数法效率高得多,而且不会产生因为惩罚系数不当导致的不可控行为。
6.3 Matlab运行效率优化技巧
NSGAII动辄200代、200个体,每个个体都要计算96时段的负荷矩阵,循环嵌套容易让程序跑得很慢。我常用的加速手段有:
- 尽量向量化。把解码过程写成分批矩阵运算,避免逐个车辆内层for循环。比如用矩阵外积方式一次性生成所有车辆的充电功率二维矩阵,代码可读性稍微差点,但速度能提升数倍
- 预分配内存。所有涉及循环的大数组,运行前用
zeros预分配,避免动态扩容 - 并行计算工具箱。目标函数评价是天然的并行任务,用
parfor替代for,在多核机器上能加速3-4倍。需要注意的是parfor里不要有依赖前后迭代顺序的变量
7. 后续扩展方向
7.1 把单台区扩展到配电网多节点
单台区场景跑通之后,下一步可以扩展到IEEE 33节点等标准配电网测试系统。这时候第二目标就要从“负荷标准差”升级为“网损最小化”或“节点电压偏差最小化”,潮流计算需要用Matpower工具箱或自己写牛顿拉夫逊潮流求解器。约束条件也随之增加节点电压上下限、支路容量等。
这种扩展会带来一个问题:潮流计算非线性强,目标函数评价成本大幅上升。一个实用的变通做法是:先用直流潮流近似筛选解集,再对Pareto前沿上的最终解做精确交流潮流校验。
7.2 考虑车主的充电行为不确定性
现实世界里车主不会完全听调度——有些人赶时间必须立刻充,有些人临时改变出行计划。这里可以用鲁棒优化或机会约束规划,把车主响应概率分布纳入模型。对于Matlab实现来说,无非是在目标函数内部增加一个蒙特卡洛抽样层,但计算量会成倍增加,建议先用小规模场景验证逻辑。
7.3 多目标结果如何辅助真实运营决策
代码跑出来的Pareto前沿,最后要落地还得转成可操作的调度指令。比较实用的做法是:把前沿解集按“用户费用-负荷平稳度”做成二维散点图,让运营方拖动鼠标选点,系统自动把对应的充电起始时段表下发到智能充电桩。这部分界面可以用Matlab的App Designer快速搭建,后端直接复用优化函数包。
我个人在实际项目中的体会是,多目标优化本身不是终点,能讲清楚前沿解集的工程含义、帮决策者选出一个可落地的折中方案,才是这项研究真正的价值所在。算法改进永远服务于工程落地,这个顺序不能搞反。