跳到内容

Disim 高速公路模拟器/教程

来自维基教科书,开放世界的开放书籍

在本教程中,我们将分析不同匝道计量方案对高速公路交通的影响。我们将集中在默特尔匝道和鲍德温最后一个入口匝道之间的州际公路 I-210 W 上。这段高速公路可以在 谷歌地图 上看到。

特别是,我们将研究 ALINEA 算法以及 ALINEA 与与队列约束成比例的控制器相结合的算法。

本节的其余部分分为问题陈述、地图构建、车辆控制器、基础设施控制器(此处为匝道计量控制器)以及结果的可视化。

问题陈述

[编辑 | 编辑源代码]
问题陈述

我们将重点研究特定的高速公路路段(即默特尔和鲍德温之间的州际公路 I-210W)。高速公路路段由 Nr 个入口匝道组成,分别标记为 R1,...,RNr 和 Ne 个出口匝道,分别标记为 E1,...,ENe。每个入口匝道 Ri 都有一个匝道计量器,允许在时间 t 每小时最多有 ri(t) 辆车进入高速公路的主线。车辆可以在入口匝道上排队,等待进入高速公路的车辆数量用 qi(t) 表示。为了实施有效的匝道计量算法,在主线入口处放置了回路探测器。这些回路探测器会汇总车辆信息,并可以测量主线上车辆的流量 fi(t)、车辆密度 ki(t) 和这些车辆的平均速度 vi(t)。每个出口匝道 Ej 都以一个分割比率 sj(t) 为特征,它是出口匝道上驶离高速公路的车辆数量与出口匝道前主线上车辆数量的比率。右侧的图总结了这个问题陈述。

PeMS 数据

此外,在本教程中,我们重点关注了从默特尔入口匝道到鲍德温最后一个入口匝道之间的州际公路 I-210W。该区域的地图可以在 谷歌地图 上看到。该路段由 4 个车道(不包括高占用率车辆 (HOV) 车道)、6 个入口匝道和 4 个出口匝道组成。主线上车辆的进入流量和速度是根据 2011 年 4 月 6 日在 https://pems.eecs.berkeley.edu 上免费提供的性能测量系统 (PeMS) 数据进行校准的。为了简化我们的讨论,所有入口匝道都经历相同的车辆流量,并且等于 2011 年 4 月 6 日默特尔入口匝道的 PeMS 流量。所有模拟都将在上午 6 点到中午之间进行。该数据在右侧可见。

默特尔入口匝道的屏幕截图

让我们看看实际地图文件的摘录。完整的地图文件可以在 Disim 发行版的 maps 文件夹中找到,名为 I-210W.map

##########
# Header #
##########

$NAME,I-210W                        # The name of the map
$LANE_WIDTH,3.5                     # The line width to display

##############################
# Body: Sequence of segments #
##############################

$SEGMENT,straight,100               # A straight segment of a 100 meters
$TYPE,entry,left                    # The first segment should be an entry
$SPEED,105                          # Maximum speed allowed in km/h (about 70 mph)
$NUM_LANES,0,4                      # We keep 0 lanes from the previous segment and add 4 new lanes
$LANE,0,2000,Myrtle                 # All lanes have a default entry rate of 2000 veh/h
$LANE,1,2000,Myrtle                 # and are all named Myrtle (it is useful to give names to entry
$LANE,2,2000,Myrtle                 # lanes).
$LANE,3,2000,Myrtle                 # Lanes are numbered from left to right.

$SEGMENT,straight,60                # We'll have a 100 m straight before the on-ramp.
$NUM_LANES,4                        # The reason to create a new segment rather than adding
                                    # an extra 100 m on the previous segment is simply for
                                    # efficiency: internally Disim only looks within neighboring
                                    # segment to find neighboring vehicles, hence the smaller
                                    # the segment the faster Disim can search.

