ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SUMO交通仿真实例:路网、车流与信号配时完整解析

SUMO交通仿真实例:路网、车流与信号配时完整解析

简介:这份资源是面向交通仿真初学者与交通工程方向学生的SUMO入门实例包,围绕四个无信号灯十字路口与多车流场景展开,帮助读者理解微观交通模拟的基本建模流程。压缩包共14个文件,约72KB,以XML配置为主,涵盖路网、车流、路由等核心定义文件,另含少量tr与文档类文件,便于对照阅读与修改。实例基于SUMO 0.19.0版本,通过分析ex4相关配置,可学习如何用XML描述道路网络、车辆流与交通规则,并观察车辆在无信号交叉口中的行驶行为。目前已有2178人学习下载,说明该实例在入门阶段具有较高参考价值。读者可借此掌握SUMO场景搭建、参数调整与模拟结果分析的基本方法,并在此基础上尝试引入信号控制、调整交通需求或优化规则,为后续交通系统研究与智能交通应用打下基础。

1. 从一份 SUMO 实例代码说起:路网、车流与信号配时到底怎么跑通

如果你刚接触交通仿真,打开 SUMO 官网看到一堆 XML 和 Python 脚本,大概率会愣住——文档写得很全,但真要从零跑出一个能看的场景,中间隔着一堆没写清楚的细节。我手里这份 SUMO 简单实例代码,就是干这个用的:它把路网生成、车流定义、信号配时、仿真执行这条链路完整串了一遍,代码量不大,但每个文件都对应仿真里一个绕不开的环节。适合两类人:一是交通工程方向的学生,需要快速搭一个能出图、能改参数的仿真环境;二是做路径规划或信号控制的开发者,想拿 SUMO 当验证平台,但不想在环境配置上耗太久。这份代码不涉及复杂路网,就是最基础的十字路口加几条车道,但正因为简单,反而容易看清每个参数到底在控制什么。

2. 拆开这份实例:路网、车流、信号三个文件怎么配合

2.1 路网文件 net.xml 的节点与边到底在描述什么

SUMO 的路网不是画出来的,是算出来的。你写一个 nod.xml 定义节点坐标,再写一个 edg.xml 定义哪些节点之间连边、几条车道、限速多少,然后用 netconvert 一跑,才生成最终的 net.xml。这份实例代码里,节点就四个,围成一个矩形路口,边一共八条,每个方向一进一出。看起来简单,但这里有个容易翻车的地方:节点坐标的坐标系是笛卡尔坐标,单位是米,不是经纬度。很多人第一次写的时候直接拿地图上的经纬度填进去,结果路网要么缩成一个点,要么拉成一条线。

<!-- nod.xml:四个节点围成十字路口 --> <nodes> <node id="A" x="0" y="0" type="priority"/> <node id="B" x="200" y="0" type="priority"/> <node id="C" x="200" y="200" type="priority"/> <node id="D" x="0" y="200" type="priority"/> </nodes>

这段代码里,type="priority" 表示无信号灯控制的路口,先让右方来车。如果你后面要加信号灯,这里得改成 traffic_light,否则信号配时文件加载不进去。x 和 y 就是平面坐标,200 米差不多是一个标准城市交叉口的间距,太小了车流会挤在一起,太大了仿真看起来像在跑空旷高速。

边文件 edg.xml 里要指定 from 和 to 节点,还有 numLanes 和 speed。这份实例给了每条边两条车道,限速 13.89 米每秒,也就是 50 公里每小时,城市道路常见值。这里有个参数叫 priority,默认是 -1,如果你不写,netconvert 会自动算,但算出来的优先级可能跟你预期不一样,导致某些方向的车死活不让行。我一般会显式写上,直行方向给高一点,转弯方向给低一点。

<!-- edg.xml:定义八条边,每个方向一进一出 --> <edges> <edge id="AB" from="A" to="B" numLanes="2" speed="13.89" priority="2"/> <edge id="BA" from="B" to="A" numLanes="2" speed="13.89" priority="2"/> <edge id="BC" from="B" to="C" numLanes="2" speed="13.89" priority="2"/> <edge id="CB" from="C" to="B" numLanes="2" speed="13.89" priority="2"/> <edge id="CD" from="C" to="D" numLanes="2" speed="13.89" priority="2"/> <edge id="DC" from="D" to="C" numLanes="2" speed="13.89" priority="2"/> <edge id="DA" from="D" to="A" numLanes="2" speed="13.89" priority="2"/> <edge id="AD" from="A" to="D" numLanes="2" speed="13.89" priority="2"/> </edges>

