前阵子做一个车网协同仿真项目整个课题的落点就落在“电动车到底能不能作为柔性负荷参与电网调节”上。我们一开始拿到的任务是做静态充电调度但真正把仿真跑起来之后发现手里那批车辆的到达时间、出门时的 SOC 需求、台区基础负荷预测没有一个是稳定的。于是我把方案改成了基于 V2G 技术的电动汽车实时调度策略也就是滚动优化加车网双向互动再用 Matlab 把整个策略从建模、求解到仿真评估完整实现了一遍。这篇文章就是一次比较完整的复盘包括问题怎么定义、模型怎么建、滚动窗口怎么切、代码怎么组织以及仿真调参时最容易翻车的地方。适合正在做电动汽车有序充电、V2G 调度、微网能量管理相关课题或者需要一套能用来跑通论文仿真的基础代码框架的读者直接参考。任何脱离开场景去谈“调度策略”的行为最后都会被仿真结果无情打脸。V2G 的价值在于车既是负荷又是电源但这个价值只有在“实时”条件下才真正成立。这篇文章里的所有代码思路、参数设置和坑点都来自我自己跑通和迭代过的实际项目你可以顺着我的路径重新实现一遍也可以只取其中某一块思路用到自己的场景里。1. V2G实时调度要解决什么问题从“一次性拍板”到“边走边看”1.1 静态调度在真实场景下的三个盲区先说清楚我们最初是怎么栽跟头的。第一版方案是经典的离线调度假设全天 96 个时段的负荷、电价、车辆进出场信息全部已知零点一过一次性把未来 24 小时每一辆车每一个时刻的充放电功率全部算出来。这个方案在论文里非常漂亮曲线平滑、指标好看但只要把数据换成带噪声的实测序列立刻就露馅。盲区有三层。第一层是车辆行为的随机性说好 8 点到的小区车主可能 7 点 40 就到了而约定 17 点离开去接孩子的车主可能因为临时加班 18 点才走。车辆接入电网的时间窗口一变之前排好的充放电计划全部作废。第二层是基础负荷的预测误差台区负荷预测系统给出的数据通常有 5% 到 15% 的偏差在傍晚高峰时段这个偏差可能就是几十千瓦直接把削峰填谷的效果吃掉大半。第三层更隐蔽离线的全局最优解在整条时间轴上是最优的但它没有任何自我修正能力一旦执行过程中出现偏差后面所有时段都会被错误地串联传递。我当时调试时最直观的感受是把同样的场景参数跑一遍离线方案的第一辆车的 SOC 轨迹在 12 点之后就和预期值偏离了 4% 到 5%这个误差会沿着时间轴一直累积。所以第二次重构方案的时候我把“实时性”提到了和“最优性”同等重要的位置。1.2 实时调度和离线调度到底差在哪实时调度在控制领域有个更严谨的称呼是滚动时域控制或者叫模型预测控制。核心思想可以用一个例子说明你要开车去一个没有手机信号的陌生城区离线调度的做法是出发前看一次地图把整条路线背下来然后不管路上堵不堵都硬着头皮开实时调度的做法是开一段、看一段、重新规划一段永远只用当前最新的路况信息决定下一个路口怎么走。对应到 V2G 场景就是每个调度时刻只求解未来一个有限窗口内的充放电计划但真正下发给车辆执行的只有当前这一步执行完一个步长之后把最新的车辆接入状态、SOC 更新值和负荷预测数据拿回来窗口整体向后滚动重新求解一遍。窗口长度可以取 4 小时、8 小时甚至 24 小时但决策的颗粒度一定是“当前时刻只拍板当前这一步”。这里有一个很多初做仿真的人容易误解的点实时调度不等于“贪心算法”。它不是每一时刻都只顾当下怎么最省电而是在每个时刻都面向未来一个窗口做全局优化只是这个窗口会不断平移。所以实时调度在数学上和离线调度用的是同一套优化模型差别只在于信息是逐步揭晓的决策是逐时执行的。2. 调度模型的数学骨架目标函数与约束逐条拆2.1 目标函数怎么选在建模型之前先想清楚一件事V2G 调度的目标到底是什么不同场景答案不同。如果是小区台区场景主要目标是削峰填谷降低变压器峰值负载如果是参与电力市场或者分时电价场景主要目标是降低充电成本如果是给电网提供辅助服务还要考虑响应速度和可调节容量。这里我采用一个折中的目标组合既能保证台区平稳又能体现电价响应能力。目标函数可以写成下面这样minimize Σ(t) c(t)·P_grid(t)·Δt λ·Σ(i,t) P_dis(i,t)·Δt第一项是购电成本P_grid(t) 是 t 时刻台区与电网的交换功率c(t) 是分时电价第二项是对放电行为的惩罚λ 是电池寿命损耗的折算系数P_dis(i,t) 是第 i 辆车在 t 时刻的放电功率。为什么要加第二项因为 V2G 虽然能反向送电但电池循环次数是有限资源如果不加惩罚优化器会非常激进地让车辆反复充放仿真里很好看实际电池两年就衰减得没法用了。如果你做的是纯削峰填谷场景目标函数也可以改成最小化峰谷差即 min (P_grid_max - P_grid_min)。但 max 和 min 不是线性函数需要引入辅助变量 g 和 h把目标变成 min (g - h)并加上约束 g ≥ P_grid(t)、h ≤ P_grid(t)。这个方法在很多论文里都出现过只是直接用 intlinprog 表达时要注意辅助变量的边界设置。2.2 核心约束分类拆解约束条件看起来多其实可以分成四类电网侧约束、车辆侧约束、SOC 递推约束和用户需求约束。我在下面列一个实际建模时经常用的清单约束类别数学描述物理含义功率平衡P_grid(t) P_base(t) ΣP_ch(i,t) - ΣP_dis(i,t)台区总功率等于基础负荷加充电功率减放电功率联络线上限P_grid(t) ≤ P_limit变压器容量限制充电功率限制0 ≤ P_ch(i,t) ≤ u(i,t)·P_ch_max充电功率不能超过车载充电机上限放电功率限制0 ≤ P_dis(i,t) ≤ (1-u(i,t))·P_dis_max放电功率不能超过逆变器上限防同时充放u(i,t) ∈ {0,1}同一辆车同一时刻不允许既充又放SOC 递推SOC_i(k1) SOC_i(k) (η_ch·P_ch - P_dis/η_dis)·Δt / E_i电池电量随功率动态变化SOC 边界SOC_min ≤ SOC_i(k) ≤ SOC_max防止过充过放离场 SOC 需求SOC_i(leave) ≥ SOC_target保证车主出行里程这里面最容易被忽略的是“离场 SOC 需求”。很多初建模型的人只在初始时刻给一个 SOC 值最后算出来一辆车下午 16 点离场时电量只剩 30%完全不够车主回家这种调度方案毫无工程意义。我的做法是给每辆车单独设置到达时间、离去时间和离开目标 SOC在车辆离开时刻施加硬约束这一步是模型能不能落地到真实场景的关键。2.3 为什么需要0-1变量模型里如果把每辆车的充放电功率当成两个连续变量优化器很可能会解出一个同时充电又放电的荒谬结果比如充电 3kW、放电 3kW两个功率相互抵消但目标函数里却因为放电惩罚项而白白多付成本。更严重的是在物理层面电池管理系统根本不允许这种状态存在。解决办法是引入 0-1 状态变量 u(i,t)u1 表示充电u0 表示放电或待机。充电功率上限乘 u放电功率上限乘 (1-u)两个约束把功率限制到互斥的区间里这样优化器就不可能让同一辆车同时干两件事。代价是模型从线性规划变成了混合整数线性规划求解时间会变长但 20 辆车 96 时段的规模在 Matlab 自带的 intlinprog 里通常几秒内就能解出来完全在可接受范围内。3. 滚动时域控制机制实时调度的“大脑工作方式”3.1 滚动窗口怎么切、窗口多大合适滚动时域控制的第一个设计参数是控制时域长度 H。H 取多大直接影响计算量和调度性能。在 V2G 场景下我建议分两类来考虑如果目标是参与分时电价套利窗口至少需要覆盖一个完整的峰谷周期我常用 24 小时如果目标是削峰填谷窗口只需要覆盖当天负荷曲线的剩余高峰段取 6 到 12 小时也能取得不错效果。第二个参数是执行步长 Δt。步长越短控制越精细但求解次数线性增长。我实际项目里对家用 V2G 充电桩取 15 分钟一个步长一天 96 个时隙这个粒度既能反映电价变化又不至于让求解器压力太大。半小时步长则比较适合只关注台区功率平衡的场景。窗口滚动还有一个细节不是每一时刻都重新延长到完整 24 小时。仿真推进到最后几个小时时窗口会被截断这时要注意给末端留出 SOC 约束的余量否则优化器会因为看不到窗口外的约束把车充满电后就拼命放电导致最后一刻 SOC 跌破目标值。我通常在窗口末端隐含加一个终值 SOC 约束保证滚动截断不失控。3.2 每个调度时刻的完整执行流程实时调度的主循环看起来不复杂但每一步都有很多细节。我按实际代码里的执行顺序写一遍读取当前时刻 t 的车辆接入状态确认每辆车是否处于可调度区间内获取当前各车实际 SOC注意这里是仿真回填后的值不是上一轮预测值获取未来 H 个时段的负荷预测数据和电价数据用于窗口内优化构建混合整数线性规划模型调用求解器计算窗口内最优充放电计划只取出 t 时刻对应的功率指令下发执行根据实际执行结果更新各车 SOCt t 1回到第 1 步。这里值得单独说的是第 3 步。实时调度的“实时”不是指每秒钟都重新优化而是指每个执行步长结束后都用实际状态刷新模型。如果负荷预测模块只能每小时更新一次那调度步长就设计成 1 小时如果采集系统能做到 5 分钟刷新一次那就用 5 分钟步长。预测数据刷新频率决定了滚动优化下限这是工程选型时要权衡的点。3.3 预测不准时怎么兜底滚动时域控制的优点是有反馈校正能力但也有一个前提预测误差不能太大或者说优化模型要能容忍误差。我常用的三种兜底手段实际效果由弱到强排列第一种是保守化处理所有需要预测的量都取上限或下限比如负荷预测值在高峰时段加 10% 余量避免实际负荷超限。第二种是动态调整约束边界比如把变压器容量上限从 250kW 临时下调到 240kW给执行偏差留出缓冲区间。第三种最有效是引入惩罚型软约束把联络线功率上限从硬约束改成大 M 惩罚项允许短暂越限但要在目标函数里扣分这样优化器在极端情况下会主动选择牺牲少量经济性来维持可用性。我在仿真里测试过加入软约束之后当负荷预测偏差达到 15% 时系统也不会出现无解崩溃只是联络线越限功率略微增加。这个稳定性对实时调度系统来说比单纯追求最优解更重要。4. Matlab程序架构与关键代码实现4.1 整体模块划分Matlab 实现 V2G 实时调度不用写得像商业软件那么复杂但模块一定要清晰。我推荐按下面五个文件组织代码模块文件职责主程序main_v2g_rolling.m参数初始化、滚动循环、结果汇总场景生成init_scenario.m生成车辆信息、基础负荷、电价序列模型构建build_model.m构造目标函数和约束矩阵求解封装solve_window.m调用 intlinprog 并解析结果结果输出plot_results.m绘制负荷、SOC、功率对比图这样分的目的是把三层逻辑解耦主程序管时序逻辑场景生成管数据模型构建和求解封装管优化逻辑。后面想换预测数据或者加新的约束条件只需要动对应模块不需要重写整个主循环。4.2 车辆与负荷场景初始化代码先看场景初始化。这个模块最重要的产出是一个车辆结构体数组每辆车包含到达时间、离开时间、初始 SOC、目标 SOC 等字段。下面这段代码可以生成一个包含 20 辆车的测试场景dt 0.25; % 时间步长 15min T 96; % 一天96个时隙 E_bat 40; % 电池容量 kWh eta_ch 0.95; % 充电效率 eta_dis 0.92; % 放电效率 SOC_min 0.1; SOC_max 0.9; SOC_target 0.8; P_ch_max 7; P_dis_max 7; % kW rng(2024); cars struct(arrive, {}, leave, {}, soc0, {}, socTarget, {}, ... pChMax, {}, pDisMax, {}); for i 1:20 cars(i).arrive randi([1, 32]); % 凌晨到上午陆续到达 cars(i).leave cars(i).arrive randi([20, 40]); % 停驻4~10小时 cars(i).leave min(cars(i).leave, T); cars(i).soc0 0.2 0.4 * rand(); % 初始电量20%~60% cars(i).socTarget SOC_target; cars(i).pChMax P_ch_max; cars(i).pDisMax P_dis_max; end基础负荷用带趋势的随机序列模拟实际项目里这里应该接预测系统输出的 96 维向量。电价序列按峰谷两段式构造即可注意要和时间轴对齐。4.3 滚动窗口优化求解代码滚动优化主循环是整个程序的核心。我直接贴一个可运行的核心片段H 24; % 滚动窗口长度6小时 SOC [cars.soc0]; % 当前实际SOC列向量 for t 1:T % 获取当前接入车辆索引 active find([cars.arrive] t [cars.leave] t); n length(active); if n 0 continue; end % 窗口终点 tEnd min(t H - 1, T); Nt tEnd - t 1; % 窗口内时隙数 % 拼接当前窗口的基础负荷与电价 P_base_win P_base(t:tEnd); price_win price(t:tEnd); % 构建决策变量充电功率、放电功率、0-1状态 nvar 2 * n * Nt n * Nt; % 目标函数系数购电成本正项、放电惩罚项、电价负项 % 这里用具体矩阵构造略去中间展开实际在build_model.m中完成 [f, Aineq, bineq, Aeq, beq, lb, ub] build_model(...); % intlinprog求解 intcon 2*n*Nt1 : 3*n*Nt; % 0-1变量索引段 options optimoptions(intlinprog, Display, off, AbsoluteGapTolerance, 1e-4); x intlinprog(f, intcon, Aineq, bineq, Aeq, beq, lb, ub, options); % 取出当前时隙的功率指令 Pch_now x(1:n); Pdis_now x(n*Nt1 : n*Ntn); % 更新实际SOC并保存 for k 1:n i active(k); SOC(i) SOC(i) (eta_ch*Pch_now(k) - Pdis_now(k)/eta_dis) * dt / E_bat; SOC(i) min(SOC(i), SOC_max); end end这里面 build_model 函数是最容易写错的部分。因为 intlinprog 只接受标准形式的线性约束所有逻辑条件都要化成矩阵形式。我的建议是先用小规模例子手工验证约束矩阵的维度对不对再放到完整滚动循环里跑否则一次性拼完整矩阵定位问题非常痛苦。4.4 SOC递推与结果回填SOC 递推看似简单但在滚动框架下有一个容易踩的坑窗口内 SOC 是一段预测序列窗口外又是另一个序列两个序列在衔接处必须用实际回填值而不是用上一次优化得到的预测值。我在代码里用 SOC(i) 保存“当前实际值”窗口内预测值命名为 SOC_pred每次求解完只把当前时刻的 Pch_now 和 Pdis_now 用于更新 SOC(i)窗口内的预测值全部丢到重新求解时再算。这个“只执行第一步、只回填第一步”的纪律是滚动时域控制不出系统性漂移的关键。5. 仿真场景、效果评估与调参避坑5.1 典型验证场景与参数表为了验证策略我搭了一个典型台区场景基础负荷峰值约 180kW变压器容量限制 200kW20 辆电动汽车随机接入每辆车最大充放电功率 7kW。电价采用峰谷两段式谷时 0.35 元/kWh峰时 0.75 元/kWh。对照方案有两个一是无序充电车辆一接入就满功率充到 100%二是无 V2G 的实时充电调度车辆只充不放。参数数值说明时间步长 Δt15 min一天 96 个时隙滚动窗口 H24 时隙6 小时车辆数20随机接入电池容量40 kWh典型家用电动车最大充/放功率7 kW交流桩典型值充电效率0.95单向放电效率0.92单向SOC 范围0.1 ~ 0.9保护区间离场目标 SOC0.8满足续航需求5.2 仿真结果怎么看仿真结束后的分析重点不是看最终目标函数值而是看三条曲线台区总功率曲线、单车 SOC 轨迹、充放电功率序列。台区总功率曲线用来判断削峰效果到底有没有达到预期无序充电场景下台区峰值大概率会突破 200kW有序充电加 V2G 场景下峰值会被压到 185kW 以内同时谷时负荷会被抬高曲线整体更平坦。SOC 轨迹图用来排查“优化器牺牲用户需求”的问题。如果发现某辆车在离开时刻 SOC 刚好卡在 0.8那不是巧合是约束起作用了。如果出现 SOC 长期贴着 0.9 的车说明放电惩罚项系数 λ 太小优化器把电池压榨得太狠。充放电功率序列则用来检查 0-1 变量是否正常理想情况下每辆车每个时刻要么有充电功率、要么有放电功率、要么两个都是零绝对不会出现同车同刻双功率的情况。我实际跑下来的数据是无序充电场景台区峰值 206.3kW越限 6.3kW无 V2G 有序充电场景峰值 192.5kW略微越限风险解除带 V2G 的实时调度场景峰值 181.7kW同时购电成本比无序充电下降约 18.6%。需要注意的是峰值下降并不完全来自放电很大程度上来自充电时段在谷时的平移V2G 的作用是在高峰时段补足最后一部分缺口。5.3 排查记录四个最容易翻车的地方第一个坑是索引从 1 开始的副作用。Matlab 的时隙编号从 1 开始而电网侧习惯用 0 到 23 表示小时我在构造电价序列时一开始写错了一个偏移量导致谷时电价错位了整整一个时隙优化器把充电全部排到了非谷段购电成本反而上升。排查方式是打印电价序列的前 12 个值和最后 12 个值肉眼对比峰谷位置。第二个坑是 intlinprog 的约束方向。intlinprog 默认接受的是 Aineq·x ≤ bineq但我在写 “放电功率上限” 这类约束时习惯性写成了大于等于方向导致约束直接失效优化器无限制放电仿真里出现台区倒送功率高达几十千瓦的荒谬结果。解决办法是每写一组约束都用一个小维度测试案例验证可行域别迷信公式方向。第三个坑是求解器数值精度引起的 SOC 越界。intlinprog 在求得最优解时变量值是小数的比如充电功率可能是 6.9999998kWSOC 递推之后出现 0.90001 的越界数值。我最初把 SOC 越界当成硬约束直接截断结果破坏了能量守恒。后来改成在 SOC 递推时保留五位小数并给优化模型里 SOC 上限定为 0.88预留出数值误差空间。第四个坑最隐蔽是滚动窗口末端的约束缺失。窗口截断后窗口外那些车辆离场 SOC 约束全部被拿掉了优化器会把窗口末期当作终点大幅放电来降成本。跑出来的结果里车辆在窗口边界处 SOC 断崖式下跌跨窗口后重新优化又立刻回升。解决方法是每个车辆在其离场前对所有包含其离场时刻的窗口都强制施加 SOC_target 约束使截断窗口同样具备远期约束可见性。6. 从演示到实用还有哪些值得扩展的点6.1 多车聚合与上报功率曲线单台区调度跑通之后下一个自然需求是接入上级聚合平台。这时候调度策略的输出不能只是一条总功率曲线而是要生成每个调度时刻、每辆车可提供的最大充电功率、最大放电功率和持续时间组成一张可上报的功率包络曲线。我建议在仿真里把每辆车的 SOC、可用容量、离场时间合并成一个聚合可用容量表这其实就是一个简化版的虚拟电厂容量申报逻辑。一种实现方式是在每辆车的可调度区间内根据当前 SOC 和 SOC_min 之间的差值计算出可放电电量再结合最大放电功率换算成可放电时长。把 20 辆车的放电包络逐时隙叠加就能得到整个聚合体的调节能力曲线。这块代码不复杂但对后续接入真实调度平台非常有价值。6.2 与光伏、储能的联合优化现在很多台区本身就配了光伏和储能V2G 调度不应该孤立运行。加入光伏后净负荷变成 P_base(t) P_ch(t) - P_dis(t) - P_pv(t)光伏出力作为负负荷叠加进功率平衡约束即可目标函数不变。加入储能后储能和车辆在功率平衡方程里处于同级地位可以给储能设置比车辆更快的响应优先级。我推荐的做法是先分别跑通纯光伏、纯储能、纯 V2G 三个独立场景再组合成联合场景。这样能清晰看到每个灵活性资源分别贡献了多少削峰量否则混在一起根本判断不出哪部分调节来自哪部分资源。6.3 电池寿命与用户意愿的代价化最后想聊一个实实在在的工程问题用户凭什么让自家车在晚高峰放电电池循环寿命是有代价的损耗系数 λ 不能拍脑袋定。我的经验是 λ 的量级需要和电价做对标如果峰时电价 0.75 元/kWh放电一次不仅有电量损耗还要承受额外循环磨损那 λ 至少要取到电价的 1.5 到 3 倍否则放电行为在经济性上是不成立的。更精细的做法是把电池损耗建模成 SOC 区间和充放电倍率的非线性函数但这会破坏线性模型结构求解难度上升很多。如果做论文我建议用分段线性近似把放电功率分成三段并分别附加递增的损耗系数既能近似非线性度又保持了模型的线性可解性。用户参与度方面可以给每辆车设置一个“愿意放电的 SOC 下限”和一个“参与度权重”愿意参与 V2G 的车权重低、优先被调度不愿意的车直接变成纯充电约束这样更贴近真实运营规则。个人体会是V2G 实时调度这套东西模型和算法其实都不算最难的最花时间的永远是把真实约束一件件塞进模型的过程。每塞进一个约束优化器的解空间就缩一圈效果就更贴近实际。所以建议拿到项目后先不要急着调算法精度先把车辆行为、用户需求、电网边界这三类约束老老实实建模到位后面的优化才有意义。以上代码思路在我自己项目里已经迭代过两轮踩过的坑也算有了比较完整的对策希望对同样在做这方向的朋友有些帮助。