ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

电动汽车充放电最优调度MATLAB源码复现与关键约束详解

电动汽车充放电最优调度MATLAB源码复现与关键约束详解

很多人拿到一篇指标不错的高水平论文,第一反应就是找代码。但现实往往是原文不公开源码,只能对着公式和图表自己复现。这篇博文就来完整拆解一个我最近做过的项目——电动汽车充放电最优调度的 MATLAB 源代码,代码按照高水平文章复现的方式编写,覆盖了模型构建、约束处理、求解器选型、结果验证四个环节,同时把我在复现过程中踩过的坑和排查经验一并整理出来。内容偏向工程落地,代码可以直接改参数跑,也适合作为写论文时的对比模型。

这个主题的水很深。电动汽车充放电调度看似只是“什么时候充、什么时候放”的问题,实际上背后是含时序耦合、整数变量、非线性约束的优化问题。复现文章最大的难点不在“能跑通”,而在“和原文趋势一致”。很多人拿到代码发现能出图,但换一套参数结果就不合理了,这往往是建模时约束写得太松或者没考虑用户行为边界导致的。

1. 项目概述与核心需求拆解

1.1 拿到标题先想什么

项目标题是“电动汽车充放电最优调度 matlab 源代码”,关键词落在“最优调度”和“复现”上。我拿到这个标题后的第一反应是:这份代码需要回答三个问题。

第一,调度的目标是什么。是让用户充电费用最低,还是让电网负荷峰谷差最小,还是让充电站运营商收益最大。目标函数不同,整个模型的性质就完全不同。第二,调度的对象是什么。是单个充电桩,还是一整个充电站,还是区域内的电动汽车群。对象规模决定了决策变量的维度。第三,约束条件有哪些。电池荷电状态(SOC)上下限、充电功率限值、用户取车时间、变压器容量,这些实际约束都会影响可行域的形状。

我复现的这份代码,目标函数是“最小化电动汽车集群的总充电费用”,同时兼顾电池寿命损耗的惩罚项。调度对象是某小区充电站内的一批电动汽车,时间尺度是24小时、15分钟一个时段,一共96个时段。这个粒度很常见,既不会因为粒度过粗忽略价格波动的影响,又不会因为粒度过细导致求解时间不可接受。

1.2 复现前需要准备的工具

复现工作依赖三样东西:MATLAB、YALMIP工具箱、一个可用的求解器(这里用的是某商业求解器的学术版)。YALMIP是一个建模工具箱,它最大的价值在于把优化问题的“建模”和“求解”分离。你在代码里写清楚目标函数和约束条件,YALMIP会帮你自动判断问题类型,然后调用对应的求解器去解。

用这套组合有三个理由。一是YALMIP语法接近数学公式,写出来的代码和论文里的公式可以一一对应,降低审阅成本。二是商业求解器在求解混合整数线性规划(MILP)时的速度和稳定性远好于开源求解器,对于96时段的调度问题,求解时间基本在几秒到几十秒的量级。三是这套工具链跨平台,不管你是Windows还是Linux环境都能跑,后续做参数敏感性分析非常方便。

提醒一句:MATLAB版本太老的话,YALMIP的兼容性会遇到问题。实测下来R2018a以后的版本都正常,更老的版本可能需要在YALMIP的官方更新日志里手动适配。

2. 调度模型设计与核心约束处理

2.1 目标函数怎么定:兼顾经济性与电池寿命

复现的第一步是把论文里的目标函数“翻译”成MATLAB代码。多数高水平文章的调度目标不是单一的,而是多目标的加权组合。我复现这篇文章的目标函数包含两部分。

第一部分是充电费用,等于每个时段的分时电价乘以该时段所有车辆的充电功率,再乘以时段时长。第二部分是电池放电损耗惩罚,因为电动汽车参与放电(V2G)会加速电池循环损耗,如果不加惩罚项,优化结果会倾向于频繁放电,这在工程上不可接受。

