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) 為特徵,它是出口匝道上駛離高速公路的車輛數量與出口匝道前主線上車輛數量的比率。右側的圖總結了這個問題陳述。

此外,在本教程中,我們重點關注了從默特爾入口匝道到鮑德溫最後一個入口匝道之間的州際公路 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 編寫的 (http://www.lua.org/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!