ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于V2G的电动汽车实时调度策略:Matlab滚动时域LP实现详解

基于V2G的电动汽车实时调度策略:Matlab滚动时域LP实现详解

很多刚开始做电动汽车调度的朋友,都会把“基于V2G技术的电动汽车实时调度策略”理解成“让每辆车算一个充放电功率”。真上手之后才发现,V2G实时调度更像是在管一支随时有人进出、电池状态千差万别的车队,同时还要和电网的负荷曲线、分时电价、变压器容量绑在一起做博弈。最近我完整实现了一遍这个策略,用的是Matlab,从建模、选算法到写代码调试,前前后后折腾了近两周。如果你也在做电动汽车调度、微电网能量管理或者储能优化方向,这篇笔记应该能帮你少走很多弯路。

1. 项目整体思路与调度模型构建

1.1 实时调度到底在调度什么

V2G(Vehicle-to-Grid)的核心价值,是让电动汽车在“从电网取电”和“向电网送电”两个方向都能灵活响应。但真正落到调度问题上,事情就具体了:每辆车有自己的接入时间、离开时间、初始SOC、目标SOC和充放电功率上限;配电台区有一条时变的基础负荷曲线和一个变压器容量约束;电网侧还有分时电价。实时调度要做的,就是在每个调度周期,比如每15分钟,根据当前已经接入车辆的状态和未来一段时间的预测数据,决定接下来每小时甚至几分钟内每辆车的充放电功率。

我用的模拟场景是某园区或小区充电站,下面有20辆电动汽车,调度间隔取15分钟,一天96个周期。车辆之间互不共享电池,但共享配电变压器容量。这种场景非常典型,既不会太简单到没有说服力,也不会因为规模太大导致Matlab跑不动。如果只做单辆车的充放电策略,你会发现约束太少,算法没什么可调;但直接做大几十辆车、上千个节点,又容易陷入调试地狱。20辆车是个很好的中间起点。

1.2 目标函数与约束条件的数学表达

调度目标不能拍脑袋定成“费用最低”。如果只优化用户充电费用,群体会自发把充电需求全部挤进谷电时段,结果峰谷差可能不减反增,配电台区在夜间又冒出一个新的负荷尖峰。所以我在目标函数里做了多目标权衡:

  • 最小化所有车辆在预测窗口内的净购电成本;
  • 最小化配电台区峰值负荷、平抑负荷波动;
  • 对频繁放电带来的电池损耗加一个惩罚项。

目标函数写出来大致是:

[ \min \quad J = \sum_{t \in T} \big[ \lambda_1 \cdot p_t \cdot \sum_i P_{net,i,t} + \lambda_2 \cdot L_{peak} + \lambda_3 \cdot \sum_i D_{i,t} \big] ]

其中 (P_{net,i,t}) 表示车辆i在t时刻从电网取电的净功率,正值是充电,负值是放电。(p_t) 是分时电价,(L_{peak}) 是窗口内最大负荷,(D_{i,t}) 是电池退化相关的放电深度惩罚。实际求解时,(L_{peak}) 我会写成线性化形式,也就是给峰值负荷增加一个辅助变量,再加不等式约束。

约束条件分为四类:

首先是配电台区功率平衡约束。某个时刻变压器出口总功率等于基础负荷加上所有车辆的净充电功率,这个总功率不能超过变压器容量上限,低于安全下限的情况也尽量避免。

其次是单车功率约束。每辆车在同一个调度周期内不能既充电又放电,充放电功率绝对值都不能超过车端变换器的额定功率。这个额定功率我按常见的7kW交流桩来设置,电池容量取50kWh,对应现在主流的长续航车型。

再是电池SOC动态方程:

[ SOC_{i,t+1} = SOC_{i,t} + \frac{P_{ch,i,t} \cdot \eta_c \cdot \Delta t}{E_i} - \frac{P_{dis,i,t} \cdot \Delta t}{E_i \cdot \eta_d} ]

这里 (P_{ch}) 是充电功率,(P_{dis}) 是放电功率;(\eta_c) 和 (\eta_d) 分别是充放电效率,我统一取0.95。SOC的范围设定在0.2到0.9之间,避免过充过放对电池的伤害。