目标函数的数学形式如下:

[ \min \sum_{t=1}^{T} \left( c_t \cdot P_{ch,t} - c_t \cdot P_{dis,t} \right) \cdot \Delta t + \lambda \cdot \sum_{t=1}^{T} P_{dis,t} \cdot \Delta t ]

其中,( c_t ) 是时段 ( t ) 的电价,( P_{ch,t} ) 和 ( P_{dis,t} ) 分别是时段 ( t ) 的总充电功率和总放电功率,( \Delta t ) 是时段时长(这里为0.25小时),( \lambda ) 是放电损耗惩罚系数。

有个细节值得注意:放电时段实际上是在卖电,收益等于电价乘以放电功率,所以目标函数里放电项是负号。加上惩罚项之后,模型会在电价足够高时才会选择放电,避免无意义的电池损耗。

2.2 核心约束:SOC递推、功率限值与充电需求

约束条件是整个模型里最容易写错、也最容易导致结果失真的部分。我复现时一共写了四类约束。

第一类约束是SOC(荷电状态)的时序递推约束。车辆在相邻时段之间的SOC变化量等于充电功率减去放电功率,再乘以充电效率,除以电池容量:

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

这里需要特别留意充放电效率的位置。充电效率是乘法项,放电效率是除法项。很多初学复现的人会把效率只写在充电侧,导致放电侧能量不守恒,结果曲线在时段切换时出现跳变,看起来特别不自然。

第二类约束是单台车的充放电功率限值。任何时刻,单台车的充电功率不能超过充电桩硬件的最大功率,放电同理。如果车辆同时有充放电状态,还需要引入0-1整数变量来确保同一时段不能既充又放。这一步会让问题变成混合整数规划,求解难度会上升一个级别,但工程师必须做,因为真实充电桩的控制逻辑不允许同时充放。

第三类约束是用户取车时的最低SOC需求。每辆车在接入电网期间需要充入一定的电量,以保证用户取车时SOC不低于设定值。这个约束写起来简单,但参数设置要合理。我在初版模型中把最低SOC设成了0.9,结果发现优化结果非常保守,放电几乎不发生,因为模型担心最后充不满。后来改成0.6反而更贴近实际,用户取车时SOC在0.6到0.8之间是通勤场景下可以接受的范围。

第四类约束是变压器容量约束。充电站接入的配电变压器有额定容量,任意时刻所有车辆的充放电净功率之和不能超过变压器容量上限。这个约束是“群体约束”,它把单台车的优化问题真正变成了集群协同调度问题,也是不同文章核心创新点最常出现的地方。

2.3 为什么选择混合整数线性规划形式

把约束全部写完之后,我面临一个问题:要不要引入整数变量。如果允许充放电同时进行,模型就是线性规划,求解快、无需求整。但工程上不允许。如果引入0-1整数变量,模型变成MILP,求解时间变长,但结果更符合实际。

我的建议是:复现阶段一定写成MILP,这符合工程实际。至于求解时间,96时段、几十辆车的规模对商业求解器来说完全在掌控范围内,不必担心算不动。

3. MATLAB源码结构与关键代码实现

3.1 整体框架:数据、建模、求解、后处理四段式

代码结构直接决定调试效率。我采用的是最常见的四段式结构:数据参数定义、YALMIP变量建模、约束写入、求解与结果后处理。四段之间用注释分割,每一段职责单一,便于定位问题。

整个代码主体大概长这样:

%% 数据参数定义 T = 96; % 调度时段数(24h * 15min) N = 30; % 电动汽车数量 dt = 0.25; % 时段时长(小时) price = xlsread('price.xlsx'); % 分时电价,维度 T x 1 Soc_init = 0.2 * ones(N,1); % 初始SOC Soc_min = 0.1; % SOC下限 Soc_max = 0.9; % SOC上限 E_batt = 60 * ones(N,1); % 电池容量(kWh) P_max = 7 * ones(N,1); % 充电桩最大功率(kW) eta_ch = 0.95; % 充电效率 eta_dis = 0.95; % 放电效率 lambda = 0.1; % 放电损耗惩罚系数

