ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

V2G电动汽车实时调度策略:园区充电桩削峰填谷实战解析

V2G电动汽车实时调度策略:园区充电桩削峰填谷实战解析

最近在做园区充电桩聚合调控的时候,被V2G技术狠狠上了一课。以前我一直觉得电动车就是单纯的用电负载,充就完了,直到被峰时变压器过载搞得头疼,才意识到电池其实是个带轮子的储能。基于V2G技术的电动汽车实时调度策略,核心就是把每辆车的充放电变成一个可实时调整的变量,低谷充电、高峰放电,既缓解电网压力,也让用户少花电费。这篇文章我不讲PPT式的概念,直接把我建模、写代码、跑仿真到踩坑的全过程拆开说,适合正在做充电桩调度策略、微电网能量管理或电动汽车聚合响应的人参考。

先说清楚价值定位。实时调度策略解决的是"随机性"问题:你提前一天做好的充电计划永远赶不上变化——用户提前拔枪、电网负荷突变、光伏被云挡住,都需要在分钟级做出新的充放电决策。我做的这套系统,本质是一个带约束的滚动优化器,根据实时电价、车辆SOC、变压器余量,每隔15分钟重新算一次每辆车该充多少、放多少。后面我会把这套策略的数学建模、代码框架、评价指标和实测坑位全部展开,提供可直接落地参考的实现思路。

1. V2G实时调度到底在调度什么:三个关键自由度

1.1 一辆参与V2G的车,比你想的多两个捭阖空间

普通充电模式下,一台电动车对充电桩来说只有"充多大功率"这一个自由度,而且功率只能往一个方向走。V2G接入之后,自由度变成了三个:

  • 充放电功率大小,即0到最大功率之间的连续值;
  • 功率方向,也就是当前时隙是充电还是放电;
  • 充放电的时间窗口,即车插在桩上的这段时间内,把功率安排到什么时刻执行。

这三个自由度放在一起,每一辆车在响应时段内就等价于一个小型储能单元。拿我仿真里常用的参数举例,某款60kWh电池包的车型,充电桩双向功率6.6kW,接入时间从18点到次日8点,那么单辆车就具备约92kWh的"吞吐机会"。如果有10辆车在同一园区,聚合起来就是接近1MW级别的可调节能力,这对一个变压器容量只有200kW的小型微电网来说,是相当可观的灵活性资源。

1.2 "实时"和"日前"的本质差别:预测总是会错

很多教材里先教的是日前调度,也就是提前24小时把96个时隙的充放电决策全部算好。这种方案在仿真环境里看起来完美,但实际运行会出问题,原因很简单:预测必然存在误差。

最典型的场景是光伏。某天上午的功率预测是100kW,但一片云飘过来,实际输出直接掉到30kW,这时候按日前计划的充电功率仍然继续给车充电,园区只能从电网买高价电,甚至造成变压器过载。同样的道理,分时电价曲线虽然大体固定,但车辆接入时间是随机的——18点插枪的那辆车可能在17:50就已经插上了,你的日前计划压根没有覆盖到。

实时调度策略的思路是滚动式决策:每个时隙开始时,读入当前真实状态(每辆车实际SOC、是否在桩、剩余接入时间、实际负荷),再用一个短预测窗口(通常未来4到12小时)重新求解,只执行当前时隙的决策,下个时隙继续更新。这就像开车看导航,不是一次性规划好全程路线就不管了,而是每经过一个路口都根据实时路况微调。

1.3 调度对象不只是电池,还有用户的耐心

技术上能放电,不等于用户愿意让你放电。这是我在实际调试中体会最深的一点。用户关心的不是削峰率,而是我明天早上出门时车还有没有电、电池会不会因为频繁充放电提前衰减。