$SEGMENT,straight,240               # We now create a straight segment of 240 meters with an on-ramp.
$TYPE,entry,right                   # The on-ramp will be entering from the right of the highway.
$NUM_LANES,4,1                      # We keep the previous 4 lanes and add a new lane (on the right).
$LANE,4,2000,Myrtle_OnRamp          # The 5th (and new lane) will be named Myrtle_OnRamp.
$RIGHT_MARKING,3,0,140,solid        # There will be a solid marking at the beginning of the on-ramp
$LEFT_MARKING,4,0,140,solid         # on the first 140 meters (0 to 140). This marking will not allow
                                    # vehicles on the 4th lane (index 3) to cross on the on-ramp and
                                    # vehicles on the on-ramp (index 4) to cross into the highway
                                    # before the 140th meter.
$TRAFFIC_LIGHT,rampmeter1,4,140     # We can now place a rampmeter at the 140th meter on the on-ramp (index 4)
$DENSITY_SENSOR,myrtle_density_1,0,10,230,nolog  # We additionally place density sensors on all lanes
$DENSITY_SENSOR,myrtle_density_2,1,10,230,nolog  # of the mainline. We do not wish to log the data of these
$DENSITY_SENSOR,myrtle_density_3,2,10,230,nolog  # sensors. They will only allow us to compute the ALINEA
$DENSITY_SENSOR,myrtle_density_4,3,10,230,nolog  # entry rate.
$DENSITY_SENSOR,rampmeter1_queue,4,1,140,nolog   # Density sensors can return the number of cars within the
                                                 # specified region (here 1-140). Hence we place this sensor
                                                 # to know what is the queue length on the on-ramp before the
                                                 # rampmeter.

$SEGMENT,circular,1000,6            # Now a circular segment of radius 1000 m and angle span of 6 degrees.
$TYPE,none,left                     # We do only wish to keep the 4 left-most lanes (hence the type none).
$NUM_LANES,4                        # We keep 4 lanes only (no new lanes).

[. . .]                             # We skip part of the file here, until the first off-ramp.

$SEGMENT,straight,200               # The off-ramp will be a straight 200 m long segment
$TYPE,exit,right                    # with an exit on the right.
$NUM_LANES,4,1                      # The exit will be an additional lane on the right.
$LANE,4,0.052,Huntington_OffRamp    # This off-ramp has a default split ratio of 5.2 percent.

$SEGMENT,circular,1000,-5           # We continue the highway with a circular segment turning to the
$NUM_LANES,4                        # left with 4 lanes.

[. . .]                             # The rest of the file is a repeat of the above line with
                                    # different lane names.

正如您所见,语法非常简单,它是一系列关键字(以美元符号 $ 开头)后跟定义关键字的参数。上面的摘录已被充分注释。请看一看,因为它包含您可能需要的所有内容。在下一节中,我们将开发驾驶员模型。

车辆行为

[编辑 | 编辑源代码]

Disim 配有一个标准驾驶员模型:Helbing [1] 的智能驾驶员模型 (IDM) 与 Treiber [2] 的最小化车道变更引起的总制动减速 (MOBIL) 模型相结合。IDM 是一种纵向模型,它根据前车来设定加速度,而 MOBIL 模型则考虑当前的 5 个相邻车辆(前车、左车道前车、左车道后车、右车道前车和右车道后车)。Disim 的当前实现只允许汽车控制器访问 6 个直接邻居的信息(如果它们在给定的 100 米半径内)。如果您有其他需求,请不要犹豫,给我在 Sourceforge 上留言,或者随意修改核心源代码(它应该在 Doxygen 下有良好的文档记录)。

让我们深入研究汽车控制器的代码。我们故意省略了原始文件的一部分,您可以在 Disim 发行版的 scripts/car 文件夹中找到它(IDM_MOBIL.lua)。

-- Variables specific to each car
vars = {}
--[[
For performance issue, Disim initializes only a minimal amount of LUA stacks
(much less than the number of cars in the simulation), hence it is important to
store any variables that you may need in your own array. We will see how this
works in the init function.
--]]

