256 lines
9.9 KiB
Python
256 lines
9.9 KiB
Python
|
|
"""
|
|
Taxi simulator
|
|
|
|
Sample run with two cars, random seed 10. This is a valid doctest.
|
|
|
|
>>> main(num_taxis=2, seed=10)
|
|
taxi: 0 Event(time=0, proc=0, action='leave garage')
|
|
taxi: 0 Event(time=4, proc=0, action='pick up passenger')
|
|
taxi: 1 Event(time=5, proc=1, action='leave garage')
|
|
taxi: 1 Event(time=9, proc=1, action='pick up passenger')
|
|
taxi: 0 Event(time=10, proc=0, action='drop off passenger')
|
|
taxi: 1 Event(time=12, proc=1, action='drop off passenger')
|
|
taxi: 0 Event(time=17, proc=0, action='pick up passenger')
|
|
taxi: 1 Event(time=19, proc=1, action='pick up passenger')
|
|
taxi: 1 Event(time=21, proc=1, action='drop off passenger')
|
|
taxi: 1 Event(time=24, proc=1, action='pick up passenger')
|
|
taxi: 0 Event(time=28, proc=0, action='drop off passenger')
|
|
taxi: 1 Event(time=28, proc=1, action='drop off passenger')
|
|
taxi: 0 Event(time=29, proc=0, action='going home')
|
|
taxi: 1 Event(time=30, proc=1, action='pick up passenger')
|
|
taxi: 1 Event(time=61, proc=1, action='drop off passenger')
|
|
taxi: 1 Event(time=62, proc=1, action='going home')
|
|
*** end of events ***
|
|
|
|
See explanation and longer sample run at the end of this module.
|
|
|
|
"""
|
|
|
|
import argparse
|
|
import collections
|
|
import queue
|
|
import random
|
|
|
|
DEFAULT_NUMBER_OF_TAXIS = 3
|
|
DEFAULT_END_TIME = 80
|
|
SEARCH_DURATION = 4
|
|
TRIP_DURATION = 10
|
|
DEPARTURE_INTERVAL = 5
|
|
|
|
Event = collections.namedtuple('Event', 'time proc action')
|
|
|
|
|
|
def compute_delay(interval):
|
|
"""Compute action delay using exponential distribution"""
|
|
return int(random.expovariate(1 / interval)) + 1
|
|
|
|
# BEGIN TAXI_PROCESS
|
|
def taxi_process(ident, trips, start_time=0): # <1>
|
|
"""Yield to simulator issuing event at each state change"""
|
|
time = yield Event(start_time, ident, 'leave garage') # <2>
|
|
for i in range(trips): # <3>
|
|
prowling_ends = time + compute_delay(SEARCH_DURATION) # <4>
|
|
time = yield Event(prowling_ends, ident, 'pick up passenger') # <5>
|
|
|
|
trip_ends = time + compute_delay(TRIP_DURATION) # <6>
|
|
time = yield Event(trip_ends, ident, 'drop off passenger') # <7>
|
|
|
|
yield Event(time + 1, ident, 'going home') # <8>
|
|
# end of taxi process # <9>
|
|
# END TAXI_PROCESS
|
|
|
|
# BEGIN TAXI_SIMULATOR
|
|
class Simulator:
|
|
|
|
def __init__(self, procs_map):
|
|
self.events = queue.PriorityQueue()
|
|
self.procs = dict(procs_map)
|
|
|
|
def run(self, end_time): # <1>
|
|
"""Schedule and display events until time is up"""
|
|
# schedule the first event for each cab
|
|
for _, proc in sorted(self.procs.items()): # <2>
|
|
first_event = next(proc) # <3>
|
|
self.events.put(first_event) # <4>
|
|
|
|
# main loop of the simulation
|
|
time = 0
|
|
while time < end_time: # <5>
|
|
if self.events.empty(): # <6>
|
|
print('*** end of events ***')
|
|
break
|
|
|
|
# get and display current event
|
|
current_event = self.events.get() # <7>
|
|
print('taxi:', current_event.proc, # <8>
|
|
current_event.proc * ' ', current_event)
|
|
|
|
# schedule next action for current proc
|
|
time = current_event.time # <9>
|
|
proc = self.procs[current_event.proc] # <10>
|
|
try:
|
|
next_event = proc.send(time) # <11>
|
|
except StopIteration:
|
|
del self.procs[current_event.proc] # <12>
|
|
else:
|
|
self.events.put(next_event) # <13>
|
|
else: # <14>
|
|
msg = '*** end of simulation time: {} events pending ***'
|
|
print(msg.format(self.events.qsize()))
|
|
# END TAXI_SIMULATOR
|
|
|
|
def main(end_time=DEFAULT_END_TIME, num_taxis=DEFAULT_NUMBER_OF_TAXIS,
|
|
seed=None):
|
|
"""Initialize random generator, build procs and run simulation"""
|
|
if seed is not None:
|
|
random.seed(seed) # get reproducible results
|
|
|
|
taxis = {i: taxi_process(i, (i + 1) * 2, i * DEPARTURE_INTERVAL)
|
|
for i in range(num_taxis)}
|
|
sim = Simulator(taxis)
|
|
sim.run(end_time)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description='Taxi fleet simulator.')
|
|
parser.add_argument('-e', '--end-time', type=int,
|
|
default=DEFAULT_END_TIME,
|
|
help='simulation end time; default = %s'
|
|
% DEFAULT_END_TIME)
|
|
parser.add_argument('-t', '--taxis', type=int,
|
|
default=DEFAULT_NUMBER_OF_TAXIS,
|
|
help='number of taxis running; default = %s'
|
|
% DEFAULT_NUMBER_OF_TAXIS)
|
|
parser.add_argument('-s', '--seed', type=int, default=None,
|
|
help='random generator seed (for testing)')
|
|
|
|
args = parser.parse_args()
|
|
main(args.end_time, args.taxis, args.seed)
|
|
|
|
|
|
"""
|
|
Notes for the ``taxi_process`` coroutine::
|
|
|
|
<1> `taxi_process` will be called once per taxi, creating a generator
|
|
object to represent its operations. `ident` is the number of the taxi
|
|
(eg. 0, 1, 2 in the sample run); `trips` is the number of trips this
|
|
taxi will make before going home; `start_time` is when the taxi
|
|
leaves the garage.
|
|
|
|
<2> The first `Event` yielded is `'leave garage'`. This suspends the
|
|
coroutine, and lets the simulation main loop proceed to the next
|
|
scheduled event. When it's time to reactivate this process, the main
|
|
loop will `send` the current simulation time, which is assigned to
|
|
`time`.
|
|
|
|
<3> This block will be repeated once for each trip.
|
|
|
|
<4> The ending time of the search for a passenger is computed.
|
|
|
|
<5> An `Event` signaling passenger pick up is yielded. The coroutine
|
|
pauses here. When the time comes to reactivate this coroutine,
|
|
the main loop will again `send` the current time.
|
|
|
|
<6> The ending time of the trip is computed, taking into account the
|
|
current `time`.
|
|
|
|
<7> An `Event` signaling passenger drop off is yielded. Coroutine
|
|
suspended again, waiting for the main loop to send the time of when
|
|
it's time to continue.
|
|
|
|
<8> The `for` loop ends after the given number of trips, and a final
|
|
`'going home'` event is yielded, to happen 1 minute after the current
|
|
time. The coroutine will suspend for the last time. When reactivated,
|
|
it will be sent the time from the simulation main loop, but here I
|
|
don't assign it to any variable because it will not be useful.
|
|
|
|
<9> When the coroutine falls off the end, the coroutine object raises
|
|
`StopIteration`.
|
|
|
|
|
|
Notes for the ``Simulator.run`` method::
|
|
|
|
<1> The simulation `end_time` is the only required argument for `run`.
|
|
|
|
<2> Use `sorted` to retrieve the `self.procs` items ordered by the
|
|
integer key; we don't care about the key, so assign it to `_`.
|
|
|
|
<3> `next(proc)` primes each coroutine by advancing it to the first
|
|
yield, so it's ready to be sent data. An `Event` is yielded.
|
|
|
|
<4> Add each event to the `self.events` `PriorityQueue`. The first
|
|
event for each taxi is `'leave garage'`, as seen in the sample run
|
|
(ex_taxi_process>>).
|
|
|
|
<5> Main loop of the simulation: run until the current `time` equals
|
|
or exceeds the `end_time`.
|
|
|
|
<6> The main loop may also exit if there are no pending events in the
|
|
queue.
|
|
|
|
<7> Get `Event` with the smallest `time` in the queue; this is the
|
|
`current_event`.
|
|
|
|
<8> Display the `Event`, identifying the taxi and adding indentation
|
|
according to the taxi id.
|
|
|
|
<9> Update the simulation time with the time of the `current_event`.
|
|
|
|
<10> Retrieve the coroutine for this taxi from the `self.procs`
|
|
dictionary.
|
|
|
|
<11> Send the `time` to the coroutine. The coroutine will yield the
|
|
`next_event` or raise `StopIteration` it's finished.
|
|
|
|
<12> If `StopIteration` was raised, delete the coroutine from the
|
|
`self.procs` dictionary.
|
|
|
|
<13> Otherwise, put the `next_event` in the queue.
|
|
|
|
<14> If the loop exits because the simulation time passed, display the
|
|
number of events pending (which may be zero by coincidence,
|
|
sometimes).
|
|
|
|
|
|
Sample run from the command line, seed=24, total elapsed time=160::
|
|
|
|
# BEGIN TAXI_SAMPLE_RUN
|
|
$ python3 taxi_sim.py -s 24 -e 160
|
|
taxi: 0 Event(time=0, proc=0, action='leave garage')
|
|
taxi: 0 Event(time=5, proc=0, action='pick up passenger')
|
|
taxi: 1 Event(time=5, proc=1, action='leave garage')
|
|
taxi: 1 Event(time=6, proc=1, action='pick up passenger')
|
|
taxi: 2 Event(time=10, proc=2, action='leave garage')
|
|
taxi: 2 Event(time=11, proc=2, action='pick up passenger')
|
|
taxi: 2 Event(time=23, proc=2, action='drop off passenger')
|
|
taxi: 0 Event(time=24, proc=0, action='drop off passenger')
|
|
taxi: 2 Event(time=24, proc=2, action='pick up passenger')
|
|
taxi: 2 Event(time=26, proc=2, action='drop off passenger')
|
|
taxi: 0 Event(time=30, proc=0, action='pick up passenger')
|
|
taxi: 2 Event(time=31, proc=2, action='pick up passenger')
|
|
taxi: 0 Event(time=43, proc=0, action='drop off passenger')
|
|
taxi: 0 Event(time=44, proc=0, action='going home')
|
|
taxi: 2 Event(time=46, proc=2, action='drop off passenger')
|
|
taxi: 2 Event(time=49, proc=2, action='pick up passenger')
|
|
taxi: 1 Event(time=70, proc=1, action='drop off passenger')
|
|
taxi: 2 Event(time=70, proc=2, action='drop off passenger')
|
|
taxi: 2 Event(time=71, proc=2, action='pick up passenger')
|
|
taxi: 2 Event(time=79, proc=2, action='drop off passenger')
|
|
taxi: 1 Event(time=88, proc=1, action='pick up passenger')
|
|
taxi: 2 Event(time=92, proc=2, action='pick up passenger')
|
|
taxi: 2 Event(time=98, proc=2, action='drop off passenger')
|
|
taxi: 2 Event(time=99, proc=2, action='going home')
|
|
taxi: 1 Event(time=102, proc=1, action='drop off passenger')
|
|
taxi: 1 Event(time=104, proc=1, action='pick up passenger')
|
|
taxi: 1 Event(time=135, proc=1, action='drop off passenger')
|
|
taxi: 1 Event(time=136, proc=1, action='pick up passenger')
|
|
taxi: 1 Event(time=151, proc=1, action='drop off passenger')
|
|
taxi: 1 Event(time=152, proc=1, action='going home')
|
|
*** end of events ***
|
|
# END TAXI_SAMPLE_RUN
|
|
|
|
"""
|