尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

电动汽车充电站日前可调度模型复现:建模、代码与避坑指南

电动汽车充电站日前可调度模型复现:建模、代码与避坑指南 上个月我花了整整一周时间把一篇关于电动汽车充电站日前可调度模型的论文从公式到代码完整复现了一遍。所谓“日前可调度模型”简单说就是充电站作为电网和车主之间的聚合商在日前24小时的时间尺度上根据预测的车流、分时电价和车辆初始电量提前把每一台充电桩的逐时充电功率排出来同时算出充电站整体在每个时段的功率可调区间。这个模型听起来不复杂真复现起来却有不少细节坑数据口径、时间粒度、SOC更新逻辑、0-1变量写法任何一处出错结果就完全对不上论文。这篇文章把我复现的过程拆成五个部分来讲先教你判断论文里的“可调度模型”究竟属于哪一种然后给出可以直接跑的数学建模和Python代码骨架再延伸讲解怎么从一条功率曲线算出一个可调度区间最后把复现中踩过的五个大坑和排查方法整理成清单。读完你应该能独立复现一类常见的充电站日前调度论文并且知道怎么往下一步探索。1. 复现前先分清你手里的“日前可调度模型”到底在算什么1.1 三种常见模型定位经济调度、可调度域、潜力评估我接触过的充电站日前调度论文表面都在说“可调度模型”实际上做的事情差别很大。如果开场就看错类型后面代码基本白写。我把最常见的三种归了个类模型类型核心输出典型目标函数复现难度站内经济调度模型每台充电桩逐时充电功率购电成本最小 / 收益最大中等可调度域可行域模型充电站整体各时段功率上下界最大化或最小化某一时段总功率偏高可调度潜力评估模型基于统计的灵活调节容量曲线无需优化多为统计估算较低第一种“站内经济调度”最常出现在期刊论文的算例部分决策变量是每台车、每个时段的充电功率约束包括变压器容量、车辆停驻窗口、SOC动态。第二种“可调度域模型”近几年特别火很多论文的关键词叫“灵活域”“可行域投影”它不关心桩到底怎么分配只关心“充电站整体在每个时段能压到多低、能顶到多高”输出是一条上下边界包络线。第三种“可调度潜力评估”更多是宏观分析用车群行为数据估算某区域可上调下调容量通常不做精细的车辆级建模。如果论文里出现大量车主出行统计曲线几乎没有逐台车的SOC约束那大概率是这类。1.2 拿到论文先看这三处立刻判断模型类型复现第一步不是写代码而是快速定位论文模型类型。我一般只查三个地方。看决策变量列表。如果公式里出现 $p_{i,t}$ 这种带车辆序号和时段下标的变量说明是车辆级经济调度模型如果只出现 $P_t^{min}, P_t^{max}$ 或者一个整体的功率变量说明是可调度域模型。看变量类型声明。出现 $x_{i,t} \in {0,1}$ 或者“binary”字样的一定是混合整数规划大概率是因为加了“某辆车在一个时间段内只能连续充电”或者“同时充电车辆数限制”这类现实约束。纯线性规划论文很少写二进制变量。看算例输出的图。如果结果图是一堆彩色柱状图横轴是24个时段纵轴是功率每根柱子对应一辆车那是经济调度模型如果输出是两条包络线夹着一条基准功率曲线那是可调度域模型。把这三处看完还分不清再看摘要里有没有“profit”“cost minimization”如果有目标函数主导有“flexible region”“boundary”以区间输出为主导。模型类型判断对了后面选求解方法、对结果的标准才有谱。1.3 数据没备齐之前不要动代码数据与工具清单复现论文最痛苦的不是代码是数据。很多论文把数据藏在附录或者一个不起眼的csv链接里甚至根本不公开。我在动手前会先列一张数据清单缺什么补什么宁可多花一天找数据也不要写一半再去换。数据项说明来源建议分时电价峰平谷或24小时逐时电价单位元/kWh当地电网公开目录电价车辆到达/离开时间每辆车的停驻时间窗口单位与调度时段一致论文附录或停车场记录初始SOC与目标SOC到达电量比例和离开期望电量比例论文表数据或正态分布生成电池容量车型对应电池kWh取典型值小车40-60SUV 70-100充电功率上限单桩功率kW常见7kW交流、60kW直流变压器/线路容量充电站总进线容量kW论文或实际配电容量充电效率交流桩0.85-0.95直流桩0.9-0.95经验值工具方面我用Python 3.11配PuLP。开源求解器用CBC它对于几十辆车、24个时段的MILP能扛得住。如果论文里明确写了Gurobi或CPLEX那你优先用这两个学术版免费性能比CBC强不少。Gurobi对MILP的对称性处理和加速技巧都很成熟复现大算例时能少受很多罪。2. 日前经济调度建模实战目标函数、约束条件与代码骨架2.1 决策变量与目标函数怎么写为什么非要用0-1变量我复现的这类模型标准形式可以写成下面这样。假设把一天分成T个时段站里有I辆车每辆车 i 有一个停驻窗口 $[\text{arrival}_i, \text{departure}_i)$只能在窗口内充电。决策变量有两个。连续变量 $p_{i,t}$ 表示第 i 辆车在第 t 个时段的充电功率单位kW二进制变量 $x_{i,t}$ 表示第 i 辆车在第 t 个时段是否处于充电状态。目标函数写成最小化当日购电成本$$\min \sum_{t1}^{T} \sum_{i1}^{I} c_t \cdot p_{i,t} \cdot \Delta t$$其中 $c_t$ 是第 t 个时段的电价$\Delta t$ 是一个时段的长度单位小时。如果时段是1小时$\Delta t1$如果时段是15分钟$\Delta t0.25$。这一步很多人丢项后面我专门说。约束条件我列了六个充电功率上限$0 \le p_{i,t} \le P_{\max,i} \cdot x_{i,t}$配变容量约束$\sum_i p_{i,t} \le S_{\text{cap}}$停驻窗口约束t 不在 $[\text{arrival}_i, \text{departure}i)$ 内时$p{i,t}0$SOC动态约束$\text{SOC}{i,t1} \text{SOC}{i,t} \eta \cdot p_{i,t} \cdot \Delta t / E_{\text{cap},i}$充电完成约束离开时 $\text{SOC}_{i,\text{departure}i} \ge \text{SOC}{i,\text{target}}$同时充电约束$\sum_i x_{i,t} \le M$可选为什么必须引入 $x_{i,t}$这是很多人偷懒的地方。如果只保留功率连续变量线性规划求解器会做出一个很假的解某辆车需求电量很小但它停得久求解器就把功率摊薄到所有时段形成“涓流慢充一整晚”的轨迹。这不违反任何约束但实际充电桩通常不是这么工作的而且一旦加入阶梯电价模型会在电价最低时段把所有电塞进去结果和论文对不上。加上二进制变量车辆只能按离散的“开/关”状态充电模型更贴合现实代价是从LP变成MILP求解时间上去了。对于24时段、20辆车这种规模差别不大几百辆车才需要认真考虑性能。2.2 最小可运行的PythonPuLP代码骨架下面是可以用PuLP直接跑的代码骨架。我故意写得朴素注释也全方便你在此基础上加自己的目标函数和约束。import pulp T 24 I 20 dt 1.0 # 电价元/kWh这里只是示例实际按论文数据填 price [0.8] * 8 [1.2] * 6 [0.4] * 7 [1.2] * 3 # 车辆数据示例用简单循环生成实际从csv读取 arrival [i % 7 for i in range(I)] departure [arrival[i] 3 for i in range(I)] soc_arr [0.2 0.4 * (i % 5) / 10 for i in range(I)] soc_tar [0.9] * I E_cap [60.0] * I P_max [7.0] * I S_cap 120.0 # 变压器容量 eta 0.9 # 充电效率 prob pulp.LpProblem(EV_Charging_DayAhead, pulp.LpMinimize) # 决策变量 p pulp.LpVariable.dicts(p, (range(I), range(T)), lowBound0) x pulp.LpVariable.dicts(x, (range(I), range(T)), catpulp.LpBinary) soc_vars pulp.LpVariable.dicts(soc, (range(I), range(T 1)), lowBound0, upBound1) # 目标函数 prob pulp.lpSum(price[t] * p[i][t] * dt for i in range(I) for t in range(T)) # 停驻窗口与功率上限 for i in range(I): for t in range(T): if t arrival[i] or t departure[i]: prob p[i][t] 0 else: prob p[i][t] P_max[i] * x[i][t] # 变压器容量约束 for t in range(T): prob pulp.lpSum(p[i][t] for i in range(I)) S_cap # SOC动态与充电完成约束 for i in range(I): prob soc_vars[i][arrival[i]] soc_arr[i] for t in range(arrival[i], departure[i]): prob soc_vars[i][t 1] soc_vars[i][t] eta * p[i][t] * dt / E_cap[i] prob soc_vars[i][departure[i]] soc_tar[i] solver pulp.PULP_CBC_CMD(msgFalse) prob.solve(solver) print(求解状态:, pulp.LpStatus[prob.status]) print(总购电成本:, pulp.value(prob.objective)) # 打印任意一辆车或总功率曲线 for t in range(T): total_power sum(p[i][t].value() for i in range(I)) print(f时段{t:2d}: P_total{total_power:.2f} kW)跑出来之后你应该能看到谷电价时段总功率顶到变压器上限峰电价时段压到很低甚至0整体是一条“削峰填谷”式的功率带。如果看到这种模式说明模型逻辑没跑偏。2.3 一个20车算例的参数推演与结果校验我给这个算例补一组具体数字方便你对结果有个心理预期。假设20辆车全部需要充到90%初始SOC最低0.2、最高0.5平均0.35电池60kWh。那么单辆车的平均充电需求是 $(0.9 - 0.35) \times 60 33 \text{kWh}$20辆车总需求约660kWh。考虑充电效率0.9实际从电网取电约733kWh。电价结构按凌晨0-7点为谷电价0.4元/度上午和傍晚为峰电价1.2元/度其余为平电价0.8元/度。变压器容量120kW谷段7小时最多能充入840kWh理论上可以把全部需求塞进谷段还不够但车辆到达和离开时间限制了“能充的时间窗”。比如有的车下午5点到达晚上8点离开峰电价时段它必须充电但电价贵模型会把它压到最小功率有的车晚上10点到达第二天早6点离开大部分时间落在谷段模型会让它尽量在谷段后半段充满。最终总成本取决于电动汽车到达分布和电价曲线的匹配程度。校验结果时我会手动算一个“理想化下限”假设所有车辆都拥有谷段全时段停驻时间那么最小购电成本约 $733 \times 0.4 293.2$ 元。实际跑出来如果成本明显大于这个数说明有相当比例车辆被迫在峰平段充电如果成本低于这个数那一定是单位换算或者数据出现了问题。3. 从功率曲线到可调度区间两次优化算出充电站的灵活边界3.1 为什么只给一条功率曲线交不了差很多复现到第2章就停了因为拿到功率曲线就以为任务完成。实际上电网调度侧要的不是“一条明天功率多大的预测”而是“这条功率可以在什么范围内动”。这就是可调度域dispatchable region存在的意义。打个比方你跟家人说“我明天晚上7点到9点去跑步”这是给了一条计划曲线你如果换种说法“我明天晚上6点到10点之间随便挑两个小时出门其他时间都在家”这是给了调度弹性。电动汽车充电站在电网眼中就是后者——调度中心希望知道每个时段能压多少、能抬多少而具体哪台桩在充那是站内的事。所以真正的日前可调度模型最终输出通常是一组上下界$P_t^{\min} \le P_t \le P_t^{\max}$。第2章的功率曲线只是“可调度域内部的一条可行解”。3.2 逐时段两次优化把区间边界算出来算这个上下界最直白的方法是对每个时段做两次优化一次求总功率最小一次求总功率最大。思路很简单。假设要求第 $t_0$ 时段的下界 $P_{t_0}^{\min}$就把目标函数从“成本最小”临时改成$$\min \sum_{i} p_{i,t_0}$$同时保留所有车辆SOC约束、停驻窗口约束、变压器容量约束。这里最关键的一个点车辆的充电完成约束必须保留否则求解器会一股脑把功率压成0得到的“下界”没有意义。上界的求法对称改成$$\max \sum_{i} p_{i,t_0}$$对每个 $t_0$ 执行一遍需要求解 $2T$ 次24时段就是48次优化。这个量对于CBC/Gurobi来说非常轻松总耗时通常不超过1分钟。工程上我建议写两层循环外层遍历时段内层遍历方向每次都重新建模不要试图复用同一个模型实例去改目标函数很容易改坏约束。代码结构大概是for t0 in range(T): for direction in (min, max): prob build_model() # 复用和2.2相同模型但目标函数按direction改 if direction min: prob pulp.lpSum(p[i][t0] for i in range(I)) else: prob -pulp.lpSum(p[i][t0] for i in range(I)) prob.solve() bounds[t0][min if direction min else max] total_power()注意如果在这一步一次性把48个模型都建好并求解内存会占用很多但优点是可以并行。我实际复现时用Gurobi多线程并行求解48个独立小模型几乎就是秒级完成。3.3 可调度域结果的三个合理性检查算完之后不能直接截图交差我一般做三个检查。第一可行性检查。取上下界的每一组边界点把相应的功率曲线回代到原始约束里检查所有车辆离开时SOC是否仍然达到目标。如果求下界时某些车辆在最后时刻SOC刚好贴着目标说明约束在起作用如果SOC有大量余量说明该时段的下界其实还能更低你的模型少了某个关键约束。第二稳定性检查。相邻时段的上下界不应该剧烈跳变。如果尖峰时段下界突然很高通常是因为很多车在尖峰时段必须充电这是合理的但如果谷段上下界完全一致说明整个充电站已经在谷段顶满变压器了灵活域退化成一条线这也是可以用来解释“为什么充电站需要扩容”的有力证据。第三一致性检查。把第2章算出来的最小成本功率曲线放进可调度域里它必须处处落在上下界之间。如果越界要么成本曲线的求解模型和可调度域的模型约束不一致要么你某个时区索引对错了。4. 复现必踩的五个坑与排查技巧4.1 时间索引没对齐电价、到达离开时间不是一个世界我复现时遇到的第一回荒谬结果是功率曲线在凌晨发生了“负需求”一样的异常跳变。查了两天才发现电价表用的是自然小时索引即0点对应列表第0个元素而车辆到达数据的时间是从早上6点开始编号的。这种错位在代码里很难肉眼发现因为所有数据都是整数只要加偏移的脚本逻辑写错一丁点就会整体平移一个时段。排查方法很简单在建模前把电价、到离时间、SOC初始值按同一时间轴打印成一张长表肉眼扫一遍。如果电价谷段的位置和车辆集中到达的时间发现有偏差立刻停下来对齐。统一的做法是定义一个time_index数组以当日0点为0当地标准时间为准。所有外部数据在进入模型前先映射到同一个时间索引上不要在目标函数或约束里临时做换算。4.2 成本漏乘时间粒度结果直接翻四倍假设你的调度时段是15分钟电价单位是元/kWh功率单位是kW那么一个时段的能量是 $p \times 0.25$ kWh。如果忘了乘以0.25相当于把15分钟的电量当成1小时来计成本直接虚高四倍。这个错误特别隐蔽尤其当你知道模型结果“差不多对”的时候。我检查它的方法很笨但很有效单独取一辆车、一个时段手工笔算期望成本再和求解器输出对比。比如7kW充电桩谷电0.4元/度15分钟充入1.75kWh成本应为0.7元。如果求解器给出2.8元那肯定漏乘了0.25。4.3 约束写得过死无解之后别急着加惩罚很多论文里的车辆参数是理想化的每辆车到达时SOC 20%离开时要求100%停驻时间却只有2小时。7kW充电桩2小时只能充14kWh对一个60kWh电池、需要48kWh电量的车来说数学上就不可能满足模型直接报infeasible。遇到这种情况第一反应不要是加惩罚项或者硬解开先检查数据本身是否合理。我通常会先把SOC目标从0.9改成0.8试跑如果可行了说明是参数问题如果还是无解再怀疑约束逻辑。参数方面可以放宽到“离开时不低于0.9允许部分车辆因实际限制降到0.85”并在论文里说明自己处理了参数松弛。这种做法并不丢人实际充电站里也有大量车辆充不满就开走的情况。4.4 二进制变量太多导致求解慢怎么瘦身把模型从LP变成MILP之后如果车辆数量上百、时段粒度又细到15分钟二进制变量的数量会爆炸。我遇到过96个时段、200辆车也就是19200个二进制变量的情况CBC跑了十分钟还出不来Gurobi也需要策略。三个最有效的瘦身手段。一是时段粗化把15分钟时段合并成1小时变量数量直接降为四分之一很多算例精度完全可以接受二是去掉冗余二进制变量如果车辆 i 只能在两个时段内充电它的 $x_{i,t}$ 只需要在这两个时段定义不需要为全天96个时段都声明变量三是增加对称性打破约束比如同一型号的车辆尽量让模型按到达时间顺序赋值避免等价解互相纠缠。再不行就换Gurobi的MIPFocus参数并开启启发式求解。复现论文场景下我们不需要证明全局最优只需要一个可行且接近最优的解设置一个0.1%的MIP gap就够了。4.5 用实时数据做日前模型越复现越不像“日前”这是我见过最多人误解的一点。日前模型的前提是“所有信息在日前已知”也就是用的是预测数据。如果在复现时把每辆车实际的到达SOC填进去等于你开了一个“上帝视角”的实时调度模型得到的结果当然比论文更好看但它不是日前可调度。正确流程是先对明天各时段到站车辆数、每辆车的初始SOC做预测得到一组预测值然后用这组预测值作为模型输入。算出的功率计划是明天要执行的目标到了明天实车到达后再做短时调整。复现论文时如果描述里有“prediction”“forecast”一定要确认你的输入数据是预测数据而不是历史实测数据。5. 复现完怎么继续探索三个低成本扩展方向5.1 把确定性变成随机性两阶段随机规划复现完确定性模型最简单有效的升级是把电动汽车到达时间、初始SOC这些不确定参数改成场景集用两阶段随机规划替代码里的单点预测。第一阶段在日前决定每个总时段的功率计划第二阶段在每个场景下站内重新分配各桩功率使得偏离日前计划的总功率最小。这样规划出来的结果不再是单条功率曲线而是一个带概率信息的调度策略。场景生成可以用蒙特卡洛抽样。先根据历史数据拟合到达时间的泊松分布和初始SOC的正态分布然后抽500个场景再用同步回代缩减到50个以内显著减少求解负担。复现代码只需要在原有模型外面套一层场景循环目标函数改成“期望成本或者期望偏差最小”。5.2 引入V2G放电和电池退化成本如果想让模型更有探索价值可以把充电功率变量从非负改成有符号允许 $p_{i,t}0$ 表示放电。这样充电站就不仅仅是负荷还能作为分布式储能向电网反送电目标函数里多出放电收益同时把电池退化成本写成关于放电深度和次数的函数。需要注意的是放电功率基数通常比充电功率大直流桩可以做到40kW以上而且SOC上下界要预留安全裕度不能把车放空。电池退化成本可以用一个线性化近似每放1kWh电电池寿命损失对应一个很小的成本系数实际值大概几分钱到一两毛钱。把退化成本压得比放电收益低模型才会理性地只在峰电价时段放电。5.3 从单车场到城市多站聚合调度视角最后一条扩展路是把单站模型扩展到多个充电站联合调度。多站之间共享一台变压器或者共享一套储能系统节点功率边界变成耦合约束。这时“日前可调度模型”的视角就从一个站的自身利益变成了区域调度中心如何协调多个可调度资源。复现上不用重写模型只需要把每个站的“可调度域”计算出来然后喂给上层协同优化。这种双层结构在论文里很常见下层每个站求自己的可行域边界上层根据边界信息做功率分配。这种结构跑起来不算难但对代码组织能力有要求建议至少先把单站模型封装成一个类输出可复用再做下一层。我的经验是先完整跑通单站的可调度域包络线再往上叠加随机性或V2G每一步改动都保留上一次的结果作为对比基线。这样即使新模型出了bug你也能立刻知道是哪里引入的问题。复现这套模型让我最深的体会是优化模型本身往往只有几行公式真正的功夫全藏在数据处理、变量设计和约束逻辑里。尤其是soc_vars[i][arrival[i]] soc_arr[i]这样一行初始化语句写对了整个模型才能跑得动写错了求解器会给出一个看起来合理实际错误的解。下次再让我复现类似论文我一定会先从数据清单和模型类型判断入手而不是打开编辑器就敲代码。
返回列表