最近一直在做电动汽车有序充电与电网互动的仿真,把V2G调度这块用粒子群算法完整跑通了一遍。这个项目核心点是:电动汽车的停留时间和充电时间不能当成固定值硬编码,而是作为每辆车独有的调度约束参与优化。标题里写的“考虑停留时间和充电时间的V2G调度”,说白了就是在满足车主充电需求的前提下,利用车辆接入电网的窗口期做充放电功率分配,最终达到削峰填谷或者成本最优的目标,整套逻辑用Matlab实现,求解器用的是粒子群算法(PSO)。
这篇文章我会把整个项目的建模思路、粒子群算法的工程化处理、Matlab代码实现的核心模块全部拆开讲,适合正在做V2G调度、微电网优化、电动汽车聚合调度的研究生和工程师参考。如果你只是想跑通一个PSO demo,也能从里面抽出通用的算法框架直接改。
1. 先把这个调度问题的边界搞清楚
1.1 V2G调度到底在优化什么
V2G(Vehicle-to-Grid)本质上是把电动汽车的动力电池当作分布式储能单元来用。车停在充电站或者家里停车场的时候,电池通过双向充电桩与电网连接,既可以充电也可以放电。调度系统要做的,就是在每辆车接入电网的时间窗口内,决策它什么时段充电、什么时段放电、充放功率是多少。
这个项目里优化目标我做的是负荷方差最小化。为什么选这个目标?因为居民区或者办公园区的配电网,基础负荷有明显的峰谷特性,大量电动汽车无序充电会进一步抬高负荷峰值。通过V2G调度,让车辆在负荷低谷多充电,在负荷高峰放电回馈电网,整体负荷曲线会变得更平缓。从工程角度讲,负荷方差小意味着变压器容量利用率高、线路损耗低、电压波动小,这是配电网运营商最关心的指标之一。
当然目标函数也可以换成充电费用最小、充电站收益最大、碳排放最低等等。项目里选用负荷方差最小化,一方面便于和“无调度”的场景做对比,能够在图上直观看到削峰填谷效果;另一方面,这个目标函数是二次型的,对粒子群算法来说梯度信息不存在也能搜索,这种非凸、非线性、带约束的优化问题,正好是PSO的舒适区。
1.2 停留时间和充电时间为什么是关键约束
先区分两个时间概念。停留时间(Stay Time)是指电动汽车从接入充电桩到离开的时长,通常由车主的出行行为决定,比如上班族早上8点到公司,下午5点下班,停留时间就是9个小时。充电时间(Charging Time)是指把电池从当前SOC充到目标SOC所需的最小时长,由电池容量、起始SOC、充电功率共同决定。
这两个时间对调度的影响完全不同。停留时间决定的是“可调度窗口”——车只有在这段时间内才受控,离开之后必须达到车主要求的电量。充电时间决定的是“最小充电需求”——无论怎么调度,总的充入电量减去放电电量,必须满足出行需求。如果停留时间刚好等于充电时间,那这辆车没有调度灵活性,只能接入即充;如果停留时间远大于充电时间,比如停留9小时但充电只需2小时,剩下7小时就可以灵活安排,甚至可以在高峰时段放电,低谷时段再补电。
我在建模时具体是这样处理的:
- 每辆车有一个出发时刻和到达时刻,停留时间 = 出发时刻 - 到达时刻,单位是小时;
- 每辆车有起始SOC和期望SOC,充电需求电量 = 电池容量 * (期望SOC - 起始SOC);
- 最小充电时间 = 充电需求电量 / 额定充电功率 / 充电效率;
- 如果最小充电时间 > 停留时间,那这辆车直接标记为“不可调度”,即接入即充、不允许放电,因为保证车主基本出行需求是最优先级的;
- 如果最小充电时间 <= 停留时间,这辆车进入调度池,允许在停留窗口内灵活充放电。
这个逻辑非常关键。很多初学者做V2G调度时不考虑这两者的关系,直接把所有车都当作柔性负荷参与调度,结果优化出来的方案里某些车离场时电量不够,或者某些车被安排了放电但根本没时间充满,方案直接不可行。把停留时间和充电时间作为分层筛选条件,先把不可行的车剔除掉,剩下才交给粒子群去优化,既能提高求解效率,也能保证调度结果的工程可用性。
1.3 时间离散化与决策变量的定义
优化调度问题一般要做时间离散化。项目里把一天24小时按15分钟一个时段切分,一天就是96个时隙。每辆车在接入窗口内的充放电功率,按每个时隙来做决策,这样一个时段的功率保持恒定,既符合实际充电桩的控制粒度,也让粒子群算法的决策变量维度可控。
决策变量定义为电动汽车聚合功率矩阵 P,维度是 N辆车 × 96时段。P(i, t) 表示第 i 辆车在第 t 时段的有功功率,正值代表充电,负值代表放电,零代表空闲。这个矩阵展平之后就是粒子群算法中一个粒子的位置向量,维度是 N * 96。比如有50辆车参与调度,决策变量维度就是 50 * 96 = 4800,这个规模对粒子群算法来说属于中等规模,完全在可接受范围内。
不过直接用 N×96 这么高维的变量,搜索空间是非常大的,很多位置的组合在物理上毫无意义,比如车还没接入就开始充电,或者车已经离开还在放电。为了让算法更高效,我在编码时做了掩码处理:每辆车只对停留窗口内的时隙做编码,窗口外的变量固定为0。这样不仅缩小了搜索空间,还天然保证了时间窗口约束。
2. 数学模型:目标函数与约束条件的工程化处理
2.1 目标函数怎么设计
负荷方差最小化的数学表达为:
min F = Σ(t=1 to T) ( P_base(t) + P_ev_agg(t) - P_avg )²
其中:
- T 是总时段数,取96;
- P_base(t) 是第 t 时段的基础负荷(不含电动汽车),单位kW;
- P_ev_agg(t) 是第 t 时段所有电动汽车的聚合充放电功率,P_ev_agg(t) = Σ(i=1 to N) P(i, t),充电为正、放电为负;
- P_avg 是加入电动汽车负荷后的全天平均负荷,P_avg = ( Σ(t) ( P_base(t) + P_ev_agg(t) ) ) / T。
这里要注意,P_avg 本身也包含决策变量,不是预先固定的常数,所以目标函数实际上是二次型加上交叉项。粒子群算法不需要显式计算梯度,直接把这个 F 作为适应度函数返回即可,这也是PSO处理这类问题的一个天然优势。
目标函数归一化问题。我试过不归一化直接算,负荷值在几千千瓦级别,算出来的F非常大,粒子群的速度和位置更新时,数值差异太大容易导致收敛不稳定。实际项目中我对目标值做了一个缩放:F_scaled = F / (T * (P_rated)²),P_rated 是配电网额定容量。这样适应度数值在1到100之间,粒子群参数更容易调。
2.2 约束条件清单与罚函数处理
这个项目里核心约束有四类。第一类是充放电功率上下限约束,单辆车的充放电功率不能超过双向充电桩的额定功率,即 -P_dis_max ≤ P(i, t) ≤ P_ch_max。第二类是SOC范围约束,任意时刻的SOC必须保持在 [SOC_min, SOC_max] 之间,保护电池不过充过放,这里取 0.2 到 0.9。第三类是时间窗口约束,车在接入窗口之外不能有充放电动作,这个通过掩码天然满足。第四类是离场电量约束,车离开时SOC不能低于期望SOC。
工程上最麻烦的是最后一类。SOC之间的关系是累积的,离场SOC = 起始SOC + (充电电量 - 放电电量) / 电池容量,其中充电还要乘效率系数,放电则要除以放电效率。这个约束如果不满足,调度方案就是不可行的。
处理方式我采用罚函数法。如果某辆车的离场SOC低于期望值,就在目标函数后面叠加一个惩罚项:
Penalty = λ * Σ(i) max( 0, SOC_expect(i) - SOC_depart(i) )²
λ 取一个较大的系数,比如10000。这样粒子一旦违反电量约束,适应度会急剧变大,算法在进化过程中会自动淘汰这些不可行解。罚函数法的好处是实现简单,不需要改变粒子群搜索机制;缺点是需要调 λ,如果太小会有大量不可行解混在种群中,如果太大则可行解和不可行解的适应度差异过小,搜索方向不清晰。我最终调下来 λ 取 10000 到 50000 之间效果比较好,具体值跟负荷量级有关,建议按目标函数量级的100到1000倍去粗调。
2.3 为什么选择粒子群算法而不是线性规划或遗传算法
这个问题的本质是带约束的非线性混合整数规划,决策变量虽然是连续的功率值,但实际中充电桩功率通常是离散挡位,加上时间窗口、SOC累积等强耦合约束,用传统线性规划或者混合整数规划求解器(比如CPLEX、Gurobi)来写约束和建模会比较繁琐,关键是很多场景下这类问题还带有非凸特性,求解器不一定能快速收敛。
粒子群算法的好处在于:它不依赖梯度信息,对目标函数的形式几乎没有要求;实现代码量很少,几十行就能写出核心框架;对中等规模决策变量问题收敛速度快,实测下来50辆车、96个时段的规模,在Matlab里跑100次迭代,大约几十秒就能得到一个工程可用的方案。
相比遗传算法,粒子群算法没有选择、交叉、变异那么多算子,参数也更少,主要就是惯性权重、两个学习因子,调参门槛低。而且粒子群算法的社会学习机制,让种群信息共享效率很高,在连续变量优化问题上通常比遗传算法收敛更快。项目里也对比过GA,同样迭代次数下,PSO的目标函数值下降更明显。当然,PSO容易早熟收敛,后期可能陷入局部最优,后面我会讲怎么通过参数设计来缓解。
3. 粒子群算法的Matlab实现细节
3.1 粒子群算法的核心流程
粒子群算法的核心逻辑很直观:想象一群鸟在搜索食物,每只鸟知道自己目前找到过的最好位置(个体极值 pbest),也知道群体中找到过的最好位置(全局极值 gbest),然后每只鸟根据这两个信息和自身当前速度,决定下一步怎么飞。对应到调度问题,每个粒子就是一个候选调度方案(所有车辆在所有时段的充放电功率矩阵),粒子的位置就是方案本身,适应度就是目标函数值。
算法的核心更新公式是:
v(i) = w * v(i) + c1 * r1 * ( pbest(i) - x(i) ) + c2 * r2 * ( gbest - x(i) )
x(i) = x(i) + v(i)
其中 v(i) 是粒子的速度向量,x(i) 是粒子的位置向量,w 是惯性权重,c1 和 c2 是学习因子,r1 和 r2 是 [0,1] 之间的随机数。惯性权重控制粒子保持原有运动趋势的程度,c1 让粒子向自己的历史最优靠拢,c2 让粒子向群体的全局最优靠拢。
Matlab核心循环我写成这样:
for iter = 1 : max_iter for i = 1 : n_pop % 计算适应度 fitness(i) = calcObjective(pop(i,:), evData, load_base, params); % 更新个体最优 if fitness(i) < fitness_pbest(i) pbest(i,:) = pop(i,:); fitness_pbest(i) = fitness(i); end % 更新全局最优 [best_fit_now, idx] = min(fitness_pbest); if best_fit_now < fitness_gbest gbest = pbest(idx,:); fitness_gbest = best_fit_now; gbest_history(iter) = fitness_gbest; end end % 更新惯性权重(线性递减) w = w_max - (w_max - w_min) * (iter / max_iter); % 更新速度和位置 for i = 1 : n_pop r1 = rand(1, dim); r2 = rand(1, dim); velocity(i,:) = w * velocity(i,:) ... + c1 * r1 .* (pbest(i,:) - pop(i,:)) ... + c2 * r2 .* (gbest - pop(i,:)); % 速度限幅 velocity(i,:) = max(min(velocity(i,:), v_max), -v_max); % 位置更新 pop(i,:) = pop(i,:) + velocity(i,:); % 边界修正 + 时间窗口掩码 pop(i,:) = applyBoundary(pop(i,:), evData, params); end % 记录收敛曲线 convergence(iter) = fitness_gbest; end这个循环结构基本就是整个项目的算法骨架。实际运行前有两个关键点要提前做好,一是初始化粒子和速度时要保证每个粒子的维度正确且有物理意义,二是边界修正函数必须包含掩码逻辑。
3.2 粒子编码与掩码修正
粒子编码是我认为整个实现中最容易踩坑的地方。一开始我直接把 N×T 的功率矩阵展平成一行向量,随机初始化。结果跑出来的方案大量违反时间窗口约束——很多车在接入之前和离开之后还有功率输出。后来我在每次位置更新之后都调用一个 applyBoundary 函数,把窗口之外的变量直接置零,窗口内的变量将越界部分拉回到边界。
这个掩码矩阵的定义要提前建好:stay_mask(i, t) = 1 表示第 i 辆车在第 t 时段处于接入状态,否则为0。无论是初始化还是迭代更新,都做一次 mask 点乘。这样保证了算法永远只搜索可行时间窗口内的功率组合,搜索空间大幅缩小,收敛速度肉眼可见地提升。
边界修正时还有一个细节:充电和放电的功率限幅取不同值。充电功率上限是充电桩额定功率,放电功率上限通常略低一些,因为放电还要考虑电池和逆变器的效率限制。修正时写成:
x = min(x, ch_max); x = max(x, -dis_max); x = x .* stay_mask;这个顺序不能反,先限幅再掩码。如果先掩码后限幅,掩码为0的位置,功率被限制到0没问题,但窗口内的值可能没有真正被限幅到位,所以顺序上先限幅再掩码最稳妥。
3.3 参数配置:惯性权重、学习因子、种群数量与迭代次数
粒子群算法的参数配置直接影响求解质量,我最终确定的参数如下:
- 种群数量 n_pop = 40;
- 最大迭代次数 max_iter = 200;
- 惯性权重 w 线性递减,从 0.9 降到 0.4;
- 学习因子 c1 = 1.5,c2 = 1.5;
- 速度上限 v_max = 0.5 * (ch_max + dis_max)。
惯性权重从大到小递减的原理是:迭代前期需要较强的全局搜索能力,粒子飞行范围大,防止过早陷入局部最优;后期需要精细的局部搜索,惯性权重变小,粒子在最优解附近仔细搜索。工程上线性递减是最常用也最稳的方案。
c1 和 c2 都取1.5是经验值。c1 太大,粒子会过多地围绕自身历史最优打转,群体共享信息不足;c2 太大,粒子会过早被全局最优吸引,种群多样性快速下降,容易早熟。两者相等时群体的探索和开发相对均衡。如果你发现收敛曲线后期震荡严重,可以把 c1、c2 改成 1.2 和 1.8,让社会学习权重更大一些,收敛会更稳定。
种群数量和迭代次数要看决策变量维度。50辆车、96时段的场景有4800维变量,40个粒子200次迭代已经能找到一个不错的次优解。如果你追求更高质量的解,可以把迭代次数提高到500,但收益会递减,实测从200次到500次目标函数值只下降了约5%左右。没必要一上来就跑到1000次,先跑通再逐步加大。
4. 案例分析:50辆电动汽车参与调度的仿真过程
4.1 基础数据设置与场景假设
我用一个典型居民区配电台区作为仿真场景,基础负荷数据采用夏季典型日的日负荷曲线,峰谷规律明显,峰值负荷约800kW,出现在晚上19点到21点,低谷负荷约300kW,出现在凌晨3点到5点。
电动汽车参数设置如下:
- 参与调度车辆数 N = 50;
- 电池容量:统一取40kWh,也可以按车型分布取30到60kWh之间的随机值;
- 起始SOC:在0.2到0.8之间均匀随机生成;
- 期望SOC:统一取0.9;
- 充电功率上限:7kW(对应家用交流慢充桩);
- 放电功率上限:5kW(考虑电池放电倍率限制);
- 充电效率:0.95;
- 放电效率:0.90;
- 停留时间:模拟上班族场景,车辆在早晨8点前陆续接入,下午17点到19点之间陆续离开,停留时间6到11小时不等。
这组数据是从实际工程调研里拿到的典型值,不同场景可以替换,代码结构不需要大改。
4.2 仿真结果解读:削峰填谷与SOC变化
跑完200次迭代后,我输出了三张关键图:收敛曲线、负荷对比曲线、车辆SOC变化热力图。
负荷对比曲线最能说明问题。无调度情况下,50辆车下班回家后同时开始充电,基础负荷和充电负荷叠加,晚高峰最高负荷飙到980kW,而且充电负荷高峰和基础负荷高峰完全重叠,这对配电网很不利。V2G调度之后,整体负荷曲线变得明显平缓,晚高峰期间部分车辆执行放电策略,向电网回馈功率,最高负荷被压到860kW左右;凌晨低谷时段,车辆集中充电,负荷低谷被填高。削峰率约12%,填谷效果也很明显。
收敛曲线显示,前50次迭代目标函数值快速下降,从初始的120左右降到50,100次之后逐渐平缓,200次时已经趋于稳定。这说明粒子群算法的参数设置合理,没有出现明显的震荡或者早熟停滞。
SOC变化热力图能直观看到每辆车在全天96个时段的SOC演化。调度结果中,大部分车辆在上午时段保持SOC不变或者小幅充电,下午高峰时段部分车辆SOC明显下降(正在执行放电),晚间低谷时段SOC快速回升,到离场时全部达到0.9以上。这验证了罚函数约束起效,离场电量需求被严格遵守。
还有几个有意思的观察。调度后有些车的充电过程不是连续的,而是“充-停-充”或者“充-放-充”的模式。这是因为算法在全局最优的驱动下,自动利用了电价低谷时段和高负荷时段的差异,把充电电量尽量分配到负荷低谷,把高峰时段的充电需求转移到低谷时段。这就是有序充电和V2G的直观效果。
5. 工程实现中的几个关键坑与解决办法
5.1 罚函数系数怎么调,收敛曲线震荡怎么办
罚函数系数λ的调整是新手最容易困惑的地方。我的经验是先设一个比较小的值,比如100,跑一遍看有多少车离场SOC不达标。如果大量不达标,把λ乘以10,再跑,如此反复。当你发现最终方案基本都能满足电量约束,但收敛曲线在后期还有小幅波动,说明λ偏大了,适当调小。找到令方案可行且曲线平缓的最小λ,就是当前问题的最优罚系数。
收敛曲线如果出现大幅震荡,原因通常是速度钳制失效,粒子位置在可行域边界来回反弹。这时候要检查 v_max 是否设置合理。v_max 设成0.5倍功率上限是我推荐的经验值,太小收敛慢,太大会导致粒子飞过整个搜索空间,震荡非常明显。
5.2 随机初始化导致每次结果不同,怎么复现
粒子群算法本身就是随机优化算法,初始化位置和速度都是随机的,所以每次运行结果会有差异。如果你需要复现论文里的实验数据,最直接的方法是在Matlab脚本开头设置随机种子:
rng(42);这个随机种子设置之后,整个实验过程用的随机数序列完全一致,多次运行结果也一致。注意要在所有随机生成操作之前设置,包括车辆参数生成和粒子初始化。
如果你对比不同算法(比如PSO和GA)的性能,必须保证所有算法用同一个初始种群,并且每种算法独立运行20次以上,取平均值和标准差来比较,单次运行结果没有统计意义。
5.3 车辆规模增大之后算法变慢的优化技巧
当车辆数增加到200辆甚至更多时,决策变量维度达到 200×96 = 19200,粒子群算法的计算时间会明显上升,主要瓶颈在适应度函数里计算聚合功率和SOC累积的过程。我做了两个优化:
一个是矩阵化计算。不要用双层for循环逐辆车遍历计算SOC,而是直接用矩阵运算。把粒子的功率矩阵 reshape 成 N×T,用 cumsum 累积计算每个时段的总充放电电量,再除以电池容量得到SOC变化量。Matlab的矩阵运算比循环快一个数量级以上。
另一个是适应度函数的向量化。把目标函数写成函数句柄,内部全部用向量操作。实测50辆车时单次适应度计算时间从0.05秒降到0.008秒左右,200次迭代的整个求解时间缩短了六成。
还有一个小技巧:如果时段的粒度不需要那么细,可以从15分钟改成30分钟或者1小时。96时段降到48或24个时段,决策变量直接减半甚至减到四分之一,迭代收敛速度会有质的提升。代价是调度指令的时间精度变低,实际工程中按15分钟控制已经足够,再粗会影响削峰填谷效果。
5.4 不可行解的提前筛除
罚函数虽然能处理不可行解,但如果初始种群中90%以上的粒子都是不可行的,搜索效率依然很低。我的做法是在粒子初始化阶段,就做一次修正:遍历每个粒子的功率矩阵,如果某辆车在停留窗口内的累计充电电量不足以满足离场需求,直接增加窗口内低谷时段的充电功率,让初始解先满足电量约束。这样可以保证初始种群中大部分粒子是可行的,算法从一组较优的起点开始搜索,收敛速度和最终解的质量都有明显提升。
这个方法本质上利用了问题本身的物理结构,比单纯的随机初始化加罚函数效果要好得多,算是工程实现中的一个小亮点。
6. 代码框架速览与扩展方向
6.1 主程序文件结构
项目Matlab代码按模块拆分,建议这样组织文件:
- main.m:主程序,负责参数设置、数据加载、算法调用、结果绘图;
- initParameters.m:初始化所有仿真参数,包括车辆参数、负荷数据、PSO参数;
- initSwarm.m:初始化粒子种群位置和速度,包含可行性修正;
- calcObjective.m:计算目标函数值,包含罚函数项;
- applyBoundary.m:位置边界修正和时间窗口掩码;
- updateVelocity.m:粒子速度更新,包含速度钳制;
- plotResults.m:绘制收敛曲线、负荷对比图、SOC热力图。
按模块拆分的好处是,后续换目标函数或者换约束条件,只需要改对应模块,其他部分可以复用。比如你想把目标函数从负荷方差最小化改成充电费用最小化,只需要重写 calcObjective.m,其他代码不用动。
6.2 两个最有价值的扩展方向
第一个扩展方向是引入实时电价机制。目前目标函数是削峰填谷,其实还可以加入分时电价,把目标改为充电费用最小化,即用户侧经济性最优。调度模型中电价作为已知输入,目标函数修改为 sum( P_ev_agg(t) * price(t) * delta_t ),放电时段电价高,车辆放电减少电费支出,这个场景更贴近车网互动的商业模式。
第二个扩展方向是多目标优化。实际工程中“削峰填谷”和“电池寿命损耗”之间存在矛盾——频繁深度放电会加速电池老化,但削峰效果更好。这时候可以采用多目标粒子群算法(MOPSO),同时优化两个目标,输出一组帕累托前沿解,供决策者在削峰效果和电池损耗之间做权衡。Matlab里可以基于现有的PSO框架加上外部档案库和拥挤度距离排序来实现。
第三个方向是加入充电功率的离散约束。实际充电桩的功率调节不是连续的,比如只有0、3.5、7kW三挡。你可以在粒子位置更新后做最近邻离散化,但这会导致梯度突变,粒子群收敛变差,更稳妥的做法是在适应度函数里增加一个离散化距离的惩罚项,让算法在优化过程中自动选择合适的挡位。
我在实际跑这个项目时最大的体会是:粒子群算法本身的实现并不难,难的是把物理约束映射到优化模型里,再通过代码把约束有效地“压”进搜索过程。停留时间和充电时间看似只是两个数字,但它们直接决定了调度窗口的宽窄和灵活性上限。把这两个时间处理好,整个模型的可解释性就上来了,结果图给导师或者甲方看,一眼就能说明白为什么有的车被调度放电了,有的车从头到尾只充电不放电。这套代码在Matlab里跑通之后,后续换数据、换场景、换目标函数,都只是改参数和改目标函数文件的功夫。