最后是离网SOC约束。每辆车离开的时候,SOC至少要达到用户设定的目标值,但也不需要强制充满。这个约束最容易导致求解失败,因为车辆接入时间短、初始SOC低、充电功率有限,很可能在离开前充不到目标值。后面我会专门讲怎么处理。

1.3 为什么“实时”比“日前”更适合V2G

很多论文先做“日前调度”,也就是基于预测曲线一次性算出24小时的充放电计划,再让车辆执行。这个方法在历史负荷平稳、车辆行踪规律时效果还行,但一旦遇到车辆接入时间随机、临时改计划、或者天气变化导致基础负荷波动,日前计划就失效了。

实时调度采用“滚动窗口”的思路:每个调度周期只求解未来4小时或者1小时的控制指令,但只执行第一个周期的功率,到下一个15分钟再刷新状态、重新求解。这也是模型预测控制(MPC)的经典框架。好处是对预测误差的容忍度高,车辆随机接入也能随时纳入优化,不需要重跑全天计划。代价是求解频率变高,算法必须足够快。我在Matlab里用线性规划实现滚动窗口,单次求解时间基本在几十毫秒量级,完全满足15分钟一个周期的实时性要求。

2. 核心算法选型与Matlab实现要点

2.1 几种常见调度算法怎么选

V2G调度算法从数学上看是一个约束优化问题,可选的方法非常多。我自己做过对比,把结论整理成表格:

算法类型优点缺点适合场景
线性规划(LP)求解快、全局最优、模型直观目标函数和约束必须是线性的功率变量连续、费用和负荷优化
混合整数线性规划(MILP)能建模0/1开关、启停状态求解速度随整数变量数明显下降需要严格禁止同时充放电、多档位控制
动态规划理论上能得到全局最优状态空间爆炸,车辆多时基本不可行单车或少车辆调度
遗传算法/粒子群适应强非线性目标参数敏感、每次结果可能不一致、实时性差离线方案设计、参数标定
MPC滚动优化能处理不确定性、滚动修正需要模型和求解器配合实时调度、复杂系统控制

我最终选了“滚动时域LP”作为主方案。原因有两个:一是V2G调度模型在15分钟粒度下,SOC更新、功率约束都可以写成线性关系,不需要非线性求解;二是LP能够保证每次求解的稳定性和速度,方便我做重复实验对比不同参数。如果你想把“同周期不能同时充放电”这个逻辑做实,可以在LP基础上加0/1变量升级成MILP,但需要接受更大的计算开销。我的经验是,在正常分时电价且目标函数有费用惩罚的前提下,LP最优解天然不会同时充放电,显式加上“功率之和小于上限”的约束已经足够了。

2.2 滚动时域LP的完整流程

实时调度的主循环我用伪代码表示会更清楚:

  1. 初始化时间戳、电价曲线、基础负荷曲线、车辆参数。
  2. 对每个调度周期k = 1:T:
    • 只保留当前仍接入、且还没到离开时间的车辆;
    • 更新每辆车的最新SOC;
    • 读取未来H个周期的电价预测和基础负荷预测;
    • 构建LP模型:定义决策变量、目标函数、约束;
    • 调用solve得到最优功率序列;
    • 只取第一个周期的控制指令,更新车辆状态;
    • 存下结果,进入下一轮。

这里有个容易被忽略的细节:每个调度周期内,离开时间早于预测窗口结束的车辆,在离开后的时段就不应该再出现在约束里。实现时我会生成一个“可用车辆-时段”的掩码矩阵,所有功率变量都乘以这个掩码。否则车辆已经离开,SOC约束还在强制它参与调度,这既不合理也会造成不可行。

2.3 Matlab代码的基本骨架与变量设计

最开始我把所有代码堆在一个大脚本里,结果改一个参数要翻半天,后来重构成了三个模块:参数定义、模型构建、滚动循环。车辆参数我统一用struct数组保存,每个元素包含电池容量、最大功率、接入/离开时间、初始SOC、SOC上下限、目标SOC和充放电效率。

核心的滚动循环代码长这样:

for k = 1:totalPeriods horizon = min(H, totalPeriods - k + 1); active = (veh.startTime <= k) & (veh.leaveTime > k); prob = optimproblem('ObjectiveSense', 'minimize'); Pch = optimvar('Pch', nEV, horizon, 'LowerBound', 0); Pdis = optimvar('Pdis', nEV, horizon, 'LowerBound', 0); % 目标函数:购电成本 + 峰负荷惩罚 + 放电惩罚 cost = 0; for t = 1:horizon netLoad = baseLoad(k+t-1) + sum(Pch(:,t)) - sum(Pdis(:,t)); cost = cost + price(k+t-1) * sum(Pch(:,t) - Pdis(:,t)) ... + penalty * netLoad; end prob.Objective = cost; % 约束:功率上限、变压器容量、SOC更新等 prob.Constraints.powerUpper = Pch + Pdis <= veh.maxPower; prob.Constraints.transformer = ... baseLoad(k:k+horizon-1) + sum(Pch,1) - sum(Pdis,1) <= transformerCap; [sol, fval, exitflag] = solve(prob); ... end

当然,这只是示意,真正跑起来还需要把掩码矩阵乘进去,并把SOC更新写成矩阵形式。SOC更新如果放在LP模型内部,需要用到过去SOC作为初值,我会把当前SOC作为常量写入,而不是作为决策变量,否则模型会变成一个动态非凸问题,不能直接用LP求解。

2.4 求解器与性能细节

Matlab里solve会自动调用默认的线性规划求解器,对几百个变量的LP来说完全足够。但如果你要跑很多组实验,建议把求解选项固定下来,比如设置“最大迭代次数”、“约束容差”,避免每次求解都启动默认逻辑。更快的做法是直接用linprog,把目标函数、不等式约束的矩阵和向量手动组装成标准形式。初期调试用optimproblem更容易定位问题,后期性能优化再迁移到linprog。

我要强调一个容易踩的坑:optimvar的维度是nEV x horizon,但写约束时sum(Pch,1)得到的是一个1 x horizon向量,而baseLoad可能是行向量也可能是列向量。Matlab对隐式扩展有它的规则,一旦方向不一致,要么报维度错误,要么得到错误的约束矩阵。我后来统一把时间维度固定为行向量,车辆维度固定为列向量,再也没因为这个出过问题。

3. 实操过程与案例实现

3.1 算例场景和参数设置

为了让结果可复现,我给一个保守但贴近实际的案例。假设配电台区变压器额定容量500kW,基础负荷取典型的夏季工作日形状,高峰集中在傍晚18点到22点。调度周期15分钟,一天96个点。电动汽车共20辆,统一采用50kWh电池,额定充放电功率7kW,充放电效率95%。

参数取值
调度周期15分钟
预测时域H16步(4小时)
车辆数20
电池容量50 kWh
最大充放电功率7 kW
SOC范围0.2 ~ 0.9
目标SOC0.9
峰时电价0.8 元/kWh
谷时电价0.3 元/kWh

车辆接入时间我随机分布在8点到20点之间,离开时间集中在17点到22点。这样做能逼出问题:很多车下午接入,晚上就要离开,充电窗口很短,还正好撞上电网负荷高峰。如果没有V2G,这一大波充电需求会硬生生叠在负荷曲线峰值上,变压器压力会非常大。

3.2 从数据生成到滚动求解的关键代码

我先把基础负荷和电价生成好,用数组保存,然后写一个车辆状态更新函数:

function socNext = updateSOC(soc, Pgrid, params) % Pgrid 正表示从电网取电,负表示向电网放电 if Pgrid >= 0 socNext = soc + params.chargeEff * Pgrid * params.dt / params.batteryCapacity; else socNext = soc + Pgrid * params.dt / (params.batteryCapacity * params.dischargeEff); end end

这里有一个非常重要的设计:Pgrid是“和电网交换的功率”,所以在放电时,电池内部实际消耗的功率是-Pgrid / dischargeEff,而不是-Pgrid。这个符号如果搞反,SOC更新就会出现“越放电电量越多”的灵异现象。我在调试时遇到过两次,代码逻辑检查了半小时才发现是效率取反。