这一段是纯粹的输入数据。电价我习惯从Excel读,因为调度结果对价格曲线非常敏感,写成数组反而不好调整。

3.2 YALMIP变量建模与目标函数写入

决策变量有三组:每辆车每个时段的充电功率、放电功率,以及一个描述充放电状态的0-1整数变量。这里用到sdpvar和binvar。

%% 决策变量定义 Pch = sdpvar(N, T); % 充电功率,连续变量 Pdis = sdpvar(N, T); % 放电功率,连续变量 SOC = sdpvar(N, T+1); % 荷电状态 u = binvar(N, T); % 0-1状态变量,1表示充电,0表示放电 %% 目标函数 cost = 0; for t = 1:T cost = cost + dt * (price(t) * sum(Pch(:,t)) - price(t) * sum(Pdis(:,t)) + lambda * sum(Pdis(:,t))); end optimize(Constraints, cost);

说明一下sdpvar和binvar的选择:连续变量用sdpvar,0-1变量用binvar。SOC变量维度是N×(T+1),比功率变量多一列,是为了直接对应SOC递推关系中的( SOC_{t+1} )和( SOC_t ),写约束时不容易越界。

3.3 约束写入:从单台车到集群联动

约束写入是整个代码中最容易出现维度错误的地方。我建议先写向量化的约束,再写循环约束,最后写集群约束,由简单到复杂逐个排查。

%% 约束条件 Constraints = []; %% 1. SOC递推约束 for i = 1:N for t = 1:T Constraints = [Constraints, ... SOC(i,t+1) == SOC(i,t) + ... (eta_ch * Pch(i,t) - Pdis(i,t)/eta_dis) * dt / E_batt(i)]; end end %% 2. 充放电功率与状态互斥约束 for i = 1:N for t = 1:T Constraints = [Constraints, 0 <= Pch(i,t) <= P_max(i) * u(i,t)]; Constraints = [Constraints, 0 <= Pdis(i,t) <= P_max(i) * (1-u(i,t))]; end end %% 3. SOC上下限约束 for i = 1:N for t = 1:T+1 Constraints = [Constraints, Soc_min <= SOC(i,t) <= Soc_max]; end end %% 4. 用户取车最低SOC约束(假设所有车辆在最后一个时段取车) for i = 1:N Constraints = [Constraints, SOC(i,T+1) >= 0.6]; end %% 5. 变压器容量约束 for t = 1:T Constraints = [Constraints, sum(Pch(:,t) - Pdis(:,t), 1) <= 250]; end

互斥约束是这套代码的灵魂。它通过变量u把充放电功率绑定在一起——u(i,t)=1表示充电,这时候Pdis的上限变成0;u(i,t)=0表示放电,Pch的上限变成0。这比给变量加Pch*Pdis==0这种非线性约束靠谱得多,因为后者会把问题变成非凸问题,求解器容易陷入局部最优。

3.4 求解设置与结果输出

求解设置需要注意两点。第一是要让optimize返回求解状态,不要忽略它。第二是在求解之后立即检查solve的数值,特别是MILP问题,存在整数变量时用optimize返回的状态判断更稳。

%% 求解 ops = sdpsettings('solver', 'gurobi', 'verbose', 2, 'mipgap', 0.01); result = optimize(Constraints, cost, ops); if result.problem == 0 disp('求解成功'); else disp('求解失败,问题类型:'); disp(result.info); end %% 结果后处理 Pch_value = value(Pch); Pdis_value = value(Pdis); SOC_value = value(SOC);

事实证明,把mipgap设为0.01(即允许最优解的相对误差在1%以内)能在不损失精度的前提下大幅缩短求解时间。对于96时段的MILP,默认的0求解精度可能要等很久,而1%的松弛通常几十秒就能出结果,曲线几乎看不出差别。