所以在策略设计里,我把用户约束放在和物理约束同等重要的位置:每次接入时读取用户设置的最低出行SOC,通常80%以上;离开前两小时内禁止深度放电;设置单日最大放电次数,避免用户第二天发现续航明显缩水。这些约束看起来是限制优化空间,但从项目落地角度看反而是保护伞,没有用户参与意愿,V2G的调度能力再强也是空中楼阁。

2. 把调度问题翻译成数学模型:目标函数与约束是怎么定出来的

2.1 时间离散化与决策变量定义

所有优化模型第一步都是把连续的时间切成时隙。我采用15分钟作为一个时隙,24小时就是96个时隙,这个粒度既能捕捉分时电价切换和负荷波动,又不至于让优化模型膨胀到无法求解。

每辆车在每个时隙,需要两个连续决策变量和一个二元变量:

变量含义取值范围
P_ch(i,t)第i辆车在第t个时隙的充电功率0 ~ P_max(i)
P_dis(i,t)第i辆车在第t个时隙的放电功率0 ~ P_max(i)
z(i,t)充放电状态指示0或1,0表示放电,1表示充电

注意P_ch和P_dis不能同时为正,物理上双向充电桩也禁止同时充放,所以必须有第三行这个二元变量。这个约束在后面建模部分是整个MILP模型"变难"的来源,也是很多初学优化的人容易忽略的。

2.2 目标函数:省钱、降峰、少衰减三条线怎么加权

我最初的模型只写了一个目标:最小化车主的购电费用,即充电花的钱减去放电赚的钱。跑完仿真后发现一个尴尬的结果——为了让用户省钱,车辆总挑谷时充电、峰时放电,变压器的峰值负荷确实有改善,但改善幅度不够,因为电网最关心的是"峰值那一两个时隙",而不是全天总量。

后来我把目标函数拆成三条线,用权重系数去平衡:

F = 用户净电费 + 电池退化成本 + 负荷峰惩罚

用户净电费是所有车辆充电购电费用减掉放电售电收益;电池退化成本按充放电吞吐量乘以一个单位退化成本系数;负荷峰惩罚是变压器关口功率超出某个安全阈值时的惩罚项,乘以一个较大的权重系数λ。

实际调参的时候,λ是最关键的一个旋钮。λ太小,系统只顾用户省钱;λ太大,电网舒服了,但车辆深度充放电太频繁,电池衰减严重。我的做法是把λ按"每超出1kW峰值罚10到50元"的量级去设定,在仿真里扫一遍,找出用户成本和削峰效果之间的平衡区。

2.3 约束条件里那些容易写错的细节

数学模型的骨架是约束,而约束的细节决定了策略能不能实际跑通。我踩过的几个具体点:

第一,SOC递推方程里,充放电效率的位置不能写错。正确的写法是:

SOC(i,t+1) = SOC(i,t) + (eta_ch * P_ch(i,t) - P_dis(i,t) / eta_dis) * dt / E(i)

充电时电网侧输入的电能有一部分变成热量损耗,所以电池实际接收的是P_ch乘以充电效率eta_ch;放电时电池侧放出的能量要除以放电效率,才是电网真正收到的功率。效率放错位置会让结果偏差15%以上,这一点后面专门讲。

第二,SOC运行边界不能只设0到100%。锂电池在极低和极高SOC下内阻都会升高,循环寿命折损严重,我一般限制在20%到90%,极端情况允许到15%,但要触发惩罚项。

第三,变压器关口功率约束是所有车辆净充电功率之和加上基础负荷不能超过变压器容量,但这个约束是可"软化"的。如果硬性约束无解,优化器会直接报infeasible,所以更稳妥的做法是把它做成带松弛变量的罚款约束。

3. 从公式到可运行代码:一套实时滚动调度的实现框架

3.1 数据准备:电价曲线、车辆参数、负荷场景

写代码第一步是准备两套数据。第一套是电价序列,我用的是典型商业分时电价:白天10点到15点、晚上18点到21点执行峰段电价1.2元/kWh,晚上23点到早上7点执行谷段电价0.3元/kWh,其余时段平时电价0.7元/kWh。第二套是车辆参数,我定义了十辆车的接入时间、初始SOC、目标SOC、电池容量和最大功率。

