"""
traffic_simpy  Version 2.0
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.

Автомобили едут по однополосной дороге и прибывают на перекресток только с одного
направления в соответствии с процессом Пуассона с заданной скоростью. Положения
и скорости автомобилей не моделируются, и, следовательно, отсутствует
система координат.
Сигнал светофора переключается с зеленого на красный через равные промежутки времени;
желтого состояния нет.
Когда автомобиль проезжает на зеленый сигнал светофора без очереди, он немедленно
проезжает перекресток и покидает модель.
Когда автомобиль проезжает на красный сигнал светофора или когда одна или несколько машин
стоят в очереди на перекрестке, он останавливается и присоединяется к очереди.
Когда загорается зеленый свет, если есть очередь, машины въезжают на перекресток
по одной. Как только автомобиль въезжает на перекресток, он проезжает перекресток,
независимо от состояния светофора, после задержки, которая является случайным результатом
треугольного распределения.


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
"""

# Import from modules and define a utility class.

from collections import deque # double-ended queue
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)

# Initializations.
random.seed(123)

# Total number of seconds to be simulated:
end_time = 400.0

# 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)


# 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("Авто_%d приехала и стала %dй в очереди в момент %.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("Авто_%d приехала на зеленый свет и проехала без очереди в момент %.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.expovariate(arrival_rate))  #(1 / t_interarrival_mean))


# Define event functions
# 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("Авто_%d проехала в %.3f, в очереди осталось %d автомашин." \
           % (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
      else:
        # Generate departure delay as a random draw from triangular distribution:
         delay = random.triangular(t_depart_left, t_depart_mode, t_depart_right)
        # Schedule next departure:
         yield env.timeout(delay)


# 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:
      # Change the light to green
      light = 'green'
      print("\nСвет ЗЕЛЕНЫЙ в момент %.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:
         start = random.triangular(t_depart_left, t_depart_mode, t_depart_right)
         start_delayed(env, departure(), delay=start)

      # Schedule event that will turn the light red:
      yield env.timeout(t_green)

      # Change the light to red
      light= 'red'
      print("\nСвет КРАСНЫЙ в момент %.3f." % env.now)

      # Schedule event that will turn the light green:
      yield env.timeout(t_red)


# 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)


# 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("\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.expovariate(arrival_rate) #(1 / 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)

# Report statistics
print("\n\n      *** Статистика ***\n")
print("Среднее число автомашин в очереди: %.3f"
  % (Q_stats.cars_waiting / Q_stats.count))
print("Среднее время ожидания в очереди (сек): %.3f"
  % (W_stats.waiting_time / W_stats.count))
