用Python实现交通模拟器

你是否曾经被困在短暂的交通堵塞中,然后它又莫名其妙地消失了,完全找不到原因?欢迎来到令人沮丧的幽灵交通拥堵世界。

在本指南中,我们将利用交通工程物理学来理解这些瓶颈为何会发生。然后,我们将从零开始构建一个交互式的 Python 交通模拟。使用 智能驾驶员模型(IDM)框架,我们将绘制出人类反应延迟如何产生大规模的车流波动。

在这个 Plotly Dash 动画教程结束时,你将拥有一个实时仪表板,证明交通拥堵如何凭空形成。以下是一个使用 600 米圆形赛道的示例:

在这个模拟中,蓝色圆圈标记的汽车执行了一次"正常"的刹车操作,导致其后方出现拥堵。由此产生的交通拥堵的强度和持续时间取决于道路上的汽车数量(交通密度)、汽车之间的间距以及刹车事件的严重程度。

随着模拟的运行,我们将测试幽灵交通拥堵对交通密度、安全行驶距离和刹车力度等参数的敏感性。我们还将探索如何在自动驾驶系统中使用智能算法(如自适应巡航控制)来减少它们的发生。

1、幽灵交通拥堵

我正在达拉斯和休斯顿之间的 I-45 公路上行驶。四月,野花盛开,我感觉自己像是在一幅莫奈的画中穿行。道路很繁忙,但我能够超速行驶,这在德克萨斯州很常见。接下来,我前面的人猛踩刹车。我也猛踩刹车,并停在路肩上以避免撞到他们。我身后传来轮胎的尖叫声,我做好了撞击的准备,但没事。几分钟后,我们又都开始超速行驶了。

我环顾四周寻找事故、封闭的车道、道路上的碎片,任何能解释刚才发生的事情的东西,但什么都没有。我刚刚经历了一次"幽灵交通拥堵"。

这种现象在道路接近其有效承载能力时出现。此时,交通流对即使是微小的干扰也变得高度敏感。轻点刹车或大型卡车变道都可能将系统推入不稳定状态。

这种不稳定性源于交通密度、跟车距离和驾驶员反应时间的相互作用。在适当的条件下,一个驾驶员轻微减速通常会导致后面的驾驶员更猛烈地刹车。每个后续驾驶员往往会放大反应,通过车流产生向后传播的减速波。随着波的增长,更上游的车辆可能被迫完全停止,即使最初的车辆只是稍微减速。

2、智能驾驶员模型

在编写 Python 代码之前,我们需要了解交通拥堵的物理学原理。为此,我们可以参考 Treiber、Hennecke 和 Helbing 于 2000 年开发的智能驾驶员模型(IDM)

交通不像水流过管道那样建模;它被建模为对周围环境做出反应的单个对象的集合。为了模拟这一点,我们使用"跟车模型",其黄金标准就是 IDM。

IDM 的美妙之处在于它成功地在一个优雅的微分方程中捕捉了人类心理学。它假设每个驾驶员都在不断平衡两个相互竞争的欲望:

  1. 自由流行为: 加速到目标巡航速度的欲望。
  2. 交互行为: 为了与前车保持安全距离而刹车的本能。

2.1 核心方程

在 IDM 中,给定车辆在任意时刻的加速度计算如下:

其中:

  • a_max 是车辆的最大舒适加速度
  • v 是汽车的当前速度
  • v₀目标巡航速度(例如,限速)。
  • s 是与前车的实际间距(距离)。
  • s*期望最小间距(安全跟车距离)

这个方程分为两半。第一部分 (1 — (v/v₀)⁴),包含速度分量,决定开阔道路驾驶。第二部分 (s*/s)² 是刹车项。

如果前方道路空旷(s 非常大),刹车项消失。汽车将以 a_max 加速,直到其速度 v 接近目标速度 v₀,此时加速度平滑降至零。

如果实际间距 s 降至期望间距 s* 以下,此项迅速主导方程,导致剧烈减速。

