晚上八点小区充电桩开始排起长队——下班回家的电动车主几乎同时插上充电枪。这个画面我再熟悉不过了但很少有人意识到它其实是电网最不愿意看到的场景负荷瞬间冲高变压器发热电网调度员只能眼睁睁看着负荷曲线被尖峰刺破。可如果换个角度想这批同时插枪的车本质上是一支移动充电宝大军正在待命。它们不仅能在低谷期多充电在高峰期还能反向放电给电网。问题是怎么调度这支大军让它们在削峰填谷里真正起作用而不是制造新的麻烦。这事儿的难点在于不能光让电网爽还得让车主愿意参与、电池损耗别太大、充电成本别涨。三个目标互相打架这就是典型的多目标优化问题。我最近用MATLAB完整跑了一遍这个场景今天直接把整个过程掰开揉碎讲清楚从数学建模到算法选型再到代码实现和踩坑经验一步不落。1. 削峰填谷的底层逻辑为什么电动汽车能当移动充电宝1.1 V2G的本质双向功率流动带来的调度自由度先把这个概念的物理本质说明白。所谓电动汽车参与削峰填谷核心依赖的是V2G技术也就是Vehicle-to-Grid车辆到电网的双向电力传输。传统充电桩是单向的电网给车充电车就是个纯负荷。而V2G充电桩支持双向功率流动车既能充电也能放电。这就让电动汽车不再是被动的用电设备而变成了一个可调度的分布式储能单元。一台家用电动汽车的电池容量通常在40到80千瓦时之间取个中间值60千瓦时。如果允许电池在20%到90%的荷电状态之间进行调度那么单台车就有大约42千瓦时的可调度容量。一台车听着不多但大规模接入后完全是另一个量级。假设一个城市有100万辆可参与V2G的电动汽车那就是4200万千瓦时的可调度容量。作为对比一个常规抽水蓄能电站的日调节库容大概也就百万千瓦时级别。换句话说一支规模化的电动汽车车队在理论储能容量上可以媲美大型储能设施。但这里有个很多人容易忽略的关键点调度自由度不等于想怎么用就怎么用。车主的出行需求是硬约束早上八点要出发上班那时候电池必须有足够的电量。所以V2G调度的本质是在车主的出行需求和电网的调节需求两条约束之间寻找平衡。这也是为什么这个问题不能简单粗暴地让所有车低谷充电、高峰放电——你得先尊重每辆车的使用习惯。1.2 削峰填谷的数学本质负荷曲线的整形那削峰填谷本身怎么度量从电网调度的角度看理想的负荷曲线应该是平坦的这样发电机组的出力稳定输电通道的利用率高系统运行成本低。反映在数学上就是净负荷曲线的方差最小化。净负荷曲线的定义很简单每个调度时段系统总负荷等于基础负荷加上电动汽车的充放电功率放电取负值。调度目标就是让这个总负荷曲线尽可能平。用公式表达就是minimize: sum(P_base(t) P_ev(t) - P_mean)^2其中P_base(t)是t时段的基础负荷P_ev(t)是t时段电动汽车集群的净充放电功率P_mean是整个调度周期内总负荷的平均值。方差越小曲线越平削峰填谷的效果就越好。这个目标函数理解起来不复杂但真正有意思的是它和其他目标之间的冲突。想让电网负荷平就得让车辆在高峰期放电、低谷期充电。可高峰期电价贵车主在高峰期放电给电网虽然能赚一笔但频繁深度充放对电池寿命的损耗不容忽视。低谷期充电虽然便宜但如果让所有车都在深夜同时充电原本的低谷又会被填成新的小高峰。三个目标互相牵扯这就是多目标优化介入的完美场景。2. 数学建模三个互相打架的目标和一组硬约束2.1 目标一系统净负荷方差最小化第一个目标我已经在上文给出净负荷方差最小。这是从电网视角出发的它直接衡量削峰填谷的效果。但要注意这个目标函数有一个隐含的问题——它只关心曲线的平坦程度完全不关心经济性。极端情况下优化算法可能让所有汽车在谷时疯狂充电、峰时疯狂放电虽然曲线是平了但每辆车都经历了一次完整的深度充放循环电池寿命会被严重加速消耗。所以它必须和下面的目标搭配使用。在MATLAB里写这个目标很简单无非是向量运算。但有个细节值得提一句如果你的基础负荷数据和电动汽车充放电功率在量级上差异很大会直接影响优化结果。比如基础负荷峰值是8000千瓦而电动汽车集群的总功率只有600千瓦那不管你怎么调度净负荷方差的变化可能只有几个百分点。这时候可以考虑对每个目标做归一化处理让它们在优化过程中比赛公平。2.2 目标二充电总费用最小第二个目标从用户视角出发车主参与V2G调度最关心的是充电花多少钱、放电赚多少钱。这里用峰谷分时电价来建模。假设谷时电价0.3元/千瓦时峰时电价1.0元/千瓦时。如果车主在谷时充电、峰时放电每千瓦时的差价就是0.7元。这笔账算下来一台60千瓦时的车如果每天在峰时放电10千瓦时收益就是7块钱一个月200多块虽然不多但足以激励一部分车主参与。目标函数写作minimize: sum(P_ev(t) * price(t) * delta_t)注意这里P_ev(t)是可正可负的充电为正、放电为负。所以这个目标函数实际上会促使优化算法把充电行为安排在低价时段、放电行为安排在高价时段。从某种意义上说这个目标本身就具备天然削峰填谷的特性因为电价设计本来就是峰高谷低经济性最优的方案自然倾向于低谷充电、高峰放电。但问题在于它只考虑钱不考虑电网负荷曲线的整体形态更不考虑电池损耗。2.3 目标三电池退化成本最小化第三个目标最容易被忽略但也最影响方案的实际落地。频繁的充放电循环会加速动力电池的容量衰减这个损耗是有真实成本的。学术上常用吞吐量模型来估算每次充放电循环会造成一定比例的容量损失折算成钱就是每千瓦时吞吐量对应的退化成本。简化建模如下minimize: beta * sum(|P_ev(t)| * delta_t)这里beta是单位充放电量对应的电池退化成本比如0.1元/千瓦时。这个系数怎么定可以参考动力电池的循环寿命数据。假设一块电池能完成3000次满充满放循环每次循环的能量吞吐量是60千瓦时总吞吐量就是18万千瓦时。电池更换成本按6万元算折合每千瓦时吞吐量的成本约为0.33元。但实际调度中很少做满充满放浅充浅放对寿命影响小很多所以beta取0.1到0.15元/千瓦时是工程上比较常见的估算范围。加了这个目标之后优化算法就不会一味地让车谷充峰放了它会在削峰收益、电价套利空间和电池损耗成本之间找一个平衡点。2.4 约束条件哪些红线不能碰多目标优化的约束条件决定了可行域的形状这块没建模好后面算法跑出来的解可能完全不可用。我实际建模时用了以下几类约束约束类型数学表达实际含义荷电状态边界SOC_min SOC(t) SOC_max电池不能过放或过充一般取0.2到0.9充放电功率边界-P_discharge_max P_ev(t) P_charge_max受充电桩和电池倍率限制能量守恒SOC(t1) SOC(t) P_ev(t)*delta_t/Capacity电池电量随时间演变出行需求SOC_T SOC_target调度结束时电量必须达到车主设定目标变压器容量P_base(t) P_ev(t) P_limit配电变压器不过载这些约束里最容易被新手忽略的是出行需求约束。很多初版模型只约束了SOC的上下限结果优化算法很聪明地把所有车在凌晨放电放到SOC_min早上车主出门时发现电池是空的这个方案显然不可行。所以我在建模时一定加入终态SOC约束保证调度周期结束时每辆车的电量不低于车主设定的目标一般设为0.8以上。另一个容易踩坑的是功率边界的设定。家用慢充桩的充电功率一般是6到7千瓦但V2G放电功率往往低于充电功率很多家用双向桩的放电功率只有3到4千瓦。如果你在模型里让放电功率和充电功率完全对称可能算出实际上无法执行的方案。不同车辆的参数也要差异化建模不能把1000辆车当成同一个聚合体来算——至少要把快充和慢充、大电池和小电池分成几个群体。3. 算法选型为什么我不用加权法而是直奔NSGA-II3.1 加权求和法的两个致命问题很多人一上来就会想多目标优化简单把三个目标加权成一个单目标不就行了比如总目标等于0.4乘以方差加0.3乘以费用加0.3乘以损耗。这个方法确实简单但它有两个致命问题。第一是权重怎么定。三个目标的量纲完全不同方差是千瓦的平方费用是元损耗也是元。你可以把方差归一化但归一化之后权重依然是个拍脑袋的主观值。你设0.4、0.3、0.3凭什么不是0.5、0.2、0.3权重不同最优解完全不同但没有一个客观标准告诉你哪个权重是对的。第二是加权法只能得到一个点而不是一条Pareto前沿。在实际工程里你告诉电网调度员最优方案是让500辆车在14点到16点各放电3.2千瓦他们很难直接接受。但如果给他们一条Pareto前沿告诉他们这是削峰效果最好的方案这是电池损耗最小的方案这是用户收益最大的方案你可以根据今天电网的运行状况挑一个折中的这样的决策体验完全不同。多目标优化的价值不仅在于找到最优解更在于给决策者提供完整的权衡视角。3.2 NSGA-II的核心机制非支配排序与拥挤度距离NSGA-II是目前工程上最成熟的多目标进化算法之一MATLAB的全局优化工具箱里内置的gamultiobj函数实现的就是NSGA-II的改进版本。它的核心机制非常有启发意义值得理解透。第一层叫非支配排序。一个解如果所有目标都不比另一个解差并且至少有一个目标更好那这个解就支配了另一个解。算法会给所有解分层第一层是不被任何解支配的Pareto前沿解第二层是去掉第一层后的非支配解以此类推。进化过程中低层级的解优先被保留。第二层叫拥挤度距离。同属一层的解之间怎么比较优劣谁周围越稀疏谁保留的优先级越高。这个机制是为了维持种群多样性防止所有解都聚集到Pareto前沿的某一段而丢失其他区域的解决方案。这两个机制合起来的策略是优先让种群向Pareto前沿推进同时保证前沿上的解分布均匀。听起来不复杂但在进化算法框架里实现得极其优雅这也是它能成为业界标配的原因。理解了这套机制你就明白为什么NSGA-II能一次性给出一整条权衡曲线——它本身就设计为多样性优先。3.3 对比实验加权法和NSGA-II在同一个案例上的差异我拿同一组基础数据分别跑了加权法和NSGA-II。加权法我试了三组权重每次得到一个最优解把它们画在三维目标空间里是三个孤立点。NSGA-II跑一次得到200个Pareto前沿解在目标空间里形成一条清晰的三维曲面。让我印象最深的一个场景是加权法得到的解要么削峰效果很好但电池损耗大到车主不可能接受要么费用很低但电网净负荷方差几乎没有改善。而NSGA-II给出的前沿中我能明显看到一段甜点区域——三个目标都相对均衡的区域。这在实际决策中价值太大了。4. MATLAB全流程实现从基础数据到Pareto前沿4.1 环境准备与基础数据构造我用的环境是MATLAB R2023b需要全局优化工具箱。这个工具箱从R2010a开始就内置了gamultiobj函数所以版本老一点也没关系。但要注意gamultiobj属于全局优化工具箱不在基础MATLAB安装包里检查一下你的license里有没有这个工具箱。基础数据需要三块典型日负荷曲线、分时电价序列、电动汽车集群参数。日负荷曲线可以自己构造也可以用实际的负荷数据。我用了一个简化版的夏季工作日负荷曲线峰值出现在19点到21点约8000千瓦谷值出现在凌晨3点到5点约3500千瓦每个时段取15分钟一天96个点。分时电价设为谷时0.3元、平时0.6元、峰时1.0元。关键参数设定如下% 基础参数配置 num_ev 1000; % 电动汽车数量 battery_capacity 60; % 单台电池容量kWh soc_initial 0.7; % 初始荷电状态 soc_target 0.8; % 调度结束目标荷电状态 soc_min 0.2; % SOC下界 soc_max 0.9; % SOC上界 charge_power_max 6; % 最大充电功率kW discharge_power_max 4; % 最大放电功率kW beta 0.12; % 电池退化成本元/kWh dt 0.25; % 时段长度小时 num_periods 96; % 调度时段数这里要特别说明discharge_power_max为什么是4而不是6。现实中很多双向充电桩的放电功率确实比充电功率低这个不对称性会直接影响最优策略的形态。如果你在模型里让两个功率完全对称算出来的方案可能在实际运行中根本无法复现。4.2 目标函数与约束条件的代码化目标函数是一个接受决策变量向量x、返回三个目标值的函数。x是96维向量代表每个时段电动汽车集群的净充放电功率正值充电、负值放电。注意我这里做的是集群级优化也就是把1000辆车看成一个虚拟储能电站来调度。如果要精细化到每辆车决策变量维度会变成96乘以1000那是一个远超常规优化算法处理能力的规模需要用分解协调或者集群控制方法来处理。目标函数代码function f ev_objective(x, load_base, price, params) % 净负荷 p_net load_base x; % 目标1净负荷方差最小化 f(1) sum((p_net - mean(p_net)).^2); % 目标2用户充电总费用最小化 f(2) sum(x .* price) * params.dt; % 目标3电池退化成本最小化 f(3) params.beta * sum(abs(x)) * params.dt; end约束函数要处理的是非线性约束。gamultiobj支持通过非线性约束函数返回不等式约束c和等式约束ceq。这里需要把SOC的动态演化过程写成约束function [c, ceq] ev_constraint(x, params) % SOC动态 soc params.soc_initial * ones(1, num_periods); for t 1:(params.num_periods-1) soc(t1) soc(t) x(t) * params.dt / params.battery_capacity; end % 不等式约束SOC在安全范围内 c1 params.soc_min - soc(2:end); c2 soc(2:end) - params.soc_max; % 终点SOC达到目标值 c3 params.soc_target - soc(end); c3 -c3; % 因为c 0表示约束满足 % 变压器容量约束净负荷不超过上限 c4 params.load_base x - params.power_limit; c [c1, c2, c3, c4]; ceq []; end这里有个容易犯错的点非线性约束函数在每个时段都做一次SOC递推如果决策变量维度是96那这个函数每次调用都会循环96次种群数量200、迭代300代总调用次数好几万次计算量确实不小。所以建议在目标函数和约束函数里尽量用向量化运算避免写太多for循环。4.3 gamultiobj参数调参与运行gamultiobj的调用语法很直白num_vars 96; lb -params.discharge_power_max * ones(1, num_vars); % 放电下限 ub params.charge_power_max * ones(1, num_vars); % 充电上限 options optimoptions(gamultiobj, ... PopulationSize, 200, ... MaxGenerations, 300, ... FunctionTolerance, 1e-4, ... Display, iter, ... UseParallel, true); [x_opt, fval] gamultiobj((x) ev_objective(x, load_base, price, params), ... num_vars, [], [], [], [], lb, ub, ... (x) ev_constraint(x, params), options);几个参数选择的经验值种群大小我建议设置在150到300之间。太小了Pareto前沿分布不均匀太大了单次迭代速度太慢。MaxGenerations设300代作为起点但如果看到优化过程还没收敛Display里显示平均距离还在下降就往500代加。FunctionTolerance设1e-4就够用设太小学会在Pareto前沿附近无谓地磨蹭很多代。matlab % 种群大小、迭代轮数影响收敛质量 但收敛质量不能光看代数要看Pareto前沿上解的分布是否均匀。4.4 Pareto前沿的可视化与最优折中解选择运行结束后fval矩阵的每一行是一个解的三个目标值。先画三维散点图看前沿形态figure; scatter3(fval(:,1), fval(:,2), fval(:,3), 20, fval(:,1), filled); xlabel(净负荷方差 (kW^2)); ylabel(用户费用 (元)); zlabel(电池损耗成本 (元)); colorbar;初次跑出来你会发现Pareto前沿可能不是一条漂亮的三维弧线而是一团杂乱的点。这很正常说明种群还没完全收敛或者约束处理有问题。我在实际调试中就遇到过好几次前沿发毛的情况后面会详细说排查思路。从Pareto前沿选折中解我推荐用模糊隶属度方法。简单来说对每个目标找到其最小值f_min和最大值f_max计算每个解的隶属度norm_f (fval - min(fval)) ./ (max(fval) - min(fval)); membership 1 - norm_f; % 值越小越好 total_score sum(membership, 2); [~, best_idx] max(total_score); best_solution x_opt(best_idx, :); best_objectives fval(best_idx, :);这个方法不偏向任何一个目标挑选出的解在三者之间取了一个综合最优的妥协点。实际项目里如果把电网方权重更高或用户方权重更高作为业务倾向可以在计算total_score时给不同目标加权重。4.5 完整代码的结构组织这个项目的代码我建议按面向对象思路组织而不是堆一堆脚本文件。用MATLAB的classdef定义一个EVSchedulingProblem类包含loadData、buildObjective、buildConstraints、solve、plotParetoFront这些方法好处是参数传递清晰换一个数据集或者改约束条件时不用大范围改代码。我一开始用函数脚本拼出来改参数改到崩溃——换一个停车场的负荷数据结果发现某个脚本里硬编码了变量名光排查就花了两小时。后来重构为类后整个流程清爽多了。5. 实测结果与调参踩坑记录5.1 案例结果三个目标的权衡关系我跑通一组完整案例后得到了几条非常有意思的结论。先看Pareto前沿它呈现出一条明显的三维曲面一端是削峰效果极佳但费用和损耗很高的方案另一端是费用和损耗都很低但削峰几乎无效的方案。最值得关注的是中间那段甜点区净负荷方差可以从原始曲线的45000降到18000同时用户费用不但没涨反而略降因为把部分充电移到了谷时电池损耗成本控制在一个可接受的范围内。这里有个反直觉的发现削峰填谷和用户省钱在大多数情况下不是冲突的而是协同的。原因是分时电价设计本身就把谷时定在电网负荷低谷时段所以在谷时多充电同时实现了削峰填谷和降低费用两个目标。真正的冲突主要发生在电池损耗和两个目标之间——需要权衡的其实是为了削峰和省钱愿意牺牲多少电池寿命。5.2 踩坑记录Pareto前沿发毛的排查链路我在调试过程中遇到了Pareto前沿散乱、不成曲面、看起来像随机噪声的情况。这里分享一下完整的排查链路希望对你有帮助。第一步我先检查了约束函数是否正确。发现终端SOC约束写错了方向。c3这行代码原本写成了soc_target - soc_end没有取负号这会导致约束变成soc_end必须大于soc_target和预期的至少达到目标值完全相反。这个方向的错误会让大部分解被排除在可行域之外前沿自然就乱了。第二步检查了决策变量的上下界。因为discharge_power_max设成4lb是-4ub是6这个本身没问题。但问题出在我把lb和ub都乘了ones(1, num_vars)如果num_vars是96那没问题。但我之前测试时num_periods设置成24后来改成96后发现lb和ub向量用的是原来的96长度初值居然在内存里维度不匹配全体赋值为0导致所有解都被约束成完全不去调度了。第三步检查种群大小和代数。当我从种群100、代数100提升到种群200、代数300之后前沿形态有了显著改善。这说明前面的设置下算法还没收敛到前沿所有解都还在前沿内部挣扎。第四步也是最隐蔽的一个坑目标函数里的维度匹配错误。x是96维的行向量load_base如果是列向量两者相加会触发隐式扩展生成一个96乘96的矩阵。sum()函数会沿着第一维求和结果f(1)变成一个1乘96的向量而不是一个标量。gamultiobj要求目标函数返回一个行向量向量长度等于目标数量维度错乱会导致它无法正确计算非支配关系。这个问题MATLAB不会报错因为它合法地返回了长度不为3的向量但优化效果一塌糊涂。排查这类问题最快的方式是在目标函数入口手动打印size(x)和size(load_base)确认两者方向一致后再继续跑。5.3 计算效率优化的几个技巧用MATLAB跑这类优化问题计算效率是个躲不开的话题。96维决策变量200个种群300代迭代每代要计算200次目标函数和约束函数每次约束函数要循环96步算SOC总计算量大概几百万次浮点运算单核跑可能要几分钟到十几分钟。几个提速技巧很实用用UseParallel开启并行计算gamultiobj会把种群评价分散到多个worker上。实测4核并行后速度提升接近3倍。SOC递推过程向量化。先算累计充电量cumsum(x)*dt/capacity然后用向量运算得到整个SOC序列替代for循环速度提升非常明显。如果多次运行同一数据集的优化把基础负荷、电价这些参数预先算好存成.mat文件避免每次重复加载和处理。5.4 从仿真到实施V2G落地的现实距离仿真模型跑出来的结果越漂亮越要冷静看一眼现实条件。我的这个模型隐含了几个重要假设1000辆车全部配备双向充电桩、车主100%愿意参与调度、电网侧能实时获取每辆车的SOC和位置信息。真实情况是双向充电桩渗透率可能只有10%到20%愿意在高峰时段放电给电网的车主更是少数需要足够高的电价激励才愿意参加。所以这个仿真模型真正的价值定位不是预测一定能在现实中实现削峰填谷而是回答如果参与率达到X%、双向桩渗透率达到Y%削峰效果能到什么程度。这类灵敏度分析才是模型在决策层面的实际用途。跑完优化后我会专门做一组参与率从20%到80%的扫描实验把净负荷方差的改善程度画成一条曲线交给规划部门作为投资双向充电桩的参考依据。另一个现实差距是预测误差。模型假设基础负荷曲线是已知的确定值但实际运行中负荷预测总有偏差充放电决策需要滚动更新。也就是说你得把上面这套静态优化包装成一个模型预测控制MPC框架每15分钟重算一次未来25小时的调度计划只执行当前时刻的动作然后滚入下一轮。这个扩展方向是V2G调度的真正工程形态MATLAB里可以通过fmincon嵌套或者用optimoptions的Replanning思路实现。写在最后跑完这个项目的最大体会是多目标优化的价值不只是求出最优解更是让决策者看见目标之间如何权衡。我最初搭好模型时总想着找到一个完美的调度方案后来发现根本不是这个逻辑——电网调度员今天可能需要优先削峰明天可能更看重用户收益后天可能变压器检修要求限制负荷。没有一劳永逸的最优只有一条可以随时挑选的Pareto前沿这才是这个模型真正有价值的产物。顺手分享一个调试小技巧每次改动目标函数或者约束之后先跑一个极小的测试——把种群设成20、代数设成20跑得快快速验证模型能正常收敛、前沿不是乱成一团。确认无误后再加大参数跑正式实验。别一上来就跑200种群300代花了半小时跑完才发现约束符号写反了那种心情我经历过太多次了。
阅读完成 · 觉得有帮助?