写完这两个文件,跑一条命令就能生成 net.xml:

netconvert --node-files nod.xml --edge-files edg.xml --output-file net.xml

如果报错说节点重复或者边找不到,先检查 id 有没有拼错,再检查 from 和 to 是不是都在 nod.xml 里定义了。netconvert 的报错信息还算清楚,但有时候它会自动修正一些错误,反而让你以为写对了,结果仿真跑起来车不动。所以生成之后最好用 sumo-gui 打开看一眼,路网形状对不对,车道数对不对,一眼就能看出来。

2.2 车流文件 rou.xml 里 departSpeed 和 departLane 的坑

车流文件是很多人卡住的地方。这份实例代码里定义了两条车流,一条从 A 到 C,一条从 B 到 D,都是直行,发车频率用 period 控制,每 2 秒发一辆。车型用的是默认的 passenger,长度 5 米,最大速度 13.89,跟路网限速一致。

<!-- rou.xml:两条直行车流,每 2 秒发一辆 --> <routes> <vType id="car" accel="2.6" decel="4.5" length="5" maxSpeed="13.89"/> <route id="routeAC" edges="AB BC"/> <route id="routeBD" edges="BC CD"/> <flow id="flowAC" type="car" route="routeAC" begin="0" end="600" period="2" departSpeed="max" departLane="best"/> <flow id="flowBD" type="car" route="routeBD" begin="0" end="600" period="2" departSpeed="max" departLane="best"/> </routes>

这里有两个参数值得单独说。departSpeed="max" 表示车辆以路网允许的最大速度发车,如果不写,默认是 0,车会从静止开始加速,路口通行效率会明显下降。departLane="best" 是让 SUMO 自己选一条最空的车道发车,如果你指定了具体车道号,比如 departLane="0",那所有车都挤在第一条车道上,另一条车道空着,仿真结果会很难看。我见过有人为了“控制变量”把所有车都固定在一条车道上,结果排队长度比实际高出一大截,这就是参数没选对。

period 和 vehsPerHour 是两种发车方式,period 是每隔多少秒发一辆,vehsPerHour 是每小时发多少辆。这份实例用的是 period,因为简单直观。但要注意,period 是均匀发车,实际交通流更接近泊松分布,如果你要做通行能力分析,最好换成 vehsPerHour 或者用 probability 参数做随机发车。

2.3 信号配时文件 tls.xml 的相位与时长怎么对应

信号配时是这份实例代码里最有价值的部分,因为它把相位、时长、黄灯这些概念都落到了具体数字上。文件里定义了一个信号灯,四个相位:南北直行、南北左转、东西直行、东西左转。每个相位有 duration 和 state,state 是一串字符,每个字符对应一条车道,G 表示绿灯,y 表示黄灯,r 表示红灯。

<!-- tls.xml:四相位信号配时 --> <tlLogic id="C" type="static" programID="0" offset="0"> <phase duration="30" state="GGrrGGrr"/> <phase duration="3" state="yyrryyrr"/> <phase duration="30" state="rrGGrrGG"/> <phase duration="3" state="rryyrryy"/> </tlLogic>

state 字符串的长度必须和路口连接数一致,写错了 SUMO 会直接报错。这份实例里每个方向两条车道,四个方向一共八条连接,所以 state 是 8 个字符。前两个字符对应南北直行,接着两个对应南北左转,然后东西直行,最后东西左转。如果你加了右转专用道,state 长度要相应增加,不然相位对不上,车会乱走。

offset 参数控制相位偏移,做绿波协调的时候会用到。这份实例里是 0,表示从仿真开始就进入第一个相位。如果你要做干线协调,把下游路口的 offset 设成上游路口行程时间,就能形成绿波。但要注意,offset 的单位是秒,而且只在 type="static" 时有效,动态信号控制不用这个参数。

3. 跑起来才算数:仿真执行、输出配置与结果查看

3.1 sumocfg 配置文件里必须写对的三个路径

SUMO 跑仿真需要一个配置文件,通常叫 sumocfg,里面指定路网文件、车流文件、信号配时文件和输出文件。这份实例代码里,sumocfg 写得很简洁,但每个路径都是相对路径,所以你必须从 sumocfg 所在目录执行命令,否则会报找不到文件。

<!-- sumocfg:仿真配置文件 --> <configuration> <input> <net-file value="net.xml"/> <route-files value="rou.xml"/> <additional-files value="tls.xml"/> </input> <time> <begin value="0"/> <end value="600"/> </time> <output> <tripinfo-output value="tripinfo.xml"/> <summary-output value="summary.xml"/> </output> </configuration>

