"""
traffic_sim.py Version 2.0
OVERVIEW
This Python script simulates automotive traffic arriving at a traffic light.
The script demonstrates the following:
(1) several basic principles of discrete-event simulation, including the
representation of system states, the connection between events and changes of
state, and the Poisson process,
(2) the use of the SimPy module,
(3) simulation of random deviates using NumPy, and
(4) the use of Python generator functions.
THE MODEL
Cars drive on a single-lane road and arrive at the intersection from one
direction only according to a Poisson process with specified rate. Positions
and velocities of the cars are not modeled, and there is thus no coordinate
system.
The traffic light switches between green and red at regular intervals; there is
no yellow state.
When a car arrives at a green light with no cars queued, it passes immediately
through the intersection and departs the simulation.
When a car arrives at a red light or with one or more cars queued at the
intersection, it stops and joins the queue.
When the light turns green, if there is a queue, cars enter the intersection one
at a time. Once a car has entered the intersection, it clears the intersection,
regardless of the state of the light, after a delay that is a random draw from a
triangular distribution.
INPUTS
All inputs are hardwired into the code.
OUTPUTS
The simulation generates a trace of events as it runs, and also reports
estimates of the average number of cars in the queue and the average waiting
time.
FUTURE WORK
This model might be extended in any of several directions. Some possibilities
include the following:
(1) There are several sources of bias in the average waiting time: (a) The
simulation starts with a green light. (b) The simulation starts with no cars
queued. (c) Cars remaining in the queue at the end of the simulation do not
contribute to the estimated waiting time. The effects of (a) and (b) might be
mitigated by eliminating the initial transient; this would involve collecting
and saving individual waiting times, choosing alternative values for the
duration of the initial transient, generating estimates using only the data
outside of the transient interval, and then selecting one of these results using
some statistical criteria. (We want the mean of the selected result to have no
statistically significant difference from results based on longer transient
intervals, but otherwise we want to discard as little of the data as possible).
(2) The delay of the driver at the head of the queue in responding to the change
of the light from red to green could be made somewhat longer than that of
drivers who are further back in the queue. This would allow for greater realism
in the model without explicitly modeling positions and velocities of the cars.
(3) One might have some fraction of the cars make a right-turn at the light,
with the remaining cars going straight, and compare the capacity of a
single-lane road, a two-lane road with no turn restrictions, and a two-lane road
with a dedicated right-turn-only lane.
AUTHOR
Dr. Phillip M. Feldman
"""
# Section 1: Import from modules and define a utility class.
from collections import deque # double-ended queue
from numpy import random
import simpy
from simpy.util import start_delayed
class Struct(object):
"""
This simple class allows one to create an object whose attributes are
initialized via keyword argument/value pairs. One can update the attributes
as needed later.
"""
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
# Section 2: Initializations.
random.seed([1, 2, 3])
# Total number of seconds to be simulated:
end_time= 200.0
# Cars cars arrive at the traffic light according to a Poisson process with an
# average rate of 0.2 per second:
arrival_rate= 0.2
t_interarrival_mean= 1.0 / arrival_rate
# Traffic light green and red durations:
t_green= 30.0; t_red= 40.0
# The time for a car at the head of the queue to depart (clear the intersection)
# is modeled as a triangular distribution with specified minimum, maximum, and
# mode.
t_depart_left= 1.6; t_depart_mode= 2.0; t_depart_right= 2.4
# Initially, no cars are waiting at the light:
queue= deque()
# Track number of cars:
arrival_count= departure_count= 0
Q_stats= Struct(count=0, cars_waiting=0)
W_stats= Struct(count=0, waiting_time=0.0)
# Section 3: Arrival event.
def arrival():
"""
This generator functions simulates the arrival of a car. Cars arrive
according to a Poisson process having rate `arrival_rate`. The times between
subsequent arrivals are i.i.d. exponential random variables with mean
t_interarrival_mean= 1.0 / arrival_rate
"""
global arrival_count, env, light, queue
while True:
arrival_count+= 1
if light == 'red' or len(queue):
# The light is red or there is a queue of cars. ==> The new car joins
# the queue. Append a tuple that contains the number of the car and
# the time at which it arrived:
queue.append((arrival_count, env.now))
print("Car #%d arrived and joined the queue at position %d at time "
"%.3f." % (arrival_count, len(queue), env.now))
else:
# The light is green and no cars are waiting. ==> The new car passes
# through the intersection immediately.
print("Car #%d arrived to a green light with no cars waiting at time "
"%.3f." % (arrival_count, env.now))
# Record waiting time statistics. (This car experienced zero waiting
# time, so we increment the count of cars, but the cumulative waiting
# time remains unchanged.
W_stats.count+= 1
# Schedule next arrival:
yield env.timeout( random.exponential(t_interarrival_mean))
# Section 4: Define event functions.
# Section 4.1: Departure event.
def departure():
"""
This generator function simulates the 'departure' of a car, i.e., a car that
previously entered the intersection clears the intersection. Once a car has
departed, we remove it from the queue, and we no longer track it in the
simulation.
"""
global env, queue
while True:
# The car that entered the intersection clears the intersection:
car_number, t_arrival= queue.popleft()
print("Car #%d departed at time %.3f, leaving %d cars in the queue."
% (car_number, env.now, len(queue)))
# Record waiting time statistics:
W_stats.count+= 1
W_stats.waiting_time+= env.now - t_arrival
# If the light is red or the queue is empty, do not schedule the next
# departure. `departure` is a generator, so the `return` statement
# terminates the iterator that the generator produces.
if light == 'red' or len(queue) == 0:
return
# Generate departure delay as a random draw from triangular distribution:
delay= random.triangular(left=t_depart_left, mode=t_depart_mode,
right=t_depart_right)
# Schedule next departure:
yield env.timeout(delay)
# Section 4.2: Light change-of-state event.
def light():
"""
This generator function simulates state changes of the traffic light. For
simplicity, the light is either green or red--there is no yellow state.
"""
global env, light
while True:
# Section 4.2.1: Change the light to green.
light= 'green'
print("\nThe light turned green at time %.3f." % env.now)
# If there are cars in the queue, schedule a departure event:
if len(queue):
# Generate departure delay as a random draw from triangular
# distribution:
delay= random.triangular(left=t_depart_left, mode=t_depart_mode,
right=t_depart_right)
start_delayed(env, departure(), delay=delay)
# Schedule event that will turn the light red:
yield env.timeout(t_green)
# Section 4.2.2: Change the light to red.
light= 'red'
print("\nThe light turned red at time %.3f." % env.now)
# Schedule event that will turn the light green:
yield env.timeout(t_red)
# Section 4.3: Schedule event that collects Q_stats.
def monitor():
"""
This generator function produces an interator that collects statistics on the
state of the queue at regular intervals. An alternative approach would be to
apply the PASTA property of the Poisson process ('Poisson Arrivals See Time
Averages') and sample the queue at instants immediately prior to arrivals.
"""
global env, Q_stats
while True:
Q_stats.count+= 1
Q_stats.cars_waiting+= len(queue)
yield env.timeout(1.0)
# Section 5: Schedule initial events and run the simulation. Note: The first
# change of the traffic light, first arrival of a car, and first statistical
# monitoring event are scheduled by invoking `env.process`. Subsequent changes
# will be scheduled by invoking the `timeout` method. With this scheme, there
# is only one event of each of these types scheduled at any time; this keeps the
# event queue short, which is good for both memory utilization and running time.
print("\nSimulation of Cars Arriving at Intersection Controlled by a Traffic "
"Light\n\n")
# Initialize environment:
env= simpy.Environment()
# Schedule first change of the traffic light:
env.process(light())
# Schedule first arrival of a car:
t_first_arrival= random.exponential(t_interarrival_mean)
start_delayed(env, arrival(), delay=t_first_arrival)
# Schedule first statistical monitoring event:
env.process(monitor())
# Let the simulation run for specified time:
env.run(until=end_time)
# Section 6: Report statistics.
print("\n\n *** Statistics ***\n\n")
print("Mean number of cars waiting: %.3f"
% (Q_stats.cars_waiting / float(Q_stats.count)))
print("Mean waiting time (seconds): %.3f"
% (W_stats.waiting_time / float(W_stats.count)))