做交通仿真的人大多经历过这么一个阶段:路网能加载了,车也能跑起来了,但屏幕上稀稀拉拉几辆车绕圈,根本看不出任何有价值的结论。我前两篇把 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 id="car" vClass="passenger" length="4.8" accel="2.6" decel="4.5" sigma="0.5" maxSpeed="16.7" speedFactor="normc(1.0,0.1,0.8,1.2)"/> <vType id="bus" vClass="bus" length="12" accel="1.2" decel="3.0" sigma="0.5" maxSpeed="13.9"/> <vType id="truck" vClass="truck" length="9.5" accel="1.0" decel="2.5" sigma="0.5" maxSpeed="11.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 id="J1" type="static" programID="custom" offset="0"> <phase duration="30" state="GGGrrrGGGrrr"/> <phase duration="3" state="yyyrrryyyrrr"/> <phase duration="25" state="rrrGGGrrrGGG"/> <phase duration="3" state="rrryyyrrryyy"/> </tlLogic> </additional>state字符串的长度等于该路口所有受控车道的数量,每个字符对应一条车道的灯色,G 是绿、y 是黄、r 是红,小写 g 表示该方向绿灯但需要让行。这个字符串很容易数错位数,我一般先在 netedit 里看自动生成的相位,复制过来再改,比自己数靠谱。
相位顺序和时长的确定,取决于你的控制策略。固定配时就用实测的最佳周期和绿信比;要做自适应控制,就先把一个基础的周期配好,运行中通过 TraCI 动态调整相位时长。
4.2 检测器的布设与输出
检测器是仿真和现实对照的桥梁。SUMO 提供了三类感应检测器,用哪类取决于你要什么数据。
E1 是单点线圈,布在车道某个位置,输出通过该断面的车辆数、平均速度、占有率,最接近现实中的感应线圈。E2 是区段检测器,覆盖一段车道,能输出区段内的车辆数、平均速度、排队长度,适合做排队评估。E3 是出入口检测器,用于统计进入和离开某个区域的流量,适合做区域交通量统计。
配置写法如下,放在 additional 文件里。
<additional> <inductionLoop id="det_1" lane="E1_0" pos="120" period="60" file="det1_out.xml"/> <laneAreaDetector id="area_1" lane="E1_0" pos="0" endPos="200" period="60" file="area_out.xml"/> </additional>period是采样周期,单位秒。做流量分析 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", index=False)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 和 PATH |
TraCI 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_number><seed value="42"/></random_number>,并且对比不同方案时用同一组种子。如果要做多次重复实验取均值,就准备一组固定种子,每个方案都跑这组种子,做配对比较。
第二个坑是检测器的时间对齐。检测器的period是从仿真开始时刻算起的固定窗口,如果你的仿真预热时间和统计时间没有分开,前几分钟的数据会被初始化效应污染,所有车辆都还没铺满路网,流量偏低。做法是设一个预热期,前几百秒不统计,或者把输出分段,只取稳定后的那段。
第三个坑是 TraCI 里读取的数据是上一步的。getLastStep系列 API 返回的是上一个仿真步结束时的状态,不是当前时刻的实时状态。你在simulationStep之后立刻读取,拿到的是刚结束的那一步的数据,这个时间差在做高频控制的时候必须考虑进去,否则控制会有一步延迟。
第四个坑是路径文件膨胀。用 randomTrips 生成一小时几千辆车,再经 duarouter 分配,routes 文件能到几十兆。如果是长时段仿真,建议按时间切片分段生成,或者用 flow 代替逐车 trip,流量大的时候文件能小一个数量级,仿真加载也快得多。
6.4 和其他仿真平台协同的注意事项
有些项目需要把交通流和别的仿真器联动,比如车辆动力学模型、外部控制算法、可视化前端。协同的通用做法是让 SUMO 通过 TraCI 暴露车辆状态,外部程序按固定的仿真步长同步读取和回写。
这里最关键的是时间同步策略。两边步长不一致的时候,需要定义谁来驱动时钟。常见方案是外部程序驱动,SUMO 每步等外部指令;或者 SUMO 驱动,外部程序被动跟随。前者容易实现但外部程序必须足够快,跟不上就会拖慢整体;后者实时性好但外部程序只能在下一次采样时介入,控制频率受限。
另外注意坐标系的转换。SUMO 用的是投影后的平面坐标,其他平台可能用经纬度或者局部坐标。转换矩阵要在初始化时算好,每一步都做一次完整投影计算会很慢。还有车辆的朝向角,SUMO 给的是弧度制且以正北为零度,很多平台用的是不同的基准,直接拿来用会得到车辆横着跑的诡异画面。
整套流程走下来,从导入路网到跑出可分析的数据,一个中等规模的片区大概要花两三天调试。时间主要花在路网清洗和需求校准上,控制逻辑本身反而写得很快。我的建议是先把数据链路打通,哪怕用随机流跑一个粗糙的结果,也要确认从仿真到数据的通路是完整的,然后再逐步替换成真实数据去提高精度。顺序反了的话,你会花大量时间在清洗一份还没被验证过的路网上。