input 里的三个文件路径最容易出问题。net-file 是 netconvert 生成的,route-files 是车流文件,additional-files 是信号配时文件。注意 additional-files 可以放多个文件,用逗号隔开,但信号配时文件必须放在这里,不能放到 route-files 里,否则 SUMO 会忽略它。我见过有人把 tls.xml 写到 route-files 里,结果信号灯一直不工作,车流堵成一锅粥,排查了半天才发现是文件放错了位置。

time 里的 begin 和 end 控制仿真起止时间,单位是秒。这份实例跑了 600 秒,也就是 10 分钟。如果你要看稳态交通流,建议至少跑 3600 秒,前 600 秒当预热,后面的数据才稳定。output 里的 tripinfo-output 记录每辆车的行程时间、等待时间、停车次数,summary-output 记录每个时间步的车辆数、平均速度、排队长度。这两个文件是后面做分析的基础,不写的话仿真跑完什么数据都没有。

3.2 命令行执行与 sumo-gui 可视化排查

配置文件写好后,用 sumo 命令跑无界面仿真,用 sumo-gui 跑可视化仿真。无界面适合批量跑数据,可视化适合排查问题。

# 无界面仿真,适合批量跑数据 sumo -c sumocfg.sumocfg # 可视化仿真,适合排查路网和车流问题 sumo-gui -c sumocfg.sumocfg

跑 sumo-gui 的时候,如果看到车在路口突然消失,或者车叠在一起,多半是路网连接没写对。netconvert 默认会生成连接,但有时候转弯连接缺失,车走到路口发现没有下一段路,就直接被移除了。这时候要在 netconvert 命令里加 --no-turnarounds 或者手动写 con.xml 补连接。另一个常见问题是车流发车时找不到车道,报错说 departLane 无效,这时候把 departLane 改成 "free" 或者 "random" 试试,别死磕 "best"。

仿真跑完后,tripinfo.xml 里每辆车都有 id、depart、arrival、duration、waitingTime、waitingCount 这些字段。如果你要看平均行程时间,用 Python 读一下就行:

import xml.etree.ElementTree as ET tree = ET.parse('tripinfo.xml') root = tree.getroot() total_duration = 0 count = 0 for trip in root.findall('tripinfo'): total_duration += float(trip.get('duration')) count += 1 print(f"平均行程时间: {total_duration / count:.2f} 秒")

这段代码把所有车的 duration 加起来除以车辆数,得到平均行程时间。如果你要分方向统计,就按 route 或者 departLane 分组,逻辑一样。注意 tripinfo 里的 duration 是秒,不是仿真步数,别搞混了。

3.3 输出文件里哪些指标值得看,哪些是噪音

tripinfo.xml 和 summary.xml 里字段很多,但真正有用的就那么几个。tripinfo 里我一般只看 duration、waitingTime、waitingCount 和 routeLength。duration 是总行程时间,waitingTime 是等待时间,waitingCount 是停车次数,routeLength 是行驶距离。这四个指标基本能反映路网通行效率。summary.xml 里看 meanSpeed、meanWaitingTime、halting 这几个,halting 是停车车辆数,排队长度直接看这个就行。

有些字段看起来有用其实很坑,比如 timeLoss,它把等待时间和减速损失都算进去了,数值偏大,不适合直接做对比。还有 CO2 排放和燃油消耗,SUMO 默认的排放模型比较粗糙,除非你专门标定了排放参数,否则这些数据只能看趋势,不能当绝对值用。我一般会把这些字段忽略掉,专注在行程时间和等待时间上。

4. 避坑与排查:这份实例代码最容易翻车的五个地方

4.1 路网生成报错“节点未定义”

现象:跑 netconvert 的时候报错说某个节点找不到,或者边文件里的 from/to 节点不存在。原因通常是 nod.xml 里节点 id 拼写错误,或者 edg.xml 里引用了不存在的节点。还有一种情况是 nod.xml 和 edg.xml 的编码不一致,一个用 UTF-8 一个用 GBK,中文注释会导致解析失败。解决方法是先检查 id 是否完全一致,包括大小写,再把两个文件的编码统一成 UTF-8,注释尽量用英文。

4.2 车流发车后原地不动

现象:仿真跑起来,车流发了车,但车停在原地不动,或者过很久才起步。原因多半是 departSpeed 没设成 max,默认是 0,车要从静止加速。另一个可能是 departLane 指定了错误的车道号,车发到了不存在或者被占用的车道上。解决方法是把 departSpeed 改成 "max",departLane 改成 "best" 或 "free",让 SUMO 自己选车道。如果还不行,检查路网限速是不是设成了 0,speed="0" 会导致车永远动不了。