接着是LP模型构建。为了兼顾速度和调试,我直接调用linprog,把目标函数系数向量f和约束矩阵Aineq、bineq组装出来。决策变量的排列方式我定为:

  • 前nEV * H个变量是各周期各车的充电功率;
  • 后nEV * H个变量是各周期各车的放电功率。

这个排列顺序一旦确定,所有矩阵的列都要按它来。我建议在做矩阵拼装时先把索引函数写好,比如:

chIdx = @(i,t) (t-1)*nEV + i; disIdx = @(i,t) nEV*H + (t-1)*nEV + i;

这样写约束时逻辑很直观,不容易索引错位。

3.3 模拟结果怎么读

我对三种策略做了对比:无调度(车辆一接入就最大功率充电)、单向分时充电(只能充电不能放电)、本文实时调度(允许V2G双向并滚动优化)。模拟趋势如下:

策略峰值负荷(kW)用户平均充电费用(元)平均离网SOC
无调度45814.20.82
单向分时充电42110.30.90
V2G滚动调度3858.60.90

这说明V2G对削峰填谷和用户成本都有明显作用。峰值负荷下降并不是因为少充了电,而是把一部分高峰充电需求前移,同时让部分车辆在傍晚高峰时放电,给其他车辆腾出负荷空间。离网SOC在V2G调度下依然能保持0.90,说明放电并没有牺牲用户出行需求。

当然,这个表里的数字只是模拟算例,不能直接套到其他场景里。如果你把峰谷价差缩小,V2G放电收益会明显下降;如果车辆数量增加而变压器容量不变,削峰空间也会跟着受限。所以做项目时一定要自己跑参数敏感性分析,而不是照抄别人结论。

4. 常见问题与调试心得

4.1 四个最容易遇到的求解报错

我调这个项目时,报错类型基本集中在四类,整理成速查表:

报错或异常可能原因解决办法
“Problem is infeasible”离网前充不到目标SOC,或变压器容量过小导致所有车辆无法同时充电把目标SOC改为软约束,加松弛变量
求解成功但SOC越界调度循环里状态更新与模型预测不一致检查充放电效率正反方向、调度周期单位
结果出现大量“既充又放”目标函数缺少惩罚,或电价有负值增加放电惩罚项,或升级为MILP
求解时间随车辆数急剧上升用optimproblem频繁创建问题对象改用linprog、避免循环内重定义
峰值负荷反而更高只优化费用,没有加入峰负荷约束目标函数中加入峰谷差或变压器容量的罚项

这里面最坑的是第一类。车辆接入时间短、初始SOC低时,LP会连一个可行解都找不到。我的解决办法是把离网SOC约束改成:

[ SOC_{i,leave} + s_i \ge SOC_{i,target} ]

其中 (s_i) 是松弛变量,同时在目标函数里加入 (M \cdot \sum s_i),M是一个很大的惩罚系数。这样大部分场景都能求解,车辆如果实在充不满,也会以最小的违约程度离开。

4.2 实时性优化的几条经验

刚开始我用optimproblem跑20辆车、96个周期,整个调度要两三分钟才能完成,作为离线仿真勉强能忍,但离“实时”差得很远。后来我发现瓶颈不在求解,而在每次循环里反复构造优化问题对象,Matlab的面向对象封装开销非常大。

把核心求解改成linprog之后,同样的算例压缩到几十秒。再进一步采用离线预计算索引矩阵、全程向量化,效果会更好。有一个更实用的技巧是缩小预测时域:如果只做1小时预测,也就是4个调度周期,计算量会降到原来的四分之一,而调度效果因为滚动更新的存在,并不会劣化太多。

如果你要处理上百辆车,我建议直接用稀疏矩阵描述约束,因为大部分约束矩阵都是对角块结构,稀疏后会快很多。Matlab里sparse函数的应用很值得花半小时研究。

4.3 能量守恒是调试的第一道关卡

调度算法写完了,怎么判断它对不对?我的方法只有一个:先查能量守恒。把整个调度周期内所有车辆的充电电量加起来,减去放电电量,再和SOC变化总量对比。对一辆车来说,关系是:

[ \sum_t P_{ch,i,t} \cdot \eta_c - \sum_t \frac{P_{dis,i,t}}{\eta_d} = E_i \cdot (SOC_{i,end} - SOC_{i,start}) ]

