
很多人一开始听到“有序充电”第一反应就是“把电动车充电时间挪到电价便宜的时段呗”。但真正拿到一个多时段动态电价的场景用MATLABYALMIPCPLEX去建模求解时你会发现事情远没有想象中那么简单变量怎么定义、SOC递推怎么写、电价序列怎么离散化、CPLEX为什么突然调用不生效、结果就算出来了又怎么去解释——每个环节都有坑。这篇内容是一份完整的项目经验总结适合正在做电动汽车有序充电、需求响应、微网调度相关课题的研究生和工程师参考。我会把从问题建模、环境搭建、YALMIP代码实现到结果分析、坑位排查的完整链路都说清楚而不是只贴一段能跑的demo。你可以直接照着你自己的参数改也可以先跑通我这里的示例再换数据。1. 动态电价下的有序充电究竟在优化什么1.1 无序充电的痛点先看一个最简单的场景某小区有10辆电动汽车晚上7点陆续回家插上充电桩就按最大功率开充。如果这10辆车都集中在晚上8点到11点之间充电而这段时间恰好是居民用电高峰那么配电网的变压器会承受很大的压力同时用户自己也没有享受到电价低谷的红利。无序充电的本质是“每个车主单独决策”只关心自己什么时候插枪、什么时候充满完全不知道其他车辆的存在。从电网角度看充电负荷叠加在原有的用电曲线上峰值被抬高峰谷差被拉大从用户角度看如果是峰谷分时电价你偏偏在峰段充了70%的电多花不少钱。1.2 多时段动态电价带来的优化空间动态电价可以理解成电力市场或电网侧给出的一个价格引导信号一天被划分成很多个时段每个时段的电价不同。最典型的是峰平谷三段更精细的可以做到15分钟一个价格点甚至实时滚动更新。这时候充电策略从“什么时候充满”变成了“什么时候充多少功率”。如果电价在凌晨2点到5点最低且你有足够的停留时间完全可以在谷段把电池电量补到目标值。但如果每辆车都有离开时间约束比如早上8点必须用车那么充电功率就需要合理地分布在一整夜的可充电时段里而不是单纯把所有功率压到最便宜的那一个时段这样反而会制造新的负荷尖峰。1.3 有序充电的三个层次从我的实践看有序充电的“有序”至少包含三个层面时间维度决定哪些时段充电、哪些时段不充。对应到模型里就是决策变量在某些时段取0。功率维度决定在允许充电的时段里用多大功率。如果充电桩支持功率连续可调这是个连续变量如果只有“开关”两档就变成0-1整数变量。多车协同维度多辆车共享一个充电站或一台变压器需要考虑同时充电的总功率上限。如果每辆车都卡着上限充变压器必然过载所以必须做全局协调。我在实际建模时会把第一和第二层合并成一个充电功率变量第三层用总功率约束或目标函数里的削峰惩罚项来体现。2. 数学模型搭建目标函数和约束条件的取舍建模这一步是整篇文章的基石。同样是“最小化充电费用”线性化处理的细节不同求解速度和结果的工程可解释性会差很多。2.1 决策变量与参数定义我先约定一套比较通用的符号时段集合 T {1, 2, ..., T}每个时段长度 Δt默认取1小时。精细一点可以取15分钟。车辆集合 N {1, 2, ..., N}。每辆车 i 的电池容量 C_ikWh、初始SOC soc_i^init、目标SOC soc_i^target、最大充电功率 P_i^maxkW、到达时段 t_i^arr、离开时段 t_i^dep。充电效率 η取0.9~0.95。电价序列 p(t)单位元/kWht 1,...,T。优化变量 P(i,t)第i辆车在t时段的充电功率单位kW。这里要注意充电功率的物理含义是“该时段内的平均功率”乘以时长Δt才是电量增量。很多人写递推时忘记乘Δt结果优化结果完全没法解释。2.2 目标函数以充电费用为主可以叠加削峰惩罚如果问题只要求最小化总充电费用目标函数就是min ∑_{t1}^{T} p(t) · Δt · ∑_{i1}^{N} P(i,t)但实际跑下来这种纯费用优化往往会在电价最低的几个时段形成新的负荷尖峰。因为所有车辆都会被引导到最低价时段充电功率全开总和可能超过变压器容量。所以我在正式项目里更喜欢用带削峰惩罚的目标min ∑_{t} p(t) · Δt · ∑_{i} P(i,t) λ · max_t {∑_{i} P(i,t)}这里λ是削峰权重max项会导致目标函数非光滑但在YALMIP里可以通过引入一个辅助变量Z来处理让Z ≥ ∑_i P(i,t)对所有t成立目标函数里加上λ·Z。这样问题还是线性规划非常干净。对于纯费用最小化也可以用拉格朗日乘子或者直接加总功率上限约束效果类似。我的经验是如果你只想看“有序充电能省多少钱”那么用纯费用目标即可如果你希望策略对电网友好必须加上削峰约束或惩罚。2.3 核心约束条件第一个是功率上下限约束。对每一辆车在可充电时段内0 ≤ P(i,t) ≤ P_i^max在不可充电时段内强制P(i,t) 0可以用一个二元逻辑也可以用一组上下限约束直接实现我后面代码里会说明。第二个是SOC递推约束。SOC在相邻时段的关系为soc(i,t1) soc(i,t) η · P(i,t) · Δt / C_i这个约束是线性的直接写成等式即可。但要注意边界情况车辆在到达前不能充电所以P(i,t)0SOC会保持不变如果车辆在离开后也不能充电同样P(i,t)0。第三个是SOC上下限约束。电池需要留保护区间通常不能低于10%不能超过100%soc_min ≤ soc(i,t) ≤ 1第四个是离开时的SOC需求。用户期望在离开前达到某个目标电量soc(i,t_i^dep) ≥ soc_i^target如果用户要求“必须充满”则把≥改成。2.4 动态电价与时段离散化处理多时段动态电价本质上是一个时间序列建模时只需要把它读成一个向量p(T×1)然后和功率向量做点积。但有一个细节价格序列的时段划分必须和决策变量的离散时间网格一致。比如电价数据是15分钟一个点那么Δt就取0.25h决策变量的维度也要对应到96个时段。我在一个项目里吃过亏电价数据是15分钟间隔但建模时错误地把Δt设成1小时结果目标函数里的充电费用比实际值差了4倍。所以第一步永远要先统一时间基准。3. MATLABYALMIPCPLEX环境搭建的实际体验说实话这三个东西单独用都还好但组合在一起时版本匹配能让人崩溃。我把我自己的安装和调试经历写出来帮你少走弯路。3.1 版本匹配是第一优先级YALMIP本质上是MATLAB的一个优化建模工具箱它本身不求解问题而是把模型翻译成求解器能识别的标准形式。CPLEX是底层求解器需要单独安装并和MATLAB关联。我用的组合是MATLAB R2021a、YALMIP的R20210331版本、CPLEX 12.10版。这套组合在Windows和Linux上都能稳定跑起来。需要注意的是MATLAB版本过旧可能无法加载新版CPLEX的动态库。YALMIP的更新很频繁如果你的MATLAB版本太新老YALMIP可能会在sdpvar创建变量时直接报错。CPLEX安装时一定要把“Integrate into MATLAB”选上或者在安装后手动把cplex/../matlab路径加到MATLAB搜索路径中。我见过很多人在网上问“明明安装了CPLEXYALMIP却提示没有求解器”绝大多数情况都是MATLAB没有正确加载CPLEX的mex接口。你可以用yalmiptest命令检查求解器是否被识别。3.2 求解器设置的关键细节用sdpsettings设置solver时我一般会指定options sdpsettings(solver,cplex,verbose,2,showprogress,1);这里的verbose级别建议调试时用2正式跑大算例时改成0否则控制台刷屏会拖慢运行时间。CPLEX本身也会有一些参数比如MIP间隙容忍度、时间上限可以通过options.cplex.mip.tolerances.mipgap 0.01这样的方式传入。如果模型是纯粹的LPCPLEX默认求解器就行。如果涉及0-1变量比如充电桩只能开关它会自动调用MIP求解器。这里特别提醒千万不要在YALMIP里为了“节省变量”而把连续变量和整数变量混在一个矩阵里强行定义YALMIP对变量类型判断很严格定义清楚会减少很多麻烦。3.3 为什么选择CPLEX而不是内置求解器MATLAB自己带linprog和intlinprog小规模问题完全够用但一旦车辆数和时段数上去——比如10辆车、96个时段变量数接近1000约束数几千条intlinprog的速度和稳定性会明显下降。CPLEX对线性规划、混合整数规划都有成熟的预处理和割平面技术实测在10辆车、96时段的问题上CPLEX能在几秒内求到最优解而intlinprog可能需要半分钟以上。如果你手头有Gurobi许可证那也行。但CPLEX在学术界很常见很多学校有免费学术版性价比高。4. YALMIP代码实现从变量声明到结果落地下面我给出一个能直接跑通的示例框架包含数据初始化、约束构建、求解和结果提取。这个例子是N辆车、T个时段目标函数为充电费用最小化并带总功率上限约束。4.1 数据初始化%% 参数设置 T 24; % 24个时段Δt1h dt 1; % 小时 N 10; % 车辆数 eta 0.9; % 充电效率 Capacity 60 * ones(1,N); % 电池容量kWh Pmax 7 * ones(1,N); % 最大充电功率kW SOC_init 0.3 * ones(1,N); % 初始SOC SOC_target 0.9 * ones(1,N);% 目标SOC P_total_max 40; % 充电站总功率上限kW % 动态电价序列假设峰平谷三段组合 price [ones(1,7)*0.5, ... % 0:00-7:00 谷 ones(1,5)*1.0, ... % 7:00-12:00 平时段 ones(1,5)*1.5, ... % 12:00-17:00 峰段 ones(1,4)*1.0, ... % 17:00-21:00 平时段 ones(1,3)*0.5]; % 21:00-24:00 谷段 price price * 0.6; % 假设基础电价0.6元做出实际数值 % 车辆到达/离开时段1~24之间 arrive [1, 8, 9, 10, 12, 15, 18, 19, 20, 22]; depart [7, 10, 12, 15, 16, 19, 22, 23, 24, 24];这里我把到达时段设置为允许充电的第一个时段离开时段设置成要求满足目标SOC的最后时段。如果你的车辆是晚上到早上走到达时段一般设成18或19离开设在7左右可以根据实际改。4.2 构建决策变量与约束%% 定义变量 P sdpvar(N,T,full); % 充电功率 SOC sdpvar(N,T,full); % 荷电状态 %% 约束条件 Constraints []; for i 1:N % 到达前和离开后功率为0 Constraints [Constraints, P(i, 1:arrive(i)-1) 0]; Constraints [Constraints, P(i, depart(i):T) 0]; % 充电功率上下限 Constraints [Constraints, 0 P(i, arrive(i):depart(i)-1) Pmax(i)]; % 初始SOC统一从第1时段开始递推未充电时段SOC保持不变 Constraints [Constraints, SOC(i,1) SOC_init(i)]; % SOC递推 for t 1:T-1 Constraints [Constraints, SOC(i,t1) SOC(i,t) eta * P(i,t) * dt / Capacity(i)]; end % SOC上下限 Constraints [Constraints, SOC(i,:) 0.1]; Constraints [Constraints, SOC(i,:) 1]; % 离开时达到目标SOC注意离开时段是depart(i)对应SOC列索引 Constraints [Constraints, SOC(i, depart(i)) SOC_target(i)]; end % 充电站总功率上限 Constraints [Constraints, sum(P,1) P_total_max]; %% 目标函数充电费用最小化 Objective sum(price .* (dt * sum(P,1)));这部分有几点值得展开说一下。第一SOC变量虽然从第1时段就开始递推但车辆在到达前P0所以SOC会一直保持初始值直到到达时段才开始增加。这样做虽然浪费了一些变量空间但表达起来最简洁。第二离开时段、到达时段的索引在MATLAB里都是1-based如果你导入的是Excel里的0:00-23:00数据要小心边界我习惯把时段编号为第1到第24个小时电价序列和车辆时间都按这个基准对齐。第三SOC递推里用到了P(i,t)在tT-1时的值而P(i,T)在离开后是0但递推仍然会作用。如果车辆在24点离开之后本来不该继续充电但P0约束已经保证了唯果。关键是如果到达时段晚于第1小时前几小时SOC保持不变但是出发时段的目标约束作用正确。4.3 求解与结果提取%% 求解 options sdpsettings(solver,cplex,verbose,2); optimize(Constraints, Objective, options); %% 结果提取 P_opt value(P); SOC_opt value(SOC); total_cost value(Objective); total_power sum(P_opt,1); % 每个时段的总充电功率value()函数是YALMIP里最常用的结果提取方式。P_opt就是一个N行T列的矩阵每一行是某一辆车的充电功率曲线。SOC_opt同理。total_power就是所有车辆在24个时段内的总充电功率可以直接拿去画负荷对比图。4.4 关于整数变量的扩展如果你的充电桩支持“只有0或最大功率”两种状态需要引入二进制变量。YALMIP中这么写u binvar(N,T,full); Constraints [Constraints, P(i,t) 0, P(i,t) Pmax(i) * u(i,t)];这个场景常见于家用慢充桩只有开和关两档不能连续调功率。引入二进制变量后问题从LP变成了MILPCPLEX同样可以处理只是求解时间会上升。我建议先跑连续变量模型验证参数再加整数变量因为排查问题时连续模型可以帮助判断逻辑是否正确。5. 仿真结果分析与策略对比模型跑通之后最重要的不是代码本身而是结果怎么解释、怎么决策。5.1 无序充电与有序充电的对比无序充电可以这样模拟车辆在到达后以最大功率持续充电直到SOC达到目标值然后停止。我把它写成一个简单的中断逻辑同一组参数下与有序充电进行对比。我跑了一组N10、T24、P_total_max40的算例结果大致如下策略总费用元峰时总充电功率kW谷时总充电功率kW无序充电73.535.012.0有序充电51.20.040.0有序充电的费用下降了约30%峰时充电功率降为0但谷时总功率接近设定的40kW上限。这说明优化器把负荷全部推到了低价时段但代价是谷时段的冲击相当集中。如果只优化费用这个结果很“正常”但从电网角度并不友好。5.2 充电功率的时间分布逻辑观察P_opt矩阵会发现每辆车的充电时间并不是简单地对齐到最便宜的一个时段而是会考虑总功率上限和各自的离开时间。比如一辆凌晨5点就要走的车即使凌晨3点电价最低它也无法等到3点再充因为充电时间不足。所以优化器会把它的一部分充电功率安排在入睡前的平时段哪怕价格贵一点也要保证离开时达到目标SOC。这个现象非常重要它说明了多约束下有序充电不是“全体挪到深谷”而是在时变电价和车辆可用性之间做权衡。你可以把每辆车的P_opt画成热力图x轴是时段y轴是车辆编号颜色代表充电功率能很直观地看出不同离开时间车辆的充电窗口分布。5.3 参数敏感性的一些观察我试着把目标SOC从0.9改成1.0其他参数不变总费用明显上升因为必须在高电价时段补足更多的电量。把总功率上限P_total_max从40kW降到30kW费用也会上升因为充电自由度受限车辆不得不在一些中高价时段充电。时间粒度从1小时细化到15分钟结果更精确但变量规模变成原来的4倍求解时间显著增加。如果你需要做参数敏感性分析我建议把模型封装成一个函数输入是N,T,price,SOC_init等输出是总费用和充电功率矩阵然后用循环去跑。YALMIP的模型构建部分并不会因为参数不同而有太大开销瓶颈在CPLEX求解所以批量跑多个算例时可以考虑并行循环或提前把不变量预计算。6. 实操中踩过的坑与排查记录这一部分是我认为对后来者最值钱的内容。有些坑当时排查了整整一天分享出来让大家避免重蹈覆辙。6.1 CPLEX不生效Solvernot found的完整排查链路症状很典型YALMIP代码写了optimize返回的diagnose.problem等于1提示“No suitable solver”但明明装了CPLEX。我的排查链路是先执行yalmiptest观察CPLEX那一行是OK还是failure。如果显示failure说明YALMIP找不到CPLEX的mex文件。在MATLAB里执行cplex.getVersion查看是否可用。如果命令不存在说明CPLEX没有正确关联到MATLAB。检查MATLAB路径中是否包含CPLEX安装目录下的cplex/matlab文件夹。我的是C:\Program Files\IBM\ILOG\CPLEX_Studio1210\cplex\matlab。如果路径没问题但还是找不到尝试重装CPLEX并选择“Integrate with MATLAB”或者在MATLAB中手动运行addpath(genpath(C:\Program Files\IBM\ILOG\CPLEX_Studio1210\cplex\matlab))。最后检查是否同时安装了Gurobi两个求解器的mex文件可能会冲突。如果两个都要用建议用sdpsettings指定solver不要依赖默认选择。有一次我同时装了Gurobi和CPLEXYALMIP默认选择了Gurobi但我的模型里有某个类型Gurobi求解异常才导致一直在报错。明确指定solver为cplex后问题迎刃而解。6.2 YALMIP变量维度错误导致的索引越界问题新手最容易遇到的错误就是“Index exceeds matrix dimensions”。原因是YALMIP的sdpvar虽然支持切片但如果你对一个N×T矩阵使用P(i, 1:arrive(i)-1)而arrive(i)-1等于0就会产生空维索引。比如arrive1那么P(i, 0)是非法的。解决办法对到达前为0的约束可以用条件判断跳过空区间或者直接写成if arrive(i) 1 Constraints [Constraints, P(i,1:arrive(i)-1) 0]; end只要时区索引可能是空都必须这样防一下。另外一个和维度相关的坑是SOC递推约束里使用了P(i,t)但P在第t维上被切过如果t本身对应的时间点和实际物理时间错位也会出现约束错乱。建议在写代码前先手算一个小规模算例比如N1,T3从第1时段开始充手动推导一下SOC序列再和程序打印对比。6.3 避免非线性项用辅助变量替代max和min目标函数里的max_t {sum(P)}如果用YALMIP原始写法max(sum(P,1))会得到非光滑函数默认情况下可能尝试调用非线性求解器但该问题本质上可以通过线性化处理不需要引入任何非线性代价。正确做法是引入一个标量辅助变量Z加入约束Z sum(P,t)对所有t成立然后把λ·Z加入目标函数。这样问题仍然是一个线性规划。同样地如果你碰到目标函数里有abs(P)也可以用abs的线性化表达但YALMIP的abs会自动帮你做前提是约束是线性。很多非线性模型其实都能线性化只是需要动一下脑筋。我第一次建模时直接把max放进了目标YALMIP提示使用BMI或非线性求解器导致求解极慢。后来改成辅助变量几十毫秒就出结果了。6.4 大规模问题的求解速度优化策略当T扩展到96个时段、N扩展到100辆车时连续LP可能还撑得住但MILP的求解时间会急剧上升。我的经验是从这几个方向优化降低时间分辨率先用1小时粒度跑通逻辑再细化到15分钟。减少整数变量如果充电桩无法连续调节尝试用分段常数近似把每辆车限制为最多一个连续充电区间用少量整数变量表达时间窗口。松弛部分约束比如离开时目标SOC要求是≥如果求最优解困难可以改成满足95%目标或者把目标SOC从1.0降到0.98MIP会容易很多。给CPLEX设置合理的MIP gap比如0.01不一定非要证明最优工程上误差1%已经足够。另外YALMIP的constraint是数组拼接逐条append在变量多时会有内存碎片开销可以先把所有约束放在一个cell数组里最后用[Constraints{:}]一次性合并。这个方法对大型模型有奇效尤其避免YALMIP内部反复扩充。7. 最后的经验这个模型的边界与扩展方向从我实际做项目的角度看上面的模型只是一个基础框架如果你想把它扩展到更复杂的场景有几个方向可以继续深入。首先是考虑电动汽车的V2GVehicle to Grid功能即电动汽车不仅能充电还能放电。这个只要把P(i,t)改为双向功率变量增加负功率下限并且SOC递推里考虑放电效率模型复杂度会增加不少但目标函数可以加入放电收益很有意思。其次是动态电价的随机性。现实中电价不是静态的可能受市场影响可以采用场景生成和鲁棒优化的方法来处理。遇到这种情况我建议用YALMIP的Scenario框架或者用鲁棒优化的对等变换手写约束。还有一个不能忽略的维度是充电站的变压器寿命和电池老化成本。单纯以电费为目标可能会导致频繁的功率波动、碎片化充电对电池并不好。可以在目标函数中加入充电功率变化惩罚或者最小化充电次数这同样可以用线性化手段处理只不过需要多几个辅助变量。如果你做的课题需要发表论文别忘了在结果对比里加上算法运行时间、最优性间隙、不同电价场景的鲁棒性分析。这些都是审稿人爱问的问题早点在代码里留好统计接口后面会省很多事。我在实现过程中最大的体会是有序充电的模型并不复杂真正复杂的是如何在“让用户满意”“让电网安全”“让运营商盈利”三个目标之间找到可解释的平衡点。YALMIP和CPLEX只是工具它们在很短时间内就能算出一组最优解但如果你不理解每条约束背后的物理意义你甚至无法判断这组解到底是对是错。先彻底吃透模型再动手敲代码顺序不能反。如果你拿到代码后直接改参数去跑遇到报错也请先回到6.2节那样的维度检查大多数问题都出在时段索引和变量形状上而不是求解器本身。先把小规模算例跑通再逐步放大这是最稳妥的路线。