4.3 信号灯不切换相位

现象:仿真跑起来,信号灯一直停在第一个相位,后面的相位不执行。原因通常是 tls.xml 没有放到 additional-files 里,或者 tlLogic 的 id 跟路网里的节点 id 不一致。还有一种可能是 state 字符串长度跟连接数不匹配,SUMO 解析失败后直接忽略了这个信号灯。解决方法是确认 tls.xml 在 additional-files 里,tlLogic 的 id 跟 nod.xml 里 type="traffic_light" 的节点 id 一致,state 长度用 netconvert 生成的 net.xml 里的连接数核对一遍。

4.4 仿真跑完 tripinfo.xml 是空的

现象:仿真正常结束,但 tripinfo.xml 里没有数据,或者只有几辆车。原因可能是仿真时间太短,车还没跑完就被截断了,或者 output 里没写 tripinfo-output。还有一种情况是车流发车时间在仿真结束之后,begin 设成了 600,end 也是 600,车根本没发出来。解决方法是把 end 设大一点,至少让所有车都能到达终点,同时确认 tripinfo-output 路径正确,文件有写入权限。

4.5 可视化界面卡顿或闪退

现象:sumo-gui 打开后界面卡住,或者跑几秒就闪退。原因通常是路网太大或者车辆太多,显卡扛不住。这份实例代码路网很小,一般不会卡,但如果你改了参数,把车辆数调到几千,sumo-gui 就会很吃力。解决方法是先用 sumo 无界面跑一遍确认逻辑没问题,再用 sumo-gui 看局部。如果非要看大规模仿真,把 GUI 的延迟调高,或者用 --start 参数从中间某个时间点开始看。

5. 从这份实例往外延伸:参数扫描与批量仿真的具体做法

这份实例代码跑通之后,最有价值的用法不是盯着一个场景看,而是拿它做参数扫描。比如你想知道信号周期从 60 秒变到 120 秒,行程时间怎么变,手动改 tls.xml 再跑一遍太慢,写个脚本批量跑才是正路。我一般用 Python 的 subprocess 调 sumo,每次改一个参数,跑完读 tripinfo.xml,把结果存到 CSV 里。

import subprocess import xml.etree.ElementTree as ET import csv results = [] for cycle in [60, 90, 120, 150]: # 修改 tls.xml 里的相位时长,这里简化处理,实际用模板替换 with open('tls_template.xml', 'r') as f: content = f.read() content = content.replace('DURATION', str(cycle // 4)) with open('tls.xml', 'w') as f: f.write(content) # 跑仿真 subprocess.run(['sumo', '-c', 'sumocfg.sumocfg'], check=True) # 读结果 tree = ET.parse('tripinfo.xml') root = tree.getroot() total_duration = sum(float(t.get('duration')) for t in root.findall('tripinfo')) count = len(root.findall('tripinfo')) avg_duration = total_duration / count if count > 0 else 0 results.append({'cycle': cycle, 'avg_duration': avg_duration}) # 写 CSV with open('results.csv', 'w', newline='') as f: writer = csv.DictWriter(f, fieldnames=['cycle', 'avg_duration']) writer.writeheader() writer.writerows(results)

这段代码的逻辑很简单:循环不同的信号周期,改 tls.xml,跑仿真,读平均行程时间,存 CSV。注意 tls_template.xml 里用 DURATION 占位,实际替换成周期除以相位数。这里假设四个相位均分周期,实际配时可能不是均分,你可以按比例改。跑完之后用 pandas 画个图,就能看出周期多长时行程时间最短。

还有一个进阶用法是加检测器。SUMO 支持 e1 检测器(感应线圈)和 e2 检测器(区域检测器),可以输出每个车道的流量、占有率、平均速度。这份实例代码没带检测器,但你可以自己在 additional-files 里加一个 det.xml,定义检测器位置和输出文件。检测器数据比 tripinfo 更细,适合做信号控制算法验证。我一般会在路口停车线前 10 米放一个 e1 检测器,采样周期设成 60 秒,输出流量和占有率,用来评估不同配时方案的效果。

最后说一个我踩过的坑:批量仿真的时候,如果 sumo 报错但 subprocess 没捕获,脚本会继续跑,结果后面全是空数据。所以 subprocess.run 一定要加 check=True,报错就停,别让错误累积。还有,每次跑完记得清空 tripinfo.xml,不然 SUMO 会追加写入,数据混在一起根本没法分析。从那以后我每次批量跑仿真前都强制删一遍输出文件,这个习惯帮我省了很多排查时间。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进