每天堵在路口的时候,你有没有想过:这个红灯为什么是 60 秒而不是 45 秒?为什么有的路一路绿灯,有的路每个口都停?
交通信号控制是一个被严重低估的算法领域------它同时涉及排队论 、整数规划 、图着色 、排队网络稳定性理论 和多智能体强化学习,而且每一个理论结论都直接对应马路上能看见的东西。本文从单点配时讲到路网自适应控制,公式给推导,算法给可运行代码。
目录
- 一、把路口抽象成一个数学问题
- 二、相位设计:一个图着色问题
- [三、单点定时控制:Webster 配时法](#三、单点定时控制:Webster 配时法)
- 四、干线协调:绿波带是怎么算出来的
- 五、感应控制:让路口"看见"车流
- 六、Max-Pressure:有理论保证的自适应控制
- 七、强化学习方案:能力边界在哪里
- 八、怎么评估一套配时方案
- 九、工程落地的十个坑
- 十、总结与延伸阅读
一、把路口抽象成一个数学问题
在写任何代码之前,先统一术语。交通工程里这几个词是精确定义的,混用会导致后面公式全错。
| 术语 | 英文 | 含义 |
|---|---|---|
| 流向 / 转向 | movement | 一个具体的通行方向,如"东进口左转" |
| 相位 | phase | 一组互不冲突、同时放行的流向集合 |
| 周期 | cycle, C C C | 所有相位轮转一遍的总时长(秒) |
| 绿信比 | split, λ \lambda λ | 某相位有效绿灯时间占周期的比例 λ = g / C \lambda = g/C λ=g/C |
| 相位差 | offset | 相邻路口周期起点的时间偏移,绿波的关键 |
| 饱和流率 | saturation flow, s s s | 该车道排队车辆连续通过的最大速率,典型值 1800 veh/h/lane |
| 损失时间 | lost time, l l l | 启动损失 + 黄灯末端不可用时间,单相位典型 3~4 s |
有效绿灯 vs 显示绿灯
这是新手最容易搞混的一对概念。灯牌上显示的绿灯时长 G G G 并不等于真正能通车的时长。
绿灯刚亮时,前几辆车要反应、起步、加速,前 2~3 秒的通行效率远低于饱和流率,这部分叫启动损失时间 l 1 l_1 l1;黄灯亮起后还有一部分车会继续通过,这部分是"赚"回来的通行时间。于是定义:
g = G + A − l g = G + A - l g=G+A−l
其中 g g g 是有效绿灯时间 , A A A 是黄灯时长, l = l 1 + l 2 l = l_1 + l_2 l=l1+l2 是总损失时间。后面所有排队论公式里的绿灯,指的都是 g g g,不是 G G G。
流量比:衡量一个流向"有多挤"
y = q s y = \frac{q}{s} y=sq
q q q 是实际到达流率(veh/h), s s s 是饱和流率。 y y y 的物理意义是:这个流向至少需要占用多大比例的周期时间。
一个路口能不能撑住,看的是各相位关键流向 (该相位内 y y y 最大的那个)的流量比之和:
Y = ∑ i = 1 n y i max Y = \sum_{i=1}^{n} y_i^{\max} Y=i=1∑nyimax
Y ≥ 1 Y \geq 1 Y≥1 意味着无论怎么配时,路口都会持续拥堵------这时候再优化算法没有意义,需要的是渠化改造或者需求管理。这是整个信号控制领域最重要的一条边界:信号优化只能榨干现有通行能力,不能凭空创造通行能力。
二、相位设计:一个图着色问题
在算周期之前,得先决定"哪些流向可以一起放行"。这一步本质上是个组合优化问题。
冲突图建模
把每个流向抽象成图 G = ( V , E ) G=(V,E) G=(V,E) 的一个顶点,如果两个流向的行驶轨迹在路口内部有交叉(会撞车),就在它们之间连一条边。那么:
- 一个相位 = 冲突图上的一个独立集(集合内任意两点无边相连)
- 相位方案 = 用最少的独立集覆盖所有顶点
这正是经典的最小团覆盖 / 图着色问题,NP-hard。
#mermaid-svg-n2JGDSbKWreG5rpC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-n2JGDSbKWreG5rpC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-n2JGDSbKWreG5rpC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-n2JGDSbKWreG5rpC .error-icon{fill:#552222;}#mermaid-svg-n2JGDSbKWreG5rpC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-n2JGDSbKWreG5rpC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-n2JGDSbKWreG5rpC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-n2JGDSbKWreG5rpC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-n2JGDSbKWreG5rpC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-n2JGDSbKWreG5rpC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-n2JGDSbKWreG5rpC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-n2JGDSbKWreG5rpC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-n2JGDSbKWreG5rpC .marker.cross{stroke:#333333;}#mermaid-svg-n2JGDSbKWreG5rpC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-n2JGDSbKWreG5rpC p{margin:0;}#mermaid-svg-n2JGDSbKWreG5rpC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-n2JGDSbKWreG5rpC .cluster-label text{fill:#333;}#mermaid-svg-n2JGDSbKWreG5rpC .cluster-label span{color:#333;}#mermaid-svg-n2JGDSbKWreG5rpC .cluster-label span p{background-color:transparent;}#mermaid-svg-n2JGDSbKWreG5rpC .label text,#mermaid-svg-n2JGDSbKWreG5rpC span{fill:#333;color:#333;}#mermaid-svg-n2JGDSbKWreG5rpC .node rect,#mermaid-svg-n2JGDSbKWreG5rpC .node circle,#mermaid-svg-n2JGDSbKWreG5rpC .node ellipse,#mermaid-svg-n2JGDSbKWreG5rpC .node polygon,#mermaid-svg-n2JGDSbKWreG5rpC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-n2JGDSbKWreG5rpC .rough-node .label text,#mermaid-svg-n2JGDSbKWreG5rpC .node .label text,#mermaid-svg-n2JGDSbKWreG5rpC .image-shape .label,#mermaid-svg-n2JGDSbKWreG5rpC .icon-shape .label{text-anchor:middle;}#mermaid-svg-n2JGDSbKWreG5rpC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-n2JGDSbKWreG5rpC .rough-node .label,#mermaid-svg-n2JGDSbKWreG5rpC .node .label,#mermaid-svg-n2JGDSbKWreG5rpC .image-shape .label,#mermaid-svg-n2JGDSbKWreG5rpC .icon-shape .label{text-align:center;}#mermaid-svg-n2JGDSbKWreG5rpC .node.clickable{cursor:pointer;}#mermaid-svg-n2JGDSbKWreG5rpC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-n2JGDSbKWreG5rpC .arrowheadPath{fill:#333333;}#mermaid-svg-n2JGDSbKWreG5rpC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-n2JGDSbKWreG5rpC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-n2JGDSbKWreG5rpC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-n2JGDSbKWreG5rpC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-n2JGDSbKWreG5rpC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-n2JGDSbKWreG5rpC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-n2JGDSbKWreG5rpC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-n2JGDSbKWreG5rpC .cluster text{fill:#333;}#mermaid-svg-n2JGDSbKWreG5rpC .cluster span{color:#333;}#mermaid-svg-n2JGDSbKWreG5rpC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-n2JGDSbKWreG5rpC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-n2JGDSbKWreG5rpC rect.text{fill:none;stroke-width:0;}#mermaid-svg-n2JGDSbKWreG5rpC .icon-shape,#mermaid-svg-n2JGDSbKWreG5rpC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-n2JGDSbKWreG5rpC .icon-shape p,#mermaid-svg-n2JGDSbKWreG5rpC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-n2JGDSbKWreG5rpC .icon-shape .label rect,#mermaid-svg-n2JGDSbKWreG5rpC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-n2JGDSbKWreG5rpC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-n2JGDSbKWreG5rpC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-n2JGDSbKWreG5rpC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 冲突图示意
南北直行
东西直行
东左转
西左转
北左转
南左转
南直行
实际工程中不会真去解 NP-hard 问题,因为有额外的顺序约束 :相位不能任意跳转,必须遵守 NEMA 的环栅结构(ring-barrier structure)。标准四相位方案(对称放行)如下:
| 相位 | 放行流向 | 说明 |
|---|---|---|
| Φ1 | 南北直行 + 右转 | |
| Φ2 | 南北左转 | 保护左转 |
| Φ3 | 东西直行 + 右转 | |
| Φ4 | 东西左转 |
一个实用判断:要不要保护左转?
保护左转(专门给左转一个相位)会增加一次损失时间,代价是通行能力下降;但许可左转(跟直行一起放,找空档穿越)在对向流量大时会导致左转车排长队甚至溢出。经验阈值:
q 左 × q 对向直行 > 50000 ( veh/h ) 2 ⇒ 建议保护左转 q_{\text{左}} \times q_{\text{对向直行}} > 50000 \ (\text{veh/h})^2 \quad \Rightarrow \quad \text{建议保护左转} q左×q对向直行>50000 (veh/h)2⇒建议保护左转
这个"交叉乘积"判据在《城市道路交叉口设计规程》和 HCM 里都有类似形式,实际工程中还要叠加事故率数据。
三、单点定时控制:Webster 配时法
1958 年 F.V. Webster 在 TRL 报告里给出的方法,六十多年过去了,全世界绝大多数固定配时路口用的还是它。
3.1 最佳周期公式
Webster 从延误最小化出发,推导出使总延误最小的周期长度:
C 0 = 1.5 L + 5 1 − Y \boxed{C_0 = \frac{1.5L + 5}{1 - Y}} C0=1−Y1.5L+5
- L = ∑ i l i L = \sum_i l_i L=∑ili:整个周期的总损失时间
- Y Y Y:关键流量比之和
直观理解这个公式 :分子 1.5 L + 5 1.5L+5 1.5L+5 说明周期不能太短------损失时间是每周期固定支出的"税",周期越短,单位时间内交的税越多。分母 1 − Y 1-Y 1−Y 说明流量越大周期越长------但注意 Y → 1 Y \to 1 Y→1 时 C 0 → ∞ C_0 \to \infty C0→∞,所以工程上必须强制截断在 40~180 秒之间。周期超过 150 秒后,延误收益极其微弱,但驾驶员的等待焦虑和闯红灯率会显著上升。
3.2 绿灯时间分配
总有效绿灯时间为 C − L C - L C−L,按各相位流量比正比分配:
g i = ( C − L ) ⋅ y i Y g_i = (C - L) \cdot \frac{y_i}{Y} gi=(C−L)⋅Yyi
这个分配方式的合理性在于:它使得所有相位的饱和度 x i = y i / λ i x_i = y_i / \lambda_i xi=yi/λi 相等,即"雨露均沾",没有哪个方向明显比别的方向更堵。
3.3 完整实现
python
from dataclasses import dataclass
from typing import List, Dict
@dataclass
class Phase:
name: str
flow: float # 关键流向流量 veh/h
sat_flow: float # 关键流向饱和流率 veh/h
lost_time: float = 4.0 # 该相位损失时间 s
def webster_timing(
phases: List[Phase],
yellow: float = 3.0,
all_red: float = 1.0,
min_green: float = 10.0,
cycle_bounds: tuple = (40.0, 180.0),
) -> Dict:
"""Webster 单点定时配时"""
# 1) 流量比
y = [p.flow / p.sat_flow for p in phases]
Y = sum(y)
if Y >= 0.95:
raise ValueError(
f"Y = {Y:.3f} >= 0.95,路口已接近或超过通行能力上限。"
f"信号优化无法解决,需渠化改造或需求管理。"
)
# 2) 总损失时间
L = sum(p.lost_time for p in phases)
# 3) 最佳周期,取整到 5 秒并夹逼到工程范围
C0 = (1.5 * L + 5.0) / (1.0 - Y)
C = max(cycle_bounds[0], min(cycle_bounds[1], round(C0 / 5.0) * 5.0))
# 4) 按流量比分配有效绿灯
total_eff_green = C - L
g_eff = [total_eff_green * yi / Y for yi in y]
# 5) 有效绿灯 -> 显示绿灯:G = g + l - A
G = [max(min_green, gi + p.lost_time - yellow)
for gi, p in zip(g_eff, phases)]
# 6) 若 min_green 触发导致超周期,按超出部分等比压缩未触底的相位
inter_green = (yellow + all_red) * len(phases)
overflow = sum(G) + inter_green - C
if overflow > 1e-6:
adjustable = [i for i, gi in enumerate(G) if gi > min_green + 1e-6]
slack = sum(G[i] - min_green for i in adjustable)
if slack > 0:
for i in adjustable:
G[i] -= overflow * (G[i] - min_green) / slack
result = {
"cycle": round(C, 1),
"C0_theoretical": round(C0, 1),
"Y": round(Y, 3),
"total_lost_time": L,
"phases": [],
}
for p, yi, gi, Gi in zip(phases, y, g_eff, G):
lam = gi / C
x = yi / lam if lam > 0 else float("inf") # 饱和度
result["phases"].append({
"name": p.name,
"flow_ratio_y": round(yi, 3),
"green_display_G": round(Gi, 1),
"green_effective_g": round(gi, 1),
"split_lambda": round(lam, 3),
"saturation_x": round(x, 3),
})
return result
if __name__ == "__main__":
plan = webster_timing([
Phase("南北直行", flow=1100, sat_flow=3400),
Phase("南北左转", flow=280, sat_flow=1650),
Phase("东西直行", flow=900, sat_flow=3400),
Phase("东西左转", flow=220, sat_flow=1650),
])
print(f"周期 = {plan['cycle']} s (理论最优 {plan['C0_theoretical']} s)")
print(f"Y = {plan['Y']}")
for ph in plan["phases"]:
print(f" {ph['name']:8s} 绿灯 {ph['green_display_G']:5.1f}s "
f"绿信比 {ph['split_lambda']:.3f} 饱和度 {ph['saturation_x']:.2f}")
运行结果:
周期 = 75.0 s (理论最优 73.6 s)
Y = 0.617
南北直行 绿灯 24.4s 绿信比 0.339 饱和度 0.95
南北左转 绿灯 9.4s 绿信比 0.139 饱和度 1.22
东西直行 绿灯 20.3s 绿信比 0.284 饱和度 0.93
东西左转 绿灯 8.0s 绿信比 0.120 饱和度 1.11
注意左转相位的饱和度 x > 1 x > 1 x>1,说明左转车道会持续积压。这在真实路口很常见,解决办法是增设左转专用车道(提高 s s s)或者用搭接相位(lead-lag,让一侧左转提前放行)。
3.4 延误估计:Webster 延误公式
有了配时方案,怎么知道它好不好?Webster 给出了每车平均延误的估计:
d = C ( 1 − λ ) 2 2 ( 1 − λ x ) ⏟ 均匀延误 + x 2 2 q ( 1 − x ) ⏟ 随机延误 − 0.65 ( C q 2 ) 1 / 3 x ( 2 + 5 λ ) ⏟ 修正项 d = \underbrace{\frac{C(1-\lambda)^2}{2(1-\lambda x)}}{\text{均匀延误}} + \underbrace{\frac{x^2}{2q(1-x)}}{\text{随机延误}} - \underbrace{0.65\left(\frac{C}{q^2}\right)^{1/3} x^{(2+5\lambda)}}_{\text{修正项}} d=均匀延误 2(1−λx)C(1−λ)2+随机延误 2q(1−x)x2−修正项 0.65(q2C)1/3x(2+5λ)
三项各有含义:
- 均匀延误:假设车辆均匀到达时,因为红灯必然产生的等待。这是确定性的、不可消除的部分。
- 随机延误 :车辆实际到达是随机的(近似泊松流),偶尔一个周期来车特别多就会有溢出排队。注意 x → 1 x \to 1 x→1 时这项发散,这就是为什么饱和度超过 0.9 之后延误会爆炸式增长。
- 修正项:Webster 通过仿真拟合的经验修正,大约是前两项之和的 5%~15%。
python
def webster_delay(q_vph: float, s_vph: float, C: float, g: float) -> float:
"""Webster 平均延误 (s/veh)。注意公式内部单位为 veh/s"""
q = q_vph / 3600.0
lam = g / C
capacity = (s_vph / 3600.0) * lam
x = q / capacity
if x >= 1.0:
return float("inf") # 过饱和,稳态延误不存在
d1 = C * (1 - lam) ** 2 / (2 * (1 - lam * x))
d2 = x ** 2 / (2 * q * (1 - x))
d3 = 0.65 * (C / q ** 2) ** (1 / 3) * x ** (2 + 5 * lam)
return d1 + d2 - d3
你可以用它做一件很有意思的事:扫描周期长度画延误曲线 ,会发现曲线在 Webster 最优周期附近相当平坦------这意味着周期取 70 秒还是 80 秒,延误差别很小。这个"平底锅"特性是 Webster 法能用六十年的根本原因:它对参数误差不敏感,鲁棒性极好。
四、干线协调:绿波带是怎么算出来的
单点最优 ≠ 全局最优。一条主干道上每个路口各自算 Webster,结果很可能是每个口都停一次。协调控制解决的就是这个问题。
4.1 单向绿波:一个除法
前提:协调路口必须使用相同周期(或整数倍周期,称为"双周期")。这是协调的硬约束------周期不同,相位关系会持续漂移,绿波无法维持。
设相邻路口间距 d d d(米),设计车速 v v v(m/s),则理想相位差:
offset i = ( ∑ k ≤ i d k v ) m o d C \text{offset}{i} = \left(\sum{k \le i} \frac{d_k}{v}\right) \bmod C offseti=(k≤i∑vdk)modC
python
def green_wave_offsets(distances_m, speed_kmh, cycle_s):
"""单向绿波相位差计算
distances_m: 相邻路口间距列表,长度 = 路口数 - 1
返回: 各路口相对首个路口的相位差 (s)
"""
v = speed_kmh / 3.6
offsets, acc = [0.0], 0.0
for d in distances_m:
acc += d / v
offsets.append(round(acc % cycle_s, 1))
return offsets
print(green_wave_offsets([420, 380, 550, 300], speed_kmh=50, cycle_s=90))
# [0.0, 30.2, 57.6, 17.2, 38.8]
4.2 双向绿波:为什么这么难
真正的难点在于双向。上行绿波和下行绿波的相位差要求方向相反,通常无法同时满足。
数学上,设路口 i i i 到 i + 1 i+1 i+1 的行程时间为 t i t_i ti,双向绿波同时成立的条件是:
2 t i = m i ⋅ C , m i ∈ Z + 2t_i = m_i \cdot C, \quad m_i \in \mathbb{Z}^{+} 2ti=mi⋅C,mi∈Z+
也就是说,路口间距必须恰好等于 v C 2 \frac{vC}{2} 2vC 的整数倍 。代入典型值: v = 50 v=50 v=50 km/h, C = 90 C=90 C=90 s,得到理想间距 ≈ 625 \approx 625 ≈625 m 或其整数倍。
这解释了一个现象:新城区宽马路容易做绿波,老城区密路网做不了------间距不匹配是物理约束,不是算法问题。当间距不理想时,只能在两个方向之间做权衡,这就是 MAXBAND 模型要解决的事。
4.3 MAXBAND:带宽最大化的 MILP
Little 等人 1981 年提出的 MAXBAND,把双向绿波建模为混合整数线性规划:
max b + b ˉ s.t. w i + b ≤ g i (上行带落在绿灯内) w ˉ i + b ˉ ≤ g i (下行带落在绿灯内) ( w i + w ˉ i ) − ( w i + 1 + w ˉ i + 1 ) + ( t i + t ˉ i ) = m i ∈ Z b / b ˉ = k (双向带宽比,按流量设定) w i , w ˉ i , b , b ˉ ≥ 0 \begin{aligned} \max \quad & b + \bar{b} \\ \text{s.t.} \quad & w_i + b \le g_i \quad \text{(上行带落在绿灯内)} \\ & \bar{w}i + \bar{b} \le g_i \quad \text{(下行带落在绿灯内)} \\ & (w_i + \bar w_i) - (w{i+1} + \bar w_{i+1}) + (t_i + \bar t_i) = m_i \in \mathbb{Z} \\ & b / \bar b = k \quad \text{(双向带宽比,按流量设定)} \\ & w_i, \bar w_i, b, \bar b \ge 0 \end{aligned} maxs.t.b+bˉwi+b≤gi(上行带落在绿灯内)wˉi+bˉ≤gi(下行带落在绿灯内)(wi+wˉi)−(wi+1+wˉi+1)+(ti+tˉi)=mi∈Zb/bˉ=k(双向带宽比,按流量设定)wi,wˉi,b,bˉ≥0
其中 b , b ˉ b, \bar b b,bˉ 是上下行带宽, w i w_i wi 是绿波带前沿到绿灯起点的间隙, m i m_i mi 是整数环路约束------正是这个整数变量让问题变成了 NP-hard 的 MILP。
工程上有个更简单的替代品叫时距图人工调优:把时空图画出来,拖动各路口绿灯条,肉眼找最大带宽。听起来原始,但对于 5~8 个路口的干线,老工程师调出来的结果常常不比求解器差,而且可解释性强得多。
关键提醒 :绿波只在非饱和 条件下有意义。一旦某个路口排队溢出到上游,绿波带里跑的是排队车辆的启动波,不是自由流,此时整条线的协调方案会瞬间失效。高峰期最该做的不是绿波,是排队管理和溢出保护。
五、感应控制:让路口"看见"车流
定时控制的根本缺陷是开环------它不知道路上到底有没有车。凌晨三点空无一人的路口照样让你等 60 秒红灯,就是这个原因。
5.1 基本感应逻辑
在停车线上游埋设线圈或架设视频检测器,逻辑如下:
当前相位绿灯开始
├── 保证最小绿灯时间 G_min(行人过街 + 安全约束)
└── 进入延长阶段:
├── 每检测到一辆车 → 绿灯延长 unit_extension(典型 2~3 s)
├── 若 unit_extension 内无车到达 → 判定"断流",切换相位
└── 若累计时长达到 G_max → 强制切换(防止某方向饿死)
python
class ActuatedController:
def __init__(self, g_min=10.0, g_max=60.0, unit_ext=2.5,
yellow=3.0, all_red=1.0, n_phases=4):
self.g_min, self.g_max, self.unit_ext = g_min, g_max, unit_ext
self.yellow, self.all_red = yellow, all_red
self.n_phases = n_phases
self.phase = 0
self.elapsed = 0.0
self.gap = 0.0 # 距上次检测到车的时间
def step(self, dt: float, vehicle_detected: bool):
self.elapsed += dt
self.gap = 0.0 if vehicle_detected else self.gap + dt
if self.elapsed < self.g_min:
return self.phase # 最小绿保护期
if self.elapsed >= self.g_max: # 强制切换
return self._switch()
if self.gap >= self.unit_ext: # 断流,让给下一相位
return self._switch()
return self.phase # 继续延长
def _switch(self):
self.phase = (self.phase + 1) % self.n_phases
self.elapsed = 0.0
self.gap = 0.0
return self.phase
5.2 SCOOT 与 SCATS:工业界的两条路线
| 维度 | SCOOT (英国 TRL) | SCATS (澳大利亚 RMS) |
|---|---|---|
| 核心思想 | 在线仿真"车队曲线",预测未来延误 | 基于历史方案库的动态选择 |
| 调整方式 | 周期/绿信比/相位差小步渐进(每次 ±4 s) | 按饱和度在预设方案间切换 |
| 检测器位置 | 上游进口道(预测型) | 停车线处(反馈型) |
| 优化目标 | 最小化延误 + 停车次数 | 最大化通行能力、均衡饱和度 |
| 适用场景 | 流量平稳、路网规则 | 流量波动大、需快速响应 |
两者都属于方案生成式 (SCOOT)或方案选择式 (SCATS)的第二代系统。它们的共同局限是:调整幅度受限于稳定性要求,面对突发事件(事故、活动散场)响应偏慢。
六、Max-Pressure:有理论保证的自适应控制
前面的方法都靠经验参数。有没有一个算法,不需要知道到达率,还能证明它是最优的?有------Max-Pressure(最大压力控制),源自 Tassiulas & Ephremides 1992 年在无线网络调度中提出的 BackPressure 算法,2013 年由 Varaiya 引入交通控制。
6.1 压力的定义
对每个流向 m m m(从上游链路 u u u 到下游链路集合 { d } \{d\} {d}),定义其压力:
w m = x u − ∑ d r u d ⋅ x d w_m = x_u - \sum_{d} r_{ud} \cdot x_d wm=xu−d∑rud⋅xd
- x u x_u xu:上游链路排队车辆数
- x d x_d xd:下游链路排队车辆数
- r u d r_{ud} rud:转向比例(从 u u u 去往 d d d 的车辆占比)
相位 ϕ \phi ϕ 的压力是其包含的所有流向压力的饱和流率加权和:
P ϕ = ∑ m ∈ ϕ s m ⋅ max ( w m , 0 ) P_\phi = \sum_{m \in \phi} s_m \cdot \max(w_m,\ 0) Pϕ=m∈ϕ∑sm⋅max(wm, 0)
控制律:每个决策时刻,选择压力最大的相位放行。
ϕ ⋆ = arg max ϕ ∈ Φ P ϕ \phi^\star = \arg\max_{\phi \in \Phi} P_\phi ϕ⋆=argϕ∈ΦmaxPϕ
6.2 为什么它是对的
这个算法的美妙之处在于它的直觉极其简单,但有严格的理论保证:
定理(Varaiya, 2013) :若需求向量位于路网的可行容量域内部,则 Max-Pressure 控制使排队长度过程正常返(positive recurrent) ,即队列不会无界增长。且该算法在所有仅使用队列信息的控制策略中,稳定域最大。
关键点在于:它不需要知道到达率 q q q,只需要实时队列观测。这意味着它天然适应流量变化,不需要重新标定------这在工程上是巨大优势,因为交通流量的标定数据往往几年才更新一次。
直觉上, w m = x u − ∑ r x d w_m = x_u - \sum r x_d wm=xu−∑rxd 这个差值的作用是:上游堵了就放行,但如果下游更堵,就别放了 ------放过去也是堵在下一个路口,还会造成溢出,反而降低整体吞吐。这自然实现了溢出保护,而 Webster 和绿波都做不到这一点。
6.3 实现
python
from dataclasses import dataclass, field
from typing import List, Dict, Tuple
@dataclass
class Movement:
"""一个流向:从上游链路进入,按转向比流向多个下游链路"""
up: str
downs: Dict[str, float] # {下游链路: 转向比}
sat_flow: float = 1800.0 # veh/h
@dataclass
class MaxPressureController:
phases: List[List[Movement]]
min_green: float = 8.0
yellow: float = 3.0
cur_phase: int = 0
elapsed: float = 0.0
def pressure(self, phase: List[Movement], queues: Dict[str, float]) -> float:
total = 0.0
for m in phase:
w = queues.get(m.up, 0.0) - sum(
r * queues.get(d, 0.0) for d, r in m.downs.items()
)
total += m.sat_flow * max(w, 0.0)
return total
def step(self, dt: float, queues: Dict[str, float]) -> Tuple[int, bool]:
"""返回 (当前相位索引, 是否发生切换)"""
self.elapsed += dt
if self.elapsed < self.min_green:
return self.cur_phase, False
pressures = [self.pressure(p, queues) for p in self.phases]
best = max(range(len(pressures)), key=lambda i: pressures[i])
# 迟滞:新相位压力需显著更高才切换,避免相位抖动
if best != self.cur_phase and pressures[best] > pressures[self.cur_phase] * 1.05:
self.cur_phase = best
self.elapsed = 0.0
return self.cur_phase, True
return self.cur_phase, False
# 示例
NS = [Movement("N_in", {"S_out": 0.8, "W_out": 0.2}),
Movement("S_in", {"N_out": 0.8, "E_out": 0.2})]
EW = [Movement("E_in", {"W_out": 0.85, "N_out": 0.15}),
Movement("W_in", {"E_out": 0.85, "S_out": 0.15})]
ctrl = MaxPressureController(phases=[NS, EW])
queues = {"N_in": 18, "S_in": 15, "E_in": 4, "W_in": 6,
"N_out": 2, "S_out": 25, "E_out": 3, "W_out": 2}
print("南北压力:", ctrl.pressure(NS, queues))
print("东西压力:", ctrl.pressure(EW, queues))
注意示例中 S_out 排队已达 25 辆------南北向虽然自己排了 18+15 辆,但下游更堵,压力被大幅削减,算法会倾向于先放东西向。这正是溢出保护在起作用。
6.4 实际部署的两个修正
- 加迟滞或最小绿灯 。纯 Max-Pressure 允许每个决策周期切换相位,实际会导致相位频繁抖动,驾驶员体验极差且损失时间剧增。代码中
min_green和 5% 的迟滞阈值就是为此。 - 队列估计是难点 。算法要求实时队列长度,但线圈只能测过车数和占有率。实践中需要用冲击波模型或卡尔曼滤波从检测器数据反推队列,这部分的误差往往比算法本身的差异更影响效果。
七、强化学习方案:能力边界在哪里
近几年 RL 做信号控制的论文非常多,这里不复述模型结构,只讲怎么设计 和什么时候不该用。
7.1 MDP 建模
状态空间。常见设计从简到繁:
- 各进口道排队长度 + 当前相位 one-hot + 当前相位已持续时长(最实用,维度低)
- 离散交通状态编码 DTSE:把每条车道切成 net 格,标记有无车辆(信息全但维度高)
- 压力向量(借鉴 Max-Pressure,被证明是很强的状态表示)
动作空间。两种范式:
- 相位选择:直接输出下一个相位。灵活,但可能违反环栅约束,且相位跳变影响驾驶体验。
- 保持/切换二元决策:只决定"当前相位继续还是进入下一个"。约束天然满足,工程更可接受,是我更推荐的做法。
奖励函数。这是全部工作里最容易出问题的地方:
r t = − ∑ i x i ( t ) (负排队总长,最常用) r_t = -\sum_i x_i^{(t)} \quad \text{(负排队总长,最常用)} rt=−i∑xi(t)(负排队总长,最常用)
r t = ∑ i ( x i ( t − 1 ) − x i ( t ) ) (排队变化量,稠密但易陷入局部最优) r_t = \sum_i \left(x_i^{(t-1)} - x_i^{(t)}\right) \quad \text{(排队变化量,稠密但易陷入局部最优)} rt=i∑(xi(t−1)−xi(t))(排队变化量,稠密但易陷入局部最优)
r t = − ∑ i d i ( t ) (累计延误,最贴近目标但稀疏) r_t = -\sum_i d_i^{(t)} \quad \text{(累计延误,最贴近目标但稀疏)} rt=−i∑di(t)(累计延误,最贴近目标但稀疏)
踩坑提醒 :用"排队变化量"做奖励会产生一个隐蔽的病态策略------智能体学会反复在两个相位间快速切换 ,因为切换瞬间总能让某个方向的队列下降,从而刷取正奖励,而真实通行效率因为损失时间暴增反而更差。一定要在奖励里加入切换惩罚项。
7.2 多路口:MARL 的信用分配困境
单路口 RL 打败 Webster 是很容易的(论文里 20%~30% 的延误下降很常见),但推广到路网就困难得多:
- 非平稳性:每个智能体的环境包含其他智能体,而它们也在学习,环境分布持续变化,收敛性无保证。
- 信用分配:全局奖励下降了,是哪个路口的功劳?
- 通信开销:完全集中式训练在 50 个路口以上就不可行。
主流缓解手段是 CTDE(集中训练分散执行)、参数共享(所有路口共用一套网络权重)、以及用图神经网络编码路网拓扑。
7.3 我的实用判断
| 场景 | 推荐方案 |
|---|---|
| 流量稳定的孤立路口 | Webster 定时,够用且免维护 |
| 流量波动大的孤立路口 | 感应控制 |
| 规则干线,需要连续通行 | 定时协调 + MAXBAND |
| 密集路网,易溢出 | Max-Pressure(性价比最高) |
| 有高保真仿真环境 + 长期投入 | RL,但先用 Max-Pressure 做 baseline |
一个反直觉的事实 :绝大多数 RL 信号控制论文对比的 baseline 是固定配时或简单感应控制,如果拿 Max-Pressure 做对比,优势会大幅缩水甚至消失。做研究的话,请务必把 Max-Pressure 加入 baseline,否则结论说服力有限。
八、怎么评估一套配时方案
只看单一指标一定会被误导,标准做法是同时看这几个:
| 指标 | 定义 | 说明 |
|---|---|---|
| 平均延误 | d ˉ = 1 N ∑ i d i \bar d = \frac{1}{N}\sum_i d_i dˉ=N1∑idi | 最常用,但会掩盖长尾 |
| 90 分位延误 | d 90 d_{90} d90 | 衡量公平性,防止某方向被饿死 |
| 平均停车次数 | 每车经过路口停车次数 | 直接关联油耗与排放 |
| 排队长度 | 周期最大排队 L max L_{\max} Lmax | 判断是否溢出,安全相关 |
| 通行量 | 单位时间通过车辆数 | 过饱和场景下的第一指标 |
| 溢出次数 | 排队超过路段长度的次数 | 过饱和路网最关键的指标 |
关于工具选择:
- SUMO:开源,微观仿真,TraCI 接口成熟,路网可从 OSM 导入。做研究首选。
- CityFlow:为 RL 优化,仿真速度比 SUMO 快一个数量级,但模型精度略低。跑大量训练回合时用它。
- VISSIM:商业软件,标定精细,国内交通设计院出方案基本用它,评审认可度高。
必须注意 :仿真结论迁移到真实世界的最大障碍不是算法,是参数标定。饱和流率、驾驶员反应时间、期望车头时距这些参数如果没有用本地实测数据标定,仿真里 30% 的提升到现场可能只剩 5%,甚至为负。
九、工程落地的十个坑
这一节是纯算法文章不会讲的,但恰恰是决定方案能不能上线的关键。
1. 最小绿灯时间由行人决定,不是由车决定。 行人过街所需时间:
G min = t 反应 + W v 行人 G_{\min} = t_{\text{反应}} + \frac{W}{v_{\text{行人}}} Gmin=t反应+v行人W
W W W 是过街宽度, v 行人 v_{\text{行人}} v行人 通常取 1.2 m/s(老年人多的区域取 1.0 m/s)。30 米宽的路口,最小绿灯至少 32 秒------任何算法都不能突破这个下限。
2. 黄灯时长与困境区。 黄灯太短会形成"两难区"(dilemma zone):驾驶员既刹不住,也过不去。安全黄灯时长:
A = t r + v 2 a + 2 g G A = t_r + \frac{v}{2a + 2gG} A=tr+2a+2gGv
t r t_r tr 为反应时间(1.0 s), a a a 为舒适减速度(3.0 m/s²), G G G 为道路坡度。50 km/h 平路约需 3.4 s。
3. 全红清空时间不能省。 R = W + L 车 v R = \frac{W + L_{\text{车}}}{v} R=vW+L车, W W W 为路口宽度。大路口需要 2~3 秒全红,省掉它会直接提高事故率。
4. 相位不能任意跳跃。 必须遵守环栅结构,且不能反向跳转。RL 直接输出相位编号的方案在真实信号机上往往无法落地,因为控制器固件不支持。
5. 检测器会坏。 线圈的年故障率在 10%~20% 量级。所有自适应算法必须有降级策略:检测失效时自动回退到定时方案,而不是按"队列=0"处理(那会导致该方向永远得不到绿灯)。
6. 公交与应急优先。 优先请求会打断正常配时,需要设计补偿机制在后续周期还回被占用的绿灯时间,否则被占用方向会持续积压。
7. 过饱和场景要换目标函数。 饱和度超过 1 时,延误最小化没有意义(延误理论上是无穷大),此时目标应改为最大化通行量 并防止溢出------这是完全不同的优化问题。
8. 相位差调整要渐进。 直接跳变相位差会造成某个周期出现异常长的红灯。工程做法是每周期最多调整 ±3~5 秒,逐步过渡到目标值。
9. 上游溢出会让协调方案失效。 排队溢出到上游路口后,绿波带里跑的是启动波而非自由流,此时应立即切换到排队管理模式,而不是坚持执行协调方案。
10. 驾驶员是有记忆的。 配时方案频繁变化会破坏驾驶员的预期,导致抢黄灯、急刹增多。稳定性本身就是一种价值,这也是为什么很多城市宁可用略微次优的固定配时。
十、总结与延伸阅读
回到开头的问题:为什么红灯是 60 秒?
现在你知道,这个数字可能来自 C 0 = 1.5 L + 5 1 − Y C_0 = \frac{1.5L+5}{1-Y} C0=1−Y1.5L+5 的计算,可能是为了和相邻路口凑成绿波而取的公共周期,也可能是三十米宽路口的行人过街时间倒逼出来的下限。
整个领域的技术演进可以概括成一条主线:
#mermaid-svg-am0rYnVaTXO2qjGF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-am0rYnVaTXO2qjGF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-am0rYnVaTXO2qjGF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-am0rYnVaTXO2qjGF .error-icon{fill:#552222;}#mermaid-svg-am0rYnVaTXO2qjGF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-am0rYnVaTXO2qjGF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-am0rYnVaTXO2qjGF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-am0rYnVaTXO2qjGF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-am0rYnVaTXO2qjGF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-am0rYnVaTXO2qjGF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-am0rYnVaTXO2qjGF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-am0rYnVaTXO2qjGF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-am0rYnVaTXO2qjGF .marker.cross{stroke:#333333;}#mermaid-svg-am0rYnVaTXO2qjGF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-am0rYnVaTXO2qjGF p{margin:0;}#mermaid-svg-am0rYnVaTXO2qjGF .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-am0rYnVaTXO2qjGF .cluster-label text{fill:#333;}#mermaid-svg-am0rYnVaTXO2qjGF .cluster-label span{color:#333;}#mermaid-svg-am0rYnVaTXO2qjGF .cluster-label span p{background-color:transparent;}#mermaid-svg-am0rYnVaTXO2qjGF .label text,#mermaid-svg-am0rYnVaTXO2qjGF span{fill:#333;color:#333;}#mermaid-svg-am0rYnVaTXO2qjGF .node rect,#mermaid-svg-am0rYnVaTXO2qjGF .node circle,#mermaid-svg-am0rYnVaTXO2qjGF .node ellipse,#mermaid-svg-am0rYnVaTXO2qjGF .node polygon,#mermaid-svg-am0rYnVaTXO2qjGF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-am0rYnVaTXO2qjGF .rough-node .label text,#mermaid-svg-am0rYnVaTXO2qjGF .node .label text,#mermaid-svg-am0rYnVaTXO2qjGF .image-shape .label,#mermaid-svg-am0rYnVaTXO2qjGF .icon-shape .label{text-anchor:middle;}#mermaid-svg-am0rYnVaTXO2qjGF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-am0rYnVaTXO2qjGF .rough-node .label,#mermaid-svg-am0rYnVaTXO2qjGF .node .label,#mermaid-svg-am0rYnVaTXO2qjGF .image-shape .label,#mermaid-svg-am0rYnVaTXO2qjGF .icon-shape .label{text-align:center;}#mermaid-svg-am0rYnVaTXO2qjGF .node.clickable{cursor:pointer;}#mermaid-svg-am0rYnVaTXO2qjGF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-am0rYnVaTXO2qjGF .arrowheadPath{fill:#333333;}#mermaid-svg-am0rYnVaTXO2qjGF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-am0rYnVaTXO2qjGF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-am0rYnVaTXO2qjGF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-am0rYnVaTXO2qjGF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-am0rYnVaTXO2qjGF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-am0rYnVaTXO2qjGF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-am0rYnVaTXO2qjGF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-am0rYnVaTXO2qjGF .cluster text{fill:#333;}#mermaid-svg-am0rYnVaTXO2qjGF .cluster span{color:#333;}#mermaid-svg-am0rYnVaTXO2qjGF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-am0rYnVaTXO2qjGF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-am0rYnVaTXO2qjGF rect.text{fill:none;stroke-width:0;}#mermaid-svg-am0rYnVaTXO2qjGF .icon-shape,#mermaid-svg-am0rYnVaTXO2qjGF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-am0rYnVaTXO2qjGF .icon-shape p,#mermaid-svg-am0rYnVaTXO2qjGF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-am0rYnVaTXO2qjGF .icon-shape .label rect,#mermaid-svg-am0rYnVaTXO2qjGF .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-am0rYnVaTXO2qjGF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-am0rYnVaTXO2qjGF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-am0rYnVaTXO2qjGF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 定时控制
Webster 1958
协调控制
MAXBAND 1981
自适应系统
SCOOT / SCATS
理论最优
Max-Pressure 2013
学习型控制
MARL
但请记住三条不会过时的判断:
- 信号优化的上限是路口的物理通行能力。 Y ≥ 1 Y \ge 1 Y≥1 时,再好的算法也救不了,该做的是渠化改造。
- Max-Pressure 是当前性价比最高的自适应方案。 理论有保证、不需要标定到达率、天然带溢出保护、代码不到一百行。
- 约束比目标函数更重要。 最小绿灯、清空时间、检测器降级------这些不满足,方案根本上不了线。
参考文献
- Webster, F.V. (1958). Traffic Signal Settings. Road Research Technical Paper No. 39, HMSO.
- Little, J.D.C., Kelson, M.D., Gartner, N.H. (1981). MAXBAND: A Program for Setting Signals on Arteries and Triangular Networks. Transportation Research Record, 795.
- Varaiya, P. (2013). Max pressure control of a network of signalized intersections. Transportation Research Part C, 36, 177--195.
- Tassiulas, L., Ephremides, A. (1992). Stability properties of constrained queueing systems and scheduling policies for maximum throughput in multihop radio networks. IEEE Transactions on Automatic Control, 37(12).
- Transportation Research Board. Highway Capacity Manual (HCM), 7th Edition.
- Wei, H. et al. (2019). PressLight: Learning Max Pressure Control to Coordinate Traffic Signals in Arterial Network. KDD 2019.
- 中华人民共和国住房和城乡建设部. 《城市道路交叉口设计规程》CJJ 152.
本文所有代码可直接运行,仿真部分建议配合 SUMO 或 CityFlow 使用。如果你在实际项目中遇到过其他坑,欢迎评论区交流。