import numpy as np # 96个时隙,每时隙15分钟 T = 96 price = np.full(T, 0.7) # 谷段:23:00-07:00,对应时隙92-95和0-27 price[92:96] = 0.3 price[0:28] = 0.3 # 峰段:10:00-15:00和18:00-21:00 price[40:60] = 1.2 price[72:84] = 1.2

这里有个细节,如果你把时隙索引和24小时对应错,仿真结果会很怪。我是先整理了一张时隙表:时隙40对应上午10点整,时隙60对应下午15点,时隙72对应晚上18点,所有后续逻辑都围绕这张表展开,避免出现"峰时便宜"之类的明显数据错误。

3.2 核心优化模块:用开源求解器把MILP跑起来

优化模型我直接用Python生态里的线性规划库建模,求解器用开源的CBC内核。下面这段代码是核心优化模块的骨架,只保留了每个时隙的决策逻辑,方便看清楚变量、目标和约束的映射关系:

import pulp def solve_one_slot(cars, price_now, base_load, feeder_limit, soc_now, dt=1/4, eta_ch=0.95, eta_dis=0.95, deg_cost=0.3): n = len(cars) prob = pulp.LpProblem("V2G_OneSlot", pulp.LpMinimize) p_ch = {i: pulp.LpVariable(f"p_ch_{i}", 0, cars[i]["p_max"]) for i in range(n)} p_dis = {i: pulp.LpVariable(f"p_dis_{i}", 0, cars[i]["p_max"]) for i in range(n)} z = {i: pulp.LpVariable(f"z_{i}", cat="Binary") for i in range(n)} # 目标:购电费用 - 售电收益 + 电池退化成本 + 峰值惩罚 prob += pulp.lpSum( p_ch[i] * price_now - p_dis[i] * price_now + deg_cost * (p_ch[i] + p_dis[i]) for i in range(n) ) for i in range(n): c = cars[i] # 不能同时充放电 prob += p_ch[i] <= c["p_max"] * z[i] prob += p_dis[i] <= c["p_max"] * (1 - z[i]) # SOC上下限(使用执行后的理论SOC) next_soc = soc_now[i] + (p_ch[i] * eta_ch - p_dis[i] / eta_dis) * dt / c["E"] prob += next_soc <= c["soc_max"] prob += next_soc >= c["soc_min"] # 变压器关口总功率约束 prob += pulp.lpSum(p_ch[i] - p_dis[i] for i in range(n)) + base_load <= feeder_limit prob.solve() return [p_ch[i].value() or 0 for i in range(n)], [p_dis[i].value() or 0 for i in range(n)]

实际项目里我还会在这个模块外层加一个状态检查,比如车辆是否在桩、用户是否设置了强制充电时段,不在桩的车辆直接跳过,避免优化器把一辆已经开走的车算出放电功率。

3.3 滚动优化外层循环:每15分钟重算一次

核心求解器只是一个时隙的决策,而实时调度策略的重点在"滚动"。整个外层循环的骨架大概是这样的:

for t in range(T): # 1. 读取当前状态:每辆车最新SOC、在桩状态、当前基础负荷 # 2. 根据预测模块生成未来H个时隙的电价和负荷预测 # 3. 调用solve_one_slot,得到当前时隙的P_ch和P_dis # 4. 把功率指令下发给充电桩模拟器 # 5. 按执行结果累加SOC,并计算实际电费

滚动窗口我一般取未来8小时即32个时隙。窗口太长,求解时间和预测误差都会变大;窗口太短,调度太短视,可能在电价最低点之前就把电池充满了。实测下来,8小时窗口在计算速度和策略效果之间比较均衡。

