ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于PSO与Voronoi图的电动汽车充电站选址定容Matlab实现

基于PSO与Voronoi图的电动汽车充电站选址定容Matlab实现 做过类似选址项目的人应该都有同感充电站建在哪、建多大看着像拍脑袋的事真要算起来牵扯的东西特别多。需求点分散在城市各处每个站的服务范围怎么划容量配大了浪费、配小了排队建站成本与用户体验之间还得权衡。我这次在Matlab里把PSO粒子群算法和Voronoi图这两样工具组合在一起专门解决电动汽车充电站的选址定容问题跑完整套流程之后最大的感受是这两者的搭配恰好把“站该建哪”和“站该管多大区域”两件事同时解决了。这套方法的适用面比想象中广无论是区域充电网络规划、城市充电桩布局还是园区、高速服务区这类场景都可以套用。如果你正在做类似的选址定容研究或者刚接触PSO、Voronoi图想找个能落地的Matlab实现思路这篇文章应该能帮你少走不少弯路。我会把建模思路、Matlab代码实现、参数调优和经验教训一起讲清楚照着做基本可以复现。1. 问题建模与整体思路拆解1.1 先把“选址定容”这件事翻译成数学语言选址定容拆开看就是两个决策充电站建在哪个坐标点站里配多少充电桩。这两个变量不是独立的站选得偏容量再大也覆盖不到核心需求区容量定得小位置再好也消化不了高峰期的车流。所以建模的第一步是把需求点和候选点都放进二维坐标系里。我习惯用网格化方式生成需求点。把规划区域划分成若干个小格子每个格子的中心作为一个需求点根据周边人口、车流量、商业热度给一个权重值。这个权重可以理解为“该区域对充电的需求强度”。如果手里有真实的充电订单数据可以直接用订单密度替代。没有数据就用交通流量或者小区户数做近似反正核心是让需求分布尽量贴近现实。决策变量设置成两部分一是充电站的平面坐标二是容量等级。容量等级我通常用离散值表示比如20kW直流桩8根、12根、16根这样分档。为什么要离散因为连续变量会让问题变成混合整数规划PSO处理起来要加很多约束离散之后每个粒子就是一个固定维度的向量目标函数直接按档位计算建设成本和运维成本简单高效。目标函数我选的是综合成本最小化。综合成本包含三块建设成本、运维成本、用户绕行时间成本。前两项好理解第三项是隐性成本我用所有需求点到其归属充电站的加权距离之和来近似距离越短用户体验越好这个值也就越低。约束条件有两个必须写进去第一每个站的负荷不能超过容量上限第二需求点只能分配给一定半径内的充电站超出半径的算覆盖失败要在目标函数里加惩罚。1.2 为什么偏偏是PSO加Voronoi这套组合先说Voronoi图。选址问题里最头疼的是“需求点到底归属哪个站”。如果拍脑袋按直线距离最近来分边界处的需求点很容易在两个站之间来回跳动导致优化过程不稳定。Voronoi图把整个区域划分成若干个单元每个充电站天然拥有一个属于自己的服务区域单元内的所有点距离该站最近。这种“几何硬划分”特性正好解决了归属问题而且在Matlab里用delaunayTriangulation就能直接算出来。再说PSO。选址定容问题的解空间是非凸的站点坐标加容量档位的组合数量极大传统枚举法在城市规模级别下根本跑不完。PSO的优势是不需要目标函数可导群体搜索的方式不容易被局部极值困死代码实现又比遗传算法简单不少。把粒子设计成“若干候选站坐标加容量档位”的向量每次迭代用Voronoi图做服务分区再计算综合成本作为适应度值就能形成一套完整的优化闭环。这两者组合还有一个隐性好处Voronoi图帮PSO大幅压缩了搜索空间。PSO只需要决策站的位置和容量不用操心分配关系分配关系完全由Voronoi图自动决定。一旦站移动分区就跟着变适应度函数给出的信息量远大于普通最近分配法。用一句大白话总结Voronoi管“地盘”PSO管“站位和规模”各司其职。2. Voronoi图在服务分区里的正确打开方式2.1 从“离谁最近”说起Voronoi图的几何直觉如果你没接触过Voronoi图可以用一个生活场景来理解。下雨天你站在一片空地上周围有几个避雨亭你肯定会跑向离自己最近的那个整片空地按“最近原则”划给各个亭子之后得到的区域拼图就是Voronoi图。每个亭子所在的区域叫一个cellcell内任意一点到该亭子的距离都小于到其他任何亭子的距离。在充电站场景里每个充电站就是“亭子”需求点就是“人”。有了Voronoi图每个站的服务范围一目了然而且还天然具备“最近分配”的合理性避免了用户舍近求远的反直觉现象。更妙的是Voronoi图不需要人为设定站的影响半径边界完全由站点位置和空间拓扑关系生成这意味着只要站点坐标变化分区结构就自动调整非常适合嵌入优化迭代过程。但这里有个隐蔽的坑靠边界的站点它的Voronoi cell会向无穷远处延伸。也就是说如果你不限制区域边界边缘站的“领地”可能大到没边。实际规划里这当然不合理必须引入规划区域的边界约束把无限维诺单元裁剪到指定区域内。这个操作叫有界Voronoibounded Voronoi也是最容易让人卡壳的地方。2.2 第一个大坑边界裁剪与无限cellMatlab自带voronoi函数输入站点坐标能直接画图但函数返回的cell信息是开放的它不会替你考虑“规划区域只到这条路为止”。我第一次跑的时候边缘站的Voronoi区域延伸到画面外需求点分配结果直接乱掉连适应度函数都算不对。解决思路是自己做裁剪。我把规划区域设置成一个矩形先用delaunayTriangulation生成三角剖分再通过voronoiDiagram函数拿到每个站点的Voronoi顶点最后对每个cell做多边形求交只保留落在矩形内部的区域。这个方法的核心代码不复杂麻烦在于索引关系容易绕晕建议用Matlab的polyshape对象把每个cell和边界矩形都转成polyshape直接用intersect函数裁剪代码简洁很多。% 生成站点Voronoi图并裁剪到矩形边界内 % xy站点坐标 N x 2 % bd边界矩形 [xmin xmax ymin ymax] dt delaunayTriangulation(xy(:,1), xy(:,2)); [V, R] voronoiDiagram(dt); xmin bd(1); xmax bd(2); ymin bd(3); ymax bd(4); boundaryPoly polyshape([xmin xmax xmax xmin], [ymin ymin ymax ymax]); cellPolys cell(size(R,1), 1); for i 1:length(R) if R{i}(1) 1 % 索引1表示无穷远顶点需要额外处理 cellPolys{i} boundaryPoly; continue; end vx V(R{i}, 1); vy V(R{i}, 2); % 剔除无穷远点 keep isfinite(vx) isfinite(vy); if sum(keep) 3 cellPolys{i} boundaryPoly; continue; end poly polyshape(vx(keep), vy(keep)); cellPolys{i} intersect(poly, boundaryPoly); end这段代码的思路是先用voronoiDiagram拿到每个站点cell的顶点索引然后逐个构建polyshape最后与边界矩形求交。无穷远顶点在结果里会用索引1标记需要单独跳过或者直接退回为整个边界区域。这算是一个比较稳妥的工程化处理方式比单纯调用voronoi函数再手动修剪要省心得多。2.3 把需求点归到充电站Matlab实操片段有了裁剪后的cell多边形需求点归属就变成判断点是否在多边形内的问题。Matlab里isinterior函数可以直接判断一组点是否在polyshape内写一个循环就能完成分配。% 需求点坐标 demandPtsM x 2权重 demandWM x 1 % 返回每个需求点归属的站编号 assignIdx assignIdx zeros(size(demandPts,1), 1); for i 1:length(cellPolys) in isinterior(cellPolys{i}, demandPts(:,1), demandPts(:,2)); assignIdx(in) i; end完整算适应度的时候再按照归属关系计算每个站的累计负荷并与容量上限对比超载部分进入罚函数。这里要注意的一个细节是Voronoi分区是随站点位置变化的所以每次迭代都需要重新生成一次Voronoi图和归属关系。这个操作的计算量随着需求点数量增长比较明显实测下来几千个需求点配合几十个站点单次适应度计算在Matlab里大概是几十毫秒级别配合PSO迭代还能接受。我自己跑过的算例里把需求点从300个加到3000个单次计算时间从10毫秒涨到80毫秒左右。如果需求点规模更大建议先用聚类把需求点聚合到几百个代表点再进优化循环精度损失不大速度能快一个量级。3. PSO算法设计与Matlab实现细节3.1 粒子编码方式站址坐标加容量档位的拼包PSO的第一步是设计粒子的数据结构。假设要规划K个充电站每个粒子就是一个长度为3K的向量前2K个元素是K个站的横纵坐标后K个元素是容量档位。注意容量档位必须处理成整数我在粒子更新后用round取整再加边界约束。这里有一个很多人会忽略的细节站点数量怎么定。Voronoi图本身不产生站点它只划分地盘站点个数K是预设参数。K太小覆盖不足K太大成本爆炸。我的做法是先用K-means对需求点聚类以轮廓系数确定一个合理的K范围再让PSO在不同K值下轮流跑一遍绘制“综合成本-K”曲线取拐点处的最优K。这算是两层优化外层枚举K内层跑PSO虽然耗时一些但比单次优化靠谱很多。粒子初始化也很关键。直接随机生成坐标容易让初始站点扎堆Voronoi分区就会很畸变PSO要花大量迭代去修正。我建议用K-means聚类的质心作为第一个粒子的初始值其他粒子在质心附近加随机扰动。这样初始种群的质量高收敛速度明显加快这个技巧实测能省掉近一半迭代次数。3.2 适应度函数与约束处理罚函数怎么写适应度函数是整套算法的“裁判”必须把建设成本、运维成本、用户时间成本、覆盖惩罚都融合进来。我设计的计算公式大致如下。function cost calcCost(x, demandPts, demandW, params) K params.K; xs x(1:K); ys x(K1:2*K); levels round(x(2*K1:3*K)); levels min(max(levels, params.minLevel), params.maxLevel); % 站点坐标 stations [xs(:), ys(:)]; % 生成有界Voronoi并分配需求点 cellPolys boundedVoronoi(stations, params.boundary); totalCost 0; for i 1:K inIdx isinterior(cellPolys{i}, demandPts(:,1), demandPts(:,2)); load sum(demandW(inIdx)); cap params.capacityPerLevel(levels(i)); % 距离成本需求点到本站的平均距离 dists sqrt((demandPts(inIdx,1)-xs(i)).^2 (demandPts(inIdx,2)-ys(i)).^2); totalCost totalCost params.alpha * sum(demandW(inIdx) .* dists); % 建设运维成本 totalCost totalCost params.buildCost(levels(i)) params.opCost(levels(i)); % 容量超载惩罚 if load cap totalCost totalCost params.penalty * (load - cap)^2; end end cost totalCost; end罚函数的设计原则是惩罚项要大到足以“劝退”优化器但又不至于完全掩盖其他成本信息。我用容量超出的平方再乘以一个较大系数这样小幅度超载不至于立刻淘汰整个粒子但持续超载的粒子会被快速淘汰。系数取多少合适我是先跑一次不带罚函数的松弛解看目标函数量级然后让罚函数系数比正常成本高一个到两个数量级。这个做法比拍脑袋设系数靠谱得多。这里还要提醒一点距离成本部分我加了一个权重系数alpha。不同类别的成本单位不一样建设成本是元距离成本是千米权重必须统一量纲。alpha可以理解为“每公里用户绕行成本折算成多少钱”实际取值根据区域经济水平来定我在算例里一般取50到200之间。3.3 速度更新与惯性权重的经验值PSO的经典速度更新公式网上一搜一大把但工程实现里真正影响效果的是惯性权重w、个体学习因子c1和群体学习因子c2的取值以及位置边界约束的处理方式。我采用的惯性权重是线性递减策略从0.9随迭代次数线性降到0.4。前期的较大权重保证全局探索能力让粒子飞得远一点不容易困在局部最优后期的较小权重增强局部搜索让粒子在最优解附近精细逼近。c1和c2都取1.5左右这让粒子既参考自己历史最优也参考群体历史最优保持探索和收敛的平衡。% PSO主循环核心更新逻辑 for iter 1:maxIter w 0.9 - (0.9 - 0.4) * iter / maxIter; for p 1:popSize r1 rand(1, dim); r2 rand(1, dim); velocity(p,:) w * velocity(p,:) ... c1 * r1 .* (pbest(p,:) - pop(p,:)) ... c2 * r2 .* (gbest - pop(p,:)); pop(p,:) pop(p,:) velocity(p,:); % 坐标边界约束 pop(p,1:2*K) max(min(pop(p,1:2*K), ub), lb); % 容量档位约束 pop(p,2*K1:3*K) round(pop(p,2*K1:3*K)); end end位置边界约束还有个小技巧。如果粒子飞出规划区域边界直接拉回边界会让粒子大量堆积在边界上影响多样性。更好的做法是“随机反弹”把飞出边界的坐标以边界为镜面反射回来这样粒子在边界附近依然有分布。这个细节在边缘站选址时尤其重要因为边缘站的Voronoi cell被边界裁剪后形状比较怪粒子的搜索行为需要更充分。还有速度上限问题。速度向量如果过大粒子会乱飞导致算法发散过小则收敛极慢。我的经验是把最大速度限制在每个维度搜索范围的10%到20%之间比如坐标范围是20公里那速度上限设在2到4公里每代。4. 完整实验流程与结果分析4.1 实验参数设置与算例设计说一个我实际跑过的标准算例规划区域是一个20km乘20km的方形区域内部随机生成500个需求点权重服从高斯分布模拟市中心高需求、郊区低需求的场景。规划4个充电站容量档位3档对应8、12、16根直流桩。PSO种群规模40迭代100代。具体参数表如下。参数取值说明规划区域20km x 20km矩形边界需求点数量500加权随机生成站点数量K4由K-means轮廓系数确定容量档位8/12/16根直流快充桩种群规模40粒子数迭代次数100收敛判据辅助惯性权重0.9→0.4线性递减c1, c21.5, 1.5学习因子这个算例规模不算大我的笔记本跑完整轮PSO大约需要3到5分钟因为每次迭代都要重新计算几千个需求点的Voronoi归属关系。如果需求点规模到5000以上建议先聚类降维否则计算时间会很难看。我试过直接用1万个需求点跑单次迭代时间直接飙到15秒以上整个优化耗时超过20分钟基本不可接受。4.2 收敛曲线与迭代过程的判读跑完之后不要急着看最终解先看收敛曲线。我的习惯是绘制每一代的群体最优适应度值正常情况下曲线应该呈现“陡降—缓降—平台”三个区间。陡降区间对应前期全局搜索阶段PSO快速找到较好的区域缓降区间是局部精细搜索平台期说明算法已经基本稳定可以提前终止。如果你的收敛曲线在前20代就完全走平通常不是好事。要么是种群的初始解质量太低导致粒子全都陷在同一片区域要么是罚函数系数设置过大把很多本可以探索的方向全都判了死刑。我遇到过一种常见情况超载惩罚过重粒子全部缩在成本低但容量不足的小规模档位上收敛曲线漂亮得像一条直线结果完全不可用。所以要结合容量利用率一起看光看成本曲线容易自欺欺人。我自己会在迭代结束后多跑几次初始化重复实验做统计对比。因为PSO是随机算法单次结果有偶然性。同一组参数跑5次取最好、最差和平均成本。如果最好和最差差距超过15%说明算法稳定性不足需要调整参数或增加种群规模。这个稳定性验证在写论文或做报告时尤其重要评审一眼就能看出你只跑了单次实验。4.3 一键出图Voronoi分区和需求覆盖可视化做完优化不能只给出一堆数字把结果可视化出来才能判断方案好坏。Matlab绘制Voronoi分区图很简单把裁剪后的cellPolys依次用plot画到同一张图上站点位置用五角星标记需求点用散点图按归属站着色。figure; hold on; colors lines(K); for i 1:K plot(cellPolys{i}, FaceColor, colors(i,:), FaceAlpha, 0.2); scatter(demandPts(assignIdxi,1), demandPts(assignIdxi,2), ... 10, colors(i,:), filled); end plot(stations(:,1), stations(:,2), kp, MarkerSize, 12, LineWidth, 2);这张图能直观看出几个问题边缘站点的服务区域是否被裁剪得合理、站点之间是否存在大片无人区、高权重需求点是否被分配到了较近的站。我在一个算例中看到过优化后的站址靠边界很近Voronoi cell被边界切得只剩一小条但该区域恰恰是高权重商业区说明这个边界位置反而合理。如果只靠数字分析很难发现这种空间的微妙关系。除了二维分区图还可以画一个三维的需求覆盖图把需求点位置作为平面坐标把“需求点加权距离”作为高度直观显示哪些区域的服务距离特别大。这些图不仅给自己看写技术报告给领导或客户汇报时也很有说服力。5. 常见问题与排查技巧实录5.1 早熟收敛与停滞陷阱PSO最大的毛病是容易早熟收敛尤其在高维搜索空间里。我遇到的典型表现是迭代60代之后适应度值几乎不动解的质量明显不如预期。排查办法是先检查种群多样性也就是看粒子之间的距离分布。可以用pdist2统计粒子之间的平均距离如果平均距离过小说明粒子群已经聚集到一起失去了探索能力。处理早熟收敛的办法有几种。最简单的方案是重新初始化部分粒子比如在迭代后期随机抽取30%的粒子赋予它们新的随机位置和速度相当于给群体“注入新鲜血液”。但这个操作需要控制频率和幅度我一般在连续10代适应度变化小于0.1%时触发一次重置。另外一种思路是引入变异算子类似遗传算法的做法以很小概率随机改变粒子的某些维度值让搜索跳出停滞区。这里还要提醒别把收敛太快误认为算法高效。收敛速度和全局搜索能力是相互制约的前文说的惯性权重线性递减就是为此设计的。如果你发现收敛曲线太陡试着把初始惯性权重从0.9提高到1.0或者增强r1、r2随机数的波动幅度让粒子前期的“飞散”范围更大。5.2 容量溢出与服务盲区我做过一次有意思的对比实验同一组需求数据分别按“Voronoi硬分区”和“容量约束修正分区”进行分配结果发现Voronoi硬分区在某些场景下会显著高估站点负荷。原因是Voronoi只考虑空间最近原则不考虑需求强度的空间不均衡。如果高权重需求点恰好集中在两个站的边界处最近分配会把它们全归到一个站另一个站容量闲置。解决方法是在容量超载时做二次修正。具体做法是当某个站的累计负荷超过容量上限时将超载部分需求点重新分配到负荷率较低的相邻站形成“Voronoi分配加容量均衡修正”的两步法。这样既保留了空间就近原则又避免了容量过度集中的问题。修正规则没有标准答案我实现的是把超载需求点按距离升序排列从最近的可接收站开始尝试分配。所谓服务盲区是指规划区域里有些高权重需求点距离任何站都很远Voronoi图即使正确画出来这部分需求点的平均距离依然很大。盲区出现的原因通常不是算法问题而是站点数量K不够。这时候重新跑不同K值比对曲线就会发现综合成本的最低点其实出现在更大的K上。所以排查盲区问题先画覆盖图确认盲区位置再考虑是否增加站点而不要盲目调PSO参数。5.3 Matlab版本、工具箱与运行效率问题这套实现依赖几个Matlab基础功能delaunayTriangulation、voronoiDiagram、polyshape的intersect和isinterior。好消息是这几个函数从R2017b之后就已经是基础函数不需要额外购买工具箱。Global Optimization Toolbox里的particleswarm函数也可以替代手写PSO但手写实现更方便嵌入定制化的Voronoi分配逻辑我建议还是自己写也不复杂。关于Matlab版本差异我在R2020b和R2023b上跑过同一套代码polyshape的性能有明显改善R2023b处理几千个多边形的求交速度快了不少。新版本的voronoiDiagram输出格式没有变但如果你从旧版本迁移代码还是要注意polyshape的输入参数格式。另一个经验是如果需求点特别多可以尝试把isinterior判断改成inpolygon虽然精度差一点但速度更快。如果追求极致性能把热循环用parfor并行化也是个选择但要注意Voronoi生成部分涉及共享变量并行化之前必须确认变量依赖关系。还有一个很实际的问题Matlab版本越新polyshape对象越稳定但如果你的机器配置一般建议把循环内的重复绘图操作去掉只在最终结果里画图。我早期调试时每次迭代都实时画Voronoi图结果一张图重绘就要半秒100代迭代硬生生多出近一分钟纯属浪费。6. 一点个人体会这套PSO加Voronoi的组合我前后用了挺长时间打磨最大的收获倒不是算法本身而是“把工程问题拆成几何问题”的思维方式。Voronoi图原本是计算几何的经典概念用在充电站选址上却异常契合因为它的核心特性和选址问题完全对口。实际跑下来在需求点规模不大、站点数量适中的场景里这套方法比传统网格搜索或单纯PSO快而且稳定给出的站点分布也很符合直觉。最后分享一个小技巧不要只盯着最优解。把PSO迭代过程中的历史群体最优粒子都保存下来画成散点图你会看到粒子在搜索空间里的移动轨迹。观察这些轨迹能帮你判断算法是否在合理探索也能帮你发现解空间的特殊结构。有一次我正是因为多画了这张历史轨迹图发现站点坐标在某条线附近反复移动才意识到高权重需求点形成的“走廊”对选址具有决定性的影响这个发现对后续业务决策的帮助比单纯跑出一组最优站址大得多。
返回列表