--[[
The init function initializes all the needed variables. One can index those variables
under the address of self. The self arguments represents the current car to initialize.
The second argument are optional arguments that are passed via the Disim
commandline via the --lua-args option.
--]]
function init(self, options)
  -- Store necessary variables specific to that car
  vars[self] = { v0 = 105/3.6, a = 1.4, b = 2.0, gamma = 4.0, t = 1.0, s0 = 2.0, b_safe = 4.0, p = 0.25, a_thr = 0.7_arg, llc = 0.0 }

  -- If the current car is a truck, simply alter the variables to show this. Trucks are
  -- slower in general...
  if (self:getType() == TRUCK) then
    vars[self].v0 = 85/3.6
    vars[self].a = 0.7
    vars[self].t = 1.5
    vars[self].s0 = 4.0
    vars[self].llc = 0.0
  end
end

--[[
The think function performs the IDM and MOBIL algorithms. This function is called
at every time step of the simulation and should compute the acceleration and lane
change depending on the surroundings (or neighboring vehicles).
--]]
function think(self, dt, neighbors)
  -- Declare variables
  local speed_pref, dist, acceleration, lane_change

  -- Get prefered speed
  speed_pref = self:getLane():getSpeedLimit()
  if (speed_pref > vars[self].v0) then
    speed_pref = vars[self].v0
  end

  -- Compute IDM acceleration
  acceleration = IDM(self, self:getLane(), self, neighbors[LEAD].car, speed_pref, neighbors[LEAD].distance)

  -- Do not lane change more than every 5 seconds
  lane_change = 0
  if (vars[self].llc > 5.0) then
    -- Perform MOBIL of the right lane
    if (self:isRightAllowed()) then
      lane_change = MOBIL(self, self:getLane():getRight(), self, speed_pref, acceleration,
                          neighbors[TRAIL].car, neighbors[TRAIL].distance,
                          neighbors[LEAD].car, neighbors[LEAD].distance,
                          neighbors[RIGHT_TRAIL].car, neighbors[RIGHT_TRAIL].distance,
                          neighbors[RIGHT_LEAD].car, neighbors[RIGHT_LEAD].distance)
    end
    -- Perform MOBIL of the left lane
    if (lane_change == 0 and self:isLeftAllowed()) then
      lane_change = -MOBIL(self, self:getLane():getLeft(), self, speed_pref, acceleration,
                           neighbors[TRAIL].car, neighbors[TRAIL].distance,
                           neighbors[LEAD].car, neighbors[LEAD].distance,
                           neighbors[LEFT_TRAIL].car, neighbors[LEFT_TRAIL].distance,
                           neighbors[LEFT_LEAD].car, neighbors[LEFT_LEAD].distance)
    end

    if (lane_change ~= 0) then
      vars[self].llc = 0.0
    end
  end

  -- Update variables
  vars[self].llc = vars[self].llc + dt;

  self:setAcceleration(acceleration)
  self:setLaneChange(lane_change)
end

--[[
The destroy function clears the variables of the self vehicle from our array of
global values. It is called when a car leave the highway.
--]]
function destroy(self)
  -- Remove the variables stored for that car
  vars[self] = nil
end

[. . .]