另一个关键点是"只执行第一个时隙的决策"。这一步看似多余,其实是整个策略实时性的灵魂。因为第二个时隙开始前,真实SOC已经发生变化,预测负荷也可能有偏差,原计划的第二个时隙决策大概率已经不合适了。每个时隙都重新算,才是"实时调度"和"固定曲线调度"的本质区别。

3.4 实时性有多"实时":求解时间窗口的控制

真正落地时,一个时隙只有15分钟,但下发指令、通信、硬件响应都要时间,所以优化求解时间必须控制在很短范围内。我的经验是给优化器设一个硬性超时限制,比如5秒。商用求解器和开源求解器在这里差距很明显,同样是10辆96时隙的MILP,开源CBC慢的时候要几十秒,一旦接近超时阈值,我就把优化时域从96收缩到32,或者关闭一部分二元变量,改用离散功率档位求解。

如果连32时隙的MILP都慢到不可接受,还有一个备用方案:把车辆按接入时段分群,每一群单独求解,群与群之间只传递变压器关口功率的预分配值。这个"分解协调"思路能显著降低单次求解规模,代价是全局最优性会损失一两个百分点,但对实时控制来说完全够用。

4. 仿真结果怎么评判:削峰率、成本节省率与用户满意度

4.1 四个指标:不要只盯着电网侧

评价一套实时调度策略,我习惯同时看四个维度的指标:

  • 削峰率:变压器关口峰值负荷相对"即插即充"基准的下降比例,反映电网受益程度;
  • 用户净成本:电费支出减放电收益,加上电池退化折算成本,反映用户愿不愿意参与;
  • SOC保障率:所有车辆在离开时达到用户目标SOC的比例,反映策略的可用性;
  • 电池循环折算:统计总充放电吞吐量换算成等效循环次数,用来评估寿命影响。

这四个指标单独看都有片面性。削峰率漂亮但SOC保障率低,说明策略牺牲了用户体验;用户成本最低但削峰率低,说明电网没有获得应有的灵活性。所以我的仿真报告里永远是一张四合一对比表,而不是单拎一个数字。

4.2 对比实验:即插即充、谷充峰放、实时滚动调度

为了让结果有参照系,我设计了三个策略做对比。第一个是基准策略"即插即充",车插上就以额定功率充电到目标SOC为止,不做任何智能决策。第二个是规则策略"谷充峰放",电价低于0.5元/kWh时充电,高于1.0元/kWh时放电,规则固定不随负荷变化。第三个就是本文的实时滚动调度策略。

仿真跑了10辆车、96个时隙、一组典型工作日的电价和负荷数据后,得到了一张这样的对比表:

策略关口峰值负荷(kW)削峰率用户日均净成本(元)折算电池损耗(元)
即插即充86.50%4210
谷充峰放77.610.3%35228
实时滚动调度(无负荷惩罚)74.214.2%34135
实时滚动调度(含负荷惩罚与放电补贴)61.429.0%30341

最后一个方案把负荷峰惩罚和电网给的放电补贴同时计入目标,电网削峰效果最好,用户净成本也最低,但电池损耗最高。这就是实时调度策略在代际转换间的真实权衡——你不能既要马儿跑,又要马儿不吃草,电池损耗就是参与V2G的显性代价。

4.3 结果的正确解读:省钱和削峰之间有个平衡点

很多人拿到仿真结果后的第一个反应是:既然最后一个方案削峰率最高又省钱,为什么不一直用这个参数?答案在于电池损耗那条线没有完全反映到用户流水中。在真实项目中,如果电网没有给放电补贴,用户单纯为了削峰去放电只会让电池提前衰减,最后换电池的钱远超省下的电费。

我做了个参数敏感性测试:把负荷峰惩罚系数从0逐步调到100,削峰率从14%升到29%左右后趋于饱和,而用户净成本在惩罚系数为20到30之间有个最低谷,之后再增大惩罚,用户成本反而上升。这个最低谷就是策略的最优工作点,实际部署时应该把系数定在这个区间,而不是盲目追求最大削峰率。

