Disim Highway Simulator/Tutorial
Scenario
editIn this tutorial, we are going to analyze the effect of different ramp-metering schemes on the highway traffic. We are going to concentrate on the interstate I-210 W highway between the Myrtle off-ramp and the last of Balwin's on-ramps. This section of highway is visible on Google Maps.
In particular, we are going to study the ALINEA algorithm and a combination of ALINEA with a queue constraint proportional controller.
The rest of this section is separated into the problem statement, the construction of the map, the car controller, the infrastructure controller (here the ramp-metering controller) and the visualization of the results.
Problem Statement
editWe will focus our study on a specific highway segment (namely the interstate I-210W between Myrtle and Baldwin). A highway segment is composed of Nr on-ramps labeled R1,...,RNr and Ne exit off-ramps labeled E1,...,ENe. Each on-ramp Ri has a ramp meter allowing a maximal rate of ri(t) vehicles per hour to enter the mainline of the highway at time t. Vehicles can queue on the on-ramp, the number of vehicles waiting to enter the highway is denoted by qi(t). To implement efficient ramp metering algorithms, loops detector are placed before the entry on the mainline. These loop detectors aggregate vehicle information and can measure the flow of vehicles on the mainline fi(t), the density of vehicles ki(t) and the average speed of those vehicles vi(t). Each off-ramp Ej is caraterized by a split ratio sj(t) which is the ratio of the number of vehicles exiting the highway over the number of vehicles on the mainline before the exit. The figure on the right summarizes this problem statement.
Furthermore, throughout this tutorial, we have focused on the interstate I-210W between the Myrtle on-ramp until the last of Baldwin’s on-ramp. The map of this area is visible on Google Maps. This section is composed of 4 lanes (excluding a High Occupancy Vehicle (HOV) lane), 6 on- ramps and 4 off-ramps. The entry flow and speed of the vehicles on the mainline are calibrated using the Performance Measurement Systems (PeMS) data available freely on https://pems.eecs.berkeley.edu for the 6th of April 2011. To simplify our discussions, all on-ramps experience the same flow of vehicles and is equal to the PeMS flow at the Myrtle on-ramp on the 6th of April 2011. All simulation will take place between 6 a.m. and noon. This data is visible on the right.
Map
editLet us have at an excerpt of the actual map file. The complete map file can be found in the maps folder of the Disim distribution under the name 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.
As you can observe the syntax is pretty simple and is a sequence of keywords (starting with the dollar sign $) followed by arguments that define the keyword. The above excerpt has been well commented. Please have a look as it contains anything that you might ever need. In the next section, we will develop the driver model.
Car Behavior
editDisim is distributed with a standard driver model: the Intelligent Driver Model (IDM) of Helbing [1] combined with the Minimizing Overall Braking decelerations Induced by Lane changes (MOBIL) model of Treiber [2]. The IDM is a longitudinal model that set the acceleration depending only the leading vehicle and the MOBIL model looks at the 5 current neighbors (leading, leading on the left lane, trailing on the left, leading on the right and trailing on the right). The current implementation of Disim only allows the car controller to access information about the 6 direct neighbors if they are within a given radius of 100 meters. Please do not hesitate to drop me line on Sourceforge if you have other needs or feel free to modify the core source code (as it is hopefully well documented under Doxygen).
Let us dive into the code of the car controller. We have purposefully omitted part of the original file which you can find under the scripts/car folder in the Disim distribution (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
[. . .]
This script was made using LUA (http://www.lua.org/manual/5.1/manual.html): Lua is an extension programming language designed to support general procedural programming with data description facilities. It also offers good support for object-oriented programming, functional programming, and data-driven programming. Lua is intended to be used as a powerful, light-weight scripting language for any program that needs one. Lua is implemented as a library, written in clean C (that is, in the common subset of ANSI C and C++).
Users that are interested in testing their own controller should learn Lua and read the Disim API page available on this website. Additionally, if you do not wish to create your own controller, you can use the IDM_MOBIL controller as is (or modify the constants in the init function).
[1] D. Helbing, A. Hennecke, V. Shvetsov, M. Treiber, Micro- and macro- simulation of freeway traffic, Mathematical and Computer Modelling, 2002, Vol. 35, pp. 517–547.
[2] M. Treiber, D. Helbing, Realistische Mikrosimulation von Strassenverkehr mit einem einfachen Modell, Symposium of Simulationstechnik ASIM, 2002, pp. 514–520.
Infrastructure Control
editThe infrastructure controller (much like the car controller for cars) is in charge to modify and act on the environment in function of the current time of day and the measurements done by the sensors placed on the road. The following controller first reads PeMS data files and update the flows and speeds of vehicles entering the highway. Note that this piece of code is very general, as it first looks through all entries of the highway and tries to find corresponding PeMS data file with the corresponding name. Then for all entries, it automatically adjusts the flow of vehicles. Additionally this script also manages the rampmeters depending on the density sensors placed the on-ramps.
-- 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
The actual file can be found in the Disim distribution under scripts/control. As you can see, you can control a fair amount of actuators in the highway and this allows you to implement very complex strategies starting with ALINEA up to coordination of rampmeters. Feel free to use and modify the above code for your research as long as you cite Disim in your work.
Running the Simulation
editAt this point, all the piece are in place to start Disim, simply open a terminal and type:
disim --start-time=07:00 --lua="IDM_MOBIL.lua" --luacontrol="I-210W.lua" --map="I-210W.map" --ncpu=6
Disim will start the simulation and open the graphical interface for you to see if everything runs smoothly. If you are happy, you can close disim and start gathering data with the following commands:
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
The command prompt will again appear when the simulation is finished. You can now explore the logs folder to see if the sensors you wanted to log are there.
Gathering Data
editStart up Octave/Matlab and go in the Disim scripts/matlab folder to start plotting your data. You should be able to generate plots like the ones on the right side.
They show the density versus the flow of vehicles at Huntington on the mainline. As we can observe without any control the density of vehicles is free to increase on the mainline and thus yields major slowdowns and create jams. With ALINEA, the diagram successfully stays in the free flow section and no jam is experienced, this comes at the cost of reaching up to 8 minutes of queuing time (as seen on Figure 13(c)). Upon the introduction of a 2-minutes queue constraint, slowdowns are experienced but jams are avoided as we stay mainly below 80 vehicles per kilometer (compared to 110 vehicles per kilometers without any control strategy). Figure 13(c) clearly shows that the maximal queuing time does not exceed 2 minutes. Finally the coordinated approach leads to similar results than the ALINEA control whilst staying within the queue constraint. It is clear that a trade-off between queuing time and travel time exists, but the introduction of our coordinated strategy not only improves the travel time by a factor two (see Figure 13(a)), but also solve the equity problem where all vehicles entering the highway experience the same delay.
Conclusion
editThis concludes our tutorial. Please do enjoy Disim and contribute to its development. There are no others like Disim and we hope to have bootstrapped a piece a software valuable to traffic engineers. Thank you for downloading Disim!