# This is taken from the PCW codes...
import time
import heapq
import random
import numpy as np
import scipy.stats as sts
import math
import functools
import operator
class Event:
'''
Store the properties of one event in the Schedule class defined below. Each
event has a time at which it needs to run, a function to call when running
the event, along with the arguments and keyword arguments to pass to that
function.
'''
def __init__(self, timestamp, function, *args, **kwargs):
self.timestamp = timestamp
self.function = function
self.args = args
self.kwargs = kwargs
def __lt__(self, other):
'''
This overloads the less-than operator in Python. We need it so the
priority queue knows how to compare two events. We want events with
earlier (smaller) times to go first.
'''
return self.timestamp < other.timestamp
def run(self, schedule):
'''
Run an event by calling the function with its arguments and keyword
arguments. The first argument to any event function is always the
schedule in which events are being tracked. The schedule object can be
used to add new events to the priority queue.
'''
self.function(schedule, *self.args, **self.kwargs)
class Schedule:
'''
Implement an event schedule using a priority queue. You can add events and
run the next event.
The `now` attribute contains the time at which the last event was run.
'''
def __init__(self):
self.now = 0 # Keep track of the current simulation time
self.priority_queue = [] # The priority queue of events to run
def add_event_at(self, timestamp, function, *args, **kwargs):
# Add an event to the schedule at a particular point in time.
heapq.heappush(
self.priority_queue,
Event(timestamp, function, *args, **kwargs))
def add_event_after(self, interval, function, *args, **kwargs):
# Add an event to the schedule after a specified time interval.
self.add_event_at(self.now + interval, function, *args, **kwargs)
def next_event_time(self):
return self.priority_queue[0].timestamp
def run_next_event(self):
# Get the next event from the priority queue and run it.
event = heapq.heappop(self.priority_queue)
self.now = event.timestamp
event.run(self)
def __repr__(self):
return (
f'Schedule() at time {self.now} ' +
f'with {len(self.priority_queue)} events in the queue')
def print_events(self):
print(repr(self))
for event in sorted(self.priority_queue):
print(f' {event.timestamp}: {event.function.__name__}')
class Bus:
def __init__(self, arrival_rate, buses, parent, max_capacity = 130):
self.max_capacity = max_capacity # this gives the ability to change the amount of people our bus can carry
self.buses = buses # This is the number of buses we're releasing
self.parent = parent
self.current_station = 0 # initiate which station we're currently on
self.customers_on_bus = 0 # initate the number of customers on the bus
self.arrival_rate = arrival_rate
self.disembarkers = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
self.people_disembarking = 0
# the distribution of how people leave the bus
self.disembarking_distribution = sts.norm(loc=0.03*self.people_disembarking, scale = 0.01*math.sqrt(self.people_disembarking))
# Define a function that alternates within the list to get to the next stop
# We have 15 unique stops
def move(self, schedule, i):
self.current_station += 1
if self.current_station > 15:
self.current_station -= 15
else:
self.current_station = self.current_station
schedule.add_event_after(0, self.find_disembarkers, i)
# Let's put the limits in: we can only start working the model if:
# (1) We have space in the bus - less than 130 people currently on the bus
# (2) We have people waiting to get onboard - more than 0 on the line
def start_serving_customer(self, schedule):
i = self.current_station
while self.customers_on_bus < 130 and self.parent.queues[self.current_station].people_in_queue > 0:
self.parent.queues[self.current_station].people_embarked += 1
self.parent.queues[self.current_station].start_times.append(schedule.now)
self.parent.queues[self.current_station].people_in_queue -= 1
self.customers_on_bus += 1
schedule.add_event_after(
self.parent.moving_distribution.rvs(),
self.move, i)
def find_disembarkers(self, schedule, i):
for x in range(self.customers_on_bus):
last_stop = i + random.randint(1,7)
if last_stop > 14:
last_stop = last_stop - 14
else:
last_stop = last_stop
self.disembarkers[last_stop] += 1
self.people_disembarking = self.disembarkers[self.current_station]
schedule.add_event_after(self.disembarking_distribution.rvs(), self.finish_serving_customer)
def finish_serving_customer(self, schedule):
self.parent.queues[self.current_station].people_disembarked += self.people_disembarking
self.customers_on_bus -= self.people_disembarking
self.people_disembarking = 0
self.disembarkers = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
schedule.add_event_after(self.parent.queues[self.current_station].embarking_distribution.rvs(), self.start_serving_customer)
class Queue:
def __init__(self):
self.people_in_queue = 0
self.people_disembarked = 0
self.people_embarked = 0
self.embarking_distribution = sts.norm(loc=0.05*self.people_in_queue, scale = 0.01*math.sqrt(self.people_in_queue))
self.arrival_times = []
self.start_times = []
def add_to_queue(self, index, schedule):
self.people_in_queue += 1
self.arrival_times.append(schedule.now)
class BusSystem:
def __init__(self, arrival_rate, buses, stations = 15):
self.queues = [(Queue()) for _ in range(stations)]
self.bus = [(Bus(arrival_rate, buses, parent = self)) for _ in range(buses)]
self.arrival_distribution = sts.expon(scale=1/arrival_rate)
self.moving_distribution = sts.norm(loc=2, scale=0.5)
self.arrival_rate = arrival_rate
self.buses = buses
def add_customer(self, schedule, index):
self.queues[index].add_to_queue(index, schedule)
schedule.add_event_after(
self.arrival_distribution.rvs(),
self.add_customer, index)
def run_queues(self, schedule):
for index in range(15):
schedule.add_event_after(
self.arrival_distribution.rvs(),
self.add_customer, index)
def run_buses(self, schedule, buses):
for s in range(buses):
schedule.add_event_after(
self.moving_distribution.rvs()+self.queues[0].embarking_distribution.rvs(),
self.bus[s].start_serving_customer)
def run_simulation(arrival_rate, run_until, buses):
schedule = Schedule()
bus_system = BusSystem(arrival_rate, buses)
bus_system.run_queues(schedule)
bus_system.run_buses(schedule, buses)
while schedule.next_event_time() < run_until:
schedule.run_next_event()
return bus_system
bus_system = run_simulation(arrival_rate=3, run_until=1000, buses = 10)
print(f'Embarked: {bus_system.queues[1].people_embarked}')
print(f'Waiting: {bus_system.queues[1].people_in_queue}')
print(f'#Off: {bus_system.queues[1].people_disembarked} disembarked customers')