4. 仿真结果分析与参数验证

4.1 结果曲线怎么看:先看趋势,再看数值

复现结果不是“出图就完事”,关键要验证结果是否符合物理规律。我按照三个维度来检查结果:充电功率曲线是否与分时电价趋势匹配、SOC曲线是否在上下限内平滑变化、变压器负载率是否在安全范围内。

我先看总充电功率曲线。正常情况下,模型会在电价低谷时段集中充电,在电价高峰时段放电。如果做出来曲线是平的,说明约束条件限制了车辆的调度灵活性,或者价格信号的波动不足以驱动优化行为。这时候首先要排除“价格曲线读错了”这种低级错误,然后再修改车辆接入时间参数。

我还发现一个常见问题:当所有车辆都允许从第1时段开始充电时,结果往往会集中在凌晨电价最低的几个时段全部充满,后面的时段几乎不动。这虽然符合“最小费用”的目标,但会导致变压器在凌晨短时重载。如果论文里有“平抑负荷波动”的目标,需要加入对充电功率曲线光滑度的约束或目标惩罚项。

4.2 放电行为的两难:损耗惩罚系数怎么取

参数敏感性分析中,放电损耗惩罚系数是影响结果最明显的参数。我测试了几组取值,结果差异非常大。取0.05时,系统在电价高峰时段放电非常积极;取0.3时,放电几乎消失。

这个系数代表的是电池循环损耗成本,理论上应该由电池更换成本、循环寿命、放电深度等决定。但论文里未必直接给出,复现时只能反复试。我的经验是先用0.1跑一版,观察放电功率占总充电功率的比例。如果比例超过20%,说明惩罚偏小;如果基本为0,说明惩罚偏大,再逐步缩放。

4.3 求解状态与误差分析

求解MILP时,gap的值需要重点关注。我用求解器的MIP gap参数时发现,初始默认值说5%的gap未必稳定,不同问题规模下的表现差异很大。建议在代码里显式指定mipgap参数,并把结果输出到日志里留档。如果gap一直不收敛,优先检查约束是否存在数值病态问题,例如效率参数单位不统一导致约束系数跨越多个数量级。

数值病态在YALMIP建模中很容易出现。比如SOC的值域是0到1,而功率的值域是0到7,两者在约束中相乘或相加时,系数会差出一个数量级。求解器在预求解阶段会做尺度变换,但极端情况下仍可能造成数值问题。对策是把SOC的约束统一乘上电池容量,转成“电量”的单位。

5. 常见问题与排查技巧实录

5.1 求解失败:YALMIP报错信息怎么定位

我在复现过程中遇到过三次求解失败,全部是约束维度不匹配导致的。YALMIP报错往往不够直观,同一个错误信息背后可能是完全不同的问题。这里记录一个通用排查流程。

第一步,检查变量维度。sdpvar(N,T)是N×T矩阵,SOC是N×(T+1)矩阵,在写SOC递推约束时,右端SOC(i,t+1)和SOC(i,t)的列索引必须和循环变量严格对应。一个常见的坑是把循环写成for t=1:T后,递推式中多写了SOC(i,t+1)而忘了左端也要对应T+1。

第二步,检查约束类型。YALMIP要求所有约束都是线性的或者二次锥的。如果用Pch(i,t)*Pdis(i,t)==0这种双线性约束,求解器会直接报“Nonconvex quadratic”错误。这时候就应该回到0-1变量互斥方案。

第三步,检查常数项。如果某个约束里出现了NaN或者Inf,YALMIP不会在建模时报错,但求解器会返回失败。出现NaN最常见的原因是Excel读入的price里有空单元格,或者电价向量维度是T+1而不是T。

5.2 求解慢:明明几十辆车,为什么要等几分钟

