Python 中的交通流量模拟2025年3月17日 | 阅读 24 分钟 众所周知,交通流量并非总是顺畅的;然而,车辆流畅地通过十字路口、转弯和在交通信号灯前停车,看起来会很壮观。这一观察让我们开始思考交通流量对人类文明的重要性。 在接下来的教程中,我们将了解交通模拟的重要性。我们还将比较模拟交通的各种可能方法,最后演示一个带有源代码的模拟。 了解交通流量模拟的重要性交通模拟的关键目的是在真实世界之外生成数据。我们不需要在真实世界中测试管理交通系统的新想法或通过传感器收集数据,而是可以使用在软件上执行的模型来预测交通流量。该模型支持加速交通系统的优化和数据收集。模拟比真实世界测试更便宜、更快。 训练机器学习 (ML) 模型需要大量数据集,这些数据集可能复杂且昂贵。通过模拟交通流量程序化地生成数据,可以轻松地进行修改以满足所需数据的精确时间。 建模我们将从对交通系统进行建模开始,以便以数学方式分析和优化交通系统。这样的模型应根据输入参数(道路网络几何形状、每分钟车辆数、车速等)真实地描绘交通流量。 根据操作的级别,交通系统模型通常分为三类:
在接下来的教程中,我们将使用微观模型。 理解微观模型微观驾驶员模型描述了单个驾驶员/车辆的行为。因此,它必须是一个多主体系统;也就是说,每辆车都独立运行,并从其环境中获取输入。 ![]() 在微观模型中,每辆车都被编号为 *i*。第 *i* 辆车跟随第 (*i*-1) 辆车。我们将第 *i* 辆车沿道路的位置表示为 *xi*,其速度表示为 *vi*,其长度表示为 *li*。这对所有车辆都适用。 ![]() 我们将车辆碰撞点之间的距离表示为 *si*,将第 *i* 辆车与其前车(编号为 *i*-1 的车辆)之间的速度差表示为 *∆vi*。 理解智能驾驶员模型 (IDM)2000 年,Treiber、Hennecke 和 Helbing 开发了一个名为智能驾驶员模型 (IDM) 的模型。该模型根据第 *i* 辆车的可变参数和其前车的可变参数来说明第 *i* 辆车的加速度。我们可以将动力学方程定义如下: ![]() 既然我们已经见过 *si*、*vi* 和 ∆*vi*,那么其他参数如下: 1. s₀ᵢ: 此参数是车辆 *i* 和 *i*-1 之间的最小期望距离。 2. v₀ᵢ: 此参数是车辆 *i* 的最大期望速度。 3. δ: 此参数是加速度指数,它控制加速度的“平滑度”。 4. Tᵢ: 此参数是车辆 *i* 驾驶员的反应时间。 5. aᵢ: 此参数是车辆 *i* 的最大加速度。 6. bᵢ: 此参数是车辆 *i* 的舒适减速度。 7. s^*: 此参数是车辆 *i* 和 *i*-1 之间的期望距离。 首先,我们将查看 *s*,它是一个由三项组成的距离。 ![]() 8. s₀ᵢ: 如前所述,此参数是最小期望距离。 9. vᵢTᵢ: 此参数是反应时间安全距离。这是驾驶员在做出反应(刹车)之前车辆行驶的距离。 由于速度是距离除以时间,因此距离是速度乘以时间。 ![]() 10. (vᵢ Δvᵢ)/√(2aᵢ bᵢ): 此参数是一个更复杂的项。它是一个基于速度差的安全距离。它表示车辆在不刹车过猛(减速度应小于 *bᵢ*)的情况下减速(不撞上前车)所需的时间。 理解智能驾驶员模型的工作原理我们可以假设车辆沿直线路径移动并遵循以下方程: ![]() 为了更好地理解上述方程,我们可以将其分为两部分。我们有自由道路加速度和交互加速度。 ![]() 自由道路加速度是在空旷道路上(没有前方车辆)的加速度。如果我们以车速 *vi* 为函数绘制加速度,我们将得到以下结果: ![]() 图片:加速度与速度的关系图 在上图中,我们注意到当车辆静止(*vi* = 0)时,加速度最大。当车速接近最大速度 *v01* 时,加速度变为零。这个陈述意味着自由道路的加速度会将车辆加速到最大速度。 如果我们想为不同的 *δ* 值绘制 v-a 图,我们将注意到它控制着驾驶员在接近最大速度时减速的快慢,进而调节加速度/减速度的平滑度。 ![]() 图片:加速度与速度的关系图 ![]() 交互加速度与与前车的交互有关。我们可以通过考虑以下情况来更好地理解其工作原理: 在自由道路上(*vi* >> *s*^*) 当前车很远时,距离 *si* 支配着期望距离 *s*^*,交互几乎为 0。 这表明我们可以通过自由道路加速度来控制车辆。 ![]() 在高速接近率下(∆*vi*) 当速度差很高时,交互加速度试图通过(vi ![]() 在小的距离差下(?? << 1 且 ∆?? = 0) 加速度变成一个简单的排斥力。 ![]() 理解交通道路网络模型![]() 图片:有向图示例 Set ![]() 我们必须对道路网络进行建模。我们可以通过有向图 *G = (V,E)* 来实现,其中:
每辆车都有一个由多条道路(边)组成的路径。我们将在同一条道路(同一条边)上对车辆应用智能驾驶员模型。当车辆到达道路末端时,我们可以将其从该道路移除并添加到其下一条道路。 我们不会在模拟中维护一个节点集(数组)。然而,每条道路都将通过其起始节点和结束节点的显式值来定义。 理解随机车辆生成器我们有两种选择来将车辆添加到模拟中: 选项一:我们可以通过创建 `Vehicle` 类的实例并将其添加到车辆列表中来手动添加每辆车到模拟中。 选项二:我们也可以根据预定义的概率使用随机方法来添加车辆。 为了选择第二个选项,我们需要定义一个随机车辆生成器。 我们可以通过两个约束来定义随机车辆生成器:
![]() 随机车辆生成器以概率 *pi* 生成车辆 *Vi*。 理解交通信号灯![]() 交通信号灯放置在顶点,并由两个区域表征:
交通流量模拟的项目代码对于这个项目,我们将采用面向对象的方法。因此,每条道路和每辆车都必须定义为一个类。 我们将在接下来的各种类中频繁使用以下初始化函数。此函数允许我们通过 `set_default_config` 函数设置当前类的默认配置。它还接收一个字典,并将字典中的每个属性设置为当前类实例的属性。这样,就不需要担心更新不同类的 `__init__` 函数或未来的更改了。 让我们看下面的代码片段来说明这一点: 文件:init.py 说明 在上面的代码片段中,我们定义了接受 `config` 作为字典的 `__init__()` 函数。在此函数中,我们使用了 `set_default_config()` 函数来设置默认配置。然后,我们使用 `for` 循环遍历 `config` 字典中的属性和值,并使用 `setattr()` 函数更新不同类的配置。 Road我们现在将创建一个 `Road` 类。让我们看下面的代码片段来演示这一点: 文件:road.py 说明 在上面的代码片段中,我们从 SciPy 包导入了 `distance` 函数,并在此类中定义了一个名为 `Road` 的类。我们在 `__init__()` 函数中初始化了一些参数,如 `self`、`start` 和 `end`。然后,我们定义了另一个名为 `initProperties()` 的函数,该函数计算道路长度的左侧以及绘制它在屏幕上的角度的正弦和余弦。 Simulation我们现在将创建一个 `Simulation` 类,并添加一些方法来将道路添加到模拟中。现在让我们看下面的代码片段来演示这一点: 文件:simulator.py 说明 在上面的代码片段中,我们从 `road.py` 文件导入了 `Road` 类。然后,我们定义了一个名为 `Simulation` 的类。我们在该类中使用了之前定义的初始化函数。然后,我们定义了另一个名为 `set_default_config()` 的函数,并将一些属性值设置为默认设置。然后,我们定义了 `createRoad()` 和 `createRoads()` 函数来创建一条或多条道路。 Window我们现在将实时在屏幕上显示模拟。为此,我们将使用 `pygame` 库并创建一个 `Window` 类,该类将 `Simulation` 类作为参数。 我们将定义支持绘制基本形状的各种绘图函数。 `loop` 方法创建一个 `pygame` 窗口,并在每一帧调用 `draw` 方法和 `loop` 参数。当模拟需要每帧更新时,这将非常有用。 让我们看下面的代码片段来说明这一点: 文件:window.py 说明 在上面的代码片段中,我们导入了 `pygame` 库。然后,我们创建了一个名为 `Window` 的类。我们在该类中初始化了一些参数,并设置了默认配置。然后,我们定义了 `loop` 函数,该函数显示一个可视化模拟的窗口并执行 `loop` 函数。然后,我们定义了 `convert`、`inverseConvert`、`the_background`、`the_line`、`the_rect`、`the_box`、`the_circle`、`the_polygon`、`the_rotated_box`、`the_rotated_rect`、`drawAxes`、`drawGrid`、`drawRoads`、`drawStatus` 和 `draw` 等函数。 我们将上述文件保存在一个名为 `trafficFlowSimulator` 的文件夹中。我们现在将创建另一个名为 `__init__.py` 的 python 文件,并从上述文件中导入类。 文件:__init__.py 说明 在上面的代码片段中,我们从之前创建的 python 文件导入了类。 现在让我们运行一个测试代码来查看输出。 文件:testCase1.py 输出 ![]() 说明 在上面的代码片段中,我们从 `trafficFlowSimulator` 导入了类。然后,我们创建了 `Simulator()` 类的对象。然后,我们使用之前创建的 `createRoad()` 函数添加了一条道路。然后,我们使用 `createRoads()` 函数添加了多条道路。最后,我们通过创建 `Window()` 类的对象并使用 `loop()` 函数来启动模拟。 Vehicles现在,我们将车辆添加到道路上。我们将使用泰勒级数来近似逼近我们在此教程建模部分中讨论过的动力学方程的解。 对于无限可微函数 f,其泰勒级数展开为: ![]() 我们将 a 替换为 x,并将 x 替换为 x + ∆x,得到: ![]() 现在我们将 f 替换为位置 x: ![]() 作为精度,我们将位置的阶数限制在 2,因为加速度是最高阶导数。我们得到方程 (2): ![]() 方程 (2) 对于速度,我们将 x 替换为 v: ![]() 我们将阶数限制在 1,因为我们拥有的最高阶导数是加速度(速度的 1 阶)。方程 (2): ![]() 方程 (1) 在每一次迭代(或每一帧)中,一旦我们使用 IDM 公式计算出加速度,我们将使用这两个方程来更新位置和速度: 方程 (1) ![]() 方程 (2) ![]() 让我们看下面的代码片段来说明这一点: 文件:numericalApprx.py 说明 由于上面的代码片段只是一个近似值,因此速度有时会变成负值(尽管模型不允许这样做)。当速度为负时会出现不稳定性,并且位置和速度会发散到负无穷。 我们可以通过预测负速度并将其设置为零来克服这个问题,然后从那里开始工作。 ![]() 让我们看下面的代码片段来说明这一点: 文件:negativeSpeed.py 说明 在上面的代码片段中,我们使用了 `if-else` 条件语句来检查速度是否为负。 为了计算 IDM 加速度,我们将前车表示为 `leadVehicle`,并在 `leadVehicle` 不为 None 时计算交互项(表示为 `alpha`)。 让我们看下面的代码片段来说明这一点: 文件:leadVehicle.py 说明 在上面的代码片段中,我们将 `alpha` 的值初始化为零。然后,我们使用了 `if` 条件语句来计算 `del_x`、`del_v` 和 `alpha` 值。 如果车辆已停止(例如,在交通信号灯前),我们将使用阻尼方程。之后,我们将所有内容合并到 `Vehicle` 类中的 `update` 方法中。 让我们看下面的代码片段来说明这一点: 文件:vehicle.py 说明 在上面的代码片段中,我们导入了所需的模块,并定义了一个名为 `Vehicle` 的类。我们使用了 `__init__()` 函数并在该类中设置了默认配置。然后,我们定义了 `initProperty()` 函数以及更新位置、速度和加速度的 `update()` 函数。我们还定义了停止、解除停止、减速和增加车辆速度的函数。 在 `Road` 类中,我们将包含一个双端队列(也称为双端队列)以跟踪车辆。像队列这样的数据结构更适合存储车辆,因为队列中的第一辆车是道路上最远的车,也是我们可以从队列中移除的第一辆车。我们可以使用 `self.vehicles.popleft()` 从双端队列中删除第一个数据元素。 我们将在 `Road` 类中包含一个 `update` 方法。让我们看下面的代码片段来理解这一点: 文件:road.py 说明 在上面的代码片段中,我们为 `Road` 类定义了一个 `update` 函数。在此函数中,我们将双端队列中车辆的数量分配给一个名为 `num` 的变量,并使用 `if` 条件检查它是否大于零并更新第一辆车。然后,我们使用从 `1` 到 `num` 的 `for` 循环,并更新双端队列中其余的车辆。 现在,让我们也向 `Simulation` 类添加一个 `update` 方法。这是演示它的代码片段: 文件:simulation.py 说明 在上面的代码片段中,我们在 `Simulation` 类中定义了一个 `update()` 函数。在此函数中,我们更新了每条道路。然后,我们检查道路是否存在越界车辆并执行相应操作。 现在,让我们回到 `Window` 类并添加一个 `run` 方法来实时更新模拟: 文件:window.py 说明 在上面的代码片段中,我们定义了一个 `run` 方法,该方法在每次循环时更新模拟。 现在,我们将手动包含车辆: 文件:testCase2.py 输出 ![]() 说明 在上面的代码片段中,我们手动将车辆添加到之前创建的道路上。我们使用了 `append()` 函数将车辆插入不同的道路。 Vehicle Generators文件:vehicleGenerator.py 说明 在上面的代码片段中,我们从 `vehicle.py` 文件导入了 `Vehicle` 类,从 `numpy` 库导入了 `randint` 函数。然后,我们创建了一个名为 `VehicleGenerators` 的类,并定义了 `__init__` 函数来设置默认配置和 `initProperties`。然后,我们包含了 `generateVehicle` 和 `update` 等函数,以返回来自 `self.vehicles` 的随机车辆,并以随机比例添加车辆。 `VehicleGenerators` 类有一个元组数组 `(odds, vehicle)`。元组的第一个数据元素是车辆在同一元组中生成的权重(不是概率)。我们使用权重,因为它们便于处理,因为我们可以使用整数。 例如,如果我们有三种车辆,权重分别为 3、1、2。这对应于 3/6、1/6、2/6,其中 6 (= 3 + 1 + 2)。 我们可以使用以下算法来实现这一点:
假设我们有权重 W1、W2、W3。以下算法将允许我们将 1 到 W1 之间的数字分配给第一辆车,将 W1 到 W1 + W2 之间的数字分配给第二辆车,将 W1 + W2 + W3 之间的数字分配给第三辆车。 文件:vehicleGenerator.py 说明 在上面的代码片段中,我们在 `generateVehicle()` 函数中定义了一个函数。我们在此函数中计算了 `self.vehicles` 中元组的之和,并返回了具有随机比例的随机车辆。 我们包含了一个名为 `lastAddedTime` 的属性,这样每当我们添加一辆车时,生成器执行函数时当前时间就会更新。当当前时间与 `lastAddedTime` 之间的时间差大于车辆生成周期时,就会添加一辆车。 添加车辆的周期是 `60/vehicleRate`,因为 `vehicleRate` 的单位是每分钟车辆数,而 `60` 是 1 分钟或 60 秒。 我们还将检查道路上是否有空间来添加即将到来的车辆。我们通过检查道路上最后一辆车与即将到来的车辆的长度和安全距离之和之间的距离来执行此操作。 让我们看下面的代码片段来说明这一点: 文件:vehicleGenerator.py 说明 在上面的代码片段中,我们定义了一个 `update()` 函数来将车辆添加到模拟中。我们使用了 `if` 条件语句来检查自上次添加车辆以来经过的时间是否大于车辆周期,并为此生成了一辆车。我们还检查了生成的车辆是否有空间,并添加了它。最后,我们重置了上次添加的时间和即将到来的车辆。 最后,我们应该通过从 `Simulation` 类调用 `update` 方法来更新车辆生成器。让我们看下面的代码片段来理解这一点: 文件:testCase3.py 输出 ![]() 说明 在上面的代码片段中,我们初始化了 `Simulation` 类的 `createGen` 函数,并为 `vehicleRate` 和 `vehicles` 等参数提供了所需的值。我们还指定了车辆的路径并执行了程序。 Traffic Lights现在,让我们将交通信号灯及其属性添加到模拟中。交通信号灯的默认属性如下: 文件:trafficLight.py 说明 在上面的代码片段中,我们定义了一个名为 `TrafficSignal` 的类。我们使用了 `__init__()` 函数来初始化此函数中的一些变量和函数。然后,我们定义了另一个函数来设置默认配置。 `self.cycle` 变量是一个元组数组,包含设置在 `self.roads` 中的每条道路的状态(例如,`True` 表示绿色,`False` 表示红色)。 在默认配置中,数据元素 `(False, True)` 表示第一组道路为红色,第二组为绿色;`(True, False)` 则相反。 我们将使用这种方法,因为它易于扩展。我们可以创建包含多条道路的交通信号灯,具有不同转向信号的交通信号灯,甚至不同交叉路口的同步交通信号灯。 交通信号灯的 update 函数将是可定制的。此函数的默认行为是对称的固定时间循环。 文件:trafficLight.py 说明 在上面的代码片段中,我们定义了 `initProperties()` 函数。然后,我们使用 `for` 循环遍历数组中的每条道路,并在该函数中设置所有信号。然后,我们定义了一个函数来返回当前周期索引。然后,我们定义了一个 update 函数,我们在其中遍历所有周期并重复。 现在,我们将这些方法包含在 `Road` 类中。 文件:road.py 说明 在上面的代码片段中,我们定义了为每个周期设置交通信号灯及其状态的函数。 我们现在将在 `Road` 类的 update 函数中添加以下代码片段。 文件:road.py 说明 在上面的代码片段中,我们检查了交通信号灯,并为特定信号执行了特定的任务集。 现在,我们将在 `Simulation` 类的 update 方法中检查交通信号灯的状态。 文件:simulator.py 说明 在上面的代码片段中,我们使用 `for` 循环遍历 `trafficSignals` 数组中的每个信号并更新它们。 这是相同的一个输出: 输出 ![]() Curves现实世界中的道路有曲线。由于我们可以在技术上通过手动编写大量道路的坐标来估计曲线来创建曲线,因此我们可以以程序化的方式执行相同的操作。 我们将为此使用贝塞尔曲线。我们将创建一个 `curve.py` 文件,其中包含用于创建曲线并按道路索引引用的函数。 让我们看下面的代码片段来说明这一点: 文件:curve.py 说明 在上面的代码片段中,我们定义了一个名为 `curvePoints()` 的函数。然后,我们检查曲线是否是直线,并相应地执行操作。然后,我们定义了另一个名为 `curveRoad()` 的函数,该函数返回弯曲的路径。 现在,让我们测试上面的代码以获得更好的说明。 文件:testCase3.py 说明 在上面的代码片段中,我们导入了包含我们之前创建的类的文件夹。然后,我们创建了 `Simulator()` 类的实例。然后,我们添加了多条道路。我们还包含了 `curveRoad()` 函数来创建弯曲的道路。然后,我们将车辆添加到模拟中并执行了程序。 输出 ![]() 局限性虽然我们可以修改 `Simulation` 类来存储与模拟相关的数据以供以后使用,但如果数据收集更顺畅会更好。 此模拟仍有许多不足之处。曲线实现很糟糕且效率低下,并导致车辆与交通信号灯之间的交互问题。 虽然有些人可能担心智能驾驶员模型有点过度,但拥有一个能够复制真实世界现象(如交通波(也称为幽灵交通蛇)和驾驶员反应时间的影响)的模型非常重要。出于同样的原因,我们选择使用智能驾驶员模型。但是,为了创建不需要精确度和极端现实主义的模拟,例如在电子游戏中,我们可以用更简单的基于逻辑的模型来替代 IDM。 完全依赖基于模拟的数据会增加过度拟合的风险。ML 模型可能会针对模拟中存在但真实世界中不存在的特征进行优化。 结论模拟是数据科学和机器学习的重要组成部分。有时,从现实世界收集数据既不可能也不昂贵。数据生成支持以相对更好的价格构建大型数据集。模拟还可以帮助填补真实世界数据的空白。在某些情况下,真实世界的数据集可能缺少对已开发模型至关重要的边缘情况。 |
我们请求您订阅我们的新闻通讯以获取最新更新。