5. 调试V2G调度代码踩过的几个坑:从求解爆炸到效率写反

5.1 车辆一多MILP就变慢,怎么拆解

第一个让我抓狂的问题是求解速度。15辆车、96时隙的MILP在开源求解器上有时要跑三分钟,而实时调度要求每个时隙几秒内出结果,完全不够用。

排查后发现,求解慢的根源是车辆接入状态和SOC边界差异太大,优化器花了大量节点在探索无效的充放电组合。我做了两个改进:一是把每辆车的可调度时段单独计算,只有接入状态为1的时隙才创建充放电变量和二元变量,接入前和离开后的时隙完全不进模型;二是把连续功率离散成五个档位,相当于把一部分连续变量变成整数变量,虽然最优性略有损失,但求解时间直接下降到原来十分之一。这个"先压缩可行域,再离散化"的做法,是让MILP模型从实验室走向实时控制的关键。

5.2 充放电效率那个eta到底放在哪里

第二个坑的隐蔽程度极高。某个版本里我把SOC递推写成了下面这样:

SOC_next = SOC_now + (P_ch - P_dis) * eta * dt / E

表面上看起来没什么问题,但充放电效率在物理方向上完全不同。充电时,电网输入1kWh,电池只存进0.95kWh;放电时,电池放出1kWh,电网只收到0.95kWh。如果充放都用同一个效率乘以净功率,放电时电池SOC的下降幅度会被低估5%,同时电网侧接收的功率被高估5%,一进一出,整个削峰效果被高估了将近10个百分点。

这个错误我在一段调试场景里花了大半天才定位到,当时的直觉是"效率位置不对导致SOC曲线越来越怪",但数据上并没有报错。排查手段很简单,电车充满一小时的SOC增量超过6.27kWh,说明效率写反了。写出正确的递推方程,对比电池SOC曲线和电网实测功率曲线,两边对得上,才算真正校正。

5.3 电池退化成本不是拍脑袋给的系数

还有一次,把电池退化成本系数随便设成0.5元/kWh,结果优化器直接不给任何车放电了,V2G完全变成单向充电。这个系数其实有明确的计算逻辑:

单位吞吐退化成本 = 电池更换成本 / 全寿命总吞吐量

比如一块60kWh的电池,更换成本6万元,全寿命循环3000次,每次循环按可吞吐的80%电量即48kWh计算,总吞吐量就是144000kWh,退化成本约为0.42元/kWh。如果是循环寿命更好的磷酸铁锂电池,3000到5000次循环,退化成本能降到0.25元左右。

这个估算表可以作为初始参数的参考:

电池容量(kWh)更换成本(元)循环寿命(次)总吞吐量(kWh)单位退化成本(元/kWh)
606000030001440000.42
606000050002400000.25
10012000040003200000.38

把退化成本计入目标函数后,峰谷价差必须超过这个单位退化成本,V2G放电才算真正有经济价值。电价差大的地区适合做深度充放,电价差小的地区则应该把V2G定位成"备用容量服务"而非"能量套利"。

5.4 出行保障约束写得太"软"带来的体验崩坏

最后一个坑来自用户侧。我最初只加了一条约束:离开时SOC不低于0.8。仿真数据也满足,但把控制策略放到实车上模拟时发现,车在离开前最后两个时隙还在深度放电,然后在剩余时间里以接近额定功率的速率猛充电,电池温度和充电体验都很差。

后来我加上了"放电保护窗口":用户设定离开时间之前的2小时(即8个时隙)内,车辆禁止放电,只能充电。同时限制单辆车的日最大放电次数为2次,避免系统为了省几分钱让同一辆车反复深度充放。加了这些软约束之后,优化器的解虽然在经济上稍微变差,但整体方案的实用性和用户接受度大幅提升,这才是策略能走出仿真环境的关键。

RELATED READING

延伸阅读

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