Disim Highway Simulator/Tutorial

Scenario edit

In 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 edit

 
Problem Statement

We 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.

 
PeMS data

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 edit

 
Screenshot of the Myrtle on-ramp

Let 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 edit

Disim 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 edit

The 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 edit

At 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 edit

Start 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.

 
Fundamental Diagrams
 
Travel and queueing times

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 edit

This 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!