此脚本是用 LUA 编写的 (https://lua.ac.cn/manual/5.1/manual.html):Lua 是一种扩展编程语言,旨在支持具有数据描述功能的一般过程式编程。它还为面向对象编程、函数式编程和数据驱动编程提供了良好的支持。Lua 旨在用作任何需要脚本语言的程序的强大、轻量级脚本语言。Lua 是作为库实现的,用干净的 C(即 ANSI C 和 C++ 的通用子集)编写。

有兴趣测试自己的控制器的用户应该学习 Lua 并阅读本网站上提供的 Disim API 页面。此外,如果您不想创建自己的控制器,您可以直接使用 IDM_MOBIL 控制器(或修改 init 函数中的常量)。

[1] D. Helbing, A. Hennecke, V. Shvetsov, M. Treiber, 高速公路交通的微观和宏观模拟,数学和计算机建模,2002 年,第 35 卷,第 517-547 页。
[2] M. Treiber, D. Helbing, 用一个简单模型进行现实的道路交通微观模拟,2002 年模拟技术研讨会 ASIM,第 514-520 页。

基础设施控制

[编辑 | 编辑源代码]

基础设施控制器(与汽车的汽车控制器类似)负责根据一天中的当前时间和放置在道路上的传感器进行的测量来修改和作用于环境。以下控制器首先读取 PeMS 数据文件并更新进入高速公路的车辆的流量和速度。请注意,这段代码非常通用,因为它首先遍历高速公路的所有入口,并尝试找到具有相应名称的相应 PeMS 数据文件。然后,对于所有入口,它会自动调整车辆流量。此外,此脚本还根据放置在入口匝道上的密度传感器来管理匝道计量器。

-- Algorithm number (0 = no control, 1 = ALINEA, 2 = ALINEA + Queue control, 3 = ALINEA + Queue control + Coordinated control)
rampcontrol = 1

-- All the ramps to control
rampstocontrol = {'myrtle', 'huntington', 'santa_anita_1', 'santa_anita_2', 'baldwin_1', 'baldwin_2'}
-- The number of lanes on the mainline at those ramps
nlanes = {4, 4, 4, 4, 4, 4}

-- The critical densities (densities at which the flow is maximal)
criticaldensity     = {27.16, 27.16, 27.16, 27.16, 27.16, 27.16}
-- The maximum allowable queue time.
criticalqueuetime   = {120.0, 120.0, 120.0, 120.0, 120.0, 120.0}

-- Some constants
K_alinea            = 0.1
K_queuetime         = 0.1
K_coordinatetime    = 0.01

-- Some variables to store the different sensors and actuators.
lanes = {}
entryrates = {}
entryspeeds = {}
rampmeters = {}
densitysensors = {}
speedlimits = {}

-- The previous time of day
previoustime = "xx:xx"
previousrampcontroltime = 1

--[[
Just like any Lua scripts, one can declare functions. This function imitates the C function printf.
--]]
function printf(...)
  io.stdout:write(string.format(unpack(arg)))
end

--[[
As for the car controller, at the creation of the highway, this function is called.
It is in charge of getting and setting all necessary variables and sensors/actuators
from the highway (for efficiency reasons).
--]]
function init(self)
  printf("Initialize control for map: %s\n", self:getName())

  -- Get all rampmeters and density sensors
  if (rampcontrol ~= 0) then
    printf("Number of controlled on-ramps: %d.\n", #rampstocontrol)
    for i,ramptocontrol in ipairs(rampstocontrol) do
      r = self:getRoadActuator(string.format("rampmeter%d",i))
      q = self:getRoadSensor(string.format("rampmeter%d_queue", i))

      -- Setup the rampmeter variables
      rampmeters[r] = {green = 3, red = 5, lastchange = 0, density = criticaldensity[i], queuelimit = criticalqueuetime[i], queue = q}
      
      -- Get and assign the corresponding road sensors.
      densitysensors[r] = {}
      printf(" %s found with sensor %s.\n", r:getName(), q:getName());
      for j = 1,nlanes[i] do
	d = self:getRoadSensor(string.format("%s_density_%d", ramptocontrol, j))
        densitysensors[r][j] = d
        printf("  %s found.\n", d:getName());
      end
    end
  end

  -- Get all entry lanes
  lanes = self:getEntryLanes();
  printf("There are %d entry lanes on this map.\n", #lanes)
  for i,lane in ipairs(lanes) do
    printf(" %d) Lane: %-30s", i, lane:getName())
    if (entryrates[lane:getName()] == nil) then
      entryrates[lane:getName()] = {}
      entryspeeds[lane:getName()] = {}
      -- Read the PEMS data file located in data/
      --[[
            It is important to note that Disim sets the current
            working directory to be the location of this script.
            This, however, is only valid for the control script
            as it is fairly inefficient to change the working
            directory for individual car behaviors scripts.
      --]]
      for t=1,2 do
        local filename = "data/" .. lane:getName() .. "_flow.txt"
        if (t == 2) then
          filename = "data/" .. lane:getName() .. "_speed.txt"
        end
        local fp = io.open(filename, "r")
        if (fp) then
          local nl = 0
          for line in fp:lines() do
            if (line:find("%d+/%d+/%d+") ~= nil) then
              nl = nl + 1
              -- date time data1 data2 ... datan data nlanes observed
              nums = {}
              for n in line:gfind("%d+%.?%d*") do
                table.insert(nums, n)
              end
              local nlanes = nums[#nums-1]
              local time = string.format("%02d:%02d", nums[4], nums[5]) -- time of day
              if (t == 1) then
                local data = nums[#nums-2]*12 -- veh/h
                entryrates[lane:getName()][time] = data/nlanes;
              else
                local data = nums[#nums-2]*1.609344 -- km/h
                entryspeeds[lane:getName()][time] = data;
              end
            end
          end
          if (t == 2) then
            printf(string.rep(" ",40));
          end
          printf("[ OK ] - read %d data points\n", nl)
          fp:close()
        else
          if (t == 2) then
            printf(string.rep(" ",40));
          end
          printf("[FAIL]\n")
        end
      end
    else
      printf("[SKIP]\n");
    end
  end
end

--[[
This function is called at every time step.
Here we control the rampmeters using
   1) ALINEA
   2) ALINEA + Queue Control
   3) Coordinated ALINEA + Queue Control
--]]
function update(self, t, dt)
  --[[
  First we update the rampmeter lights from red to green and vice-versa.
  --]]
  if (rampcontrol > 0) then
    for rampmeter, dts in pairs(rampmeters) do
      -- Setting the ramp meter light
      dts.lastchange = dts.lastchange + dt
      if (rampmeter:getColor() == RED and dts.lastchange > dts.red) then
        dts.lastchange = 0
        rampmeter:green()
      elseif (rampmeter:getColor() == GREEN and dts.lastchange > dts.green) then
        dts.lastchange = 0
        rampmeter:red()
      end
    end

    --[[
    If we control use queue control 
    --]]
    if (rampcontrol > 1) then

      -- If we use the coordinated approach, gather all queue lengths
      if (rampcontrol == 3)
        -- Gather queue lengths
        queuelength = {}
        for rampmeter, dts in pairs(rampmeters) do
          queuelength[rampmeter] = dts.queue:getVehicleCount()
          -- Transform the queue length in a waiting time.
          queuelength[rampmeter] = queuelength[rampmeter]*(dts.red + dts.green)
        end
      end

      -- Apply control
      for rampmeter, dts in pairs(rampmeters) do
        q,newvalue = rampmeter:getInstantQueueLength()
        if (rampcontrol == 2 or rampcontrol == 3) then
          q = dts.queue:getVehicleCount()
          q = q*(dts.red + dts.green)
          -- keep same newvalue variable (this variable indicates that one car passed by the traffic light).
        end
        -- Coordination
        if (newvalue and rampcontrol == 3) then
          -- Consensus control of queue lengths
          for rm, ql in pairs(queuelength) do
            dts.red = dts.red - (q - ql)*K_coordinatetime
          end
        end
        -- Queue control
        if (newvalue and q > dts.queuelimit) then
          dts.red = dts.red - (q - dts.queuelimit)*K_queuetime
        end
        if (dts.red < 0) then
          dts.red = 0
        elseif (dts.red > 50) then
          dts.red = 50
        end
      end
    end

    -- Aggregation of data happens every minute
    if (t - previousrampcontroltime >= 60) then
      previousrampcontroltime = previousrampcontroltime + 60

      for rampmeter, ds in pairs(densitysensors) do
        -- Compute average density
        density = 0
        for i,d in ipairs(ds) do
          density = density + d:getValue();
        end
        density = density / #ds
        -- Change rate
        dts = rampmeters[rampmeter]
        dts.red = dts.red + (density - dts.density)*K_alinea
        if (dts.red < 0) then
          dts.red = 0
        elseif (dts.red > 50) then
          dts.red = 50
        end
      end
    end
  end

  --[[
        Here we set the entry rates and speeds from the
        saved data read in the init function. The lookup
        is fairly simple as we can get the time of the day
        from the utily function getTimeOfDay.
  --]]

  -- Get the previous 5 min boundary (as the data is stored)
  local timeofday = self:getTimeOfDay(t)
  h,m = timeofday:match("(%d+):(%d+)")
  h,m = tonumber(h), tonumber(m)
  m = math.floor(m/5)*5
  timeofday = string.format("%02d:%02d", h, m)
  if (previoustime == timeofday) then
    return
  end
  previoustime = timeofday

  -- Change entry rate only if previous boundary was different
  for i,lane in ipairs(lanes) do
    if (entryrates[lane:getName()][timeofday] ~= nil) then
      lane:setEntryRate(entryrates[lane:getName()][timeofday])
    end
    if (entryspeeds[lane:getName()][timeofday] ~= nil) then
      lane:setEntrySpeed(entryspeeds[lane:getName()][timeofday])
    end
  end
end

--[[
This function is called when the simulation is stopped.
--]]
function destroy(self)
  
end

实际文件可以在 Disim 发行版的 scripts/control 中找到。正如您所见,您可以控制高速公路上的大量执行器,这使您可以实施从 ALINEA 到匝道计量器协调的非常复杂的策略。只要您在作品中引用 Disim,请随意使用和修改以上代码进行您的研究。

运行模拟

[编辑 | 编辑源代码]

此时,所有部分都已到位,可以启动 Disim,只需打开一个终端并输入

disim --start-time=07:00 --lua="IDM_MOBIL.lua" --luacontrol="I-210W.lua" --map="I-210W.map" --ncpu=6

Disim 将启动模拟并为您打开图形界面,以查看一切是否顺利运行。如果您满意,您可以关闭 Disim 并使用以下命令开始收集数据

mkdir logs
disim --start-time=06:00 --duration=21600 --lua="IDM_MOBIL.lua" --luacontrol="I-210W.lua" --map="I-210W.map" --ncpu=6 --record --nogui --time-step=0.5 --log

模拟完成后,命令提示符将再次出现。您现在可以浏览 logs 文件夹,查看您想要记录的传感器是否在那里。

收集数据

[编辑 | 编辑源代码]

启动 Octave/Matlab 并进入 Disim scripts/matlab 文件夹以开始绘制您的数据。您应该能够生成类似于右侧的图形。

基本图
行驶时间和排队时间

它们显示了洪廷顿主线上车辆的密度与流量。正如我们所见,没有任何控制的情况下,主线上车辆的密度可以自由增加,从而导致严重的减速并造成堵塞。使用 ALINEA,该图成功地保持在自由流部分,并且没有出现堵塞,但代价是达到最长 8 分钟的排队时间(如图形 13(c) 所示)。在引入 2 分钟队列约束后,发生了减速,但由于我们主要保持在每公里 80 辆车以下(与没有任何控制策略的情况下每公里 110 辆车相比),因此避免了堵塞。图 13(c) 清楚地表明,最大的排队时间不超过 2 分钟。最后,协调方法在保持队列约束的同时,产生了与 ALINEA 控制类似的结果。很明显,排队时间和行驶时间之间存在权衡,但引入我们的协调策略不仅将行驶时间提高了两倍(参见图 13(a)),而且还解决了所有进入高速公路的车辆都经历相同延迟的公平问题。

本教程到此结束。请尽情享受 Disim 并为其发展做出贡献。Disim 独一无二,我们希望它能成为交通工程师宝贵软件的一部分。感谢您下载 Disim!

华夏公益教科书