4. 路网生成(三)

上一章我们聊了怎么用netedit画路网,说实话,画个简单的十字路口确实挺快的。但真实项目里,交叉口往往复杂得多——多相位信号灯、让行规则、甚至还有环岛。这时候纯靠鼠标点,效率就太低了。

我个人习惯是:用XML手写交叉口定义。你想想看,一段几百行的XML,就能精确控制每个车道的转向、每组信号灯的时序、每条支路的优先级。而且可复用、可版本管理,这才是工程化的做法。

4.1 交叉口的核心结构

在SUMO里,交叉口其实就是一个junction节点。但真正决定交叉口行为的,是它连接的edge和lane,以及我们附加的trafficLight和rightOfWay规则。

先看一个最简单的四路交叉口XML骨架:

<junction id="J1" x="1000" y="1000" type="traffic_light">
  <request index="0" response="0001" foes="0001" cont="0"/>
  <request index="1" response="0010" foes="0010" cont="0"/>
  <request index="2" response="0100" foes="0100" cont="0"/>
  <request index="3" response="1000" foes="1000" cont="0"/>
</junction>

这里type="traffic_light"告诉SUMO:这个交叉口由信号灯控制。而request元素定义了每个进口道的冲突关系。嗯,这里要注意——response和foes的值是二进制掩码,每一位代表一个流向是否冲突。我第一次看这个的时候也懵了,后来发现其实有规律:0001表示只有第0个流向是冲突的。

核心要点:交叉口的行为由junction的type属性和内部的request列表共同决定。type决定了控制方式,request决定了谁让谁。

4.2 信号灯交叉口定义

信号灯交叉口,说白了就是给交叉口配一套红绿灯方案。SUMO里用tlLogic元素来定义。

我曾经在做一个城市主干道项目时,需要模拟一个五相位信号灯。当时用netedit配了半天,总是对不齐相位时间。后来直接写XML,十分钟搞定。

<tlLogic id="J1" type="static" programID="default" offset="0">
  <phase duration="30" state="GGGgrrrrrGGGgrrrrr"/>
  <phase duration="3"  state="yyygrrrrryyygrrrrr"/>
  <phase duration="2"  state="rrrgrrrrrrrrgrrrrr"/>
  <phase duration="30" state="rrrrGGGgrrrrGGGg"/>
  <phase duration="3"  state="rrrryyygrrrrryyyg"/>
  <phase duration="2"  state="rrrrrrrgrrrrrrrrg"/>
</tlLogic>

这里state字符串的每个字符代表一个信号灯组:

字符 含义
G 绿灯(直行或右转)
g 绿灯(仅左转)
y 黄灯
r 红灯

你想想看,这个字符串的顺序必须和交叉口连接的车道顺序一致。我刚开始做的时候,顺序搞反了,结果左转绿灯亮的时候直行也在走,差点出事故——当然,仿真里的事故就是数据异常。

我的小技巧:写信号灯方案前,先在纸上画出每个相位的流向图。然后按SUMO要求的顺序(通常是顺时针从北开始)排列state字符串。这样不容易乱。

4.3 优先规则(让行/停让)

不是所有交叉口都有信号灯。很多次干路交叉口用的是让行规则。SUMO里用rightOfWay和priority来控制。

举个例子,一个T型路口,支路需要让行主路:

<junction id="J2" x="2000" y="1500" type="priority">
  <request index="0" response="0011" foes="0011" cont="0"/>
  <request index="1" response="0011" foes="0011" cont="0"/>
  <request index="2" response="1100" foes="1100" cont="0"/>
  <request index="3" response="1100" foes="1100" cont="0"/>
</junction>

这里type="priority"表示优先规则控制。注意看response的值:主路方向(index 0和1)的response是0011,表示它们不需要让行支路(因为支路的掩码位是1和0?不对,这里要仔细看)。

其实SUMO的优先规则逻辑是这样的:

  • response:当前流向需要让行的其他流向掩码
  • foes:与当前流向冲突的所有流向掩码

如果response全为0,表示这个流向是主路,不需要让任何人。如果response非0,表示它需要让那些掩码对应的流向。

