ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

考虑不同充电需求的电动汽车协调充电调度复现指南

2026/10/3 3:24:41 拓冰建站 浏览量
考虑不同充电需求的电动汽车协调充电调度复现指南 半年前我对着某篇期刊论文里的算法流程图整整两天没跑出一条像样的充电功率曲线。最后发现问题根本不在算法实现而在需求数据构造——当时我把几十辆车的到达时刻、离开时刻、目标SOC一股脑揉成了同一个优化模板。电动汽车协调充电调度这东西论文里一句话带过的是“不同充电需求”落到代码里却是完全不同的可行域、约束组和目标权重。这篇博文就围绕“考虑不同充电需求的电动汽车协调充电调度方法”这条线从需求分类、数学建模、Cvxpy代码实现到复现排错完整走一遍。整个过程用开源求解器就能跑不依赖商业License适合正在复现充电调度论文、做电网或交通电气化方向毕设、以及想把手里的调度算法从“能跑”推进到“跑对”的同学。我尽量把那些论文不写、课堂不讲的细节都摊开说。1. 需求不是字符串而是可行域为什么分类决定了调度质量1.1 一场充电调度本质上在解“并发任务的资源分配”如果你只把“不同充电需求”理解成“有的车想充到80%有的车想充到100%”复现出来的代码一定走样。我自己的经验是把每一辆车当成一个独立任务来看。这个任务由六个参数唯一确定接入时刻车什么时候插枪离开时刻车什么时候必须走初始SOC插枪时电池还剩多少电目标SOC用户希望离开时达到多少额定充电功率这辆车或这台桩最大能扛多少kW电池容量kWh决定每个SOC点对应的能量这六个参数一组合一辆车在实际调度中的可行动作范围就定死了。同样是“我要充电”一辆早上8点接入、下午5点取走的通勤车和一辆下午2点接入、3点就必须走的应急车在数学上完全不是同一个问题。前者的功率可以摊到低价时段慢慢充后者即便电价再贵也得在1小时内灌进尽可能多的电。用并发资源的语言翻译一下接入时刻是任务就绪时间离开时刻是截止期需要的充电电能量是任务的负载量充电功率是处理速率变压器和线路容量是共享资源分时电价则是随时间波动的资源成本。整个调度问题就是在这些并发任务之间分配功率这个稀缺资源让总成本最低同时尽量不违约。这个视角一旦建立后面写约束的时候就非常顺每个任务的功率上下限由车辆和桩决定时间窗口由接入和离开决定累计能量由SOC演化方程决定共享资源上限由变压器决定。1.2 紧急度把“不同充电需求”变成一个可计算指标需求差异不能只停留在自然语言层面要复现调度代码必须把它量化成一个可比较、可参与计算的指标。我在复现里最常用的量化指标是紧急度θθ ΔE / (T_avail × P_max)其中ΔE是目标SOC与初始SOC之间需要补入的能量kWhT_avail是可用充电时间小时P_max是额定充电功率上限kW。这个指标的实际含义是在理想情况下按最大功率连续充电、刚好能完成需求所需的充电时长占可用时长的比例。如果θ0.5说明这台车用一半的可用时间就能充满很从容如果θ0.9说明它几乎要从头到尾顶着最大功率充稍有波动就完不成目标。有了这个指标我习惯把需求分成四个层级层级紧急度范围典型场景调度处理方式宽松型θ ≤ 0.5单位停车场白天停满8小时可灵活分配功率尽量在低谷时段充电常规型0.5 θ ≤ 0.8家用车辆夜间充电正常约束可在峰谷间平移部分功率紧张型0.8 θ ≤ 1.0网约车快充补电基本锁定高功率留给调度的弹性很小极限型θ 1.0突发应急出行即便满功率也无法满足目标必须做软约束或降级目标紧急度不直接出现在模型变量里但它决定了三件事一是目标函数里用户满意度惩罚项的权重二是软约束的松弛范围三是在模型无解时优先舍弃哪些需求。后面讲代码和排错时会反复看到这个影子。2. 复现前必须吃透的三个数学点目标项量纲、SOC更新、软硬约束2.1 目标函数不要只写购电成本满意度必须有明确的量纲很多初学者复现时直接写min Σ 电价 × 功率。这个目标本身没错但只适用于“所有车辆需求都必须在离开时满足”的硬约束场景。一旦变压器容量不够模型直接无解目标函数再漂亮也白搭。更贴近论文做法的目标函数其实是两项min Σ_t price[t] × P_total[t] × dt λ × Σ_i penalty_i第一项是购电成本第二项是用户满意度损失。关键问题在于penalty_i 的量纲是什么λ 怎么取才能和购电成本“同台竞技”。我在代码里让 penalty_i 表示“车辆i在离开时刻的目标SOC与实际SOC之间的差额”。这是一个0到1之间的无量纲数比如目标0.9、实际0.85差额就是0.05。如果λ取2000那么差5个百分点就对应100元的惩罚。这样设定后λ就有了非常直观的物理含义每差1个SOC点的价格。实际调参时你可以先跑一遍纯成本模型看哪些车无法满足目标然后根据商业场景给“1个SOC点”定个价格比如快充车辆每少1个百分点补偿20元λ就取2000。需要提醒的是惩罚项必须作用在“无法满足的部分”而不是全部目标差额。如果一个需求本来就能满足这辆车不应该产生惩罚。所以要引入辅助变量u_i ≥ 0并加约束v[soc_target] - S[i, v[dep]] ≤ u_i这样u_i只在“实际SOC低于目标”时才会大于0。这个技巧比直接用 max(0, target - actual) 这类非光滑函数更适合求解器。2.2 SOC演化效率系数和能量单位是事故高发区SOC演化方程是所有复现代码中错误率最高的地方。公式本身不复杂S[i, t1] S[i, t] eff_i × P[i, t] × dt / cap_i但这里几乎每个符号都能埋坑。先说效率eff电池充电不可能100%把电网电能转成电池化学能一般取0.9到0.95之间。有些人复现时把这个系数漏了结果所有车辆的SOC虚高10%调度结果看起来“全都能满足”实际工程里根本充不到。再说能量单位电池容量cap_i是kWh功率P[i,t]是kW时间dt是小时三者相乘/相除之后才能得到无量纲的SOC增量。如果时间步长是15分钟dt0.25忘了乘这个0.25SOC增量直接放大4倍一晚上能把电池充爆。我见过一个最隐蔽的错误把SOC方程写成 S[i,t1] S[i,t] P[i,t] × dt / cap_i看起来对但P[i,t]用的是“桩侧交流功率”而效率是0.9结果每次迭代都高估SOC——小算例看不出来一跑48辆车、24个时段的场景误差会被累积到不可接受的程度。所以复现代码里效率系数放哪一侧、以什么单位参与运算必须在写代码前用计算器先验算一个单步过程。2.3 硬约束与软约束的分界线决定了模型会不会“假死”这个点我是在被OSQP返回“infeasible”折磨了一下午之后才彻底想明白的。充电调度里的约束可以分成两类第一类是物理硬约束比如充电功率不能超过额定值、总功率不能超过变压器容量。这些约束没有任何商量余地必须写成硬约束。第二类是合约软约束比如“用户希望在8点前充到90%”。这类约束看起来是硬性的但它的本质是交易条款在资源不足时是可以协商的——协商的结果就是赔钱或赔积分。复现论文代码时如果你把第二类约束也全部写成硬约束典型后果是变压器容量稍微紧张一点整个优化模型直接无解。而论文里跑出来的漂亮的调度结果大概率是用了软约束。所以我的建议是一开始就把“离开时SOC不低于目标”写成带辅助变量u_i的软约束让模型有退路。后文第4节还会专门讲这个问题。3. 可复现的主干代码从数据准备到曲线输出3.1 数据准备把需求画像翻译成参数字典我用的是Cvxpy OSQP这套免费开源组合原因很简单论文复现阶段最重要的是模型逻辑正确、结果可解释没必要一开始就被Gurobi的License卡住。Cvxpy的建模语法和论文公式几乎一一对应后面想切Gurobi只需要改求解器参数和个别接口。先安装依赖pip install cvxpy numpy matplotlib然后是核心数据定义。这里我把每辆车定义成一个字典字段和论文里的“需求”一一对应import numpy as np import cvxpy as cp T 24 # 离散时段数 dt 1.0 # 每个时段的小时数 # 分时电价单位元/kWh # 0-6点低谷0.3元6-18点高峰1.5元18-24点平段0.8元 price np.array([0.3]*6 [1.5]*12 [0.8]*6) # 车辆集群 vehicles [ {arr: 0, dep: 24, soc0: 0.2, soc_target: 0.9, pmax: 7, cap: 60, eff: 0.92}, {arr: 0, dep: 24, soc0: 0.5, soc_target: 1.0, pmax: 6, cap: 40, eff: 0.92}, {arr: 8, dep: 20, soc0: 0.4, soc_target: 0.85, pmax: 11, cap: 80, eff: 0.93}, ] cap_limit 200.0 # 变压器/线路容量上限单位kW N len(vehicles)这里有个非常容易出错的地方dep24表示第24个时段的起点也就是一天结束时。在时间离散语义里充电活动发生在 [arr, dep) 这个半开区间即arr时刻之后、dep时刻之前。如果dep8说明车在第8小时开始时离开那么最后一个可充电时段是 [7, 8)对应索引7。很多复现代码错在把dep当“最后可充时刻”结果多充了一个小时。这个坑在第4节展开。3.2 用Cvxpy构建优化问题变量、约束、目标一次说清核心代码分四步定义变量、拼约束、写目标、求解。我直接给出可以跑的完整版本注释尽量详细。# 1. 定义优化变量 # P[i, t] 表示第i辆车在第t个时段的充电功率非负 P cp.Variable((N, T), nonnegTrue) # S[i, t] 表示第i辆车在第t个时段初的SOC维度T1是为了覆盖dep时刻 S cp.Variable((N, T 1)) # u[i] 表示第i辆车离开时未满足的目标SOC差额非负 u cp.Variable(N, nonnegTrue) constraints [] for i, v in enumerate(vehicles): # 初始SOC constraints.append(S[i, 0] v[soc0]) # 充电功率只能在车辆接入期间存在且不超过额定值 mask np.zeros(T) mask[v[arr]:v[dep]] 1.0 constraints.append(P[i, :] v[pmax] * mask) # SOC演化方程必须带效率系数和dt for t in range(T): constraints.append( S[i, t 1] S[i, t] v[eff] * P[i, t] * dt / v[cap] ) # 离开时刻的目标差额软约束 constraints.append(v[soc_target] - S[i, v[dep]] u[i]) # 变压器容量约束物理硬约束 constraints.append(cp.sum(P, axis0) cap_limit) # 目标函数购电成本 满意度惩罚 cost sum(price[t] * cp.sum(P[:, t]) * dt for t in range(T)) lambda_penalty 2000.0 penalty lambda_penalty * cp.sum(u) prob cp.Problem(cp.Minimize(cost penalty), constraints) prob.solve(solvercp.OSQP, eps_abs1e-8, eps_rel1e-8) print(求解状态:, prob.status) print(总成本(元):, round(cost.value, 3)) print(不满意惩罚(元):, round(penalty.value, 3))说几个代码里容易被忽略的细节第一S变量的维度是(N, T1)不是(N, T)。因为要记录第0时刻的初始SOC和之后T个时段的转移结果S[i, dep]这个值必须存在。维度少一约束就写不进去。第二mask那边用 v[arr]:v[dep]保证半开区间语义。这个切片写法对应的是“arr到dep之前的时段”。如果你用数字手搓索引比如写 range(arr, dep1)模型就会允许车辆在离开时刻之后多充1个小时。短时段场景看不出问题全天24小时场景下总负荷曲线会多处一小截功率削峰效果直接被污染。第三OSQP默认容差比较松对于SOC量级0到1、功率量级几十到几百、成本量级几千的混合问题默认容差可能返回假可行。我在solve里显式设了eps_abs和eps_rel到1e-8。这个数值经验在论文复现里特别重要很多“结果怪怪的”其实不是模型错是求解器容差在作怪。3.3 结果提取与可视化看图检查不能只盯数字求解完之后不要只打印总成本一定要把功率曲线和SOC曲线画出来。我习惯的检查顺序是先看每辆车的SOC曲线是否单调上升、离开时刻是否到达目标再看总功率曲线是否在任何时刻超过变压器容量。两张图都干净才能说代码基本跑对了。P_opt np.round(P.value, 4) S_opt np.round(S.value, 4) total_load P_opt.sum(axis0) # 每辆车离开时刻的SOC print(车辆离开SOC:) for i, v in enumerate(vehicles): print(f 车{i}: 实际 {S_opt[i, v[dep]]:.3f}, 目标 {v[soc_target]}) # 总负荷曲线峰值 print(总负荷峰值(kW):, total_load.max()) print(变压器容量(kW):, cap_limit) import matplotlib.pyplot as plt t_axis np.arange(T) fig, (ax1, ax2) plt.subplots(2, 1, figsize(10, 6), sharexTrue) ax1.plot(t_axis, total_load, markero, label总负荷) ax1.axhline(cap_limit, colorred, linestyle--, label容量上限) ax1.set_ylabel(功率/kW) ax1.legend() ax1.grid(True) for i, v in enumerate(vehicles): ax2.plot(np.arange(T 1), S_opt[i, :], marker., labelf车{i}) ax2.set_xlabel(时段) ax2.set_ylabel(SOC) ax2.legend() ax2.grid(True) plt.show()如果SOC曲线出现下降或锯齿优先检查效率和dt如果总功率贴上限很久很久说明调度确实在压着容量跑如果某辆车全程零功率大概率是mask切片把可充时段全屏蔽了。这些结果层面的“意外”个个都指向模型或数据的问题。4. 复现路上的五个真实翻车点4.1 接入与离开时刻的“半开区间”语义错位这个坑我在3.1已经提过这里专门说清楚。假设某辆车8点接入、17点离开时间粒度为1小时。它真正能充电的时段是8点到16点59分也就是 [8, 17) 这个半开区间。在Python切片里mask[8:17] 1正好覆盖索引8到16共9个时段。很多人习惯写成mask[8:18]甚至直接用range(8, 171)这就会多给1个小时的充电窗口。多给1小时看起来只影响一个时段但优化器是“贪”的——它会把这个多出来的窗口安排在电价最低的时段。如果那个时段恰好在凌晨低谷末尾结果就是所有车都往那个时段挤变压器容量直接爆炸然后你反过头来怀疑约束写错了其实错的只是区间边界。我的排查方法是打印每辆车的可充电窗口起点和终点和原始车辆参数逐条核对。不要相信自己的记忆切片这种东西差一个数字视觉上根本看不出来。4.2 功率、容量、时间步的数值错配复现公文里最常见的数据单位组合是功率kW容量kWh时间小时。三个单位看着统一但实际代码里时间步长很可能不是1小时。如果你把原始数据从15分钟粒度重采样成1小时功率数值没问题但dt必须跟着改反过来如果你拿到的是15分钟粒度的电价和功率曲线却让dt1那SOC增量会被高估4倍。这类错误有个典型特征SOC曲线在短时间内冲到1.0并保持平顶。一旦看到平顶SOC第一反应不是“电池满了”而是检查“dt乘了吗”“效率乘了吗”“容量单位对吗”这三个问题。用计算器算一遍60kWh电池7kW功率1小时应该增加7×0.92/600.107个SOC点。如果代码跑出来的每小时SOC增长超过这个值单位链一定断了。4.3 变压器容量不足时的infeasible硬约束的假死与软约束的救场一旦多辆车同时接入且需求都比较紧变压器容量约束和“离开时SOC不低于目标”的硬约束会直接打架求解器返回infeasible。此时很多人会去调节能器参数、换求解器其实问题在模型设计。正确做法在第2.3已经埋了伏笔把“目标SOC”从硬约束改成软约束。具体操作就是代码里的辅助变量u_i让“无法满足”这件事变成一个可量化的成本而不是一个不可行的区域。这不是投机取巧而是论文里常见的“需求响应”“可削减负荷”建模用户允许在极端场景下降低充电目标但需要获得补偿。我在实际复现中会做一个对比实验同一组数据分别用硬约束和软约束跑一遍。硬约束返回infeasible软约束会给出一个带惩罚成本的最优解。这个对比本身就是论文里非常有说服力的图表素材。4.4 OSQP默认容差太大假可行与假不可行Cvxpy背后默认调用OSQP求解凸二次规划。OSQP是ADMM类算法收敛判据是残差小于容差。默认eps_abs和eps_rel为1e-5对很多问题够用但充电调度目标函数的数值跨度比较大成本项几千、SOC量级0到1、惩罚项系数2000约束矩阵的条件数偏高。默认容差下可能出现两种恶劣情况假不可行明明有可行解求解器报infeasible。假可行状态说是optimal但S[i, dep]和目标SOC差了好几个百分点检查约束时又对不上。我后来养成的习惯是任何论文复现的求解调用都显式指定eps_abs1e-8, eps_rel1e-8必要时把max_iter提高到200000。别小看这组参数它能把一堆莫名其妙的“玄学问题”直接变成确定性错误。如果你坚持用Gurobi那默认容差反而偏严可以适当放宽但一定要知道自己在改什么。4.5 电价曲线与负荷曲线的时间轴没对齐最后一个坑不是模型问题是数据对齐问题。分时电价通常以“0-6点低谷、6-18点高峰、18-24点平段”这样的自然时间划分。但如果你的调度起点不是0点或者车辆数据里的时间基准和电价时间基准有偏移就会出现“低谷充电却按高峰电价计费”的诡异结果。我踩过一次很实际的坑某份数据里的负荷曲线起点是上午10点电价表按0点开始排列两个时间轴错位10小时结果优化器疯狂在“看起来便宜”的时段充电实际对应的是电网高峰。这个错位不报错也不影响可行性只是成本计算结果完全失真。复现任何带分时电价的调度论文第一步永远是画一张时间轴对照表把电价时段和调度时段的起始点对齐再进模型。5. 从“能跑”到“跑对”手算验证与自动校验5.1 一个能拿计算器验证的最小算例代码跑通之后最重要的事是用一个手算能算清的小例子做验证。我推荐从单台车起步。构造这么个场景一辆车0点接入、24点离开初始SOC 0.2目标SOC 0.9额定功率7kW电池容量60kWh充电效率0.92电价谷段0.3元0-6点、峰段1.5元6-18点、平段0.8元18-24点。需要补入的能量ΔSOC0.9-0.20.7对应0.7×6042kWh的电池侧能量折算到电网侧是42/0.9245.65kWh。低谷时段6小时按7kW能充42kWh电网侧能量只差3.65kWh。所以最优策略是低谷期全速充6小时然后在高峰开始后继续以大约3.65kW再充1小时因为功率可以低于额定值所以不必凑满7kW。手算的结论前6个时段功率为7kW第7个时段功率约3.65kW之后为0总成本42×0.33.65×1.512.65.47518.075元。拿这个手算结果去对照代码输出如果功率曲线、总成本都和手算一致说明SOC演化方程、电价累加、目标函数的单位链全部正确。我从不让代码直接去跑几十辆车的复杂场景必须先过这一关。5.2 自动校验清单把正确性检查写进代码手算只能验证极小算例复杂场景必须靠程序自动校验。我通常在求解之后加一段断言代码作为复现的“最后防线”# 校验1任何时段总功率不超过变压器容量 assert np.all(total_load cap_limit 1e-4), 总负荷越限 # 校验2任何车辆的充电功率不超过额定值 assert np.all(P_opt np.array([v[pmax] for v in vehicles])[:, None] 1e-4), 功率越限 # 校验3非接入时段不允许充电 for i, v in enumerate(vehicles): mask np.zeros(T) mask[v[arr]:v[dep]] 1 assert np.all(P_opt[i, mask 0] 1e-4), f车{i}在非接入时段充电 # 校验4SOC终值不低于目标软约束允许小误差但差距必须在惩罚范围内 for i, v in enumerate(vehicles): assert S_opt[i, v[dep]] v[soc_target] - 1e-3 or u.value[i] 0, f车{i}未达目标且无惩罚 print(所有自动校验通过)这四条断言能抓住绝大多数“看起来能跑、实际是错的”情况。第四条写得比较宽如果你用的是硬约束版可以直接收紧为S[i, dep] target - 1e-4。我个人更推荐输出所有车辆的“目标SOC vs 实际SOC”表格肉眼扫一遍比断言更能发现模式性的偏移。5.3 没有削峰项时解不唯一为什么同样代码结果不同复现多车协调调度时另一个让新手困惑的现象是同一份代码、同一份数据不同机器上跑出来结果不一样但总成本又一样。这很可能是“目标函数不含削峰项”导致的解不唯一。举一个真实的算例。两辆车都0点接入、24点离开。A车初始0.2、目标0.9、7kW、60kWhC车初始0.5、目标1.0、6kW、40kWh。变压器容量10kW电价同前。A需要电网侧45.65kWhC需要13.04kWh总需求58.69kWh。低谷6小时变压器最多供给60kWh理论上都塞进低谷也能完成。但两车同时满功率是7613kW超过10kW容量。可行的分配方案非常多A跑6kW、C跑4kW或者A跑5kW、C跑5kW只要6小时内总能满足总需求购电成本都是0.3×58.6917.6元没有差别。于是优化器返回的功率曲线是“众多最优解之一”而不是唯一答案。这也是为什么很多论文里目标函数除了成本还会加一个负荷方差或峰值惩罚项——不为别的就是为了让解唯一、让调度曲线可复现、可比较。如果你复现的论文里没写削峰项只看功率曲线形状自然很难跟原论文完全一致。这属于目标函数设计层面的正常差异不是代码bug。6. 复现之后还能往哪走从离线调度到工程落地6.1 滚动时域调度别把24小时当成铁板一块上面所有代码都是离线优化把全天24小时一次性算完假设所有车辆需求提前已知。真实场景里车辆是陆续接入的初始SOC也在实时变化。更贴近工程的做法是滚动时域调度MPC每15分钟或每小时重新跑一次优化只执行当前时段的功率指令后面的时段作为预测窗口。这样做的好处是能吸收新接入车辆和SOC测量误差代价是每次重新求解的耗时必须压到分钟级以下——好在CvxpyOSQP对这个规模的凸问题非常快。我在扩展代码时会保留离线模型的全部约束只把“接入时间窗”改成“从当前时刻到预测窗口终点”再把每辆车的初始SOC换成最新遥测值。这套MPC框架和本文的模型几乎完全复用改动量很小。6.2 从功率曲线到真实的充电桩通信协议层的考虑调度代码算出来的是功率参考值真正要执行还得把功率指令下发到充电桩。这个环节就绕不开通信协议了——常见的有充电桩与充电站控制器之间的CAN报文以及部分场景下的Modbus RTU寄存器读写。功率调度值和实际执行值之间的偏差、报文解析时的字节序、寄存器地址映射都是工程落地的经典问题。不过我的态度是复现论文阶段先把优化模型做对通信协议是后话。如果你只是为了复现算法、跑通实验Cvxpy这一层就够了如果要做真桩测试再考虑CAN报文解析或Modbus协议栈那是另一个完全不亚于调度的深坑。复现这类调度代码我的核心建议一直只有一个先让单台车在计算器层面验证SOC演化再加多车、加变压器容量、加各种花式需求。上面这套框架把需求画像、数学建模、代码实现和常见排错串起来之后我已经用它复现过三篇不同期刊的充电调度论文每篇的“需求差异”处理方式不同但底层模型几乎都是这篇文章讲的东西。把这条主线吃透你手里的调度代码就不会再“玄学”了。