SUMO交通仿真进阶:真实路网导入、OD需求生成与TraCI交互实战
发布时间:2026/10/1 5:10:49来源:尧图网络
做交通仿真的人大多经历过这么一个阶段路网能加载了车也能跑起来了但屏幕上稀稀拉拉几辆车绕圈根本看不出任何有价值的结论。我前两篇把 SUMO 的安装、基本路网、最小可运行配置捋了一遍那只能算环境通了离能出结果还有一段不短的距离。这一篇要解决的就是从能跑到能用的跨越怎么把真实地图吃进来、怎么生成有意义的交通需求、怎么让信号灯按你的意志动作、怎么通过 TraCI 把外部程序接进来、仿真跑完之后数据怎么落地成能画图的表格。如果你正在做交通流分析、信号配时评估、自动驾驶算法的路测前仿真或者只是想把 SUMO 当成一个可控的车辆行为沙盒这篇里的配置和脚本都能直接拿去改。我尽量把每个参数为什么这么设讲清楚也会把那些官方文档里不写、但实际一跑就报错的地方标出来。下面所有内容基于 SUMO 1.19 到 1.20 这一代版本Python 侧用的是 pip 安装的 traci 包操作系统以 Ubuntu 为主Windows 下路径换成反斜杠即可逻辑完全一致。1. 第三篇到底要补上哪几块拼图1.1 复盘前两篇留下的三个空洞先把定位说清楚。第一篇解决的是环境本身SUMO_HOME 环境变量、sumo 与 sumo-gui 两个可执行文件、Python 的 traci 模块能不能 import 成功这些是地基。第二篇通常是把一个手写的小路网跑通.net.xml加.rou.xml加一个.sumocfg配置文件点开 GUI 能看到车在动。但跑通之后你会发现三个明显的空洞。第一个是路网来源手画的路网只有几个十字路口没有真实的转弯渠化、没有车道宽度差异、没有交通岛评估结果没有说服力。第二个是需求来源第二篇里往往是写死几条flow车辆数量、出发时间、起讫点都是拍脑袋定的这种输入下得出的延误、排队长度没有任何参考价值。第三个是交互能力仿真跑完就完了外部程序既不能在运行中介入控制也拿不到细粒度的过程数据。这三个空洞对应本篇的三个主攻方向真实路网的导入与清洗、基于 OD 的交通需求生成、TraCI 实时交互与数据采集。我把它们放在一起讲是因为这三块在实际项目里是咬合的路网决定了你能布哪些检测器需求决定了你需要什么精度的输出而输出精度又反过来决定你有没有必要上 TraCI。1.2 什么样的结果才算能用先立一个验收标准不然容易陷入无止境的调参。我个人的判断线是三条。第一条仿真结束后能导出一份逐车的 tripinfo里面有每辆车的出发时间、到达时间、行驶距离、平均速度、等待时长。这份数据能直接丢进 pandas算出全网平均行程时间、平均延误。第二条能在关键断面上输出流量和占有率的时间序列粒度可以到 1 分钟甚至 30 秒这样你才能画出流量曲线去和实测数据对比。第三条能通过一个外部脚本在仿真运行过程中读取车辆位置、修改信号配时并且修改立刻在下一步生效。只要这三条都达成这个仿真环境就具备了做严肃分析的基础。达不到的话无论路网画得多漂亮都只是动画演示。下面的章节会沿着这个标准逐项来。1.3 一个常见的认知误区很多人第一次接触 SUMO 会把它和机器人仿真平台混为一谈搜索的时候经常把 Gazebo、ROS 2 那套环境搭建和 SUMO 放在一起看。这两类工具的目标完全不同Gazebo 那类平台关心的是单个智能体的传感器数据、物理碰撞、关节动力学SUMO 关心的是宏观和微观的交通流车辆本身被抽象成带跟驰模型和换道模型的矩形。这个区别决定了你在 SUMO 里是找不到激光雷达点云和相机图像的。如果项目需要车辆感知算法的闭环测试正确的做法是交通侧用 SUMO 跑车流感知侧用另一个仿真器跑传感器两边通过 TraCI 或者联合仿真框架交换车辆位姿而不是指望 SUMO 自己产生原始传感器数据。想清楚这条边界能省掉大量无效折腾。2. 把真实路网吃进来并洗干净2.1 从 OSM 数据到可仿真的 net 文件真实路网最方便的来源是 OpenStreetMap导出.osm文件之后用 netconvert 转换。命令看着简单参数却直接决定成败。netconvert \ --osm-files map.osm \ --output-file map.net.xml \ --geometry.remove \ --ramps.guess \ --junctions.join \ --tls.guess-signals \ --tls.discard-simple \ --no-turnarounds \ --proj.utm逐条解释为什么。--geometry.remove会把道路上多余的形状点去掉把弯曲的边界简化成折线节点数量能砍掉一大半后续仿真速度提升非常明显。--ramps.guess让工具自动识别匝道高速和快速路场景必须有它否则匝道和主路会被当成普通交叉口处理产生莫名其妙的停车等待。--junctions.join用于合并距离过近的相邻路口OSM 里双向道路常常被画成两条独立中心线不合并的话每个路口都会裂成两个信号灯也没法统一控制。--tls.guess-signals根据道路等级和车道数推测哪里应该有信号灯属于半自动方案结果需要人工核对。--tls.discard-simple会丢掉那些只有两条支路的简易信号灯避免每个丁字口都亮红灯。--no-turnarounds禁止在路口原地掉头除非你的场景确实需要掉头车流否则这个参数能砍掉大量不合理的掉头轨迹。--proj.utm把经纬度投影到 UTM 平面坐标这样仿真里的距离单位才是米。不投影的话坐标全是度数跟驰模型里那些以米为单位的参数就失去意义了车距会算得离谱。2.2 转换之后必须做的三项体检netconvert 不报错不代表路网可用。我每次转完都会做三件事。第一用 sumo-gui 打开 net 文件看路网是否闭合。常见的毛病是某些路段悬空端点没有连到任何路口这种路段上的车开到头就消失了。在 GUI 里把视图拉远悬空的边会明显突出来。第二检查车道连接。在 GUI 里点开某个复杂路口看有没有车道之间的连线异常比如左转车道连到了直行车道。OSM 数据里这种情况不算少见尤其是那些后期改造过的路口。修正的办法是在.net.xml里手工编辑connection标签或者写一个 netconvert 的补丁文件用--connection-files挂进去。第三统计路段数量和总长度。路网规模直接决定仿真性能一般城市级路网几千条边、几万个节点还算可控再大就要考虑分区仿真或者用宏微观混合方案了。一条经验是如果你只是研究某个走廊或者某个片区就不要把整个城市导进来用--keep-edges.in-geo-boundary配合边界坐标裁一个矩形出来性能能提升一个数量级。注意从 OSM 转出来的路网默认限速往往偏低甚至缺失跟驰模型会把限速当成期望速度直接导致全网车速偏慢。转换后建议批量核对主要道路的speed属性按实际道路等级修正。2.3 用 netedit 做局部精修netconvert 出来的是半成品真正的精修要靠 netedit这是 SUMO 自带的图形化路网编辑器。常用的几个操作我列一下。拖拽节点调整路口位置修掉那些因为投影误差偏移的路口。框选一组边统一修改车道数和限速适合整条主干道规整化。手动添加或删除车道连接处理那些自动推断错误的路口。给路段加检测器位置标记方便后续布设感应线圈。删除多余的步行道和自行车道如果你的仿真只关心机动车流这些边留着只会拖慢速度还占内存。我一般会在 netedit 里花半小时过一遍主要路口把明显不合理的连接清掉。这半小时的投入换回来的是后面调参时少掉一半的诡异现象。3. 交通需求生成从拍脑袋到有依据3.1 随机流适合什么时候用SUMO 自带randomTrips.py能基于路网快速生成出行需求脚本位置在$SUMO_HOME/tools/下面。典型用法是这样。python3 $SUMO_HOME/tools/randomTrips.py \ -n map.net.xml \ -o trips.trips.xml \ -b 0 -e 3600 \ -p 1.2 \ --fringe-factor 10 \ --min-distance 300 \ --validate-p 1.2表示平均每 1.2 秒生成一辆车-b和-e是仿真起止时间这里是一个小时。--fringe-factor 10是个关键参数它让起讫点更倾向于落在路网边缘模拟过境交通值越大这种倾向越强。不加这个参数车流会在路网内部随机出现和消失看起来就像车辆凭空冒出来非常不真实。--min-distance 300过滤掉太短的出行避免车辆刚出发就到终点。这套随机流适合两类场景压力测试看路网在多大流量下开始崩溃或者算法调试你只关心控制逻辑对不对不关心绝对数值准不准。它不适合做任何定量评估因为它的出行分布和真实城市完全对不上。3.2 OD 矩阵驱动的需求生成要做定量分析必须有 OD 矩阵。流程是先把土地性质或者交通调查数据整理成小区间的出行量再转换成 SUMO 能识别的格式最后用 duarouter 做路径分配。OD 数据通常是一个二维表行是起点小区列是终点小区单元格是出行量。SUMO 里没有直接的 OD 概念需要先用一个脚本把 OD 展开成逐车或者逐流的形式。展开的时候要注意两个坑一是时间分布把所有出行量平均摊到整个时段是不对的早晚高峰的形态必须保留我一般用一个分段的时间分布曲线去调制二是小区映射OD 里的小区编号要能对应到路网上的一段边或者一个节点集合映射错了整批数据的起讫点都会飘。展开成行程文件后用 duarouter 做分配。duarouter \ -n map.net.xml \ -r trips.trips.xml \ -o routes.rou.xml \ --ignore-errors \ --repair \ --routing-algorithm dijkstra \ --weights.random-factor 1.2--ignore-errors和--repair一起用能自动修复那些起讫点不在路网上的行程否则一条坏数据就会让整个分配中断。--weights.random-factor 1.2给路段权重加随机扰动让同一对起讫点的车辆走不同路径避免所有车都挤在最短路那一条上这更接近真实驾驶员的路径选择行为。想要更精细的话可以把--routing-algorithm换成CH或者CHWrapper在大型路网上速度快很多但需要先做一次预处理。3.3 车辆类型与跟驰模型的匹配需求文件里的车辆不是千篇一律的不同类型车的长度、加速度、最大速度、跟驰参数都不一样。一个合理的做法是在 routes 文件里先定义几个 vType然后在 flow 或 trip 里引用。vType idcar vClasspassenger length4.8 accel2.6 decel4.5 sigma0.5 maxSpeed16.7 speedFactornormc(1.0,0.1,0.8,1.2)/ vType idbus vClassbus length12 accel1.2 decel3.0 sigma0.5 maxSpeed13.9/ vType idtruck vClasstruck length9.5 accel1.0 decel2.5 sigma0.5 maxSpeed11.1/sigma是跟驰模型的随机项控制驾驶员行为的波动0 表示完全确定性的理想驾驶0.5 是默认值。speedFactor用了正态分布表示不同驾驶员的期望速度差异均值 1.0、标准差 0.1范围限制在 0.8 到 1.2 之间。这个参数对结果影响很大设成固定值会让所有车整齐划一地跑排队消散过程过于理想化。车型比例也是要调的一个全小汽车的场景和一个含 15% 货车的场景通行能力差别很明显。如果做通行能力评估建议至少分小汽车、公交、货车三类按实测比例配置。4. 信号控制与检测器的联动配置4.1 信号配时文件的结构SUMO 的信号灯逻辑定义在.net.xml里netconvert 会自动生成一套默认配时通常是四相位或者根据进口道数量自适应。要修改的话有两种办法直接改 net 文件里的tlLogic标签或者写一个 additional 文件覆盖。后者更推荐因为 net 文件是自动生成的重新转换一次你的修改就没了。additional 文件里这样写additional tlLogic idJ1 typestatic programIDcustom offset0 phase duration30 stateGGGrrrGGGrrr/ phase duration3 stateyyyrrryyyrrr/ phase duration25 staterrrGGGrrrGGG/ phase duration3 staterrryyyrrryyy/ /tlLogic /additionalstate字符串的长度等于该路口所有受控车道的数量每个字符对应一条车道的灯色G 是绿、y 是黄、r 是红小写 g 表示该方向绿灯但需要让行。这个字符串很容易数错位数我一般先在 netedit 里看自动生成的相位复制过来再改比自己数靠谱。相位顺序和时长的确定取决于你的控制策略。固定配时就用实测的最佳周期和绿信比要做自适应控制就先把一个基础的周期配好运行中通过 TraCI 动态调整相位时长。4.2 检测器的布设与输出检测器是仿真和现实对照的桥梁。SUMO 提供了三类感应检测器用哪类取决于你要什么数据。E1 是单点线圈布在车道某个位置输出通过该断面的车辆数、平均速度、占有率最接近现实中的感应线圈。E2 是区段检测器覆盖一段车道能输出区段内的车辆数、平均速度、排队长度适合做排队评估。E3 是出入口检测器用于统计进入和离开某个区域的流量适合做区域交通量统计。配置写法如下放在 additional 文件里。additional inductionLoop iddet_1 laneE1_0 pos120 period60 filedet1_out.xml/ laneAreaDetector idarea_1 laneE1_0 pos0 endPos200 period60 filearea_out.xml/ /additionalperiod是采样周期单位秒。做流量分析 60 秒够用做信号优化可能要 15 秒甚至更细。采样周期设得太细会导致输出文件巨大一个几百个检测器的小时级仿真能轻松产出几百兆的 XML。注意检测器的 pos 是相对于车道起点的位置起点是车辆进入该车道的方向。放在路口停车线前 2 到 5 米是通行能力统计的常规位置放在路段中部测的是自由流速度位置选错数据含义完全变了。4.3 用 TraCI 动态改配时静态配时只能验证既定方案真正有意思的是运行中动态调整。核心 API 是traci.trafficlight.setPhase和setPhaseDuration。import traci traci.start([sumo, -c, sim.sumocfg]) step 0 while step 3600: traci.simulationStep() step 1 # 每 5 秒检查一次 if step % 5 0: queue traci.lane.getLastStepHaltingNumber(E1_0) if queue 8: current traci.trafficlight.getPhase(J1) # 延长当前绿灯相位 logic traci.trafficlight.getAllProgramLogics(J1)[0] traci.trafficlight.setPhaseDuration( J1, logic.phases[current].duration 5 ) traci.close()这里有个容易踩的点setPhaseDuration只对当前正在进行或者下一个将要执行的相位有效而且设置的时长会在该相位结束后失效下一轮又回到程序里定义的时长。如果你的控制逻辑是每周期都要重新计算就必须每个周期都调用一次不能指望设一次就一直生效。另外traci.trafficlight.getAllProgramLogics每次调用都会从仿真内核拉取完整配时信息在循环里高频调用会有性能开销建议在启动时读取一次缓存起来后续只读相位编号。5. TraCI 接口的完整实操与数据落地5.1 连接机制与时序关系TraCI 本质是一个 socket 通信协议Python 脚本作为客户端SUMO 作为服务端。两者是异步的脚本发出指令后仿真并不立即执行而是等你调用simulationStep()时才推进一个时间步把这段时间内收到的指令一起生效。这个机制决定了你的控制逻辑必须在simulationStep的间隙里做决策。典型结构是一个主循环每次步进之后读取状态、计算决策、下发控制再进入下一次步进。import traci sumo_cmd [ sumo, -c, sim.sumocfg, --step-length, 0.5, --tripinfo-output, tripinfo.xml, --summary-output, summary.xml, --no-warnings, true, ] traci.start(sumo_cmd) veh_count 0 while traci.simulation.getMinExpectedNumber() 0: traci.simulationStep() if traci.simulation.getTime() % 60 0: for lane_id in [E1_0, E2_0, E3_0]: flow traci.lane.getLastStepVehicleNumber(lane_id) speed traci.lane.getLastStepMeanSpeed(lane_id) occ traci.lane.getLastStepOccupancy(lane_id) print(f{traci.simulation.getTime():.0f}, f{lane_id},{flow},{speed:.2f},{occ:.2f}) traci.close()getMinExpectedNumber()返回的是还在仿真里或者还没出发的车辆总数它归零就说明仿真可以结束了。这个判断比固定跑够多少步要合理能避免车还没跑完就强行关停。--step-length 0.5把步长设为 0.5 秒精度更高但速度减半。一般交通流分析 1 秒步长足够涉及安全距离计算或者快速反应的自动驾驶算法建议 0.1 到 0.2 秒。5.2 关键 API 速查与使用场景TraCI 的 API 很多实际项目里高频使用的其实就那几十个我按用途整理一下。API获取内容典型用途traci.vehicle.getPosition车辆 x y 坐标联合仿真位置同步traci.vehicle.getSpeed瞬时速度速度分布统计traci.vehicle.getRoadID所在道路 ID区域流量统计traci.vehicle.getAccumulatedWaitingTime累计等待时间延误评估traci.lane.getLastStepVehicleNumber上一步车道车辆数流量统计traci.lane.getLastStepOccupancy车道占有率拥堵判别traci.edge.getLastStepMeanSpeed路段平均速度速度云图traci.simulation.getTime当前仿真时间时序记录traci.simulation.getLoadedIDList已加载车辆 ID批量操作traci.vehicle.moveTo强制移动车辆干预测试getPosition在联合仿真里用得最多它返回的是米制平面坐标直接就能映射到另一个仿真器的坐标系里。getAccumulatedWaitingTime是累计值跟驰模型判定速度为低速时开始计时评估路口延误时比单纯看行程时间更准确。高频调用这些 API 会有开销。一个经验值是如果你的仿真里有一万辆车每秒对全部车辆调用一次getPosition仿真速度会掉到实时的一倍以下。做法是按需采样或者只对关注区域的车辆做高频读取其他车辆用统计量代替。5.3 输出文件的解析与二次加工仿真跑完你得到的是tripinfo.xml、summary.xml这些 XML。直接用文本编辑器看意义不大得转成表格。我一般写一个小脚本一次性处理。import xml.etree.ElementTree as ET import pandas as pd def parse_tripinfo(path): root ET.parse(path).getroot() rows [] for t in root.findall(tripinfo): rows.append({ id: t.get(id), depart: float(t.get(depart)), duration: float(t.get(duration)), routeLength: float(t.get(routeLength)), waitingTime: float(t.get(waitingTime)), timeLoss: float(t.get(timeLoss)), avgSpeed: float(t.get(routeLength)) / max(float(t.get(duration)), 1e-6), }) df pd.DataFrame(rows) return df df parse_tripinfo(tripinfo.xml) print(df[[duration, waitingTime, timeLoss]].describe()) df.to_csv(trips.csv, indexFalse)timeLoss是 SUMO 直接给出的关键指标表示车辆因为跟车、信号、拥堵而损失的时间等于实际行程时间减去以期望速度自由行驶所需时间。这个指标比行程时间更适合做方案对比因为它剥离了道路长度的影响可以直接跨路段比较。检测器的输出也可以用类似的方式解析按时间和检测器 ID 展开成宽表然后就能画流量曲线、算流量守恒、做和实测数据的相关性分析了。6. 常见故障与实测避坑清单6.1 启动阶段的报错速查仿真跑不起来的时候九成的错误集中在这几类。我整理成一张表遇到直接对号入座。报错信息关键词根因处理办法No connection between edge车道没有连通netedit 补连接或加 connection 补丁Vehicle ... has no valid route起讫点不在路网上用 duarouter 的 repair 重建路径Edge ... is not known引用了不存在的边 ID核对 net 文件里的边命名Teleporting vehicle长时间堵死被传送加--time-to-teleport调大或排查号志死锁sumo-gui: command not found环境变量未设检查 SUMO_HOME 和 PATHTraCI connection refused端口被占用或版本不匹配换端口或对齐 traci 与 sumo 版本Teleporting vehicle是新手最容易被吓到的一个。它的意思是某辆车堵在路口太久超过了传送阈值仿真器把它瞬移到了下一条路段。这不是崩溃但会扭曲统计结果。如果这辆车是被信号灯卡住的说明配时有问题如果是被前车堵死的可能是跟驰参数太保守或者路网里出现了环形死锁比如四辆车在无信号路口互相让行谁都不动。后一种情况在低流量无信号路口反而更容易出现解决办法是加优先规则或者干脆设成让行控制。版本不匹配也是个高频坑。pip 装的 traci 版本必须和 sumo 可执行文件版本一致跨大版本经常出现协议不兼容表现为连接建立后立刻断开或者 API 调用返回空值。6.2 性能优化的几个关键开关仿真慢得受不了的时候先别急着升级硬件这几个参数往往能立竿见影。--no-warnings true关掉警告输出大批量仿真时能省不少 IO。--no-step-log true关掉逐步日志。--threads N在多核机器上开启并行计算对大型路网有效但注意不是所有模块都支持并行实测加速比通常到不了核心数那么多。--step-method.ballistic用更高效的积分方法精度略降速度明显提升。真正的大头在路网规模。如果你的仿真动辄跑几个小时先看看是不是导入了太多无关道路。把高速公路的应急车道、辅路、停车场内部道路都去掉边数能砍掉三成速度提升同样量级。另外检测器不要见缝就布只在你真正要分析的位置设每一个检测器每一步都要做一遍状态统计几百个检测器的开销不容忽视。6.3 我在实际项目里踩过的坑第一个坑是随机种子。SUMO 默认每次运行的随机数序列不同同一份配置跑两遍结果能差出百分之几。做方案对比的时候这种随机差异会淹没真实的方案差异。务必在配置里显式设置random_numberseed value42//random_number并且对比不同方案时用同一组种子。如果要做多次重复实验取均值就准备一组固定种子每个方案都跑这组种子做配对比较。第二个坑是检测器的时间对齐。检测器的period是从仿真开始时刻算起的固定窗口如果你的仿真预热时间和统计时间没有分开前几分钟的数据会被初始化效应污染所有车辆都还没铺满路网流量偏低。做法是设一个预热期前几百秒不统计或者把输出分段只取稳定后的那段。第三个坑是 TraCI 里读取的数据是上一步的。getLastStep系列 API 返回的是上一个仿真步结束时的状态不是当前时刻的实时状态。你在simulationStep之后立刻读取拿到的是刚结束的那一步的数据这个时间差在做高频控制的时候必须考虑进去否则控制会有一步延迟。第四个坑是路径文件膨胀。用 randomTrips 生成一小时几千辆车再经 duarouter 分配routes 文件能到几十兆。如果是长时段仿真建议按时间切片分段生成或者用 flow 代替逐车 trip流量大的时候文件能小一个数量级仿真加载也快得多。6.4 和其他仿真平台协同的注意事项有些项目需要把交通流和别的仿真器联动比如车辆动力学模型、外部控制算法、可视化前端。协同的通用做法是让 SUMO 通过 TraCI 暴露车辆状态外部程序按固定的仿真步长同步读取和回写。这里最关键的是时间同步策略。两边步长不一致的时候需要定义谁来驱动时钟。常见方案是外部程序驱动SUMO 每步等外部指令或者 SUMO 驱动外部程序被动跟随。前者容易实现但外部程序必须足够快跟不上就会拖慢整体后者实时性好但外部程序只能在下一次采样时介入控制频率受限。另外注意坐标系的转换。SUMO 用的是投影后的平面坐标其他平台可能用经纬度或者局部坐标。转换矩阵要在初始化时算好每一步都做一次完整投影计算会很慢。还有车辆的朝向角SUMO 给的是弧度制且以正北为零度很多平台用的是不同的基准直接拿来用会得到车辆横着跑的诡异画面。整套流程走下来从导入路网到跑出可分析的数据一个中等规模的片区大概要花两三天调试。时间主要花在路网清洗和需求校准上控制逻辑本身反而写得很快。我的建议是先把数据链路打通哪怕用随机流跑一个粗糙的结果也要确认从仿真到数据的通路是完整的然后再逐步替换成真实数据去提高精度。顺序反了的话你会花大量时间在清洗一份还没被验证过的路网上。
网站建设高端定制企业官网