做电力调度相关工作的朋友应该都有体会晚上看负荷曲线最不想看到的就是“晚高峰上又叠了一个充电高峰”。我最早接触这个课题是在处理某地区配电网负荷分析时发现电动车渗透率上来之后19点到21点的峰段被明显抬升而凌晨3点到5点的谷段几乎没人用电。浪费和紧张同时存在这就是削峰填谷要解决的问题。这篇博文完整梳理了一套基于削峰填谷的电动汽车多目标优化调度策略并给出Matlab代码实现思路。它做的事情可以用一句话概括在已知基础负荷、峰谷电价、每辆车的充电需求和可充电时间窗的前提下通过优化每辆车每个时段的充放电功率达到“电网负荷曲线更平、用户充电费用更低、电池寿命折损更小”三个目标之间的平衡。无论你是做电力系统方向的学生、刚入门微电网调度的工程师还是想自己复现一个多目标优化项目的开发者这篇内容都能帮你省下大量翻文献和调bug的时间。1. 为什么要做削峰填谷电动汽车充电与电网负荷的错位矛盾1.1 从一条日负荷曲线看峰谷差的代价先看一条典型的冬季日负荷曲线凌晨4点前后是全天最低点大概3000 kW晚上19点到21点出现晚高峰可以冲到5200 kW左右峰谷差率算下来大约是42%。这个数字意味着什么意味着配电网、变电站、线路都要按5200 kW这个“最坏情况”来配置容量但一天里真正用到这么高负荷的时间其实只有两三个小时剩下大部分时间里设备都在“大材小用”。这就是峰谷差的核心代价电力系统是按峰值投资的却按平均值运行。为了顶住这一两个小时的尖峰调度侧要投入成本更高的调峰机组或者依赖抽水蓄能这类灵活性资源而谷段机组利用小时数低整体运行效率也跟着被拖累。对配电网来说峰谷差越大变压器和线路的重载时间越长寿命折损和故障概率都会上升。所以“削峰填谷”一直是电网运行里的老话题只不过以前调度的对象是火电机组现在多了一个新的可调对象——电动汽车。1.2 电动汽车充电需求的天生弹性电动汽车和冰箱、空调这类刚性负荷最大的不同在于它的充电需求在时间上是高度可平移的。一辆车晚上18点插上充电枪用户真正在乎的往往是“第二天早上出门时电量够不够”而不是“18点01分必须充到80%”。这个“只在乎终态、不在乎过程”的特性就是调度可以发挥的空间。进一步拆解一辆电动汽车的充电需求可以用三个要素描述一是可充电时间窗也就是从接入电网到离开电网的这段时间二是电量需求由初始SOC和目标SOC决定三是功率上限交流慢充一般是7 kW直流快充可以到60 kW甚至更高。只要在时间窗内、功率上限以内把充电需求安排在哪个时段对用户来说几乎没有体感差异。这也是为什么学术界和工程界都把它叫做“柔性负荷”——它不像数据中心那样不能停电也不像工厂产线那样必须连续用电调节空间非常大。如果车辆支持V2G车到网双向充放电那就更进了一步它不只是可以“晚点充电”还可以在晚高峰时段把电池里的电反向送回电网相当于一台移动的小型储能。这等于把“削峰”和“填谷”两件事都交给了同一批车来干。1.3 无序充电的连锁反应以及“分时电价不是万能药”那如果什么都不做让车主到家就插枪、满功率开充会发生什么假设一个小区有100辆电动车每辆车到家时间是18点到21点之间且大部分集中在19点前后。这批车同时以7 kW功率充电光这一个小区就会多出约500 kW的负荷而且恰好叠加在系统晚高峰上形成所谓的“峰上加峰”。后果是连锁的配电变压器负载率飙升低压线路末端电压被压下去严重的时候会触发开关保护更隐蔽的问题是设备长期重载会加速老化这部分成本最终还是会摊到所有用户头上。所以很多地区虽然推广了分时电价但如果缺少有序调度高峰期的问题依然存在。分时电价能解决一部分问题却解决不了全部。原因在于当谷段电价在22点开始生效时所有车主都会“理性地”把充电启动时间设在22点整结果就是在23点到凌晨1点之间重新造出一个新的小高峰。这个现象在工程上有个形象的叫法——“峰谷迁移谷中造峰”。电价信号只能引导大家往低价时段挪却没办法协调“挪完之后大家别挤在一起”。要解决这个协调问题就必须引入集中式或分布式的优化调度给每辆车分配一个具体的充电时段和功率。这正是本文这套多目标模型要做的事。2. 多目标调度模型把电网、用户、电池三方诉求写进数学公式2.1 目标函数一最小化负荷方差让曲线“压峰填谷”削峰填谷最直观的数学表达就是让“叠加了电动汽车充电负荷之后的总负荷曲线”尽可能平坦。常用做法是最小化总负荷的方差min f1 Σᵗ ( P_load(t) Σₙ [P_ch,n(t) − P_dis,n(t)] − P_avg )²其中P_load(t)是t时段的基础负荷P_ch,n(t)和P_dis,n(t)分别是第n辆车的充电和放电功率P_avg是整个调度周期内总负荷的平均值。为什么选方差而不是直接选“峰谷差”因为方差把整条曲线的波动都纳入了惩罚优化结果会更平滑而单纯缩小峰谷差可能出现“峰值虽然降了但曲线在别的地方出现锯齿状波动”的情况。实际项目里也可以两个指标都算出来方差做优化目标峰谷差率做结果评价指标二者搭配使用。我个人的体会是目标函数选方差还有一个好处它对“平均负荷”这个量非常敏感而平均负荷在我们这个模型里几乎是常数总充电电量基本由用户需求决定所以方差最小化本质上就是在压制曲线的波动幅度。数学性质上它是个凸二次函数对后面选求解器非常友好。2.2 目标函数二最小化用户充电费用第二个目标站在用户角度在分时电价机制下同样的电量放在谷段充和在峰段充电费能差出两三倍。以一组典型电价为例峰段1.0元/kWh、平段0.6元/kWh、谷段0.35元/kWh一辆车充50度电峰段充要花50元谷段充只要17.5元。这笔账用户自己也会算但问题在于“每个人都会算”恰恰导致了谷段拥堵。模型里把这个目标写成min f2 Σₜ price(t) · [ Σₙ P_ch,n(t) − Σₙ P_dis,n(t) ] · Δt注意放电功率前面是减号意思是用户在峰段放电可以获得收入相当于把电“卖”回给电网用来抵扣自己的充电成本。这样处理之后V2G行为在用户侧就有了经济动力——只要放电收益高于电池折损成本用户就愿意在峰段放电、在谷段补电。2.3 目标函数三最小化电池寿命折损第三个目标是对V2G行为的“刹车”。锂电池的循环寿命和充放电深度、吞吐电量强相关如果模型里只看电网和用户两个目标优化器会倾向于让电池拼命放电峰段放出去、谷段充回来一天两个循环电池寿命快速衰减。这种结果对电网短期好看长期却是“杀鸡取卵”。工程上常用简化的线性折损模型来描述这个成本min f3 c_bat · Σₜ Σₙ [ P_ch,n(t) P_dis,n(t) ] · Δt即电池折损成本与总吞吐电量充入能量 放出能量成正比c_bat是单位吞吐电量的折损成本单位是元/kWh。我仿真时一般取0.120.2元/kWh这个范围这个数字大致对应“按电池全生命周期总吞吐量估算的折旧摊还”。关于c_bat怎么标定、取大取小有什么后果后面第5章会专门展开讲这里先记住它是个“成本系数”就够了。2.4 约束条件逐个梳理SOC递推、功率边界、时间窗、容量上限目标函数定好了约束条件才是真正决定模型“像不像现实”的部分。一个完整的单时段、多车辆调度模型至少要有下面这几类约束约束类型数学表达物理含义SOC动态递推SOC(t1) SOC(t) [ ηc·P_ch(t) − P_dis(t)/ηd ]·Δt / E_B电池电量随充放电变化ηc和ηd是充放电效率SOC上下限SOC_min ≤ SOC(t) ≤ SOC_max一般取0.20.9防止过充过放充放电功率边界0 ≤ P_ch ≤ P_ch_max0 ≤ P_dis ≤ P_dis_max受充电桩和电池允许电流限制时间窗约束车未接入时段 P_ch P_dis 0车辆只能在插枪时间段内参与调度用户离网电量需求SOC(t_dep) ≥ SOC_req保证用户第二天出行有足够电量配电容量约束P_base(t) Σₙ(P_ch−P_dis) ≤ P_limit变压器和线路容量上限可选的“硬约束”SOC动态递推是整个模型的核心它把每一辆车的“历史电量”和“当前充放行为”串起来。这里有个容易被忽略的细节放电时能量从电池流到电网中间经过电池放电效率ηd和逆变器效率所以实际放出的功率要除以效率也就是P_dis/ηd才是电池侧的真实出力。效率参数要不要分方向设很多论文写得比较含糊但你写代码时一定要分开否则放电过程的能量守恒算出来是错的。还有一个值得注意的点SOC_req这个约束非常关键它是“用户利益”在约束层面的底线。如果去掉它优化器为了填谷可能让某些车一直充不满电用户早上出门发现电量不够模型再漂亮也没法落地。2.5 三个目标互相打架加权法与帕累托折中三个目标放在一起本质上是在三个方向上拉扯只最小化负荷方差会让所有车在峰段放电、谷段充电电池折损飙高用户的电池“白打工”只最小化用户费用会让所有车挤在谷段充电谷段重新堆出高峰削峰填谷变成“削峰造峰”只最小化电池折损最激进的方案是“完全不调度”回到无序充电一切免谈。所以不能只求某一个目标的最优而是要求一组“折中解”。最常用的处理方法是加权求和法min F w1·f1/f1* w2·f2/f2* w3·f3/f3*其中f1*、f2*、f3*分别是每个目标单独优化时的最优值用于归一化w1、w2、w3是权重。改变权重组合就能得到一组帕累托前沿上的解比如w1大就偏向电网利益w2大就偏向用户利益。需要提醒的是归一化这一步不是可选项而是必选项——三个目标的量纲完全不一样负荷方差是kW²量级动辄几十万充电费用是几十到几百元电池折损成本更小不归一化的话小权重目标会被大数值目标直接“淹没”这一点在第5章会细说。如果想做更完整的帕累托分析可以用NSGA-II这类多目标进化算法一次性求出整个解集但日常研究和工程验证加权法加权重扫描已经足够而且求解稳定、速度快、可解释性强。3. Matlab代码实现从求解器选型到模块化仿真框架3.1 先选求解路线精确求解器还是智能优化算法拿到这个模型第一步不是写代码而是先回答一个问题用什么算法来解这个选择基本决定了后续代码的写法和求解速度。这个模型在连续变量、线性约束、二次方差目标的设定下是一个带约束的二次规划QP问题如果再引入“同一时段不能同时充放电”这种互斥逻辑就会变成混合整数二次规划MIQP。对这种问题成熟的做法是用YALMIP建模后台调CPLEX或Gurobi这类商业求解器求解也可以用Matlab自带的intlinprog线性化后或quadprog来解。优点是解的质量有保证速度快适合做精确的仿真验证。如果模型里加入了非线性效率曲线、电池老化非线性项或者你想直接研究帕累托前沿那就更适合用智能优化算法比如粒子群PSO、遗传算法GA、NSGA-II。这类方法的优点是不挑模型形态缺点是每次求解结果有随机性、需要多次运行取统计而且在大规模场景下收敛速度会明显下降。我的建议是能用凸模型就不用黑箱进化算法。实际项目里我通常分两步走——先用凸优化加权法快速得到一组基准解验证约束和参数设置没问题再用NSGA-II去补充多目标分析。这样既保证了核心结果的可靠性又能给论文或报告提供帕累托前沿图。求解路线适用模型求解稳定性实现难度适用规模matlab YALMIP CPLEXLP/QP/MIQP高中中大规模matlab 自带 intlinprog/quadprog线性化后的 MILP/QP高低中小规模PSO/GA/NSGA-II任意非线性中有随机性中高中规模3.2 工程化代码结构按“数据-建模-求解-画图”分层很多新手写优化代码喜欢把所有的内容堆进一个脚本里跑通一次就完事。但只要你需要换一组参数、换一个场景、或者扫一遍权重做敏感性分析这种“大脚本”就会变得非常痛苦。我建议从一开始就按“数据、建模、求解、画图”四层去组织文件。一个可参考的Matlab工程结构是这样的ev_scheduling/ ├── main.m % 主流程数据准备、调用建模与求解、汇总结果 ├── load_case_data.m % 设置基础负荷曲线、电价、变压器容量 ├── generate_ev_scenarios.m % 生成车辆样本到达/离开时刻、初始SOC、目标SOC ├── build_optimization_model.m % 用YALMIP构建变量、约束和目标 ├── run_solver.m % 调求解器返回最优解与求解状态 ├── plot_results.m % 画负荷曲线、SOC曲线、费用对比 └── evaluate_metrics.m % 计算峰谷差率、方差削减率、平均费用等指标main.m只管流程编排不懂模型细节build_optimization_model.m只负责把数学问题翻译成YALMIP语句不关心数据从哪来plot_results.m只负责可视化。这样每层都可以独立替换——今天用100辆车明天换300辆只改generate_ev_scenarios.m的参数今天用加权法明天换NSGA-II只动main.m里对求解器的调用方式。3.3 核心代码片段YALMIP建模的实际写法下面这段代码是build_optimization_model.m的核心逻辑演示我做了简化便于你理解整体结构。注意这里的写法是基于常见实践的参考实现具体到你自己的项目需要把车辆参数改成按样本逐个赋值。%% 基础参数示意 T 24; % 调度时段数1小时分辨率 N 100; % 车辆数 E_B 60; % 电池容量 kWh P_ch_max 7; % 最大充电功率 kW P_dis_max 7; % 最大放电功率 kW eta_c 0.95; % 充电效率 eta_d 0.95; % 放电效率 SOC_min 0.2; % 电池SOC下限 SOC_max 0.9; % 电池SOC上限 %% 定义优化变量 P_ch sdpvar(T, N, full); % 充电功率 P_dis sdpvar(T, N, full); % 放电功率 SOC sdpvar(T 1, N, full); % SOC状态时间轴比功率多1个点 %% 构建约束 Constraints []; for n 1:N % SOC递推约束 for t 1:T Constraints [Constraints, ... SOC(t1, n) SOC(t, n) ... (eta_c * P_ch(t, n) - P_dis(t, n) / eta_d) * 1 / E_B]; end % 功率边界与SOC边界 for t 1:T Constraints [Constraints, ... 0 P_ch(t, n) P_ch_max, ... 0 P_dis(t, n) P_dis_max, ... SOC_min SOC(t, n) SOC_max]; end % 时间窗外不可充放电t_arr/t_dep为车辆接入与离开时段 for t 1:T if t t_arr(n) || t t_dep(n) Constraints [Constraints, P_ch(t, n) 0, P_dis(t, n) 0]; end end % 离网电量约束 Constraints [Constraints, SOC(t_dep(n), n) SOC_req(n)]; end % 配电容量约束可选 P_net P_base sum(P_ch - P_dis, 2); for t 1:T Constraints [Constraints, P_net(t) P_limit]; end %% 目标函数三个目标加权归一化系数norm1/norm2/norm3事先算好 P_net_all P_base sum(P_ch - P_dis, 2); obj_var sum((P_net_all - mean(P_net_all)).^2); % 电网方差 obj_cost sum(sum(P_ch .* price * 1 - P_dis .* price * 1)); % 用户费用 obj_bat sum(sum(P_ch P_dis)) * c_bat; % 电池折损 obj w1 * obj_var / norm1 w2 * obj_cost / norm2 w3 * obj_bat / norm3; %% 求解 ops sdpsettings(solver, cplex, verbose, 2); sol optimize(Constraints, obj, ops); if sol.problem 0 P_ch_opt value(P_ch); P_dis_opt value(P_dis); SOC_opt value(SOC); else disp([求解失败, sol.info]); end这段代码只有几十行但麻雀虽小五脏俱全。你复现的时候重点检查三个地方一是sdpvar的维度是不是T×N二是时间窗约束里“t_arr/t_dep”的边界三是price矩阵的维度要和P_ch对齐。这三个位置是我见过最容易出错的报错信息五花八门但根源基本都是“矩阵维度对不上”。如果不用YALMIP也可以直接用Matlab的optimproblem基于问题的优化接口把约束和目标翻译成optimvar、optimconstr的写法调用intlinprog或quadprog求解。功能稍弱一些但可以避免额外配环境。4. 仿真案例与结果解读三组对照实验验证调度效果4.1 典型参数设置负荷、电价、EV出行数据仿真案例的价值在于“可复现”。我下面用的是一组我自己构造的典型参数不是某个电网的真实数据但量级上非常接近实际工程场景你可以直接拿来当初始值跑。基础负荷取24点曲线凌晨低谷3000 kW晚高峰5200 kW用一条带双峰的曲线插值生成。电价按三段式设置峰段08:00-11:00、18:00-21:00为1.0元/kWh平段11:00-18:00、21:00-22:00为0.6元/kWh谷段22:00-08:00为0.35元/kWh。变压器容量上限设为5600 kW给基础负荷留10%左右的裕量。车辆参数100辆电动车电池容量60 kWh交流充电桩额定功率7 kW放电功率同样按7 kW算。每辆车的接入时间按正态分布采样均值19:00、标准差1.5小时截断在17:00-23:00之间离开时间集中在次日07:00左右。初始SOC在0.20.5之间均匀分布离网时目标SOC不低于0.3如果用户要求充满就把SOC_req设为0.9。4.2 三组场景对比无序充电 vs 单目标削峰 vs 多目标调度我做了三组仿真对照分别对应三种“调度观”场景A无序充电。车辆到家立即以7 kW功率充电充满即停不参与任何优化场景B单目标削峰。只优化负荷方差这一个目标不关心用户费用和电池损耗场景C多目标加权调度。权重取w10.5、w20.3、w30.2三个目标一起优化。三组实验跑完之后把关键指标汇总起来看差异非常明显指标场景A无序场景B单目标削峰场景C多目标峰谷差率43.5%19.2%26.8%负荷方差相对无序充电100%22%45%用户日均充电费用约42元约25元约20元电池吞吐电量V2G强度低很高中等单目标削峰的“削峰填谷”效果确实最好峰谷差率从43.5%压到19.2%但代价是车辆在峰段大量放电、谷段大量充电电池吞吐电量被拉得很高。多目标调度为了兼顾电池寿命主动减少了一部分V2G行为峰谷差率只降到26.8%但用户费用最低电池折损也温和得多。这就是三个目标互相博弈的最直观体现——所谓“最优调度”本质上是在三个诉求之间找一个大家都愿意接受的折中。4.3 结果指标怎么算以及曲线怎么画跑完优化之后别急着截图。先把指标体系定义清楚否则你没法跟别人解释“为什么你这个调度好”。常用的评价指标有三个峰谷差率 (P_max − P_min) / P_max × 100%衡量曲线极限波动负荷方差削减率 (方差无序 − 方差优化) / 方差无序 × 100%衡量整体平滑程度用户费用节省率 (费用无序 − 费用优化) / 费用无序 × 100%衡量用户经济收益。画图方面我最常用的是一张“四曲线图”基础负荷、无序充电总负荷、单目标优化总负荷、多目标优化总负荷四条线放在同一个坐标系里一眼就能看出“峰被压下去、谷被填起来”的过程。再配一张某辆典型车辆的SOC曲线图展示它的充放电行为如何随时间变化。输出图片时记得把Figure的Renderer设为painters线宽统一到2.0以上导出成矢量图放进论文里才会清晰。5. 复现过程中的坑与调参经验5.1 权重没归一化优化等于白做这是新手最容易踩的坑我甚至可以说十个跑这个模型的人里至少有五个会在这里卡住。假设你直接写obj 0.5*obj_var 0.3*obj_cost 0.2*obj_bat不除以各自的基准值会发生什么负荷方差是kW²量级100辆车场景下方差随便就是几十万甚至上百万用户费用是几十块钱电池折损成本可能只有几块钱。这种量级差距下哪怕权重只有0.1方差项的数值也把其他两项碾压得干干净净。你以为你在做多目标实际上优化器眼里只有一个目标压方差。正确的做法是先把每个目标单独优化一遍记录各自的单目标最优值作为归一化基准f1*、f2*、f3*再在加权目标里用f_i / f_i*。我在3.3的代码片段里预留了norm1/norm2/norm3这三个变量就是为了干这个。权重扫描之前先把它们算好存成常量后面每次调权重都不用重新算。5.2 电池损耗系数V2G用多用少的“生死线”c_bat这个系数直接决定了模型对V2G的态度。我做过一组敏感性实验c_bat从0.05元/kWh一直变到0.4元/kWh结果非常有意思——系数低于0.15左右时优化器会让车辆尽可能多地参与峰段放电V2G吞吐量非常大系数高于0.3时放电行为基本消失调度退化成“只有充电的谷段错峰”。中间那段区间V2G吞吐量随系数变化非常陡峭。这说明电池损耗系数不是随便拍脑袋定的。工程上标定它有两种思路一种是从电池循环寿命出发按照“总吞吐电量对应循环次数循环次数决定报废时间”来折算另一种是从更换成本出发把电池更换费用摊到总吞吐电量上。两种方法算出来的数量级通常在0.10.25元/kWh之间。我建议你至少做一组敏感性分析把c_bat作为横轴、V2G总吞吐量作为纵轴画条曲线这样既能证明你的模型对参数是“可解释”的也能告诉读者你的结论在什么参数范围内成立。5.3 时间分辨率、随机场景与求解器降级时间分辨率从1小时改成15分钟T从24变成96之后模型能更准确地捕捉充电过程的变化但变量数直接翻4倍求解时间可能增加一个数量级。对毕业论文或小规模验证1小时分辨率足够但如果你要做工程级别的调度方案建议至少用15分钟分辨率并且注意SOC递推约束里的Δt要跟着改。还有一个容易被忽略的问题单次确定性场景的优化结果往往过于乐观。如果所有车辆都假设19点接入、7点离开、初始SOC都取中值那么优化器相当于“看到了未来的全部信息”结果自然漂亮。但现实中车辆接入时间、初始SOC都有随机性。我的做法是跑蒙特卡洛生成20组不同的车辆出行样本分别求解把结果的均值和标准差都报出来。这样你的结论就不是“碰运气”出来的而是具有统计意义的。如果你没有商业求解器的授权也别着急。模型线性化之后把二次方差目标改成线性化处理或者直接调用quadprog处理二次项用Matlab自带的quadprog或intlinprog一样能跑。我试过在T24、N100的规模下自带的求解器虽然比CPLEX慢一些但一两分钟以内也能出结果完全够用。5.4 车辆规模大了怎么办聚合建模思路当车辆数从100辆变成1000辆甚至更多时T×N的变量矩阵会让求解器直接“吃不消”。这个问题的标准解法是“聚合分群”把出行行为相近的车辆聚成几个集群比如晚归短途、晚归长途、全天在场等类型每个集群用一辆“等效车”代表等效车的电池容量和功率是集群内所有车的累加初始SOC取平均值。聚合之后N从1000降到510模型规模瞬间回到可控范围。代价是丢失了车辆之间的个体差异但如果你关心的是“总负荷曲线”而不是“某辆车几点几分充了多少电”这个精度损失完全可以接受。实际工程中聚合调度加实时修正是很主流的落地思路。整套模型从搭框架到调通参数我花时间最多的地方反而不是写代码而是不断问自己“这个约束漏了没有”“这个参数取多少才合理”。优化结果不会告诉你它自己有多可信——只有当你把每个数字背后的物理含义都想通了结果才真正属于你。最后再分享一个小建议先把N设成5辆、T设成24跑通全流程确认负荷曲线和SOC曲线看起来“符合直觉”之后再逐步扩大规模。这样你能更快定位是算法问题还是参数问题比自己闷头调一个上千变量的模型高效得多。
阅读完成 · 觉得有帮助?