2.2 计算期望间距(s*

IDM 的真正天才之处在于它如何计算期望最小间距 s*。人类驾驶员不会始终保持静态的 10 米间距。他们的期望间距会根据速度和接近前车的速度动态变化。

期望间距计算如下:

以下是驱动此方程的因素:

  • s₀拥堵距离(红灯处停车时汽车之间的最小物理间距,通常约 2 米)。
  • T安全时间间距(驾驶员的反应时间余量,例如 1.4 秒)。
  • Delta v接近速率(本车与前车之间的速度差)。
  • b舒适刹车减速度

2.3 这如何产生"幽灵"波

仔细观察期望间距方程中的最后一项:

这是幽灵交通拥堵的数学催化剂。

想象一排汽车以稳定的平衡状态愉快地行驶。突然,前车轻点刹车。对于紧跟其后的汽车,接近速率(Delta v)急剧上升。由于此项乘以当前速度(v),以高速公路速度行驶会大大放大这种效果。

后车驾驶员的大脑(由 IDM 表示)立即计算出其期望间距(s*)必须远大于其实际间距(s)。IDM 方程通过比前车稍重的刹车力来响应。

当这种计算向后传递到一排汽车中时,每个后续驾驶员都比前一个刹车稍重一点。车队前部 10% 的速度减少可能会在几辆车后放大为 100% 的减少(完全停止)。初始干扰随车辆向前移动,但由此产生的冲击波通过交通流向后传播,凭空制造出拥堵。

3、设计道路

模拟中使用的一个重要考虑是道路设计。我们将使其成为圆形,以便在显示中很好地展示,但我们如何确定其长度?

首先,我们需要知道限速。对于此模拟,我们将使用 22 m/s(略低于 50 mph)。

接下来,我们需要知道道路可以处理的最大间距。这些是驾驶员为了安全而保持的汽车之间的间距,以为单位(我们使用秒是因为驾驶员判断时间比判断距离更容易)。我们将从不安全的 1 秒间距开始,一直到更安全的 4 秒间距。

为了确定道路可以处理的最大间距,我们必须查看以 22 m/s 的目标速度行驶的汽车所需的空间。

每辆汽车需要 6.5 米的基线静态空间(4.5 米物理长度 + 2.0 米最小拥堵距离)。时间间距决定了剩余空间。

注意: 拥堵距离是汽车在红灯处停车时彼此之间的距离(约 2 米)。

如果我们使用 1200 米的道路:

  • 15 辆车时,每辆车有 80 米的总空间(1200 / 15)。减去 6.5 米静态空间,还剩 73.5 米用于时间间距。在 22 m/s 时,最大间距为 3.34 秒(73.5 / 22)。
  • 25 辆车时,每辆车只有 48 米的空间。最大间距降至仅 1.88 秒

让我们看看实际意义。以下是 15 辆车在 1.2 公里道路上,时间间距为 3 秒,"正常"刹车力度为 3.5 m/s² 的情况:

在这种情况下,只有轻微干扰,没有幽灵交通拥堵。条件(交通密度、时间间距和刹车强度)过于平衡,轻微刹车事件不会产生太大影响。

现在,让我们通过增加十辆车使道路更拥堵:

道路上有 25 辆车时,驾驶员无法再维持 3 秒的目标间距。因此,你首先注意到的是所有汽车标记都变成浅绿色,表示它们正在减速以实现期望间距。然后,在刹车事件之后,发生幽灵拥堵。

现在变得有趣了。让我们将时间间距改为 1 秒,看看拥堵无法形成:

这不是支持尾随的论点!我们模拟中的虚拟驾驶员具有完美的反应时间,但人类没有。在现实生活中,这种行为会导致事故。但这确实为自动驾驶汽车和自适应巡航控制提出了一个有趣的观点:如果时间间距规则过于严格,交通流可能会受到干扰。这可以通过在特定情况下允许缓冲来缓解(即,在刹车时允许时间间距减小)。我们将在本文末尾探讨这种情况。

注意: 这里没有碰撞检测代码。代码不会说*"如果新位置 > 前方位置,则停在前方位置。"*它只是盲目地向前移动坐标。如果数学计算指示重叠,Plotly 会愉快地将两个标记绘制在彼此之上。
但是,当事故发生时,代码不会崩溃,这是因为地板值:np.maximum(gaps, MIN_GAP)。如果两辆汽车重叠,真正的 gaps 值变为负数。np.maximum 方法将该负数强制提升到 0.1。IDM 数学看到一个微小的 0.1 米间距,惊慌失措,并计算出巨大的负加速度以尝试分离它们。在屏幕上,这通常看起来像一辆汽车穿过另一辆,后面的汽车剧烈减速或向后闪烁。
通过将刹车干扰延迟到第 100 步,我们为模拟提供了 10 秒的"预热期"。这允许车辆动态调整速度并根据选定的安全时间间距进入自然平衡,然后再面对交通冲击波。没有这个"稳定"时间,初始瞬态间距误差可能会触发过早、不现实的事故。

4、刹车力度

幽灵交通拥堵的一个特点是,它们可能由正常到剧烈的刹车事件引起,但不会使刹车的汽车完全停止。但什么构成"正常"和"剧烈"刹车?

得益于远程信息处理和车队监控系统,我们有一些有用的刹车事件经验法则:

在模拟中,2–4 m/s² 的减速度范围和 5 秒刹车间隔进行评估。此范围代表驾驶员对意外道路事件(如较慢行驶的十八轮卡车突然并入同一车道)的反应。

现在我们理解了数学原理,可以将其转化为向量化的 Python 模拟。

5、代码

模拟代码是在 JupyterLab 中编写的。你可以直接从本文复制它到单元格中,或从此 Gist 下载:幽灵交通拥堵模拟器

要作为独立脚本运行,你需要:

  • 将代码保存在名为 traffic_sim.py 的文件中。
  • 确保本地 Python 环境中安装了所需的库(NumPy、Plotly、Dash)
  • 从终端运行脚本:python traffic_sim.py
  • 终端将输出本地 Web 地址(通常是 [http://127.0.0.1:8050/](http://127.0.0.1:8050/))。在 Web 浏览器中打开该链接以查看和交互仪表板。

要在 Google Colab 中运行,你需要:

  • 安装 Dash。
  • 将执行行从:
if __name__ == '__main__':
    app.run(debug=True)

更新为:

if __name__ == '__main__':
    app.run(jupyter_mode="inline", debug=False)

以下是模拟和仪表板的完整注释代码。库版本为:Python 3.14.2NumPy 2.5.1Plotly 6.7.0Dash 3.4.0

import numpy as np
import plotly.graph_objects as go
from dash import Dash, dcc, html, Input, Output

# ----------------------------
# Global Constants & Static Variables
# ----------------------------
# Road Geometry
THETA = np.linspace(0, 2 * np.pi, 300)
ROAD_X = np.cos(THETA)
ROAD_Y = np.sin(THETA)
ROAD_LENGTH = 1200.0

# Time and Duration
SIM_DT = 0.1
SIM_STEPS = 1800

# Vehicle & Driver Characteristics
CAR_LENGTH = 4.5          # m
DESIRED_SPEED = 22.0      # m/s; close to 50 mph
MAX_ACCEL = 1.2           # m/s^2
COMFORTABLE_BRAKE = 2.0   # m/s^2
REACTION_DELAY_STEPS = 10  # Translates to human reaction time of 1 s
JAM_DISTANCE = 2.0        # m
MIN_GAP = 0.1             # floor value in meters

# Disturbance Parameters
BRAKE_START_STEP = 100
BRAKE_END_STEP = 150

# Animation Settings
FRAME_STRIDE = 4
FRAME_DURATION_MS = 120

# -----------------------------------
# Helper Functions for User Interface
# -----------------------------------
def _get_gap_marks():
    """Generates the formatted marks for the safe gap slider."""
    marks = {}
    for i in np.arange(1.0, 4.2, 0.2):
        val = round(i, 1)
        is_int = val.is_integer()
        k = int(val) if is_int else val
        v = str(int(val)) if is_int else str(val)
        marks[k] = v
    return marks

def _get_brake_marks():
    """Generates the formatted marks for the braking slider."""
    return {
        round(i, 1): str(round(i, 1))
        for i in np.arange(2.0, 4.1, 0.5)}

# ----------------------------
# Core Simulation Function
# ----------------------------
def generate_traffic_fig(N, safe_time_gap, initial_brake):
    """Generates animated Plotly figure of traffic on a circular road."""
    N = int(N)
    state_key = f"{N}-{safe_time_gap}-{initial_brake}"

    # Initialization
    all_positions = np.zeros((SIM_STEPS, N))
    all_speeds = np.zeros((SIM_STEPS, N))
    all_positions[0] = np.linspace(0, ROAD_LENGTH, N, endpoint=False)
    all_speeds[0] = np.ones(N) * DESIRED_SPEED

    def acceleration_vectorized(my_speeds, front_speeds, gaps):
        """Calculates instantaneous acceleration for all vehicles using the IDM."""
        v_diff = my_speeds - front_speeds
        sqrt_ab = np.sqrt(MAX_ACCEL * COMFORTABLE_BRAKE)

        desired_gaps = (
            JAM_DISTANCE
            + my_speeds * safe_time_gap
            + (my_speeds * v_diff) / (2 * sqrt_ab))

        accel_term = (my_speeds / DESIRED_SPEED)**4
        gap_term = (desired_gaps / np.maximum(gaps, MIN_GAP))**2

        return MAX_ACCEL * (1 - accel_term - gap_term)

    # Main simulation loop
    for t in range(SIM_STEPS - 1):
        delay_t = max(0, t - REACTION_DELAY_STEPS)
        del_pos = all_positions[delay_t]
        del_spd = all_speeds[delay_t]

        # Shift array to get front car states; modulo handles road wrap
        front_pos = np.roll(del_pos, -1)
        gaps = (front_pos - del_pos) % ROAD_LENGTH - CAR_LENGTH

        front_spd = np.roll(del_spd, -1)
        accels = acceleration_vectorized(all_speeds[t], front_spd, gaps)

        # Initial disturbance using a boolean multiplier
        is_braking = BRAKE_START_STEP < t < BRAKE_END_STEP
        accels[0] -= initial_brake * is_braking

        # Inline updates
        new_speeds = all_speeds[t] + accels * SIM_DT
        all_speeds[t + 1] = np.maximum(new_speeds, 0.0)

        new_positions = all_positions[t] + all_speeds[t + 1] * SIM_DT
        all_positions[t + 1] = new_positions % ROAD_LENGTH

    # Plotly setup
    all_angles = 2 * np.pi * all_positions / ROAD_LENGTH
    all_x, all_y = np.cos(all_angles), np.sin(all_angles)

    # Load all static traces at once
    fig = go.Figure(data=[
        go.Scatter(
            x=ROAD_X, y=ROAD_Y, mode="lines",
            line=dict(color="black", width=3),
            hoverinfo="skip", showlegend=False
        ),
        go.Scatter(
            x=all_x[0], y=all_y[0], mode="markers", showlegend=False,
            marker=dict(
                size=14, color=all_speeds[0], colorscale="RdYlGn",
                cmin=0, cmax=DESIRED_SPEED,
                colorbar=dict(title="Speed<br>m/s")
            ),
            text=[f"Car {i}" for i in range(N)],
            hovertemplate="%{text}<br>Speed: %{marker.color:.1f} m/s"
                          "<extra></extra>"
        ),
        go.Scatter(
            x=[all_x[0][0]], y=[all_y[0][0]],
            mode="markers", showlegend=False,
            marker=dict(
                size=24, color="rgba(0,0,0,0)",
                line=dict(color="blue", width=4)
            ),
            hoverinfo="skip"
        )
    ])

    # Build frames using raw dictionaries for faster Plotly rendering
    fig.frames = [
        dict(
            data=[
                dict(
                    x=all_x[f], y=all_y[f],
                    marker=dict(color=all_speeds[f])
                ),
                dict(x=[all_x[f][0]], y=[all_y[f][0]])
            ],
            traces=[1, 2], name=f"{state_key}_{f}"
        ) for f in range(0, SIM_STEPS, FRAME_STRIDE)
    ]

    slider_steps = [
        dict(
            method="animate", label=f"{f * SIM_DT:.0f}s",
            args=[
                [f"{state_key}_{f}"],
                dict(
                    mode="immediate",
                    frame=dict(duration=0, redraw=False),
                    transition=dict(duration=0)
                )
            ]
        ) for f in range(0, SIM_STEPS, FRAME_STRIDE)
    ]

    # Pre-define Play/Pause arguments to keep layout clean
    play_args = [
        None,
        dict(
            frame=dict(duration=FRAME_DURATION_MS, redraw=False),
            transition=dict(duration=0),
            fromcurrent=True, mode="immediate"
        )
    ]
    pause_args = [
        [None],
        dict(
            frame=dict(duration=0, redraw=False),
            transition=dict(duration=0),
            mode="immediate"
        )
    ]

    fig.update_layout(
        title=dict(text="Phantom Traffic Jam on a Circular Road", x=0.5),
        uirevision=state_key, width=610, height=585,
        xaxis=dict(
            range=[-1.35, 1.35], visible=False,
            scaleanchor="y", scaleratio=1
        ),
        yaxis=dict(range=[-1.35, 1.35], visible=False),
        plot_bgcolor="white", margin=dict(t=50, b=50, l=50, r=50),
        updatemenus=[
            dict(
                type="buttons", showactive=False, x=0.1, y=-0.05,
                direction="left",
                buttons=[
                    dict(label="Play", method="animate", args=play_args),
                    dict(label="Pause", method="animate", args=pause_args)
                ]
            )
        ],
        sliders=[
            dict(active=0, x=0.1, y=-0.12, len=0.8, steps=slider_steps)
        ]
    )

    return fig

# ----------------------------
# Dash Application Setup
# ----------------------------
app = Dash(__name__)

# Common styles used across elements
hdr_style = {'textAlign': 'center', 'fontFamily': 'Arial'}
lbl_style = {'fontWeight': 'bold', 'fontFamily': 'Arial'}
main_flex = {
    'display': 'flex', 'flexDirection': 'row',
    'justifyContent': 'center', 'alignItems': 'center'
}
col_flex = {
    'display': 'flex', 'flexDirection': 'column',
    'justifyContent': 'center', 'width': '350px',
    'paddingLeft': '25px'
}
alert_style = {
    'backgroundColor': '#fff3cd',
    'color': '#856404',
    'border': '1px solid #ffeeba',
    'borderRadius': '5px',
    'padding': '12px',
    'marginBottom': '30px',
    'fontFamily': 'Arial',
    'fontSize': '14px',
    'lineHeight': '1.4'
}

app.layout = html.Div([
    html.H2("Traffic Simulation Controller", style=hdr_style),

    # Main container: Side-by-side flex layout
    html.Div([

        # Left Side: The Graph Container
        html.Div(id='graph-container'),

        # Right Side: Vertical Column for Sliders
        html.Div([

            # --- ANNOTATION BOX ---
            html.Div([
                html.Span("⚠️ Note: ", style={'fontWeight': 'bold'}),
                "Please press the ",
                html.Span(
                    "Pause",
                    style={'fontWeight': 'bold', 'textDecoration': 'underline'}
                ),
                " button before adjusting the parameters."
            ], style=alert_style),

            html.Div([
                html.Label('Number of Cars:', style=lbl_style),
                dcc.Slider(
                    id='cars-slider', min=15, max=25, step=1, value=20,
                    marks={i: str(i) for i in range(15, 26)}
                )
            ], style={'marginBottom': '40px'}),

            html.Div([
                html.Label('Safe Time Gap (s):', style=lbl_style),
                dcc.Slider(
                    id='gap-slider', min=1.0, max=4.0, step=0.1, value=3.0,
                    marks=_get_gap_marks()
                )
            ], style={'marginBottom': '40px'}),

            html.Div([
                html.Label('Initial Braking Magnitude (m/s²):', style=lbl_style),
                dcc.Slider(
                    id='brake-slider', min=2.0, max=4.0, step=0.1, value=3.5,
                    marks=_get_brake_marks()
                )
            ])

        ], style=col_flex)

    ], style=main_flex)
])

@app.callback(
    Output('graph-container', 'children'),
    [
        Input('cars-slider', 'value'),
        Input('gap-slider', 'value'),
        Input('brake-slider', 'value')
    ]
)
def update_simulation(num_cars, time_gap, initial_brake):
    fig = generate_traffic_fig(num_cars, time_gap, initial_brake)

    # Create unique ID for every single combination of slider values
    uid = f"graph-{num_cars}-{time_gap}-{initial_brake}"

    # Force Dash/React to destroy the old component and wipe memory
    return dcc.Graph(
        id=uid,
        figure=fig,
        config={'displayModeBar': False}
    )

if __name__ == '__main__':
    app.run(debug=True)

在下一节中,我们将研究如何改进加速控制以减少幽灵交通拥堵的可能性。

6、自适应时间间距

之前的模拟假设所有驾驶员都试图与前车保持恒定的时间间距。虽然这种简化对于说明幽灵交通拥堵如何出现很有用,但它并不能准确反映真实的驾驶行为。当车辆突然刹车时,驾驶员通常愿意——有时被迫——暂时减少跟车距离,以避免过度减速。这种反应不仅对乘客更舒适,而且通过降低被后车追尾的风险来提高安全性。

严格的自适应巡航控制(ACC)系统如果不编程协作逻辑,可能会加剧交通拥堵。通过引入智能算法,它们可以模仿人类驾驶员并灵活管理刹车情况。为了在模拟中加入这种行为,最简单且最现实的方法是在 acceleration_vectorized 函数中引入自适应时间间距

我们不再将 safe_time_gap 视为严格规则,而是根据汽车的实际行驶速度对其进行缩放。如果速度下降,驾驶员愿意接受更小的间距。

我们可以这样计算动态间距:

# 间距随着汽车速度下降而缩小,最小为 1.0 秒
dynamic_time_gap = 1.0 + (safe_time_gap - 1.0) * (my_speeds / DESIRED_SPEED)

注意术语:(safe_time_gap - 1.0)。因为我们使用 1.0 秒作为驾驶员完全停止时接受的绝对最小"基础"间距,所以我们必须从滑块值中减去该基线,以免在全速时意外地重复计算它。

以下是重构的 acceleration_vectorized 函数:

    def acceleration_vectorized(my_speeds, front_speeds, gaps):
        """Calculates acceleration with a speed-adaptive time gap."""
        v_diff = my_speeds - front_speeds
        sqrt_ab = np.sqrt(MAX_ACCEL * COMFORTABLE_BRAKE)

        # Adapt gap based on speed: lower speeds = tighter acceptable gaps
        base_gap = np.minimum(safe_time_gap, 1.0)
        dynamic_gap = base_gap + (
            (safe_time_gap - base_gap) * (my_speeds / DESIRED_SPEED)
        )

        desired_gaps = (
            JAM_DISTANCE
            + my_speeds * dynamic_gap
            + (my_speeds * v_diff) / (2 * sqrt_ab)
        )

        accel_term = (my_speeds / DESIRED_SPEED)**4
        gap_term = (desired_gaps / np.maximum(gaps, MIN_GAP))**2

        return MAX_ACCEL * (1 - accel_term - gap_term)

通过使时间间距动态化,我们向物理模型注入了真实的人类心理学层。这消除了"过度反应恐慌",让交通表现得更像可压缩流体。现在,随着前车刹车和车队速度下降一半,期望时间间距也减半(降至允许的最小值)。这使得汽车队列可以紧密压缩在一起,而不会像之前的代码那样触发恐慌刹车冲击波。

在交通工程中,这种行为增加了道路的"容量状态"。模拟现在可以优雅地处理更大的间距设置。汽车刹车时仍会有轻微波动,但波会被吸收和消散,而不是迫使圆形赛道的一部分完全停止。

以下是 20 辆车、3 秒时间间距和 4 m/s² "剧烈"刹车的示例:

注意标记如何压缩在一起以消散刹车事件的冲击。在对几辆车造成短暂干扰后,交通恢复到初始状态。

7、结束语

计算机建模提供了一种有效且经济高效的方式来探索和理解复杂系统。在这个例子中,我们看到看似微不足道的刹车事件如何在交通中产生连锁反应,产生熟悉但常常令人困惑的幽灵交通拥堵现象。像这样的模拟洞察可以帮助指导自动驾驶汽车和自适应巡航控制系统的设计,实现更智能的控制策略——例如动态调整跟车距离——以改善交通流、安全性和驾驶舒适度。


原文链接: Why Traffic Suddenly Stops for No Apparent Reason

汇智网翻译整理,转载请标明出处