如果模型很小但求解非常慢,问题通常不在问题规模,而在整数变量建模方式。用循环逐条添加互斥约束时,YALMIP生成的约束矩阵规模会比正确写法大好几倍,因为每一条约束都会生成单独的行。正确做法是直接使用矩阵形式一次性构建约束。

比如对于所有车、所有时段的互斥约束,可以写成:

Constraints = [Constraints, 0 <= Pch <= P_max .* u]; Constraints = [Constraints, 0 <= Pdis <= P_max .* (1-u)];

这里的P_max需要扩展成N×T矩阵,用repmat(P_max,1,T)和u对齐。这样写出来的代码不仅在视觉上简洁,YALMIP内部的预处理效率也会大幅提升,求解时间差距可能达到5到10倍。

5.3 复现结果的电池寿命惩罚问题

我在早期版本里没有加放电惩罚项,结果放出的电量非常多,曲线非常“漂亮”,但仔细一看完全不符合电池寿命逻辑——每辆车一天内完成两次完整充放循环,这种策略在真实场景中标定会立刻被否决。这就是前面提到的“目标函数不合理导致结果看似最优实则荒谬”的典型例子。

所以在复现文章时,一定要先确认原文的目标函数是否考虑了电池寿命惩罚。有些文章把寿命约束放到约束条件里,用充放电循环次数上限来限制;有些文章直接做成惩罚项。两者差别很大,前者是硬约束,后者是软约束。我建议优先把充放电次数限制写成硬约束,这样不会因为惩罚系数取值不合适而影响整体结果。具体实现可以用累计充放电状态变化的绝对值来统计循环次数,虽然需要引入更多整数变量,但对于小规模问题负担不大。

5.4 换数据源时的适配问题

我的代码默认从Excel读取电价数据,如果用户想换成实测的实时电价序列,只需要修改Excel文件内容,不需要改动模型代码。但有两点要注意:一是时间间隔必须和模型中的dt保持一致,如果实际数据是30分钟一个点,那么dt要改成0.5,同时T要变成48,SOC递推式中的dt也会同步更新,这不是只改一个变量就能解决的;二是电价数据的单位要统一成“元/kWh”,如果原数据是“分/kWh”,数值会差100倍,目标函数量级发生变化后,惩罚系数lambda也要随之调整。

我把这套参数适配的逻辑封装成了一个单独的输入检查函数,它会在跑模型前自动校验电价序列长度、单位、SOC初始值范围、电池容量正负,防止低级错误浪费调试时间。

6. 扩展方向与个人体会

这个项目做完之后,我最大的体会是:复现一篇高水平文章的代码,真正花时间的不是写代码本身,而是理解文章里的模型假设和参数选择。很多细节文章里不会写出来,比如充放电效率是在充电侧乘还是在放电侧除,惩罚系数怎么标定,SOC初始值怎么设定。这些信息只能靠对实际物理过程的理解去反推,甚至是试错试出来的。

如果这份代码要继续扩展,我认为有三个方向比较有实际价值。一是把单站调度扩展成多站协同调度,加入配电网潮流约束,这时候模型就会从简单的MILP变成带二阶锥约束的混合整数规划,求解复杂度和代码框架都需要升级。二是加入电动汽车用户的不确定性,用场景法或机会约束方法描述用户到达时间和充电需求的不确定性。三是把离线调度变成滚动时域控制,用模型预测控制的思路,每个时段都根据最新状态重新求解未来一段时间的调度策略,这样更贴近实际运行场景。

最后再分享一个小技巧:调试这类调度模型时,把电价数据排列成阶梯状、执行一个简单案例来测试——比如只有1辆车、2个时段。如果1辆车2时段的模型能正确跑出直觉上合理的解,再扩展到30辆车96时段,定位问题的难度会小很多。很多人一上来就跑大模型,出了问题根本无法判断是建模错误还是数值问题。从小案例起步,逐步放大,是我个人认为最稳的复现路线。

RELATED READING

延伸阅读

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