我曾经踩过的坑:在定义让行规则时,忘记设置cont="0"。结果车辆在交叉口内犹豫不决,一直停在原地。后来查文档才发现,cont表示是否允许连续通过,0表示必须完全停车观察。

4.4 复杂交叉口实战案例

好了,理论说完了,我们来看一个真实项目中的案例。这是一个五岔路口,包含信号灯和让行规则的混合控制。

先画个结构图:

五岔路口结构图 J1 北 (信号灯) 南 (信号灯) 东 (让行) 西 (让行) 西北 (停让) 信号灯控制 让行控制 停让控制

对应的XML定义如下:

<!-- 五岔路口定义 -->
<junction id="J1" x="3000" y="2000" type="traffic_light">
  <!-- 北向和南向是信号灯控制的主路 -->
  <request index="0" response="0000" foes="1110" cont="0"/>
  <request index="1" response="0000" foes="1101" cont="0"/>
  
  <!-- 东向和西向是让行支路 -->
  <request index="2" response="0011" foes="1011" cont="0"/>
  <request index="3" response="0011" foes="0111" cont="0"/>
  
  <!-- 西北向是停让支路 -->
  <request index="4" response="1111" foes="1111" cont="0"/>
</junction>

<!-- 信号灯方案 -->
<tlLogic id="J1" type="static" programID="phase1" offset="0">
  <phase duration="40" state="GGGgrrrrrrrrrrr"/>
  <phase duration="3"  state="yyygrrrrrrrrrrr"/>
  <phase duration="2"  state="rrrrrrrrrrrrrrr"/>
  <phase duration="40" state="rrrrGGGgrrrrrrr"/>
  <phase duration="3"  state="rrrryyygrrrrrrr"/>
  <phase duration="2"  state="rrrrrrrrrrrrrrr"/>
</tlLogic>

这里要注意:信号灯只控制北向和南向(前两个流向)。东向和西向虽然也经过交叉口,但它们受让行规则控制,信号灯state里对应的字符是r(红灯)——实际上它们不看信号灯,只看让行标志。

关键理解:在SUMO里,信号灯和让行规则可以共存。信号灯控制的是它管辖的流向,其他流向由request中的response决定优先级。这种混合控制在实际项目中非常常见。

4.5 验证与调试

写完了XML,怎么验证对不对?我一般用三步:

  1. 语法检查:用sumo -c your.sumocfg --save-configuration看有没有报错
  2. 可视化检查:用sumo-gui打开,观察车辆是否按预期走
  3. 数据检查:看tripinfo.xml里的等待时间、停车次数是否合理

我曾经遇到一个情况:车辆在交叉口内全部堵死,怎么调参数都没用。后来用--junction-output选项导出交叉口状态,才发现有一个request的foes写错了,导致两个流向互相死锁。

调试小工具:在sumo-gui里,右键点击交叉口,选择"Show Phase Definition",可以实时看到当前信号灯状态和每个流向的优先级。这个功能我几乎天天用。

4.6 性能优化建议

当路网规模变大,交叉口数量上千时,XML文件的解析效率就成了问题。我个人建议:

  • 使用<include>标签拆分文件,比如信号灯方案单独放一个文件
  • 避免在request中使用过于复杂的掩码,能用简单规则就用简单规则
  • 对于规则相同的交叉口,使用<type>模板复用

嗯,说到模板复用,SUMO支持定义交叉口类型:

<type id="standard_cross" priority="1" numLanes="2" speed="13.89"/>

<junction id="J10" x="5000" y="3000" type="standard_cross"/>
<junction id="J11" x="5100" y="3000" type="standard_cross"/>

这样当你要修改所有标准交叉口的参数时,只需要改一处。我在做城市级路网时,这个特性帮我省了不少时间。

好了,关于用XML定义复杂交叉口,核心就是这些。记住:信号灯管时序,让行管优先级,request管冲突。这三样搞明白了,再复杂的交叉口你也能用XML写出来。

最后提醒一句:XML定义交叉口虽然强大,但不要过度设计。能用简单规则解决的,就别搞复杂的信号灯方案。仿真不是炫技,是解决问题。