
前几天有个做交通咨询的朋友来问我SUMO 仿真环境搭到第三步为什么总觉得跑是跑起来了但结果不对劲——窗口里车在动输出文件也生成了可一旦有人追问这个交叉口的平均延误到底是怎么算出来的为什么同样的配置跑两遍数字不一样就很难答上来。这个问题非常典型。前两步我们把工具装好、把路网导进来、让车跑起来解决的是能不能跑第三步要解决的是跑得对不对、数据能不能用、结论经不经得起追问。这一步做完仿真才从演示变成了分析工具。这篇内容围绕SUMO 仿真环境的第三阶段搭建展开重点不是安装而是路网逻辑收尾、交通需求生成、信号与时序配置、输出口径统一、TraCI 外部接入以及批量实验与排错。适合已经把基础环境跑通、准备拿仿真结果写报告或做方案比选的人如果你连 net.xml 都还没生成建议先把前面的环节补上。下面所有步骤和参数都是我按实际项目里的做法整理的命令行和配置文件都尽量给到可以直接抄的程度同时会把为什么这么设讲清楚。1. 第三阶段真正要啃的硬骨头三类核心问题进入第三步之后最容易犯的错误是继续沿用第二步的心态——只要能跑通就算完成。实际上从能跑到跑得对之间隔着三堵墙路网的通行逻辑是否正确、交通需求是否和路网容量匹配、输出数据口径是否统一。这三件事任何一件没处理好后面做出来的延误、排队、通行能力全是假的而且假得很隐蔽因为软件不会报错。1.1 几何正确不等于通行逻辑正确把一张 OSM 地图或者 CAD 图纸转成net.xmlnetconvert 只保证了几何形状能连起来不保证连接关系符合真实通行规则。我见过太多案例交叉口内部该有的左转连接缺失车辆在路口前莫名减速两条同向车道在交叉口被生成了一个直行车道 → 左转车道的错误连接主路和支路之间没有设置让行支路车流直接切断主干道。这些问题在可视化里表现得很含蓄——车流速度偏低、局部排队长度异常——单看画面很难发现但放进拥堵分析里就是灾难。判断依据很简单打开plain.con.xml看一眼连接表。正常情况下一个四路交叉口每条进口道的每个车道应该都能找到对应的下游连接并且带方向标记s 直行、l 左转、r 右转、t 掉头。如果某个进口道只有直行连接或者左转连接只从最右侧车道发出那基本就是自动生成时判断错了。1.2 需求流量与路网容量不匹配跑出来的延误没有意义仿真里有一个很反直觉的现象需求超出容量太多时平均延误并不会无限增长而是会稳定在一个看起来还行的数值上。原因是被堵住的车根本没成功插入路网它们在等待队列里被丢弃或者延迟出发压根没进入统计。于是你看到的是延误不高、路网畅通实际上是路网入口处堆了一大堆没进去的车。所以第三步必须做一件事把车辆的插入失败数量当成一个核心指标来监控。summary输出里的waiting字段、statistic-output里的 teleport 统计都是判断这个问题的主要依据。我一般的经验阈值是——如果稳定运行阶段 waiting 车辆持续超过总需求车辆的 5%那这份需求就不能用来做延误对比需要先调整出发分布或者降低流量。1.3 输出口径不统一同一份仿真的两套数据对不上这个坑更隐蔽。比如你用summary里的平均车速去算行程时间又用tripinfo里的 duration 去算两边数值对不上开会时就解释不清。原因通常是口径不同summary统计的是当步所有在网车辆的快照平均值包含正在等待的车tripinfo统计的是完成行程的车辆的平均值不包含还没到达的车。统计期间也不一样一个是逐步统计一个是到达时一次性写入。解决方式是在动手之前先定一套指标口径把每个指标定义到从哪来、怎么算、包含谁。下面的表是我常用的口径约定直接照着用能省掉很多扯皮。指标推荐数据源计算口径常见误用平均行程时间tripinfo只统计已到达车辆duration字段拿 summary 的 meanTravelTime 直接对比平均延误tripinfotimeLoss字段或用 duration 减自由流时间把等待时间等同于延误排队长度edgeData / laneData按周期取waitingTime或 halting 统计用瞬时快照代替周期统计通行量edgeData每个统计区间跨断面的车辆数用插入数代替通过数路网平均速度summary逐步平均值再做时间平均直接取最后一步的值2. 路网收尾连接关系、转向禁行与优先权的三处硬伤前面已经说过路网逻辑的重要性这一节把具体怎么改讲透。netconvert 提供了大量参数来影响连接生成但绝大多数人只会用默认值结果是问题被一路带到仿真里。我的原则是能用参数解决的不手改必须手改的单独维护一个补丁文件保证下次重新转路网时改动不会丢。2.1 交叉口内部连接是怎么算出来的netconvert 生成连接时大致按几个步骤走识别哪些车道引向同一个下游边按车道位置和几何角度给每个潜在连接打分然后用一套启发式规则挑选可行的车道配对最后为每条连接计算方向和冲突关系。这套机制在标准十字路口表现不错但在畸形交叉口、匝道汇入、单行道与双向路混排的地方经常出错。几个真正有用的参数netconvert --sumo-net-file base.net.xml \ --plain-output-prefix plain \ --no-turnarounds true \ --junctions.corner-detail 5 \ --junctions.internal-link-detail 5 \ --check-lane-foes.all true \ --output-file net.net.xml--no-turnarounds true禁止掉头连接。城区路网里掉头通常只允许在特定位置让 netconvert 自动生成一堆掉头连接会显著改变通行能力。--junctions.corner-detail和--junctions.internal-link-detail控制路口内部形状点数量。这两个值调大能改善可视化里的转弯轨迹也会轻微增加计算量5 到 10 之间比较平衡。--check-lane-foes.all true强制做冲突检查。这个开着会让转换变慢但能提前暴露两条连接互相打架的问题。--plain-output-prefix把生成结果回写成明文 XML这是排查连接问题最关键的一步因为net.xml里的连接信息是压缩过的读起来很痛苦。提示转换完成后不要立刻开始跑仿真。先用一个极小的需求文件比如 10 辆车跑 60 秒配合 GUI 的显示车道连接功能逐个路口看一遍转弯轨迹。这一步花 20 分钟能省下后面几天的返工。2.2 车道数收缩与转向禁行带来的消失车道车道数变化是另一个高频出错点。一条 3 车道的城市道路在路口前变成 2 车道netconvert 默认会在收缩位置生成一个消失车道但它的处理策略和真实标线不一致现实中通常是提前几百米开始逐步合并而软件往往在路口前一小段才处理造成一个假的瓶颈。处理思路有两条。第一条是显式声明车道连接用connection补丁文件强制指定哪条车道连向哪条connections connection fromeast_in toeast_out fromLane0 toLane0/ connection fromeast_in toeast_out fromLane1 toLane1/ connection fromeast_in tonorth_out fromLane2 toLane0/ connection fromeast_in tosouth_out fromLane2 toLane1/ /connections第二条是在路网源头就把车道数改对比如在节点处提前设置numLanes让收缩发生在路段中间而不是路口。我一般优先选第二条因为补丁文件越多后期维护成本越高。转向禁行同理。现实中的禁止左转如果不在net.xml里声明仿真会生成左转车流这是最常见的路网失真来源之一。禁行的表达方式是在connections里对该方向不写任何连接而不是写一个禁止标记——netconvert 的逻辑是没声明就是允许自动生成。所以如果要做禁行必须同时用--no-turnarounds配合显式连接表把路口的所有连接关系完全接管过来。2.3 让行、优先权与 keep-clear 的显式声明在没有信号灯的路口谁让谁完全由路网的优先权决定。netconvert 会通过边类型priority属性和车道数推断主次关系但推断结果经常和实际不符。判断方法是看仿真中支路车流是否直接汇入主路——如果是说明支路被赋予了过高优先权。需要关注的两处边的 priority 属性主干道通常设为 10 以上支路设为 1 到 3。这个值直接影响让行判定和连接生成。路口类型type属性priority表示无信号让行路口right_before_left表示全向停让traffic_light表示信号控制。这几个值写错整个路口的行为逻辑就完全变了。还有一个容易被忽略的keep-clear设置。它控制车辆是否允许在路口内停留影响的是路口溢出这一现象的仿真真实性。当你的路网里存在短距离路口串联比如两个路口之间只有 30 米时开启 keep-clear 能避免车辆堵在路口中间造成死锁。参数是--default.junctions.keep-clear true也可以按路口单独设置。2.4 路网自查从明文文件回读到一致性检查我现在的固定流程是转换 → 回读明文 → 肉眼扫连接 → 小需求试跑 → 通过后才进入需求生成阶段。回读命令就是 2.1 里那条--plain-output-prefix生成的文件包括plain.nod.xml节点用于检查路口类型和坐标plain.edg.xml边用于检查车道数、限速、优先级plain.con.xml连接用于检查转向逻辑plain.tll.xml信号配时用于检查相位是否合理plain.typ.xml类型定义用于检查道路分级这四个文件加起来不到 1000 行一个中等规模的城区路网 20 分钟能扫完。重点看三类异常连接数明显偏少的进口道、限速为 0 或异常高的边、以及被意外生成为信号路口的位置。注意--plain-output-prefix生成的明文文件可以直接修改后再用 netconvert 重新编译回 net.xml这是官方支持的往返编辑流程。比起直接改 net.xml这种方式的改动是可追溯、可复现的。3. 交通需求从哪来手写、随机生成与 OD 反推三条路路网收尾之后就是需求。很多人这一步做得很随意随便写几十辆车跑一跑然后拿结果去对比方案。这是最浪费的一步——路网修得再好需求不对结论就是空的。需求生成有三种主流做法适用场景完全不同下面分别说。3.1 手写 route 文件验证逻辑够用做评估不够手写需求的最大价值不是评估而是验证。几十辆车、固定的出发时间、明确的路径用来检查某个路口的行为是否符合预期非常高效。结构大致是这样routes vType idcar vClasspassenger length4.8 accel2.6 decel4.5 sigma0.5/ route idr_east_west edgeseast_in east_mid west_out/ flow idf_main typecar router_east_west begin0 end3600 vehsPerHour600/ /routes这里有几个细节值得强调。vehsPerHour、period、probability三个属性是互斥的只能用一个。vehsPerHour是固定间隔发车period是它的倒数形式周期秒数probability是每一步按概率决定是否发车后者产生的到达分布更接近随机流。做对比实验我一般用vehsPerHour因为不同方案使用的需求完全一致便于控制变量做鲁棒性测试则用probability配合不同随机种子。还有一个很多人不知道的用法SUMO 支持直接在需求文件里写trip而不用提前算好路径trip idt1 typecar depart12.5 fromeast_in towest_out/仿真运行时会自动为其算路。这在小规模验证阶段特别方便缺点是大规模运行时每次动态算路会拖慢速度而且路径不一定符合真实车流的分布。3.2 randomTrips.py 参数组合与三个常见误用randomTrips.py是 SUMO 工具包里最好用的脚本之一位于SUMO_HOME/tools/目录。基本调用python $SUMO_HOME/tools/randomTrips.py \ -n net.net.xml \ -r routes.rou.xml \ -b 0 -e 3600 \ -p 1.2 \ --fringe-factor 10 \ --min-distance 300 \ --trip-attributes typecar departLanebest departSpeedmax \ --validate参数说明和踩坑点-p是平均发车间隔秒值越小流量越大。注意它是平均间隔实际出发时间带随机扰动不是等间隔。--fringe-factor控制起终点落在路网边缘的倾向。默认值 1做穿越型交通时通常要调到 5 到 20否则大量车辆会在路网内部绕短圈。--min-distance过滤掉过短的行程。默认 0 会导致一堆车只走三四十米就到达把平均行程时间算得极低。这个参数不设数据基本没法用。--validate会检查生成的行程是否能走通建议一直开着。--seed固定随机种子做对比实验时必须固定。三个高频误用第一只给-o不给-r结果是得到一堆 trip 文件却以为已经生成路径第二忘记指定--trip-attributes所有车用默认车型和默认车道选择策略不符合真实驾驶行为第三流量算错——很多人把-p当成每秒发车数实际它是每辆车平均间隔秒数搞反了会差几十倍。3.3 od2trips 与 duarouter从 OD 矩阵到可执行路径如果你手上有实际调查得到的 OD 矩阵比如小区之间的出行量randomTrips 就不适用了应该走od2trips加duarouter这条链路。第一步是定义交通小区TAZ用节点的形式列出每个小区包含哪些边tazs taz idzone_A edgeseast_in_1 east_in_2/ taz idzone_B edgeswest_out_1 west_out_2/ /tazs第二步准备 OD 矩阵格式是起点、终点、车辆数三列用制表符分隔zone_A zone_B 600 zone_B zone_A 540 zone_A zone_A 120第三步转换python $SUMO_HOME/tools/od2trips.py \ -n net.net.xml \ --taz-files zones.taz.xml \ -d od_matrix.txt \ -o trips.trips.xml \ --begin 0 --end 3600 \ --spread.uniform true--spread.uniform表示把区间内的车辆均匀铺开不设的话所有车会在begin时刻集中出发造成一个假的冲击流量。转换完之后还要用 duarouter 算路duarouter -n net.net.xml -r trips.trips.xml -o routes.rou.xml \ --ignore-errors true \ --repair true \ --weights.random-factor 1.5 \ --seed 42--ignore-errors和--repair用来处理那些起终点本身无法连通的行程不加的话一个坏行程会让整个转换中断。--weights.random-factor给路段权重加随机扰动让不同车辆在同一 OD 下选择略有差异的路径比所有车走同一条路更接近真实。3.4 vType 不定制等于用同一款车跑完整个城市最后一个被严重低估的环节是车型定义。默认车型的加速度、车头时距、跟车模型系数都是通用值用来做方案比选时问题不大但如果你的场景涉及公交、货车、非机动车混行就必须定制。我常用的几个关键属性属性含义经验取值length车长米小汽车 4.5-5.0公交 12货车 8-16accel/decel加减速度m/s²小汽车 2.6/4.5公交 1.2/3.0sigma驾驶不完美度0.5 默认越小越理想speedFactor期望速度系数正态分布 normc(1,0.1,0.2,2)minGap停车时最小间距小汽车 2.5公交 3.0carFollowModel跟车模型Krauss 通用IDM 更平滑lcStrategic换道意愿默认 1拥堵场景可调高speedFactor用分布而不是固定值这一点很关键。如果所有车的期望速度完全一样仿真里会出现非常整齐的车队通行能力被高估。用正态分布让每辆车有自己的速度偏好仿真结果会明显更接近实测。4. 信号控制与仿真时序让结果失真的那些细节需求配好之后很多人就直接跑了忽略了两个同样重要的参数组信号配时和仿真时序。这两块的错误不会导致报错但会让结果系统性偏离。4.1 自动生成配时方案的边界在哪里netconvert 可以通过--tls.guess true自动猜测哪些路口需要信号灯并通过--tls.guess-signals识别路网中已有的信号标记。自动生成的配时方案特点是相位数量最少、绿信比按车道数大致分配、周期固定。对于一个标准十字路口这套方案能跑但绝对不适合做信号优化对比——因为它本身就没有优化过。需要干预的情况有三种路口有多个转向阶段需要保护左转必须手工拆相位、路口是畸形交叉口自动相位可能产生冲突、以及需要设置协调控制绿波带必须手工配 offset。手工配时的基本结构additional tlLogic idJ1 typestatic programIDcustom offset0 phase duration30 stateGGGrrrGGGrrr/ phase duration3 stateyyyrrryyyrrr/ phase duration2 staterrrrrrrrrrrr/ phase duration25 staterrrGGGrrrGGG/ phase duration3 staterrryyyrrryyy/ phase duration2 staterrrrrrrrrrrr/ /tlLogic /additionalstate字符串的每一位对应一个受控连接linkIndex顺序由路网决定不能随便猜。正确做法是从plain.tll.xml里读出每个 linkIndex 对应的连接再按顺序写字符串。这一步写错的话相位和实际车流完全对不上但仿真不会报错非常危险。4.2 黄灯、全红与清空时间state 字符串里每个字符的含义state 字符串里不只有 G、y、r 三种实际支持的有九种理解它们才能写出正确的相位字符含义G绿灯有优先通行权g绿灯需让行保护左转未受保护时常用y黄灯Y黄灯有优先通行权r红灯u红灯加制动提示用于清空o黄灯加制动提示O黄灯优先通行加制动提示s停止让行用于特殊控制黄灯时长的经验值是按 3 秒设但如果路口限速高、进口道宽需要用清空时间公式校验黄灯时长 ≈ 反应时间1 秒加 车速除以二倍减速度。限速 60 km/h约 16.7 m/s、减速度取 3 m/s² 时清空时间约 1 16.7/6 ≈ 3.8 秒。这时候还按 3 秒设就会出现车辆在红灯时仍在路口内的情况仿真的路口通行能力会被高估。全红时间按路口对角线长度除以低速约 10 m/s估算。30 米的交叉口大约需要 3 秒不要为了追求通行能力把全红设成 0那样路口内的冲突会以车辆互相穿过的形式表现出来统计上完全失真。4.3 步长、种子与预热时间仿真步长默认 1 秒这个值对城市交通够用但涉及加减速细节、行人过街、非机动车道行为时0.1 到 0.5 秒的步长更合适。代价是计算时间成倍增长1 小时仿真 0.1 秒步长意味着 36000 步比 1 秒步长慢十倍。sumo -c sim.sumocfg \ --step-length 0.5 \ --seed 42 \ --begin 0 --end 7200 \ --no-step-log true \ --duration-log.statistics true随机种子必须固定这是做对比实验的前提。默认种子是 23423但很多人不知道这一点做方案对比时没有显式指定结果两个方案的差异里混进了随机性。预热时间这件事我的做法是把仿真时长设成预热时长加分析时长然后在输出统计里用时间窗口过滤掉前段。比如预热 600 秒、分析 3600 秒总时长设 4200 秒edgeData的begin设为 600。这样比用负数起始时间更稳妥因为很多后处理脚本对负时间不友好。4.4 车辆插不进去waiting 与 teleport 的排查顺序仿真跑完之后如果发现waiting数量一直很高或者日志里出现 teleport 警告按下面的顺序排查看出发时刻的流量峰值。如果所有车都在begin时刻出发入口瞬间过载。用--spread.uniform或按时间分段铺开。看入口车道数和发车密度是否匹配。一条入口车道理论上每秒最多通过约 0.5 辆车按 1800 辆/小时算如果设置的发车强度超过这个值插不进去是必然的。看 departSpeed 设置。departSpeedmax要求插入位置有足够的安全间隙密集车流下会大幅降低插入成功率。改成departSpeeddesired或适当降低速度更容易插入。看 departLane 策略。departLanebest会选最优车道但在多车道入口上可能导致某条车道过载可以试free或random。最后看 teleport 参数。默认等待 300 秒后车辆会被传送这个机制本来是防止网格锁死但会把真实的拥堵数据抹掉。做拥堵分析时我一般把它调大--time-to-teleport 900甚至关闭同时接受仿真变慢的代价。提示statistic-output里的teleports统计必须每次跑完都看一眼。teleport 数量超过总车辆的 1%基本可以判断这份结果不适合做拥堵分析。5. 输出配置把四类数据一次配明白第三步最后一个技术环节是输出。这一步做得好不好直接决定后面能不能出结论。我的建议是完全放弃命令行堆参数的做法改用配置文件加 additional 文件的方式好处是配置可复用、可版本管理、可批量实验。5.1 用 additional 文件组织输出而不是堆命令行完整配置分成三层主配置.sumocfg、路网输入、额外的信号与输出定义。configuration input net-file valuenet.net.xml/ route-files valueroutes.rou.xml/ additional-files valuetls.add.xml,output.add.xml/ /input time begin value0/ end value4200/ step-length value1/ /time processing time-to-teleport value900/ ignore-route-errors valuetrue/ /processing report no-step-log valuetrue/ duration-log.statistics valuetrue/ /report /configuration而output.add.xml里集中放周期统计和边数据定义additional edgeData ided_hour fileedgeData.xml begin600 end4200 excludeEmptytrue withInternaltrue/ edgeData ided_5min fileedgeData_300.xml period300 begin600 end4200 excludeEmptytrue/ /additional三层分离的好处是换需求只改route-files换输出策略只改 additional做批量实验时用命令行覆盖单个参数即可。SUMO 的参数优先级是命令行大于配置文件所以批量实验完全不需要改文件。5.2 四类输出文件的字段与适用场景输出开启方式粒度主要用途summary--summary-output每仿真步宏观趋势、在网车辆数、平均速度随时间变化tripinfo--tripinfo-output每辆车到达时行程时间、延误、停车次数方案对比核心数据edgeDataadditional 定义每统计周期断面流量、平均速度、行驶时间、排队做路段分析fcd--fcd-output每仿真步每车轨迹数据用于可视化动画、微观行为分析tripinfo的几个字段必须搞清楚含义因为报告里最常引用的就是它字段含义使用注意duration从出发到到达的总时间包含等待时间timeLoss相对自由流行驶的损失时间更接近延误报告优选waitingTime速度低于阈值的累计时间不等于延误只是低速时长departDelay实际出发与计划出发的差值大于 0 说明插入受阻routeLength实际行驶距离可用于校验路径是否异常vaporized是否被中途移除出现该标记的车辆要单独剔除做方案对比时我会同时输出tripinfo和edgeData。前者给全局指标后者给路段级指标两套数据相互印证。如果发现tripinfo显示延误下降但edgeData显示某几个路段排队变长说明改善是局部的不能一概而论。5.3 分段统计与时间窗口edgeData的period属性决定统计区间长度。做拥堵演化分析时用 300 秒一个区间做全天流量统计时用 3600 秒。有一个细节很容易被忽略excludeEmptytrue会跳过没有车辆通过的路段如果不设输出文件里会出现大量零值记录后处理时容易把没数据当成流量为零。fcd文件体积非常大1 小时、每小时 2000 辆车的仿真输出可能有几百 MB。用的时候一定要限制时间窗口或者只对特定车辆开启fcd-export filefcd.xml begin1800 end2400 vTypesbus/只导出公交车辆的轨迹文件能小两个数量级分析公交专用道的效果反而更清楚。5.4 后处理把 XML 变成能看懂的指标SUMO 输出的都是 XML直接看很痛苦。我一般用 pandas 加 ElementTree 两步处理先把 XML 转成 DataFrame再做分组聚合。核心思路如下import xml.etree.ElementTree as ET import pandas as pd tree ET.parse(tripinfo.xml) rows [e.attrib for e in tree.getroot()] df pd.DataFrame(rows).astype({duration: float, timeLoss: float, depart: float, waitingTime: float}) df df[df[vaporized].isna()] if vaporized in df else df df[period] pd.cut(df[depart], binsrange(600, 4300, 600)) summary df.groupby(period).agg( veh(duration, size), avg_duration(duration, mean), avg_timeloss(timeLoss, mean))注意vaporized的过滤逻辑——被中途移除的车辆没有正常到达留在统计里会拉低平均行程时间。这个细节不做数据就有系统性偏差。另外提醒一句edgeData里同时有traveltime和speed两个字段前者是整个统计周期内通过该路段车辆的平均行驶时间后者是周期内的平均速度。做拥堵分析用traveltime更稳定因为它不依赖采样时刻做速度分布分析才用speed。6. TraCI 接入让外部程序真正控制仿真如果你只需要跑固定场景出数据到第 5 节就可以收工了。但大多数实际项目需要仿真与外部逻辑联动动态信号控制、车辆路径重规划、实时数据注入、外部算法对比测试。这时候必须用 TraCI。6.1 remote-port 与 traci.start 的握手TraCI 的工作方式是把 SUMO 当成一个服务进程启动外部程序通过 TCP 连接发指令。启动方式有两种我推荐显式指定端口sumo -c sim.sumocfg --remote-port 8813 --num-clients 1然后在 Python 里连接import traci traci.start([sumo, -c, sim.sumocfg, --remote-port, 8813, --no-step-log, true]) for step in range(3600): traci.simulationStep() t traci.simulation.getTime() if step % 300 0: print(t, traci.simulation.getLoadedIDList().__len__(), traci.simulation.getArrivedNumber()) traci.close()--num-clients在多进程并行控制时需要显式指定。端口被占用是新手最常遇到的错误排查方法就是换端口不要浪费时间在环境上。6.2 simulationStep 与 step 的语义差别这是 TraCI 里最容易被误解的一对调用。simulationStep()让仿真推进一个步长默认 1 秒同时把该步内的订阅数据刷新回来。而simulationStep(target_time)带参数时会一直推进到目标时间点中间的所有步结果被跳过——如果你在中间需要做控制决策用带参数的版本会丢失大量决策机会。所以控制类应用应该用无参版本逐 step 循环批量数据采集才用带目标时间的版本。这一点搞错的话效果是控制逻辑反应迟钝因为你实际上只在每 N 秒有机会干预一次。另一个关键点TraCI 的所有查询返回的都是上一步结束时刻的状态。也就是说在step循环里traci.simulation.getTime()返回的是刚结束的那一步的时间不是即将开始的那一步。做控制时如果按当前时间做判断、按下一时刻执行逻辑一定要理清否则会出现用未来数据做当前决策的错位。6.3 订阅机制为什么循环里不要逐车取值很多人写 TraCI 代码时习惯这样for veh in traci.vehicle.getIDList(): speed traci.vehicle.getSpeed(veh) pos traci.vehicle.getPosition(veh)每一次getSpeed都是一次 TCP 往返。上千辆车、几千步循环下来通信开销会成为绝对瓶颈仿真时间被通信拖长好几倍。正确做法是用订阅机制一次性拿回所有需要的数据import traci.constants as tc traci.vehicle.subscribe(veh_id, (tc.VAR_SPEED, tc.VAR_POSITION, tc.VAR_ROAD_ID, tc.VAR_LANE_INDEX)) results traci.vehicle.getSubscriptionResults(veh_id)订阅之后每次simulationStep()会自动把订阅变量刷新到本地缓存取值不再产生网络往返。实测下来千车规模下订阅方式比逐车查询快一个数量级。如果需要按时间窗口订阅比如只在某段时间采集可以给subscribe传begin和end参数超出窗口后自动停止刷新避免无谓的开销。6.4 联调最容易卡住的三个点第一加载顺序问题。TraCI 的模块加载有隐含依赖比如用了traci.vehicle再调traci.simulation有时会触发未初始化的错误。稳妥做法是在脚本开头就 import 需要用到的所有模块或者统一使用traci.start之后的第一次调用做一次完整探测。第二性能瓶颈在 Python 而不在 SUMO。用 libsumo 替代 traci 可以绕开 TCP把 SUMO 作为库直接加载速度提升明显代价是不支持分布式和部分高级功能。如果做单机大批量实验libsumo 是更好的选择import libsumo as traci traci.start([sumo, -c, sim.sumocfg])代码几乎不用改只需替换导入语句这也是我建议把 TraCI 调用集中封装的原因。第三异常退出导致进程残留。脚本报错时如果没执行traci.close()SUMO 进程会一直占着端口下一次连接直接失败。稳妥写法是用try/finally包住主循环或者用with语法。踩过几次之后我现在所有脚本都强制加 finally 关闭。7. 批量实验与性能调优跑得慢、跑不动、结果诡异到了这一步单个场景已经能跑通了剩下的问题是规模化和稳定性。这一节讲三件在真实项目里最耗时间的事。7.1 性能瓶颈定位哪些设置最吃时间SUMO 的性能问题通常来自四个地方按影响从大到小排因素影响优化方向输出文件过多、频率过高极大只保留必要输出缩短时间窗口仿真步长过小大城市交通 0.5-1 秒足够动态算路trip 未预处理中到大提前用 duarouter 算好路径路网规模与车道数中剔除分析范围外的路网一个 1000 车规模、1 小时时长、包含 fcd 输出的仿真可能比不开 fcd 慢五到十倍。做参数标定时批量跑几百次这个差异就是几小时和几天的区别。定位方法很简单先用--no-step-log true关闭逐步输出跑一次记录墙钟时间然后逐个开启输出观察时间变化。我一般的做法是保留tripinfo和edgeData这两个是必需品其他的按需临时开。7.2 批量实验配置文件的复用与参数覆盖做方案对比时几十个组合是常态。我的做法是写一个 shell 脚本固定配置文件用命令行覆盖关键参数用--output-prefix隔离结果for seed in 42 43 44; do for cfg in base caseA caseB; do sumo -c ${cfg}.sumocfg \ --seed ${seed} \ --output-prefix ${cfg}_s${seed}_ \ --no-step-log true done done--output-prefix会把所有输出文件加上统一前缀不需要为每个组合单独改配置文件后处理时按前缀匹配即可。这个参数是批量实验效率提升最明显的一个很多人不知道它的存在。另一个技巧是用--save-template生成一份包含全部可调参数和默认值的配置文件需要确认某个参数的官方名称和取值范围时直接在这份文件里搜比翻文档快得多。7.3 警告日志怎么读SUMO 的警告信息量很大但有价值的主要是这几类关于车辆插入失败的出现频率高说明需求配置或路网入口有问题。关于路径未找到的说明需求里存在不连通的起终点需要修 OD 或者修路网。关于 teleport 的说明局部出现死锁或长时间等待。关于车型重复定义的多个文件里定义了同名 vTypeSUMO 会用第一个并忽略后面的容易造成参数不生效。看日志的方法不是一行行读而是先统计各类警告的数量再挑数量最多的那类深入。命令上可以用2warn.log把标准错误重定向出去然后用grep -c分类计数。这一步做熟了一份日志三十秒就能定位问题。7.4 复现性同一份配置跑两次结果不同的原因最后说一个经常被质疑的点。同一份配置两次跑出来的平均行程时间不一样原因可能有四个第一随机种子没固定。默认种子虽然是固定的但如果配置里用了--random特性或者从系统时间取种子结果就会变。第二用了动态算路但没固定算路算法的随机性。duarouter 的权重扰动和算路顺序都可能带随机性需要在算路阶段就固定种子。第三输出时间窗口依赖的预热未真正完成。如果预热时间不够路网在第 600 秒时还没进入稳定状态那么第 600 秒到 4200 秒的统计里混入了非稳态数据两次的差异会被放大。第四统计口径本身包含了未完成行程的车辆。用过滤后的tripinfo而不是全部记录能显著降低这种波动。我的建议是做方案对比时每个方案至少跑三个不同种子取平均值并且把种子间的标准差一并写进报告。标准差太大比如超过均值的 10%说明当前需求配置本身的随机性过高需要先增加流量或者延长统计时长让结果收敛之后再比较方案。关于 SUMO 仿真环境第三阶段我个人最大的体会是真正的技术含量不在写配置文件而在于对每一个参数的为什么有清楚的认识。一个--min-distance没设可能让整个项目的行程时间指标偏低 30%一个state字符串顺序写错可能让信号优化方案的结论完全反转。这些坑软件都不会提示你只能靠提前知道。另外一个实用建议是把每次实验的完整配置、命令、种子和输出目录名记录在一个表格里看似麻烦等一周后有人问那个右转专用道的方案是哪个配置跑出来的时你会庆幸自己记了。