路网能跑通了、车也能动了,结果一看输出文件全是空的——这大概是每个用 SUMO 的人都会经历的第三阶段。前面两篇我们把 SUMO 装好、把路网从 OSM 或者手写节点的方式建出来了,net.net.xml躺在目录里看着挺像回事,可一旦开始跑仿真,要么路网上一辆车都没有,要么车全堵在路口表演行为艺术。这篇就专门讲这一段的破局思路:sumo 仿真环境从"能打开"到"能出数据"之间,到底缺了哪几块拼图。
这一篇的定位是系列里的"需求与运行"环节,核心围绕三件事展开:交通需求怎么从零生成、sumocfg配置文件到底该怎么写、仿真跑完之后那些 xml 输出怎么读。适合已经能把路网导入进来、但卡在"跑不出有意义结果"这一步的朋友,也适合想从 GUI 点点点升级到脚本批量化跑仿真的同学。整套流程我用下来是稳定的,但踩坑的地方相当多,尤其是需求生成脚本的参数和路由计算这两块,后面会重点展开。需要说明的是,文中涉及的参数取值和目录组织方式,是基于城市道路通行场景的常见实践补全的,你在具体项目里需要按自己的路网尺度微调。
1. 第三篇要解决的问题:从路网到仿真的最后一段路
1.1 前两篇留下的半成品到底缺什么
跑过前两篇的朋友应该有个共同体验:netconvert或netgenerate生成的net.net.xml是完整的,SUMO 能加载它、能画出来、能看到车道线和路口。但这时候你点下运行按钮,仿真时间在走,路上却一辆车没有。这不是软件坏了,而是 SUMO 的设计哲学决定的——路网和需求是彻底解耦的。
路网只描述"物理空间",也就是哪里有条路、这条路几条车道、限速多少、哪个路口和哪个路口相连。它完全不知道上面该有多少车、这些车从哪来、要到哪去。需求文件才负责描述"交通流",包括车辆从哪条边出发、走哪条路径、什么时候进入、开多快。这两者最后由一个配置文件粘在一起。所以缺的拼图就是两块:需求(车流)和配置(把路网、需求、时间、输出串起来的东西)。
很多人第一次用 SUMO 会想当然地觉得"路网建好了仿真就能跑",这个直觉来自其他一些集成度更高的仿真软件,它们把路网和交通流放在一个工程里管理。SUMO 走的是 UNIX 那套"每个工具只干一件事"的路线,好处是灵活、可脚本化、适合批量做场景,代价就是初次上手要理解这个拼装逻辑。我个人的体会是,一旦理解了这个解耦思想,后面做参数扫描和批量实验会非常舒服,因为你可以只换需求文件、复用同一个路网,跑几十组对比实验。
1.2 需求、路网、配置三者的依赖关系
先把依赖关系理清楚,后面所有操作都能对号入座。路网是基础,提供边(edge)的 id、车道数、连接关系(connection)。需求依赖路网,因为车辆出发时要说清楚在哪个 edge 上出现,路径也要沿着路网里真实存在的连接走,写错了 SUMO 直接报错说这条边不存在或者这两条边不连通。配置依赖前两者,它是仿真启动时的总入口,告诉 SUMO 去哪个文件加载路网、去哪个文件加载车辆、仿真从第几秒跑到第几秒、输出写到哪里。
这个依赖链有个很实际的推论:需求文件不能脱离路网单独生成。你没法先随便编一堆车的路径,再去找一个路网来配它。正确顺序永远是先有路网、再基于路网生成需求、最后写配置。我见过有同学从别处拷了一份routes.rou.xml过来,结果里面引用的 edge id 跟自己路网的完全对不上,一运行满屏报错,排查半天才发现是文件压根不配套。
还有一点容易被忽略:路口连接关系会影响路由可行性。比如路网里有条边 A 和边 B,物理上看着是接着的,但如果net.net.xml里的 connection 没定义 A 到 B 的转向,路由算法就不会给出经过 A→B 的路径。这个问题在手工编辑路网时特别常见,输出表现为"某几辆车找不到路径"或者"路径绕了很远"。所以生成需求前,建议先确认路网是连通的。
1.3 工程目录怎么组织才不返工
这一条是纯经验,教科书上不会写。我强烈建议在动手之前先把目录结构定下来,否则做到后面文件版本一多,你自己都分不清哪个是最新的。我的习惯是按阶段分目录,大致长这样:
project/ ├── net/ # 路网相关 │ └── net.net.xml ├── demand/ # 需求相关 │ ├── trips.trips.xml │ ├── routes.rou.xml │ └── vtypes.add.xml ├── config/ # 仿真配置 │ └── sim.sumocfg ├── output/ # 仿真输出 │ ├── tripinfo.xml │ ── summary.xml ── scripts/ # 脚本 └── run.py这样做有几个好处。第一,路网和需求分离,换需求做对比实验时不用动路网。第二,输出单独放一个目录,方便批量清理,跑完一轮删掉 output 里的内容就行,不会误删源文件。第三,脚本独立出来,后面要做参数扫描或者接 TraCI,直接在这里扩展。
提示:全流程里所有的相对路径都是相对于你启动 SUMO 时的工作目录,不是相对于配置文件所在目录。这是个非常经典的踩坑点,配置文件里写
net.net.xml而实际文件在net/子目录下,结果就是一句"could not access file"。
2. 交通需求建模:让路网上真正跑起车来
2.1 trips、flows、routes、OD 四种需求写法怎么选
SUMO 的需求描述有好几种格式,新手最容易在这里犯迷糊。我按抽象层次从低到高捋一遍,你按手头数据的精度选。
routes(路径级)是最直接的,每辆车明确写清楚出发时间、出发边、完整路径。适合你已经知道每辆车的确切路径,或者用于调试和教学。
<vehicle id="v0" depart="0.00"> <route edges="E0 E3 E7 E12"/> </vehicle>trips(起讫级)只给出起点边和终点边,路径交给路由工具算。适合你有 OD 信息但没有具体路径的场景。
<trip id="t0" depart="0.00" from="E0" to="E12"/>flows(流量级)是在 trips 基础上加了流量概念,用number或period指定一段时间内发多少车。做宏观流量验证时用它最省事。
<flow id="f0" begin="0" end="3600" number="600" from="E0" to="E12"/>OD 矩阵是最宏观的,一个矩阵描述各交通小区之间的出行量。它需要配合od2trips转成 trips,再配合duarouter算路径。适合从交通调查数据出发做大尺度场景。
选型逻辑很简单:数据精度到什么程度,就用什么格式。只有路口转向流量,用 flows 或者 OD;有完整的路径调查,用 routes。我日常做场景验证大多用 flows,因为它写起来快、改起来方便,改一个 number 就能调流量强度。
2.2 randomTrips.py 关键参数逐个拆解
手上没有真实数据时,SUMO 自带的randomTrips.py是最常用的造数据工具,它在tools/目录下。这个脚本参数多得吓人,但真正天天用到的就那么十来个,我把最关键的几个按重要性排一下。
| 参数 | 作用 | 常用取值 |
|---|---|---|
-n | 指定路网文件 | net.net.xml |
-o | 输出 trips 文件 | trips.trips.xml |
-r | 输出路由文件(内部调用 duarouter) | routes.rou.xml |
--begin/--end | 仿真起止时间 | 0/3600 |
--period | 平均发车间隔(秒) | 1表示每秒一辆 |
--fringe-factor | 边缘边吸引力倍数 | 10让车多走边界 |
--seed | 随机种子 | 42保证可复现 |
--trip-attributes | 给车辆附加属性 | departLane="best" |
先说--period,这是控制流量强度的核心。它表示平均每隔多少秒产生一辆车,--period 1就是每秒一辆,一小时 3600 辆;--period 5就是每小时 720 辆。注意这里的"平均"是随机的,实际发车间隔是围绕这个均值的随机分布,不是精确等距。如果你想要精确的发车间隔,改用--insertion-rate或者直接在 flows 里写period。
--fringe-factor这个参数很有意思,值得单独说。SUMO 路网里的边分两类:边缘边(fringe edge)是只连了一头的、类似城市出入口的路,内部边是两头都连着的。默认情况下车辆起终点在城市内部随机撒,会导致大量短途出行、车刚上路就到终点了。把--fringe-factor调大,就会让边缘边作为起终点的权重更高,车就更倾向于从城市一头开到另一头,跑出长途出行。我做城市级仿真时一般设到 10 以上。
--trip-attributes是新手最容易忽略但特别有用的参数。它的写法有点绕,需要转义引号:
python $SUMO_HOME/tools/randomTrips.py \ -n net/net.net.xml \ -r demand/routes.rou.xml \ --begin 0 --end 3600 \ --period 2 \ --fringe-factor 10 \ --seed 42 \ --trip-attributes "departLane=\"best\" departSpeed=\"max\""departLane="best"表示让车选最合适的那条车道出发,而不是默认的第 0 条车道。这个参数的作用在下面"常见问题"里会展开,简单说就是避免车辆在出发边就发生挤兑。
注意:
randomTrips.py内部调用 duarouter 时如果找不到路径会静默丢弃这些车,所以最终车辆数往往少于你按 period 算出来的理论值。想要知道实际生成了多少车,跑完后统计一下 rou 文件里的 vehicle 数量。
2.3 duarouter 路由计算与算法选择
如果randomTrips.py带了-r参数,它内部已经帮你调了duarouter,你不用手动再来一遍。但很多场景下我是分开跑的:先用randomTrips.py只生成 trips,再单独跑duarouter,这样方便在中途对 trips 做二次处理,也方便观察哪些 OD 对算不出路径。
duarouter \ -n net/net.net.xml \ --route-files demand/trips.trips.xml \ -o demand/routes.rou.xml \ --ignore-errors true \ --routing-algorithm dijkstra--routing-algorithm有三个选项:dijkstra、astar、CH。Dijkstra 是最稳妥的,保证最短路径,速度对中小路网完全够用。A* 在有大范围路网时更快,但需要额外的启发式。CH(Contraction Hierarchies)是预处理型的,第一次跑会慢,之后查询极快,适合超大规模路网做反复查询。我一般在路网小于几千条边时直接用 Dijkstra,省心。
--ignore-errors true建议加上。路网里难免有些孤立的边,或者某些 OD 对之间确实不连通,不加这个参数 duarouter 遇到一个算不出路径的 trip 就可能中断整个流程。加上之后它会跳过这些 OD 对并给出警告,你跑完看警告数量就知道路网连通性有没有问题——警告特别多的时候,八成是路网本身有问题,而不是需求的问题。
duarouter 算出来的路径是不是物理上合理,还有个细节值得看:是否绕行。有时你会发现某辆车从相隔很近的两个点之间,路径却绕了大半个城市。这通常不是算法的问题,而是路网本身缺连接。排查方法是在 GUI 里加载这个路径文件,打开车辆轨迹显示,看它到底走了哪条线,然后回到路网里检查那几个关键路口的 connection。
2.4 车辆类型 vType:跟驰模型参数怎么定
需求文件里除了车的行程,还得定义车的"性格",也就是vType。这是需求建模里技术含量最高的部分,因为它直接决定仿真的行为真实度。
<vType id="car" vClass="passenger" accel="2.6" decel="4.5" sigma="0.5" length="5.0" minGap="2.5" maxSpeed="16.67" carFollowModel="Krauss" lcModel="LC2013" speedFactor="normc(1.0,0.1,0.2,2.0)"/>逐个解释关键参数。accel是最大加速度,单位 m/s²,普通小汽车 2.6 左右是常见的标定值。decel是舒适减速度,取正值 4.5 表示减速能力,注意这里填正数,SUMO 内部处理方向。sigma是驾驶员不完美程度,0 表示完美跟驰、1 表示非常随机的驾驶行为,取 0.5 是比较折中的默认值。
maxSpeed单位是 m/s,16.67 对应 60 km/h。这里有个坑:这个值是车辆自身的限速,路网每条边也有一个限速,两者取小。很多人设了maxSpeed却发现车开得比预期慢,就是因为路段限速更低,把车卡住了。
speedFactor是我最推荐加的参数,用normc分布给每辆车分配一个期望速度因子。上面这行表示均值 1.0、标准差 0.1、截断在 0.2 到 2.0 之间的正态分布。有了它,仿真里车流速度就会自然分层,不会出现所有车速度一模一样的"机器人队伍"效果,通行能力和实际的差异会小很多。
carFollowModel默认就是 Krauss,这是 SUMO 的招牌模型,参数少、稳定、够用。如果做精细的能耗或排放研究,可以换成 IDM,它的加减速曲线更平滑。换模型的时候要注意参数是跟着模型走的,IDM 有自己的参数集,直接把 Krauss 的参数套过去不会正常工作。
提示:
vType既可以写在路由文件里,也可以单独放在一个additional文件里通过配置引入。我推荐后者,因为车辆类型在多个场景间通常是复用的,单独一个vtypes.add.xml方便维护。
3. 仿真配置与运行:从能跑到跑得对
3.1 sumocfg 文件结构拆解
sumocfg是 XML 格式,把所有输入输出串起来。它的结构是按功能分块,我按块说明。
<configuration> <input> <net-file value="net/net.net.xml"/> <route-files value="demand/routes.rou.xml"/> <additional-files value="demand/vtypes.add.xml"/> </input> <time> <begin value="0"/> <end value="3600"/> <step-length value="1.0"/> </time> <output> <tripinfo-output value="output/tripinfo.xml"/> <summary-output value="output/summary.xml"/> <vehroute-output value="output/vehroutes.xml"/> </output> <processing> <time-to-teleport value="300"/> <ignore-route-errors value="true"/> </processing> <random_number> <seed value="42"/> </random_number> </configuration>input块里,route-files可以写多个文件用逗号隔开,这对多类车辆场景很实用。additional-files用来加载信号配时、公交站、检测器这些附加定义,它和主路网是两个入口,很多新手会忘了这个块导致信号配时没生效。
processing块里的time-to-teleport是必须理解的参数。SUMO 里如果一辆车在同一位置堵超过这个秒数,会被瞬移到下游,默认是 300 秒。这个机制本意是防止死锁,但它会悄悄改变你的拥堵统计结果。做拥堵研究时我一般把它设成 -1 关闭瞬移,让车真正堵在那里,否则你研究的拥堵其实是"瞬移前的拥堵",数据会失真。当然关掉之后要小心局部的死锁把整个仿真拖死,需要配合观察。
3.2 时间步长、随机种子与可复现性
step-length是仿真步长,默认 1 秒。这个值直接决定计算量和精度。步长 1 秒时,高速行驶的车在一步内移动十几米,跟驰模型的计算精度会打折。做高速场景或者细致的排放研究时,我会把它降到 0.1 甚至更小,代价是仿真时间成倍增长。
seed是随机种子,这个值的重要性怎么强调都不为过。SUMO 里车辆的发车时刻、speedFactor 的随机抽样、车道选择等等,全都受这个种子控制。同一个配置、同一个种子,跑两次结果应该完全一致。这个特性是做对比实验的生命线:改一个参数,其他全固定,这样结果差异才能归因到那个参数上。
我的习惯是每组对比实验跑 5 到 10 个不同种子,然后取平均值和方差。只跑一个种子就下结论,很容易被随机波动带偏。种子建议用固定的几个数字(比如 42、123、456),这样整个项目组跑出来的结果都可比。
注意:
step-length改了之后,--period或者 flows 里的发车时刻不用改,因为它们是按仿真时间算的,不是按步数。但如果你在脚本里用步数当循环条件,要注意换算。
3.3 命令行运行与 GUI 调试的分工
sumo是无界面版本,sumo-gui是带界面版本,两者接受完全一样的参数。日常使用我的分工是:调试用 GUI,出数据用命令行。
命令行跑:
sumo -c config/sim.sumocfg --tripinfo-output output/tripinfo.xml命令行跑的优势是快,没有渲染开销,批量跑几十组实验时差距非常明显。而且它能直接嵌进 shell 或者 Python 脚本里循环。
GUI 跑:
sumo-gui -c config/sim.sumocfgGUI 的价值在于可视化排查。几个我觉得最实用的功能:把车辆颜色按速度映射,一眼就能看出哪段路在堵;打开"显示车辆路线"选项,能看到每辆车的完整轨迹,用来验证路由是否合理;调慢仿真速度,看路口转向的行为细节。
GUI 里还有一个隐藏技巧:延迟时间设置。在界面上把 delay 设成 100ms 以上,你可以肉眼看清每辆车的换道过程和路口排队形成过程。对于要写报告或者给非技术人员演示的场景,这个功能救过我好几次。
3.4 输出文件解读:tripinfo、summary、edgeData
仿真跑完,输出目录里会有一堆 xml,第一次看很容易懵。我挑三个最常用的说清楚它们各自能回答什么问题。
summary.xml是每个时间步一条记录,包含当前在路网上的车辆数、平均速度、总行驶里程、总等待时间等汇总指标。做宏观趋势分析、画仿真过程曲线用它。它粒度粗但数据量小,适合看整体。
tripinfo.xml是每完成一次旅程一条记录,包含这辆车的行驶时间、等待时间、行驶距离、平均速度等。做单车级别的统计分析、算平均延误用它。这是做交通评价最常用的文件。
<tripinfo id="v0" depart="12.00" arrival="245.30" duration="233.30" routeLength="1850.40" waitingTime="38.20" timeLoss="62.10" avgSpeed="7.93"/>timeLoss这个字段特别值得关注,它表示这辆车相对理想自由流状态损失了多少时间,本质就是延误。做信号配时优化时,这个值就是你要优化的目标函数。waitingTime只统计速度接近零的时间,和timeLoss是两回事,别混用。
edgeData.xml是每条边一条记录,统计这条边上的车流量、平均速度、密度、排队长度等。做路段级的通行能力分析、画热力图用它。这个文件默认不输出,需要在配置里显式加<edgeData>定义或者用--edgedata-output参数。
| 输出文件 | 粒度 | 主要用途 |
|---|---|---|
| summary.xml | 每时间步 | 整体趋势、过程曲线 |
| tripinfo.xml | 每辆车 | 延误统计、行程时间分析 |
| edgeData.xml | 每条边 | 路段流量、密度、排队 |
| vehroute.xml | 每辆车 | 路径核查、路由验证 |
4. 进阶玩法:TraCI 接口与信号配时
4.1 TraCI 能做什么,为什么值得学
TraCI 全称 Traffic Control Interface,是 SUMO 提供的一个在仿真运行过程中实时读写状态的接口。前面所有内容都是"配置好一次性跑完",TraCI 让你能在每一仿真步里做决策。它的典型用途有三类:信号灯的动态控制、车辆路径的实时重规划、以及把 SUMO 当成一个环境来训练控制算法。
为什么值得学?因为静态配置能做的实验是有上限的。比如你想验证一个自适应信号控制策略,用静态的tlLogic根本没法表达"根据排队长度动态切换相位"这个逻辑,必须用 TraCI 在运行中读取检测器数据、计算、再下发控制指令。这套流程跑通之后,SUMO 就从"交通仿真软件"变成了"交通控制实验平台",玩法完全不一样了。
Python 是本领域使用最广泛的接入方式,因为 SUMO 官方的traci库就是 Python 的,而且配合 numpy、pandas 做数据分析特别顺。下面给一个最小可运行的模板。
4.2 Python 控制实战:一个最小可运行脚本
import traci import sumolib # 启动仿真,注意 sumoBinary 用无界面版本提速 sumo_binary = sumolib.checkBinary("sumo") traci.start([sumo_binary, "-c", "config/sim.sumocfg"]) step = 0 while traci.simulation.getMinExpectedNumber() > 0: traci.simulationStep() step += 1 # 示例:读取某条边的排队车辆数 queue = traci.edge.getLastStepHaltingNumber("E0") # 示例:读取所有车辆的当前速度 for veh_id in traci.vehicle.getIDList(): speed = traci.vehicle.getSpeed(veh_id) # 示例:仿真中期动态改一辆车的路线 if step == 500: traci.vehicle.changeTarget("v0", "E20") traci.close()这段代码里有几个点值得展开。sumolib.checkBinary是官方推荐的方式,它会去环境变量里找 SUMO 的安装路径,比硬编码路径可移植性好得多。getMinExpectedNumber()返回的是路网上还有多少辆车加上还有多少辆没发出来,用这个当循环条件比固定跑 3600 步更合理,仿真会自然结束。
traci.vehicle.getIDList()每步都要调用的话,在大规模场景下会有性能开销。如果你的场景车辆上万,建议不要每步全量遍历,改成只查询你关心的边或者区域,或者每隔几步查一次。
提示:TraCI 脚本里所有的距离单位是米、速度单位是 m/s、时间单位是秒。到这一步千万别再想着用什么 km/h,界面上显示的是 km/h,接口里返回的全是 m/s,单位搞混是初学者最常犯的错。
4.3 tlLogic 信号配时方案的手写与调试
信号配时是仿真里绕不开的东西,静态的tlLogic定义长这样:
<tlLogic id="C0" type="static" programID="0" offset="0"> <phase duration="42" state="GGrrGGrr"/> <phase duration="3" state="yyrryyrr"/> <phase duration="30" state="rrGGrrGG"/> <phase duration="3" state="rryyrryy"/> </tlLogic>state字符串的每一位对应一个车道或者一个连接,G是绿灯、y是黄灯、r是红灯。这个字符串的长度必须和这个路口被信号控制的车道数严格匹配,写长了写短了 SUMO 都会报错。新手最容易在这里翻车。
怎么知道该写多长?在 GUI 里点开这个路口的信号灯信息面板,或者用netconvert --sumo-net-file配合检查连接数。更稳的办法是先用netconvert自动生成信号配时(加--tls.default-type参数),把生成结果当模板改,比自己从零数位数靠谱得多。
offset参数用于做干线协调。相邻信号灯设置不同的 offset,让车流能一路绿灯通过,这就是所谓的绿波带。这是做干线优化的核心手段,调 offset 的过程用 TraCI 自动化做效率最高,手动调基本不可能找到最优解。
| 配时字段 | 含义 | 调试要点 |
|---|---|---|
| duration | 相位持续秒数 | 总和为一个周期 |
| state | 各位灯色 | 位数必须与连接数一致 |
| offset | 周期偏移 | 用于干线协调绿波 |
| programID | 方案编号 | 多方案切换时索引 |
5. 踩坑实录:常见报错与排查思路
5.1 报错速查表
下面这些是我实际踩过、并且反复见到别人踩的报错,整理成速查表。
| 报错信息关键词 | 根本原因 | 解决方向 |
|---|---|---|
could not access file | 相对路径错位 | 检查工作目录,统一用相对项目根的路径 |
Edge 'xxx' is not known | 需求里的边 id 与路网不符 | 比对 rou 文件和 net 文件的 id |
No connection between edge | 路网缺转向连接 | 用 netconvert 重建或手工补 connection |
Vehicle 'xxx' has no valid route | 路由算不出路径 | 检查连通性,用 duarouter 单独验证 |
Invalid position for vehicle | 出发车道越界 | 用小车道数或改 departLane |
The traffic light 'xxx' has no program | tlLogic 未定义 | 补 additional 文件里的信号定义 |
could not access file出现频率最高。它的本质是 SUMO 不关心你的配置文件放在哪,它只关心你启动命令时所在的目录。解决办法是要么每次都从项目根目录启动,要么在脚本里用os.chdir切到项目根,再做相对路径引用。我现在的习惯是脚本一开头就切目录,从不依赖命令行位置。
5.2 仿真结果不合理的几种典型症状
程序不报错不代表结果对。下面几种症状我遇到太多次,都是"能跑但结果离谱"的类型。
第一种是所有车速度偏低。八成是maxSpeed和路段限速的双重限制在起作用,或者sigma设太大导致跟驰行为过于保守。排查方法是单独放一辆车在空旷路段跑,看它能不能跑到期望速度。
第二种是大量车在路口莫名消失。这通常是time-to-teleport在起作用,车堵太久被瞬移了。把它设成 -1 再看,如果瞬间出现严重排队甚至死锁,说明路口的通行能力设计有问题,需要检查信号配时或者转向连接。
第三种是通行量明显低于理论值。检查车辆插入是否成功,有没有出现"无法插入"的警告。发车太密集时,插入边被占满,后面的车会插入失败被丢弃,实际流量就上不去。解决办法是给出发边留足够车道、用departLane="best"、或者降低发车频率。
第四种是tripinfo 里 timeLoss 全都巨大。这往往不是仿真问题,而是路网限速设置不合理,或者你把begin时间设成负数导致车在外面排队了很久。先确认基础时空参数,再看行为。
5.3 性能与规模的取舍经验
最后聊聊规模。SUMO 跑多大路网、多少车是可行的,这个问题没有标准答案,但我可以给一些经验值。普通笔记本跑几千辆车、几千条边的路网,实时比大概在 1:5 到 1:20 之间,也就是仿真 1 小时实际算几分钟。上到几万辆车,就需要考虑关掉不必要的输出、加大 step-length、用 CH 路由这些优化手段了。
输出文件的体积经常比计算时间更让人头疼。netstate-dump这种每步全量车辆状态的输出会爆炸式增长,除非做轨迹研究,否则别开。我一般只保留 tripinfo 和 summary,需要路段数据时再加 edgeData,按需输出。
还有一个容易忽略的点:仿真跑不完不一定是卡死。如果路网上有严重的瓶颈,车辆持续累积,getMinExpectedNumber()一直大于 0,仿真永远不会自然结束。这种情况要么设个仿真时间上限强行结束,要么去解决那个瓶颈。我个人的习惯是在脚本里加一个硬性的步数上限作为兜底,防止批处理任务挂在某一组参数上整夜不结束。
踩过几次坑之后我逐渐形成一个习惯:任何一组新参数,先跑 600 秒看趋势,没问题再拉长到完整时长。600 秒足够暴露需求生成、路由、信号配时这三块的基础问题,能省下大量全时长跑废的时间。规模这件事没有银弹,真的是靠一轮轮试出来的手感。