如果左端明显大于右端,说明代码可能在某个环节让能量凭空产生。最常见的原因是放电效率用反,或者SOC更新时用了与LP模型内不同的公式。

我记得有一次结果看起来特别好,峰谷差小、成本低,但车主平均离网SOC只有0.75,明显低于预期。一查发现是放电惩罚写成了负的,等于系统通过反复放电“赚钱”,最后所有车都被放空了。所以做这类项目,不能只看目标函数值,每个约束的实际状态都要在结果里输出出来检查。

4.4 我的调试工作流

我现在的习惯是先跑一个3辆车、2小时的小场景,把功率曲线和SOC曲线画出来,逐行确认每辆车的动作都符合预期。小场景没问题之后,再逐步放大车辆数和时间窗口。每做一次改动,我都会记录三个指标:总充电量、总放电量、全部车的离网SOC分布。如果这三个指标相对基线有明显异常,就先停下加日志排查,而不是继续调参数。

另外,我会保留上一轮调度结果作为对照。有次为了测试新约束,我把结果图和前一天的旧图叠在一起看,一下发现新代码在晚上8点后把所有车都强制放电了,原因是变压器约束那里的baseLoad索引写错了一位,把负荷序列整体左移了一个周期。这类索引错误,在没有对照图时很难一眼看出来。

5. 扩展方向与个人体会

5.1 从仿真到落地还要补哪些模块

仿真能跑通,和真车能上线之间还有很大距离。第一个缺口是通讯延迟和数据质量,调度中心拿到的SOC、功率、车辆连接状态都不是实时完美的,需要考虑测量噪声和丢包。第二个缺口是预测更新,真实电价和负荷预测每隔几分钟就会更新,滚动优化需要接入实时预测接口。第三个缺口是执行偏差,实际功率指令到达充电桩后,可能因为车载协议限制无法精确执行。

我目前还在扩展的方向是把光伏发电的不确定性加进MPC里。当屋顶光伏出力预测有误差时,调度策略需要预留一部分电池容量来应对功率偏差,这就变成了随机模型预测控制。目标函数里还要加“预测偏差导致的不平衡惩罚”,比确定性模型复杂不少,但更贴近实际园区综合能源系统的需求。

5.2 代码工程化与组织建议

如果你打算长期在这个项目上迭代,我强烈建议把代码模块化。我的项目文件分成了三个层次:

  • 配置层:一个params_define.m,集中管理所有参数,包括车辆数、电池容量、效率、预测时域、惩罚系数。
  • 模型层:一个build_problem.m,根据当前周期和车辆状态构建LP或MILP问题,返回目标系数和约束矩阵。
  • 调度层:一个run_scheduler.m,负责滚动循环、状态更新、结果存储。

这样当你需要换一套电价曲线、换一组车辆参数或者换一个求解器时,不需要动核心逻辑。我甚至会把目标函数里的惩罚系数也做成数组,方便做灵敏度分析时批量修改。

存储结果时,我建议把所有中间变量统一写进一个states结构体,每个调度周期往里面追加一条记录。最后画图时,只用一段通用的画图函数从states里提取数据,比在循环里散落一堆plot要清爽得多。

5.3 个人体会

这个项目做下来,最深的感受是“算法本身不难,难在验证”。市面上很多V2G调度论文展示的效果很漂亮,但代码细节往往经不起反复推敲。最快的学习方式,不是直接套用论文参数,而是先写一个最简单的规则调度做基线,再上优化算法。如果没有对薄弱基线,你会很难判断峰值负荷下降究竟是因为调度算法有效,还是因为某些参数调得顺手。

我自己踩过多次坑之后,现在已经形成习惯:每一步改动都留下一个可以回滚的脚本,每次实验都保存结果数据和参数文件。这样做虽然烦琐,但绝大多数项目里的“玄学问题”,最后都能在对比中找到答案。V2G实时调度是一个值得长期深耕的方向,如果你也想动手做一遍,我建议从今天这个20辆车、15分钟粒度的案例开始,先把能量守恒跑通,再逐步叠加预测和不确定性,每一步都看得见摸得着,比直接啃论文踏实得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进