傍晚六点半,小区的充电桩跟前已经排了一溜车。我一同事就住那个小区,他说每到这个点变压器就嗡嗡响,物业群里隔三差五通知“充电桩功率受限,请错峰充电”。他看了眼自己那台电车,满电剩35%,明天早上还要跑高速,不充不行。电价呢?这会儿正好是峰段,1.2元一度,充一晚上比夏天开空调还贵。这不光是钱的问题——大家都挤在这个点插枪,电网侧负荷峰上加峰,变压器说炸就炸。
这就是“无序充电”的真实代价。而我们要做的有序充电策略优化,简单说就是:用数学模型给一堆电动车排一个“充电计划表”,让每台车在满足出行需求的前提下,尽量去电价低、电网负荷低的时段充电,同时把配电网的功率红线顶住。今天这篇我会从问题本质、数学建模、Matlab代码实现到踩坑实录,完整拆一遍这个课题。不管你是做毕业设计、写论文,还是充电运营想落地一套调度逻辑,这套框架都能直接改着用。
整个仿真环境我基于Matlab做的,核心用到了多时段动态电价、混合整数线性规划(MILP)和粒子群对比实验。读完你可以得到一个可复现的小规模算例,以及一套遇到“无解”“收敛慢”“谷时挤爆”时候的排查思路。
1. 有序充电到底在优化什么
1.1 从无序到有序:问题本质
首先把“无序”描述清楚。所谓无序充电,就是电动车用户回到家,插枪就充,充满为止,不做任何时间上的调整。这个行为的直接后果有两个:第一,用户侧,下班时段正好是晚高峰电价,充一次电的花费比夜里贵出一倍还不止;第二,电网侧,下班时段本来就有生活用电高峰,再加上一堆充电枪同时拉功率,配电变压器的负载率很容易冲到100%以上。
我见过一个实际案例:某小区变压器容量630kVA,夏天晚高峰基础负荷大约380kW,装了30台7kW慢充桩,如果20台同时充,那就是140kW叠加上去,总负荷直接520kW,变压器过载率超过80%。这就是必须做有序充电的硬理由——不是“钱多钱少”的问题,是电网设备扛不扛得住的问题。
有序充电的核心,是把“充电负荷”在时间和空间上重新分配:把一部分可平移的充电需求,从峰时段挪到平、谷时段,同时在变电站或台区功率约束下保证“同一时刻充电车辆的总功率不超过限值”。注意这里有个关键前提:充电需求本身不是刚性的,只要你设定了“离开时SOC达到某个值”,那么在到达和离开之间存在一个时间窗,窗口内的充电时段分配就是可以调的。
做个类比你就懂了:演唱会散场,一万个人同时涌向唯一的出口,那是无序;管理员把观众按区域分批放行,虽然有人要多等十分钟,但所有人能安全出去,场馆也不会踩踏。有序充电就是给电动汽车排队放行,只不过排队的依据不是手臂上贴的号,而是电价、SOC、离开始时间、变压器余量这些数据。
1.2 多时段动态电价模型是怎么来的
“多时段动态电价”这个词,字面理解就是把一天分成多个时段,每个时段给一个电价,电价值会随着供需情况变化。这里面有两层:静态分时电价和动态电价。
静态分时电价你们很熟悉,就是峰、平、谷三段,比如早上8点到晚上10点峰段1.2元,夜里谷段0.38元,这个表是固定的,各地区电网会发布。但动态电价不是死表,它是基于日前负荷预测、新能源出力预测、发电成本算出来的,时间粒度可以细到96个点(每15分钟一个),电价值从一个时段到另一个时段是变动的。多时段的意思就是离散化,把连续的一天切成一排时间窗口,然后在这个离散序列上做优化。
到这里你可能会问:直接用峰谷平三段不是挺好吗,为什么要搞那么多时段?因为时间粒度越细,调度自由度越高。三段电价下,所有车都会挤到同一个谷段充电,谷时会不会又变成“第二个峰”?这就是我之前跑仿真踩过的坑,后面详细说。而动态电价序列有波动性和不确定性,反而会逼着优化算法去权衡“这个谷是一般低”“那个谷是特别低”,从而让充电负荷摊得更开。
在Matlab仿真里,我通常这样生成动态电价:先设置基准峰谷差,然后在每个时段上叠加一个服从正态分布的随机扰动,再加一点时间相关性,模拟“预测电价”。这样后面做敏感性分析时,只要调整扰动方差,就能看策略的鲁棒性。
1.3 策略优化的三个典型目标
做有序充电第一步不是写代码,是先想清楚“优化什么”。我见过很多同学一上来就抄公式,目标函数五花八门,最后约束一加,程序无论如何也不收敛。先把目标定清楚,后面一切才顺。常见的三个目标:
第一个是最小化充电费用,这个最直白。用户充电花的钱最少,目标函数就是每个时段充电功率乘以该时段电价的累计和。适合以用户侧为主体的算例,但有一个隐含问题:如果电网不过问,所有车会自觉跑到最便宜的谷段充电,谷段负荷也会爆。
第二个是最小化负荷峰谷差,也就是削峰填谷。电网或充电站运营方关注这个,希望充电负荷叠加基础负荷后,全天曲线尽可能平缓。峰谷差小,变压器利用率就高,不用扩容。这个目标的数学形式通常写成最小化最大功率,或者用一个松弛变量把“全天任意时刻总负荷”压到尽可能低。
第三个是综合多目标,比如用户费用、电网峰谷差、电池寿命、用户满意度(充不满的惩罚)一起加权。学术里很流行,工程上也有用。不过我不建议第一个算例就上多目标,权重怎么定都能吵半天。
我的建议是,毕设或者论文先做“最小充电费用+功率约束”的版本,跑通之后再加峰谷差目标,用权重或者分层优化两个阶段处理。这样既有逻辑递进,又能写清楚“为什么我的策略有效”。下面第二部分我就按这个思路把数学建模完整铺开。
2. 数学模型搭建,别着急写代码
2.1 决策变量与参数定义
建模之前,先把要做决策的东西拎出来。在有序充电里,最基本的决策是:哪辆车,在哪个时段,充多少功率。如果功率是连续的,决策变量是P(i,t),第i辆车第t个时段的充电功率(kW);如果只考虑“充/不充”,决策变量是二进制u(i,t),1表示充,0表示不充,充电功率就固定为Pmax(比如7kW)。
我建议小规模算例用二进制变量更直观。因为家用交流桩功率不可调,要充就是7kW,不充就是0,你让它用5.3kW反而偏离实际。真正可连续调节的是V2G桩或直流快充,但那些场景约束更复杂,后面再扩展。
除了决策变量,还需要一组输入参数:
- 车辆总数 n;
- 时段总数 T,以及每时段时长 dt(小时);
- 电价序列 price(t),长度 T;
- 每辆车的电池容量 Cap(i);
- 到达时的SOC soc0(i);
- 离开时目标SOC socTarget(i);
- SOC上限 socMax(i)(通常100%);
- 最大充电功率 Pmax(i);
- 允许充电时间窗 [a(i), d(i)](到达时段到离开时段)。
这里有个容易出错的地方:SOC一般用百分比,但电量用kWh,建模时要统一单位。电池容量50kWh,SOC从30%充到90%,需要充0.6×50=30kWh。充电效率η也要提前定,一般0.9~0.95,数学上是SOC增量等于η×功率×时长÷容量。
2.2 目标函数:从省钱到削峰填谷
最基础的充电费用目标函数写出来是这样:
min Σ_{i=1..n} Σ_{t=1..T} Pmax(i) × dt × price(t) × u(i,t)
如果用了连续功率变量,把Pmax(i)×u(i,t)换成 P(i,t) 就行。这个公式非常简单,但足够说明问题:算法会在“贵的时段少充”“便宜的时段多充”之间寻找平衡。
如果你要把峰谷差也纳入优化,常见做法是加一个辅助变量Z表示“全时段最大总负荷”,目标函数变成:
min Σ费用 + λ × Z
同时加约束:任意时段t,基础负荷 baseLoad(t) + 充电总功率 ≤ Z。λ是峰谷差惩罚系数,调它就是在“省钱”和“削峰”之间找平衡。
我自己的体会是,lambda取电价均值的0.3到0.5倍,削峰效果就比较明显,又不至于让费用涨太多。具体数值可以扫描一下,做张表敏感性分析,很容易写进论文里。
2.3 核心约束的建模细节
约束条件比目标函数重要得多,目标函数再好,约束不自洽,求解器直接给你报“无可行解”。
第一个约束是充电时间窗:u(i,t) = 0,如果t在到达时间之前或离开时间之后。这个必须硬性施加,否则求解器为了省钱,会把充电任务安排到车还没到的时候。
第二个约束是目标SOC约束:每辆车在充电时间窗内累计充电量,必须达到目标所需电量。写成数学:
Σ_{t=1..T} Pmax(i) × dt × η × u(i,t) ≥ (socTarget(i) - soc0(i)) × Cap(i)
这个约束是线性的,Matlab优化工具箱能直接处理。注意如果差值是负的(到了甚至比目标还高),这条约束自动满足,说明这辆车根本不需要充电——这在代码里要容错,别让它参与优化。
第三个约束是电池SOC上下限。很多教材只约束最终SOC,忽略了中间过程。比如一辆车SOC已经95%,你在窗口中段给它充一小时,直接过充,这不现实。所以最好加累计约束:任意时段结束,累计充电量不超过 (capMax(i)-soc0(i))×Cap(i)。别小看这条,它会把可行域收得很紧。
第四个约束是配电变压器容量约束:任意时段,所有正在充电的车辆功率之和,加上基础负荷,不超过变压器可分配功率。因为一台7kW充电桩不是“想充就充”,台区功率就那么多,这条才是有序充电的“硬约束”。
最后还要说明一下,这类问题如果你用二进制变量,本质是MILP;连续功率版本是LP。LP更快,但无法表达“要么不充要么满功率”的场景。我建议第一版就做MILP,规模小(个位数车辆、24个时段)求解速度非常快,后期再考虑换成启发式算法。
2.4 为什么这个模型能实现“有序”
把目标函数和约束放一块看,有序的逻辑就清晰了:时间窗约束保证了需求真实性,目标SOC约束保证了用户不被饿死,变压器功率约束防止了同时刻拥挤,而目标函数里的电价项则引导充电负荷向低电价、低负荷时段流动。四者合在一起,系统就会在可行域里自动找“最不挤、最便宜、又能把车充满”的方案。
有一个特别值得强调的点:单纯“费用最小化+功率上限”有时会产生一个新的“谷时拥堵”。举例,夜里3点电价最低,所有车都想在这个时段充,但配电网功率上限只有80kW,一堆车排队到这个点还是会受限。这时候有两种解法:一是把功率上限调低,强行错开;二是在目标里加峰谷差惩罚项,让算法自己把负荷摊到次低价时段。后者更符合动态电价的初衷——电价波动本身就是负荷引导信号,但电价也不能单独扛下所有责任。
3. Matlab代码实现与求解流程
3.1 两种求解路线怎么选
动手写Matlab代码之前,先说清楚求解路线。主流两条:
第一条是“基于优化工具箱的精确解法”。用linprog(线性规划)或intlinprog(混合整数线性规划),只要你的模型是线性目标+线性约束,就能用。优点:收敛快、有全局最优性保证、不需要调算法参数。缺点:规模大了之后,整数变量一多,求解时间会爆炸,而且不好直接往模型里加非线性约束(比如电池寿命衰减曲线)。
第二条是“基于智能算法的启发式解法”。比如粒子群(PSO)、遗传算法(GA)、模拟退火(SA),把充电计划编码成个体,迭代搜索。优点:能处理非线性、非凸、甚至黑箱约束,适合大规模集群;缺点:没有最优性保证,参数(粒子数、惯性权重、交叉率)要靠经验调,而且容易陷入局部最优。
我的建议是:算例车辆少于20台、时段数24~96,用intlinprog;如果做几百台车的集群仿真,或者要加入非常复杂的目标,再用PSO/GA。而且PSO和GA的代码尽量自己写主框架,Matlab自带的particleswarm虽然能用,但加约束麻烦,不如手写灵活。
3.2 基于intlinprog的小规模复现案例
我用Matlab的“问题式建模”(problem-based approach)给大家展示核心代码,这个从R2017b开始都支持,语法直观,不用手拼约束矩阵。
假设场景:5辆电动车,24个时段(每小时1个),电价序列给定,每辆车的参数如下表:
| 车辆 | 电池容量(kWh) | 初始SOC | 目标SOC | 充电功率(kW) | 到达时段 | 离开时段 |
|---|---|---|---|---|---|---|
| 1 | 50 | 35% | 90% | 7 | 4 | 20 |
| 2 | 60 | 40% | 100% | 7 | 6 | 24 |
| 3 | 40 | 50% | 90% | 7 | 2 | 10 |
| 4 | 45 | 30% | 85% | 7 | 5 | 18 |
| 5 | 55 | 45% | 95% | 7 | 3 | 16 |
基础负荷baseLoad我就用一个典型曲线(这里不贴全部数据),变压器可分配给充电的总功率上限Plimit设为30kW。注意5台车×7kW如果同时充是35kW,所以必须错峰。
代码核心段:
T = 24; dt = 1; n = 5; price = [0.38 0.38 0.38 0.38 0.38 0.45 0.8 1.0 ... ]; % 按实际填入24个值 Cap = [50 60 40 45 55]'; soc0 = [0.35 0.40 0.50 0.30 0.45]'; socTarget = [0.90 1.00 0.90 0.85 0.95]'; Pmax = 7 * ones(n,1); a = [4 6 2 5 3]'; d = [20 24 10 18 16]'; eta = 0.9; % 决策变量:u(i,t) 二进制,1表示充电 u = optimvar('u', n, T, 'Type', 'integer', 'LowerBound', 0, 'UpperBound', 1); % 目标:最小化充电费用 priceMat = repmat(price, n, 1); prob = optimproblem('Objective', sum(sum(Pmax .* dt * u .* priceMat)), ... 'ObjectiveSense', 'min'); % 约束1:到达前/离开后不能充电,直接通过上界矩阵处理 uUpper = ones(n, T); for i = 1:n uUpper(i, 1:a(i)-1) = 0; uUpper(i, d(i)+1:T) = 0; end u.UpperBound = uUpper; % 约束2:目标SOC必须达到 prob.Constraints.socTarget = ... (Pmax .* dt .* eta) .* sum(u, 2) >= (socTarget - soc0) .* Cap; % 约束3:电池SOC不超过上限(用累计电量上限) for i = 1:n cumCap = cumsum(u(i,:), 2); prob.Constraints.socMax(i) = (Pmax(i) .* dt .* eta) .* cumCap <= ... (1 - soc0(i)) * Cap(i); end % 约束4:台区充电总功率上限 prob.Constraints.powerLimit = Pmax' * u <= Plimit; % 1*T维 % 求解 [sol, fval] = solve(prob); uOpt = round(sol.u);运行之后会得到一个n×T的0/1矩阵,把每一列求和乘上Pmax,就是每个时段的充电功率曲线。把这条曲线和电价叠加画在一张图里,你能很明显看到:充电负荷大多落到了低电价时段,且任何时刻总功率不超过30kW。
这里有三个细节提醒。第一,cumsum(u(i,:), 2)生成的上三角累积矩阵乘上“单位时段充电电量”,本质上就是“截止时段t累计充了多少电”,然后让它不超过剩余可充容量,这个约束比只约束终点严格得多,可以避免中间过充。第二,Pmax' * u是矩阵乘法,得到1×T的向量,正好表示每个时段所有车总功率。第三,由于是MILP,求解器可能有一些数值误差,解出来u的值可能是0.999999,所以最后必须round。
3.3 基于粒子群算法的功率调度框架
如果你要做大规模场景,整数变量上千个,intlinprog会相当吃力。这时候可以试试粒子群。我自己的习惯是用PSO去搜索“每辆车开始充电的时间点”。
核心思路很简单:每个粒子代表一组解,内容是n辆车在时间窗内的充电起始时段。粒子位置x = [s1, s2, ..., sn],其中si在[a(i), d(i)-ceil(needHours(i))]之间随机初始化。needHours是满足目标SOC所需最少充电小时数:
needHours(i) = ceil((socTarget(i)-soc0(i))×Cap(i) / (Pmax(i)×dt×η))
约束的处理用罚函数。比如某一辆车算完开始时间后,连续充电needHours直到满足目标SOC,然后判断:总功率是否超过Plimit?最后SOC是否超过100%?如果违反,就在适应度上加上很大的罚值。
伪代码框架:
for iter = 1:maxIter for p = 1:popSize x = particles(p, :); % 每辆车充电起始时刻 schedule = zeros(n, T); for i = 1:n start = round(x(i)); dur = needHours(i); schedule(i, start:start+dur-1) = 1; end power = Pmax' * schedule; % 每个时段总功率 over = max(0, power - Plimit); fee = sum(sum(Pmax .* dt .* schedule .* priceMat)); fitness(p) = fee + 5000 * sum(over); % 罚函数 end % 更新pbest、gbest ... end这种连续充电时长假设比“每时段可断充”更贴近慢充场景,但限制也多:如果窗口内连续充不完,或者会超容量,就无解。要做更自由的调度,还是回到MILP。所以我把智能算法的定位放在“大规模近似解”和“给定模型校验用”。
3.4 动态电价场景生成与对比验证
动态电价的构造看起来简单,其实要讲技巧。我用的是“基准分时电价+随机扰动+相关性平滑”的生成方式:
rng(2025); T = 96; % 15分钟粒度 priceBase = zeros(1,T); % 峰平谷时间表 peakIdx = [17*4+1 : 21*4, 8*4+1 : 10*4]; flatIdx = [10*4+1 : 14*4, 6*4+1 : 7*4]; valleyIdx = setdiff(1:T, [peakIdx flatIdx]); priceBase(peakIdx) = 1.1; priceBase(flatIdx) = 0.6; priceBase(valleyIdx) = 0.3; % 加扰动并平滑 noise = 0.05 * randn(1,T); noise = smoothdata(noise, 'gaussian', 8); price = priceBase + noise; price(price < 0.2) = 0.2;为什么要平滑?真实电价不会从1.1瞬间跳到0.3,相邻时段之间有连续性。如果你给它加一个白噪声这种没有时间相关性的扰动,算法会“钻空子”,把充电任务全都安排到一个极低并孤立的时间点上,这在工程上根本不可行。所以必须平滑,或者用AR模型生成。
场景构造好之后,建议做三组对比:无序充电(回家即插即充)、有序充电(仅考虑费用)、有序充电(费用+削峰)。统计指标选三个:总电费、峰时段最大负荷、平均SOC完成度。
我自己跑过的典型结果(动态电价场景,5车,Plimit=30kW)大致是:无序充电电费约66元,峰值负荷52kW(超限);单纯费用优化的充电电费约43元,节省35%左右,但最大负荷稳定在30kW以下;如果再加削峰权重,费用会上升到47元左右,换来的是负荷曲线更平坦,全天最高负荷降到27kW上下。这个对比很能说明问题:“钱”和“电网安全”之间确实有一个Pareto权衡,写文章时用这张表很有说服力。
4. 常见问题与排查技巧实录
4.1 求解器报“无可行解”,九成是约束写死
这个问题太常见了,我当初第一次跑通之前被卡了整整一个晚上。报错信息基本是:“No feasible solution found. infeasible.”。原因通常是三类。
第一类是目标SOC需求大于时间窗内最大可充量。比如一辆车到达时SOC 20%,目标100%,电池容量60kWh,7kW桩,需要充6.8小时,而它的充电窗口只有5小时,那肯定无解。这个在建模前就应该手动验证:先算每辆车的needHours,再对比窗口长度。
第二类是变压器功率约束定得太苛刻,比如Plimit设了15kW,但5辆车每辆至少需要充一段时间,窗口又重叠,无论如何都满足不了。遇到这种情况,要么放宽Plimit,要么减少参与调度的车辆数量,要么扩大时间窗。
第三类是SOC上限约束写反了,比如把某个时段之后的累计电量约束加到了全部时段,导致本来可充电的时段也被锁死。我当时是把cumsum写成了sum(u(:,1:t))的循环版本,结果t从1开始没问题,到了t=2就把前两个时段的累计都锁住了,相当于把一个“爬坡上限”错误地当成了“总上限”。
4.2 变量索引越界,尤其是到达时段等于1
如果到达时段是1,代码里1:a(i)-1会变成1:0,在Matlab里这个向量是空的,倒不会越界,但逻辑可能出问题。更麻烦的是离开时段等于T的情况,d(i)+1:T如果d=T,就变成T+1:T,结果又是空的,看着没报错,实际约束漏了。
我现在的习惯是:所有时间窗参数一律先做一次清洗:
a = max(a, 1); d = min(d, T);然后把“不允许充电”的部分单独用约束写,而不是悄悄改上界矩阵。这样即使数据异常,至少能通过约束数量发现问题。
4.3 求解结果看起来合理,但画图后露馅
有时候fval降下来了,SOC约束也都满足,但你把充电负荷画出来一看——所有车都在同一个最便宜的时段充,总功率顶在Plimit上限一动不动。这个结果虽然“可行”,但在工程上极不健康,因为它是“谷时拥堵”。
排查思路:先打印每辆车开始充电时段和结束充电时段,看看是不是出现“扎堆”。如果扎堆,说明目标函数里缺了峰谷差惩罚项,或者Plimit设得太大,在谷时段根本不形成约束。我通常这样处理:把Plimit设成比“总充电需求/可利用窗口”稍微紧一点的值,让算法不得不铺开充。
另一个隐蔽问题是“最小化费用”可能导致一辆车中途多次启停,比如0点充半小时、1点充半小时、2点半又充半小时,虽然满足所有约束,但频繁启停对电池和接触器都不友好。想避免这个,需要引入“最小连续充电时长”约束,或者把变量改成“充电开始时间”。
4.4 算例验证与效率提升的实操建议
模型跑通之后,一定要做两个验证。第一个是“极端验证”:把电价全部设成同一个常数,有序充电的结果应该退化为“所有车尽量均匀分布,且总费用最低”,可以用来排查目标函数方向对不对。第二个是“手工验证”:挑一辆车,手动算它需要充几个小时,在哪个时段充最省,然后用代码输出对比。如果我调程序时能有一次通过手工计算对上的,后面大规模算例的信心就会很足。
效率方面,MILP的求解时间对整数变量个数非常敏感。5辆车24时段的算例几乎瞬时完成;改成100辆车96时段,intlinprog可能会跑几分钟甚至更久。我的应对手段有三个:一是把时间粒度从15分钟放宽到1小时,前提是电价序列也跟着重采样;二是把每辆车的时间窗先压窄,不要给全天,只给真实可用窗口,能大幅缩小变量规模;三是用“启发式初值+intlinprog”,先用粒子群跑一个粗略可行解,作为x0传给intlinprog,热启动往往能快很多倍。
4.5 关于多目标权重和结果呈现的一句总结
最后再给一个细节:如果你最后用了“费用+λ峰谷差”这种加权目标,论文里一定要放λ灵敏度分析。λ=0时费用最低但峰谷差大;λ=0.2、0.5、1.0,费用会逐渐升高,峰谷差逐渐减小。把这条曲线画出来,评阅人一眼就能看出你理解了策略的权衡,而不是只会套一个目标跑结果。我就是靠这张图,把一个原本平平无奇的算例撑成了有点深度的对比实验。
另外,写代码不要一次性写完整版。先写2辆车、6个时段的迷你算例,甚至可以直接手算验证;跑通了再扩到5辆、24时段,最后才上96时段的大场景。这种“从小模型到大模型”的开发习惯,能帮你省掉至少两天的调试时间。我在这个课题上踩过的坑,大部分都是因为贪快想一次写完,结果被一堆维度不匹配、约束冲突的问题淹没。